Skip to main content

tessera_foundation/
gesture.rs

1//! Pointer gesture recognizers for tap, drag, long-press, and scroll.
2//!
3//! ## Usage
4//!
5//! Use recognizers to derive reusable pointer interaction behavior in shared
6//! modifiers and components.
7
8use 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/// Shared bookkeeping for a single press across pointer updates.
19///
20/// Recorded press positions anchor gesture travel. Synthetic events without
21/// a position fall back to the first movement sample.
22#[derive(Clone, Copy, Debug, PartialEq, Default)]
23struct PressTrack {
24    /// Recorded press position, or the first available movement sample.
25    start: Option<PxPosition>,
26    /// Most recent movement sample, used as the delta baseline.
27    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    /// Folds one movement sample into the track and reports the derived facts.
37    ///
38    /// `slop_px` is the slop radius used to compute
39    /// `Observation::exceeded_slop`. The first sample after a press
40    /// (`Observation::started_now`) only establishes the origin, so it reports
41    /// no travel, no delta, and cannot have exceeded the slop yet.
42    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/// Facts derived from one movement sample relative to the tracked press.
71#[derive(Clone, Copy, Debug, PartialEq)]
72struct Observation {
73    /// `true` when this sample established the slop origin, i.e. it is the
74    /// first movement sample observed after the press.
75    started_now: bool,
76    /// `true` when the sample lies strictly farther than the slop radius from
77    /// the origin. Always `false` for the establishing sample.
78    exceeded_slop: bool,
79    /// Travel from the origin along each axis, in pixels.
80    travel: (f32, f32),
81    /// Movement since the previous sample, in pixels.
82    delta: (Px, Px),
83}
84
85/// Configuration for tap gesture recognition.
86#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct TapSettings {
88    /// Mouse button or press key that starts the gesture.
89    pub button: PressKeyEventType,
90    /// Maximum pointer travel before the tap is canceled.
91    pub slop_px: f32,
92    /// Whether to consume the press event immediately.
93    pub consume_on_press: bool,
94    /// Whether to consume the release event.
95    pub consume_on_release: bool,
96    /// Whether to consume the completed tap event.
97    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/// Per-update tap recognition output.
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
114pub struct TapResult {
115    /// Whether a press started this update.
116    pub pressed: bool,
117    /// Whether a release happened this update.
118    pub released: bool,
119    /// Whether a full tap gesture completed this update.
120    pub tapped: bool,
121    /// Timestamp for the press, when present.
122    pub press_timestamp: Option<Instant>,
123    /// Timestamp for the release, when present.
124    pub release_timestamp: Option<Instant>,
125}
126
127/// Stateful tap gesture recognizer.
128#[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    /// Creates a tap recognizer with custom settings.
138    pub fn new(settings: TapSettings) -> Self {
139        Self {
140            settings,
141            ..Self::default()
142        }
143    }
144
145    /// Updates the recognizer with the current pointer pass and events.
146    ///
147    /// # Position source
148    ///
149    /// Positions come from the samples themselves in a shared coordinate space.
150    ///
151    /// # Slop origin
152    ///
153    /// The recorded press position anchors slop and movement. A press without
154    /// a recorded position uses the first movement sample as its origin.
155    ///
156    /// # `within_bounds`
157    ///
158    /// `within_bounds` is a semantic flag the caller computes from the node's
159    /// relative coordinates; it reports whether the pointer was inside this
160    /// node when the update ran.
161    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    /// Recognizes taps while claiming each press before ancestor tap handlers.
171    ///
172    /// Use for nested click targets. Claims preserve samples for drag and
173    /// scroll observers that use [`Self::update`].
174    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    /// Cancels the active press without producing a tap.
257    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/// Configuration for drag gesture recognition.
269#[derive(Clone, Copy, Debug, PartialEq)]
270pub struct DragSettings {
271    /// Minimum pointer travel required to start dragging.
272    pub slop_px: f32,
273    /// Whether drag events should be consumed after dragging starts.
274    pub consume_when_dragging: bool,
275    /// Optional axis lock for drag movement.
276    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/// Axis constraint for drag gesture recognition.
290#[derive(Clone, Copy, Debug, PartialEq, Eq)]
291pub enum DragAxis {
292    /// Horizontal-only dragging.
293    Horizontal,
294    /// Vertical-only dragging.
295    Vertical,
296}
297
298/// Per-update drag recognition output.
299#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
300pub struct DragResult {
301    /// Whether dragging started this update.
302    pub started: bool,
303    /// Whether a drag delta was produced this update.
304    pub updated: bool,
305    /// Whether dragging ended this update.
306    pub ended: bool,
307    /// Horizontal drag delta for this update.
308    pub delta_x: Px,
309    /// Vertical drag delta for this update.
310    pub delta_y: Px,
311}
312
313/// Stateful drag gesture recognizer.
314#[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    /// Creates a drag recognizer with custom settings.
325    pub fn new(settings: DragSettings) -> Self {
326        Self {
327            settings,
328            ..Self::default()
329        }
330    }
331
332    /// Updates the recognizer with the current pointer pass and events.
333    ///
334    /// # Slop origin
335    ///
336    /// The recorded press position anchors slop and movement. A press without
337    /// a recorded position uses the first movement sample as its origin.
338    ///
339    /// # `within_bounds`
340    ///
341    /// `within_bounds` is a semantic flag the caller computes from the node's
342    /// relative coordinates; it reports whether the pointer was inside this
343    /// node when the update ran.
344    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    /// Replaces the recognizer settings while preserving active gesture state.
432    pub fn set_settings(&mut self, settings: DragSettings) {
433        self.settings = settings;
434    }
435
436    /// Returns whether dragging is currently active.
437    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/// Configuration for long-press gesture recognition.
462#[derive(Clone, Copy, Debug, PartialEq)]
463pub struct LongPressSettings {
464    /// Minimum press duration before the gesture triggers.
465    pub threshold: Duration,
466    /// Maximum pointer travel before the press is canceled.
467    pub slop_px: f32,
468    /// Whether to consume the triggering event.
469    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/// Per-update long-press recognition output.
483#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
484pub struct LongPressResult {
485    /// Whether the long press triggered this update.
486    pub triggered: bool,
487    /// Whether the pointer was released this update.
488    pub released: bool,
489}
490
491/// Stateful long-press gesture recognizer.
492#[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    /// Creates a long-press recognizer with custom settings.
506    pub fn new(settings: LongPressSettings) -> Self {
507        Self {
508            settings,
509            ..Self::default()
510        }
511    }
512
513    /// Updates the recognizer with the current pointer pass and events.
514    ///
515    /// # Slop origin
516    ///
517    /// The recorded press position anchors slop and movement. A press without
518    /// a recorded position uses the first movement sample as its origin.
519    ///
520    /// # `within_bounds`
521    ///
522    /// `within_bounds` is a semantic flag the caller computes from the node's
523    /// relative coordinates; it reports whether the pointer was inside this
524    /// node when the update ran.
525    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    /// Updates a press using the UI frame clock, including final-pass drag
535    /// cancellation.
536    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    /// Observes the samples belonging to the supplied tap result, including
547    /// consumed releases.
548    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    /// Reports whether a frame-clock press still needs its deadline checked.
581    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    /// Reports whether the current press has already triggered.
589    pub fn has_triggered(&self) -> bool {
590        self.triggered
591    }
592
593    /// Reports whether a pointer press is being tracked.
594    pub fn is_active(&self) -> bool {
595        self.active_pointer.is_some()
596    }
597
598    /// Cancels the current press without activation.
599    pub fn cancel(&mut self) {
600        self.reset();
601    }
602
603    /// Advances a stationary press and returns true only at its first deadline.
604    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/// Configuration for scroll gesture aggregation.
740#[derive(Clone, Copy, Debug, PartialEq, Default)]
741pub struct ScrollSettings {
742    /// Whether to consume scroll events after they are aggregated.
743    pub consume: bool,
744}
745
746/// Aggregated scroll recognition output.
747#[derive(Clone, Debug, Default, PartialEq)]
748pub struct ScrollResult {
749    /// Number of scroll events observed in this update.
750    pub event_count: usize,
751    /// Total horizontal scroll delta.
752    pub delta_x: f32,
753    /// Total vertical scroll delta.
754    pub delta_y: f32,
755    /// Source of the first observed scroll event.
756    pub source: Option<ScrollEventSource>,
757    /// Delta unit of the first observed scroll event.
758    pub unit: Option<ScrollDeltaUnit>,
759}
760
761impl ScrollResult {
762    /// Returns whether any scroll event was observed in this update.
763    pub fn has_scroll(&self) -> bool {
764        self.event_count > 0
765    }
766}
767
768/// Metadata for a single scroll event routed through
769/// `ScrollRecognizer::for_each`.
770#[derive(Clone, Copy, Debug, PartialEq, Eq)]
771pub struct ScrollChangeContext {
772    /// Pointer id that produced the scroll event.
773    pub pointer_id: PointerId,
774    /// Timestamp of the scroll event.
775    pub timestamp: Instant,
776}
777
778/// Stateful scroll event recognizer and aggregator.
779#[derive(Clone, Copy, Debug, PartialEq, Default)]
780pub struct ScrollRecognizer {
781    settings: ScrollSettings,
782}
783
784impl ScrollRecognizer {
785    /// Creates a scroll recognizer with custom settings.
786    pub fn new(settings: ScrollSettings) -> Self {
787        Self { settings }
788    }
789
790    /// Aggregates scroll events and allows each one to be handled before
791    /// optional consumption.
792    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    /// Aggregates scroll events and optionally consumes them.
838    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        // Positionless synthetic presses retain the movement-origin fallback.
978        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}