tessera_ui/
cursor.rs

1//! Cursor state management and event handling system.
2//!
3//! This module provides comprehensive cursor and touch event handling for the Tessera UI framework.
4//! It manages cursor position tracking, event queuing, touch gesture recognition, and inertial
5//! scrolling for smooth user interactions.
6//!
7//! # Key Features
8//!
9//! - **Multi-touch Support**: Tracks multiple simultaneous touch points with unique IDs
10//! - **Inertial Scrolling**: Provides smooth momentum-based scrolling after touch gestures
11//! - **Event Queuing**: Maintains a bounded queue of cursor events for processing
12//! - **Velocity Tracking**: Calculates touch velocities for natural gesture recognition
13//! - **Cross-platform**: Handles both mouse and touch input events consistently
14//!
15//! # Usage
16//!
17//! The main entry point is [`CursorState`], which maintains all cursor-related state:
18//!
19//! ```rust,ignore
20//! use tessera_ui::cursor::CursorState;
21//! use tessera_ui::PxPosition;
22//!
23//! let mut cursor_state = CursorState::default();
24//!
25//! // Handle touch start
26//! cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
27//!
28//! // Process events
29//! let events = cursor_state.take_events();
30//! for event in events {
31//!     match event.content {
32//!         CursorEventContent::Pressed(_) => println!("Touch started"),
33//!         CursorEventContent::Scroll(scroll) => {
34//!             println!("Scroll: dx={}, dy={}", scroll.delta_x, scroll.delta_y);
35//!         }
36//!         _ => {}
37//!     }
38//! }
39//! ```
40
41use std::{
42    collections::{HashMap, VecDeque},
43    time::{Duration, Instant},
44};
45
46use crate::PxPosition;
47
48/// Maximum number of events to keep in the queue to prevent memory issues during UI jank.
49const KEEP_EVENTS_COUNT: usize = 10;
50
51/// Controls how quickly inertial scrolling decelerates (higher = faster slowdown).
52const INERTIA_DECAY_CONSTANT: f32 = 5.0;
53
54/// Minimum velocity threshold below which inertial scrolling stops (pixels per second).
55const MIN_INERTIA_VELOCITY: f32 = 10.0;
56
57/// Minimum velocity from a gesture required to start inertial scrolling (pixels per second).
58const INERTIA_MIN_VELOCITY_THRESHOLD_FOR_START: f32 = 50.0;
59
60/// Multiplier applied to initial inertial velocity (typically 1.0 for natural feel).
61const INERTIA_MOMENTUM_FACTOR: f32 = 1.0;
62
63/// Maximum inertial velocity to keep flicks controllable (pixels per second).
64const MAX_INERTIA_VELOCITY: f32 = 6000.0;
65
66/// Tracks the state of a single touch point for gesture recognition and velocity calculation.
67///
68/// This struct maintains the necessary information to track touch movement, calculate
69/// velocities, and determine when to trigger inertial scrolling.
70///
71/// # Example
72///
73/// ```rust,ignore
74/// let touch_state = TouchPointState {
75///     last_position: PxPosition::new(100.0, 200.0),
76///     last_update_time: Instant::now(),
77///     velocity_tracker: VelocityTracker::new(Instant::now()),
78/// };
79/// ```
80#[derive(Debug, Clone)]
81struct TouchPointState {
82    /// The last recorded position of this touch point.
83    last_position: PxPosition,
84    /// Timestamp of the last position update.
85    last_update_time: Instant,
86    /// Tracks recent velocity samples and temporal metadata for momentum calculation.
87    velocity_tracker: VelocityTracker,
88    /// Tracks whether this touch gesture generated a scroll event.
89    ///
90    /// When set, the gesture should be treated as a drag/scroll rather than a tap.
91    generated_scroll_event: bool,
92}
93
94/// Maintains a short window of velocity samples for inertia calculations.
95#[derive(Debug, Clone)]
96struct VelocityTracker {
97    samples: VecDeque<(Instant, f32, f32)>,
98    last_sample_time: Instant,
99}
100
101const VELOCITY_SAMPLE_WINDOW: Duration = Duration::from_millis(90);
102const VELOCITY_IDLE_CUTOFF: Duration = Duration::from_millis(65);
103
104/// Represents an active inertial scrolling session.
105///
106/// When a touch gesture ends with sufficient velocity, this struct tracks
107/// the momentum and gradually decelerates the scroll movement over time.
108///
109/// # Example
110///
111/// ```rust,ignore
112/// let inertia = ActiveInertia {
113///     velocity_x: 200.0,  // pixels per second
114///     velocity_y: -150.0, // pixels per second  
115///     last_tick_time: Instant::now(),
116/// };
117/// ```
118#[derive(Debug, Clone)]
119struct ActiveInertia {
120    /// Current horizontal velocity in pixels per second.
121    velocity_x: f32,
122    /// Current vertical velocity in pixels per second.
123    velocity_y: f32,
124    /// Timestamp of the last inertia calculation update.
125    last_tick_time: Instant,
126}
127
128fn clamp_inertia_velocity(vx: f32, vy: f32) -> (f32, f32) {
129    if !vx.is_finite() || !vy.is_finite() {
130        return (0.0, 0.0);
131    }
132
133    let magnitude_sq = vx * vx + vy * vy;
134    if !magnitude_sq.is_finite() {
135        return (0.0, 0.0);
136    }
137
138    let magnitude = magnitude_sq.sqrt();
139    if magnitude > MAX_INERTIA_VELOCITY && MAX_INERTIA_VELOCITY > 0.0 {
140        let scale = MAX_INERTIA_VELOCITY / magnitude;
141        return (vx * scale, vy * scale);
142    }
143
144    (vx, vy)
145}
146
147/// Configuration settings for touch scrolling behavior.
148///
149/// This struct controls various aspects of how touch gestures are interpreted
150/// and converted into scroll events.
151///
152/// # Example
153///
154/// ```rust,ignore
155/// let config = TouchScrollConfig {
156///     min_move_threshold: 3.0,  // More sensitive
157///     enabled: true,
158/// };
159/// ```
160#[derive(Debug, Clone)]
161struct TouchScrollConfig {
162    /// Minimum movement distance in pixels required to trigger a scroll event.
163    ///
164    /// Smaller values make scrolling more sensitive but may cause jitter.
165    /// Larger values require more deliberate movement but provide stability.
166    min_move_threshold: f32,
167    /// Whether touch scrolling is currently enabled.
168    enabled: bool,
169}
170
171impl Default for TouchScrollConfig {
172    fn default() -> Self {
173        Self {
174            // Reduced threshold for more responsive touch
175            min_move_threshold: 5.0,
176            enabled: true,
177        }
178    }
179}
180
181/// Central state manager for cursor and touch interactions.
182///
183/// `CursorState` is the main interface for handling all cursor-related events in the Tessera
184/// UI framework. It manages cursor position tracking, event queuing, multi-touch support,
185/// and provides smooth inertial scrolling for touch gestures.
186///
187/// # Key Responsibilities
188///
189/// - **Position Tracking**: Maintains current cursor/touch position
190/// - **Event Management**: Queues and processes cursor events with bounded storage
191/// - **Multi-touch Support**: Tracks multiple simultaneous touch points
192/// - **Inertial Scrolling**: Provides momentum-based scrolling after touch gestures
193/// - **Cross-platform Input**: Handles both mouse and touch events uniformly
194///
195/// # Usage
196///
197/// ```rust,ignore
198/// use tessera_ui::cursor::{CursorState, CursorEventContent};
199/// use tessera_ui::PxPosition;
200///
201/// let mut cursor_state = CursorState::default();
202///
203/// // Handle a touch gesture
204/// cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
205/// cursor_state.handle_touch_move(0, PxPosition::new(110.0, 190.0));
206/// cursor_state.handle_touch_end(0);
207///
208/// // Process accumulated events
209/// let events = cursor_state.take_events();
210/// for event in events {
211///     match event.content {
212///         CursorEventContent::Pressed(_) => println!("Touch started"),
213///         CursorEventContent::Scroll(scroll) => {
214///             println!("Scrolling: dx={}, dy={}", scroll.delta_x, scroll.delta_y);
215///         }
216///         CursorEventContent::Released(_) => println!("Touch ended"),
217///     }
218/// }
219/// ```
220///
221/// # Thread Safety
222///
223/// `CursorState` is not thread-safe and should be used from a single thread,
224/// typically the main UI thread where input events are processed.
225#[derive(Default)]
226pub struct CursorState {
227    /// Current cursor position, if any cursor is active.
228    position: Option<PxPosition>,
229    /// Bounded queue of cursor events awaiting processing.
230    events: VecDeque<CursorEvent>,
231    /// Active touch points mapped by their unique touch IDs.
232    touch_points: HashMap<u64, TouchPointState>,
233    /// Configuration settings for touch scrolling behavior.
234    touch_scroll_config: TouchScrollConfig,
235    /// Current inertial scrolling state, if active.
236    active_inertia: Option<ActiveInertia>,
237    /// If true, the cursor position will be cleared on the next frame.
238    clear_position_on_next_frame: bool,
239}
240
241impl CursorState {
242    /// Cleans up the cursor state at the end of a frame.
243    pub(crate) fn frame_cleanup(&mut self) {
244        if self.clear_position_on_next_frame {
245            self.update_position(None);
246            self.clear_position_on_next_frame = false;
247        }
248    }
249
250    /// Adds a cursor event to the processing queue.
251    ///
252    /// Events are stored in a bounded queue to prevent memory issues during UI performance
253    /// problems. If the queue exceeds [`KEEP_EVENTS_COUNT`], the oldest events are discarded.
254    ///
255    /// # Arguments
256    ///
257    /// * `event` - The cursor event to add to the queue
258    ///
259    /// # Example
260    ///
261    /// ```rust,ignore
262    /// use tessera_ui::cursor::{
263    ///     CursorState, CursorEvent, CursorEventContent, GestureState, PressKeyEventType,
264    /// };
265    /// use std::time::Instant;
266    ///
267    /// let mut cursor_state = CursorState::default();
268    /// let event = CursorEvent {
269    ///     timestamp: Instant::now(),
270    ///     content: CursorEventContent::Pressed(PressKeyEventType::Left),
271    ///     gesture_state: GestureState::TapCandidate,
272    /// };
273    /// cursor_state.push_event(event);
274    /// ```
275    pub fn push_event(&mut self, event: CursorEvent) {
276        self.events.push_back(event);
277
278        // Maintain bounded queue size to prevent memory issues during UI jank
279        if self.events.len() > KEEP_EVENTS_COUNT {
280            self.events.pop_front();
281        }
282    }
283
284    /// Updates the current cursor position.
285    ///
286    /// This method accepts any type that can be converted into `Option<PxPosition>`,
287    /// allowing for flexible position updates including clearing the position by
288    /// passing `None`.
289    ///
290    /// # Arguments
291    ///
292    /// * `position` - New cursor position or `None` to clear the position
293    ///
294    /// # Example
295    ///
296    /// ```rust,ignore
297    /// use tessera_ui::cursor::CursorState;
298    /// use tessera_ui::PxPosition;
299    ///
300    /// let mut cursor_state = CursorState::default();
301    ///
302    /// // Set position
303    /// cursor_state.update_position(PxPosition::new(100.0, 200.0));
304    ///
305    /// // Clear position
306    /// cursor_state.update_position(None);
307    /// ```
308    pub fn update_position(&mut self, position: impl Into<Option<PxPosition>>) {
309        self.position = position.into();
310    }
311
312    /// Processes active inertial scrolling and generates scroll events.
313    ///
314    /// This method is called internally to update inertial scrolling state and generate
315    /// appropriate scroll events. It handles velocity decay over time and stops inertia
316    /// when velocity falls below the minimum threshold.
317    ///
318    /// The method calculates scroll deltas based on current velocity and elapsed time,
319    /// applies exponential decay to the velocity, and queues scroll events for processing.
320    ///
321    /// # Implementation Details
322    ///
323    /// - Uses exponential decay with [`INERTIA_DECAY_CONSTANT`] for natural deceleration
324    /// - Stops inertia when velocity drops below [`MIN_INERTIA_VELOCITY`]
325    /// - Generates scroll events with calculated position deltas
326    /// - Handles edge cases like zero delta time gracefully
327    fn process_and_queue_inertial_scroll(&mut self) {
328        // Handle active inertia with clear, small responsibilities.
329        if let Some(mut inertia) = self.active_inertia.take() {
330            let now = Instant::now();
331            let delta_time = now.duration_since(inertia.last_tick_time).as_secs_f32();
332
333            if delta_time <= 0.0 {
334                // Called multiple times in the same instant; reinsert for next frame.
335                self.active_inertia = Some(inertia);
336                return;
337            }
338
339            // Compute scroll delta and emit event if meaningful.
340            let scroll_delta_x = inertia.velocity_x * delta_time;
341            let scroll_delta_y = inertia.velocity_y * delta_time;
342            if scroll_delta_x.abs() > 0.01 || scroll_delta_y.abs() > 0.01 {
343                self.push_scroll_event(now, scroll_delta_x, scroll_delta_y);
344            }
345
346            // Apply exponential decay to velocities.
347            let decay = (-INERTIA_DECAY_CONSTANT * delta_time).exp();
348            inertia.velocity_x *= decay;
349            inertia.velocity_y *= decay;
350            inertia.last_tick_time = now;
351
352            // Reinsert inertia only if still above threshold.
353            if inertia.velocity_x.abs() >= MIN_INERTIA_VELOCITY
354                || inertia.velocity_y.abs() >= MIN_INERTIA_VELOCITY
355            {
356                self.active_inertia = Some(inertia);
357            }
358        }
359    }
360
361    // Helper: push a scroll event with consistent construction.
362    fn push_scroll_event(&mut self, timestamp: Instant, dx: f32, dy: f32) {
363        self.push_event(CursorEvent {
364            timestamp,
365            content: CursorEventContent::Scroll(ScrollEventConent {
366                delta_x: dx,
367                delta_y: dy,
368            }),
369            gesture_state: GestureState::Dragged,
370        });
371    }
372
373    /// Retrieves and clears all pending cursor events.
374    ///
375    /// This method processes any active inertial scrolling, then returns all queued
376    /// cursor events and clears the internal event queue. Events are returned in
377    /// chronological order (oldest first).
378    ///
379    /// This is typically called once per frame by the UI framework to process
380    /// all accumulated input events.
381    ///
382    /// # Returns
383    ///
384    /// A vector of [`CursorEvent`]s ordered from oldest to newest.
385    ///
386    /// # Example
387    ///
388    /// ```rust,ignore
389    /// use tessera_ui::cursor::CursorState;
390    ///
391    /// let mut cursor_state = CursorState::default();
392    ///
393    /// // ... handle some input events ...
394    ///
395    /// // Process all events at once
396    /// let events = cursor_state.take_events();
397    /// for event in events {
398    ///     println!("Event at {:?}: {:?}", event.timestamp, event.content);
399    /// }
400    /// ```
401    ///
402    /// # Note
403    ///
404    /// Events are ordered from oldest to newest to ensure proper event processing order.
405    pub fn take_events(&mut self) -> Vec<CursorEvent> {
406        self.process_and_queue_inertial_scroll();
407        self.events.drain(..).collect()
408    }
409
410    /// Clears all cursor state and pending events.
411    ///
412    /// This method resets the cursor state to its initial condition by:
413    /// - Clearing all queued events
414    /// - Removing cursor position information
415    /// - Stopping any active inertial scrolling
416    /// - Clearing all touch point tracking
417    ///
418    /// This is typically used when the UI context changes significantly,
419    /// such as when switching between different UI screens or when input
420    /// focus changes.
421    ///
422    /// # Example
423    ///
424    /// ```rust,ignore
425    /// use tessera_ui::cursor::CursorState;
426    ///
427    /// let mut cursor_state = CursorState::default();
428    ///
429    /// // ... handle various input events ...
430    ///
431    /// // Reset everything when changing UI context
432    /// cursor_state.clear();
433    /// ```
434    pub fn clear(&mut self) {
435        self.events.clear();
436        self.update_position(None);
437        self.active_inertia = None;
438        self.touch_points.clear();
439        self.clear_position_on_next_frame = false;
440    }
441
442    /// Returns the current cursor position, if any.
443    ///
444    /// The position represents the last known location of the cursor or active touch point.
445    /// Returns `None` if no cursor is currently active or if the position has been cleared.
446    ///
447    /// # Returns
448    ///
449    /// - `Some(PxPosition)` if a cursor position is currently tracked
450    /// - `None` if no cursor is active
451    ///
452    /// # Example
453    ///
454    /// ```rust,ignore
455    /// use tessera_ui::cursor::CursorState;
456    /// use tessera_ui::PxPosition;
457    ///
458    /// let mut cursor_state = CursorState::default();
459    ///
460    /// // Initially no position
461    /// assert_eq!(cursor_state.position(), None);
462    ///
463    /// // After setting position
464    /// cursor_state.update_position(PxPosition::new(100.0, 200.0));
465    /// assert_eq!(cursor_state.position(), Some(PxPosition::new(100.0, 200.0)));
466    /// ```
467    pub fn position(&self) -> Option<PxPosition> {
468        self.position
469    }
470
471    /// Handles the start of a touch gesture.
472    ///
473    /// This method registers a new touch point and generates a press event. It also
474    /// stops any active inertial scrolling since a new touch interaction has begun.
475    ///
476    /// # Arguments
477    ///
478    /// * `touch_id` - Unique identifier for this touch point
479    /// * `position` - Initial position of the touch in pixel coordinates
480    ///
481    /// # Example
482    ///
483    /// ```rust,ignore
484    /// use tessera_ui::cursor::CursorState;
485    /// use tessera_ui::PxPosition;
486    ///
487    /// let mut cursor_state = CursorState::default();
488    /// cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
489    ///
490    /// // This generates a Pressed event and updates the cursor position
491    /// let events = cursor_state.take_events();
492    /// assert_eq!(events.len(), 1);
493    /// ```
494    pub fn handle_touch_start(&mut self, touch_id: u64, position: PxPosition) {
495        self.active_inertia = None; // Stop any existing inertia on new touch
496        let now = Instant::now();
497
498        self.touch_points.insert(
499            touch_id,
500            TouchPointState {
501                last_position: position,
502                last_update_time: now,
503                velocity_tracker: VelocityTracker::new(now),
504                generated_scroll_event: false,
505            },
506        );
507        self.update_position(position);
508        let press_event = CursorEvent {
509            timestamp: now,
510            content: CursorEventContent::Pressed(PressKeyEventType::Left),
511            gesture_state: GestureState::TapCandidate,
512        };
513        self.push_event(press_event);
514    }
515
516    /// Handles touch movement and generates scroll events when appropriate.
517    ///
518    /// This method tracks touch movement, calculates velocities for inertial scrolling,
519    /// and generates scroll events when the movement exceeds the minimum threshold.
520    /// It also maintains a velocity history for momentum calculation.
521    ///
522    /// # Arguments
523    ///
524    /// * `touch_id` - Unique identifier for the touch point being moved
525    /// * `current_position` - New position of the touch in pixel coordinates
526    ///
527    /// # Returns
528    ///
529    /// - `Some(CursorEvent)` containing a scroll event if movement exceeds threshold
530    /// - `None` if movement is below threshold or touch scrolling is disabled
531    ///
532    /// # Example
533    ///
534    /// ```rust,ignore
535    /// use tessera_ui::cursor::CursorState;
536    /// use tessera_ui::PxPosition;
537    ///
538    /// let mut cursor_state = CursorState::default();
539    /// cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
540    ///
541    /// // Move touch point - may generate scroll event
542    /// if let Some(scroll_event) = cursor_state.handle_touch_move(0, PxPosition::new(110.0, 190.0)) {
543    ///     println!("Scroll detected!");
544    /// }
545    /// ```
546    pub fn handle_touch_move(
547        &mut self,
548        touch_id: u64,
549        current_position: PxPosition,
550    ) -> Option<CursorEvent> {
551        let now = Instant::now();
552        self.update_position(current_position);
553
554        if !self.touch_scroll_config.enabled {
555            return None;
556        }
557
558        if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
559            let delta_x = (current_position.x - touch_state.last_position.x).to_f32();
560            let delta_y = (current_position.y - touch_state.last_position.y).to_f32();
561            let move_distance = (delta_x * delta_x + delta_y * delta_y).sqrt();
562            let time_delta = now
563                .duration_since(touch_state.last_update_time)
564                .as_secs_f32();
565
566            touch_state.last_position = current_position;
567            touch_state.last_update_time = now;
568
569            if move_distance >= self.touch_scroll_config.min_move_threshold {
570                // Stop any active inertia when user actively moves the touch.
571                self.active_inertia = None;
572
573                if time_delta > 0.0 {
574                    let velocity_x = delta_x / time_delta;
575                    let velocity_y = delta_y / time_delta;
576                    touch_state
577                        .velocity_tracker
578                        .push(now, velocity_x, velocity_y);
579                }
580
581                touch_state.generated_scroll_event = true;
582
583                // Return a scroll event for immediate feedback.
584                return Some(CursorEvent {
585                    timestamp: now,
586                    content: CursorEventContent::Scroll(ScrollEventConent {
587                        delta_x, // Direct scroll delta for touch move
588                        delta_y,
589                    }),
590                    gesture_state: GestureState::Dragged,
591                });
592            }
593        }
594        None
595    }
596
597    /// Handles the end of a touch gesture and potentially starts inertial scrolling.
598    ///
599    /// This method processes the end of a touch interaction by:
600    /// - Calculating average velocity from recent touch movement
601    /// - Starting inertial scrolling if velocity exceeds the threshold
602    /// - Generating a release event
603    /// - Cleaning up touch point tracking
604    ///
605    /// # Arguments
606    ///
607    /// * `touch_id` - Unique identifier for the touch point that ended
608    ///
609    /// # Example
610    ///
611    /// ```rust,ignore
612    /// use tessera_ui::cursor::CursorState;
613    /// use tessera_ui::PxPosition;
614    ///
615    /// let mut cursor_state = CursorState::default();
616    /// cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
617    /// cursor_state.handle_touch_move(0, PxPosition::new(150.0, 180.0));
618    /// cursor_state.handle_touch_end(0);
619    ///
620    /// // May start inertial scrolling based on gesture velocity
621    /// let events = cursor_state.take_events();
622    /// // Events may include scroll events from inertia
623    /// ```
624    pub fn handle_touch_end(&mut self, touch_id: u64) {
625        let now = Instant::now();
626        let mut was_drag = false;
627
628        if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
629            was_drag |= touch_state.generated_scroll_event;
630            if self.touch_scroll_config.enabled {
631                if let Some((avg_vx, avg_vy)) = touch_state.velocity_tracker.resolve(now) {
632                    let velocity_magnitude = (avg_vx * avg_vx + avg_vy * avg_vy).sqrt();
633                    if velocity_magnitude > INERTIA_MIN_VELOCITY_THRESHOLD_FOR_START {
634                        let (inertia_vx, inertia_vy) = clamp_inertia_velocity(
635                            avg_vx * INERTIA_MOMENTUM_FACTOR,
636                            avg_vy * INERTIA_MOMENTUM_FACTOR,
637                        );
638                        self.active_inertia = Some(ActiveInertia {
639                            velocity_x: inertia_vx,
640                            velocity_y: inertia_vy,
641                            last_tick_time: now,
642                        });
643                    } else {
644                        self.active_inertia = None;
645                    }
646                } else {
647                    self.active_inertia = None;
648                }
649            } else {
650                self.active_inertia = None; // Scrolling disabled
651            }
652        } else {
653            self.active_inertia = None; // No touch state present
654        }
655
656        if self.active_inertia.is_some() {
657            was_drag = true;
658        }
659
660        self.touch_points.remove(&touch_id);
661        let release_event = CursorEvent {
662            timestamp: now,
663            content: CursorEventContent::Released(PressKeyEventType::Left),
664            gesture_state: if was_drag {
665                GestureState::Dragged
666            } else {
667                GestureState::TapCandidate
668            },
669        };
670        self.push_event(release_event);
671
672        if self.touch_points.is_empty() && self.active_inertia.is_none() {
673            self.clear_position_on_next_frame = true;
674        }
675    }
676}
677
678impl VelocityTracker {
679    fn new(now: Instant) -> Self {
680        Self {
681            samples: VecDeque::new(),
682            last_sample_time: now,
683        }
684    }
685
686    fn push(&mut self, now: Instant, vx: f32, vy: f32) {
687        let (vx, vy) = clamp_inertia_velocity(vx, vy);
688        self.samples.push_back((now, vx, vy));
689        self.last_sample_time = now;
690        self.prune(now);
691    }
692
693    fn resolve(&mut self, now: Instant) -> Option<(f32, f32)> {
694        self.prune(now);
695
696        if self.samples.is_empty() {
697            return None;
698        }
699
700        let idle_time = now.duration_since(self.last_sample_time);
701        if idle_time >= VELOCITY_IDLE_CUTOFF {
702            self.samples.clear();
703            return None;
704        }
705
706        let mut weighted_sum_x = 0.0f32;
707        let mut weighted_sum_y = 0.0f32;
708        let mut total_weight = 0.0f32;
709        let window_secs = VELOCITY_SAMPLE_WINDOW.as_secs_f32().max(f32::EPSILON);
710
711        for &(timestamp, vx, vy) in &self.samples {
712            let age_secs = now
713                .duration_since(timestamp)
714                .as_secs_f32()
715                .clamp(0.0, window_secs);
716            let weight = (window_secs - age_secs).max(0.0);
717            if weight > 0.0 {
718                weighted_sum_x += vx * weight;
719                weighted_sum_y += vy * weight;
720                total_weight += weight;
721            }
722        }
723
724        if total_weight <= f32::EPSILON {
725            self.samples.clear();
726            return None;
727        }
728
729        let avg_x = weighted_sum_x / total_weight;
730        let avg_y = weighted_sum_y / total_weight;
731
732        let damping = 1.0 - idle_time.as_secs_f32() / VELOCITY_IDLE_CUTOFF.as_secs_f32();
733        let damping = damping.clamp(0.0, 1.0);
734        let (avg_x, avg_y) = clamp_inertia_velocity(avg_x * damping, avg_y * damping);
735
736        Some((avg_x, avg_y))
737    }
738
739    fn prune(&mut self, now: Instant) {
740        while let Some(&(timestamp, _, _)) = self.samples.front() {
741            if now.duration_since(timestamp) > VELOCITY_SAMPLE_WINDOW {
742                self.samples.pop_front();
743            } else {
744                break;
745            }
746        }
747    }
748}
749
750/// Represents a single cursor or touch event with timing information.
751///
752/// `CursorEvent` encapsulates all types of cursor interactions including presses,
753/// releases, and scroll actions. Each event includes a timestamp for precise
754/// timing and ordering of input events.
755///
756/// # Example
757///
758/// ```rust,ignore
759/// use tessera_ui::cursor::{CursorEvent, CursorEventContent, PressKeyEventType};
760/// use std::time::Instant;
761///
762/// let event = CursorEvent {
763///     timestamp: Instant::now(),
764///     content: CursorEventContent::Pressed(PressKeyEventType::Left),
765/// };
766///
767/// match event.content {
768///     CursorEventContent::Pressed(button) => println!("Button pressed: {:?}", button),
769///     CursorEventContent::Released(button) => println!("Button released: {:?}", button),
770///     CursorEventContent::Scroll(scroll) => {
771///         println!("Scroll: dx={}, dy={}", scroll.delta_x, scroll.delta_y);
772///     }
773/// }
774/// ```
775#[derive(Debug, Clone)]
776pub struct CursorEvent {
777    /// Timestamp indicating when this event occurred.
778    pub timestamp: Instant,
779    /// The specific type and data of this cursor event.
780    pub content: CursorEventContent,
781    /// Classification of the gesture associated with this event.
782    ///
783    /// Events originating from touch scrolling will mark this as [`GestureState::Dragged`],
784    /// allowing downstream components to distinguish tap candidates from scroll gestures.
785    pub gesture_state: GestureState,
786}
787
788/// Contains scroll movement data for scroll events.
789///
790/// `ScrollEventConent` represents the amount of scrolling that occurred,
791/// with positive values typically indicating rightward/downward movement
792/// and negative values indicating leftward/upward movement.
793///
794/// # Example
795///
796/// ```rust,ignore
797/// use tessera_ui::cursor::ScrollEventConent;
798///
799/// let scroll = ScrollEventConent {
800///     delta_x: 10.0,   // Scroll right 10 pixels
801///     delta_y: -20.0,  // Scroll up 20 pixels
802/// };
803///
804/// println!("Horizontal scroll: {}", scroll.delta_x);
805/// println!("Vertical scroll: {}", scroll.delta_y);
806/// ```
807#[derive(Debug, Clone, PartialEq)]
808pub struct ScrollEventConent {
809    /// Horizontal scroll distance in pixels.
810    ///
811    /// Positive values indicate rightward scrolling,
812    /// negative values indicate leftward scrolling.
813    pub delta_x: f32,
814    /// Vertical scroll distance in pixels.
815    ///
816    /// Positive values indicate downward scrolling,
817    /// negative values indicate upward scrolling.
818    pub delta_y: f32,
819}
820
821/// Enumeration of all possible cursor event types.
822///
823/// `CursorEventContent` represents the different kinds of interactions
824/// that can occur with cursor or touch input, including button presses,
825/// releases, and scroll actions.
826///
827/// # Example
828///
829/// ```rust,ignore
830/// use tessera_ui::cursor::{CursorEventContent, PressKeyEventType, ScrollEventConent};
831///
832/// // Handle different event types
833/// match event_content {
834///     CursorEventContent::Pressed(PressKeyEventType::Left) => {
835///         println!("Left button pressed");
836///     }
837///     CursorEventContent::Released(PressKeyEventType::Right) => {
838///         println!("Right button released");
839///     }
840///     CursorEventContent::Scroll(scroll) => {
841///         println!("Scrolled by ({}, {})", scroll.delta_x, scroll.delta_y);
842///     }
843/// }
844/// ```
845#[derive(Debug, Clone, PartialEq)]
846pub enum CursorEventContent {
847    /// A cursor button or touch point was pressed.
848    Pressed(PressKeyEventType),
849    /// A cursor button or touch point was released.
850    Released(PressKeyEventType),
851    /// A scroll action occurred (mouse wheel, touch drag, or inertial scroll).
852    Scroll(ScrollEventConent),
853}
854
855/// Describes the high-level gesture classification of a cursor event.
856#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
857pub enum GestureState {
858    /// Indicates the event is part of a potential tap/click interaction.
859    #[default]
860    TapCandidate,
861    /// Indicates the event happened during a drag/scroll gesture.
862    Dragged,
863}
864
865impl CursorEventContent {
866    /// Creates a cursor press/release event from winit mouse button events.
867    ///
868    /// This method converts winit's mouse button events into Tessera's cursor event format.
869    /// It handles the three standard mouse buttons (left, right, middle) and ignores
870    /// any additional buttons that may be present on some mice.
871    ///
872    /// # Arguments
873    ///
874    /// * `state` - Whether the button was pressed or released
875    /// * `button` - Which mouse button was affected
876    ///
877    /// # Returns
878    ///
879    /// - `Some(CursorEventContent)` for supported mouse buttons
880    /// - `None` for unsupported mouse buttons
881    ///
882    /// # Example
883    ///
884    /// ```rust,ignore
885    /// use tessera_ui::cursor::CursorEventContent;
886    /// use winit::event::{ElementState, MouseButton};
887    ///
888    /// let press_event = CursorEventContent::from_press_event(
889    ///     ElementState::Pressed,
890    ///     MouseButton::Left
891    /// );
892    ///
893    /// if let Some(event) = press_event {
894    ///     println!("Created cursor event: {:?}", event);
895    /// }
896    /// ```
897    pub fn from_press_event(
898        state: winit::event::ElementState,
899        button: winit::event::MouseButton,
900    ) -> Option<Self> {
901        let event_type = match button {
902            winit::event::MouseButton::Left => PressKeyEventType::Left,
903            winit::event::MouseButton::Right => PressKeyEventType::Right,
904            winit::event::MouseButton::Middle => PressKeyEventType::Middle,
905            _ => return None, // Ignore other buttons
906        };
907        let state = match state {
908            winit::event::ElementState::Pressed => Self::Pressed(event_type),
909            winit::event::ElementState::Released => Self::Released(event_type),
910        };
911        Some(state)
912    }
913
914    /// Creates a scroll event from winit mouse wheel events.
915    ///
916    /// This method converts winit's mouse scroll delta into Tessera's scroll event format.
917    /// It handles both line-based scrolling (typical mouse wheels) and pixel-based
918    /// scrolling (trackpads, precision mice) by applying appropriate scaling.
919    ///
920    /// # Arguments
921    ///
922    /// * `delta` - The scroll delta from winit
923    ///
924    /// # Returns
925    ///
926    /// A `CursorEventContent::Scroll` event with scaled delta values.
927    ///
928    /// # Example
929    ///
930    /// ```rust,ignore
931    /// use tessera_ui::cursor::CursorEventContent;
932    /// use winit::event::MouseScrollDelta;
933    ///
934    /// let scroll_event = CursorEventContent::from_scroll_event(
935    ///     MouseScrollDelta::LineDelta(0.0, 1.0)  // Scroll down one line
936    /// );
937    ///
938    /// match scroll_event {
939    ///     CursorEventContent::Scroll(scroll) => {
940    ///         println!("Scroll delta: ({}, {})", scroll.delta_x, scroll.delta_y);
941    ///     }
942    ///     _ => {}
943    /// }
944    /// ```
945    pub fn from_scroll_event(delta: winit::event::MouseScrollDelta) -> Self {
946        let (delta_x, delta_y) = match delta {
947            winit::event::MouseScrollDelta::LineDelta(x, y) => (x, y),
948            winit::event::MouseScrollDelta::PixelDelta(delta) => (delta.x as f32, delta.y as f32),
949        };
950
951        const MOUSE_WHEEL_SPEED_MULTIPLIER: f32 = 50.0;
952        Self::Scroll(ScrollEventConent {
953            delta_x: delta_x * MOUSE_WHEEL_SPEED_MULTIPLIER,
954            delta_y: delta_y * MOUSE_WHEEL_SPEED_MULTIPLIER,
955        })
956    }
957}
958
959/// Represents the different types of cursor buttons or touch interactions.
960///
961/// `PressKeyEventType` identifies which button was pressed or released in
962/// a cursor event. This covers the three standard mouse buttons that are
963/// commonly supported across different platforms and input devices.
964///
965/// # Example
966///
967/// ```rust,ignore
968/// use tessera_ui::cursor::PressKeyEventType;
969///
970/// match button_type {
971///     PressKeyEventType::Left => println!("Primary button (usually left-click)"),
972///     PressKeyEventType::Right => println!("Secondary button (usually right-click)"),
973///     PressKeyEventType::Middle => println!("Middle button (usually scroll wheel click)"),
974/// }
975/// ```
976#[derive(Debug, Clone, PartialEq, Eq)]
977pub enum PressKeyEventType {
978    /// The primary mouse button (typically left button) or primary touch.
979    Left,
980    /// The secondary mouse button (typically right button).
981    Right,
982    /// The middle mouse button (typically scroll wheel click).
983    Middle,
984}