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