Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 32 additions & 38 deletions winit-wayland/src/window/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ impl WindowState {
has_pending_move: None,
text_input_state: None,
max_surface_size: None,
min_surface_size: MIN_WINDOW_SIZE,

@njallam njallam Sep 27, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading alacritty/alacritty#9047, I'm now wondering if this is wrong, and set_min_surface_size is supposed to be called by the application when using set_surface_resize_increments?

(not the best line selection, but I mean this commit in general)

min_surface_size: LogicalSize::new(0, 0),
resize_increments: None,
pointer_constraints,
pointers: Default::default(),
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -498,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) {
Expand Down Expand Up @@ -571,40 +567,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<LogicalSize<u32>>) {
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_default();
self.reload_min_max_hints();
}

/// Set maximum inner window size.
pub fn set_max_surface_size(&mut self, size: Option<LogicalSize<u32>>) {
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.
Expand All @@ -624,8 +596,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.stateless_size, Some(self.stateless_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<u32>| {
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<u32>) {
Expand Down
35 changes: 24 additions & 11 deletions winit-wayland/src/window/state/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand All @@ -161,16 +165,19 @@ 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 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);
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, height).into();
}
}
Expand All @@ -189,6 +196,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| {
Expand All @@ -203,7 +213,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 {
Expand Down
2 changes: 2 additions & 0 deletions winit/src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ 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 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, keep the alignment when an interactive resize ends, and ignore a zero increment instead of panicking.

### Changed

Expand Down
Loading