Skip to main content

tessera_ui/
cursor.rs

1//! # Cursor management
2//!
3//! This module provides comprehensive cursor and touch event handling for the
4//! Tessera UI framework. It manages cursor position tracking, event queuing,
5//! touch gesture recognition, and scroll event generation for smooth user
6//! interactions.
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/// Maximum number of events to keep in the queue to prevent memory issues
18/// during UI jank.
19const KEEP_EVENTS_COUNT: usize = 10;
20
21/// Tracks the state of a single touch point for gesture recognition and
22/// scroll tracking.
23///
24/// This struct maintains the necessary information to track touch movement and
25/// determine when to trigger scrolling.
26#[derive(Debug, Clone)]
27struct TouchPointState {
28    /// The last recorded position of this touch point.
29    last_position: PxPosition,
30    /// Timestamp of the last position update.
31    last_update_time: Instant,
32    /// Tracks whether this touch gesture generated a scroll event.
33    ///
34    /// When set, the gesture should be treated as a drag/scroll rather than a
35    /// tap.
36    generated_scroll_event: bool,
37}
38
39/// Configuration settings for touch scrolling behavior.
40///
41/// This struct controls various aspects of how touch gestures are interpreted
42/// and converted into scroll events.
43#[derive(Debug, Clone)]
44struct TouchScrollConfig {
45    /// Minimum movement distance in pixels required to trigger a scroll event.
46    ///
47    /// Smaller values make scrolling more sensitive but may cause jitter.
48    /// Larger values require more deliberate movement but provide stability.
49    min_move_threshold: f32,
50    /// Whether touch scrolling is currently enabled.
51    enabled: bool,
52}
53
54impl Default for TouchScrollConfig {
55    fn default() -> Self {
56        Self {
57            // Reduced threshold for more responsive touch
58            min_move_threshold: 5.0,
59            enabled: true,
60        }
61    }
62}
63
64/// Central state manager for cursor and touch interactions.
65///
66/// `CursorState` is the main interface for handling all cursor-related events
67/// in the Tessera UI framework. It manages cursor position tracking, pointer
68/// change queuing, and multi-touch support for touch gestures.
69#[derive(Default)]
70pub struct CursorState {
71    /// Current cursor position, if any cursor is active.
72    position: Option<PxPosition>,
73    /// Bounded queue of pointer changes awaiting processing.
74    events: VecDeque<PointerChange>,
75    /// Active touch points mapped by their unique touch IDs.
76    touch_points: HashMap<u64, TouchPointState>,
77    /// Configuration settings for touch scrolling behavior.
78    touch_scroll_config: TouchScrollConfig,
79    /// If true, the cursor position will be cleared on the next frame.
80    clear_position_on_next_frame: bool,
81}
82
83impl CursorState {
84    /// Cleans up the cursor state at the end of a frame.
85    pub(crate) fn frame_cleanup(&mut self) {
86        if self.clear_position_on_next_frame {
87            self.update_position(None);
88            self.clear_position_on_next_frame = false;
89        }
90    }
91
92    /// Adds a pointer change to the processing queue.
93    ///
94    /// Events are stored in a bounded queue to prevent memory issues during UI
95    /// performance problems. If the queue exceeds [`KEEP_EVENTS_COUNT`],
96    /// the oldest events are discarded.
97    ///
98    /// # Arguments
99    ///
100    /// * `event` - The pointer change to add to the queue
101    pub fn push_event(&mut self, event: PointerChange) {
102        self.events.push_back(event);
103
104        // Maintain bounded queue size to prevent memory issues during UI jank
105        if self.events.len() > KEEP_EVENTS_COUNT {
106            self.events.pop_front();
107        }
108    }
109
110    /// Updates the current cursor position.
111    ///
112    /// This method accepts any type that can be converted into
113    /// `Option<PxPosition>`, allowing for flexible position updates
114    /// including clearing the position by passing `None`.
115    ///
116    /// # Arguments
117    ///
118    /// * `position` - New cursor position or `None` to clear the position
119    pub fn update_position(&mut self, position: impl Into<Option<PxPosition>>) {
120        self.position = position.into();
121    }
122
123    /// Retrieves and clears all pending pointer changes.
124    ///
125    /// This method returns all queued pointer changes and clears the internal
126    /// event queue. Events are returned in chronological order (oldest first).
127    ///
128    /// This is typically called once per frame by the UI framework to process
129    /// all accumulated input events.
130    ///
131    /// # Returns
132    ///
133    /// A vector of [`PointerChange`]s ordered from oldest to newest.
134    ///
135    /// # Note
136    ///
137    /// Events are ordered from oldest to newest to ensure proper event
138    /// processing order.
139    pub fn take_events(&mut self) -> Vec<PointerChange> {
140        self.events.drain(..).collect()
141    }
142
143    /// Clears all cursor state and pending events.
144    ///
145    /// This is typically used when the UI context changes significantly,
146    /// such as when switching between different UI screens or when input
147    /// focus changes.
148    pub fn clear(&mut self) {
149        self.events.clear();
150        self.update_position(None);
151        self.touch_points.clear();
152        self.clear_position_on_next_frame = false;
153    }
154
155    /// Returns the current cursor position, if any.
156    ///
157    /// The position represents the last known location of the cursor or active
158    /// touch point. Returns `None` if no cursor is currently active or if
159    /// the position has been cleared.
160    ///
161    /// # Returns
162    ///
163    /// - `Some(PxPosition)` if a cursor position is currently tracked
164    /// - `None` if no cursor is active
165    pub fn position(&self) -> Option<PxPosition> {
166        self.position
167    }
168
169    /// Handles the start of a touch gesture.
170    ///
171    /// This method registers a new touch point and generates a press event.
172    ///
173    /// # Arguments
174    ///
175    /// * `touch_id` - Unique identifier for this touch point
176    /// * `position` - Initial position of the touch in pixel coordinates
177    pub fn handle_touch_start(&mut self, touch_id: u64, position: PxPosition) {
178        self.clear_position_on_next_frame = false;
179        let now = Instant::now();
180
181        self.touch_points.insert(
182            touch_id,
183            TouchPointState {
184                last_position: position,
185                last_update_time: now,
186                generated_scroll_event: false,
187            },
188        );
189        self.update_position(position);
190        let press_event = PointerChange {
191            timestamp: now,
192            pointer_id: touch_id,
193            content: CursorEventContent::Pressed(PressKeyEventType::Left),
194            gesture_state: GestureState::TapCandidate,
195            consumed: false,
196        };
197        self.push_event(press_event);
198    }
199
200    /// Handles touch movement and generates scroll events when appropriate.
201    ///
202    /// This method tracks touch movement and generates scroll events when the
203    /// movement exceeds the minimum threshold.
204    ///
205    /// # Arguments
206    ///
207    /// * `touch_id` - Unique identifier for the touch point being moved
208    /// * `current_position` - New position of the touch in pixel coordinates
209    ///
210    /// # Returns
211    ///
212    /// - `Some(PointerChange)` containing a scroll event if movement exceeds
213    ///   threshold
214    /// - `None` if movement is below threshold or touch scrolling is disabled
215    pub fn handle_touch_move(
216        &mut self,
217        touch_id: u64,
218        current_position: PxPosition,
219    ) -> Option<PointerChange> {
220        let now = Instant::now();
221        self.update_position(current_position);
222
223        self.push_event(PointerChange {
224            timestamp: now,
225            pointer_id: touch_id,
226            content: CursorEventContent::Moved(current_position),
227            gesture_state: GestureState::TapCandidate,
228            consumed: false,
229        });
230
231        if !self.touch_scroll_config.enabled {
232            return None;
233        }
234
235        if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
236            let delta_x = (current_position.x - touch_state.last_position.x).to_f32();
237            let delta_y = (current_position.y - touch_state.last_position.y).to_f32();
238            let move_distance = (delta_x * delta_x + delta_y * delta_y).sqrt();
239            touch_state.last_position = current_position;
240            touch_state.last_update_time = now;
241
242            if move_distance >= self.touch_scroll_config.min_move_threshold {
243                touch_state.generated_scroll_event = true;
244
245                // Return a scroll event for immediate feedback.
246                return Some(PointerChange {
247                    timestamp: now,
248                    pointer_id: touch_id,
249                    content: CursorEventContent::Scroll(ScrollEventContent {
250                        delta_x, // Direct scroll delta for touch move
251                        delta_y,
252                        unit: ScrollDeltaUnit::Pixel,
253                        source: ScrollEventSource::Touch,
254                    }),
255                    gesture_state: GestureState::Dragged,
256                    consumed: false,
257                });
258            }
259        }
260        None
261    }
262
263    /// Handles the end of a touch gesture and emits a release event.
264    ///
265    /// This method processes the end of a touch interaction by:
266    /// - Determining whether the gesture was a drag
267    /// - Generating a release event
268    /// - Cleaning up touch point tracking
269    ///
270    /// # Arguments
271    ///
272    /// * `touch_id` - Unique identifier for the touch point that ended
273    pub fn handle_touch_end(&mut self, touch_id: u64) {
274        let now = Instant::now();
275        let mut was_drag = false;
276
277        if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
278            was_drag |= touch_state.generated_scroll_event;
279        }
280
281        self.touch_points.remove(&touch_id);
282        let release_event = PointerChange {
283            timestamp: now,
284            pointer_id: touch_id,
285            content: CursorEventContent::Released(PressKeyEventType::Left),
286            gesture_state: if was_drag {
287                GestureState::Dragged
288            } else {
289                GestureState::TapCandidate
290            },
291            consumed: false,
292        };
293        self.push_event(release_event);
294
295        if self.touch_points.is_empty() {
296            self.clear_position_on_next_frame = true;
297        }
298    }
299}
300
301/// Represents a single pointer change with timing information.
302///
303/// `PointerChange` encapsulates all pointer interactions including
304/// presses, releases, and scroll actions. Each event includes a timestamp for
305/// precise timing and ordering of input events.
306#[derive(Debug, Clone)]
307pub struct PointerChange {
308    /// Timestamp indicating when this event occurred.
309    pub timestamp: Instant,
310    /// Pointer identifier for this input stream.
311    pub pointer_id: PointerId,
312    /// The specific type and data of this pointer change.
313    pub content: CursorEventContent,
314    /// Classification of the gesture associated with this event.
315    ///
316    /// Events originating from touch scrolling will mark this as
317    /// [`GestureState::Dragged`], allowing downstream components to
318    /// distinguish tap candidates from scroll gestures.
319    pub gesture_state: GestureState,
320    /// Whether this change has been consumed by a handler.
321    pub(crate) consumed: bool,
322}
323
324impl PointerChange {
325    /// Marks this change as consumed.
326    pub fn consume(&mut self) {
327        self.consumed = true;
328    }
329
330    /// Returns whether this change has already been consumed.
331    pub fn is_consumed(&self) -> bool {
332        self.consumed
333    }
334}
335
336/// Contains scroll movement data for scroll events.
337///
338/// `ScrollEventContent` represents the amount of scrolling that occurred,
339/// with positive values typically indicating rightward/downward movement
340/// and negative values indicating leftward/upward movement.
341#[derive(Debug, Clone, PartialEq)]
342pub struct ScrollEventContent {
343    /// Horizontal scroll distance in pixels.
344    pub delta_x: f32,
345    /// Vertical scroll distance in pixels.
346    pub delta_y: f32,
347    /// Unit used by the originating platform event.
348    pub unit: ScrollDeltaUnit,
349    /// The input source that produced the scroll event.
350    pub source: ScrollEventSource,
351}
352
353/// Enumeration of all possible cursor event types.
354///
355/// `CursorEventContent` represents the different kinds of interactions
356/// that can occur with cursor or touch input, including button presses,
357/// releases, and scroll actions.
358#[derive(Debug, Clone, PartialEq)]
359pub enum CursorEventContent {
360    /// The pointer moved to a new absolute position.
361    Moved(PxPosition),
362    /// A cursor button or touch point was pressed.
363    Pressed(PressKeyEventType),
364    /// A cursor button or touch point was released.
365    Released(PressKeyEventType),
366    /// A scroll action occurred (mouse wheel or touch drag).
367    Scroll(ScrollEventContent),
368}
369
370/// Describes the high-level gesture classification of a cursor event.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
372pub enum GestureState {
373    /// Indicates the event is part of a potential tap/click interaction.
374    #[default]
375    TapCandidate,
376    /// Indicates the event happened during a drag/scroll gesture.
377    Dragged,
378}
379
380impl CursorEventContent {
381    /// Creates a cursor press/release event from winit mouse button events.
382    ///
383    /// This method converts winit's mouse button events into Tessera's cursor
384    /// event format. It handles the three standard mouse buttons (left,
385    /// right, middle) and ignores any additional buttons that may be
386    /// present on some mice.
387    ///
388    /// # Arguments
389    ///
390    /// * `state` - Whether the button was pressed or released
391    /// * `button` - Which mouse button was affected
392    ///
393    /// # Returns
394    ///
395    /// - `Some(CursorEventContent)` for supported mouse buttons
396    /// - `None` for unsupported mouse buttons
397    pub fn from_press_event(
398        state: winit::event::ElementState,
399        button: winit::event::MouseButton,
400    ) -> Option<Self> {
401        let event_type = match button {
402            winit::event::MouseButton::Left => PressKeyEventType::Left,
403            winit::event::MouseButton::Right => PressKeyEventType::Right,
404            winit::event::MouseButton::Middle => PressKeyEventType::Middle,
405            _ => return None, // Ignore other buttons
406        };
407        let state = match state {
408            winit::event::ElementState::Pressed => Self::Pressed(event_type),
409            winit::event::ElementState::Released => Self::Released(event_type),
410        };
411        Some(state)
412    }
413
414    /// Creates a scroll event from winit mouse wheel events.
415    ///
416    /// This method converts winit's mouse scroll delta into Tessera's scroll
417    /// event format while preserving whether the platform reported the value
418    /// in line or pixel units.
419    ///
420    /// # Arguments
421    ///
422    /// * `delta` - The scroll delta from winit
423    ///
424    /// # Returns
425    ///
426    /// A `CursorEventContent::Scroll` event with raw line or pixel delta
427    /// values.
428    pub fn from_scroll_event(delta: winit::event::MouseScrollDelta) -> Self {
429        let (delta_x, delta_y, unit) = match delta {
430            winit::event::MouseScrollDelta::LineDelta(x, y) => (x, y, ScrollDeltaUnit::Line),
431            winit::event::MouseScrollDelta::PixelDelta(delta) => {
432                (delta.x as f32, delta.y as f32, ScrollDeltaUnit::Pixel)
433            }
434        };
435
436        Self::Scroll(ScrollEventContent {
437            delta_x,
438            delta_y,
439            unit,
440            source: ScrollEventSource::Wheel,
441        })
442    }
443}
444
445/// Represents the different types of cursor buttons or touch interactions.
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub enum PressKeyEventType {
448    /// The primary mouse button (typically left button) or primary touch.
449    Left,
450    /// The secondary mouse button (typically right button).
451    Right,
452    /// The middle mouse button (typically scroll wheel click).
453    Middle,
454}
455
456/// Indicates the input source for a scroll event.
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub enum ScrollEventSource {
459    /// Scroll generated from a touch drag gesture.
460    Touch,
461    /// Scroll generated by a mouse wheel or trackpad.
462    Wheel,
463}
464
465/// Indicates the unit used by the platform to describe a scroll delta.
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum ScrollDeltaUnit {
468    /// Delta is expressed as a logical number of lines or wheel steps.
469    Line,
470    /// Delta is expressed in pixels.
471    Pixel,
472}