diff --git a/CHANGELOG.md b/CHANGELOG.md index bd8812a9..ae7f4993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - `AvailableMonitorsIter` now implements `Debug`. - Fixed quirk on macOS where certain keys would generate characters at twice the normal rate when held down. - On X11, all event loops now share the same `XConnection`. +- **Breaking:** `Window::set_cursor_state` and `CursorState` enum removed in favor of the more composable `Window::grab_cursor` and `Window::hide_cursor`. As a result, grabbing the cursor no longer automatically hides it; you must call both methods to retain the old behavior on Windows and macOS. `Cursor::NoneCursor` has been removed, as it's no longer useful. # Version 0.15.1 (2018-06-13) diff --git a/examples/cursor.rs b/examples/cursor.rs index 682d30ca..d2df71a7 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -8,7 +8,7 @@ fn main() { let window = winit::WindowBuilder::new().build(&events_loop).unwrap(); window.set_title("A fantastic window!"); - let cursors = [MouseCursor::Default, MouseCursor::Crosshair, MouseCursor::Hand, MouseCursor::Arrow, MouseCursor::Move, MouseCursor::Text, MouseCursor::Wait, MouseCursor::Help, MouseCursor::Progress, MouseCursor::NotAllowed, MouseCursor::ContextMenu, MouseCursor::NoneCursor, MouseCursor::Cell, MouseCursor::VerticalText, MouseCursor::Alias, MouseCursor::Copy, MouseCursor::NoDrop, MouseCursor::Grab, MouseCursor::Grabbing, MouseCursor::AllScroll, MouseCursor::ZoomIn, MouseCursor::ZoomOut, MouseCursor::EResize, MouseCursor::NResize, MouseCursor::NeResize, MouseCursor::NwResize, MouseCursor::SResize, MouseCursor::SeResize, MouseCursor::SwResize, MouseCursor::WResize, MouseCursor::EwResize, MouseCursor::NsResize, MouseCursor::NeswResize, MouseCursor::NwseResize, MouseCursor::ColResize, MouseCursor::RowResize]; + let cursors = [MouseCursor::Default, MouseCursor::Crosshair, MouseCursor::Hand, MouseCursor::Arrow, MouseCursor::Move, MouseCursor::Text, MouseCursor::Wait, MouseCursor::Help, MouseCursor::Progress, MouseCursor::NotAllowed, MouseCursor::ContextMenu, MouseCursor::Cell, MouseCursor::VerticalText, MouseCursor::Alias, MouseCursor::Copy, MouseCursor::NoDrop, MouseCursor::Grab, MouseCursor::Grabbing, MouseCursor::AllScroll, MouseCursor::ZoomIn, MouseCursor::ZoomOut, MouseCursor::EResize, MouseCursor::NResize, MouseCursor::NeResize, MouseCursor::NwResize, MouseCursor::SResize, MouseCursor::SeResize, MouseCursor::SwResize, MouseCursor::WResize, MouseCursor::EwResize, MouseCursor::NsResize, MouseCursor::NeswResize, MouseCursor::NwseResize, MouseCursor::ColResize, MouseCursor::RowResize]; let mut cursor_idx = 0; events_loop.run_forever(|event| { diff --git a/examples/cursor_grab.rs b/examples/cursor_grab.rs new file mode 100644 index 00000000..641a324d --- /dev/null +++ b/examples/cursor_grab.rs @@ -0,0 +1,38 @@ +extern crate winit; + +fn main() { + let mut events_loop = winit::EventsLoop::new(); + + let window = winit::WindowBuilder::new() + .with_title("Super Cursor Grab'n'Hide Simulator 9000") + .build(&events_loop) + .unwrap(); + + events_loop.run_forever(|event| { + if let winit::Event::WindowEvent { event, .. } = event { + use winit::WindowEvent::*; + match event { + CloseRequested => return winit::ControlFlow::Break, + KeyboardInput { + input: winit::KeyboardInput { + state: winit::ElementState::Released, + virtual_keycode: Some(key), + modifiers, + .. + }, + .. + } => { + use winit::VirtualKeyCode::*; + match key { + Escape => return winit::ControlFlow::Break, + G => window.grab_cursor(!modifiers.shift).unwrap(), + H => window.hide_cursor(!modifiers.shift), + _ => (), + } + } + _ => (), + } + } + winit::ControlFlow::Continue + }); +} diff --git a/examples/grabbing.rs b/examples/grabbing.rs deleted file mode 100644 index b2416501..00000000 --- a/examples/grabbing.rs +++ /dev/null @@ -1,45 +0,0 @@ -extern crate winit; - -use winit::{ControlFlow, WindowEvent, ElementState, KeyboardInput}; - -fn main() { - let mut events_loop = winit::EventsLoop::new(); - - let window = winit::WindowBuilder::new().build(&events_loop).unwrap(); - window.set_title("winit - Cursor grabbing test"); - - let mut grabbed = false; - - events_loop.run_forever(|event| { - println!("{:?}", event); - - match event { - winit::Event::WindowEvent { event, .. } => { - match event { - WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Pressed, .. }, .. } => { - if grabbed { - grabbed = false; - window.set_cursor_state(winit::CursorState::Normal) - .ok().expect("could not ungrab mouse cursor"); - } else { - grabbed = true; - window.set_cursor_state(winit::CursorState::Grab) - .ok().expect("could not grab mouse cursor"); - } - }, - - WindowEvent::CloseRequested => return ControlFlow::Break, - - a @ WindowEvent::CursorMoved { .. } => { - println!("{:?}", a); - }, - - _ => (), - } - } - _ => {} - } - - ControlFlow::Continue - }); -} diff --git a/src/lib.rs b/src/lib.rs index 44a1cb4a..d3971574 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -355,7 +355,6 @@ pub enum MouseCursor { /// Cursor showing that something cannot be done. NotAllowed, ContextMenu, - NoneCursor, Cell, VerticalText, Alias, @@ -391,29 +390,6 @@ impl Default for MouseCursor { } } -/// Describes how winit handles the cursor. -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum CursorState { - /// Normal cursor behavior. - Normal, - - /// The cursor will be invisible when over the window. - Hide, - - /// Grabs the mouse cursor. The cursor's motion will be confined to this - /// window and the window has exclusive access to further events regarding - /// the cursor. - /// - /// This is useful for first-person cameras for example. - Grab, -} - -impl Default for CursorState { - fn default() -> Self { - CursorState::Normal - } -} - /// Attributes to use when creating a window. #[derive(Debug, Clone)] pub struct WindowAttributes { diff --git a/src/platform/android/mod.rs b/src/platform/android/mod.rs index 94c8182d..5b140276 100644 --- a/src/platform/android/mod.rs +++ b/src/platform/android/mod.rs @@ -12,7 +12,6 @@ use std::sync::mpsc::{Receiver, channel}; use { CreationError, - CursorState, Event, LogicalPosition, LogicalSize, @@ -337,9 +336,13 @@ impl Window { } #[inline] - pub fn set_cursor_state(&self, _state: CursorState) -> Result<(), String> { + pub fn grab_cursor(&self, _grab: bool) -> Result<(), String> { + Err("Cursor grabbing is not possible on Android.".to_owned()) + } + + #[inline] + pub fn hide_cursor(&self, _hide: bool) { // N/A - Ok(()) } #[inline] diff --git a/src/platform/emscripten/mod.rs b/src/platform/emscripten/mod.rs index 41da4213..f68df623 100644 --- a/src/platform/emscripten/mod.rs +++ b/src/platform/emscripten/mod.rs @@ -150,7 +150,8 @@ impl EventsLoop { pub struct WindowId(usize); pub struct Window2 { - cursor_state: Mutex<::CursorState>, + cursor_grabbed: Mutex, + cursor_hidden: Mutex, is_fullscreen: bool, events: Box>>, } @@ -374,7 +375,8 @@ impl Window { } let w = Window2 { - cursor_state: Default::default(), + cursor_grabbed: Default::default(), + cursor_hidden: Default::default(), events: Default::default(), is_fullscreen: attribs.fullscreen.is_some(), }; @@ -498,48 +500,57 @@ impl Window { } #[inline] - pub fn show(&self) {} - #[inline] - pub fn hide(&self) {} + pub fn show(&self) { + // N/A + } #[inline] - pub fn set_cursor(&self, _cursor: ::MouseCursor) {} + pub fn hide(&self) { + // N/A + } #[inline] - pub fn set_cursor_state(&self, state: ::CursorState) -> Result<(), String> { + pub fn set_cursor(&self, _cursor: ::MouseCursor) { + // N/A + } + + #[inline] + pub fn grab_cursor(&self, grab: bool) -> Result<(), String> { + let mut grabbed_lock = self.window.cursor_grabbed.lock().unwrap(); + if grab == *grabbed_lock { return Ok(()); } unsafe { - use ::CursorState::*; - - let mut old_state = self.window.cursor_state.lock().unwrap(); - if state == *old_state { - return Ok(()); + if grab { + em_try(ffi::emscripten_set_pointerlockchange_callback( + ptr::null(), + 0 as *mut c_void, + ffi::EM_FALSE, + Some(pointerlockchange_callback), + ))?; + em_try(ffi::emscripten_request_pointerlock(ptr::null(), ffi::EM_TRUE))?; + } else { + em_try(ffi::emscripten_set_pointerlockchange_callback( + ptr::null(), + 0 as *mut c_void, + ffi::EM_FALSE, + None, + ))?; + em_try(ffi::emscripten_exit_pointerlock())?; } - - // Set or unset grab callback - match state { - Hide | Normal => em_try(ffi::emscripten_set_pointerlockchange_callback(ptr::null(), 0 as *mut c_void, ffi::EM_FALSE, None))?, - Grab => em_try(ffi::emscripten_set_pointerlockchange_callback(ptr::null(), 0 as *mut c_void, ffi::EM_FALSE, Some(pointerlockchange_callback)))?, - } - - // Go back to normal cursor state - match *old_state { - Hide => show_mouse(), - Grab => em_try(ffi::emscripten_exit_pointerlock())?, - Normal => (), - } - - // Set cursor from normal cursor state - match state { - Hide => ffi::emscripten_hide_mouse(), - Grab => em_try(ffi::emscripten_request_pointerlock(ptr::null(), ffi::EM_TRUE))?, - Normal => (), - } - - // Update - *old_state = state; - - Ok(()) } + *grabbed_lock = grab; + Ok(()) + } + + #[inline] + pub fn hide_cursor(&self, hide: bool) { + let mut hidden_lock = self.window.cursor_hidden.lock().unwrap(); + if hide == *hidden_lock { return; } + if hide { + unsafe { ffi::emscripten_hide_mouse() }; + } else { + show_mouse(); + } + *hidden_lock = hide; } #[inline] @@ -610,7 +621,8 @@ impl Drop for Window { unsafe { // Return back to normal cursor state - let _ = self.set_cursor_state(::CursorState::Normal); + self.hide_cursor(false); + self.grab_cursor(false); // Exit fullscreen if on if self.window.is_fullscreen { diff --git a/src/platform/ios/mod.rs b/src/platform/ios/mod.rs index 96f9aef0..10e6ddef 100644 --- a/src/platform/ios/mod.rs +++ b/src/platform/ios/mod.rs @@ -69,7 +69,6 @@ use objc::runtime::{BOOL, Class, Object, Sel, YES}; use { CreationError, - CursorState, Event, LogicalPosition, LogicalSize, @@ -394,9 +393,13 @@ impl Window { } #[inline] - pub fn set_cursor_state(&self, _cursor_state: CursorState) -> Result<(), String> { + pub fn grab_cursor(&self, _grab: bool) -> Result<(), String> { + Err("Cursor grabbing is not possible on iOS.".to_owned()) + } + + #[inline] + pub fn hide_cursor(&self, _hide: bool) { // N/A - Ok(()) } #[inline] diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 9e209521..c5e7cac8 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -11,17 +11,13 @@ use sctk::reexports::client::ConnectError; use { CreationError, - CursorState, EventsLoopClosed, Icon, - LogicalPosition, - LogicalSize, MouseCursor, - PhysicalPosition, - PhysicalSize, ControlFlow, WindowAttributes, }; +use dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; use window::MonitorId as RootMonitorId; use self::x11::{XConnection, XError}; use self::x11::ffi::XVisualInfo; @@ -252,10 +248,18 @@ impl Window { } #[inline] - pub fn set_cursor_state(&self, state: CursorState) -> Result<(), String> { + pub fn grab_cursor(&self, grab: bool) -> Result<(), String> { match self { - &Window::X(ref w) => w.set_cursor_state(state), - &Window::Wayland(ref w) => w.set_cursor_state(state) + &Window::X(ref window) => window.grab_cursor(grab), + &Window::Wayland(ref _window) => Err("Cursor grabbing is not yet possible on Wayland.".to_owned()), + } + } + + #[inline] + pub fn hide_cursor(&self, hide: bool) { + match self { + &Window::X(ref window) => window.hide_cursor(hide), + &Window::Wayland(ref _window) => unimplemented!(), } } diff --git a/src/platform/linux/wayland/window.rs b/src/platform/linux/wayland/window.rs index cf6b8fff..085c336d 100644 --- a/src/platform/linux/wayland/window.rs +++ b/src/platform/linux/wayland/window.rs @@ -1,7 +1,8 @@ use std::collections::VecDeque; use std::sync::{Arc, Mutex, Weak}; -use {CreationError, CursorState, MouseCursor, WindowAttributes, LogicalPosition, LogicalSize}; +use {CreationError, MouseCursor, WindowAttributes}; +use dpi::{LogicalPosition, LogicalSize}; use platform::MonitorId as PlatformMonitorId; use window::MonitorId as RootMonitorId; @@ -239,17 +240,6 @@ impl Window { // TODO } - #[inline] - pub fn set_cursor_state(&self, state: CursorState) -> Result<(), String> { - use CursorState::{Grab, Hide, Normal}; - // TODO : not yet possible on wayland to grab cursor - match state { - Grab => Err("Cursor cannot be grabbed on wayland yet.".to_string()), - Hide => Err("Cursor cannot be hidden on wayland yet.".to_string()), - Normal => Ok(()), - } - } - #[inline] pub fn hidpi_factor(&self) -> i32 { self.monitors.lock().unwrap().compute_hidpi_factor() diff --git a/src/platform/linux/x11/window.rs b/src/platform/linux/x11/window.rs index e2a2fd23..1f2c9b3d 100644 --- a/src/platform/linux/x11/window.rs +++ b/src/platform/linux/x11/window.rs @@ -7,8 +7,9 @@ use std::sync::Arc; use libc; use parking_lot::Mutex; -use {CursorState, Icon, LogicalPosition, LogicalSize, MouseCursor, WindowAttributes}; +use {Icon, MouseCursor, WindowAttributes}; use CreationError::{self, OsError}; +use dpi::{LogicalPosition, LogicalSize}; use platform::MonitorId as PlatformMonitorId; use platform::PlatformSpecificWindowBuilderAttributes; use platform::x11::MonitorId as X11MonitorId; @@ -61,7 +62,8 @@ pub struct UnownedWindow { root: ffi::Window, // never changes screen_id: i32, // never changes cursor: Mutex, - cursor_state: Mutex, + cursor_grabbed: Mutex, + cursor_hidden: Mutex, ime_sender: Mutex, pub multitouch: bool, // never changes pub shared_state: Mutex, @@ -160,7 +162,8 @@ impl UnownedWindow { root, screen_id, cursor: Default::default(), - cursor_state: Default::default(), + cursor_grabbed: Default::default(), + cursor_hidden: Default::default(), ime_sender: Mutex::new(event_loop.ime_sender.clone()), multitouch: window_attrs.multitouch, shared_state: SharedState::new(), @@ -976,9 +979,6 @@ impl UnownedWindow { MouseCursor::ZoomIn => load(b"zoom-in\0"), MouseCursor::ZoomOut => load(b"zoom-out\0"), - - MouseCursor::NoneCursor => self.create_empty_cursor() - .expect("Failed to create empty cursor"), } } @@ -995,7 +995,7 @@ impl UnownedWindow { #[inline] pub fn set_cursor(&self, cursor: MouseCursor) { *self.cursor.lock() = cursor; - if *self.cursor_state.lock() != CursorState::Hide { + if !*self.cursor_hidden.lock() { self.update_cursor(self.get_cursor(cursor)); } } @@ -1039,67 +1039,73 @@ impl UnownedWindow { } #[inline] - pub fn set_cursor_state(&self, state: CursorState) -> Result<(), String> { - use CursorState::*; - - let mut cursor_state_lock = self.cursor_state.lock(); - - match (state, *cursor_state_lock) { - (Normal, Normal) | (Hide, Hide) | (Grab, Grab) => return Ok(()), - _ => {}, + pub fn grab_cursor(&self, grab: bool) -> Result<(), String> { + let mut grabbed_lock = self.cursor_grabbed.lock(); + if grab == *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 *cursor_state_lock { - Grab => { - unsafe { - (self.xconn.xlib.XUngrabPointer)(self.xconn.display, ffi::CurrentTime); - self.xconn.flush_requests().expect("Failed to call XUngrabPointer"); - } - }, - Normal => {}, - Hide => self.update_cursor(self.get_cursor(*self.cursor.lock())), + 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| err.to_owned()) + } else { + self.xconn.flush_requests() + .map_err(|err| format!("Failed to call `XUngrabPointer`: {:?}", err)) + }; + if result.is_ok() { + *grabbed_lock = grab; } + result + } - match state { - Normal => { - *cursor_state_lock = state; - Ok(()) - }, - Hide => { - *cursor_state_lock = state; - self.update_cursor( - self.create_empty_cursor().expect("Failed to create empty cursor") - ); - Ok(()) - }, - Grab => { - unsafe { - // Ungrab before grabbing to prevent passive grabs - // from causing AlreadyGrabbed - (self.xconn.xlib.XUngrabPointer)(self.xconn.display, ffi::CurrentTime); - - match (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 - ) { - ffi::GrabSuccess => { - *cursor_state_lock = state; - Ok(()) - }, - ffi::AlreadyGrabbed | ffi::GrabInvalidTime | - ffi::GrabNotViewable | ffi::GrabFrozen - => Err("cursor could not be grabbed".to_string()), - _ => unreachable!(), - } - } - }, - } + #[inline] + pub fn hide_cursor(&self, hide: bool) { + let mut hidden_lock = self.cursor_hidden.lock(); + if hide == *hidden_lock {return; } + let cursor = if hide { + self.create_empty_cursor().expect("Failed to create empty cursor") + } else { + self.get_cursor(*self.cursor.lock()) + }; + *hidden_lock = hide; + drop(hidden_lock); + self.update_cursor(cursor); } #[inline] diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index 06a8f978..d41680b7 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -27,7 +27,6 @@ use objc::declare::ClassDecl; use { CreationError, - CursorState, Event, LogicalPosition, LogicalSize, @@ -527,6 +526,7 @@ pub struct Window2 { pub window: IdRef, pub delegate: WindowDelegate, pub input_context: IdRef, + cursor_hidden: Cell, } unsafe impl Send for Window2 {} @@ -674,6 +674,7 @@ impl Window2 { window: window, delegate: WindowDelegate::new(delegate_state), input_context, + cursor_hidden: Default::default(), }; // Set fullscreen mode after we setup everything @@ -980,7 +981,7 @@ impl Window2 { MouseCursor::SeResize | MouseCursor::SwResize | MouseCursor::NwseResize | MouseCursor::NeswResize | - MouseCursor::Cell | MouseCursor::NoneCursor | + MouseCursor::Cell | MouseCursor::Wait | MouseCursor::Progress | MouseCursor::Help | MouseCursor::Move | MouseCursor::AllScroll | MouseCursor::ZoomIn | MouseCursor::ZoomOut => "arrowCursor", @@ -994,25 +995,25 @@ impl Window2 { } } - pub fn set_cursor_state(&self, state: CursorState) -> Result<(), String> { - let cls = Class::get("NSCursor").unwrap(); + #[inline] + pub fn grab_cursor(&self, grab: bool) -> Result<(), String> { + // TODO: Do this for real https://stackoverflow.com/a/40922095/5435443 + CGDisplay::associate_mouse_and_mouse_cursor_position(!grab) + .map_err(|status| format!("Failed to grab cursor: `CGError` {:?}", status)) + } - // TODO: Check for errors. - match state { - CursorState::Normal => { - let _: () = unsafe { msg_send![cls, unhide] }; - let _ = CGDisplay::associate_mouse_and_mouse_cursor_position(true); - Ok(()) - }, - CursorState::Hide => { - let _: () = unsafe { msg_send![cls, hide] }; - Ok(()) - }, - CursorState::Grab => { - let _: () = unsafe { msg_send![cls, hide] }; - let _ = CGDisplay::associate_mouse_and_mouse_cursor_position(false); - Ok(()) + #[inline] + pub fn hide_cursor(&self, hide: bool) { + let cursor_class = Class::get("NSCursor").unwrap(); + // macOS uses a "hide counter" like Windows does, so we avoid incrementing it more than once. + // (otherwise, `hide_cursor(false)` would need to be called n times!) + if hide != self.cursor_hidden.get() { + if hide { + let _: () = unsafe { msg_send![cursor_class, hide] }; + } else { + let _: () = unsafe { msg_send![cursor_class, unhide] }; } + self.cursor_hidden.replace(hide); } } diff --git a/src/platform/windows/events_loop.rs b/src/platform/windows/events_loop.rs index 1f51ac1d..ca90c4db 100644 --- a/src/platform/windows/events_loop.rs +++ b/src/platform/windows/events_loop.rs @@ -40,7 +40,6 @@ use winapi::um::winnt::{LONG, LPCSTR, SHORT}; use { ControlFlow, - CursorState, Event, EventsLoopClosed, KeyboardInput, @@ -83,8 +82,8 @@ pub struct SavedWindowInfo { pub struct WindowState { /// Cursor to set at the next `WM_SETCURSOR` event received. pub cursor: Cursor, - /// Cursor state to set at the next `WM_SETCURSOR` event received. - pub cursor_state: CursorState, + pub cursor_grabbed: bool, + pub cursor_hidden: bool, /// Used by `WM_GETMINMAXINFO`. pub max_size: Option, pub min_size: Option, diff --git a/src/platform/windows/window.rs b/src/platform/windows/window.rs index 516ec933..06d0701b 100644 --- a/src/platform/windows/window.rs +++ b/src/platform/windows/window.rs @@ -17,7 +17,6 @@ use winapi::um::winnt::{LONG, LPCWSTR}; use { CreationError, - CursorState, Icon, LogicalPosition, LogicalSize, @@ -26,9 +25,9 @@ use { PhysicalSize, WindowAttributes, }; -use platform::platform::{Cursor, EventsLoop, PlatformSpecificWindowBuilderAttributes, WindowId}; +use platform::platform::{Cursor, PlatformSpecificWindowBuilderAttributes, WindowId}; use platform::platform::dpi::{BASE_DPI, dpi_to_scale_factor, get_window_dpi, get_window_scale_factor}; -use platform::platform::events_loop::{self, DESTROY_MSG_ID, INITIAL_DPI_MSG_ID}; +use platform::platform::events_loop::{self, DESTROY_MSG_ID, EventsLoop, INITIAL_DPI_MSG_ID}; use platform::platform::icon::{self, IconType, WinIcon}; use platform::platform::raw_input::register_all_mice_and_keyboards_for_raw_input; use platform::platform::util; @@ -336,7 +335,6 @@ impl Window { MouseCursor::Wait => winuser::IDC_WAIT, MouseCursor::Progress => winuser::IDC_APPSTARTING, MouseCursor::Help => winuser::IDC_HELP, - MouseCursor::NoneCursor => ptr::null(), _ => winuser::IDC_ARROW, // use arrow for the missing cases. }; @@ -363,83 +361,78 @@ impl Window { Ok(util::rect_eq(&client_rect, &clip_rect)) } - fn change_cursor_state( - window: &WindowWrapper, - current_state: CursorState, - state: CursorState, - ) -> Result { - match (current_state, state) { - (CursorState::Normal, CursorState::Normal) - | (CursorState::Hide, CursorState::Hide) - | (CursorState::Grab, CursorState::Grab) => (), // no-op - - (CursorState::Normal, CursorState::Hide) => unsafe { - winuser::ShowCursor(FALSE); - }, - - (CursorState::Grab, CursorState::Hide) => unsafe { - if winuser::ClipCursor(ptr::null()) == 0 { - return Err("`ClipCursor` failed".to_owned()); - } - }, - - (CursorState::Hide, CursorState::Normal) => unsafe { - winuser::ShowCursor(TRUE); - }, - - (CursorState::Normal, CursorState::Grab) - | (CursorState::Hide, CursorState::Grab) => unsafe { - let mut rect = mem::uninitialized(); - if winuser::GetClientRect(window.0, &mut rect) == 0 { - return Err("`GetClientRect` failed".to_owned()); - } - if winuser::ClientToScreen(window.0, &mut rect.left as *mut _ as LPPOINT) == 0 { - return Err("`ClientToScreen` (left, top) failed".to_owned()); - } - if winuser::ClientToScreen(window.0, &mut rect.right as *mut _ as LPPOINT) == 0 { - return Err("`ClientToScreen` (right, bottom) failed".to_owned()); - } - if winuser::ClipCursor(&rect) == 0 { - return Err("`ClipCursor` failed".to_owned()); - } - if current_state != CursorState::Hide { - winuser::ShowCursor(FALSE); - } - }, - - (CursorState::Grab, CursorState::Normal) => unsafe { - if winuser::ClipCursor(ptr::null()) == 0 { - return Err("`ClipCursor` failed".to_owned()); - } - winuser::ShowCursor(TRUE); - }, - }; - Ok(state) + pub(crate) unsafe fn grab_cursor_inner(window: &WindowWrapper, grab: bool) -> Result<(), String> { + if grab { + let mut rect = mem::uninitialized(); + if winuser::GetClientRect(window.0, &mut rect) == 0 { + return Err("`GetClientRect` failed".to_owned()); + } + // A `POINT` is two `LONG`s (x, y), and the `RECT` field after `left` is `top`. + if winuser::ClientToScreen(window.0, &mut rect.left as *mut _ as LPPOINT) == 0 { + return Err("`ClientToScreen` (left, top) failed".to_owned()); + } + if winuser::ClientToScreen(window.0, &mut rect.right as *mut _ as LPPOINT) == 0 { + return Err("`ClientToScreen` (right, bottom) failed".to_owned()); + } + if winuser::ClipCursor(&rect) == 0 { + return Err("`ClipCursor` failed".to_owned()); + } + } else { + if winuser::ClipCursor(ptr::null()) == 0 { + return Err("`ClipCursor` failed".to_owned()); + } + } + Ok(()) } - pub fn set_cursor_state(&self, state: CursorState) -> Result<(), String> { - let is_grabbed = unsafe { self.cursor_is_grabbed() }?; - let (tx, rx) = channel(); - let window = self.window.clone(); + #[inline] + pub fn grab_cursor(&self, grab: bool) -> Result<(), String> { + let currently_grabbed = unsafe { self.cursor_is_grabbed() }?; let window_state = Arc::clone(&self.window_state); + { + let window_state_lock = window_state.lock().unwrap(); + if currently_grabbed == grab + && grab == window_state_lock.cursor_grabbed { + return Ok(()); + } + } + let window = self.window.clone(); + let (tx, rx) = channel(); self.events_loop_proxy.execute_in_thread(move |_| { - let mut window_state_lock = window_state.lock().unwrap(); - // We should probably also check if the cursor is hidden, - // but `GetCursorInfo` isn't in winapi-rs yet, and it doesn't seem to matter as much. - let current_state = match window_state_lock.cursor_state { - CursorState::Normal if is_grabbed => CursorState::Grab, - CursorState::Grab if !is_grabbed => CursorState::Normal, - current_state => current_state, - }; - let result = Self::change_cursor_state(&window, current_state, state) - .map(|_| { - window_state_lock.cursor_state = state; - }); + let result = unsafe { Self::grab_cursor_inner(&window, grab) }; + if result.is_ok() { + window_state.lock().unwrap().cursor_grabbed = grab; + } let _ = tx.send(result); }); rx.recv().unwrap() } + pub(crate) unsafe fn hide_cursor_inner(hide: bool) { + if hide { + winuser::ShowCursor(FALSE); + } else { + winuser::ShowCursor(TRUE); + } + } + + #[inline] + pub fn hide_cursor(&self, hide: bool) { + let window_state = Arc::clone(&self.window_state); + { + let window_state_lock = window_state.lock().unwrap(); + // We don't want to increment/decrement the display count more than once! + if hide == window_state_lock.cursor_hidden { return; } + } + let (tx, rx) = channel(); + self.events_loop_proxy.execute_in_thread(move |_| { + unsafe { Self::hide_cursor_inner(hide) }; + window_state.lock().unwrap().cursor_hidden = hide; + let _ = tx.send(()); + }); + rx.recv().unwrap() + } + #[inline] pub fn get_hidpi_factor(&self) -> f64 { get_window_scale_factor(self.window.0, self.window.1) @@ -518,26 +511,28 @@ impl Window { } unsafe fn restore_saved_window(&self) { - let mut window_state = self.window_state.lock().unwrap(); + let (rect, mut style, ex_style) = { + let mut window_state_lock = self.window_state.lock().unwrap(); - // 'saved_window_info' can be None if the window has never been - // in fullscreen mode before this method gets called. - if window_state.saved_window_info.is_none() { - return; - } + // 'saved_window_info' can be None if the window has never been + // in fullscreen mode before this method gets called. + if window_state_lock.saved_window_info.is_none() { + return; + } - // Reset original window style and size. The multiple window size/moves - // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be - // repainted. Better-looking methods welcome. - { - let saved_window_info = window_state.saved_window_info.as_mut().unwrap(); + let saved_window_info = window_state_lock.saved_window_info.as_mut().unwrap(); + + // Reset original window style and size. The multiple window size/moves + // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be + // repainted. Better-looking methods welcome. saved_window_info.is_fullscreen = false; - } - let saved_window_info = window_state.saved_window_info.as_ref().unwrap(); - let rect = saved_window_info.rect.clone(); + let rect = saved_window_info.rect.clone(); + let (style, ex_style) = (saved_window_info.style, saved_window_info.ex_style); + (rect, style, ex_style) + }; let window = self.window.clone(); - let (mut style, ex_style) = (saved_window_info.style, saved_window_info.ex_style); + let window_state = Arc::clone(&self.window_state); let maximized = self.maximized.get(); let resizable = self.resizable.get(); @@ -545,6 +540,8 @@ impl Window { // We're restoring the window to its size and position from before being fullscreened. // `ShowWindow` resizes the window, so it must be called from the main thread. self.events_loop_proxy.execute_in_thread(move |_| { + let _ = Self::grab_cursor_inner(&window, false); + if resizable { style |= winuser::WS_SIZEBOX as LONG; } else { @@ -577,6 +574,9 @@ impl Window { ); mark_fullscreen(window.0, false); + + let window_state_lock = window_state.lock().unwrap(); + let _ = Self::grab_cursor_inner(&window, window_state_lock.cursor_grabbed); }); } @@ -588,10 +588,13 @@ impl Window { let (x, y): (i32, i32) = inner.get_position().into(); let (width, height): (u32, u32) = inner.get_dimensions().into(); let window = self.window.clone(); + let window_state = Arc::clone(&self.window_state); let (style, ex_style) = self.set_fullscreen_style(); self.events_loop_proxy.execute_in_thread(move |_| { + let _ = Self::grab_cursor_inner(&window, false); + winuser::SetWindowLongW( window.0, winuser::GWL_STYLE, @@ -622,6 +625,9 @@ impl Window { ); mark_fullscreen(window.0, true); + + let window_state_lock = window_state.lock().unwrap(); + let _ = Self::grab_cursor_inner(&window, window_state_lock.cursor_grabbed); }); } &None => { @@ -980,7 +986,8 @@ unsafe fn init( .map(|logical_size| PhysicalSize::from_logical(logical_size, dpi_factor)); let mut window_state = events_loop::WindowState { cursor: Cursor(winuser::IDC_ARROW), // use arrow by default - cursor_state: CursorState::Normal, + cursor_grabbed: false, + cursor_hidden: false, max_size, min_size, mouse_in_window: false, diff --git a/src/window.rs b/src/window.rs index f545f4ce..4ad34af6 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,7 +2,6 @@ use std::collections::vec_deque::IntoIter as VecDequeIter; use { CreationError, - CursorState, EventsLoop, Icon, LogicalPosition, @@ -327,12 +326,31 @@ impl Window { self.window.set_cursor_position(position) } - /// Sets how winit handles the cursor. See the documentation of `CursorState` for details. + /// Grabs the cursor, preventing it from leaving the window. /// - /// Has no effect on Android. + /// ## Platform-specific + /// + /// On macOS, this presently merely locks the cursor in a fixed location, which looks visually awkward. + /// + /// This has no effect on Android or iOS. #[inline] - pub fn set_cursor_state(&self, state: CursorState) -> Result<(), String> { - self.window.set_cursor_state(state) + pub fn grab_cursor(&self, grab: bool) -> Result<(), String> { + self.window.grab_cursor(grab) + } + + /// Hides the cursor, making it invisible but still usable. + /// + /// ## Platform-specific + /// + /// On Windows and X11, the cursor is only hidden within the confines of the window. + /// + /// On macOS, the cursor is hidden as long as the window has input focus, even if the cursor is outside of the + /// window. + /// + /// This has no effect on Android or iOS. + #[inline] + pub fn hide_cursor(&self, hide: bool) { + self.window.hide_cursor(hide) } /// Sets the window to maximized or back