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

commiteeb7e7436771f5385fd78b1baa63fad166fefe8b
parent0c0d90b404
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-25 10:04
fix(gestures): fire the overview toggle once per swipe

Since a swipe stopped ending at its first fire, every swipe bind could
fire again with more travel, so one long four-finger swipe could toggle
the overview open and shut. Only focus and pan binds step repeatedly
now. Any other swipe bind, the overview toggle included, fires once
and the rest of that gesture is ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md            |  5 ++++-
 src/server/cursor.rs | 25 ++++++++++++++++++++-----
 2 files changed, 24 insertions(+), 6 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 066ee6ad..7915901d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -551,7 +551,10 @@ not end the swipe** (since 2026-09-24): the accumulated travel restarts
 from zero at the fire, and a further `window_manager {
 swipe_repeat_threshold }` of travel (libinput units, default four times
 `swipe_threshold`; `WindowManager::swipe_repeat_threshold`) without
-lifting fires again — three windows over is one long swipe, with more
+lifting fires again — for a focus or pan bind (`action_navigates`) only:
+any other swipe bind, the four-finger overview toggle included, fires
+once per gesture and the rest of it is ignored (`Cursor::swipe_spent`).
+Three windows over is one long swipe, with more
 resistance after the first step so it does not run on through the next
 window — and a reversal after a step goes straight back. The lean
 toward a further step is smaller too: it reaches `window_manager {
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index e4199bf7..738f86c8 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -101,6 +101,11 @@ pub struct Cursor {
     /// again — it records that clients were sent a cancelled end and hear
     /// nothing more of it; a pinch fires once.
     pub gesture_triggered: bool,
+    /// The in-flight swipe fired a bind that is not a step (anything
+    /// `action_navigates` rejects: the overview toggle, a spawn): the rest
+    /// of the gesture fires nothing more, so one swipe toggles the
+    /// overview once however far the fingers go. Cleared at swipe begin.
+    pub swipe_spent: bool,
     /// Camera offset (virtual units, `[x, y]`) the in-flight swipe has
     /// peeked the desktop by so far — see `swipe_peek_for`. Zero outside a
     /// swipe, and restarts from zero at each fire, like the travel.
@@ -201,6 +206,7 @@ impl Default for Cursor {
             gesture_dy: 0.0,
             gesture_scale: 1.0,
             gesture_triggered: false,
+            swipe_spent: false,
             swipe_peek: [0.0, 0.0],
             inject_swipe_fingers: 3,
             panning_gesture_active: false,
@@ -3702,6 +3708,7 @@ unsafe extern "C" fn handle_swipe_begin(listener: *mut ffi::wl_listener, data: *
     cursor.gesture_dx = 0.0;
     cursor.gesture_dy = 0.0;
     cursor.gesture_triggered = false;
+    cursor.swipe_spent = false;
     cursor.swipe_peek = [0.0, 0.0];
 
     log::info!("handle_swipe_begin: fingers={}", (*event).fingers);
@@ -3730,11 +3737,16 @@ unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data:
     }
     seat.handle_activity();
 
-    // A bind that already fired does not end the gesture: the travel
-    // restarts from zero at the fire (below), so the fingers can keep
-    // going and step focus again — or turn round and step back — without
-    // lifting. `gesture_triggered` only records that clients were sent
-    // their (cancelled) end, so they hear nothing more of this swipe.
+    // A step does not end the gesture: the travel restarts from zero at
+    // the fire (below), so the fingers can keep going and step focus
+    // again — or turn round and step back — without lifting.
+    // `gesture_triggered` only records that clients were sent their
+    // (cancelled) end, so they hear nothing more of this swipe. Any other
+    // bind (the overview toggle, a spawn) fires once per gesture: after
+    // it the swipe is spent and the rest of it is ignored.
+    if cursor.swipe_spent {
+        return;
+    }
 
     cursor.gesture_dx += (*event).dx;
     cursor.gesture_dy += (*event).dy;
@@ -3802,6 +3814,9 @@ unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data:
         // aims along it (`swipe_focus_vector`).
         let travel = (cursor.gesture_dx, cursor.gesture_dy);
         let first_fire = !cursor.gesture_triggered;
+        if !action_navigates(matched_action) {
+            cursor.swipe_spent = true;
+        }
         cursor.gesture_triggered = true;
         // The next step needs a full threshold of fresh travel from here,
         // on both axes: a long swipe steps once per threshold, and a