Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/platform/wasm/mod.rs
1pub(crate) mod hidden_input;
2pub(crate) mod mobile_detection;
3pub(crate) mod soft_keyboard;
4 
5use gloo::events::{EventListener, EventListenerOptions};
6use wasm_bindgen::{JsCast, UnwrapThrowExt};
7 
8use crate::{keymap::Keystroke, windowing::winit::app::CustomEvent};
9 
10pub use hidden_input::{HiddenInput, HiddenInputEvent, InputCallback};
11pub use mobile_detection::{is_mobile_device, is_mobile_user_agent};
12pub use soft_keyboard::{SoftKeyboardInput, SoftKeyboardManager, SoftKeyboardState};
13 
14// Re-export a couple winit types and modules as the concrete implementations
15// for the wasm platform.
16pub use crate::windowing::winit::app::App;
17 
18// Re-export the functions from the core crate.
19pub use strato_ui_core::platform::wasm::*;
20 
21use super::KEYS_TO_IGNORE;
22 
23fn get_visual_viewport_dimensions() -> Option<(f32, f32)> {
24 let window = gloo::utils::window();
25 let vv = js_sys::Reflect::get(&window, &"visualViewport".into()).ok()?;
26 let width = js_sys::Reflect::get(&vv, &"width".into())
27 .ok()
28 .and_then(|v| v.as_f64())? as f32;
29 let height = js_sys::Reflect::get(&vv, &"height".into())
30 .ok()
31 .and_then(|v| v.as_f64())? as f32;
32 (width > 0.0 && height > 0.0).then_some((width, height))
33}
34 
35/// Listens for visual viewport changes (e.g., soft keyboard appearing) on mobile.
36pub(crate) fn setup_visual_viewport_resize_listener(
37 event_loop_proxy: winit::event_loop::EventLoopProxy<CustomEvent>,
38) {
39 if !mobile_detection::is_mobile_device() {
40 return;
41 }
42 
43 let window = gloo::utils::window();
44 let visual_viewport = js_sys::Reflect::get(&window, &"visualViewport".into())
45 .ok()
46 .and_then(|v| v.dyn_into::<web_sys::EventTarget>().ok());
47 
48 let Some(visual_viewport) = visual_viewport else {
49 log::warn!("Visual viewport API not available");
50 return;
51 };
52 
53 // Fire once immediately so the first render uses the correct visual viewport height.
54 if let Some((width, height)) = get_visual_viewport_dimensions() {
55 let _ = event_loop_proxy.send_event(CustomEvent::VisualViewportResized { width, height });
56 }
57 
58 EventListener::new(&visual_viewport, "resize", move |_| {
59 if let Some((width, height)) = get_visual_viewport_dimensions() {
60 log::debug!("Visual viewport resized to {}x{}", width, height);
61 let _ =
62 event_loop_proxy.send_event(CustomEvent::VisualViewportResized { width, height });
63 }
64 })
65 .forget();
66}
67 
68/// Adds an event listener to the main canvas element which calls preventDefault on all important
69/// events except for those we explicitly want to pass through to the browser.
70pub(crate) fn add_prevent_default_listener(canvas: &web_sys::HtmlCanvasElement) {
71 // Event types where we unconditionally call prevent_default.
72 let events_types_to_prevent = [
73 "touchstart",
74 "wheel",
75 "contextmenu",
76 "pointerdown",
77 "pointermove",
78 ];
79 
80 // Keyboard events where we call prevent_default in some cases.
81 let key_events_to_partially_prevent = ["keyup", "keydown"];
82 
83 for event_type in events_types_to_prevent.into_iter() {
84 let prevent_default_listener = Box::new(EventListener::new_with_options(
85 canvas,
86 event_type,
87 EventListenerOptions::enable_prevent_default(),
88 move |event| {
89 event.prevent_default();
90 },
91 ));
92 
93 // We want this to live for the lifetime of the page and we're never going to need to
94 // interact with it again, so we leak it so it can live forever.
95 Box::leak(prevent_default_listener);
96 }
97 
98 for event_type in key_events_to_partially_prevent.into_iter() {
99 let prevent_default_listener = Box::new(EventListener::new_with_options(
100 canvas,
101 event_type,
102 EventListenerOptions::enable_prevent_default(),
103 move |event| {
104 let event = event.dyn_ref::<web_sys::KeyboardEvent>().unwrap_throw();
105 let keystroke = Keystroke {
106 ctrl: event.ctrl_key(),
107 alt: event.alt_key(),
108 shift: event.shift_key(),
109 cmd: event.meta_key(), // The browser's 'meta' corresponds to our 'command'.
110 meta: false,
111 key: event.key(),
112 };
113 
114 let allow_default_event = KEYS_TO_IGNORE.contains(&keystroke);
115 if !allow_default_event {
116 event.prevent_default();
117 }
118 },
119 ));
120 Box::leak(prevent_default_listener);
121 }
122}
123 
124pub(crate) fn add_paste_listener(event_loop_proxy: winit::event_loop::EventLoopProxy<CustomEvent>) {
125 EventListener::new(&gloo::utils::document(), "paste", move |event| {
126 let event = event.dyn_ref::<web_sys::ClipboardEvent>().unwrap_throw();
127 let Some(data) = event.clipboard_data() else {
128 log::warn!("Received paste event without clipboard data.");
129 return;
130 };
131 
132 let content = crate::clipboard::ClipboardContent {
133 plain_text: data.get_data("text").unwrap_or_default(),
134 html: data
135 .get_data("text/html")
136 .ok()
137 .and_then(|s| (!s.is_empty()).then_some(s)), // Set this to None if the html data is empty
138 ..Default::default()
139 };
140 
141 let _ = event_loop_proxy.send_event(CustomEvent::Clipboard(
142 crate::windowing::winit::app::ClipboardEvent::Paste(content),
143 ));
144 })
145 .forget();
146}
147 
148pub(crate) fn add_network_connection_listener(
149 event_loop_proxy: winit::event_loop::EventLoopProxy<CustomEvent>,
150) {
151 let event_loop_proxy_clone = event_loop_proxy.clone();
152 
153 EventListener::new(&gloo::utils::window(), "offline", move |_event| {
154 let _ = event_loop_proxy_clone
155 .send_event(crate::windowing::winit::app::CustomEvent::InternetDisconnected);
156 })
157 .forget();
158 
159 EventListener::new(&gloo::utils::window(), "online", move |_event| {
160 let _ = event_loop_proxy
161 .send_event(crate::windowing::winit::app::CustomEvent::InternetConnected);
162 })
163 .forget();
164}
165 
166pub(crate) fn add_system_theme_listener(
167 event_loop_proxy: winit::event_loop::EventLoopProxy<CustomEvent>,
168) {
169 // This could alternatively be written as a listener on "(prefers-color-scheme: light)".
170 if let Ok(Some(media_query_list)) =
171 gloo::utils::window().match_media("(prefers-color-scheme: dark)")
172 {
173 EventListener::new(&media_query_list, "change", move |_event| {
174 let _ = event_loop_proxy
175 .send_event(crate::windowing::winit::app::CustomEvent::SystemThemeChanged);
176 })
177 .forget();
178 }
179}
180