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

commit48dc32d57c602cf96c3fbdfad0c143972f0b243d
parent8f4560dc98
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-24 22:25
feat(gestures): lean more slowly toward steps after the first

Once a three-finger swipe has switched focus, the camera's lean toward
the next step now reaches window_manager { swipe_repeat_peek } at the
repeat threshold instead of swipe_peek. It defaults to half of
swipe_peek, so a swipe that has just switched does not tug the camera
toward the next window as eagerly. The first step's lean is unchanged.

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

 CLAUDE.md                    |  6 +++++-
 src/server/config.rs         | 15 ++++++++++++++-
 src/server/cursor.rs         |  6 +++++-
 src/server/window_manager.rs |  6 ++++++
 4 files changed, 30 insertions(+), 3 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 062c8212..966f109b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -548,7 +548,11 @@ 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
 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 factor was
+window — and a reversal after a step goes straight back. The lean
+toward a further step is smaller too: it reaches `window_manager {
+swipe_repeat_peek }` (screen px, default half of `swipe_peek`;
+`WindowManager::swipe_repeat_peek_px`) at the repeat threshold, so it
+moves far more slowly per unit of travel than the first step's lean. The factor was
 two at first and read as too eager: replaying a session's logged swipes
 (every `handle_swipe_update` is logged at info with its delta, in
 `$XDG_RUNTIME_DIR/cce/cce.log`) showed ordinary single swipes travelling
diff --git a/src/server/config.rs b/src/server/config.rs
index a52cba43..13adb9c2 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -451,6 +451,12 @@ pub struct WindowManagerConfig {
     /// under `input`, because input.kdl's `input {}` block replaces
     /// config.kdl's wholesale. See "Swipe binds peek" in CLAUDE.md.
     pub swipe_peek: Option<f64>,
+    /// The lean toward each FURTHER step of a swipe that has already
+    /// switched focus, screen px at `swipe_repeat_threshold` (default half
+    /// of `swipe_peek`; 0 turns it off). Slower than the first step's
+    /// lean, so a swipe that has just switched reads as settled. KDL:
+    /// `swipe_repeat_peek (f64)30.0`.
+    pub swipe_repeat_peek: Option<f64>,
     /// Accumulated travel (libinput units, roughly mm) at which a swipe
     /// bind fires (default 70). KDL: `swipe_threshold (f64)70.0`.
     pub swipe_threshold: Option<f64>,
@@ -2451,11 +2457,12 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
         let touchpad_view_swipe = get_child_arg_string_opt(node, "touchpad_view_swipe");
         let touchpad_view_sensitivity = get_child_arg_f64_opt(node, "touchpad_view_sensitivity");
         let swipe_peek = get_child_arg_f64_opt(node, "swipe_peek");
+        let swipe_repeat_peek = get_child_arg_f64_opt(node, "swipe_repeat_peek");
         let swipe_threshold = get_child_arg_f64_opt(node, "swipe_threshold");
         let swipe_repeat_threshold = get_child_arg_f64_opt(node, "swipe_repeat_threshold");
         let touchpad_view_invert = get_child_arg_bool_opt(node, "touchpad_view_invert");
         let touchpad_hscroll_shift_apps = get_child_args_string_vec_opt(node, "touchpad_hscroll_shift_apps");
-        window_manager = Some(WindowManagerConfig { close_window, toggle_fullscreen, toggle_overview, window_switcher, window_switcher_prev, center_on_spawn, on_app_exit, corner_shape, rounded_apps, bevel_apps, xwayland_hidpi, xwayland_hidpi_except, touchpad_view_apps, touchpad_view_swipe, touchpad_view_sensitivity, swipe_peek, swipe_threshold, swipe_repeat_threshold, touchpad_view_invert, touchpad_hscroll_shift_apps });
+        window_manager = Some(WindowManagerConfig { close_window, toggle_fullscreen, toggle_overview, window_switcher, window_switcher_prev, center_on_spawn, on_app_exit, corner_shape, rounded_apps, bevel_apps, xwayland_hidpi, xwayland_hidpi_except, touchpad_view_apps, touchpad_view_swipe, touchpad_view_sensitivity, swipe_peek, swipe_repeat_peek, swipe_threshold, swipe_repeat_threshold, touchpad_view_invert, touchpad_hscroll_shift_apps });
     }
 
     Ok(Config {
@@ -2533,6 +2540,10 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
             .and_then(|w| w.swipe_peek)
             .filter(|v| v.is_finite() && *v >= 0.0)
             .unwrap_or(60.0);
+        state.swipe_repeat_peek_px = tv
+            .and_then(|w| w.swipe_repeat_peek)
+            .filter(|v| v.is_finite() && *v >= 0.0)
+            .unwrap_or(state.swipe_peek_px * 0.5);
         state.swipe_threshold = tv
             .and_then(|w| w.swipe_threshold)
             .filter(|v| v.is_finite() && *v > 0.0)
@@ -3072,6 +3083,7 @@ style {
                 toggle_fullscreen (keybind)"super+f"
                 toggle_overview ("menu:swipe_up,swipe_down,swipe_left,swipe_right,pinch_in,pinch_out")"swipe_up"
                 swipe_peek (f64)40.0
+                swipe_repeat_peek (f64)15.0
                 swipe_threshold (f64)80.0
                 swipe_repeat_threshold (f64)200.0
             }
@@ -3083,6 +3095,7 @@ style {
         assert_eq!(wm.toggle_fullscreen, Some("super+f".to_string()));
         assert_eq!(wm.toggle_overview, Some("swipe_up".to_string()));
         assert_eq!(wm.swipe_peek, Some(40.0));
+        assert_eq!(wm.swipe_repeat_peek, Some(15.0));
         assert_eq!(wm.swipe_threshold, Some(80.0));
         assert_eq!(wm.swipe_repeat_threshold, Some(200.0));
         // Absent means "unset", which the apply step reads as the centring default.
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index daa56bcd..9f869137 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -3813,7 +3813,11 @@ unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data:
     // real move.
     {
         let wm = &mut (*seat.server).wm;
-        let peek_px = wm.swipe_peek_px;
+        // After a step the lean is slower as well as longer to fill: it
+        // reaches `swipe_repeat_peek` (default half of `swipe_peek`) at the
+        // repeat threshold, so a swipe that has just switched focus does
+        // not tug the camera toward the next window as eagerly.
+        let peek_px = if cursor.gesture_triggered { wm.swipe_repeat_peek_px } else { wm.swipe_peek_px };
         let (dx, dy) = (cursor.gesture_dx, cursor.gesture_dy);
         let want = if dx.abs() >= dy.abs() {
             [swipe_peek_for(dx, navigates[0], navigates[1], threshold, peek_px, wm.desk_zoom), 0.0]
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index f63c2708..a47228d4 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -217,6 +217,10 @@ pub struct WindowManager {
     /// directional swipe bind at its threshold, screen px (default 60;
     /// 0 disables). See `cursor::swipe_peek_for`.
     pub swipe_peek_px: f64,
+    /// `window_manager { swipe_repeat_peek }`: the lean toward each further
+    /// step once a swipe has switched focus, screen px at
+    /// `swipe_repeat_threshold` (default half of `swipe_peek_px`).
+    pub swipe_repeat_peek_px: f64,
     /// `window_manager { swipe_threshold }`: accumulated swipe travel
     /// (libinput units) at which a swipe bind fires (default 70).
     pub swipe_threshold: f64,
@@ -536,6 +540,7 @@ impl WindowManager {
         self.touchpad_view_swipe_tumble = false;
         self.touchpad_view_sensitivity = 1.0;
         self.swipe_peek_px = 60.0;
+        self.swipe_repeat_peek_px = 30.0;
         self.swipe_threshold = 70.0;
         self.swipe_repeat_threshold = 280.0;
         self.touchpad_view_invert = false;
@@ -619,6 +624,7 @@ impl WindowManager {
         self.touchpad_view_swipe_tumble = false;
         self.touchpad_view_sensitivity = 1.0;
         self.swipe_peek_px = 60.0;
+        self.swipe_repeat_peek_px = 30.0;
         self.swipe_threshold = 70.0;
         self.swipe_repeat_threshold = 280.0;
         self.touchpad_view_invert = false;