diff --git a/CHANGELOG.md b/CHANGELOG.md index 68fda1a8..eb1d4fe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,10 @@ And please only add new entries to the top of this list, right below the `# Unre - On Web, use `Window.requestIdleCallback()` for `ControlFlow::Poll` when available. - **Breaking:** On Web, the canvas size is not controlled by Winit anymore and external changes to the canvas size will be reported through `WindowEvent::Resized`. +- On Web, respect `EventLoopWindowTarget::listen_device_events()` settings. +- On Web, fix `DeviceEvent::MouseMotion` only being emitted for each canvas instead of the whole window. +- On Web, add `DeviceEvent::Motion`, `DeviceEvent::MouseWheel`, `DeviceEvent::Button` and + `DeviceEvent::Key` support. # 0.28.6 diff --git a/src/event_loop.rs b/src/event_loop.rs index 7c81bbe1..cf8f6a12 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -356,11 +356,11 @@ impl EventLoopWindowTarget { /// /// ## Platform-specific /// - /// - **Wayland / macOS / iOS / Android / Web / Orbital:** Unsupported. + /// - **Wayland / macOS / iOS / Android / Orbital:** Unsupported. /// /// [`DeviceEvent`]: crate::event::DeviceEvent pub fn listen_device_events(&self, _allowed: DeviceEvents) { - #[cfg(any(x11_platform, wayland_platform, windows))] + #[cfg(any(x11_platform, wasm_platform, wayland_platform, windows))] self.p.listen_device_events(_allowed); } } diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index f255dc2f..df4c7fab 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -1,17 +1,24 @@ +use super::super::DeviceId; use super::{backend, state::State}; use crate::dpi::PhysicalSize; -use crate::event::{Event, StartCause}; -use crate::event_loop::ControlFlow; +use crate::event::{ + DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, RawKeyEvent, StartCause, +}; +use crate::event_loop::{ControlFlow, DeviceEvents}; +use crate::platform_impl::platform::backend::EventListenerHandle; use crate::window::WindowId; +use std::sync::atomic::Ordering; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, clone::Clone, collections::{HashSet, VecDeque}, iter, ops::Deref, rc::{Rc, Weak}, }; +use wasm_bindgen::prelude::Closure; +use web_sys::{KeyboardEvent, PointerEvent, WheelEvent}; use web_time::{Duration, Instant}; pub struct Shared(Rc>); @@ -24,6 +31,8 @@ impl Clone for Shared { } } +type OnEventHandle = RefCell>>; + pub struct Execution { runner: RefCell>, events: RefCell>>, @@ -33,6 +42,13 @@ pub struct Execution { redraw_pending: RefCell>, destroy_pending: RefCell>, unload_event_handle: RefCell>, + device_events: Cell, + on_mouse_move: OnEventHandle, + on_wheel: OnEventHandle, + on_mouse_press: OnEventHandle, + on_mouse_release: OnEventHandle, + on_key_press: OnEventHandle, + on_key_release: OnEventHandle, } enum RunnerEnum { @@ -131,6 +147,13 @@ impl Shared { redraw_pending: RefCell::new(HashSet::new()), destroy_pending: RefCell::new(VecDeque::new()), unload_event_handle: RefCell::new(None), + device_events: Cell::default(), + on_mouse_move: RefCell::new(None), + on_wheel: RefCell::new(None), + on_mouse_press: RefCell::new(None), + on_mouse_release: RefCell::new(None), + on_key_press: RefCell::new(None), + on_key_release: RefCell::new(None), })) } @@ -165,6 +188,184 @@ impl Shared { Some(backend::on_unload(self.window(), move || { close_instance.handle_unload() })); + + let runner = self.clone(); + let window = self.window().clone(); + *self.0.on_mouse_move.borrow_mut() = Some(EventListenerHandle::new( + self.window(), + "pointermove", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + let pointer_type = event.pointer_type(); + + if pointer_type != "mouse" { + return; + } + + // chorded button event + let device_id = RootDeviceId(DeviceId(event.pointer_id())); + + if let Some(button) = backend::event::mouse_button(&event) { + debug_assert_eq!( + pointer_type, "mouse", + "expect pointer type of a chorded button event to be a mouse" + ); + + let state = if backend::event::mouse_buttons(&event).contains(button.into()) { + ElementState::Pressed + } else { + ElementState::Released + }; + + runner.send_event(Event::DeviceEvent { + device_id, + event: DeviceEvent::Button { + button: button.to_id(), + state, + }, + }); + + return; + } + + // pointer move event + let mut delta = backend::event::MouseDelta::init(&window, &event); + runner.send_events(backend::event::pointer_move_event(event).flat_map(|event| { + let delta = delta + .delta(&event) + .to_physical(backend::scale_factor(&window)); + + let x_motion = (delta.x != 0.0).then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Motion { + axis: 0, + value: delta.x, + }, + }); + + let y_motion = (delta.y != 0.0).then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Motion { + axis: 1, + value: delta.y, + }, + }); + + x_motion + .into_iter() + .chain(y_motion) + .chain(iter::once(Event::DeviceEvent { + device_id, + event: DeviceEvent::MouseMotion { + delta: (delta.x, delta.y), + }, + })) + })); + }), + )); + let runner = self.clone(); + let window = self.window().clone(); + *self.0.on_wheel.borrow_mut() = Some(EventListenerHandle::new( + self.window(), + "wheel", + Closure::new(move |event: WheelEvent| { + if !runner.device_events() { + return; + } + + if let Some(delta) = backend::event::mouse_scroll_delta(&window, &event) { + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(0)), + event: DeviceEvent::MouseWheel { delta }, + }); + } + }), + )); + let runner = self.clone(); + *self.0.on_mouse_press.borrow_mut() = Some(EventListenerHandle::new( + self.window(), + "pointerdown", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + if event.pointer_type() != "mouse" { + return; + } + + let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(event.pointer_id())), + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Pressed, + }, + }); + }), + )); + let runner = self.clone(); + *self.0.on_mouse_release.borrow_mut() = Some(EventListenerHandle::new( + self.window(), + "pointerup", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + if event.pointer_type() != "mouse" { + return; + } + + let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(event.pointer_id())), + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Released, + }, + }); + }), + )); + let runner = self.clone(); + *self.0.on_key_press.borrow_mut() = Some(EventListenerHandle::new( + self.window(), + "keydown", + Closure::new(move |event: KeyboardEvent| { + if !runner.device_events() { + return; + } + + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(unsafe { DeviceId::dummy() }), + event: DeviceEvent::Key(RawKeyEvent { + physical_key: backend::event::key_code(&event), + state: ElementState::Pressed, + }), + }); + }), + )); + let runner = self.clone(); + *self.0.on_key_release.borrow_mut() = Some(EventListenerHandle::new( + self.window(), + "keyup", + Closure::new(move |event: KeyboardEvent| { + if !runner.device_events() { + return; + } + + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(unsafe { DeviceId::dummy() }), + event: DeviceEvent::Key(RawKeyEvent { + physical_key: backend::event::key_code(&event), + state: ElementState::Released, + }), + }); + }), + )); } // Generate a strictly increasing ID @@ -405,6 +606,12 @@ impl Shared { self.handle_event(Event::LoopDestroyed, control); let all_canvases = std::mem::take(&mut *self.0.all_canvases.borrow_mut()); *self.0.unload_event_handle.borrow_mut() = None; + *self.0.on_mouse_move.borrow_mut() = None; + *self.0.on_wheel.borrow_mut() = None; + *self.0.on_mouse_press.borrow_mut() = None; + *self.0.on_mouse_release.borrow_mut() = None; + *self.0.on_key_press.borrow_mut() = None; + *self.0.on_key_release.borrow_mut() = None; // Dropping the `Runner` drops the event handler closure, which will in // turn drop all `Window`s moved into the closure. *self.0.runner.borrow_mut() = RunnerEnum::Destroyed; @@ -450,6 +657,24 @@ impl Shared { RunnerEnum::Destroyed => ControlFlow::Exit, } } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.0.device_events.set(allowed) + } + + pub fn device_events(&self) -> bool { + match self.0.device_events.get() { + DeviceEvents::Always => true, + DeviceEvents::WhenFocused => self.0.all_canvases.borrow().iter().any(|(_, canvas)| { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().has_focus.load(Ordering::Relaxed) + } else { + false + } + }), + DeviceEvents::Never => false, + } + } } pub(crate) enum EventWrapper { diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 5f477d5c..8cfbb427 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -3,8 +3,7 @@ use std::clone::Clone; use std::collections::{vec_deque::IntoIter as VecDequeIter, VecDeque}; use std::iter; use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::atomic::Ordering; use raw_window_handle::{RawDisplayHandle, WebDisplayHandle}; @@ -18,9 +17,10 @@ use super::{ window::WindowId, }; use crate::event::{ - DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, KeyEvent, Touch, TouchPhase, - WindowEvent, + DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, KeyEvent, RawKeyEvent, Touch, + TouchPhase, WindowEvent, }; +use crate::event_loop::DeviceEvents; use crate::keyboard::ModifiersState; use crate::window::{Theme, WindowId as RootWindowId}; @@ -82,7 +82,6 @@ impl EventLoopWindowTarget { canvas: &Rc>, id: WindowId, prevent_default: bool, - has_focus: Arc, ) { self.runner.add_canvas(RootWindowId(id), canvas); let canvas_clone = canvas.clone(); @@ -92,10 +91,10 @@ impl EventLoopWindowTarget { canvas.on_touch_start(prevent_default); let runner = self.runner.clone(); - let has_focus_clone = has_focus.clone(); + let has_focus = canvas.has_focus.clone(); let modifiers = self.modifiers.clone(); canvas.on_blur(move || { - has_focus_clone.store(false, Ordering::Relaxed); + has_focus.store(false, Ordering::Relaxed); let clear_modifiers = (!modifiers.get().is_empty()).then(|| { modifiers.set(ModifiersState::empty()); @@ -116,9 +115,9 @@ impl EventLoopWindowTarget { }); let runner = self.runner.clone(); - let has_focus_clone = has_focus.clone(); + let has_focus = canvas.has_focus.clone(); canvas.on_focus(move || { - if !has_focus_clone.swap(true, Ordering::Relaxed) { + if !has_focus.swap(true, Ordering::Relaxed) { runner.send_event(Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::Focused(true), @@ -138,24 +137,36 @@ impl EventLoopWindowTarget { } }); + let device_id = RootDeviceId(unsafe { DeviceId::dummy() }); + + let device_event = runner.device_events().then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Key(RawKeyEvent { + physical_key, + state: ElementState::Pressed, + }), + }); + runner.send_events( - iter::once(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::KeyboardInput { - device_id: RootDeviceId(unsafe { DeviceId::dummy() }), - event: KeyEvent { - physical_key, - logical_key, - text, - location, - state: ElementState::Pressed, - repeat, - platform_specific: KeyEventExtra, + device_event + .into_iter() + .chain(iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Pressed, + repeat, + platform_specific: KeyEventExtra, + }, + is_synthetic: false, }, - is_synthetic: false, - }, - }) - .chain(modifiers_changed), + })) + .chain(modifiers_changed), ); }, prevent_default, @@ -173,29 +184,42 @@ impl EventLoopWindowTarget { } }); + let device_id = RootDeviceId(unsafe { DeviceId::dummy() }); + + let device_event = runner.device_events().then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Key(RawKeyEvent { + physical_key, + state: ElementState::Pressed, + }), + }); + runner.send_events( - iter::once(Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::KeyboardInput { - device_id: RootDeviceId(unsafe { DeviceId::dummy() }), - event: KeyEvent { - physical_key, - logical_key, - text, - location, - state: ElementState::Released, - repeat, - platform_specific: KeyEventExtra, + device_event + .into_iter() + .chain(iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Released, + repeat, + platform_specific: KeyEventExtra, + }, + is_synthetic: false, }, - is_synthetic: false, - }, - }) - .chain(modifiers_changed), + })) + .chain(modifiers_changed), ) }, prevent_default, ); + let has_focus = canvas.has_focus.clone(); canvas.on_cursor_leave({ let runner = self.runner.clone(); let has_focus = has_focus.clone(); @@ -290,13 +314,34 @@ impl EventLoopWindowTarget { |(position, delta)| { let device_id = RootDeviceId(DeviceId(pointer_id)); - [ - Event::DeviceEvent { + let device_events = runner.device_events().then(|| { + let x_motion = (delta.x != 0.0).then_some(Event::DeviceEvent { device_id, - event: DeviceEvent::MouseMotion { - delta: (delta.x, delta.y), + event: DeviceEvent::Motion { + axis: 0, + value: delta.x, }, - }, + }); + + let y_motion = (delta.y != 0.0).then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Motion { + axis: 1, + value: delta.y, + }, + }); + + x_motion.into_iter().chain(y_motion).chain(iter::once( + Event::DeviceEvent { + device_id, + event: DeviceEvent::MouseMotion { + delta: (delta.x, delta.y), + }, + }, + )) + }); + + device_events.into_iter().flatten().chain(iter::once( Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::CursorMoved { @@ -304,7 +349,7 @@ impl EventLoopWindowTarget { position, }, }, - ] + )) }, ))); } @@ -359,38 +404,41 @@ impl EventLoopWindowTarget { } }); - let button_event = if buttons.contains(button.into()) { - Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::MouseInput { - device_id: RootDeviceId(DeviceId(pointer_id)), - state: ElementState::Pressed, - button, - }, - } + let device_id = RootDeviceId(DeviceId(pointer_id)); + + let state = if buttons.contains(button.into()) { + ElementState::Pressed } else { - Event::WindowEvent { - window_id: RootWindowId(id), - event: WindowEvent::MouseInput { - device_id: RootDeviceId(DeviceId(pointer_id)), - state: ElementState::Released, - button, - }, - } + ElementState::Released }; + let device_event = runner.device_events().then(|| Event::DeviceEvent { + device_id, + event: DeviceEvent::Button { + button: button.to_id(), + state, + }, + }); + // A chorded button 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(modifiers.into_iter().chain([ + runner.send_events(modifiers.into_iter().chain(device_event).chain([ Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::CursorMoved { - device_id: RootDeviceId(DeviceId(pointer_id)), + device_id, position, }, }, - button_event, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + device_id, + state, + button, + }, + }, ])); } }, @@ -425,21 +473,30 @@ impl EventLoopWindowTarget { } }); + let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); + let device_event = runner.device_events().then(|| Event::DeviceEvent { + device_id, + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Pressed, + }, + }); + // 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(modifiers.into_iter().chain([ + runner.send_events(modifiers.into_iter().chain(device_event).chain([ Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::CursorMoved { - device_id: RootDeviceId(DeviceId(pointer_id)), + device_id, position, }, }, Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::MouseInput { - device_id: RootDeviceId(DeviceId(pointer_id)), + device_id, state: ElementState::Pressed, button, }, @@ -509,21 +566,30 @@ impl EventLoopWindowTarget { } }); + let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); + let device_event = runner.device_events().then(|| Event::DeviceEvent { + device_id, + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Pressed, + }, + }); + // A mouse up 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(modifiers.into_iter().chain([ + runner.send_events(modifiers.into_iter().chain(device_event).chain([ Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::CursorMoved { - device_id: RootDeviceId(DeviceId(pointer_id)), + device_id, position, }, }, Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::MouseInput { - device_id: RootDeviceId(DeviceId(pointer_id)), + device_id, state: ElementState::Released, button, }, @@ -577,16 +643,21 @@ impl EventLoopWindowTarget { } }); - runner.send_events(modifiers_changed.into_iter().chain(iter::once( - Event::WindowEvent { + let device_event = runner.device_events().then_some(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(pointer_id)), + event: DeviceEvent::MouseWheel { delta }, + }); + + runner.send_events(modifiers_changed.into_iter().chain(device_event).chain( + iter::once(Event::WindowEvent { window_id: RootWindowId(id), event: WindowEvent::MouseWheel { device_id: RootDeviceId(DeviceId(pointer_id)), delta, phase: TouchPhase::Moved, }, - }, - ))); + }), + )); }, prevent_default, ); @@ -661,4 +732,8 @@ impl EventLoopWindowTarget { pub fn raw_display_handle(&self) -> RawDisplayHandle { RawDisplayHandle::Web(WebDisplayHandle::empty()) } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.runner.listen_device_events(allowed) + } } diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 9138d405..67a383e8 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -12,6 +12,8 @@ use crate::window::{WindowAttributes, WindowId as RootWindowId}; use std::cell::{Cell, RefCell}; use std::rc::Rc; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; use js_sys::Promise; use smol_str::SmolStr; @@ -24,6 +26,7 @@ use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, WheelEvent}; pub struct Canvas { common: Common, id: WindowId, + pub has_focus: Arc, on_touch_start: Option>, on_touch_end: Option>, on_focus: Option>, @@ -91,6 +94,7 @@ impl Canvas { wants_fullscreen: Rc::new(RefCell::new(false)), }, id, + has_focus: Arc::new(AtomicBool::new(false)), on_touch_start: None, on_touch_end: None, on_blur: None, diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/event.rs index 4d42a68a..92020463 100644 --- a/src/platform_impl/web/web_sys/event.rs +++ b/src/platform_impl/web/web_sys/event.rs @@ -57,6 +57,17 @@ pub fn mouse_button(event: &MouseEvent) -> Option { } } +impl MouseButton { + pub fn to_id(self) -> u32 { + match self { + MouseButton::Left => 0, + MouseButton::Right => 1, + MouseButton::Middle => 2, + MouseButton::Other(value) => value.into(), + } + } +} + pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { LogicalPosition { x: event.offset_x() as f64, diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 8f6cf630..367680d2 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -1,5 +1,5 @@ mod canvas; -mod event; +pub mod event; mod event_handle; mod media_query_handle; mod pointer; @@ -8,6 +8,7 @@ mod timeout; pub use self::canvas::Canvas; pub use self::event::ButtonsState; +pub use self::event_handle::EventListenerHandle; pub use self::resize_scaling::ResizeScaleHandle; pub use self::timeout::{IdleCallback, Timeout}; diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 2557054a..6a379921 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -51,12 +51,12 @@ impl Window { let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); - let has_focus = Arc::new(AtomicBool::new(false)); - target.register(&canvas, id, prevent_default, has_focus.clone()); + target.register(&canvas, id, prevent_default); let runner = target.runner.clone(); let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); + let has_focus = canvas.borrow().has_focus.clone(); let window = Window { id, has_focus,