Skip to main content

tessera_ui/
cursor.rs

1//! Cursor and touch input queues.
2//!
3//! ## Usage
4//!
5//! Track pointer positions and preserve ordered mouse, touch, and scroll
6//! events.
7
8use std::collections::{HashMap, VecDeque};
9
10use crate::{PxPosition, time::Instant};
11
12/// Pointer identifier used by input changes.
13pub type PointerId = u64;
14/// Pointer identifier reserved for mouse input.
15pub const MOUSE_POINTER_ID: PointerId = 0;
16
17/// Soft queue limit for compacting redundant motion samples during UI jank.
18/// Gesture boundaries and scroll deltas are never evicted to meet this limit.
19const KEEP_EVENTS_COUNT: usize = 128;
20
21#[derive(Debug, Clone)]
22struct TouchPointState {
23    pointer_id: PointerId,
24    last_position: PxPosition,
25    start_position: PxPosition,
26    last_update_time: Instant,
27    generated_scroll_event: bool,
28}
29
30#[derive(Debug, Clone)]
31struct TouchScrollConfig {
32    min_move_threshold: f32,
33    enabled: bool,
34}
35
36impl Default for TouchScrollConfig {
37    fn default() -> Self {
38        Self {
39            // Reduced threshold for more responsive touch
40            min_move_threshold: 5.0,
41            enabled: true,
42        }
43    }
44}
45
46/// Central state manager for cursor and touch interactions.
47///
48/// `CursorState` is the main interface for handling all cursor-related events
49/// in the Tessera UI framework. It manages cursor position tracking, pointer
50/// change queuing, and multi-touch support for touch gestures.
51#[derive(Default)]
52pub struct CursorState {
53    next_touch_pointer_id: PointerId,
54    /// Current cursor position, if any cursor is active.
55    position: Option<PxPosition>,
56    pointer_positions: HashMap<PointerId, Option<PxPosition>>,
57    /// Pointer changes awaiting dispatch.
58    events: VecDeque<PointerChange>,
59    /// Active touch points mapped by their unique touch IDs.
60    touch_points: HashMap<u64, TouchPointState>,
61    /// Configuration settings for touch scrolling behavior.
62    touch_scroll_config: TouchScrollConfig,
63    /// If true, the cursor position will be cleared on the next frame.
64    clear_position_on_next_frame: bool,
65}
66
67impl CursorState {
68    /// Cleans up the cursor state at the end of a frame.
69    pub(crate) fn frame_cleanup(&mut self) {
70        if self.clear_position_on_next_frame {
71            self.position = None;
72            self.clear_position_on_next_frame = false;
73        }
74    }
75
76    /// Adds a pointer change to the processing queue.
77    ///
78    /// Each sample retains its pointer's position when queued. Motion samples
79    /// are compacted above a soft queue limit; scroll deltas and gesture
80    /// boundaries remain available even during a long frame.
81    ///
82    /// # Arguments
83    ///
84    /// * `event` - The pointer change to add to the queue
85    pub fn push_event(&mut self, mut event: PointerChange) {
86        if event.position_snapshot().is_none() {
87            let position = self
88                .pointer_positions
89                .get(&event.pointer_id)
90                .copied()
91                .unwrap_or_else(|| {
92                    if event.pointer_id == MOUSE_POINTER_ID {
93                        None
94                    } else {
95                        self.position
96                    }
97                });
98            event = event.with_position(position);
99            self.position = position;
100        }
101        self.events.push_back(event);
102        while self.events.len() > KEEP_EVENTS_COUNT {
103            let redundant = self.events.iter().enumerate().find_map(|(index, queued)| {
104                if !matches!(queued.content, CursorEventContent::Moved(_)) {
105                    return None;
106                }
107                self.events
108                    .iter()
109                    .skip(index + 1)
110                    .find(|next| next.pointer_id == queued.pointer_id)
111                    .filter(|next| matches!(next.content, CursorEventContent::Moved(_)))
112                    .map(|_| index)
113            });
114            if let Some(index) = redundant {
115                self.events.remove(index);
116            } else {
117                break;
118            }
119        }
120    }
121
122    /// Updates the mouse position and current cursor position.
123    ///
124    /// For another pointer, use [`Self::update_pointer_position`].
125    /// This method accepts any type that can be converted into
126    /// `Option<PxPosition>`, allowing for flexible position updates
127    /// including clearing the position by passing `None`.
128    ///
129    /// # Arguments
130    ///
131    /// * `position` - New cursor position or `None` to clear the position
132    pub fn update_position(&mut self, position: impl Into<Option<PxPosition>>) {
133        self.update_pointer_position(MOUSE_POINTER_ID, position);
134    }
135
136    /// Updates one pointer's tracked position and the current cursor position.
137    ///
138    /// Use distinct identifiers for independent synthetic input streams.
139    pub fn update_pointer_position(
140        &mut self,
141        pointer_id: PointerId,
142        position: impl Into<Option<PxPosition>>,
143    ) {
144        let position = position.into();
145        self.pointer_positions.insert(pointer_id, position);
146        self.position = position;
147    }
148
149    /// Retrieves and clears all pending pointer changes.
150    ///
151    /// This method returns all queued pointer changes and clears the internal
152    /// event queue. Events are returned in chronological order (oldest first).
153    ///
154    /// This is typically called once per frame by the UI framework to process
155    /// all accumulated input events.
156    ///
157    /// # Returns
158    ///
159    /// A vector of [`PointerChange`]s ordered from oldest to newest.
160    ///
161    /// # Note
162    ///
163    /// Events are ordered from oldest to newest to ensure proper event
164    /// processing order.
165    pub fn take_events(&mut self) -> Vec<PointerChange> {
166        self.events.drain(..).collect()
167    }
168
169    /// Clears all cursor state and pending events.
170    ///
171    /// This is typically used when the UI context changes significantly,
172    /// such as when switching between different UI screens or when input
173    /// focus changes.
174    pub fn clear(&mut self) {
175        self.events.clear();
176        self.pointer_positions.clear();
177        self.position = None;
178        self.touch_points.clear();
179        self.clear_position_on_next_frame = false;
180    }
181
182    /// Returns the current cursor position, if any.
183    ///
184    /// The position represents the last known location of the cursor or active
185    /// touch point. Returns `None` if no cursor is currently active or if
186    /// the position has been cleared.
187    ///
188    /// # Returns
189    ///
190    /// - `Some(PxPosition)` if a cursor position is currently tracked
191    /// - `None` if no cursor is active
192    pub fn position(&self) -> Option<PxPosition> {
193        self.position
194    }
195
196    /// Handles the start of a touch gesture.
197    ///
198    /// This method registers a new touch point and generates a press event.
199    ///
200    /// # Arguments
201    ///
202    /// * `touch_id` - Unique identifier for this touch point
203    /// * `position` - Initial position of the touch in pixel coordinates
204    pub fn handle_touch_start(&mut self, touch_id: u64, position: PxPosition) {
205        self.clear_position_on_next_frame = false;
206        let now = Instant::now();
207        let mut pointer_id = self.next_touch_pointer_id.wrapping_add(1);
208        while pointer_id == MOUSE_POINTER_ID
209            || self
210                .touch_points
211                .values()
212                .any(|touch| touch.pointer_id == pointer_id)
213        {
214            pointer_id = pointer_id.wrapping_add(1);
215        }
216        self.next_touch_pointer_id = pointer_id;
217
218        self.touch_points.insert(
219            touch_id,
220            TouchPointState {
221                pointer_id,
222                last_position: position,
223                start_position: position,
224                last_update_time: now,
225                generated_scroll_event: false,
226            },
227        );
228        self.update_pointer_position(pointer_id, position);
229        let press_event = PointerChange {
230            timestamp: now,
231            pointer_id,
232            content: CursorEventContent::Pressed(PressKeyEventType::Left),
233            gesture_state: GestureState::TapCandidate,
234            sampled_position: Default::default(),
235            consumed: false,
236            tap_claimed: false,
237            scroll_discrete: None,
238        };
239        self.push_event(press_event);
240    }
241
242    /// Handles touch movement and generates scroll events when appropriate.
243    ///
244    /// This method tracks touch movement and generates scroll events when the
245    /// movement exceeds the minimum threshold.
246    ///
247    /// # Arguments
248    ///
249    /// * `touch_id` - Unique identifier for the touch point being moved
250    /// * `current_position` - New position of the touch in pixel coordinates
251    ///
252    /// # Returns
253    ///
254    /// - `Some(PointerChange)` containing a scroll event if movement exceeds
255    ///   threshold
256    /// - `None` if movement is below threshold or touch scrolling is disabled
257    pub fn handle_touch_move(
258        &mut self,
259        touch_id: u64,
260        current_position: PxPosition,
261    ) -> Option<PointerChange> {
262        let now = Instant::now();
263        let touch_state = self.touch_points.get_mut(&touch_id)?;
264        let pointer_id = touch_state.pointer_id;
265        let mut delta_x = current_position.x - touch_state.last_position.x;
266        let mut delta_y = current_position.y - touch_state.last_position.y;
267        let cumulative_x = current_position.x - touch_state.start_position.x;
268        let cumulative_y = current_position.y - touch_state.start_position.y;
269        let distance = cumulative_x.hypot(cumulative_y);
270        if self.touch_scroll_config.enabled
271            && !touch_state.generated_scroll_event
272            && distance >= self.touch_scroll_config.min_move_threshold
273        {
274            touch_state.generated_scroll_event = true;
275            let beyond_slop = (distance - self.touch_scroll_config.min_move_threshold) / distance;
276            delta_x = cumulative_x * beyond_slop;
277            delta_y = cumulative_y * beyond_slop;
278        }
279        touch_state.last_position = current_position;
280        touch_state.last_update_time = now;
281        let dragging = touch_state.generated_scroll_event;
282        let gesture_state = if dragging {
283            GestureState::Dragged
284        } else {
285            GestureState::TapCandidate
286        };
287        self.update_pointer_position(pointer_id, current_position);
288        self.push_event(PointerChange {
289            timestamp: now,
290            pointer_id,
291            content: CursorEventContent::Moved(current_position),
292            gesture_state,
293            sampled_position: Default::default(),
294            consumed: false,
295            tap_claimed: false,
296            scroll_discrete: None,
297        });
298        (self.touch_scroll_config.enabled && dragging).then_some(PointerChange {
299            timestamp: now,
300            pointer_id,
301            content: CursorEventContent::Scroll(ScrollEventContent {
302                delta_x,
303                delta_y,
304                unit: ScrollDeltaUnit::Pixel,
305                source: ScrollEventSource::Touch,
306            }),
307            gesture_state,
308            sampled_position: PointerPosition::At(current_position),
309            consumed: false,
310            tap_claimed: false,
311            scroll_discrete: None,
312        })
313    }
314
315    /// Ends a touch as a completed gesture and emits a release event.
316    pub fn handle_touch_end(&mut self, touch_id: u64) {
317        self.finish_touch(touch_id, false);
318    }
319
320    /// Cancels a touch gesture. Cancellation never produces a tap or fling.
321    pub fn handle_touch_cancel(&mut self, touch_id: u64) {
322        self.finish_touch(touch_id, true);
323    }
324
325    fn finish_touch(&mut self, touch_id: u64, cancelled: bool) {
326        let now = Instant::now();
327        let touch_state = match self.touch_points.remove(&touch_id) {
328            Some(state) => state,
329            None => return,
330        };
331        let was_drag = touch_state.generated_scroll_event;
332        self.update_pointer_position(touch_state.pointer_id, touch_state.last_position);
333        self.push_event(PointerChange {
334            timestamp: now,
335            pointer_id: touch_state.pointer_id,
336            content: if cancelled {
337                CursorEventContent::Cancelled(PressKeyEventType::Left)
338            } else {
339                CursorEventContent::Released(PressKeyEventType::Left)
340            },
341            gesture_state: if cancelled || was_drag {
342                GestureState::Dragged
343            } else {
344                GestureState::TapCandidate
345            },
346            sampled_position: Default::default(),
347            consumed: false,
348            tap_claimed: false,
349            scroll_discrete: None,
350        });
351        self.pointer_positions.remove(&touch_state.pointer_id);
352        if self.touch_points.is_empty() {
353            self.clear_position_on_next_frame = true;
354        }
355    }
356}
357
358#[derive(Clone, Copy, Debug, Default)]
359pub(crate) enum PointerPosition {
360    #[default]
361    Unspecified,
362    At(PxPosition),
363    Unavailable,
364}
365
366/// Represents a single pointer change with timing information.
367///
368/// `PointerChange` encapsulates all pointer interactions including
369/// presses, releases, and scroll actions. Each event includes a timestamp for
370/// precise timing and ordering of input events.
371#[derive(Debug, Clone)]
372pub struct PointerChange {
373    /// Timestamp indicating when this event occurred.
374    pub timestamp: Instant,
375    /// Pointer identifier for this input stream.
376    pub pointer_id: PointerId,
377    /// The specific type and data of this pointer change.
378    pub content: CursorEventContent,
379    /// Classification of the gesture associated with this event.
380    ///
381    /// Events originating from touch scrolling will mark this as
382    /// [`GestureState::Dragged`], allowing downstream components to
383    /// distinguish tap candidates from scroll gestures.
384    pub gesture_state: GestureState,
385    /// Whether this change has been consumed by a handler.
386    pub(crate) consumed: bool,
387    pub(crate) tap_claimed: bool,
388    pub(crate) scroll_discrete: Option<bool>,
389    pub(crate) sampled_position: PointerPosition,
390}
391
392impl PointerChange {
393    /// Creates an unconsumed pointer sample with an explicit event timestamp.
394    pub fn new(
395        timestamp: Instant,
396        pointer_id: PointerId,
397        content: CursorEventContent,
398        gesture_state: GestureState,
399    ) -> Self {
400        Self {
401            timestamp,
402            pointer_id,
403            content,
404            gesture_state,
405            sampled_position: Default::default(),
406            consumed: false,
407            tap_claimed: false,
408            scroll_discrete: None,
409        }
410    }
411
412    /// Associates the sample with its window position at the time of input.
413    ///
414    /// Passing `None` records an unavailable position rather than using a later
415    /// cursor location. Movement events always use their own coordinates.
416    pub fn with_position(mut self, position: impl Into<Option<PxPosition>>) -> Self {
417        self.sampled_position = match position.into() {
418            Some(position) => PointerPosition::At(position),
419            None => PointerPosition::Unavailable,
420        };
421        self
422    }
423
424    /// Returns the sample's recorded absolute position, when available.
425    pub fn position(&self) -> Option<PxPosition> {
426        self.position_snapshot().flatten()
427    }
428
429    pub(crate) fn position_snapshot(&self) -> Option<Option<PxPosition>> {
430        if let CursorEventContent::Moved(position) = self.content {
431            return Some(Some(position));
432        }
433        match self.sampled_position {
434            PointerPosition::Unspecified => None,
435            PointerPosition::At(position) => Some(Some(position)),
436            PointerPosition::Unavailable => Some(None),
437        }
438    }
439
440    pub(crate) fn resolved_position(&self, fallback: Option<PxPosition>) -> Option<PxPosition> {
441        self.position_snapshot().unwrap_or(fallback)
442    }
443
444    /// Returns whether this scroll sample represents a discrete wheel step.
445    ///
446    /// The classification follows the original input when an ancestor
447    /// receives a smaller remainder or a different coordinate space.
448    pub fn is_discrete_scroll(&self) -> bool {
449        self.scroll_discrete.unwrap_or_else(|| {
450            matches!(&self.content, CursorEventContent::Scroll(event)
451                if crate::scroll::is_discrete_wheel(event))
452        })
453    }
454
455    /// Marks this change as consumed.
456    pub fn consume(&mut self) {
457        self.consumed = true;
458    }
459
460    /// Returns whether this change has already been consumed.
461    pub fn is_consumed(&self) -> bool {
462        self.consumed
463    }
464
465    /// Claims a press for one tap recognizer without consuming the sample.
466    ///
467    /// Returns `false` for a non-press sample or a press already claimed by
468    /// another recognizer. Drag and scroll observers can still inspect it.
469    pub fn claim_tap_press(&mut self) -> bool {
470        if self.tap_claimed || !matches!(self.content, CursorEventContent::Pressed(_)) {
471            return false;
472        }
473        self.tap_claimed = true;
474        true
475    }
476}
477
478/// Contains scroll movement data for scroll events.
479///
480/// `ScrollEventContent` represents the amount of scrolling that occurred,
481/// with positive values typically indicating rightward/downward movement
482/// and negative values indicating leftward/upward movement.
483#[derive(Debug, Clone, PartialEq)]
484pub struct ScrollEventContent {
485    /// Horizontal scroll distance in pixels.
486    pub delta_x: f32,
487    /// Vertical scroll distance in pixels.
488    pub delta_y: f32,
489    /// Unit used by the originating platform event.
490    pub unit: ScrollDeltaUnit,
491    /// The input source that produced the scroll event.
492    pub source: ScrollEventSource,
493}
494
495impl ScrollEventContent {
496    /// Returns the horizontal and vertical delta in physical pixels.
497    pub fn pixel_delta(&self) -> (f32, f32) {
498        crate::normalize_platform_scroll_delta(self.delta_x, self.delta_y, self.unit, self.source)
499    }
500
501    /// Converts the sample to pixels so handlers can forward the remaining distance.
502    pub fn normalize_to_pixels(&mut self) {
503        (self.delta_x, self.delta_y) = self.pixel_delta();
504        self.unit = ScrollDeltaUnit::Pixel;
505    }
506
507    pub(crate) fn from_wheel(delta: winit::event::MouseScrollDelta) -> Self {
508        let (delta_x, delta_y, unit) = match delta {
509            winit::event::MouseScrollDelta::LineDelta(x, y) => (x, y, ScrollDeltaUnit::Line),
510            winit::event::MouseScrollDelta::PixelDelta(delta) => {
511                (delta.x as f32, delta.y as f32, ScrollDeltaUnit::Pixel)
512            }
513        };
514        Self {
515            delta_x,
516            delta_y,
517            unit,
518            source: ScrollEventSource::Wheel,
519        }
520    }
521}
522
523/// Enumeration of all possible cursor event types.
524///
525/// `CursorEventContent` represents the different kinds of interactions
526/// that can occur with cursor or touch input, including button presses,
527/// releases, and scroll actions.
528#[derive(Debug, Clone, PartialEq)]
529pub enum CursorEventContent {
530    /// The pointer moved to a new absolute position.
531    Moved(PxPosition),
532    /// A cursor button or touch point was pressed.
533    Pressed(PressKeyEventType),
534    /// A cursor button or touch point was released.
535    Released(PressKeyEventType),
536    /// A touch point was cancelled by the platform.
537    Cancelled(PressKeyEventType),
538    /// A scroll action occurred (mouse wheel or touch drag).
539    Scroll(ScrollEventContent),
540}
541
542/// Describes the high-level gesture classification of a cursor event.
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
544pub enum GestureState {
545    /// Indicates the event is part of a potential tap/click interaction.
546    #[default]
547    TapCandidate,
548    /// Indicates the event happened during a drag/scroll gesture.
549    Dragged,
550}
551
552impl CursorEventContent {
553    /// Creates a cursor press/release event from winit mouse button events.
554    ///
555    /// This method converts winit's mouse button events into Tessera's cursor
556    /// event format. It handles the three standard mouse buttons (left,
557    /// right, middle) and ignores any additional buttons that may be
558    /// present on some mice.
559    ///
560    /// # Arguments
561    ///
562    /// * `state` - Whether the button was pressed or released
563    /// * `button` - Which mouse button was affected
564    ///
565    /// # Returns
566    ///
567    /// - `Some(CursorEventContent)` for supported mouse buttons
568    /// - `None` for unsupported mouse buttons
569    pub fn from_press_event(
570        state: winit::event::ElementState,
571        button: winit::event::MouseButton,
572    ) -> Option<Self> {
573        let event_type = match button {
574            winit::event::MouseButton::Left => PressKeyEventType::Left,
575            winit::event::MouseButton::Right => PressKeyEventType::Right,
576            winit::event::MouseButton::Middle => PressKeyEventType::Middle,
577            _ => return None, // Ignore other buttons
578        };
579        let state = match state {
580            winit::event::ElementState::Pressed => Self::Pressed(event_type),
581            winit::event::ElementState::Released => Self::Released(event_type),
582        };
583        Some(state)
584    }
585
586    /// Creates a scroll event from winit mouse wheel events.
587    ///
588    /// This method converts winit's mouse scroll delta into Tessera's scroll
589    /// event format while preserving whether the platform reported the value
590    /// in line or pixel units.
591    ///
592    /// # Arguments
593    ///
594    /// * `delta` - The scroll delta from winit
595    ///
596    /// # Returns
597    ///
598    /// A `CursorEventContent::Scroll` event with raw line or pixel delta
599    /// values.
600    pub fn from_scroll_event(delta: winit::event::MouseScrollDelta) -> Self {
601        Self::Scroll(ScrollEventContent::from_wheel(delta))
602    }
603}
604
605/// Represents the different types of cursor buttons or touch interactions.
606#[derive(Debug, Clone, Copy, PartialEq, Eq)]
607pub enum PressKeyEventType {
608    /// The primary mouse button (typically left button) or primary touch.
609    Left,
610    /// The secondary mouse button (typically right button).
611    Right,
612    /// The middle mouse button (typically scroll wheel click).
613    Middle,
614}
615
616/// Indicates the input source for a scroll event.
617#[derive(Debug, Clone, Copy, PartialEq, Eq)]
618pub enum ScrollEventSource {
619    /// Scroll generated from a touch drag gesture.
620    Touch,
621    /// Scroll generated by a mouse wheel or trackpad.
622    Wheel,
623}
624
625/// Indicates the unit used by the platform to describe a scroll delta.
626#[derive(Debug, Clone, Copy, PartialEq, Eq)]
627pub enum ScrollDeltaUnit {
628    /// Delta is expressed as a logical number of lines or wheel steps.
629    Line,
630    /// Delta is expressed in pixels.
631    Pixel,
632}
633
634#[cfg(test)]
635mod queue_tests {
636    use super::*;
637    use crate::Px;
638    #[test]
639    fn long_frame_preserves_touch_boundaries_and_displacement() {
640        let _runtime_session = crate::RuntimeSession::new();
641        let _runtime_scope = _runtime_session.enter();
642        let mut cursor = CursorState::default();
643        cursor.handle_touch_start(1, PxPosition::ZERO);
644        for x in 1..=300 {
645            if let Some(scroll) = cursor.handle_touch_move(1, PxPosition::new(Px(x), Px(0))) {
646                cursor.push_event(scroll);
647            }
648        }
649        cursor.handle_touch_end(1);
650        let events = cursor.take_events();
651        assert!(matches!(events[0].content, CursorEventContent::Pressed(_)));
652        assert!(matches!(
653            events
654                .last()
655                .expect("touch end must enqueue a release")
656                .content,
657            CursorEventContent::Released(_)
658        ));
659        let distance: f32 = events
660            .iter()
661            .filter_map(|event| match &event.content {
662                CursorEventContent::Scroll(scroll) => Some(scroll.delta_x),
663                _ => None,
664            })
665            .sum();
666        assert!((distance - 295.0).abs() < 0.001);
667    }
668}
669#[cfg(test)]
670mod tests {
671    use super::{CursorEventContent, CursorState, GestureState, PxPosition};
672    use crate::Px;
673
674    fn position(x: i32) -> PxPosition {
675        PxPosition::new(Px::new(x), Px::ZERO)
676    }
677
678    #[test]
679    fn queued_samples_keep_their_original_positions_including_unavailable_positions() {
680        let _runtime_session = crate::RuntimeSession::new();
681        let _runtime_scope = _runtime_session.enter();
682        let mut cursor = CursorState::default();
683        let sample = |content| {
684            super::PointerChange::new(
685                crate::time::Instant::now(),
686                super::MOUSE_POINTER_ID,
687                content,
688                GestureState::TapCandidate,
689            )
690        };
691        cursor.update_position(position(10));
692        cursor.push_event(sample(CursorEventContent::Pressed(
693            super::PressKeyEventType::Left,
694        )));
695        cursor.update_position(position(20));
696        cursor.push_event(sample(CursorEventContent::Released(
697            super::PressKeyEventType::Left,
698        )));
699        cursor.update_position(None);
700        cursor.push_event(sample(CursorEventContent::Pressed(
701            super::PressKeyEventType::Left,
702        )));
703        cursor.update_position(position(30));
704        let events = cursor.take_events();
705        assert_eq!(
706            events
707                .iter()
708                .map(super::PointerChange::position)
709                .collect::<Vec<_>>(),
710            [Some(position(10)), Some(position(20)), None]
711        );
712        assert_eq!(events[2].resolved_position(Some(position(30))), None);
713        let legacy = sample(CursorEventContent::Pressed(super::PressKeyEventType::Left));
714        assert_eq!(
715            legacy.resolved_position(Some(position(30))),
716            Some(position(30))
717        );
718    }
719
720    #[test]
721    fn independent_pointers_and_delayed_touch_scroll_keep_their_own_positions() {
722        let _runtime_session = crate::RuntimeSession::new();
723        let _runtime_scope = _runtime_session.enter();
724        let mut cursor = CursorState::default();
725        cursor.update_position(position(100));
726        cursor.handle_touch_start(1, position(0));
727        cursor.handle_touch_start(2, position(20));
728        let scroll = cursor.handle_touch_move(1, position(10)).unwrap();
729        cursor.handle_touch_move(2, position(50));
730        cursor.push_event(scroll);
731        cursor.handle_touch_end(1);
732        cursor.push_event(super::PointerChange::new(
733            crate::time::Instant::now(),
734            super::MOUSE_POINTER_ID,
735            CursorEventContent::Pressed(super::PressKeyEventType::Left),
736            GestureState::TapCandidate,
737        ));
738        let events = cursor.take_events();
739        assert_eq!(events[0].position(), Some(position(0)));
740        assert_eq!(events[1].position(), Some(position(20)));
741        assert_eq!(events[4].position(), Some(position(10)));
742        assert_eq!(events[5].position(), Some(position(10)));
743        assert_eq!(events[6].position(), Some(position(100)));
744    }
745
746    #[test]
747    fn platform_touch_identifiers_never_alias_mouse_and_keep_their_pointer_identity() {
748        let _runtime_session = crate::RuntimeSession::new();
749        let _runtime_scope = _runtime_session.enter();
750        let mut cursor = CursorState::default();
751        cursor.handle_touch_start(0, position(0));
752        cursor.handle_touch_start(u64::MAX, position(10));
753        let start = cursor.take_events();
754        let first = start[0].pointer_id;
755        let second = start[1].pointer_id;
756        assert_ne!(first, super::MOUSE_POINTER_ID);
757        assert_ne!(second, super::MOUSE_POINTER_ID);
758        assert_ne!(first, second);
759        cursor.handle_touch_move(0, position(2));
760        cursor.handle_touch_end(0);
761        cursor.handle_touch_cancel(u64::MAX);
762        let end = cursor.take_events();
763        assert_eq!(end[0].pointer_id, first);
764        assert_eq!(end[1].pointer_id, first);
765        assert_eq!(end[2].pointer_id, second);
766
767        cursor.next_touch_pointer_id = u64::MAX;
768        cursor.handle_touch_start(5, position(0));
769        assert_ne!(cursor.take_events()[0].pointer_id, super::MOUSE_POINTER_ID);
770    }
771
772    #[test]
773    fn touch_slop_is_cumulative_and_follow_up_micro_moves_scroll() {
774        let _runtime_session = crate::RuntimeSession::new();
775        let _runtime_scope = _runtime_session.enter();
776        let mut cursor = CursorState::default();
777        cursor.handle_touch_start(1, position(0));
778        assert!(cursor.handle_touch_move(1, position(2)).is_none());
779        assert!(cursor.handle_touch_move(1, position(4)).is_none());
780        let first = cursor
781            .handle_touch_move(1, position(6))
782            .expect("slop crossed");
783        assert_eq!(first.gesture_state, GestureState::Dragged);
784        let second = cursor
785            .handle_touch_move(1, position(7))
786            .expect("drag remains active");
787        assert_eq!(second.gesture_state, GestureState::Dragged);
788        assert!(
789            matches!(second.content, CursorEventContent::Scroll(scroll) if scroll.delta_x == 1.0)
790        );
791    }
792
793    #[test]
794    fn cancelled_touch_is_not_a_tap_boundary() {
795        let _runtime_session = crate::RuntimeSession::new();
796        let _runtime_scope = _runtime_session.enter();
797        let mut cursor = CursorState::default();
798        cursor.handle_touch_start(1, position(0));
799        cursor.handle_touch_cancel(1);
800        let events = cursor.take_events();
801        assert!(matches!(
802            events.last().map(|event| &event.content),
803            Some(CursorEventContent::Cancelled(_))
804        ));
805        assert_eq!(
806            events.last().map(|event| event.gesture_state),
807            Some(GestureState::Dragged)
808        );
809    }
810}