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
7use std::{
8    collections::{HashMap, VecDeque},
9    time::{Duration, Instant},
10};
11
12use crate::PxPosition;
13
14/// Maximum number of events to keep in the queue to prevent memory issues during UI jank.
15const KEEP_EVENTS_COUNT: usize = 10;
16
17/// Controls how quickly inertial scrolling decelerates (higher = faster slowdown).
18const INERTIA_DECAY_CONSTANT: f32 = 5.0;
19
20/// Minimum velocity threshold below which inertial scrolling stops (pixels per second).
21const MIN_INERTIA_VELOCITY: f32 = 10.0;
22
23/// Minimum velocity from a gesture required to start inertial scrolling (pixels per second).
24const INERTIA_MIN_VELOCITY_THRESHOLD_FOR_START: f32 = 50.0;
25
26/// Multiplier applied to initial inertial velocity (typically 1.0 for natural feel).
27const INERTIA_MOMENTUM_FACTOR: f32 = 1.0;
28
29/// Maximum inertial velocity to keep flicks controllable (pixels per second).
30const MAX_INERTIA_VELOCITY: f32 = 6000.0;
31
32/// Tracks the state of a single touch point for gesture recognition and velocity calculation.
33///
34/// This struct maintains the necessary information to track touch movement, calculate
35/// velocities, and determine when to trigger inertial scrolling.
36#[derive(Debug, Clone)]
37struct TouchPointState {
38    /// The last recorded position of this touch point.
39    last_position: PxPosition,
40    /// Timestamp of the last position update.
41    last_update_time: Instant,
42    /// Tracks recent velocity samples and temporal metadata for momentum calculation.
43    velocity_tracker: VelocityTracker,
44    /// Tracks whether this touch gesture generated a scroll event.
45    ///
46    /// When set, the gesture should be treated as a drag/scroll rather than a tap.
47    generated_scroll_event: bool,
48}
49
50/// Maintains a short window of velocity samples for inertia calculations.
51#[derive(Debug, Clone)]
52struct VelocityTracker {
53    samples: VecDeque<(Instant, f32, f32)>,
54    last_sample_time: Instant,
55}
56
57const VELOCITY_SAMPLE_WINDOW: Duration = Duration::from_millis(90);
58const VELOCITY_IDLE_CUTOFF: Duration = Duration::from_millis(65);
59
60/// Represents an active inertial scrolling session.
61///
62/// When a touch gesture ends with sufficient velocity, this struct tracks
63/// the momentum and gradually decelerates the scroll movement over time.
64#[derive(Debug, Clone)]
65struct ActiveInertia {
66    /// Current horizontal velocity in pixels per second.
67    velocity_x: f32,
68    /// Current vertical velocity in pixels per second.
69    velocity_y: f32,
70    /// Timestamp of the last inertia calculation update.
71    last_tick_time: Instant,
72}
73
74fn clamp_inertia_velocity(vx: f32, vy: f32) -> (f32, f32) {
75    if !vx.is_finite() || !vy.is_finite() {
76        return (0.0, 0.0);
77    }
78
79    let magnitude_sq = vx * vx + vy * vy;
80    if !magnitude_sq.is_finite() {
81        return (0.0, 0.0);
82    }
83
84    let magnitude = magnitude_sq.sqrt();
85    if magnitude > MAX_INERTIA_VELOCITY && MAX_INERTIA_VELOCITY > 0.0 {
86        let scale = MAX_INERTIA_VELOCITY / magnitude;
87        return (vx * scale, vy * scale);
88    }
89
90    (vx, vy)
91}
92
93/// Configuration settings for touch scrolling behavior.
94///
95/// This struct controls various aspects of how touch gestures are interpreted
96/// and converted into scroll events.
97#[derive(Debug, Clone)]
98struct TouchScrollConfig {
99    /// Minimum movement distance in pixels required to trigger a scroll event.
100    ///
101    /// Smaller values make scrolling more sensitive but may cause jitter.
102    /// Larger values require more deliberate movement but provide stability.
103    min_move_threshold: f32,
104    /// Whether touch scrolling is currently enabled.
105    enabled: bool,
106}
107
108impl Default for TouchScrollConfig {
109    fn default() -> Self {
110        Self {
111            // Reduced threshold for more responsive touch
112            min_move_threshold: 5.0,
113            enabled: true,
114        }
115    }
116}
117
118/// Central state manager for cursor and touch interactions.
119///
120/// `CursorState` is the main interface for handling all cursor-related events in the Tessera
121/// UI framework. It manages cursor position tracking, event queuing, multi-touch support,
122/// and provides smooth inertial scrolling for touch gestures.
123#[derive(Default)]
124pub struct CursorState {
125    /// Current cursor position, if any cursor is active.
126    position: Option<PxPosition>,
127    /// Bounded queue of cursor events awaiting processing.
128    events: VecDeque<CursorEvent>,
129    /// Active touch points mapped by their unique touch IDs.
130    touch_points: HashMap<u64, TouchPointState>,
131    /// Configuration settings for touch scrolling behavior.
132    touch_scroll_config: TouchScrollConfig,
133    /// Current inertial scrolling state, if active.
134    active_inertia: Option<ActiveInertia>,
135    /// If true, the cursor position will be cleared on the next frame.
136    clear_position_on_next_frame: bool,
137}
138
139impl CursorState {
140    /// Cleans up the cursor state at the end of a frame.
141    pub(crate) fn frame_cleanup(&mut self) {
142        if self.clear_position_on_next_frame {
143            self.update_position(None);
144            self.clear_position_on_next_frame = false;
145        }
146    }
147
148    /// Adds a cursor event to the processing queue.
149    ///
150    /// Events are stored in a bounded queue to prevent memory issues during UI performance
151    /// problems. If the queue exceeds [`KEEP_EVENTS_COUNT`], the oldest events are discarded.
152    ///
153    /// # Arguments
154    ///
155    /// * `event` - The cursor event to add to the queue
156    pub fn push_event(&mut self, event: CursorEvent) {
157        self.events.push_back(event);
158
159        // Maintain bounded queue size to prevent memory issues during UI jank
160        if self.events.len() > KEEP_EVENTS_COUNT {
161            self.events.pop_front();
162        }
163    }
164
165    /// Updates the current cursor position.
166    ///
167    /// This method accepts any type that can be converted into `Option<PxPosition>`,
168    /// allowing for flexible position updates including clearing the position by
169    /// passing `None`.
170    ///
171    /// # Arguments
172    ///
173    /// * `position` - New cursor position or `None` to clear the position
174    pub fn update_position(&mut self, position: impl Into<Option<PxPosition>>) {
175        self.position = position.into();
176    }
177
178    /// Processes active inertial scrolling and generates scroll events.
179    ///
180    /// This method is called internally to update inertial scrolling state and generate
181    /// appropriate scroll events. It handles velocity decay over time and stops inertia
182    /// when velocity falls below the minimum threshold.
183    ///
184    /// The method calculates scroll deltas based on current velocity and elapsed time,
185    /// applies exponential decay to the velocity, and queues scroll events for processing.
186    ///
187    /// # Implementation Details
188    ///
189    /// - Uses exponential decay with [`INERTIA_DECAY_CONSTANT`] for natural deceleration
190    /// - Stops inertia when velocity drops below [`MIN_INERTIA_VELOCITY`]
191    /// - Generates scroll events with calculated position deltas
192    /// - Handles edge cases like zero delta time gracefully
193    fn process_and_queue_inertial_scroll(&mut self) {
194        // Handle active inertia with clear, small responsibilities.
195        if let Some(mut inertia) = self.active_inertia.take() {
196            let now = Instant::now();
197            let delta_time = now.duration_since(inertia.last_tick_time).as_secs_f32();
198
199            if delta_time <= 0.0 {
200                // Called multiple times in the same instant; reinsert for next frame.
201                self.active_inertia = Some(inertia);
202                return;
203            }
204
205            // Compute scroll delta and emit event if meaningful.
206            let scroll_delta_x = inertia.velocity_x * delta_time;
207            let scroll_delta_y = inertia.velocity_y * delta_time;
208            if scroll_delta_x.abs() > 0.01 || scroll_delta_y.abs() > 0.01 {
209                self.push_scroll_event(now, scroll_delta_x, scroll_delta_y);
210            }
211
212            // Apply exponential decay to velocities.
213            let decay = (-INERTIA_DECAY_CONSTANT * delta_time).exp();
214            inertia.velocity_x *= decay;
215            inertia.velocity_y *= decay;
216            inertia.last_tick_time = now;
217
218            // Reinsert inertia only if still above threshold.
219            if inertia.velocity_x.abs() >= MIN_INERTIA_VELOCITY
220                || inertia.velocity_y.abs() >= MIN_INERTIA_VELOCITY
221            {
222                self.active_inertia = Some(inertia);
223            }
224        }
225    }
226
227    // Helper: push a scroll event with consistent construction.
228    fn push_scroll_event(&mut self, timestamp: Instant, dx: f32, dy: f32) {
229        self.push_event(CursorEvent {
230            timestamp,
231            content: CursorEventContent::Scroll(ScrollEventConent {
232                delta_x: dx,
233                delta_y: dy,
234            }),
235            gesture_state: GestureState::Dragged,
236        });
237    }
238
239    /// Retrieves and clears all pending cursor events.
240    ///
241    /// This method processes any active inertial scrolling, then returns all queued
242    /// cursor events and clears the internal event queue. Events are returned in
243    /// chronological order (oldest first).
244    ///
245    /// This is typically called once per frame by the UI framework to process
246    /// all accumulated input events.
247    ///
248    /// # Returns
249    ///
250    /// A vector of [`CursorEvent`]s ordered from oldest to newest.
251    ///
252    /// # Note
253    ///
254    /// Events are ordered from oldest to newest to ensure proper event processing order.
255    pub fn take_events(&mut self) -> Vec<CursorEvent> {
256        self.process_and_queue_inertial_scroll();
257        self.events.drain(..).collect()
258    }
259
260    /// Clears all cursor state and pending events.
261    ///
262    /// This is typically used when the UI context changes significantly,
263    /// such as when switching between different UI screens or when input
264    /// focus changes.
265    pub fn clear(&mut self) {
266        self.events.clear();
267        self.update_position(None);
268        self.active_inertia = None;
269        self.touch_points.clear();
270        self.clear_position_on_next_frame = false;
271    }
272
273    /// Returns the current cursor position, if any.
274    ///
275    /// The position represents the last known location of the cursor or active touch point.
276    /// Returns `None` if no cursor is currently active or if the position has been cleared.
277    ///
278    /// # Returns
279    ///
280    /// - `Some(PxPosition)` if a cursor position is currently tracked
281    /// - `None` if no cursor is active
282    pub fn position(&self) -> Option<PxPosition> {
283        self.position
284    }
285
286    /// Handles the start of a touch gesture.
287    ///
288    /// This method registers a new touch point and generates a press event. It also
289    /// stops any active inertial scrolling since a new touch interaction has begun.
290    ///
291    /// # Arguments
292    ///
293    /// * `touch_id` - Unique identifier for this touch point
294    /// * `position` - Initial position of the touch in pixel coordinates
295    pub fn handle_touch_start(&mut self, touch_id: u64, position: PxPosition) {
296        self.active_inertia = None; // Stop any existing inertia on new touch
297        let now = Instant::now();
298
299        self.touch_points.insert(
300            touch_id,
301            TouchPointState {
302                last_position: position,
303                last_update_time: now,
304                velocity_tracker: VelocityTracker::new(now),
305                generated_scroll_event: false,
306            },
307        );
308        self.update_position(position);
309        let press_event = CursorEvent {
310            timestamp: now,
311            content: CursorEventContent::Pressed(PressKeyEventType::Left),
312            gesture_state: GestureState::TapCandidate,
313        };
314        self.push_event(press_event);
315    }
316
317    /// Handles touch movement and generates scroll events when appropriate.
318    ///
319    /// This method tracks touch movement, calculates velocities for inertial scrolling,
320    /// and generates scroll events when the movement exceeds the minimum threshold.
321    /// It also maintains a velocity history for momentum calculation.
322    ///
323    /// # Arguments
324    ///
325    /// * `touch_id` - Unique identifier for the touch point being moved
326    /// * `current_position` - New position of the touch in pixel coordinates
327    ///
328    /// # Returns
329    ///
330    /// - `Some(CursorEvent)` containing a scroll event if movement exceeds threshold
331    /// - `None` if movement is below threshold or touch scrolling is disabled
332    pub fn handle_touch_move(
333        &mut self,
334        touch_id: u64,
335        current_position: PxPosition,
336    ) -> Option<CursorEvent> {
337        let now = Instant::now();
338        self.update_position(current_position);
339
340        if !self.touch_scroll_config.enabled {
341            return None;
342        }
343
344        if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
345            let delta_x = (current_position.x - touch_state.last_position.x).to_f32();
346            let delta_y = (current_position.y - touch_state.last_position.y).to_f32();
347            let move_distance = (delta_x * delta_x + delta_y * delta_y).sqrt();
348            let time_delta = now
349                .duration_since(touch_state.last_update_time)
350                .as_secs_f32();
351
352            touch_state.last_position = current_position;
353            touch_state.last_update_time = now;
354
355            if move_distance >= self.touch_scroll_config.min_move_threshold {
356                // Stop any active inertia when user actively moves the touch.
357                self.active_inertia = None;
358
359                if time_delta > 0.0 {
360                    let velocity_x = delta_x / time_delta;
361                    let velocity_y = delta_y / time_delta;
362                    touch_state
363                        .velocity_tracker
364                        .push(now, velocity_x, velocity_y);
365                }
366
367                touch_state.generated_scroll_event = true;
368
369                // Return a scroll event for immediate feedback.
370                return Some(CursorEvent {
371                    timestamp: now,
372                    content: CursorEventContent::Scroll(ScrollEventConent {
373                        delta_x, // Direct scroll delta for touch move
374                        delta_y,
375                    }),
376                    gesture_state: GestureState::Dragged,
377                });
378            }
379        }
380        None
381    }
382
383    /// Handles the end of a touch gesture and potentially starts inertial scrolling.
384    ///
385    /// This method processes the end of a touch interaction by:
386    /// - Calculating average velocity from recent touch movement
387    /// - Starting inertial scrolling if velocity exceeds the threshold
388    /// - Generating a release event
389    /// - Cleaning up touch point tracking
390    ///
391    /// # Arguments
392    ///
393    /// * `touch_id` - Unique identifier for the touch point that ended
394    pub fn handle_touch_end(&mut self, touch_id: u64) {
395        let now = Instant::now();
396        let mut was_drag = false;
397
398        if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
399            was_drag |= touch_state.generated_scroll_event;
400            if self.touch_scroll_config.enabled {
401                if let Some((avg_vx, avg_vy)) = touch_state.velocity_tracker.resolve(now) {
402                    let velocity_magnitude = (avg_vx * avg_vx + avg_vy * avg_vy).sqrt();
403                    if velocity_magnitude > INERTIA_MIN_VELOCITY_THRESHOLD_FOR_START {
404                        let (inertia_vx, inertia_vy) = clamp_inertia_velocity(
405                            avg_vx * INERTIA_MOMENTUM_FACTOR,
406                            avg_vy * INERTIA_MOMENTUM_FACTOR,
407                        );
408                        self.active_inertia = Some(ActiveInertia {
409                            velocity_x: inertia_vx,
410                            velocity_y: inertia_vy,
411                            last_tick_time: now,
412                        });
413                    } else {
414                        self.active_inertia = None;
415                    }
416                } else {
417                    self.active_inertia = None;
418                }
419            } else {
420                self.active_inertia = None; // Scrolling disabled
421            }
422        } else {
423            self.active_inertia = None; // No touch state present
424        }
425
426        if self.active_inertia.is_some() {
427            was_drag = true;
428        }
429
430        self.touch_points.remove(&touch_id);
431        let release_event = CursorEvent {
432            timestamp: now,
433            content: CursorEventContent::Released(PressKeyEventType::Left),
434            gesture_state: if was_drag {
435                GestureState::Dragged
436            } else {
437                GestureState::TapCandidate
438            },
439        };
440        self.push_event(release_event);
441
442        if self.touch_points.is_empty() && self.active_inertia.is_none() {
443            self.clear_position_on_next_frame = true;
444        }
445    }
446}
447
448impl VelocityTracker {
449    fn new(now: Instant) -> Self {
450        Self {
451            samples: VecDeque::new(),
452            last_sample_time: now,
453        }
454    }
455
456    fn push(&mut self, now: Instant, vx: f32, vy: f32) {
457        let (vx, vy) = clamp_inertia_velocity(vx, vy);
458        self.samples.push_back((now, vx, vy));
459        self.last_sample_time = now;
460        self.prune(now);
461    }
462
463    fn resolve(&mut self, now: Instant) -> Option<(f32, f32)> {
464        self.prune(now);
465
466        if self.samples.is_empty() {
467            return None;
468        }
469
470        let idle_time = now.duration_since(self.last_sample_time);
471        if idle_time >= VELOCITY_IDLE_CUTOFF {
472            self.samples.clear();
473            return None;
474        }
475
476        let mut weighted_sum_x = 0.0f32;
477        let mut weighted_sum_y = 0.0f32;
478        let mut total_weight = 0.0f32;
479        let window_secs = VELOCITY_SAMPLE_WINDOW.as_secs_f32().max(f32::EPSILON);
480
481        for &(timestamp, vx, vy) in &self.samples {
482            let age_secs = now
483                .duration_since(timestamp)
484                .as_secs_f32()
485                .clamp(0.0, window_secs);
486            let weight = (window_secs - age_secs).max(0.0);
487            if weight > 0.0 {
488                weighted_sum_x += vx * weight;
489                weighted_sum_y += vy * weight;
490                total_weight += weight;
491            }
492        }
493
494        if total_weight <= f32::EPSILON {
495            self.samples.clear();
496            return None;
497        }
498
499        let avg_x = weighted_sum_x / total_weight;
500        let avg_y = weighted_sum_y / total_weight;
501
502        let damping = 1.0 - idle_time.as_secs_f32() / VELOCITY_IDLE_CUTOFF.as_secs_f32();
503        let damping = damping.clamp(0.0, 1.0);
504        let (avg_x, avg_y) = clamp_inertia_velocity(avg_x * damping, avg_y * damping);
505
506        Some((avg_x, avg_y))
507    }
508
509    fn prune(&mut self, now: Instant) {
510        while let Some(&(timestamp, _, _)) = self.samples.front() {
511            if now.duration_since(timestamp) > VELOCITY_SAMPLE_WINDOW {
512                self.samples.pop_front();
513            } else {
514                break;
515            }
516        }
517    }
518}
519
520/// Represents a single cursor or touch event with timing information.
521///
522/// `CursorEvent` encapsulates all types of cursor interactions including presses,
523/// releases, and scroll actions. Each event includes a timestamp for precise
524/// timing and ordering of input events.
525#[derive(Debug, Clone)]
526pub struct CursorEvent {
527    /// Timestamp indicating when this event occurred.
528    pub timestamp: Instant,
529    /// The specific type and data of this cursor event.
530    pub content: CursorEventContent,
531    /// Classification of the gesture associated with this event.
532    ///
533    /// Events originating from touch scrolling will mark this as [`GestureState::Dragged`],
534    /// allowing downstream components to distinguish tap candidates from scroll gestures.
535    pub gesture_state: GestureState,
536}
537
538/// Contains scroll movement data for scroll events.
539///
540/// `ScrollEventConent` represents the amount of scrolling that occurred,
541/// with positive values typically indicating rightward/downward movement
542/// and negative values indicating leftward/upward movement.
543#[derive(Debug, Clone, PartialEq)]
544pub struct ScrollEventConent {
545    /// Horizontal scroll distance in pixels.
546    pub delta_x: f32,
547    /// Vertical scroll distance in pixels.
548    pub delta_y: f32,
549}
550
551/// Enumeration of all possible cursor event types.
552///
553/// `CursorEventContent` represents the different kinds of interactions
554/// that can occur with cursor or touch input, including button presses,
555/// releases, and scroll actions.
556#[derive(Debug, Clone, PartialEq)]
557pub enum CursorEventContent {
558    /// A cursor button or touch point was pressed.
559    Pressed(PressKeyEventType),
560    /// A cursor button or touch point was released.
561    Released(PressKeyEventType),
562    /// A scroll action occurred (mouse wheel, touch drag, or inertial scroll).
563    Scroll(ScrollEventConent),
564}
565
566/// Describes the high-level gesture classification of a cursor event.
567#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
568pub enum GestureState {
569    /// Indicates the event is part of a potential tap/click interaction.
570    #[default]
571    TapCandidate,
572    /// Indicates the event happened during a drag/scroll gesture.
573    Dragged,
574}
575
576impl CursorEventContent {
577    /// Creates a cursor press/release event from winit mouse button events.
578    ///
579    /// This method converts winit's mouse button events into Tessera's cursor event format.
580    /// It handles the three standard mouse buttons (left, right, middle) and ignores
581    /// any additional buttons that may be present on some mice.
582    ///
583    /// # Arguments
584    ///
585    /// * `state` - Whether the button was pressed or released
586    /// * `button` - Which mouse button was affected
587    ///
588    /// # Returns
589    ///
590    /// - `Some(CursorEventContent)` for supported mouse buttons
591    /// - `None` for unsupported mouse buttons
592    pub fn from_press_event(
593        state: winit::event::ElementState,
594        button: winit::event::MouseButton,
595    ) -> Option<Self> {
596        let event_type = match button {
597            winit::event::MouseButton::Left => PressKeyEventType::Left,
598            winit::event::MouseButton::Right => PressKeyEventType::Right,
599            winit::event::MouseButton::Middle => PressKeyEventType::Middle,
600            _ => return None, // Ignore other buttons
601        };
602        let state = match state {
603            winit::event::ElementState::Pressed => Self::Pressed(event_type),
604            winit::event::ElementState::Released => Self::Released(event_type),
605        };
606        Some(state)
607    }
608
609    /// Creates a scroll event from winit mouse wheel events.
610    ///
611    /// This method converts winit's mouse scroll delta into Tessera's scroll event format.
612    /// It handles both line-based scrolling (typical mouse wheels) and pixel-based
613    /// scrolling (trackpads, precision mice) by applying appropriate scaling.
614    ///
615    /// # Arguments
616    ///
617    /// * `delta` - The scroll delta from winit
618    ///
619    /// # Returns
620    ///
621    /// A `CursorEventContent::Scroll` event with scaled delta values.
622    pub fn from_scroll_event(delta: winit::event::MouseScrollDelta) -> Self {
623        let (delta_x, delta_y) = match delta {
624            winit::event::MouseScrollDelta::LineDelta(x, y) => (x, y),
625            winit::event::MouseScrollDelta::PixelDelta(delta) => (delta.x as f32, delta.y as f32),
626        };
627
628        const MOUSE_WHEEL_SPEED_MULTIPLIER: f32 = 50.0;
629        Self::Scroll(ScrollEventConent {
630            delta_x: delta_x * MOUSE_WHEEL_SPEED_MULTIPLIER,
631            delta_y: delta_y * MOUSE_WHEEL_SPEED_MULTIPLIER,
632        })
633    }
634}
635
636/// Represents the different types of cursor buttons or touch interactions.
637#[derive(Debug, Clone, PartialEq, Eq)]
638pub enum PressKeyEventType {
639    /// The primary mouse button (typically left button) or primary touch.
640    Left,
641    /// The secondary mouse button (typically right button).
642    Right,
643    /// The middle mouse button (typically scroll wheel click).
644    Middle,
645}