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

commit081eed5f723473425dd12abc24bef46134883e7b
parentfd1562b8db
authorLucas Galante <[email protected]>
date2026-09-18 15:01
fix(input): a Tiled window's pointer resize snaps to whole cells

Both places that compute a drag's size — the seat op's Resize arm and
get_active_resize_dimensions for the arrange snapshot — use
snap::resize_axis_tiled when the window was grabbed Tiled, so each
dragged edge lands on a cell edge and the window stays Tiled on
release; Floating windows keep the magnetic pull. Moving a Tiled window
already snapped hard (snap_move_tiled). Needs cce-window-manager@$(git -C ../cce-window-manager rev-parse --short HEAD).

Verified in a shadow on a 460x260 grid: right-edge drag 2 cols → 1,
bottom-edge drag to exactly 4 rows, body drag to the next column,
left-edge drag 1 col → 2, a short drag unchanged — Tiled throughout. A
client whose xdg min size exceeds one cell (cce-color-editor's height)
still clamps and demotes, as the size contract requires.

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

 CLAUDE.md                    |  8 +++++++-
 src/server/seat.rs           | 43 ++++++++++++++++++++++++++++++-------------
 src/server/window_manager.rs | 35 ++++++++++++++++++++++++++---------
 3 files changed, 63 insertions(+), 23 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index b4a1a02..9253a9b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -324,7 +324,13 @@ treats them as opaque.
   `Tiled` (grid-aligned; the window reports xdg maximized), `Fullscreen`,
   `Popup`, `Overlay`, `Status`, `Utility`. Tiled-ness is geometric: the seat
   op's end (`seat.rs::op_end`) promotes/demotes via
-  `policy::snap::is_cell_aligned`. `Utility` is the one mode a client asks for
+  `policy::snap::is_cell_aligned`. **A window grabbed Tiled snaps HARD
+  through the whole drag** — its move lands on cell starts
+  (`snap::snap_move_tiled`) and its resize lands each dragged edge on a cell
+  edge, whole cells only (`snap::resize_axis_tiled`, used by both the seat
+  op and `get_active_resize_dimensions`) — so it comes out of the drag still
+  Tiled; the magnetic pull (`snap_move`, `resize_axis`) is for Floating
+  windows deciding whether to tile. `Utility` is the one mode a client asks for
   outright — `cce_window_management.rs` sets it on `set_utility` — and it is a
   self-sizing float: no resize affordance, no saved geometry (see
   `xdg_toplevel.rs`, which sizes it and `Status` from their own content, and
diff --git a/src/server/seat.rs b/src/server/seat.rs
index 0a2778e..2d633fc 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -1464,19 +1464,36 @@ impl Seat {
                             (*win).resize_edges = Some(edges);
                         }
 
-                        // Magnetic grid snap pulls the dragged edge onto the
-                        // visible cell edges; the anchored edge is untouched.
-                        // Must match get_active_resize_dimensions, which
-                        // recomputes this for the arrange snapshot — both go
-                        // through snap::resize_axis.
-                        let new_w = crate::policy::snap::resize_axis(
-                            op.start_win_virtual_x, op.start_win_w as f64, virtual_dx,
-                            edges.left, edges.right, 50.0, &sp.x(),
-                        ) as u32;
-                        let new_h = crate::policy::snap::resize_axis(
-                            op.start_win_virtual_y, op.start_win_h as f64, virtual_dy,
-                            edges.top, edges.bottom, 50.0, &sp.y(),
-                        ) as u32;
+                        // A window grabbed Tiled snaps HARD: the dragged edge
+                        // lands on a cell edge from any distance and the size
+                        // stays whole cells, so it is still Tiled on release.
+                        // A Floating one gets the magnetic pull onto the
+                        // visible cell edges; the anchored edge is untouched
+                        // either way. Must match get_active_resize_dimensions,
+                        // which recomputes this for the arrange snapshot.
+                        let (new_w, new_h) = if op.start_was_tiled {
+                            (
+                                crate::policy::snap::resize_axis_tiled(
+                                    op.start_win_virtual_x, op.start_win_w as f64, virtual_dx,
+                                    edges.left, edges.right, &sp.x(),
+                                ) as u32,
+                                crate::policy::snap::resize_axis_tiled(
+                                    op.start_win_virtual_y, op.start_win_h as f64, virtual_dy,
+                                    edges.top, edges.bottom, &sp.y(),
+                                ) as u32,
+                            )
+                        } else {
+                            (
+                                crate::policy::snap::resize_axis(
+                                    op.start_win_virtual_x, op.start_win_w as f64, virtual_dx,
+                                    edges.left, edges.right, 50.0, &sp.x(),
+                                ) as u32,
+                                crate::policy::snap::resize_axis(
+                                    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
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index a0ca407..5ffc806 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2906,16 +2906,33 @@ impl WindowManager {
                         let virtual_dy = dy as f64 / scale + (self.desk_pan_y - op.start_pan_y);
                         // Same math (and snapping) as the seat op's Resize
                         // arm — this recomputation feeds the arrange
-                        // snapshot and must not diverge from it.
+                        // snapshot and must not diverge from it: hard
+                        // whole-cell snap for a window grabbed Tiled,
+                        // magnetic pull for a Floating one.
                         let sp = self.layout.snap_params().for_zoom(self.desk_zoom);
-                        let new_w = crate::policy::snap::resize_axis(
-                            op.start_win_virtual_x, op.start_win_w as f64, virtual_dx,
-                            edges.left, edges.right, 50.0, &sp.x(),
-                        ) as u32;
-                        let new_h = crate::policy::snap::resize_axis(
-                            op.start_win_virtual_y, op.start_win_h as f64, virtual_dy,
-                            edges.top, edges.bottom, 50.0, &sp.y(),
-                        ) as u32;
+                        let (new_w, new_h) = if op.start_was_tiled {
+                            (
+                                crate::policy::snap::resize_axis_tiled(
+                                    op.start_win_virtual_x, op.start_win_w as f64, virtual_dx,
+                                    edges.left, edges.right, &sp.x(),
+                                ) as u32,
+                                crate::policy::snap::resize_axis_tiled(
+                                    op.start_win_virtual_y, op.start_win_h as f64, virtual_dy,
+                                    edges.top, edges.bottom, &sp.y(),
+                                ) as u32,
+                            )
+                        } else {
+                            (
+                                crate::policy::snap::resize_axis(
+                                    op.start_win_virtual_x, op.start_win_w as f64, virtual_dx,
+                                    edges.left, edges.right, 50.0, &sp.x(),
+                                ) as u32,
+                                crate::policy::snap::resize_axis(
+                                    op.start_win_virtual_y, op.start_win_h as f64, virtual_dy,
+                                    edges.top, edges.bottom, 50.0, &sp.y(),
+                                ) as u32,
+                            )
+                        };
                         // Same clamp as the seat op (see its Resize arm).
                         return Some((*win_ptr).wm_scheduled.dimensions_hint.clamp(new_w, new_h));
                     }