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