@@ -127,6 +127,7 @@ pub struct WindowState {
127127 /// Min size.
128128 min_inner_size : LogicalSize < u32 > ,
129129 max_inner_size : Option < LogicalSize < u32 > > ,
130+ resize_increments : Option < LogicalSize < u32 > > ,
130131
131132 /// The size of the window when no states were applied to it. The primary use for it
132133 /// is to fallback to original window size, before it was maximized, if the compositor
@@ -202,6 +203,7 @@ impl WindowState {
202203 last_configure : None ,
203204 max_inner_size : None ,
204205 min_inner_size : MIN_WINDOW_SIZE ,
206+ resize_increments : None ,
205207 pointer_constraints,
206208 pointers : Default :: default ( ) ,
207209 queue_handle : queue_handle. clone ( ) ,
@@ -340,6 +342,42 @@ impl WindowState {
340342 . unwrap_or ( new_size. height ) ;
341343 }
342344
345+ // Apply size increments.
346+ //
347+ // We conditionally apply increments to avoid conflicts with the compositor's layout rules:
348+ // 1. If the window is floating (constrain == true), we snap to increments to ensure the
349+ // app's grid alignment.
350+ // 2. If the user is interactively resizing (is_resizing), we snap the size to provide
351+ // feedback.
352+ //
353+ // However, we MUST NOT snap if the compositor enforces a specific size (constrain == false,
354+ // or states like Maximized/Tiled). Snapping in these cases (e.g. corner tiling) would
355+ // shrink the window below the allocated area, creating visible gaps between valid
356+ // windows or screen edges.
357+ if ( constrain || configure. is_resizing ( ) )
358+ && !configure. is_maximized ( )
359+ && !configure. is_fullscreen ( )
360+ && !configure. is_tiled ( )
361+ {
362+ if let Some ( increments) = self . resize_increments {
363+ // We use min size as a base size for the increments, similar to how X11 does it.
364+ //
365+ // This ensures that we can always reach the min size and the increments are
366+ // calculated from it.
367+ let ( delta_width, delta_height) = (
368+ new_size. width . saturating_sub ( self . min_inner_size . width ) ,
369+ new_size. height . saturating_sub ( self . min_inner_size . height ) ,
370+ ) ;
371+
372+ let width = self . min_inner_size . width
373+ + ( delta_width / increments. width ) * increments. width ;
374+ let height = self . min_inner_size . height
375+ + ( delta_height / increments. height ) * increments. height ;
376+
377+ new_size = ( width, height) . into ( ) ;
378+ }
379+ }
380+
343381 let new_state = configure. state ;
344382 let old_state = self . last_configure . as_ref ( ) . map ( |configure| configure. state ) ;
345383
@@ -725,6 +763,18 @@ impl WindowState {
725763 self . selected_cursor = SelectedCursor :: Custom ( cursor) ;
726764 }
727765
766+ /// Set the resize increments of the window.
767+ pub fn set_resize_increments ( & mut self , increments : Option < LogicalSize < u32 > > ) {
768+ self . resize_increments = increments;
769+ // NOTE: We don't update the window size here, because it will be done on the next resize
770+ // or configure event.
771+ }
772+
773+ /// Get the resize increments of the window.
774+ pub fn resize_increments ( & self ) -> Option < LogicalSize < u32 > > {
775+ self . resize_increments
776+ }
777+
728778 fn apply_custom_cursor ( & self , cursor : & CustomCursor ) {
729779 self . apply_on_pointer ( |pointer, data| {
730780 let surface = pointer. surface ( ) ;
0 commit comments