euv_ui/component/touch/hook/impl.rs
1use super::*;
2
3/// Implementation of touch point extraction from DOM touch events.
4impl NativeTouchPoint {
5 /// Extracts all active touch points from a `TouchEvent`.
6 ///
7 /// Iterates over the `touches` list of the given `TouchEvent` and
8 /// builds a `Vec<NativeTouchPoint>` with each touch point's
9 /// identifier, viewport coordinates, screen coordinates, page
10 /// coordinates, and offset coordinates relative to the target element.
11 ///
12 /// The offset coordinates (`offset_x`, `offset_y`) are computed by
13 /// subtracting the target element's bounding rect from the touch's
14 /// client coordinates, since the browser `Touch` object does not
15 /// provide `offsetX`/`offsetY` directly.
16 ///
17 /// Uses web-sys typed getters (`TouchEvent::touches()`,
18 /// `TouchList::get`, `Touch::client_x()`) instead of
19 /// `Reflect::get(event, "clientX")`. The Reflect path allocates a
20 /// `JsValue::from_str` per field per touch (7 fields × N touches per
21 /// event) on every `touchmove` (60-120Hz); the typed getters skip
22 /// the string lookup and the per-field JS string allocation.
23 ///
24 /// # Arguments
25 ///
26 /// - `&Event` - The native DOM touch event.
27 ///
28 /// # Returns
29 ///
30 /// - `Vec<NativeTouchPoint>` - All currently active touch points.
31 pub fn extract_all(event: &Event) -> Vec<NativeTouchPoint> {
32 let touch_event: &TouchEvent = event.unchecked_ref::<TouchEvent>();
33 let touches: TouchList = touch_event.touches();
34 let target: JsValue = event
35 .target()
36 .map_or(JsValue::NULL, |event_target: EventTarget| {
37 event_target.into()
38 });
39 let element: Element = target.unchecked_into();
40 let rect: DomRect = element.get_bounding_client_rect();
41 let rect_left: f64 = rect.left();
42 let rect_top: f64 = rect.top();
43 let length: u32 = touches.length();
44 (0..length)
45 .filter_map(|index: u32| touches.get(index))
46 .map(|touch: Touch| {
47 let identifier: i32 = touch.identifier();
48 let client_x: i32 = touch.client_x();
49 let client_y: i32 = touch.client_y();
50 let screen_x: i32 = touch.screen_x();
51 let screen_y: i32 = touch.screen_y();
52 let page_x: i32 = touch.page_x();
53 let page_y: i32 = touch.page_y();
54 let offset_x: i32 = (client_x as f64 - rect_left).round() as i32;
55 let offset_y: i32 = (client_y as f64 - rect_top).round() as i32;
56 NativeTouchPoint {
57 identifier,
58 client_x,
59 client_y,
60 screen_x,
61 screen_y,
62 offset_x,
63 offset_y,
64 page_x,
65 page_y,
66 }
67 })
68 .collect()
69 }
70
71 /// Extracts the changed touch points from a `TouchEvent`.
72 ///
73 /// The `changedTouches` list contains touch points that have changed
74 /// since the last touch event:
75 /// - For `touchstart` - newly added touch points.
76 /// - For `touchmove` - touch points that have moved.
77 /// - For `touchend` / `touchcancel` - removed touch points.
78 ///
79 /// This is useful for determining which specific fingers were lifted
80 /// in a `touchend` event, since the `touches` list no longer contains
81 /// them.
82 ///
83 /// Uses web-sys typed getters (`TouchEvent::changed_touches()`,
84 /// `TouchList::get`, `Touch::client_x()`) to avoid the per-field
85 /// `Reflect::get` + `JsValue::from_str` allocation cost on the hot
86 /// `touchmove` path.
87 ///
88 /// # Arguments
89 ///
90 /// - `&Event` - The native DOM touch event.
91 ///
92 /// # Returns
93 ///
94 /// - `Vec<NativeTouchPoint>` - The touch points that changed in this event.
95 pub fn extract_changed(event: &Event) -> Vec<NativeTouchPoint> {
96 let touch_event: &TouchEvent = event.unchecked_ref::<TouchEvent>();
97 let touches: TouchList = touch_event.changed_touches();
98 let target: JsValue = event
99 .target()
100 .map_or(JsValue::NULL, |event_target: EventTarget| {
101 event_target.into()
102 });
103 let element: Element = target.unchecked_into();
104 let rect: DomRect = element.get_bounding_client_rect();
105 let rect_left: f64 = rect.left();
106 let rect_top: f64 = rect.top();
107 let length: u32 = touches.length();
108 (0..length)
109 .filter_map(|index: u32| touches.get(index))
110 .map(|touch: Touch| {
111 let identifier: i32 = touch.identifier();
112 let client_x: i32 = touch.client_x();
113 let client_y: i32 = touch.client_y();
114 let screen_x: i32 = touch.screen_x();
115 let screen_y: i32 = touch.screen_y();
116 let page_x: i32 = touch.page_x();
117 let page_y: i32 = touch.page_y();
118 let offset_x: i32 = (client_x as f64 - rect_left).round() as i32;
119 let offset_y: i32 = (client_y as f64 - rect_top).round() as i32;
120 NativeTouchPoint {
121 identifier,
122 client_x,
123 client_y,
124 screen_x,
125 screen_y,
126 offset_x,
127 offset_y,
128 page_x,
129 page_y,
130 }
131 })
132 .collect()
133 }
134}
135
136/// Implementation of high-precision touch point extraction from DOM touch events.
137impl NativeTouchPointF64 {
138 /// Extracts all active touch points with high-precision `f64` offset coordinates
139 /// from a `TouchEvent`.
140 ///
141 /// Similar to `NativeTouchPoint::extract_all`, but returns `f64` precision for
142 /// offset/client coordinates, which is essential for canvas drawing
143 /// and other pixel-precise interactions.
144 ///
145 /// Uses web-sys typed getters (`TouchEvent::touches()`,
146 /// `TouchList::get`, `Touch::client_x()`) instead of
147 /// `Reflect::get(event, "clientX")`. web-sys `Touch` exposes
148 /// `client_x`/`page_x`/etc. as `i32`; we widen to `f64` to preserve
149 /// the `NativeTouchPointF64` high-precision contract without losing
150 /// sub-pixel information on the offset computation.
151 ///
152 /// # Arguments
153 ///
154 /// - `&Event` - The native DOM touch event.
155 ///
156 /// # Returns
157 ///
158 /// - `Vec<NativeTouchPointF64>` - All currently active touch points with `f64` coordinates.
159 pub fn extract_all(event: &Event) -> Vec<NativeTouchPointF64> {
160 let touch_event: &TouchEvent = event.unchecked_ref::<TouchEvent>();
161 let touches: TouchList = touch_event.touches();
162 let target: JsValue = event
163 .target()
164 .map_or(JsValue::NULL, |event_target: EventTarget| {
165 event_target.into()
166 });
167 let element: Element = target.unchecked_into();
168 let rect: DomRect = element.get_bounding_client_rect();
169 let rect_left: f64 = rect.left();
170 let rect_top: f64 = rect.top();
171 let length: u32 = touches.length();
172 (0..length)
173 .filter_map(|index: u32| touches.get(index))
174 .map(|touch: Touch| {
175 let identifier: i32 = touch.identifier();
176 let client_x: f64 = touch.client_x() as f64;
177 let client_y: f64 = touch.client_y() as f64;
178 let screen_x: f64 = touch.screen_x() as f64;
179 let screen_y: f64 = touch.screen_y() as f64;
180 let page_x: f64 = touch.page_x() as f64;
181 let page_y: f64 = touch.page_y() as f64;
182 let offset_x: f64 = client_x - rect_left;
183 let offset_y: f64 = client_y - rect_top;
184 NativeTouchPointF64 {
185 identifier,
186 client_x,
187 client_y,
188 screen_x,
189 screen_y,
190 offset_x,
191 offset_y,
192 page_x,
193 page_y,
194 }
195 })
196 .collect()
197 }
198}
199
200/// Implementation of gesture recognition over raw touch points.
201///
202/// The recognizer is a pure state machine: it is fed touch point lists and
203/// timestamps and reports what it decides. It never touches the DOM itself,
204/// which keeps the classification rules testable without a browser.
205impl EuvGestureRecognizer {
206 /// Creates a recognizer with the default thresholds.
207 ///
208 /// Equivalent to [`EuvGestureRecognizer::default`]; the inherent form
209 /// exists so callers read as `EuvGestureRecognizer::new()` rather than
210 /// reaching for the trait.
211 ///
212 /// # Returns
213 ///
214 /// - `EuvGestureRecognizer` - A recognizer using
215 /// [`EuvGestureConfig::default`].
216 pub fn new() -> EuvGestureRecognizer {
217 Self::default()
218 }
219
220 /// Creates a recognizer with caller-supplied thresholds.
221 ///
222 /// # Arguments
223 ///
224 /// - `EuvGestureConfig` - The thresholds to classify against.
225 ///
226 /// # Returns
227 ///
228 /// - `EuvGestureRecognizer` - A recognizer using the given thresholds.
229 pub fn with_config(config: EuvGestureConfig) -> EuvGestureRecognizer {
230 EuvGestureRecognizer { config }
231 }
232
233 /// Mounts the reactive signals and returns the handlers that drive them.
234 ///
235 /// Wire the returned handlers to a single element in `html!`:
236 ///
237 /// ```ignore
238 /// let state: EuvGestureState = EuvGestureRecognizer::new().use_gesture();
239 /// div {
240 /// ontouchstart: state.on_start
241 /// ontouchmove: state.on_move
242 /// ontouchend: state.on_end
243 /// ontouchcancel: state.on_cancel
244 /// }
245 /// ```
246 ///
247 /// Per-move bookkeeping lives in a non-reactive cell so a 120Hz
248 /// `touchmove` does not allocate; only `last_gesture` is written when a
249 /// gesture completes.
250 ///
251 /// A long press is resolved on `touchend` from the measured elapsed time
252 /// rather than by a timer, so a press that is released after the
253 /// threshold still reports `LongPress` and a cancelled press reports
254 /// nothing.
255 ///
256 /// # Returns
257 ///
258 /// - `EuvGestureState` - The reactive signals plus the four handlers.
259 pub fn use_gesture(self) -> EuvGestureState {
260 let last_gesture: Signal<Option<EuvGesture>> = App::use_signal(|| None);
261 let drag: Signal<Option<EuvDrag>> = App::use_signal(|| None);
262 let pinch: Signal<Option<EuvPinch>> = App::use_signal(|| None);
263 let progress: Rc<RefCell<GestureProgress>> =
264 Rc::new(RefCell::new(GestureProgress::default()));
265 let recognizer: EuvGestureRecognizer = self;
266 let start_progress: Rc<RefCell<GestureProgress>> = Rc::clone(&progress);
267 let start_gesture: Signal<Option<EuvGesture>> = last_gesture;
268 let start_drag: Signal<Option<EuvDrag>> = drag;
269 let start_pinch: Signal<Option<EuvPinch>> = pinch;
270 let on_start: Option<Rc<dyn Fn(Event)>> = Some(Rc::new(move |event: Event| {
271 Self::suppress_page_scroll(&event);
272 let points: Vec<NativeTouchPoint> = NativeTouchPoint::extract_all(&event);
273 if points.is_empty() {
274 return;
275 }
276 // All four handlers share this one cell, and the browser dispatches
277 // touch events independently of one another, so a `touchstart` can
278 // land while a `touchmove` frame still holds the guard. A contended
279 // begin is skipped: the next `touchstart` re-seeds every field, so
280 // the only loss is the current sequence's origin.
281 if let Ok(mut slot) = start_progress.try_borrow_mut() {
282 slot.begin(&points);
283 }
284 start_gesture.set(None);
285 start_drag.set(None);
286 if let Some(reading) = recognizer.pinch_from(&points, recognizer.config.swipe_threshold)
287 {
288 start_pinch.set(Some(reading));
289 }
290 }));
291 let move_progress: Rc<RefCell<GestureProgress>> = Rc::clone(&progress);
292 let move_drag: Signal<Option<EuvDrag>> = drag;
293 let move_pinch: Signal<Option<EuvPinch>> = pinch;
294 let move_recognizer: EuvGestureRecognizer = self;
295 let on_move: Option<Rc<dyn Fn(Event)>> = Some(Rc::new(move |event: Event| {
296 Self::suppress_page_scroll(&event);
297 let points: Vec<NativeTouchPoint> = NativeTouchPoint::extract_all(&event);
298 if points.is_empty() {
299 return;
300 }
301 // Read everything out of the cell, drop the guard, and only then
302 // touch the signals: a `Signal::set` runs listeners synchronously and
303 // a listener may re-enter this handler, and borrowing across it would
304 // abort the WASM instance with an already-borrowed panic. A contended
305 // cell drops this frame's reading; the next `touchmove` at 60-120Hz
306 // recomputes it, so the drag and pinch signals are one frame stale
307 // rather than the app being aborted.
308 let Ok(mut slot) = move_progress.try_borrow_mut() else {
309 return;
310 };
311 slot.advance(&points);
312 let readings: (Option<EuvDrag>, Option<EuvPinch>) = {
313 let drag_reading: Option<EuvDrag> = slot.primary().map(|point: GesturePoint| {
314 move_recognizer.drag_from(
315 &point,
316 slot.get_start_x(),
317 slot.get_start_y(),
318 *slot.get_travel(),
319 )
320 });
321 let pinch_reading: Option<EuvPinch> = move_recognizer
322 .pinch_from(&points, slot.get_pinch_start_distance())
323 .filter(|candidate: &EuvPinch| {
324 move_recognizer.pinch_is_significant(
325 candidate.distance,
326 slot.get_pinch_start_distance(),
327 )
328 });
329 (drag_reading, pinch_reading)
330 };
331 drop(slot);
332 if let Some(reading) = readings.0 {
333 move_drag.set(Some(reading));
334 }
335 if let Some(reading) = readings.1 {
336 move_pinch.set(Some(reading));
337 }
338 }));
339 let end_progress: Rc<RefCell<GestureProgress>> = Rc::clone(&progress);
340 let end_gesture: Signal<Option<EuvGesture>> = last_gesture;
341 let end_drag: Signal<Option<EuvDrag>> = drag;
342 let end_pinch: Signal<Option<EuvPinch>> = pinch;
343 let end_recognizer: EuvGestureRecognizer = self;
344 let on_end: Option<Rc<dyn Fn(Event)>> = Some(Rc::new(move |_: Event| {
345 // `finish` reads the clock through `now_millis`, so the guard must
346 // not outlive this block. A contended cell reports "no gesture" and
347 // the next `touchstart` re-seeds the cell.
348 let outcome: Option<EuvGesture> = match end_progress.try_borrow_mut() {
349 Ok(mut slot) => {
350 let result: Option<EuvGesture> = slot.finish(&end_recognizer.config);
351 slot.reset();
352 result
353 }
354 Err(_) => None,
355 };
356 end_drag.set(None);
357 end_pinch.set(None);
358 if let Some(gesture) = outcome {
359 end_gesture.set(Some(gesture));
360 }
361 }));
362 let cancel_progress: Rc<RefCell<GestureProgress>> = Rc::clone(&progress);
363 let cancel_drag: Signal<Option<EuvDrag>> = drag;
364 let cancel_pinch: Signal<Option<EuvPinch>> = pinch;
365 let on_cancel: Option<Rc<dyn Fn(Event)>> = Some(Rc::new(move |_: Event| {
366 // Best-effort reset. A contended cell means a `touchmove` frame is
367 // mid-flight and will read the same cell on its way out; the reset
368 // is recomputed by the next `touchstart`, so skipping it costs one
369 // stale gesture. `borrow_mut` would abort the instance.
370 if let Ok(mut slot) = cancel_progress.try_borrow_mut() {
371 slot.reset();
372 }
373 cancel_drag.set(None);
374 cancel_pinch.set(None);
375 }));
376 EuvGestureState {
377 last_gesture,
378 drag,
379 pinch,
380 on_start,
381 on_move,
382 on_end,
383 on_cancel,
384 }
385 }
386
387 /// Calls `prevent_default` on a touch event so the browser does not also
388 /// scroll or zoom the page under a gesture.
389 ///
390 /// Guarded on `cancelable` because a passive or already-dispatched event
391 /// rejects the call.
392 ///
393 /// # Arguments
394 ///
395 /// - `&Event` - The touch event to cancel default handling on.
396 fn suppress_page_scroll(event: &Event) {
397 if event.cancelable() {
398 event.prevent_default();
399 }
400 }
401
402 /// Decides which single-finger gesture, if any, a finished touch
403 /// represents.
404 ///
405 /// A touch is a tap or long press only when the straight-line distance
406 /// from start to end is within `tap_slop`; that is deliberately separate
407 /// from the path length a drag accumulates, so a finger that wanders in
408 /// a small loop is not mistaken for a tap. Otherwise the dominant axis
409 /// of the displacement wins, and the gesture only fires past
410 /// `swipe_threshold`.
411 ///
412 /// # Arguments
413 ///
414 /// - `f64` - X displacement from start to end, in CSS pixels.
415 /// - `f64` - Y displacement from start to end, in CSS pixels.
416 /// - `f64` - Elapsed time from `touchstart` to `touchend`, in
417 /// milliseconds.
418 /// - `f64` - Path length travelled, in CSS pixels. A finger that loops
419 /// back to its origin has near-zero displacement but a long path, and
420 /// must not be classified as a tap.
421 ///
422 /// # Returns
423 ///
424 /// - `Option<EuvGesture>` - The recognized gesture, or `None` when the
425 /// movement is too small to be either a tap or a swipe.
426 pub fn classify(
427 &self,
428 dx: f64,
429 dy: f64,
430 elapsed_millis: f64,
431 travel: f64,
432 ) -> Option<EuvGesture> {
433 let distance: f64 = (dx * dx + dy * dy).sqrt();
434 if distance <= self.get_config().get_tap_slop()
435 && travel <= self.get_config().get_tap_slop()
436 {
437 if elapsed_millis >= self.get_config().get_long_press_millis() {
438 return Some(EuvGesture::LongPress);
439 }
440 return Some(EuvGesture::Tap);
441 }
442 if distance < self.get_config().get_swipe_threshold() {
443 return None;
444 }
445 if dx.abs() >= dy.abs() {
446 Some(if dx > 0.0 {
447 EuvGesture::Right
448 } else {
449 EuvGesture::Left
450 })
451 } else {
452 Some(if dy > 0.0 {
453 EuvGesture::Down
454 } else {
455 EuvGesture::Up
456 })
457 }
458 }
459
460 /// Builds the two-finger pinch description for the current touch points.
461 ///
462 /// Returns `None` unless exactly two points are supplied, so a
463 /// one-finger drag is never mistaken for a degenerate pinch.
464 ///
465 /// # Arguments
466 ///
467 /// - `&[NativeTouchPoint]` - The active touch points, which must hold
468 /// exactly two entries.
469 /// - `f64` - The distance recorded when the pinch began, used as the
470 /// baseline for `delta`.
471 ///
472 /// # Returns
473 ///
474 /// - `Option<EuvPinch>` - The pinch description, or `None` when the point
475 /// count is not two.
476 pub fn pinch_from(&self, points: &[NativeTouchPoint], start_distance: f64) -> Option<EuvPinch> {
477 if points.len() != 2 {
478 return None;
479 }
480 let first: &NativeTouchPoint = &points[0];
481 let second: &NativeTouchPoint = &points[1];
482 let dx: f64 = f64::from(second.client_x) - f64::from(first.client_x);
483 let dy: f64 = f64::from(second.client_y) - f64::from(first.client_y);
484 let distance: f64 = (dx * dx + dy * dy).sqrt();
485 Some(EuvPinch {
486 distance,
487 start_distance,
488 center_x: f64::from(first.client_x + second.client_x) / 2.0,
489 center_y: f64::from(first.client_y + second.client_y) / 2.0,
490 delta: distance - start_distance,
491 })
492 }
493
494 /// Decides whether a pinch has grown or shrunk far enough to be worth
495 /// reporting.
496 ///
497 /// Comparing the current distance against the pinch baseline as a ratio
498 /// keeps the threshold meaningful at any zoom level, where an absolute
499 /// pixel delta would be noise when zoomed out and too small when zoomed
500 /// in.
501 ///
502 /// # Arguments
503 ///
504 /// - `f64` - Distance between the two fingers now.
505 /// - `f64` - Distance between the two fingers when the pinch began.
506 ///
507 /// # Returns
508 ///
509 /// - `bool` - `true` when the relative change exceeds
510 /// `pinch_threshold`.
511 pub fn pinch_is_significant(&self, distance: f64, start_distance: f64) -> bool {
512 if start_distance <= 0.0 {
513 return false;
514 }
515 (distance - start_distance).abs() / start_distance
516 >= self.get_config().get_pinch_threshold()
517 }
518
519 /// Builds the single-finger drag description for the current point.
520 ///
521 /// # Arguments
522 ///
523 /// - `&GesturePoint` - The finger that is moving.
524 /// - `f64` - X of the point where this drag began.
525 /// - `f64` - Y of the point where this drag began.
526 /// - `f64` - Path length accumulated so far, in CSS pixels.
527 ///
528 /// # Returns
529 ///
530 /// - `EuvDrag` - The drag description for this move.
531 pub fn drag_from(
532 &self,
533 point: &GesturePoint,
534 start_x: f64,
535 start_y: f64,
536 travel: f64,
537 ) -> EuvDrag {
538 let x: f64 = point.client_x;
539 let y: f64 = point.client_y;
540 EuvDrag {
541 x,
542 y,
543 delta_x: x - start_x,
544 delta_y: y - start_y,
545 travel,
546 }
547 }
548}
549
550impl EuvGesture {
551 /// Returns the stable lowercase token for this gesture.
552 ///
553 /// Used for telemetry and `data-gesture` attributes, so the spelling is
554 /// part of the public contract and must not change between releases.
555 ///
556 /// # Returns
557 ///
558 /// - `&'static str` - The wire name: one of `left`, `right`, `up`,
559 /// `down`, `tap`, or `long-press`.
560 pub fn name(self) -> &'static str {
561 let index: usize = match self {
562 Self::Left => 0,
563 Self::Right => 1,
564 Self::Up => 2,
565 Self::Down => 3,
566 Self::Tap => 4,
567 Self::LongPress => 5,
568 };
569 GESTURE_NAMES[index]
570 }
571}
572impl Default for EuvGestureConfig {
573 /// Returns the default thresholds, tuned for a finger on glass.
574 ///
575 /// 48px is roughly the width of an adult fingertip contact patch, so a
576 /// smaller travel is treated as jitter rather than intent. 500ms is the
577 /// conventional long-press boundary used by both iOS and Android.
578 ///
579 /// # Returns
580 ///
581 /// - `EuvGestureConfig` - The default threshold set.
582 fn default() -> Self {
583 Self {
584 swipe_threshold: 48.0,
585 tap_slop: 10.0,
586 long_press_millis: 500.0,
587 pinch_threshold: 0.01,
588 }
589 }
590}
591
592impl Default for EuvGestureRecognizer {
593 /// Returns a recognizer using the default thresholds.
594 ///
595 /// # Returns
596 ///
597 /// - `EuvGestureRecognizer` - A recognizer using
598 /// [`EuvGestureConfig::default`].
599 fn default() -> Self {
600 Self {
601 config: EuvGestureConfig::default(),
602 }
603 }
604}
605
606impl GestureProgress {
607 /// Records the origin of a new touch sequence.
608 ///
609 /// When two or more fingers land together, the inter-finger distance is
610 /// captured as the pinch baseline, so a later spread is measured against
611 /// the initial separation rather than against zero.
612 ///
613 /// # Arguments
614 ///
615 /// - `&[NativeTouchPoint]` - The touch points active at `touchstart`.
616 pub fn begin(&mut self, points: &[NativeTouchPoint]) {
617 let Some(first) = points.first() else {
618 return;
619 };
620 self.set_start_x(f64::from(first.client_x));
621 self.set_start_y(f64::from(first.client_y));
622 self.set_last_x(self.get_start_x());
623 self.set_last_y(self.get_start_y());
624 self.set_travel(0.0);
625 self.set_started_at(now_millis());
626 self.set_active(true);
627 let baseline: f64 = match points {
628 [one, two, ..] => {
629 let dx: f64 = f64::from(two.client_x - one.client_x);
630 let dy: f64 = f64::from(two.client_y - one.client_y);
631 (dx * dx + dy * dy).sqrt()
632 }
633 _ => 0.0,
634 };
635 self.set_pinch_start_distance(baseline);
636 }
637
638 /// Folds a `touchmove` reading into the running totals.
639 ///
640 /// Travel is measured segment by segment rather than from the origin so
641 /// that a finger drawing a closed loop accumulates a large distance,
642 /// which is what separates a drag from a wandering tap.
643 ///
644 /// # Arguments
645 ///
646 /// - `&[NativeTouchPoint]` - The touch points active at `touchmove`.
647 pub fn advance(&mut self, points: &[NativeTouchPoint]) {
648 let Some(first) = points.first() else {
649 return;
650 };
651 let x: f64 = f64::from(first.client_x);
652 let y: f64 = f64::from(first.client_y);
653 let step_x: f64 = x - self.get_last_x();
654 let step_y: f64 = y - self.get_last_y();
655 self.set_travel(self.get_travel() + (step_x * step_x + step_y * step_y).sqrt());
656 self.set_last_x(x);
657 self.set_last_y(y);
658 }
659
660 /// Returns the first tracked finger, or `None` when no sequence is
661 /// active.
662 ///
663 ///
664 /// # Returns
665 ///
666 /// - `Option<GesturePoint>` - The live position of the tracked finger,
667 /// or `None` when no sequence is in flight.
668 pub fn primary(&self) -> Option<GesturePoint> {
669 if !self.get_active() {
670 return None;
671 }
672 Some(GesturePoint {
673 client_x: self.get_last_x(),
674 client_y: self.get_last_y(),
675 })
676 }
677
678 /// Classifies the finished sequence and marks the cell inactive.
679 ///
680 /// A tap requires both a small straight-line displacement and a short
681 /// accumulated path. Checking only the displacement would misread a
682 /// finger that traces a closed loop and returns to its origin as a tap,
683 /// even though it travelled hundreds of pixels; the path length is what
684 /// separates the two cases.
685 ///
686 /// # Arguments
687 ///
688 /// - `&EuvGestureConfig` - The thresholds to classify against.
689 ///
690 /// # Returns
691 ///
692 /// - `Option<EuvGesture>` - The recognised gesture, or `None` when no
693 /// sequence was in flight.
694 pub fn finish(&mut self, config: &EuvGestureConfig) -> Option<EuvGesture> {
695 if !self.get_active() {
696 return None;
697 }
698 self.set_active(false);
699 let dx: f64 = self.get_last_x() - self.get_start_x();
700 let dy: f64 = self.get_last_y() - self.get_start_y();
701 let elapsed: f64 = now_millis() - self.get_started_at();
702 let distance: f64 = (dx * dx + dy * dy).sqrt();
703 if distance <= config.tap_slop && *self.get_travel() <= config.tap_slop {
704 return Some(if elapsed >= config.long_press_millis {
705 EuvGesture::LongPress
706 } else {
707 EuvGesture::Tap
708 });
709 }
710 if distance < config.swipe_threshold {
711 return None;
712 }
713 Some(if dx.abs() >= dy.abs() {
714 if dx > 0.0 {
715 EuvGesture::Right
716 } else {
717 EuvGesture::Left
718 }
719 } else if dy > 0.0 {
720 EuvGesture::Down
721 } else {
722 EuvGesture::Up
723 })
724 }
725
726 /// Clears all progress, used when a sequence is cancelled.
727 ///
728 /// # Arguments
729 ///
730 pub fn reset(&mut self) {
731 *self = Self::default();
732 }
733}