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, event
68/// 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 cursor events 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 cursor event 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 cursor event 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 cursor events.
124    ///
125    /// This method returns all queued cursor events 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                        source: ScrollEventSource::Touch,
253                    }),
254                    gesture_state: GestureState::Dragged,
255                    consumed: false,
256                });
257            }
258        }
259        None
260    }
261
262    /// Handles the end of a touch gesture and emits a release event.
263    ///
264    /// This method processes the end of a touch interaction by:
265    /// - Determining whether the gesture was a drag
266    /// - Generating a release event
267    /// - Cleaning up touch point tracking
268    ///
269    /// # Arguments
270    ///
271    /// * `touch_id` - Unique identifier for the touch point that ended
272    pub fn handle_touch_end(&mut self, touch_id: u64) {
273        let now = Instant::now();
274        let mut was_drag = false;
275
276        if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
277            was_drag |= touch_state.generated_scroll_event;
278        }
279
280        self.touch_points.remove(&touch_id);
281        let release_event = PointerChange {
282            timestamp: now,
283            pointer_id: touch_id,
284            content: CursorEventContent::Released(PressKeyEventType::Left),
285            gesture_state: if was_drag {
286                GestureState::Dragged
287            } else {
288                GestureState::TapCandidate
289            },
290            consumed: false,
291        };
292        self.push_event(release_event);
293
294        if self.touch_points.is_empty() {
295            self.clear_position_on_next_frame = true;
296        }
297    }
298}
299
300/// Represents a single pointer change with timing information.
301///
302/// `PointerChange` encapsulates all pointer interactions including
303/// presses, releases, and scroll actions. Each event includes a timestamp for
304/// precise timing and ordering of input events.
305#[derive(Debug, Clone)]
306pub struct PointerChange {
307    /// Timestamp indicating when this event occurred.
308    pub timestamp: Instant,
309    /// Pointer identifier for this input stream.
310    pub pointer_id: PointerId,
311    /// The specific type and data of this cursor event.
312    pub content: CursorEventContent,
313    /// Classification of the gesture associated with this event.
314    ///
315    /// Events originating from touch scrolling will mark this as
316    /// [`GestureState::Dragged`], allowing downstream components to
317    /// distinguish tap candidates from scroll gestures.
318    pub gesture_state: GestureState,
319    /// Whether this change has been consumed by a handler.
320    pub(crate) consumed: bool,
321}
322
323impl PointerChange {
324    /// Marks this change as consumed.
325    pub fn consume(&mut self) {
326        self.consumed = true;
327    }
328
329    /// Returns whether this change has already been consumed.
330    pub fn is_consumed(&self) -> bool {
331        self.consumed
332    }
333}
334
335/// Contains scroll movement data for scroll events.
336///
337/// `ScrollEventContent` represents the amount of scrolling that occurred,
338/// with positive values typically indicating rightward/downward movement
339/// and negative values indicating leftward/upward movement.
340#[derive(Debug, Clone, PartialEq)]
341pub struct ScrollEventContent {
342    /// Horizontal scroll distance in pixels.
343    pub delta_x: f32,
344    /// Vertical scroll distance in pixels.
345    pub delta_y: f32,
346    /// The input source that produced the scroll event.
347    pub source: ScrollEventSource,
348}
349
350/// Enumeration of all possible cursor event types.
351///
352/// `CursorEventContent` represents the different kinds of interactions
353/// that can occur with cursor or touch input, including button presses,
354/// releases, and scroll actions.
355#[derive(Debug, Clone, PartialEq)]
356pub enum CursorEventContent {
357    /// The pointer moved to a new absolute position.
358    Moved(PxPosition),
359    /// A cursor button or touch point was pressed.
360    Pressed(PressKeyEventType),
361    /// A cursor button or touch point was released.
362    Released(PressKeyEventType),
363    /// A scroll action occurred (mouse wheel or touch drag).
364    Scroll(ScrollEventContent),
365}
366
367/// Backward-compatible alias for older naming.
368pub type CursorEvent = PointerChange;
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. It handles both line-based scrolling (typical mouse
418    /// wheels) and pixel-based scrolling (trackpads, precision mice) by
419    /// applying appropriate scaling.
420    ///
421    /// # Arguments
422    ///
423    /// * `delta` - The scroll delta from winit
424    ///
425    /// # Returns
426    ///
427    /// A `CursorEventContent::Scroll` event with scaled delta values.
428    pub fn from_scroll_event(delta: winit::event::MouseScrollDelta) -> Self {
429        let (delta_x, delta_y) = match delta {
430            winit::event::MouseScrollDelta::LineDelta(x, y) => (x, y),
431            winit::event::MouseScrollDelta::PixelDelta(delta) => (delta.x as f32, delta.y as f32),
432        };
433
434        const MOUSE_WHEEL_SPEED_MULTIPLIER: f32 = 50.0;
435        Self::Scroll(ScrollEventContent {
436            delta_x: delta_x * MOUSE_WHEEL_SPEED_MULTIPLIER,
437            delta_y: delta_y * MOUSE_WHEEL_SPEED_MULTIPLIER,
438            source: ScrollEventSource::Wheel,
439        })
440    }
441}
442
443/// Represents the different types of cursor buttons or touch interactions.
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub enum PressKeyEventType {
446    /// The primary mouse button (typically left button) or primary touch.
447    Left,
448    /// The secondary mouse button (typically right button).
449    Right,
450    /// The middle mouse button (typically scroll wheel click).
451    Middle,
452}
453
454/// Indicates the input source for a scroll event.
455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456pub enum ScrollEventSource {
457    /// Scroll generated from a touch drag gesture.
458    Touch,
459    /// Scroll generated by a mouse wheel or trackpad.
460    Wheel,
461}