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

commit08a40f14544dcac1aa23ea9415e6a0f8a524e610
parent4a9d4eb80a
authorLucas Galante <[email protected]>
date2026-09-21 14:45
feat(config): window_manager { swipe_peek } sets the swipe lean

The lean a directional swipe bind gives the desktop before it fires was
a constant, 60 screen px at the threshold. It is now `swipe_peek` under
`window_manager` in config.kdl (0 turns it off), applied on reload.

It lives under `window_manager`, not `input` beside the scroll settings,
because input.kdl's `input {}` block replaces config.kdl's wholesale, so
a key there is dead for anyone with pointer settings in input.kdl.

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

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

diff --git a/CLAUDE.md b/CLAUDE.md
index 48280d3..a8b98ba 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -511,7 +511,9 @@ directional focus or pan (`focus_left (gesture)"swipe3_left"` in input.kdl)
 fires once the accumulated travel passes `cursor::SWIPE_TRIGGER_DISTANCE`
 (50 libinput units). Short of that the camera *leans* toward the bind the
 swipe is heading for, 1:1 with the fingers and proportional to the travel —
-`SWIPE_PEEK_PX` (60 screen px) at the threshold, clamped there — and eases
+`window_manager { swipe_peek }` screen px at the threshold (default 60, 0
+disables; `WindowManager::swipe_peek_px` — not under `input`, whose
+config.kdl block input.kdl's replaces wholesale), clamped there — and eases
 back to where it started if the fingers lift first (`handle_swipe_end`), so
 a hesitant swipe shows where it would go without going. Only binds whose
 action `cursor::action_navigates` (focus/pan left/right/up/down) peek, and
diff --git a/src/server/config.rs b/src/server/config.rs
index d5cc2fb..d531e64 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -445,6 +445,12 @@ pub struct WindowManagerConfig {
     pub touchpad_view_swipe: Option<String>,
     /// Finger-to-pointer distance factor for the emulated drag (default 1).
     pub touchpad_view_sensitivity: Option<f64>,
+    /// How far the desktop leans toward a directional swipe bind by the
+    /// time the swipe reaches its threshold, screen px (default 60; 0
+    /// turns the lean off). KDL: `swipe_peek (f64)60.0`. Lives here, not
+    /// 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>,
     /// Reverse the drag direction, on top of the natural-scroll correction
     /// the drag already makes. KDL: `touchpad_view_invert (bool)true`.
     pub touchpad_view_invert: Option<bool>,
@@ -2436,9 +2442,10 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
         let touchpad_view_apps = get_child_args_string_vec_opt(node, "touchpad_view_apps");
         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 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, 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, touchpad_view_invert, touchpad_hscroll_shift_apps });
     }
 
     Ok(Config {
@@ -2512,6 +2519,10 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
             .and_then(|w| w.touchpad_view_swipe.as_deref())
             .map_or(false, |s| s.eq_ignore_ascii_case("tumble"));
         state.touchpad_view_sensitivity = tv.and_then(|w| w.touchpad_view_sensitivity).unwrap_or(1.0);
+        state.swipe_peek_px = tv
+            .and_then(|w| w.swipe_peek)
+            .filter(|v| v.is_finite() && *v >= 0.0)
+            .unwrap_or(60.0);
         state.touchpad_view_invert = tv.and_then(|w| w.touchpad_view_invert).unwrap_or(false);
         state.touchpad_hscroll_shift_apps = tv.and_then(|w| w.touchpad_hscroll_shift_apps.clone()).unwrap_or_default();
     }
@@ -3042,6 +3053,7 @@ style {
                 close_window (keybind)"super+q"
                 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
             }
         "#;
         let config = parse_kdl_config(content).unwrap();
@@ -3050,6 +3062,7 @@ style {
         assert_eq!(wm.close_window, Some("super+q".to_string()));
         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));
         // Absent means "unset", which the apply step reads as the centring default.
         assert_eq!(wm.center_on_spawn, None);
     }
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index ce4c2e0..d653703 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -3532,11 +3532,11 @@ unsafe extern "C" fn handle_touch_frame(listener: *mut ffi::wl_listener, _data:
 
 /// Accumulated swipe travel (libinput units) at which a directional swipe
 /// bind fires. Until then the camera *peeks*: it pans toward the swipe
-/// direction in proportion to the travel, up to `SWIPE_PEEK_PX` on screen
-/// at the threshold, and eases back if the fingers lift short of it — so
-/// a hesitant three-finger swipe shows where it would go without going.
+/// direction in proportion to the travel, up to `window_manager {
+/// swipe_peek }` screen px (`WindowManager::swipe_peek_px`, default 60) at the
+/// threshold, and eases back if the fingers lift short of it — so a
+/// hesitant three-finger swipe shows where it would go without going.
 const SWIPE_TRIGGER_DISTANCE: f64 = 50.0;
-const SWIPE_PEEK_PX: f64 = 60.0;
 
 /// Does firing `action` carry the view in the swipe's direction? Only
 /// such binds peek the camera beforehand: an overview toggle or a spawn
@@ -3547,12 +3547,13 @@ fn action_navigates(action: crate::config::Action) -> bool {
 }
 
 /// The peek the accumulated travel `d` along one axis calls for, in
-/// virtual units: proportional and clamped at the threshold, and only
-/// toward a direction that has a navigating bind (`neg` / `pos`) — a
-/// swipe with nothing bound its way leaves the desktop still.
-fn swipe_peek_for(d: f64, neg: bool, pos: bool, zoom: f64) -> f64 {
+/// virtual units: proportional and clamped at the threshold, `peek_px`
+/// on screen there, and only toward a direction that has a navigating
+/// bind (`neg` / `pos`) — a swipe with nothing bound its way leaves the
+/// desktop still.
+fn swipe_peek_for(d: f64, neg: bool, pos: bool, peek_px: f64, zoom: f64) -> f64 {
     if (d < 0.0 && neg) || (d > 0.0 && pos) {
-        (d / SWIPE_TRIGGER_DISTANCE).clamp(-1.0, 1.0) * SWIPE_PEEK_PX / zoom.max(1e-6)
+        (d / SWIPE_TRIGGER_DISTANCE).clamp(-1.0, 1.0) * peek_px / zoom.max(1e-6)
     } else {
         0.0
     }
@@ -3741,9 +3742,10 @@ unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data:
     // reversal leans back through zero.
     {
         let wm = &mut (*seat.server).wm;
+        let peek_px = wm.swipe_peek_px;
         let want = [
-            swipe_peek_for(cursor.gesture_dx, navigates[0], navigates[1], wm.desk_zoom),
-            swipe_peek_for(cursor.gesture_dy, navigates[2], navigates[3], wm.desk_zoom),
+            swipe_peek_for(cursor.gesture_dx, navigates[0], navigates[1], peek_px, wm.desk_zoom),
+            swipe_peek_for(cursor.gesture_dy, navigates[2], navigates[3], peek_px, wm.desk_zoom),
         ];
         let delta = [want[0] - cursor.swipe_peek[0], want[1] - cursor.swipe_peek[1]];
         if delta != [0.0, 0.0] {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 3996233..205fe3f 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -210,6 +210,10 @@ pub struct WindowManager {
     pub touchpad_view_swipe_tumble: bool,
     pub touchpad_view_sensitivity: f64,
     pub touchpad_view_invert: bool,
+    /// `window_manager { swipe_peek }`: the desktop's lean toward a
+    /// directional swipe bind at its threshold, screen px (default 60;
+    /// 0 disables). See `cursor::swipe_peek_for`.
+    pub swipe_peek_px: f64,
     /// See `WindowManagerConfig::touchpad_hscroll_shift_apps`.
     pub touchpad_hscroll_shift_apps: Vec<String>,
     /// Live override-redirect X11 surfaces (menus, tooltips, combo lists),
@@ -520,6 +524,7 @@ impl WindowManager {
         self.touchpad_view_apps = Vec::new();
         self.touchpad_view_swipe_tumble = false;
         self.touchpad_view_sensitivity = 1.0;
+        self.swipe_peek_px = 60.0;
         self.touchpad_view_invert = false;
         self.touchpad_hscroll_shift_apps = Vec::new();
         self.display = std::collections::HashMap::new();
@@ -600,6 +605,7 @@ impl WindowManager {
         self.touchpad_view_apps = Vec::new();
         self.touchpad_view_swipe_tumble = false;
         self.touchpad_view_sensitivity = 1.0;
+        self.swipe_peek_px = 60.0;
         self.touchpad_view_invert = false;
         self.touchpad_hscroll_shift_apps = Vec::new();
         self.display = std::collections::HashMap::new();