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