1use std::collections::VecDeque;
10use std::mem;
11use std::time::Duration;
12
13use scheduler::Instant;
14use smallvec::SmallVec;
15
16use crate::{
17 Axis, GestureEvent, InputEvent, IsZero, Modifiers, MouseButton, MouseDownEvent, MouseEvent,
18 MouseUpEvent, Pixels, PlatformInput, Point, ScrollDelta, ScrollEvent, ScrollSource, TouchEvent,
19 TouchId, TouchPhase, point, px, seal::Sealed,
20};
21
22const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
23
24fn dominant_axis(delta: Point<Pixels>) -> Axis {
25 if delta.x.abs() <= delta.y.abs() {
26 Axis::Vertical
27 } else {
28 Axis::Horizontal
29 }
30}
31
32fn lock_delta_to_axis(delta: &mut Point<Pixels>, axis: Axis) {
33 match axis {
34 Axis::Vertical => delta.x = Pixels::ZERO,
35 Axis::Horizontal => delta.y = Pixels::ZERO,
36 }
37}
38
39fn movements_oppose(left: Point<Pixels>, right: Point<Pixels>) -> bool {
40 f32::from(left.x) * f32::from(right.x) + f32::from(left.y) * f32::from(right.y) < 0.
41}
42
43#[derive(Clone, Copy, Debug, Default)]
45pub struct OngoingScroll {
46 last_event: Option<Instant>,
47 axis: Option<Axis>,
48}
49
50impl OngoingScroll {
51 pub fn filter(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase) {
56 self.filter_at(delta, touch_phase, Instant::now())
57 }
58
59 fn filter_at(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase, now: Instant) {
60 const UNLOCK_PERCENT: f32 = 1.9;
61 const UNLOCK_LOWER_BOUND: Pixels = px(6.);
62
63 if matches!(touch_phase, TouchPhase::Ended | TouchPhase::Cancelled) {
64 self.last_event = None;
65 self.axis = None;
66 return;
67 }
68
69 let x = delta.x.abs();
70 let y = delta.y.abs();
71 if x.is_zero() && y.is_zero() {
72 if touch_phase == TouchPhase::Started {
73 self.last_event = None;
74 self.axis = None;
75 }
76 return;
77 }
78
79 let starts_new_gesture = touch_phase == TouchPhase::Started
80 || self
81 .last_event
82 .is_none_or(|last_event| now.duration_since(last_event) >= SCROLL_EVENT_SEPARATION);
83 let mut axis = self.axis;
84 if starts_new_gesture {
85 axis = Some(dominant_axis(*delta));
86 } else if x.max(y) >= UNLOCK_LOWER_BOUND {
87 match axis {
88 Some(Axis::Vertical) if x > y && x >= y * UNLOCK_PERCENT => {
89 axis = None;
90 }
91 Some(Axis::Horizontal) if y > x && y >= x * UNLOCK_PERCENT => {
92 axis = None;
93 }
94 _ => {}
95 }
96 }
97
98 self.last_event = Some(now);
99 self.axis = axis;
100 if let Some(axis) = axis {
101 lock_delta_to_axis(delta, axis);
102 }
103 }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct GestureTuning {
111 pub touch_slop: Pixels,
114 pub multi_tap_interval: Duration,
116 pub multi_tap_slop: Pixels,
118 pub long_press_duration: Duration,
121 pub scroll_physics: ScrollPhysics,
123 pub min_fling_velocity: f32,
126}
127
128impl Default for GestureTuning {
129 fn default() -> Self {
130 Self {
131 touch_slop: px(8.),
132 multi_tap_interval: Duration::from_millis(400),
133 multi_tap_slop: px(16.),
134 long_press_duration: Duration::from_millis(500),
135 scroll_physics: ScrollPhysics::ios(),
136 min_fling_velocity: 50.,
137 }
138 }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq)]
147pub enum ScrollPhysics {
148 Exponential {
151 decay_per_ms: f32,
154 },
155 FrictionSpline {
160 friction: f32,
163 pixels_per_inch: f32,
168 },
169}
170
171impl ScrollPhysics {
172 pub fn ios() -> Self {
174 Self::Exponential {
175 decay_per_ms: 0.998,
176 }
177 }
178
179 pub fn android() -> Self {
186 Self::FrictionSpline {
187 friction: 0.015,
188 pixels_per_inch: 160.,
189 }
190 }
191
192 fn fling_duration(self, speed: f32) -> Duration {
195 match self {
196 Self::Exponential { decay_per_ms } => {
197 if speed <= MOMENTUM_STOP_VELOCITY {
198 return Duration::ZERO;
199 }
200 let milliseconds = (MOMENTUM_STOP_VELOCITY / speed).ln() / decay_per_ms.ln();
201 Duration::from_secs_f32(milliseconds / 1000.)
202 }
203 Self::FrictionSpline {
204 friction,
205 pixels_per_inch,
206 } => {
207 if speed <= 0. {
208 return Duration::ZERO;
209 }
210 let deceleration = friction_spline::deceleration(speed, friction, pixels_per_inch);
211 let seconds = (deceleration / (friction_spline::deceleration_rate() - 1.)).exp();
212 Duration::from_secs_f64(seconds)
213 }
214 }
215 }
216
217 fn fling_distance(self, speed: f32, elapsed: Duration) -> f32 {
221 let duration = self.fling_duration(speed);
222 if duration.is_zero() {
223 return 0.;
224 }
225 let elapsed = elapsed.min(duration);
226 match self {
227 Self::Exponential { decay_per_ms } => {
228 let milliseconds = elapsed.as_secs_f32() * 1000.;
231 (speed / 1000.) * (decay_per_ms.powf(milliseconds) - 1.) / decay_per_ms.ln()
232 }
233 Self::FrictionSpline {
234 friction,
235 pixels_per_inch,
236 } => {
237 let deceleration = friction_spline::deceleration(speed, friction, pixels_per_inch);
238 let rate = friction_spline::deceleration_rate();
239 let total_distance = friction as f64
240 * friction_spline::physical_coefficient(pixels_per_inch)
241 * (rate / (rate - 1.) * deceleration).exp();
242 let progress = elapsed.as_secs_f64() / duration.as_secs_f64();
243 total_distance as f32 * friction_spline::distance_coefficient(progress as f32)
244 }
245 }
246 }
247}
248
249mod friction_spline {
254 use std::sync::LazyLock;
255
256 const NB_SAMPLES: usize = 100;
257 const INFLEXION: f32 = 0.35;
258 const START_TENSION: f32 = 0.5;
259 const END_TENSION: f32 = 1.0;
260 const P1: f32 = START_TENSION * INFLEXION;
261 const P2: f32 = 1.0 - END_TENSION * (1.0 - INFLEXION);
262
263 pub(super) fn deceleration_rate() -> f64 {
265 0.78f64.ln() / 0.9f64.ln()
266 }
267
268 static SPLINE_POSITION: LazyLock<[f32; NB_SAMPLES + 1]> = LazyLock::new(|| {
273 let mut spline_position = [0f32; NB_SAMPLES + 1];
274 let mut x_min = 0f32;
275 for (i, sample) in spline_position.iter_mut().take(NB_SAMPLES).enumerate() {
276 let alpha = i as f32 / NB_SAMPLES as f32;
277 let mut x_max = 1f32;
278 let (x, coefficient) = loop {
279 let x = x_min + (x_max - x_min) / 2.;
280 let coefficient = 3. * x * (1. - x);
281 let time = coefficient * ((1. - x) * P1 + x * P2) + x * x * x;
282 if (time - alpha).abs() < 1e-5 {
283 break (x, coefficient);
284 }
285 if time > alpha {
286 x_max = x;
287 } else {
288 x_min = x;
289 }
290 };
291 *sample = coefficient * ((1. - x) * START_TENSION + x) + x * x * x;
292 }
293 spline_position[NB_SAMPLES] = 1.;
294 spline_position
295 });
296
297 pub(super) fn physical_coefficient(pixels_per_inch: f32) -> f64 {
301 9.80665 * 39.37 * pixels_per_inch as f64 * 0.84
302 }
303
304 pub(super) fn deceleration(speed: f32, friction: f32, pixels_per_inch: f32) -> f64 {
306 (INFLEXION as f64 * speed as f64
307 / (friction as f64 * physical_coefficient(pixels_per_inch)))
308 .ln()
309 }
310
311 pub(super) fn distance_coefficient(time: f32) -> f32 {
315 if time >= 1. {
316 return 1.;
317 }
318 let index = ((NB_SAMPLES as f32 * time) as usize).min(NB_SAMPLES - 1);
319 let time_lower = index as f32 / NB_SAMPLES as f32;
320 let time_upper = (index + 1) as f32 / NB_SAMPLES as f32;
321 let distance_lower = SPLINE_POSITION[index];
322 let distance_upper = SPLINE_POSITION[index + 1];
323 let velocity_coefficient = (distance_upper - distance_lower) / (time_upper - time_lower);
324 distance_lower + (time - time_lower) * velocity_coefficient
325 }
326
327 #[cfg(test)]
328 pub(super) fn bezier_time_and_position(parameter: f32) -> (f32, f32) {
329 let coefficient = 3. * parameter * (1. - parameter);
330 let cubed = parameter * parameter * parameter;
331 (
332 coefficient * ((1. - parameter) * P1 + parameter * P2) + cubed,
333 coefficient * ((1. - parameter) * START_TENSION + parameter) + cubed,
334 )
335 }
336
337 #[cfg(test)]
338 pub(super) fn spline_position_samples() -> &'static [f32; NB_SAMPLES + 1] {
339 &SPLINE_POSITION
340 }
341}
342
343#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
349pub struct GestureKinds {
350 pub tap: bool,
352 pub long_press: bool,
354 pub pan: bool,
357 pub pinch: bool,
359}
360
361impl GestureKinds {
362 pub const NONE: Self = Self {
364 tap: false,
365 long_press: false,
366 pan: false,
367 pinch: false,
368 };
369
370 pub const ALL: Self = Self {
372 tap: true,
373 long_press: true,
374 pan: true,
375 pinch: true,
376 };
377}
378
379#[derive(Clone, Debug)]
382pub struct TouchDragEvent {
383 pub phase: TouchPhase,
385 pub start_position: Point<Pixels>,
387 pub position: Point<Pixels>,
389}
390
391impl Sealed for TouchDragEvent {}
392impl InputEvent for TouchDragEvent {
393 fn to_platform_input(self) -> PlatformInput {
394 PlatformInput::TouchDrag(self)
395 }
396}
397impl GestureEvent for TouchDragEvent {}
398impl MouseEvent for TouchDragEvent {}
399
400#[derive(Clone, Debug, Default)]
410pub struct BackGestureEvent {
411 pub phase: BackGesturePhase,
413 pub start_position: Point<Pixels>,
415 pub progress: f32,
417}
418
419#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
421pub enum BackGesturePhase {
422 #[default]
425 Started,
426 Progressed,
428 Committed,
430 Cancelled,
432}
433
434impl Sealed for BackGestureEvent {}
435impl InputEvent for BackGestureEvent {
436 fn to_platform_input(self) -> PlatformInput {
437 PlatformInput::BackGesture(self)
438 }
439}
440impl GestureEvent for BackGestureEvent {}
441impl MouseEvent for BackGestureEvent {}
442
443#[derive(Clone, Debug)]
445pub struct LongPressEvent {
446 pub phase: TouchPhase,
448 pub start_position: Point<Pixels>,
450 pub position: Point<Pixels>,
452}
453
454impl Default for LongPressEvent {
455 fn default() -> Self {
456 Self {
457 phase: TouchPhase::Started,
458 start_position: Point::default(),
459 position: Point::default(),
460 }
461 }
462}
463
464impl Sealed for LongPressEvent {}
465impl InputEvent for LongPressEvent {
466 fn to_platform_input(self) -> PlatformInput {
467 PlatformInput::LongPress(self)
468 }
469}
470impl GestureEvent for LongPressEvent {}
471impl MouseEvent for LongPressEvent {}
472
473pub trait PlatformGestures {
478 fn tuning(&self) -> GestureTuning {
480 GestureTuning::default()
481 }
482
483 fn native_recognizers(&self) -> GestureKinds {
485 GestureKinds::NONE
486 }
487}
488
489pub struct NullPlatformGestures;
492
493impl PlatformGestures for NullPlatformGestures {}
494
495const MAX_FLING_VELOCITY: f32 = 8000.;
498
499const MOMENTUM_STOP_VELOCITY: f32 = 10.;
504
505const VELOCITY_WINDOW: Duration = Duration::from_millis(100);
508
509const VELOCITY_ASSUME_STOPPED_GAP: Duration = Duration::from_millis(40);
514
515const VELOCITY_MAX_SAMPLES: usize = 20;
516
517pub(crate) struct TouchGestureRecognizer {
530 tuning: GestureTuning,
531 state: TouchGestureState,
532 momentum: Option<Momentum>,
533 last_tap: Option<CompletedTap>,
534}
535
536#[derive(Debug)]
539pub(crate) enum RecognizedTouchGesture {
540 Scroll(ScrollEvent),
543 Tap {
545 down: MouseDownEvent,
546 up: MouseUpEvent,
547 },
548 TouchDrag(TouchDragEvent),
549 LongPress(LongPressEvent),
550}
551
552enum TouchGestureState {
553 Idle,
554 Pending {
557 touch: ActiveTouch,
558 deadline: Instant,
559 long_press_offered: bool,
560 touch_drag_offered: bool,
561 },
562 Panning {
565 touch: ActiveTouch,
566 axis: Axis,
567 },
568 Caught(ActiveTouch),
572 LongPressing(ActiveTouch),
573 TouchDragging(ActiveTouch),
574}
575
576struct ActiveTouch {
577 id: TouchId,
578 start_position: Point<Pixels>,
579 last_position: Point<Pixels>,
581 emitted_position: Point<Pixels>,
586 last_movement: Point<Pixels>,
589 velocity_tracker: VelocityTracker,
590}
591
592struct CompletedTap {
593 position: Point<Pixels>,
594 time: Instant,
595 count: usize,
596}
597
598struct Momentum {
603 position: Point<Pixels>,
606 direction: Point<f32>,
608 speed: f32,
610 started_at: Instant,
611 duration: Duration,
612 emitted_distance: f32,
614}
615
616impl TouchGestureRecognizer {
617 pub(crate) fn new(tuning: GestureTuning) -> Self {
618 Self {
619 tuning,
620 state: TouchGestureState::Idle,
621 momentum: None,
622 last_tap: None,
623 }
624 }
625
626 pub(crate) fn handle_event(
627 &mut self,
628 event: &TouchEvent,
629 ) -> SmallVec<[RecognizedTouchGesture; 2]> {
630 self.handle_event_at(event, Instant::now())
631 }
632
633 fn handle_event_at(
634 &mut self,
635 event: &TouchEvent,
636 now: Instant,
637 ) -> SmallVec<[RecognizedTouchGesture; 2]> {
638 let mut recognized = SmallVec::new();
639 match event.phase {
640 TouchPhase::Started => {
641 let caught_fling = if let Some(momentum) = self.momentum.take() {
642 recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
643 momentum.position,
644 Point::default(),
645 TouchPhase::Ended,
646 )));
647 true
648 } else {
649 false
650 };
651 if matches!(self.state, TouchGestureState::Idle) {
652 let mut velocity_tracker = VelocityTracker::default();
653 velocity_tracker.push(now, event.position);
654 let touch = ActiveTouch {
655 id: event.id,
656 start_position: event.position,
657 last_position: event.position,
658 emitted_position: event.position,
659 last_movement: Point::default(),
660 velocity_tracker,
661 };
662 if caught_fling {
663 recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
671 touch.start_position,
672 Point::default(),
673 TouchPhase::Started,
674 )));
675 self.state = TouchGestureState::Caught(touch);
676 } else {
677 self.state = TouchGestureState::Pending {
678 touch,
679 deadline: now + self.tuning.long_press_duration,
680 long_press_offered: false,
681 touch_drag_offered: false,
682 };
683 }
684 }
685 }
686 TouchPhase::Moved => match mem::replace(&mut self.state, TouchGestureState::Idle) {
687 TouchGestureState::Pending {
688 mut touch,
689 deadline,
690 long_press_offered,
691 touch_drag_offered,
692 } if touch.id == event.id => {
693 touch.velocity_tracker.push(now, event.position);
694 touch.last_position = event.position;
695 let accumulated = event.position - touch.start_position;
696 if accumulated.magnitude() > f64::from(self.tuning.touch_slop) {
697 let mut target = event.predicted_position.unwrap_or(event.position);
701 let axis = dominant_axis(accumulated);
702 let mut delta = target - touch.start_position;
703 lock_delta_to_axis(&mut delta, axis);
704 touch.last_movement = accumulated;
705 lock_delta_to_axis(&mut touch.last_movement, axis);
706 if movements_oppose(delta, touch.last_movement) {
707 target = event.position;
708 delta = accumulated;
709 lock_delta_to_axis(&mut delta, axis);
710 }
711 touch.emitted_position = target;
712 recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
713 touch.start_position,
714 delta,
715 TouchPhase::Started,
716 )));
717 self.state = TouchGestureState::Panning { touch, axis };
718 } else {
719 self.state = TouchGestureState::Pending {
720 touch,
721 deadline,
722 long_press_offered,
723 touch_drag_offered,
724 };
725 }
726 }
727 TouchGestureState::Caught(touch) if touch.id == event.id => {
728 let accumulated = event.position - touch.start_position;
729 if accumulated == Point::default() {
730 self.state = TouchGestureState::Caught(touch);
731 } else {
732 let axis = dominant_axis(accumulated);
735 self.state = TouchGestureState::Panning { touch, axis };
736 return self.handle_event_at(event, now);
737 }
738 }
739 TouchGestureState::Panning { mut touch, axis } if touch.id == event.id => {
740 let mut raw_delta = event.position - touch.last_position;
741 lock_delta_to_axis(&mut raw_delta, axis);
742 if raw_delta != Point::default() {
743 touch.last_movement = raw_delta;
744 }
745 touch.velocity_tracker.push(now, event.position);
746 touch.last_position = event.position;
747 let mut target = event.predicted_position.unwrap_or(event.position);
748 let mut delta = target - touch.emitted_position;
749 lock_delta_to_axis(&mut delta, axis);
750 if movements_oppose(delta, touch.last_movement) {
754 target = event.position;
755 delta = target - touch.emitted_position;
756 lock_delta_to_axis(&mut delta, axis);
757 if movements_oppose(delta, touch.last_movement) {
758 target = touch.emitted_position;
759 delta = Point::default();
760 }
761 }
762 touch.emitted_position = target;
763 recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
764 touch.start_position,
765 delta,
766 TouchPhase::Moved,
767 )));
768 self.state = TouchGestureState::Panning { touch, axis };
769 }
770 TouchGestureState::LongPressing(mut touch) if touch.id == event.id => {
771 touch.last_position = event.position;
772 recognized.push(RecognizedTouchGesture::LongPress(LongPressEvent {
773 phase: TouchPhase::Moved,
774 start_position: touch.start_position,
775 position: event.position,
776 }));
777 self.state = TouchGestureState::LongPressing(touch);
778 }
779 TouchGestureState::TouchDragging(mut touch) if touch.id == event.id => {
780 touch.last_position = event.position;
781 recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent {
782 phase: TouchPhase::Moved,
783 start_position: touch.start_position,
784 position: event.position,
785 }));
786 self.state = TouchGestureState::TouchDragging(touch);
787 }
788 other => self.state = other,
789 },
790 TouchPhase::Ended => match mem::replace(&mut self.state, TouchGestureState::Idle) {
791 TouchGestureState::Pending { touch, .. } if touch.id == event.id => {
792 let tap_count = match &self.last_tap {
793 Some(tap)
794 if now.duration_since(tap.time) <= self.tuning.multi_tap_interval
795 && (event.position - tap.position).magnitude()
796 <= f64::from(self.tuning.multi_tap_slop) =>
797 {
798 tap.count + 1
799 }
800 _ => 1,
801 };
802 self.last_tap = Some(CompletedTap {
803 position: event.position,
804 time: now,
805 count: tap_count,
806 });
807 recognized.push(RecognizedTouchGesture::Tap {
808 down: MouseDownEvent {
809 button: MouseButton::Left,
810 position: event.position,
811 modifiers: Modifiers::default(),
812 click_count: tap_count,
813 first_mouse: false,
814 },
815 up: MouseUpEvent {
816 button: MouseButton::Left,
817 position: event.position,
818 modifiers: Modifiers::default(),
819 click_count: tap_count,
820 },
821 });
822 }
823 TouchGestureState::Caught(touch) if touch.id == event.id => {
824 recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
826 touch.start_position,
827 Point::default(),
828 TouchPhase::Ended,
829 )));
830 }
831 TouchGestureState::Panning { touch, axis } if touch.id == event.id => {
832 let finger_stopped =
839 touch
840 .velocity_tracker
841 .latest_sample_time()
842 .is_none_or(|latest| {
843 now.duration_since(latest) > VELOCITY_ASSUME_STOPPED_GAP
844 });
845 let mut velocity = if finger_stopped {
846 Point::default()
847 } else {
848 touch.velocity_tracker.velocity()
849 };
850 match axis {
851 Axis::Vertical => velocity.x = 0.,
852 Axis::Horizontal => velocity.y = 0.,
853 }
854 let speed = (velocity.x.powi(2) + velocity.y.powi(2)).sqrt();
855 let mut release_delta = event.position - touch.emitted_position;
856 lock_delta_to_axis(&mut release_delta, axis);
857 if speed >= self.tuning.min_fling_velocity {
858 let direction = point(velocity.x / speed, velocity.y / speed);
859 let speed = speed.min(MAX_FLING_VELOCITY);
860 let duration = self.tuning.scroll_physics.fling_duration(speed);
861 if !duration.is_zero() {
862 let total_distance =
863 self.tuning.scroll_physics.fling_distance(speed, duration);
864 let overshoot = -(f32::from(release_delta.x) * direction.x
872 + f32::from(release_delta.y) * direction.y);
873 let emitted_distance = if overshoot > 0. && overshoot < total_distance {
874 release_delta +=
875 point(px(direction.x * overshoot), px(direction.y * overshoot));
876 overshoot
877 } else {
878 0.
879 };
880 self.momentum = Some(Momentum {
881 position: touch.start_position,
882 direction,
883 speed,
884 started_at: now,
885 duration,
886 emitted_distance,
887 });
888 }
889 }
890 recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
891 touch.start_position,
892 release_delta,
893 TouchPhase::Ended,
894 )));
895 }
896 TouchGestureState::LongPressing(touch) if touch.id == event.id => {
897 recognized.push(RecognizedTouchGesture::LongPress(LongPressEvent {
898 phase: TouchPhase::Ended,
899 start_position: touch.start_position,
900 position: event.position,
901 }));
902 }
903 TouchGestureState::TouchDragging(touch) if touch.id == event.id => {
904 recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent {
905 phase: TouchPhase::Ended,
906 start_position: touch.start_position,
907 position: event.position,
908 }));
909 }
910 other => self.state = other,
911 },
912 TouchPhase::Cancelled => match mem::replace(&mut self.state, TouchGestureState::Idle) {
913 TouchGestureState::Pending { touch, .. } if touch.id == event.id => {}
914 TouchGestureState::Panning { touch, .. } | TouchGestureState::Caught(touch)
915 if touch.id == event.id =>
916 {
917 recognized.push(RecognizedTouchGesture::Scroll(scroll_event(
918 touch.start_position,
919 Point::default(),
920 TouchPhase::Cancelled,
921 )));
922 }
923 TouchGestureState::LongPressing(touch) if touch.id == event.id => {
924 recognized.push(RecognizedTouchGesture::LongPress(LongPressEvent {
925 phase: TouchPhase::Cancelled,
926 start_position: touch.start_position,
927 position: event.position,
928 }));
929 }
930 TouchGestureState::TouchDragging(touch) if touch.id == event.id => {
931 recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent {
932 phase: TouchPhase::Cancelled,
933 start_position: touch.start_position,
934 position: event.position,
935 }));
936 }
937 other => self.state = other,
938 },
939 }
940 recognized
941 }
942
943 pub(crate) fn pending_long_press(&self) -> Option<(TouchId, Duration)> {
944 let TouchGestureState::Pending {
945 touch,
946 deadline,
947 long_press_offered: false,
948 ..
949 } = &self.state
950 else {
951 return None;
952 };
953 Some((touch.id, deadline.saturating_duration_since(Instant::now())))
954 }
955
956 pub(crate) fn offer_long_press(&mut self, id: TouchId) -> Option<RecognizedTouchGesture> {
957 let TouchGestureState::Pending {
958 touch,
959 long_press_offered,
960 ..
961 } = &mut self.state
962 else {
963 return None;
964 };
965 if touch.id != id || *long_press_offered {
966 return None;
967 }
968 *long_press_offered = true;
969 Some(RecognizedTouchGesture::LongPress(LongPressEvent {
970 phase: TouchPhase::Started,
971 start_position: touch.start_position,
972 position: touch.last_position,
973 }))
974 }
975
976 pub(crate) fn resolve_long_press(&mut self, claimed: bool) {
977 if !claimed {
978 return;
979 }
980 let state = mem::replace(&mut self.state, TouchGestureState::Idle);
981 self.state = match state {
982 TouchGestureState::Pending {
983 touch,
984 long_press_offered: true,
985 ..
986 } => TouchGestureState::LongPressing(touch),
987 other => other,
988 };
989 }
990
991 pub(crate) fn offer_touch_drag(&mut self, id: TouchId) -> Option<RecognizedTouchGesture> {
992 let TouchGestureState::Pending {
993 touch,
994 touch_drag_offered,
995 ..
996 } = &mut self.state
997 else {
998 return None;
999 };
1000 if touch.id != id || *touch_drag_offered {
1001 return None;
1002 }
1003 *touch_drag_offered = true;
1004 Some(RecognizedTouchGesture::TouchDrag(TouchDragEvent {
1005 phase: TouchPhase::Started,
1006 start_position: touch.start_position,
1007 position: touch.last_position,
1008 }))
1009 }
1010
1011 pub(crate) fn resolve_touch_drag(&mut self, claimed: bool) {
1012 if !claimed {
1013 return;
1014 }
1015 let state = mem::replace(&mut self.state, TouchGestureState::Idle);
1016 self.state = match state {
1017 TouchGestureState::Pending {
1018 touch,
1019 touch_drag_offered: true,
1020 ..
1021 } => TouchGestureState::TouchDragging(touch),
1022 other => other,
1023 };
1024 }
1025
1026 pub(crate) fn has_momentum(&self) -> bool {
1027 self.momentum.is_some()
1028 }
1029
1030 pub(crate) fn tick_momentum(&mut self) -> Option<RecognizedTouchGesture> {
1034 self.tick_momentum_at(Instant::now())
1035 }
1036
1037 fn tick_momentum_at(&mut self, now: Instant) -> Option<RecognizedTouchGesture> {
1038 let momentum = self.momentum.as_mut()?;
1039 let elapsed = now.duration_since(momentum.started_at);
1040 let distance = self
1041 .tuning
1042 .scroll_physics
1043 .fling_distance(momentum.speed, elapsed);
1044 let step = (distance - momentum.emitted_distance).max(0.);
1047 momentum.emitted_distance = momentum.emitted_distance.max(distance);
1048 let delta = point(
1049 px(momentum.direction.x * step),
1050 px(momentum.direction.y * step),
1051 );
1052 let position = momentum.position;
1053 if elapsed >= momentum.duration {
1054 self.momentum = None;
1055 Some(RecognizedTouchGesture::Scroll(scroll_event(
1056 position,
1057 delta,
1058 TouchPhase::Ended,
1059 )))
1060 } else {
1061 Some(RecognizedTouchGesture::Scroll(scroll_event(
1062 position,
1063 delta,
1064 TouchPhase::Moved,
1065 )))
1066 }
1067 }
1068}
1069
1070fn scroll_event(
1071 position: Point<Pixels>,
1072 delta: Point<Pixels>,
1073 touch_phase: TouchPhase,
1074) -> ScrollEvent {
1075 ScrollEvent {
1076 position,
1077 delta: ScrollDelta::Pixels(delta),
1078 modifiers: Modifiers::default(),
1079 touch_phase,
1080 source: ScrollSource::Touch,
1081 }
1082}
1083
1084#[derive(Default)]
1086struct VelocityTracker {
1087 samples: VecDeque<(Instant, Point<Pixels>)>,
1088}
1089
1090impl VelocityTracker {
1091 fn push(&mut self, time: Instant, position: Point<Pixels>) {
1092 self.samples.push_back((time, position));
1093 while self.samples.len() > VELOCITY_MAX_SAMPLES {
1094 self.samples.pop_front();
1095 }
1096 }
1097
1098 fn latest_sample_time(&self) -> Option<Instant> {
1099 self.samples.back().map(|(time, _)| *time)
1100 }
1101
1102 fn velocity(&self) -> Point<f32> {
1111 let Some((newest_time, _)) = self.samples.back() else {
1112 return Point::default();
1113 };
1114 let mut times_seconds: SmallVec<[f64; VELOCITY_MAX_SAMPLES]> = SmallVec::new();
1115 let mut horizontal: SmallVec<[f64; VELOCITY_MAX_SAMPLES]> = SmallVec::new();
1116 let mut vertical: SmallVec<[f64; VELOCITY_MAX_SAMPLES]> = SmallVec::new();
1117 let mut previous_time = *newest_time;
1118 for (time, position) in self.samples.iter().rev() {
1119 let age = newest_time.duration_since(*time);
1120 if age > VELOCITY_WINDOW
1121 || previous_time.duration_since(*time) > VELOCITY_ASSUME_STOPPED_GAP
1122 {
1123 break;
1124 }
1125 previous_time = *time;
1126 times_seconds.push(-age.as_secs_f64());
1127 horizontal.push(f64::from(f32::from(position.x)));
1128 vertical.push(f64::from(f32::from(position.y)));
1129 }
1130
1131 let endpoint_estimate = |values: &[f64]| -> f32 {
1132 let elapsed = -times_seconds.last().copied().unwrap_or(0.);
1133 if elapsed <= f64::EPSILON {
1134 return 0.;
1135 }
1136 ((values.first().copied().unwrap_or(0.) - values.last().copied().unwrap_or(0.))
1137 / elapsed) as f32
1138 };
1139 if times_seconds.len() < 3 {
1140 return point(endpoint_estimate(&horizontal), endpoint_estimate(&vertical));
1141 }
1142 point(
1143 quadratic_velocity_at_newest(×_seconds, &horizontal).map_or_else(
1144 || endpoint_estimate(&horizontal),
1145 |velocity| velocity as f32,
1146 ),
1147 quadratic_velocity_at_newest(×_seconds, &vertical)
1148 .map_or_else(|| endpoint_estimate(&vertical), |velocity| velocity as f32),
1149 )
1150 }
1151}
1152
1153fn quadratic_velocity_at_newest(times: &[f64], values: &[f64]) -> Option<f64> {
1158 let count = times.len() as f64;
1159 let (mut sum_t1, mut sum_t2, mut sum_t3, mut sum_t4) = (0., 0., 0., 0.);
1160 let (mut sum_v, mut sum_vt, mut sum_vt2) = (0., 0., 0.);
1161 for (&time, &value) in times.iter().zip(values) {
1162 let time_squared = time * time;
1163 sum_t1 += time;
1164 sum_t2 += time_squared;
1165 sum_t3 += time_squared * time;
1166 sum_t4 += time_squared * time_squared;
1167 sum_v += value;
1168 sum_vt += value * time;
1169 sum_vt2 += value * time_squared;
1170 }
1171 let determinant = count * (sum_t2 * sum_t4 - sum_t3 * sum_t3)
1174 - sum_t1 * (sum_t1 * sum_t4 - sum_t3 * sum_t2)
1175 + sum_t2 * (sum_t1 * sum_t3 - sum_t2 * sum_t2);
1176 if determinant.abs() < 1e-12 {
1177 return None;
1178 }
1179 let linear_determinant = count * (sum_vt * sum_t4 - sum_t3 * sum_vt2)
1180 - sum_v * (sum_t1 * sum_t4 - sum_t3 * sum_t2)
1181 + sum_t2 * (sum_t1 * sum_vt2 - sum_vt * sum_t2);
1182 Some(linear_determinant / determinant)
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187 use super::*;
1188 use crate::point;
1189
1190 #[test]
1191 fn ongoing_scroll_locks_to_dominant_axis() {
1192 let now = Instant::now();
1193 let mut ongoing_scroll = OngoingScroll::default();
1194 let mut horizontal_delta = point(px(10.), px(2.));
1195 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
1196 assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
1197 assert_eq!(horizontal_delta, point(px(10.), px(0.)));
1198
1199 let mut continued_delta = point(px(3.), px(2.));
1200 ongoing_scroll.filter_at(
1201 &mut continued_delta,
1202 TouchPhase::Moved,
1203 now + Duration::from_millis(1),
1204 );
1205 assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
1206 assert_eq!(continued_delta, point(px(3.), px(0.)));
1207 }
1208
1209 #[test]
1210 fn ongoing_scroll_unlocks_when_direction_changes() {
1211 let now = Instant::now();
1212 let mut ongoing_scroll = OngoingScroll::default();
1213 let mut horizontal_delta = point(px(10.), px(2.));
1214 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
1215
1216 let mut vertical_delta = point(px(2.), px(10.));
1217 ongoing_scroll.filter_at(
1218 &mut vertical_delta,
1219 TouchPhase::Moved,
1220 now + Duration::from_millis(1),
1221 );
1222 assert_eq!(ongoing_scroll.axis, None);
1223 assert_eq!(vertical_delta, point(px(2.), px(10.)));
1224 }
1225
1226 #[test]
1227 fn ongoing_scroll_starts_new_gesture_at_timeout_boundary() {
1228 let now = Instant::now();
1229 let mut ongoing_scroll = OngoingScroll::default();
1230 let mut horizontal_delta = point(px(10.), px(2.));
1231 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
1232
1233 let mut vertical_delta = point(px(2.), px(10.));
1234 ongoing_scroll.filter_at(
1235 &mut vertical_delta,
1236 TouchPhase::Moved,
1237 now + SCROLL_EVENT_SEPARATION,
1238 );
1239 assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
1240 assert_eq!(vertical_delta, point(px(0.), px(10.)));
1241 }
1242
1243 #[test]
1244 fn ongoing_scroll_ignores_zero_delta_and_resets_when_ended() {
1245 let now = Instant::now();
1246 let mut ongoing_scroll = OngoingScroll::default();
1247 let mut horizontal_delta = point(px(10.), px(2.));
1248 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
1249
1250 let mut zero_delta = Point::default();
1251 ongoing_scroll.filter_at(
1252 &mut zero_delta,
1253 TouchPhase::Ended,
1254 now + Duration::from_millis(1),
1255 );
1256 assert_eq!(ongoing_scroll.axis, None);
1257
1258 let mut vertical_delta = point(px(2.), px(3.));
1259 ongoing_scroll.filter_at(
1260 &mut vertical_delta,
1261 TouchPhase::Moved,
1262 now + Duration::from_millis(2),
1263 );
1264 assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
1265 assert_eq!(vertical_delta, point(px(0.), px(3.)));
1266 }
1267
1268 #[test]
1269 fn ongoing_scroll_ignores_zero_delta_movement() {
1270 let now = Instant::now();
1271 let mut ongoing_scroll = OngoingScroll::default();
1272 let mut horizontal_delta = point(px(10.), px(2.));
1273 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
1274
1275 let mut zero_delta = Point::default();
1276 ongoing_scroll.filter_at(
1277 &mut zero_delta,
1278 TouchPhase::Moved,
1279 now + SCROLL_EVENT_SEPARATION,
1280 );
1281
1282 let mut vertical_delta = point(px(2.), px(10.));
1283 ongoing_scroll.filter_at(
1284 &mut vertical_delta,
1285 TouchPhase::Moved,
1286 now + SCROLL_EVENT_SEPARATION,
1287 );
1288 assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
1289 assert_eq!(vertical_delta, point(px(0.), px(10.)));
1290 }
1291
1292 #[test]
1293 fn ongoing_scroll_supports_moved_only_platforms() {
1294 let now = Instant::now();
1295 let mut ongoing_scroll = OngoingScroll::default();
1296 let mut horizontal_delta = point(px(10.), px(2.));
1297 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
1298 assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
1299 assert_eq!(horizontal_delta, point(px(10.), px(0.)));
1300 }
1301
1302 #[test]
1303 fn touch_within_slop_resolves_to_tap() {
1304 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1305 let now = Instant::now();
1306 let touch = TouchId(1);
1307
1308 let recognized =
1309 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 10.), now);
1310 assert!(recognized.is_empty());
1311 let recognized = recognizer.handle_event_at(
1312 &touch_event(touch, TouchPhase::Moved, 12., 11.),
1313 now + Duration::from_millis(20),
1314 );
1315 assert!(recognized.is_empty());
1316
1317 let recognized = recognizer.handle_event_at(
1318 &touch_event(touch, TouchPhase::Ended, 12., 11.),
1319 now + Duration::from_millis(60),
1320 );
1321 let [RecognizedTouchGesture::Tap { down, up }] = recognized.as_slice() else {
1322 panic!("expected tap, got {recognized:?}");
1323 };
1324 assert_eq!(down.click_count, 1);
1325 assert_eq!(down.position, point(px(12.), px(11.)));
1326 assert_eq!(up.click_count, 1);
1327 }
1328
1329 #[test]
1330 fn consecutive_taps_accumulate_tap_count() {
1331 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1332 let now = Instant::now();
1333
1334 recognizer.handle_event_at(&touch_event(TouchId(1), TouchPhase::Started, 10., 10.), now);
1335 recognizer.handle_event_at(
1336 &touch_event(TouchId(1), TouchPhase::Ended, 10., 10.),
1337 now + Duration::from_millis(40),
1338 );
1339
1340 let second_down = now + Duration::from_millis(200);
1341 recognizer.handle_event_at(
1342 &touch_event(TouchId(2), TouchPhase::Started, 14., 10.),
1343 second_down,
1344 );
1345 let recognized = recognizer.handle_event_at(
1346 &touch_event(TouchId(2), TouchPhase::Ended, 14., 10.),
1347 second_down + Duration::from_millis(40),
1348 );
1349 let [RecognizedTouchGesture::Tap { down, .. }] = recognized.as_slice() else {
1350 panic!("expected tap, got {recognized:?}");
1351 };
1352 assert_eq!(down.click_count, 2);
1353
1354 let late_down = second_down + Duration::from_secs(2);
1355 recognizer.handle_event_at(
1356 &touch_event(TouchId(3), TouchPhase::Started, 14., 10.),
1357 late_down,
1358 );
1359 let recognized = recognizer.handle_event_at(
1360 &touch_event(TouchId(3), TouchPhase::Ended, 14., 10.),
1361 late_down + Duration::from_millis(40),
1362 );
1363 let [RecognizedTouchGesture::Tap { down, .. }] = recognized.as_slice() else {
1364 panic!("expected tap, got {recognized:?}");
1365 };
1366 assert_eq!(down.click_count, 1);
1367 }
1368
1369 #[test]
1370 fn touch_beyond_slop_resolves_to_pan() {
1371 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1372 let now = Instant::now();
1373 let touch = TouchId(1);
1374
1375 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now);
1376
1377 let recognized = recognizer.handle_event_at(
1378 &touch_event(touch, TouchPhase::Moved, 100., 120.),
1379 now + Duration::from_millis(16),
1380 );
1381 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1382 panic!("expected scroll, got {recognized:?}");
1383 };
1384 assert_eq!(scroll.touch_phase, TouchPhase::Started);
1385 assert_eq!(scroll.position, point(px(100.), px(100.)));
1386 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(20.)));
1387
1388 let recognized = recognizer.handle_event_at(
1389 &touch_event(touch, TouchPhase::Moved, 100., 135.),
1390 now + Duration::from_millis(32),
1391 );
1392 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1393 panic!("expected scroll, got {recognized:?}");
1394 };
1395 assert_eq!(scroll.touch_phase, TouchPhase::Moved);
1396 assert_eq!(scroll.position, point(px(100.), px(100.)));
1397 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(15.)));
1398
1399 let recognized = recognizer.handle_event_at(
1400 &touch_event(touch, TouchPhase::Ended, 100., 135.),
1401 now + Duration::from_millis(48),
1402 );
1403 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1404 panic!("expected scroll, got {recognized:?}");
1405 };
1406 assert_eq!(scroll.touch_phase, TouchPhase::Ended);
1407 }
1408
1409 #[test]
1410 fn touch_pan_stays_locked_to_its_initial_dominant_axis() {
1411 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1412 let now = Instant::now();
1413 let touch = TouchId(1);
1414
1415 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now);
1416
1417 let recognized = recognizer.handle_event_at(
1418 &touch_event(touch, TouchPhase::Moved, 104., 120.),
1419 now + Duration::from_millis(16),
1420 );
1421 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1422 panic!("expected scroll, got {recognized:?}");
1423 };
1424 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(20.)));
1425
1426 let recognized = recognizer.handle_event_at(
1427 &touch_event(touch, TouchPhase::Moved, 134., 125.),
1428 now + Duration::from_millis(32),
1429 );
1430 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1431 panic!("expected scroll, got {recognized:?}");
1432 };
1433 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(5.)));
1434 }
1435
1436 #[test]
1437 fn touch_pan_locks_to_horizontal_axis() {
1438 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1439 let now = Instant::now();
1440 let touch = TouchId(1);
1441
1442 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now);
1443
1444 let recognized = recognizer.handle_event_at(
1445 &touch_event(touch, TouchPhase::Moved, 120., 104.),
1446 now + Duration::from_millis(16),
1447 );
1448 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1449 panic!("expected scroll, got {recognized:?}");
1450 };
1451 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(20.), px(0.)));
1452 }
1453
1454 #[test]
1455 fn predicted_positions_lead_the_pan_but_totals_converge_on_release() {
1456 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1457 let now = Instant::now();
1458 let touch = TouchId(1);
1459
1460 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now);
1461
1462 let mut moved = touch_event(touch, TouchPhase::Moved, 100., 120.);
1464 moved.predicted_position = Some(point(px(106.), px(128.)));
1465 let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(16));
1466 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1467 panic!("expected scroll, got {recognized:?}");
1468 };
1469 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(28.)));
1470
1471 let mut moved = touch_event(touch, TouchPhase::Moved, 100., 130.);
1474 moved.predicted_position = Some(point(px(104.), px(134.)));
1475 let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(32));
1476 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1477 panic!("expected scroll, got {recognized:?}");
1478 };
1479 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(6.)));
1480
1481 let recognized = recognizer.handle_event_at(
1485 &touch_event(touch, TouchPhase::Ended, 100., 130.),
1486 now + Duration::from_millis(120),
1487 );
1488 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1489 panic!("expected scroll, got {recognized:?}");
1490 };
1491 assert_eq!(scroll.touch_phase, TouchPhase::Ended);
1492 assert!(!recognizer.has_momentum());
1493 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-4.)));
1494 }
1495
1496 #[test]
1497 fn predicted_positions_do_not_emit_false_reversals() {
1498 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1499 let now = Instant::now();
1500 let touch = TouchId(1);
1501
1502 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now);
1503
1504 let mut moved = touch_event(touch, TouchPhase::Moved, 100., 120.);
1505 moved.predicted_position = Some(point(px(100.), px(130.)));
1506 let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(16));
1507 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1508 panic!("expected scroll, got {recognized:?}");
1509 };
1510 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(30.)));
1511
1512 let mut moved = touch_event(touch, TouchPhase::Moved, 100., 125.);
1513 moved.predicted_position = Some(point(px(100.), px(127.)));
1514 let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(32));
1515 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1516 panic!("expected scroll, got {recognized:?}");
1517 };
1518 assert_eq!(
1519 scroll.delta.pixel_delta(px(16.)),
1520 Point::<Pixels>::default()
1521 );
1522
1523 let mut moved = touch_event(touch, TouchPhase::Moved, 100., 125.);
1524 moved.predicted_position = Some(point(px(100.), px(126.)));
1525 let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(40));
1526 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1527 panic!("expected scroll, got {recognized:?}");
1528 };
1529 assert_eq!(
1530 scroll.delta.pixel_delta(px(16.)),
1531 Point::<Pixels>::default()
1532 );
1533
1534 let mut moved = touch_event(touch, TouchPhase::Moved, 100., 132.);
1535 moved.predicted_position = Some(point(px(100.), px(136.)));
1536 let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(48));
1537 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1538 panic!("expected scroll, got {recognized:?}");
1539 };
1540 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(6.)));
1541
1542 let mut moved = touch_event(touch, TouchPhase::Moved, 100., 124.);
1543 moved.predicted_position = Some(point(px(100.), px(140.)));
1544 let recognized = recognizer.handle_event_at(&moved, now + Duration::from_millis(64));
1545 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1546 panic!("expected scroll, got {recognized:?}");
1547 };
1548 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-12.)));
1549 }
1550
1551 #[test]
1552 fn predicted_overshoot_folds_into_the_fling_without_scrolling_backwards() {
1553 let now = Instant::now();
1554 let mut total_with_prediction = 0f32;
1555 let mut total_without_prediction = 0f32;
1556 for use_prediction in [true, false] {
1557 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1558 let mut total = 0f32;
1559 let mut drain = |recognized: &[RecognizedTouchGesture], upward_only: bool| {
1560 for gesture in recognized {
1561 let RecognizedTouchGesture::Scroll(scroll) = gesture else {
1562 panic!("expected scroll, got {gesture:?}");
1563 };
1564 let delta = scroll.delta.pixel_delta(px(16.)).y;
1565 if upward_only {
1566 assert!(
1567 delta <= px(0.),
1568 "content moved backwards by {delta:?} during an upward gesture"
1569 );
1570 }
1571 total += f32::from(delta);
1572 }
1573 };
1574
1575 recognizer.handle_event_at(
1576 &touch_event(TouchId(1), TouchPhase::Started, 100., 500.),
1577 now,
1578 );
1579 for step in 1..=5u64 {
1580 let raw_y = 500. - step as f32 * 40.;
1581 let mut moved = touch_event(TouchId(1), TouchPhase::Moved, 100., raw_y);
1582 if use_prediction {
1583 moved.predicted_position = Some(point(px(100.), px(raw_y - 25.)));
1584 }
1585 let recognized =
1586 recognizer.handle_event_at(&moved, now + Duration::from_millis(step * 16));
1587 drain(&recognized, use_prediction);
1588 }
1589 let recognized = recognizer.handle_event_at(
1592 &touch_event(TouchId(1), TouchPhase::Ended, 100., 300.),
1593 now + Duration::from_millis(90),
1594 );
1595 drain(&recognized, use_prediction);
1596 assert!(recognizer.has_momentum());
1597 let mut tick = now + Duration::from_millis(91);
1598 while recognizer.has_momentum() {
1599 if let Some(gesture) = recognizer.tick_momentum_at(tick) {
1600 drain(&[gesture], use_prediction);
1601 }
1602 tick += Duration::from_millis(16);
1603 }
1604
1605 if use_prediction {
1606 total_with_prediction = total;
1607 } else {
1608 total_without_prediction = total;
1609 }
1610 }
1611 assert!(
1614 (total_with_prediction - total_without_prediction).abs() < 0.01,
1615 "totals diverged: {total_with_prediction} vs {total_without_prediction}"
1616 );
1617 }
1618
1619 #[test]
1620 fn fast_release_starts_momentum_that_decays_to_a_stop() {
1621 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1622 let now = Instant::now();
1623 let touch = TouchId(1);
1624
1625 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 300.), now);
1626 for step in 1..=5 {
1627 recognizer.handle_event_at(
1628 &touch_event(touch, TouchPhase::Moved, 100., 300. - step as f32 * 20.),
1629 now + Duration::from_millis(step * 16),
1630 );
1631 }
1632 recognizer.handle_event_at(
1633 &touch_event(touch, TouchPhase::Ended, 100., 200.),
1634 now + Duration::from_millis(6 * 16),
1635 );
1636 assert!(recognizer.has_momentum());
1637
1638 let tick = now + Duration::from_millis(6 * 16 + 16);
1639 let recognized = recognizer.tick_momentum_at(tick);
1640 let Some(RecognizedTouchGesture::Scroll(scroll)) = recognized else {
1641 panic!("expected momentum scroll, got {recognized:?}");
1642 };
1643 assert_eq!(scroll.touch_phase, TouchPhase::Moved);
1644 assert_eq!(scroll.position, point(px(100.), px(300.)));
1645 let delta = scroll.delta.pixel_delta(px(16.));
1646 assert!(
1647 delta.y < px(0.),
1648 "momentum should continue upward, got {delta:?}"
1649 );
1650 assert!(
1652 delta.x.abs() < px(0.001),
1653 "expected no x motion, got {delta:?}"
1654 );
1655
1656 let mut last_phase = TouchPhase::Moved;
1657 let mut ticks = 0;
1658 let mut time = tick;
1659 while recognizer.has_momentum() {
1660 time += Duration::from_millis(16);
1661 ticks += 1;
1662 assert!(ticks < 1000, "momentum never stopped");
1663 if let Some(RecognizedTouchGesture::Scroll(scroll)) = recognizer.tick_momentum_at(time)
1664 {
1665 last_phase = scroll.touch_phase;
1666 }
1667 }
1668 assert_eq!(last_phase, TouchPhase::Ended);
1669 assert!(recognizer.tick_momentum_at(time).is_none());
1670 }
1671
1672 #[test]
1673 fn diagonal_release_flings_only_on_locked_axis() {
1674 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1675 let now = Instant::now();
1676 let touch = TouchId(1);
1677
1678 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 300.), now);
1679 for step in 1..=5 {
1680 let recognized = recognizer.handle_event_at(
1681 &touch_event(
1682 touch,
1683 TouchPhase::Moved,
1684 100. + step as f32 * 3.,
1685 300. - step as f32 * 20.,
1686 ),
1687 now + Duration::from_millis(step * 16),
1688 );
1689 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1690 panic!("expected scroll, got {recognized:?}");
1691 };
1692 assert_eq!(scroll.delta.pixel_delta(px(16.)).x, px(0.));
1693 }
1694 recognizer.handle_event_at(
1695 &touch_event(touch, TouchPhase::Ended, 115., 200.),
1696 now + Duration::from_millis(6 * 16),
1697 );
1698 assert!(recognizer.has_momentum());
1699
1700 let mut time = now + Duration::from_millis(6 * 16);
1701 while recognizer.has_momentum() {
1702 time += Duration::from_millis(16);
1703 if let Some(RecognizedTouchGesture::Scroll(scroll)) = recognizer.tick_momentum_at(time)
1704 {
1705 let delta = scroll.delta.pixel_delta(px(16.));
1706 assert_eq!(delta.x, px(0.));
1707 assert!(delta.y <= px(0.));
1708 }
1709 }
1710 }
1711
1712 #[test]
1713 fn slow_release_does_not_start_momentum() {
1714 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1715 let now = Instant::now();
1716 let touch = TouchId(1);
1717
1718 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 300.), now);
1719 recognizer.handle_event_at(
1720 &touch_event(touch, TouchPhase::Moved, 100., 280.),
1721 now + Duration::from_millis(16),
1722 );
1723 recognizer.handle_event_at(
1724 &touch_event(touch, TouchPhase::Moved, 100., 279.),
1725 now + Duration::from_millis(500),
1726 );
1727 recognizer.handle_event_at(
1728 &touch_event(touch, TouchPhase::Ended, 100., 279.),
1729 now + Duration::from_millis(600),
1730 );
1731 assert!(!recognizer.has_momentum());
1732 }
1733
1734 #[test]
1735 fn new_touch_interrupts_momentum() {
1736 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1737 let now = Instant::now();
1738
1739 recognizer.handle_event_at(
1740 &touch_event(TouchId(1), TouchPhase::Started, 100., 300.),
1741 now,
1742 );
1743 for step in 1..=3 {
1744 recognizer.handle_event_at(
1745 &touch_event(
1746 TouchId(1),
1747 TouchPhase::Moved,
1748 100.,
1749 300. - step as f32 * 33.,
1750 ),
1751 now + Duration::from_millis(step * 16),
1752 );
1753 }
1754 recognizer.handle_event_at(
1755 &touch_event(TouchId(1), TouchPhase::Ended, 100., 200.),
1756 now + Duration::from_millis(64),
1757 );
1758 assert!(recognizer.has_momentum());
1759
1760 let recognized = recognizer.handle_event_at(
1761 &touch_event(TouchId(2), TouchPhase::Started, 100., 200.),
1762 now + Duration::from_millis(200),
1763 );
1764 assert!(!recognizer.has_momentum());
1765 let [
1766 RecognizedTouchGesture::Scroll(closing),
1767 RecognizedTouchGesture::Scroll(opening),
1768 ] = recognized.as_slice()
1769 else {
1770 panic!("expected closing and opening scrolls, got {recognized:?}");
1771 };
1772 assert_eq!(closing.touch_phase, TouchPhase::Ended);
1773 assert!(closing.delta.pixel_delta(px(16.)).is_zero());
1774 assert_eq!(opening.touch_phase, TouchPhase::Started);
1775 assert!(opening.delta.pixel_delta(px(16.)).is_zero());
1776 }
1777
1778 #[test]
1779 fn catching_a_fling_pans_immediately_and_never_taps() {
1780 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1781 let now = Instant::now();
1782
1783 recognizer.handle_event_at(
1784 &touch_event(TouchId(1), TouchPhase::Started, 100., 300.),
1785 now,
1786 );
1787 for step in 1..=3 {
1788 recognizer.handle_event_at(
1789 &touch_event(
1790 TouchId(1),
1791 TouchPhase::Moved,
1792 100.,
1793 300. - step as f32 * 33.,
1794 ),
1795 now + Duration::from_millis(step * 16),
1796 );
1797 }
1798 recognizer.handle_event_at(
1799 &touch_event(TouchId(1), TouchPhase::Ended, 100., 200.),
1800 now + Duration::from_millis(64),
1801 );
1802 assert!(recognizer.has_momentum());
1803
1804 recognizer.handle_event_at(
1805 &touch_event(TouchId(2), TouchPhase::Started, 100., 200.),
1806 now + Duration::from_millis(200),
1807 );
1808
1809 let recognized = recognizer.handle_event_at(
1811 &touch_event(TouchId(2), TouchPhase::Moved, 100., 197.),
1812 now + Duration::from_millis(216),
1813 );
1814 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1815 panic!("expected scroll, got {recognized:?}");
1816 };
1817 assert_eq!(scroll.touch_phase, TouchPhase::Moved);
1818 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-3.)));
1819
1820 let recognized = recognizer.handle_event_at(
1822 &touch_event(TouchId(2), TouchPhase::Ended, 100., 197.),
1823 now + Duration::from_millis(232),
1824 );
1825 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1826 panic!("expected scroll, got {recognized:?}");
1827 };
1828 assert_eq!(scroll.touch_phase, TouchPhase::Ended);
1829 }
1830
1831 #[test]
1832 fn catching_a_fling_takes_the_axis_from_the_new_touch() {
1833 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1834 let now = Instant::now();
1835
1836 recognizer.handle_event_at(
1838 &touch_event(TouchId(1), TouchPhase::Started, 300., 300.),
1839 now,
1840 );
1841 for step in 1..=3 {
1842 recognizer.handle_event_at(
1843 &touch_event(
1844 TouchId(1),
1845 TouchPhase::Moved,
1846 300. - step as f32 * 33.,
1847 300.,
1848 ),
1849 now + Duration::from_millis(step * 16),
1850 );
1851 }
1852 recognizer.handle_event_at(
1853 &touch_event(TouchId(1), TouchPhase::Ended, 200., 300.),
1854 now + Duration::from_millis(64),
1855 );
1856 assert!(recognizer.has_momentum());
1857
1858 recognizer.handle_event_at(
1861 &touch_event(TouchId(2), TouchPhase::Started, 200., 300.),
1862 now + Duration::from_millis(200),
1863 );
1864 let recognized = recognizer.handle_event_at(
1865 &touch_event(TouchId(2), TouchPhase::Moved, 201., 280.),
1866 now + Duration::from_millis(216),
1867 );
1868 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1869 panic!("expected scroll, got {recognized:?}");
1870 };
1871 assert_eq!(scroll.touch_phase, TouchPhase::Moved);
1872 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-20.)));
1873
1874 let recognized = recognizer.handle_event_at(
1876 &touch_event(TouchId(2), TouchPhase::Moved, 205., 260.),
1877 now + Duration::from_millis(232),
1878 );
1879 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1880 panic!("expected scroll, got {recognized:?}");
1881 };
1882 assert_eq!(scroll.delta.pixel_delta(px(16.)), point(px(0.), px(-20.)));
1883 }
1884
1885 #[test]
1886 fn cancelled_pan_emits_cancelled_scroll_and_no_tap() {
1887 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1888 let now = Instant::now();
1889 let touch = TouchId(1);
1890
1891 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now);
1892 recognizer.handle_event_at(
1893 &touch_event(touch, TouchPhase::Moved, 100., 150.),
1894 now + Duration::from_millis(16),
1895 );
1896 let recognized = recognizer.handle_event_at(
1897 &touch_event(touch, TouchPhase::Cancelled, 100., 150.),
1898 now + Duration::from_millis(32),
1899 );
1900 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1901 panic!("expected cancelled scroll, got {recognized:?}");
1902 };
1903 assert_eq!(scroll.touch_phase, TouchPhase::Cancelled);
1904 assert!(!recognizer.has_momentum());
1905
1906 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1907 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 100., 100.), now);
1908 let recognized = recognizer.handle_event_at(
1909 &touch_event(touch, TouchPhase::Cancelled, 100., 102.),
1910 now + Duration::from_millis(16),
1911 );
1912 assert!(recognized.is_empty(), "cancelled tap must not click");
1913 }
1914
1915 #[test]
1916 fn concurrent_touches_are_ignored_while_one_is_active() {
1917 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
1918 let now = Instant::now();
1919
1920 recognizer.handle_event_at(
1921 &touch_event(TouchId(1), TouchPhase::Started, 100., 100.),
1922 now,
1923 );
1924 let recognized = recognizer.handle_event_at(
1925 &touch_event(TouchId(2), TouchPhase::Started, 200., 200.),
1926 now + Duration::from_millis(8),
1927 );
1928 assert!(recognized.is_empty());
1929 let recognized = recognizer.handle_event_at(
1930 &touch_event(TouchId(2), TouchPhase::Moved, 200., 300.),
1931 now + Duration::from_millis(16),
1932 );
1933 assert!(recognized.is_empty());
1934 let recognized = recognizer.handle_event_at(
1935 &touch_event(TouchId(2), TouchPhase::Ended, 200., 300.),
1936 now + Duration::from_millis(24),
1937 );
1938 assert!(recognized.is_empty());
1939
1940 let recognized = recognizer.handle_event_at(
1942 &touch_event(TouchId(1), TouchPhase::Moved, 100., 150.),
1943 now + Duration::from_millis(32),
1944 );
1945 let [RecognizedTouchGesture::Scroll(scroll)] = recognized.as_slice() else {
1946 panic!("expected scroll, got {recognized:?}");
1947 };
1948 assert_eq!(scroll.touch_phase, TouchPhase::Started);
1949 }
1950
1951 #[test]
1952 fn spline_position_table_matches_the_bezier_curve() {
1953 let samples = friction_spline::spline_position_samples();
1954 assert!(samples[0].abs() < 1e-4);
1957 assert_eq!(samples[100], 1.);
1958 for window in samples.windows(2) {
1959 assert!(window[0] < window[1], "table must be strictly increasing");
1960 }
1961 for (i, &stored_position) in samples.iter().enumerate().take(100) {
1965 let alpha = i as f32 / 100.;
1966 let (mut lower, mut upper) = (0f32, 1f32);
1967 for _ in 0..50 {
1968 let middle = (lower + upper) / 2.;
1969 let (time, _) = friction_spline::bezier_time_and_position(middle);
1970 if time > alpha {
1971 upper = middle;
1972 } else {
1973 lower = middle;
1974 }
1975 }
1976 let (time, position) = friction_spline::bezier_time_and_position((lower + upper) / 2.);
1977 assert!(
1978 (time - alpha).abs() < 1e-4,
1979 "sample {i}: time {time} != {alpha}"
1980 );
1981 assert!(
1982 (position - stored_position).abs() < 1e-3,
1983 "sample {i}: position {position} != stored {stored_position}"
1984 );
1985 }
1986 }
1987
1988 #[test]
1989 fn fling_curves_are_sane_for_both_physics() {
1990 for physics in [ScrollPhysics::ios(), ScrollPhysics::android()] {
1991 let slow = physics.fling_duration(500.);
1992 let fast = physics.fling_duration(4000.);
1993 assert!(slow > Duration::ZERO, "{physics:?}");
1994 assert!(fast > slow, "faster flings must coast longer: {physics:?}");
1995
1996 let halfway = physics.fling_distance(4000., fast / 2);
1997 let total = physics.fling_distance(4000., fast);
1998 assert!(halfway > 0. && halfway < total, "{physics:?}");
1999 assert!(
2000 physics.fling_distance(4000., fast * 2) == total,
2001 "distance must not grow past the fling duration: {physics:?}"
2002 );
2003 assert!(
2004 physics.fling_distance(4000., fast) > physics.fling_distance(500., slow),
2005 "faster flings must travel further: {physics:?}"
2006 );
2007 }
2008 }
2009
2010 #[test]
2011 fn momentum_is_frame_rate_independent() {
2012 let total_distance_with_tick_length = |tick: Duration| -> f32 {
2015 let mut recognizer = TouchGestureRecognizer::new(GestureTuning {
2016 scroll_physics: ScrollPhysics::android(),
2017 ..GestureTuning::default()
2018 });
2019 let now = Instant::now();
2020 recognizer.handle_event_at(
2021 &touch_event(TouchId(1), TouchPhase::Started, 100., 500.),
2022 now,
2023 );
2024 for step in 1..=3 {
2025 recognizer.handle_event_at(
2026 &touch_event(
2027 TouchId(1),
2028 TouchPhase::Moved,
2029 100.,
2030 500. - step as f32 * 40.,
2031 ),
2032 now + Duration::from_millis(step * 16),
2033 );
2034 }
2035 recognizer.handle_event_at(
2036 &touch_event(TouchId(1), TouchPhase::Ended, 100., 380.),
2037 now + Duration::from_millis(64),
2038 );
2039 assert!(recognizer.has_momentum());
2040
2041 let mut total = 0f32;
2042 let mut time = now + Duration::from_millis(64);
2043 let mut guard = 0;
2044 while recognizer.has_momentum() {
2045 time += tick;
2046 guard += 1;
2047 assert!(guard < 10_000, "momentum never stopped");
2048 if let Some(RecognizedTouchGesture::Scroll(scroll)) =
2049 recognizer.tick_momentum_at(time)
2050 {
2051 total += f32::from(scroll.delta.pixel_delta(px(16.)).y);
2052 }
2053 }
2054 total
2055 };
2056
2057 let smooth = total_distance_with_tick_length(Duration::from_millis(16));
2058 let stalled = total_distance_with_tick_length(Duration::from_secs(10));
2059 assert!(
2060 (smooth - stalled).abs() < 0.01,
2061 "expected identical fling distance, got {smooth} vs {stalled}"
2062 );
2063 }
2064
2065 #[test]
2066 fn flick_velocity_reflects_release_speed_not_window_average() {
2067 let mut velocity_tracker = VelocityTracker::default();
2071 let start = Instant::now();
2072 for step in 0..=6 {
2073 let t = step as f32 * 0.016;
2074 velocity_tracker.push(
2075 start + Duration::from_millis(step * 16),
2076 point(px(0.), px(1000. * t * t)),
2077 );
2078 }
2079 let velocity = velocity_tracker.velocity();
2080 let release_speed = 2. * 1000. * 0.096;
2081 assert!(
2082 (velocity.y - release_speed).abs() < 1.,
2083 "expected ≈{release_speed} px/s at release, got {} px/s",
2084 velocity.y
2085 );
2086 assert_eq!(velocity.x, 0.);
2087 }
2088
2089 #[test]
2090 fn samples_before_a_pause_do_not_contribute_velocity() {
2091 let mut velocity_tracker = VelocityTracker::default();
2095 let start = Instant::now();
2096 velocity_tracker.push(start, point(px(0.), px(0.)));
2097 velocity_tracker.push(start + Duration::from_millis(16), point(px(0.), px(50.)));
2098 velocity_tracker.push(start + Duration::from_millis(80), point(px(0.), px(52.)));
2099 velocity_tracker.push(start + Duration::from_millis(96), point(px(0.), px(54.)));
2100 let velocity = velocity_tracker.velocity();
2101 assert!(
2102 velocity.y < 200.,
2103 "pre-pause motion leaked into the estimate: {} px/s",
2104 velocity.y
2105 );
2106 }
2107
2108 #[test]
2109 fn claimed_touch_drag_emits_phased_stream_without_pan_or_tap() {
2110 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2111 let touch = TouchId(1);
2112 let now = Instant::now();
2113 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now);
2114 let Some(RecognizedTouchGesture::TouchDrag(started)) = recognizer.offer_touch_drag(touch)
2115 else {
2116 panic!("expected touch drag");
2117 };
2118 assert_eq!(started.phase, TouchPhase::Started);
2119 assert_eq!(started.start_position, point(px(10.), px(20.)));
2120 recognizer.resolve_touch_drag(true);
2121
2122 let moved = recognizer.handle_event_at(
2123 &touch_event(touch, TouchPhase::Moved, 40., 50.),
2124 now + Duration::from_millis(10),
2125 );
2126 let [RecognizedTouchGesture::TouchDrag(moved)] = moved.as_slice() else {
2127 panic!("expected moved touch drag, got {moved:?}");
2128 };
2129 assert_eq!(moved.phase, TouchPhase::Moved);
2130 assert_eq!(moved.position, point(px(40.), px(50.)));
2131
2132 let ended = recognizer.handle_event_at(
2133 &touch_event(touch, TouchPhase::Ended, 45., 55.),
2134 now + Duration::from_millis(20),
2135 );
2136 let [RecognizedTouchGesture::TouchDrag(ended)] = ended.as_slice() else {
2137 panic!("expected ended touch drag, got {ended:?}");
2138 };
2139 assert_eq!(ended.phase, TouchPhase::Ended);
2140 assert_eq!(ended.position, point(px(45.), px(55.)));
2141 }
2142
2143 #[test]
2144 fn unclaimed_touch_drag_remains_a_pan_candidate() {
2145 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2146 let touch = TouchId(1);
2147 let now = Instant::now();
2148 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now);
2149 assert!(recognizer.offer_touch_drag(touch).is_some());
2150 recognizer.resolve_touch_drag(false);
2151
2152 let moved = recognizer.handle_event_at(
2153 &touch_event(touch, TouchPhase::Moved, 20., 0.),
2154 now + Duration::from_millis(10),
2155 );
2156 assert!(matches!(
2157 moved.as_slice(),
2158 [RecognizedTouchGesture::Scroll(ScrollEvent {
2159 touch_phase: TouchPhase::Started,
2160 ..
2161 })]
2162 ));
2163 }
2164
2165 #[test]
2166 fn claimed_long_press_emits_phased_stream_without_tap() {
2167 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2168 let touch = TouchId(1);
2169 let now = Instant::now();
2170 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now);
2171 let Some(RecognizedTouchGesture::LongPress(started)) = recognizer.offer_long_press(touch)
2172 else {
2173 panic!("expected long press");
2174 };
2175 assert_eq!(started.phase, TouchPhase::Started);
2176 assert_eq!(started.start_position, point(px(10.), px(20.)));
2177 recognizer.resolve_long_press(true);
2178
2179 let moved = recognizer.handle_event_at(
2180 &touch_event(touch, TouchPhase::Moved, 12., 21.),
2181 now + Duration::from_millis(510),
2182 );
2183 let [RecognizedTouchGesture::LongPress(moved)] = moved.as_slice() else {
2184 panic!("expected moved long press, got {moved:?}");
2185 };
2186 assert_eq!(moved.phase, TouchPhase::Moved);
2187
2188 let ended = recognizer.handle_event_at(
2189 &touch_event(touch, TouchPhase::Ended, 12., 21.),
2190 now + Duration::from_millis(520),
2191 );
2192 let [RecognizedTouchGesture::LongPress(ended)] = ended.as_slice() else {
2193 panic!("expected ended long press, got {ended:?}");
2194 };
2195 assert_eq!(ended.phase, TouchPhase::Ended);
2196 }
2197
2198 #[test]
2199 fn unclaimed_long_press_remains_a_tap_candidate() {
2200 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2201 let touch = TouchId(1);
2202 let now = Instant::now();
2203 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now);
2204 assert!(recognizer.offer_long_press(touch).is_some());
2205 recognizer.resolve_long_press(false);
2206
2207 let ended = recognizer.handle_event_at(
2208 &touch_event(touch, TouchPhase::Ended, 10., 20.),
2209 now + Duration::from_millis(510),
2210 );
2211 assert!(matches!(
2212 ended.as_slice(),
2213 [RecognizedTouchGesture::Tap { .. }]
2214 ));
2215 }
2216
2217 #[test]
2218 fn unclaimed_long_press_can_still_become_a_pan() {
2219 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2220 let touch = TouchId(1);
2221 let now = Instant::now();
2222 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now);
2223 assert!(recognizer.offer_long_press(touch).is_some());
2224 recognizer.resolve_long_press(false);
2225
2226 let moved = recognizer.handle_event_at(
2227 &touch_event(touch, TouchPhase::Moved, 20., 0.),
2228 now + Duration::from_millis(510),
2229 );
2230 assert!(matches!(
2231 moved.as_slice(),
2232 [RecognizedTouchGesture::Scroll(ScrollEvent {
2233 touch_phase: TouchPhase::Started,
2234 ..
2235 })]
2236 ));
2237 }
2238
2239 #[test]
2240 fn long_press_offer_is_one_shot_and_specific_to_pending_touch() {
2241 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2242 let touch = TouchId(1);
2243 recognizer.handle_event(&touch_event(touch, TouchPhase::Started, 0., 0.));
2244
2245 assert!(
2246 recognizer
2247 .handle_event(&touch_event(TouchId(2), TouchPhase::Moved, 20., 0.))
2248 .is_empty()
2249 );
2250 assert!(recognizer.offer_long_press(TouchId(2)).is_none());
2251 assert!(recognizer.offer_long_press(touch).is_some());
2252 assert!(recognizer.offer_long_press(touch).is_none());
2253 }
2254
2255 #[test]
2256 fn long_press_cannot_be_offered_after_pending_touch_resolves() {
2257 for phase in [TouchPhase::Ended, TouchPhase::Cancelled, TouchPhase::Moved] {
2258 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2259 let touch = TouchId(1);
2260 let now = Instant::now();
2261 recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now);
2262 let position = if phase == TouchPhase::Moved { 20. } else { 0. };
2263 recognizer.handle_event_at(
2264 &touch_event(touch, phase, position, 0.),
2265 now + Duration::from_millis(10),
2266 );
2267 assert!(recognizer.offer_long_press(touch).is_none());
2268 }
2269 }
2270
2271 #[test]
2272 fn claimed_long_press_emits_cancelled_for_its_touch_only() {
2273 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2274 let touch = TouchId(1);
2275 recognizer.handle_event(&touch_event(touch, TouchPhase::Started, 4., 5.));
2276 assert!(recognizer.offer_long_press(touch).is_some());
2277 recognizer.resolve_long_press(true);
2278
2279 assert!(
2280 recognizer
2281 .handle_event(&touch_event(TouchId(2), TouchPhase::Cancelled, 9., 9.))
2282 .is_empty()
2283 );
2284 let cancelled = recognizer.handle_event(&touch_event(touch, TouchPhase::Cancelled, 6., 7.));
2285 let [RecognizedTouchGesture::LongPress(cancelled)] = cancelled.as_slice() else {
2286 panic!("expected cancelled long press, got {cancelled:?}");
2287 };
2288 assert_eq!(cancelled.phase, TouchPhase::Cancelled);
2289 assert_eq!(cancelled.start_position, point(px(4.), px(5.)));
2290 assert_eq!(cancelled.position, point(px(6.), px(7.)));
2291 }
2292
2293 #[test]
2294 fn unrelated_touch_cannot_end_or_cancel_pending_touch() {
2295 for phase in [TouchPhase::Ended, TouchPhase::Cancelled] {
2296 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2297 let touch = TouchId(1);
2298 recognizer.handle_event(&touch_event(touch, TouchPhase::Started, 4., 5.));
2299
2300 assert!(
2301 recognizer
2302 .handle_event(&touch_event(TouchId(2), phase, 9., 9.))
2303 .is_empty()
2304 );
2305 assert!(recognizer.offer_long_press(touch).is_some());
2306 }
2307 }
2308
2309 #[test]
2310 fn completed_touch_id_cannot_claim_replacement_touch() {
2311 let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
2312 let completed_touch = TouchId(1);
2313 let replacement_touch = TouchId(2);
2314 recognizer.handle_event(&touch_event(completed_touch, TouchPhase::Started, 0., 0.));
2315 recognizer.handle_event(&touch_event(completed_touch, TouchPhase::Cancelled, 0., 0.));
2316 recognizer.handle_event(&touch_event(replacement_touch, TouchPhase::Started, 5., 5.));
2317
2318 assert!(recognizer.offer_long_press(completed_touch).is_none());
2319 assert!(recognizer.offer_long_press(replacement_touch).is_some());
2320 }
2321
2322 fn touch_event(id: TouchId, phase: TouchPhase, x: f32, y: f32) -> TouchEvent {
2323 TouchEvent {
2324 id,
2325 phase,
2326 position: point(px(x), px(y)),
2327 predicted_position: None,
2328 force: None,
2329 }
2330 }
2331}