1#![allow(dead_code)]
7
8use std::cell::RefCell;
9use std::collections::VecDeque;
10use std::rc::Rc;
11
12use wasm_bindgen::prelude::*;
13use wasm_bindgen::JsCast;
14use web_sys::{
15 HtmlCanvasElement, Window, Document, Event, MouseEvent, KeyboardEvent,
16 WheelEvent, TouchEvent, CompositionEvent,
17};
18
19use uzor::input::events::KeyCode;
20use uzor::input::state::{ModifierKeys, MouseButton};
21use uzor::platform::{
22 backends::PlatformBackend,
23 types::{PlatformError, WindowId, SystemIntegration},
24 ImeEvent, PlatformEvent, SystemTheme, WindowConfig,
25};
26use uzor::input::cursor::CursorIcon;
27
28pub use uzor;
29
30#[derive(Clone)]
39pub struct WebPlatform {
40 state: Rc<RefCell<WebPlatformState>>,
41}
42
43struct WebPlatformState {
44 window: Window,
45 document: Document,
46 canvas: HtmlCanvasElement,
47 window_id: WindowId,
48 config: WindowConfig,
49 event_queue: VecDeque<PlatformEvent>,
50 scale_factor: f64,
51 cursor_icon: CursorIcon,
52 cursor_visible: bool,
53 ime_position: (f64, f64),
54 ime_allowed: bool,
55 _listeners: Vec<EventListener>,
57}
58
59struct EventListener {
60 _closure: Closure<dyn FnMut(Event)>,
61}
62
63impl WebPlatform {
64 pub fn new(canvas_id: &str) -> Result<Self, String> {
74 let window = web_sys::window()
76 .ok_or_else(|| "No window object available".to_string())?;
77
78 let document = window
79 .document()
80 .ok_or_else(|| "No document object available".to_string())?;
81
82 let canvas = document
84 .get_element_by_id(canvas_id)
85 .ok_or_else(|| format!("Canvas element '{}' not found", canvas_id))?
86 .dyn_into::<HtmlCanvasElement>()
87 .map_err(|_| format!("Element '{}' is not a canvas", canvas_id))?;
88
89 let scale_factor = window.device_pixel_ratio();
91
92 let width = canvas.client_width() as u32;
94 let height = canvas.client_height() as u32;
95 let config = WindowConfig {
96 title: "Web Canvas".to_string(),
97 width,
98 height,
99 ..WindowConfig::default()
100 };
101
102 let state = Rc::new(RefCell::new(WebPlatformState {
103 window,
104 document,
105 canvas,
106 window_id: WindowId::new(),
107 config,
108 event_queue: VecDeque::new(),
109 scale_factor,
110 cursor_icon: CursorIcon::Default,
111 cursor_visible: true,
112 ime_position: (0.0, 0.0),
113 ime_allowed: false,
114 _listeners: Vec::new(),
115 }));
116
117 Self::setup_event_listeners(&state)?;
119
120 Ok(Self { state })
121 }
122
123 pub fn canvas(&self) -> HtmlCanvasElement {
125 self.state.borrow().canvas.clone()
126 }
127
128 fn setup_event_listeners(state: &Rc<RefCell<WebPlatformState>>) -> Result<(), String> {
129 let mut state_mut = state.borrow_mut();
130 let canvas = state_mut.canvas.clone();
131 let canvas_target = canvas.clone().dyn_into::<web_sys::EventTarget>()
132 .map_err(|_| "Canvas is not an EventTarget")?;
133
134 Self::add_mouse_listener(&mut state_mut, &canvas_target, "mousedown", state)?;
136 Self::add_mouse_listener(&mut state_mut, &canvas_target, "mousemove", state)?;
137 Self::add_mouse_listener(&mut state_mut, &canvas_target, "mouseup", state)?;
138 Self::add_mouse_listener(&mut state_mut, &canvas_target, "mouseenter", state)?;
139 Self::add_mouse_listener(&mut state_mut, &canvas_target, "mouseleave", state)?;
140
141 Self::add_wheel_listener(&mut state_mut, &canvas_target, state)?;
143
144 Self::add_touch_listener(&mut state_mut, &canvas_target, "touchstart", state)?;
146 Self::add_touch_listener(&mut state_mut, &canvas_target, "touchmove", state)?;
147 Self::add_touch_listener(&mut state_mut, &canvas_target, "touchend", state)?;
148 Self::add_touch_listener(&mut state_mut, &canvas_target, "touchcancel", state)?;
149
150 Self::add_keyboard_listener(&mut state_mut, &canvas_target, "keydown", state)?;
152 Self::add_keyboard_listener(&mut state_mut, &canvas_target, "keyup", state)?;
153
154 Self::add_focus_listener(&mut state_mut, &canvas_target, state)?;
156
157 Self::add_ime_listener(&mut state_mut, &canvas_target, "compositionstart", state)?;
159 Self::add_ime_listener(&mut state_mut, &canvas_target, "compositionupdate", state)?;
160 Self::add_ime_listener(&mut state_mut, &canvas_target, "compositionend", state)?;
161
162 Ok(())
163 }
164
165 fn add_mouse_listener(
166 state_mut: &mut WebPlatformState,
167 target: &web_sys::EventTarget,
168 event_type: &str,
169 state_ref: &Rc<RefCell<WebPlatformState>>,
170 ) -> Result<(), String> {
171 let state_clone = state_ref.clone();
172 let event_type_str = event_type.to_string();
173
174 let closure = Closure::wrap(Box::new(move |event: Event| {
175 if let Ok(mouse_event) = event.dyn_into::<MouseEvent>() {
176 let mut state = state_clone.borrow_mut();
177 let platform_event = Self::map_mouse_event(&event_type_str, &mouse_event);
178 if let Some(evt) = platform_event {
179 state.event_queue.push_back(evt);
180 }
181 }
182 }) as Box<dyn FnMut(Event)>);
183
184 target.add_event_listener_with_callback(event_type, closure.as_ref().unchecked_ref())
185 .map_err(|_| format!("Failed to add {} listener", event_type))?;
186
187 state_mut._listeners.push(EventListener { _closure: closure });
188 Ok(())
189 }
190
191 fn add_wheel_listener(
192 state_mut: &mut WebPlatformState,
193 target: &web_sys::EventTarget,
194 state_ref: &Rc<RefCell<WebPlatformState>>,
195 ) -> Result<(), String> {
196 let state_clone = state_ref.clone();
197
198 let closure = Closure::wrap(Box::new(move |event: Event| {
199 event.prevent_default();
200
201 if let Ok(wheel_event) = event.dyn_into::<WheelEvent>() {
202 let mut state = state_clone.borrow_mut();
203 let dx = wheel_event.delta_x();
204 let dy = wheel_event.delta_y();
205
206 state.event_queue.push_back(PlatformEvent::Scroll {
207 dx: -dx,
208 dy: -dy
209 });
210 }
211 }) as Box<dyn FnMut(Event)>);
212
213 target.add_event_listener_with_callback("wheel", closure.as_ref().unchecked_ref())
214 .map_err(|_| "Failed to add wheel listener")?;
215
216 state_mut._listeners.push(EventListener { _closure: closure });
217 Ok(())
218 }
219
220 fn add_touch_listener(
221 state_mut: &mut WebPlatformState,
222 target: &web_sys::EventTarget,
223 event_type: &str,
224 state_ref: &Rc<RefCell<WebPlatformState>>,
225 ) -> Result<(), String> {
226 let state_clone = state_ref.clone();
227 let event_type_str = event_type.to_string();
228
229 let closure = Closure::wrap(Box::new(move |event: Event| {
230 event.prevent_default();
231
232 if let Ok(touch_event) = event.dyn_into::<TouchEvent>() {
233 let mut state = state_clone.borrow_mut();
234 let events = Self::map_touch_event(&event_type_str, &touch_event);
235 for evt in events {
236 state.event_queue.push_back(evt);
237 }
238 }
239 }) as Box<dyn FnMut(Event)>);
240
241 target.add_event_listener_with_callback(event_type, closure.as_ref().unchecked_ref())
242 .map_err(|_| format!("Failed to add {} listener", event_type))?;
243
244 state_mut._listeners.push(EventListener { _closure: closure });
245 Ok(())
246 }
247
248 fn add_keyboard_listener(
249 state_mut: &mut WebPlatformState,
250 target: &web_sys::EventTarget,
251 event_type: &str,
252 state_ref: &Rc<RefCell<WebPlatformState>>,
253 ) -> Result<(), String> {
254 let state_clone = state_ref.clone();
255 let event_type_str = event_type.to_string();
256
257 let closure = Closure::wrap(Box::new(move |event: Event| {
258 if let Ok(keyboard_event) = event.dyn_into::<KeyboardEvent>() {
259 let mut state = state_clone.borrow_mut();
260 let platform_event = Self::map_keyboard_event(&event_type_str, &keyboard_event);
261 if let Some(evt) = platform_event {
262 state.event_queue.push_back(evt);
263 }
264 }
265 }) as Box<dyn FnMut(Event)>);
266
267 target.add_event_listener_with_callback(event_type, closure.as_ref().unchecked_ref())
268 .map_err(|_| format!("Failed to add {} listener", event_type))?;
269
270 state_mut._listeners.push(EventListener { _closure: closure });
271 Ok(())
272 }
273
274 fn add_focus_listener(
275 state_mut: &mut WebPlatformState,
276 target: &web_sys::EventTarget,
277 state_ref: &Rc<RefCell<WebPlatformState>>,
278 ) -> Result<(), String> {
279 let state_clone = state_ref.clone();
280
281 let focus_closure = Closure::wrap(Box::new(move |_event: Event| {
282 let mut state = state_clone.borrow_mut();
283 state.event_queue.push_back(PlatformEvent::WindowFocused(true));
284 }) as Box<dyn FnMut(Event)>);
285
286 target.add_event_listener_with_callback("focus", focus_closure.as_ref().unchecked_ref())
287 .map_err(|_| "Failed to add focus listener")?;
288
289 state_mut._listeners.push(EventListener { _closure: focus_closure });
290
291 let state_clone2 = state_ref.clone();
292 let blur_closure = Closure::wrap(Box::new(move |_event: Event| {
293 let mut state = state_clone2.borrow_mut();
294 state.event_queue.push_back(PlatformEvent::WindowFocused(false));
295 }) as Box<dyn FnMut(Event)>);
296
297 target.add_event_listener_with_callback("blur", blur_closure.as_ref().unchecked_ref())
298 .map_err(|_| "Failed to add blur listener")?;
299
300 state_mut._listeners.push(EventListener { _closure: blur_closure });
301
302 Ok(())
303 }
304
305 fn add_ime_listener(
306 state_mut: &mut WebPlatformState,
307 target: &web_sys::EventTarget,
308 event_type: &str,
309 state_ref: &Rc<RefCell<WebPlatformState>>,
310 ) -> Result<(), String> {
311 let state_clone = state_ref.clone();
312 let event_type_str = event_type.to_string();
313
314 let closure = Closure::wrap(Box::new(move |event: Event| {
315 if let Ok(composition_event) = event.dyn_into::<CompositionEvent>() {
316 let mut state = state_clone.borrow_mut();
317 let ime_event = Self::map_ime_event(&event_type_str, &composition_event);
318 if let Some(evt) = ime_event {
319 state.event_queue.push_back(PlatformEvent::Ime(evt));
320 }
321 }
322 }) as Box<dyn FnMut(Event)>);
323
324 target.add_event_listener_with_callback(event_type, closure.as_ref().unchecked_ref())
325 .map_err(|_| format!("Failed to add {} listener", event_type))?;
326
327 state_mut._listeners.push(EventListener { _closure: closure });
328 Ok(())
329 }
330
331 fn map_mouse_event(event_type: &str, event: &MouseEvent) -> Option<PlatformEvent> {
332 let x = event.offset_x() as f64;
333 let y = event.offset_y() as f64;
334 let button = Self::map_mouse_button(event.button());
335
336 match event_type {
337 "mousedown" => Some(PlatformEvent::PointerDown { x, y, button }),
338 "mouseup" => Some(PlatformEvent::PointerUp { x, y, button }),
339 "mousemove" => Some(PlatformEvent::PointerMoved { x, y }),
340 "mouseenter" => Some(PlatformEvent::PointerEntered),
341 "mouseleave" => Some(PlatformEvent::PointerLeft),
342 _ => None,
343 }
344 }
345
346 fn map_touch_event(event_type: &str, event: &TouchEvent) -> Vec<PlatformEvent> {
347 let mut events = Vec::new();
348
349 match event_type {
350 "touchstart" => {
351 let touches = event.changed_touches();
352 for i in 0..touches.length() {
353 if let Some(touch) = touches.item(i) {
354 events.push(PlatformEvent::TouchStart {
355 id: touch.identifier() as u64,
356 x: touch.client_x() as f64,
357 y: touch.client_y() as f64,
358 });
359 }
360 }
361 }
362 "touchmove" => {
363 let touches = event.changed_touches();
364 for i in 0..touches.length() {
365 if let Some(touch) = touches.item(i) {
366 events.push(PlatformEvent::TouchMove {
367 id: touch.identifier() as u64,
368 x: touch.client_x() as f64,
369 y: touch.client_y() as f64,
370 });
371 }
372 }
373 }
374 "touchend" => {
375 let touches = event.changed_touches();
376 for i in 0..touches.length() {
377 if let Some(touch) = touches.item(i) {
378 events.push(PlatformEvent::TouchEnd {
379 id: touch.identifier() as u64,
380 x: touch.client_x() as f64,
381 y: touch.client_y() as f64,
382 });
383 }
384 }
385 }
386 "touchcancel" => {
387 let touches = event.changed_touches();
388 for i in 0..touches.length() {
389 if let Some(touch) = touches.item(i) {
390 events.push(PlatformEvent::TouchCancel {
391 id: touch.identifier() as u64,
392 });
393 }
394 }
395 }
396 _ => {}
397 }
398
399 events
400 }
401
402 fn map_keyboard_event(event_type: &str, event: &KeyboardEvent) -> Option<PlatformEvent> {
403 let key = Self::map_keycode(&event.code());
404 let modifiers = Self::get_modifiers(event);
405
406 match event_type {
407 "keydown" => Some(PlatformEvent::KeyDown { key, modifiers }),
408 "keyup" => Some(PlatformEvent::KeyUp { key, modifiers }),
409 _ => None,
410 }
411 }
412
413 fn map_ime_event(event_type: &str, event: &CompositionEvent) -> Option<ImeEvent> {
414 match event_type {
415 "compositionstart" => Some(ImeEvent::Enabled),
416 "compositionupdate" => {
417 let data = event.data().unwrap_or_default();
418 Some(ImeEvent::Preedit(data, None))
419 }
420 "compositionend" => {
421 let data = event.data().unwrap_or_default();
422 Some(ImeEvent::Commit(data))
423 }
424 _ => None,
425 }
426 }
427
428 fn map_mouse_button(button: i16) -> MouseButton {
429 match button {
430 0 => MouseButton::Left,
431 1 => MouseButton::Middle,
432 2 => MouseButton::Right,
433 _ => MouseButton::Left,
434 }
435 }
436
437 fn get_modifiers(event: &KeyboardEvent) -> ModifierKeys {
438 ModifierKeys {
439 shift: event.shift_key(),
440 ctrl: event.ctrl_key(),
441 alt: event.alt_key(),
442 meta: event.meta_key(),
443 }
444 }
445
446 fn map_keycode(code: &str) -> KeyCode {
447 match code {
448 "KeyA" => KeyCode::A,
450 "KeyB" => KeyCode::B,
451 "KeyC" => KeyCode::C,
452 "KeyD" => KeyCode::D,
453 "KeyE" => KeyCode::E,
454 "KeyF" => KeyCode::F,
455 "KeyG" => KeyCode::G,
456 "KeyH" => KeyCode::H,
457 "KeyI" => KeyCode::I,
458 "KeyJ" => KeyCode::J,
459 "KeyK" => KeyCode::K,
460 "KeyL" => KeyCode::L,
461 "KeyM" => KeyCode::M,
462 "KeyN" => KeyCode::N,
463 "KeyO" => KeyCode::O,
464 "KeyP" => KeyCode::P,
465 "KeyQ" => KeyCode::Q,
466 "KeyR" => KeyCode::R,
467 "KeyS" => KeyCode::S,
468 "KeyT" => KeyCode::T,
469 "KeyU" => KeyCode::U,
470 "KeyV" => KeyCode::V,
471 "KeyW" => KeyCode::W,
472 "KeyX" => KeyCode::X,
473 "KeyY" => KeyCode::Y,
474 "KeyZ" => KeyCode::Z,
475
476 "Digit0" => KeyCode::Num0,
478 "Digit1" => KeyCode::Num1,
479 "Digit2" => KeyCode::Num2,
480 "Digit3" => KeyCode::Num3,
481 "Digit4" => KeyCode::Num4,
482 "Digit5" => KeyCode::Num5,
483 "Digit6" => KeyCode::Num6,
484 "Digit7" => KeyCode::Num7,
485 "Digit8" => KeyCode::Num8,
486 "Digit9" => KeyCode::Num9,
487
488 "Enter" => KeyCode::Enter,
490 "Escape" => KeyCode::Escape,
491 "Backspace" => KeyCode::Backspace,
492 "Tab" => KeyCode::Tab,
493 "Space" => KeyCode::Space,
494
495 "ArrowLeft" => KeyCode::ArrowLeft,
497 "ArrowRight" => KeyCode::ArrowRight,
498 "ArrowUp" => KeyCode::ArrowUp,
499 "ArrowDown" => KeyCode::ArrowDown,
500
501 "F1" => KeyCode::F1,
503 "F2" => KeyCode::F2,
504 "F3" => KeyCode::F3,
505 "F4" => KeyCode::F4,
506 "F5" => KeyCode::F5,
507 "F6" => KeyCode::F6,
508 "F7" => KeyCode::F7,
509 "F8" => KeyCode::F8,
510 "F9" => KeyCode::F9,
511 "F10" => KeyCode::F10,
512 "F11" => KeyCode::F11,
513 "F12" => KeyCode::F12,
514
515 "Delete" => KeyCode::Delete,
517 "Home" => KeyCode::Home,
518 "End" => KeyCode::End,
519 "PageUp" => KeyCode::PageUp,
520 "PageDown" => KeyCode::PageDown,
521
522 _ => KeyCode::Unknown,
523 }
524 }
525
526 fn cursor_icon_to_css(icon: CursorIcon) -> &'static str {
527 icon.css_name()
528 }
529}
530
531unsafe impl Send for WebPlatform {}
538unsafe impl Sync for WebPlatform {}
539
540impl PlatformBackend for WebPlatform {
545 fn name(&self) -> &'static str {
546 todo!("not yet implemented for this platform")
547 }
548
549 fn create_window(&mut self, config: WindowConfig) -> Result<WindowId, PlatformError> {
550 let mut state = self.state.borrow_mut();
551
552 let canvas = &state.canvas;
554 canvas.set_width((config.width as f64 * state.scale_factor) as u32);
555 canvas.set_height((config.height as f64 * state.scale_factor) as u32);
556
557 state.document.set_title(&config.title);
559
560 state.config = config;
561 state.event_queue.push_back(PlatformEvent::WindowCreated);
562
563 Ok(state.window_id)
564 }
565
566 fn close_window(&mut self, _window_id: WindowId) -> Result<(), PlatformError> {
567 let mut state = self.state.borrow_mut();
568 state.event_queue.push_back(PlatformEvent::WindowDestroyed);
569 Ok(())
570 }
571
572 fn primary_window(&self) -> Option<WindowId> {
573 todo!("not yet implemented for this platform")
574 }
575
576 fn poll_events(&mut self) -> Vec<PlatformEvent> {
577 todo!("not yet implemented for this platform")
578 }
579
580 fn request_redraw(&self, _id: WindowId) {
581 }
583}
584
585impl SystemIntegration for WebPlatform {
586 fn get_clipboard(&self) -> Option<String> {
587 todo!("not yet implemented for this platform")
588 }
589
590 fn set_clipboard(&self, _text: &str) {
591 todo!("not yet implemented for this platform")
592 }
593
594 fn get_system_theme(&self) -> Option<SystemTheme> {
595 let state = self.state.borrow();
596
597 if let Ok(Some(media_query)) = state.window.match_media("(prefers-color-scheme: dark)") {
599 if media_query.matches() {
600 return Some(SystemTheme::Dark);
601 }
602 }
603
604 Some(SystemTheme::Light)
605 }
606}
607
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn test_keycode_mapping() {
615 assert_eq!(WebPlatform::map_keycode("KeyA"), KeyCode::A);
616 assert_eq!(WebPlatform::map_keycode("Digit5"), KeyCode::Num5);
617 assert_eq!(WebPlatform::map_keycode("Enter"), KeyCode::Enter);
618 assert_eq!(WebPlatform::map_keycode("ArrowLeft"), KeyCode::ArrowLeft);
619 assert_eq!(WebPlatform::map_keycode("Unknown"), KeyCode::Unknown);
620 }
621
622 #[test]
623 fn test_mouse_button_mapping() {
624 assert_eq!(WebPlatform::map_mouse_button(0), MouseButton::Left);
625 assert_eq!(WebPlatform::map_mouse_button(1), MouseButton::Middle);
626 assert_eq!(WebPlatform::map_mouse_button(2), MouseButton::Right);
627 }
628
629 #[test]
630 fn test_cursor_icon_css() {
631 assert_eq!(WebPlatform::cursor_icon_to_css(CursorIcon::Default), "default");
632 assert_eq!(WebPlatform::cursor_icon_to_css(CursorIcon::PointingHand), "pointer");
633 assert_eq!(WebPlatform::cursor_icon_to_css(CursorIcon::Text), "text");
634 assert_eq!(WebPlatform::cursor_icon_to_css(CursorIcon::Grab), "grab");
635 assert_eq!(WebPlatform::cursor_icon_to_css(CursorIcon::ResizeVertical), "ns-resize");
636 }
637}