Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix(cursor): keep an emulated view drag alive across a mid-gesture pause
A trackpad swipe in Houdini stopped being tracked partway through: the rest
of the gesture moved nothing and only lifting the fingers and swiping again
recovered it.
libinput posts nothing at all while the fingers rest on the pad. A capture of
one real swipe (libinput debug-events on the touchpad) paused 3.3 s between
its two halves — and a shorter 343 ms pause at a direction reversal — with the
zero-delta lift arriving only at the true end of the gesture. VIEW_DRAG_IDLE_MS
was 180 ms, so every such pause tore the drag down mid-swipe, and the Space
release/re-press that the resume then cost is exactly the ordering hazard
`button_down` documents: Houdini's input generator can see the press before the
release and never re-enters view mode.
The lift is what ends a drag, so the timer is now a net for a lost lift alone,
at 5 s. Because that leaves the synthetic Space and button held far longer, a
real key press ends the drag too — typing must not reach the app as a chord.
`[ViewDrag] end` gains a reason= field (lift/idle/motion/button/key/ctrl/pinch)
so the next stall is readable straight out of the session log.
Verified headless: one begin and one end reason=lift across injected 0.5 s and
1.0 s mid-gesture pauses, and end reason=idle once the lift is withheld.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/server/cursor.rs | 33 ++++++++++++++++++++++-----------
src/server/keyboard_group.rs | 12 ++++++++++++
2 files changed, 34 insertions(+), 11 deletions(-)
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index b7614f9..141fa4c 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -1132,7 +1132,7 @@ unsafe extern "C" fn handle_motion(listener: *mut ffi::wl_listener, data: *mut s
// Real pointer motion ends an emulated view drag: the client must not
// see the synthetic drag position and the true one interleaved.
if cursor.view_drag.is_some() {
- cursor.end_view_drag();
+ cursor.end_view_drag("motion");
}
let mut dx = (*event).delta_x;
@@ -1229,7 +1229,7 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
let cursor = &mut *crate::container_of!(listener, Cursor, button_listener);
let event = data as *mut ffi::wlr_pointer_button_event;
if cursor.view_drag.is_some() {
- cursor.end_view_drag();
+ cursor.end_view_drag("button");
}
let seat = &mut *cursor.seat;
@@ -2368,8 +2368,16 @@ const KEY_SPACE: u32 = 57;
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
const BTN_MIDDLE: u32 = 0x112;
-/// Finger silence that ends a swipe drag when no zero-delta event came.
-const VIEW_DRAG_IDLE_MS: i32 = 180;
+/// Safety net that ends a swipe drag when the lift event never came.
+/// Only that: libinput posts nothing at all while the fingers rest on the
+/// pad mid-gesture — a captured Houdini swipe paused 3.3 s between two
+/// halves of one scroll, the zero-delta lift arriving only at the true end
+/// — so a short timeout tears the drag down inside the gesture, and the
+/// Space release/re-press a resume then costs can drop Houdini out of view
+/// mode for the rest of the swipe (the ordering hazard `button_down`
+/// documents, now mid-swipe). The lift ends a drag; so, at once, do real
+/// pointer motion, a button and a key press.
+const VIEW_DRAG_IDLE_MS: i32 = 5000;
/// Drag distance (layout px) per e-fold of pinch scale. Measured against
/// Houdini 22 (depth of the world origin in view space, which is what a
/// dolly changes — Houdini dollies toward the point under the pointer, so
@@ -2453,9 +2461,12 @@ impl Cursor {
self.arm_view_drag_timer();
}
- pub unsafe fn end_view_drag(&mut self) {
+ /// `reason` names what ended it — "lift", "idle", "motion", "button",
+ /// "key", "ctrl" or "pinch" — so a stall reported later is diagnosable
+ /// from the session log alone.
+ pub unsafe fn end_view_drag(&mut self, reason: &str) {
let Some(d) = self.view_drag.take() else { return };
- log::info!("[ViewDrag] end button={:#x} from_pinch={} at surface ({:.0}, {:.0})", d.button, d.from_pinch, d.sx, d.sy);
+ log::info!("[ViewDrag] end reason={} button={:#x} from_pinch={} at surface ({:.0}, {:.0})", reason, d.button, d.from_pinch, d.sx, d.sy);
if !self.view_drag_timer.is_null() {
ffi::wl_event_source_timer_update(self.view_drag_timer, 0);
}
@@ -2488,7 +2499,7 @@ impl Cursor {
if delta == 0.0 {
// The fingers lifted.
if self.view_drag.is_some() {
- self.end_view_drag();
+ self.end_view_drag("lift");
return true;
}
return false;
@@ -2496,7 +2507,7 @@ impl Cursor {
if modifiers & CTRL != 0 {
// Houdini's own wheel modifier: a plain scroll.
if self.view_drag.is_some() {
- self.end_view_drag();
+ self.end_view_drag("ctrl");
}
return false;
}
@@ -2523,7 +2534,7 @@ impl Cursor {
pub unsafe fn view_drag_pinch_begin(&mut self) -> bool {
if self.view_drag.is_some() {
- self.end_view_drag();
+ self.end_view_drag("pinch");
}
self.begin_view_drag(BTN_RIGHT, true)
}
@@ -2544,7 +2555,7 @@ impl Cursor {
pub unsafe fn view_drag_pinch_end(&mut self) -> bool {
if matches!(&self.view_drag, Some(d) if d.from_pinch) {
- self.end_view_drag();
+ self.end_view_drag("lift");
return true;
}
false
@@ -2553,7 +2564,7 @@ impl Cursor {
unsafe extern "C" fn handle_view_drag_timeout(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
let cursor = &mut *(data as *mut Cursor);
- cursor.end_view_drag();
+ cursor.end_view_drag("idle");
0
}
diff --git a/src/server/keyboard_group.rs b/src/server/keyboard_group.rs
index 830874e..94c1afa 100644
--- a/src/server/keyboard_group.rs
+++ b/src/server/keyboard_group.rs
@@ -337,6 +337,18 @@ unsafe extern "C" fn handle_group_key(listener: *mut ffi::wl_listener, data: *mu
return;
}
+ // A real key press ends an emulated view drag (see `cursor::ViewDrag`):
+ // the drag holds Space and a pointer button down on the client's behalf,
+ // and a key pressed on top of that reaches the app as a chord nobody
+ // asked for. The drag's own synthetic Space goes straight to the seat and
+ // never passes through here.
+ if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED
+ && !group.seat.is_null()
+ && (*group.seat).cursor.view_drag.is_some()
+ {
+ (*group.seat).cursor.end_view_drag("key");
+ }
+
// Cancel active binding repeats
let seat_groups_head = &mut (*group.seat).keyboard_groups as *mut ffi::wl_list as *mut crate::server::WlList;
let mut curr_g = (*seat_groups_head).next;