1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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);
}
|