Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web: use raw data in DeviceEvent::MouseMotion when using CursorGrabMode::Confined #3803

Merged
merged 1 commit into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
22 changes: 22 additions & 0 deletions src/platform/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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].
Expand Down
6 changes: 5 additions & 1 deletion src/platform_impl/web/event_loop/window_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<WeakShared> {
self.runner.waker()
}
Expand Down
82 changes: 82 additions & 0 deletions src/platform_impl/web/lock.rs
Original file line number Diff line number Diff line change
@@ -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<bool> = 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 <https://issues.chromium.org/issues/40833850>.
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<dyn FnMut(JsValue)> = 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<dyn FnMut(JsValue)> = Closure::new(|error: JsValue| {
if let Some(error) = error.dyn_ref::<DomException>() {
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);
}
1 change: 1 addition & 0 deletions src/platform_impl/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod device;
mod error;
mod event_loop;
mod keyboard;
mod lock;
mod main_thread;
mod monitor;
mod web_sys;
Expand Down
9 changes: 0 additions & 9 deletions src/platform_impl/web/web_sys/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 42 additions & 12 deletions src/platform_impl/web/web_sys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -173,9 +173,24 @@ pub enum Engine {
WebKit,
}

struct UserAgentData {
engine: Option<Engine>,
chrome_linux: bool,
}

thread_local! {
static USER_AGENT_DATA: OnceCell<UserAgentData> = 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<Engine> {
static ENGINE: OnceLock<Option<Engine>> = 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)]
Expand All @@ -189,35 +204,48 @@ pub fn engine(window: &Window) -> Option<Engine> {
#[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()
.map(NavigatorUaBrandVersion::unchecked_from_js)
.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)
Expand All @@ -228,6 +256,8 @@ pub fn engine(window: &Window) -> Option<Engine> {
} else {
None
}
}
})
};

UserAgentData { engine, chrome_linux: false }
}
}
22 changes: 16 additions & 6 deletions src/platform_impl/web/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<rwh_06::RawWindowHandle, rwh_06::HandleError> {
Expand Down Expand Up @@ -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]
Expand Down
Loading