diff --git a/CHANGELOG.md b/CHANGELOG.md index f84edf04..bc628208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ And please only add new entries to the top of this list, right below the `# Unre - On Wayland, `RedrawRequested` not emitted during resize. - **Breaking:** Remove the unstable `xlib_xconnection()` function from the private interface. - Added Orbital support for Redox OS +- On X11, added `drag_resize_window` method. # 0.27.5 diff --git a/FEATURES.md b/FEATURES.md index 285332af..96e8c4a5 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -215,6 +215,7 @@ Legend: |Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ |**N/A** | |Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❓ |**N/A** | |Drag window with cursor |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** |**N/A** | +|Resize with cursor |❌ |❌ |✔️ |❌ |**N/A**|**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/window_drag_resize.rs b/examples/window_drag_resize.rs new file mode 100644 index 00000000..95a67377 --- /dev/null +++ b/examples/window_drag_resize.rs @@ -0,0 +1,141 @@ +//! Demonstrates capability to create in-app draggable regions for client-side decoration support. + +use simple_logger::SimpleLogger; +use winit::{ + event::{ + ElementState, Event, KeyboardInput, MouseButton, StartCause, VirtualKeyCode, WindowEvent, + }, + event_loop::{ControlFlow, EventLoop}, + window::{CursorIcon, ResizeDirection, WindowBuilder}, +}; + +const BORDER: f64 = 8.0; + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_inner_size(winit::dpi::LogicalSize::new(600.0, 400.0)) + .with_min_inner_size(winit::dpi::LogicalSize::new(400.0, 200.0)) + .with_decorations(false) + .build(&event_loop) + .unwrap(); + + let mut border = false; + let mut cursor_location = None; + + event_loop.run(move |event, _, control_flow| match event { + Event::NewEvents(StartCause::Init) => { + eprintln!("Press 'B' to toggle borderless") + } + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::CursorMoved { position, .. } => { + if !window.is_decorated() { + let new_location = + cursor_resize_direction(window.inner_size(), position, BORDER); + + if new_location != cursor_location { + cursor_location = new_location; + window.set_cursor_icon(cursor_direction_icon(cursor_location)) + } + } + } + + WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + .. + } => { + if let Some(dir) = cursor_location { + let _res = window.drag_resize_window(dir); + } + } + WindowEvent::KeyboardInput { + input: + KeyboardInput { + state: ElementState::Released, + virtual_keycode: Some(VirtualKeyCode::B), + .. + }, + .. + } => { + border = !border; + window.set_decorations(border); + } + _ => (), + }, + _ => (), + }); +} + +fn cursor_direction_icon(resize_direction: Option) -> CursorIcon { + match resize_direction { + Some(resize_direction) => match resize_direction { + ResizeDirection::East => CursorIcon::EResize, + ResizeDirection::North => CursorIcon::NResize, + ResizeDirection::NorthEast => CursorIcon::NeResize, + ResizeDirection::NorthWest => CursorIcon::NwResize, + ResizeDirection::South => CursorIcon::SResize, + ResizeDirection::SouthEast => CursorIcon::SeResize, + ResizeDirection::SouthWest => CursorIcon::SwResize, + ResizeDirection::West => CursorIcon::WResize, + }, + None => CursorIcon::Default, + } +} + +fn cursor_resize_direction( + win_size: winit::dpi::PhysicalSize, + position: winit::dpi::PhysicalPosition, + border_size: f64, +) -> Option { + enum XDirection { + West, + East, + Default, + } + + enum YDirection { + North, + South, + Default, + } + + let xdir = if position.x < border_size { + XDirection::West + } else if position.x > (win_size.width as f64 - border_size) { + XDirection::East + } else { + XDirection::Default + }; + + let ydir = if position.y < border_size { + YDirection::North + } else if position.y > (win_size.height as f64 - border_size) { + YDirection::South + } else { + YDirection::Default + }; + + Some(match xdir { + XDirection::West => match ydir { + YDirection::North => ResizeDirection::NorthWest, + YDirection::South => ResizeDirection::SouthWest, + YDirection::Default => ResizeDirection::West, + }, + + XDirection::East => match ydir { + YDirection::North => ResizeDirection::NorthEast, + YDirection::South => ResizeDirection::SouthEast, + YDirection::Default => ResizeDirection::East, + }, + + XDirection::Default => match ydir { + YDirection::North => ResizeDirection::North, + YDirection::South => ResizeDirection::South, + YDirection::Default => return None, + }, + }) +} diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 4e903a96..6e1ce364 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -24,7 +24,7 @@ use crate::{ error, event::{self, StartCause, VirtualKeyCode}, event_loop::{self, ControlFlow, EventLoopWindowTarget as RootELW}, - window::{self, CursorGrabMode, Theme, WindowButtons, WindowLevel}, + window::{self, CursorGrabMode, ResizeDirection, Theme, WindowButtons, WindowLevel}, }; fn ndk_keycode_to_virtualkeycode(keycode: Keycode) -> Option { @@ -1021,6 +1021,15 @@ impl Window { )) } + pub fn drag_resize_window( + &self, + _direction: ResizeDirection, + ) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + pub fn set_cursor_hittest(&self, _hittest: bool) -> 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 466dacb6..8435faeb 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -26,8 +26,8 @@ use crate::{ monitor, EventLoopWindowTarget, Fullscreen, MonitorHandle, }, window::{ - CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons, - WindowId as RootWindowId, WindowLevel, + CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowId as RootWindowId, WindowLevel, }, }; @@ -200,6 +200,10 @@ impl Inner { Err(ExternalError::NotSupported(NotSupportedError::new())) } + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 70bbb6f4..8ddc9a7a 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -34,8 +34,8 @@ use crate::{ }, icon::Icon, window::{ - CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons, - WindowLevel, + CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowLevel, }, }; @@ -424,6 +424,11 @@ impl Window { x11_or_wayland!(match self; Window(window) => window.drag_window()) } + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.drag_resize_window(direction)) + } + #[inline] pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { x11_or_wayland!(match self; Window(w) => w.set_cursor_hittest(hittest)) diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 9ddfd9e9..7b00027d 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -19,7 +19,8 @@ use crate::platform_impl::{ PlatformSpecificWindowBuilderAttributes as PlatformAttributes, }; use crate::window::{ - CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons, + CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, }; use super::env::WindowingFeatures; @@ -560,6 +561,11 @@ impl Window { Ok(()) } + #[inline] + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + #[inline] pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { self.send_request(WindowRequest::PassthroughMouseInput(!hittest)); diff --git a/src/platform_impl/linux/x11/util/wm.rs b/src/platform_impl/linux/x11/util/wm.rs index 368a059c..03890fac 100644 --- a/src/platform_impl/linux/x11/util/wm.rs +++ b/src/platform_impl/linux/x11/util/wm.rs @@ -4,6 +4,17 @@ use once_cell::sync::Lazy; use super::*; +// https://specifications.freedesktop.org/wm-spec/latest/ar01s04.html#idm46075117309248 +pub const MOVERESIZE_TOPLEFT: isize = 0; +pub const MOVERESIZE_TOP: isize = 1; +pub const MOVERESIZE_TOPRIGHT: isize = 2; +pub const MOVERESIZE_RIGHT: isize = 3; +pub const MOVERESIZE_BOTTOMRIGHT: isize = 4; +pub const MOVERESIZE_BOTTOM: isize = 5; +pub const MOVERESIZE_BOTTOMLEFT: isize = 6; +pub const MOVERESIZE_LEFT: isize = 7; +pub const MOVERESIZE_MOVE: isize = 8; + // This info is global to the window manager. static SUPPORTED_HINTS: Lazy>> = Lazy::new(|| Mutex::new(Vec::with_capacity(0))); diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 2c9926f6..f0a8bf46 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -21,8 +21,8 @@ use crate::{ PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode, }, window::{ - CursorGrabMode, CursorIcon, Icon, Theme, UserAttentionType, WindowAttributes, - WindowButtons, WindowLevel, + CursorGrabMode, CursorIcon, Icon, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, }, }; @@ -1433,7 +1433,27 @@ impl UnownedWindow { Err(ExternalError::NotSupported(NotSupportedError::new())) } + /// Moves the window while it is being dragged. pub fn drag_window(&self) -> Result<(), ExternalError> { + self.drag_initiate(util::MOVERESIZE_MOVE) + } + + /// Resizes the window while it is being dragged. + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + self.drag_initiate(match direction { + ResizeDirection::East => util::MOVERESIZE_RIGHT, + ResizeDirection::North => util::MOVERESIZE_TOP, + ResizeDirection::NorthEast => util::MOVERESIZE_TOPRIGHT, + ResizeDirection::NorthWest => util::MOVERESIZE_TOPLEFT, + ResizeDirection::South => util::MOVERESIZE_BOTTOM, + ResizeDirection::SouthEast => util::MOVERESIZE_BOTTOMRIGHT, + ResizeDirection::SouthWest => util::MOVERESIZE_BOTTOMLEFT, + ResizeDirection::West => util::MOVERESIZE_LEFT, + }) + } + + /// Initiates a drag operation while the left mouse button is pressed. + fn drag_initiate(&self, action: isize) -> Result<(), ExternalError> { let pointer = self .xconn .query_pointer(self.xwindow, util::VIRTUAL_CORE_POINTER) @@ -1464,7 +1484,7 @@ impl UnownedWindow { [ (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 + action.try_into().unwrap(), ffi::Button1 as c_long, 1, ], diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 57216eb8..042c52a9 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -32,8 +32,8 @@ use crate::{ Fullscreen, OsError, }, window::{ - CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons, - WindowId as RootWindowId, WindowLevel, + CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowId as RootWindowId, WindowLevel, }, }; use core_graphics::display::{CGDisplay, CGPoint}; @@ -784,6 +784,11 @@ impl WinitWindow { Ok(()) } + #[inline] + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + #[inline] pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { util::set_ignore_mouse_events_sync(self, !hittest); diff --git a/src/platform_impl/orbital/window.rs b/src/platform_impl/orbital/window.rs index 40ed39d1..f08f6d23 100644 --- a/src/platform_impl/orbital/window.rs +++ b/src/platform_impl/orbital/window.rs @@ -348,6 +348,16 @@ impl Window { )) } + #[inline] + pub fn drag_resize_window( + &self, + _direction: window::ResizeDirection, + ) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + #[inline] pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), error::ExternalError> { Err(error::ExternalError::NotSupported( diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 7f39f608..4d335c94 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -3,8 +3,8 @@ use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; use crate::event; use crate::icon::Icon; use crate::window::{ - CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons, - WindowId as RootWI, WindowLevel, + CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowId as RootWI, WindowLevel, }; use raw_window_handle::{RawDisplayHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle}; @@ -267,6 +267,11 @@ impl Window { Err(ExternalError::NotSupported(NotSupportedError::new())) } + #[inline] + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + #[inline] pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index a93d97be..9cf6278c 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -72,8 +72,8 @@ use crate::{ Fullscreen, PlatformSpecificWindowBuilderAttributes, WindowId, }, window::{ - CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons, - WindowLevel, + CursorGrabMode, CursorIcon, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowLevel, }, }; @@ -432,6 +432,11 @@ impl Window { Ok(()) } + #[inline] + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + #[inline] pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { let window = self.window.clone(); diff --git a/src/window.rs b/src/window.rs index 3c7f4797..1c8b2db9 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1159,6 +1159,19 @@ impl Window { self.window.drag_window() } + /// Resizes 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 + /// + /// Only X11 is supported at this time. + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + self.window.drag_resize_window(direction) + } + /// Modifies whether the window catches cursor events. /// /// If `true`, the window will catch the cursor events. If `false`, events are passed through @@ -1353,6 +1366,35 @@ impl Default for CursorIcon { } } +/// Defines the orientation that a window resize will be performed. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum ResizeDirection { + East, + North, + NorthEast, + NorthWest, + South, + SouthEast, + SouthWest, + West, +} + +impl From for CursorIcon { + fn from(direction: ResizeDirection) -> Self { + use ResizeDirection::*; + match direction { + East => CursorIcon::EResize, + North => CursorIcon::NResize, + NorthEast => CursorIcon::NeResize, + NorthWest => CursorIcon::NwResize, + South => CursorIcon::SResize, + SouthEast => CursorIcon::SeResize, + SouthWest => CursorIcon::SwResize, + West => CursorIcon::WResize, + } + } +} + /// Fullscreen modes. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Fullscreen {