1use alloc::{collections::btree_map::BTreeMap, vec::Vec};
17#[cfg(feature = "std")]
18use std::sync::atomic::{AtomicU64, Ordering};
19
20use azul_core::{
21 dom::{DomId, NodeId, OptionDomNodeId},
22 drag::{
23 ActiveDragType, AutoScrollDirection, DragContext, DragData, DragEffect, DropEffect,
24 FileDropDrag, NodeDrag, ScrollbarAxis, ScrollbarThumbDrag, TextSelectionDrag,
25 WindowMoveDrag, WindowResizeDrag, WindowResizeEdge,
26 },
27 geom::{LogicalPosition, PhysicalPositionI32},
28 hit_test::HitTest,
29 selection::TextCursor,
30 task::{Duration as CoreDuration, Instant as CoreInstant},
31 window::WindowPosition,
32};
33use azul_css::AzString;
34use azul_css::{impl_option, impl_option_inner, StringVec};
35
36pub use azul_core::drag::{
38 ActiveDragType as DragType, AutoScrollDirection as AutoScroll, DragContext as UnifiedDragContext,
39 DragData as UnifiedDragData, DragEffect as UnifiedDragEffect, DropEffect as UnifiedDropEffect,
40 ScrollbarAxis as ScrollAxis, ScrollbarThumbDrag as ScrollbarDrag,
41};
42
43#[cfg(feature = "std")]
44static NEXT_EVENT_ID: AtomicU64 = AtomicU64::new(1);
45
46#[cfg(feature = "std")]
48pub fn allocate_event_id() -> u64 {
49 NEXT_EVENT_ID.fetch_add(1, Ordering::Relaxed)
50}
51
52#[cfg(not(feature = "std"))]
54pub fn allocate_event_id() -> u64 {
55 0
56}
57
58fn duration_to_millis(duration: CoreDuration) -> u64 {
63 match duration {
64 #[cfg(feature = "std")]
65 CoreDuration::System(system_diff) => {
66 let std_duration: std::time::Duration = system_diff.into();
67 std_duration.as_millis() as u64
68 }
69 #[cfg(not(feature = "std"))]
70 CoreDuration::System(system_diff) => {
71 system_diff.secs * 1000 + (system_diff.nanos / 1_000_000) as u64
73 }
74 CoreDuration::Tick(tick_diff) => {
75 tick_diff.tick_diff
79 }
80 }
81}
82
83pub const MAX_SAMPLES_PER_SESSION: usize = 1000;
88
89pub const DEFAULT_SAMPLE_TIMEOUT_MS: u64 = 2000;
94
95const DRAIN_BATCH_SIZE: usize = 100;
99
100#[derive(Debug, Clone, Copy, PartialEq)]
102pub struct GestureDetectionConfig {
103 pub drag_distance_threshold: f32,
105 pub double_click_time_threshold_ms: u64,
107 pub double_click_distance_threshold: f32,
109 pub long_press_time_threshold_ms: u64,
111 pub long_press_distance_threshold: f32,
113 pub min_samples_for_gesture: usize,
115 pub swipe_velocity_threshold: f32,
117 pub pinch_scale_threshold: f32,
119 pub rotation_angle_threshold: f32,
121 pub sample_cleanup_interval_ms: u64,
123}
124
125impl Default for GestureDetectionConfig {
126 fn default() -> Self {
127 Self {
128 drag_distance_threshold: 5.0,
129 double_click_time_threshold_ms: 500,
130 double_click_distance_threshold: 5.0,
131 long_press_time_threshold_ms: 500,
132 long_press_distance_threshold: 10.0,
133 min_samples_for_gesture: 2,
134 swipe_velocity_threshold: 500.0, pinch_scale_threshold: 0.1, rotation_angle_threshold: 0.1, sample_cleanup_interval_ms: DEFAULT_SAMPLE_TIMEOUT_MS,
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq)]
144pub struct InputSample {
145 pub position: LogicalPosition,
147 pub screen_position: LogicalPosition,
156 pub timestamp: CoreInstant,
158 pub button_state: u8,
160 pub event_id: u64,
162 pub pressure: f32,
164 pub tilt: (f32, f32),
167 pub touch_radius: (f32, f32),
170}
171
172impl_option!(
173 InputSample,
174 OptionInputSample,
175 copy = false,
176 [Debug, Clone, PartialEq]
177);
178
179#[derive(Debug, Clone, PartialEq)]
181pub struct InputSession {
182 pub samples: Vec<InputSample>,
184 pub ended: bool,
186 pub session_id: u64,
188 pub window_position_at_start: azul_core::window::WindowPosition,
191}
192
193impl InputSession {
194 fn new(session_id: u64, first_sample: InputSample, window_position: azul_core::window::WindowPosition) -> Self {
196 Self {
197 samples: vec![first_sample],
198 ended: false,
199 session_id,
200 window_position_at_start: window_position,
201 }
202 }
203
204 pub fn first_sample(&self) -> Option<&InputSample> {
206 self.samples.first()
207 }
208
209 pub fn last_sample(&self) -> Option<&InputSample> {
211 self.samples.last()
212 }
213
214 pub fn duration_ms(&self) -> Option<u64> {
216 let first = self.first_sample()?;
217 let last = self.last_sample()?;
218 let duration = last.timestamp.duration_since(&first.timestamp);
219 Some(duration_to_millis(duration))
220 }
221
222 pub fn total_distance(&self) -> f32 {
224 if self.samples.len() < 2 {
225 return 0.0;
226 }
227
228 let mut total = 0.0;
229 for i in 1..self.samples.len() {
230 let prev = &self.samples[i - 1];
231 let curr = &self.samples[i];
232 let dx = curr.position.x - prev.position.x;
233 let dy = curr.position.y - prev.position.y;
234 total += (dx * dx + dy * dy).sqrt();
235 }
236 total
237 }
238
239 pub fn direct_distance(&self) -> Option<f32> {
241 let first = self.first_sample()?;
242 let last = self.last_sample()?;
243 let dx = last.position.x - first.position.x;
244 let dy = last.position.y - first.position.y;
245 Some((dx * dx + dy * dy).sqrt())
246 }
247}
248
249#[derive(Debug, Clone, Copy, PartialEq)]
251pub struct DetectedDrag {
252 pub start_position: LogicalPosition,
254 pub current_position: LogicalPosition,
256 pub direct_distance: f32,
258 pub total_distance: f32,
260 pub duration_ms: u64,
262 pub sample_count: usize,
264 pub session_id: u64,
266}
267
268#[derive(Debug, Clone, Copy, PartialEq)]
270#[repr(C)]
271pub struct DetectedLongPress {
272 pub position: LogicalPosition,
274 pub duration_ms: u64,
276 pub callback_invoked: bool,
278 pub session_id: u64,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284#[repr(C)]
285pub enum GestureDirection {
286 Up,
287 Down,
288 Left,
289 Right,
290}
291
292impl_option!(
293 GestureDirection,
294 OptionGestureDirection,
295 [Debug, Clone, Copy, PartialEq, Eq]
296);
297impl_option!(
298 DetectedPinch,
299 OptionDetectedPinch,
300 [Debug, Clone, Copy, PartialEq]
301);
302impl_option!(
303 DetectedRotation,
304 OptionDetectedRotation,
305 [Debug, Clone, Copy, PartialEq]
306);
307impl_option!(
308 DetectedLongPress,
309 OptionDetectedLongPress,
310 [Debug, Clone, Copy, PartialEq]
311);
312
313#[derive(Debug, Clone, Copy, PartialEq)]
315#[repr(C)]
316pub struct DetectedPinch {
317 pub scale: f32,
319 pub center: LogicalPosition,
321 pub initial_distance: f32,
323 pub current_distance: f32,
325 pub duration_ms: u64,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq)]
331#[repr(C)]
332pub struct DetectedRotation {
333 pub angle_radians: f32,
335 pub center: LogicalPosition,
337 pub duration_ms: u64,
339}
340
341pub type NodeDragState = NodeDrag;
348
349pub type WindowDragState = WindowMoveDrag;
352
353pub type FileDropState = FileDropDrag;
356
357#[derive(Debug, Clone, Copy, PartialEq)]
359#[repr(C)]
360pub struct PenState {
361 pub position: LogicalPosition,
363 pub pressure: f32,
365 pub tilt: crate::callbacks::PenTilt,
367 pub in_contact: bool,
369 pub is_eraser: bool,
371 pub barrel_button_pressed: bool,
373 pub device_id: u64,
375 pub tangential_pressure: f32,
379 pub barrel_roll_rad: f32,
386 pub tool_id: u32,
392}
393
394impl_option!(PenState, OptionPenState, [Debug, Clone, Copy, PartialEq]);
395
396impl Default for PenState {
397 fn default() -> Self {
398 Self {
399 position: LogicalPosition::zero(),
400 pressure: 0.0,
401 tilt: crate::callbacks::PenTilt {
402 x_tilt: 0.0,
403 y_tilt: 0.0,
404 },
405 in_contact: false,
406 is_eraser: false,
407 barrel_button_pressed: false,
408 device_id: 0,
409 tangential_pressure: 0.0,
410 barrel_roll_rad: 0.0,
411 tool_id: 0,
412 }
413 }
414}
415
416#[derive(Debug, Clone, Copy, PartialEq)]
422#[repr(C)]
423pub struct WacomPadState {
424 pub express_keys: u32,
427 pub touch_ring: f32,
430 pub touch_ring_active: bool,
432 pub device_id: u64,
434}
435
436impl_option!(
437 WacomPadState,
438 OptionWacomPadState,
439 [Debug, Clone, Copy, PartialEq]
440);
441
442impl Default for WacomPadState {
443 fn default() -> Self {
444 Self {
445 express_keys: 0,
446 touch_ring: 0.0,
447 touch_ring_active: false,
448 device_id: 0,
449 }
450 }
451}
452
453impl WacomPadState {
454 pub fn express_key(&self, index: u32) -> bool {
456 index < 32 && (self.express_keys & (1u32 << index)) != 0
457 }
458}
459
460#[derive(Debug, Clone, PartialEq)]
474pub struct GestureAndDragManager {
475 pub config: GestureDetectionConfig,
477 pub input_sessions: Vec<InputSession>,
479 pub active_drag: Option<DragContext>,
481 pub pen_state: Option<PenState>,
483 pub previous_pen_state: Option<PenState>,
485 pub pen_event_pending: bool,
487 pub pad_state: Option<WacomPadState>,
490 long_press_callbacks_invoked: Vec<u64>,
492 next_session_id: u64,
494 pub native_gesture: Option<NativeGestureEvent>,
507}
508
509#[derive(Debug, Clone, Copy, PartialEq)]
520#[repr(C, u8)]
521pub enum NativeGestureEvent {
522 DoubleClick,
524 LongPress(DetectedLongPress),
527 Swipe(GestureDirection),
530 Pinch(DetectedPinch),
533 Rotation(DetectedRotation),
536}
537
538pub type GestureManager = GestureAndDragManager;
540
541impl Default for GestureAndDragManager {
542 fn default() -> Self {
543 Self::new()
544 }
545}
546
547impl GestureAndDragManager {
548 pub fn debug_counts(&self) -> (usize, usize) {
551 (self.input_sessions.len(), self.long_press_callbacks_invoked.len())
552 }
553
554 pub fn new() -> Self {
556 Self {
557 config: GestureDetectionConfig::default(),
558 input_sessions: Vec::new(),
559 next_session_id: 1,
560 active_drag: None,
561 pen_state: None,
562 previous_pen_state: None,
563 pen_event_pending: false,
564 pad_state: None,
565 long_press_callbacks_invoked: Vec::new(),
566 native_gesture: None,
567 }
568 }
569
570 pub fn inject_native_gesture(&mut self, gesture: NativeGestureEvent) {
576 self.native_gesture = Some(gesture);
577 }
578
579 pub fn clear_native_gesture(&mut self) {
583 self.native_gesture = None;
584 }
585
586 pub fn with_config(config: GestureDetectionConfig) -> Self {
588 Self {
589 config,
590 ..Self::new()
591 }
592 }
593
594 pub fn start_input_session(
606 &mut self,
607 position: LogicalPosition,
608 timestamp: CoreInstant,
609 button_state: u8,
610 window_position: azul_core::window::WindowPosition,
611 screen_position: LogicalPosition,
612 ) -> u64 {
613 self.start_input_session_with_pen(
614 position,
615 timestamp,
616 button_state,
617 allocate_event_id(),
618 0.5, (0.0, 0.0), (0.0, 0.0), window_position,
622 screen_position,
623 )
624 }
625
626 pub fn start_input_session_with_pen(
628 &mut self,
629 position: LogicalPosition,
630 timestamp: CoreInstant,
631 button_state: u8,
632 event_id: u64,
633 pressure: f32,
634 tilt: (f32, f32),
635 touch_radius: (f32, f32),
636 window_position: azul_core::window::WindowPosition,
637 screen_position: LogicalPosition,
638 ) -> u64 {
639 let last_ended_idx = self.input_sessions.iter().rposition(|s| s.ended);
643 let mut idx = 0usize;
644 self.input_sessions.retain(|session| {
645 let keep = !session.ended || Some(idx) == last_ended_idx;
646 idx += 1;
647 keep
648 });
649
650 let session_id = self.next_session_id;
651 self.next_session_id += 1;
652
653 let sample = InputSample {
654 position,
655 screen_position,
656 timestamp,
657 button_state,
658 event_id,
659 pressure,
660 tilt,
661 touch_radius,
662 };
663
664 let session = InputSession::new(session_id, sample, window_position);
665 self.input_sessions.push(session);
666
667 session_id
668 }
669
670 pub fn record_input_sample(
677 &mut self,
678 position: LogicalPosition,
679 timestamp: CoreInstant,
680 button_state: u8,
681 screen_position: LogicalPosition,
682 ) -> bool {
683 self.record_input_sample_with_pen(
684 position,
685 timestamp,
686 button_state,
687 allocate_event_id(),
688 0.5, (0.0, 0.0), (0.0, 0.0), screen_position,
692 )
693 }
694
695 pub fn record_input_sample_with_pen(
697 &mut self,
698 position: LogicalPosition,
699 timestamp: CoreInstant,
700 button_state: u8,
701 event_id: u64,
702 pressure: f32,
703 tilt: (f32, f32),
704 touch_radius: (f32, f32),
705 screen_position: LogicalPosition,
706 ) -> bool {
707 let session = match self.input_sessions.last_mut() {
708 Some(s) => s,
709 None => return false,
710 };
711
712 if session.ended {
713 return false;
714 }
715
716 if session.samples.len() >= MAX_SAMPLES_PER_SESSION {
718 let remove_count = session.samples.len() - MAX_SAMPLES_PER_SESSION + DRAIN_BATCH_SIZE;
720 session.samples.drain(0..remove_count);
721 }
722
723 session.samples.push(InputSample {
724 position,
725 screen_position,
726 timestamp,
727 button_state,
728 event_id,
729 pressure,
730 tilt,
731 touch_radius,
732 });
733
734 true
735 }
736
737 pub fn end_current_session(&mut self) {
742 if let Some(session) = self.input_sessions.last_mut() {
743 session.ended = true;
744 }
745 }
746
747 pub fn clear_old_sessions(&mut self, current_time: CoreInstant) {
752 self.input_sessions.retain(|session| {
753 if let Some(last_sample) = session.last_sample() {
754 let duration = current_time.duration_since(&last_sample.timestamp);
755 let age_ms = duration_to_millis(duration);
756 age_ms < self.config.sample_cleanup_interval_ms
757 } else {
758 false
759 }
760 });
761
762 let valid_session_ids: Vec<u64> =
764 self.input_sessions.iter().map(|s| s.session_id).collect();
765
766 self.long_press_callbacks_invoked
767 .retain(|id| valid_session_ids.contains(id));
768 }
769
770 pub fn clear_all_sessions(&mut self) {
774 self.input_sessions.clear();
775 self.long_press_callbacks_invoked.clear();
776 }
777
778 pub fn update_pen_state(
785 &mut self,
786 position: LogicalPosition,
787 pressure: f32,
788 tilt: (f32, f32),
789 in_contact: bool,
790 is_eraser: bool,
791 barrel_button_pressed: bool,
792 device_id: u64,
793 ) {
794 self.update_pen_state_full(
795 position,
796 pressure,
797 tilt,
798 in_contact,
799 is_eraser,
800 barrel_button_pressed,
801 device_id,
802 0.0,
803 0.0,
804 0,
805 );
806 }
807
808 pub fn update_pen_state_full(
811 &mut self,
812 position: LogicalPosition,
813 pressure: f32,
814 tilt: (f32, f32),
815 in_contact: bool,
816 is_eraser: bool,
817 barrel_button_pressed: bool,
818 device_id: u64,
819 tangential_pressure: f32,
820 barrel_roll_rad: f32,
821 tool_id: u32,
822 ) {
823 self.previous_pen_state = self.pen_state.clone();
824 self.pen_state = Some(PenState {
825 position,
826 pressure,
827 tilt: crate::callbacks::PenTilt {
828 x_tilt: tilt.0,
829 y_tilt: tilt.1,
830 },
831 in_contact,
832 is_eraser,
833 barrel_button_pressed,
834 device_id,
835 tangential_pressure,
836 barrel_roll_rad,
837 tool_id,
838 });
839 self.pen_event_pending = true;
840 }
841
842 pub fn clear_pen_state(&mut self) {
844 self.previous_pen_state = self.pen_state.clone();
845 self.pen_state = None;
846 self.pen_event_pending = true;
847 }
848
849 pub fn get_pen_state(&self) -> Option<&PenState> {
851 self.pen_state.as_ref()
852 }
853
854 pub fn get_previous_pen_state(&self) -> Option<&PenState> {
856 self.previous_pen_state.as_ref()
857 }
858
859 pub fn clear_pen_event_pending(&mut self) {
861 self.pen_event_pending = false;
862 }
863
864 pub fn update_pad_state(&mut self, pad: WacomPadState) {
866 self.pad_state = Some(pad);
867 }
868
869 pub fn get_pad_state(&self) -> Option<&WacomPadState> {
871 self.pad_state.as_ref()
872 }
873
874 pub fn clear_pad_state(&mut self) {
876 self.pad_state = None;
877 }
878
879 pub fn detect_drag(&self) -> Option<DetectedDrag> {
885 let session = self.get_current_session()?;
886
887 if session.samples.len() < self.config.min_samples_for_gesture {
888 return None;
889 }
890
891 let direct_distance = session.direct_distance()?;
892
893 if direct_distance >= self.config.drag_distance_threshold {
894 let first = session.first_sample()?;
895 let last = session.last_sample()?;
896
897 Some(DetectedDrag {
898 start_position: first.position,
899 current_position: last.position,
900 direct_distance,
901 total_distance: session.total_distance(),
902 duration_ms: session.duration_ms()?,
903 sample_count: session.samples.len(),
904 session_id: session.session_id,
905 })
906 } else {
907 None
908 }
909 }
910
911 pub fn detect_long_press(&self) -> Option<DetectedLongPress> {
916 if let Some(NativeGestureEvent::LongPress(lp)) = self.native_gesture {
917 return Some(lp);
918 }
919 let session = self.get_current_session()?;
920
921 if session.ended {
922 return None; }
924
925 let duration_ms = session.duration_ms()?;
926
927 if duration_ms < self.config.long_press_time_threshold_ms {
928 return None;
929 }
930
931 let distance = session.direct_distance()?;
932
933 if distance <= self.config.long_press_distance_threshold {
934 let first = session.first_sample()?;
935 let callback_invoked = self
936 .long_press_callbacks_invoked
937 .contains(&session.session_id);
938
939 Some(DetectedLongPress {
940 position: first.position,
941 duration_ms,
942 callback_invoked,
943 session_id: session.session_id,
944 })
945 } else {
946 None
947 }
948 }
949
950 pub fn mark_long_press_callback_invoked(&mut self, session_id: u64) {
955 if !self.long_press_callbacks_invoked.contains(&session_id) {
956 self.long_press_callbacks_invoked.push(session_id);
957 }
958 }
959
960 pub fn detect_double_click(&self) -> bool {
964 if matches!(self.native_gesture, Some(NativeGestureEvent::DoubleClick)) {
965 return true;
966 }
967 let sessions = &self.input_sessions;
968 if sessions.len() < 2 {
969 return false;
970 }
971
972 let prev_session = &sessions[sessions.len() - 2];
973 let last_session = &sessions[sessions.len() - 1];
974
975 if !prev_session.ended || !last_session.ended {
977 return false;
978 }
979
980 let prev_first = prev_session.first_sample();
981 let last_first = last_session.first_sample();
982 let (prev_first, last_first) = match (prev_first, last_first) {
983 (Some(p), Some(l)) => (p, l),
984 _ => return false,
985 };
986
987 let duration = last_first.timestamp.duration_since(&prev_first.timestamp);
988 let time_delta_ms = duration_to_millis(duration);
989 if time_delta_ms > self.config.double_click_time_threshold_ms {
990 return false;
991 }
992
993 let dx = last_first.position.x - prev_first.position.x;
994 let dy = last_first.position.y - prev_first.position.y;
995 let distance = (dx * dx + dy * dy).sqrt();
996
997 distance < self.config.double_click_distance_threshold
998 }
999
1000 pub fn detect_click_count(&self) -> u32 {
1006 let sessions = &self.input_sessions;
1007 let n = sessions.len();
1008 if n == 0 {
1009 return 1;
1010 }
1011
1012 let mut recent: Vec<&InputSession> = Vec::new();
1020 for s in sessions.iter().rev() {
1021 if !s.ended {
1022 continue;
1023 }
1024 recent.push(s);
1025 if recent.len() >= 3 {
1026 break;
1027 }
1028 }
1029
1030 if recent.is_empty() {
1031 return 1;
1032 }
1033
1034 let mut count = 1u32;
1038
1039 for i in 0..recent.len() - 1 {
1040 let later = recent[i];
1041 let earlier = recent[i + 1];
1042
1043 let later_start = match later.first_sample() {
1044 Some(s) => s,
1045 None => break,
1046 };
1047 let earlier_start = match earlier.first_sample() {
1048 Some(s) => s,
1049 None => break,
1050 };
1051
1052 let duration = later_start.timestamp.duration_since(&earlier_start.timestamp);
1053 let time_delta_ms = duration_to_millis(duration);
1054 if time_delta_ms > self.config.double_click_time_threshold_ms {
1055 break;
1056 }
1057
1058 let dx = later_start.position.x - earlier_start.position.x;
1059 let dy = later_start.position.y - earlier_start.position.y;
1060 let distance = (dx * dx + dy * dy).sqrt();
1061 if distance >= self.config.double_click_distance_threshold {
1062 break;
1063 }
1064
1065 count += 1;
1066 }
1067
1068 if count > 3 { 1 } else { count }
1070 }
1071
1072 pub fn get_drag_direction(&self) -> Option<GestureDirection> {
1074 let session = self.get_current_session()?;
1075 let first = session.first_sample()?;
1076 let last = session.last_sample()?;
1077
1078 let dx = last.position.x - first.position.x;
1079 let dy = last.position.y - first.position.y;
1080
1081 let direction = match (dx.abs() > dy.abs(), dx > 0.0, dy > 0.0) {
1082 (true, true, _) => GestureDirection::Right,
1083 (true, false, _) => GestureDirection::Left,
1084 (false, _, true) => GestureDirection::Down,
1085 (false, _, false) => GestureDirection::Up,
1086 };
1087 Some(direction)
1088 }
1089
1090 pub fn get_gesture_velocity(&self) -> Option<f32> {
1092 let session = self.get_current_session()?;
1093
1094 if session.samples.len() < 2 {
1095 return None;
1096 }
1097
1098 let total_distance = session.total_distance();
1099 let duration_ms = session.duration_ms()?;
1100
1101 if duration_ms == 0 {
1102 return None;
1103 }
1104
1105 let duration_secs = duration_ms as f32 / 1000.0;
1106 Some(total_distance / duration_secs)
1107 }
1108
1109 pub fn is_swipe(&self) -> bool {
1111 self.get_gesture_velocity()
1112 .map(|v| v >= self.config.swipe_velocity_threshold)
1113 .unwrap_or(false)
1114 }
1115
1116 pub fn detect_swipe_direction(&self) -> Option<GestureDirection> {
1120 if let Some(NativeGestureEvent::Swipe(d)) = self.native_gesture {
1121 return Some(d);
1122 }
1123 if !self.is_swipe() {
1125 return None;
1126 }
1127
1128 self.get_drag_direction()
1130 }
1131
1132 pub fn detect_pinch(&self) -> Option<DetectedPinch> {
1137 if let Some(NativeGestureEvent::Pinch(p)) = self.native_gesture {
1138 return Some(p);
1139 }
1140 if self.input_sessions.len() < 2 {
1142 return None;
1143 }
1144
1145 let session1 = &self.input_sessions[self.input_sessions.len() - 2];
1147 let session2 = &self.input_sessions[self.input_sessions.len() - 1];
1148
1149 let first1 = session1.first_sample()?;
1151 let first2 = session2.first_sample()?;
1152 let last1 = session1.last_sample()?;
1153 let last2 = session2.last_sample()?;
1154
1155 let dx_initial = first2.position.x - first1.position.x;
1157 let dy_initial = first2.position.y - first1.position.y;
1158 let initial_distance = (dx_initial * dx_initial + dy_initial * dy_initial).sqrt();
1159
1160 let dx_current = last2.position.x - last1.position.x;
1162 let dy_current = last2.position.y - last1.position.y;
1163 let current_distance = (dx_current * dx_current + dy_current * dy_current).sqrt();
1164
1165 if initial_distance < 1.0 {
1167 return None;
1168 }
1169
1170 let scale = current_distance / initial_distance;
1172
1173 let scale_threshold = 1.0 + self.config.pinch_scale_threshold;
1175 if scale > 1.0 / scale_threshold && scale < scale_threshold {
1176 return None; }
1178
1179 let center = LogicalPosition {
1181 x: (last1.position.x + last2.position.x) / 2.0,
1182 y: (last1.position.y + last2.position.y) / 2.0,
1183 };
1184
1185 let duration = last1.timestamp.duration_since(&first1.timestamp);
1187 let duration_ms = duration_to_millis(duration);
1188
1189 Some(DetectedPinch {
1190 scale,
1191 center,
1192 initial_distance,
1193 current_distance,
1194 duration_ms,
1195 })
1196 }
1197
1198 pub fn detect_rotation(&self) -> Option<DetectedRotation> {
1203 if let Some(NativeGestureEvent::Rotation(r)) = self.native_gesture {
1204 return Some(r);
1205 }
1206 if self.input_sessions.len() < 2 {
1208 return None;
1209 }
1210
1211 let session1 = &self.input_sessions[self.input_sessions.len() - 2];
1213 let session2 = &self.input_sessions[self.input_sessions.len() - 1];
1214
1215 let first1 = session1.first_sample()?;
1217 let first2 = session2.first_sample()?;
1218 let last1 = session1.last_sample()?;
1219 let last2 = session2.last_sample()?;
1220
1221 let center = LogicalPosition {
1223 x: (last1.position.x + last2.position.x) / 2.0,
1224 y: (last1.position.y + last2.position.y) / 2.0,
1225 };
1226
1227 let dx_initial = first2.position.x - first1.position.x;
1229 let dy_initial = first2.position.y - first1.position.y;
1230 let initial_angle = dy_initial.atan2(dx_initial);
1231
1232 let dx_current = last2.position.x - last1.position.x;
1234 let dy_current = last2.position.y - last1.position.y;
1235 let current_angle = dy_current.atan2(dx_current);
1236
1237 let mut angle_diff = current_angle - initial_angle;
1239
1240 const PI: f32 = core::f32::consts::PI;
1242 while angle_diff > PI {
1243 angle_diff -= 2.0 * PI;
1244 }
1245 while angle_diff < -PI {
1246 angle_diff += 2.0 * PI;
1247 }
1248
1249 if angle_diff.abs() < self.config.rotation_angle_threshold {
1251 return None;
1252 }
1253
1254 let duration = last1.timestamp.duration_since(&first1.timestamp);
1256 let duration_ms = duration_to_millis(duration);
1257
1258 Some(DetectedRotation {
1259 angle_radians: angle_diff,
1260 center,
1261 duration_ms,
1262 })
1263 }
1264
1265 pub fn get_current_session(&self) -> Option<&InputSession> {
1267 self.input_sessions.last()
1268 }
1269
1270 pub fn get_current_mouse_position(&self) -> Option<LogicalPosition> {
1272 self.get_current_session()
1273 .and_then(|s| s.last_sample())
1274 .map(|sample| sample.position)
1275 }
1276
1277 pub fn get_drag_delta(&self) -> Option<(f32, f32)> {
1282 let session = self.get_current_session()?;
1283 let first = session.first_sample()?;
1284 let last = session.last_sample()?;
1285 Some((
1286 last.position.x - first.position.x,
1287 last.position.y - first.position.y,
1288 ))
1289 }
1290
1291 pub fn get_drag_delta_screen(&self) -> Option<(f32, f32)> {
1303 let session = self.get_current_session()?;
1304 let first = session.first_sample()?;
1305 let last = session.last_sample()?;
1306 Some((
1307 last.screen_position.x - first.screen_position.x,
1308 last.screen_position.y - first.screen_position.y,
1309 ))
1310 }
1311
1312 pub fn get_drag_delta_screen_incremental(&self) -> Option<(f32, f32)> {
1331 let session = self.get_current_session()?;
1332 let len = session.samples.len();
1333 if len < 2 {
1334 return None;
1335 }
1336 let prev = &session.samples[len - 2];
1337 let last = &session.samples[len - 1];
1338 Some((
1339 last.screen_position.x - prev.screen_position.x,
1340 last.screen_position.y - prev.screen_position.y,
1341 ))
1342 }
1343
1344 pub fn get_window_position_at_session_start(&self) -> Option<azul_core::window::WindowPosition> {
1348 let session = self.get_current_session()?;
1349 Some(session.window_position_at_start)
1350 }
1351
1352 pub fn get_drag_context(&self) -> Option<&DragContext> {
1358 self.active_drag.as_ref()
1359 }
1360
1361 pub fn get_drag_context_mut(&mut self) -> Option<&mut DragContext> {
1363 self.active_drag.as_mut()
1364 }
1365
1366 pub fn activate_text_selection_drag(
1368 &mut self,
1369 dom_id: DomId,
1370 anchor_ifc_node: NodeId,
1371 start_mouse_position: LogicalPosition,
1372 ) {
1373 let session_id = self.current_session_id().unwrap_or(0);
1374 self.active_drag = Some(DragContext::text_selection(
1375 dom_id,
1376 anchor_ifc_node,
1377 start_mouse_position,
1378 session_id,
1379 ));
1380 }
1381
1382 pub fn activate_scrollbar_drag(
1384 &mut self,
1385 scroll_container_node: NodeId,
1386 axis: ScrollbarAxis,
1387 start_mouse_position: LogicalPosition,
1388 start_scroll_offset: f32,
1389 track_length_px: f32,
1390 content_length_px: f32,
1391 viewport_length_px: f32,
1392 ) {
1393 let session_id = self.current_session_id().unwrap_or(0);
1394 self.active_drag = Some(DragContext::scrollbar_thumb(
1395 scroll_container_node,
1396 axis,
1397 start_mouse_position,
1398 start_scroll_offset,
1399 track_length_px,
1400 content_length_px,
1401 viewport_length_px,
1402 session_id,
1403 ));
1404 }
1405
1406 pub fn activate_node_drag(
1408 &mut self,
1409 dom_id: DomId,
1410 node_id: NodeId,
1411 drag_data: DragData,
1412 _start_hit_test: Option<HitTest>,
1413 ) {
1414 if let Some(detected) = self.detect_drag() {
1415 self.active_drag = Some(DragContext::node_drag(
1416 dom_id,
1417 node_id,
1418 detected.start_position,
1419 drag_data,
1420 detected.session_id,
1421 ));
1422 }
1423 }
1424
1425 pub fn activate_window_drag(
1427 &mut self,
1428 initial_window_position: WindowPosition,
1429 _start_hit_test: Option<HitTest>,
1430 ) {
1431 if let Some(detected) = self.detect_drag() {
1432 self.active_drag = Some(DragContext::window_move(
1433 detected.start_position,
1434 initial_window_position,
1435 detected.session_id,
1436 ));
1437 }
1438 }
1439
1440 pub fn start_file_drop(&mut self, files: Vec<AzString>, position: LogicalPosition) {
1442 let session_id = self.current_session_id().unwrap_or(0);
1443 self.active_drag = Some(DragContext::file_drop(files, position, session_id));
1444 }
1445
1446 pub fn update_active_drag_positions(&mut self, position: LogicalPosition) {
1448 if let Some(ref mut drag) = self.active_drag {
1449 drag.update_position(position);
1450 }
1451 }
1452
1453 pub fn update_drop_target(&mut self, target: Option<azul_core::dom::DomNodeId>) {
1455 if let Some(ref mut drag) = self.active_drag {
1456 match &mut drag.drag_type {
1457 ActiveDragType::Node(ref mut node_drag) => {
1458 node_drag.current_drop_target = target.into();
1459 }
1460 ActiveDragType::FileDrop(ref mut file_drop) => {
1461 file_drop.drop_target = target.into();
1462 }
1463 _ => {}
1464 }
1465 }
1466 }
1467
1468 pub fn update_auto_scroll_direction(&mut self, direction: AutoScrollDirection) {
1470 if let Some(ref mut drag) = self.active_drag {
1471 if let Some(text_drag) = drag.as_text_selection_mut() {
1472 text_drag.auto_scroll_direction = direction;
1473 }
1474 }
1475 }
1476
1477 pub fn end_drag(&mut self) -> Option<DragContext> {
1479 self.active_drag.take()
1480 }
1481
1482 pub fn cancel_drag(&mut self) {
1484 if let Some(ref mut drag) = self.active_drag {
1485 drag.cancelled = true;
1486 }
1487 self.active_drag = None;
1488 }
1489
1490 pub fn is_dragging(&self) -> bool {
1496 self.active_drag.is_some()
1497 }
1498
1499 pub fn is_text_selection_dragging(&self) -> bool {
1501 self.active_drag.as_ref().is_some_and(|d| d.is_text_selection())
1502 }
1503
1504 pub fn is_scrollbar_dragging(&self) -> bool {
1506 self.active_drag.as_ref().is_some_and(|d| d.is_scrollbar_thumb())
1507 }
1508
1509 pub fn is_node_dragging_any(&self) -> bool {
1511 self.active_drag.as_ref().is_some_and(|d| d.is_node_drag())
1512 }
1513
1514 pub fn is_node_drag_active(&self) -> bool {
1516 self.is_node_dragging_any()
1517 }
1518
1519 pub fn is_node_dragging(&self, dom_id: DomId, node_id: NodeId) -> bool {
1521 self.active_drag.as_ref().is_some_and(|d| {
1522 if let Some(node_drag) = d.as_node_drag() {
1523 node_drag.dom_id == dom_id && node_drag.node_id == node_id
1524 } else {
1525 false
1526 }
1527 })
1528 }
1529
1530 pub fn is_window_dragging(&self) -> bool {
1532 self.active_drag.as_ref().is_some_and(|d| d.is_window_move())
1533 }
1534
1535 pub fn is_file_dropping(&self) -> bool {
1537 self.active_drag.as_ref().is_some_and(|d| d.is_file_drop())
1538 }
1539
1540 pub fn session_count(&self) -> usize {
1542 self.input_sessions.len()
1543 }
1544
1545 pub fn current_session_id(&self) -> Option<u64> {
1547 self.get_current_session().map(|s| s.session_id)
1548 }
1549
1550 pub fn get_node_drag(&self) -> Option<&NodeDrag> {
1557 self.active_drag.as_ref().and_then(|d| d.as_node_drag())
1558 }
1559
1560 pub fn get_window_drag(&self) -> Option<&WindowMoveDrag> {
1563 self.active_drag.as_ref().and_then(|d| d.as_window_move())
1564 }
1565
1566 pub fn get_file_drop(&self) -> Option<&FileDropDrag> {
1569 self.active_drag.as_ref().and_then(|d| d.as_file_drop())
1570 }
1571
1572 pub fn end_node_drag(&mut self) -> Option<DragContext> {
1575 if self.active_drag.as_ref().is_some_and(|d| d.is_node_drag()) {
1576 self.end_drag()
1577 } else {
1578 None
1579 }
1580 }
1581
1582 pub fn end_window_drag(&mut self) -> Option<DragContext> {
1585 if self.active_drag.as_ref().is_some_and(|d| d.is_window_move()) {
1586 self.end_drag()
1587 } else {
1588 None
1589 }
1590 }
1591
1592 pub fn end_file_drop(&mut self) -> Option<DragContext> {
1595 if self.active_drag.as_ref().is_some_and(|d| d.is_file_drop()) {
1596 self.end_drag()
1597 } else {
1598 None
1599 }
1600 }
1601
1602 pub fn cancel_file_drop(&mut self) {
1605 if self.active_drag.as_ref().is_some_and(|d| d.is_file_drop()) {
1606 self.cancel_drag();
1607 }
1608 }
1609
1610 pub fn get_window_drag_delta(&self) -> Option<(i32, i32)> {
1619 let drag = self.active_drag.as_ref()?.as_window_move()?;
1620
1621 let delta_x = drag.current_position.x - drag.start_position.x;
1622 let delta_y = drag.current_position.y - drag.start_position.y;
1623
1624 match drag.initial_window_position {
1625 WindowPosition::Initialized(_initial_pos) => Some((delta_x as i32, delta_y as i32)),
1626 _ => None,
1627 }
1628 }
1629
1630 pub fn get_window_position_from_drag(&self) -> Option<WindowPosition> {
1634 let drag = self.active_drag.as_ref()?.as_window_move()?;
1635
1636 let delta_x = drag.current_position.x - drag.start_position.x;
1637 let delta_y = drag.current_position.y - drag.start_position.y;
1638
1639 match drag.initial_window_position {
1640 WindowPosition::Initialized(initial_pos) => {
1641 Some(WindowPosition::Initialized(PhysicalPositionI32::new(
1642 initial_pos.x + delta_x as i32,
1643 initial_pos.y + delta_y as i32,
1644 )))
1645 }
1646 _ => None,
1647 }
1648 }
1649
1650 pub fn get_scrollbar_scroll_offset(&self) -> Option<f32> {
1652 self.active_drag.as_ref()?.calculate_scrollbar_scroll_offset()
1653 }
1654
1655 pub fn remap_node_ids(
1660 &mut self,
1661 dom_id: azul_core::dom::DomId,
1662 node_id_map: &std::collections::BTreeMap<azul_core::id::NodeId, azul_core::id::NodeId>,
1663 ) {
1664 if let Some(ref mut drag) = self.active_drag {
1665 if !drag.remap_node_ids(dom_id, node_id_map) {
1666 drag.cancelled = true;
1668 self.active_drag = None;
1669 }
1670 }
1671 }
1672}