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}