Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix(cursor): the touchpad view drag follows the fingers under natural scrolling
libinput flips a finger delta's sign for a natural-scrolling touchpad
before the compositor sees it. That is right for a scroll the client
receives (content follows the fingers) and wrong for the emulated view
drag, which replays the delta as pointer motion: the view follows the
pointer, so with natural scrolling on it ran against the fingers in
Houdini's viewport and network editor while the parameter pane, a plain
scroll, felt right.
view_drag_axis now asks the source device
(libinput_device_config_scroll_get_natural_scroll_enabled) and undoes the
flip; touchpad_view_invert stays a deliberate reversal on top.
pointer-scroll gains a natural token so an injected swipe can stand in
for such a device, which is how this was verified in a cce-shadow session:
fingers-down moves the synthetic pointer down under both settings.
src/cce_ctl.rs | 7 +++++--
src/server/config.rs | 3 ++-
src/server/cursor.rs | 33 +++++++++++++++++++++++++++++++++
src/server/window_manager.rs | 14 +++++++++++---
4 files changed, 51 insertions(+), 6 deletions(-)
diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index e0ecd43..fe160d2 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -86,8 +86,11 @@ fn usage(name: &str, to_stderr: bool) {
print(" pointer-location");
print(" pointer-move-to <x> <y> (layout pixels)");
print(" pointer-move-by <dx> <dy>");
- print(" pointer-scroll <dy> [dx] [finger] (positive dy scrolls down; 15 = one notch;");
- print(" pointer-scroll finger-stop finger = a two-finger swipe, ended by finger-stop)");
+ print(" pointer-scroll <dy> [dx] [finger] [natural]");
+ print(" (positive dy scrolls down; 15 = one notch; finger = a");
+ print(" two-finger swipe, ended by finger-stop; natural = from");
+ print(" a natural-scrolling touchpad, deltas already flipped)");
+ print(" pointer-scroll finger-stop");
print(" pointer-pinch <scale> [rotation] [steps] | begin | update <scale> [rotation] | end");
print(" touchpad-view-regions <x11:ID|id|app_id> clear | <x,y,w,h> ...");
print(" (limit a touchpad_view_apps drag to these window-local");
diff --git a/src/server/config.rs b/src/server/config.rs
index ee9b77f..f070484 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -418,7 +418,8 @@ 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>,
- /// Reverse the drag direction. KDL: `touchpad_view_invert (bool)true`.
+ /// 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>,
}
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 6f0aec9..a835820 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -94,6 +94,10 @@ pub struct Cursor {
pub gesture_scale: f64,
pub gesture_triggered: bool,
pub panning_gesture_active: bool,
+ /// What `pointer-scroll ... natural` sets: an injected finger scroll
+ /// (no device behind it) reads as coming from a natural-scrolling
+ /// touchpad, so the view drag's un-inversion can be exercised headlessly.
+ pub inject_natural: bool,
/// Finger-pan velocity estimate per axis (`[x, y]`, virtual units/s) and
/// the hardware timestamp of each axis's last finger event, for the
/// kinetic desktop coast on the lift.
@@ -176,6 +180,7 @@ impl Default for Cursor {
gesture_scale: 1.0,
gesture_triggered: false,
panning_gesture_active: false,
+ inject_natural: false,
pan_vel: [0.0, 0.0],
pan_last_msec: [0, 0],
pinch_zoom_active: false,
@@ -2353,6 +2358,13 @@ unsafe extern "C" fn handle_axis(listener: *mut ffi::wl_listener, data: *mut std
/// dollies (right button, distance from the log of the scale), and Ctrl
/// + swipe passes through as a plain scroll — the same modifier Houdini
/// itself assigns to "simulate the mouse wheel" in gesture mode.
+///
+/// Natural scrolling is undone here. libinput flips the sign of a finger
+/// delta before the compositor sees it, which is right for a scroll (the
+/// content follows the fingers) and wrong for a drag replayed as pointer
+/// motion: the view follows the pointer, so the pointer has to go where
+/// the fingers went. `axis_event_is_natural` asks the source device, and
+/// `touchpad_view_invert` then means "backwards" on top of that either way.
pub struct ViewDrag {
pub button: u32,
pub surface: *mut ffi::wlr_surface,
@@ -2436,6 +2448,24 @@ impl Cursor {
Some((window, result.surface, result.sx, result.sy, ratio))
}
+ /// Whether the touchpad behind an axis event has natural scrolling on,
+ /// i.e. its deltas arrive sign-flipped. An injected event has no device
+ /// and answers with `inject_natural` (see `pointer-scroll ... natural`).
+ unsafe fn axis_event_is_natural(&self, event: *const ffi::wlr_pointer_axis_event) -> bool {
+ let pointer = (*event).pointer;
+ if pointer.is_null() {
+ return self.inject_natural;
+ }
+ let dev = &mut (*pointer).base as *mut ffi::wlr_input_device;
+ if !ffi::wlr_input_device_is_libinput(dev) {
+ return false;
+ }
+ let handle = ffi::wlr_libinput_get_device_handle(dev);
+ !handle.is_null()
+ && ffi::libinput_device_config_scroll_has_natural_scroll(handle) != 0
+ && ffi::libinput_device_config_scroll_get_natural_scroll_enabled(handle) != 0
+ }
+
unsafe fn begin_view_drag(&mut self, button: u32, from_pinch: bool) -> bool {
let Some((window, surface, sx, sy, ratio)) = self.view_drag_target() else { return false };
let seat = &mut *self.seat;
@@ -2544,6 +2574,9 @@ impl Cursor {
}
let wm = &(*(*self.seat).server).wm;
let mut step = delta * wm.touchpad_view_sensitivity;
+ if self.axis_event_is_natural(event) {
+ step = -step;
+ }
if wm.touchpad_view_invert {
step = -step;
}
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 0469aac..2be9c84 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -5381,11 +5381,19 @@ impl WindowManager {
self.for_each_cursor(|cursor| cursor.inject_finger_stop());
return "ok\n".to_string();
}
- let finger = parts.last().map_or(false, |p| *p == "finger");
+ // `natural` marks the swipe as coming from a natural-scrolling
+ // touchpad: the deltas are given as libinput would deliver
+ // them (already sign-flipped), and the view drag undoes that.
+ let finger = parts.iter().any(|p| *p == "finger");
+ let natural = parts.iter().any(|p| *p == "natural");
let dy = parts[1].parse::<f64>();
- let dx = parts.get(2).filter(|p| **p != "finger").map(|v| v.parse::<f64>()).unwrap_or(Ok(0.0));
+ let dx = parts.get(2).filter(|p| **p != "finger" && **p != "natural").map(|v| v.parse::<f64>()).unwrap_or(Ok(0.0));
if let (Ok(dy), Ok(dx)) = (dy, dx) {
- self.for_each_cursor(|cursor| cursor.inject_scroll(dy, dx, finger));
+ self.for_each_cursor(|cursor| {
+ cursor.inject_natural = natural;
+ cursor.inject_scroll(dy, dx, finger);
+ cursor.inject_natural = false;
+ });
"ok\n".to_string()
} else {
"error: invalid dy or dx\n".to_string()