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//!
7//! # Key Features
8//!
9//! - **Multi-touch Support**: Tracks multiple simultaneous touch points with unique IDs
10//! - **Inertial Scrolling**: Provides smooth momentum-based scrolling after touch gestures
11//! - **Event Queuing**: Maintains a bounded queue of cursor events for processing
12//! - **Velocity Tracking**: Calculates touch velocities for natural gesture recognition
13//! - **Cross-platform**: Handles both mouse and touch input events consistently
14//!
15//! # Usage
16//!
17//! The main entry point is [`CursorState`], which maintains all cursor-related state:
18//!
19//! ```rust,ignore
20//! use tessera_ui::cursor::CursorState;
21//! use tessera_ui::PxPosition;
22//!
23//! let mut cursor_state = CursorState::default();
24//!
25//! // Handle touch start
26//! cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
27//!
28//! // Process events
29//! let events = cursor_state.take_events();
30//! for event in events {
31//! match event.content {
32//! CursorEventContent::Pressed(_) => println!("Touch started"),
33//! CursorEventContent::Scroll(scroll) => {
34//! println!("Scroll: dx={}, dy={}", scroll.delta_x, scroll.delta_y);
35//! }
36//! _ => {}
37//! }
38//! }
39//! ```
40
41use std::{
42 collections::{HashMap, VecDeque},
43 time::{Duration, Instant},
44};
45
46use crate::PxPosition;
47
48/// Maximum number of events to keep in the queue to prevent memory issues during UI jank.
49const KEEP_EVENTS_COUNT: usize = 10;
50
51/// Controls how quickly inertial scrolling decelerates (higher = faster slowdown).
52const INERTIA_DECAY_CONSTANT: f32 = 5.0;
53
54/// Minimum velocity threshold below which inertial scrolling stops (pixels per second).
55const MIN_INERTIA_VELOCITY: f32 = 10.0;
56
57/// Minimum velocity from a gesture required to start inertial scrolling (pixels per second).
58const INERTIA_MIN_VELOCITY_THRESHOLD_FOR_START: f32 = 50.0;
59
60/// Multiplier applied to initial inertial velocity (typically 1.0 for natural feel).
61const INERTIA_MOMENTUM_FACTOR: f32 = 1.0;
62
63/// Maximum inertial velocity to keep flicks controllable (pixels per second).
64const MAX_INERTIA_VELOCITY: f32 = 6000.0;
65
66/// Tracks the state of a single touch point for gesture recognition and velocity calculation.
67///
68/// This struct maintains the necessary information to track touch movement, calculate
69/// velocities, and determine when to trigger inertial scrolling.
70///
71/// # Example
72///
73/// ```rust,ignore
74/// let touch_state = TouchPointState {
75/// last_position: PxPosition::new(100.0, 200.0),
76/// last_update_time: Instant::now(),
77/// velocity_tracker: VelocityTracker::new(Instant::now()),
78/// };
79/// ```
80#[derive(Debug, Clone)]
81struct TouchPointState {
82 /// The last recorded position of this touch point.
83 last_position: PxPosition,
84 /// Timestamp of the last position update.
85 last_update_time: Instant,
86 /// Tracks recent velocity samples and temporal metadata for momentum calculation.
87 velocity_tracker: VelocityTracker,
88 /// Tracks whether this touch gesture generated a scroll event.
89 ///
90 /// When set, the gesture should be treated as a drag/scroll rather than a tap.
91 generated_scroll_event: bool,
92}
93
94/// Maintains a short window of velocity samples for inertia calculations.
95#[derive(Debug, Clone)]
96struct VelocityTracker {
97 samples: VecDeque<(Instant, f32, f32)>,
98 last_sample_time: Instant,
99}
100
101const VELOCITY_SAMPLE_WINDOW: Duration = Duration::from_millis(90);
102const VELOCITY_IDLE_CUTOFF: Duration = Duration::from_millis(65);
103
104/// Represents an active inertial scrolling session.
105///
106/// When a touch gesture ends with sufficient velocity, this struct tracks
107/// the momentum and gradually decelerates the scroll movement over time.
108///
109/// # Example
110///
111/// ```rust,ignore
112/// let inertia = ActiveInertia {
113/// velocity_x: 200.0, // pixels per second
114/// velocity_y: -150.0, // pixels per second
115/// last_tick_time: Instant::now(),
116/// };
117/// ```
118#[derive(Debug, Clone)]
119struct ActiveInertia {
120 /// Current horizontal velocity in pixels per second.
121 velocity_x: f32,
122 /// Current vertical velocity in pixels per second.
123 velocity_y: f32,
124 /// Timestamp of the last inertia calculation update.
125 last_tick_time: Instant,
126}
127
128fn clamp_inertia_velocity(vx: f32, vy: f32) -> (f32, f32) {
129 if !vx.is_finite() || !vy.is_finite() {
130 return (0.0, 0.0);
131 }
132
133 let magnitude_sq = vx * vx + vy * vy;
134 if !magnitude_sq.is_finite() {
135 return (0.0, 0.0);
136 }
137
138 let magnitude = magnitude_sq.sqrt();
139 if magnitude > MAX_INERTIA_VELOCITY && MAX_INERTIA_VELOCITY > 0.0 {
140 let scale = MAX_INERTIA_VELOCITY / magnitude;
141 return (vx * scale, vy * scale);
142 }
143
144 (vx, vy)
145}
146
147/// Configuration settings for touch scrolling behavior.
148///
149/// This struct controls various aspects of how touch gestures are interpreted
150/// and converted into scroll events.
151///
152/// # Example
153///
154/// ```rust,ignore
155/// let config = TouchScrollConfig {
156/// min_move_threshold: 3.0, // More sensitive
157/// enabled: true,
158/// };
159/// ```
160#[derive(Debug, Clone)]
161struct TouchScrollConfig {
162 /// Minimum movement distance in pixels required to trigger a scroll event.
163 ///
164 /// Smaller values make scrolling more sensitive but may cause jitter.
165 /// Larger values require more deliberate movement but provide stability.
166 min_move_threshold: f32,
167 /// Whether touch scrolling is currently enabled.
168 enabled: bool,
169}
170
171impl Default for TouchScrollConfig {
172 fn default() -> Self {
173 Self {
174 // Reduced threshold for more responsive touch
175 min_move_threshold: 5.0,
176 enabled: true,
177 }
178 }
179}
180
181/// Central state manager for cursor and touch interactions.
182///
183/// `CursorState` is the main interface for handling all cursor-related events in the Tessera
184/// UI framework. It manages cursor position tracking, event queuing, multi-touch support,
185/// and provides smooth inertial scrolling for touch gestures.
186///
187/// # Key Responsibilities
188///
189/// - **Position Tracking**: Maintains current cursor/touch position
190/// - **Event Management**: Queues and processes cursor events with bounded storage
191/// - **Multi-touch Support**: Tracks multiple simultaneous touch points
192/// - **Inertial Scrolling**: Provides momentum-based scrolling after touch gestures
193/// - **Cross-platform Input**: Handles both mouse and touch events uniformly
194///
195/// # Usage
196///
197/// ```rust,ignore
198/// use tessera_ui::cursor::{CursorState, CursorEventContent};
199/// use tessera_ui::PxPosition;
200///
201/// let mut cursor_state = CursorState::default();
202///
203/// // Handle a touch gesture
204/// cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
205/// cursor_state.handle_touch_move(0, PxPosition::new(110.0, 190.0));
206/// cursor_state.handle_touch_end(0);
207///
208/// // Process accumulated events
209/// let events = cursor_state.take_events();
210/// for event in events {
211/// match event.content {
212/// CursorEventContent::Pressed(_) => println!("Touch started"),
213/// CursorEventContent::Scroll(scroll) => {
214/// println!("Scrolling: dx={}, dy={}", scroll.delta_x, scroll.delta_y);
215/// }
216/// CursorEventContent::Released(_) => println!("Touch ended"),
217/// }
218/// }
219/// ```
220///
221/// # Thread Safety
222///
223/// `CursorState` is not thread-safe and should be used from a single thread,
224/// typically the main UI thread where input events are processed.
225#[derive(Default)]
226pub struct CursorState {
227 /// Current cursor position, if any cursor is active.
228 position: Option<PxPosition>,
229 /// Bounded queue of cursor events awaiting processing.
230 events: VecDeque<CursorEvent>,
231 /// Active touch points mapped by their unique touch IDs.
232 touch_points: HashMap<u64, TouchPointState>,
233 /// Configuration settings for touch scrolling behavior.
234 touch_scroll_config: TouchScrollConfig,
235 /// Current inertial scrolling state, if active.
236 active_inertia: Option<ActiveInertia>,
237 /// If true, the cursor position will be cleared on the next frame.
238 clear_position_on_next_frame: bool,
239}
240
241impl CursorState {
242 /// Cleans up the cursor state at the end of a frame.
243 pub(crate) fn frame_cleanup(&mut self) {
244 if self.clear_position_on_next_frame {
245 self.update_position(None);
246 self.clear_position_on_next_frame = false;
247 }
248 }
249
250 /// Adds a cursor event to the processing queue.
251 ///
252 /// Events are stored in a bounded queue to prevent memory issues during UI performance
253 /// problems. If the queue exceeds [`KEEP_EVENTS_COUNT`], the oldest events are discarded.
254 ///
255 /// # Arguments
256 ///
257 /// * `event` - The cursor event to add to the queue
258 ///
259 /// # Example
260 ///
261 /// ```rust,ignore
262 /// use tessera_ui::cursor::{
263 /// CursorState, CursorEvent, CursorEventContent, GestureState, PressKeyEventType,
264 /// };
265 /// use std::time::Instant;
266 ///
267 /// let mut cursor_state = CursorState::default();
268 /// let event = CursorEvent {
269 /// timestamp: Instant::now(),
270 /// content: CursorEventContent::Pressed(PressKeyEventType::Left),
271 /// gesture_state: GestureState::TapCandidate,
272 /// };
273 /// cursor_state.push_event(event);
274 /// ```
275 pub fn push_event(&mut self, event: CursorEvent) {
276 self.events.push_back(event);
277
278 // Maintain bounded queue size to prevent memory issues during UI jank
279 if self.events.len() > KEEP_EVENTS_COUNT {
280 self.events.pop_front();
281 }
282 }
283
284 /// Updates the current cursor position.
285 ///
286 /// This method accepts any type that can be converted into `Option<PxPosition>`,
287 /// allowing for flexible position updates including clearing the position by
288 /// passing `None`.
289 ///
290 /// # Arguments
291 ///
292 /// * `position` - New cursor position or `None` to clear the position
293 ///
294 /// # Example
295 ///
296 /// ```rust,ignore
297 /// use tessera_ui::cursor::CursorState;
298 /// use tessera_ui::PxPosition;
299 ///
300 /// let mut cursor_state = CursorState::default();
301 ///
302 /// // Set position
303 /// cursor_state.update_position(PxPosition::new(100.0, 200.0));
304 ///
305 /// // Clear position
306 /// cursor_state.update_position(None);
307 /// ```
308 pub fn update_position(&mut self, position: impl Into<Option<PxPosition>>) {
309 self.position = position.into();
310 }
311
312 /// Processes active inertial scrolling and generates scroll events.
313 ///
314 /// This method is called internally to update inertial scrolling state and generate
315 /// appropriate scroll events. It handles velocity decay over time and stops inertia
316 /// when velocity falls below the minimum threshold.
317 ///
318 /// The method calculates scroll deltas based on current velocity and elapsed time,
319 /// applies exponential decay to the velocity, and queues scroll events for processing.
320 ///
321 /// # Implementation Details
322 ///
323 /// - Uses exponential decay with [`INERTIA_DECAY_CONSTANT`] for natural deceleration
324 /// - Stops inertia when velocity drops below [`MIN_INERTIA_VELOCITY`]
325 /// - Generates scroll events with calculated position deltas
326 /// - Handles edge cases like zero delta time gracefully
327 fn process_and_queue_inertial_scroll(&mut self) {
328 // Handle active inertia with clear, small responsibilities.
329 if let Some(mut inertia) = self.active_inertia.take() {
330 let now = Instant::now();
331 let delta_time = now.duration_since(inertia.last_tick_time).as_secs_f32();
332
333 if delta_time <= 0.0 {
334 // Called multiple times in the same instant; reinsert for next frame.
335 self.active_inertia = Some(inertia);
336 return;
337 }
338
339 // Compute scroll delta and emit event if meaningful.
340 let scroll_delta_x = inertia.velocity_x * delta_time;
341 let scroll_delta_y = inertia.velocity_y * delta_time;
342 if scroll_delta_x.abs() > 0.01 || scroll_delta_y.abs() > 0.01 {
343 self.push_scroll_event(now, scroll_delta_x, scroll_delta_y);
344 }
345
346 // Apply exponential decay to velocities.
347 let decay = (-INERTIA_DECAY_CONSTANT * delta_time).exp();
348 inertia.velocity_x *= decay;
349 inertia.velocity_y *= decay;
350 inertia.last_tick_time = now;
351
352 // Reinsert inertia only if still above threshold.
353 if inertia.velocity_x.abs() >= MIN_INERTIA_VELOCITY
354 || inertia.velocity_y.abs() >= MIN_INERTIA_VELOCITY
355 {
356 self.active_inertia = Some(inertia);
357 }
358 }
359 }
360
361 // Helper: push a scroll event with consistent construction.
362 fn push_scroll_event(&mut self, timestamp: Instant, dx: f32, dy: f32) {
363 self.push_event(CursorEvent {
364 timestamp,
365 content: CursorEventContent::Scroll(ScrollEventConent {
366 delta_x: dx,
367 delta_y: dy,
368 }),
369 gesture_state: GestureState::Dragged,
370 });
371 }
372
373 /// Retrieves and clears all pending cursor events.
374 ///
375 /// This method processes any active inertial scrolling, then returns all queued
376 /// cursor events and clears the internal event queue. Events are returned in
377 /// chronological order (oldest first).
378 ///
379 /// This is typically called once per frame by the UI framework to process
380 /// all accumulated input events.
381 ///
382 /// # Returns
383 ///
384 /// A vector of [`CursorEvent`]s ordered from oldest to newest.
385 ///
386 /// # Example
387 ///
388 /// ```rust,ignore
389 /// use tessera_ui::cursor::CursorState;
390 ///
391 /// let mut cursor_state = CursorState::default();
392 ///
393 /// // ... handle some input events ...
394 ///
395 /// // Process all events at once
396 /// let events = cursor_state.take_events();
397 /// for event in events {
398 /// println!("Event at {:?}: {:?}", event.timestamp, event.content);
399 /// }
400 /// ```
401 ///
402 /// # Note
403 ///
404 /// Events are ordered from oldest to newest to ensure proper event processing order.
405 pub fn take_events(&mut self) -> Vec<CursorEvent> {
406 self.process_and_queue_inertial_scroll();
407 self.events.drain(..).collect()
408 }
409
410 /// Clears all cursor state and pending events.
411 ///
412 /// This method resets the cursor state to its initial condition by:
413 /// - Clearing all queued events
414 /// - Removing cursor position information
415 /// - Stopping any active inertial scrolling
416 /// - Clearing all touch point tracking
417 ///
418 /// This is typically used when the UI context changes significantly,
419 /// such as when switching between different UI screens or when input
420 /// focus changes.
421 ///
422 /// # Example
423 ///
424 /// ```rust,ignore
425 /// use tessera_ui::cursor::CursorState;
426 ///
427 /// let mut cursor_state = CursorState::default();
428 ///
429 /// // ... handle various input events ...
430 ///
431 /// // Reset everything when changing UI context
432 /// cursor_state.clear();
433 /// ```
434 pub fn clear(&mut self) {
435 self.events.clear();
436 self.update_position(None);
437 self.active_inertia = None;
438 self.touch_points.clear();
439 self.clear_position_on_next_frame = false;
440 }
441
442 /// Returns the current cursor position, if any.
443 ///
444 /// The position represents the last known location of the cursor or active touch point.
445 /// Returns `None` if no cursor is currently active or if the position has been cleared.
446 ///
447 /// # Returns
448 ///
449 /// - `Some(PxPosition)` if a cursor position is currently tracked
450 /// - `None` if no cursor is active
451 ///
452 /// # Example
453 ///
454 /// ```rust,ignore
455 /// use tessera_ui::cursor::CursorState;
456 /// use tessera_ui::PxPosition;
457 ///
458 /// let mut cursor_state = CursorState::default();
459 ///
460 /// // Initially no position
461 /// assert_eq!(cursor_state.position(), None);
462 ///
463 /// // After setting position
464 /// cursor_state.update_position(PxPosition::new(100.0, 200.0));
465 /// assert_eq!(cursor_state.position(), Some(PxPosition::new(100.0, 200.0)));
466 /// ```
467 pub fn position(&self) -> Option<PxPosition> {
468 self.position
469 }
470
471 /// Handles the start of a touch gesture.
472 ///
473 /// This method registers a new touch point and generates a press event. It also
474 /// stops any active inertial scrolling since a new touch interaction has begun.
475 ///
476 /// # Arguments
477 ///
478 /// * `touch_id` - Unique identifier for this touch point
479 /// * `position` - Initial position of the touch in pixel coordinates
480 ///
481 /// # Example
482 ///
483 /// ```rust,ignore
484 /// use tessera_ui::cursor::CursorState;
485 /// use tessera_ui::PxPosition;
486 ///
487 /// let mut cursor_state = CursorState::default();
488 /// cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
489 ///
490 /// // This generates a Pressed event and updates the cursor position
491 /// let events = cursor_state.take_events();
492 /// assert_eq!(events.len(), 1);
493 /// ```
494 pub fn handle_touch_start(&mut self, touch_id: u64, position: PxPosition) {
495 self.active_inertia = None; // Stop any existing inertia on new touch
496 let now = Instant::now();
497
498 self.touch_points.insert(
499 touch_id,
500 TouchPointState {
501 last_position: position,
502 last_update_time: now,
503 velocity_tracker: VelocityTracker::new(now),
504 generated_scroll_event: false,
505 },
506 );
507 self.update_position(position);
508 let press_event = CursorEvent {
509 timestamp: now,
510 content: CursorEventContent::Pressed(PressKeyEventType::Left),
511 gesture_state: GestureState::TapCandidate,
512 };
513 self.push_event(press_event);
514 }
515
516 /// Handles touch movement and generates scroll events when appropriate.
517 ///
518 /// This method tracks touch movement, calculates velocities for inertial scrolling,
519 /// and generates scroll events when the movement exceeds the minimum threshold.
520 /// It also maintains a velocity history for momentum calculation.
521 ///
522 /// # Arguments
523 ///
524 /// * `touch_id` - Unique identifier for the touch point being moved
525 /// * `current_position` - New position of the touch in pixel coordinates
526 ///
527 /// # Returns
528 ///
529 /// - `Some(CursorEvent)` containing a scroll event if movement exceeds threshold
530 /// - `None` if movement is below threshold or touch scrolling is disabled
531 ///
532 /// # Example
533 ///
534 /// ```rust,ignore
535 /// use tessera_ui::cursor::CursorState;
536 /// use tessera_ui::PxPosition;
537 ///
538 /// let mut cursor_state = CursorState::default();
539 /// cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
540 ///
541 /// // Move touch point - may generate scroll event
542 /// if let Some(scroll_event) = cursor_state.handle_touch_move(0, PxPosition::new(110.0, 190.0)) {
543 /// println!("Scroll detected!");
544 /// }
545 /// ```
546 pub fn handle_touch_move(
547 &mut self,
548 touch_id: u64,
549 current_position: PxPosition,
550 ) -> Option<CursorEvent> {
551 let now = Instant::now();
552 self.update_position(current_position);
553
554 if !self.touch_scroll_config.enabled {
555 return None;
556 }
557
558 if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
559 let delta_x = (current_position.x - touch_state.last_position.x).to_f32();
560 let delta_y = (current_position.y - touch_state.last_position.y).to_f32();
561 let move_distance = (delta_x * delta_x + delta_y * delta_y).sqrt();
562 let time_delta = now
563 .duration_since(touch_state.last_update_time)
564 .as_secs_f32();
565
566 touch_state.last_position = current_position;
567 touch_state.last_update_time = now;
568
569 if move_distance >= self.touch_scroll_config.min_move_threshold {
570 // Stop any active inertia when user actively moves the touch.
571 self.active_inertia = None;
572
573 if time_delta > 0.0 {
574 let velocity_x = delta_x / time_delta;
575 let velocity_y = delta_y / time_delta;
576 touch_state
577 .velocity_tracker
578 .push(now, velocity_x, velocity_y);
579 }
580
581 touch_state.generated_scroll_event = true;
582
583 // Return a scroll event for immediate feedback.
584 return Some(CursorEvent {
585 timestamp: now,
586 content: CursorEventContent::Scroll(ScrollEventConent {
587 delta_x, // Direct scroll delta for touch move
588 delta_y,
589 }),
590 gesture_state: GestureState::Dragged,
591 });
592 }
593 }
594 None
595 }
596
597 /// Handles the end of a touch gesture and potentially starts inertial scrolling.
598 ///
599 /// This method processes the end of a touch interaction by:
600 /// - Calculating average velocity from recent touch movement
601 /// - Starting inertial scrolling if velocity exceeds the threshold
602 /// - Generating a release event
603 /// - Cleaning up touch point tracking
604 ///
605 /// # Arguments
606 ///
607 /// * `touch_id` - Unique identifier for the touch point that ended
608 ///
609 /// # Example
610 ///
611 /// ```rust,ignore
612 /// use tessera_ui::cursor::CursorState;
613 /// use tessera_ui::PxPosition;
614 ///
615 /// let mut cursor_state = CursorState::default();
616 /// cursor_state.handle_touch_start(0, PxPosition::new(100.0, 200.0));
617 /// cursor_state.handle_touch_move(0, PxPosition::new(150.0, 180.0));
618 /// cursor_state.handle_touch_end(0);
619 ///
620 /// // May start inertial scrolling based on gesture velocity
621 /// let events = cursor_state.take_events();
622 /// // Events may include scroll events from inertia
623 /// ```
624 pub fn handle_touch_end(&mut self, touch_id: u64) {
625 let now = Instant::now();
626 let mut was_drag = false;
627
628 if let Some(touch_state) = self.touch_points.get_mut(&touch_id) {
629 was_drag |= touch_state.generated_scroll_event;
630 if self.touch_scroll_config.enabled {
631 if let Some((avg_vx, avg_vy)) = touch_state.velocity_tracker.resolve(now) {
632 let velocity_magnitude = (avg_vx * avg_vx + avg_vy * avg_vy).sqrt();
633 if velocity_magnitude > INERTIA_MIN_VELOCITY_THRESHOLD_FOR_START {
634 let (inertia_vx, inertia_vy) = clamp_inertia_velocity(
635 avg_vx * INERTIA_MOMENTUM_FACTOR,
636 avg_vy * INERTIA_MOMENTUM_FACTOR,
637 );
638 self.active_inertia = Some(ActiveInertia {
639 velocity_x: inertia_vx,
640 velocity_y: inertia_vy,
641 last_tick_time: now,
642 });
643 } else {
644 self.active_inertia = None;
645 }
646 } else {
647 self.active_inertia = None;
648 }
649 } else {
650 self.active_inertia = None; // Scrolling disabled
651 }
652 } else {
653 self.active_inertia = None; // No touch state present
654 }
655
656 if self.active_inertia.is_some() {
657 was_drag = true;
658 }
659
660 self.touch_points.remove(&touch_id);
661 let release_event = CursorEvent {
662 timestamp: now,
663 content: CursorEventContent::Released(PressKeyEventType::Left),
664 gesture_state: if was_drag {
665 GestureState::Dragged
666 } else {
667 GestureState::TapCandidate
668 },
669 };
670 self.push_event(release_event);
671
672 if self.touch_points.is_empty() && self.active_inertia.is_none() {
673 self.clear_position_on_next_frame = true;
674 }
675 }
676}
677
678impl VelocityTracker {
679 fn new(now: Instant) -> Self {
680 Self {
681 samples: VecDeque::new(),
682 last_sample_time: now,
683 }
684 }
685
686 fn push(&mut self, now: Instant, vx: f32, vy: f32) {
687 let (vx, vy) = clamp_inertia_velocity(vx, vy);
688 self.samples.push_back((now, vx, vy));
689 self.last_sample_time = now;
690 self.prune(now);
691 }
692
693 fn resolve(&mut self, now: Instant) -> Option<(f32, f32)> {
694 self.prune(now);
695
696 if self.samples.is_empty() {
697 return None;
698 }
699
700 let idle_time = now.duration_since(self.last_sample_time);
701 if idle_time >= VELOCITY_IDLE_CUTOFF {
702 self.samples.clear();
703 return None;
704 }
705
706 let mut weighted_sum_x = 0.0f32;
707 let mut weighted_sum_y = 0.0f32;
708 let mut total_weight = 0.0f32;
709 let window_secs = VELOCITY_SAMPLE_WINDOW.as_secs_f32().max(f32::EPSILON);
710
711 for &(timestamp, vx, vy) in &self.samples {
712 let age_secs = now
713 .duration_since(timestamp)
714 .as_secs_f32()
715 .clamp(0.0, window_secs);
716 let weight = (window_secs - age_secs).max(0.0);
717 if weight > 0.0 {
718 weighted_sum_x += vx * weight;
719 weighted_sum_y += vy * weight;
720 total_weight += weight;
721 }
722 }
723
724 if total_weight <= f32::EPSILON {
725 self.samples.clear();
726 return None;
727 }
728
729 let avg_x = weighted_sum_x / total_weight;
730 let avg_y = weighted_sum_y / total_weight;
731
732 let damping = 1.0 - idle_time.as_secs_f32() / VELOCITY_IDLE_CUTOFF.as_secs_f32();
733 let damping = damping.clamp(0.0, 1.0);
734 let (avg_x, avg_y) = clamp_inertia_velocity(avg_x * damping, avg_y * damping);
735
736 Some((avg_x, avg_y))
737 }
738
739 fn prune(&mut self, now: Instant) {
740 while let Some(&(timestamp, _, _)) = self.samples.front() {
741 if now.duration_since(timestamp) > VELOCITY_SAMPLE_WINDOW {
742 self.samples.pop_front();
743 } else {
744 break;
745 }
746 }
747 }
748}
749
750/// Represents a single cursor or touch event with timing information.
751///
752/// `CursorEvent` encapsulates all types of cursor interactions including presses,
753/// releases, and scroll actions. Each event includes a timestamp for precise
754/// timing and ordering of input events.
755///
756/// # Example
757///
758/// ```rust,ignore
759/// use tessera_ui::cursor::{CursorEvent, CursorEventContent, PressKeyEventType};
760/// use std::time::Instant;
761///
762/// let event = CursorEvent {
763/// timestamp: Instant::now(),
764/// content: CursorEventContent::Pressed(PressKeyEventType::Left),
765/// };
766///
767/// match event.content {
768/// CursorEventContent::Pressed(button) => println!("Button pressed: {:?}", button),
769/// CursorEventContent::Released(button) => println!("Button released: {:?}", button),
770/// CursorEventContent::Scroll(scroll) => {
771/// println!("Scroll: dx={}, dy={}", scroll.delta_x, scroll.delta_y);
772/// }
773/// }
774/// ```
775#[derive(Debug, Clone)]
776pub struct CursorEvent {
777 /// Timestamp indicating when this event occurred.
778 pub timestamp: Instant,
779 /// The specific type and data of this cursor event.
780 pub content: CursorEventContent,
781 /// Classification of the gesture associated with this event.
782 ///
783 /// Events originating from touch scrolling will mark this as [`GestureState::Dragged`],
784 /// allowing downstream components to distinguish tap candidates from scroll gestures.
785 pub gesture_state: GestureState,
786}
787
788/// Contains scroll movement data for scroll events.
789///
790/// `ScrollEventConent` represents the amount of scrolling that occurred,
791/// with positive values typically indicating rightward/downward movement
792/// and negative values indicating leftward/upward movement.
793///
794/// # Example
795///
796/// ```rust,ignore
797/// use tessera_ui::cursor::ScrollEventConent;
798///
799/// let scroll = ScrollEventConent {
800/// delta_x: 10.0, // Scroll right 10 pixels
801/// delta_y: -20.0, // Scroll up 20 pixels
802/// };
803///
804/// println!("Horizontal scroll: {}", scroll.delta_x);
805/// println!("Vertical scroll: {}", scroll.delta_y);
806/// ```
807#[derive(Debug, Clone, PartialEq)]
808pub struct ScrollEventConent {
809 /// Horizontal scroll distance in pixels.
810 ///
811 /// Positive values indicate rightward scrolling,
812 /// negative values indicate leftward scrolling.
813 pub delta_x: f32,
814 /// Vertical scroll distance in pixels.
815 ///
816 /// Positive values indicate downward scrolling,
817 /// negative values indicate upward scrolling.
818 pub delta_y: f32,
819}
820
821/// Enumeration of all possible cursor event types.
822///
823/// `CursorEventContent` represents the different kinds of interactions
824/// that can occur with cursor or touch input, including button presses,
825/// releases, and scroll actions.
826///
827/// # Example
828///
829/// ```rust,ignore
830/// use tessera_ui::cursor::{CursorEventContent, PressKeyEventType, ScrollEventConent};
831///
832/// // Handle different event types
833/// match event_content {
834/// CursorEventContent::Pressed(PressKeyEventType::Left) => {
835/// println!("Left button pressed");
836/// }
837/// CursorEventContent::Released(PressKeyEventType::Right) => {
838/// println!("Right button released");
839/// }
840/// CursorEventContent::Scroll(scroll) => {
841/// println!("Scrolled by ({}, {})", scroll.delta_x, scroll.delta_y);
842/// }
843/// }
844/// ```
845#[derive(Debug, Clone, PartialEq)]
846pub enum CursorEventContent {
847 /// A cursor button or touch point was pressed.
848 Pressed(PressKeyEventType),
849 /// A cursor button or touch point was released.
850 Released(PressKeyEventType),
851 /// A scroll action occurred (mouse wheel, touch drag, or inertial scroll).
852 Scroll(ScrollEventConent),
853}
854
855/// Describes the high-level gesture classification of a cursor event.
856#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
857pub enum GestureState {
858 /// Indicates the event is part of a potential tap/click interaction.
859 #[default]
860 TapCandidate,
861 /// Indicates the event happened during a drag/scroll gesture.
862 Dragged,
863}
864
865impl CursorEventContent {
866 /// Creates a cursor press/release event from winit mouse button events.
867 ///
868 /// This method converts winit's mouse button events into Tessera's cursor event format.
869 /// It handles the three standard mouse buttons (left, right, middle) and ignores
870 /// any additional buttons that may be present on some mice.
871 ///
872 /// # Arguments
873 ///
874 /// * `state` - Whether the button was pressed or released
875 /// * `button` - Which mouse button was affected
876 ///
877 /// # Returns
878 ///
879 /// - `Some(CursorEventContent)` for supported mouse buttons
880 /// - `None` for unsupported mouse buttons
881 ///
882 /// # Example
883 ///
884 /// ```rust,ignore
885 /// use tessera_ui::cursor::CursorEventContent;
886 /// use winit::event::{ElementState, MouseButton};
887 ///
888 /// let press_event = CursorEventContent::from_press_event(
889 /// ElementState::Pressed,
890 /// MouseButton::Left
891 /// );
892 ///
893 /// if let Some(event) = press_event {
894 /// println!("Created cursor event: {:?}", event);
895 /// }
896 /// ```
897 pub fn from_press_event(
898 state: winit::event::ElementState,
899 button: winit::event::MouseButton,
900 ) -> Option<Self> {
901 let event_type = match button {
902 winit::event::MouseButton::Left => PressKeyEventType::Left,
903 winit::event::MouseButton::Right => PressKeyEventType::Right,
904 winit::event::MouseButton::Middle => PressKeyEventType::Middle,
905 _ => return None, // Ignore other buttons
906 };
907 let state = match state {
908 winit::event::ElementState::Pressed => Self::Pressed(event_type),
909 winit::event::ElementState::Released => Self::Released(event_type),
910 };
911 Some(state)
912 }
913
914 /// Creates a scroll event from winit mouse wheel events.
915 ///
916 /// This method converts winit's mouse scroll delta into Tessera's scroll event format.
917 /// It handles both line-based scrolling (typical mouse wheels) and pixel-based
918 /// scrolling (trackpads, precision mice) by applying appropriate scaling.
919 ///
920 /// # Arguments
921 ///
922 /// * `delta` - The scroll delta from winit
923 ///
924 /// # Returns
925 ///
926 /// A `CursorEventContent::Scroll` event with scaled delta values.
927 ///
928 /// # Example
929 ///
930 /// ```rust,ignore
931 /// use tessera_ui::cursor::CursorEventContent;
932 /// use winit::event::MouseScrollDelta;
933 ///
934 /// let scroll_event = CursorEventContent::from_scroll_event(
935 /// MouseScrollDelta::LineDelta(0.0, 1.0) // Scroll down one line
936 /// );
937 ///
938 /// match scroll_event {
939 /// CursorEventContent::Scroll(scroll) => {
940 /// println!("Scroll delta: ({}, {})", scroll.delta_x, scroll.delta_y);
941 /// }
942 /// _ => {}
943 /// }
944 /// ```
945 pub fn from_scroll_event(delta: winit::event::MouseScrollDelta) -> Self {
946 let (delta_x, delta_y) = match delta {
947 winit::event::MouseScrollDelta::LineDelta(x, y) => (x, y),
948 winit::event::MouseScrollDelta::PixelDelta(delta) => (delta.x as f32, delta.y as f32),
949 };
950
951 const MOUSE_WHEEL_SPEED_MULTIPLIER: f32 = 50.0;
952 Self::Scroll(ScrollEventConent {
953 delta_x: delta_x * MOUSE_WHEEL_SPEED_MULTIPLIER,
954 delta_y: delta_y * MOUSE_WHEEL_SPEED_MULTIPLIER,
955 })
956 }
957}
958
959/// Represents the different types of cursor buttons or touch interactions.
960///
961/// `PressKeyEventType` identifies which button was pressed or released in
962/// a cursor event. This covers the three standard mouse buttons that are
963/// commonly supported across different platforms and input devices.
964///
965/// # Example
966///
967/// ```rust,ignore
968/// use tessera_ui::cursor::PressKeyEventType;
969///
970/// match button_type {
971/// PressKeyEventType::Left => println!("Primary button (usually left-click)"),
972/// PressKeyEventType::Right => println!("Secondary button (usually right-click)"),
973/// PressKeyEventType::Middle => println!("Middle button (usually scroll wheel click)"),
974/// }
975/// ```
976#[derive(Debug, Clone, PartialEq, Eq)]
977pub enum PressKeyEventType {
978 /// The primary mouse button (typically left button) or primary touch.
979 Left,
980 /// The secondary mouse button (typically right button).
981 Right,
982 /// The middle mouse button (typically scroll wheel click).
983 Middle,
984}