1use alloc::vec::Vec;
17#[cfg(feature = "std")]
18use std::sync::atomic::{AtomicU64, Ordering};
19
20use azul_core::{
21 dom::{DomId, NodeId},
22 drag::{ActiveDragType, AutoScrollDirection, DragContext, DragData},
23 geom::{LogicalPosition, PhysicalPositionI32},
24 hit_test::HitTest,
25 task::{Duration as CoreDuration, Instant as CoreInstant},
26 window::WindowPosition,
27};
28use azul_css::{impl_option, impl_option_inner};
29
30
31#[cfg(feature = "std")]
32static NEXT_EVENT_ID: AtomicU64 = AtomicU64::new(1);
33
34#[cfg(feature = "std")]
36pub fn allocate_event_id() -> u64 {
37 NEXT_EVENT_ID.fetch_add(1, Ordering::Relaxed)
38}
39
40#[cfg(not(feature = "std"))]
42pub fn allocate_event_id() -> u64 {
43 0
44}
45
46const fn duration_to_millis(duration: CoreDuration) -> u64 {
58 duration.as_millis_u64()
59}
60
61pub const MAX_SAMPLES_PER_SESSION: usize = 1000;
66
67pub const DEFAULT_SAMPLE_TIMEOUT_MS: u64 = 2000;
72
73const DRAIN_BATCH_SIZE: usize = 100;
77
78pub const TOUCH_CONTACT_BUTTON_STATE: u8 = 0x01;
83
84#[derive(Debug, Clone, Copy, PartialEq)]
86pub struct GestureDetectionConfig {
87 pub drag_distance_threshold: f32,
89 pub double_click_time_threshold_ms: u64,
91 pub double_click_distance_threshold: f32,
93 pub long_press_time_threshold_ms: u64,
95 pub long_press_distance_threshold: f32,
97 pub min_samples_for_gesture: usize,
99 pub swipe_velocity_threshold: f32,
101 pub pinch_scale_threshold: f32,
103 pub rotation_angle_threshold: f32,
105 pub sample_cleanup_interval_ms: u64,
107}
108
109impl Default for GestureDetectionConfig {
110 fn default() -> Self {
111 Self {
112 drag_distance_threshold: 5.0,
113 double_click_time_threshold_ms: 500,
114 double_click_distance_threshold: 5.0,
115 long_press_time_threshold_ms: 500,
116 long_press_distance_threshold: 10.0,
117 min_samples_for_gesture: 2,
118 swipe_velocity_threshold: 500.0, pinch_scale_threshold: 0.1, rotation_angle_threshold: 0.1, sample_cleanup_interval_ms: DEFAULT_SAMPLE_TIMEOUT_MS,
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq)]
128pub struct InputSample {
129 pub position: LogicalPosition,
131 pub screen_position: LogicalPosition,
140 pub timestamp: CoreInstant,
142 pub button_state: u8,
144 pub event_id: u64,
146 pub pressure: f32,
148 pub tilt: (f32, f32),
151 pub touch_radius: (f32, f32),
154}
155
156impl_option!(
157 InputSample,
158 OptionInputSample,
159 copy = false,
160 [Debug, Clone, PartialEq]
161);
162
163#[derive(Debug, Clone, PartialEq)]
165pub struct InputSession {
166 pub samples: Vec<InputSample>,
168 pub ended: bool,
170 pub session_id: u64,
172 pub window_position_at_start: WindowPosition,
175}
176
177impl InputSession {
178 fn new(session_id: u64, first_sample: InputSample, window_position: WindowPosition) -> Self {
180 Self {
181 samples: vec![first_sample],
182 ended: false,
183 session_id,
184 window_position_at_start: window_position,
185 }
186 }
187
188 #[must_use] pub fn first_sample(&self) -> Option<&InputSample> {
190 self.samples.first()
191 }
192
193 #[must_use] pub fn last_sample(&self) -> Option<&InputSample> {
195 self.samples.last()
196 }
197
198 #[must_use] pub fn duration_ms(&self) -> Option<u64> {
200 let first = self.first_sample()?;
201 let last = self.last_sample()?;
202 let duration = last.timestamp.duration_since(&first.timestamp);
203 Some(duration_to_millis(duration))
204 }
205
206 #[must_use] pub fn total_distance(&self) -> f32 {
208 if self.samples.len() < 2 {
209 return 0.0;
210 }
211
212 let mut total = 0.0;
213 for i in 1..self.samples.len() {
214 let prev = &self.samples[i - 1];
215 let curr = &self.samples[i];
216 let dx = curr.position.x - prev.position.x;
217 let dy = curr.position.y - prev.position.y;
218 total += dx.hypot(dy);
219 }
220 total
221 }
222
223 #[must_use] pub fn direct_distance(&self) -> Option<f32> {
225 let first = self.first_sample()?;
226 let last = self.last_sample()?;
227 let dx = last.position.x - first.position.x;
228 let dy = last.position.y - first.position.y;
229 Some(dx.hypot(dy))
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq)]
235pub struct DetectedDrag {
236 pub start_position: LogicalPosition,
238 pub current_position: LogicalPosition,
240 pub direct_distance: f32,
242 pub total_distance: f32,
244 pub duration_ms: u64,
246 pub sample_count: usize,
248 pub session_id: u64,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254#[repr(C)]
255pub struct DetectedLongPress {
256 pub position: LogicalPosition,
258 pub duration_ms: u64,
260 pub callback_invoked: bool,
262 pub session_id: u64,
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268#[repr(C)]
269pub enum GestureDirection {
270 Up,
271 Down,
272 Left,
273 Right,
274}
275
276impl_option!(
277 GestureDirection,
278 OptionGestureDirection,
279 [Debug, Clone, Copy, PartialEq, Eq]
280);
281impl_option!(
282 DetectedPinch,
283 OptionDetectedPinch,
284 [Debug, Clone, Copy, PartialEq]
285);
286impl_option!(
287 DetectedRotation,
288 OptionDetectedRotation,
289 [Debug, Clone, Copy, PartialEq]
290);
291impl_option!(
292 DetectedLongPress,
293 OptionDetectedLongPress,
294 [Debug, Clone, Copy, PartialEq, Eq]
295);
296
297#[derive(Debug, Clone, Copy, PartialEq)]
299#[repr(C)]
300pub struct DetectedPinch {
301 pub scale: f32,
303 pub center: LogicalPosition,
305 pub initial_distance: f32,
307 pub current_distance: f32,
309 pub duration_ms: u64,
311}
312
313#[derive(Debug, Clone, Copy, PartialEq)]
315#[repr(C)]
316pub struct DetectedRotation {
317 pub angle_radians: f32,
319 pub center: LogicalPosition,
321 pub duration_ms: u64,
323}
324
325
326#[derive(Debug, Clone, Copy, PartialEq)]
328#[repr(C)]
329pub struct PenState {
330 pub position: LogicalPosition,
332 pub pressure: f32,
334 pub tilt: crate::callbacks::PenTilt,
336 pub in_contact: bool,
338 pub is_eraser: bool,
340 pub barrel_button_pressed: bool,
342 pub device_id: u64,
344 pub tangential_pressure: f32,
348 pub barrel_roll_rad: f32,
355 pub tool_id: u32,
361}
362
363impl_option!(PenState, OptionPenState, [Debug, Clone, Copy, PartialEq]);
364
365impl Default for PenState {
366 fn default() -> Self {
367 Self {
368 position: LogicalPosition::zero(),
369 pressure: 0.0,
370 tilt: crate::callbacks::PenTilt {
371 x_tilt: 0.0,
372 y_tilt: 0.0,
373 },
374 in_contact: false,
375 is_eraser: false,
376 barrel_button_pressed: false,
377 device_id: 0,
378 tangential_pressure: 0.0,
379 barrel_roll_rad: 0.0,
380 tool_id: 0,
381 }
382 }
383}
384
385#[derive(Debug, Clone, Copy, PartialEq)]
393#[repr(C)]
394pub struct WacomPadState {
395 pub express_keys: u32,
398 pub touch_ring: f32,
401 pub touch_ring_active: bool,
403 pub device_id: u64,
405}
406
407impl_option!(
408 WacomPadState,
409 OptionWacomPadState,
410 [Debug, Clone, Copy, PartialEq]
411);
412
413impl Default for WacomPadState {
414 fn default() -> Self {
415 Self {
416 express_keys: 0,
417 touch_ring: 0.0,
418 touch_ring_active: false,
419 device_id: 0,
420 }
421 }
422}
423
424impl WacomPadState {
425 #[must_use] pub const fn express_key(&self, index: u32) -> bool {
427 index < 32 && (self.express_keys & (1u32 << index)) != 0
428 }
429}
430
431#[derive(Debug, Clone, PartialEq)]
445pub struct GestureAndDragManager {
446 pub config: GestureDetectionConfig,
448 pub input_sessions: Vec<InputSession>,
450 pub active_drag: Option<DragContext>,
452 pub pen_state: Option<PenState>,
454 pub previous_pen_state: Option<PenState>,
456 pub pen_event_pending: bool,
458 pub pad_state: Option<WacomPadState>,
461 long_press_callbacks_invoked: Vec<u64>,
463 next_session_id: u64,
465 pub native_gesture: Option<NativeGestureEvent>,
478 touch_sessions: alloc::collections::btree_map::BTreeMap<u64, u64>,
486}
487
488#[derive(Debug, Clone, Copy, PartialEq)]
499#[repr(C, u8)]
500pub enum NativeGestureEvent {
501 DoubleClick,
503 LongPress(DetectedLongPress),
506 Swipe(GestureDirection),
509 Pinch(DetectedPinch),
512 Rotation(DetectedRotation),
515}
516
517
518impl Default for GestureAndDragManager {
519 fn default() -> Self {
520 Self::new()
521 }
522}
523
524impl GestureAndDragManager {
525 #[must_use] pub const fn debug_counts(&self) -> (usize, usize) {
528 (self.input_sessions.len(), self.long_press_callbacks_invoked.len())
529 }
530
531 #[must_use] pub fn new() -> Self {
533 Self {
534 config: GestureDetectionConfig::default(),
535 input_sessions: Vec::new(),
536 next_session_id: 1,
537 active_drag: None,
538 pen_state: None,
539 previous_pen_state: None,
540 pen_event_pending: false,
541 pad_state: None,
542 long_press_callbacks_invoked: Vec::new(),
543 native_gesture: None,
544 touch_sessions: alloc::collections::btree_map::BTreeMap::new(),
545 }
546 }
547
548 pub const fn inject_native_gesture(&mut self, gesture: NativeGestureEvent) {
554 self.native_gesture = Some(gesture);
555 }
556
557 pub const fn clear_native_gesture(&mut self) {
561 self.native_gesture = None;
562 }
563
564 #[must_use] pub fn with_config(config: GestureDetectionConfig) -> Self {
566 Self {
567 config,
568 ..Self::new()
569 }
570 }
571
572 pub fn start_input_session(
584 &mut self,
585 position: LogicalPosition,
586 timestamp: CoreInstant,
587 button_state: u8,
588 window_position: WindowPosition,
589 screen_position: LogicalPosition,
590 ) -> u64 {
591 self.start_input_session_with_pen(
592 position,
593 timestamp,
594 button_state,
595 allocate_event_id(),
596 0.5, (0.0, 0.0), (0.0, 0.0), window_position,
600 screen_position,
601 )
602 }
603
604 pub fn start_input_session_with_pen(
606 &mut self,
607 position: LogicalPosition,
608 timestamp: CoreInstant,
609 button_state: u8,
610 event_id: u64,
611 pressure: f32,
612 tilt: (f32, f32),
613 touch_radius: (f32, f32),
614 window_position: WindowPosition,
615 screen_position: LogicalPosition,
616 ) -> u64 {
617 let last_ended_idx = self.input_sessions.iter().rposition(|s| s.ended);
621 let mut idx = 0usize;
622 self.input_sessions.retain(|session| {
623 let keep = !session.ended || Some(idx) == last_ended_idx;
624 idx += 1;
625 keep
626 });
627
628 let session_id = self.next_session_id;
629 self.next_session_id += 1;
630
631 let sample = InputSample {
632 position,
633 screen_position,
634 timestamp,
635 button_state,
636 event_id,
637 pressure,
638 tilt,
639 touch_radius,
640 };
641
642 let session = InputSession::new(session_id, sample, window_position);
643 self.input_sessions.push(session);
644
645 session_id
646 }
647
648 pub fn record_input_sample(
655 &mut self,
656 position: LogicalPosition,
657 timestamp: CoreInstant,
658 button_state: u8,
659 screen_position: LogicalPosition,
660 ) -> bool {
661 self.record_input_sample_with_pen(
662 position,
663 timestamp,
664 button_state,
665 allocate_event_id(),
666 0.5, (0.0, 0.0), (0.0, 0.0), screen_position,
670 )
671 }
672
673 pub fn record_input_sample_with_pen(
675 &mut self,
676 position: LogicalPosition,
677 timestamp: CoreInstant,
678 button_state: u8,
679 event_id: u64,
680 pressure: f32,
681 tilt: (f32, f32),
682 touch_radius: (f32, f32),
683 screen_position: LogicalPosition,
684 ) -> bool {
685 let Some(session) = self.input_sessions.last_mut() else {
686 return false;
687 };
688
689 if session.ended {
690 return false;
691 }
692
693 if session.samples.len() >= MAX_SAMPLES_PER_SESSION {
695 let remove_count = session.samples.len() - MAX_SAMPLES_PER_SESSION + DRAIN_BATCH_SIZE;
697 session.samples.drain(0..remove_count);
698 }
699
700 session.samples.push(InputSample {
701 position,
702 screen_position,
703 timestamp,
704 button_state,
705 event_id,
706 pressure,
707 tilt,
708 touch_radius,
709 });
710
711 true
712 }
713
714 pub fn end_current_session(&mut self) {
719 if let Some(session) = self.input_sessions.last_mut() {
720 session.ended = true;
721 }
722 }
723
724 pub fn touch_down(
728 &mut self,
729 touch_id: u64,
730 position: LogicalPosition,
731 timestamp: CoreInstant,
732 window_position: WindowPosition,
733 screen_position: LogicalPosition,
734 ) {
735 let session_id = self.start_input_session(
736 position,
737 timestamp,
738 TOUCH_CONTACT_BUTTON_STATE,
739 window_position,
740 screen_position,
741 );
742 self.touch_sessions.insert(touch_id, session_id);
743 }
744
745 pub fn touch_move(
750 &mut self,
751 touch_id: u64,
752 position: LogicalPosition,
753 timestamp: CoreInstant,
754 screen_position: LogicalPosition,
755 ) -> bool {
756 let Some(session_id) = self.touch_sessions.get(&touch_id).copied() else {
757 return false;
758 };
759 self.record_sample_for_session(session_id, position, timestamp, screen_position)
760 }
761
762 pub fn touch_up(
765 &mut self,
766 touch_id: u64,
767 position: LogicalPosition,
768 timestamp: CoreInstant,
769 screen_position: LogicalPosition,
770 ) {
771 let Some(session_id) = self.touch_sessions.remove(&touch_id) else {
772 return;
773 };
774 let _ = self.record_sample_for_session(session_id, position, timestamp, screen_position);
775 if let Some(session) = self
776 .input_sessions
777 .iter_mut()
778 .find(|s| s.session_id == session_id)
779 {
780 session.ended = true;
781 }
782 }
783
784 pub fn touch_cancel_all(&mut self) {
787 let ids: Vec<u64> = self.touch_sessions.values().copied().collect();
788 self.touch_sessions.clear();
789 for session_id in ids {
790 if let Some(session) = self
791 .input_sessions
792 .iter_mut()
793 .find(|s| s.session_id == session_id)
794 {
795 session.ended = true;
796 }
797 }
798 }
799
800 fn record_sample_for_session(
804 &mut self,
805 session_id: u64,
806 position: LogicalPosition,
807 timestamp: CoreInstant,
808 screen_position: LogicalPosition,
809 ) -> bool {
810 let Some(session) = self
811 .input_sessions
812 .iter_mut()
813 .find(|s| s.session_id == session_id)
814 else {
815 return false;
816 };
817 if session.ended {
818 return false;
819 }
820 if session.samples.len() >= MAX_SAMPLES_PER_SESSION {
821 let remove_count =
822 session.samples.len() - MAX_SAMPLES_PER_SESSION + DRAIN_BATCH_SIZE;
823 session.samples.drain(0..remove_count);
824 }
825 session.samples.push(InputSample {
826 position,
827 screen_position,
828 timestamp,
829 button_state: TOUCH_CONTACT_BUTTON_STATE,
830 event_id: allocate_event_id(),
831 pressure: 0.5,
832 tilt: (0.0, 0.0),
833 touch_radius: (0.0, 0.0),
834 });
835 true
836 }
837
838 #[allow(clippy::needless_pass_by_value)]
845 pub fn clear_old_sessions(&mut self, current_time: CoreInstant) {
846 self.input_sessions.retain(|session| {
847 if let Some(last_sample) = session.last_sample() {
848 let duration = current_time.duration_since(&last_sample.timestamp);
849 let age_ms = duration_to_millis(duration);
850 age_ms < self.config.sample_cleanup_interval_ms
851 } else {
852 false
853 }
854 });
855
856 let valid_session_ids: Vec<u64> =
858 self.input_sessions.iter().map(|s| s.session_id).collect();
859
860 self.long_press_callbacks_invoked
861 .retain(|id| valid_session_ids.contains(id));
862 }
863
864 pub fn clear_all_sessions(&mut self) {
868 self.input_sessions.clear();
869 self.long_press_callbacks_invoked.clear();
870 }
871
872 pub const fn update_pen_state(
879 &mut self,
880 position: LogicalPosition,
881 pressure: f32,
882 tilt: (f32, f32),
883 in_contact: bool,
884 is_eraser: bool,
885 barrel_button_pressed: bool,
886 device_id: u64,
887 ) {
888 self.update_pen_state_full(
889 position,
890 pressure,
891 tilt,
892 in_contact,
893 is_eraser,
894 barrel_button_pressed,
895 device_id,
896 0.0,
897 0.0,
898 0,
899 );
900 }
901
902 pub const fn update_pen_state_full(
905 &mut self,
906 position: LogicalPosition,
907 pressure: f32,
908 tilt: (f32, f32),
909 in_contact: bool,
910 is_eraser: bool,
911 barrel_button_pressed: bool,
912 device_id: u64,
913 tangential_pressure: f32,
914 barrel_roll_rad: f32,
915 tool_id: u32,
916 ) {
917 self.previous_pen_state = self.pen_state;
918 self.pen_state = Some(PenState {
919 position,
920 pressure,
921 tilt: crate::callbacks::PenTilt {
922 x_tilt: tilt.0,
923 y_tilt: tilt.1,
924 },
925 in_contact,
926 is_eraser,
927 barrel_button_pressed,
928 device_id,
929 tangential_pressure,
930 barrel_roll_rad,
931 tool_id,
932 });
933 self.pen_event_pending = true;
934 }
935
936 pub const fn clear_pen_state(&mut self) {
938 self.previous_pen_state = self.pen_state;
939 self.pen_state = None;
940 self.pen_event_pending = true;
941 }
942
943 #[must_use] pub const fn get_pen_state(&self) -> Option<&PenState> {
945 self.pen_state.as_ref()
946 }
947
948 #[must_use] pub const fn get_previous_pen_state(&self) -> Option<&PenState> {
950 self.previous_pen_state.as_ref()
951 }
952
953 pub const fn clear_pen_event_pending(&mut self) {
955 self.pen_event_pending = false;
956 }
957
958 pub const fn update_pad_state(&mut self, pad: WacomPadState) {
960 self.pad_state = Some(pad);
961 }
962
963 #[must_use] pub const fn get_pad_state(&self) -> Option<&WacomPadState> {
965 self.pad_state.as_ref()
966 }
967
968 pub const fn clear_pad_state(&mut self) {
970 self.pad_state = None;
971 }
972
973 #[must_use] pub fn detect_drag(&self) -> Option<DetectedDrag> {
979 let session = self.get_current_session()?;
980
981 if session.samples.len() < self.config.min_samples_for_gesture {
982 return None;
983 }
984
985 let direct_distance = session.direct_distance()?;
986
987 if direct_distance >= self.config.drag_distance_threshold {
988 let first = session.first_sample()?;
989 let last = session.last_sample()?;
990
991 Some(DetectedDrag {
992 start_position: first.position,
993 current_position: last.position,
994 direct_distance,
995 total_distance: session.total_distance(),
996 duration_ms: session.duration_ms()?,
997 sample_count: session.samples.len(),
998 session_id: session.session_id,
999 })
1000 } else {
1001 None
1002 }
1003 }
1004
1005 #[must_use] pub fn detect_long_press(&self) -> Option<DetectedLongPress> {
1010 if let Some(NativeGestureEvent::LongPress(lp)) = self.native_gesture {
1011 return Some(lp);
1012 }
1013 let session = self.get_current_session()?;
1014
1015 if session.ended {
1016 return None; }
1018
1019 let duration_ms = session.duration_ms()?;
1020
1021 if duration_ms < self.config.long_press_time_threshold_ms {
1022 return None;
1023 }
1024
1025 let distance = session.direct_distance()?;
1026
1027 if distance <= self.config.long_press_distance_threshold {
1028 let first = session.first_sample()?;
1029 let callback_invoked = self
1030 .long_press_callbacks_invoked
1031 .contains(&session.session_id);
1032
1033 Some(DetectedLongPress {
1034 position: first.position,
1035 duration_ms,
1036 callback_invoked,
1037 session_id: session.session_id,
1038 })
1039 } else {
1040 None
1041 }
1042 }
1043
1044 pub fn mark_current_long_press_invoked(&mut self) {
1053 if let Some(id) = self.get_current_session().map(|s| s.session_id) {
1054 self.mark_long_press_callback_invoked(id);
1055 }
1056 }
1057
1058 pub fn mark_long_press_callback_invoked(&mut self, session_id: u64) {
1059 if !self.long_press_callbacks_invoked.contains(&session_id) {
1060 self.long_press_callbacks_invoked.push(session_id);
1061 }
1062 }
1063
1064 #[must_use] pub fn detect_double_click(&self) -> bool {
1068 if matches!(self.native_gesture, Some(NativeGestureEvent::DoubleClick)) {
1069 return true;
1070 }
1071 let sessions = &self.input_sessions;
1072 if sessions.len() < 2 {
1073 return false;
1074 }
1075
1076 let prev_session = &sessions[sessions.len() - 2];
1077 let last_session = &sessions[sessions.len() - 1];
1078
1079 if !prev_session.ended || !last_session.ended {
1081 return false;
1082 }
1083
1084 let prev_first = prev_session.first_sample();
1085 let last_first = last_session.first_sample();
1086 let (Some(prev_first), Some(last_first)) = (prev_first, last_first) else {
1087 return false;
1088 };
1089
1090 let duration = last_first.timestamp.duration_since(&prev_first.timestamp);
1091 let time_delta_ms = duration_to_millis(duration);
1092 if time_delta_ms > self.config.double_click_time_threshold_ms {
1093 return false;
1094 }
1095
1096 let dx = last_first.position.x - prev_first.position.x;
1097 let dy = last_first.position.y - prev_first.position.y;
1098 let distance = dx.hypot(dy);
1099
1100 distance < self.config.double_click_distance_threshold
1101 }
1102
1103 #[must_use] pub fn detect_click_count(&self) -> u32 {
1109 let sessions = &self.input_sessions;
1110 let n = sessions.len();
1111 if n == 0 {
1112 return 1;
1113 }
1114
1115 let mut recent: Vec<&InputSession> = Vec::new();
1123 for s in sessions.iter().rev() {
1124 if !s.ended {
1125 continue;
1126 }
1127 recent.push(s);
1128 if recent.len() >= 3 {
1129 break;
1130 }
1131 }
1132
1133 if recent.is_empty() {
1134 return 1;
1135 }
1136
1137 let mut count = 1u32;
1141
1142 for i in 0..recent.len() - 1 {
1143 let later = recent[i];
1144 let earlier = recent[i + 1];
1145
1146 let Some(later_start) = later.first_sample() else {
1147 break;
1148 };
1149 let Some(earlier_start) = earlier.first_sample() else {
1150 break;
1151 };
1152
1153 let duration = later_start.timestamp.duration_since(&earlier_start.timestamp);
1154 let time_delta_ms = duration_to_millis(duration);
1155 if time_delta_ms > self.config.double_click_time_threshold_ms {
1156 break;
1157 }
1158
1159 let dx = later_start.position.x - earlier_start.position.x;
1160 let dy = later_start.position.y - earlier_start.position.y;
1161 let distance = dx.hypot(dy);
1162 if distance >= self.config.double_click_distance_threshold {
1163 break;
1164 }
1165
1166 count += 1;
1167 }
1168
1169 if count > 3 { 1 } else { count }
1171 }
1172
1173 #[must_use] pub fn get_drag_direction(&self) -> Option<GestureDirection> {
1175 let session = self.get_current_session()?;
1176 let first = session.first_sample()?;
1177 let last = session.last_sample()?;
1178
1179 let dx = last.position.x - first.position.x;
1180 let dy = last.position.y - first.position.y;
1181
1182 let direction = match (dx.abs() > dy.abs(), dx > 0.0, dy > 0.0) {
1183 (true, true, _) => GestureDirection::Right,
1184 (true, false, _) => GestureDirection::Left,
1185 (false, _, true) => GestureDirection::Down,
1186 (false, _, false) => GestureDirection::Up,
1187 };
1188 Some(direction)
1189 }
1190
1191 #[allow(clippy::cast_precision_loss)] #[must_use] pub fn get_gesture_velocity(&self) -> Option<f32> {
1194 let session = self.get_current_session()?;
1195
1196 if session.samples.len() < 2 {
1197 return None;
1198 }
1199
1200 let total_distance = session.total_distance();
1201 let duration_ms = session.duration_ms()?;
1202
1203 if duration_ms == 0 {
1204 return None;
1205 }
1206
1207 let duration_secs = duration_ms as f32 / 1000.0;
1208 Some(total_distance / duration_secs)
1209 }
1210
1211 #[must_use] pub fn is_swipe(&self) -> bool {
1213 self.get_gesture_velocity()
1214 .is_some_and(|v| v >= self.config.swipe_velocity_threshold)
1215 }
1216
1217 #[must_use] pub fn detect_swipe_direction(&self) -> Option<GestureDirection> {
1221 if let Some(NativeGestureEvent::Swipe(d)) = self.native_gesture {
1222 return Some(d);
1223 }
1224 if !self.is_swipe() {
1226 return None;
1227 }
1228
1229 self.get_drag_direction()
1231 }
1232
1233 #[allow(clippy::similar_names)] #[must_use] pub fn detect_pinch(&self) -> Option<DetectedPinch> {
1239 if let Some(NativeGestureEvent::Pinch(p)) = self.native_gesture {
1240 return Some(p);
1241 }
1242 if self.input_sessions.len() < 2 {
1244 return None;
1245 }
1246
1247 let session1 = &self.input_sessions[self.input_sessions.len() - 2];
1249 let session2 = &self.input_sessions[self.input_sessions.len() - 1];
1250
1251 if session1.ended || session2.ended {
1257 return None;
1258 }
1259
1260 let first1 = session1.first_sample()?;
1262 let first2 = session2.first_sample()?;
1263 let last1 = session1.last_sample()?;
1264 let last2 = session2.last_sample()?;
1265
1266 let dx_initial = first2.position.x - first1.position.x;
1268 let dy_initial = first2.position.y - first1.position.y;
1269 let initial_distance = dx_initial.hypot(dy_initial);
1270
1271 let dx_current = last2.position.x - last1.position.x;
1273 let dy_current = last2.position.y - last1.position.y;
1274 let current_distance = dx_current.hypot(dy_current);
1275
1276 if initial_distance < 1.0 {
1278 return None;
1279 }
1280
1281 let scale = current_distance / initial_distance;
1283
1284 let scale_threshold = 1.0 + self.config.pinch_scale_threshold;
1286 if scale > 1.0 / scale_threshold && scale < scale_threshold {
1287 return None; }
1289
1290 let center = LogicalPosition {
1292 x: f32::midpoint(last1.position.x, last2.position.x),
1293 y: f32::midpoint(last1.position.y, last2.position.y),
1294 };
1295
1296 let duration = last1.timestamp.duration_since(&first1.timestamp);
1298 let duration_ms = duration_to_millis(duration);
1299
1300 Some(DetectedPinch {
1301 scale,
1302 center,
1303 initial_distance,
1304 current_distance,
1305 duration_ms,
1306 })
1307 }
1308
1309 #[allow(clippy::similar_names)] #[must_use] pub fn detect_rotation(&self) -> Option<DetectedRotation> {
1315 const PI: f32 = core::f32::consts::PI;
1316 if let Some(NativeGestureEvent::Rotation(r)) = self.native_gesture {
1317 return Some(r);
1318 }
1319 if self.input_sessions.len() < 2 {
1321 return None;
1322 }
1323
1324 let session1 = &self.input_sessions[self.input_sessions.len() - 2];
1326 let session2 = &self.input_sessions[self.input_sessions.len() - 1];
1327
1328 if session1.ended || session2.ended {
1332 return None;
1333 }
1334
1335 let first1 = session1.first_sample()?;
1337 let first2 = session2.first_sample()?;
1338 let last1 = session1.last_sample()?;
1339 let last2 = session2.last_sample()?;
1340
1341 let center = LogicalPosition {
1343 x: f32::midpoint(last1.position.x, last2.position.x),
1344 y: f32::midpoint(last1.position.y, last2.position.y),
1345 };
1346
1347 let dx_initial = first2.position.x - first1.position.x;
1349 let dy_initial = first2.position.y - first1.position.y;
1350 let initial_angle = dy_initial.atan2(dx_initial);
1351
1352 let dx_current = last2.position.x - last1.position.x;
1354 let dy_current = last2.position.y - last1.position.y;
1355 let current_angle = dy_current.atan2(dx_current);
1356
1357 let mut angle_diff = current_angle - initial_angle;
1359
1360 #[allow(clippy::while_float)] while angle_diff > PI {
1363 angle_diff -= 2.0 * PI;
1364 }
1365 #[allow(clippy::while_float)] while angle_diff < -PI {
1367 angle_diff += 2.0 * PI;
1368 }
1369
1370 if angle_diff.abs() < self.config.rotation_angle_threshold {
1372 return None;
1373 }
1374
1375 let duration = last1.timestamp.duration_since(&first1.timestamp);
1377 let duration_ms = duration_to_millis(duration);
1378
1379 Some(DetectedRotation {
1380 angle_radians: angle_diff,
1381 center,
1382 duration_ms,
1383 })
1384 }
1385
1386 #[must_use] pub fn get_current_session(&self) -> Option<&InputSession> {
1388 self.input_sessions.last()
1389 }
1390
1391 #[must_use] pub fn get_current_mouse_position(&self) -> Option<LogicalPosition> {
1393 self.get_current_session()
1394 .and_then(|s| s.last_sample())
1395 .map(|sample| sample.position)
1396 }
1397
1398 #[must_use] pub fn get_drag_delta(&self) -> Option<(f32, f32)> {
1403 let session = self.get_current_session()?;
1404 let first = session.first_sample()?;
1405 let last = session.last_sample()?;
1406 Some((
1407 last.position.x - first.position.x,
1408 last.position.y - first.position.y,
1409 ))
1410 }
1411
1412 #[must_use] pub fn get_drag_delta_screen(&self) -> Option<(f32, f32)> {
1424 let session = self.get_current_session()?;
1425 let first = session.first_sample()?;
1426 let last = session.last_sample()?;
1427 Some((
1428 last.screen_position.x - first.screen_position.x,
1429 last.screen_position.y - first.screen_position.y,
1430 ))
1431 }
1432
1433 #[must_use] pub fn get_drag_delta_screen_incremental(&self) -> Option<(f32, f32)> {
1452 let session = self.get_current_session()?;
1453 let len = session.samples.len();
1454 if len < 2 {
1455 return None;
1456 }
1457 let prev = &session.samples[len - 2];
1458 let last = &session.samples[len - 1];
1459 Some((
1460 last.screen_position.x - prev.screen_position.x,
1461 last.screen_position.y - prev.screen_position.y,
1462 ))
1463 }
1464
1465 #[must_use] pub fn get_window_position_at_session_start(&self) -> Option<WindowPosition> {
1469 let session = self.get_current_session()?;
1470 Some(session.window_position_at_start)
1471 }
1472
1473 #[must_use] pub const fn get_drag_context(&self) -> Option<&DragContext> {
1479 self.active_drag.as_ref()
1480 }
1481
1482 pub const fn get_drag_context_mut(&mut self) -> Option<&mut DragContext> {
1484 self.active_drag.as_mut()
1485 }
1486
1487 pub fn activate_node_drag(
1496 &mut self,
1497 dom_id: DomId,
1498 node_id: NodeId,
1499 drag_data: DragData,
1500 _start_hit_test: Option<HitTest>,
1501 ) {
1502 if let Some(detected) = self.detect_drag() {
1503 self.active_drag = Some(DragContext::node_drag(
1504 dom_id,
1505 node_id,
1506 detected.start_position,
1507 drag_data,
1508 detected.session_id,
1509 ));
1510 }
1511 }
1512
1513 pub fn activate_window_drag(
1515 &mut self,
1516 initial_window_position: WindowPosition,
1517 _start_hit_test: Option<HitTest>,
1518 ) {
1519 if let Some(detected) = self.detect_drag() {
1520 self.active_drag = Some(DragContext::window_move(
1521 detected.start_position,
1522 initial_window_position,
1523 detected.session_id,
1524 ));
1525 }
1526 }
1527
1528 pub const fn update_active_drag_positions(&mut self, position: LogicalPosition) {
1534 if let Some(ref mut drag) = self.active_drag {
1535 drag.update_position(position);
1536 }
1537 }
1538
1539 pub fn update_drop_target(&mut self, target: Option<azul_core::dom::DomNodeId>) {
1541 if let Some(ref mut drag) = self.active_drag {
1542 match &mut drag.drag_type {
1543 ActiveDragType::Node(ref mut node_drag) => {
1544 node_drag.current_drop_target = target.into();
1545 }
1546 ActiveDragType::FileDrop(ref mut file_drop) => {
1547 file_drop.drop_target = target.into();
1548 }
1549 _ => {}
1550 }
1551 }
1552 }
1553
1554 pub const fn update_auto_scroll_direction(&mut self, direction: AutoScrollDirection) {
1556 if let Some(ref mut drag) = self.active_drag {
1557 if let Some(text_drag) = drag.as_text_selection_mut() {
1558 text_drag.auto_scroll_direction = direction;
1559 }
1560 }
1561 }
1562
1563 pub const fn end_drag(&mut self) -> Option<DragContext> {
1565 self.active_drag.take()
1566 }
1567
1568 pub fn cancel_drag(&mut self) {
1570 if let Some(ref mut drag) = self.active_drag {
1571 drag.cancelled = true;
1572 }
1573 self.active_drag = None;
1574 }
1575
1576 #[must_use] pub const fn is_dragging(&self) -> bool {
1582 self.active_drag.is_some()
1583 }
1584
1585 #[must_use] pub fn is_text_selection_dragging(&self) -> bool {
1587 self.active_drag.as_ref().is_some_and(DragContext::is_text_selection)
1588 }
1589
1590 #[must_use] pub fn is_scrollbar_dragging(&self) -> bool {
1592 self.active_drag.as_ref().is_some_and(DragContext::is_scrollbar_thumb)
1593 }
1594
1595 #[must_use] pub fn is_node_drag_active(&self) -> bool {
1597 self.active_drag.as_ref().is_some_and(DragContext::is_node_drag)
1598 }
1599
1600 #[must_use] pub fn is_node_dragging(&self, dom_id: DomId, node_id: NodeId) -> bool {
1602 self.active_drag.as_ref().is_some_and(|d| {
1603 d.as_node_drag().is_some_and(|node_drag| node_drag.dom_id == dom_id && node_drag.node_id == node_id)
1604 })
1605 }
1606
1607 #[must_use] pub fn is_window_dragging(&self) -> bool {
1609 self.active_drag.as_ref().is_some_and(DragContext::is_window_move)
1610 }
1611
1612 #[must_use] pub fn is_file_dropping(&self) -> bool {
1614 self.active_drag.as_ref().is_some_and(DragContext::is_file_drop)
1615 }
1616
1617 #[must_use] pub const fn session_count(&self) -> usize {
1619 self.input_sessions.len()
1620 }
1621
1622 #[must_use] pub fn current_session_id(&self) -> Option<u64> {
1624 self.get_current_session().map(|s| s.session_id)
1625 }
1626
1627 #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn get_window_drag_delta(&self) -> Option<(i32, i32)> {
1637 let drag = self.active_drag.as_ref()?.as_window_move()?;
1638
1639 let delta_x = drag.current_position.x - drag.start_position.x;
1640 let delta_y = drag.current_position.y - drag.start_position.y;
1641
1642 match drag.initial_window_position {
1643 WindowPosition::Initialized(_initial_pos) => Some((delta_x as i32, delta_y as i32)),
1644 _ => None,
1645 }
1646 }
1647
1648 #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn get_window_position_from_drag(&self) -> Option<WindowPosition> {
1653 let drag = self.active_drag.as_ref()?.as_window_move()?;
1654
1655 let delta_x = drag.current_position.x - drag.start_position.x;
1656 let delta_y = drag.current_position.y - drag.start_position.y;
1657
1658 match drag.initial_window_position {
1659 WindowPosition::Initialized(initial_pos) => {
1660 Some(WindowPosition::Initialized(PhysicalPositionI32::new(
1661 initial_pos.x + delta_x as i32,
1662 initial_pos.y + delta_y as i32,
1663 )))
1664 }
1665 _ => None,
1666 }
1667 }
1668
1669 #[must_use] pub fn get_scrollbar_scroll_offset(&self) -> Option<f32> {
1671 self.active_drag.as_ref()?.calculate_scrollbar_scroll_offset()
1672 }
1673
1674}
1675
1676impl crate::managers::NodeIdRemap for GestureAndDragManager {
1677 fn remap_node_ids(&mut self, dom_id: DomId, map: &crate::managers::NodeIdMap) {
1683 if let Some(ref mut drag) = self.active_drag {
1684 if !drag.remap_node_ids(dom_id, map.as_btree_map()) {
1685 drag.cancelled = true;
1687 self.active_drag = None;
1688 }
1689 }
1690 }
1691}
1692
1693#[cfg(test)]
1694mod touch_session_tests {
1695 use super::*;
1696 use azul_core::task::{Instant as TestInstant, SystemTick};
1697
1698 fn ts(n: u64) -> CoreInstant {
1711 use std::sync::OnceLock;
1716 static BASE: OnceLock<std::time::Instant> = OnceLock::new();
1717 let base = *BASE.get_or_init(std::time::Instant::now);
1718 (base + core::time::Duration::from_millis(n)).into()
1719 }
1720
1721 fn pos(x: f32, y: f32) -> LogicalPosition {
1722 LogicalPosition { x, y }
1723 }
1724
1725 #[test]
1726 fn two_fingers_open_two_concurrent_sessions() {
1727 let mut m = GestureAndDragManager::new();
1728 m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
1729 m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
1730 assert_eq!(m.input_sessions.len(), 2);
1731 assert!(!m.input_sessions[0].ended);
1732 assert!(!m.input_sessions[1].ended);
1733 }
1734
1735 #[test]
1736 fn moves_land_in_the_correct_session_not_the_last_one() {
1737 let mut m = GestureAndDragManager::new();
1738 m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
1739 m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
1740 assert!(m.touch_move(1, pos(90.0, 100.0), ts(2), pos(90.0, 100.0)));
1744 assert_eq!(m.input_sessions[0].samples.len(), 2, "finger 1 session grew");
1745 assert_eq!(m.input_sessions[1].samples.len(), 1, "finger 2 session untouched");
1746 }
1747
1748 #[test]
1749 fn spread_gesture_is_detected_as_pinch_out() {
1750 let mut m = GestureAndDragManager::new();
1751 m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
1752 m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
1753 m.touch_move(1, pos(50.0, 100.0), ts(2), pos(50.0, 100.0));
1755 m.touch_move(2, pos(250.0, 100.0), ts(3), pos(250.0, 100.0));
1756 let pinch = m.detect_pinch().expect("two concurrent touch sessions must yield a pinch");
1757 assert!(
1758 pinch.scale > 1.5,
1759 "spread must read as pinch-out (scale {}), initial {} current {}",
1760 pinch.scale,
1761 pinch.initial_distance,
1762 pinch.current_distance
1763 );
1764 }
1765
1766 #[test]
1767 fn touch_up_ends_only_its_own_session() {
1768 let mut m = GestureAndDragManager::new();
1769 m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
1770 m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
1771 m.touch_up(1, pos(100.0, 100.0), ts(2), pos(100.0, 100.0));
1772 assert!(m.input_sessions[0].ended);
1773 assert!(!m.input_sessions[1].ended);
1774 assert!(!m.touch_move(1, pos(0.0, 0.0), ts(3), pos(0.0, 0.0)));
1776 }
1777}
1778
1779#[cfg(test)]
1780#[allow(clippy::float_cmp, clippy::unreadable_literal)]
1781mod autotest_generated {
1782 use azul_core::{
1783 drag::ScrollbarAxis, geom::PhysicalPositionI32, styled_dom::NodeHierarchyItemId,
1784 task::{SystemTick, SystemTickDiff, SystemTimeDiff},
1785 };
1786
1787 use super::*;
1788
1789 fn ts(n: u64) -> CoreInstant {
1795 use std::sync::OnceLock;
1800 static BASE: OnceLock<std::time::Instant> = OnceLock::new();
1801 let base = *BASE.get_or_init(std::time::Instant::now);
1802 (base + core::time::Duration::from_millis(n)).into()
1803 }
1804
1805 fn pos(x: f32, y: f32) -> LogicalPosition {
1806 LogicalPosition { x, y }
1807 }
1808
1809 fn sample(x: f32, y: f32, tick: u64) -> InputSample {
1811 InputSample {
1812 position: pos(x, y),
1813 screen_position: pos(x, y),
1814 timestamp: ts(tick),
1815 button_state: 0x01,
1816 event_id: 0,
1817 pressure: 0.5,
1818 tilt: (0.0, 0.0),
1819 touch_radius: (0.0, 0.0),
1820 }
1821 }
1822
1823 fn ended_session(session_id: u64, samples: Vec<InputSample>) -> InputSession {
1826 InputSession {
1827 samples,
1828 ended: true,
1829 session_id,
1830 window_position_at_start: WindowPosition::Uninitialized,
1831 }
1832 }
1833
1834 fn dragging_manager(
1837 from: LogicalPosition,
1838 to: LogicalPosition,
1839 hold_ms: u64,
1840 ) -> GestureAndDragManager {
1841 let mut m = GestureAndDragManager::new();
1842 m.start_input_session(from, ts(0), 0x01, WindowPosition::Uninitialized, from);
1843 let recorded = m.record_input_sample(to, ts(hold_ms), 0x01, to);
1844 assert!(recorded);
1845 m
1846 }
1847
1848 #[test]
1851 fn duration_to_millis_tick_zero_and_max_do_not_panic() {
1852 assert_eq!(
1853 duration_to_millis(CoreDuration::Tick(SystemTickDiff { tick_diff: 0 })),
1854 0
1855 );
1856 assert_eq!(
1857 duration_to_millis(CoreDuration::Tick(SystemTickDiff {
1858 tick_diff: u64::MAX
1859 })),
1860 u64::MAX
1861 );
1862 }
1863
1864 #[test]
1869 fn duration_to_millis_converts_ticks_at_the_nominal_frame_rate() {
1870 assert_eq!(
1871 duration_to_millis(CoreDuration::Tick(SystemTickDiff { tick_diff: 60 })),
1872 1_000
1873 );
1874 assert_eq!(
1875 duration_to_millis(CoreDuration::Tick(SystemTickDiff { tick_diff: 30 })),
1876 500
1877 );
1878 assert_eq!(
1879 duration_to_millis(CoreDuration::Tick(SystemTickDiff { tick_diff: 1 })),
1880 16
1881 );
1882 }
1883
1884 #[cfg(feature = "std")]
1885 #[test]
1886 fn duration_to_millis_system_zero_and_sub_millisecond_floor() {
1887 assert_eq!(
1888 duration_to_millis(CoreDuration::System(SystemTimeDiff { secs: 0, nanos: 0 })),
1889 0
1890 );
1891 assert_eq!(
1893 duration_to_millis(CoreDuration::System(SystemTimeDiff {
1894 secs: 0,
1895 nanos: 999_999
1896 })),
1897 0
1898 );
1899 assert_eq!(
1900 duration_to_millis(CoreDuration::System(SystemTimeDiff {
1901 secs: 2,
1902 nanos: 500_000_000
1903 })),
1904 2500
1905 );
1906 }
1907
1908 #[cfg(feature = "std")]
1909 #[test]
1910 fn duration_to_millis_system_max_truncates_instead_of_panicking() {
1911 let d = CoreDuration::System(SystemTimeDiff {
1915 secs: u64::MAX,
1916 nanos: 999_999_999,
1917 });
1918 let expected = ((u64::MAX as u128) * 1000 + 999) as u64;
1919 assert_eq!(duration_to_millis(d), expected);
1920 }
1921
1922 #[test]
1925 fn express_key_out_of_range_index_is_false_not_a_shift_overflow() {
1926 let pad = WacomPadState {
1928 express_keys: u32::MAX,
1929 touch_ring: 0.0,
1930 touch_ring_active: false,
1931 device_id: 0,
1932 };
1933 assert!(pad.express_key(31));
1934 assert!(!pad.express_key(32));
1935 assert!(!pad.express_key(33));
1936 assert!(!pad.express_key(u32::MAX));
1937 }
1938
1939 #[test]
1940 fn express_key_default_pad_has_no_keys_held() {
1941 let pad = WacomPadState::default();
1942 for i in 0..40u32 {
1943 assert!(!pad.express_key(i), "bit {i} must be unset on a default pad");
1944 }
1945 }
1946
1947 #[test]
1948 fn express_key_bitset_round_trips_every_bit() {
1949 for bit in 0..32u32 {
1950 let pad = WacomPadState {
1951 express_keys: 1u32 << bit,
1952 touch_ring: 0.0,
1953 touch_ring_active: false,
1954 device_id: 0,
1955 };
1956 for probe in 0..32u32 {
1957 assert_eq!(
1958 pad.express_key(probe),
1959 probe == bit,
1960 "encode bit {bit} -> decode probe {probe}"
1961 );
1962 }
1963 }
1964 }
1965
1966 #[test]
1969 fn input_session_new_holds_its_construction_invariants() {
1970 let s = InputSession::new(
1971 u64::MAX,
1972 sample(1.0, 2.0, 7),
1973 WindowPosition::Initialized(PhysicalPositionI32::new(-5, 9)),
1974 );
1975 assert_eq!(s.session_id, u64::MAX);
1976 assert!(!s.ended);
1977 assert_eq!(s.samples.len(), 1);
1978 assert_eq!(s.first_sample(), s.last_sample());
1979 assert_eq!(
1980 s.window_position_at_start,
1981 WindowPosition::Initialized(PhysicalPositionI32::new(-5, 9))
1982 );
1983 assert_eq!(s.total_distance(), 0.0);
1984 assert_eq!(s.direct_distance(), Some(0.0));
1985 assert_eq!(s.duration_ms(), Some(0));
1986 }
1987
1988 #[test]
1989 fn empty_session_getters_return_none_instead_of_panicking() {
1990 let s = InputSession {
1991 samples: Vec::new(),
1992 ended: false,
1993 session_id: 0,
1994 window_position_at_start: WindowPosition::Uninitialized,
1995 };
1996 assert!(s.first_sample().is_none());
1997 assert!(s.last_sample().is_none());
1998 assert!(s.duration_ms().is_none());
1999 assert!(s.direct_distance().is_none());
2000 assert_eq!(s.total_distance(), 0.0);
2001 }
2002
2003 #[test]
2004 fn duration_ms_saturates_to_zero_when_time_runs_backwards() {
2005 let s = InputSession {
2007 samples: vec![sample(0.0, 0.0, 900), sample(0.0, 0.0, 100)],
2008 ended: false,
2009 session_id: 1,
2010 window_position_at_start: WindowPosition::Uninitialized,
2011 };
2012 assert_eq!(s.duration_ms(), Some(0));
2013 }
2014
2015 #[cfg(feature = "std")]
2016 #[test]
2017 fn duration_ms_with_mismatched_instant_kinds_is_zero() {
2018 let mut first = sample(0.0, 0.0, 0);
2024 first.timestamp = CoreInstant::now(); let mut last = sample(0.0, 0.0, 5_000);
2026 last.timestamp = CoreInstant::Tick(SystemTick::new(5_000)); let s = InputSession {
2028 samples: vec![first, last],
2029 ended: false,
2030 session_id: 1,
2031 window_position_at_start: WindowPosition::Uninitialized,
2032 };
2033 assert_eq!(s.duration_ms(), Some(0));
2034 }
2035
2036 #[test]
2037 fn total_distance_sums_the_path_while_direct_distance_is_the_chord() {
2038 let s = InputSession {
2039 samples: vec![
2040 sample(0.0, 0.0, 0),
2041 sample(3.0, 0.0, 1),
2042 sample(3.0, 4.0, 2),
2043 ],
2044 ended: false,
2045 session_id: 1,
2046 window_position_at_start: WindowPosition::Uninitialized,
2047 };
2048 assert_eq!(s.total_distance(), 7.0);
2049 assert_eq!(s.direct_distance(), Some(5.0));
2050 }
2051
2052 #[test]
2053 fn distances_with_nan_coordinates_are_nan_and_do_not_panic() {
2054 let s = InputSession {
2055 samples: vec![sample(0.0, 0.0, 0), sample(f32::NAN, f32::NAN, 1)],
2056 ended: false,
2057 session_id: 1,
2058 window_position_at_start: WindowPosition::Uninitialized,
2059 };
2060 assert!(s.total_distance().is_nan());
2061 assert!(s.direct_distance().is_some_and(f32::is_nan));
2062 }
2063
2064 #[test]
2065 fn distances_at_f32_extremes_saturate_to_infinity_instead_of_panicking() {
2066 let s = InputSession {
2067 samples: vec![
2068 sample(-f32::MAX, -f32::MAX, 0),
2069 sample(f32::MAX, f32::MAX, 1),
2070 ],
2071 ended: false,
2072 session_id: 1,
2073 window_position_at_start: WindowPosition::Uninitialized,
2074 };
2075 assert!(s.total_distance().is_infinite());
2076 assert!(s.direct_distance().is_some_and(f32::is_infinite));
2077 }
2078
2079 #[test]
2082 fn new_manager_is_inert_and_every_detector_is_quiet() {
2083 let m = GestureAndDragManager::new();
2084 assert_eq!(m.session_count(), 0);
2085 assert_eq!(m.debug_counts(), (0, 0));
2086 assert!(m.current_session_id().is_none());
2087 assert!(m.get_current_session().is_none());
2088 assert!(m.get_current_mouse_position().is_none());
2089 assert!(m.get_pen_state().is_none());
2090 assert!(m.get_previous_pen_state().is_none());
2091 assert!(m.get_pad_state().is_none());
2092 assert!(m.get_drag_context().is_none());
2093 assert!(m.detect_drag().is_none());
2094 assert!(m.detect_long_press().is_none());
2095 assert!(!m.detect_double_click());
2096 assert!(m.get_drag_direction().is_none());
2097 assert!(m.get_gesture_velocity().is_none());
2098 assert!(!m.is_swipe());
2099 assert!(m.detect_swipe_direction().is_none());
2100 assert!(m.detect_pinch().is_none());
2101 assert!(m.detect_rotation().is_none());
2102 assert!(m.get_drag_delta().is_none());
2103 assert!(m.get_drag_delta_screen().is_none());
2104 assert!(m.get_drag_delta_screen_incremental().is_none());
2105 assert!(m.get_window_position_at_session_start().is_none());
2106 assert!(m.get_window_drag_delta().is_none());
2107 assert!(m.get_window_position_from_drag().is_none());
2108 assert!(m.get_scrollbar_scroll_offset().is_none());
2109 assert!(!m.is_dragging());
2110 assert!(!m.is_text_selection_dragging());
2111 assert!(!m.is_scrollbar_dragging());
2112 assert!(!m.is_node_drag_active());
2113 assert!(!m.is_window_dragging());
2114 assert!(!m.is_file_dropping());
2115 assert!(!m.is_node_dragging(DomId::ROOT_ID, NodeId::ZERO));
2116 assert_eq!(m.detect_click_count(), 1);
2118 assert_eq!(m, GestureAndDragManager::default());
2119 }
2120
2121 #[test]
2122 fn with_config_keeps_extreme_thresholds_verbatim_and_still_starts_at_session_1() {
2123 let cfg = GestureDetectionConfig {
2124 drag_distance_threshold: f32::NAN,
2125 double_click_time_threshold_ms: u64::MAX,
2126 double_click_distance_threshold: f32::INFINITY,
2127 long_press_time_threshold_ms: 0,
2128 long_press_distance_threshold: -1.0,
2129 min_samples_for_gesture: usize::MAX,
2130 swipe_velocity_threshold: 0.0,
2131 pinch_scale_threshold: f32::MAX,
2132 rotation_angle_threshold: -0.0,
2133 sample_cleanup_interval_ms: 0,
2134 };
2135 let mut m = GestureAndDragManager::with_config(cfg);
2136 assert!(m.config.drag_distance_threshold.is_nan());
2137 assert_eq!(m.config.double_click_time_threshold_ms, u64::MAX);
2138 assert_eq!(m.config.min_samples_for_gesture, usize::MAX);
2139 assert_eq!(m.session_count(), 0);
2140 let id = m.start_input_session(
2141 pos(0.0, 0.0),
2142 ts(0),
2143 0x01,
2144 WindowPosition::Uninitialized,
2145 pos(0.0, 0.0),
2146 );
2147 assert_eq!(id, 1, "with_config must not disturb the session counter");
2148 assert!(m.detect_drag().is_none());
2150 }
2151
2152 #[test]
2155 fn session_ids_are_monotonic_starting_at_one() {
2156 let mut m = GestureAndDragManager::new();
2157 for expected in 1..=5u64 {
2158 let id = m.start_input_session(
2159 pos(0.0, 0.0),
2160 ts(expected),
2161 0x01,
2162 WindowPosition::Uninitialized,
2163 pos(0.0, 0.0),
2164 );
2165 assert_eq!(id, expected);
2166 assert_eq!(m.current_session_id(), Some(expected));
2167 m.end_current_session();
2168 }
2169 }
2170
2171 #[test]
2172 fn session_id_counter_at_the_u64_boundary_does_not_overflow() {
2173 let mut m = GestureAndDragManager::new();
2174 m.next_session_id = u64::MAX - 1;
2175 let id = m.start_input_session(
2176 pos(0.0, 0.0),
2177 ts(0),
2178 0xFF,
2179 WindowPosition::Uninitialized,
2180 pos(0.0, 0.0),
2181 );
2182 assert_eq!(id, u64::MAX - 1);
2183 assert_eq!(m.next_session_id, u64::MAX);
2184 }
2185
2186 #[test]
2187 fn recording_without_or_after_a_session_returns_false() {
2188 let mut m = GestureAndDragManager::new();
2189 assert!(!m.record_input_sample(pos(1.0, 1.0), ts(1), 0x01, pos(1.0, 1.0)));
2190 m.start_input_session(
2191 pos(0.0, 0.0),
2192 ts(0),
2193 0x01,
2194 WindowPosition::Uninitialized,
2195 pos(0.0, 0.0),
2196 );
2197 assert!(m.record_input_sample(pos(1.0, 1.0), ts(1), 0x01, pos(1.0, 1.0)));
2198 m.end_current_session();
2199 assert!(!m.record_input_sample(pos(2.0, 2.0), ts(2), 0x01, pos(2.0, 2.0)));
2200 m.end_current_session();
2202 m.clear_all_sessions();
2203 m.end_current_session();
2204 assert_eq!(m.session_count(), 0);
2205 }
2206
2207 #[test]
2208 fn sample_count_stays_bounded_by_max_samples_per_session() {
2209 let mut m = GestureAndDragManager::new();
2210 m.start_input_session(
2211 pos(0.0, 0.0),
2212 ts(0),
2213 0x01,
2214 WindowPosition::Uninitialized,
2215 pos(0.0, 0.0),
2216 );
2217 for i in 1..=(MAX_SAMPLES_PER_SESSION as u64 + 200) {
2218 assert!(m.record_input_sample(pos(i as f32, 0.0), ts(i), 0x01, pos(i as f32, 0.0)));
2219 assert!(
2220 m.get_current_session().unwrap().samples.len() <= MAX_SAMPLES_PER_SESSION,
2221 "sample buffer grew past MAX_SAMPLES_PER_SESSION at i={i}"
2222 );
2223 }
2224 let last = m.get_current_mouse_position().unwrap();
2226 assert_eq!(last.x, (MAX_SAMPLES_PER_SESSION + 200) as f32);
2227 }
2228
2229 #[test]
2230 fn pen_samples_accept_nan_inf_and_extreme_values() {
2231 let mut m = GestureAndDragManager::new();
2232 let id = m.start_input_session_with_pen(
2233 pos(f32::NAN, f32::INFINITY),
2234 ts(0),
2235 0xFF,
2236 u64::MAX,
2237 f32::NAN,
2238 (f32::INFINITY, f32::NEG_INFINITY),
2239 (-f32::MAX, f32::MAX),
2240 WindowPosition::Uninitialized,
2241 pos(f32::NEG_INFINITY, f32::NAN),
2242 );
2243 assert_eq!(id, 1);
2244 assert!(m.record_input_sample_with_pen(
2245 pos(0.0, 0.0),
2246 ts(u64::MAX),
2247 0x00,
2248 0,
2249 -1.0e30,
2250 (f32::NAN, f32::NAN),
2251 (f32::NAN, f32::NAN),
2252 pos(0.0, 0.0),
2253 ));
2254 let session = m.get_current_session().unwrap();
2255 assert_eq!(session.samples.len(), 2);
2256 let first = session.first_sample().unwrap();
2257 assert!(first.pressure.is_nan());
2258 assert!(first.tilt.0.is_infinite());
2259 assert_eq!(first.button_state, 0xFF);
2260 assert_eq!(first.event_id, u64::MAX);
2261 assert_eq!(session.duration_ms(), Some(u64::MAX));
2263 assert!(m.detect_drag().is_none_or(|d| !d.direct_distance.is_finite()));
2267 assert!(m.get_drag_direction().is_some());
2268 }
2269
2270 #[test]
2271 fn starting_a_session_prunes_all_but_the_newest_ended_session() {
2272 let mut m = GestureAndDragManager::new();
2273 for tick in [0u64, 10, 20] {
2274 m.start_input_session(
2275 pos(0.0, 0.0),
2276 ts(tick),
2277 0x01,
2278 WindowPosition::Uninitialized,
2279 pos(0.0, 0.0),
2280 );
2281 m.end_current_session();
2282 }
2283 assert_eq!(m.session_count(), 2);
2285 assert_eq!(m.input_sessions[0].session_id, 2);
2286 assert_eq!(m.input_sessions[1].session_id, 3);
2287 assert_eq!(m.detect_click_count(), 2);
2291 }
2292
2293 #[test]
2296 fn touch_ids_at_zero_and_u64_max_are_tracked_independently() {
2297 let mut m = GestureAndDragManager::new();
2298 m.touch_down(
2299 0,
2300 pos(0.0, 0.0),
2301 ts(0),
2302 WindowPosition::Uninitialized,
2303 pos(0.0, 0.0),
2304 );
2305 m.touch_down(
2306 u64::MAX,
2307 pos(50.0, 0.0),
2308 ts(1),
2309 WindowPosition::Uninitialized,
2310 pos(50.0, 0.0),
2311 );
2312 assert_eq!(m.session_count(), 2);
2313 assert!(m.touch_move(0, pos(1.0, 1.0), ts(2), pos(1.0, 1.0)));
2314 assert!(m.touch_move(u64::MAX, pos(60.0, 0.0), ts(3), pos(60.0, 0.0)));
2315 assert_eq!(m.input_sessions[0].samples.len(), 2);
2316 assert_eq!(m.input_sessions[1].samples.len(), 2);
2317 m.touch_up(0, pos(1.0, 1.0), ts(4), pos(1.0, 1.0));
2318 assert!(m.input_sessions[0].ended);
2319 assert!(!m.input_sessions[1].ended);
2320 }
2321
2322 #[test]
2323 fn touch_events_for_unknown_ids_are_ignored_without_panicking() {
2324 let mut m = GestureAndDragManager::new();
2325 assert!(!m.touch_move(42, pos(0.0, 0.0), ts(0), pos(0.0, 0.0)));
2326 m.touch_up(42, pos(0.0, 0.0), ts(1), pos(0.0, 0.0));
2327 m.touch_cancel_all(); assert_eq!(m.session_count(), 0);
2329 }
2330
2331 #[test]
2332 fn a_repeated_touch_down_for_the_same_id_rebinds_to_the_newest_session() {
2333 let mut m = GestureAndDragManager::new();
2334 m.touch_down(
2335 7,
2336 pos(0.0, 0.0),
2337 ts(0),
2338 WindowPosition::Uninitialized,
2339 pos(0.0, 0.0),
2340 );
2341 m.touch_down(
2342 7,
2343 pos(9.0, 9.0),
2344 ts(1),
2345 WindowPosition::Uninitialized,
2346 pos(9.0, 9.0),
2347 );
2348 assert_eq!(m.touch_sessions.len(), 1, "the id map must not grow");
2349 assert_eq!(m.session_count(), 2);
2350 assert_eq!(m.touch_sessions.get(&7).copied(), Some(2));
2351 m.touch_up(7, pos(9.0, 9.0), ts(2), pos(9.0, 9.0));
2354 assert!(!m.input_sessions[0].ended);
2355 assert!(m.input_sessions[1].ended);
2356 assert!(m.touch_sessions.is_empty());
2357 }
2358
2359 #[test]
2360 fn touch_cancel_all_ends_every_finger_and_empties_the_id_map() {
2361 let mut m = GestureAndDragManager::new();
2362 for id in 0..3u64 {
2363 m.touch_down(
2364 id,
2365 pos(id as f32 * 10.0, 0.0),
2366 ts(id),
2367 WindowPosition::Uninitialized,
2368 pos(id as f32 * 10.0, 0.0),
2369 );
2370 }
2371 m.touch_cancel_all();
2372 assert!(m.touch_sessions.is_empty());
2373 assert!(m.input_sessions.iter().all(|s| s.ended));
2374 assert!(!m.touch_move(1, pos(0.0, 0.0), ts(9), pos(0.0, 0.0)));
2375 }
2376
2377 #[test]
2378 fn touch_moves_after_clear_all_sessions_are_dropped_not_resurrected() {
2379 let mut m = GestureAndDragManager::new();
2380 m.touch_down(
2381 1,
2382 pos(0.0, 0.0),
2383 ts(0),
2384 WindowPosition::Uninitialized,
2385 pos(0.0, 0.0),
2386 );
2387 m.clear_all_sessions();
2388 assert!(!m.touch_move(1, pos(5.0, 5.0), ts(1), pos(5.0, 5.0)));
2391 assert_eq!(m.session_count(), 0);
2392 }
2393
2394 #[test]
2395 fn record_sample_for_session_rejects_unknown_and_ended_sessions() {
2396 let mut m = GestureAndDragManager::new();
2397 assert!(!m.record_sample_for_session(u64::MAX, pos(0.0, 0.0), ts(0), pos(0.0, 0.0)));
2398 let id = m.start_input_session(
2399 pos(0.0, 0.0),
2400 ts(0),
2401 0x01,
2402 WindowPosition::Uninitialized,
2403 pos(0.0, 0.0),
2404 );
2405 assert!(m.record_sample_for_session(id, pos(1.0, 0.0), ts(1), pos(1.0, 0.0)));
2406 assert!(!m.record_sample_for_session(0, pos(1.0, 0.0), ts(1), pos(1.0, 0.0)));
2407 m.end_current_session();
2408 assert!(!m.record_sample_for_session(id, pos(2.0, 0.0), ts(2), pos(2.0, 0.0)));
2409 assert_eq!(m.input_sessions[0].samples.len(), 2);
2410 }
2411
2412 #[test]
2413 fn record_sample_for_session_is_also_bounded_by_max_samples() {
2414 let mut m = GestureAndDragManager::new();
2415 m.touch_down(
2416 1,
2417 pos(0.0, 0.0),
2418 ts(0),
2419 WindowPosition::Uninitialized,
2420 pos(0.0, 0.0),
2421 );
2422 for i in 1..=(MAX_SAMPLES_PER_SESSION as u64 + 150) {
2423 assert!(m.touch_move(1, pos(i as f32, 0.0), ts(i), pos(i as f32, 0.0)));
2424 }
2425 assert!(m.input_sessions[0].samples.len() <= MAX_SAMPLES_PER_SESSION);
2426 }
2427
2428 #[test]
2431 fn clear_old_sessions_reaps_stale_sessions_and_their_long_press_ids() {
2432 let mut m = GestureAndDragManager::new();
2433 let old = m.start_input_session(
2434 pos(0.0, 0.0),
2435 ts(0),
2436 0x01,
2437 WindowPosition::Uninitialized,
2438 pos(0.0, 0.0),
2439 );
2440 m.end_current_session();
2441 m.mark_long_press_callback_invoked(old);
2442 let fresh = m.start_input_session(
2443 pos(0.0, 0.0),
2444 ts(10_000),
2445 0x01,
2446 WindowPosition::Uninitialized,
2447 pos(0.0, 0.0),
2448 );
2449 m.mark_long_press_callback_invoked(fresh);
2450 assert_eq!(m.debug_counts(), (2, 2));
2451
2452 m.clear_old_sessions(ts(10_050));
2454 assert_eq!(m.session_count(), 1);
2455 assert_eq!(m.current_session_id(), Some(fresh));
2456 assert_eq!(
2457 m.debug_counts(),
2458 (1, 1),
2459 "long-press bookkeeping must not grow unboundedly"
2460 );
2461 }
2462
2463 #[test]
2464 fn clear_old_sessions_drops_sessions_that_have_no_samples() {
2465 let mut m = GestureAndDragManager::new();
2466 m.input_sessions.push(InputSession {
2467 samples: Vec::new(),
2468 ended: false,
2469 session_id: 99,
2470 window_position_at_start: WindowPosition::Uninitialized,
2471 });
2472 m.clear_old_sessions(ts(0));
2473 assert_eq!(m.session_count(), 0);
2474 }
2475
2476 #[test]
2477 fn clear_old_sessions_with_a_backwards_clock_keeps_everything() {
2478 let mut m = GestureAndDragManager::new();
2479 m.start_input_session(
2480 pos(0.0, 0.0),
2481 ts(5_000),
2482 0x01,
2483 WindowPosition::Uninitialized,
2484 pos(0.0, 0.0),
2485 );
2486 m.clear_old_sessions(ts(0));
2488 assert_eq!(m.session_count(), 1);
2489 }
2490
2491 #[test]
2492 fn clear_all_sessions_resets_both_counters() {
2493 let mut m = GestureAndDragManager::new();
2494 m.start_input_session(
2495 pos(0.0, 0.0),
2496 ts(0),
2497 0x01,
2498 WindowPosition::Uninitialized,
2499 pos(0.0, 0.0),
2500 );
2501 m.mark_current_long_press_invoked();
2502 assert_eq!(m.debug_counts(), (1, 1));
2503 m.clear_all_sessions();
2504 assert_eq!(m.debug_counts(), (0, 0));
2505 assert!(m.get_current_session().is_none());
2506 }
2507
2508 #[test]
2509 fn long_press_invocation_marks_are_deduplicated() {
2510 let mut m = GestureAndDragManager::new();
2511 for _ in 0..100 {
2512 m.mark_long_press_callback_invoked(u64::MAX);
2513 m.mark_long_press_callback_invoked(0);
2514 }
2515 assert_eq!(m.debug_counts(), (0, 2));
2516 m.mark_current_long_press_invoked();
2518 assert_eq!(m.debug_counts(), (0, 2));
2519 }
2520
2521 #[test]
2524 fn detect_drag_fires_exactly_at_the_distance_threshold() {
2525 let m = dragging_manager(pos(0.0, 0.0), pos(3.0, 4.0), 20);
2527 let drag = m.detect_drag().expect("distance == threshold must be a drag");
2528 assert_eq!(drag.direct_distance, 5.0);
2529 assert_eq!(drag.total_distance, 5.0);
2530 assert_eq!(drag.sample_count, 2);
2531 assert_eq!(drag.duration_ms, 20);
2532 assert_eq!(drag.session_id, 1);
2533 assert_eq!(drag.start_position, pos(0.0, 0.0));
2534 assert_eq!(drag.current_position, pos(3.0, 4.0));
2535
2536 let m = dragging_manager(pos(0.0, 0.0), pos(4.9, 0.0), 20);
2538 assert!(m.detect_drag().is_none());
2539 }
2540
2541 #[test]
2542 fn detect_drag_with_nan_movement_returns_none() {
2543 let m = dragging_manager(pos(0.0, 0.0), pos(f32::NAN, f32::NAN), 20);
2544 assert!(
2545 m.detect_drag().is_none(),
2546 "NaN distance is never >= threshold"
2547 );
2548 }
2549
2550 #[test]
2551 fn detect_drag_needs_min_samples_for_gesture() {
2552 let mut m = GestureAndDragManager::new();
2553 m.start_input_session(
2554 pos(0.0, 0.0),
2555 ts(0),
2556 0x01,
2557 WindowPosition::Uninitialized,
2558 pos(500.0, 500.0),
2559 );
2560 assert!(m.detect_drag().is_none(), "one sample is not a gesture");
2561 }
2562
2563 #[test]
2564 fn detect_long_press_honours_time_and_distance_thresholds() {
2565 let m = dragging_manager(pos(10.0, 10.0), pos(10.0, 10.0), 500);
2567 let lp = m.detect_long_press().expect("500ms hold is a long press");
2568 assert_eq!(lp.duration_ms, 500);
2569 assert_eq!(lp.position, pos(10.0, 10.0));
2570 assert!(!lp.callback_invoked);
2571 assert_eq!(lp.session_id, 1);
2572
2573 let m = dragging_manager(pos(10.0, 10.0), pos(10.0, 10.0), 499);
2575 assert!(m.detect_long_press().is_none());
2576
2577 let m = dragging_manager(pos(0.0, 0.0), pos(11.0, 0.0), 800);
2579 assert!(m.detect_long_press().is_none());
2580 }
2581
2582 #[test]
2583 fn detect_long_press_stops_at_button_up_and_after_being_marked() {
2584 let mut m = dragging_manager(pos(10.0, 10.0), pos(10.0, 10.0), 600);
2585 assert!(m.detect_long_press().is_some());
2586
2587 m.mark_current_long_press_invoked();
2588 let lp = m.detect_long_press().expect("still held");
2589 assert!(
2590 lp.callback_invoked,
2591 "a marked long press must report callback_invoked"
2592 );
2593
2594 m.end_current_session();
2595 assert!(
2596 m.detect_long_press().is_none(),
2597 "a released button cannot be a long press"
2598 );
2599 }
2600
2601 #[test]
2604 fn detect_double_click_checks_both_timing_and_distance() {
2605 let mut m = GestureAndDragManager::new();
2606 m.input_sessions = vec![
2607 ended_session(1, vec![sample(10.0, 10.0, 0)]),
2608 ended_session(2, vec![sample(11.0, 11.0, 100)]),
2609 ];
2610 assert!(m.detect_double_click());
2611
2612 m.input_sessions[1].samples[0].timestamp = ts(501);
2614 assert!(!m.detect_double_click());
2615
2616 m.input_sessions[1].samples[0].timestamp = ts(100);
2618 m.input_sessions[1].samples[0].position = pos(100.0, 10.0);
2619 assert!(!m.detect_double_click());
2620
2621 m.input_sessions[1].samples[0].position = pos(11.0, 11.0);
2623 m.input_sessions[1].ended = false;
2624 assert!(!m.detect_double_click());
2625 }
2626
2627 #[test]
2628 fn detect_double_click_needs_two_sessions() {
2629 let mut m = GestureAndDragManager::new();
2630 m.input_sessions = vec![ended_session(1, vec![sample(0.0, 0.0, 0)])];
2631 assert!(!m.detect_double_click());
2632 }
2633
2634 #[test]
2635 fn detect_click_count_counts_up_to_three_and_stops_at_the_first_gap() {
2636 let mut m = GestureAndDragManager::new();
2637 m.input_sessions = vec![
2639 ended_session(1, vec![sample(10.0, 10.0, 0)]),
2640 ended_session(2, vec![sample(10.0, 11.0, 100)]),
2641 ended_session(3, vec![sample(11.0, 10.0, 200)]),
2642 ];
2643 assert_eq!(m.detect_click_count(), 3);
2644
2645 m.input_sessions[2].samples[0].timestamp = ts(900);
2647 assert_eq!(m.detect_click_count(), 1);
2648
2649 m.input_sessions[2].samples[0].timestamp = ts(200);
2652 m.input_sessions[0].samples[0].timestamp = ts(u64::MAX);
2653 assert_eq!(m.detect_click_count(), 3);
2654
2655 m.input_sessions[0].samples[0].timestamp = ts(0);
2657 m.input_sessions[0].samples[0].position = pos(500.0, 500.0);
2658 assert_eq!(m.detect_click_count(), 2);
2659 }
2660
2661 #[test]
2662 fn detect_click_count_ignores_live_sessions_and_defaults_to_one() {
2663 let mut m = GestureAndDragManager::new();
2664 m.start_input_session(
2666 pos(0.0, 0.0),
2667 ts(0),
2668 0x01,
2669 WindowPosition::Uninitialized,
2670 pos(0.0, 0.0),
2671 );
2672 assert_eq!(m.detect_click_count(), 1);
2673 assert_eq!(GestureAndDragManager::new().detect_click_count(), 1);
2674 }
2675
2676 #[test]
2677 fn detect_click_count_with_empty_sample_vec_does_not_panic() {
2678 let mut m = GestureAndDragManager::new();
2679 m.input_sessions = vec![
2680 ended_session(1, Vec::new()),
2681 ended_session(2, vec![sample(0.0, 0.0, 10)]),
2682 ];
2683 assert_eq!(m.detect_click_count(), 1);
2684 assert!(!m.detect_double_click());
2685 }
2686
2687 #[test]
2690 fn drag_direction_is_deterministic_for_stationary_and_nan_input() {
2691 let m = dragging_manager(pos(5.0, 5.0), pos(5.0, 5.0), 10);
2693 assert_eq!(m.get_drag_direction(), Some(GestureDirection::Up));
2694
2695 let m = dragging_manager(pos(0.0, 0.0), pos(f32::NAN, f32::NAN), 10);
2697 assert_eq!(m.get_drag_direction(), Some(GestureDirection::Up));
2698 }
2699
2700 #[test]
2701 fn drag_direction_picks_the_dominant_axis() {
2702 let cases = [
2703 (pos(100.0, 1.0), GestureDirection::Right),
2704 (pos(-100.0, 1.0), GestureDirection::Left),
2705 (pos(1.0, 100.0), GestureDirection::Down),
2706 (pos(1.0, -100.0), GestureDirection::Up),
2707 (pos(50.0, 50.0), GestureDirection::Down),
2709 ];
2710 for (to, expected) in cases {
2711 let m = dragging_manager(pos(0.0, 0.0), to, 10);
2712 assert_eq!(
2713 m.get_drag_direction(),
2714 Some(expected),
2715 "drag to ({}, {})",
2716 to.x,
2717 to.y
2718 );
2719 }
2720 }
2721
2722 #[test]
2723 fn gesture_velocity_returns_none_instead_of_dividing_by_zero() {
2724 let m = dragging_manager(pos(0.0, 0.0), pos(100.0, 0.0), 0);
2726 assert!(m.get_gesture_velocity().is_none());
2727 assert!(!m.is_swipe());
2728 assert!(m.detect_swipe_direction().is_none());
2729
2730 let mut m = GestureAndDragManager::new();
2732 m.start_input_session(
2733 pos(0.0, 0.0),
2734 ts(0),
2735 0x01,
2736 WindowPosition::Uninitialized,
2737 pos(0.0, 0.0),
2738 );
2739 assert!(m.get_gesture_velocity().is_none());
2740 }
2741
2742 #[test]
2743 fn swipe_needs_velocity_above_the_configured_threshold() {
2744 let fast = dragging_manager(pos(0.0, 0.0), pos(60.0, 0.0), 100);
2746 assert!(fast.get_gesture_velocity().unwrap() > 500.0);
2747 assert!(fast.is_swipe());
2748 assert_eq!(
2749 fast.detect_swipe_direction(),
2750 Some(GestureDirection::Right)
2751 );
2752
2753 let slow = dragging_manager(pos(0.0, 0.0), pos(0.0, -40.0), 100);
2755 assert!(!slow.is_swipe());
2756 assert!(slow.detect_swipe_direction().is_none());
2757 }
2758
2759 #[test]
2760 fn gesture_velocity_with_infinite_travel_saturates_to_infinity() {
2761 let m = dragging_manager(pos(-f32::MAX, 0.0), pos(f32::MAX, 0.0), 1);
2762 let v = m.get_gesture_velocity().expect("two samples, 1ms apart");
2763 assert!(v.is_infinite(), "expected saturation to +inf, got {v}");
2764 assert!(m.is_swipe());
2765 }
2766
2767 #[test]
2770 fn pinch_and_rotation_ignore_sequential_mouse_sessions() {
2771 let mut m = GestureAndDragManager::new();
2774 m.start_input_session(
2775 pos(0.0, 0.0),
2776 ts(0),
2777 0x01,
2778 WindowPosition::Uninitialized,
2779 pos(0.0, 0.0),
2780 );
2781 m.end_current_session();
2782 m.start_input_session(
2783 pos(200.0, 0.0),
2784 ts(10),
2785 0x01,
2786 WindowPosition::Uninitialized,
2787 pos(200.0, 0.0),
2788 );
2789 m.record_input_sample(pos(400.0, 0.0), ts(20), 0x01, pos(400.0, 0.0));
2790 assert_eq!(m.session_count(), 2);
2791 assert!(m.detect_pinch().is_none(), "an ended session is not a finger");
2792 assert!(m.detect_rotation().is_none());
2793 }
2794
2795 #[test]
2796 fn pinch_returns_none_when_the_fingers_start_on_top_of_each_other() {
2797 let mut m = GestureAndDragManager::new();
2798 m.touch_down(
2799 1,
2800 pos(100.0, 100.0),
2801 ts(0),
2802 WindowPosition::Uninitialized,
2803 pos(100.0, 100.0),
2804 );
2805 m.touch_down(
2806 2,
2807 pos(100.5, 100.0),
2808 ts(1),
2809 WindowPosition::Uninitialized,
2810 pos(100.5, 100.0),
2811 );
2812 m.touch_move(1, pos(0.0, 100.0), ts(2), pos(0.0, 100.0));
2814 assert!(m.detect_pinch().is_none());
2815 }
2816
2817 #[test]
2818 fn pinch_below_the_scale_threshold_is_not_reported() {
2819 let mut m = GestureAndDragManager::new();
2820 m.touch_down(
2821 1,
2822 pos(100.0, 100.0),
2823 ts(0),
2824 WindowPosition::Uninitialized,
2825 pos(100.0, 100.0),
2826 );
2827 m.touch_down(
2828 2,
2829 pos(200.0, 100.0),
2830 ts(1),
2831 WindowPosition::Uninitialized,
2832 pos(200.0, 100.0),
2833 );
2834 m.touch_move(2, pos(205.0, 100.0), ts(2), pos(205.0, 100.0));
2836 assert!(m.detect_pinch().is_none());
2837 }
2838
2839 #[test]
2840 fn pinch_in_reports_a_scale_below_one() {
2841 let mut m = GestureAndDragManager::new();
2842 m.touch_down(
2843 1,
2844 pos(0.0, 0.0),
2845 ts(0),
2846 WindowPosition::Uninitialized,
2847 pos(0.0, 0.0),
2848 );
2849 m.touch_down(
2850 2,
2851 pos(200.0, 0.0),
2852 ts(1),
2853 WindowPosition::Uninitialized,
2854 pos(200.0, 0.0),
2855 );
2856 m.touch_move(1, pos(50.0, 0.0), ts(10), pos(50.0, 0.0));
2857 m.touch_move(2, pos(150.0, 0.0), ts(11), pos(150.0, 0.0));
2858 let p = m.detect_pinch().expect("200px -> 100px is a pinch in");
2859 assert_eq!(p.initial_distance, 200.0);
2860 assert_eq!(p.current_distance, 100.0);
2861 assert_eq!(p.scale, 0.5);
2862 assert_eq!(p.center, pos(100.0, 0.0));
2863 assert_eq!(p.duration_ms, 10);
2864 }
2865
2866 #[test]
2867 fn pinch_with_infinite_coordinates_saturates_instead_of_panicking() {
2868 let mut m = GestureAndDragManager::new();
2869 m.touch_down(
2870 1,
2871 pos(0.0, 0.0),
2872 ts(0),
2873 WindowPosition::Uninitialized,
2874 pos(0.0, 0.0),
2875 );
2876 m.touch_down(
2877 2,
2878 pos(10.0, 0.0),
2879 ts(1),
2880 WindowPosition::Uninitialized,
2881 pos(10.0, 0.0),
2882 );
2883 m.touch_move(1, pos(-f32::MAX, 0.0), ts(2), pos(-f32::MAX, 0.0));
2885 m.touch_move(2, pos(f32::MAX, 0.0), ts(3), pos(f32::MAX, 0.0));
2886 let p = m.detect_pinch().expect("an overflowing spread is still a pinch");
2887 assert!(
2888 !p.scale.is_finite(),
2889 "expected a saturated (non-finite) scale, got {}",
2890 p.scale
2891 );
2892 assert!(!p.scale.is_nan());
2893 }
2894
2895 #[test]
2896 fn pinch_and_rotation_with_nan_coordinates_never_panic() {
2897 let mut m = GestureAndDragManager::new();
2898 m.touch_down(
2899 1,
2900 pos(f32::NAN, f32::NAN),
2901 ts(0),
2902 WindowPosition::Uninitialized,
2903 pos(f32::NAN, f32::NAN),
2904 );
2905 m.touch_down(
2906 2,
2907 pos(200.0, 100.0),
2908 ts(1),
2909 WindowPosition::Uninitialized,
2910 pos(200.0, 100.0),
2911 );
2912 assert!(m.detect_pinch().is_none_or(|p| !p.scale.is_finite()));
2915 assert!(m
2916 .detect_rotation()
2917 .is_none_or(|r| !r.angle_radians.is_finite()));
2918 }
2919
2920 #[test]
2921 fn rotation_normalisation_terminates_for_extreme_coordinates() {
2922 let mut m = GestureAndDragManager::new();
2925 m.touch_down(
2926 1,
2927 pos(-f32::MAX, -f32::MAX),
2928 ts(0),
2929 WindowPosition::Uninitialized,
2930 pos(0.0, 0.0),
2931 );
2932 m.touch_down(
2933 2,
2934 pos(f32::MAX, f32::MAX),
2935 ts(1),
2936 WindowPosition::Uninitialized,
2937 pos(0.0, 0.0),
2938 );
2939 m.touch_move(2, pos(-f32::MAX, f32::MAX), ts(2), pos(0.0, 0.0));
2940 let r = m.detect_rotation();
2941 assert!(r.is_none_or(|r| r.angle_radians.abs() <= core::f32::consts::PI + 1.0e-4));
2942 }
2943
2944 #[test]
2945 fn rotation_reports_the_signed_angle_between_the_two_fingers() {
2946 let mut m = GestureAndDragManager::new();
2947 m.touch_down(
2948 1,
2949 pos(0.0, 0.0),
2950 ts(0),
2951 WindowPosition::Uninitialized,
2952 pos(0.0, 0.0),
2953 );
2954 m.touch_down(
2955 2,
2956 pos(10.0, 0.0),
2957 ts(1),
2958 WindowPosition::Uninitialized,
2959 pos(10.0, 0.0),
2960 );
2961 m.touch_move(2, pos(0.0, 10.0), ts(50), pos(0.0, 10.0));
2963 let r = m.detect_rotation().expect("a quarter turn is a rotation");
2964 assert!(
2965 (r.angle_radians - core::f32::consts::FRAC_PI_2).abs() < 1.0e-4,
2966 "expected ~PI/2, got {}",
2967 r.angle_radians
2968 );
2969 assert_eq!(r.center, pos(0.0, 5.0));
2970 }
2971
2972 #[test]
2973 fn rotation_below_the_angle_threshold_is_not_reported() {
2974 let mut m = GestureAndDragManager::new();
2975 m.touch_down(
2976 1,
2977 pos(0.0, 0.0),
2978 ts(0),
2979 WindowPosition::Uninitialized,
2980 pos(0.0, 0.0),
2981 );
2982 m.touch_down(
2983 2,
2984 pos(1000.0, 0.0),
2985 ts(1),
2986 WindowPosition::Uninitialized,
2987 pos(1000.0, 0.0),
2988 );
2989 m.touch_move(2, pos(1000.0, 50.0), ts(2), pos(1000.0, 50.0));
2991 assert!(m.detect_rotation().is_none());
2992 }
2993
2994 #[test]
2997 fn injected_native_gestures_win_over_the_in_process_detector() {
2998 let mut m = GestureAndDragManager::new();
2999
3000 m.inject_native_gesture(NativeGestureEvent::DoubleClick);
3001 assert!(m.detect_double_click(), "no sessions, but the OS said so");
3002 m.clear_native_gesture();
3003 assert!(!m.detect_double_click());
3004
3005 let lp = DetectedLongPress {
3006 position: pos(3.0, 4.0),
3007 duration_ms: u64::MAX,
3008 callback_invoked: true,
3009 session_id: u64::MAX,
3010 };
3011 m.inject_native_gesture(NativeGestureEvent::LongPress(lp));
3012 assert_eq!(m.detect_long_press(), Some(lp));
3013
3014 m.inject_native_gesture(NativeGestureEvent::Swipe(GestureDirection::Left));
3015 assert_eq!(m.detect_swipe_direction(), Some(GestureDirection::Left));
3016 assert!(
3017 !m.is_swipe(),
3018 "is_swipe() is velocity-only and ignores the native override"
3019 );
3020
3021 let pinch = DetectedPinch {
3022 scale: f32::INFINITY,
3023 center: pos(0.0, 0.0),
3024 initial_distance: 0.0,
3025 current_distance: f32::NAN,
3026 duration_ms: 0,
3027 };
3028 m.inject_native_gesture(NativeGestureEvent::Pinch(pinch));
3029 let got = m.detect_pinch().expect("native pinch is passed through");
3030 assert!(got.scale.is_infinite());
3031
3032 let rot = DetectedRotation {
3033 angle_radians: -core::f32::consts::PI,
3034 center: pos(1.0, 1.0),
3035 duration_ms: 7,
3036 };
3037 m.inject_native_gesture(NativeGestureEvent::Rotation(rot));
3038 assert_eq!(m.detect_rotation(), Some(rot));
3039
3040 m.clear_native_gesture();
3041 assert!(m.detect_long_press().is_none());
3042 assert!(m.detect_pinch().is_none());
3043 assert!(m.detect_rotation().is_none());
3044 assert!(m.detect_swipe_direction().is_none());
3045 }
3046
3047 #[test]
3050 fn pen_state_stores_extremes_verbatim_and_tracks_the_previous_state() {
3051 let mut m = GestureAndDragManager::new();
3052 m.update_pen_state(
3053 pos(1.0, 2.0),
3054 f32::NAN,
3055 (f32::INFINITY, f32::NEG_INFINITY),
3056 true,
3057 true,
3058 true,
3059 u64::MAX,
3060 );
3061 assert!(m.pen_event_pending);
3062 assert!(m.get_previous_pen_state().is_none());
3063 let pen = *m.get_pen_state().expect("pen state was just set");
3064 assert!(pen.pressure.is_nan());
3065 assert!(pen.tilt.x_tilt.is_infinite());
3066 assert!(pen.tilt.y_tilt.is_infinite());
3067 assert!(pen.in_contact && pen.is_eraser && pen.barrel_button_pressed);
3068 assert_eq!(pen.device_id, u64::MAX);
3069 assert_eq!(pen.tangential_pressure, 0.0);
3071 assert_eq!(pen.barrel_roll_rad, 0.0);
3072 assert_eq!(pen.tool_id, 0);
3073
3074 m.clear_pen_event_pending();
3075 assert!(!m.pen_event_pending);
3076
3077 m.update_pen_state_full(
3078 pos(0.0, 0.0),
3079 1.0,
3080 (0.0, 0.0),
3081 false,
3082 false,
3083 false,
3084 0,
3085 f32::NAN,
3086 -f32::MAX,
3087 u32::MAX,
3088 );
3089 assert!(m.pen_event_pending);
3090 let prev = *m.get_previous_pen_state().expect("previous pen state kept");
3091 assert_eq!(prev.device_id, u64::MAX);
3092 let now = *m.get_pen_state().unwrap();
3093 assert!(now.tangential_pressure.is_nan());
3094 assert_eq!(now.barrel_roll_rad, -f32::MAX);
3095 assert_eq!(now.tool_id, u32::MAX);
3096
3097 m.clear_pen_state();
3098 assert!(m.get_pen_state().is_none());
3099 assert_eq!(m.get_previous_pen_state().map(|p| p.tool_id), Some(u32::MAX));
3100 assert!(m.pen_event_pending);
3101
3102 m.clear_pen_state();
3104 assert!(m.get_pen_state().is_none());
3105 assert!(m.get_previous_pen_state().is_none());
3106 }
3107
3108 #[test]
3109 fn pad_state_round_trips_and_clears() {
3110 let mut m = GestureAndDragManager::new();
3111 assert!(m.get_pad_state().is_none());
3112 m.update_pad_state(WacomPadState {
3113 express_keys: 0b1010,
3114 touch_ring: f32::NAN,
3115 touch_ring_active: true,
3116 device_id: u64::MAX,
3117 });
3118 let pad = *m.get_pad_state().expect("pad state was just set");
3119 assert!(!pad.express_key(0));
3120 assert!(pad.express_key(1));
3121 assert!(!pad.express_key(2));
3122 assert!(pad.express_key(3));
3123 assert!(pad.touch_ring.is_nan());
3124 assert_eq!(pad.device_id, u64::MAX);
3125 m.clear_pad_state();
3126 assert!(m.get_pad_state().is_none());
3127 m.clear_pad_state();
3128 assert!(m.get_pad_state().is_none());
3129 }
3130
3131 #[test]
3134 fn drag_deltas_use_window_local_and_screen_coordinates_independently() {
3135 let mut m = GestureAndDragManager::new();
3136 m.start_input_session(
3137 pos(10.0, 10.0),
3138 ts(0),
3139 0x01,
3140 WindowPosition::Initialized(PhysicalPositionI32::new(100, 100)),
3141 pos(110.0, 110.0),
3142 );
3143 assert_eq!(m.get_drag_delta(), Some((0.0, 0.0)));
3145 assert_eq!(m.get_drag_delta_screen(), Some((0.0, 0.0)));
3146 assert!(m.get_drag_delta_screen_incremental().is_none());
3147
3148 m.record_input_sample(pos(15.0, 10.0), ts(10), 0x01, pos(120.0, 130.0));
3149 m.record_input_sample(pos(20.0, 10.0), ts(20), 0x01, pos(125.0, 132.0));
3150 assert_eq!(m.get_drag_delta(), Some((10.0, 0.0)));
3151 assert_eq!(m.get_drag_delta_screen(), Some((15.0, 22.0)));
3152 assert_eq!(m.get_drag_delta_screen_incremental(), Some((5.0, 2.0)));
3153 assert_eq!(
3154 m.get_window_position_at_session_start(),
3155 Some(WindowPosition::Initialized(PhysicalPositionI32::new(
3156 100, 100
3157 )))
3158 );
3159 assert_eq!(m.get_current_mouse_position(), Some(pos(20.0, 10.0)));
3160 }
3161
3162 #[test]
3163 fn drag_deltas_at_f32_extremes_stay_finite_or_saturate() {
3164 let m = dragging_manager(pos(-f32::MAX, -f32::MAX), pos(f32::MAX, f32::MAX), 5);
3165 let (dx, dy) = m.get_drag_delta().expect("two samples");
3166 assert!(dx.is_infinite() && dy.is_infinite());
3167 let (sx, sy) = m.get_drag_delta_screen().expect("two samples");
3168 assert!(sx.is_infinite() && sy.is_infinite());
3169 }
3170
3171 #[test]
3174 fn activating_a_node_drag_without_a_detected_drag_is_a_no_op() {
3175 let mut m = GestureAndDragManager::new();
3176 m.activate_node_drag(DomId::ROOT_ID, NodeId::new(1), DragData::new(), None);
3178 assert!(!m.is_dragging());
3179
3180 let mut m = dragging_manager(pos(0.0, 0.0), pos(1.0, 1.0), 10);
3182 m.activate_node_drag(DomId::ROOT_ID, NodeId::new(1), DragData::new(), None);
3183 assert!(!m.is_node_drag_active());
3184 m.activate_window_drag(WindowPosition::Uninitialized, None);
3185 assert!(!m.is_window_dragging());
3186 }
3187
3188 #[test]
3189 fn node_drag_context_tracks_its_own_node_and_drop_target() {
3190 let mut m = dragging_manager(pos(0.0, 0.0), pos(100.0, 0.0), 10);
3191 let mut data = DragData::new();
3192 data.set_text("payload");
3193 m.activate_node_drag(DomId::ROOT_ID, NodeId::new(4), data, None);
3194
3195 assert!(m.is_dragging());
3196 assert!(m.is_node_drag_active());
3197 assert!(m.is_node_dragging(DomId::ROOT_ID, NodeId::new(4)));
3198 assert!(!m.is_node_dragging(DomId::ROOT_ID, NodeId::new(5)));
3199 assert!(!m.is_node_dragging(DomId { inner: 7 }, NodeId::new(4)));
3200 assert!(!m.is_window_dragging());
3201 assert!(!m.is_file_dropping());
3202 assert!(!m.is_text_selection_dragging());
3203 assert!(!m.is_scrollbar_dragging());
3204 assert!(m.get_window_drag_delta().is_none());
3205 assert!(m.get_scrollbar_scroll_offset().is_none());
3206
3207 m.update_active_drag_positions(pos(42.0, -7.0));
3208 assert_eq!(
3209 m.get_drag_context().unwrap().current_position(),
3210 pos(42.0, -7.0)
3211 );
3212
3213 m.update_drop_target(Some(azul_core::dom::DomNodeId {
3214 dom: DomId::ROOT_ID,
3215 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(9))),
3216 }));
3217 let nd = m
3218 .get_drag_context()
3219 .and_then(DragContext::as_node_drag)
3220 .expect("node drag");
3221 assert_eq!(
3222 nd.current_drop_target
3223 .into_option()
3224 .and_then(|t| t.node.into_crate_internal()),
3225 Some(NodeId::new(9))
3226 );
3227 assert_eq!(nd.drag_data.get_data("text/plain"), Some(&b"payload"[..]));
3228
3229 m.update_drop_target(None);
3231 assert!(m
3232 .get_drag_context()
3233 .and_then(DragContext::as_node_drag)
3234 .unwrap()
3235 .current_drop_target
3236 .into_option()
3237 .is_none());
3238
3239 m.update_auto_scroll_direction(AutoScrollDirection::DownRight);
3241 assert!(m.is_node_drag_active());
3242
3243 let ctx = m.end_drag().expect("the drag context is returned");
3244 assert_eq!(ctx.session_id, 1);
3245 assert!(!m.is_dragging());
3246 assert!(m.end_drag().is_none());
3247 }
3248
3249 #[test]
3250 fn drop_target_and_auto_scroll_updates_without_a_drag_do_not_panic() {
3251 let mut m = GestureAndDragManager::new();
3252 m.update_drop_target(None);
3253 m.update_active_drag_positions(pos(f32::NAN, f32::INFINITY));
3254 m.update_auto_scroll_direction(AutoScrollDirection::UpLeft);
3255 m.cancel_drag();
3256 assert!(!m.is_dragging());
3257 assert!(m.get_drag_context_mut().is_none());
3258 }
3259
3260 #[test]
3261 fn text_selection_context_accepts_the_auto_scroll_direction() {
3262 let mut m = GestureAndDragManager::new();
3263 m.active_drag = Some(DragContext::text_selection(
3264 DomId::ROOT_ID,
3265 NodeId::new(2),
3266 pos(0.0, 0.0),
3267 11,
3268 ));
3269 assert!(m.is_text_selection_dragging());
3270 assert!(!m.is_node_drag_active());
3271 m.update_auto_scroll_direction(AutoScrollDirection::DownRight);
3272 assert_eq!(
3273 m.get_drag_context()
3274 .and_then(DragContext::as_text_selection)
3275 .map(|t| t.auto_scroll_direction),
3276 Some(AutoScrollDirection::DownRight)
3277 );
3278 m.update_drop_target(None);
3280 assert!(m.is_text_selection_dragging());
3281
3282 m.cancel_drag();
3283 assert!(!m.is_dragging());
3284 assert!(!m.is_text_selection_dragging());
3285 }
3286
3287 fn window_dragging_manager(initial: WindowPosition) -> GestureAndDragManager {
3290 let mut m = dragging_manager(pos(0.0, 0.0), pos(100.0, 0.0), 10);
3291 m.activate_window_drag(initial, None);
3292 assert!(m.is_window_dragging());
3293 m
3294 }
3295
3296 #[test]
3297 fn window_drag_delta_needs_an_initialized_window_position() {
3298 let m = window_dragging_manager(WindowPosition::Uninitialized);
3299 assert!(m.get_window_drag_delta().is_none());
3300 assert!(m.get_window_position_from_drag().is_none());
3301 }
3302
3303 #[test]
3304 fn window_drag_delta_is_measured_from_the_drag_start() {
3305 let mut m =
3306 window_dragging_manager(WindowPosition::Initialized(PhysicalPositionI32::new(10, 20)));
3307 m.update_active_drag_positions(pos(30.5, -20.9));
3308 assert_eq!(m.get_window_drag_delta(), Some((30, -20)));
3310 assert_eq!(
3311 m.get_window_position_from_drag(),
3312 Some(WindowPosition::Initialized(PhysicalPositionI32::new(40, 0)))
3313 );
3314 }
3315
3316 #[test]
3317 fn window_drag_delta_saturates_the_float_to_int_cast() {
3318 let mut m =
3319 window_dragging_manager(WindowPosition::Initialized(PhysicalPositionI32::new(0, 0)));
3320 m.update_active_drag_positions(pos(f32::MAX, -f32::MAX));
3321 assert_eq!(
3322 m.get_window_drag_delta(),
3323 Some((i32::MAX, i32::MIN)),
3324 "float->int casts must saturate, not wrap or trap"
3325 );
3326 assert_eq!(
3327 m.get_window_position_from_drag(),
3328 Some(WindowPosition::Initialized(PhysicalPositionI32::new(
3329 i32::MAX,
3330 i32::MIN
3331 )))
3332 );
3333 }
3334
3335 #[test]
3336 fn window_drag_delta_with_nan_position_is_zero_not_a_trap() {
3337 let mut m =
3338 window_dragging_manager(WindowPosition::Initialized(PhysicalPositionI32::new(3, 4)));
3339 m.update_active_drag_positions(pos(f32::NAN, f32::NAN));
3340 assert_eq!(m.get_window_drag_delta(), Some((0, 0)));
3342 assert_eq!(
3343 m.get_window_position_from_drag(),
3344 Some(WindowPosition::Initialized(PhysicalPositionI32::new(3, 4)))
3345 );
3346 }
3347
3348 #[test]
3349 fn window_position_from_drag_at_the_i32_extremes_does_not_overflow() {
3350 let mut m = window_dragging_manager(WindowPosition::Initialized(
3352 PhysicalPositionI32::new(i32::MAX, i32::MAX),
3353 ));
3354 m.update_active_drag_positions(pos(-f32::MAX, -f32::MAX));
3355 assert_eq!(
3356 m.get_window_position_from_drag(),
3357 Some(WindowPosition::Initialized(PhysicalPositionI32::new(-1, -1)))
3358 );
3359 }
3360
3361 fn scrollbar_manager(
3364 start_offset: f32,
3365 track: f32,
3366 content: f32,
3367 viewport: f32,
3368 ) -> GestureAndDragManager {
3369 let mut m = GestureAndDragManager::new();
3370 m.active_drag = Some(DragContext::scrollbar_thumb(
3371 DomId::ROOT_ID,
3372 NodeId::new(1),
3373 ScrollbarAxis::Vertical,
3374 pos(0.0, 0.0),
3375 start_offset,
3376 track,
3377 content,
3378 viewport,
3379 1,
3380 ));
3381 m
3382 }
3383
3384 #[test]
3385 fn scrollbar_offset_scales_the_mouse_delta_and_clamps_to_the_range() {
3386 let mut m = scrollbar_manager(0.0, 100.0, 1000.0, 100.0);
3387 assert!(m.is_scrollbar_dragging());
3388 assert_eq!(m.get_scrollbar_scroll_offset(), Some(0.0));
3389
3390 m.update_active_drag_positions(pos(0.0, 45.0));
3392 let half = m.get_scrollbar_scroll_offset().expect("scrollbar drag");
3393 assert!((half - 450.0).abs() < 0.5, "expected ~450, got {half}");
3394
3395 m.update_active_drag_positions(pos(0.0, 1.0e9));
3397 assert_eq!(m.get_scrollbar_scroll_offset(), Some(900.0));
3398
3399 m.update_active_drag_positions(pos(0.0, -1.0e9));
3401 assert_eq!(m.get_scrollbar_scroll_offset(), Some(0.0));
3402 }
3403
3404 #[test]
3405 fn scrollbar_offset_with_nothing_to_scroll_returns_the_start_offset() {
3406 let mut m = scrollbar_manager(42.0, 100.0, 50.0, 100.0);
3408 m.update_active_drag_positions(pos(0.0, 500.0));
3409 assert_eq!(m.get_scrollbar_scroll_offset(), Some(42.0));
3410
3411 let mut m = scrollbar_manager(7.0, 0.0, 1000.0, 100.0);
3413 m.update_active_drag_positions(pos(0.0, 500.0));
3414 assert_eq!(m.get_scrollbar_scroll_offset(), Some(7.0));
3415 }
3416
3417 #[test]
3418 fn scrollbar_offset_with_a_nan_mouse_position_does_not_panic() {
3419 let mut m = scrollbar_manager(0.0, 100.0, 1000.0, 100.0);
3420 m.update_active_drag_positions(pos(f32::NAN, f32::NAN));
3421 let v = m.get_scrollbar_scroll_offset();
3422 assert!(
3423 v.is_some_and(f32::is_nan),
3424 "a NaN mouse position must propagate as NaN, not panic: {v:?}"
3425 );
3426 }
3427
3428 #[cfg(feature = "std")]
3431 #[test]
3432 fn allocate_event_id_is_strictly_monotonic() {
3433 let a = allocate_event_id();
3434 let b = allocate_event_id();
3435 let c = allocate_event_id();
3436 assert!(a < b && b < c, "ids must increase: {a} {b} {c}");
3437 }
3438
3439 #[cfg(not(feature = "std"))]
3440 #[test]
3441 fn allocate_event_id_is_zero_without_std() {
3442 assert_eq!(allocate_event_id(), 0);
3443 }
3444
3445 #[cfg(feature = "std")]
3446 #[test]
3447 fn recorded_samples_get_distinct_event_ids() {
3448 let mut m = GestureAndDragManager::new();
3449 m.start_input_session(
3450 pos(0.0, 0.0),
3451 ts(0),
3452 0x01,
3453 WindowPosition::Uninitialized,
3454 pos(0.0, 0.0),
3455 );
3456 m.record_input_sample(pos(1.0, 0.0), ts(1), 0x01, pos(1.0, 0.0));
3457 let s = m.get_current_session().unwrap();
3458 assert_ne!(s.samples[0].event_id, s.samples[1].event_id);
3459 assert!(s.samples[0].event_id < s.samples[1].event_id);
3460 }
3461}