window management library
git clone https://git.lucas.co/cce-window-manager.git
fix: hard-snap displaced tiled windows — the magnetic snap had a dead band
The overview displace path snapped a tiled candidate's exit spot with
snap_move at a threshold widened to 0.45 * cell_size, claiming the
abutting position 'always finds its cell'. It cannot: snap targets are
one PERIOD apart, so the farthest landing spot is (cell + gap)/2 from
any target — always more than 0.45 * cell. In the resulting dead band
(gap + 0.1 * cell wide per period, ~67px at cell 512 / gap 16, ~13% of
positions per axis) the snap silently returned the spot unchanged, the
tiled window came to rest mid-cell, and the arrange pass's tiled_span
then expanded it to every cell the off-grid box touched — displaced
windows visibly grew a row or column, and kept the size even when the
ledger returned them home.
Use snap_move_tiled instead: the thresholdless hard snap that exists
precisely because a tiled window resting between squares is not a
reachable state. Regression test pins a landing spot 256px off-target,
squarely in the old dead band.
Shadow-verified: an overview drag covering a tiled 1032x504 window
displaces it to the abutting cell-aligned spot with its size unchanged.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/overview.rs | 38 ++++++++++++++++++++++++++++++++------
1 file changed, 32 insertions(+), 6 deletions(-)
diff --git a/src/overview.rs b/src/overview.rs
index b001257..83d992f 100644
--- a/src/overview.rs
+++ b/src/overview.rs
@@ -88,13 +88,16 @@ pub fn displace(
ny = if dy >= 0.0 { my - gap - c.h } else { my + mh + gap };
}
- // A tiled candidate stays tiled: snap the landing spot to the cell
- // edges (its size is already cell-quantized, so a one-edge snap
- // aligns the whole box). Threshold is widened to half a period so
- // the abutting position always finds its cell.
+ // A tiled candidate stays tiled: hard-snap the landing spot to the
+ // nearest cell edges, no threshold (its size is already
+ // cell-quantized, so aligning the low edges aligns the whole box).
+ // Magnetic snapping cannot be widened into a guarantee — targets
+ // are one PERIOD apart, so any threshold below (cell + gap)/2
+ // leaves a dead band around the midpoint where the exit spot rests
+ // mid-cell, and the arrange pass then expands the "tiled" window to
+ // every cell the off-grid box touches.
if c.tiled {
- let wide = SnapParams { threshold: p.cell_size * 0.45, ..*p };
- let (sx, sy) = snap::snap_move(nx, ny, c.w, c.h, &wide);
+ let (sx, sy) = snap::snap_move_tiled(nx, ny, p);
nx = sx;
ny = sy;
}
@@ -188,6 +191,29 @@ mod tests {
assert_eq!(d[0].1, (4.0, 4.0));
}
+ #[test]
+ fn tiled_exit_in_magnetic_dead_band_still_snaps() {
+ // With a gap the grid period is 528, so snap targets are 528 apart
+ // and the old widened magnetic snap (threshold 0.45 * cell = 230.4)
+ // had a ~67px dead band around the midpoint: a landing spot ~256px
+ // from the nearest edge stayed mid-cell, and the arrange pass then
+ // grew the "tiled" window to every cell it touched.
+ let p = SnapParams { cell_size: 512.0, gap_width: 16.0, cell_inset: 4.0, threshold: 24.0 };
+ // Tiled candidate filling cell row 1 exactly: visible box
+ // y [532, 1036] → y=532, h=504.
+ let covered = DisplaceCandidate { x: 4.0, y: 532.0, w: 504.0, h: 504.0, tiled: true };
+ // Dragged DOWN onto it; the mover's mid-drag y is not grid-aligned.
+ // Overlap y [780, 1036] = 256 of 504 → ~51% of the smaller window.
+ let moved = (4.0, 780.0, 600.0, 600.0);
+ let d = displace(moved, (0.0, 300.0), &[covered], &p, 16.0);
+ assert_eq!(d.len(), 1);
+ // Downward drag: the candidate exits above, abutting the mover:
+ // raw y = 780 - 16 - 504 = 260 — 256 from the nearest low target
+ // (4), squarely in the old dead band. The hard snap lands it there
+ // anyway; x is untouched and already aligned.
+ assert_eq!(d[0].1, (4.0, 4.0));
+ }
+
#[test]
fn multiple_covered_windows_each_displace() {
let a = cand(1000.0, 100.0, 400.0, 400.0);