srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/input/gestures.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-02-15 14:56:00 +0200
committersrdusr <[email protected]>2025-02-15 14:56:00 +0200
commit0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd (patch)
tree7672d0af277664f457c6c9462925c0005fe35dcf /crates/wayland/src/input/gestures.rs
parent413daa7ba2ea0ebd1424c024fd0566423aaea3f8 (diff)
downloadsrdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.tar.gz
srdwm-0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd.zip
Checkpoint: preserve all uncommitted rust-rewrite worktree work
Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens.
Diffstat (limited to 'crates/wayland/src/input/gestures.rs')
-rw-r--r--crates/wayland/src/input/gestures.rs131
1 files changed, 131 insertions, 0 deletions
diff --git a/crates/wayland/src/input/gestures.rs b/crates/wayland/src/input/gestures.rs
new file mode 100644
index 0000000..f88c633
--- /dev/null
+++ b/crates/wayland/src/input/gestures.rs
@@ -0,0 +1,131 @@
+//! `SUPER`+scroll and touchpad-swipe workspace switching.
+
+use crate::state::CompState;
+
+use super::keyboard::core_modifiers_from_xkb;
+use super::{notify_idle_activity, DRAG_MODIFIER};
+
+/// Modifier+scroll cycles workspaces, consuming the event.
+///
+/// Returns `true` if it handled the scroll, in which case the caller must
+/// *not* also forward it to the client. Generic over the input backend for
+/// the same reason the keyboard handler is: both backends deliver scroll
+/// through smithay's `PointerAxisEvent` trait.
+pub(crate) fn handle_workspace_scroll<B, E>(state: &mut CompState, event: &E) -> bool
+where
+ B: smithay::backend::input::InputBackend,
+ E: smithay::backend::input::PointerAxisEvent<B>,
+{
+ use smithay::backend::input::Axis;
+
+ notify_idle_activity(state);
+ if state.lock.locked {
+ return false;
+ }
+ let mods = state.seat.get_keyboard().map(|k| core_modifiers_from_xkb(&k.modifier_state()));
+ if !mods.is_some_and(|m| m.contains(DRAG_MODIFIER)) {
+ return false;
+ }
+ let Some(v) = event.amount(Axis::Vertical).filter(|v| *v != 0.0) else { return false };
+ // Scrolling down (positive) advances, matching `workspace, e+1`.
+ switch_workspace_relative(state, v > 0.0)
+}
+
+/// Switches to the next (`forward`) or previous workspace in id order,
+/// wrapping around, and fires the two follow-up broadcasts a plain
+/// `WindowManager::switch_workspace` call alone doesn't cover. The shared
+/// body behind every *relative* workspace switch - `SUPER`+scroll above,
+/// and a 3+-finger touchpad swipe (`handle_gesture_swipe_end` below) --
+/// pulled out here rather than duplicated a second time: both gaps below
+/// were found missing for the scroll gesture specifically during this same
+/// session, and nothing about either is scroll-only, so a second call site
+/// copy-pasting the same steps would have been one missed broadcast away
+/// from reintroducing the exact bug that was just fixed once already.
+/// Returns `false` (and does nothing) if there are no workspaces at all.
+fn switch_workspace_relative(state: &mut CompState, forward: bool) -> bool {
+ let mut wm = state.wm.borrow_mut();
+ let ids: Vec<_> = wm.workspaces().iter().map(|w| w.id).collect();
+ if ids.is_empty() {
+ return false;
+ }
+ let current = ids.iter().position(|&id| id == wm.current_workspace()).unwrap_or(0);
+ let next = if forward { (current + 1) % ids.len() } else { (current + ids.len() - 1) % ids.len() };
+ wm.switch_workspace(ids[next]);
+ drop(wm);
+ // Without this, the switch above is invisible: nothing shows or hides
+ // a single window for the new workspace until `main.rs`'s `sync()`
+ // runs, which only happens when a polled event sets `dirty` - see
+ // `srdwm_core::Event::WorkspaceChanged`'s doc comment. Found live-
+ // testing the unrelated `ext_workspace_v1` protocol's own `activate`
+ // request, which has the identical problem; the scroll gesture had the
+ // exact same bug already, just never one anyone traced back this far.
+ state.pending.borrow_mut().push(srdwm_core::Event::WorkspaceChanged);
+ // Same reasoning as `foreign_toplevel::send_state`'s call sites: without
+ // this, a dock's workspace pill only ever tracked switches driven
+ // through `ext_workspace_handle_v1.activate` itself, going stale the
+ // moment a gesture (or any other non-protocol trigger) changed the
+ // active workspace instead.
+ crate::workspace::broadcast_active_workspace(state);
+ true
+}
+
+/// A 3+-finger touchpad swipe just started - resets the running horizontal
+/// offset `handle_gesture_swipe_update` accumulates into, or leaves it
+/// `None` while the session is locked so a swipe over the lock screen does
+/// nothing (matching every other pointer/keyboard path's "locked: no normal
+/// handling" rule - see this module's own doc comment).
+pub(crate) fn handle_gesture_swipe_begin<B, E>(state: &mut CompState, event: &E)
+where
+ B: smithay::backend::input::InputBackend,
+ E: smithay::backend::input::GestureBeginEvent<B>,
+{
+ notify_idle_activity(state);
+ state.gesture_swipe = if state.lock.locked { None } else { Some((event.fingers(), 0.0)) };
+}
+
+/// Accumulates one update's worth of horizontal motion into the swipe
+/// started by `handle_gesture_swipe_begin` - `delta_x` is relative to the
+/// *previous* update, not a running total (see `gesture_swipe`'s own doc
+/// comment on `CompState`), so summing here is the only way to know the
+/// swipe's real total distance once it ends.
+pub(crate) fn handle_gesture_swipe_update<B, E>(state: &mut CompState, event: &E)
+where
+ B: smithay::backend::input::InputBackend,
+ E: smithay::backend::input::GestureSwipeUpdateEvent<B>,
+{
+ if let Some((_, total_dx)) = state.gesture_swipe.as_mut() {
+ *total_dx += event.delta_x();
+ }
+}
+
+/// A touchpad swipe just ended - switches workspace if it was a genuine
+/// 3+-finger swipe past `SWIPE_THRESHOLD` and wasn't cancelled (a libinput
+/// gesture is marked cancelled when it doesn't resolve to a clean single
+/// direction, e.g. the fingers moved back and forth). Below the threshold
+/// or below 3 fingers, this does nothing - the same "did you mean it"
+/// floor a mis-clicked drag gets elsewhere in this file, and 2-finger
+/// motion is already handled as ordinary scroll (`PointerAxis`) rather
+/// than reaching here at all on a correctly configured touchpad.
+///
+/// Deliberately claimed entirely by the compositor rather than forwarded to
+/// the focused client, unlike pinch/hold (forwarded as-is in
+/// `udev::session`): `wp_pointer_gestures` swipe is specifically the
+/// 3/4-finger overview-style gesture, and the handful of desktops that
+/// support it at all (GNOME, sway, Hyprland) all reserve it for workspace
+/// switching the same way - there is no real client-side consumer to lose
+/// by not forwarding it. Swipe left (negative `total_dx`) advances to the
+/// next workspace, right goes back, matching macOS's own convention for
+/// swiping between spaces.
+const SWIPE_THRESHOLD: f64 = 60.0;
+
+pub(crate) fn handle_gesture_swipe_end<B, E>(state: &mut CompState, event: &E)
+where
+ B: smithay::backend::input::InputBackend,
+ E: smithay::backend::input::GestureEndEvent<B>,
+{
+ let Some((fingers, total_dx)) = state.gesture_swipe.take() else { return };
+ if event.cancelled() || fingers < 3 || total_dx.abs() < SWIPE_THRESHOLD {
+ return;
+ }
+ switch_workspace_relative(state, total_dx < 0.0);
+}