Skip to content

Commit

Permalink
Web: fix MouseMotion coordinate space (#3770)
Browse files Browse the repository at this point in the history
  • Loading branch information
daxpedda authored and kchibisov committed Jul 16, 2024
1 parent bf68ac0 commit 949cb0f
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 53 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ features = [
'MediaQueryList',
'MessageChannel',
'MessagePort',
'Navigator',
'Node',
'PageTransitionEvent',
'PointerEvent',
Expand Down
1 change: 1 addition & 0 deletions src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
89 changes: 37 additions & 52 deletions src/platform_impl/web/web_sys/event.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -95,42 +97,48 @@ pub fn mouse_position(event: &MouseEvent) -> LogicalPosition<f64> {
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 <https://bugzilla.mozilla.org/show_bug.cgi?id=1753724>.
pub struct MouseDelta(Option<MouseDeltaInner>);

pub struct MouseDeltaInner {
old_position: LogicalPosition<f64>,
old_delta: LogicalPosition<f64>,
// See <https://github.com/w3c/pointerlock/issues/42>.
pub enum MouseDelta {
Chromium,
Gecko { old_position: LogicalPosition<f64>, old_delta: LogicalPosition<f64> },
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<f64> {
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()
},
}
}
}
Expand Down Expand Up @@ -238,29 +246,6 @@ pub fn pointer_move_event(event: PointerEvent) -> impl Iterator<Item = PointerEv
}
}

// TODO: Remove when all browsers implement it correctly.
// See <https://github.com/rust-windowing/winit/issues/2875>.
pub fn has_pointer_raw_support(window: &web_sys::Window) -> bool {
thread_local! {
static POINTER_RAW_SUPPORT: OnceCell<bool> = 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 <https://bugs.webkit.org/show_bug.cgi?id=210454>.
pub fn has_coalesced_events_support(event: &PointerEvent) -> bool {
Expand Down
75 changes: 74 additions & 1 deletion src/platform_impl/web/web_sys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@ 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;
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);
Expand Down Expand Up @@ -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<Engine> {
static ENGINE: OnceLock<Option<Engine>> = 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<NavigatorUaData>;

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
}
}
})
}

0 comments on commit 949cb0f

Please sign in to comment.