diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 5d4dc5bd31..321904d2e4 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::Confined`. ### Changed @@ -69,6 +72,8 @@ changelog entry. - Change signature of `EventLoop::run_app`, `EventLoopExtPumpEvents::pump_app_events` and `EventLoopExtRunOnDemand::run_app_on_demand` to accept a `impl ApplicationHandler` directly, instead of requiring a `&mut` reference to it. +- On Web, `CursorGrabMode::Confined` 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 e0a6d4376d..4c28f510bb 100644 --- a/src/event.rs +++ b/src/event.rs @@ -467,6 +467,21 @@ pub enum DeviceEvent { /// /// This represents raw, unfiltered physical motion. Not to be confused with /// [`WindowEvent::CursorMoved`]. + /// + /// ## Platform-specific + /// + /// **Web:** Always returns OS accelerated data unless [`CursorGrabMode::Confined`] is used depending on browser support, 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::Confined`]: crate::window::CursorGrabMode::Confined MouseMotion { /// (x, y) change in position in unspecified units. /// diff --git a/src/platform/web.rs b/src/platform/web.rs index c3e570c3ed..9cb5112a82 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -83,6 +83,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 { @@ -98,6 +106,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 { @@ -261,6 +273,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 { @@ -288,6 +305,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 f697142a08..4f92cc467a 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, Weak}; 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, Execution}; use super::window::WindowId; @@ -698,6 +698,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 f515bbf5ce..b0418725a3 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -171,15 +171,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/web_sys/schedule.rs b/src/platform_impl/web/web_sys/schedule.rs index 48ba50e4cf..259be3dd93 100644 --- a/src/platform_impl/web/web_sys/schedule.rs +++ b/src/platform_impl/web/web_sys/schedule.rs @@ -1,7 +1,7 @@ use std::cell::OnceCell; use std::time::Duration; -use js_sys::{Array, Function, Object, Promise, Reflect}; +use js_sys::{Array, Function, Object, Promise}; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; @@ -78,9 +78,9 @@ impl Schedule { let scheduler = window.scheduler(); let closure = Closure::new(f); - let mut options = SchedulerPostTaskOptions::new(); + let options: SchedulerPostTaskOptions = Object::new().unchecked_into(); let controller = AbortController::new().expect("Failed to create `AbortController`"); - options.signal(&controller.signal()); + options.set_signal(&controller.signal()); if let Some(duration) = duration { // `Duration::as_millis()` always rounds down (because of truncation), we want to round @@ -91,7 +91,7 @@ impl Schedule { .and_then(|secs| secs.checked_add(duration.subsec_micros().div_ceil(1000).into())) .unwrap_or(u64::MAX); - options.delay(duration as f64); + options.set_delay(duration as f64); } thread_local! { @@ -310,22 +310,10 @@ extern "C" { ) -> Promise; type SchedulerPostTaskOptions; -} -impl SchedulerPostTaskOptions { - fn new() -> Self { - Object::new().unchecked_into() - } + #[wasm_bindgen(method, setter, js_name = delay)] + fn set_delay(this: &SchedulerPostTaskOptions, value: f64); - fn delay(&mut self, val: f64) -> &mut Self { - let r = Reflect::set(self, &JsValue::from("delay"), &val.into()); - debug_assert!(r.is_ok(), "Failed to set `delay` property"); - self - } - - fn signal(&mut self, val: &AbortSignal) -> &mut Self { - let r = Reflect::set(self, &JsValue::from("signal"), &val.into()); - debug_assert!(r.is_ok(), "Failed to set `signal` property"); - self - } + #[wasm_bindgen(method, setter, js_name = signal)] + fn set_signal(this: &SchedulerPostTaskOptions, value: &AbortSignal); } diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 29d854047f..3155e37cc3 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; @@ -86,6 +86,15 @@ impl Window { self.inner.dispatch(move |inner| inner.canvas.borrow().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.borrow().window(), + inner.canvas.borrow().document(), + ) + }) + } + #[cfg(feature = "rwh_06")] #[inline] pub fn raw_window_handle_rwh_06(&self) -> Result { @@ -228,15 +237,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.borrow().document().exit_pointer_lock(), + CursorGrabMode::Locked => lock::request_pointer_lock( + self.canvas.borrow().window(), + self.canvas.borrow().document(), + self.canvas.borrow().raw(), + ), CursorGrabMode::Confined => { return Err(ExternalError::NotSupported(NotSupportedError::new())) }, - }; + } - self.canvas.borrow().set_cursor_lock(lock).map_err(ExternalError::Os) + Ok(()) } #[inline] diff --git a/src/window.rs b/src/window.rs index d95e868065..cc1a23749b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1478,6 +1478,23 @@ impl Window { /// .unwrap(); /// # } /// ``` + /// + /// ## Platform-specific + /// + /// **Web:** Always returns an [`ExternalError::NotSupported`] with + /// [`CursorGrabMode::Confined`]. [`DeviceEvent::MouseMotion`] might return OS accelerated data + /// depending on browser support, 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] + /// [`DeviceEvent::MouseMotion`]: crate::event::DeviceEvent::MouseMotion #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { let _span = tracing::debug_span!(