diff --git a/CHANGELOG.md b/CHANGELOG.md index 5140ea6e..5f0b622b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - On Android, bump `ndk` and `ndk-glue` to 0.3: use predefined constants for event `ident`. - On Windows, fixed `WindowEvent::ThemeChanged` not properly firing and fixed `Window::theme` returning the wrong theme. - On Web, added support for `DeviceEvent::MouseMotion` to listen for relative mouse movements. +- Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland. # 0.24.0 (2020-12-09) diff --git a/Cargo.toml b/Cargo.toml index 64b1bee1..2db7219d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ features = [ [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] wayland-client = { version = "0.28", features = [ "dlopen"] , optional = true } -sctk = { package = "smithay-client-toolkit", version = "0.12", optional = true } +sctk = { package = "smithay-client-toolkit", version = "0.12.3", optional = true } mio = { version = "0.6", optional = true } mio-extras = { version = "2.0", optional = true } x11-dl = { version = "2.18.5", optional = true } diff --git a/FEATURES.md b/FEATURES.md index 4ebb6429..8bea2043 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -209,6 +209,7 @@ Legend: |Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❓ | |Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ | |Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❓ | +|Drag window with cursor |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** | ### Pending API Reworks Changes in the API that have been agreed upon but aren't implemented across all platforms. diff --git a/examples/drag_window.rs b/examples/drag_window.rs new file mode 100644 index 00000000..a408c7c7 --- /dev/null +++ b/examples/drag_window.rs @@ -0,0 +1,73 @@ +use simple_logger::SimpleLogger; +use winit::{ + event::{ + ElementState, Event, KeyboardInput, MouseButton, StartCause, VirtualKeyCode, WindowEvent, + }, + event_loop::{ControlFlow, EventLoop}, + window::{Window, WindowBuilder, WindowId}, +}; + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let window_1 = WindowBuilder::new().build(&event_loop).unwrap(); + let window_2 = WindowBuilder::new().build(&event_loop).unwrap(); + + let mut switched = false; + let mut entered_id = window_2.id(); + + event_loop.run(move |event, _, control_flow| match event { + Event::NewEvents(StartCause::Init) => { + eprintln!("Switch which window is to be dragged by pressing \"x\".") + } + Event::WindowEvent { event, window_id } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + .. + } => { + let window = if (window_id == window_1.id() && switched) + || (window_id == window_2.id() && !switched) + { + &window_2 + } else { + &window_1 + }; + + window.drag_window().unwrap() + } + WindowEvent::CursorEntered { .. } => { + entered_id = window_id; + name_windows(entered_id, switched, &window_1, &window_2) + } + WindowEvent::KeyboardInput { + input: + KeyboardInput { + state: ElementState::Released, + virtual_keycode: Some(VirtualKeyCode::X), + .. + }, + .. + } => { + switched = !switched; + name_windows(entered_id, switched, &window_1, &window_2); + println!("Switched!") + } + _ => (), + }, + _ => (), + }); +} + +fn name_windows(window_id: WindowId, switched: bool, window_1: &Window, window_2: &Window) { + let (drag_target, other) = + if (window_id == window_1.id() && switched) || (window_id == window_2.id() && !switched) { + (&window_2, &window_1) + } else { + (&window_1, &window_2) + }; + drag_target.set_title("drag target"); + other.set_title("winit window"); +} diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 0365a40e..543bca1b 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -556,6 +556,12 @@ impl Window { pub fn set_cursor_visible(&self, _: bool) {} + pub fn drag_window(&self) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + pub fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { let a_native_window = if let Some(native_window) = ndk_glue::native_window().as_ref() { unsafe { native_window.ptr().as_mut() as *mut _ as *mut _ } diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 7a3665a2..1f6e5591 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -182,6 +182,10 @@ impl Inner { debug!("`Window::set_cursor_visible` is ignored on iOS") } + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + pub fn set_minimized(&self, _minimized: bool) { warn!("`Window::set_minimized` is ignored on iOS") } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index e845b83b..480fc8af 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -358,6 +358,11 @@ impl Window { x11_or_wayland!(match self; Window(window) => window.set_cursor_visible(visible)) } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.drag_window()) + } + #[inline] pub fn scale_factor(&self) -> f64 { x11_or_wayland!(match self; Window(w) => w.scale_factor() as f64) diff --git a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs index 7d291713..62b21143 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs @@ -4,6 +4,7 @@ use std::cell::RefCell; use std::rc::Rc; use sctk::reexports::client::protocol::wl_pointer::{self, Event as PointerEvent}; +use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_v1::Event as RelativePointerEvent; use sctk::seat::pointer::ThemedPointer; @@ -28,6 +29,7 @@ pub(super) fn handle_pointer( event: PointerEvent, pointer_data: &Rc>, winit_state: &mut WinitState, + seat: WlSeat, ) { let event_sink = &mut winit_state.event_sink; let mut pointer_data = pointer_data.borrow_mut(); @@ -59,6 +61,7 @@ pub(super) fn handle_pointer( confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), pointer_constraints: pointer_data.pointer_constraints.clone(), latest_serial: pointer_data.latest_serial.clone(), + seat, }; window_handle.pointer_entered(winit_pointer); @@ -101,6 +104,7 @@ pub(super) fn handle_pointer( confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), pointer_constraints: pointer_data.pointer_constraints.clone(), latest_serial: pointer_data.latest_serial.clone(), + seat, }; window_handle.pointer_left(winit_pointer); diff --git a/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/src/platform_impl/linux/wayland/seat/pointer/mod.rs index 5debc8cb..7ae1f251 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/mod.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -13,6 +13,7 @@ use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_p use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; use sctk::seat::pointer::{ThemeManager, ThemedPointer}; +use sctk::window::{ConceptFrame, Window}; use crate::event::ModifiersState; use crate::platform_impl::wayland::event_loop::WinitState; @@ -35,6 +36,9 @@ pub struct WinitPointer { /// Latest observed serial in pointer events. latest_serial: Rc>, + + /// Seat. + seat: WlSeat, } impl PartialEq for WinitPointer { @@ -144,6 +148,10 @@ impl WinitPointer { confined_pointer.destroy(); } } + + pub fn drag_window(&self, window: &Window) { + window.start_interactive_move(&self.seat, self.latest_serial.get()); + } } /// A pointer wrapper for easy releasing and managing pointers. @@ -172,11 +180,18 @@ impl Pointers { pointer_constraints.clone(), modifiers_state, ))); + let pointer_seat = seat.detach(); let pointer = theme_manager.theme_pointer_with_impl( seat, move |event, pointer, mut dispatch_data| { let winit_state = dispatch_data.get::().unwrap(); - handlers::handle_pointer(pointer, event, &pointer_data, winit_state); + handlers::handle_pointer( + pointer, + event, + &pointer_data, + winit_state, + pointer_seat.clone(), + ); }, ); diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index d51f47a7..5f59651e 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -586,6 +586,18 @@ impl Window { Err(ExternalError::NotSupported(NotSupportedError::new())) } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + let drag_window_request = WindowRequest::DragWindow; + self.window_requests + .lock() + .unwrap() + .push(drag_window_request); + self.event_loop_awakener.ping(); + + Ok(()) + } + #[inline] pub fn set_ime_position(&self, position: Position) { let scale_factor = self.scale_factor() as f64; diff --git a/src/platform_impl/linux/wayland/window/shim.rs b/src/platform_impl/linux/wayland/window/shim.rs index 59cf921a..9397559f 100644 --- a/src/platform_impl/linux/wayland/window/shim.rs +++ b/src/platform_impl/linux/wayland/window/shim.rs @@ -34,6 +34,9 @@ pub enum WindowRequest { /// Grab cursor. GrabCursor(bool), + /// Drag window. + DragWindow, + /// Maximize the window. Maximize(bool), @@ -268,6 +271,12 @@ impl WindowHandle { pointer.set_cursor(Some(cursor_icon)); } } + + pub fn drag_window(&self) { + for pointer in self.pointers.iter() { + pointer.drag_window(&self.window); + } + } } #[inline] @@ -299,6 +308,9 @@ pub fn handle_window_requests(winit_state: &mut WinitState) { WindowRequest::GrabCursor(grab) => { window_handle.set_cursor_grab(grab); } + WindowRequest::DragWindow => { + window_handle.drag_window(); + } WindowRequest::Maximize(maximize) => { if maximize { window_handle.window.set_maximized(); diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 0fa64cf1..b4ca7c40 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1276,6 +1276,46 @@ impl UnownedWindow { self.set_cursor_position_physical(x, y) } + pub fn drag_window(&self) -> Result<(), ExternalError> { + let pointer = self + .xconn + .query_pointer(self.xwindow, util::VIRTUAL_CORE_POINTER) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))?; + + let window = self.inner_position().map_err(ExternalError::NotSupported)?; + + let message = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_MOVERESIZE\0") }; + + // 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(); + 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; + + // we keep the lock until we are done + self.xconn + .send_client_msg( + self.xwindow, + self.root, + message, + Some(ffi::SubstructureRedirectMask | ffi::SubstructureNotifyMask), + [ + (window.x as c_long + pointer.win_x as c_long), + (window.y as c_long + pointer.win_y as c_long), + 8, // _NET_WM_MOVERESIZE_MOVE + ffi::Button1 as c_long, + 1, + ], + ) + .flush() + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err)))) + } + pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) { let _ = self .ime_sender diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 82c1b6ae..b57fd5ff 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -636,6 +636,16 @@ impl UnownedWindow { Ok(()) } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + unsafe { + let event: id = msg_send![NSApp(), currentEvent]; + let _: () = msg_send![*self.ns_window, performWindowDragWithEvent: event]; + } + + Ok(()) + } + pub(crate) fn is_zoomed(&self) -> bool { // because `isZoomed` doesn't work if the window's borderless, // we make it resizable temporalily. diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 63e39481..a51210ca 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -222,6 +222,11 @@ impl Window { } } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + #[inline] pub fn set_minimized(&self, _minimized: bool) { // Intentionally a no-op, as canvases cannot be 'minimized' diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index cc04b8f7..775fa7b4 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -14,8 +14,8 @@ use std::{ use winapi::{ ctypes::c_int, shared::{ - minwindef::{HINSTANCE, UINT}, - windef::{HWND, POINT, RECT}, + minwindef::{HINSTANCE, LPARAM, UINT, WPARAM}, + windef::{HWND, POINT, POINTS, RECT}, }, um::{ combaseapi, dwmapi, @@ -26,7 +26,7 @@ use winapi::{ oleidl::LPDROPTARGET, shobjidl_core::{CLSID_TaskbarList, ITaskbarList2}, wingdi::{CreateRectRgn, DeleteObject}, - winnt::LPCWSTR, + winnt::{LPCWSTR, SHORT}, winuser, }, }; @@ -357,6 +357,30 @@ impl Window { Ok(()) } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + unsafe { + let points = { + let mut pos = mem::zeroed(); + winuser::GetCursorPos(&mut pos); + pos + }; + let points = POINTS { + x: points.x as SHORT, + y: points.y as SHORT, + }; + winuser::ReleaseCapture(); + winuser::PostMessageW( + self.window.0, + winuser::WM_NCLBUTTONDOWN, + winuser::HTCAPTION as WPARAM, + &points as *const _ as LPARAM, + ); + } + + Ok(()) + } + #[inline] pub fn id(&self) -> WindowId { WindowId(self.window.0) diff --git a/src/window.rs b/src/window.rs index 46da474a..fd7f295c 100644 --- a/src/window.rs +++ b/src/window.rs @@ -764,6 +764,22 @@ impl Window { pub fn set_cursor_visible(&self, visible: bool) { self.window.set_cursor_visible(visible) } + + /// Moves the window with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + /// + /// ## Platform-specific + /// + /// - **X11:** Un-grabs the cursor. + /// - **Wayland:** Requires the cursor to be inside the window to be dragged. + /// - **macOS:** May prevent the button release event to be triggered. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.window.drag_window() + } } /// Monitor info functions.