From 9e6f666616341ddd76fc61c6a0ed8dcbf3392a28 Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Mon, 13 Jun 2022 09:43:14 +0300 Subject: [PATCH] Refine `Window::set_cursor_grab` API This commit renames `Window::set_cursor_grab` to `Window::set_cursor_grab_mode`. The new API now accepts enumeration to control the way cursor grab is performed. The value could be: `lock`, `confine`, or `none`. This commit also implements `Window::set_cursor_position` for Wayland, since it's tied to locked cursor. Implements API from #1677. --- CHANGELOG.md | 2 + FEATURES.md | 8 +- examples/cursor_grab.rs | 24 +++- examples/multithreaded.rs | 18 ++- src/platform_impl/android/mod.rs | 5 +- src/platform_impl/ios/window.rs | 5 +- src/platform_impl/linux/mod.rs | 10 +- src/platform_impl/linux/wayland/env.rs | 10 +- .../linux/wayland/seat/pointer/data.rs | 6 +- .../linux/wayland/seat/pointer/handlers.rs | 2 + .../linux/wayland/seat/pointer/mod.rs | 77 ++++++++++++ src/platform_impl/linux/wayland/window/mod.rs | 41 +++++-- .../linux/wayland/window/shim.rs | 68 +++++++---- src/platform_impl/linux/x11/window.rs | 113 ++++++++++-------- src/platform_impl/macos/window.rs | 15 ++- src/platform_impl/web/web_sys/canvas.rs | 4 +- src/platform_impl/web/window.rs | 14 ++- src/platform_impl/windows/window.rs | 14 ++- src/window.rs | 54 +++++++-- 19 files changed, 359 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d98478e9..fdc63ad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,8 @@ And please only add new entries to the top of this list, right below the `# Unre - **Breaking:** On X11, device events are now ignored for unfocused windows by default, use `EventLoopWindowTarget::set_device_event_filter` to set the filter level. - Implemented `Default` on `EventLoop<()>`. - Implemented `Eq` for `Fullscreen`, `Theme`, and `UserAttentionType`. +- **Breaking:** `Window::set_cursor_grab` now accepts `CursorGrabMode` to control grabbing behavior. +- On Wayland, add support for `Window::set_cursor_position`. # 0.26.1 (2022-01-05) diff --git a/FEATURES.md b/FEATURES.md index 02625f06..3610f4b7 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -100,7 +100,8 @@ If your PR makes notable changes to Winit's features, please update this section ### Input Handling - **Mouse events**: Generating mouse events associated with pointer motion, click, and scrolling events. - **Mouse set location**: Forcibly changing the location of the pointer. -- **Cursor grab**: Locking the cursor so it cannot exit the client area of a window. +- **Cursor locking**: Locking the cursor inside the window so it cannot move. +- **Cursor confining**: Confining the cursor to the window bounds so it cannot leave them. - **Cursor icon**: Changing the cursor icon, or hiding the cursor. - **Cursor hittest**: Handle or ignore mouse events for a window. - **Touch events**: Single-touch events. @@ -197,8 +198,9 @@ Legend: |Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |----------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | |Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|✔️ | -|Mouse set location |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A**|**N/A**| -|Cursor grab |✔️ |▢[#165] |▢[#242] |✔️ |**N/A**|**N/A**|✔️ | +|Mouse set location |✔️ |✔️ |✔️ |✔️(when locked) |**N/A**|**N/A**|**N/A**| +|Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ | +|Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ | |Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ | |Cursor hittest |✔️ |✔️ |❌ |✔️ |**N/A**|**N/A**|❌ | |Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | diff --git a/examples/cursor_grab.rs b/examples/cursor_grab.rs index 496b6770..cff77885 100644 --- a/examples/cursor_grab.rs +++ b/examples/cursor_grab.rs @@ -4,7 +4,7 @@ use simple_logger::SimpleLogger; use winit::{ event::{DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, WindowEvent}, event_loop::EventLoop, - window::WindowBuilder, + window::{CursorGrabMode, WindowBuilder}, }; fn main() { @@ -34,11 +34,23 @@ fn main() { .. } => { use winit::event::VirtualKeyCode::*; - match key { - Escape => control_flow.set_exit(), - G => window.set_cursor_grab(!modifiers.shift()).unwrap(), - H => window.set_cursor_visible(modifiers.shift()), - _ => (), + let result = match key { + Escape => { + control_flow.set_exit(); + Ok(()) + } + G => window.set_cursor_grab(CursorGrabMode::Confined), + L => window.set_cursor_grab(CursorGrabMode::Locked), + A => window.set_cursor_grab(CursorGrabMode::None), + H => { + window.set_cursor_visible(modifiers.shift()); + Ok(()) + } + _ => Ok(()), + }; + + if let Err(err) = result { + println!("error: {}", err); } } WindowEvent::ModifiersChanged(m) => modifiers = m, diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index d4f76529..9dc12507 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -9,7 +9,7 @@ fn main() { dpi::{PhysicalPosition, PhysicalSize, Position, Size}, event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event_loop::EventLoop, - window::{CursorIcon, Fullscreen, WindowBuilder}, + window::{CursorGrabMode, CursorIcon, Fullscreen, WindowBuilder}, }; const WINDOW_COUNT: usize = 3; @@ -88,7 +88,21 @@ fn main() { } (false, _) => None, }), - G => window.set_cursor_grab(state).unwrap(), + L if state => { + if let Err(err) = window.set_cursor_grab(CursorGrabMode::Locked) { + println!("error: {}", err); + } + } + G if state => { + if let Err(err) = window.set_cursor_grab(CursorGrabMode::Confined) { + println!("error: {}", err); + } + } + G | L if !state => { + if let Err(err) = window.set_cursor_grab(CursorGrabMode::None) { + println!("error: {}", err); + } + } H => window.set_cursor_visible(!state), I => { println!("Info:"); diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index c472e107..d7b643b3 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -20,7 +20,8 @@ use crate::{ error, event::{self, VirtualKeyCode}, event_loop::{self, ControlFlow}, - monitor, window, + monitor, + window::{self, CursorGrabMode}, }; static CONFIG: Lazy> = Lazy::new(|| { @@ -765,7 +766,7 @@ impl Window { )) } - pub fn set_cursor_grab(&self, _: bool) -> Result<(), error::ExternalError> { + pub fn set_cursor_grab(&self, _: CursorGrabMode) -> Result<(), error::ExternalError> { Err(error::ExternalError::NotSupported( error::NotSupportedError::new(), )) diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index b597f934..52356099 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -23,7 +23,8 @@ use crate::{ monitor, view, EventLoopWindowTarget, MonitorHandle, }, window::{ - CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWindowId, + CursorGrabMode, CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, + WindowId as RootWindowId, }, }; @@ -184,7 +185,7 @@ impl Inner { Err(ExternalError::NotSupported(NotSupportedError::new())) } - pub fn set_cursor_grab(&self, _grab: bool) -> Result<(), ExternalError> { + pub fn set_cursor_grab(&self, _: CursorGrabMode) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 919d4d74..b2e77947 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -9,8 +9,6 @@ #[cfg(all(not(feature = "x11"), not(feature = "wayland")))] compile_error!("Please select a feature to build for unix: `x11`, `wayland`"); -#[cfg(feature = "wayland")] -use crate::window::Theme; #[cfg(feature = "wayland")] use std::error::Error; @@ -28,6 +26,8 @@ use raw_window_handle::RawWindowHandle; pub use self::x11::XNotSupported; #[cfg(feature = "x11")] use self::x11::{ffi::XVisualInfo, util::WindowType as XWindowType, XConnection, XError}; +#[cfg(feature = "wayland")] +use crate::window::Theme; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, @@ -37,7 +37,7 @@ use crate::{ }, icon::Icon, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, - window::{CursorIcon, Fullscreen, UserAttentionType, WindowAttributes}, + window::{CursorGrabMode, CursorIcon, Fullscreen, UserAttentionType, WindowAttributes}, }; pub(crate) use crate::icon::RgbaIcon as PlatformIcon; @@ -388,8 +388,8 @@ impl Window { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(grab)) + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(mode)) } #[inline] diff --git a/src/platform_impl/linux/wayland/env.rs b/src/platform_impl/linux/wayland/env.rs index ae213cb6..f3e9b240 100644 --- a/src/platform_impl/linux/wayland/env.rs +++ b/src/platform_impl/linux/wayland/env.rs @@ -24,23 +24,23 @@ use sctk::shm::ShmHandler; /// Set of extra features that are supported by the compositor. #[derive(Debug, Clone, Copy)] pub struct WindowingFeatures { - cursor_grab: bool, + pointer_constraints: bool, xdg_activation: bool, } impl WindowingFeatures { /// Create `WindowingFeatures` based on the presented interfaces. pub fn new(env: &Environment) -> Self { - let cursor_grab = env.get_global::().is_some(); + let pointer_constraints = env.get_global::().is_some(); let xdg_activation = env.get_global::().is_some(); Self { - cursor_grab, + pointer_constraints, xdg_activation, } } - pub fn cursor_grab(&self) -> bool { - self.cursor_grab + pub fn pointer_constraints(&self) -> bool { + self.pointer_constraints } pub fn xdg_activation(&self) -> bool { diff --git a/src/platform_impl/linux/wayland/seat/pointer/data.rs b/src/platform_impl/linux/wayland/seat/pointer/data.rs index 310e692b..17e7a57a 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/data.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/data.rs @@ -5,8 +5,9 @@ use std::rc::Rc; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::reexports::client::Attached; -use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::{ZwpPointerConstraintsV1}; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1; use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; use crate::event::{ModifiersState, TouchPhase}; @@ -25,6 +26,7 @@ pub(super) struct PointerData { pub pointer_constraints: Option>, pub confined_pointer: Rc>>, + pub locked_pointer: Rc>>, /// Latest observed serial in pointer events. pub latest_serial: Rc>, @@ -39,6 +41,7 @@ pub(super) struct PointerData { impl PointerData { pub fn new( confined_pointer: Rc>>, + locked_pointer: Rc>>, pointer_constraints: Option>, modifiers_state: Rc>, ) -> Self { @@ -47,6 +50,7 @@ impl PointerData { latest_serial: Rc::new(Cell::new(0)), latest_enter_serial: Rc::new(Cell::new(0)), confined_pointer, + locked_pointer, modifiers_state, pointer_constraints, axis_data: AxisData::new(), diff --git a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs index 54ca1939..7348cc66 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs @@ -60,6 +60,7 @@ pub(super) fn handle_pointer( let winit_pointer = WinitPointer { pointer, confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), + locked_pointer: Rc::downgrade(&pointer_data.locked_pointer), pointer_constraints: pointer_data.pointer_constraints.clone(), latest_serial: pointer_data.latest_serial.clone(), latest_enter_serial: pointer_data.latest_enter_serial.clone(), @@ -104,6 +105,7 @@ pub(super) fn handle_pointer( let winit_pointer = WinitPointer { pointer, confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), + locked_pointer: Rc::downgrade(&pointer_data.locked_pointer), pointer_constraints: pointer_data.pointer_constraints.clone(), latest_serial: pointer_data.latest_serial.clone(), latest_enter_serial: pointer_data.latest_enter_serial.clone(), diff --git a/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/src/platform_impl/linux/wayland/seat/pointer/mod.rs index e1b34a5e..b43deebb 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/mod.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -11,6 +11,7 @@ use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_rela use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1; use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_pointer_constraints_v1::{ZwpPointerConstraintsV1, Lifetime}; use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; +use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; use sctk::seat::pointer::{ThemeManager, ThemedPointer}; use sctk::window::Window; @@ -35,9 +36,13 @@ pub struct WinitPointer { /// Cursor to handle confine requests. confined_pointer: Weak>>, + /// Cursor to handle locked requests. + locked_pointer: Weak>>, + /// Latest observed serial in pointer events. /// used by Window::start_interactive_move() latest_serial: Rc>, + /// Latest observed serial in pointer enter events. /// used by Window::set_cursor() latest_enter_serial: Rc>, @@ -157,6 +162,52 @@ impl WinitPointer { } } + pub fn lock(&self, surface: &WlSurface) { + let pointer_constraints = match &self.pointer_constraints { + Some(pointer_constraints) => pointer_constraints, + None => return, + }; + + let locked_pointer = match self.locked_pointer.upgrade() { + Some(locked_pointer) => locked_pointer, + // A pointer is gone. + None => return, + }; + + *locked_pointer.borrow_mut() = Some(init_locked_pointer( + pointer_constraints, + surface, + &*self.pointer, + )); + } + + pub fn unlock(&self) { + let locked_pointer = match self.locked_pointer.upgrade() { + Some(locked_pointer) => locked_pointer, + // A pointer is gone. + None => return, + }; + + let mut locked_pointer = locked_pointer.borrow_mut(); + + if let Some(locked_pointer) = locked_pointer.take() { + locked_pointer.destroy(); + } + } + + pub fn set_cursor_position(&self, surface_x: u32, surface_y: u32) { + let locked_pointer = match self.locked_pointer.upgrade() { + Some(locked_pointer) => locked_pointer, + // A pointer is gone. + None => return, + }; + + let locked_pointer = locked_pointer.borrow_mut(); + if let Some(locked_pointer) = locked_pointer.as_ref() { + locked_pointer.set_cursor_position_hint(surface_x.into(), surface_y.into()); + } + } + pub fn drag_window(&self, window: &Window) { // WlPointer::setart_interactive_move() expects the last serial of *any* // pointer event (compare to set_cursor()). @@ -174,6 +225,9 @@ pub(super) struct Pointers { /// Confined pointer. confined_pointer: Rc>>, + + /// Locked pointer. + locked_pointer: Rc>>, } impl Pointers { @@ -185,11 +239,15 @@ impl Pointers { modifiers_state: Rc>, ) -> Self { let confined_pointer = Rc::new(RefCell::new(None)); + let locked_pointer = Rc::new(RefCell::new(None)); + let pointer_data = Rc::new(RefCell::new(PointerData::new( confined_pointer.clone(), + locked_pointer.clone(), pointer_constraints.clone(), modifiers_state, ))); + let pointer_seat = seat.detach(); let pointer = theme_manager.theme_pointer_with_impl( seat, @@ -216,6 +274,7 @@ impl Pointers { pointer, relative_pointer, confined_pointer, + locked_pointer, } } } @@ -232,6 +291,11 @@ impl Drop for Pointers { confined_pointer.destroy(); } + // Drop lock ponter. + if let Some(locked_pointer) = self.locked_pointer.borrow_mut().take() { + locked_pointer.destroy(); + } + // Drop the pointer itself in case it's possible. if self.pointer.as_ref().version() >= 3 { self.pointer.release(); @@ -264,3 +328,16 @@ pub(super) fn init_confined_pointer( confined_pointer.detach() } + +pub(super) fn init_locked_pointer( + pointer_constraints: &Attached, + surface: &WlSurface, + pointer: &WlPointer, +) -> ZwpLockedPointerV1 { + let locked_pointer = + pointer_constraints.lock_pointer(surface, pointer, None, Lifetime::Persistent); + + locked_pointer.quick_assign(move |_, _, _| {}); + + locked_pointer.detach() +} diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index f9c10797..fe7fb425 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -17,7 +17,9 @@ use crate::platform_impl::{ MonitorHandle as PlatformMonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes as PlatformAttributes, }; -use crate::window::{CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes}; +use crate::window::{ + CursorGrabMode, CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes, +}; use super::env::WindowingFeatures; use super::event_loop::WinitState; @@ -72,6 +74,9 @@ pub struct Window { /// Whether the window is decorated. decorated: AtomicBool, + + /// Grabbing mode. + cursor_grab_mode: Mutex, } impl Window { @@ -285,6 +290,7 @@ impl Window { windowing_features, resizeable: AtomicBool::new(attributes.resizable), decorated: AtomicBool::new(attributes.decorations), + cursor_grab_mode: Mutex::new(CursorGrabMode::None), }; Ok(window) @@ -474,12 +480,17 @@ impl Window { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - if !self.windowing_features.cursor_grab() { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + if !self.windowing_features.pointer_constraints() { + if mode == CursorGrabMode::None { + return Ok(()); + } + return Err(ExternalError::NotSupported(NotSupportedError::new())); } - self.send_request(WindowRequest::GrabCursor(grab)); + *self.cursor_grab_mode.lock().unwrap() = mode; + self.send_request(WindowRequest::SetCursorGrabMode(mode)); Ok(()) } @@ -494,15 +505,19 @@ impl Window { } #[inline] - pub fn set_cursor_position(&self, _: Position) -> Result<(), ExternalError> { - // XXX This is possible if the locked pointer is being used. We don't have any - // API for that right now, but it could be added in - // https://github.com/rust-windowing/winit/issues/1677. - // - // This function is essential for the locked pointer API. - // - // See pointer-constraints-unstable-v1.xml. - Err(ExternalError::NotSupported(NotSupportedError::new())) + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + // Positon can be set only for locked cursor. + if *self.cursor_grab_mode.lock().unwrap() != CursorGrabMode::Locked { + return Err(ExternalError::Os(os_error!(OsError::WaylandMisc( + "cursor position can be set only for locked cursor." + )))); + } + + let scale_factor = self.scale_factor() as f64; + let position = position.to_logical(scale_factor); + self.send_request(WindowRequest::SetLockedCursorPosition(position)); + + Ok(()) } #[inline] diff --git a/src/platform_impl/linux/wayland/window/shim.rs b/src/platform_impl/linux/wayland/window/shim.rs index 84d511f9..08e2a316 100644 --- a/src/platform_impl/linux/wayland/window/shim.rs +++ b/src/platform_impl/linux/wayland/window/shim.rs @@ -19,7 +19,7 @@ use crate::platform_impl::wayland::event_loop::{EventSink, WinitState}; use crate::platform_impl::wayland::seat::pointer::WinitPointer; use crate::platform_impl::wayland::seat::text_input::TextInputHandler; use crate::platform_impl::wayland::WindowId; -use crate::window::{CursorIcon, Theme, UserAttentionType}; +use crate::window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType}; use super::WinitFrame; @@ -40,8 +40,11 @@ pub enum WindowRequest { /// Change the cursor icon. NewCursorIcon(CursorIcon), - /// Grab cursor. - GrabCursor(bool), + /// Change cursor grabbing mode. + SetCursorGrabMode(CursorGrabMode), + + /// Set cursor position. + SetLockedCursorPosition(LogicalPosition), /// Drag window. DragWindow, @@ -172,7 +175,7 @@ pub struct WindowHandle { cursor_visible: Cell, /// Cursor confined to the surface. - confined: Cell, + cursor_grab_mode: Cell, /// Pointers over the current surface. pointers: Vec, @@ -208,7 +211,7 @@ impl WindowHandle { pending_window_requests, cursor_icon: Cell::new(CursorIcon::Default), is_resizable: Cell::new(true), - confined: Cell::new(false), + cursor_grab_mode: Cell::new(CursorGrabMode::None), cursor_visible: Cell::new(true), pointers: Vec::new(), text_inputs: Vec::new(), @@ -219,24 +222,37 @@ impl WindowHandle { } } - pub fn set_cursor_grab(&self, grab: bool) { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) { // The new requested state matches the current confine status, return. - if self.confined.get() == grab { + let old_mode = self.cursor_grab_mode.replace(mode); + if old_mode == mode { return; } - self.confined.replace(grab); + // Clear old pointer data. + match old_mode { + CursorGrabMode::None => (), + CursorGrabMode::Confined => self.pointers.iter().for_each(|p| p.unconfine()), + CursorGrabMode::Locked => self.pointers.iter().for_each(|p| p.unlock()), + } - for pointer in self.pointers.iter() { - if self.confined.get() { - let surface = self.window.surface(); - pointer.confine(surface); - } else { - pointer.unconfine(); + let surface = self.window.surface(); + match mode { + CursorGrabMode::Locked => self.pointers.iter().for_each(|p| p.lock(surface)), + CursorGrabMode::Confined => self.pointers.iter().for_each(|p| p.confine(surface)), + CursorGrabMode::None => { + // Current lock/confine was already removed. } } } + pub fn set_locked_cursor_position(&self, position: LogicalPosition) { + // XXX the cursor locking is ensured inside `Window`. + self.pointers + .iter() + .for_each(|p| p.set_cursor_position(position.x, position.y)); + } + pub fn set_user_attention(&self, request_type: Option) { let xdg_activation = match self.xdg_activation.as_ref() { None => return, @@ -284,10 +300,13 @@ impl WindowHandle { let position = self.pointers.iter().position(|p| *p == pointer); if position.is_none() { - if self.confined.get() { - let surface = self.window.surface(); - pointer.confine(surface); + let surface = self.window.surface(); + match self.cursor_grab_mode.get() { + CursorGrabMode::None => (), + CursorGrabMode::Locked => pointer.lock(surface), + CursorGrabMode::Confined => pointer.confine(surface), } + self.pointers.push(pointer); } @@ -302,9 +321,11 @@ impl WindowHandle { if let Some(position) = position { let pointer = self.pointers.remove(position); - // Drop the confined pointer. - if self.confined.get() { - pointer.unconfine(); + // Drop the grabbing mode. + match self.cursor_grab_mode.get() { + CursorGrabMode::None => (), + CursorGrabMode::Locked => pointer.unlock(), + CursorGrabMode::Confined => pointer.unconfine(), } } } @@ -428,8 +449,11 @@ pub fn handle_window_requests(winit_state: &mut WinitState) { let event_sink = &mut winit_state.event_sink; window_handle.set_ime_allowed(allow, event_sink); } - WindowRequest::GrabCursor(grab) => { - window_handle.set_cursor_grab(grab); + WindowRequest::SetCursorGrabMode(mode) => { + window_handle.set_cursor_grab(mode); + } + WindowRequest::SetLockedCursorPosition(position) => { + window_handle.set_locked_cursor_position(position); } WindowRequest::DragWindow => { window_handle.drag_window(); diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 89ac8e46..cb657553 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -22,7 +22,7 @@ use crate::{ MonitorHandle as PlatformMonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode, }, - window::{CursorIcon, Fullscreen, Icon, UserAttentionType, WindowAttributes}, + window::{CursorGrabMode, CursorIcon, Fullscreen, Icon, UserAttentionType, WindowAttributes}, }; use super::{ @@ -106,7 +106,7 @@ pub struct UnownedWindow { root: ffi::Window, // never changes screen_id: i32, // never changes cursor: Mutex, - cursor_grabbed: Mutex, + cursor_grabbed_mode: Mutex, cursor_visible: Mutex, ime_sender: Mutex, pub shared_state: Mutex, @@ -276,7 +276,7 @@ impl UnownedWindow { root, screen_id, cursor: Default::default(), - cursor_grabbed: Mutex::new(false), + cursor_grabbed_mode: Mutex::new(CursorGrabMode::None), cursor_visible: Mutex::new(true), ime_sender: Mutex::new(event_loop.ime_sender.clone()), shared_state: SharedState::new(guessed_monitor, &window_attrs), @@ -1272,64 +1272,75 @@ impl UnownedWindow { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - let mut grabbed_lock = self.cursor_grabbed.lock(); - if grab == *grabbed_lock { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let mut grabbed_lock = self.cursor_grabbed_mode.lock(); + if mode == *grabbed_lock { return Ok(()); } + unsafe { // We ungrab before grabbing to prevent passive grabs from causing `AlreadyGrabbed`. // Therefore, this is common to both codepaths. (self.xconn.xlib.XUngrabPointer)(self.xconn.display, ffi::CurrentTime); } - let result = if grab { - let result = unsafe { - (self.xconn.xlib.XGrabPointer)( - self.xconn.display, - self.xwindow, - ffi::True, - (ffi::ButtonPressMask - | ffi::ButtonReleaseMask - | ffi::EnterWindowMask - | ffi::LeaveWindowMask - | ffi::PointerMotionMask - | ffi::PointerMotionHintMask - | ffi::Button1MotionMask - | ffi::Button2MotionMask - | ffi::Button3MotionMask - | ffi::Button4MotionMask - | ffi::Button5MotionMask - | ffi::ButtonMotionMask - | ffi::KeymapStateMask) as c_uint, - ffi::GrabModeAsync, - ffi::GrabModeAsync, - self.xwindow, - 0, - ffi::CurrentTime, - ) - }; - match result { - ffi::GrabSuccess => Ok(()), - ffi::AlreadyGrabbed => { - Err("Cursor could not be grabbed: already grabbed by another client") - } - ffi::GrabInvalidTime => Err("Cursor could not be grabbed: invalid time"), - ffi::GrabNotViewable => { - Err("Cursor could not be grabbed: grab location not viewable") - } - ffi::GrabFrozen => Err("Cursor could not be grabbed: frozen by another client"), - _ => unreachable!(), - } - .map_err(|err| ExternalError::Os(os_error!(OsError::XMisc(err)))) - } else { - self.xconn + let result = match mode { + CursorGrabMode::None => self + .xconn .flush_requests() - .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err)))) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err)))), + CursorGrabMode::Confined => { + let result = unsafe { + (self.xconn.xlib.XGrabPointer)( + self.xconn.display, + self.xwindow, + ffi::True, + (ffi::ButtonPressMask + | ffi::ButtonReleaseMask + | ffi::EnterWindowMask + | ffi::LeaveWindowMask + | ffi::PointerMotionMask + | ffi::PointerMotionHintMask + | ffi::Button1MotionMask + | ffi::Button2MotionMask + | ffi::Button3MotionMask + | ffi::Button4MotionMask + | ffi::Button5MotionMask + | ffi::ButtonMotionMask + | ffi::KeymapStateMask) as c_uint, + ffi::GrabModeAsync, + ffi::GrabModeAsync, + self.xwindow, + 0, + ffi::CurrentTime, + ) + }; + + match result { + ffi::GrabSuccess => Ok(()), + ffi::AlreadyGrabbed => { + Err("Cursor could not be confined: already confined by another client") + } + ffi::GrabInvalidTime => Err("Cursor could not be confined: invalid time"), + ffi::GrabNotViewable => { + Err("Cursor could not be confined: confine location not viewable") + } + ffi::GrabFrozen => { + Err("Cursor could not be confined: frozen by another client") + } + _ => unreachable!(), + } + .map_err(|err| ExternalError::Os(os_error!(OsError::XMisc(err)))) + } + CursorGrabMode::Locked => { + return Err(ExternalError::NotSupported(NotSupportedError::new())); + } }; + if result.is_ok() { - *grabbed_lock = grab; + *grabbed_lock = mode; } + result } @@ -1386,14 +1397,14 @@ impl UnownedWindow { // we can't use `set_cursor_grab(false)` here because it doesn't run `XUngrabPointer` // if the cursor isn't currently grabbed - let mut grabbed_lock = self.cursor_grabbed.lock(); + let mut grabbed_lock = self.cursor_grabbed_mode.lock(); unsafe { (self.xconn.xlib.XUngrabPointer)(self.xconn.display, ffi::CurrentTime); } self.xconn .flush_requests() .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))?; - *grabbed_lock = false; + *grabbed_lock = CursorGrabMode::None; // we keep the lock until we are done self.xconn diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 4c1ec0be..35c944ce 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -29,7 +29,8 @@ use crate::{ OsError, }, window::{ - CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWindowId, + CursorGrabMode, CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, + WindowId as RootWindowId, }, }; use cocoa::{ @@ -621,9 +622,17 @@ impl UnownedWindow { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let associate_mouse_cursor = match mode { + CursorGrabMode::Locked => false, + CursorGrabMode::None => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + // TODO: Do this for real https://stackoverflow.com/a/40922095/5435443 - CGDisplay::associate_mouse_and_mouse_cursor_position(!grab) + CGDisplay::associate_mouse_and_mouse_cursor_position(associate_mouse_cursor) .map_err(|status| ExternalError::Os(os_error!(OsError::CGError(status)))) } diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 83669b2d..521e88a8 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -89,8 +89,8 @@ impl Canvas { }) } - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), RootOE> { - if grab { + pub fn set_cursor_lock(&self, lock: bool) -> Result<(), RootOE> { + if lock { self.raw().request_pointer_lock(); } else { let window = web_sys::window() diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index bd5791cb..51dc98ae 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -4,7 +4,7 @@ use crate::event; use crate::icon::Icon; use crate::monitor::MonitorHandle as RootMH; use crate::window::{ - CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWI, + CursorGrabMode, CursorIcon, Fullscreen, UserAttentionType, WindowAttributes, WindowId as RootWI, }; use raw_window_handle::{RawWindowHandle, WebHandle}; @@ -216,10 +216,18 @@ impl Window { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let lock = match mode { + CursorGrabMode::None => false, + CursorGrabMode::Locked => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + self.canvas .borrow() - .set_cursor_grab(grab) + .set_cursor_lock(lock) .map_err(ExternalError::Os) } diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 3e0d98fd..bc55a2a4 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -69,7 +69,7 @@ use crate::{ window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, Parent, PlatformSpecificWindowBuilderAttributes, WindowId, }, - window::{CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes}, + window::{CursorGrabMode, CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes}, }; /// The Win32 implementation of the main `Window` object. @@ -276,7 +276,15 @@ impl Window { } #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let confine = match mode { + CursorGrabMode::None => false, + CursorGrabMode::Confined => true, + CursorGrabMode::Locked => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + } + }; + let window = self.window.clone(); let window_state = Arc::clone(&self.window_state); let (tx, rx) = channel(); @@ -286,7 +294,7 @@ impl Window { let result = window_state .lock() .mouse - .set_cursor_flags(window.0, |f| f.set(CursorFlags::GRABBED, grab)) + .set_cursor_flags(window.0, |f| f.set(CursorFlags::GRABBED, confine)) .map_err(|e| ExternalError::Os(os_error!(e))); let _ = tx.send(result); }); diff --git a/src/window.rs b/src/window.rs index 50fcdbdc..666ebe36 100644 --- a/src/window.rs +++ b/src/window.rs @@ -906,18 +906,24 @@ impl Window { self.window.set_cursor_position(position.into()) } - /// Grabs the cursor, preventing it from leaving the window. + /// Set grabbing [mode]([`CursorGrabMode`]) on the cursor preventing it from leaving the window. /// - /// There's no guarantee that the cursor will be hidden. You should - /// hide it by yourself if you want so. + /// # Example /// - /// ## Platform-specific + /// First try confining the cursor, and if that fails, try locking it instead. /// - /// - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. - /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + /// ```no-run + /// # use winit::event_loop::EventLoop; + /// # use winit::window::{CursorGrabMode, Window}; + /// # let mut event_loop = EventLoop::new(); + /// # let window = Window::new(&event_loop).unwrap(); + /// window.set_cursor_grab(CursorGrabMode::Confined) + /// .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Locked)) + /// .unwrap(); + /// ``` #[inline] - pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { - self.window.set_cursor_grab(grab) + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + self.window.set_cursor_grab(mode) } /// Modifies the cursor's visibility. @@ -1028,6 +1034,38 @@ unsafe impl raw_window_handle::HasRawWindowHandle for Window { } } +/// The behavior of cursor grabbing. +/// +/// Use this enum with [`Window::set_cursor_grab`] to grab the cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorGrabMode { + /// No grabbing of the cursor is performed. + None, + + /// The cursor is confined to the window area. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **macOS:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - ** iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + Confined, + + /// The cursor is locked inside the window area to the certain position. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **X11 / Windows:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - ** iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + Locked, +} + /// Describes the appearance of the mouse cursor. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]