Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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));
}