GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/backend/touch.rs (12.6K)
1 // Touchscreen input (wl_touch).
2 //
3 // A cce-ui app has no touch widgets: everything it knows is a pointer that
4 // hovers, presses, drags and scrolls. So a finger is translated into those,
5 // and which one it becomes is decided by what the finger does, the way every
6 // touch surface decides it:
7 //
8 // - a TAP (down and up without leaving the slop) is a click where it landed;
9 // - a finger that MOVES past the slop scrolls whatever is under it, content
10 // following the finger, and the lift is a trackpad lift — a flick coasts
11 // through the same `ScrollMotion` a two-finger trackpad swipe does;
12 // - a finger HELD still for `HOLD_MS` and then moved is a held left button:
13 // a slider's thumb, a text selection, a drag-and-drop source.
14 //
15 // No timer is needed for the hold: nothing is sent while the finger rests
16 // inside the slop, so the decision is only taken at the first motion past
17 // it, from how long the finger had been down by then.
18 //
19 // Only the first finger is followed; others are ignored until it lifts.
20 // Before this module the compositor drove every cce-ui window by touch with
21 // an emulated pointer, so a finger drag was a held button and selected
22 // rather than scrolled. Binding wl_touch is what moves cce-ui windows onto
23 // the compositor's touch route (cce-compositor's `cursor::TouchRoute`).
24 //
25 // Not here: window moves and CSD resizes by finger. A touch serial does not
26 // satisfy the compositor's pointer-grab check, so `xdg_toplevel.move` from a
27 // touch would be refused. Overview and the Super-held adjust mode, where the
28 // compositor drives the pointer itself, move windows by finger instead.
29 //
30 // The tracker is platform-neutral, and what its actions do is the driver's
31 // (`Driver::touch`); only the `wl_touch` binding below is Wayland's.
32
33 /// Travel (logical px) a finger may wander and still be a tap or a hold.
34 pub const SLOP: f32 = 10.0;
35 /// How long a finger must rest before moving for the motion to be a drag
36 /// rather than a scroll.
37 pub const HOLD_MS: u32 = 400;
38
39 /// What a touch asks of the app, in pointer terms.
40 #[derive(Debug, Clone, Copy, PartialEq)]
41 pub enum TouchAction {
42 /// Move the hover to here (the finger arrived; nothing pressed yet).
43 Hover(f32, f32),
44 Press(f32, f32),
45 /// Motion with the button held.
46 Drag(f32, f32),
47 Release(f32, f32),
48 /// Scroll by this much finger travel, content following the finger.
49 Scroll(f32, f32),
50 /// The scrolling finger lifted: may coast.
51 ScrollEnd,
52 /// The finger is gone; no hover is left behind.
53 Leave,
54 }
55
56 #[derive(Debug, Clone, Copy, PartialEq)]
57 enum Phase {
58 /// Down, inside the slop: still a tap or a hold.
59 Pending,
60 Scrolling,
61 Dragging,
62 }
63
64 #[derive(Debug, Clone, Copy)]
65 struct Finger {
66 id: i32,
67 phase: Phase,
68 start: (f32, f32),
69 last: (f32, f32),
70 down_ms: u32,
71 }
72
73 /// The gesture of the one finger being followed. Pure: it maps touch events
74 /// to `TouchAction`s and holds no Wayland state, so it is tested directly.
75 #[derive(Debug, Default)]
76 pub struct TouchTracker {
77 finger: Option<Finger>,
78 }
79
80 impl TouchTracker {
81 pub fn down(&mut self, id: i32, x: f32, y: f32, time_ms: u32) -> Vec<TouchAction> {
82 if self.finger.is_some() {
83 return Vec::new();
84 }
85 self.finger = Some(Finger { id, phase: Phase::Pending, start: (x, y), last: (x, y), down_ms: time_ms });
86 vec![TouchAction::Hover(x, y)]
87 }
88
89 pub fn motion(&mut self, id: i32, x: f32, y: f32, time_ms: u32) -> Vec<TouchAction> {
90 let Some(f) = self.finger.as_mut().filter(|f| f.id == id) else { return Vec::new() };
91 let prev = f.last;
92 f.last = (x, y);
93 match f.phase {
94 Phase::Pending => {
95 let (dx, dy) = (x - f.start.0, y - f.start.1);
96 if dx.hypot(dy) < SLOP {
97 return Vec::new();
98 }
99 if time_ms.wrapping_sub(f.down_ms) >= HOLD_MS {
100 f.phase = Phase::Dragging;
101 vec![TouchAction::Press(f.start.0, f.start.1), TouchAction::Drag(x, y)]
102 } else {
103 // The whole travel from the down point, not just past the
104 // slop: the content stays under the finger.
105 f.phase = Phase::Scrolling;
106 vec![TouchAction::Scroll(dx, dy)]
107 }
108 }
109 Phase::Scrolling => {
110 let (dx, dy) = (x - prev.0, y - prev.1);
111 if dx == 0.0 && dy == 0.0 {
112 Vec::new()
113 } else {
114 vec![TouchAction::Scroll(dx, dy)]
115 }
116 }
117 Phase::Dragging => vec![TouchAction::Drag(x, y)],
118 }
119 }
120
121 pub fn up(&mut self, id: i32) -> Vec<TouchAction> {
122 if self.finger.is_none_or(|f| f.id != id) {
123 return Vec::new();
124 }
125 let f = self.finger.take().unwrap();
126 match f.phase {
127 Phase::Pending => vec![
128 TouchAction::Press(f.start.0, f.start.1),
129 TouchAction::Release(f.start.0, f.start.1),
130 TouchAction::Leave,
131 ],
132 Phase::Scrolling => vec![TouchAction::ScrollEnd, TouchAction::Leave],
133 Phase::Dragging => vec![TouchAction::Release(f.last.0, f.last.1), TouchAction::Leave],
134 }
135 }
136
137 /// The compositor took the sequence back (`wl_touch.cancel`). A tap that
138 /// never became anything clicks nothing; a held button is released where
139 /// it is, as a pointer leaving mid-drag is (`PointerEventKind::Leave`).
140 pub fn cancel(&mut self) -> Vec<TouchAction> {
141 let Some(f) = self.finger.take() else { return Vec::new() };
142 match f.phase {
143 Phase::Pending => vec![TouchAction::Leave],
144 Phase::Scrolling => vec![TouchAction::ScrollEnd, TouchAction::Leave],
145 Phase::Dragging => vec![TouchAction::Release(f.last.0, f.last.1), TouchAction::Leave],
146 }
147 }
148
149 pub fn id(&self) -> Option<i32> {
150 self.finger.map(|f| f.id)
151 }
152 }
153
154 /// The `wl_touch` binding: a finger's events, in the window's coordinates,
155 /// through the tracker and on to the driver.
156 #[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
157 mod wayland {
158 use smithay_client_toolkit::reexports::client::protocol::{wl_surface::WlSurface, wl_touch::WlTouch};
159 use smithay_client_toolkit::reexports::client::{Connection, QueueHandle};
160 use smithay_client_toolkit::seat::touch::TouchHandler;
161
162 use super::TouchAction;
163 use crate::backend::shell::Shell;
164 use crate::backend::window_runner::{Application, EngineState};
165
166 impl<A: Application> EngineState<A> {
167 /// A surface-local touch position in the app's window coordinates: the
168 /// forced-scale divide and the menu popup's offset, as the pointer path
169 /// applies them.
170 fn touch_pos(&self, (x, y): (f64, f64)) -> (f32, f32) {
171 let forced = crate::scale::forced_scale().unwrap_or(1.0);
172 let (ox, oy) = self.touch_offset;
173 (x as f32 / forced + ox, y as f32 / forced + oy)
174 }
175
176 /// The seat stopped offering touch: end the finger's gesture as a
177 /// cancel would, and drop the object.
178 pub(crate) fn touch_lost(&mut self) {
179 if let Some(touch) = self.touch.take() {
180 touch.release();
181 }
182 self.cancel_touch();
183 }
184
185 fn cancel_touch(&mut self) {
186 let actions = self.touch_tracker.cancel();
187 self.run_touch_actions(actions);
188 self.touch_scroll_at = None;
189 }
190
191 fn run_touch_actions(&mut self, actions: Vec<TouchAction>) {
192 let scroll_at = self.touch_scroll_at;
193 let (driver, t) = self.turn();
194 driver.touch(t, actions, scroll_at);
195 }
196 }
197
198 impl<A: Application> TouchHandler for EngineState<A> {
199 fn down(
200 &mut self,
201 _conn: &Connection,
202 _qh: &QueueHandle<Self>,
203 _touch: &WlTouch,
204 _serial: u32,
205 time: u32,
206 surface: WlSurface,
207 id: i32,
208 position: (f64, f64),
209 ) {
210 if self.touch_tracker.id().is_some() {
211 return;
212 }
213 // An event on the context menu's popup surface is the app's too, at
214 // the popup's offset from the window; fixed for the finger's life,
215 // since every later event of it is about the same surface.
216 self.touch_offset = self.menu_popup_offset(&surface).unwrap_or((0.0, 0.0));
217 let (x, y) = self.touch_pos(position);
218 // A scroll is dispatched where the finger went down: the gesture
219 // belongs to what it began on, as a trackpad scroll does
220 // (`scroll_initiate_widget_id`), however far the content moves.
221 self.touch_scroll_at = Some((x, y));
222 let actions = self.touch_tracker.down(id, x, y, time);
223 self.run_touch_actions(actions);
224 }
225
226 fn up(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _touch: &WlTouch, _serial: u32, _time: u32, id: i32) {
227 let actions = self.touch_tracker.up(id);
228 self.run_touch_actions(actions);
229 if self.touch_tracker.id().is_none() {
230 self.touch_scroll_at = None;
231 }
232 }
233
234 fn motion(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _touch: &WlTouch, time: u32, id: i32, position: (f64, f64)) {
235 if self.touch_tracker.id() != Some(id) {
236 return;
237 }
238 let (x, y) = self.touch_pos(position);
239 let actions = self.touch_tracker.motion(id, x, y, time);
240 self.run_touch_actions(actions);
241 }
242
243 fn shape(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlTouch, _: i32, _: f64, _: f64) {}
244
245 fn orientation(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlTouch, _: i32, _: f64) {}
246
247 fn cancel(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _touch: &WlTouch) {
248 self.cancel_touch();
249 }
250 }
251
252 smithay_client_toolkit::delegate_touch!(@<A: Application> EngineState<A>);
253 }
254
255 #[cfg(test)]
256 mod tests {
257 use super::*;
258 use TouchAction::*;
259
260 #[test]
261 fn a_tap_clicks_where_it_landed() {
262 let mut t = TouchTracker::default();
263 assert_eq!(t.down(1, 50.0, 60.0, 1000), vec![Hover(50.0, 60.0)]);
264 // Jitter inside the slop is nothing yet.
265 assert!(t.motion(1, 53.0, 62.0, 1050).is_empty());
266 assert_eq!(t.up(1), vec![Press(50.0, 60.0), Release(50.0, 60.0), Leave]);
267 assert_eq!(t.id(), None);
268 }
269
270 #[test]
271 fn a_quick_drag_scrolls_with_the_finger_and_coasts_on_the_lift() {
272 let mut t = TouchTracker::default();
273 t.down(1, 100.0, 300.0, 0);
274 // Past the slop: the whole travel from the down point.
275 assert_eq!(t.motion(1, 100.0, 280.0, 80), vec![Scroll(0.0, -20.0)]);
276 assert_eq!(t.motion(1, 95.0, 250.0, 100), vec![Scroll(-5.0, -30.0)]);
277 // A repeated position is not an empty scroll.
278 assert!(t.motion(1, 95.0, 250.0, 110).is_empty());
279 assert_eq!(t.up(1), vec![ScrollEnd, Leave]);
280 }
281
282 #[test]
283 fn a_hold_then_move_is_a_held_button_drag() {
284 let mut t = TouchTracker::default();
285 t.down(1, 10.0, 10.0, 5000);
286 assert!(t.motion(1, 12.0, 10.0, 5200).is_empty());
287 assert_eq!(t.motion(1, 40.0, 10.0, 5000 + HOLD_MS), vec![Press(10.0, 10.0), Drag(40.0, 10.0)]);
288 assert_eq!(t.motion(1, 60.0, 15.0, 5600), vec![Drag(60.0, 15.0)]);
289 assert_eq!(t.up(1), vec![Release(60.0, 15.0), Leave]);
290 }
291
292 #[test]
293 fn the_hold_survives_the_clock_wrapping() {
294 let mut t = TouchTracker::default();
295 t.down(1, 0.0, 0.0, u32::MAX - 100);
296 assert_eq!(t.motion(1, 30.0, 0.0, HOLD_MS)[0], Press(0.0, 0.0));
297 }
298
299 #[test]
300 fn only_the_first_finger_is_followed() {
301 let mut t = TouchTracker::default();
302 t.down(1, 0.0, 0.0, 0);
303 assert!(t.down(2, 100.0, 100.0, 10).is_empty());
304 assert!(t.motion(2, 300.0, 300.0, 20).is_empty());
305 assert!(t.up(2).is_empty());
306 assert_eq!(t.id(), Some(1));
307 // Once it lifts, a new finger is followed.
308 t.up(1);
309 assert_eq!(t.down(2, 5.0, 5.0, 30), vec![Hover(5.0, 5.0)]);
310 }
311
312 #[test]
313 fn a_cancel_clicks_nothing_and_releases_a_held_button() {
314 let mut t = TouchTracker::default();
315 t.down(1, 0.0, 0.0, 0);
316 assert_eq!(t.cancel(), vec![Leave]);
317
318 t.down(1, 0.0, 0.0, 0);
319 t.motion(1, 0.0, 50.0, 10);
320 assert_eq!(t.cancel(), vec![ScrollEnd, Leave]);
321
322 t.down(1, 0.0, 0.0, 0);
323 t.motion(1, 50.0, 0.0, HOLD_MS);
324 assert_eq!(t.cancel(), vec![Release(50.0, 0.0), Leave]);
325 assert!(t.cancel().is_empty());
326 }
327 }