diff options
| author | srdusr <[email protected]> | 2025-10-03 22:59:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-10-03 22:59:00 +0200 |
| commit | 4eb36873d4ca4b0faf0e54dbeab8f5f6450dd704 (patch) | |
| tree | 657c89f7398f98b0305b9cd69889c232608f3027 /crates/wayland/src | |
| parent | d7031dd060ec12e6de334518aa75122c65794047 (diff) | |
| download | srdwm-4eb36873d4ca4b0faf0e54dbeab8f5f6450dd704.tar.gz srdwm-4eb36873d4ca4b0faf0e54dbeab8f5f6450dd704.zip | |
Make the secondary-cursor sprite opt-in and expire stale entries
Live report: a second cursor appeared uninvited and unusably (frozen,
uncontrollable) on screen. Multi-cursor Phase 1 rendered one sprite per
physical libinput pointer device that had ever reported a position, with
no way to turn it off and no expiry - so a phantom device (a real mouse's
side-button/scroll cluster enumerating as its own HID path is a common
case) that reports once and never moves again left a frozen ghost sprite
with nothing to control or dismiss it.
Adds general.multi_cursor (default false, live-settable via
`srd set multi_cursor <bool>`) and keys secondary_cursors to
(Point, Instant) so both the recording side (udev/session.rs) and the
render side (udev/render.rs) drop any entry older than
SECONDARY_CURSOR_TIMEOUT (1.5s). The "agent controls a window without
interrupting the user" use case this report also raised was never gated
on this flag - that's Multi-cursor Phase 2's pinned virtual-pointer
delivery, which never shows a visible cursor at all.
Diffstat (limited to 'crates/wayland/src')
| -rw-r--r-- | crates/wayland/src/udev/mod.rs | 49 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 27 | ||||
| -rw-r--r-- | crates/wayland/src/udev/session.rs | 31 |
3 files changed, 84 insertions, 23 deletions
diff --git a/crates/wayland/src/udev/mod.rs b/crates/wayland/src/udev/mod.rs index 5ed96e4..b0a2639 100644 --- a/crates/wayland/src/udev/mod.rs +++ b/crates/wayland/src/udev/mod.rs @@ -107,6 +107,17 @@ pub(crate) struct DrmBuffer { image: Image<'static, 'static>, } +/// How long a secondary-cursor entry (`UdevState::secondary_cursors`) is +/// trusted after its own device's last real motion event before it's +/// treated as stale and pruned/skipped - see that field's own doc +/// comment for the frozen-ghost-cursor bug this exists to close. Short +/// enough that a genuinely idle second device's sprite actually +/// disappears at a human-noticeable timescale (not "eventually, whenever +/// something else happens to touch this map"), generous enough that +/// briefly pausing mid-gesture with a real second device doesn't flicker +/// its own cursor away and back. +pub(crate) const SECONDARY_CURSOR_TIMEOUT: Duration = Duration::from_millis(1500); + /// One connector+CRTC pair srdwm scans out to - i.e. one physical monitor. /// /// Each head owns its own scanout buffers, damage tracker and flip state, @@ -209,20 +220,30 @@ pub(crate) struct UdevState { /// monitors; clamped to the union of all head rectangles. pub(crate) pointer_pos: Point<f64, Logical>, /// Multi-cursor mode, Phase 1: every physical pointer/trackpad's own - /// last-known position, keyed by its real libinput device identity - /// (`smithay::backend::input::Event::device()`, confirmed `Device: - /// PartialEq + Eq + Hash` by reading smithay's own trait definition). - /// Purely a *visual* addition - `pointer_pos` above is still the one - /// position that actually drives clicks/drags/hit-testing, updated by - /// whichever device moved most recently exactly as before, so nothing - /// about existing interactive behaviour changes. This is what lets a - /// mouse and a trackpad each show their own live cursor sprite instead - /// of only the most-recently-moved device having a visible pointer at - /// all - see `docs/TODO.md`'s "Multi-cursor" plan for what later - /// phases would still need (per-device *interaction*, not just - /// per-device *rendering*, and the real `wl_seat` ecosystem wall a - /// second seat runs into for arbitrary client content). - pub(crate) secondary_cursors: HashMap<smithay::reexports::input::Device, Point<f64, Logical>>, + /// last-known position *and when it was last actually recorded*, keyed + /// by its real libinput device identity (`smithay::backend::input:: + /// Event::device()`, confirmed `Device: PartialEq + Eq + Hash` by + /// reading smithay's own trait definition). Purely a *visual* + /// addition - `pointer_pos` above is still the one position that + /// actually drives clicks/drags/hit-testing, updated by whichever + /// device moved most recently exactly as before, so nothing about + /// existing interactive behaviour changes. + /// + /// Gated behind `WindowManager::multi_cursor_enabled` (off by + /// default) and the timestamp both exist for the same real, reported + /// bug: real hardware routinely reports what is genuinely one mouse + /// as more than one distinct libinput device (a side-button/scroll + /// cluster on its own HID path, concretely) - the first motion event + /// from that phantom device seeded a permanent entry here, rendered + /// every frame forever after at wherever the pointer happened to be + /// at that one moment, since nothing ever moved that specific device + /// identity again. Reported live as "I see two cursors and can't even + /// control the other one" - a frozen, uncontrollable ghost, exactly + /// what an unpruned entry here looks like. `render_udev_frame` now + /// skips (and `handle_libinput_event` now prunes) any entry older + /// than `SECONDARY_CURSOR_TIMEOUT`, so only a device that has *itself* + /// moved recently ever shows a sprite. + pub(crate) secondary_cursors: HashMap<smithay::reexports::input::Device, (Point<f64, Logical>, Instant)>, /// A clone of the same `LibSeatSession` `platform.rs` opened the DRM /// device with (`LibSeatSession` is cheaply `Clone` - see its own /// derive - all clones share the same underlying seat connection). diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs index 4bfe04d..dc84a2e 100644 --- a/crates/wayland/src/udev/render.rs +++ b/crates/wayland/src/udev/render.rs @@ -321,13 +321,28 @@ impl CompState { // (`cursor_status`/`cursor_buffers`) rather than each // device getting its own - a real visual distinction // between devices is a later-phase refinement, not needed - // to prove multiple live positions render at all. - let active_device = udev.secondary_cursors.iter().find(|&(_, &p)| p == pointer_pos).map(|(d, _)| d.clone()); - for (device, &pos) in &udev.secondary_cursors { - if Some(device) == active_device.as_ref() { - continue; + // to prove multiple live positions render at all. Gated + // on `general.multi_cursor` (off by default) and on each + // entry's own recency: a device that reported a position + // once and then never moved again - the real, reported + // live bug - stops rendering after `SECONDARY_CURSOR_ + // TIMEOUT` instead of sitting frozen on screen forever. + if self.wm.borrow().multi_cursor_enabled { + let now = std::time::Instant::now(); + let active_device = udev + .secondary_cursors + .iter() + .find(|&(_, &(p, _))| p == pointer_pos) + .map(|(d, _)| d.clone()); + for (device, &(pos, seen)) in &udev.secondary_cursors { + if Some(device) == active_device.as_ref() { + continue; + } + if now.duration_since(seen) >= super::SECONDARY_CURSOR_TIMEOUT { + continue; + } + custom_elements.extend(crate::cursor::render_elements(&cursor_status, &cursor_buffers, &mut udev.renderer, pos, origin, hsize)); } - custom_elements.extend(crate::cursor::render_elements(&cursor_status, &cursor_buffers, &mut udev.renderer, pos, origin, hsize)); } // Night light/reading mode - a translucent full-output // overlay, pushed right after the cursor so it colours diff --git a/crates/wayland/src/udev/session.rs b/crates/wayland/src/udev/session.rs index c80592e..e2f6e2b 100644 --- a/crates/wayland/src/udev/session.rs +++ b/crates/wayland/src/udev/session.rs @@ -319,6 +319,29 @@ pub(crate) fn register_udev_monitor(handle: &LoopHandle<'static, CompState>, sea Ok(()) } +/// Records `device`'s own live position for Multi-cursor Phase 1's +/// secondary-cursor rendering, and prunes every entry (not just this +/// device's own) older than `SECONDARY_CURSOR_TIMEOUT` - see +/// `UdevState::secondary_cursors`'s own doc comment for why both the +/// config gate and the pruning exist. A no-op, and clears the map +/// outright, when the feature is off: toggling `general.multi_cursor` +/// off live must not leave a stale sprite rendering from before the +/// toggle (`render_udev_frame` also checks this same flag, but clearing +/// here too keeps the map itself from silently growing while the +/// feature is disabled). +fn record_secondary_cursor(state: &mut CompState, device: smithay::reexports::input::Device, pos: Point<f64, Logical>) { + if !state.wm.borrow().multi_cursor_enabled { + if let Some(udev) = state.udev.as_mut() { + udev.secondary_cursors.clear(); + } + return; + } + let Some(udev) = state.udev.as_mut() else { return }; + let now = Instant::now(); + udev.secondary_cursors.insert(device, (pos, now)); + udev.secondary_cursors.retain(|_, (_, seen)| now.duration_since(*seen) < SECONDARY_CURSOR_TIMEOUT); +} + fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputBackend>) { match event { InputEvent::Keyboard { event } => handle_keyboard_key_event(state, &event), @@ -339,8 +362,10 @@ fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputB // cursors`'s own doc comment): records this specific physical // device's own position too, purely for rendering its own // cursor sprite - `pos`/`handle_pointer_position` below are - // still the one interactive position, unchanged. - udev.secondary_cursors.insert(event.device(), pos); + // still the one interactive position, unchanged. Opt-in via + // `general.multi_cursor` (`record_secondary_cursor` is a + // no-op, and clears any stale entries, when it's off). + record_secondary_cursor(state, event.device(), pos); handle_pointer_position(state, pos, event.time_msec()); } // Absolute-positioning devices (a touchscreen, a drawing tablet, @@ -368,7 +393,7 @@ fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputB udev.pointer_pos.x = (pos.x + min_x).clamp(min_x, (max_x - 1.0).max(min_x)); udev.pointer_pos.y = (pos.y + min_y).clamp(min_y, (max_y - 1.0).max(min_y)); let pos = udev.pointer_pos; - udev.secondary_cursors.insert(event.device(), pos); + record_secondary_cursor(state, event.device(), pos); handle_pointer_position(state, pos, event.time_msec()); } InputEvent::PointerButton { event } => { |