1use crate::geometry::PropertyValue;
24use crate::interaction::{
25 InteractionButton, InteractionEvent, InteractionEventKind, InteractionModifiers,
26 InteractionTarget, PointerKind, ScreenPoint,
27};
28use crate::picking::{PickHit, PickOptions, PickQuery, PickResult};
29use crate::query::FeatureStateId;
30use crate::MapState;
31
32#[derive(Debug, Clone)]
42pub struct InteractionConfig {
43 pub drag_threshold_px: f64,
46
47 pub double_click_window_secs: f64,
50
51 pub auto_hover_state: bool,
55
56 pub auto_select_state: bool,
60
61 pub interactive_layers: Vec<String>,
65
66 pub tolerance_meters: f64,
69}
70
71impl Default for InteractionConfig {
72 fn default() -> Self {
73 Self {
74 drag_threshold_px: 5.0,
75 double_click_window_secs: 0.3,
76 auto_hover_state: true,
77 auto_select_state: false,
78 interactive_layers: Vec::new(),
79 tolerance_meters: 0.0,
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
90struct HitSnapshot {
91 hit: PickHit,
93 target: InteractionTarget,
95}
96
97#[derive(Debug)]
115pub struct InteractionManager {
116 config: InteractionConfig,
118
119 cursor: ScreenPoint,
121
122 prev_hit: Option<HitSnapshot>,
124
125 hovered: Option<FeatureStateId>,
127
128 selected: Option<FeatureStateId>,
130
131 pointer_down_pos: Option<ScreenPoint>,
133
134 pointer_down_time: Option<f64>,
136
137 dragging: bool,
139
140 last_click_time: Option<f64>,
143
144 current_time: f64,
147
148 events: Vec<InteractionEvent>,
150}
151
152impl InteractionManager {
153 pub fn new() -> Self {
155 Self::with_config(InteractionConfig::default())
156 }
157
158 pub fn with_config(config: InteractionConfig) -> Self {
160 Self {
161 config,
162 cursor: ScreenPoint::default(),
163 prev_hit: None,
164 hovered: None,
165 selected: None,
166 pointer_down_pos: None,
167 pointer_down_time: None,
168 dragging: false,
169 last_click_time: None,
170 current_time: 0.0,
171 events: Vec::new(),
172 }
173 }
174
175 pub fn config(&self) -> &InteractionConfig {
177 &self.config
178 }
179
180 pub fn config_mut(&mut self) -> &mut InteractionConfig {
182 &mut self.config
183 }
184
185 pub fn hovered(&self) -> Option<&FeatureStateId> {
187 self.hovered.as_ref()
188 }
189
190 pub fn selected(&self) -> Option<&FeatureStateId> {
192 self.selected.as_ref()
193 }
194
195 pub fn cursor(&self) -> ScreenPoint {
197 self.cursor
198 }
199
200 pub fn is_dragging(&self) -> bool {
202 self.dragging
203 }
204
205 pub fn update_pointer_move(
217 &mut self,
218 map: &mut MapState,
219 x: f64,
220 y: f64,
221 time: f64,
222 pointer_kind: PointerKind,
223 modifiers: InteractionModifiers,
224 ) {
225 self.current_time = time;
226 self.cursor = ScreenPoint::new(x, y);
227
228 if let Some(down_pos) = self.pointer_down_pos {
230 let dx = x - down_pos.x;
231 let dy = y - down_pos.y;
232 if (dx * dx + dy * dy).sqrt() > self.config.drag_threshold_px {
233 self.dragging = true;
234 }
235 }
236
237 let result = self.pick_at(map, x, y);
239 let current = top_hit_snapshot(&result);
240
241 let prev_target = self.prev_hit.as_ref().map(|s| &s.target);
243 let curr_target = current.as_ref().map(|s| &s.target);
244
245 if prev_target != curr_target {
246 if let Some(prev) = &self.prev_hit {
248 let mut event =
249 self.base_event(InteractionEventKind::MouseLeave, pointer_kind, modifiers);
250 event.target = Some(prev.target.clone());
251 event.related_target = curr_target.cloned();
253 self.events.push(event);
254 }
255
256 if self.config.auto_hover_state {
258 if let Some(prev_id) = self.hovered.take() {
259 map.set_feature_state_property(
260 &prev_id.source_id,
261 &prev_id.feature_id,
262 "hover",
263 PropertyValue::Bool(false),
264 );
265 }
266 }
267
268 if let Some(curr) = ¤t {
270 let mut event =
271 self.base_event(InteractionEventKind::MouseEnter, pointer_kind, modifiers);
272 event.target = Some(curr.target.clone());
273 event.hit = Some(curr.hit.clone());
274 event.related_target = prev_target.cloned();
276 self.events.push(event);
277
278 if self.config.auto_hover_state {
280 if let Some(id) = feature_state_id_from_target(&curr.target) {
281 map.set_feature_state_property(
282 &id.source_id,
283 &id.feature_id,
284 "hover",
285 PropertyValue::Bool(true),
286 );
287 self.hovered = Some(id);
288 }
289 }
290 }
291 }
292
293 let mut move_event =
295 self.base_event(InteractionEventKind::MouseMove, pointer_kind, modifiers);
296 if let Some(curr) = ¤t {
297 move_event.target = Some(curr.target.clone());
298 move_event.hit = Some(curr.hit.clone());
299 }
300 self.events.push(move_event);
301
302 self.prev_hit = current;
304 }
305
306 #[allow(clippy::too_many_arguments)]
313 pub fn update_pointer_down(
314 &mut self,
315 map: &MapState,
316 x: f64,
317 y: f64,
318 time: f64,
319 button: InteractionButton,
320 pointer_kind: PointerKind,
321 modifiers: InteractionModifiers,
322 ) {
323 let _ = map; self.current_time = time;
325 self.pointer_down_pos = Some(ScreenPoint::new(x, y));
326 self.pointer_down_time = Some(time);
327 self.dragging = false;
328
329 let mut event = self.base_event(InteractionEventKind::MouseDown, pointer_kind, modifiers);
330 event.button = Some(button);
331 if let Some(snapshot) = &self.prev_hit {
333 event.target = Some(snapshot.target.clone());
334 event.hit = Some(snapshot.hit.clone());
335 }
336 self.events.push(event);
337 }
338
339 #[allow(clippy::too_many_arguments)]
350 pub fn update_pointer_up(
351 &mut self,
352 map: &mut MapState,
353 x: f64,
354 y: f64,
355 time: f64,
356 button: InteractionButton,
357 pointer_kind: PointerKind,
358 modifiers: InteractionModifiers,
359 ) {
360 self.current_time = time;
361 self.cursor = ScreenPoint::new(x, y);
362
363 let mut up_event = self.base_event(InteractionEventKind::MouseUp, pointer_kind, modifiers);
365 up_event.button = Some(button);
366 if let Some(snapshot) = &self.prev_hit {
367 up_event.target = Some(snapshot.target.clone());
368 up_event.hit = Some(snapshot.hit.clone());
369 }
370 self.events.push(up_event);
371
372 let is_click = !self.dragging;
374
375 if is_click {
376 let mut click_event =
378 self.base_event(InteractionEventKind::Click, pointer_kind, modifiers);
379 click_event.button = Some(button);
380 if let Some(snapshot) = &self.prev_hit {
381 click_event.target = Some(snapshot.target.clone());
382 click_event.hit = Some(snapshot.hit.clone());
383 }
384 self.events.push(click_event);
385
386 if self.config.auto_select_state {
388 self.update_selection(map);
389 }
390
391 if let Some(prev_click) = self.last_click_time {
393 if (time - prev_click) <= self.config.double_click_window_secs {
394 let mut dbl_event =
395 self.base_event(InteractionEventKind::DoubleClick, pointer_kind, modifiers);
396 dbl_event.button = Some(button);
397 if let Some(snapshot) = &self.prev_hit {
398 dbl_event.target = Some(snapshot.target.clone());
399 dbl_event.hit = Some(snapshot.hit.clone());
400 }
401 self.events.push(dbl_event);
402 self.last_click_time = None;
404 } else {
405 self.last_click_time = Some(time);
406 }
407 } else {
408 self.last_click_time = Some(time);
409 }
410 }
411
412 self.pointer_down_pos = None;
414 self.pointer_down_time = None;
415 self.dragging = false;
416 }
417
418 pub fn update_pointer_leave(
423 &mut self,
424 map: &mut MapState,
425 pointer_kind: PointerKind,
426 modifiers: InteractionModifiers,
427 ) {
428 if let Some(prev) = self.prev_hit.take() {
430 let mut event =
431 self.base_event(InteractionEventKind::MouseLeave, pointer_kind, modifiers);
432 event.target = Some(prev.target);
433 self.events.push(event);
434 }
435
436 if self.config.auto_hover_state {
438 if let Some(prev_id) = self.hovered.take() {
439 map.set_feature_state_property(
440 &prev_id.source_id,
441 &prev_id.feature_id,
442 "hover",
443 PropertyValue::Bool(false),
444 );
445 }
446 }
447
448 self.pointer_down_pos = None;
449 self.pointer_down_time = None;
450 self.dragging = false;
451 }
452
453 pub fn drain_events(&mut self) -> Vec<InteractionEvent> {
463 std::mem::take(&mut self.events)
464 }
465
466 pub fn pending_event_count(&self) -> usize {
468 self.events.len()
469 }
470
471 fn pick_at(&self, map: &MapState, x: f64, y: f64) -> PickResult {
477 let mut options = PickOptions::new();
478 options.tolerance_meters = self.config.tolerance_meters;
479 options.limit = 1;
480 map.pick(PickQuery::screen(x, y), options)
481 }
482
483 fn base_event(
485 &self,
486 kind: InteractionEventKind,
487 pointer_kind: PointerKind,
488 modifiers: InteractionModifiers,
489 ) -> InteractionEvent {
490 InteractionEvent::new(kind, pointer_kind, self.cursor).with_modifiers(modifiers)
491 }
492
493 fn update_selection(&mut self, map: &mut MapState) {
495 if let Some(prev_id) = self.selected.take() {
497 map.set_feature_state_property(
498 &prev_id.source_id,
499 &prev_id.feature_id,
500 "selected",
501 PropertyValue::Bool(false),
502 );
503 }
504
505 if let Some(snapshot) = &self.prev_hit {
507 if let Some(id) = feature_state_id_from_target(&snapshot.target) {
508 map.set_feature_state_property(
509 &id.source_id,
510 &id.feature_id,
511 "selected",
512 PropertyValue::Bool(true),
513 );
514 self.selected = Some(id);
515 }
516 }
517 }
518}
519
520impl Default for InteractionManager {
521 fn default() -> Self {
522 Self::new()
523 }
524}
525
526fn top_hit_snapshot(result: &PickResult) -> Option<HitSnapshot> {
532 result.first().map(|hit| HitSnapshot {
533 target: InteractionTarget::from_pick_hit(hit),
534 hit: hit.clone(),
535 })
536}
537
538fn feature_state_id_from_target(target: &InteractionTarget) -> Option<FeatureStateId> {
541 match (&target.source_id, &target.feature_id) {
542 (Some(source_id), Some(feature_id)) => {
543 Some(FeatureStateId::new(source_id.clone(), feature_id.clone()))
544 }
545 _ => None,
546 }
547}
548
549#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::geometry::{Feature, FeatureCollection, Geometry, Point};
557 use crate::layers::{VectorLayer, VectorStyle};
558 use rustial_math::GeoCoord;
559
560 fn map_with_point_at_center() -> MapState {
563 let mut state = MapState::new();
564 state.set_viewport(800, 600);
565 let target = GeoCoord::from_lat_lon(0.0, 0.0);
566 state.set_camera_target(target);
567 state.set_camera_distance(500.0);
568
569 let fc = FeatureCollection {
570 features: vec![Feature {
571 geometry: Geometry::Point(Point { coord: target }),
572 properties: Default::default(),
573 }],
574 };
575 let vl = VectorLayer::new("points", fc, VectorStyle::default());
576 state.push_layer(Box::new(vl));
577 state.update();
578 state
579 }
580
581 #[test]
584 fn pointer_move_over_feature_emits_enter_then_move() {
585 let mut map = map_with_point_at_center();
586 let mut mgr = InteractionManager::new();
587
588 mgr.update_pointer_move(
590 &mut map,
591 400.0,
592 300.0,
593 0.0,
594 PointerKind::Mouse,
595 InteractionModifiers::default(),
596 );
597
598 let events = mgr.drain_events();
599 assert!(
601 events.len() >= 2,
602 "expected at least enter + move, got {}",
603 events.len()
604 );
605 assert_eq!(events[0].kind, InteractionEventKind::MouseEnter);
606 assert_eq!(events[1].kind, InteractionEventKind::MouseMove);
607 assert!(
608 events[0].target.is_some(),
609 "enter event should have a target"
610 );
611 }
612
613 #[test]
614 fn pointer_move_away_emits_leave() {
615 let mut map = map_with_point_at_center();
616 let mut mgr = InteractionManager::new();
617
618 mgr.update_pointer_move(
620 &mut map,
621 400.0,
622 300.0,
623 0.0,
624 PointerKind::Mouse,
625 InteractionModifiers::default(),
626 );
627 mgr.drain_events(); mgr.update_pointer_move(
631 &mut map,
632 10.0,
633 10.0,
634 0.1,
635 PointerKind::Mouse,
636 InteractionModifiers::default(),
637 );
638
639 let events = mgr.drain_events();
640 let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
642 assert!(
643 kinds.contains(&InteractionEventKind::MouseLeave),
644 "expected MouseLeave, got {:?}",
645 kinds
646 );
647 }
648
649 #[test]
652 fn click_within_threshold_emits_click() {
653 let mut map = map_with_point_at_center();
654 let mut mgr = InteractionManager::new();
655
656 mgr.update_pointer_move(
658 &mut map,
659 400.0,
660 300.0,
661 0.0,
662 PointerKind::Mouse,
663 InteractionModifiers::default(),
664 );
665 mgr.drain_events();
666
667 mgr.update_pointer_down(
669 &map,
670 400.0,
671 300.0,
672 1.0,
673 InteractionButton::Primary,
674 PointerKind::Mouse,
675 InteractionModifiers::default(),
676 );
677 mgr.drain_events();
678
679 mgr.update_pointer_up(
681 &mut map,
682 401.0,
683 300.0,
684 1.1,
685 InteractionButton::Primary,
686 PointerKind::Mouse,
687 InteractionModifiers::default(),
688 );
689
690 let events = mgr.drain_events();
691 let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
692 assert!(kinds.contains(&InteractionEventKind::MouseUp));
693 assert!(
694 kinds.contains(&InteractionEventKind::Click),
695 "expected Click, got {:?}",
696 kinds
697 );
698 }
699
700 #[test]
701 fn drag_beyond_threshold_suppresses_click() {
702 let mut map = map_with_point_at_center();
703 let mut mgr = InteractionManager::new();
704
705 mgr.update_pointer_move(
706 &mut map,
707 400.0,
708 300.0,
709 0.0,
710 PointerKind::Mouse,
711 InteractionModifiers::default(),
712 );
713 mgr.drain_events();
714
715 mgr.update_pointer_down(
717 &map,
718 400.0,
719 300.0,
720 1.0,
721 InteractionButton::Primary,
722 PointerKind::Mouse,
723 InteractionModifiers::default(),
724 );
725 mgr.drain_events();
726
727 mgr.update_pointer_move(
729 &mut map,
730 420.0,
731 320.0,
732 1.05,
733 PointerKind::Mouse,
734 InteractionModifiers::default(),
735 );
736 mgr.drain_events();
737
738 mgr.update_pointer_up(
740 &mut map,
741 420.0,
742 320.0,
743 1.1,
744 InteractionButton::Primary,
745 PointerKind::Mouse,
746 InteractionModifiers::default(),
747 );
748
749 let events = mgr.drain_events();
750 let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
751 assert!(kinds.contains(&InteractionEventKind::MouseUp));
752 assert!(
753 !kinds.contains(&InteractionEventKind::Click),
754 "Click should be suppressed after drag, got {:?}",
755 kinds
756 );
757 }
758
759 #[test]
762 fn two_clicks_within_window_emit_double_click() {
763 let mut map = map_with_point_at_center();
764 let mut mgr = InteractionManager::new();
765
766 mgr.update_pointer_move(
767 &mut map,
768 400.0,
769 300.0,
770 0.0,
771 PointerKind::Mouse,
772 InteractionModifiers::default(),
773 );
774 mgr.drain_events();
775
776 mgr.update_pointer_down(
778 &map,
779 400.0,
780 300.0,
781 1.0,
782 InteractionButton::Primary,
783 PointerKind::Mouse,
784 InteractionModifiers::default(),
785 );
786 mgr.update_pointer_up(
787 &mut map,
788 400.0,
789 300.0,
790 1.05,
791 InteractionButton::Primary,
792 PointerKind::Mouse,
793 InteractionModifiers::default(),
794 );
795 mgr.drain_events(); mgr.update_pointer_down(
799 &map,
800 400.0,
801 300.0,
802 1.2,
803 InteractionButton::Primary,
804 PointerKind::Mouse,
805 InteractionModifiers::default(),
806 );
807 mgr.update_pointer_up(
808 &mut map,
809 400.0,
810 300.0,
811 1.25,
812 InteractionButton::Primary,
813 PointerKind::Mouse,
814 InteractionModifiers::default(),
815 );
816
817 let events = mgr.drain_events();
818 let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
819 assert!(
820 kinds.contains(&InteractionEventKind::DoubleClick),
821 "expected DoubleClick, got {:?}",
822 kinds
823 );
824 }
825
826 #[test]
827 fn two_clicks_outside_window_do_not_emit_double_click() {
828 let mut map = map_with_point_at_center();
829 let mut mgr = InteractionManager::new();
830
831 mgr.update_pointer_move(
832 &mut map,
833 400.0,
834 300.0,
835 0.0,
836 PointerKind::Mouse,
837 InteractionModifiers::default(),
838 );
839 mgr.drain_events();
840
841 mgr.update_pointer_down(
843 &map,
844 400.0,
845 300.0,
846 1.0,
847 InteractionButton::Primary,
848 PointerKind::Mouse,
849 InteractionModifiers::default(),
850 );
851 mgr.update_pointer_up(
852 &mut map,
853 400.0,
854 300.0,
855 1.05,
856 InteractionButton::Primary,
857 PointerKind::Mouse,
858 InteractionModifiers::default(),
859 );
860 mgr.drain_events();
861
862 mgr.update_pointer_down(
864 &map,
865 400.0,
866 300.0,
867 2.0,
868 InteractionButton::Primary,
869 PointerKind::Mouse,
870 InteractionModifiers::default(),
871 );
872 mgr.update_pointer_up(
873 &mut map,
874 400.0,
875 300.0,
876 2.05,
877 InteractionButton::Primary,
878 PointerKind::Mouse,
879 InteractionModifiers::default(),
880 );
881
882 let events = mgr.drain_events();
883 let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
884 assert!(
885 !kinds.contains(&InteractionEventKind::DoubleClick),
886 "DoubleClick should NOT fire outside timing window, got {:?}",
887 kinds
888 );
889 }
890
891 #[test]
894 fn drain_events_clears_pending_queue() {
895 let mut map = map_with_point_at_center();
896 let mut mgr = InteractionManager::new();
897
898 mgr.update_pointer_move(
899 &mut map,
900 400.0,
901 300.0,
902 0.0,
903 PointerKind::Mouse,
904 InteractionModifiers::default(),
905 );
906 assert!(mgr.pending_event_count() > 0);
907
908 let events = mgr.drain_events();
909 assert!(!events.is_empty());
910 assert_eq!(mgr.pending_event_count(), 0);
911 }
912
913 #[test]
916 fn pointer_leave_emits_leave_and_clears_hover() {
917 let mut map = map_with_point_at_center();
918 let mut mgr = InteractionManager::new();
919
920 mgr.update_pointer_move(
922 &mut map,
923 400.0,
924 300.0,
925 0.0,
926 PointerKind::Mouse,
927 InteractionModifiers::default(),
928 );
929 mgr.drain_events();
930 assert!(mgr.hovered().is_some() || mgr.prev_hit.is_some());
931
932 mgr.update_pointer_leave(
934 &mut map,
935 PointerKind::Mouse,
936 InteractionModifiers::default(),
937 );
938
939 let events = mgr.drain_events();
940 let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
941 assert!(
942 kinds.contains(&InteractionEventKind::MouseLeave),
943 "expected MouseLeave on pointer leave, got {:?}",
944 kinds
945 );
946 assert!(mgr.prev_hit.is_none());
947 }
948
949 #[test]
952 fn auto_hover_state_sets_and_clears_feature_state() {
953 let mut map = map_with_point_at_center();
954 let mut mgr = InteractionManager::new();
955
956 mgr.update_pointer_move(
958 &mut map,
959 400.0,
960 300.0,
961 0.0,
962 PointerKind::Mouse,
963 InteractionModifiers::default(),
964 );
965 mgr.drain_events();
966
967 if let Some(id) = mgr.hovered() {
968 let state = map.feature_state(&id.source_id, &id.feature_id);
969 let hover_val = state.and_then(|s| s.get("hover")).and_then(|v| v.as_bool());
970 assert_eq!(hover_val, Some(true));
971 }
972
973 mgr.update_pointer_move(
975 &mut map,
976 10.0,
977 10.0,
978 0.1,
979 PointerKind::Mouse,
980 InteractionModifiers::default(),
981 );
982 mgr.drain_events();
983
984 assert!(mgr.hovered().is_none());
986 }
987}