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}