git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitffb2074103469cd5994ecfa051aaf25bf9cf6306
parent94e6e3fb18
authorLucas Galante <[email protected]>
date2026-09-03 22:05
fix(resize): clamp interactive resizes to the client's min/max size hint

The overview resize op fed `snap::resize_axis`'s result straight into a
configure, floored only at 50px, so a drag could push a toplevel far
below the min size it declared through xdg-shell. cce-ui applies a
configure as-is, and cce-data-editor (min 600x450) panicked in the
tessellator once its layout had less room than its fixed parts — the
window simply vanished mid-drag. A window restored from state below its
minimum then sat there unresizable.

`DimensionsHint::clamp` applies the declared range (0 = unset, xdg's
convention; min wins over a contradictory max) in both places that
derive the in-flight size — the seat op's Resize arm and
`get_active_resize_dimensions` for the arrange snapshot — so they cannot
diverge. Verified in headless shadows at output scale 1 and 2: a right-
edge drag stops at 600 wide, a bottom-right corner drag at 600x450, a
left-edge drag past the minimum holds the right edge anchored and grows
again on the way back, and the client stays alive throughout.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/server/seat.rs           |  5 +++++
 src/server/window.rs         | 24 ++++++++++++++++++++++++
 src/server/window_manager.rs |  3 ++-
 3 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/src/server/seat.rs b/src/server/seat.rs
index cff4058..8bde122 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1411,6 +1411,11 @@ impl Seat {
                             op.start_win_virtual_y, op.start_win_h as f64, virtual_dy,
                             edges.top, edges.bottom, 50.0, &sp.y(),
                         ) as u32;
+                        // The client's xdg min/max size is a contract, not a
+                        // suggestion: a configure below it is applied by
+                        // cce-ui as-is, and a layout with less room than its
+                        // fixed parts panicked cce-data-editor mid-drag.
+                        let (new_w, new_h) = (*win).wm_scheduled.dimensions_hint.clamp(new_w, new_h);
 
                         (*win).virtual_x = vx;
                         (*win).virtual_y = vy;
diff --git a/src/server/window.rs b/src/server/window.rs
index 42f0a72..9f4d3c7 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -50,6 +50,30 @@ pub struct DimensionsHint {
     pub max_height: u32,
 }
 
+impl DimensionsHint {
+    /// Clamp a requested content size to the client's declared range; a
+    /// zero bound is "unset" (xdg-shell's convention) and leaves that side
+    /// alone. A max below the min is the client's own contradiction and
+    /// the min wins.
+    pub fn clamp(&self, width: u32, height: u32) -> (u32, u32) {
+        let mut w = width;
+        let mut h = height;
+        if self.max_width > 0 {
+            w = w.min(self.max_width);
+        }
+        if self.max_height > 0 {
+            h = h.min(self.max_height);
+        }
+        if self.min_width > 0 {
+            w = w.max(self.min_width);
+        }
+        if self.min_height > 0 {
+            h = h.max(self.min_height);
+        }
+        (w, h)
+    }
+}
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct Edges {
     pub top: bool,
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 714be91..e9bd5b2 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2104,7 +2104,8 @@ impl WindowManager {
                             op.start_win_virtual_y, op.start_win_h as f64, virtual_dy,
                             edges.top, edges.bottom, 50.0, &sp.y(),
                         ) as u32;
-                        return Some((new_w, new_h));
+                        // Same clamp as the seat op (see its Resize arm).
+                        return Some((*win_ptr).wm_scheduled.dimensions_hint.clamp(new_w, new_h));
                     }
                 }
             }