git.lucas.co / cce-window-manager
window management library
git clone https://git.lucas.co/cce-window-manager.git

commit64c490dcbca2a8f614139368d0a8a588553872d6
parentc796832525
authorLucas Galante <[email protected]>
date2026-07-28 10:13
feat: pan module — cell-aligned viewport pan stepping

aligned_step() gives the PanLeft/Right/Up/Down actions their targets: the
adjacent pan offset that is a multiple of the grid period (cell_size +
gap_width), so a keyed pan always lands with the viewport origin on a cell
boundary. One full period from an aligned start; just the remaining
fraction from an unaligned one, re-aligning a free-form pan on the first
step. Offsets within 0.5 (the animation's snap tolerance) count as aligned
so a finished animation steps a whole period, never a crumb.

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

 src/lib.rs |  2 ++
 src/pan.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 72 insertions(+)

diff --git a/src/lib.rs b/src/lib.rs
index 0c74fad..5b7c81a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -18,6 +18,7 @@
 //   - `focus`: directional focus selection (which window is "up/left/…"
 //     of the focused one) over virtual-surface center points.
 //   - `tiling`: `TilingMode` and pure layout formulas.
+//   - `pan`: cell-aligned viewport panning (the PanLeft/… step targets).
 //   - `snap`: magnetic grid snapping for interactive move/resize.
 //   - `state`: persisted session state (serialization/matching only; the
 //     save/load I/O stays in the compositor).
@@ -28,6 +29,7 @@ pub mod api;
 pub mod arrange;
 pub mod bindings;
 pub mod focus;
+pub mod pan;
 pub mod slotmap;
 pub mod snap;
 pub mod state;
diff --git a/src/pan.rs b/src/pan.rs
new file mode 100644
index 0000000..39c276e
--- /dev/null
+++ b/src/pan.rs
@@ -0,0 +1,70 @@
+// Cell-aligned viewport panning.
+//
+// The desktop grid repeats every `period = cell_size + gap_width` virtual
+// units, with cell k's rect starting at k*period. A pan offset is "aligned"
+// when it is a multiple of the period: the viewport origin then sits exactly
+// on a cell boundary, so the visible grid is in phase with the screen edge.
+//
+// `aligned_step` is the policy behind the PanLeft/Right/Up/Down actions: a
+// press moves the viewport to the ADJACENT aligned offset in that direction —
+// one full period from an aligned start, or just the remaining fraction from
+// an unaligned one (a free-form pan re-aligns on the first keyed step rather
+// than staying forever out of phase). The mechanism animates toward the
+// returned value; repeated presses mid-flight should pass the current
+// animation target as `current` so each press queues one more cell.
+
+/// Offsets within this distance of an aligned value count as aligned — the
+/// same tolerance the pan animation uses to snap onto its target, so a
+/// finished animation's landing point steps a full period, never a crumb.
+const ALIGN_EPSILON: f64 = 0.5;
+
+/// The next cell-aligned pan offset from `current`, one step in `dir`
+/// (negative = left/up, positive = right/down). Returns `current` unchanged
+/// for a degenerate period or a zero direction.
+pub fn aligned_step(current: f64, period: f64, dir: f64) -> f64 {
+    if period <= 0.0 || dir == 0.0 {
+        return current;
+    }
+    if dir > 0.0 {
+        (((current + ALIGN_EPSILON) / period).floor() + 1.0) * period
+    } else {
+        (((current - ALIGN_EPSILON) / period).ceil() - 1.0) * period
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const P: f64 = 512.0;
+
+    #[test]
+    fn aligned_start_steps_a_full_period() {
+        assert_eq!(aligned_step(0.0, P, 1.0), 512.0);
+        assert_eq!(aligned_step(0.0, P, -1.0), -512.0);
+        assert_eq!(aligned_step(-1024.0, P, 1.0), -512.0);
+    }
+
+    #[test]
+    fn unaligned_start_realigns_first() {
+        // Mid-cell pans land on the adjacent boundary, not a full period out.
+        assert_eq!(aligned_step(100.0, P, 1.0), 512.0);
+        assert_eq!(aligned_step(100.0, P, -1.0), 0.0);
+        assert_eq!(aligned_step(-100.0, P, -1.0), -512.0);
+    }
+
+    #[test]
+    fn near_aligned_counts_as_aligned() {
+        // The animation stops within 0.5 of its target; a step from there
+        // must cross a whole period, not crawl onto the boundary it's on.
+        assert_eq!(aligned_step(511.9, P, 1.0), 1024.0);
+        assert_eq!(aligned_step(512.1, P, -1.0), 0.0);
+    }
+
+    #[test]
+    fn degenerate_inputs_are_inert() {
+        assert_eq!(aligned_step(100.0, 0.0, 1.0), 100.0);
+        assert_eq!(aligned_step(100.0, -5.0, 1.0), 100.0);
+        assert_eq!(aligned_step(100.0, P, 0.0), 100.0);
+    }
+}