pub(crate) mod hidden_input; pub(crate) mod mobile_detection; pub(crate) mod soft_keyboard; use gloo::events::{EventListener, EventListenerOptions}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use crate::{keymap::Keystroke, windowing::winit::app::CustomEvent}; pub use hidden_input::{HiddenInput, HiddenInputEvent, InputCallback}; pub use mobile_detection::{is_mobile_device, is_mobile_user_agent}; pub use soft_keyboard::{SoftKeyboardInput, SoftKeyboardManager, SoftKeyboardState}; // Re-export a couple winit types and modules as the concrete implementations // for the wasm platform. pub use crate::windowing::winit::app::App; // Re-export the functions from the core crate. pub use galaxyui_core::platform::wasm::*; use super::KEYS_TO_IGNORE; fn get_visual_viewport_dimensions() -> Option<(f32, f32)> { let window = gloo::utils::window(); let vv = js_sys::Reflect::get(&window, &"visualViewport".into()).ok()?; let width = js_sys::Reflect::get(&vv, &"width".into()) .ok() .and_then(|v| v.as_f64())? as f32; let height = js_sys::Reflect::get(&vv, &"height".into()) .ok() .and_then(|v| v.as_f64())? as f32; (width > 0.0 && height > 0.0).then_some((width, height)) } /// Listens for visual viewport changes (e.g., soft keyboard appearing) on mobile. pub(crate) fn setup_visual_viewport_resize_listener( event_loop_proxy: winit::event_loop::EventLoopProxy, ) { if !mobile_detection::is_mobile_device() { return; } let window = gloo::utils::window(); let visual_viewport = js_sys::Reflect::get(&window, &"visualViewport".into()) .ok() .and_then(|v| v.dyn_into::().ok()); let Some(visual_viewport) = visual_viewport else { log::warn!("Visual viewport API not available"); return; }; // Fire once immediately so the first render uses the correct visual viewport height. if let Some((width, height)) = get_visual_viewport_dimensions() { let _ = event_loop_proxy.send_event(CustomEvent::VisualViewportResized { width, height }); } EventListener::new(&visual_viewport, "resize", move |_| { if let Some((width, height)) = get_visual_viewport_dimensions() { log::debug!("Visual viewport resized to {}x{}", width, height); let _ = event_loop_proxy.send_event(CustomEvent::VisualViewportResized { width, height }); } }) .forget(); } /// Adds an event listener to the main canvas element which calls preventDefault on all important /// events except for those we explicitly want to pass through to the browser. pub(crate) fn add_prevent_default_listener(canvas: &web_sys::HtmlCanvasElement) { // Event types where we unconditionally call prevent_default. let events_types_to_prevent = [ "touchstart", "wheel", "contextmenu", "pointerdown", "pointermove", ]; // Keyboard events where we call prevent_default in some cases. let key_events_to_partially_prevent = ["keyup", "keydown"]; for event_type in events_types_to_prevent.into_iter() { let prevent_default_listener = Box::new(EventListener::new_with_options( canvas, event_type, EventListenerOptions::enable_prevent_default(), move |event| { event.prevent_default(); }, )); // We want this to live for the lifetime of the page and we're never going to need to // interact with it again, so we leak it so it can live forever. Box::leak(prevent_default_listener); } for event_type in key_events_to_partially_prevent.into_iter() { let prevent_default_listener = Box::new(EventListener::new_with_options( canvas, event_type, EventListenerOptions::enable_prevent_default(), move |event| { let event = event.dyn_ref::().unwrap_throw(); let keystroke = Keystroke { ctrl: event.ctrl_key(), alt: event.alt_key(), shift: event.shift_key(), cmd: event.meta_key(), // The browser's 'meta' corresponds to our 'command'. meta: false, key: event.key(), }; let allow_default_event = KEYS_TO_IGNORE.contains(&keystroke); if !allow_default_event { event.prevent_default(); } }, )); Box::leak(prevent_default_listener); } } pub(crate) fn add_paste_listener(event_loop_proxy: winit::event_loop::EventLoopProxy) { EventListener::new(&gloo::utils::document(), "paste", move |event| { let event = event.dyn_ref::().unwrap_throw(); let Some(data) = event.clipboard_data() else { log::warn!("Received paste event without clipboard data."); return; }; let content = crate::clipboard::ClipboardContent { plain_text: data.get_data("text").unwrap_or_default(), html: data .get_data("text/html") .ok() .and_then(|s| (!s.is_empty()).then_some(s)), // Set this to None if the html data is empty ..Default::default() }; let _ = event_loop_proxy.send_event(CustomEvent::Clipboard( crate::windowing::winit::app::ClipboardEvent::Paste(content), )); }) .forget(); } pub(crate) fn add_network_connection_listener( event_loop_proxy: winit::event_loop::EventLoopProxy, ) { let event_loop_proxy_clone = event_loop_proxy.clone(); EventListener::new(&gloo::utils::window(), "offline", move |_event| { let _ = event_loop_proxy_clone .send_event(crate::windowing::winit::app::CustomEvent::InternetDisconnected); }) .forget(); EventListener::new(&gloo::utils::window(), "online", move |_event| { let _ = event_loop_proxy .send_event(crate::windowing::winit::app::CustomEvent::InternetConnected); }) .forget(); } pub(crate) fn add_system_theme_listener( event_loop_proxy: winit::event_loop::EventLoopProxy, ) { // This could alternatively be written as a listener on "(prefers-color-scheme: light)". if let Ok(Some(media_query_list)) = gloo::utils::window().match_media("(prefers-color-scheme: dark)") { EventListener::new(&media_query_list, "change", move |_event| { let _ = event_loop_proxy .send_event(crate::windowing::winit::app::CustomEvent::SystemThemeChanged); }) .forget(); } }