From 6bb43fd130a4fa37a2335ac5ba5858bcc99756fa Mon Sep 17 00:00:00 2001 From: Pedro Macedo Date: Sun, 1 Mar 2026 10:28:35 -0300 Subject: [PATCH] wayland: implement resize increments --- src/changelog/unreleased.md | 4 ++ src/platform_impl/linux/wayland/window/mod.rs | 19 +++++-- .../linux/wayland/window/state.rs | 50 +++++++++++++++++++ src/window.rs | 3 +- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 5ed7d6bb5..e70657fa7 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -40,6 +40,10 @@ changelog entry. ## Unreleased +### Added + +- On Wayland, add `Window::set_resize_increments`. + ### Fixed - On macOS, fixed crash when dragging non-file content onto window. diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 047affeb0..6d29a5a5f 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -166,6 +166,12 @@ impl Window { Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor), } + // Apply resize increments. + if let Some(increments) = attributes.resize_increments { + let increments = increments.to_logical(window_state.scale_factor()); + window_state.set_resize_increments(Some(increments)); + } + // Activate the window when the token is passed. if let (Some(xdg_activation), Some(token)) = (xdg_activation.as_ref(), attributes.platform_specific.activation_token) @@ -333,12 +339,19 @@ impl Window { #[inline] pub fn resize_increments(&self) -> Option> { - None + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + window_state + .resize_increments() + .map(|size| super::logical_to_physical_rounded(size, scale_factor)) } #[inline] - pub fn set_resize_increments(&self, _increments: Option) { - warn!("`set_resize_increments` is not implemented for Wayland"); + pub fn set_resize_increments(&self, increments: Option) { + let mut window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + let increments = increments.map(|size| size.to_logical(scale_factor)); + window_state.set_resize_increments(increments); } #[inline] diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index 846dc44c2..246ba3a70 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -127,6 +127,7 @@ pub struct WindowState { /// Min size. min_inner_size: LogicalSize, max_inner_size: Option>, + resize_increments: Option>, /// The size of the window when no states were applied to it. The primary use for it /// is to fallback to original window size, before it was maximized, if the compositor @@ -202,6 +203,7 @@ impl WindowState { last_configure: None, max_inner_size: None, min_inner_size: MIN_WINDOW_SIZE, + resize_increments: None, pointer_constraints, pointers: Default::default(), queue_handle: queue_handle.clone(), @@ -340,6 +342,42 @@ impl WindowState { .unwrap_or(new_size.height); } + // Apply size increments. + // + // We conditionally apply increments to avoid conflicts with the compositor's layout rules: + // 1. If the window is floating (constrain == true), we snap to increments to ensure the + // app's grid alignment. + // 2. If the user is interactively resizing (is_resizing), we snap the size to provide + // feedback. + // + // However, we MUST NOT snap if the compositor enforces a specific size (constrain == false, + // or states like Maximized/Tiled). Snapping in these cases (e.g. corner tiling) would + // shrink the window below the allocated area, creating visible gaps between valid + // windows or screen edges. + if (constrain || configure.is_resizing()) + && !configure.is_maximized() + && !configure.is_fullscreen() + && !configure.is_tiled() + { + if let Some(increments) = self.resize_increments { + // We use min size as a base size for the increments, similar to how X11 does it. + // + // This ensures that we can always reach the min size and the increments are + // calculated from it. + let (delta_width, delta_height) = ( + new_size.width.saturating_sub(self.min_inner_size.width), + new_size.height.saturating_sub(self.min_inner_size.height), + ); + + let width = self.min_inner_size.width + + (delta_width / increments.width) * increments.width; + let height = self.min_inner_size.height + + (delta_height / increments.height) * increments.height; + + new_size = (width, height).into(); + } + } + let new_state = configure.state; let old_state = self.last_configure.as_ref().map(|configure| configure.state); @@ -725,6 +763,18 @@ impl WindowState { self.selected_cursor = SelectedCursor::Custom(cursor); } + /// Set the resize increments of the window. + pub fn set_resize_increments(&mut self, increments: Option>) { + self.resize_increments = increments; + // NOTE: We don't update the window size here, because it will be done on the next resize + // or configure event. + } + + /// Get the resize increments of the window. + pub fn resize_increments(&self) -> Option> { + self.resize_increments + } + fn apply_custom_cursor(&self, cursor: &CustomCursor) { self.apply_on_pointer(|pointer, data| { let surface = pointer.surface(); diff --git a/src/window.rs b/src/window.rs index 0db22bb32..90848d334 100644 --- a/src/window.rs +++ b/src/window.rs @@ -884,7 +884,7 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS / Android / Web / Wayland / Orbital:** Always returns [`None`]. + /// - **iOS / Android / Web / Orbital:** Always returns [`None`]. #[inline] pub fn resize_increments(&self) -> Option> { let _span = tracing::debug_span!("winit::Window::resize_increments",).entered(); @@ -900,7 +900,6 @@ impl Window { /// /// - **macOS:** Increments are converted to logical size and then macOS rounds them to whole /// numbers. - /// - **Wayland:** Not implemented. /// - **iOS / Android / Web / Orbital:** Unsupported. #[inline] pub fn set_resize_increments>(&self, increments: Option) {