1use std::time::Duration;
9
10use tessera_ui::{
11 CursorEventContent, GestureState, PointerChange, PointerEventPass, PointerId,
12 PressKeyEventType, Px, PxPosition, ScrollDeltaUnit, ScrollEventContent, ScrollEventSource,
13 time::Instant,
14};
15
16const DEFAULT_SLOP_PX: f32 = 8.0;
17
18#[derive(Clone, Copy, Debug, PartialEq, Default)]
23struct PressTrack {
24 start: Option<PxPosition>,
26 last: Option<PxPosition>,
28}
29
30impl PressTrack {
31 fn on_press(&mut self, position: Option<PxPosition>) {
32 self.start = position;
33 self.last = position;
34 }
35
36 fn observe(&mut self, position: PxPosition, slop_px: f32) -> Observation {
43 let Some(start) = self.start else {
44 self.start = Some(position);
45 self.last = Some(position);
46 return Observation {
47 started_now: true,
48 exceeded_slop: false,
49 travel: (0.0, 0.0),
50 delta: (Px::ZERO, Px::ZERO),
51 };
52 };
53 let delta = match self.last {
54 Some(last) => (
55 Px((position.x - last.x).round() as i32),
56 Px((position.y - last.y).round() as i32),
57 ),
58 None => (Px::ZERO, Px::ZERO),
59 };
60 self.last = Some(position);
61 Observation {
62 started_now: false,
63 exceeded_slop: start.distance_to(position) > slop_px,
64 travel: (position.x - start.x, position.y - start.y),
65 delta,
66 }
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq)]
72struct Observation {
73 started_now: bool,
76 exceeded_slop: bool,
79 travel: (f32, f32),
81 delta: (Px, Px),
83}
84
85#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct TapSettings {
88 pub button: PressKeyEventType,
90 pub slop_px: f32,
92 pub consume_on_press: bool,
94 pub consume_on_release: bool,
96 pub consume_on_tap: bool,
98}
99
100impl Default for TapSettings {
101 fn default() -> Self {
102 Self {
103 button: PressKeyEventType::Left,
104 slop_px: DEFAULT_SLOP_PX,
105 consume_on_press: false,
106 consume_on_release: false,
107 consume_on_tap: false,
108 }
109 }
110}
111
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
114pub struct TapResult {
115 pub pressed: bool,
117 pub released: bool,
119 pub tapped: bool,
121 pub press_timestamp: Option<Instant>,
123 pub release_timestamp: Option<Instant>,
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Default)]
129pub struct TapRecognizer {
130 settings: TapSettings,
131 active_pointer: Option<PointerId>,
132 track: PressTrack,
133 canceled: bool,
134}
135
136impl TapRecognizer {
137 pub fn new(settings: TapSettings) -> Self {
139 Self {
140 settings,
141 ..Self::default()
142 }
143 }
144
145 pub fn update(
162 &mut self,
163 pass: PointerEventPass,
164 pointer_changes: &mut [PointerChange],
165 within_bounds: bool,
166 ) -> TapResult {
167 self.update_inner(pass, pointer_changes, within_bounds, false)
168 }
169
170 pub fn update_exclusive(
175 &mut self,
176 pass: PointerEventPass,
177 pointer_changes: &mut [PointerChange],
178 within_bounds: bool,
179 ) -> TapResult {
180 self.update_inner(pass, pointer_changes, within_bounds, true)
181 }
182
183 fn update_inner(
184 &mut self,
185 pass: PointerEventPass,
186 pointer_changes: &mut [PointerChange],
187 within_bounds: bool,
188 exclusive: bool,
189 ) -> TapResult {
190 if pass != PointerEventPass::Main {
191 return TapResult::default();
192 }
193
194 let mut result = TapResult::default();
195 for change in pointer_changes.iter_mut() {
196 if change.is_consumed() {
197 continue;
198 }
199 match change.content {
200 CursorEventContent::Pressed(button)
201 if button == self.settings.button && within_bounds =>
202 {
203 if exclusive && !change.claim_tap_press() {
204 continue;
205 }
206 self.active_pointer = Some(change.pointer_id);
207 self.track.on_press(change.position());
208 self.canceled = false;
209 result.pressed = true;
210 result.press_timestamp = Some(change.timestamp);
211 if self.settings.consume_on_press {
212 change.consume();
213 }
214 }
215 CursorEventContent::Moved(position) => {
216 if Some(change.pointer_id) == self.active_pointer {
217 let observation = self.track.observe(position, self.settings.slop_px);
218 if observation.exceeded_slop {
219 self.canceled = true;
220 }
221 }
222 }
223 CursorEventContent::Scroll(_) if Some(change.pointer_id) == self.active_pointer => {
224 self.canceled = true;
225 }
226 CursorEventContent::Cancelled(button) if button == self.settings.button => {
227 if Some(change.pointer_id) == self.active_pointer {
228 result.released = true;
229 self.reset();
230 }
231 }
232 CursorEventContent::Released(button) if button == self.settings.button => {
233 if Some(change.pointer_id) != self.active_pointer {
234 continue;
235 }
236 result.released = true;
237 result.release_timestamp = Some(change.timestamp);
238 let tapped = within_bounds
239 && !self.canceled
240 && change.gesture_state == GestureState::TapCandidate;
241 if tapped {
242 result.tapped = true;
243 }
244 if self.settings.consume_on_release || (self.settings.consume_on_tap && tapped)
245 {
246 change.consume();
247 }
248 self.reset();
249 }
250 _ => {}
251 }
252 }
253 result
254 }
255
256 pub fn cancel(&mut self) {
258 self.reset();
259 }
260
261 fn reset(&mut self) {
262 self.active_pointer = None;
263 self.track.on_press(None);
264 self.canceled = false;
265 }
266}
267
268#[derive(Clone, Copy, Debug, PartialEq)]
270pub struct DragSettings {
271 pub slop_px: f32,
273 pub consume_when_dragging: bool,
275 pub axis: Option<DragAxis>,
277}
278
279impl Default for DragSettings {
280 fn default() -> Self {
281 Self {
282 slop_px: DEFAULT_SLOP_PX,
283 consume_when_dragging: true,
284 axis: None,
285 }
286 }
287}
288
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
291pub enum DragAxis {
292 Horizontal,
294 Vertical,
296}
297
298#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
300pub struct DragResult {
301 pub started: bool,
303 pub updated: bool,
305 pub ended: bool,
307 pub delta_x: Px,
309 pub delta_y: Px,
311}
312
313#[derive(Clone, Copy, Debug, PartialEq, Default)]
315pub struct DragRecognizer {
316 delta_remainder: PxPosition,
317 settings: DragSettings,
318 active_pointer: Option<PointerId>,
319 track: PressTrack,
320 dragging: bool,
321}
322
323impl DragRecognizer {
324 pub fn new(settings: DragSettings) -> Self {
326 Self {
327 settings,
328 ..Self::default()
329 }
330 }
331
332 pub fn update(
345 &mut self,
346 pass: PointerEventPass,
347 pointer_changes: &mut [PointerChange],
348 within_bounds: bool,
349 ) -> DragResult {
350 if pass != PointerEventPass::Main {
351 return DragResult::default();
352 }
353
354 let mut result = DragResult::default();
355 for change in pointer_changes.iter_mut() {
356 if change.is_consumed() {
357 continue;
358 }
359 match change.content {
360 CursorEventContent::Pressed(PressKeyEventType::Left) if within_bounds => {
361 self.delta_remainder = PxPosition::ZERO;
362 self.active_pointer = Some(change.pointer_id);
363 self.track.on_press(change.position());
364 self.dragging = false;
365 }
366 CursorEventContent::Moved(position) => {
367 if Some(change.pointer_id) != self.active_pointer {
368 continue;
369 }
370 let observation = self.track.observe(position, self.settings.slop_px);
371 if observation.started_now {
372 continue;
373 }
374 if !self.dragging {
375 let (travel_x, travel_y) = observation.travel;
376 let should_start = match self.settings.axis {
377 Some(DragAxis::Horizontal) => {
378 travel_x.abs() > self.settings.slop_px
379 && travel_x.abs() >= travel_y.abs()
380 }
381 Some(DragAxis::Vertical) => {
382 travel_y.abs() > self.settings.slop_px
383 && travel_y.abs() >= travel_x.abs()
384 }
385 None => observation.exceeded_slop,
386 };
387 if should_start {
388 self.dragging = true;
389 result.started = true;
390 }
391 }
392 if self.dragging {
393 let (raw_dx, raw_dy) = observation.delta;
394 let (dx, dy) = match self.settings.axis {
395 Some(DragAxis::Horizontal) => (raw_dx, Px::ZERO),
396 Some(DragAxis::Vertical) => (Px::ZERO, raw_dy),
397 None => (raw_dx, raw_dy),
398 };
399 if dx != Px::ZERO || dy != Px::ZERO {
400 result.delta_x += dx;
401 result.delta_y += dy;
402 result.updated = true;
403 }
404 if self.settings.consume_when_dragging {
405 change.consume();
406 }
407 }
408 }
409 CursorEventContent::Cancelled(PressKeyEventType::Left) => {
410 if Some(change.pointer_id) == self.active_pointer {
411 self.reset();
412 }
413 }
414 CursorEventContent::Released(PressKeyEventType::Left) => {
415 if Some(change.pointer_id) != self.active_pointer {
416 continue;
417 }
418 result.ended = true;
419 if self.dragging && self.settings.consume_when_dragging {
420 change.consume();
421 }
422 self.reset();
423 }
424 CursorEventContent::Scroll(_) => {}
425 _ => {}
426 }
427 }
428 result
429 }
430
431 pub fn set_settings(&mut self, settings: DragSettings) {
433 self.settings = settings;
434 }
435
436 pub fn is_dragging(&self) -> bool {
438 self.dragging
439 }
440
441 pub(crate) fn local_integer_delta(
442 &mut self,
443 x: Px,
444 y: Px,
445 transform: tessera_ui::LocalToWindow,
446 ) -> (Px, Px) {
447 let window = PxPosition::new(x, y) + self.delta_remainder;
448 let local = transform.inverse_vector(window);
449 let rounded = (local.x_px(), local.y_px());
450 self.delta_remainder = window - transform.map_vector(PxPosition::new(rounded.0, rounded.1));
451 rounded
452 }
453
454 fn reset(&mut self) {
455 self.active_pointer = None;
456 self.track.on_press(None);
457 self.dragging = false;
458 }
459}
460
461#[derive(Clone, Copy, Debug, PartialEq)]
463pub struct LongPressSettings {
464 pub threshold: Duration,
466 pub slop_px: f32,
468 pub consume_on_trigger: bool,
470}
471
472impl Default for LongPressSettings {
473 fn default() -> Self {
474 Self {
475 threshold: Duration::from_millis(500),
476 slop_px: DEFAULT_SLOP_PX,
477 consume_on_trigger: false,
478 }
479 }
480}
481
482#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
484pub struct LongPressResult {
485 pub triggered: bool,
487 pub released: bool,
489}
490
491#[derive(Clone, Copy, Debug, PartialEq, Default)]
493pub struct LongPressRecognizer {
494 settings: LongPressSettings,
495 active_pointer: Option<PointerId>,
496 track: PressTrack,
497 press_time: Option<Instant>,
498 canceled: bool,
499 triggered: bool,
500 frame_press_start: Option<u64>,
501 within_bounds: bool,
502}
503
504impl LongPressRecognizer {
505 pub fn new(settings: LongPressSettings) -> Self {
507 Self {
508 settings,
509 ..Self::default()
510 }
511 }
512
513 pub fn update(
526 &mut self,
527 pass: PointerEventPass,
528 pointer_changes: &mut [PointerChange],
529 within_bounds: bool,
530 ) -> LongPressResult {
531 self.update_inner(pass, pointer_changes, within_bounds, None, None)
532 }
533
534 pub fn update_at_frame_nanos(
537 &mut self,
538 pass: PointerEventPass,
539 pointer_changes: &mut [PointerChange],
540 within_bounds: bool,
541 frame_nanos: u64,
542 ) -> LongPressResult {
543 self.update_frame_inner(pass, pointer_changes, within_bounds, frame_nanos, None)
544 }
545
546 pub fn update_after_tap(
549 &mut self,
550 pass: PointerEventPass,
551 pointer_changes: &mut [PointerChange],
552 within_bounds: bool,
553 frame_nanos: u64,
554 tap: TapResult,
555 ) -> LongPressResult {
556 self.update_frame_inner(pass, pointer_changes, within_bounds, frame_nanos, Some(tap))
557 }
558
559 fn update_frame_inner(
560 &mut self,
561 pass: PointerEventPass,
562 pointer_changes: &mut [PointerChange],
563 within_bounds: bool,
564 frame_nanos: u64,
565 tap: Option<TapResult>,
566 ) -> LongPressResult {
567 if pass == PointerEventPass::Final {
568 if pointer_changes.iter().any(|change| {
569 Some(change.pointer_id) == self.active_pointer
570 && (change.gesture_state == GestureState::Dragged
571 || matches!(change.content, CursorEventContent::Scroll(_)))
572 }) {
573 self.canceled = true;
574 }
575 return LongPressResult::default();
576 }
577 self.update_inner(pass, pointer_changes, within_bounds, Some(frame_nanos), tap)
578 }
579
580 pub fn needs_frame(&self) -> bool {
582 self.active_pointer.is_some()
583 && self.frame_press_start.is_some()
584 && !self.canceled
585 && !self.triggered
586 }
587
588 pub fn has_triggered(&self) -> bool {
590 self.triggered
591 }
592
593 pub fn is_active(&self) -> bool {
595 self.active_pointer.is_some()
596 }
597
598 pub fn cancel(&mut self) {
600 self.reset();
601 }
602
603 pub fn advance_frame_nanos(&mut self, frame_nanos: u64) -> bool {
605 if !self.needs_frame() || !self.within_bounds {
606 return false;
607 }
608 let elapsed = frame_nanos.saturating_sub(self.frame_press_start.unwrap());
609 if u128::from(elapsed) < self.settings.threshold.as_nanos() {
610 return false;
611 }
612 self.triggered = true;
613 true
614 }
615
616 fn update_inner(
617 &mut self,
618 pass: PointerEventPass,
619 pointer_changes: &mut [PointerChange],
620 within_bounds: bool,
621 frame_nanos: Option<u64>,
622 tap: Option<TapResult>,
623 ) -> LongPressResult {
624 if pass != PointerEventPass::Main {
625 return LongPressResult::default();
626 }
627 let mut result = LongPressResult::default();
628 for change in pointer_changes.iter_mut() {
629 let owned = tap.is_some_and(|tap| match change.content {
630 CursorEventContent::Pressed(PressKeyEventType::Left) => tap.pressed,
631 CursorEventContent::Released(PressKeyEventType::Left)
632 | CursorEventContent::Cancelled(PressKeyEventType::Left) => tap.released,
633 _ => false,
634 });
635 if change.is_consumed() && !owned {
636 continue;
637 }
638 match change.content {
639 CursorEventContent::Pressed(PressKeyEventType::Left)
640 if within_bounds && tap.is_none_or(|tap| tap.pressed) =>
641 {
642 self.active_pointer = Some(change.pointer_id);
643 self.within_bounds = within_bounds;
644 self.track.on_press(change.position());
645 self.press_time = Some(change.timestamp);
646 self.frame_press_start = frame_nanos;
647 self.canceled = false;
648 self.triggered = false;
649 }
650 CursorEventContent::Moved(position) => {
651 if Some(change.pointer_id) != self.active_pointer {
652 continue;
653 }
654 self.within_bounds = within_bounds;
655 let observation = self.track.observe(position, self.settings.slop_px);
656 if observation.exceeded_slop || (frame_nanos.is_some() && !within_bounds) {
657 self.canceled = true;
658 }
659 self.try_trigger(
660 change.timestamp,
661 within_bounds,
662 frame_nanos,
663 change,
664 &mut result,
665 );
666 }
667 CursorEventContent::Scroll(_) if Some(change.pointer_id) == self.active_pointer => {
668 self.canceled = true;
669 }
670 CursorEventContent::Cancelled(PressKeyEventType::Left) => {
671 if Some(change.pointer_id) == self.active_pointer {
672 self.reset();
673 }
674 }
675 CursorEventContent::Released(PressKeyEventType::Left) => {
676 if Some(change.pointer_id) != self.active_pointer {
677 continue;
678 }
679 self.within_bounds = within_bounds;
680 self.try_trigger(
681 change.timestamp,
682 within_bounds,
683 frame_nanos,
684 change,
685 &mut result,
686 );
687 result.released = true;
688 self.reset();
689 }
690 _ => {}
691 }
692 }
693 result
694 }
695
696 fn try_trigger(
697 &mut self,
698 now: Instant,
699 within_bounds: bool,
700 frame_nanos: Option<u64>,
701 change: &mut PointerChange,
702 result: &mut LongPressResult,
703 ) {
704 if let Some(frame) = frame_nanos {
705 if self.advance_frame_nanos(frame) {
706 result.triggered = true;
707 if self.settings.consume_on_trigger {
708 change.consume();
709 }
710 return;
711 }
712 }
713 if self.triggered || self.canceled || !within_bounds {
714 return;
715 }
716 let Some(start) = self.press_time else {
717 return;
718 };
719 if now.duration_since(start) >= self.settings.threshold {
720 self.triggered = true;
721 result.triggered = true;
722 if self.settings.consume_on_trigger {
723 change.consume();
724 }
725 }
726 }
727
728 fn reset(&mut self) {
729 self.active_pointer = None;
730 self.track.on_press(None);
731 self.press_time = None;
732 self.frame_press_start = None;
733 self.within_bounds = false;
734 self.canceled = false;
735 self.triggered = false;
736 }
737}
738
739#[derive(Clone, Copy, Debug, PartialEq, Default)]
741pub struct ScrollSettings {
742 pub consume: bool,
744}
745
746#[derive(Clone, Debug, Default, PartialEq)]
748pub struct ScrollResult {
749 pub event_count: usize,
751 pub delta_x: f32,
753 pub delta_y: f32,
755 pub source: Option<ScrollEventSource>,
757 pub unit: Option<ScrollDeltaUnit>,
759}
760
761impl ScrollResult {
762 pub fn has_scroll(&self) -> bool {
764 self.event_count > 0
765 }
766}
767
768#[derive(Clone, Copy, Debug, PartialEq, Eq)]
771pub struct ScrollChangeContext {
772 pub pointer_id: PointerId,
774 pub timestamp: Instant,
776}
777
778#[derive(Clone, Copy, Debug, PartialEq, Default)]
780pub struct ScrollRecognizer {
781 settings: ScrollSettings,
782}
783
784impl ScrollRecognizer {
785 pub fn new(settings: ScrollSettings) -> Self {
787 Self { settings }
788 }
789
790 pub fn for_each(
793 &mut self,
794 pass: PointerEventPass,
795 pointer_changes: &mut [PointerChange],
796 mut handle_scroll: impl FnMut(ScrollChangeContext, &mut ScrollEventContent),
797 ) -> ScrollResult {
798 if pass != PointerEventPass::Main {
799 return ScrollResult::default();
800 }
801
802 let mut result = ScrollResult::default();
803 for change in pointer_changes.iter_mut() {
804 if change.is_consumed() {
805 continue;
806 }
807 let CursorEventContent::Scroll(ref mut scroll) = change.content else {
808 continue;
809 };
810 if result.source.is_none() {
811 result.source = Some(scroll.source);
812 }
813 if result.unit.is_none() {
814 result.unit = Some(scroll.unit);
815 }
816 result.event_count += 1;
817 result.delta_x += scroll.delta_x;
818 result.delta_y += scroll.delta_y;
819
820 handle_scroll(
821 ScrollChangeContext {
822 pointer_id: change.pointer_id,
823 timestamp: change.timestamp,
824 },
825 scroll,
826 );
827
828 if self.settings.consume
829 || (scroll.delta_x.abs() <= f32::EPSILON && scroll.delta_y.abs() <= f32::EPSILON)
830 {
831 change.consume();
832 }
833 }
834 result
835 }
836
837 pub fn update(
839 &mut self,
840 pass: PointerEventPass,
841 pointer_changes: &mut [PointerChange],
842 ) -> ScrollResult {
843 if pass != PointerEventPass::Main {
844 return ScrollResult::default();
845 }
846
847 let mut result = ScrollResult::default();
848 for change in pointer_changes.iter_mut() {
849 if change.is_consumed() {
850 continue;
851 }
852 let CursorEventContent::Scroll(ref scroll) = change.content else {
853 continue;
854 };
855 if result.source.is_none() {
856 result.source = Some(scroll.source);
857 }
858 if result.unit.is_none() {
859 result.unit = Some(scroll.unit);
860 }
861 result.event_count += 1;
862 result.delta_x += scroll.delta_x;
863 result.delta_y += scroll.delta_y;
864 if self.settings.consume {
865 change.consume();
866 }
867 }
868 result
869 }
870}
871
872#[cfg(test)]
873mod cancellation_tests {
874 use super::*;
875 #[test]
876 fn cancelled_touch_clears_tap_drag_and_long_press_without_completion() {
877 let mut tap = TapRecognizer::default();
878 let mut drag = DragRecognizer::default();
879 let mut long_press = LongPressRecognizer::default();
880 let now = Instant::now();
881 for content in [
882 CursorEventContent::Pressed(PressKeyEventType::Left),
883 CursorEventContent::Cancelled(PressKeyEventType::Left),
884 ] {
885 let change = PointerChange::new(now, 1, content, GestureState::TapCandidate);
886 let tapped = tap.update(PointerEventPass::Main, &mut [change.clone()], true);
887 let dragged = drag.update(PointerEventPass::Main, &mut [change.clone()], true);
888 let pressed = long_press.update(PointerEventPass::Main, &mut [change], true);
889 assert!(!tapped.tapped);
890 assert!(tapped.release_timestamp.is_none());
891 assert!(!dragged.ended);
892 assert!(!pressed.triggered);
893 }
894 assert!(tap.active_pointer.is_none());
895 assert!(drag.active_pointer.is_none());
896 assert!(long_press.active_pointer.is_none());
897 }
898}
899#[cfg(test)]
900mod slop_tests {
901 use super::*;
902
903 const POINTER: PointerId = 7;
904
905 fn change(content: CursorEventContent, at: Instant) -> PointerChange {
906 PointerChange::new(at, POINTER, content, GestureState::TapCandidate)
907 }
908
909 fn moved(x: i32, y: i32) -> CursorEventContent {
910 CursorEventContent::Moved(PxPosition::new(Px::new(x), Px::new(y)))
911 }
912
913 fn press(tap: &mut TapRecognizer, at: Instant) {
914 let mut changes = [change(
915 CursorEventContent::Pressed(PressKeyEventType::Left),
916 at,
917 )];
918 tap.update(PointerEventPass::Main, &mut changes, true);
919 }
920
921 #[test]
922 fn exclusive_taps_leave_claimed_presses_visible_to_scroll_observers() {
923 let mut child = TapRecognizer::default();
924 let mut parent = TapRecognizer::default();
925 let mut observer = TapRecognizer::default();
926 let now = Instant::now();
927 for _ in 0..2 {
928 let mut down = [change(
929 CursorEventContent::Pressed(PressKeyEventType::Left),
930 now,
931 )];
932 assert!(
933 !child
934 .update_exclusive(PointerEventPass::Initial, &mut down, true)
935 .pressed
936 );
937 assert!(
938 child
939 .update_exclusive(PointerEventPass::Main, &mut down, true)
940 .pressed
941 );
942 assert!(
943 !parent
944 .update_exclusive(PointerEventPass::Main, &mut down, true)
945 .pressed
946 );
947 assert!(!down[0].is_consumed());
948 assert!(
949 observer
950 .update(PointerEventPass::Main, &mut down, true)
951 .pressed
952 );
953 let mut up = [change(
954 CursorEventContent::Released(PressKeyEventType::Left),
955 now,
956 )];
957 assert!(
958 child
959 .update_exclusive(PointerEventPass::Main, &mut up, true)
960 .tapped
961 );
962 assert!(
963 !parent
964 .update_exclusive(PointerEventPass::Main, &mut up, true)
965 .tapped
966 );
967 assert!(
968 observer
969 .update(PointerEventPass::Main, &mut up, true)
970 .released
971 );
972 }
973 }
974
975 #[test]
976 fn first_move_sample_establishes_the_slop_origin() {
977 let mut tap = TapRecognizer::default();
979 let now = Instant::now();
980 press(&mut tap, now);
981
982 let mut first = [change(moved(1000, 1000), now)];
983 tap.update(PointerEventPass::Main, &mut first, true);
984 let mut same = [change(moved(1000, 1000), now)];
985 tap.update(PointerEventPass::Main, &mut same, true);
986
987 assert!(!tap.canceled);
988 }
989
990 #[test]
991 fn recorded_press_anchors_the_first_drag_and_cancels_a_travelled_tap() {
992 let now = Instant::now();
993 let press = change(CursorEventContent::Pressed(PressKeyEventType::Left), now)
994 .with_position(PxPosition::from_f32_xy(100.0, 200.0));
995 let mut drag = DragRecognizer::default();
996 let mut tap = TapRecognizer::default();
997 drag.update(PointerEventPass::Main, &mut [press.clone()], true);
998 tap.update(PointerEventPass::Main, &mut [press], true);
999 let movement = change(moved(100, 180), now + Duration::from_millis(16));
1000 let result = drag.update(PointerEventPass::Main, &mut [movement.clone()], true);
1001 assert!(result.started && result.updated);
1002 assert_eq!(result.delta_y, Px::new(-20));
1003 tap.update(PointerEventPass::Main, &mut [movement], true);
1004 let result = tap.update(
1005 PointerEventPass::Main,
1006 &mut [change(
1007 CursorEventContent::Released(PressKeyEventType::Left),
1008 now + Duration::from_millis(32),
1009 )],
1010 true,
1011 );
1012 assert!(!result.tapped);
1013 }
1014
1015 #[test]
1016 fn recorded_press_cancels_long_press_on_the_first_large_movement() {
1017 let now = Instant::now();
1018 let mut recognizer = LongPressRecognizer::default();
1019 let press = change(CursorEventContent::Pressed(PressKeyEventType::Left), now)
1020 .with_position(PxPosition::from_f32_xy(100.0, 200.0));
1021 recognizer.update(PointerEventPass::Main, &mut [press], true);
1022 recognizer.update(
1023 PointerEventPass::Main,
1024 &mut [change(moved(100, 180), now + Duration::from_millis(16))],
1025 true,
1026 );
1027 let result = recognizer.update(
1028 PointerEventPass::Main,
1029 &mut [change(moved(100, 180), now + Duration::from_millis(800))],
1030 true,
1031 );
1032 assert!(!result.triggered);
1033 }
1034
1035 #[test]
1036 fn tap_cancels_once_travel_exceeds_slop() {
1037 let mut tap = TapRecognizer::new(TapSettings {
1038 slop_px: 8.0,
1039 ..Default::default()
1040 });
1041 let now = Instant::now();
1042 press(&mut tap, now);
1043
1044 let mut origin = [change(moved(0, 0), now)];
1045 tap.update(PointerEventPass::Main, &mut origin, true);
1046 let mut within = [change(moved(4, 4), now)];
1047 tap.update(PointerEventPass::Main, &mut within, true);
1048 assert!(!tap.canceled);
1049
1050 let mut beyond = [change(moved(50, 0), now)];
1051 tap.update(PointerEventPass::Main, &mut beyond, true);
1052 assert!(tap.canceled);
1053
1054 let mut release = [change(
1055 CursorEventContent::Released(PressKeyEventType::Left),
1056 now,
1057 )];
1058 let result = tap.update(PointerEventPass::Main, &mut release, true);
1059 assert!(result.released);
1060 assert!(!result.tapped);
1061 }
1062
1063 #[test]
1064 fn tap_completes_when_release_stays_within_slop_and_bounds() {
1065 let mut tap = TapRecognizer::default();
1066 let now = Instant::now();
1067 press(&mut tap, now);
1068
1069 let mut origin = [change(moved(0, 0), now)];
1070 tap.update(PointerEventPass::Main, &mut origin, true);
1071 let mut release = [change(
1072 CursorEventContent::Released(PressKeyEventType::Left),
1073 now,
1074 )];
1075 let result = tap.update(PointerEventPass::Main, &mut release, true);
1076
1077 assert!(result.tapped);
1078 }
1079
1080 #[test]
1081 fn press_outside_bounds_does_not_begin_a_gesture() {
1082 let mut tap = TapRecognizer::default();
1083 let now = Instant::now();
1084 let mut press = [change(
1085 CursorEventContent::Pressed(PressKeyEventType::Left),
1086 now,
1087 )];
1088 let result = tap.update(PointerEventPass::Main, &mut press, false);
1089
1090 assert!(!result.pressed);
1091 assert!(tap.active_pointer.is_none());
1092 }
1093
1094 #[test]
1095 fn drag_starts_after_slop_and_reports_sample_deltas() {
1096 let mut drag = DragRecognizer::new(DragSettings {
1097 slop_px: 8.0,
1098 ..Default::default()
1099 });
1100 let now = Instant::now();
1101 let mut press = [change(
1102 CursorEventContent::Pressed(PressKeyEventType::Left),
1103 now,
1104 )];
1105 drag.update(PointerEventPass::Main, &mut press, true);
1106
1107 let mut origin = [change(moved(0, 0), now)];
1108 let first = drag.update(PointerEventPass::Main, &mut origin, true);
1109 assert!(!first.started);
1110
1111 let mut within = [change(moved(5, 0), now)];
1112 assert!(
1113 !drag
1114 .update(PointerEventPass::Main, &mut within, true)
1115 .started
1116 );
1117
1118 let mut beyond = [change(moved(20, 0), now)];
1119 let started = drag.update(PointerEventPass::Main, &mut beyond, true);
1120 assert!(started.started);
1121 assert_eq!(started.delta_x, Px::new(15));
1122 assert_eq!(started.delta_y, Px::ZERO);
1123 }
1124
1125 #[test]
1126 fn local_integer_drag_delta_retains_fractional_movement_in_window_units() {
1127 let mut drag = DragRecognizer::new(DragSettings {
1128 slop_px: 0.0,
1129 ..Default::default()
1130 });
1131 let now = Instant::now();
1132 let mut press = [
1133 change(CursorEventContent::Pressed(PressKeyEventType::Left), now)
1134 .with_position(PxPosition::ZERO),
1135 ];
1136 drag.update(PointerEventPass::Main, &mut press, true);
1137 let mut deltas = Vec::new();
1138 for x in 1..=5 {
1139 let mut movement = [change(moved(x, 0), now)];
1140 let result = drag.update(PointerEventPass::Main, &mut movement, true);
1141 assert!(result.updated);
1142 let transform = tessera_ui::LocalToWindow::new(
1143 0.625,
1144 PxPosition::from_f32_xy(x as f32 * 10.25, 20.5),
1145 )
1146 .unwrap();
1147 deltas.push(
1148 drag.local_integer_delta(result.delta_x, result.delta_y, transform)
1149 .0
1150 .0,
1151 );
1152 }
1153 assert_eq!(deltas, [2, 1, 2, 1, 2]);
1154 assert_eq!(
1155 deltas.iter().sum::<i32>(),
1156 8,
1157 "Five window pixels must preserve exactly eight local pixels"
1158 );
1159 }
1160
1161 #[test]
1162 fn horizontal_drag_ignores_vertical_travel() {
1163 let mut drag = DragRecognizer::new(DragSettings {
1164 slop_px: 8.0,
1165 axis: Some(DragAxis::Horizontal),
1166 ..Default::default()
1167 });
1168 let now = Instant::now();
1169 let mut press = [change(
1170 CursorEventContent::Pressed(PressKeyEventType::Left),
1171 now,
1172 )];
1173 drag.update(PointerEventPass::Main, &mut press, true);
1174 let mut origin = [change(moved(0, 0), now)];
1175 drag.update(PointerEventPass::Main, &mut origin, true);
1176
1177 let mut vertical = [change(moved(0, 40), now)];
1178 let result = drag.update(PointerEventPass::Main, &mut vertical, true);
1179
1180 assert!(!result.started);
1181 assert!(!drag.is_dragging());
1182 }
1183
1184 #[test]
1185 fn long_press_triggers_after_the_threshold() {
1186 let mut long_press = LongPressRecognizer::default();
1187 let start = Instant::now();
1188 let mut press = [change(
1189 CursorEventContent::Pressed(PressKeyEventType::Left),
1190 start,
1191 )];
1192 long_press.update(PointerEventPass::Main, &mut press, true);
1193 let mut origin = [change(moved(0, 0), start)];
1194 long_press.update(PointerEventPass::Main, &mut origin, true);
1195
1196 let mut hold = [change(moved(0, 0), start + Duration::from_millis(600))];
1197 let result = long_press.update(PointerEventPass::Main, &mut hold, true);
1198
1199 assert!(result.triggered);
1200 }
1201
1202 #[test]
1203 fn frame_long_press_deadline_is_one_shot_and_respects_cancellation_and_pointer_identity() {
1204 let start = Instant::now();
1205 let mut recognizer = LongPressRecognizer::default();
1206 let mut press = [change(
1207 CursorEventContent::Pressed(PressKeyEventType::Left),
1208 start,
1209 )];
1210 recognizer.update_at_frame_nanos(PointerEventPass::Main, &mut press, true, 10_000_000);
1211 assert!(!recognizer.advance_frame_nanos(509_999_999));
1212 let mut other = [PointerChange::new(
1213 start,
1214 POINTER + 1,
1215 moved(100, 100),
1216 GestureState::TapCandidate,
1217 )];
1218 recognizer.update_at_frame_nanos(PointerEventPass::Main, &mut other, false, 400_000_000);
1219 assert!(recognizer.advance_frame_nanos(510_000_000));
1220 assert!(recognizer.has_triggered());
1221 assert!(!recognizer.needs_frame());
1222 assert!(!recognizer.advance_frame_nanos(900_000_000));
1223 let mut release = [change(
1224 CursorEventContent::Released(PressKeyEventType::Left),
1225 start,
1226 )];
1227 let result = recognizer.update_at_frame_nanos(
1228 PointerEventPass::Main,
1229 &mut release,
1230 true,
1231 900_000_000,
1232 );
1233 assert!(result.released);
1234 assert!(!result.triggered);
1235 assert!(!recognizer.is_active());
1236 for cancelled in [true, false] {
1237 let mut press = [change(
1238 CursorEventContent::Pressed(PressKeyEventType::Left),
1239 start,
1240 )];
1241 recognizer.update_at_frame_nanos(
1242 PointerEventPass::Main,
1243 &mut press,
1244 true,
1245 1_000_000_000,
1246 );
1247 let mut end = [change(
1248 if cancelled {
1249 CursorEventContent::Cancelled(PressKeyEventType::Left)
1250 } else {
1251 moved(0, 0)
1252 },
1253 start,
1254 )];
1255 if !cancelled {
1256 end[0].gesture_state = GestureState::Dragged;
1257 }
1258 recognizer.update_at_frame_nanos(
1259 if cancelled {
1260 PointerEventPass::Main
1261 } else {
1262 PointerEventPass::Final
1263 },
1264 &mut end,
1265 true,
1266 1_100_000_000,
1267 );
1268 assert!(!recognizer.needs_frame());
1269 assert!(!recognizer.advance_frame_nanos(2_000_000_000));
1270 }
1271 }
1272
1273 #[test]
1274 fn frame_long_press_observes_owned_consumed_release_and_event_time_backlogs() {
1275 let start = Instant::now();
1276 let mut tap = TapRecognizer::new(TapSettings {
1277 consume_on_tap: true,
1278 ..Default::default()
1279 });
1280 let mut long = LongPressRecognizer::default();
1281 let mut press = [change(
1282 CursorEventContent::Pressed(PressKeyEventType::Left),
1283 start,
1284 )];
1285 let result = tap.update_exclusive(PointerEventPass::Main, &mut press, true);
1286 long.update_after_tap(PointerEventPass::Main, &mut press, true, 0, result);
1287 let mut release = [change(
1288 CursorEventContent::Released(PressKeyEventType::Left),
1289 start + Duration::from_millis(600),
1290 )];
1291 let result = tap.update_exclusive(PointerEventPass::Main, &mut release, true);
1292 assert!(result.tapped);
1293 assert!(release[0].is_consumed());
1294 let long_result =
1295 long.update_after_tap(PointerEventPass::Main, &mut release, true, 0, result);
1296 assert!(long_result.triggered);
1297 assert!(long_result.released);
1298 assert!(!long.is_active());
1299 let mut child = TapRecognizer::default();
1300 let mut press = [change(
1301 CursorEventContent::Pressed(PressKeyEventType::Left),
1302 start,
1303 )];
1304 child.update_exclusive(PointerEventPass::Main, &mut press, true);
1305 let result = tap.update_exclusive(PointerEventPass::Main, &mut press, true);
1306 assert!(!result.pressed);
1307 long.update_after_tap(PointerEventPass::Main, &mut press, true, 0, result);
1308 assert!(!long.needs_frame());
1309 }
1310
1311 #[test]
1312 fn long_press_is_canceled_by_travel_beyond_slop() {
1313 let mut long_press = LongPressRecognizer::default();
1314 let start = Instant::now();
1315 let mut press = [change(
1316 CursorEventContent::Pressed(PressKeyEventType::Left),
1317 start,
1318 )];
1319 long_press.update(PointerEventPass::Main, &mut press, true);
1320 let mut origin = [change(moved(0, 0), start)];
1321 long_press.update(PointerEventPass::Main, &mut origin, true);
1322
1323 let mut beyond = [change(moved(40, 0), start)];
1324 long_press.update(PointerEventPass::Main, &mut beyond, true);
1325 assert!(long_press.canceled);
1326
1327 let mut hold = [change(moved(40, 0), start + Duration::from_millis(600))];
1328 let result = long_press.update(PointerEventPass::Main, &mut hold, true);
1329 assert!(!result.triggered);
1330 }
1331}