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
46#[allow(clippy::cast_possible_truncation)] fn duration_to_millis(duration: CoreDuration) -> u64 {
52 match duration {
53 #[cfg(feature = "std")]
54 CoreDuration::System(system_diff) => {
55 let std_duration: std::time::Duration = system_diff.into();
56 std_duration.as_millis() as u64
57 }
58 #[cfg(not(feature = "std"))]
59 CoreDuration::System(system_diff) => {
60 system_diff.secs * 1000 + (system_diff.nanos / 1_000_000) as u64
62 }
63 CoreDuration::Tick(tick_diff) => {
64 tick_diff.tick_diff
68 }
69 }
70}
71
72pub const MAX_SAMPLES_PER_SESSION: usize = 1000;
77
78pub const DEFAULT_SAMPLE_TIMEOUT_MS: u64 = 2000;
83
84const DRAIN_BATCH_SIZE: usize = 100;
88
89pub const TOUCH_CONTACT_BUTTON_STATE: u8 = 0x01;
94
95#[derive(Debug, Clone, Copy, PartialEq)]
97pub struct GestureDetectionConfig {
98 pub drag_distance_threshold: f32,
100 pub double_click_time_threshold_ms: u64,
102 pub double_click_distance_threshold: f32,
104 pub long_press_time_threshold_ms: u64,
106 pub long_press_distance_threshold: f32,
108 pub min_samples_for_gesture: usize,
110 pub swipe_velocity_threshold: f32,
112 pub pinch_scale_threshold: f32,
114 pub rotation_angle_threshold: f32,
116 pub sample_cleanup_interval_ms: u64,
118}
119
120impl Default for GestureDetectionConfig {
121 fn default() -> Self {
122 Self {
123 drag_distance_threshold: 5.0,
124 double_click_time_threshold_ms: 500,
125 double_click_distance_threshold: 5.0,
126 long_press_time_threshold_ms: 500,
127 long_press_distance_threshold: 10.0,
128 min_samples_for_gesture: 2,
129 swipe_velocity_threshold: 500.0, pinch_scale_threshold: 0.1, rotation_angle_threshold: 0.1, sample_cleanup_interval_ms: DEFAULT_SAMPLE_TIMEOUT_MS,
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq)]
139pub struct InputSample {
140 pub position: LogicalPosition,
142 pub screen_position: LogicalPosition,
151 pub timestamp: CoreInstant,
153 pub button_state: u8,
155 pub event_id: u64,
157 pub pressure: f32,
159 pub tilt: (f32, f32),
162 pub touch_radius: (f32, f32),
165}
166
167impl_option!(
168 InputSample,
169 OptionInputSample,
170 copy = false,
171 [Debug, Clone, PartialEq]
172);
173
174#[derive(Debug, Clone, PartialEq)]
176pub struct InputSession {
177 pub samples: Vec<InputSample>,
179 pub ended: bool,
181 pub session_id: u64,
183 pub window_position_at_start: WindowPosition,
186}
187
188impl InputSession {
189 fn new(session_id: u64, first_sample: InputSample, window_position: WindowPosition) -> Self {
191 Self {
192 samples: vec![first_sample],
193 ended: false,
194 session_id,
195 window_position_at_start: window_position,
196 }
197 }
198
199 #[must_use] pub fn first_sample(&self) -> Option<&InputSample> {
201 self.samples.first()
202 }
203
204 #[must_use] pub fn last_sample(&self) -> Option<&InputSample> {
206 self.samples.last()
207 }
208
209 #[must_use] pub fn duration_ms(&self) -> Option<u64> {
211 let first = self.first_sample()?;
212 let last = self.last_sample()?;
213 let duration = last.timestamp.duration_since(&first.timestamp);
214 Some(duration_to_millis(duration))
215 }
216
217 #[must_use] pub fn total_distance(&self) -> f32 {
219 if self.samples.len() < 2 {
220 return 0.0;
221 }
222
223 let mut total = 0.0;
224 for i in 1..self.samples.len() {
225 let prev = &self.samples[i - 1];
226 let curr = &self.samples[i];
227 let dx = curr.position.x - prev.position.x;
228 let dy = curr.position.y - prev.position.y;
229 total += dx.hypot(dy);
230 }
231 total
232 }
233
234 #[must_use] pub fn direct_distance(&self) -> Option<f32> {
236 let first = self.first_sample()?;
237 let last = self.last_sample()?;
238 let dx = last.position.x - first.position.x;
239 let dy = last.position.y - first.position.y;
240 Some(dx.hypot(dy))
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq)]
246pub struct DetectedDrag {
247 pub start_position: LogicalPosition,
249 pub current_position: LogicalPosition,
251 pub direct_distance: f32,
253 pub total_distance: f32,
255 pub duration_ms: u64,
257 pub sample_count: usize,
259 pub session_id: u64,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265#[repr(C)]
266pub struct DetectedLongPress {
267 pub position: LogicalPosition,
269 pub duration_ms: u64,
271 pub callback_invoked: bool,
273 pub session_id: u64,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279#[repr(C)]
280pub enum GestureDirection {
281 Up,
282 Down,
283 Left,
284 Right,
285}
286
287impl_option!(
288 GestureDirection,
289 OptionGestureDirection,
290 [Debug, Clone, Copy, PartialEq, Eq]
291);
292impl_option!(
293 DetectedPinch,
294 OptionDetectedPinch,
295 [Debug, Clone, Copy, PartialEq]
296);
297impl_option!(
298 DetectedRotation,
299 OptionDetectedRotation,
300 [Debug, Clone, Copy, PartialEq]
301);
302impl_option!(
303 DetectedLongPress,
304 OptionDetectedLongPress,
305 [Debug, Clone, Copy, PartialEq, Eq]
306);
307
308#[derive(Debug, Clone, Copy, PartialEq)]
310#[repr(C)]
311pub struct DetectedPinch {
312 pub scale: f32,
314 pub center: LogicalPosition,
316 pub initial_distance: f32,
318 pub current_distance: f32,
320 pub duration_ms: u64,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq)]
326#[repr(C)]
327pub struct DetectedRotation {
328 pub angle_radians: f32,
330 pub center: LogicalPosition,
332 pub duration_ms: u64,
334}
335
336
337#[derive(Debug, Clone, Copy, PartialEq)]
339#[repr(C)]
340pub struct PenState {
341 pub position: LogicalPosition,
343 pub pressure: f32,
345 pub tilt: crate::callbacks::PenTilt,
347 pub in_contact: bool,
349 pub is_eraser: bool,
351 pub barrel_button_pressed: bool,
353 pub device_id: u64,
355 pub tangential_pressure: f32,
359 pub barrel_roll_rad: f32,
366 pub tool_id: u32,
372}
373
374impl_option!(PenState, OptionPenState, [Debug, Clone, Copy, PartialEq]);
375
376impl Default for PenState {
377 fn default() -> Self {
378 Self {
379 position: LogicalPosition::zero(),
380 pressure: 0.0,
381 tilt: crate::callbacks::PenTilt {
382 x_tilt: 0.0,
383 y_tilt: 0.0,
384 },
385 in_contact: false,
386 is_eraser: false,
387 barrel_button_pressed: false,
388 device_id: 0,
389 tangential_pressure: 0.0,
390 barrel_roll_rad: 0.0,
391 tool_id: 0,
392 }
393 }
394}
395
396#[derive(Debug, Clone, Copy, PartialEq)]
404#[repr(C)]
405pub struct WacomPadState {
406 pub express_keys: u32,
409 pub touch_ring: f32,
412 pub touch_ring_active: bool,
414 pub device_id: u64,
416}
417
418impl_option!(
419 WacomPadState,
420 OptionWacomPadState,
421 [Debug, Clone, Copy, PartialEq]
422);
423
424impl Default for WacomPadState {
425 fn default() -> Self {
426 Self {
427 express_keys: 0,
428 touch_ring: 0.0,
429 touch_ring_active: false,
430 device_id: 0,
431 }
432 }
433}
434
435impl WacomPadState {
436 #[must_use] pub const fn express_key(&self, index: u32) -> bool {
438 index < 32 && (self.express_keys & (1u32 << index)) != 0
439 }
440}
441
442#[derive(Debug, Clone, PartialEq)]
456pub struct GestureAndDragManager {
457 pub config: GestureDetectionConfig,
459 pub input_sessions: Vec<InputSession>,
461 pub active_drag: Option<DragContext>,
463 pub pen_state: Option<PenState>,
465 pub previous_pen_state: Option<PenState>,
467 pub pen_event_pending: bool,
469 pub pad_state: Option<WacomPadState>,
472 long_press_callbacks_invoked: Vec<u64>,
474 next_session_id: u64,
476 pub native_gesture: Option<NativeGestureEvent>,
489 touch_sessions: alloc::collections::btree_map::BTreeMap<u64, u64>,
497}
498
499#[derive(Debug, Clone, Copy, PartialEq)]
510#[repr(C, u8)]
511pub enum NativeGestureEvent {
512 DoubleClick,
514 LongPress(DetectedLongPress),
517 Swipe(GestureDirection),
520 Pinch(DetectedPinch),
523 Rotation(DetectedRotation),
526}
527
528
529impl Default for GestureAndDragManager {
530 fn default() -> Self {
531 Self::new()
532 }
533}
534
535impl GestureAndDragManager {
536 #[must_use] pub const fn debug_counts(&self) -> (usize, usize) {
539 (self.input_sessions.len(), self.long_press_callbacks_invoked.len())
540 }
541
542 #[must_use] pub fn new() -> Self {
544 Self {
545 config: GestureDetectionConfig::default(),
546 input_sessions: Vec::new(),
547 next_session_id: 1,
548 active_drag: None,
549 pen_state: None,
550 previous_pen_state: None,
551 pen_event_pending: false,
552 pad_state: None,
553 long_press_callbacks_invoked: Vec::new(),
554 native_gesture: None,
555 touch_sessions: alloc::collections::btree_map::BTreeMap::new(),
556 }
557 }
558
559 pub const fn inject_native_gesture(&mut self, gesture: NativeGestureEvent) {
565 self.native_gesture = Some(gesture);
566 }
567
568 pub const fn clear_native_gesture(&mut self) {
572 self.native_gesture = None;
573 }
574
575 #[must_use] pub fn with_config(config: GestureDetectionConfig) -> Self {
577 Self {
578 config,
579 ..Self::new()
580 }
581 }
582
583 pub fn start_input_session(
595 &mut self,
596 position: LogicalPosition,
597 timestamp: CoreInstant,
598 button_state: u8,
599 window_position: WindowPosition,
600 screen_position: LogicalPosition,
601 ) -> u64 {
602 self.start_input_session_with_pen(
603 position,
604 timestamp,
605 button_state,
606 allocate_event_id(),
607 0.5, (0.0, 0.0), (0.0, 0.0), window_position,
611 screen_position,
612 )
613 }
614
615 pub fn start_input_session_with_pen(
617 &mut self,
618 position: LogicalPosition,
619 timestamp: CoreInstant,
620 button_state: u8,
621 event_id: u64,
622 pressure: f32,
623 tilt: (f32, f32),
624 touch_radius: (f32, f32),
625 window_position: WindowPosition,
626 screen_position: LogicalPosition,
627 ) -> u64 {
628 let last_ended_idx = self.input_sessions.iter().rposition(|s| s.ended);
632 let mut idx = 0usize;
633 self.input_sessions.retain(|session| {
634 let keep = !session.ended || Some(idx) == last_ended_idx;
635 idx += 1;
636 keep
637 });
638
639 let session_id = self.next_session_id;
640 self.next_session_id += 1;
641
642 let sample = InputSample {
643 position,
644 screen_position,
645 timestamp,
646 button_state,
647 event_id,
648 pressure,
649 tilt,
650 touch_radius,
651 };
652
653 let session = InputSession::new(session_id, sample, window_position);
654 self.input_sessions.push(session);
655
656 session_id
657 }
658
659 pub fn record_input_sample(
666 &mut self,
667 position: LogicalPosition,
668 timestamp: CoreInstant,
669 button_state: u8,
670 screen_position: LogicalPosition,
671 ) -> bool {
672 self.record_input_sample_with_pen(
673 position,
674 timestamp,
675 button_state,
676 allocate_event_id(),
677 0.5, (0.0, 0.0), (0.0, 0.0), screen_position,
681 )
682 }
683
684 pub fn record_input_sample_with_pen(
686 &mut self,
687 position: LogicalPosition,
688 timestamp: CoreInstant,
689 button_state: u8,
690 event_id: u64,
691 pressure: f32,
692 tilt: (f32, f32),
693 touch_radius: (f32, f32),
694 screen_position: LogicalPosition,
695 ) -> bool {
696 let Some(session) = self.input_sessions.last_mut() else {
697 return false;
698 };
699
700 if session.ended {
701 return false;
702 }
703
704 if session.samples.len() >= MAX_SAMPLES_PER_SESSION {
706 let remove_count = session.samples.len() - MAX_SAMPLES_PER_SESSION + DRAIN_BATCH_SIZE;
708 session.samples.drain(0..remove_count);
709 }
710
711 session.samples.push(InputSample {
712 position,
713 screen_position,
714 timestamp,
715 button_state,
716 event_id,
717 pressure,
718 tilt,
719 touch_radius,
720 });
721
722 true
723 }
724
725 pub fn end_current_session(&mut self) {
730 if let Some(session) = self.input_sessions.last_mut() {
731 session.ended = true;
732 }
733 }
734
735 pub fn touch_down(
739 &mut self,
740 touch_id: u64,
741 position: LogicalPosition,
742 timestamp: CoreInstant,
743 window_position: WindowPosition,
744 screen_position: LogicalPosition,
745 ) {
746 let session_id = self.start_input_session(
747 position,
748 timestamp,
749 TOUCH_CONTACT_BUTTON_STATE,
750 window_position,
751 screen_position,
752 );
753 self.touch_sessions.insert(touch_id, session_id);
754 }
755
756 pub fn touch_move(
761 &mut self,
762 touch_id: u64,
763 position: LogicalPosition,
764 timestamp: CoreInstant,
765 screen_position: LogicalPosition,
766 ) -> bool {
767 let Some(session_id) = self.touch_sessions.get(&touch_id).copied() else {
768 return false;
769 };
770 self.record_sample_for_session(session_id, position, timestamp, screen_position)
771 }
772
773 pub fn touch_up(
776 &mut self,
777 touch_id: u64,
778 position: LogicalPosition,
779 timestamp: CoreInstant,
780 screen_position: LogicalPosition,
781 ) {
782 let Some(session_id) = self.touch_sessions.remove(&touch_id) else {
783 return;
784 };
785 let _ = self.record_sample_for_session(session_id, position, timestamp, screen_position);
786 if let Some(session) = self
787 .input_sessions
788 .iter_mut()
789 .find(|s| s.session_id == session_id)
790 {
791 session.ended = true;
792 }
793 }
794
795 pub fn touch_cancel_all(&mut self) {
798 let ids: Vec<u64> = self.touch_sessions.values().copied().collect();
799 self.touch_sessions.clear();
800 for session_id in ids {
801 if let Some(session) = self
802 .input_sessions
803 .iter_mut()
804 .find(|s| s.session_id == session_id)
805 {
806 session.ended = true;
807 }
808 }
809 }
810
811 fn record_sample_for_session(
815 &mut self,
816 session_id: u64,
817 position: LogicalPosition,
818 timestamp: CoreInstant,
819 screen_position: LogicalPosition,
820 ) -> bool {
821 let Some(session) = self
822 .input_sessions
823 .iter_mut()
824 .find(|s| s.session_id == session_id)
825 else {
826 return false;
827 };
828 if session.ended {
829 return false;
830 }
831 if session.samples.len() >= MAX_SAMPLES_PER_SESSION {
832 let remove_count =
833 session.samples.len() - MAX_SAMPLES_PER_SESSION + DRAIN_BATCH_SIZE;
834 session.samples.drain(0..remove_count);
835 }
836 session.samples.push(InputSample {
837 position,
838 screen_position,
839 timestamp,
840 button_state: TOUCH_CONTACT_BUTTON_STATE,
841 event_id: allocate_event_id(),
842 pressure: 0.5,
843 tilt: (0.0, 0.0),
844 touch_radius: (0.0, 0.0),
845 });
846 true
847 }
848
849 #[allow(clippy::needless_pass_by_value)]
856 pub fn clear_old_sessions(&mut self, current_time: CoreInstant) {
857 self.input_sessions.retain(|session| {
858 if let Some(last_sample) = session.last_sample() {
859 let duration = current_time.duration_since(&last_sample.timestamp);
860 let age_ms = duration_to_millis(duration);
861 age_ms < self.config.sample_cleanup_interval_ms
862 } else {
863 false
864 }
865 });
866
867 let valid_session_ids: Vec<u64> =
869 self.input_sessions.iter().map(|s| s.session_id).collect();
870
871 self.long_press_callbacks_invoked
872 .retain(|id| valid_session_ids.contains(id));
873 }
874
875 pub fn clear_all_sessions(&mut self) {
879 self.input_sessions.clear();
880 self.long_press_callbacks_invoked.clear();
881 }
882
883 pub const fn update_pen_state(
890 &mut self,
891 position: LogicalPosition,
892 pressure: f32,
893 tilt: (f32, f32),
894 in_contact: bool,
895 is_eraser: bool,
896 barrel_button_pressed: bool,
897 device_id: u64,
898 ) {
899 self.update_pen_state_full(
900 position,
901 pressure,
902 tilt,
903 in_contact,
904 is_eraser,
905 barrel_button_pressed,
906 device_id,
907 0.0,
908 0.0,
909 0,
910 );
911 }
912
913 pub const fn update_pen_state_full(
916 &mut self,
917 position: LogicalPosition,
918 pressure: f32,
919 tilt: (f32, f32),
920 in_contact: bool,
921 is_eraser: bool,
922 barrel_button_pressed: bool,
923 device_id: u64,
924 tangential_pressure: f32,
925 barrel_roll_rad: f32,
926 tool_id: u32,
927 ) {
928 self.previous_pen_state = self.pen_state;
929 self.pen_state = Some(PenState {
930 position,
931 pressure,
932 tilt: crate::callbacks::PenTilt {
933 x_tilt: tilt.0,
934 y_tilt: tilt.1,
935 },
936 in_contact,
937 is_eraser,
938 barrel_button_pressed,
939 device_id,
940 tangential_pressure,
941 barrel_roll_rad,
942 tool_id,
943 });
944 self.pen_event_pending = true;
945 }
946
947 pub const fn clear_pen_state(&mut self) {
949 self.previous_pen_state = self.pen_state;
950 self.pen_state = None;
951 self.pen_event_pending = true;
952 }
953
954 #[must_use] pub const fn get_pen_state(&self) -> Option<&PenState> {
956 self.pen_state.as_ref()
957 }
958
959 #[must_use] pub const fn get_previous_pen_state(&self) -> Option<&PenState> {
961 self.previous_pen_state.as_ref()
962 }
963
964 pub const fn clear_pen_event_pending(&mut self) {
966 self.pen_event_pending = false;
967 }
968
969 pub const fn update_pad_state(&mut self, pad: WacomPadState) {
971 self.pad_state = Some(pad);
972 }
973
974 #[must_use] pub const fn get_pad_state(&self) -> Option<&WacomPadState> {
976 self.pad_state.as_ref()
977 }
978
979 pub const fn clear_pad_state(&mut self) {
981 self.pad_state = None;
982 }
983
984 #[must_use] pub fn detect_drag(&self) -> Option<DetectedDrag> {
990 let session = self.get_current_session()?;
991
992 if session.samples.len() < self.config.min_samples_for_gesture {
993 return None;
994 }
995
996 let direct_distance = session.direct_distance()?;
997
998 if direct_distance >= self.config.drag_distance_threshold {
999 let first = session.first_sample()?;
1000 let last = session.last_sample()?;
1001
1002 Some(DetectedDrag {
1003 start_position: first.position,
1004 current_position: last.position,
1005 direct_distance,
1006 total_distance: session.total_distance(),
1007 duration_ms: session.duration_ms()?,
1008 sample_count: session.samples.len(),
1009 session_id: session.session_id,
1010 })
1011 } else {
1012 None
1013 }
1014 }
1015
1016 #[must_use] pub fn detect_long_press(&self) -> Option<DetectedLongPress> {
1021 if let Some(NativeGestureEvent::LongPress(lp)) = self.native_gesture {
1022 return Some(lp);
1023 }
1024 let session = self.get_current_session()?;
1025
1026 if session.ended {
1027 return None; }
1029
1030 let duration_ms = session.duration_ms()?;
1031
1032 if duration_ms < self.config.long_press_time_threshold_ms {
1033 return None;
1034 }
1035
1036 let distance = session.direct_distance()?;
1037
1038 if distance <= self.config.long_press_distance_threshold {
1039 let first = session.first_sample()?;
1040 let callback_invoked = self
1041 .long_press_callbacks_invoked
1042 .contains(&session.session_id);
1043
1044 Some(DetectedLongPress {
1045 position: first.position,
1046 duration_ms,
1047 callback_invoked,
1048 session_id: session.session_id,
1049 })
1050 } else {
1051 None
1052 }
1053 }
1054
1055 pub fn mark_current_long_press_invoked(&mut self) {
1064 if let Some(id) = self.get_current_session().map(|s| s.session_id) {
1065 self.mark_long_press_callback_invoked(id);
1066 }
1067 }
1068
1069 pub fn mark_long_press_callback_invoked(&mut self, session_id: u64) {
1070 if !self.long_press_callbacks_invoked.contains(&session_id) {
1071 self.long_press_callbacks_invoked.push(session_id);
1072 }
1073 }
1074
1075 #[must_use] pub fn detect_double_click(&self) -> bool {
1079 if matches!(self.native_gesture, Some(NativeGestureEvent::DoubleClick)) {
1080 return true;
1081 }
1082 let sessions = &self.input_sessions;
1083 if sessions.len() < 2 {
1084 return false;
1085 }
1086
1087 let prev_session = &sessions[sessions.len() - 2];
1088 let last_session = &sessions[sessions.len() - 1];
1089
1090 if !prev_session.ended || !last_session.ended {
1092 return false;
1093 }
1094
1095 let prev_first = prev_session.first_sample();
1096 let last_first = last_session.first_sample();
1097 let (Some(prev_first), Some(last_first)) = (prev_first, last_first) else {
1098 return false;
1099 };
1100
1101 let duration = last_first.timestamp.duration_since(&prev_first.timestamp);
1102 let time_delta_ms = duration_to_millis(duration);
1103 if time_delta_ms > self.config.double_click_time_threshold_ms {
1104 return false;
1105 }
1106
1107 let dx = last_first.position.x - prev_first.position.x;
1108 let dy = last_first.position.y - prev_first.position.y;
1109 let distance = dx.hypot(dy);
1110
1111 distance < self.config.double_click_distance_threshold
1112 }
1113
1114 #[must_use] pub fn detect_click_count(&self) -> u32 {
1120 let sessions = &self.input_sessions;
1121 let n = sessions.len();
1122 if n == 0 {
1123 return 1;
1124 }
1125
1126 let mut recent: Vec<&InputSession> = Vec::new();
1134 for s in sessions.iter().rev() {
1135 if !s.ended {
1136 continue;
1137 }
1138 recent.push(s);
1139 if recent.len() >= 3 {
1140 break;
1141 }
1142 }
1143
1144 if recent.is_empty() {
1145 return 1;
1146 }
1147
1148 let mut count = 1u32;
1152
1153 for i in 0..recent.len() - 1 {
1154 let later = recent[i];
1155 let earlier = recent[i + 1];
1156
1157 let Some(later_start) = later.first_sample() else {
1158 break;
1159 };
1160 let Some(earlier_start) = earlier.first_sample() else {
1161 break;
1162 };
1163
1164 let duration = later_start.timestamp.duration_since(&earlier_start.timestamp);
1165 let time_delta_ms = duration_to_millis(duration);
1166 if time_delta_ms > self.config.double_click_time_threshold_ms {
1167 break;
1168 }
1169
1170 let dx = later_start.position.x - earlier_start.position.x;
1171 let dy = later_start.position.y - earlier_start.position.y;
1172 let distance = dx.hypot(dy);
1173 if distance >= self.config.double_click_distance_threshold {
1174 break;
1175 }
1176
1177 count += 1;
1178 }
1179
1180 if count > 3 { 1 } else { count }
1182 }
1183
1184 #[must_use] pub fn get_drag_direction(&self) -> Option<GestureDirection> {
1186 let session = self.get_current_session()?;
1187 let first = session.first_sample()?;
1188 let last = session.last_sample()?;
1189
1190 let dx = last.position.x - first.position.x;
1191 let dy = last.position.y - first.position.y;
1192
1193 let direction = match (dx.abs() > dy.abs(), dx > 0.0, dy > 0.0) {
1194 (true, true, _) => GestureDirection::Right,
1195 (true, false, _) => GestureDirection::Left,
1196 (false, _, true) => GestureDirection::Down,
1197 (false, _, false) => GestureDirection::Up,
1198 };
1199 Some(direction)
1200 }
1201
1202 #[allow(clippy::cast_precision_loss)] #[must_use] pub fn get_gesture_velocity(&self) -> Option<f32> {
1205 let session = self.get_current_session()?;
1206
1207 if session.samples.len() < 2 {
1208 return None;
1209 }
1210
1211 let total_distance = session.total_distance();
1212 let duration_ms = session.duration_ms()?;
1213
1214 if duration_ms == 0 {
1215 return None;
1216 }
1217
1218 let duration_secs = duration_ms as f32 / 1000.0;
1219 Some(total_distance / duration_secs)
1220 }
1221
1222 #[must_use] pub fn is_swipe(&self) -> bool {
1224 self.get_gesture_velocity()
1225 .is_some_and(|v| v >= self.config.swipe_velocity_threshold)
1226 }
1227
1228 #[must_use] pub fn detect_swipe_direction(&self) -> Option<GestureDirection> {
1232 if let Some(NativeGestureEvent::Swipe(d)) = self.native_gesture {
1233 return Some(d);
1234 }
1235 if !self.is_swipe() {
1237 return None;
1238 }
1239
1240 self.get_drag_direction()
1242 }
1243
1244 #[allow(clippy::similar_names)] #[must_use] pub fn detect_pinch(&self) -> Option<DetectedPinch> {
1250 if let Some(NativeGestureEvent::Pinch(p)) = self.native_gesture {
1251 return Some(p);
1252 }
1253 if self.input_sessions.len() < 2 {
1255 return None;
1256 }
1257
1258 let session1 = &self.input_sessions[self.input_sessions.len() - 2];
1260 let session2 = &self.input_sessions[self.input_sessions.len() - 1];
1261
1262 if session1.ended || session2.ended {
1268 return None;
1269 }
1270
1271 let first1 = session1.first_sample()?;
1273 let first2 = session2.first_sample()?;
1274 let last1 = session1.last_sample()?;
1275 let last2 = session2.last_sample()?;
1276
1277 let dx_initial = first2.position.x - first1.position.x;
1279 let dy_initial = first2.position.y - first1.position.y;
1280 let initial_distance = dx_initial.hypot(dy_initial);
1281
1282 let dx_current = last2.position.x - last1.position.x;
1284 let dy_current = last2.position.y - last1.position.y;
1285 let current_distance = dx_current.hypot(dy_current);
1286
1287 if initial_distance < 1.0 {
1289 return None;
1290 }
1291
1292 let scale = current_distance / initial_distance;
1294
1295 let scale_threshold = 1.0 + self.config.pinch_scale_threshold;
1297 if scale > 1.0 / scale_threshold && scale < scale_threshold {
1298 return None; }
1300
1301 let center = LogicalPosition {
1303 x: f32::midpoint(last1.position.x, last2.position.x),
1304 y: f32::midpoint(last1.position.y, last2.position.y),
1305 };
1306
1307 let duration = last1.timestamp.duration_since(&first1.timestamp);
1309 let duration_ms = duration_to_millis(duration);
1310
1311 Some(DetectedPinch {
1312 scale,
1313 center,
1314 initial_distance,
1315 current_distance,
1316 duration_ms,
1317 })
1318 }
1319
1320 #[allow(clippy::similar_names)] #[must_use] pub fn detect_rotation(&self) -> Option<DetectedRotation> {
1326 const PI: f32 = core::f32::consts::PI;
1327 if let Some(NativeGestureEvent::Rotation(r)) = self.native_gesture {
1328 return Some(r);
1329 }
1330 if self.input_sessions.len() < 2 {
1332 return None;
1333 }
1334
1335 let session1 = &self.input_sessions[self.input_sessions.len() - 2];
1337 let session2 = &self.input_sessions[self.input_sessions.len() - 1];
1338
1339 if session1.ended || session2.ended {
1343 return None;
1344 }
1345
1346 let first1 = session1.first_sample()?;
1348 let first2 = session2.first_sample()?;
1349 let last1 = session1.last_sample()?;
1350 let last2 = session2.last_sample()?;
1351
1352 let center = LogicalPosition {
1354 x: f32::midpoint(last1.position.x, last2.position.x),
1355 y: f32::midpoint(last1.position.y, last2.position.y),
1356 };
1357
1358 let dx_initial = first2.position.x - first1.position.x;
1360 let dy_initial = first2.position.y - first1.position.y;
1361 let initial_angle = dy_initial.atan2(dx_initial);
1362
1363 let dx_current = last2.position.x - last1.position.x;
1365 let dy_current = last2.position.y - last1.position.y;
1366 let current_angle = dy_current.atan2(dx_current);
1367
1368 let mut angle_diff = current_angle - initial_angle;
1370
1371 #[allow(clippy::while_float)] while angle_diff > PI {
1374 angle_diff -= 2.0 * PI;
1375 }
1376 #[allow(clippy::while_float)] while angle_diff < -PI {
1378 angle_diff += 2.0 * PI;
1379 }
1380
1381 if angle_diff.abs() < self.config.rotation_angle_threshold {
1383 return None;
1384 }
1385
1386 let duration = last1.timestamp.duration_since(&first1.timestamp);
1388 let duration_ms = duration_to_millis(duration);
1389
1390 Some(DetectedRotation {
1391 angle_radians: angle_diff,
1392 center,
1393 duration_ms,
1394 })
1395 }
1396
1397 #[must_use] pub fn get_current_session(&self) -> Option<&InputSession> {
1399 self.input_sessions.last()
1400 }
1401
1402 #[must_use] pub fn get_current_mouse_position(&self) -> Option<LogicalPosition> {
1404 self.get_current_session()
1405 .and_then(|s| s.last_sample())
1406 .map(|sample| sample.position)
1407 }
1408
1409 #[must_use] pub fn get_drag_delta(&self) -> Option<(f32, f32)> {
1414 let session = self.get_current_session()?;
1415 let first = session.first_sample()?;
1416 let last = session.last_sample()?;
1417 Some((
1418 last.position.x - first.position.x,
1419 last.position.y - first.position.y,
1420 ))
1421 }
1422
1423 #[must_use] pub fn get_drag_delta_screen(&self) -> Option<(f32, f32)> {
1435 let session = self.get_current_session()?;
1436 let first = session.first_sample()?;
1437 let last = session.last_sample()?;
1438 Some((
1439 last.screen_position.x - first.screen_position.x,
1440 last.screen_position.y - first.screen_position.y,
1441 ))
1442 }
1443
1444 #[must_use] pub fn get_drag_delta_screen_incremental(&self) -> Option<(f32, f32)> {
1463 let session = self.get_current_session()?;
1464 let len = session.samples.len();
1465 if len < 2 {
1466 return None;
1467 }
1468 let prev = &session.samples[len - 2];
1469 let last = &session.samples[len - 1];
1470 Some((
1471 last.screen_position.x - prev.screen_position.x,
1472 last.screen_position.y - prev.screen_position.y,
1473 ))
1474 }
1475
1476 #[must_use] pub fn get_window_position_at_session_start(&self) -> Option<WindowPosition> {
1480 let session = self.get_current_session()?;
1481 Some(session.window_position_at_start)
1482 }
1483
1484 #[must_use] pub const fn get_drag_context(&self) -> Option<&DragContext> {
1490 self.active_drag.as_ref()
1491 }
1492
1493 pub const fn get_drag_context_mut(&mut self) -> Option<&mut DragContext> {
1495 self.active_drag.as_mut()
1496 }
1497
1498 pub fn activate_node_drag(
1507 &mut self,
1508 dom_id: DomId,
1509 node_id: NodeId,
1510 drag_data: DragData,
1511 _start_hit_test: Option<HitTest>,
1512 ) {
1513 if let Some(detected) = self.detect_drag() {
1514 self.active_drag = Some(DragContext::node_drag(
1515 dom_id,
1516 node_id,
1517 detected.start_position,
1518 drag_data,
1519 detected.session_id,
1520 ));
1521 }
1522 }
1523
1524 pub fn activate_window_drag(
1526 &mut self,
1527 initial_window_position: WindowPosition,
1528 _start_hit_test: Option<HitTest>,
1529 ) {
1530 if let Some(detected) = self.detect_drag() {
1531 self.active_drag = Some(DragContext::window_move(
1532 detected.start_position,
1533 initial_window_position,
1534 detected.session_id,
1535 ));
1536 }
1537 }
1538
1539 pub const fn update_active_drag_positions(&mut self, position: LogicalPosition) {
1545 if let Some(ref mut drag) = self.active_drag {
1546 drag.update_position(position);
1547 }
1548 }
1549
1550 pub fn update_drop_target(&mut self, target: Option<azul_core::dom::DomNodeId>) {
1552 if let Some(ref mut drag) = self.active_drag {
1553 match &mut drag.drag_type {
1554 ActiveDragType::Node(ref mut node_drag) => {
1555 node_drag.current_drop_target = target.into();
1556 }
1557 ActiveDragType::FileDrop(ref mut file_drop) => {
1558 file_drop.drop_target = target.into();
1559 }
1560 _ => {}
1561 }
1562 }
1563 }
1564
1565 pub const fn update_auto_scroll_direction(&mut self, direction: AutoScrollDirection) {
1567 if let Some(ref mut drag) = self.active_drag {
1568 if let Some(text_drag) = drag.as_text_selection_mut() {
1569 text_drag.auto_scroll_direction = direction;
1570 }
1571 }
1572 }
1573
1574 pub const fn end_drag(&mut self) -> Option<DragContext> {
1576 self.active_drag.take()
1577 }
1578
1579 pub fn cancel_drag(&mut self) {
1581 if let Some(ref mut drag) = self.active_drag {
1582 drag.cancelled = true;
1583 }
1584 self.active_drag = None;
1585 }
1586
1587 #[must_use] pub const fn is_dragging(&self) -> bool {
1593 self.active_drag.is_some()
1594 }
1595
1596 #[must_use] pub fn is_text_selection_dragging(&self) -> bool {
1598 self.active_drag.as_ref().is_some_and(DragContext::is_text_selection)
1599 }
1600
1601 #[must_use] pub fn is_scrollbar_dragging(&self) -> bool {
1603 self.active_drag.as_ref().is_some_and(DragContext::is_scrollbar_thumb)
1604 }
1605
1606 #[must_use] pub fn is_node_drag_active(&self) -> bool {
1608 self.active_drag.as_ref().is_some_and(DragContext::is_node_drag)
1609 }
1610
1611 #[must_use] pub fn is_node_dragging(&self, dom_id: DomId, node_id: NodeId) -> bool {
1613 self.active_drag.as_ref().is_some_and(|d| {
1614 d.as_node_drag().is_some_and(|node_drag| node_drag.dom_id == dom_id && node_drag.node_id == node_id)
1615 })
1616 }
1617
1618 #[must_use] pub fn is_window_dragging(&self) -> bool {
1620 self.active_drag.as_ref().is_some_and(DragContext::is_window_move)
1621 }
1622
1623 #[must_use] pub fn is_file_dropping(&self) -> bool {
1625 self.active_drag.as_ref().is_some_and(DragContext::is_file_drop)
1626 }
1627
1628 #[must_use] pub const fn session_count(&self) -> usize {
1630 self.input_sessions.len()
1631 }
1632
1633 #[must_use] pub fn current_session_id(&self) -> Option<u64> {
1635 self.get_current_session().map(|s| s.session_id)
1636 }
1637
1638 #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn get_window_drag_delta(&self) -> Option<(i32, i32)> {
1648 let drag = self.active_drag.as_ref()?.as_window_move()?;
1649
1650 let delta_x = drag.current_position.x - drag.start_position.x;
1651 let delta_y = drag.current_position.y - drag.start_position.y;
1652
1653 match drag.initial_window_position {
1654 WindowPosition::Initialized(_initial_pos) => Some((delta_x as i32, delta_y as i32)),
1655 _ => None,
1656 }
1657 }
1658
1659 #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn get_window_position_from_drag(&self) -> Option<WindowPosition> {
1664 let drag = self.active_drag.as_ref()?.as_window_move()?;
1665
1666 let delta_x = drag.current_position.x - drag.start_position.x;
1667 let delta_y = drag.current_position.y - drag.start_position.y;
1668
1669 match drag.initial_window_position {
1670 WindowPosition::Initialized(initial_pos) => {
1671 Some(WindowPosition::Initialized(PhysicalPositionI32::new(
1672 initial_pos.x + delta_x as i32,
1673 initial_pos.y + delta_y as i32,
1674 )))
1675 }
1676 _ => None,
1677 }
1678 }
1679
1680 #[must_use] pub fn get_scrollbar_scroll_offset(&self) -> Option<f32> {
1682 self.active_drag.as_ref()?.calculate_scrollbar_scroll_offset()
1683 }
1684
1685}
1686
1687impl crate::managers::NodeIdRemap for GestureAndDragManager {
1688 fn remap_node_ids(&mut self, dom_id: DomId, map: &crate::managers::NodeIdMap) {
1694 if let Some(ref mut drag) = self.active_drag {
1695 if !drag.remap_node_ids(dom_id, map.as_btree_map()) {
1696 drag.cancelled = true;
1698 self.active_drag = None;
1699 }
1700 }
1701 }
1702}
1703
1704#[cfg(test)]
1705mod touch_session_tests {
1706 use super::*;
1707 use azul_core::task::{Instant as TestInstant, SystemTick};
1708
1709 fn ts(n: u64) -> CoreInstant {
1710 TestInstant::Tick(SystemTick::new(n))
1711 }
1712
1713 fn pos(x: f32, y: f32) -> LogicalPosition {
1714 LogicalPosition { x, y }
1715 }
1716
1717 #[test]
1718 fn two_fingers_open_two_concurrent_sessions() {
1719 let mut m = GestureAndDragManager::new();
1720 m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
1721 m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
1722 assert_eq!(m.input_sessions.len(), 2);
1723 assert!(!m.input_sessions[0].ended);
1724 assert!(!m.input_sessions[1].ended);
1725 }
1726
1727 #[test]
1728 fn moves_land_in_the_correct_session_not_the_last_one() {
1729 let mut m = GestureAndDragManager::new();
1730 m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
1731 m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
1732 assert!(m.touch_move(1, pos(90.0, 100.0), ts(2), pos(90.0, 100.0)));
1736 assert_eq!(m.input_sessions[0].samples.len(), 2, "finger 1 session grew");
1737 assert_eq!(m.input_sessions[1].samples.len(), 1, "finger 2 session untouched");
1738 }
1739
1740 #[test]
1741 fn spread_gesture_is_detected_as_pinch_out() {
1742 let mut m = GestureAndDragManager::new();
1743 m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
1744 m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
1745 m.touch_move(1, pos(50.0, 100.0), ts(2), pos(50.0, 100.0));
1747 m.touch_move(2, pos(250.0, 100.0), ts(3), pos(250.0, 100.0));
1748 let pinch = m.detect_pinch().expect("two concurrent touch sessions must yield a pinch");
1749 assert!(
1750 pinch.scale > 1.5,
1751 "spread must read as pinch-out (scale {}), initial {} current {}",
1752 pinch.scale,
1753 pinch.initial_distance,
1754 pinch.current_distance
1755 );
1756 }
1757
1758 #[test]
1759 fn touch_up_ends_only_its_own_session() {
1760 let mut m = GestureAndDragManager::new();
1761 m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
1762 m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
1763 m.touch_up(1, pos(100.0, 100.0), ts(2), pos(100.0, 100.0));
1764 assert!(m.input_sessions[0].ended);
1765 assert!(!m.input_sessions[1].ended);
1766 assert!(!m.touch_move(1, pos(0.0, 0.0), ts(3), pos(0.0, 0.0)));
1768 }
1769}
1770
1771#[cfg(test)]
1772#[allow(clippy::float_cmp, clippy::unreadable_literal)]
1773mod autotest_generated {
1774 use azul_core::{
1775 drag::ScrollbarAxis, geom::PhysicalPositionI32, styled_dom::NodeHierarchyItemId,
1776 task::{SystemTick, SystemTickDiff, SystemTimeDiff},
1777 };
1778
1779 use super::*;
1780
1781 fn ts(n: u64) -> CoreInstant {
1785 CoreInstant::Tick(SystemTick::new(n))
1786 }
1787
1788 fn pos(x: f32, y: f32) -> LogicalPosition {
1789 LogicalPosition { x, y }
1790 }
1791
1792 fn sample(x: f32, y: f32, tick: u64) -> InputSample {
1794 InputSample {
1795 position: pos(x, y),
1796 screen_position: pos(x, y),
1797 timestamp: ts(tick),
1798 button_state: 0x01,
1799 event_id: 0,
1800 pressure: 0.5,
1801 tilt: (0.0, 0.0),
1802 touch_radius: (0.0, 0.0),
1803 }
1804 }
1805
1806 fn ended_session(session_id: u64, samples: Vec<InputSample>) -> InputSession {
1809 InputSession {
1810 samples,
1811 ended: true,
1812 session_id,
1813 window_position_at_start: WindowPosition::Uninitialized,
1814 }
1815 }
1816
1817 fn dragging_manager(
1820 from: LogicalPosition,
1821 to: LogicalPosition,
1822 hold_ms: u64,
1823 ) -> GestureAndDragManager {
1824 let mut m = GestureAndDragManager::new();
1825 m.start_input_session(from, ts(0), 0x01, WindowPosition::Uninitialized, from);
1826 let recorded = m.record_input_sample(to, ts(hold_ms), 0x01, to);
1827 assert!(recorded);
1828 m
1829 }
1830
1831 #[test]
1834 fn duration_to_millis_tick_zero_and_max_do_not_panic() {
1835 assert_eq!(
1836 duration_to_millis(CoreDuration::Tick(SystemTickDiff { tick_diff: 0 })),
1837 0
1838 );
1839 assert_eq!(
1840 duration_to_millis(CoreDuration::Tick(SystemTickDiff {
1841 tick_diff: u64::MAX
1842 })),
1843 u64::MAX
1844 );
1845 }
1846
1847 #[cfg(feature = "std")]
1848 #[test]
1849 fn duration_to_millis_system_zero_and_sub_millisecond_floor() {
1850 assert_eq!(
1851 duration_to_millis(CoreDuration::System(SystemTimeDiff { secs: 0, nanos: 0 })),
1852 0
1853 );
1854 assert_eq!(
1856 duration_to_millis(CoreDuration::System(SystemTimeDiff {
1857 secs: 0,
1858 nanos: 999_999
1859 })),
1860 0
1861 );
1862 assert_eq!(
1863 duration_to_millis(CoreDuration::System(SystemTimeDiff {
1864 secs: 2,
1865 nanos: 500_000_000
1866 })),
1867 2500
1868 );
1869 }
1870
1871 #[cfg(feature = "std")]
1872 #[test]
1873 fn duration_to_millis_system_max_truncates_instead_of_panicking() {
1874 let d = CoreDuration::System(SystemTimeDiff {
1878 secs: u64::MAX,
1879 nanos: 999_999_999,
1880 });
1881 let expected = ((u64::MAX as u128) * 1000 + 999) as u64;
1882 assert_eq!(duration_to_millis(d), expected);
1883 }
1884
1885 #[test]
1888 fn express_key_out_of_range_index_is_false_not_a_shift_overflow() {
1889 let pad = WacomPadState {
1891 express_keys: u32::MAX,
1892 touch_ring: 0.0,
1893 touch_ring_active: false,
1894 device_id: 0,
1895 };
1896 assert!(pad.express_key(31));
1897 assert!(!pad.express_key(32));
1898 assert!(!pad.express_key(33));
1899 assert!(!pad.express_key(u32::MAX));
1900 }
1901
1902 #[test]
1903 fn express_key_default_pad_has_no_keys_held() {
1904 let pad = WacomPadState::default();
1905 for i in 0..40u32 {
1906 assert!(!pad.express_key(i), "bit {i} must be unset on a default pad");
1907 }
1908 }
1909
1910 #[test]
1911 fn express_key_bitset_round_trips_every_bit() {
1912 for bit in 0..32u32 {
1913 let pad = WacomPadState {
1914 express_keys: 1u32 << bit,
1915 touch_ring: 0.0,
1916 touch_ring_active: false,
1917 device_id: 0,
1918 };
1919 for probe in 0..32u32 {
1920 assert_eq!(
1921 pad.express_key(probe),
1922 probe == bit,
1923 "encode bit {bit} -> decode probe {probe}"
1924 );
1925 }
1926 }
1927 }
1928
1929 #[test]
1932 fn input_session_new_holds_its_construction_invariants() {
1933 let s = InputSession::new(
1934 u64::MAX,
1935 sample(1.0, 2.0, 7),
1936 WindowPosition::Initialized(PhysicalPositionI32::new(-5, 9)),
1937 );
1938 assert_eq!(s.session_id, u64::MAX);
1939 assert!(!s.ended);
1940 assert_eq!(s.samples.len(), 1);
1941 assert_eq!(s.first_sample(), s.last_sample());
1942 assert_eq!(
1943 s.window_position_at_start,
1944 WindowPosition::Initialized(PhysicalPositionI32::new(-5, 9))
1945 );
1946 assert_eq!(s.total_distance(), 0.0);
1947 assert_eq!(s.direct_distance(), Some(0.0));
1948 assert_eq!(s.duration_ms(), Some(0));
1949 }
1950
1951 #[test]
1952 fn empty_session_getters_return_none_instead_of_panicking() {
1953 let s = InputSession {
1954 samples: Vec::new(),
1955 ended: false,
1956 session_id: 0,
1957 window_position_at_start: WindowPosition::Uninitialized,
1958 };
1959 assert!(s.first_sample().is_none());
1960 assert!(s.last_sample().is_none());
1961 assert!(s.duration_ms().is_none());
1962 assert!(s.direct_distance().is_none());
1963 assert_eq!(s.total_distance(), 0.0);
1964 }
1965
1966 #[test]
1967 fn duration_ms_saturates_to_zero_when_time_runs_backwards() {
1968 let s = InputSession {
1970 samples: vec![sample(0.0, 0.0, 900), sample(0.0, 0.0, 100)],
1971 ended: false,
1972 session_id: 1,
1973 window_position_at_start: WindowPosition::Uninitialized,
1974 };
1975 assert_eq!(s.duration_ms(), Some(0));
1976 }
1977
1978 #[cfg(feature = "std")]
1979 #[test]
1980 fn duration_ms_with_mismatched_instant_kinds_is_zero() {
1981 let mut first = sample(0.0, 0.0, 0);
1982 first.timestamp = CoreInstant::now(); let last = sample(0.0, 0.0, 5_000); let s = InputSession {
1985 samples: vec![first, last],
1986 ended: false,
1987 session_id: 1,
1988 window_position_at_start: WindowPosition::Uninitialized,
1989 };
1990 assert_eq!(s.duration_ms(), Some(0));
1991 }
1992
1993 #[test]
1994 fn total_distance_sums_the_path_while_direct_distance_is_the_chord() {
1995 let s = InputSession {
1996 samples: vec![
1997 sample(0.0, 0.0, 0),
1998 sample(3.0, 0.0, 1),
1999 sample(3.0, 4.0, 2),
2000 ],
2001 ended: false,
2002 session_id: 1,
2003 window_position_at_start: WindowPosition::Uninitialized,
2004 };
2005 assert_eq!(s.total_distance(), 7.0);
2006 assert_eq!(s.direct_distance(), Some(5.0));
2007 }
2008
2009 #[test]
2010 fn distances_with_nan_coordinates_are_nan_and_do_not_panic() {
2011 let s = InputSession {
2012 samples: vec![sample(0.0, 0.0, 0), sample(f32::NAN, f32::NAN, 1)],
2013 ended: false,
2014 session_id: 1,
2015 window_position_at_start: WindowPosition::Uninitialized,
2016 };
2017 assert!(s.total_distance().is_nan());
2018 assert!(s.direct_distance().is_some_and(f32::is_nan));
2019 }
2020
2021 #[test]
2022 fn distances_at_f32_extremes_saturate_to_infinity_instead_of_panicking() {
2023 let s = InputSession {
2024 samples: vec![
2025 sample(-f32::MAX, -f32::MAX, 0),
2026 sample(f32::MAX, f32::MAX, 1),
2027 ],
2028 ended: false,
2029 session_id: 1,
2030 window_position_at_start: WindowPosition::Uninitialized,
2031 };
2032 assert!(s.total_distance().is_infinite());
2033 assert!(s.direct_distance().is_some_and(f32::is_infinite));
2034 }
2035
2036 #[test]
2039 fn new_manager_is_inert_and_every_detector_is_quiet() {
2040 let m = GestureAndDragManager::new();
2041 assert_eq!(m.session_count(), 0);
2042 assert_eq!(m.debug_counts(), (0, 0));
2043 assert!(m.current_session_id().is_none());
2044 assert!(m.get_current_session().is_none());
2045 assert!(m.get_current_mouse_position().is_none());
2046 assert!(m.get_pen_state().is_none());
2047 assert!(m.get_previous_pen_state().is_none());
2048 assert!(m.get_pad_state().is_none());
2049 assert!(m.get_drag_context().is_none());
2050 assert!(m.detect_drag().is_none());
2051 assert!(m.detect_long_press().is_none());
2052 assert!(!m.detect_double_click());
2053 assert!(m.get_drag_direction().is_none());
2054 assert!(m.get_gesture_velocity().is_none());
2055 assert!(!m.is_swipe());
2056 assert!(m.detect_swipe_direction().is_none());
2057 assert!(m.detect_pinch().is_none());
2058 assert!(m.detect_rotation().is_none());
2059 assert!(m.get_drag_delta().is_none());
2060 assert!(m.get_drag_delta_screen().is_none());
2061 assert!(m.get_drag_delta_screen_incremental().is_none());
2062 assert!(m.get_window_position_at_session_start().is_none());
2063 assert!(m.get_window_drag_delta().is_none());
2064 assert!(m.get_window_position_from_drag().is_none());
2065 assert!(m.get_scrollbar_scroll_offset().is_none());
2066 assert!(!m.is_dragging());
2067 assert!(!m.is_text_selection_dragging());
2068 assert!(!m.is_scrollbar_dragging());
2069 assert!(!m.is_node_drag_active());
2070 assert!(!m.is_window_dragging());
2071 assert!(!m.is_file_dropping());
2072 assert!(!m.is_node_dragging(DomId::ROOT_ID, NodeId::ZERO));
2073 assert_eq!(m.detect_click_count(), 1);
2075 assert_eq!(m, GestureAndDragManager::default());
2076 }
2077
2078 #[test]
2079 fn with_config_keeps_extreme_thresholds_verbatim_and_still_starts_at_session_1() {
2080 let cfg = GestureDetectionConfig {
2081 drag_distance_threshold: f32::NAN,
2082 double_click_time_threshold_ms: u64::MAX,
2083 double_click_distance_threshold: f32::INFINITY,
2084 long_press_time_threshold_ms: 0,
2085 long_press_distance_threshold: -1.0,
2086 min_samples_for_gesture: usize::MAX,
2087 swipe_velocity_threshold: 0.0,
2088 pinch_scale_threshold: f32::MAX,
2089 rotation_angle_threshold: -0.0,
2090 sample_cleanup_interval_ms: 0,
2091 };
2092 let mut m = GestureAndDragManager::with_config(cfg);
2093 assert!(m.config.drag_distance_threshold.is_nan());
2094 assert_eq!(m.config.double_click_time_threshold_ms, u64::MAX);
2095 assert_eq!(m.config.min_samples_for_gesture, usize::MAX);
2096 assert_eq!(m.session_count(), 0);
2097 let id = m.start_input_session(
2098 pos(0.0, 0.0),
2099 ts(0),
2100 0x01,
2101 WindowPosition::Uninitialized,
2102 pos(0.0, 0.0),
2103 );
2104 assert_eq!(id, 1, "with_config must not disturb the session counter");
2105 assert!(m.detect_drag().is_none());
2107 }
2108
2109 #[test]
2112 fn session_ids_are_monotonic_starting_at_one() {
2113 let mut m = GestureAndDragManager::new();
2114 for expected in 1..=5u64 {
2115 let id = m.start_input_session(
2116 pos(0.0, 0.0),
2117 ts(expected),
2118 0x01,
2119 WindowPosition::Uninitialized,
2120 pos(0.0, 0.0),
2121 );
2122 assert_eq!(id, expected);
2123 assert_eq!(m.current_session_id(), Some(expected));
2124 m.end_current_session();
2125 }
2126 }
2127
2128 #[test]
2129 fn session_id_counter_at_the_u64_boundary_does_not_overflow() {
2130 let mut m = GestureAndDragManager::new();
2131 m.next_session_id = u64::MAX - 1;
2132 let id = m.start_input_session(
2133 pos(0.0, 0.0),
2134 ts(0),
2135 0xFF,
2136 WindowPosition::Uninitialized,
2137 pos(0.0, 0.0),
2138 );
2139 assert_eq!(id, u64::MAX - 1);
2140 assert_eq!(m.next_session_id, u64::MAX);
2141 }
2142
2143 #[test]
2144 fn recording_without_or_after_a_session_returns_false() {
2145 let mut m = GestureAndDragManager::new();
2146 assert!(!m.record_input_sample(pos(1.0, 1.0), ts(1), 0x01, pos(1.0, 1.0)));
2147 m.start_input_session(
2148 pos(0.0, 0.0),
2149 ts(0),
2150 0x01,
2151 WindowPosition::Uninitialized,
2152 pos(0.0, 0.0),
2153 );
2154 assert!(m.record_input_sample(pos(1.0, 1.0), ts(1), 0x01, pos(1.0, 1.0)));
2155 m.end_current_session();
2156 assert!(!m.record_input_sample(pos(2.0, 2.0), ts(2), 0x01, pos(2.0, 2.0)));
2157 m.end_current_session();
2159 m.clear_all_sessions();
2160 m.end_current_session();
2161 assert_eq!(m.session_count(), 0);
2162 }
2163
2164 #[test]
2165 fn sample_count_stays_bounded_by_max_samples_per_session() {
2166 let mut m = GestureAndDragManager::new();
2167 m.start_input_session(
2168 pos(0.0, 0.0),
2169 ts(0),
2170 0x01,
2171 WindowPosition::Uninitialized,
2172 pos(0.0, 0.0),
2173 );
2174 for i in 1..=(MAX_SAMPLES_PER_SESSION as u64 + 200) {
2175 assert!(m.record_input_sample(pos(i as f32, 0.0), ts(i), 0x01, pos(i as f32, 0.0)));
2176 assert!(
2177 m.get_current_session().unwrap().samples.len() <= MAX_SAMPLES_PER_SESSION,
2178 "sample buffer grew past MAX_SAMPLES_PER_SESSION at i={i}"
2179 );
2180 }
2181 let last = m.get_current_mouse_position().unwrap();
2183 assert_eq!(last.x, (MAX_SAMPLES_PER_SESSION + 200) as f32);
2184 }
2185
2186 #[test]
2187 fn pen_samples_accept_nan_inf_and_extreme_values() {
2188 let mut m = GestureAndDragManager::new();
2189 let id = m.start_input_session_with_pen(
2190 pos(f32::NAN, f32::INFINITY),
2191 ts(0),
2192 0xFF,
2193 u64::MAX,
2194 f32::NAN,
2195 (f32::INFINITY, f32::NEG_INFINITY),
2196 (-f32::MAX, f32::MAX),
2197 WindowPosition::Uninitialized,
2198 pos(f32::NEG_INFINITY, f32::NAN),
2199 );
2200 assert_eq!(id, 1);
2201 assert!(m.record_input_sample_with_pen(
2202 pos(0.0, 0.0),
2203 ts(u64::MAX),
2204 0x00,
2205 0,
2206 -1.0e30,
2207 (f32::NAN, f32::NAN),
2208 (f32::NAN, f32::NAN),
2209 pos(0.0, 0.0),
2210 ));
2211 let session = m.get_current_session().unwrap();
2212 assert_eq!(session.samples.len(), 2);
2213 let first = session.first_sample().unwrap();
2214 assert!(first.pressure.is_nan());
2215 assert!(first.tilt.0.is_infinite());
2216 assert_eq!(first.button_state, 0xFF);
2217 assert_eq!(first.event_id, u64::MAX);
2218 assert_eq!(session.duration_ms(), Some(u64::MAX));
2220 assert!(m.detect_drag().is_none_or(|d| !d.direct_distance.is_finite()));
2224 assert!(m.get_drag_direction().is_some());
2225 }
2226
2227 #[test]
2228 fn starting_a_session_prunes_all_but_the_newest_ended_session() {
2229 let mut m = GestureAndDragManager::new();
2230 for tick in [0u64, 10, 20] {
2231 m.start_input_session(
2232 pos(0.0, 0.0),
2233 ts(tick),
2234 0x01,
2235 WindowPosition::Uninitialized,
2236 pos(0.0, 0.0),
2237 );
2238 m.end_current_session();
2239 }
2240 assert_eq!(m.session_count(), 2);
2242 assert_eq!(m.input_sessions[0].session_id, 2);
2243 assert_eq!(m.input_sessions[1].session_id, 3);
2244 assert_eq!(m.detect_click_count(), 2);
2248 }
2249
2250 #[test]
2253 fn touch_ids_at_zero_and_u64_max_are_tracked_independently() {
2254 let mut m = GestureAndDragManager::new();
2255 m.touch_down(
2256 0,
2257 pos(0.0, 0.0),
2258 ts(0),
2259 WindowPosition::Uninitialized,
2260 pos(0.0, 0.0),
2261 );
2262 m.touch_down(
2263 u64::MAX,
2264 pos(50.0, 0.0),
2265 ts(1),
2266 WindowPosition::Uninitialized,
2267 pos(50.0, 0.0),
2268 );
2269 assert_eq!(m.session_count(), 2);
2270 assert!(m.touch_move(0, pos(1.0, 1.0), ts(2), pos(1.0, 1.0)));
2271 assert!(m.touch_move(u64::MAX, pos(60.0, 0.0), ts(3), pos(60.0, 0.0)));
2272 assert_eq!(m.input_sessions[0].samples.len(), 2);
2273 assert_eq!(m.input_sessions[1].samples.len(), 2);
2274 m.touch_up(0, pos(1.0, 1.0), ts(4), pos(1.0, 1.0));
2275 assert!(m.input_sessions[0].ended);
2276 assert!(!m.input_sessions[1].ended);
2277 }
2278
2279 #[test]
2280 fn touch_events_for_unknown_ids_are_ignored_without_panicking() {
2281 let mut m = GestureAndDragManager::new();
2282 assert!(!m.touch_move(42, pos(0.0, 0.0), ts(0), pos(0.0, 0.0)));
2283 m.touch_up(42, pos(0.0, 0.0), ts(1), pos(0.0, 0.0));
2284 m.touch_cancel_all(); assert_eq!(m.session_count(), 0);
2286 }
2287
2288 #[test]
2289 fn a_repeated_touch_down_for_the_same_id_rebinds_to_the_newest_session() {
2290 let mut m = GestureAndDragManager::new();
2291 m.touch_down(
2292 7,
2293 pos(0.0, 0.0),
2294 ts(0),
2295 WindowPosition::Uninitialized,
2296 pos(0.0, 0.0),
2297 );
2298 m.touch_down(
2299 7,
2300 pos(9.0, 9.0),
2301 ts(1),
2302 WindowPosition::Uninitialized,
2303 pos(9.0, 9.0),
2304 );
2305 assert_eq!(m.touch_sessions.len(), 1, "the id map must not grow");
2306 assert_eq!(m.session_count(), 2);
2307 assert_eq!(m.touch_sessions.get(&7).copied(), Some(2));
2308 m.touch_up(7, pos(9.0, 9.0), ts(2), pos(9.0, 9.0));
2311 assert!(!m.input_sessions[0].ended);
2312 assert!(m.input_sessions[1].ended);
2313 assert!(m.touch_sessions.is_empty());
2314 }
2315
2316 #[test]
2317 fn touch_cancel_all_ends_every_finger_and_empties_the_id_map() {
2318 let mut m = GestureAndDragManager::new();
2319 for id in 0..3u64 {
2320 m.touch_down(
2321 id,
2322 pos(id as f32 * 10.0, 0.0),
2323 ts(id),
2324 WindowPosition::Uninitialized,
2325 pos(id as f32 * 10.0, 0.0),
2326 );
2327 }
2328 m.touch_cancel_all();
2329 assert!(m.touch_sessions.is_empty());
2330 assert!(m.input_sessions.iter().all(|s| s.ended));
2331 assert!(!m.touch_move(1, pos(0.0, 0.0), ts(9), pos(0.0, 0.0)));
2332 }
2333
2334 #[test]
2335 fn touch_moves_after_clear_all_sessions_are_dropped_not_resurrected() {
2336 let mut m = GestureAndDragManager::new();
2337 m.touch_down(
2338 1,
2339 pos(0.0, 0.0),
2340 ts(0),
2341 WindowPosition::Uninitialized,
2342 pos(0.0, 0.0),
2343 );
2344 m.clear_all_sessions();
2345 assert!(!m.touch_move(1, pos(5.0, 5.0), ts(1), pos(5.0, 5.0)));
2348 assert_eq!(m.session_count(), 0);
2349 }
2350
2351 #[test]
2352 fn record_sample_for_session_rejects_unknown_and_ended_sessions() {
2353 let mut m = GestureAndDragManager::new();
2354 assert!(!m.record_sample_for_session(u64::MAX, pos(0.0, 0.0), ts(0), pos(0.0, 0.0)));
2355 let id = m.start_input_session(
2356 pos(0.0, 0.0),
2357 ts(0),
2358 0x01,
2359 WindowPosition::Uninitialized,
2360 pos(0.0, 0.0),
2361 );
2362 assert!(m.record_sample_for_session(id, pos(1.0, 0.0), ts(1), pos(1.0, 0.0)));
2363 assert!(!m.record_sample_for_session(0, pos(1.0, 0.0), ts(1), pos(1.0, 0.0)));
2364 m.end_current_session();
2365 assert!(!m.record_sample_for_session(id, pos(2.0, 0.0), ts(2), pos(2.0, 0.0)));
2366 assert_eq!(m.input_sessions[0].samples.len(), 2);
2367 }
2368
2369 #[test]
2370 fn record_sample_for_session_is_also_bounded_by_max_samples() {
2371 let mut m = GestureAndDragManager::new();
2372 m.touch_down(
2373 1,
2374 pos(0.0, 0.0),
2375 ts(0),
2376 WindowPosition::Uninitialized,
2377 pos(0.0, 0.0),
2378 );
2379 for i in 1..=(MAX_SAMPLES_PER_SESSION as u64 + 150) {
2380 assert!(m.touch_move(1, pos(i as f32, 0.0), ts(i), pos(i as f32, 0.0)));
2381 }
2382 assert!(m.input_sessions[0].samples.len() <= MAX_SAMPLES_PER_SESSION);
2383 }
2384
2385 #[test]
2388 fn clear_old_sessions_reaps_stale_sessions_and_their_long_press_ids() {
2389 let mut m = GestureAndDragManager::new();
2390 let old = m.start_input_session(
2391 pos(0.0, 0.0),
2392 ts(0),
2393 0x01,
2394 WindowPosition::Uninitialized,
2395 pos(0.0, 0.0),
2396 );
2397 m.end_current_session();
2398 m.mark_long_press_callback_invoked(old);
2399 let fresh = m.start_input_session(
2400 pos(0.0, 0.0),
2401 ts(10_000),
2402 0x01,
2403 WindowPosition::Uninitialized,
2404 pos(0.0, 0.0),
2405 );
2406 m.mark_long_press_callback_invoked(fresh);
2407 assert_eq!(m.debug_counts(), (2, 2));
2408
2409 m.clear_old_sessions(ts(10_050));
2411 assert_eq!(m.session_count(), 1);
2412 assert_eq!(m.current_session_id(), Some(fresh));
2413 assert_eq!(
2414 m.debug_counts(),
2415 (1, 1),
2416 "long-press bookkeeping must not grow unboundedly"
2417 );
2418 }
2419
2420 #[test]
2421 fn clear_old_sessions_drops_sessions_that_have_no_samples() {
2422 let mut m = GestureAndDragManager::new();
2423 m.input_sessions.push(InputSession {
2424 samples: Vec::new(),
2425 ended: false,
2426 session_id: 99,
2427 window_position_at_start: WindowPosition::Uninitialized,
2428 });
2429 m.clear_old_sessions(ts(0));
2430 assert_eq!(m.session_count(), 0);
2431 }
2432
2433 #[test]
2434 fn clear_old_sessions_with_a_backwards_clock_keeps_everything() {
2435 let mut m = GestureAndDragManager::new();
2436 m.start_input_session(
2437 pos(0.0, 0.0),
2438 ts(5_000),
2439 0x01,
2440 WindowPosition::Uninitialized,
2441 pos(0.0, 0.0),
2442 );
2443 m.clear_old_sessions(ts(0));
2445 assert_eq!(m.session_count(), 1);
2446 }
2447
2448 #[test]
2449 fn clear_all_sessions_resets_both_counters() {
2450 let mut m = GestureAndDragManager::new();
2451 m.start_input_session(
2452 pos(0.0, 0.0),
2453 ts(0),
2454 0x01,
2455 WindowPosition::Uninitialized,
2456 pos(0.0, 0.0),
2457 );
2458 m.mark_current_long_press_invoked();
2459 assert_eq!(m.debug_counts(), (1, 1));
2460 m.clear_all_sessions();
2461 assert_eq!(m.debug_counts(), (0, 0));
2462 assert!(m.get_current_session().is_none());
2463 }
2464
2465 #[test]
2466 fn long_press_invocation_marks_are_deduplicated() {
2467 let mut m = GestureAndDragManager::new();
2468 for _ in 0..100 {
2469 m.mark_long_press_callback_invoked(u64::MAX);
2470 m.mark_long_press_callback_invoked(0);
2471 }
2472 assert_eq!(m.debug_counts(), (0, 2));
2473 m.mark_current_long_press_invoked();
2475 assert_eq!(m.debug_counts(), (0, 2));
2476 }
2477
2478 #[test]
2481 fn detect_drag_fires_exactly_at_the_distance_threshold() {
2482 let m = dragging_manager(pos(0.0, 0.0), pos(3.0, 4.0), 20);
2484 let drag = m.detect_drag().expect("distance == threshold must be a drag");
2485 assert_eq!(drag.direct_distance, 5.0);
2486 assert_eq!(drag.total_distance, 5.0);
2487 assert_eq!(drag.sample_count, 2);
2488 assert_eq!(drag.duration_ms, 20);
2489 assert_eq!(drag.session_id, 1);
2490 assert_eq!(drag.start_position, pos(0.0, 0.0));
2491 assert_eq!(drag.current_position, pos(3.0, 4.0));
2492
2493 let m = dragging_manager(pos(0.0, 0.0), pos(4.9, 0.0), 20);
2495 assert!(m.detect_drag().is_none());
2496 }
2497
2498 #[test]
2499 fn detect_drag_with_nan_movement_returns_none() {
2500 let m = dragging_manager(pos(0.0, 0.0), pos(f32::NAN, f32::NAN), 20);
2501 assert!(
2502 m.detect_drag().is_none(),
2503 "NaN distance is never >= threshold"
2504 );
2505 }
2506
2507 #[test]
2508 fn detect_drag_needs_min_samples_for_gesture() {
2509 let mut m = GestureAndDragManager::new();
2510 m.start_input_session(
2511 pos(0.0, 0.0),
2512 ts(0),
2513 0x01,
2514 WindowPosition::Uninitialized,
2515 pos(500.0, 500.0),
2516 );
2517 assert!(m.detect_drag().is_none(), "one sample is not a gesture");
2518 }
2519
2520 #[test]
2521 fn detect_long_press_honours_time_and_distance_thresholds() {
2522 let m = dragging_manager(pos(10.0, 10.0), pos(10.0, 10.0), 500);
2524 let lp = m.detect_long_press().expect("500ms hold is a long press");
2525 assert_eq!(lp.duration_ms, 500);
2526 assert_eq!(lp.position, pos(10.0, 10.0));
2527 assert!(!lp.callback_invoked);
2528 assert_eq!(lp.session_id, 1);
2529
2530 let m = dragging_manager(pos(10.0, 10.0), pos(10.0, 10.0), 499);
2532 assert!(m.detect_long_press().is_none());
2533
2534 let m = dragging_manager(pos(0.0, 0.0), pos(11.0, 0.0), 800);
2536 assert!(m.detect_long_press().is_none());
2537 }
2538
2539 #[test]
2540 fn detect_long_press_stops_at_button_up_and_after_being_marked() {
2541 let mut m = dragging_manager(pos(10.0, 10.0), pos(10.0, 10.0), 600);
2542 assert!(m.detect_long_press().is_some());
2543
2544 m.mark_current_long_press_invoked();
2545 let lp = m.detect_long_press().expect("still held");
2546 assert!(
2547 lp.callback_invoked,
2548 "a marked long press must report callback_invoked"
2549 );
2550
2551 m.end_current_session();
2552 assert!(
2553 m.detect_long_press().is_none(),
2554 "a released button cannot be a long press"
2555 );
2556 }
2557
2558 #[test]
2561 fn detect_double_click_checks_both_timing_and_distance() {
2562 let mut m = GestureAndDragManager::new();
2563 m.input_sessions = vec![
2564 ended_session(1, vec![sample(10.0, 10.0, 0)]),
2565 ended_session(2, vec![sample(11.0, 11.0, 100)]),
2566 ];
2567 assert!(m.detect_double_click());
2568
2569 m.input_sessions[1].samples[0].timestamp = ts(501);
2571 assert!(!m.detect_double_click());
2572
2573 m.input_sessions[1].samples[0].timestamp = ts(100);
2575 m.input_sessions[1].samples[0].position = pos(100.0, 10.0);
2576 assert!(!m.detect_double_click());
2577
2578 m.input_sessions[1].samples[0].position = pos(11.0, 11.0);
2580 m.input_sessions[1].ended = false;
2581 assert!(!m.detect_double_click());
2582 }
2583
2584 #[test]
2585 fn detect_double_click_needs_two_sessions() {
2586 let mut m = GestureAndDragManager::new();
2587 m.input_sessions = vec![ended_session(1, vec![sample(0.0, 0.0, 0)])];
2588 assert!(!m.detect_double_click());
2589 }
2590
2591 #[test]
2592 fn detect_click_count_counts_up_to_three_and_stops_at_the_first_gap() {
2593 let mut m = GestureAndDragManager::new();
2594 m.input_sessions = vec![
2596 ended_session(1, vec![sample(10.0, 10.0, 0)]),
2597 ended_session(2, vec![sample(10.0, 11.0, 100)]),
2598 ended_session(3, vec![sample(11.0, 10.0, 200)]),
2599 ];
2600 assert_eq!(m.detect_click_count(), 3);
2601
2602 m.input_sessions[2].samples[0].timestamp = ts(900);
2604 assert_eq!(m.detect_click_count(), 1);
2605
2606 m.input_sessions[2].samples[0].timestamp = ts(200);
2609 m.input_sessions[0].samples[0].timestamp = ts(u64::MAX);
2610 assert_eq!(m.detect_click_count(), 3);
2611
2612 m.input_sessions[0].samples[0].timestamp = ts(0);
2614 m.input_sessions[0].samples[0].position = pos(500.0, 500.0);
2615 assert_eq!(m.detect_click_count(), 2);
2616 }
2617
2618 #[test]
2619 fn detect_click_count_ignores_live_sessions_and_defaults_to_one() {
2620 let mut m = GestureAndDragManager::new();
2621 m.start_input_session(
2623 pos(0.0, 0.0),
2624 ts(0),
2625 0x01,
2626 WindowPosition::Uninitialized,
2627 pos(0.0, 0.0),
2628 );
2629 assert_eq!(m.detect_click_count(), 1);
2630 assert_eq!(GestureAndDragManager::new().detect_click_count(), 1);
2631 }
2632
2633 #[test]
2634 fn detect_click_count_with_empty_sample_vec_does_not_panic() {
2635 let mut m = GestureAndDragManager::new();
2636 m.input_sessions = vec![
2637 ended_session(1, Vec::new()),
2638 ended_session(2, vec![sample(0.0, 0.0, 10)]),
2639 ];
2640 assert_eq!(m.detect_click_count(), 1);
2641 assert!(!m.detect_double_click());
2642 }
2643
2644 #[test]
2647 fn drag_direction_is_deterministic_for_stationary_and_nan_input() {
2648 let m = dragging_manager(pos(5.0, 5.0), pos(5.0, 5.0), 10);
2650 assert_eq!(m.get_drag_direction(), Some(GestureDirection::Up));
2651
2652 let m = dragging_manager(pos(0.0, 0.0), pos(f32::NAN, f32::NAN), 10);
2654 assert_eq!(m.get_drag_direction(), Some(GestureDirection::Up));
2655 }
2656
2657 #[test]
2658 fn drag_direction_picks_the_dominant_axis() {
2659 let cases = [
2660 (pos(100.0, 1.0), GestureDirection::Right),
2661 (pos(-100.0, 1.0), GestureDirection::Left),
2662 (pos(1.0, 100.0), GestureDirection::Down),
2663 (pos(1.0, -100.0), GestureDirection::Up),
2664 (pos(50.0, 50.0), GestureDirection::Down),
2666 ];
2667 for (to, expected) in cases {
2668 let m = dragging_manager(pos(0.0, 0.0), to, 10);
2669 assert_eq!(
2670 m.get_drag_direction(),
2671 Some(expected),
2672 "drag to ({}, {})",
2673 to.x,
2674 to.y
2675 );
2676 }
2677 }
2678
2679 #[test]
2680 fn gesture_velocity_returns_none_instead_of_dividing_by_zero() {
2681 let m = dragging_manager(pos(0.0, 0.0), pos(100.0, 0.0), 0);
2683 assert!(m.get_gesture_velocity().is_none());
2684 assert!(!m.is_swipe());
2685 assert!(m.detect_swipe_direction().is_none());
2686
2687 let mut m = GestureAndDragManager::new();
2689 m.start_input_session(
2690 pos(0.0, 0.0),
2691 ts(0),
2692 0x01,
2693 WindowPosition::Uninitialized,
2694 pos(0.0, 0.0),
2695 );
2696 assert!(m.get_gesture_velocity().is_none());
2697 }
2698
2699 #[test]
2700 fn swipe_needs_velocity_above_the_configured_threshold() {
2701 let fast = dragging_manager(pos(0.0, 0.0), pos(60.0, 0.0), 100);
2703 assert!(fast.get_gesture_velocity().unwrap() > 500.0);
2704 assert!(fast.is_swipe());
2705 assert_eq!(
2706 fast.detect_swipe_direction(),
2707 Some(GestureDirection::Right)
2708 );
2709
2710 let slow = dragging_manager(pos(0.0, 0.0), pos(0.0, -40.0), 100);
2712 assert!(!slow.is_swipe());
2713 assert!(slow.detect_swipe_direction().is_none());
2714 }
2715
2716 #[test]
2717 fn gesture_velocity_with_infinite_travel_saturates_to_infinity() {
2718 let m = dragging_manager(pos(-f32::MAX, 0.0), pos(f32::MAX, 0.0), 1);
2719 let v = m.get_gesture_velocity().expect("two samples, 1ms apart");
2720 assert!(v.is_infinite(), "expected saturation to +inf, got {v}");
2721 assert!(m.is_swipe());
2722 }
2723
2724 #[test]
2727 fn pinch_and_rotation_ignore_sequential_mouse_sessions() {
2728 let mut m = GestureAndDragManager::new();
2731 m.start_input_session(
2732 pos(0.0, 0.0),
2733 ts(0),
2734 0x01,
2735 WindowPosition::Uninitialized,
2736 pos(0.0, 0.0),
2737 );
2738 m.end_current_session();
2739 m.start_input_session(
2740 pos(200.0, 0.0),
2741 ts(10),
2742 0x01,
2743 WindowPosition::Uninitialized,
2744 pos(200.0, 0.0),
2745 );
2746 m.record_input_sample(pos(400.0, 0.0), ts(20), 0x01, pos(400.0, 0.0));
2747 assert_eq!(m.session_count(), 2);
2748 assert!(m.detect_pinch().is_none(), "an ended session is not a finger");
2749 assert!(m.detect_rotation().is_none());
2750 }
2751
2752 #[test]
2753 fn pinch_returns_none_when_the_fingers_start_on_top_of_each_other() {
2754 let mut m = GestureAndDragManager::new();
2755 m.touch_down(
2756 1,
2757 pos(100.0, 100.0),
2758 ts(0),
2759 WindowPosition::Uninitialized,
2760 pos(100.0, 100.0),
2761 );
2762 m.touch_down(
2763 2,
2764 pos(100.5, 100.0),
2765 ts(1),
2766 WindowPosition::Uninitialized,
2767 pos(100.5, 100.0),
2768 );
2769 m.touch_move(1, pos(0.0, 100.0), ts(2), pos(0.0, 100.0));
2771 assert!(m.detect_pinch().is_none());
2772 }
2773
2774 #[test]
2775 fn pinch_below_the_scale_threshold_is_not_reported() {
2776 let mut m = GestureAndDragManager::new();
2777 m.touch_down(
2778 1,
2779 pos(100.0, 100.0),
2780 ts(0),
2781 WindowPosition::Uninitialized,
2782 pos(100.0, 100.0),
2783 );
2784 m.touch_down(
2785 2,
2786 pos(200.0, 100.0),
2787 ts(1),
2788 WindowPosition::Uninitialized,
2789 pos(200.0, 100.0),
2790 );
2791 m.touch_move(2, pos(205.0, 100.0), ts(2), pos(205.0, 100.0));
2793 assert!(m.detect_pinch().is_none());
2794 }
2795
2796 #[test]
2797 fn pinch_in_reports_a_scale_below_one() {
2798 let mut m = GestureAndDragManager::new();
2799 m.touch_down(
2800 1,
2801 pos(0.0, 0.0),
2802 ts(0),
2803 WindowPosition::Uninitialized,
2804 pos(0.0, 0.0),
2805 );
2806 m.touch_down(
2807 2,
2808 pos(200.0, 0.0),
2809 ts(1),
2810 WindowPosition::Uninitialized,
2811 pos(200.0, 0.0),
2812 );
2813 m.touch_move(1, pos(50.0, 0.0), ts(10), pos(50.0, 0.0));
2814 m.touch_move(2, pos(150.0, 0.0), ts(11), pos(150.0, 0.0));
2815 let p = m.detect_pinch().expect("200px -> 100px is a pinch in");
2816 assert_eq!(p.initial_distance, 200.0);
2817 assert_eq!(p.current_distance, 100.0);
2818 assert_eq!(p.scale, 0.5);
2819 assert_eq!(p.center, pos(100.0, 0.0));
2820 assert_eq!(p.duration_ms, 10);
2821 }
2822
2823 #[test]
2824 fn pinch_with_infinite_coordinates_saturates_instead_of_panicking() {
2825 let mut m = GestureAndDragManager::new();
2826 m.touch_down(
2827 1,
2828 pos(0.0, 0.0),
2829 ts(0),
2830 WindowPosition::Uninitialized,
2831 pos(0.0, 0.0),
2832 );
2833 m.touch_down(
2834 2,
2835 pos(10.0, 0.0),
2836 ts(1),
2837 WindowPosition::Uninitialized,
2838 pos(10.0, 0.0),
2839 );
2840 m.touch_move(1, pos(-f32::MAX, 0.0), ts(2), pos(-f32::MAX, 0.0));
2842 m.touch_move(2, pos(f32::MAX, 0.0), ts(3), pos(f32::MAX, 0.0));
2843 let p = m.detect_pinch().expect("an overflowing spread is still a pinch");
2844 assert!(
2845 !p.scale.is_finite(),
2846 "expected a saturated (non-finite) scale, got {}",
2847 p.scale
2848 );
2849 assert!(!p.scale.is_nan());
2850 }
2851
2852 #[test]
2853 fn pinch_and_rotation_with_nan_coordinates_never_panic() {
2854 let mut m = GestureAndDragManager::new();
2855 m.touch_down(
2856 1,
2857 pos(f32::NAN, f32::NAN),
2858 ts(0),
2859 WindowPosition::Uninitialized,
2860 pos(f32::NAN, f32::NAN),
2861 );
2862 m.touch_down(
2863 2,
2864 pos(200.0, 100.0),
2865 ts(1),
2866 WindowPosition::Uninitialized,
2867 pos(200.0, 100.0),
2868 );
2869 assert!(m.detect_pinch().is_none_or(|p| !p.scale.is_finite()));
2872 assert!(m
2873 .detect_rotation()
2874 .is_none_or(|r| !r.angle_radians.is_finite()));
2875 }
2876
2877 #[test]
2878 fn rotation_normalisation_terminates_for_extreme_coordinates() {
2879 let mut m = GestureAndDragManager::new();
2882 m.touch_down(
2883 1,
2884 pos(-f32::MAX, -f32::MAX),
2885 ts(0),
2886 WindowPosition::Uninitialized,
2887 pos(0.0, 0.0),
2888 );
2889 m.touch_down(
2890 2,
2891 pos(f32::MAX, f32::MAX),
2892 ts(1),
2893 WindowPosition::Uninitialized,
2894 pos(0.0, 0.0),
2895 );
2896 m.touch_move(2, pos(-f32::MAX, f32::MAX), ts(2), pos(0.0, 0.0));
2897 let r = m.detect_rotation();
2898 assert!(r.is_none_or(|r| r.angle_radians.abs() <= core::f32::consts::PI + 1.0e-4));
2899 }
2900
2901 #[test]
2902 fn rotation_reports_the_signed_angle_between_the_two_fingers() {
2903 let mut m = GestureAndDragManager::new();
2904 m.touch_down(
2905 1,
2906 pos(0.0, 0.0),
2907 ts(0),
2908 WindowPosition::Uninitialized,
2909 pos(0.0, 0.0),
2910 );
2911 m.touch_down(
2912 2,
2913 pos(10.0, 0.0),
2914 ts(1),
2915 WindowPosition::Uninitialized,
2916 pos(10.0, 0.0),
2917 );
2918 m.touch_move(2, pos(0.0, 10.0), ts(50), pos(0.0, 10.0));
2920 let r = m.detect_rotation().expect("a quarter turn is a rotation");
2921 assert!(
2922 (r.angle_radians - core::f32::consts::FRAC_PI_2).abs() < 1.0e-4,
2923 "expected ~PI/2, got {}",
2924 r.angle_radians
2925 );
2926 assert_eq!(r.center, pos(0.0, 5.0));
2927 }
2928
2929 #[test]
2930 fn rotation_below_the_angle_threshold_is_not_reported() {
2931 let mut m = GestureAndDragManager::new();
2932 m.touch_down(
2933 1,
2934 pos(0.0, 0.0),
2935 ts(0),
2936 WindowPosition::Uninitialized,
2937 pos(0.0, 0.0),
2938 );
2939 m.touch_down(
2940 2,
2941 pos(1000.0, 0.0),
2942 ts(1),
2943 WindowPosition::Uninitialized,
2944 pos(1000.0, 0.0),
2945 );
2946 m.touch_move(2, pos(1000.0, 50.0), ts(2), pos(1000.0, 50.0));
2948 assert!(m.detect_rotation().is_none());
2949 }
2950
2951 #[test]
2954 fn injected_native_gestures_win_over_the_in_process_detector() {
2955 let mut m = GestureAndDragManager::new();
2956
2957 m.inject_native_gesture(NativeGestureEvent::DoubleClick);
2958 assert!(m.detect_double_click(), "no sessions, but the OS said so");
2959 m.clear_native_gesture();
2960 assert!(!m.detect_double_click());
2961
2962 let lp = DetectedLongPress {
2963 position: pos(3.0, 4.0),
2964 duration_ms: u64::MAX,
2965 callback_invoked: true,
2966 session_id: u64::MAX,
2967 };
2968 m.inject_native_gesture(NativeGestureEvent::LongPress(lp));
2969 assert_eq!(m.detect_long_press(), Some(lp));
2970
2971 m.inject_native_gesture(NativeGestureEvent::Swipe(GestureDirection::Left));
2972 assert_eq!(m.detect_swipe_direction(), Some(GestureDirection::Left));
2973 assert!(
2974 !m.is_swipe(),
2975 "is_swipe() is velocity-only and ignores the native override"
2976 );
2977
2978 let pinch = DetectedPinch {
2979 scale: f32::INFINITY,
2980 center: pos(0.0, 0.0),
2981 initial_distance: 0.0,
2982 current_distance: f32::NAN,
2983 duration_ms: 0,
2984 };
2985 m.inject_native_gesture(NativeGestureEvent::Pinch(pinch));
2986 let got = m.detect_pinch().expect("native pinch is passed through");
2987 assert!(got.scale.is_infinite());
2988
2989 let rot = DetectedRotation {
2990 angle_radians: -core::f32::consts::PI,
2991 center: pos(1.0, 1.0),
2992 duration_ms: 7,
2993 };
2994 m.inject_native_gesture(NativeGestureEvent::Rotation(rot));
2995 assert_eq!(m.detect_rotation(), Some(rot));
2996
2997 m.clear_native_gesture();
2998 assert!(m.detect_long_press().is_none());
2999 assert!(m.detect_pinch().is_none());
3000 assert!(m.detect_rotation().is_none());
3001 assert!(m.detect_swipe_direction().is_none());
3002 }
3003
3004 #[test]
3007 fn pen_state_stores_extremes_verbatim_and_tracks_the_previous_state() {
3008 let mut m = GestureAndDragManager::new();
3009 m.update_pen_state(
3010 pos(1.0, 2.0),
3011 f32::NAN,
3012 (f32::INFINITY, f32::NEG_INFINITY),
3013 true,
3014 true,
3015 true,
3016 u64::MAX,
3017 );
3018 assert!(m.pen_event_pending);
3019 assert!(m.get_previous_pen_state().is_none());
3020 let pen = *m.get_pen_state().expect("pen state was just set");
3021 assert!(pen.pressure.is_nan());
3022 assert!(pen.tilt.x_tilt.is_infinite());
3023 assert!(pen.tilt.y_tilt.is_infinite());
3024 assert!(pen.in_contact && pen.is_eraser && pen.barrel_button_pressed);
3025 assert_eq!(pen.device_id, u64::MAX);
3026 assert_eq!(pen.tangential_pressure, 0.0);
3028 assert_eq!(pen.barrel_roll_rad, 0.0);
3029 assert_eq!(pen.tool_id, 0);
3030
3031 m.clear_pen_event_pending();
3032 assert!(!m.pen_event_pending);
3033
3034 m.update_pen_state_full(
3035 pos(0.0, 0.0),
3036 1.0,
3037 (0.0, 0.0),
3038 false,
3039 false,
3040 false,
3041 0,
3042 f32::NAN,
3043 -f32::MAX,
3044 u32::MAX,
3045 );
3046 assert!(m.pen_event_pending);
3047 let prev = *m.get_previous_pen_state().expect("previous pen state kept");
3048 assert_eq!(prev.device_id, u64::MAX);
3049 let now = *m.get_pen_state().unwrap();
3050 assert!(now.tangential_pressure.is_nan());
3051 assert_eq!(now.barrel_roll_rad, -f32::MAX);
3052 assert_eq!(now.tool_id, u32::MAX);
3053
3054 m.clear_pen_state();
3055 assert!(m.get_pen_state().is_none());
3056 assert_eq!(m.get_previous_pen_state().map(|p| p.tool_id), Some(u32::MAX));
3057 assert!(m.pen_event_pending);
3058
3059 m.clear_pen_state();
3061 assert!(m.get_pen_state().is_none());
3062 assert!(m.get_previous_pen_state().is_none());
3063 }
3064
3065 #[test]
3066 fn pad_state_round_trips_and_clears() {
3067 let mut m = GestureAndDragManager::new();
3068 assert!(m.get_pad_state().is_none());
3069 m.update_pad_state(WacomPadState {
3070 express_keys: 0b1010,
3071 touch_ring: f32::NAN,
3072 touch_ring_active: true,
3073 device_id: u64::MAX,
3074 });
3075 let pad = *m.get_pad_state().expect("pad state was just set");
3076 assert!(!pad.express_key(0));
3077 assert!(pad.express_key(1));
3078 assert!(!pad.express_key(2));
3079 assert!(pad.express_key(3));
3080 assert!(pad.touch_ring.is_nan());
3081 assert_eq!(pad.device_id, u64::MAX);
3082 m.clear_pad_state();
3083 assert!(m.get_pad_state().is_none());
3084 m.clear_pad_state();
3085 assert!(m.get_pad_state().is_none());
3086 }
3087
3088 #[test]
3091 fn drag_deltas_use_window_local_and_screen_coordinates_independently() {
3092 let mut m = GestureAndDragManager::new();
3093 m.start_input_session(
3094 pos(10.0, 10.0),
3095 ts(0),
3096 0x01,
3097 WindowPosition::Initialized(PhysicalPositionI32::new(100, 100)),
3098 pos(110.0, 110.0),
3099 );
3100 assert_eq!(m.get_drag_delta(), Some((0.0, 0.0)));
3102 assert_eq!(m.get_drag_delta_screen(), Some((0.0, 0.0)));
3103 assert!(m.get_drag_delta_screen_incremental().is_none());
3104
3105 m.record_input_sample(pos(15.0, 10.0), ts(10), 0x01, pos(120.0, 130.0));
3106 m.record_input_sample(pos(20.0, 10.0), ts(20), 0x01, pos(125.0, 132.0));
3107 assert_eq!(m.get_drag_delta(), Some((10.0, 0.0)));
3108 assert_eq!(m.get_drag_delta_screen(), Some((15.0, 22.0)));
3109 assert_eq!(m.get_drag_delta_screen_incremental(), Some((5.0, 2.0)));
3110 assert_eq!(
3111 m.get_window_position_at_session_start(),
3112 Some(WindowPosition::Initialized(PhysicalPositionI32::new(
3113 100, 100
3114 )))
3115 );
3116 assert_eq!(m.get_current_mouse_position(), Some(pos(20.0, 10.0)));
3117 }
3118
3119 #[test]
3120 fn drag_deltas_at_f32_extremes_stay_finite_or_saturate() {
3121 let m = dragging_manager(pos(-f32::MAX, -f32::MAX), pos(f32::MAX, f32::MAX), 5);
3122 let (dx, dy) = m.get_drag_delta().expect("two samples");
3123 assert!(dx.is_infinite() && dy.is_infinite());
3124 let (sx, sy) = m.get_drag_delta_screen().expect("two samples");
3125 assert!(sx.is_infinite() && sy.is_infinite());
3126 }
3127
3128 #[test]
3131 fn activating_a_node_drag_without_a_detected_drag_is_a_no_op() {
3132 let mut m = GestureAndDragManager::new();
3133 m.activate_node_drag(DomId::ROOT_ID, NodeId::new(1), DragData::new(), None);
3135 assert!(!m.is_dragging());
3136
3137 let mut m = dragging_manager(pos(0.0, 0.0), pos(1.0, 1.0), 10);
3139 m.activate_node_drag(DomId::ROOT_ID, NodeId::new(1), DragData::new(), None);
3140 assert!(!m.is_node_drag_active());
3141 m.activate_window_drag(WindowPosition::Uninitialized, None);
3142 assert!(!m.is_window_dragging());
3143 }
3144
3145 #[test]
3146 fn node_drag_context_tracks_its_own_node_and_drop_target() {
3147 let mut m = dragging_manager(pos(0.0, 0.0), pos(100.0, 0.0), 10);
3148 let mut data = DragData::new();
3149 data.set_text("payload");
3150 m.activate_node_drag(DomId::ROOT_ID, NodeId::new(4), data, None);
3151
3152 assert!(m.is_dragging());
3153 assert!(m.is_node_drag_active());
3154 assert!(m.is_node_dragging(DomId::ROOT_ID, NodeId::new(4)));
3155 assert!(!m.is_node_dragging(DomId::ROOT_ID, NodeId::new(5)));
3156 assert!(!m.is_node_dragging(DomId { inner: 7 }, NodeId::new(4)));
3157 assert!(!m.is_window_dragging());
3158 assert!(!m.is_file_dropping());
3159 assert!(!m.is_text_selection_dragging());
3160 assert!(!m.is_scrollbar_dragging());
3161 assert!(m.get_window_drag_delta().is_none());
3162 assert!(m.get_scrollbar_scroll_offset().is_none());
3163
3164 m.update_active_drag_positions(pos(42.0, -7.0));
3165 assert_eq!(
3166 m.get_drag_context().unwrap().current_position(),
3167 pos(42.0, -7.0)
3168 );
3169
3170 m.update_drop_target(Some(azul_core::dom::DomNodeId {
3171 dom: DomId::ROOT_ID,
3172 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(9))),
3173 }));
3174 let nd = m
3175 .get_drag_context()
3176 .and_then(DragContext::as_node_drag)
3177 .expect("node drag");
3178 assert_eq!(
3179 nd.current_drop_target
3180 .into_option()
3181 .and_then(|t| t.node.into_crate_internal()),
3182 Some(NodeId::new(9))
3183 );
3184 assert_eq!(nd.drag_data.get_data("text/plain"), Some(&b"payload"[..]));
3185
3186 m.update_drop_target(None);
3188 assert!(m
3189 .get_drag_context()
3190 .and_then(DragContext::as_node_drag)
3191 .unwrap()
3192 .current_drop_target
3193 .into_option()
3194 .is_none());
3195
3196 m.update_auto_scroll_direction(AutoScrollDirection::DownRight);
3198 assert!(m.is_node_drag_active());
3199
3200 let ctx = m.end_drag().expect("the drag context is returned");
3201 assert_eq!(ctx.session_id, 1);
3202 assert!(!m.is_dragging());
3203 assert!(m.end_drag().is_none());
3204 }
3205
3206 #[test]
3207 fn drop_target_and_auto_scroll_updates_without_a_drag_do_not_panic() {
3208 let mut m = GestureAndDragManager::new();
3209 m.update_drop_target(None);
3210 m.update_active_drag_positions(pos(f32::NAN, f32::INFINITY));
3211 m.update_auto_scroll_direction(AutoScrollDirection::UpLeft);
3212 m.cancel_drag();
3213 assert!(!m.is_dragging());
3214 assert!(m.get_drag_context_mut().is_none());
3215 }
3216
3217 #[test]
3218 fn text_selection_context_accepts_the_auto_scroll_direction() {
3219 let mut m = GestureAndDragManager::new();
3220 m.active_drag = Some(DragContext::text_selection(
3221 DomId::ROOT_ID,
3222 NodeId::new(2),
3223 pos(0.0, 0.0),
3224 11,
3225 ));
3226 assert!(m.is_text_selection_dragging());
3227 assert!(!m.is_node_drag_active());
3228 m.update_auto_scroll_direction(AutoScrollDirection::DownRight);
3229 assert_eq!(
3230 m.get_drag_context()
3231 .and_then(DragContext::as_text_selection)
3232 .map(|t| t.auto_scroll_direction),
3233 Some(AutoScrollDirection::DownRight)
3234 );
3235 m.update_drop_target(None);
3237 assert!(m.is_text_selection_dragging());
3238
3239 m.cancel_drag();
3240 assert!(!m.is_dragging());
3241 assert!(!m.is_text_selection_dragging());
3242 }
3243
3244 fn window_dragging_manager(initial: WindowPosition) -> GestureAndDragManager {
3247 let mut m = dragging_manager(pos(0.0, 0.0), pos(100.0, 0.0), 10);
3248 m.activate_window_drag(initial, None);
3249 assert!(m.is_window_dragging());
3250 m
3251 }
3252
3253 #[test]
3254 fn window_drag_delta_needs_an_initialized_window_position() {
3255 let m = window_dragging_manager(WindowPosition::Uninitialized);
3256 assert!(m.get_window_drag_delta().is_none());
3257 assert!(m.get_window_position_from_drag().is_none());
3258 }
3259
3260 #[test]
3261 fn window_drag_delta_is_measured_from_the_drag_start() {
3262 let mut m =
3263 window_dragging_manager(WindowPosition::Initialized(PhysicalPositionI32::new(10, 20)));
3264 m.update_active_drag_positions(pos(30.5, -20.9));
3265 assert_eq!(m.get_window_drag_delta(), Some((30, -20)));
3267 assert_eq!(
3268 m.get_window_position_from_drag(),
3269 Some(WindowPosition::Initialized(PhysicalPositionI32::new(40, 0)))
3270 );
3271 }
3272
3273 #[test]
3274 fn window_drag_delta_saturates_the_float_to_int_cast() {
3275 let mut m =
3276 window_dragging_manager(WindowPosition::Initialized(PhysicalPositionI32::new(0, 0)));
3277 m.update_active_drag_positions(pos(f32::MAX, -f32::MAX));
3278 assert_eq!(
3279 m.get_window_drag_delta(),
3280 Some((i32::MAX, i32::MIN)),
3281 "float->int casts must saturate, not wrap or trap"
3282 );
3283 assert_eq!(
3284 m.get_window_position_from_drag(),
3285 Some(WindowPosition::Initialized(PhysicalPositionI32::new(
3286 i32::MAX,
3287 i32::MIN
3288 )))
3289 );
3290 }
3291
3292 #[test]
3293 fn window_drag_delta_with_nan_position_is_zero_not_a_trap() {
3294 let mut m =
3295 window_dragging_manager(WindowPosition::Initialized(PhysicalPositionI32::new(3, 4)));
3296 m.update_active_drag_positions(pos(f32::NAN, f32::NAN));
3297 assert_eq!(m.get_window_drag_delta(), Some((0, 0)));
3299 assert_eq!(
3300 m.get_window_position_from_drag(),
3301 Some(WindowPosition::Initialized(PhysicalPositionI32::new(3, 4)))
3302 );
3303 }
3304
3305 #[test]
3306 fn window_position_from_drag_at_the_i32_extremes_does_not_overflow() {
3307 let mut m = window_dragging_manager(WindowPosition::Initialized(
3309 PhysicalPositionI32::new(i32::MAX, i32::MAX),
3310 ));
3311 m.update_active_drag_positions(pos(-f32::MAX, -f32::MAX));
3312 assert_eq!(
3313 m.get_window_position_from_drag(),
3314 Some(WindowPosition::Initialized(PhysicalPositionI32::new(-1, -1)))
3315 );
3316 }
3317
3318 fn scrollbar_manager(
3321 start_offset: f32,
3322 track: f32,
3323 content: f32,
3324 viewport: f32,
3325 ) -> GestureAndDragManager {
3326 let mut m = GestureAndDragManager::new();
3327 m.active_drag = Some(DragContext::scrollbar_thumb(
3328 DomId::ROOT_ID,
3329 NodeId::new(1),
3330 ScrollbarAxis::Vertical,
3331 pos(0.0, 0.0),
3332 start_offset,
3333 track,
3334 content,
3335 viewport,
3336 1,
3337 ));
3338 m
3339 }
3340
3341 #[test]
3342 fn scrollbar_offset_scales_the_mouse_delta_and_clamps_to_the_range() {
3343 let mut m = scrollbar_manager(0.0, 100.0, 1000.0, 100.0);
3344 assert!(m.is_scrollbar_dragging());
3345 assert_eq!(m.get_scrollbar_scroll_offset(), Some(0.0));
3346
3347 m.update_active_drag_positions(pos(0.0, 45.0));
3349 let half = m.get_scrollbar_scroll_offset().expect("scrollbar drag");
3350 assert!((half - 450.0).abs() < 0.5, "expected ~450, got {half}");
3351
3352 m.update_active_drag_positions(pos(0.0, 1.0e9));
3354 assert_eq!(m.get_scrollbar_scroll_offset(), Some(900.0));
3355
3356 m.update_active_drag_positions(pos(0.0, -1.0e9));
3358 assert_eq!(m.get_scrollbar_scroll_offset(), Some(0.0));
3359 }
3360
3361 #[test]
3362 fn scrollbar_offset_with_nothing_to_scroll_returns_the_start_offset() {
3363 let mut m = scrollbar_manager(42.0, 100.0, 50.0, 100.0);
3365 m.update_active_drag_positions(pos(0.0, 500.0));
3366 assert_eq!(m.get_scrollbar_scroll_offset(), Some(42.0));
3367
3368 let mut m = scrollbar_manager(7.0, 0.0, 1000.0, 100.0);
3370 m.update_active_drag_positions(pos(0.0, 500.0));
3371 assert_eq!(m.get_scrollbar_scroll_offset(), Some(7.0));
3372 }
3373
3374 #[test]
3375 fn scrollbar_offset_with_a_nan_mouse_position_does_not_panic() {
3376 let mut m = scrollbar_manager(0.0, 100.0, 1000.0, 100.0);
3377 m.update_active_drag_positions(pos(f32::NAN, f32::NAN));
3378 let v = m.get_scrollbar_scroll_offset();
3379 assert!(
3380 v.is_some_and(f32::is_nan),
3381 "a NaN mouse position must propagate as NaN, not panic: {v:?}"
3382 );
3383 }
3384
3385 #[cfg(feature = "std")]
3388 #[test]
3389 fn allocate_event_id_is_strictly_monotonic() {
3390 let a = allocate_event_id();
3391 let b = allocate_event_id();
3392 let c = allocate_event_id();
3393 assert!(a < b && b < c, "ids must increase: {a} {b} {c}");
3394 }
3395
3396 #[cfg(not(feature = "std"))]
3397 #[test]
3398 fn allocate_event_id_is_zero_without_std() {
3399 assert_eq!(allocate_event_id(), 0);
3400 }
3401
3402 #[cfg(feature = "std")]
3403 #[test]
3404 fn recorded_samples_get_distinct_event_ids() {
3405 let mut m = GestureAndDragManager::new();
3406 m.start_input_session(
3407 pos(0.0, 0.0),
3408 ts(0),
3409 0x01,
3410 WindowPosition::Uninitialized,
3411 pos(0.0, 0.0),
3412 );
3413 m.record_input_sample(pos(1.0, 0.0), ts(1), 0x01, pos(1.0, 0.0));
3414 let s = m.get_current_session().unwrap();
3415 assert_ne!(s.samples[0].event_id, s.samples[1].event_id);
3416 assert!(s.samples[0].event_id < s.samples[1].event_id);
3417 }
3418}