180 lines
6.6 KiB
Rust
180 lines
6.6 KiB
Rust
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<CustomEvent>,
|
|
) {
|
|
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::<web_sys::EventTarget>().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::<web_sys::KeyboardEvent>().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<CustomEvent>) {
|
|
EventListener::new(&gloo::utils::document(), "paste", move |event| {
|
|
let event = event.dyn_ref::<web_sys::ClipboardEvent>().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<CustomEvent>,
|
|
) {
|
|
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<CustomEvent>,
|
|
) {
|
|
// 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();
|
|
}
|
|
}
|