From 949cb0f203b6fc4f9cd59ddd11bb4019dbeedc34 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sat, 29 Jun 2024 00:52:05 +0200 Subject: [PATCH] Web: fix `MouseMotion` coordinate space (#3770) --- Cargo.toml | 1 + src/changelog/unreleased.md | 1 + src/platform_impl/web/web_sys/event.rs | 89 +++++++++++--------------- src/platform_impl/web/web_sys/mod.rs | 75 +++++++++++++++++++++- 4 files changed, 113 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8ed5f8bc59..e36fc26014 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -309,6 +309,7 @@ features = [ 'MediaQueryList', 'MessageChannel', 'MessagePort', + 'Navigator', 'Node', 'PageTransitionEvent', 'PointerEvent', diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 11f65fb388..d867b31cc1 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -50,3 +50,4 @@ changelog entry. - On Web, fix `WindowEvent::Resized` not using `requestAnimationFrame` when sending `WindowEvent::RedrawRequested` and also potentially causing `WindowEvent::RedrawRequested` to not be de-duplicated. +- Account for different browser engine implementations of pointer movement coordinate space. diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/event.rs index 51df63714b..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::dpi::LogicalPosition; use crate::event::{MouseButton, MouseScrollDelta}; use crate::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; +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,42 +97,48 @@ pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { LogicalPosition { x: event.offset_x(), y: event.offset_y() } } -// TODO: Remove this when Firefox supports correct movement values in coalesced events. +// 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 . -pub struct MouseDelta(Option); - -pub struct MouseDeltaInner { - old_position: LogicalPosition, - old_delta: LogicalPosition, +// See . +pub enum MouseDelta { + Chromium, + Gecko { old_position: LogicalPosition, old_delta: LogicalPosition }, + Other, } impl MouseDelta { pub fn init(window: &web_sys::Window, event: &PointerEvent) -> Self { - // Firefox has wrong movement values in coalesced events, we will detect that by checking - // for `pointerrawupdate` support. Presumably an implementation of `pointerrawupdate` - // should require correct movement values, otherwise uncoalesced events might be broken as - // well. - Self((!has_pointer_raw_support(window) && has_coalesced_events_support(event)).then(|| { - MouseDeltaInner { + 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 { - x: event.movement_x() as f64, - y: event.movement_y() as f64, - }, - } - })) + old_delta: LogicalPosition::new( + event.movement_x() as f64, + event.movement_y() as f64, + ), + }, + _ => Self::Other, + } } - pub fn delta(&mut self, event: &MouseEvent) -> LogicalPosition { - if let Some(inner) = &mut self.0 { - let new_position = mouse_position(event); - let x = new_position.x - inner.old_position.x + inner.old_delta.x; - let y = new_position.y - inner.old_position.y + inner.old_delta.y; - inner.old_position = new_position; - inner.old_delta = LogicalPosition::new(0., 0.); - LogicalPosition::new(x, y) - } else { - LogicalPosition { x: event.movement_x() as f64, y: event.movement_y() as f64 } + 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() + }, } } } @@ -238,29 +246,6 @@ pub fn pointer_move_event(event: PointerEvent) -> impl Iterator. -pub fn has_pointer_raw_support(window: &web_sys::Window) -> bool { - thread_local! { - static POINTER_RAW_SUPPORT: OnceCell = const { OnceCell::new() }; - } - - POINTER_RAW_SUPPORT.with(|support| { - *support.get_or_init(|| { - #[wasm_bindgen] - extern "C" { - type PointerRawSupport; - - #[wasm_bindgen(method, getter, js_name = onpointerrawupdate)] - fn has_on_pointerrawupdate(this: &PointerRawSupport) -> JsValue; - } - - let support: &PointerRawSupport = window.unchecked_ref(); - !support.has_on_pointerrawupdate().is_undefined() - }) - }) -} - // TODO: Remove when Safari supports `getCoalescedEvents`. // See . pub fn has_coalesced_events_support(event: &PointerEvent) -> bool { 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 + } + } + }) +}