StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! WebAssembly platform implementation |
| 2 | |
| 3 | use crate::{Platform, PlatformError, Window, WindowBuilder, WindowId, WindowInner}; |
| 4 | use strato_core::event::Event; |
| 5 | use wasm_bindgen::prelude::*; |
| 6 | use wasm_bindgen::JsCast; |
| 7 | use web_sys::{Document, HtmlCanvasElement, Window as WebWindow}; |
| 8 | |
| 9 | /// Web platform implementation |
| 10 | pub struct WebPlatform { |
| 11 | canvas: Option<HtmlCanvasElement>, |
| 12 | window_id: WindowId, |
| 13 | } |
| 14 | |
| 15 | impl WebPlatform { |
| 16 | /// Create a new web platform |
| 17 | pub fn new() -> Self { |
| 18 | Self { |
| 19 | canvas: None, |
| 20 | window_id: 0, |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | /// Get the web window |
| 25 | fn web_window() -> Result<WebWindow, PlatformError> { |
| 26 | web_sys::window().ok_or_else(|| PlatformError::Wasm("Failed to get window".to_string())) |
| 27 | } |
| 28 | |
| 29 | /// Get the document |
| 30 | fn document() -> Result<Document, PlatformError> { |
| 31 | Self::web_window()? |
| 32 | .document() |
| 33 | .ok_or_else(|| PlatformError::Wasm("Failed to get document".to_string())) |
| 34 | } |
| 35 | |
| 36 | /// Create a canvas element |
| 37 | fn create_canvas(builder: &WindowBuilder) -> Result<HtmlCanvasElement, PlatformError> { |
| 38 | let document = Self::document()?; |
| 39 | |
| 40 | let canvas = document |
| 41 | .create_element("canvas") |
| 42 | .map_err(|e| PlatformError::Wasm(format!("Failed to create canvas: {:?}", e)))? |
| 43 | .dyn_into::<HtmlCanvasElement>() |
| 44 | .map_err(|_| PlatformError::Wasm("Failed to cast to HtmlCanvasElement".to_string()))?; |
| 45 | |
| 46 | canvas.set_width(builder.size.width as u32); |
| 47 | canvas.set_height(builder.size.height as u32); |
| 48 | canvas.set_id("strato-sdk-canvas"); |
| 49 | |
| 50 | // Set canvas style |
| 51 | let style = canvas.style(); |
| 52 | style.set_property("display", "block").ok(); |
| 53 | style.set_property("margin", "0 auto").ok(); |
| 54 | |
| 55 | // Append to body |
| 56 | document |
| 57 | .body() |
| 58 | .ok_or_else(|| PlatformError::Wasm("No body element".to_string()))? |
| 59 | .append_child(&canvas) |
| 60 | .map_err(|e| PlatformError::Wasm(format!("Failed to append canvas: {:?}", e)))?; |
| 61 | |
| 62 | Ok(canvas) |
| 63 | } |
| 64 | |
| 65 | /// Setup event listeners |
| 66 | fn setup_event_listeners(canvas: &HtmlCanvasElement) -> Result<(), PlatformError> { |
| 67 | let canvas_clone = canvas.clone(); |
| 68 | |
| 69 | // Mouse move |
| 70 | let mouse_move = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { |
| 71 | // Handle mouse move |
| 72 | let _x = event.offset_x(); |
| 73 | let _y = event.offset_y(); |
| 74 | // TODO: Dispatch to event handler |
| 75 | }) as Box<dyn FnMut(_)>); |
| 76 | |
| 77 | canvas |
| 78 | .add_event_listener_with_callback("mousemove", mouse_move.as_ref().unchecked_ref()) |
| 79 | .map_err(|e| { |
| 80 | PlatformError::Wasm(format!("Failed to add mousemove listener: {:?}", e)) |
| 81 | })?; |
| 82 | mouse_move.forget(); |
| 83 | |
| 84 | // Mouse down |
| 85 | let mouse_down = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { |
| 86 | // Handle mouse down |
| 87 | let _button = event.button(); |
| 88 | // TODO: Dispatch to event handler |
| 89 | }) as Box<dyn FnMut(_)>); |
| 90 | |
| 91 | canvas |
| 92 | .add_event_listener_with_callback("mousedown", mouse_down.as_ref().unchecked_ref()) |
| 93 | .map_err(|e| { |
| 94 | PlatformError::Wasm(format!("Failed to add mousedown listener: {:?}", e)) |
| 95 | })?; |
| 96 | mouse_down.forget(); |
| 97 | |
| 98 | // Mouse up |
| 99 | let mouse_up = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { |
| 100 | // Handle mouse up |
| 101 | let _button = event.button(); |
| 102 | // TODO: Dispatch to event handler |
| 103 | }) as Box<dyn FnMut(_)>); |
| 104 | |
| 105 | canvas |
| 106 | .add_event_listener_with_callback("mouseup", mouse_up.as_ref().unchecked_ref()) |
| 107 | .map_err(|e| PlatformError::Wasm(format!("Failed to add mouseup listener: {:?}", e)))?; |
| 108 | mouse_up.forget(); |
| 109 | |
| 110 | // Keyboard events |
| 111 | let keydown = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| { |
| 112 | // Handle keydown |
| 113 | let _key = event.key(); |
| 114 | // TODO: Dispatch to event handler |
| 115 | }) as Box<dyn FnMut(_)>); |
| 116 | |
| 117 | Self::document()? |
| 118 | .add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref()) |
| 119 | .map_err(|e| PlatformError::Wasm(format!("Failed to add keydown listener: {:?}", e)))?; |
| 120 | keydown.forget(); |
| 121 | |
| 122 | let keyup = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| { |
| 123 | // Handle keyup |
| 124 | let _key = event.key(); |
| 125 | // TODO: Dispatch to event handler |
| 126 | }) as Box<dyn FnMut(_)>); |
| 127 | |
| 128 | Self::document()? |
| 129 | .add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref()) |
| 130 | .map_err(|e| PlatformError::Wasm(format!("Failed to add keyup listener: {:?}", e)))?; |
| 131 | keyup.forget(); |
| 132 | |
| 133 | // Resize observer |
| 134 | let resize = Closure::wrap(Box::new(move || { |
| 135 | // Handle resize |
| 136 | // TODO: Dispatch resize event |
| 137 | }) as Box<dyn FnMut()>); |
| 138 | |
| 139 | Self::web_window()? |
| 140 | .add_event_listener_with_callback("resize", resize.as_ref().unchecked_ref()) |
| 141 | .map_err(|e| PlatformError::Wasm(format!("Failed to add resize listener: {:?}", e)))?; |
| 142 | resize.forget(); |
| 143 | |
| 144 | Ok(()) |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | impl Platform for WebPlatform { |
| 149 | fn init() -> Result<Self, PlatformError> { |
| 150 | // Set panic hook for better error messages |
| 151 | console_error_panic_hook::set_once(); |
| 152 | |
| 153 | Ok(Self::new()) |
| 154 | } |
| 155 | |
| 156 | fn create_window(&mut self, builder: WindowBuilder) -> Result<Window, PlatformError> { |
| 157 | // In web, we typically have one canvas |
| 158 | if self.canvas.is_some() { |
| 159 | return Err(PlatformError::Wasm("Canvas already created".to_string())); |
| 160 | } |
| 161 | |
| 162 | let canvas = Self::create_canvas(&builder)?; |
| 163 | Self::setup_event_listeners(&canvas)?; |
| 164 | |
| 165 | // Set document title |
| 166 | Self::document()?.set_title(&builder.title); |
| 167 | |
| 168 | let window_id = self.window_id; |
| 169 | self.window_id += 1; |
| 170 | |
| 171 | self.canvas = Some(canvas.clone()); |
| 172 | |
| 173 | Ok(Window { |
| 174 | id: window_id, |
| 175 | inner: WindowInner::Web(canvas), |
| 176 | }) |
| 177 | } |
| 178 | |
| 179 | fn run_event_loop<F>(&mut self, callback: F) -> Result<(), PlatformError> |
| 180 | where |
| 181 | F: FnMut(Event) + 'static, |
| 182 | { |
| 183 | // In web, the event loop is handled by the browser |
| 184 | // We use requestAnimationFrame for the render loop |
| 185 | |
| 186 | let window = Self::web_window()?; |
| 187 | let callback = std::rc::Rc::new(std::cell::RefCell::new(callback)); |
| 188 | |
| 189 | // Animation frame loop |
| 190 | let f = std::rc::Rc::new(std::cell::RefCell::new(None)); |
| 191 | let g = f.clone(); |
| 192 | |
| 193 | *g.borrow_mut() = Some(Closure::wrap(Box::new(move || { |
| 194 | // Request next frame |
| 195 | request_animation_frame(f.borrow().as_ref().unwrap()); |
| 196 | |
| 197 | // Handle frame update |
| 198 | // TODO: Dispatch update event |
| 199 | }) as Box<dyn FnMut()>)); |
| 200 | |
| 201 | request_animation_frame(g.borrow().as_ref().unwrap()); |
| 202 | |
| 203 | Ok(()) |
| 204 | } |
| 205 | |
| 206 | fn request_redraw(&self, _window_id: WindowId) { |
| 207 | // In web, we use requestAnimationFrame |
| 208 | // This is handled in the event loop |
| 209 | } |
| 210 | |
| 211 | fn exit(&mut self) { |
| 212 | // Can't really exit in web |
| 213 | // Could navigate away or close tab |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | /// Request animation frame helper |
| 218 | fn request_animation_frame(f: &Closure<dyn FnMut()>) { |
| 219 | web_sys::window() |
| 220 | .unwrap() |
| 221 | .request_animation_frame(f.as_ref().unchecked_ref()) |
| 222 | .unwrap(); |
| 223 | } |
| 224 |