diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 02a091c76f..06f794cbc5 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -44,6 +44,9 @@ changelog entry. - Add `ActiveEventLoop::create_proxy()`. - On Web, implement `Error` for `platform::web::CustomCursorError`. +- On Web, add `ActiveEventLoopExtWeb::is_cursor_lock_raw()` to determine if + `DeviceEvent::MouseMotion` is returning raw data, not OS accelerated, when using + `CursorGrabMode::Locked`. ### Changed @@ -71,6 +74,8 @@ changelog entry. `EventLoopExtRunOnDemand::run_app_on_demand` to accept a `impl ApplicationHandler` directly, instead of requiring a `&mut` reference to it. - On Web, `Window::canvas()` now returns a reference. +- On Web, `CursorGrabMode::Locked` now lets `DeviceEvent::MouseMotion` return raw data, not OS + accelerated, if the browser supports it. ### Removed diff --git a/src/event.rs b/src/event.rs index 5cb8037993..3d14f70417 100644 --- a/src/event.rs +++ b/src/event.rs @@ -467,6 +467,22 @@ pub enum DeviceEvent { /// /// This represents raw, unfiltered physical motion. Not to be confused with /// [`WindowEvent::CursorMoved`]. + /// + /// ## Platform-specific + /// + /// **Web:** Only returns raw data, not OS accelerated, if [`CursorGrabMode::Locked`] is used + /// and browser support is available, see + #[cfg_attr( + any(web_platform, docsrs), + doc = "[`ActiveEventLoopExtWeb::is_cursor_lock_raw()`][crate::platform::web::ActiveEventLoopExtWeb::is_cursor_lock_raw()]." + )] + #[cfg_attr( + not(any(web_platform, docsrs)), + doc = "`ActiveEventLoopExtWeb::is_cursor_lock_raw()`." + )] + /// + #[rustfmt::skip] + /// [`CursorGrabMode::Locked`]: crate::window::CursorGrabMode::Locked MouseMotion { /// (x, y) change in position in unspecified units. /// diff --git a/src/platform/web.rs b/src/platform/web.rs index f737ba3058..2974ba3d66 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -84,6 +84,14 @@ pub trait WindowExtWeb { /// Some events are impossible to prevent. E.g. Firefox allows to access the native browser /// context menu with Shift+Rightclick. fn set_prevent_default(&self, prevent_default: bool); + + /// Returns whether using [`CursorGrabMode::Locked`] returns raw, un-accelerated mouse input. + /// + /// This is the same as [`ActiveEventLoop::is_cursor_lock_raw()`], and is provided for + /// convenience. + /// + /// [`CursorGrabMode::Locked`]: crate::window::CursorGrabMode::Locked + fn is_cursor_lock_raw(&self) -> bool; } impl WindowExtWeb for Window { @@ -99,6 +107,10 @@ impl WindowExtWeb for Window { fn set_prevent_default(&self, prevent_default: bool) { self.window.set_prevent_default(prevent_default) } + + fn is_cursor_lock_raw(&self) -> bool { + self.window.is_cursor_lock_raw() + } } pub trait WindowAttributesExtWeb { @@ -262,6 +274,11 @@ pub trait ActiveEventLoopExtWeb { /// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the /// cursor has completely finished loading. fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture; + + /// Returns whether using [`CursorGrabMode::Locked`] returns raw, un-accelerated mouse input. + /// + /// [`CursorGrabMode::Locked`]: crate::window::CursorGrabMode::Locked + fn is_cursor_lock_raw(&self) -> bool; } impl ActiveEventLoopExtWeb for ActiveEventLoop { @@ -289,6 +306,11 @@ impl ActiveEventLoopExtWeb for ActiveEventLoop { fn wait_until_strategy(&self) -> WaitUntilStrategy { self.p.wait_until_strategy() } + + #[inline] + fn is_cursor_lock_raw(&self) -> bool { + self.p.is_cursor_lock_raw() + } } /// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll]. diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index ba3b4f4a5a..4cca979432 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use web_sys::Element; use super::super::monitor::MonitorHandle; -use super::super::KeyEventExtra; +use super::super::{lock, KeyEventExtra}; use super::device::DeviceId; use super::runner::{EventWrapper, WeakShared}; use super::window::WindowId; @@ -652,6 +652,10 @@ impl ActiveEventLoop { self.runner.wait_until_strategy() } + pub(crate) fn is_cursor_lock_raw(&self) -> bool { + lock::is_cursor_lock_raw(self.runner.window(), self.runner.document()) + } + pub(crate) fn waker(&self) -> Waker { self.runner.waker() } diff --git a/src/platform_impl/web/lock.rs b/src/platform_impl/web/lock.rs new file mode 100644 index 0000000000..b4c752219b --- /dev/null +++ b/src/platform_impl/web/lock.rs @@ -0,0 +1,82 @@ +use std::cell::OnceCell; + +use js_sys::{Object, Promise}; +use tracing::error; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{console, Document, DomException, Element, Window}; + +pub(crate) fn is_cursor_lock_raw(window: &Window, document: &Document) -> bool { + thread_local! { + static IS_CURSOR_LOCK_RAW: OnceCell = const { OnceCell::new() }; + } + + IS_CURSOR_LOCK_RAW.with(|cell| { + *cell.get_or_init(|| { + // TODO: Remove when Chrome can better advertise that they don't support unaccelerated + // movement on Linux. + // See . + if super::web_sys::chrome_linux(window) { + return false; + } + + let element: ElementExt = document.create_element("div").unwrap().unchecked_into(); + let promise = element.request_pointer_lock(); + + if promise.is_undefined() { + false + } else { + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|_| ()); + } + + let promise: Promise = promise.unchecked_into(); + let _ = REJECT_HANDLER.with(|handler| promise.catch(handler)); + true + } + }) + }) +} + +pub(crate) fn request_pointer_lock(window: &Window, document: &Document, element: &Element) { + if is_cursor_lock_raw(window, document) { + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|error: JsValue| { + if let Some(error) = error.dyn_ref::() { + error!("Failed to lock pointer. {}: {}", error.name(), error.message()); + } else { + console::error_1(&error); + error!("Failed to lock pointer"); + } + }); + } + + let element: &ElementExt = element.unchecked_ref(); + let options: PointerLockOptions = Object::new().unchecked_into(); + options.set_unadjusted_movement(true); + let _ = REJECT_HANDLER + .with(|handler| element.request_pointer_lock_with_options(&options).catch(handler)); + } else { + element.request_pointer_lock(); + } +} + +#[wasm_bindgen] +extern "C" { + type ElementExt; + + #[wasm_bindgen(method, js_name = requestPointerLock)] + fn request_pointer_lock(this: &ElementExt) -> JsValue; + + #[wasm_bindgen(method, js_name = requestPointerLock)] + fn request_pointer_lock_with_options( + this: &ElementExt, + options: &PointerLockOptions, + ) -> Promise; + + type PointerLockOptions; + + #[wasm_bindgen(method, setter, js_name = unadjustedMovement)] + fn set_unadjusted_movement(this: &PointerLockOptions, value: bool); +} diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index 9a8e3c6f95..7e42a9fd02 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -26,6 +26,7 @@ mod device; mod error; mod event_loop; mod keyboard; +mod lock; mod main_thread; mod monitor; mod web_sys; diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 7a046a6205..d2a9947e8b 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -176,15 +176,6 @@ impl Canvas { }) } - pub fn set_cursor_lock(&self, lock: bool) -> Result<(), RootOE> { - if lock { - self.raw().request_pointer_lock(); - } else { - self.common.document.exit_pointer_lock(); - } - Ok(()) - } - pub fn set_attribute(&self, attribute: &str, value: &str) { self.common .raw diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 97019666b6..8d1c8cb66d 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -9,7 +9,7 @@ mod pointer; mod resize_scaling; mod schedule; -use std::sync::OnceLock; +use std::cell::OnceCell; use js_sys::Array; use wasm_bindgen::closure::Closure; @@ -173,9 +173,24 @@ pub enum Engine { WebKit, } +struct UserAgentData { + engine: Option, + chrome_linux: bool, +} + +thread_local! { + static USER_AGENT_DATA: OnceCell = const { OnceCell::new() }; +} + +pub fn chrome_linux(window: &Window) -> bool { + USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(window)).chrome_linux) +} + pub fn engine(window: &Window) -> Option { - static ENGINE: OnceLock> = OnceLock::new(); + USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(window)).engine) +} +fn user_agent(window: &Window) -> UserAgentData { #[wasm_bindgen] extern "C" { #[wasm_bindgen(extends = Navigator)] @@ -189,16 +204,19 @@ pub fn engine(window: &Window) -> Option { #[wasm_bindgen(method, getter)] fn brands(this: &NavigatorUaData) -> Array; + #[wasm_bindgen(method, getter)] + fn platform(this: &NavigatorUaData) -> String; + type NavigatorUaBrandVersion; #[wasm_bindgen(method, getter)] fn brand(this: &NavigatorUaBrandVersion) -> String; } - *ENGINE.get_or_init(|| { - let navigator: NavigatorExt = window.navigator().unchecked_into(); + let navigator: NavigatorExt = window.navigator().unchecked_into(); - if let Some(data) = navigator.user_agent_data() { + if let Some(data) = navigator.user_agent_data() { + let engine = 'engine: { for brand in data .brands() .iter() @@ -206,18 +224,28 @@ pub fn engine(window: &Window) -> Option { .map(|brand| brand.brand()) { match brand.as_str() { - "Chromium" => return Some(Engine::Chromium), + "Chromium" => break 'engine Some(Engine::Chromium), // TODO: verify when Firefox actually implements it. - "Gecko" => return Some(Engine::Gecko), + "Gecko" => break 'engine Some(Engine::Gecko), // TODO: verify when Safari actually implements it. - "WebKit" => return Some(Engine::WebKit), + "WebKit" => break 'engine Some(Engine::WebKit), _ => (), } } None - } else { - let data = navigator.user_agent().ok()?; + }; + + let chrome_linux = matches!(engine, Some(Engine::Chromium)) + .then(|| data.platform() == "Linux") + .unwrap_or(false); + + UserAgentData { engine, chrome_linux } + } else { + let engine = 'engine: { + let Ok(data) = navigator.user_agent() else { + break 'engine None; + }; if data.contains("Chrome/") { Some(Engine::Chromium) @@ -228,6 +256,8 @@ pub fn engine(window: &Window) -> Option { } else { None } - } - }) + }; + + UserAgentData { engine, chrome_linux: false } + } } diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index f29386c1fc..3ac87aa3ad 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -8,7 +8,7 @@ use web_sys::HtmlCanvasElement; use super::main_thread::{MainThreadMarker, MainThreadSafe}; use super::monitor::MonitorHandle; use super::r#async::Dispatcher; -use super::{backend, ActiveEventLoop, Fullscreen}; +use super::{backend, lock, ActiveEventLoop, Fullscreen}; use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; use crate::icon::Icon; @@ -78,6 +78,12 @@ impl Window { self.inner.dispatch(move |inner| inner.canvas.prevent_default.set(prevent_default)) } + pub(crate) fn is_cursor_lock_raw(&self) -> bool { + self.inner.queue(move |inner| { + lock::is_cursor_lock_raw(inner.canvas.window(), inner.canvas.document()) + }) + } + #[cfg(feature = "rwh_06")] #[inline] pub fn raw_window_handle_rwh_06(&self) -> Result { @@ -235,15 +241,19 @@ impl Inner { #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { - let lock = match mode { - CursorGrabMode::None => false, - CursorGrabMode::Locked => true, + match mode { + CursorGrabMode::None => self.canvas.document().exit_pointer_lock(), + CursorGrabMode::Locked => lock::request_pointer_lock( + self.canvas.window(), + self.canvas.document(), + self.canvas.raw(), + ), CursorGrabMode::Confined => { return Err(ExternalError::NotSupported(NotSupportedError::new())) }, - }; + } - self.canvas.set_cursor_lock(lock).map_err(ExternalError::Os) + Ok(()) } #[inline]