1use crate::map::animation::Animation;
105use crate::map::objects::{
106 CometDirection, ContextMenuManager, MapBounds, MapLabel, MapPoint, MapSegment, MapSettings,
107 MarkerContext, NodeAnimation, NodeContext, NotificationContext, RawLine, RawPoint,
108 SegmentAnimation, SelectionContext, SteadyAnimation, SteadySegmentAnimation, TextSettings,
109 VisibilitySetting,
110};
111use crate::map::theme::{ColorMode, MapTheme, Style, Theme, ThemeColors};
112use egui::{widgets::*, *};
113use kdtree::KdTree;
114use kdtree::distance::squared_euclidean;
115use std::collections::{HashMap, HashSet};
116use std::rc::Rc;
117use std::time::Instant;
118
119use self::objects::{NodeTemplate, SegmentTemplate};
120
121pub mod animation;
122pub mod objects;
123pub mod theme;
124
125const SEGMENT_EFFECT_ALPHA_BOOST: f32 = 0.2;
132
133fn scale_alpha(color: Color32, factor: f32) -> Color32 {
145 let [r, g, b, a] = color.to_srgba_unmultiplied();
146 Color32::from_rgba_unmultiplied(r, g, b, (a as f32 * factor.clamp(0.0, 1.0)).round() as u8)
147}
148
149#[derive(Clone)]
188pub struct Map {
189 zoom: f32,
190 previous_zoom: f32,
191 points: Option<HashMap<usize, MapPoint>>,
192 segments: Option<rstar::RTree<MapSegment>>,
193 labels: Vec<MapLabel>,
194 tree: Option<KdTree<f32, usize, [f32; 2]>>,
195 visible_points: Vec<isize>,
196 map_area: Rect,
197 reference: MapBounds,
198 current: MapBounds,
199 current_index: usize,
200 notifications: HashMap<usize, Notification>,
201 node_states: HashMap<usize, NodeState>,
202 segment_notifications: HashMap<(usize, usize), SegmentNotification>,
203 segment_states: HashMap<(usize, usize), SegmentState>,
204 segment_ids: HashSet<(usize, usize)>,
208 min_size: (Option<f32>, Option<f32>),
209 max_size: (Option<f32>, Option<f32>),
210 pub settings: MapSettings,
213 menu_manager: Option<Rc<dyn ContextMenuManager>>,
214 node_template: Option<Rc<dyn NodeTemplate>>,
215 segment_template: Option<Rc<dyn SegmentTemplate>>,
216 markers: HashMap<usize, usize>,
217 theme: Rc<dyn MapTheme>,
219}
220
221#[derive(Clone, Copy, Debug)]
223struct Notification {
224 started: Instant,
225 animation: NodeAnimation,
226 color: Option<Color32>,
228}
229
230#[derive(Clone, Copy, Debug)]
232struct NodeState {
233 animation: SteadyAnimation,
234 color: Option<Color32>,
236}
237
238#[derive(Clone, Copy, Debug)]
240struct SegmentNotification {
241 started: Instant,
242 animation: SegmentAnimation,
243 color: Option<Color32>,
245}
246
247#[derive(Clone, Copy, Debug)]
249struct SegmentState {
250 animation: SteadySegmentAnimation,
251 color: Option<Color32>,
253}
254
255pub struct NodeHandle<'a> {
275 map: &'a mut Map,
276 id: usize,
277 color: Option<Color32>,
278}
279
280impl NodeHandle<'_> {
281 pub fn color(mut self, color: Color32) -> Self {
285 self.color = Some(color);
286 self
287 }
288
289 fn notify_with(self, animation: NodeAnimation, at: Instant) {
290 self.map.notifications.insert(
291 self.id,
292 Notification {
293 started: at,
294 animation,
295 color: self.color,
296 },
297 );
298 }
299
300 fn set_state(self, animation: SteadyAnimation) {
301 self.map.node_states.insert(
302 self.id,
303 NodeState {
304 animation,
305 color: self.color,
306 },
307 );
308 }
309
310 pub fn pulse(self, at: Instant) {
312 self.notify_with(NodeAnimation::Pulse, at);
313 }
314
315 pub fn ripple(self, at: Instant) {
317 self.notify_with(NodeAnimation::Ripple, at);
318 }
319
320 pub fn countdown(self, at: Instant) {
322 self.notify_with(NodeAnimation::CountdownArc, at);
323 }
324
325 pub fn scale_in(self, at: Instant) {
327 self.notify_with(NodeAnimation::ScaleIn, at);
328 }
329
330 pub fn crosshair(self, at: Instant) {
332 self.notify_with(NodeAnimation::Crosshair, at);
333 }
334
335 pub fn halo(self) {
337 self.set_state(SteadyAnimation::Halo);
338 }
339
340 pub fn blink(self) {
342 self.set_state(SteadyAnimation::Blink);
343 }
344
345 pub fn orbit(self) {
347 self.set_state(SteadyAnimation::Orbit);
348 }
349
350 pub fn clear(self) {
352 self.map.notifications.remove(&self.id);
353 self.map.node_states.remove(&self.id);
354 }
355}
356
357pub struct SegmentHandle<'a> {
372 map: &'a mut Map,
373 id: (usize, usize),
374 color: Option<Color32>,
375}
376
377impl SegmentHandle<'_> {
378 pub fn color(mut self, color: Color32) -> Self {
382 self.color = Some(color);
383 self
384 }
385
386 pub fn flash(self, at: Instant) {
389 self.map.segment_notifications.insert(
390 self.id,
391 SegmentNotification {
392 started: at,
393 animation: SegmentAnimation::FlashDecay,
394 color: self.color,
395 },
396 );
397 }
398
399 pub fn comet_once(self, at: Instant, direction: CometDirection) {
404 self.map.segment_notifications.insert(
405 self.id,
406 SegmentNotification {
407 started: at,
408 animation: SegmentAnimation::Comet(direction),
409 color: self.color,
410 },
411 );
412 }
413
414 pub fn wipe(self, at: Instant) {
418 self.map.segment_notifications.insert(
419 self.id,
420 SegmentNotification {
421 started: at,
422 animation: SegmentAnimation::Wipe,
423 color: self.color,
424 },
425 );
426 }
427
428 pub fn comet(self) {
431 self.map.segment_states.insert(
432 self.id,
433 SegmentState {
434 animation: SteadySegmentAnimation::Comet,
435 color: self.color,
436 },
437 );
438 }
439
440 pub fn dash(self) {
443 self.map.segment_states.insert(
444 self.id,
445 SegmentState {
446 animation: SteadySegmentAnimation::Dash,
447 color: self.color,
448 },
449 );
450 }
451
452 pub fn glow_band(self) {
456 self.map.segment_states.insert(
457 self.id,
458 SegmentState {
459 animation: SteadySegmentAnimation::GlowBand,
460 color: self.color,
461 },
462 );
463 }
464
465 pub fn chevrons(self) {
468 self.map.segment_states.insert(
469 self.id,
470 SegmentState {
471 animation: SteadySegmentAnimation::Chevrons,
472 color: self.color,
473 },
474 );
475 }
476
477 pub fn clear(self) {
479 self.map.segment_notifications.remove(&self.id);
480 self.map.segment_states.remove(&self.id);
481 }
482}
483
484impl Default for Map {
485 fn default() -> Self {
487 Map::new()
488 }
489}
490
491impl Widget for &mut Map {
492 fn ui(self, ui: &mut egui::Ui) -> Response {
495 let rect = self.calculate_widget_dimensions(ui);
496
497 let reference_dist = rect.distance();
499 let resized = reference_dist != self.reference.dist;
508 self.reference.dist = reference_dist;
509
510 self.assign_visual_style(ui);
511
512 let canvas = egui::Frame::canvas(ui.style()).inner_margin(Margin::symmetric(3, 5));
513
514 let frame_margin = canvas.total_margin().sum();
524 let painter_size = (self.map_area.size() - frame_margin).max(Vec2::ZERO);
525
526 let inner_response = canvas.show(ui, |ui| {
527 let _span = tracing::info_span!("paint_map").entered();
528
529 if ui.is_rect_visible(self.map_area) {
530 let (resp, paint) =
531 ui.allocate_painter(painter_size, egui::Sense::click_and_drag());
532 let vec = resp.drag_delta();
533 if vec.length() != 0.0 {
534 let _span = tracing::info_span!("calculating_points_in_visible_area").entered();
535
536 let coords = RawPoint::from(vec.to_pos2());
537 let new_pos = self.reference.pos - (coords / self.zoom);
538 self.set_pos(new_pos.into());
539 }
540 if self.zoom < self.settings.line_visible_zoom {
541 let mut text_settings = TextSettings {
543 size: self.settings.label_text_size,
547 anchor: Align2::CENTER_CENTER,
548 family: FontFamily::Proportional,
549 text: String::new(),
550 position: RawPoint::default(),
551 text_color: ui.visuals().text_color(),
552 };
553 for label in &self.labels {
554 text_settings.text.clone_from(&label.text);
555 text_settings.position = RawPoint::from(label.center);
556 self.paint_label(&paint, &text_settings);
557 }
558 }
559
560 let rect_midpoint = RawPoint::from(resp.rect.center());
566 let min_point = self.current.pos - rect_midpoint;
567 let vec_points = &self.visible_points;
568 let hashm = &self.points;
569
570 let now = Instant::now();
574 self.notifications
575 .retain(|_, n| now.duration_since(n.started).as_secs_f32() < 10.0);
576 self.segment_notifications
577 .retain(|_, n| now.duration_since(n.started).as_secs_f32() < 10.0);
578
579 for segment in self.paint_map_lines(&paint, &min_point) {
580 self.segment_notifications.remove(&segment);
581 }
582
583 if let Ok(nodes_to_remove) =
584 self.paint_map_points(vec_points, hashm, &paint, ui, &min_point, &resp)
585 {
586 for node in nodes_to_remove {
587 self.notifications.remove(&node);
588 }
589 }
590
591 for marker in &self.markers {
592 if let Some(point) = self.points.as_ref().unwrap().get(marker.1) {
593 let adjusted_point = RawPoint::from(point.coords) * self.zoom - min_point;
594 if let Some(template) = &self.node_template {
595 template.marker_ui(
596 ui,
597 MarkerContext {
598 position: adjusted_point.into(),
599 zoom: self.zoom,
600 kind: self.settings.marker_animation,
601 node_id: *marker.1,
602 },
603 );
604 } else {
605 let color = if ui.visuals().dark_mode {
606 Color32::LIGHT_GREEN
607 } else {
608 Color32::GREEN
609 };
610 let time = ui.input(|i| i.time) as f32;
614 let effect = match self.settings.marker_animation {
615 SteadyAnimation::Blink => Animation::blink,
616 SteadyAnimation::Halo => Animation::halo,
617 SteadyAnimation::Orbit => Animation::orbit,
618 };
619 effect(ui.painter(), adjusted_point.into(), self.zoom, time, color);
620 ui.ctx().request_repaint();
622 }
623 }
624 }
625
626 self.paint_sub_components(ui, self.map_area);
627
628 self.capture_mouse_events(ui, &resp);
629
630 if self.zoom != self.previous_zoom || resized {
631 let _span = tracing::info_span!("calculating viewport with zoom").entered();
632 self.adjust_bounds();
633 self.calculate_visible_points();
634 self.previous_zoom = self.zoom;
635 }
636
637 if let Some(menu_mon) = &mut self.menu_manager {
638 resp.context_menu(|ui| {
639 menu_mon.ui(ui);
640 });
641 }
642
643 #[cfg(feature = "debug_overlay")]
644 self.print_debug_info(ui, &resp);
645 }
646 });
647 inner_response.response
652 }
653}
654
655impl Map {
656 pub fn new() -> Self {
661 let settings = MapSettings::default();
662 Self {
663 zoom: 1.0,
664 previous_zoom: 1.0,
665 map_area: Rect::NOTHING,
666 tree: None,
667 points: None,
668 labels: Vec::new(),
669 visible_points: Vec::new(),
670 current: MapBounds::default(),
671 reference: MapBounds::default(),
672 settings,
673 min_size: (None, None),
674 max_size: (None, None),
675 current_index: 0,
676 notifications: HashMap::new(),
677 node_states: HashMap::new(),
678 segment_notifications: HashMap::new(),
679 segment_states: HashMap::new(),
680 segment_ids: HashSet::new(),
681 menu_manager: None,
682 node_template: None,
683 segment_template: None,
684 markers: HashMap::new(),
685 segments: None,
686 theme: Rc::new(Theme::default()),
687 }
688 }
689
690 fn calculate_widget_dimensions(&mut self, ui: &mut Ui) -> RawLine {
691 let available = ui.available_rect_before_wrap();
692 let mut size = available.size();
693 if let Some(max_width) = self.max_size.0 {
694 size.x = size.x.min(max_width);
695 }
696 if let Some(max_height) = self.max_size.1 {
697 size.y = size.y.min(max_height);
698 }
699 if let Some(min_width) = self.min_size.0 {
700 size.x = size.x.max(min_width);
701 }
702 if let Some(min_height) = self.min_size.1 {
703 size.y = size.y.max(min_height);
704 }
705 self.map_area = Rect::from_min_size(available.min, size);
706 RawLine::new(
707 RawPoint::from(self.map_area.left_top()),
708 RawPoint::from(self.map_area.right_bottom()),
709 )
710 }
711
712 fn calculate_visible_points(&mut self) {
713 let _span = tracing::info_span!("calculate_visible_points").entered();
714 if self.current.dist > 0.0
715 && self.current.dist < f32::INFINITY
716 && let Some(tree) = &self.tree
717 {
718 let center = self.current.pos / self.zoom;
719 let radius = (self.current.dist / 2.0).powi(2);
731 let point: [f32; 2] = center.into();
732 let vis_pos = tree.within(&point, radius, &squared_euclidean).unwrap();
733 self.visible_points.clear();
734 for point in vis_pos {
735 self.visible_points.push(point.1.cast_signed());
736 }
737 }
738 }
739
740 pub fn add_points(&mut self, points: Vec<MapPoint>) {
768 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
769 let mut hash_map = HashMap::new();
770 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
771 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
772 for entry in points {
773 for i in 0..min.components.len() {
774 if entry.coords[i] < min.components[i] {
775 min.components[i] = entry.coords[i];
776 }
777 if entry.coords[i] > max.components[i] {
778 max.components[i] = entry.coords[i];
779 }
780 }
781 let _result = tree.add(entry.coords, entry.get_id());
782 hash_map.insert(entry.get_id(), entry);
783 }
784 self.reference.min = min;
786 self.reference.max = max;
787 self.points = Some(hash_map);
788 self.tree = Some(tree);
789 self.reference.pos = RawLine::new(min, max).midpoint();
790 if self.map_area.area() == 0.0 {
794 self.reference.dist = 3000.00;
795 } else {
796 let rect = RawLine::new(
797 RawPoint::from(self.map_area.left_top()),
798 RawPoint::from(self.map_area.right_bottom()),
799 );
800 self.reference.dist = rect.distance();
801 }
802 self.current = self.reference.clone();
803 self.calculate_visible_points();
804 }
805
806 pub fn add_hashmap_points(&mut self, hash_map: HashMap<usize, MapPoint>) {
836 let _span = tracing::info_span!("add_hashmap_points").entered();
837 let mut min = RawPoint::new(f32::INFINITY, f32::INFINITY);
838 let mut max = RawPoint::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
839 let mut tree = KdTree::<f32, usize, [f32; 2]>::new(2);
840
841 for entry in hash_map.iter() {
842 for i in 0..min.components.len() {
843 if entry.1.coords[i] < min.components[i] {
844 min.components[i] = entry.1.coords[i];
845 }
846 if entry.1.coords[i] > max.components[i] {
847 max.components[i] = entry.1.coords[i];
848 }
849 }
850 let _result = tree.add(entry.1.coords, *entry.0);
851 }
852
853 self.reference.min = min;
855 self.reference.max = max;
856 self.points = Some(hash_map);
857 self.tree = Some(tree);
858 self.reference.pos = RawLine::new(min, max).midpoint();
859 if self.map_area.area() == 0.0 {
863 self.reference.dist = 3000.00;
864 } else {
865 let rect = RawLine::new(
866 RawPoint::from(self.map_area.left_top()),
867 RawPoint::from(self.map_area.right_bottom()),
868 );
869 self.reference.dist = rect.distance();
870 }
871 self.current = self.reference.clone();
872 self.calculate_visible_points();
873 }
874
875 pub fn set_pos_from_nodeid(&mut self, node_id: usize) -> bool {
903 let _span = tracing::info_span!("set_pos_from_nodeid").entered();
904 if let Some(hash_map) = &self.points
905 && let Some(map_point) = hash_map.get(&node_id)
906 {
907 self.reference.pos = RawPoint::from(map_point.coords);
908 self.adjust_bounds();
909 self.calculate_visible_points();
910 true
911 } else {
912 tracing::warn!(
913 node_id,
914 loaded_nodes = self.points.as_ref().map_or(0, |p| p.len()),
915 "set_pos_from_nodeid: unknown node id, the view was left unchanged"
916 );
917 false
918 }
919 }
920
921 pub fn set_pos(&mut self, position: [f32; 2]) {
923 let _span = tracing::info_span!("set_pos").entered();
924 let point = RawPoint::from(position);
925 self.reference.pos = point;
926 self.adjust_bounds();
927 self.calculate_visible_points();
928 }
929
930 pub fn get_pos(&self) -> [f32; 2] {
932 let _span = tracing::info_span!("get_pos").entered();
933 self.reference.pos.into()
934 }
935
936 pub fn add_labels(&mut self, labels: Vec<MapLabel>) {
941 let _span = tracing::info_span!("add_labels").entered();
942 self.labels = labels;
943 }
944
945 pub fn add_lines(&mut self, segments: Vec<MapSegment>) {
957 let _span = tracing::info_span!("add_lines").entered();
958 self.segment_ids = segments.iter().map(|s| s.id).collect();
963 self.segments = Some(rstar::RTree::bulk_load(segments));
964 }
965
966 pub fn add_hashmap_lines(&mut self, segments: HashMap<(usize, usize), MapSegment>) {
975 let _span = tracing::info_span!("add_hashmap_lines").entered();
976 let segments: Vec<MapSegment> = segments.into_values().collect();
977 self.segment_ids = segments.iter().map(|s| s.id).collect();
978 self.segments = Some(rstar::RTree::bulk_load(segments));
979 }
980
981 fn adjust_bounds(&mut self) {
982 let _span = tracing::info_span!("adjust_bounds").entered();
983 self.current.max = self.reference.max * self.zoom;
984 self.current.min = self.reference.min * self.zoom;
985 self.current.dist = self.reference.dist / self.zoom;
986 self.current.pos = self.reference.pos * self.zoom;
987 }
988
989 fn capture_mouse_events(&mut self, ui: &Ui, _resp: &Response) {
990 let _span = tracing::info_span!("capture_mouse_events").entered();
991 if ui.rect_contains_pointer(self.map_area) {
993 ui.input(|x| {
994 let _span = tracing::info_span!("capture_mouse_events_input").entered();
995
996 if !x.events.is_empty() {
997 for event in &x.events {
998 match event {
999 Event::MouseWheel {
1000 unit: _,
1001 delta,
1002 modifiers,
1003 phase: _,
1004 } => {
1005 #[cfg(target_os = "macos")]
1006 let zoom_modifier = if modifiers.mac_cmd {
1007 delta.y / 80.00
1008 } else {
1009 delta.y / 400.00
1010 };
1011
1012 #[cfg(not(target_os = "macos"))]
1013 let zoom_modifier = if modifiers.ctrl {
1014 delta.y / 8.00
1015 } else {
1016 delta.y / 40.00
1017 };
1018
1019 let mut pre_zoom = self.zoom + zoom_modifier;
1020 if pre_zoom > self.settings.max_zoom {
1021 pre_zoom = self.settings.max_zoom;
1022 }
1023 if pre_zoom < self.settings.min_zoom {
1024 pre_zoom = self.settings.min_zoom;
1025 }
1026 self.zoom = pre_zoom;
1027 }
1028 _ => {
1029 continue;
1030 }
1031 };
1032 }
1033 }
1034 });
1035 }
1036 }
1037
1038 pub fn set_zoom(&mut self, value: f32) {
1043 if value >= self.settings.min_zoom && value <= self.settings.max_zoom {
1044 self.zoom = value;
1045 }
1046 }
1047
1048 pub fn get_zoom(&mut self) -> f32 {
1050 self.zoom
1051 }
1052
1053 fn current_style(&self) -> &Style {
1056 self.settings
1057 .styles
1058 .get(self.current_index)
1059 .or(self.settings.styles.first())
1060 .expect("MapSettings::styles must not be empty")
1061 }
1062
1063 fn assign_visual_style(&mut self, ui_obj: &mut Ui) {
1064 let style_index = ui_obj.visuals().dark_mode as usize;
1065
1066 if self.current_index != style_index {
1067 let _span = tracing::info_span!("asign_visual_style").entered();
1068
1069 self.current_index = style_index;
1070 let map_style = self.settings.styles.get_mut(style_index).unwrap();
1071 let visuals = &ui_obj.style().visuals;
1072 map_style.background_color = visuals.extreme_bg_color;
1073 }
1074 }
1075
1076 fn color_mode(&self) -> ColorMode {
1079 ColorMode::from_dark_mode(self.current_index == 1)
1080 }
1081
1082 fn theme_colors(&self) -> ThemeColors {
1088 self.theme.colors(self.color_mode())
1089 }
1090
1091 #[cfg(feature = "debug_overlay")]
1100 fn print_debug_info(&mut self, ui: &mut Ui, resp: &Response) {
1101 let _span = tracing::info_span!("printing debug data").entered();
1102
1103 let p = |v: f32| format!("{v:.2}");
1104 let mut rows: Vec<(String, Color32)> = vec![
1105 (
1106 format!(
1107 "MIN {}, {}",
1108 p(self.current.min.components[0]),
1109 p(self.current.min.components[1])
1110 ),
1111 Color32::LIGHT_GREEN,
1112 ),
1113 (
1114 format!(
1115 "MAX {}, {}",
1116 p(self.current.max.components[0]),
1117 p(self.current.max.components[1])
1118 ),
1119 Color32::LIGHT_GREEN,
1120 ),
1121 (
1122 format!(
1123 "CUR {}, {}",
1124 p(self.current.pos.components[0]),
1125 p(self.current.pos.components[1])
1126 ),
1127 Color32::LIGHT_GREEN,
1128 ),
1129 (
1130 format!("DST {}", p(self.current.dist)),
1131 Color32::LIGHT_GREEN,
1132 ),
1133 (format!("ZOM {}", self.zoom), Color32::GREEN),
1134 (
1135 format!(
1136 "REC {}, {} .. {}, {}",
1137 p(self.map_area.left_top().x),
1138 p(self.map_area.left_top().y),
1139 p(self.map_area.right_bottom().x),
1140 p(self.map_area.right_bottom().y)
1141 ),
1142 Color32::LIGHT_GREEN,
1143 ),
1144 ];
1145 if let Some(points) = &self.points {
1146 rows.push((format!("NUM {}", points.len()), Color32::LIGHT_GREEN));
1147 }
1148 if !self.visible_points.is_empty() {
1149 rows.push((
1150 format!("VIS {}", self.visible_points.len()),
1151 Color32::LIGHT_GREEN,
1152 ));
1153 }
1154 if let Some(pointer_pos) = resp.hover_pos() {
1155 rows.push((
1156 format!("HVR {}, {}", p(pointer_pos.x), p(pointer_pos.y)),
1157 Color32::LIGHT_BLUE,
1158 ));
1159 }
1160 let drag = resp.drag_delta();
1161 if drag.length() != 0.0 {
1162 rows.push((format!("DRG {}, {}", p(drag.x), p(drag.y)), Color32::GOLD));
1163 }
1164
1165 let overlay_rect = Rect::from_min_max(
1175 self.map_area.left_top() + Vec2::new(6.0, 6.0),
1176 self.map_area.right_bottom(),
1177 );
1178 let mut overlay_ui = ui.new_child(
1179 UiBuilder::new()
1180 .max_rect(overlay_rect)
1181 .layout(Layout::top_down(Align::Min)),
1182 );
1183 CollapsingHeader::new(RichText::new("dbg").monospace().small().weak())
1186 .id_salt("egui_map_debug_overlay")
1187 .default_open(false)
1188 .show_background(false)
1189 .show(&mut overlay_ui, |ui| {
1190 for (text, color) in rows {
1191 ui.label(RichText::new(text).monospace().small().color(color));
1192 }
1193 });
1194 }
1195
1196 fn paint_sub_components(&mut self, ui_obj: &mut Ui, rect: Rect) {
1197 let _span = tracing::info_span!("map_ui_paint_sub_components").entered();
1198 let zoom_slider = egui::Slider::new(
1199 &mut self.zoom,
1200 self.settings.min_zoom..=self.settings.max_zoom,
1201 )
1202 .show_value(false)
1203 .orientation(SliderOrientation::Vertical);
1204 let mut pos1 = rect.right_top();
1205 let mut pos2 = rect.right_top();
1206 pos1.x -= 80.0;
1207 pos1.y += 120.0;
1208 pos2.x -= 60.0;
1209 pos2.y += 240.0;
1210
1211 let sub_rect = egui::Rect::from_two_pos(pos1, pos2);
1212 let ui_builder = egui::UiBuilder::new().clone().max_rect(sub_rect);
1213 ui_obj.scope_builder(ui_builder, |ui_obj| {
1214 ui_obj.add(zoom_slider);
1215 });
1216 }
1217
1218 fn paint_map_points(
1219 &self,
1220 vec_points: &Vec<isize>,
1221 hashm: &Option<HashMap<usize, MapPoint>>,
1222 paint: &Painter,
1223 ui_obj: &mut Ui,
1224 min_point: &RawPoint,
1225 resp: &Response,
1226 ) -> Result<Vec<usize>, ()> {
1227 let _span = tracing::info_span!("paint_map_points").entered();
1234 let mut nearest_id = None;
1235 let mut nodes_to_remove = Vec::new();
1236 let mut shape_vec = vec![];
1237
1238 if hashm.is_none() {
1239 return Err(());
1240 }
1241 if vec_points.is_empty() {
1242 return Err(());
1243 }
1244 if self.settings.node_text_visibility == VisibilitySetting::Hover
1246 && resp.hovered()
1247 && let Some(point) = resp.hover_pos()
1248 {
1249 let raw_point = RawPoint::from(point);
1250 let hovered_map_point = (*min_point + raw_point) / self.zoom;
1251 if let Ok(nearest_node) = self.tree.as_ref().unwrap().nearest(
1252 &hovered_map_point.components,
1253 1,
1254 &squared_euclidean,
1255 ) {
1256 nearest_id = Some(nearest_node.first().unwrap().1);
1257 }
1258 }
1259 let mut text_settings = TextSettings {
1261 size: self.settings.node_text_size,
1265 anchor: Align2::LEFT_BOTTOM,
1266 family: FontFamily::Proportional,
1267 text: String::new(),
1268 position: RawPoint::default(),
1269 text_color: ui_obj.visuals().text_color(),
1270 };
1271
1272 for temp_point in vec_points {
1274 let parsed_point = temp_point.cast_unsigned();
1275 if let Some(system) = hashm.as_ref().unwrap().get(&parsed_point) {
1276 let viewport_point = RawPoint::from(system.coords) * self.zoom - min_point;
1277 if let Some(node_template) = &self.node_template {
1278 if nearest_id.unwrap_or(&0usize) == &system.get_id() {
1279 node_template.selection_ui(
1280 ui_obj,
1281 SelectionContext {
1282 position: viewport_point.into(),
1283 zoom: self.zoom,
1284 point: system,
1285 color: self.theme_colors().selected,
1286 },
1287 );
1288 }
1289 } else if self.zoom > self.settings.label_visible_zoom
1290 && self.settings.node_text_visibility == VisibilitySetting::Always
1291 || (self.settings.node_text_visibility == VisibilitySetting::Hover
1292 && nearest_id.unwrap_or(&0usize) == &system.get_id())
1293 {
1294 let mut viewport_text = viewport_point;
1295 viewport_text.components[0] += 3.0 * self.zoom;
1296 viewport_text.components[1] -= 3.0 * self.zoom;
1297 text_settings.position = viewport_text;
1298 text_settings.text = system.get_name();
1299 self.paint_label(paint, &text_settings);
1300 }
1301
1302 let system_id = system.get_id();
1303
1304 if let Some(state) = self.node_states.get(&system_id) {
1307 let color = state.color.unwrap_or(self.theme_colors().alert);
1308 if let Some(template) = &self.node_template {
1309 template.marker_ui(
1316 ui_obj,
1317 MarkerContext {
1318 position: viewport_point.into(),
1319 zoom: self.zoom,
1320 kind: state.animation,
1321 node_id: system_id,
1322 },
1323 );
1324 } else {
1325 let effect = match state.animation {
1326 SteadyAnimation::Blink => Animation::blink,
1327 SteadyAnimation::Halo => Animation::halo,
1328 SteadyAnimation::Orbit => Animation::orbit,
1329 };
1330 let time = ui_obj.input(|i| i.time) as f32;
1333 effect(paint, viewport_point.into(), self.zoom, time, color);
1334 }
1335 ui_obj.ctx().request_repaint();
1337 }
1338
1339 if let Some(notification) = self.notifications.get(&system_id) {
1340 let color = notification.color.unwrap_or(self.theme_colors().alert);
1341 if let Some(template) = &self.node_template {
1342 template.notification_ui(
1343 ui_obj,
1344 NotificationContext {
1345 position: viewport_point.into(),
1346 zoom: self.zoom,
1347 initial_time: notification.started,
1348 color,
1349 kind: notification.animation,
1350 node_id: system_id,
1351 },
1352 );
1353 } else {
1354 let effect = match notification.animation {
1355 NodeAnimation::Pulse => Animation::pulse,
1356 NodeAnimation::Ripple => Animation::ripple,
1357 NodeAnimation::CountdownArc => Animation::countdown_arc,
1358 NodeAnimation::ScaleIn => Animation::scale_in,
1359 NodeAnimation::Crosshair => Animation::crosshair,
1360 };
1361 if effect(
1362 paint,
1363 viewport_point.into(),
1364 self.zoom,
1365 notification.started,
1366 color,
1367 ) {
1368 ui_obj.ctx().request_repaint();
1369 } else {
1370 nodes_to_remove.push(system_id);
1371 }
1372 }
1373 }
1374 let node_color = system.color.unwrap_or(self.theme_colors().node);
1379 if let Some(node_template) = &self.node_template {
1380 node_template.node_ui(
1381 ui_obj,
1382 NodeContext {
1383 position: viewport_point.into(),
1384 zoom: self.zoom,
1385 point: system,
1386 color: node_color,
1387 },
1388 );
1389 } else {
1390 shape_vec.push(Shape::circle_filled(
1391 viewport_point.into(),
1392 4.00 * self.zoom,
1393 node_color,
1394 ));
1395 }
1396 }
1397 }
1398 paint.extend(shape_vec);
1399 Ok(nodes_to_remove)
1400 }
1401
1402 fn paint_map_lines(&self, painter: &Painter, min_point: &RawPoint) -> Vec<(usize, usize)> {
1406 let _span = tracing::info_span!("paint_map_lines").entered();
1407 let mut segments_to_remove = Vec::new();
1408
1409 if self.zoom <= self.settings.line_visible_zoom {
1410 return segments_to_remove;
1411 }
1412 let Some(segments) = &self.segments else {
1413 return segments_to_remove;
1414 };
1415
1416 let line_fade = ((self.zoom - self.settings.line_visible_zoom) / 0.80).clamp(0.0, 1.0);
1423
1424 let default_stroke = self.current_style().line_width.map(|width| {
1431 let segment_color = self.theme_colors().segment;
1432 let color = if line_fade >= 1.0 {
1433 segment_color
1434 } else {
1435 let mut tup_stroke = segment_color.to_tuple();
1436 tup_stroke.3 = (255.0 * line_fade).round() as u8;
1437 Color32::from_rgba_unmultiplied(
1438 tup_stroke.0,
1439 tup_stroke.1,
1440 tup_stroke.2,
1441 tup_stroke.3,
1442 )
1443 };
1444 Stroke::new(width, color)
1445 });
1446
1447 let center = self.current.pos / self.zoom;
1451 let padding = default_stroke.map(|s| s.width).unwrap_or(0.0) / self.zoom;
1452 let half = RawPoint::new(
1453 self.map_area.width() / 2.0 / self.zoom + padding,
1454 self.map_area.height() / 2.0 / self.zoom + padding,
1455 );
1456 let query = rstar::AABB::from_corners((center - half).into(), (center + half).into());
1457
1458 let effect_fade = (line_fade + SEGMENT_EFFECT_ALPHA_BOOST).min(1.0);
1463
1464 for segment in segments.locate_in_envelope_intersecting(query) {
1465 let raw_line = segment.raw_line();
1466 let pos_a: Pos2 = (raw_line.points[0] * self.zoom - min_point).into();
1467 let pos_b: Pos2 = (raw_line.points[1] * self.zoom - min_point).into();
1468
1469 if let Some(template) = &self.segment_template {
1477 template.segment_ui(painter, pos_a, pos_b, self.zoom, segment);
1478 } else if let Some(stroke) = default_stroke {
1479 painter.add(Shape::line_segment([pos_a, pos_b], stroke));
1480 }
1481
1482 if let Some(state) = self.segment_states.get(&segment.id) {
1486 let color = scale_alpha(
1487 state.color.unwrap_or(self.theme_colors().alert),
1488 effect_fade,
1489 );
1490 if let Some(template) = &self.segment_template {
1491 let time = painter.ctx().input(|i| i.time) as f32;
1492 template.segment_state_ui(painter, pos_a, pos_b, self.zoom, time, color);
1493 } else {
1494 let effect = match state.animation {
1495 SteadySegmentAnimation::Comet => Animation::comet,
1496 SteadySegmentAnimation::Dash => Animation::dash,
1497 SteadySegmentAnimation::GlowBand => Animation::glow_band,
1498 SteadySegmentAnimation::Chevrons => Animation::chevrons,
1499 };
1500 let time = painter.ctx().input(|i| i.time) as f32;
1501 effect(painter, pos_a, pos_b, self.zoom, time, color);
1502 }
1503 painter.ctx().request_repaint();
1505 }
1506
1507 if let Some(notification) = self.segment_notifications.get(&segment.id) {
1508 let color = scale_alpha(
1509 notification.color.unwrap_or(self.theme_colors().alert),
1510 effect_fade,
1511 );
1512 let still_playing = if let Some(template) = &self.segment_template {
1513 template.segment_notification_ui(
1514 painter,
1515 pos_a,
1516 pos_b,
1517 self.zoom,
1518 notification.started,
1519 color,
1520 )
1521 } else {
1522 match notification.animation {
1523 SegmentAnimation::FlashDecay => Animation::flash_decay(
1524 painter,
1525 pos_a,
1526 pos_b,
1527 self.zoom,
1528 notification.started,
1529 color,
1530 ),
1531 SegmentAnimation::Comet(direction) => Animation::comet_once(
1532 painter,
1533 pos_a,
1534 pos_b,
1535 self.zoom,
1536 notification.started,
1537 color,
1538 direction,
1539 ),
1540 SegmentAnimation::Wipe => Animation::wipe(
1541 painter,
1542 pos_a,
1543 pos_b,
1544 self.zoom,
1545 notification.started,
1546 color,
1547 ),
1548 }
1549 };
1550 if still_playing {
1551 painter.ctx().request_repaint();
1552 } else {
1553 segments_to_remove.push(segment.id);
1554 }
1555 }
1556 }
1557 segments_to_remove
1558 }
1559
1560 fn paint_label(&self, paint: &Painter, text_settings: &TextSettings) {
1561 let _span = tracing::info_span!("paint_label").entered();
1562 paint.text(
1563 text_settings.position.into(),
1564 text_settings.anchor,
1565 text_settings.text.clone(),
1566 FontId::new(text_settings.size, text_settings.family.clone()),
1567 text_settings.text_color,
1568 );
1569 }
1570
1571 #[deprecated(
1594 since = "0.4.0",
1595 note = "use `map.node(id)` and pick an effect, e.g. `if let Some(n) = map.node(id) { n.pulse(time) }`"
1596 )]
1597 pub fn notify(&mut self, id_node: usize, time: Instant) {
1598 let _span = tracing::info_span!("notify").entered();
1599 self.notifications.insert(
1600 id_node,
1601 Notification {
1602 started: time,
1603 animation: NodeAnimation::Pulse,
1604 color: None,
1605 },
1606 );
1607 }
1608
1609 pub fn node(&mut self, id: usize) -> Option<NodeHandle<'_>> {
1638 if !self
1639 .points
1640 .as_ref()
1641 .is_some_and(|points| points.contains_key(&id))
1642 {
1643 return None;
1644 }
1645 Some(NodeHandle {
1646 map: self,
1647 id,
1648 color: None,
1649 })
1650 }
1651
1652 pub fn segment(&mut self, id: (usize, usize)) -> Option<SegmentHandle<'_>> {
1679 if !self.segment_ids.contains(&id) {
1680 return None;
1681 }
1682 Some(SegmentHandle {
1683 map: self,
1684 id,
1685 color: None,
1686 })
1687 }
1688
1689 pub fn line_at(&self, point: [f32; 2], tolerance: f32) -> Option<(usize, usize)> {
1703 let _span = tracing::info_span!("line_at").entered();
1704 let segments = self.segments.as_ref()?;
1705 let tolerance = tolerance.max(0.0);
1706
1707 let center = RawPoint::from(point);
1708 let padding = RawPoint::new(tolerance, tolerance);
1709 let query = rstar::AABB::from_corners((center - padding).into(), (center + padding).into());
1710
1711 let mut closest: Option<(f32, (usize, usize))> = None;
1712 for segment in segments.locate_in_envelope_intersecting(query) {
1713 let distance = segment.raw_line().distance_to_point(center);
1714 if distance <= tolerance && closest.as_ref().is_none_or(|(best, _)| distance < *best) {
1715 closest = Some((distance, segment.id));
1716 }
1717 }
1718 closest.map(|(_, id)| id)
1719 }
1720
1721 pub fn set_context_manager(&mut self, manager: Rc<dyn ContextMenuManager>) {
1724 self.menu_manager = Some(manager);
1725 }
1726
1727 pub fn set_node_template(&mut self, template: Rc<dyn NodeTemplate>) {
1735 self.node_template = Some(template);
1736 }
1737
1738 pub fn set_segment_template(&mut self, template: Rc<dyn SegmentTemplate>) {
1744 self.segment_template = Some(template);
1745 }
1746
1747 pub fn set_theme(&mut self, new_theme: Rc<dyn MapTheme>) {
1757 self.theme = new_theme;
1758 }
1759
1760 pub fn update_marker(&mut self, id: usize, node_id: usize) {
1765 self.markers
1766 .entry(id)
1767 .and_modify(|value| *value = node_id)
1768 .or_insert(node_id);
1769 }
1770
1771 pub fn allocate_at_least(&mut self, width: Option<f32>, height: Option<f32>) {
1774 self.min_size = (width, height);
1775 }
1776
1777 pub fn allocate_at_most(&mut self, width: Option<f32>, height: Option<f32>) {
1780 self.max_size = (width, height);
1781 }
1782}
1783
1784#[cfg(test)]
1785mod tests {
1786 use super::*;
1787 use std::time::Duration;
1788
1789 fn sample_points() -> Vec<MapPoint> {
1790 let mut map = Vec::new();
1791 map.push(MapPoint::new(1, [0.0, 0.0]));
1792 map.push(MapPoint::new(2, [10.0, 10.0]));
1793 map.push(MapPoint::new(3, [-10.0, -10.0]));
1794 map
1795 }
1796
1797 #[test]
1800 fn map_new_initial_state() {
1801 let map = Map::new();
1802 assert_eq!(map.zoom, 1.0);
1803 assert_eq!(map.previous_zoom, 1.0);
1804 assert!(map.points.is_none());
1805 assert!(map.segments.is_none());
1806 assert!(map.tree.is_none());
1807 assert!(map.labels.is_empty());
1808 assert!(map.visible_points.is_empty());
1809 assert!(map.markers.is_empty());
1810 assert!(map.notifications.is_empty());
1811 assert!(map.node_states.is_empty());
1812 assert!(map.segment_notifications.is_empty());
1813 assert!(map.segment_states.is_empty());
1814 assert!(map.segment_ids.is_empty());
1815 assert_eq!(map.min_size, (None, None));
1816 assert_eq!(map.max_size, (None, None));
1817 assert_eq!(map.current_index, 0);
1818 }
1819
1820 #[test]
1821 fn map_default_equals_new() {
1822 let map = Map::default();
1823 assert_eq!(map.zoom, 1.0);
1824 assert!(map.points.is_none());
1825 }
1826
1827 #[test]
1830 fn set_zoom_within_range() {
1831 let mut map = Map::new();
1832 map.set_zoom(1.5);
1833 assert_eq!(map.get_zoom(), 1.5);
1834 }
1835
1836 #[test]
1837 fn set_zoom_at_exact_limits() {
1838 let mut map = Map::new();
1839 map.set_zoom(map.settings.min_zoom);
1840 assert_eq!(map.get_zoom(), 0.1);
1841 map.set_zoom(map.settings.max_zoom);
1842 assert_eq!(map.get_zoom(), 2.0);
1843 }
1844
1845 #[test]
1846 fn set_zoom_out_of_range_is_ignored() {
1847 let mut map = Map::new();
1848 let initial = map.get_zoom();
1849 map.set_zoom(0.05); assert_eq!(map.get_zoom(), initial);
1851 map.set_zoom(2.5); assert_eq!(map.get_zoom(), initial);
1853 }
1854
1855 #[test]
1858 fn add_hashmap_points_computes_bounds() {
1859 let mut map = Map::new();
1860 map.add_points(sample_points());
1861
1862 assert_eq!(map.reference.min.components, [-10.0, -10.0]);
1863 assert_eq!(map.reference.max.components, [10.0, 10.0]);
1864 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
1866 assert_eq!(map.reference.dist, 3000.0);
1868 assert_eq!(map.current.min.components, map.reference.min.components);
1870 assert_eq!(map.current.max.components, map.reference.max.components);
1871 assert_eq!(map.current.pos.components, map.reference.pos.components);
1872 assert_eq!(map.current.dist, map.reference.dist);
1873 assert!(map.points.is_some());
1874 assert!(map.tree.is_some());
1875 assert_eq!(map.points.as_ref().unwrap().len(), 3);
1876 }
1877
1878 #[test]
1879 fn add_hashmap_points_populates_visible_points() {
1880 let mut map = Map::new();
1881 map.add_points(sample_points());
1882 assert_eq!(map.visible_points.len(), 3);
1884 }
1885
1886 fn render_line_segments(map: &mut Map) -> Vec<[egui::Pos2; 2]> {
1889 use egui::{Context, RawInput, Shape};
1890 let ctx = Context::default();
1891 let input = RawInput {
1892 screen_rect: Some(egui::Rect::from_min_size(
1893 egui::Pos2::ZERO,
1894 egui::vec2(500.0, 500.0),
1895 )),
1896 ..RawInput::default()
1897 };
1898 let mut output = ctx.run_ui(input, |ui| {
1899 ui.add(&mut *map);
1900 });
1901 output.textures_delta.clear();
1907 output
1908 .shapes
1909 .iter()
1910 .filter_map(|cs| match cs.shape {
1911 Shape::LineSegment { points, .. } => Some(points),
1912 _ => None,
1913 })
1914 .collect()
1915 }
1916
1917 #[test]
1918 fn segment_crossing_viewport_is_painted_even_with_far_endpoints() {
1919 let mut map = Map::new();
1924 map.set_zoom(1.0);
1925 let mut lines = Vec::new();
1926 lines.push(MapSegment::new((1, 2), [-4000.0, -1.0], [4000.0, 1.0]));
1927 map.add_lines(lines);
1928 map.set_pos([0.0, 0.0]);
1929
1930 let segments = render_line_segments(&mut map);
1931 assert_eq!(segments.len(), 1);
1932 }
1933
1934 #[test]
1935 fn segment_outside_viewport_is_not_painted() {
1936 let mut map = Map::new();
1937 map.set_zoom(1.0);
1938 let mut lines = Vec::new();
1939 lines.push(MapSegment::new(
1940 (1, 2),
1941 [10_000.0, 10_000.0],
1942 [10_100.0, 10_100.0],
1943 ));
1944 map.add_lines(lines);
1945 map.set_pos([0.0, 0.0]);
1946
1947 assert!(render_line_segments(&mut map).is_empty());
1948 }
1949
1950 #[test]
1951 fn add_lines_builds_segment_tree() {
1952 let mut map = Map::new();
1953 map.add_points(sample_points());
1954 let mut lines = Vec::new();
1955 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
1956 map.add_lines(lines);
1957
1958 let tree = map
1959 .segments
1960 .as_ref()
1961 .expect("add_lines must build the segment tree");
1962 assert_eq!(tree.size(), 1);
1963
1964 let hit_query = rstar::AABB::from_corners([-1.0, -1.0], [1.0, 1.0]);
1967 let hits: Vec<_> = tree.locate_in_envelope_intersecting(hit_query).collect();
1968 assert_eq!(hits.len(), 1);
1969 assert_eq!(hits[0].id, (1, 2));
1970
1971 let miss_query = rstar::AABB::from_corners([100.0, 100.0], [200.0, 200.0]);
1972 assert_eq!(tree.locate_in_envelope_intersecting(miss_query).count(), 0);
1973 }
1974
1975 #[test]
1976 fn add_lines_populates_segment_ids() {
1977 let mut map = Map::new();
1978 map.add_lines(vec![
1979 MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]),
1980 MapSegment::new((3, 4), [1.0, 1.0], [2.0, 2.0]),
1981 ]);
1982 assert!(map.segment_ids.contains(&(1, 2)));
1983 assert!(map.segment_ids.contains(&(3, 4)));
1984 assert_eq!(map.segment_ids.len(), 2);
1985
1986 map.add_hashmap_lines(HashMap::from([(
1988 (5, 6),
1989 MapSegment::new((5, 6), [0.0, 0.0], [1.0, 1.0]),
1990 )]));
1991 assert_eq!(map.segment_ids, HashSet::from([(5, 6)]));
1992 }
1993
1994 #[test]
1995 fn map_check_line_is_painted_on_first_frame() {
1996 use egui::{Context, RawInput, Shape};
1997
1998 let mut map = Map::new();
2000 map.set_zoom(1.0);
2001
2002 let mut point_a = MapPoint::new(0, [0.0, 0.0]);
2003 point_a.connections.push((0, 1));
2004 let mut point_b = MapPoint::new(1, [50.0, 50.0]);
2005 point_b.connections.push((0, 1));
2006
2007 let mut lines = Vec::new();
2008 lines.push(MapSegment::new((0, 1), point_a.coords, point_b.coords));
2009
2010 let mut points = Vec::new();
2011 points.push(point_a);
2012 points.push(point_b);
2013 map.add_points(points);
2015 map.add_lines(lines);
2016
2017 map.set_pos([25.0, 25.0]);
2018
2019 let ctx = Context::default();
2021 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(500.0, 500.0));
2022 let input = RawInput {
2023 screen_rect: Some(screen),
2024 ..RawInput::default()
2025 };
2026
2027 let mut output1 = ctx.run_ui(input.clone(), |ui| {
2028 ui.add(&mut map);
2029 });
2030
2031 let segments1: Vec<[egui::Pos2; 2]> = output1
2032 .shapes
2033 .iter()
2034 .filter_map(|cs| match cs.shape {
2035 Shape::LineSegment { points, .. } => Some(points),
2036 _ => None,
2037 })
2038 .collect();
2039 output1.textures_delta.clear();
2044
2045 assert!(
2046 !segments1.is_empty(),
2047 "Frame 1: no LineSegment shapes painted (map lines did not draw)"
2048 );
2049
2050 let expected_a = egui::pos2(225.0, 225.0);
2053 let expected_b = egui::pos2(275.0, 275.0);
2054 let tolerance = 2.0;
2055 let found_on_frame1 = segments1.iter().any(|[p1, p2]| {
2056 let d_a1 = p1.distance(expected_a);
2057 let d_b1 = p2.distance(expected_b);
2058 let d_a2 = p2.distance(expected_a);
2059 let d_b2 = p1.distance(expected_b);
2060 (d_a1 < tolerance && d_b1 < tolerance) || (d_a2 < tolerance && d_b2 < tolerance)
2061 });
2062 assert!(
2063 found_on_frame1,
2064 "Frame 1: no LineSegment matches expected endpoints (~225,225 -> ~275,275); got {:?}",
2065 segments1
2066 );
2067
2068 let mut output2 = ctx.run_ui(input, |ui| {
2070 ui.add(&mut map);
2071 });
2072
2073 let segments2: Vec<[egui::Pos2; 2]> = output2
2074 .shapes
2075 .iter()
2076 .filter_map(|cs| match cs.shape {
2077 Shape::LineSegment { points, .. } => Some(points),
2078 _ => None,
2079 })
2080 .collect();
2081 output2.textures_delta.clear();
2082
2083 assert_eq!(
2084 segments1.len(),
2085 segments2.len(),
2086 "Frame 2: expected {} line segments (no duplication across frames), got {}",
2087 segments1.len(),
2088 segments2.len()
2089 );
2090 }
2091
2092 #[test]
2095 fn set_pos_and_get_pos_roundtrip() {
2096 let mut map = Map::new();
2097 map.set_pos([25.0, -35.0]);
2098 assert_eq!(map.get_pos(), [25.0, -35.0]);
2099 }
2100
2101 #[test]
2102 fn set_pos_from_nodeid_with_valid_id() {
2103 let mut map = Map::new();
2104 map.add_points(sample_points());
2105 assert!(map.set_pos_from_nodeid(2));
2106 assert_eq!(map.get_pos(), [10.0, 10.0]);
2107 }
2108
2109 #[test]
2116 fn centered_node_is_painted_at_the_middle_of_the_drawable_area() {
2117 use egui::{Context, RawInput, Shape};
2118
2119 for screen_size in [egui::vec2(500.0, 500.0), egui::vec2(800.0, 400.0)] {
2120 for zoom in [1.0, 2.0] {
2121 let mut map = Map::new();
2122 map.set_zoom(zoom);
2123 map.add_points(vec![MapPoint::new(7, [123.0, 456.0])]);
2124 map.set_pos_from_nodeid(7);
2125
2126 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, screen_size);
2127 let ctx = Context::default();
2128 let mut widget_rect = egui::Rect::NOTHING;
2129 let mut output = ctx.run_ui(
2130 RawInput {
2131 screen_rect: Some(screen),
2132 ..RawInput::default()
2133 },
2134 |ui| {
2135 widget_rect = ui.add(&mut map).rect;
2136 },
2137 );
2138
2139 assert!(
2141 screen.contains_rect(widget_rect),
2142 "{screen_size:?} zoom {zoom}: widget rect {widget_rect:?} overflows {screen:?}"
2143 );
2144
2145 let (node_center, drawable) = output
2148 .shapes
2149 .iter()
2150 .find_map(|cs| match &cs.shape {
2151 Shape::Circle(circle) => Some((circle.center, cs.clip_rect)),
2152 _ => None,
2153 })
2154 .expect("the node must be painted");
2155
2156 assert!(
2157 node_center.distance(drawable.center()) < 0.5,
2158 "{screen_size:?} zoom {zoom}: node painted at {node_center:?} but the \
2159 drawable area {drawable:?} is centred at {:?}",
2160 drawable.center()
2161 );
2162
2163 output.textures_delta.clear();
2164 }
2165 }
2166 }
2167
2168 #[test]
2169 fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
2170 let mut map = Map::new();
2171 map.add_points(sample_points());
2172 let before = map.reference.pos.components;
2173 assert!(!map.set_pos_from_nodeid(999));
2175 assert_eq!(map.reference.pos.components, before);
2176 }
2177
2178 #[test]
2179 fn set_pos_from_nodeid_without_points_does_nothing() {
2180 let mut map = Map::new();
2181 assert!(!map.set_pos_from_nodeid(1));
2182 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
2183 }
2184
2185 #[test]
2188 fn add_labels_stores_labels() {
2189 let mut map = Map::new();
2190 let label = MapLabel {
2191 text: "Region".to_string(),
2192 center: Pos2::new(1.0, 2.0),
2193 };
2194 map.add_labels(vec![label]);
2195 assert_eq!(map.labels.len(), 1);
2196 assert_eq!(map.labels[0].text, "Region");
2197 }
2198
2199 #[test]
2200 fn add_lines_stores_lines() {
2201 let mut map = Map::new();
2202 let mut lines = Vec::new();
2203 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [1.0, 1.0]));
2204 map.add_lines(lines);
2205 let tree = map.segments.as_ref().unwrap();
2206 assert_eq!(tree.size(), 1);
2207 assert_eq!(
2208 tree.locate_in_envelope_intersecting(rstar::AABB::from_corners(
2209 [-1.0, -1.0],
2210 [2.0, 2.0],
2211 ))
2212 .next()
2213 .unwrap()
2214 .id,
2215 (1, 2)
2216 );
2217 }
2218
2219 #[test]
2222 fn line_at_returns_closest_line_within_tolerance() {
2223 let mut map = Map::new();
2224 map.add_points(sample_points());
2225 let mut lines = Vec::new();
2226 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 0.0]));
2227 lines.push(MapSegment::new((3, 4), [20.0, -5.0], [20.0, 5.0]));
2228 map.add_lines(lines);
2229
2230 let hit = map.line_at([5.0, 1.5], 2.0).expect("line must be hit");
2232 assert_eq!(hit, (1, 2));
2233
2234 let hit = map.line_at([19.0, 0.0], 2.0).expect("line must be hit");
2236 assert_eq!(hit, (3, 4));
2237 }
2238
2239 #[test]
2240 fn line_at_returns_none_beyond_tolerance() {
2241 let mut map = Map::new();
2242 map.add_points(sample_points());
2243 let mut lines = Vec::new();
2244 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
2245 map.add_lines(lines);
2246
2247 assert!(map.line_at([5.0, 4.0], 0.8).is_some());
2250 assert!(map.line_at([5.0, 4.0], 0.5).is_none());
2251 assert!(map.line_at([100.0, 100.0], 5.0).is_none());
2252 }
2253
2254 #[test]
2255 fn line_at_returns_none_without_lines() {
2256 let map = Map::new();
2257 assert!(map.line_at([0.0, 0.0], 10.0).is_none());
2258 }
2259
2260 #[test]
2261 fn line_at_negative_tolerance_behaves_like_zero() {
2262 let mut map = Map::new();
2263 map.add_points(sample_points());
2264 let mut lines = Vec::new();
2265 lines.push(MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]));
2266 map.add_lines(lines);
2267
2268 assert!(map.line_at([5.0, 5.0], -1.0).is_some());
2270 assert!(map.line_at([5.0, 5.1], -1.0).is_none());
2271 }
2272
2273 #[test]
2276 #[allow(deprecated)]
2277 fn deprecated_notify_still_records_a_pulse() {
2278 let mut map = Map::new();
2279 let t1 = Instant::now();
2280 map.notify(5, t1);
2281 let recorded = map.notifications.get(&5).expect("notify must record");
2282 assert_eq!(recorded.started, t1);
2283 assert_eq!(recorded.animation, NodeAnimation::Pulse);
2284 assert_eq!(recorded.color, None);
2285
2286 let t2 = t1 + Duration::from_secs(1);
2287 map.notify(5, t2);
2288 assert_eq!(map.notifications.get(&5).unwrap().started, t2);
2289 assert_eq!(map.notifications.len(), 1);
2290 }
2291
2292 fn map_with_nodes() -> Map {
2295 let mut map = Map::new();
2296 map.add_points(vec![
2297 MapPoint::new(1, [0.0, 0.0]),
2298 MapPoint::new(2, [10.0, 10.0]),
2299 ]);
2300 map
2301 }
2302
2303 #[test]
2304 fn node_returns_none_for_an_unknown_id() {
2305 let mut map = map_with_nodes();
2306 assert!(map.node(1).is_some());
2307 assert!(map.node(999).is_none());
2308 assert!(Map::new().node(1).is_none());
2310 }
2311
2312 #[test]
2313 fn each_event_effect_records_its_own_animation() {
2314 let now = Instant::now();
2315 for (apply, expected) in [
2316 (
2317 Box::new(|n: NodeHandle| n.pulse(now)) as Box<dyn FnOnce(NodeHandle)>,
2318 NodeAnimation::Pulse,
2319 ),
2320 (
2321 Box::new(|n: NodeHandle| n.ripple(now)),
2322 NodeAnimation::Ripple,
2323 ),
2324 (
2325 Box::new(|n: NodeHandle| n.countdown(now)),
2326 NodeAnimation::CountdownArc,
2327 ),
2328 (
2329 Box::new(|n: NodeHandle| n.scale_in(now)),
2330 NodeAnimation::ScaleIn,
2331 ),
2332 (
2333 Box::new(|n: NodeHandle| n.crosshair(now)),
2334 NodeAnimation::Crosshair,
2335 ),
2336 ] {
2337 let mut map = map_with_nodes();
2338 apply(map.node(1).unwrap());
2339 let recorded = map.notifications.get(&1).expect("effect must be recorded");
2340 assert_eq!(recorded.animation, expected);
2341 assert_eq!(recorded.started, now);
2342 assert!(map.node_states.is_empty());
2344 }
2345 }
2346
2347 #[test]
2348 fn each_steady_effect_records_lasting_state() {
2349 for (apply, expected) in [
2350 (
2351 Box::new(|n: NodeHandle| n.halo()) as Box<dyn FnOnce(NodeHandle)>,
2352 SteadyAnimation::Halo,
2353 ),
2354 (Box::new(|n: NodeHandle| n.blink()), SteadyAnimation::Blink),
2355 (Box::new(|n: NodeHandle| n.orbit()), SteadyAnimation::Orbit),
2356 ] {
2357 let mut map = map_with_nodes();
2358 apply(map.node(1).unwrap());
2359 assert_eq!(map.node_states.get(&1).unwrap().animation, expected);
2360 assert!(map.notifications.is_empty());
2362 }
2363 }
2364
2365 #[test]
2366 fn color_modifier_reaches_both_families() {
2367 let mut map = map_with_nodes();
2368 map.node(1)
2369 .unwrap()
2370 .color(Color32::RED)
2371 .pulse(Instant::now());
2372 map.node(2).unwrap().color(Color32::BLUE).halo();
2373
2374 assert_eq!(map.notifications.get(&1).unwrap().color, Some(Color32::RED));
2375 assert_eq!(map.node_states.get(&2).unwrap().color, Some(Color32::BLUE));
2376 }
2377
2378 #[test]
2379 fn a_node_can_carry_state_and_a_notification_at_once() {
2380 let mut map = map_with_nodes();
2381 map.node(1).unwrap().halo();
2382 map.node(1).unwrap().ripple(Instant::now());
2383
2384 assert!(map.node_states.contains_key(&1));
2385 assert!(map.notifications.contains_key(&1));
2386 }
2387
2388 #[test]
2389 fn clear_removes_both_families_for_that_node_only() {
2390 let mut map = map_with_nodes();
2391 map.node(1).unwrap().halo();
2392 map.node(1).unwrap().ripple(Instant::now());
2393 map.node(2).unwrap().halo();
2394
2395 map.node(1).unwrap().clear();
2396
2397 assert!(!map.node_states.contains_key(&1));
2398 assert!(!map.notifications.contains_key(&1));
2399 assert!(map.node_states.contains_key(&2), "node 2 must be untouched");
2400 }
2401
2402 #[test]
2403 fn re_triggering_replaces_the_previous_effect() {
2404 let mut map = map_with_nodes();
2405 map.node(1).unwrap().pulse(Instant::now());
2406 map.node(1).unwrap().crosshair(Instant::now());
2407
2408 assert_eq!(map.notifications.len(), 1);
2409 assert_eq!(
2410 map.notifications.get(&1).unwrap().animation,
2411 NodeAnimation::Crosshair
2412 );
2413 }
2414
2415 fn map_with_segments() -> Map {
2418 let mut map = Map::new();
2419 map.add_lines(vec![
2420 MapSegment::new((1, 2), [0.0, 0.0], [10.0, 0.0]),
2421 MapSegment::new((3, 4), [0.0, 10.0], [10.0, 10.0]),
2422 ]);
2423 map
2424 }
2425
2426 #[test]
2427 fn segment_returns_none_for_an_unknown_id() {
2428 let mut map = map_with_segments();
2429 assert!(map.segment((1, 2)).is_some());
2430 assert!(map.segment((404, 404)).is_none());
2431 assert!(Map::new().segment((1, 2)).is_none());
2433 }
2434
2435 #[test]
2436 fn flash_records_a_segment_notification() {
2437 let mut map = map_with_segments();
2438 let now = Instant::now();
2439 map.segment((1, 2)).unwrap().flash(now);
2440
2441 let recorded = map
2442 .segment_notifications
2443 .get(&(1, 2))
2444 .expect("flash must be recorded");
2445 assert_eq!(recorded.animation, SegmentAnimation::FlashDecay);
2446 assert_eq!(recorded.started, now);
2447 assert!(map.segment_states.is_empty());
2449 }
2450
2451 #[test]
2452 fn each_event_segment_effect_records_its_own_notification() {
2453 for (apply, expected) in [
2454 (
2455 Box::new(|s: SegmentHandle, at: Instant| s.flash(at))
2456 as Box<dyn FnOnce(SegmentHandle, Instant)>,
2457 SegmentAnimation::FlashDecay,
2458 ),
2459 (
2460 Box::new(|s: SegmentHandle, at: Instant| s.comet_once(at, CometDirection::Forward)),
2461 SegmentAnimation::Comet(CometDirection::Forward),
2462 ),
2463 (
2464 Box::new(|s: SegmentHandle, at: Instant| s.comet_once(at, CometDirection::Reverse)),
2465 SegmentAnimation::Comet(CometDirection::Reverse),
2466 ),
2467 (
2468 Box::new(|s: SegmentHandle, at: Instant| s.wipe(at)),
2469 SegmentAnimation::Wipe,
2470 ),
2471 ] {
2472 let mut map = map_with_segments();
2473 let now = Instant::now();
2474 apply(map.segment((1, 2)).unwrap(), now);
2475
2476 let recorded = map
2477 .segment_notifications
2478 .get(&(1, 2))
2479 .expect("the effect must be recorded");
2480 assert_eq!(recorded.animation, expected);
2481 assert_eq!(recorded.started, now);
2482 assert!(map.segment_states.is_empty());
2484 }
2485 }
2486
2487 #[test]
2488 fn each_steady_segment_effect_records_lasting_state() {
2489 for (apply, expected) in [
2490 (
2491 Box::new(|s: SegmentHandle| s.comet()) as Box<dyn FnOnce(SegmentHandle)>,
2492 SteadySegmentAnimation::Comet,
2493 ),
2494 (
2495 Box::new(|s: SegmentHandle| s.dash()),
2496 SteadySegmentAnimation::Dash,
2497 ),
2498 (
2499 Box::new(|s: SegmentHandle| s.glow_band()),
2500 SteadySegmentAnimation::GlowBand,
2501 ),
2502 (
2503 Box::new(|s: SegmentHandle| s.chevrons()),
2504 SteadySegmentAnimation::Chevrons,
2505 ),
2506 ] {
2507 let mut map = map_with_segments();
2508 apply(map.segment((1, 2)).unwrap());
2509 assert_eq!(map.segment_states.get(&(1, 2)).unwrap().animation, expected);
2510 assert!(map.segment_notifications.is_empty());
2512 }
2513 }
2514
2515 #[test]
2516 fn steady_segment_effect_alpha_tracks_the_lines_zoom_fade_with_a_head_start() {
2517 let mut map = map_with_segments();
2524 map.set_zoom(0.6);
2525 let color = Color32::from_rgba_unmultiplied(10, 20, 30, 200);
2526 map.segment((1, 2)).unwrap().color(color).comet();
2527
2528 let expected_fill = scale_alpha(color, 0.7);
2534 assert_eq!(
2535 expected_fill.a(),
2536 140,
2537 "sanity-check the hand-derived alpha"
2538 );
2539
2540 let ctx = Context::default();
2541 let screen_rect = Rect::from_min_size(Pos2::ZERO, vec2(400.0, 300.0));
2542 let mut out = ctx.run_ui(
2543 RawInput {
2544 screen_rect: Some(screen_rect),
2545 ..RawInput::default()
2546 },
2547 |ui| {
2548 ui.add(&mut map);
2549 },
2550 );
2551 out.textures_delta.clear();
2555
2556 let comet_circle = out
2557 .shapes
2558 .iter()
2559 .find_map(|cs| match &cs.shape {
2560 Shape::Circle(c) => Some(*c),
2561 _ => None,
2562 })
2563 .expect("comet must paint a filled circle");
2564
2565 assert_eq!(
2566 comet_circle.fill, expected_fill,
2567 "the comet's fill must be exactly scale_alpha(color, effect_fade) -- the line's \
2568 zoom fade with the documented head start applied"
2569 );
2570 }
2571
2572 #[test]
2573 fn color_modifier_reaches_both_segment_families() {
2574 let mut map = map_with_segments();
2575 map.segment((1, 2))
2576 .unwrap()
2577 .color(Color32::RED)
2578 .flash(Instant::now());
2579 map.segment((3, 4)).unwrap().color(Color32::BLUE).comet();
2580
2581 assert_eq!(
2582 map.segment_notifications.get(&(1, 2)).unwrap().color,
2583 Some(Color32::RED)
2584 );
2585 assert_eq!(
2586 map.segment_states.get(&(3, 4)).unwrap().color,
2587 Some(Color32::BLUE)
2588 );
2589 }
2590
2591 #[test]
2592 fn a_segment_can_carry_state_and_a_notification_at_once() {
2593 let mut map = map_with_segments();
2594 map.segment((1, 2)).unwrap().comet();
2595 map.segment((1, 2)).unwrap().flash(Instant::now());
2596
2597 assert!(map.segment_states.contains_key(&(1, 2)));
2598 assert!(map.segment_notifications.contains_key(&(1, 2)));
2599 }
2600
2601 #[test]
2602 fn clear_removes_both_segment_families_for_that_id_only() {
2603 let mut map = map_with_segments();
2604 map.segment((1, 2)).unwrap().comet();
2605 map.segment((1, 2)).unwrap().flash(Instant::now());
2606 map.segment((3, 4)).unwrap().comet();
2607
2608 map.segment((1, 2)).unwrap().clear();
2609
2610 assert!(!map.segment_states.contains_key(&(1, 2)));
2611 assert!(!map.segment_notifications.contains_key(&(1, 2)));
2612 assert!(
2613 map.segment_states.contains_key(&(3, 4)),
2614 "segment (3, 4) must be untouched"
2615 );
2616 }
2617
2618 #[test]
2619 fn re_flashing_a_segment_restarts_it() {
2620 let mut map = map_with_segments();
2621 let t1 = Instant::now();
2622 map.segment((1, 2)).unwrap().flash(t1);
2623 let t2 = t1 + Duration::from_secs(1);
2624 map.segment((1, 2)).unwrap().flash(t2);
2625
2626 assert_eq!(map.segment_notifications.len(), 1);
2627 assert_eq!(map.segment_notifications.get(&(1, 2)).unwrap().started, t2);
2628 }
2629
2630 #[test]
2631 fn update_marker_inserts_and_updates() {
2632 let mut map = Map::new();
2633 map.update_marker(1, 100);
2634 assert_eq!(map.markers.get(&1), Some(&100));
2635 map.update_marker(1, 200);
2636 assert_eq!(map.markers.get(&1), Some(&200));
2637 assert_eq!(map.markers.len(), 1);
2638 }
2639
2640 #[test]
2643 fn allocate_at_least_sets_min_size() {
2644 let mut map = Map::new();
2645 map.allocate_at_least(Some(100.0), None);
2646 assert_eq!(map.min_size, (Some(100.0), None));
2647 }
2648
2649 #[test]
2650 fn allocate_at_most_sets_max_size() {
2651 let mut map = Map::new();
2652 map.allocate_at_most(None, Some(200.0));
2653 assert_eq!(map.max_size, (None, Some(200.0)));
2654 }
2655
2656 #[test]
2659 fn adjust_bounds_scales_with_zoom() {
2660 let mut map = Map::new();
2661 map.reference.min = RawPoint::new(-10.0, -20.0);
2662 map.reference.max = RawPoint::new(10.0, 20.0);
2663 map.reference.pos = RawPoint::new(5.0, 5.0);
2664 map.reference.dist = 100.0;
2665 map.set_zoom(2.0);
2666 map.adjust_bounds();
2667
2668 assert_eq!(map.current.max.components, [20.0, 40.0]);
2669 assert_eq!(map.current.min.components, [-20.0, -40.0]);
2670 assert_eq!(map.current.pos.components, [10.0, 10.0]);
2671 assert_eq!(map.current.dist, 50.0);
2672 }
2673
2674 #[test]
2677 fn theme_colors_are_resolved_live_from_the_installed_theme() {
2678 let mut map = Map::new();
2683 map.set_theme(Rc::new(Theme::ArticCyan));
2684
2685 let light = Theme::ArticCyan.colors(ColorMode::Light);
2686 let dark = Theme::ArticCyan.colors(ColorMode::Dark);
2687
2688 assert_eq!(map.current_index, 0, "Map::new starts in light mode");
2689 assert_eq!(map.theme_colors(), light);
2690
2691 map.current_index = 1;
2694 assert_eq!(map.theme_colors(), dark);
2695 }
2696
2697 #[test]
2698 fn a_custom_map_theme_reaches_the_painted_node() {
2699 use crate::map::theme::ThemeColors;
2703 use egui::{Context, RawInput, Shape};
2704
2705 struct FixedPalette;
2706 impl MapTheme for FixedPalette {
2707 fn colors(&self, _mode: ColorMode) -> ThemeColors {
2708 ThemeColors {
2709 node: Color32::from_rgb(1, 2, 3),
2710 segment: Color32::from_rgb(4, 5, 6),
2711 selected: Color32::from_rgb(7, 8, 9),
2712 alert: Color32::from_rgb(10, 11, 12),
2713 text: Color32::from_rgb(13, 14, 15),
2714 }
2715 }
2716 }
2717
2718 let mut map = Map::new();
2719 map.set_theme(Rc::new(FixedPalette));
2720 map.add_points(vec![MapPoint::new(1, [0.0, 0.0])]);
2721
2722 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(200.0, 200.0));
2723 let ctx = Context::default();
2724 let mut output = ctx.run_ui(
2725 RawInput {
2726 screen_rect: Some(screen),
2727 ..RawInput::default()
2728 },
2729 |ui| {
2730 ui.add(&mut map);
2731 },
2732 );
2733 output.textures_delta.clear();
2735
2736 let fill = output
2737 .shapes
2738 .iter()
2739 .find_map(|cs| match &cs.shape {
2740 Shape::Circle(circle) => Some(circle.fill),
2741 _ => None,
2742 })
2743 .expect("the node must be painted");
2744
2745 assert_eq!(
2746 fill,
2747 Color32::from_rgb(1, 2, 3),
2748 "the node fill must come from the installed MapTheme, in either color mode"
2749 );
2750 }
2751
2752 #[test]
2753 fn a_custom_map_theme_reaches_the_selection_highlight() {
2754 use crate::map::theme::ThemeColors;
2760 use egui::{Context, Event, RawInput};
2761 use std::cell::RefCell;
2762
2763 struct FixedPalette;
2764 impl MapTheme for FixedPalette {
2765 fn colors(&self, _mode: ColorMode) -> ThemeColors {
2766 ThemeColors {
2767 node: Color32::from_rgb(1, 2, 3),
2768 segment: Color32::from_rgb(4, 5, 6),
2769 selected: Color32::from_rgb(7, 8, 9),
2770 alert: Color32::from_rgb(10, 11, 12),
2771 text: Color32::from_rgb(13, 14, 15),
2772 }
2773 }
2774 }
2775
2776 #[derive(Default)]
2777 struct RecordingTemplate {
2778 seen: RefCell<Option<(usize, Color32)>>,
2779 }
2780 impl NodeTemplate for RecordingTemplate {
2781 fn node_ui(&self, _ui: &mut Ui, _ctx: NodeContext) {}
2782 fn selection_ui(&self, _ui: &mut Ui, ctx: SelectionContext) {
2783 *self.seen.borrow_mut() = Some((ctx.point.get_id(), ctx.color));
2784 }
2785 fn notification_ui(&self, _ui: &mut Ui, _ctx: NotificationContext) -> bool {
2786 false
2787 }
2788 fn marker_ui(&self, _ui: &mut Ui, _ctx: MarkerContext) {}
2789 }
2790
2791 let mut map = Map::new();
2792 map.set_theme(Rc::new(FixedPalette));
2793 map.settings.node_text_visibility = VisibilitySetting::Hover;
2794 map.add_points(vec![MapPoint::new(9, [0.0, 0.0])]);
2795 let template = Rc::new(RecordingTemplate::default());
2796 map.set_node_template(template.clone());
2797
2798 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(200.0, 200.0));
2799 let ctx = Context::default();
2800 for pass in 0..2 {
2804 let events = if pass == 1 {
2805 vec![Event::PointerMoved(screen.center())]
2806 } else {
2807 Vec::new()
2808 };
2809 let mut output = ctx.run_ui(
2810 RawInput {
2811 screen_rect: Some(screen),
2812 events,
2813 ..RawInput::default()
2814 },
2815 |ui| {
2816 ui.add(&mut map);
2817 },
2818 );
2819 output.textures_delta.clear();
2821 }
2822
2823 let (id, color) = template
2824 .seen
2825 .borrow()
2826 .expect("selection_ui must be called while the pointer hovers the map");
2827 assert_eq!(
2828 id, 9,
2829 "the highlighted node must be the one under the pointer"
2830 );
2831 assert_eq!(
2832 color,
2833 Color32::from_rgb(7, 8, 9),
2834 "the highlight color must come from the installed MapTheme's `selected`"
2835 );
2836 }
2837}