From f64404b2a3cf7085d69e696c17ca4a8b8d8da09c Mon Sep 17 00:00:00 2001 From: daxpedda Date: Mon, 1 Jul 2024 13:30:00 +0200 Subject: [PATCH] Web: fix `MouseMotion` coordinate space --- Cargo.toml | 1 + src/changelog/unreleased.md | 2 +- src/platform_impl/web/event_loop/runner.rs | 21 +++--- src/platform_impl/web/web_sys/event.rs | 55 ++++++++++++---- src/platform_impl/web/web_sys/mod.rs | 75 +++++++++++++++++++++- 5 files changed, 126 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4302edf6e6..270df0280c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -302,6 +302,7 @@ web_sys = { package = "web-sys", version = "0.3.64", features = [ 'MediaQueryList', 'MessageChannel', 'MessagePort', + 'Navigator', 'Node', 'PageTransitionEvent', 'PointerEvent', diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 9b0751f207..385c1d0340 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -56,7 +56,6 @@ changelog entry. to send specific data to be processed on the main thread. - Changed `EventLoopProxy::send_event` to `EventLoopProxy::wake_up`, it now only wakes up the loop. -- On Web, slightly improve accuracy of `DeviceEvent::MouseMotion`. - `ApplicationHandler::create|destroy_surfaces()` was split off from `ApplicationHandler::resumed/suspended()`. @@ -80,3 +79,4 @@ changelog entry. ### Fixed - On Wayland, avoid crashing when compositor is misbehaving. +- Account for different browser engine implementations of pointer movement coordinate space. diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index d7a23d5f4e..5a7bbbb826 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -234,7 +234,7 @@ impl Shared { )); let runner = self.clone(); - let mut delta = backend::event::MouseDelta::new(); + let window = self.window().clone(); *self.0.on_mouse_move.borrow_mut() = Some(EventListenerHandle::new( self.window().clone(), "pointermove", @@ -273,26 +273,23 @@ impl Shared { } // pointer move event + let mut delta = backend::event::MouseDelta::init(&window, &event); runner.send_events(backend::event::pointer_move_event(event).flat_map(|event| { - let delta = delta.delta(&event); - - if delta.x == 0 && delta.y == 0 { - return None.into_iter().chain(None).chain(None); - } + let delta = delta.delta(&event).to_physical(backend::scale_factor(&window)); - let x_motion = (delta.x != 0).then_some(Event::DeviceEvent { + let x_motion = (delta.x != 0.0).then_some(Event::DeviceEvent { device_id, - event: DeviceEvent::Motion { axis: 0, value: delta.x.into() }, + event: DeviceEvent::Motion { axis: 0, value: delta.x }, }); - let y_motion = (delta.y != 0).then_some(Event::DeviceEvent { + let y_motion = (delta.y != 0.0).then_some(Event::DeviceEvent { device_id, - event: DeviceEvent::Motion { axis: 1, value: delta.y.into() }, + event: DeviceEvent::Motion { axis: 1, value: delta.y }, }); - x_motion.into_iter().chain(y_motion).chain(Some(Event::DeviceEvent { + x_motion.into_iter().chain(y_motion).chain(iter::once(Event::DeviceEvent { device_id, - event: DeviceEvent::MouseMotion { delta: (delta.x.into(), delta.y.into()) }, + event: DeviceEvent::MouseMotion { delta: (delta.x, delta.y) }, })) })); }), diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/event.rs index b76cce9b5a..ddd125765c 100644 --- a/src/platform_impl/web/web_sys/event.rs +++ b/src/platform_impl/web/web_sys/event.rs @@ -1,13 +1,15 @@ use crate::event::{MouseButton, MouseScrollDelta}; use crate::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; -use dpi::{LogicalPosition, PhysicalPosition}; +use dpi::{LogicalPosition, PhysicalPosition, Position}; use smol_str::SmolStr; use std::cell::OnceCell; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{KeyboardEvent, MouseEvent, PointerEvent, WheelEvent}; +use super::Engine; + bitflags::bitflags! { // https://www.w3.org/TR/pointerevents3/#the-buttons-property #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -95,23 +97,48 @@ pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { LogicalPosition { x: event.offset_x(), y: event.offset_y() } } -pub struct MouseDelta(Option>); +// TODO: Remove this when Firefox supports correct movement values in coalesced events and browsers +// have agreed on what coordinate space `movementX/Y` is using. +// See . +// See . +pub enum MouseDelta { + Chromium, + Gecko { old_position: LogicalPosition, old_delta: LogicalPosition }, + Other, +} impl MouseDelta { - pub fn new() -> Self { - Self(None) + pub fn init(window: &web_sys::Window, event: &PointerEvent) -> Self { + match super::engine(window) { + Some(Engine::Chromium) => Self::Chromium, + // Firefox has wrong movement values in coalesced events. + Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko { + old_position: mouse_position(event), + old_delta: LogicalPosition::new( + event.movement_x() as f64, + event.movement_y() as f64, + ), + }, + _ => Self::Other, + } } - pub fn delta(&mut self, event: &MouseEvent) -> PhysicalPosition { - let new = PhysicalPosition::new(event.screen_x(), event.screen_y()); - - if let Some(old) = self.0 { - let delta = PhysicalPosition::new(new.x - old.x, new.y - old.y); - self.0 = Some(new); - delta - } else { - self.0 = Some(new); - PhysicalPosition::default() + pub fn delta(&mut self, event: &MouseEvent) -> Position { + match self { + MouseDelta::Chromium => { + PhysicalPosition::new(event.movement_x(), event.movement_y()).into() + }, + MouseDelta::Gecko { old_position, old_delta } => { + let new_position = mouse_position(event); + let x = new_position.x - old_position.x + old_delta.x; + let y = new_position.y - old_position.y + old_delta.y; + *old_position = new_position; + *old_delta = LogicalPosition::new(0., 0.); + LogicalPosition::new(x, y).into() + }, + MouseDelta::Other => { + LogicalPosition::new(event.movement_x(), event.movement_y()).into() + }, } } } diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index b0a8fbae42..08962b49bc 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -9,6 +9,8 @@ mod pointer; mod resize_scaling; mod schedule; +use std::sync::OnceLock; + pub use self::canvas::{Canvas, Style}; pub use self::event::ButtonsState; pub use self::event_handle::EventListenerHandle; @@ -16,8 +18,13 @@ pub use self::resize_scaling::ResizeScaleHandle; pub use self::schedule::Schedule; use crate::dpi::{LogicalPosition, LogicalSize}; +use js_sys::Array; use wasm_bindgen::closure::Closure; -use web_sys::{Document, HtmlCanvasElement, PageTransitionEvent, VisibilityState}; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use web_sys::{ + Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState, Window, +}; pub fn throw(msg: &str) { wasm_bindgen::throw_str(msg); @@ -158,3 +165,69 @@ pub fn is_visible(document: &Document) -> bool { } pub type RawCanvasType = HtmlCanvasElement; + +#[derive(Clone, Copy)] +pub enum Engine { + Chromium, + Gecko, + WebKit, +} + +pub fn engine(window: &Window) -> Option { + static ENGINE: OnceLock> = OnceLock::new(); + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(extends = Navigator)] + type NavigatorExt; + + #[wasm_bindgen(method, getter, js_name = userAgentData)] + fn user_agent_data(this: &NavigatorExt) -> Option; + + type NavigatorUaData; + + #[wasm_bindgen(method, getter)] + fn brands(this: &NavigatorUaData) -> Array; + + type NavigatorUaBrandVersion; + + #[wasm_bindgen(method, getter)] + fn brand(this: &NavigatorUaBrandVersion) -> String; + } + + *ENGINE.get_or_init(|| { + let navigator: NavigatorExt = window.navigator().unchecked_into(); + + if let Some(data) = navigator.user_agent_data() { + for brand in data + .brands() + .iter() + .map(NavigatorUaBrandVersion::unchecked_from_js) + .map(|brand| brand.brand()) + { + match brand.as_str() { + "Chromium" => return Some(Engine::Chromium), + // TODO: verify when Firefox actually implements it. + "Gecko" => return Some(Engine::Gecko), + // TODO: verify when Safari actually implements it. + "WebKit" => return Some(Engine::WebKit), + _ => (), + } + } + + None + } else { + let data = navigator.user_agent().ok()?; + + if data.contains("Chrome/") { + Some(Engine::Chromium) + } else if data.contains("Gecko/") { + Some(Engine::Gecko) + } else if data.contains("AppleWebKit/") { + Some(Engine::WebKit) + } else { + None + } + } + }) +}