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}