From f43ce2a131052d3aa5ab9de0c087e790758e97b0 Mon Sep 17 00:00:00 2001 From: Ryo Hirayama Date: Fri, 23 Dec 2022 14:55:22 +0900 Subject: [PATCH] Web touch event (#2188) * feat: add pointer events to web * feat: remove PointerType for touch events * Remove duplicate * Changelog and features * Remove PointerType * feat: renamed events, added touch type guard * Rename * Flip the y axis * Fix physical position and add force * Update comment * Update features * Use normalized force * Remove unnecessary todos * Update comment * Refactor add touch_handler * Rephrase by Liamolucko * Update CHANGELOG.md * Fix duplicate mouse and touch events * Removed workaround for scale factor * Flip the y axis * Fix * Fmt * Replace `match` with a single pattern with `if let` * Update documentation * Have one callback per event * Remove a comment * Fix * Remove y-axis flip * Update src/event.rs Co-authored-by: Mads Marquart * Fix platform specific comment * Fix extra argument to `touch_position` function Co-authored-by: Dany Sluijk Co-authored-by: Johan Klokkhammer Helsing Co-authored-by: oscrim Co-authored-by: Mads Marquart --- CHANGELOG.md | 1 + FEATURES.md | 4 +- src/event.rs | 8 + .../web/event_loop/window_target.rs | 127 ++++++++++----- src/platform_impl/web/web_sys/canvas.rs | 73 ++++++--- .../web/web_sys/canvas/pointer_handler.rs | 144 ++++++++++++++---- src/platform_impl/web/web_sys/event.rs | 10 +- 7 files changed, 272 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 878cad81..9b269d61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- **Breaking:** On Web, touch input no longer fires `WindowEvent::Cursor*`, `WindowEvent::MouseInput`, or `DeviceEvent::MouseMotion` like other platforms, but instead it fires `WindowEvent::Touch`. - **Breaking:** Removed platform specific `WindowBuilder::with_parent` API in favor of `WindowBuilder::with_parent_window`. - On Windows, retain `WS_MAXIMIZE` window style when un-minimizing a maximized window. - On Windows, fix left mouse button release event not being sent after `Window::drag_window`. diff --git a/FEATURES.md b/FEATURES.md index 313d58b7..460f61ff 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -205,8 +205,8 @@ Legend: |Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ | |Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ | |Cursor hittest |✔️ |✔️ |❌ |✔️ |**N/A**|**N/A**|❌ | -|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | -|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |❌ | +|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |️✔️ | +|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |️✔️ | |Multitouch |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | |Keyboard events |✔️ |✔️ |✔️ |✔️ |✔️ |❌ |✔️ | |Drag & Drop |▢[#720] |▢[#720] |▢[#720] |❌[#306] |**N/A**|**N/A**|❓ | diff --git a/src/event.rs b/src/event.rs index da237672..7e169c4b 100644 --- a/src/event.rs +++ b/src/event.rs @@ -477,6 +477,10 @@ pub enum WindowEvent<'a> { }, /// Touch event has been received + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. Touch(Touch), /// The window's scale factor has changed. @@ -924,6 +928,10 @@ pub enum TouchPhase { /// A [`TouchPhase::Cancelled`] event is emitted when the system has canceled tracking this /// touch, such as when the window loses focus, or on iOS if the user moves the /// device against their face. +/// +/// ## Platform-specific +/// +/// - **macOS:** Unsupported. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Touch { pub device_id: DeviceId, diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 77d8e83c..94d17209 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -11,7 +11,7 @@ use super::{ }; use crate::dpi::{PhysicalSize, Size}; use crate::event::{ - DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, KeyboardInput, TouchPhase, + DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, KeyboardInput, Touch, TouchPhase, WindowEvent, }; use crate::window::{Theme, WindowId as RootWindowId}; @@ -154,6 +154,7 @@ impl EventLoopWindowTarget { }); let runner = self.runner.clone(); + let runner_touch = self.runner.clone(); canvas.on_cursor_move( move |pointer_id, position, delta, modifiers| { runner.send_event(Event::WindowEvent { @@ -171,51 +172,93 @@ impl EventLoopWindowTarget { }, }); }, + move |device_id, location, force| { + runner_touch.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Moved, + force: Some(force), + location, + }), + }); + }, prevent_default, ); let runner = self.runner.clone(); - canvas.on_mouse_press(move |pointer_id, position, button, modifiers| { - // A mouse down event may come in without any prior CursorMoved events, - // therefore we should send a CursorMoved event to make sure that the - // user code has the correct cursor position. - runner.send_events( - std::iter::once(Event::WindowEvent { + let runner_touch = self.runner.clone(); + canvas.on_mouse_press( + move |pointer_id, position, button, modifiers| { + // A mouse down event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events( + std::iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }) + .chain(std::iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { + device_id: RootDeviceId(DeviceId(pointer_id)), + position, + modifiers, + }, + })) + .chain(std::iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + device_id: RootDeviceId(DeviceId(pointer_id)), + state: ElementState::Pressed, + button, + modifiers, + }, + })), + ); + }, + move |device_id, location, force| { + runner_touch.send_event(Event::WindowEvent { window_id: RootWindowId(id), - event: WindowEvent::Focused(true), - }) - .chain(std::iter::once(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::CursorMoved { - device_id: RootDeviceId(DeviceId(pointer_id)), - position, - modifiers, - }, - })) - .chain(std::iter::once(Event::WindowEvent { + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Started, + force: Some(force), + location, + }), + }); + }, + ); + + let runner = self.runner.clone(); + let runner_touch = self.runner.clone(); + canvas.on_mouse_release( + move |pointer_id, button, modifiers| { + runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::MouseInput { device_id: RootDeviceId(DeviceId(pointer_id)), - state: ElementState::Pressed, + state: ElementState::Released, button, modifiers, }, - })), - ); - }); - - let runner = self.runner.clone(); - canvas.on_mouse_release(move |pointer_id, button, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::MouseInput { - device_id: RootDeviceId(DeviceId(pointer_id)), - state: ElementState::Released, - button, - modifiers, - }, - }); - }); + }); + }, + move |device_id, location, force| { + runner_touch.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Ended, + force: Some(force), + location, + }), + }); + }, + ); let runner = self.runner.clone(); canvas.on_mouse_wheel( @@ -263,6 +306,20 @@ impl EventLoopWindowTarget { runner.request_redraw(RootWindowId(id)); }); + let runner = self.runner.clone(); + canvas.on_touch_cancel(move |device_id, location, force| { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Cancelled, + force: Some(force), + location, + }), + }); + }); + let runner = self.runner.clone(); canvas.on_dark_mode(move |is_dark_mode| { let theme = if is_dark_mode { diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 2e875874..0c0693c6 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -3,7 +3,9 @@ use super::event_handle::EventListenerHandle; use super::media_query_handle::MediaQueryListHandle; use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; use crate::error::OsError as RootOE; -use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; +use crate::event::{ + Force, ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode, +}; use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes}; use std::cell::RefCell; @@ -262,35 +264,55 @@ impl Canvas { } } - pub fn on_mouse_release(&mut self, handler: F) + pub fn on_mouse_release(&mut self, mouse_handler: M, touch_handler: T) where - F: 'static + FnMut(i32, MouseButton, ModifiersState), - { - match &mut self.mouse_state { - MouseState::HasPointerEvent(h) => h.on_mouse_release(&self.common, handler), - MouseState::NoPointerEvent(h) => h.on_mouse_release(&self.common, handler), - } - } - - pub fn on_mouse_press(&mut self, handler: F) - where - F: 'static + FnMut(i32, PhysicalPosition, MouseButton, ModifiersState), - { - match &mut self.mouse_state { - MouseState::HasPointerEvent(h) => h.on_mouse_press(&self.common, handler), - MouseState::NoPointerEvent(h) => h.on_mouse_press(&self.common, handler), - } - } - - pub fn on_cursor_move(&mut self, handler: F, prevent_default: bool) - where - F: 'static + FnMut(i32, PhysicalPosition, PhysicalPosition, ModifiersState), + M: 'static + FnMut(i32, MouseButton, ModifiersState), + T: 'static + FnMut(i32, PhysicalPosition, Force), { match &mut self.mouse_state { MouseState::HasPointerEvent(h) => { - h.on_cursor_move(&self.common, handler, prevent_default) + h.on_mouse_release(&self.common, mouse_handler, touch_handler) } - MouseState::NoPointerEvent(h) => h.on_cursor_move(&self.common, handler), + MouseState::NoPointerEvent(h) => h.on_mouse_release(&self.common, mouse_handler), + } + } + + pub fn on_mouse_press(&mut self, mouse_handler: M, touch_handler: T) + where + M: 'static + FnMut(i32, PhysicalPosition, MouseButton, ModifiersState), + T: 'static + FnMut(i32, PhysicalPosition, Force), + { + match &mut self.mouse_state { + MouseState::HasPointerEvent(h) => { + h.on_mouse_press(&self.common, mouse_handler, touch_handler) + } + MouseState::NoPointerEvent(h) => h.on_mouse_press(&self.common, mouse_handler), + } + } + + pub fn on_cursor_move( + &mut self, + mouse_handler: M, + touch_handler: T, + prevent_default: bool, + ) where + M: 'static + FnMut(i32, PhysicalPosition, PhysicalPosition, ModifiersState), + T: 'static + FnMut(i32, PhysicalPosition, Force), + { + match &mut self.mouse_state { + MouseState::HasPointerEvent(h) => { + h.on_cursor_move(&self.common, mouse_handler, touch_handler, prevent_default) + } + MouseState::NoPointerEvent(h) => h.on_cursor_move(&self.common, mouse_handler), + } + } + + pub fn on_touch_cancel(&mut self, handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, Force), + { + if let MouseState::HasPointerEvent(h) = &mut self.mouse_state { + h.on_touch_cancel(&self.common, handler) } } @@ -451,6 +473,7 @@ impl Common { } } +/// Pointer events are supported or not. enum MouseState { HasPointerEvent(pointer_handler::PointerHandler), NoPointerEvent(mouse_handler::MouseHandler), diff --git a/src/platform_impl/web/web_sys/canvas/pointer_handler.rs b/src/platform_impl/web/web_sys/canvas/pointer_handler.rs index e907f54e..43412c3b 100644 --- a/src/platform_impl/web/web_sys/canvas/pointer_handler.rs +++ b/src/platform_impl/web/web_sys/canvas/pointer_handler.rs @@ -1,6 +1,7 @@ use super::event; use super::EventListenerHandle; use crate::dpi::PhysicalPosition; +use crate::event::Force; use crate::event::{ModifiersState, MouseButton}; use web_sys::PointerEvent; @@ -12,6 +13,7 @@ pub(super) struct PointerHandler { on_cursor_move: Option>, on_pointer_press: Option>, on_pointer_release: Option>, + on_touch_cancel: Option>, } impl PointerHandler { @@ -22,6 +24,7 @@ impl PointerHandler { on_cursor_move: None, on_pointer_press: None, on_pointer_release: None, + on_touch_cancel: None, } } @@ -32,6 +35,13 @@ impl PointerHandler { self.on_cursor_leave = Some(canvas_common.add_event( "pointerout", move |event: PointerEvent| { + // touch events are handled separately + // handling them here would produce duplicate mouse events, inconsistent with + // other platforms. + if event.pointer_type() == "touch" { + return; + } + handler(event.pointer_id()); }, )); @@ -44,70 +54,139 @@ impl PointerHandler { self.on_cursor_enter = Some(canvas_common.add_event( "pointerover", move |event: PointerEvent| { + // touch events are handled separately + // handling them here would produce duplicate mouse events, inconsistent with + // other platforms. + if event.pointer_type() == "touch" { + return; + } + handler(event.pointer_id()); }, )); } - pub fn on_mouse_release(&mut self, canvas_common: &super::Common, mut handler: F) - where - F: 'static + FnMut(i32, MouseButton, ModifiersState), + pub fn on_mouse_release( + &mut self, + canvas_common: &super::Common, + mut mouse_handler: M, + mut touch_handler: T, + ) where + M: 'static + FnMut(i32, MouseButton, ModifiersState), + T: 'static + FnMut(i32, PhysicalPosition, Force), { + let canvas = canvas_common.raw.clone(); self.on_pointer_release = Some(canvas_common.add_user_event( "pointerup", move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); + if event.pointer_type() == "touch" { + touch_handler( + event.pointer_id(), + event::touch_position(&event, &canvas) + .to_physical(super::super::scale_factor()), + Force::Normalized(event.pressure() as f64), + ); + } else { + mouse_handler( + event.pointer_id(), + event::mouse_button(&event), + event::mouse_modifiers(&event), + ); + } }, )); } - pub fn on_mouse_press(&mut self, canvas_common: &super::Common, mut handler: F) - where - F: 'static + FnMut(i32, PhysicalPosition, MouseButton, ModifiersState), + pub fn on_mouse_press( + &mut self, + canvas_common: &super::Common, + mut mouse_handler: M, + mut touch_handler: T, + ) where + M: 'static + FnMut(i32, PhysicalPosition, MouseButton, ModifiersState), + T: 'static + FnMut(i32, PhysicalPosition, Force), { let canvas = canvas_common.raw.clone(); self.on_pointer_press = Some(canvas_common.add_user_event( "pointerdown", move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_position(&event).to_physical(super::super::scale_factor()), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); + if event.pointer_type() == "touch" { + touch_handler( + event.pointer_id(), + event::touch_position(&event, &canvas) + .to_physical(super::super::scale_factor()), + Force::Normalized(event.pressure() as f64), + ); + } else { + mouse_handler( + event.pointer_id(), + event::mouse_position(&event).to_physical(super::super::scale_factor()), + event::mouse_button(&event), + event::mouse_modifiers(&event), + ); - // Error is swallowed here since the error would occur every time the mouse is - // clicked when the cursor is grabbed, and there is probably not a situation where - // this could fail, that we care if it fails. - let _e = canvas.set_pointer_capture(event.pointer_id()); + // Error is swallowed here since the error would occur every time the mouse is + // clicked when the cursor is grabbed, and there is probably not a situation where + // this could fail, that we care if it fails. + let _e = canvas.set_pointer_capture(event.pointer_id()); + } }, )); } - pub fn on_cursor_move( + pub fn on_cursor_move( &mut self, canvas_common: &super::Common, - mut handler: F, + mut mouse_handler: M, + mut touch_handler: T, prevent_default: bool, ) where - F: 'static + FnMut(i32, PhysicalPosition, PhysicalPosition, ModifiersState), + M: 'static + FnMut(i32, PhysicalPosition, PhysicalPosition, ModifiersState), + T: 'static + FnMut(i32, PhysicalPosition, Force), { + let canvas = canvas_common.raw.clone(); self.on_cursor_move = Some(canvas_common.add_event( "pointermove", move |event: PointerEvent| { - if prevent_default { - event.prevent_default(); + if event.pointer_type() == "touch" { + if prevent_default { + // prevent scroll on mobile web + event.prevent_default(); + } + touch_handler( + event.pointer_id(), + event::touch_position(&event, &canvas) + .to_physical(super::super::scale_factor()), + Force::Normalized(event.pressure() as f64), + ); + } else { + mouse_handler( + event.pointer_id(), + event::mouse_position(&event).to_physical(super::super::scale_factor()), + event::mouse_delta(&event).to_physical(super::super::scale_factor()), + event::mouse_modifiers(&event), + ); + } + }, + )); + } + + pub fn on_touch_cancel(&mut self, canvas_common: &super::Common, mut handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, Force), + { + let canvas = canvas_common.raw.clone(); + self.on_touch_cancel = Some(canvas_common.add_event( + "pointercancel", + move |event: PointerEvent| { + if event.pointer_type() == "touch" { + handler( + event.pointer_id(), + event::touch_position(&event, &canvas) + .to_physical(super::super::scale_factor()), + Force::Normalized(event.pressure() as f64), + ); } - handler( - event.pointer_id(), - event::mouse_position(&event).to_physical(super::super::scale_factor()), - event::mouse_delta(&event).to_physical(super::super::scale_factor()), - event::mouse_modifiers(&event), - ); }, )); } @@ -118,5 +197,6 @@ impl PointerHandler { self.on_cursor_move = None; self.on_pointer_press = None; self.on_pointer_release = None; + self.on_touch_cancel = None; } } diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/event.rs index 7dfbd8cf..a75c0e1b 100644 --- a/src/platform_impl/web/web_sys/event.rs +++ b/src/platform_impl/web/web_sys/event.rs @@ -2,7 +2,7 @@ use crate::dpi::LogicalPosition; use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; use std::convert::TryInto; -use web_sys::{HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent}; +use web_sys::{HtmlCanvasElement, KeyboardEvent, MouseEvent, PointerEvent, WheelEvent}; pub fn mouse_button(event: &MouseEvent) -> MouseButton { match event.button() { @@ -246,3 +246,11 @@ pub fn codepoint(event: &KeyboardEvent) -> char { // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key event.key().chars().next().unwrap() } + +pub fn touch_position(event: &PointerEvent, _canvas: &HtmlCanvasElement) -> LogicalPosition { + // TODO: Should this handle more, like `mouse_position_by_client` does? + LogicalPosition { + x: event.client_x() as f64, + y: event.client_y() as f64, + } +}