Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix(gestures): keep the pan that brings a swiped-to window into view
When a three-finger swipe switched focus, the gesture dropped any camera
pan heading back against its lean, so the camera never reversed. That
pan exists only when the new window crosses a screen edge, so dropping
it left the window clipped whenever the lean overshot it or leaned away
from its side. The diagonal lean and aiming by finger direction made
that common.
The pan is now kept. It is only as large as bringing the window in
needs, so a window the lean leaves fully in view still gets no pan and
the camera does not spring back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 14 +++++++++-----
src/server/cursor.rs | 31 ++++++++++---------------------
2 files changed, 19 insertions(+), 26 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 6a04c611..066ee6ad 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -585,11 +585,15 @@ its entry rule. The keyboard's focus chords stay four-way. Only binds whose
action `cursor::action_navigates` (focus/pan left/right/up/down) peek, and
only toward a direction that has one; a four-finger overview toggle leaves
the desktop still. When the bind fires (`handle_swipe_update`) the action
-runs against the camera where the lean left it, and **the camera never
-reverses at the fire**: a window that needs a pan gets its ease from
-there, a window already in view sets no target and the camera simply stops
-where the lean left it, and a target on the leaned axis that would head
-back toward where the swipe began is dropped. It does not predict the
+runs against the camera where the lean left it: a window that needs a pan
+gets its ease from there, and a window already in view sets no target, so
+the camera simply stops where the lean left it rather than springing back.
+**A pan back against the lean is kept.** It arises only when the lean
+pushed the new window's near edge off screen, or leaned away from the side
+it sits on, and it is only as large as bringing the window in needs. Until
+2026-09-24 such a target was dropped so the camera never reversed, which
+left the newly focused window clipped whenever the lean overshot; the
+diagonal lean and aiming by finger direction made that common. It does not predict the
destination (tried on 2026-09-22 — a lean along the policy's predicted pan,
nothing at all when the target was in view — and retired the same day: the
lean is meant to answer the finger, not the layout), and it does not spring
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index ecc6dfbf..e4199bf7 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -3856,27 +3856,16 @@ unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data:
_ => (*seat.server).wm.execute_action(&matched_action, matched_command.as_deref()),
}
- // No reversal at the fire. The action set a pan target if the
- // window it focused needs one (from the leaned camera — so a
- // window the lean already brought fully into view asks for
- // nothing, and the camera stops right here); it set none if the
- // window is already in view, and then the camera stays where the
- // lean left it rather than springing back. Either way the lean
- // only ever continues in its own direction: a target on the leaned
- // axis that lies back toward where the swipe began is dropped.
- {
- let wm = &mut (*seat.server).wm;
- for (axis, target) in [(0usize, &mut wm.target_desk_pan_x), (1usize, &mut wm.target_desk_pan_y)] {
- let here = if axis == 0 { wm.desk_pan_x } else { wm.desk_pan_y };
- if lean[axis] != 0.0 {
- if let Some(t) = *target {
- if (t - here) * lean[axis].signum() < 0.0 {
- *target = None;
- }
- }
- }
- }
- }
+ // The action ran against the leaned camera. It set a pan target
+ // only if the window it focused crosses a screen edge from there,
+ // and only as far as bringing it in needs (`pan_into_view`), so a
+ // window the lean left fully in view asks for nothing and the
+ // camera stops right here rather than springing back. A target
+ // that heads back against the lean is kept: it means the lean
+ // pushed the window's near edge off screen, or leaned away from
+ // the side the window sits on, and dropping it (as this did until
+ // 2026-09-24, to keep the camera from ever reversing) left the
+ // newly focused window clipped.
if first_fire {
let pointer_gestures = (*seat.server).input_manager.pointer_gestures;