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