From c090cf4b83106bba4eaf182ac4e693e0601de549 Mon Sep 17 00:00:00 2001 From: Nicholas Allam Date: Tue, 22 Sep 2026 07:15:07 +0100 Subject: [PATCH 1/5] wayland: preserve requested minimum and maximum surface sizes `set_min_surface_size` and `set_max_surface_size` stored the sizes with the decoration borders added, so `reload_min_max_hints` added the borders a second time. `set_resizable(false)` also overwrote the stored sizes with the current size, so the window stayed fixed at that size after `set_resizable(true)`. Store the sizes as requested, as X11 does, and let `reload_min_max_hints` alone build the hints, adding the borders and fixing both hints to the current size while not resizable. Unlike X11, a limit set while not resizable is now only sent once the window is resizable again. Sending it straight away would replace one of the two fixed hints, either letting the window resize or putting the minimum above the maximum. The stored minimum is no longer clamped to `MIN_WINDOW_SIZE`, so clamp the size snapped to resize increments instead. --- winit-wayland/src/window/state.rs | 66 +++++++++------------ winit-wayland/src/window/state/configure.rs | 5 +- winit/src/changelog/unreleased.md | 1 + 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 78f2de75cd..a1a05978ed 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -372,13 +372,7 @@ impl WindowState { } self.resizable = resizable; - if resizable { - // Restore min/max sizes of the window. - self.reload_min_max_hints(); - } else { - self.set_min_surface_size(Some(self.size)); - self.set_max_surface_size(Some(self.size)); - } + self.reload_min_max_hints(); // Reload the state on the frame as well. if let Some(frame) = self.frame.as_mut() { @@ -571,40 +565,16 @@ impl WindowState { self.resize_increments } - /// Set maximum inner window size. + /// Set minimum inner window size. pub fn set_min_surface_size(&mut self, size: Option>) { - let Some(xdg_toplevel) = self.window.xdg_toplevel() else { return }; - - // Ensure that the window has the right minimum size. - let mut size = size.unwrap_or(MIN_WINDOW_SIZE); - size.width = size.width.max(MIN_WINDOW_SIZE.width); - size.height = size.height.max(MIN_WINDOW_SIZE.height); - - // Add the borders. - let size = self - .frame - .as_ref() - .map(|frame| frame.add_borders(size.width, size.height).into()) - .unwrap_or(size); - - self.min_surface_size = size; - xdg_toplevel.set_min_size(size.width as _, size.height as _); + self.min_surface_size = size.unwrap_or(MIN_WINDOW_SIZE); + self.reload_min_max_hints(); } /// Set maximum inner window size. pub fn set_max_surface_size(&mut self, size: Option>) { - let Some(xdg_toplevel) = self.window.xdg_toplevel() else { return }; - - let size = size.map(|size| { - self.frame - .as_ref() - .map(|frame| frame.add_borders(size.width, size.height).into()) - .unwrap_or(size) - }); - self.max_surface_size = size; - let size = size.unwrap_or_default(); - xdg_toplevel.set_max_size(size.width as _, size.height as _); + self.reload_min_max_hints(); } /// Set the CSD theme. @@ -624,8 +594,30 @@ impl WindowState { /// Reload the hints for minimum and maximum sizes. pub fn reload_min_max_hints(&mut self) { - self.set_min_surface_size(Some(self.min_surface_size)); - self.set_max_surface_size(self.max_surface_size); + let Some(xdg_toplevel) = self.window.xdg_toplevel() else { return }; + + let (mut min, max) = if self.resizable { + (self.min_surface_size, self.max_surface_size) + } else { + (self.size, Some(self.size)) + }; + + // Ensure that the window has the right minimum size. + min.width = min.width.max(MIN_WINDOW_SIZE.width); + min.height = min.height.max(MIN_WINDOW_SIZE.height); + + // Add the borders. + let add_borders = |size: LogicalSize| { + self.frame + .as_ref() + .map(|frame| frame.add_borders(size.width, size.height).into()) + .unwrap_or(size) + }; + + let min = add_borders(min); + xdg_toplevel.set_min_size(min.width as _, min.height as _); + let max = max.map(add_borders).unwrap_or_default(); + xdg_toplevel.set_max_size(max.width as _, max.height as _); } pub fn show_window_menu(&self, position: LogicalPosition) { diff --git a/winit-wayland/src/window/state/configure.rs b/winit-wayland/src/window/state/configure.rs index 61d4593944..3d57301317 100644 --- a/winit-wayland/src/window/state/configure.rs +++ b/winit-wayland/src/window/state/configure.rs @@ -10,7 +10,7 @@ use tracing::warn; #[cfg(feature = "sctk-adwaita")] use super::create_sctk_adwaita_config; -use super::{WindowState, WindowType, WinitFrame}; +use super::{MIN_WINDOW_SIZE, WindowState, WindowType, WinitFrame}; impl WindowState { pub fn configure_popup(&mut self, configure: PopupConfigure) -> bool { @@ -171,7 +171,8 @@ impl WindowState { let height = self.min_surface_size.height + (delta_height / increments.height) * increments.height; - new_size = (width, height).into(); + new_size = + (width.max(MIN_WINDOW_SIZE.width), height.max(MIN_WINDOW_SIZE.height)).into(); } } diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index a0e7d3a474..5e2824294c 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -45,6 +45,7 @@ changelog entry. - On Windows, fix a crash occurring when trying to create a DXGI surface on a window that is both fullscreen and always on top. - On Wayland, use the touch serial for `Window::drag_window`, `Window::drag_resize_window` and `Window::show_window_menu` when the interaction was started by touch. - On Windows, fix a crash when moving a window that owns windows not created by winit, such as native dialogs or application helper windows. +- On Wayland, fix minimum and maximum size hints being lost when toggling `Window::set_resizable`. ### Changed From d1540ceed346b8d453a180ac9d034a602318efce Mon Sep 17 00:00:00 2001 From: Nicholas Allam Date: Tue, 22 Sep 2026 07:16:53 +0100 Subject: [PATCH 2/5] wayland: update size hints when the decoration borders change The hints were sent before the frame existed and never re-sent, so they left out the titlebar and went stale when fullscreen or decorations changed the borders. Reload them on every resize. A non-resizable window is pinned to its stateless size, so maximizing it does not move the pin. Changing the decoration mode now also resizes, so the window geometry and hints follow the frame being added or removed. --- winit-wayland/src/window/state.rs | 4 +++- winit-wayland/src/window/state/configure.rs | 8 +++++++- winit/src/changelog/unreleased.md | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index a1a05978ed..8cdc2906e1 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -492,6 +492,8 @@ impl WindowState { // the redraw scheduling is done on the caller side. let _ = self.set_blur(true); } + + self.reload_min_max_hints(); } pub(crate) fn set_maximized(&self, maximized: bool) { @@ -599,7 +601,7 @@ impl WindowState { let (mut min, max) = if self.resizable { (self.min_surface_size, self.max_surface_size) } else { - (self.size, Some(self.size)) + (self.stateless_size, Some(self.stateless_size)) }; // Ensure that the window has the right minimum size. diff --git a/winit-wayland/src/window/state/configure.rs b/winit-wayland/src/window/state/configure.rs index 3d57301317..d656fb16cb 100644 --- a/winit-wayland/src/window/state/configure.rs +++ b/winit-wayland/src/window/state/configure.rs @@ -190,6 +190,9 @@ impl WindowState { let new_state = configure.state; if let WindowType::Window { last_configure, .. } = &mut self.window { let old_state = last_configure.as_ref().map(|configure| configure.state); + let decoration_mode_changed = last_configure + .as_ref() + .is_some_and(|last| last.decoration_mode != configure.decoration_mode); let state_change_requires_resize = old_state .map(|old_state| { @@ -204,7 +207,10 @@ impl WindowState { // NOTE: Set the configure before doing a resize, since we query it during it. *last_configure = Some(configure); - if state_change_requires_resize || new_size != self.surface_size() { + if state_change_requires_resize + || new_size != self.surface_size() + || decoration_mode_changed + { self.resize(new_size); true } else { diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index 5e2824294c..68c4bac02e 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -45,7 +45,7 @@ changelog entry. - On Windows, fix a crash occurring when trying to create a DXGI surface on a window that is both fullscreen and always on top. - On Wayland, use the touch serial for `Window::drag_window`, `Window::drag_resize_window` and `Window::show_window_menu` when the interaction was started by touch. - On Windows, fix a crash when moving a window that owns windows not created by winit, such as native dialogs or application helper windows. -- On Wayland, fix minimum and maximum size hints being lost when toggling `Window::set_resizable`. +- On Wayland, fix minimum and maximum size hints being off by the client-side decoration borders, and being lost when toggling `Window::set_resizable`. ### Changed From 588de9f2476dc3a42336f0d7839b5ee290dc515c Mon Sep 17 00:00:00 2001 From: Nicholas Allam Date: Tue, 22 Sep 2026 07:16:53 +0100 Subject: [PATCH 3/5] wayland: align resize increments to the requested minimum Increments were aligned from the internal `2x1` minimum, so an aligned `w x h` with increments `a x b` snapped to `(w - a + 2) x (h - b + 1)`. Align from the requested minimum, or from zero, and round a size below one increment up to the first aligned size. --- winit-wayland/src/window/state.rs | 4 ++-- winit-wayland/src/window/state/configure.rs | 20 ++++++++++---------- winit/src/changelog/unreleased.md | 1 + 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 8cdc2906e1..1ea8e55076 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -232,7 +232,7 @@ impl WindowState { has_pending_move: None, text_input_state: None, max_surface_size: None, - min_surface_size: MIN_WINDOW_SIZE, + min_surface_size: LogicalSize::new(0, 0), resize_increments: None, pointer_constraints, pointers: Default::default(), @@ -569,7 +569,7 @@ impl WindowState { /// Set minimum inner window size. pub fn set_min_surface_size(&mut self, size: Option>) { - self.min_surface_size = size.unwrap_or(MIN_WINDOW_SIZE); + self.min_surface_size = size.unwrap_or_default(); self.reload_min_max_hints(); } diff --git a/winit-wayland/src/window/state/configure.rs b/winit-wayland/src/window/state/configure.rs index d656fb16cb..cd2c90eaa2 100644 --- a/winit-wayland/src/window/state/configure.rs +++ b/winit-wayland/src/window/state/configure.rs @@ -161,18 +161,18 @@ impl WindowState { // // 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_surface_size.width), - new_size.height.saturating_sub(self.min_surface_size.height), + let snap = |size: u32, min: u32, increment: u32, floor: u32| { + let steps = size.saturating_sub(min) / increment; + let floor_steps = floor.saturating_sub(min).div_ceil(increment); + min + steps.max(floor_steps) * increment + }; + let min = self.min_surface_size; + let (width, height) = ( + snap(new_size.width, min.width, increments.width, MIN_WINDOW_SIZE.width), + snap(new_size.height, min.height, increments.height, MIN_WINDOW_SIZE.height), ); - let width = self.min_surface_size.width - + (delta_width / increments.width) * increments.width; - let height = self.min_surface_size.height - + (delta_height / increments.height) * increments.height; - - new_size = - (width.max(MIN_WINDOW_SIZE.width), height.max(MIN_WINDOW_SIZE.height)).into(); + new_size = (width, height).into(); } } diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index 68c4bac02e..f96de64035 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -46,6 +46,7 @@ changelog entry. - On Wayland, use the touch serial for `Window::drag_window`, `Window::drag_resize_window` and `Window::show_window_menu` when the interaction was started by touch. - On Windows, fix a crash when moving a window that owns windows not created by winit, such as native dialogs or application helper windows. - On Wayland, fix minimum and maximum size hints being off by the client-side decoration borders, and being lost when toggling `Window::set_resizable`. +- On Wayland, with resize increments: align from the requested minimum size instead of an internal `2x1`, and round a surface smaller than one increment up to one. ### Changed From df5282108603706644700a23c503050b17fa8f45 Mon Sep 17 00:00:00 2001 From: Nicholas Allam Date: Tue, 22 Sep 2026 07:17:59 +0100 Subject: [PATCH 4/5] wayland: keep resize increments aligned when a resize ends The configure that clears `RESIZING` was not snapped, so releasing the mouse returned the window to the unaligned pointer-derived size. Also snap the configure that follows a resize. --- winit-wayland/src/window/state/configure.rs | 6 +++++- winit/src/changelog/unreleased.md | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/winit-wayland/src/window/state/configure.rs b/winit-wayland/src/window/state/configure.rs index cd2c90eaa2..608e215fa9 100644 --- a/winit-wayland/src/window/state/configure.rs +++ b/winit-wayland/src/window/state/configure.rs @@ -151,7 +151,11 @@ impl WindowState { // 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()) + let was_resizing = matches!( + &self.window, + WindowType::Window { last_configure: Some(last), .. } if last.is_resizing() + ); + if (constrain || configure.is_resizing() || was_resizing) && !configure.is_maximized() && !configure.is_fullscreen() && !configure.is_tiled() diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index f96de64035..a171e2b026 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -46,7 +46,7 @@ changelog entry. - On Wayland, use the touch serial for `Window::drag_window`, `Window::drag_resize_window` and `Window::show_window_menu` when the interaction was started by touch. - On Windows, fix a crash when moving a window that owns windows not created by winit, such as native dialogs or application helper windows. - On Wayland, fix minimum and maximum size hints being off by the client-side decoration borders, and being lost when toggling `Window::set_resizable`. -- On Wayland, with resize increments: align from the requested minimum size instead of an internal `2x1`, and round a surface smaller than one increment up to one. +- On Wayland, with resize increments: align from the requested minimum size instead of an internal `2x1`, round a surface smaller than one increment up to one, and keep the alignment when an interactive resize ends. ### Changed From 3031f0d300bd072da42f3a5552042ec27161c763 Mon Sep 17 00:00:00 2001 From: Nicholas Allam Date: Sun, 13 Sep 2026 10:34:10 +0100 Subject: [PATCH 5/5] wayland: ignore zero resize increments An increment of `0` panicked with a division by zero on the first snapped configure. Treat it as `1`, which constrains nothing. --- winit-wayland/src/window/state/configure.rs | 2 ++ winit/src/changelog/unreleased.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/winit-wayland/src/window/state/configure.rs b/winit-wayland/src/window/state/configure.rs index 608e215fa9..6404b074e7 100644 --- a/winit-wayland/src/window/state/configure.rs +++ b/winit-wayland/src/window/state/configure.rs @@ -165,6 +165,8 @@ impl WindowState { // // This ensures that we can always reach the min size and the increments are // calculated from it. + let increments = + LogicalSize::new(increments.width.max(1), increments.height.max(1)); let snap = |size: u32, min: u32, increment: u32, floor: u32| { let steps = size.saturating_sub(min) / increment; let floor_steps = floor.saturating_sub(min).div_ceil(increment); diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index a171e2b026..79b025d8a2 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -46,7 +46,7 @@ changelog entry. - On Wayland, use the touch serial for `Window::drag_window`, `Window::drag_resize_window` and `Window::show_window_menu` when the interaction was started by touch. - On Windows, fix a crash when moving a window that owns windows not created by winit, such as native dialogs or application helper windows. - On Wayland, fix minimum and maximum size hints being off by the client-side decoration borders, and being lost when toggling `Window::set_resizable`. -- On Wayland, with resize increments: align from the requested minimum size instead of an internal `2x1`, round a surface smaller than one increment up to one, and keep the alignment when an interactive resize ends. +- On Wayland, with resize increments: align from the requested minimum size instead of an internal `2x1`, round a surface smaller than one increment up to one, keep the alignment when an interactive resize ends, and ignore a zero increment instead of panicking. ### Changed