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 /// back to its origin has near-zero displacement but a long path, and
416 /// must not be classified as a tap.
417 ///
418 /// # Returns
419 ///
420 /// - `Option<EuvGesture>` - The recognized gesture, or `None` when the
421 /// movement is too small to be either a tap or a swipe.
422 pub fn classify(
423 &self,
424 dx: f64,
425 dy: f64,
426 elapsed_millis: f64,
427 travel: f64,
428 ) -> Option<EuvGesture> {
429 let distance: f64 = (dx * dx + dy * dy).sqrt();
430 if distance <= self.get_config().get_tap_slop()
431 && travel <= self.get_config().get_tap_slop()
432 {
433 if elapsed_millis >= self.get_config().get_long_press_millis() {
434 return Some(EuvGesture::LongPress);
435 }
436 return Some(EuvGesture::Tap);
437 }
438 if distance < self.get_config().get_swipe_threshold() {
439 return None;
440 }
441 if dx.abs() >= dy.abs() {
442 Some(if dx > 0.0 {
443 EuvGesture::Right
444 } else {
445 EuvGesture::Left
446 })
447 } else {
448 Some(if dy > 0.0 {
449 EuvGesture::Down
450 } else {
451 EuvGesture::Up
452 })
453 }
454 }
455
456 /// Builds the two-finger pinch description for the current touch points.
457 ///
458 /// Returns `None` unless exactly two points are supplied, so a
459 /// one-finger drag is never mistaken for a degenerate pinch.
460 ///
461 /// # Arguments
462 ///
463 /// - `&[NativeTouchPoint]` - The active touch points, which must hold
464 /// exactly two entries.
465 /// - `f64` - The distance recorded when the pinch began, used as the
466 /// baseline for `delta`.
467 ///
468 /// # Returns
469 ///
470 /// - `Option<EuvPinch>` - The pinch description, or `None` when the point
471 /// count is not two.
472 pub fn pinch_from(&self, points: &[NativeTouchPoint], start_distance: f64) -> Option<EuvPinch> {
473 if points.len() != 2 {
474 return None;
475 }
476 let first: &NativeTouchPoint = &points[0];
477 let second: &NativeTouchPoint = &points[1];
478 let dx: f64 = f64::from(second.client_x) - f64::from(first.client_x);
479 let dy: f64 = f64::from(second.client_y) - f64::from(first.client_y);
480 let distance: f64 = (dx * dx + dy * dy).sqrt();
481 Some(EuvPinch {
482 distance,
483 start_distance,
484 center_x: f64::from(first.client_x + second.client_x) / 2.0,
485 center_y: f64::from(first.client_y + second.client_y) / 2.0,
486 delta: distance - start_distance,
487 })
488 }
489
490 /// Decides whether a pinch has grown or shrunk far enough to be worth
491 /// reporting.
492 ///
493 /// Comparing the current distance against the pinch baseline as a ratio
494 /// keeps the threshold meaningful at any zoom level, where an absolute
495 /// pixel delta would be noise when zoomed out and too small when zoomed
496 /// in.
497 ///
498 /// # Arguments
499 ///
500 /// - `f64` - Distance between the two fingers now.
501 ///
502 /// # Returns
503 ///
504 /// - `bool` - `true` when the relative change exceeds
505 /// `pinch_threshold`.
506 pub fn pinch_is_significant(&self, distance: f64, start_distance: f64) -> bool {
507 if start_distance <= 0.0 {
508 return false;
509 }
510 (distance - start_distance).abs() / start_distance
511 >= self.get_config().get_pinch_threshold()
512 }
513
514 /// Builds the single-finger drag description for the current point.
515 ///
516 /// # Arguments
517 ///
518 /// - `&GesturePoint` - The finger that is moving.
519 /// - `f64` - X of the point where this drag began.
520 ///
521 /// # Returns
522 ///
523 /// - `EuvDrag` - The drag description for this move.
524 pub fn drag_from(
525 &self,
526 point: &GesturePoint,
527 start_x: f64,
528 start_y: f64,
529 travel: f64,
530 ) -> EuvDrag {
531 let x: f64 = point.client_x;
532 let y: f64 = point.client_y;
533 EuvDrag {
534 x,
535 y,
536 delta_x: x - start_x,
537 delta_y: y - start_y,
538 travel,
539 }
540 }
541}
542
543/// Inherent implementation of [`EuvGesture`].
544impl EuvGesture {
545 /// Returns the stable lowercase token for this gesture.
546 ///
547 /// Used for telemetry and `data-gesture` attributes, so the spelling is
548 /// part of the public contract and must not change between releases.
549 ///
550 /// # Returns
551 ///
552 /// - `&'static str` - The wire name: one of `left`, `right`, `up`,
553 /// `down`, `tap`, or `long-press`.
554 pub fn name(self) -> &'static str {
555 let index: usize = match self {
556 Self::Left => 0,
557 Self::Right => 1,
558 Self::Up => 2,
559 Self::Down => 3,
560 Self::Tap => 4,
561 Self::LongPress => 5,
562 };
563 GESTURE_NAMES[index]
564 }
565}
566/// Default construction for [`EuvGestureConfig`].
567impl Default for EuvGestureConfig {
568 /// Returns the default thresholds, tuned for a finger on glass.
569 ///
570 /// 48px is roughly the width of an adult fingertip contact patch, so a
571 /// smaller travel is treated as jitter rather than intent. 500ms is the
572 /// conventional long-press boundary used by both iOS and Android.
573 ///
574 /// # Returns
575 ///
576 /// - `EuvGestureConfig` - The default threshold set.
577 fn default() -> Self {
578 Self {
579 swipe_threshold: 48.0,
580 tap_slop: 10.0,
581 long_press_millis: 500.0,
582 pinch_threshold: 0.01,
583 }
584 }
585}
586
587/// Default construction for [`EuvGestureRecognizer`].
588impl Default for EuvGestureRecognizer {
589 /// Returns a recognizer using the default thresholds.
590 ///
591 /// # Returns
592 ///
593 /// - `EuvGestureRecognizer` - A recognizer using
594 /// [`EuvGestureConfig::default`].
595 fn default() -> Self {
596 Self {
597 config: EuvGestureConfig::default(),
598 }
599 }
600}
601
602/// Inherent implementation of [`GestureProgress`].
603impl GestureProgress {
604 /// Records the origin of a new touch sequence.
605 ///
606 /// When two or more fingers land together, the inter-finger distance is
607 /// captured as the pinch baseline, so a later spread is measured against
608 /// the initial separation rather than against zero.
609 ///
610 /// # Arguments
611 ///
612 /// - `&[NativeTouchPoint]` - The touch points active at `touchstart`.
613 pub fn begin(&mut self, points: &[NativeTouchPoint]) {
614 let Some(first) = points.first() else {
615 return;
616 };
617 self.set_start_x(f64::from(first.client_x));
618 self.set_start_y(f64::from(first.client_y));
619 self.set_last_x(self.get_start_x());
620 self.set_last_y(self.get_start_y());
621 self.set_travel(0.0);
622 self.set_started_at(now_millis());
623 self.set_active(true);
624 let baseline: f64 = match points {
625 [one, two, ..] => {
626 let dx: f64 = f64::from(two.client_x - one.client_x);
627 let dy: f64 = f64::from(two.client_y - one.client_y);
628 (dx * dx + dy * dy).sqrt()
629 }
630 _ => 0.0,
631 };
632 self.set_pinch_start_distance(baseline);
633 }
634
635 /// Folds a `touchmove` reading into the running totals.
636 ///
637 /// Travel is measured segment by segment rather than from the origin so
638 /// that a finger drawing a closed loop accumulates a large distance,
639 /// which is what separates a drag from a wandering tap.
640 ///
641 /// # Arguments
642 ///
643 /// - `&[NativeTouchPoint]` - The touch points active at `touchmove`.
644 pub fn advance(&mut self, points: &[NativeTouchPoint]) {
645 let Some(first) = points.first() else {
646 return;
647 };
648 let x: f64 = f64::from(first.client_x);
649 let y: f64 = f64::from(first.client_y);
650 let step_x: f64 = x - self.get_last_x();
651 let step_y: f64 = y - self.get_last_y();
652 self.set_travel(self.get_travel() + (step_x * step_x + step_y * step_y).sqrt());
653 self.set_last_x(x);
654 self.set_last_y(y);
655 }
656
657 /// Returns the first tracked finger, or `None` when no sequence is
658 /// active.
659 ///
660 ///
661 /// # Returns
662 ///
663 /// - `Option<GesturePoint>` - The live position of the tracked finger,
664 /// or `None` when no sequence is in flight.
665 pub fn primary(&self) -> Option<GesturePoint> {
666 if !self.get_active() {
667 return None;
668 }
669 Some(GesturePoint {
670 client_x: self.get_last_x(),
671 client_y: self.get_last_y(),
672 })
673 }
674
675 /// Classifies the finished sequence and marks the cell inactive.
676 ///
677 /// A tap requires both a small straight-line displacement and a short
678 /// accumulated path. Checking only the displacement would misread a
679 /// finger that traces a closed loop and returns to its origin as a tap,
680 /// even though it travelled hundreds of pixels; the path length is what
681 /// separates the two cases.
682 ///
683 /// # Arguments
684 ///
685 /// - `&EuvGestureConfig` - The thresholds to classify against.
686 ///
687 /// # Returns
688 ///
689 /// - `Option<EuvGesture>` - The recognised gesture, or `None` when no
690 /// sequence was in flight.
691 pub fn finish(&mut self, config: &EuvGestureConfig) -> Option<EuvGesture> {
692 if !self.get_active() {
693 return None;
694 }
695 self.set_active(false);
696 let dx: f64 = self.get_last_x() - self.get_start_x();
697 let dy: f64 = self.get_last_y() - self.get_start_y();
698 let elapsed: f64 = now_millis() - self.get_started_at();
699 let distance: f64 = (dx * dx + dy * dy).sqrt();
700 if distance <= config.tap_slop && self.get_travel() <= config.tap_slop {
701 return Some(if elapsed >= config.long_press_millis {
702 EuvGesture::LongPress
703 } else {
704 EuvGesture::Tap
705 });
706 }
707 if distance < config.swipe_threshold {
708 return None;
709 }
710 Some(if dx.abs() >= dy.abs() {
711 if dx > 0.0 {
712 EuvGesture::Right
713 } else {
714 EuvGesture::Left
715 }
716 } else if dy > 0.0 {
717 EuvGesture::Down
718 } else {
719 EuvGesture::Up
720 })
721 }
722
723 /// Clears all progress, used when a sequence is cancelled.
724 ///
725 /// # Arguments
726 ///
727 pub fn reset(&mut self) {
728 *self = Self::default();
729 }
730}