git.lucas.co / cce-ui
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 }