Web: use raw data in DeviceEvent::MouseMotion (#3803)
Only supported on Chrome MacOS and Windows.
This commit is contained in:
parent
ef580b817d
commit
2e97ab3d4f
9 changed files with 190 additions and 29 deletions
|
|
@ -44,6 +44,9 @@ changelog entry.
|
||||||
|
|
||||||
- Add `ActiveEventLoop::create_proxy()`.
|
- Add `ActiveEventLoop::create_proxy()`.
|
||||||
- On Web, implement `Error` for `platform::web::CustomCursorError`.
|
- 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
|
### Changed
|
||||||
|
|
||||||
|
|
@ -71,6 +74,8 @@ changelog entry.
|
||||||
`EventLoopExtRunOnDemand::run_app_on_demand` to accept a `impl ApplicationHandler` directly,
|
`EventLoopExtRunOnDemand::run_app_on_demand` to accept a `impl ApplicationHandler` directly,
|
||||||
instead of requiring a `&mut` reference to it.
|
instead of requiring a `&mut` reference to it.
|
||||||
- On Web, `Window::canvas()` now returns a reference.
|
- 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
|
### Removed
|
||||||
|
|
||||||
|
|
|
||||||
16
src/event.rs
16
src/event.rs
|
|
@ -467,6 +467,22 @@ pub enum DeviceEvent {
|
||||||
///
|
///
|
||||||
/// This represents raw, unfiltered physical motion. Not to be confused with
|
/// This represents raw, unfiltered physical motion. Not to be confused with
|
||||||
/// [`WindowEvent::CursorMoved`].
|
/// [`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 {
|
MouseMotion {
|
||||||
/// (x, y) change in position in unspecified units.
|
/// (x, y) change in position in unspecified units.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,14 @@ pub trait WindowExtWeb {
|
||||||
/// Some events are impossible to prevent. E.g. Firefox allows to access the native browser
|
/// Some events are impossible to prevent. E.g. Firefox allows to access the native browser
|
||||||
/// context menu with Shift+Rightclick.
|
/// context menu with Shift+Rightclick.
|
||||||
fn set_prevent_default(&self, prevent_default: bool);
|
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 {
|
impl WindowExtWeb for Window {
|
||||||
|
|
@ -99,6 +107,10 @@ impl WindowExtWeb for Window {
|
||||||
fn set_prevent_default(&self, prevent_default: bool) {
|
fn set_prevent_default(&self, prevent_default: bool) {
|
||||||
self.window.set_prevent_default(prevent_default)
|
self.window.set_prevent_default(prevent_default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_cursor_lock_raw(&self) -> bool {
|
||||||
|
self.window.is_cursor_lock_raw()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WindowAttributesExtWeb {
|
pub trait WindowAttributesExtWeb {
|
||||||
|
|
@ -262,6 +274,11 @@ pub trait ActiveEventLoopExtWeb {
|
||||||
/// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the
|
/// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the
|
||||||
/// cursor has completely finished loading.
|
/// cursor has completely finished loading.
|
||||||
fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture;
|
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 {
|
impl ActiveEventLoopExtWeb for ActiveEventLoop {
|
||||||
|
|
@ -289,6 +306,11 @@ impl ActiveEventLoopExtWeb for ActiveEventLoop {
|
||||||
fn wait_until_strategy(&self) -> WaitUntilStrategy {
|
fn wait_until_strategy(&self) -> WaitUntilStrategy {
|
||||||
self.p.wait_until_strategy()
|
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].
|
/// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll].
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use std::rc::Rc;
|
||||||
use web_sys::Element;
|
use web_sys::Element;
|
||||||
|
|
||||||
use super::super::monitor::MonitorHandle;
|
use super::super::monitor::MonitorHandle;
|
||||||
use super::super::KeyEventExtra;
|
use super::super::{lock, KeyEventExtra};
|
||||||
use super::device::DeviceId;
|
use super::device::DeviceId;
|
||||||
use super::runner::{EventWrapper, WeakShared};
|
use super::runner::{EventWrapper, WeakShared};
|
||||||
use super::window::WindowId;
|
use super::window::WindowId;
|
||||||
|
|
@ -652,6 +652,10 @@ impl ActiveEventLoop {
|
||||||
self.runner.wait_until_strategy()
|
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> {
|
pub(crate) fn waker(&self) -> Waker<WeakShared> {
|
||||||
self.runner.waker()
|
self.runner.waker()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
82
src/platform_impl/web/lock.rs
Normal file
82
src/platform_impl/web/lock.rs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ mod device;
|
||||||
mod error;
|
mod error;
|
||||||
mod event_loop;
|
mod event_loop;
|
||||||
mod keyboard;
|
mod keyboard;
|
||||||
|
mod lock;
|
||||||
mod main_thread;
|
mod main_thread;
|
||||||
mod monitor;
|
mod monitor;
|
||||||
mod web_sys;
|
mod web_sys;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
pub fn set_attribute(&self, attribute: &str, value: &str) {
|
||||||
self.common
|
self.common
|
||||||
.raw
|
.raw
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ mod pointer;
|
||||||
mod resize_scaling;
|
mod resize_scaling;
|
||||||
mod schedule;
|
mod schedule;
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
use std::cell::OnceCell;
|
||||||
|
|
||||||
use js_sys::Array;
|
use js_sys::Array;
|
||||||
use wasm_bindgen::closure::Closure;
|
use wasm_bindgen::closure::Closure;
|
||||||
|
|
@ -173,9 +173,24 @@ pub enum Engine {
|
||||||
WebKit,
|
WebKit,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn engine(window: &Window) -> Option<Engine> {
|
struct UserAgentData {
|
||||||
static ENGINE: OnceLock<Option<Engine>> = OnceLock::new();
|
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> {
|
||||||
|
USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(window)).engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_agent(window: &Window) -> UserAgentData {
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#[wasm_bindgen(extends = Navigator)]
|
#[wasm_bindgen(extends = Navigator)]
|
||||||
|
|
@ -189,16 +204,19 @@ pub fn engine(window: &Window) -> Option<Engine> {
|
||||||
#[wasm_bindgen(method, getter)]
|
#[wasm_bindgen(method, getter)]
|
||||||
fn brands(this: &NavigatorUaData) -> Array;
|
fn brands(this: &NavigatorUaData) -> Array;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, getter)]
|
||||||
|
fn platform(this: &NavigatorUaData) -> String;
|
||||||
|
|
||||||
type NavigatorUaBrandVersion;
|
type NavigatorUaBrandVersion;
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
#[wasm_bindgen(method, getter)]
|
||||||
fn brand(this: &NavigatorUaBrandVersion) -> String;
|
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
|
for brand in data
|
||||||
.brands()
|
.brands()
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -206,18 +224,28 @@ pub fn engine(window: &Window) -> Option<Engine> {
|
||||||
.map(|brand| brand.brand())
|
.map(|brand| brand.brand())
|
||||||
{
|
{
|
||||||
match brand.as_str() {
|
match brand.as_str() {
|
||||||
"Chromium" => return Some(Engine::Chromium),
|
"Chromium" => break 'engine Some(Engine::Chromium),
|
||||||
// TODO: verify when Firefox actually implements it.
|
// 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.
|
// TODO: verify when Safari actually implements it.
|
||||||
"WebKit" => return Some(Engine::WebKit),
|
"WebKit" => break 'engine Some(Engine::WebKit),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
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/") {
|
if data.contains("Chrome/") {
|
||||||
Some(Engine::Chromium)
|
Some(Engine::Chromium)
|
||||||
|
|
@ -228,6 +256,8 @@ pub fn engine(window: &Window) -> Option<Engine> {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
})
|
|
||||||
|
UserAgentData { engine, chrome_linux: false }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use web_sys::HtmlCanvasElement;
|
||||||
use super::main_thread::{MainThreadMarker, MainThreadSafe};
|
use super::main_thread::{MainThreadMarker, MainThreadSafe};
|
||||||
use super::monitor::MonitorHandle;
|
use super::monitor::MonitorHandle;
|
||||||
use super::r#async::Dispatcher;
|
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::dpi::{PhysicalPosition, PhysicalSize, Position, Size};
|
||||||
use crate::error::{ExternalError, NotSupportedError, OsError as RootOE};
|
use crate::error::{ExternalError, NotSupportedError, OsError as RootOE};
|
||||||
use crate::icon::Icon;
|
use crate::icon::Icon;
|
||||||
|
|
@ -78,6 +78,12 @@ impl Window {
|
||||||
self.inner.dispatch(move |inner| inner.canvas.prevent_default.set(prevent_default))
|
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")]
|
#[cfg(feature = "rwh_06")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
|
pub fn raw_window_handle_rwh_06(&self) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
|
||||||
|
|
@ -235,15 +241,19 @@ impl Inner {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
|
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
|
||||||
let lock = match mode {
|
match mode {
|
||||||
CursorGrabMode::None => false,
|
CursorGrabMode::None => self.canvas.document().exit_pointer_lock(),
|
||||||
CursorGrabMode::Locked => true,
|
CursorGrabMode::Locked => lock::request_pointer_lock(
|
||||||
|
self.canvas.window(),
|
||||||
|
self.canvas.document(),
|
||||||
|
self.canvas.raw(),
|
||||||
|
),
|
||||||
CursorGrabMode::Confined => {
|
CursorGrabMode::Confined => {
|
||||||
return Err(ExternalError::NotSupported(NotSupportedError::new()))
|
return Err(ExternalError::NotSupported(NotSupportedError::new()))
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
self.canvas.set_cursor_lock(lock).map_err(ExternalError::Os)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue