1use std::collections::{HashMap, VecDeque};
9
10use crate::{PxPosition, time::Instant};
11
12pub type PointerId = u64;
14pub const MOUSE_POINTER_ID: PointerId = 0;
16
17const KEEP_EVENTS_COUNT: usize = 128;
20
21#[derive(Debug, Clone)]
22struct TouchPointState {
23 pointer_id: PointerId,
24 last_position: PxPosition,
25 start_position: PxPosition,
26 last_update_time: Instant,
27 generated_scroll_event: bool,
28}
29
30#[derive(Debug, Clone)]
31struct TouchScrollConfig {
32 min_move_threshold: f32,
33 enabled: bool,
34}
35
36impl Default for TouchScrollConfig {
37 fn default() -> Self {
38 Self {
39 min_move_threshold: 5.0,
41 enabled: true,
42 }
43 }
44}
45
46#[derive(Default)]
52pub struct CursorState {
53 next_touch_pointer_id: PointerId,
54 position: Option<PxPosition>,
56 pointer_positions: HashMap<PointerId, Option<PxPosition>>,
57 events: VecDeque<PointerChange>,
59 touch_points: HashMap<u64, TouchPointState>,
61 touch_scroll_config: TouchScrollConfig,
63 clear_position_on_next_frame: bool,
65}
66
67impl CursorState {
68 pub(crate) fn frame_cleanup(&mut self) {
70 if self.clear_position_on_next_frame {
71 self.position = None;
72 self.clear_position_on_next_frame = false;
73 }
74 }
75
76 pub fn push_event(&mut self, mut event: PointerChange) {
86 if event.position_snapshot().is_none() {
87 let position = self
88 .pointer_positions
89 .get(&event.pointer_id)
90 .copied()
91 .unwrap_or_else(|| {
92 if event.pointer_id == MOUSE_POINTER_ID {
93 None
94 } else {
95 self.position
96 }
97 });
98 event = event.with_position(position);
99 self.position = position;
100 }
101 self.events.push_back(event);
102 while self.events.len() > KEEP_EVENTS_COUNT {
103 let redundant = self.events.iter().enumerate().find_map(|(index, queued)| {
104 if !matches!(queued.content, CursorEventContent::Moved(_)) {
105 return None;
106 }
107 self.events
108 .iter()
109 .skip(index + 1)
110 .find(|next| next.pointer_id == queued.pointer_id)
111 .filter(|next| matches!(next.content, CursorEventContent::Moved(_)))
112 .map(|_| index)
113 });
114 if let Some(index) = redundant {
115 self.events.remove(index);
116 } else {
117 break;
118 }
119 }
120 }
121
122 pub fn update_position(&mut self, position: impl Into<Option<PxPosition>>) {
133 self.update_pointer_position(MOUSE_POINTER_ID, position);
134 }
135
136 pub fn update_pointer_position(
140 &mut self,
141 pointer_id: PointerId,
142 position: impl Into<Option<PxPosition>>,
143 ) {
144 let position = position.into();
145 self.pointer_positions.insert(pointer_id, position);
146 self.position = position;
147 }
148
149 pub fn take_events(&mut self) -> Vec<PointerChange> {
166 self.events.drain(..).collect()
167 }
168
169 pub fn clear(&mut self) {
175 self.events.clear();
176 self.pointer_positions.clear();
177 self.position = None;
178 self.touch_points.clear();
179 self.clear_position_on_next_frame = false;
180 }
181
182 pub fn position(&self) -> Option<PxPosition> {
193 self.position
194 }
195
196 pub fn handle_touch_start(&mut self, touch_id: u64, position: PxPosition) {
205 self.clear_position_on_next_frame = false;
206 let now = Instant::now();
207 let mut pointer_id = self.next_touch_pointer_id.wrapping_add(1);
208 while pointer_id == MOUSE_POINTER_ID
209 || self
210 .touch_points
211 .values()
212 .any(|touch| touch.pointer_id == pointer_id)
213 {
214 pointer_id = pointer_id.wrapping_add(1);
215 }
216 self.next_touch_pointer_id = pointer_id;
217
218 self.touch_points.insert(
219 touch_id,
220 TouchPointState {
221 pointer_id,
222 last_position: position,
223 start_position: position,
224 last_update_time: now,
225 generated_scroll_event: false,
226 },
227 );
228 self.update_pointer_position(pointer_id, position);
229 let press_event = PointerChange {
230 timestamp: now,
231 pointer_id,
232 content: CursorEventContent::Pressed(PressKeyEventType::Left),
233 gesture_state: GestureState::TapCandidate,
234 sampled_position: Default::default(),
235 consumed: false,
236 tap_claimed: false,
237 scroll_discrete: None,
238 };
239 self.push_event(press_event);
240 }
241
242 pub fn handle_touch_move(
258 &mut self,
259 touch_id: u64,
260 current_position: PxPosition,
261 ) -> Option<PointerChange> {
262 let now = Instant::now();
263 let touch_state = self.touch_points.get_mut(&touch_id)?;
264 let pointer_id = touch_state.pointer_id;
265 let mut delta_x = current_position.x - touch_state.last_position.x;
266 let mut delta_y = current_position.y - touch_state.last_position.y;
267 let cumulative_x = current_position.x - touch_state.start_position.x;
268 let cumulative_y = current_position.y - touch_state.start_position.y;
269 let distance = cumulative_x.hypot(cumulative_y);
270 if self.touch_scroll_config.enabled
271 && !touch_state.generated_scroll_event
272 && distance >= self.touch_scroll_config.min_move_threshold
273 {
274 touch_state.generated_scroll_event = true;
275 let beyond_slop = (distance - self.touch_scroll_config.min_move_threshold) / distance;
276 delta_x = cumulative_x * beyond_slop;
277 delta_y = cumulative_y * beyond_slop;
278 }
279 touch_state.last_position = current_position;
280 touch_state.last_update_time = now;
281 let dragging = touch_state.generated_scroll_event;
282 let gesture_state = if dragging {
283 GestureState::Dragged
284 } else {
285 GestureState::TapCandidate
286 };
287 self.update_pointer_position(pointer_id, current_position);
288 self.push_event(PointerChange {
289 timestamp: now,
290 pointer_id,
291 content: CursorEventContent::Moved(current_position),
292 gesture_state,
293 sampled_position: Default::default(),
294 consumed: false,
295 tap_claimed: false,
296 scroll_discrete: None,
297 });
298 (self.touch_scroll_config.enabled && dragging).then_some(PointerChange {
299 timestamp: now,
300 pointer_id,
301 content: CursorEventContent::Scroll(ScrollEventContent {
302 delta_x,
303 delta_y,
304 unit: ScrollDeltaUnit::Pixel,
305 source: ScrollEventSource::Touch,
306 }),
307 gesture_state,
308 sampled_position: PointerPosition::At(current_position),
309 consumed: false,
310 tap_claimed: false,
311 scroll_discrete: None,
312 })
313 }
314
315 pub fn handle_touch_end(&mut self, touch_id: u64) {
317 self.finish_touch(touch_id, false);
318 }
319
320 pub fn handle_touch_cancel(&mut self, touch_id: u64) {
322 self.finish_touch(touch_id, true);
323 }
324
325 fn finish_touch(&mut self, touch_id: u64, cancelled: bool) {
326 let now = Instant::now();
327 let touch_state = match self.touch_points.remove(&touch_id) {
328 Some(state) => state,
329 None => return,
330 };
331 let was_drag = touch_state.generated_scroll_event;
332 self.update_pointer_position(touch_state.pointer_id, touch_state.last_position);
333 self.push_event(PointerChange {
334 timestamp: now,
335 pointer_id: touch_state.pointer_id,
336 content: if cancelled {
337 CursorEventContent::Cancelled(PressKeyEventType::Left)
338 } else {
339 CursorEventContent::Released(PressKeyEventType::Left)
340 },
341 gesture_state: if cancelled || was_drag {
342 GestureState::Dragged
343 } else {
344 GestureState::TapCandidate
345 },
346 sampled_position: Default::default(),
347 consumed: false,
348 tap_claimed: false,
349 scroll_discrete: None,
350 });
351 self.pointer_positions.remove(&touch_state.pointer_id);
352 if self.touch_points.is_empty() {
353 self.clear_position_on_next_frame = true;
354 }
355 }
356}
357
358#[derive(Clone, Copy, Debug, Default)]
359pub(crate) enum PointerPosition {
360 #[default]
361 Unspecified,
362 At(PxPosition),
363 Unavailable,
364}
365
366#[derive(Debug, Clone)]
372pub struct PointerChange {
373 pub timestamp: Instant,
375 pub pointer_id: PointerId,
377 pub content: CursorEventContent,
379 pub gesture_state: GestureState,
385 pub(crate) consumed: bool,
387 pub(crate) tap_claimed: bool,
388 pub(crate) scroll_discrete: Option<bool>,
389 pub(crate) sampled_position: PointerPosition,
390}
391
392impl PointerChange {
393 pub fn new(
395 timestamp: Instant,
396 pointer_id: PointerId,
397 content: CursorEventContent,
398 gesture_state: GestureState,
399 ) -> Self {
400 Self {
401 timestamp,
402 pointer_id,
403 content,
404 gesture_state,
405 sampled_position: Default::default(),
406 consumed: false,
407 tap_claimed: false,
408 scroll_discrete: None,
409 }
410 }
411
412 pub fn with_position(mut self, position: impl Into<Option<PxPosition>>) -> Self {
417 self.sampled_position = match position.into() {
418 Some(position) => PointerPosition::At(position),
419 None => PointerPosition::Unavailable,
420 };
421 self
422 }
423
424 pub fn position(&self) -> Option<PxPosition> {
426 self.position_snapshot().flatten()
427 }
428
429 pub(crate) fn position_snapshot(&self) -> Option<Option<PxPosition>> {
430 if let CursorEventContent::Moved(position) = self.content {
431 return Some(Some(position));
432 }
433 match self.sampled_position {
434 PointerPosition::Unspecified => None,
435 PointerPosition::At(position) => Some(Some(position)),
436 PointerPosition::Unavailable => Some(None),
437 }
438 }
439
440 pub(crate) fn resolved_position(&self, fallback: Option<PxPosition>) -> Option<PxPosition> {
441 self.position_snapshot().unwrap_or(fallback)
442 }
443
444 pub fn is_discrete_scroll(&self) -> bool {
449 self.scroll_discrete.unwrap_or_else(|| {
450 matches!(&self.content, CursorEventContent::Scroll(event)
451 if crate::scroll::is_discrete_wheel(event))
452 })
453 }
454
455 pub fn consume(&mut self) {
457 self.consumed = true;
458 }
459
460 pub fn is_consumed(&self) -> bool {
462 self.consumed
463 }
464
465 pub fn claim_tap_press(&mut self) -> bool {
470 if self.tap_claimed || !matches!(self.content, CursorEventContent::Pressed(_)) {
471 return false;
472 }
473 self.tap_claimed = true;
474 true
475 }
476}
477
478#[derive(Debug, Clone, PartialEq)]
484pub struct ScrollEventContent {
485 pub delta_x: f32,
487 pub delta_y: f32,
489 pub unit: ScrollDeltaUnit,
491 pub source: ScrollEventSource,
493}
494
495impl ScrollEventContent {
496 pub fn pixel_delta(&self) -> (f32, f32) {
498 crate::normalize_platform_scroll_delta(self.delta_x, self.delta_y, self.unit, self.source)
499 }
500
501 pub fn normalize_to_pixels(&mut self) {
503 (self.delta_x, self.delta_y) = self.pixel_delta();
504 self.unit = ScrollDeltaUnit::Pixel;
505 }
506
507 pub(crate) fn from_wheel(delta: winit::event::MouseScrollDelta) -> Self {
508 let (delta_x, delta_y, unit) = match delta {
509 winit::event::MouseScrollDelta::LineDelta(x, y) => (x, y, ScrollDeltaUnit::Line),
510 winit::event::MouseScrollDelta::PixelDelta(delta) => {
511 (delta.x as f32, delta.y as f32, ScrollDeltaUnit::Pixel)
512 }
513 };
514 Self {
515 delta_x,
516 delta_y,
517 unit,
518 source: ScrollEventSource::Wheel,
519 }
520 }
521}
522
523#[derive(Debug, Clone, PartialEq)]
529pub enum CursorEventContent {
530 Moved(PxPosition),
532 Pressed(PressKeyEventType),
534 Released(PressKeyEventType),
536 Cancelled(PressKeyEventType),
538 Scroll(ScrollEventContent),
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
544pub enum GestureState {
545 #[default]
547 TapCandidate,
548 Dragged,
550}
551
552impl CursorEventContent {
553 pub fn from_press_event(
570 state: winit::event::ElementState,
571 button: winit::event::MouseButton,
572 ) -> Option<Self> {
573 let event_type = match button {
574 winit::event::MouseButton::Left => PressKeyEventType::Left,
575 winit::event::MouseButton::Right => PressKeyEventType::Right,
576 winit::event::MouseButton::Middle => PressKeyEventType::Middle,
577 _ => return None, };
579 let state = match state {
580 winit::event::ElementState::Pressed => Self::Pressed(event_type),
581 winit::event::ElementState::Released => Self::Released(event_type),
582 };
583 Some(state)
584 }
585
586 pub fn from_scroll_event(delta: winit::event::MouseScrollDelta) -> Self {
601 Self::Scroll(ScrollEventContent::from_wheel(delta))
602 }
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq)]
607pub enum PressKeyEventType {
608 Left,
610 Right,
612 Middle,
614}
615
616#[derive(Debug, Clone, Copy, PartialEq, Eq)]
618pub enum ScrollEventSource {
619 Touch,
621 Wheel,
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq)]
627pub enum ScrollDeltaUnit {
628 Line,
630 Pixel,
632}
633
634#[cfg(test)]
635mod queue_tests {
636 use super::*;
637 use crate::Px;
638 #[test]
639 fn long_frame_preserves_touch_boundaries_and_displacement() {
640 let _runtime_session = crate::RuntimeSession::new();
641 let _runtime_scope = _runtime_session.enter();
642 let mut cursor = CursorState::default();
643 cursor.handle_touch_start(1, PxPosition::ZERO);
644 for x in 1..=300 {
645 if let Some(scroll) = cursor.handle_touch_move(1, PxPosition::new(Px(x), Px(0))) {
646 cursor.push_event(scroll);
647 }
648 }
649 cursor.handle_touch_end(1);
650 let events = cursor.take_events();
651 assert!(matches!(events[0].content, CursorEventContent::Pressed(_)));
652 assert!(matches!(
653 events
654 .last()
655 .expect("touch end must enqueue a release")
656 .content,
657 CursorEventContent::Released(_)
658 ));
659 let distance: f32 = events
660 .iter()
661 .filter_map(|event| match &event.content {
662 CursorEventContent::Scroll(scroll) => Some(scroll.delta_x),
663 _ => None,
664 })
665 .sum();
666 assert!((distance - 295.0).abs() < 0.001);
667 }
668}
669#[cfg(test)]
670mod tests {
671 use super::{CursorEventContent, CursorState, GestureState, PxPosition};
672 use crate::Px;
673
674 fn position(x: i32) -> PxPosition {
675 PxPosition::new(Px::new(x), Px::ZERO)
676 }
677
678 #[test]
679 fn queued_samples_keep_their_original_positions_including_unavailable_positions() {
680 let _runtime_session = crate::RuntimeSession::new();
681 let _runtime_scope = _runtime_session.enter();
682 let mut cursor = CursorState::default();
683 let sample = |content| {
684 super::PointerChange::new(
685 crate::time::Instant::now(),
686 super::MOUSE_POINTER_ID,
687 content,
688 GestureState::TapCandidate,
689 )
690 };
691 cursor.update_position(position(10));
692 cursor.push_event(sample(CursorEventContent::Pressed(
693 super::PressKeyEventType::Left,
694 )));
695 cursor.update_position(position(20));
696 cursor.push_event(sample(CursorEventContent::Released(
697 super::PressKeyEventType::Left,
698 )));
699 cursor.update_position(None);
700 cursor.push_event(sample(CursorEventContent::Pressed(
701 super::PressKeyEventType::Left,
702 )));
703 cursor.update_position(position(30));
704 let events = cursor.take_events();
705 assert_eq!(
706 events
707 .iter()
708 .map(super::PointerChange::position)
709 .collect::<Vec<_>>(),
710 [Some(position(10)), Some(position(20)), None]
711 );
712 assert_eq!(events[2].resolved_position(Some(position(30))), None);
713 let legacy = sample(CursorEventContent::Pressed(super::PressKeyEventType::Left));
714 assert_eq!(
715 legacy.resolved_position(Some(position(30))),
716 Some(position(30))
717 );
718 }
719
720 #[test]
721 fn independent_pointers_and_delayed_touch_scroll_keep_their_own_positions() {
722 let _runtime_session = crate::RuntimeSession::new();
723 let _runtime_scope = _runtime_session.enter();
724 let mut cursor = CursorState::default();
725 cursor.update_position(position(100));
726 cursor.handle_touch_start(1, position(0));
727 cursor.handle_touch_start(2, position(20));
728 let scroll = cursor.handle_touch_move(1, position(10)).unwrap();
729 cursor.handle_touch_move(2, position(50));
730 cursor.push_event(scroll);
731 cursor.handle_touch_end(1);
732 cursor.push_event(super::PointerChange::new(
733 crate::time::Instant::now(),
734 super::MOUSE_POINTER_ID,
735 CursorEventContent::Pressed(super::PressKeyEventType::Left),
736 GestureState::TapCandidate,
737 ));
738 let events = cursor.take_events();
739 assert_eq!(events[0].position(), Some(position(0)));
740 assert_eq!(events[1].position(), Some(position(20)));
741 assert_eq!(events[4].position(), Some(position(10)));
742 assert_eq!(events[5].position(), Some(position(10)));
743 assert_eq!(events[6].position(), Some(position(100)));
744 }
745
746 #[test]
747 fn platform_touch_identifiers_never_alias_mouse_and_keep_their_pointer_identity() {
748 let _runtime_session = crate::RuntimeSession::new();
749 let _runtime_scope = _runtime_session.enter();
750 let mut cursor = CursorState::default();
751 cursor.handle_touch_start(0, position(0));
752 cursor.handle_touch_start(u64::MAX, position(10));
753 let start = cursor.take_events();
754 let first = start[0].pointer_id;
755 let second = start[1].pointer_id;
756 assert_ne!(first, super::MOUSE_POINTER_ID);
757 assert_ne!(second, super::MOUSE_POINTER_ID);
758 assert_ne!(first, second);
759 cursor.handle_touch_move(0, position(2));
760 cursor.handle_touch_end(0);
761 cursor.handle_touch_cancel(u64::MAX);
762 let end = cursor.take_events();
763 assert_eq!(end[0].pointer_id, first);
764 assert_eq!(end[1].pointer_id, first);
765 assert_eq!(end[2].pointer_id, second);
766
767 cursor.next_touch_pointer_id = u64::MAX;
768 cursor.handle_touch_start(5, position(0));
769 assert_ne!(cursor.take_events()[0].pointer_id, super::MOUSE_POINTER_ID);
770 }
771
772 #[test]
773 fn touch_slop_is_cumulative_and_follow_up_micro_moves_scroll() {
774 let _runtime_session = crate::RuntimeSession::new();
775 let _runtime_scope = _runtime_session.enter();
776 let mut cursor = CursorState::default();
777 cursor.handle_touch_start(1, position(0));
778 assert!(cursor.handle_touch_move(1, position(2)).is_none());
779 assert!(cursor.handle_touch_move(1, position(4)).is_none());
780 let first = cursor
781 .handle_touch_move(1, position(6))
782 .expect("slop crossed");
783 assert_eq!(first.gesture_state, GestureState::Dragged);
784 let second = cursor
785 .handle_touch_move(1, position(7))
786 .expect("drag remains active");
787 assert_eq!(second.gesture_state, GestureState::Dragged);
788 assert!(
789 matches!(second.content, CursorEventContent::Scroll(scroll) if scroll.delta_x == 1.0)
790 );
791 }
792
793 #[test]
794 fn cancelled_touch_is_not_a_tap_boundary() {
795 let _runtime_session = crate::RuntimeSession::new();
796 let _runtime_scope = _runtime_session.enter();
797 let mut cursor = CursorState::default();
798 cursor.handle_touch_start(1, position(0));
799 cursor.handle_touch_cancel(1);
800 let events = cursor.take_events();
801 assert!(matches!(
802 events.last().map(|event| &event.content),
803 Some(CursorEventContent::Cancelled(_))
804 ));
805 assert_eq!(
806 events.last().map(|event| event.gesture_state),
807 Some(GestureState::Dragged)
808 );
809 }
810}