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 vec![
1791 MapPoint::new(1, [0.0, 0.0]),
1792 MapPoint::new(2, [10.0, 10.0]),
1793 MapPoint::new(3, [-10.0, -10.0]),
1794 ]
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 lines = vec![MapSegment::new((1, 2), [-4000.0, -1.0], [4000.0, 1.0])];
1926 map.add_lines(lines);
1927 map.set_pos([0.0, 0.0]);
1928
1929 let segments = render_line_segments(&mut map);
1930 assert_eq!(segments.len(), 1);
1931 }
1932
1933 #[test]
1934 fn segment_outside_viewport_is_not_painted() {
1935 let mut map = Map::new();
1936 map.set_zoom(1.0);
1937 let lines = vec![MapSegment::new(
1938 (1, 2),
1939 [10_000.0, 10_000.0],
1940 [10_100.0, 10_100.0],
1941 )];
1942 map.add_lines(lines);
1943 map.set_pos([0.0, 0.0]);
1944
1945 assert!(render_line_segments(&mut map).is_empty());
1946 }
1947
1948 #[test]
1949 fn add_lines_builds_segment_tree() {
1950 let mut map = Map::new();
1951 map.add_points(sample_points());
1952 let lines = vec![MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0])];
1953 map.add_lines(lines);
1954
1955 let tree = map
1956 .segments
1957 .as_ref()
1958 .expect("add_lines must build the segment tree");
1959 assert_eq!(tree.size(), 1);
1960
1961 let hit_query = rstar::AABB::from_corners([-1.0, -1.0], [1.0, 1.0]);
1964 let hits: Vec<_> = tree.locate_in_envelope_intersecting(hit_query).collect();
1965 assert_eq!(hits.len(), 1);
1966 assert_eq!(hits[0].id, (1, 2));
1967
1968 let miss_query = rstar::AABB::from_corners([100.0, 100.0], [200.0, 200.0]);
1969 assert_eq!(tree.locate_in_envelope_intersecting(miss_query).count(), 0);
1970 }
1971
1972 #[test]
1973 fn add_lines_populates_segment_ids() {
1974 let mut map = Map::new();
1975 map.add_lines(vec![
1976 MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0]),
1977 MapSegment::new((3, 4), [1.0, 1.0], [2.0, 2.0]),
1978 ]);
1979 assert!(map.segment_ids.contains(&(1, 2)));
1980 assert!(map.segment_ids.contains(&(3, 4)));
1981 assert_eq!(map.segment_ids.len(), 2);
1982
1983 map.add_hashmap_lines(HashMap::from([(
1985 (5, 6),
1986 MapSegment::new((5, 6), [0.0, 0.0], [1.0, 1.0]),
1987 )]));
1988 assert_eq!(map.segment_ids, HashSet::from([(5, 6)]));
1989 }
1990
1991 #[test]
1992 fn map_check_line_is_painted_on_first_frame() {
1993 use egui::{Context, RawInput, Shape};
1994
1995 let mut map = Map::new();
1997 map.set_zoom(1.0);
1998
1999 let mut point_a = MapPoint::new(0, [0.0, 0.0]);
2000 point_a.connections.push((0, 1));
2001 let mut point_b = MapPoint::new(1, [50.0, 50.0]);
2002 point_b.connections.push((0, 1));
2003
2004 let lines = vec![MapSegment::new((0, 1), point_a.coords, point_b.coords)];
2005
2006 let points = vec![point_a, point_b];
2007 map.add_points(points);
2009 map.add_lines(lines);
2010
2011 map.set_pos([25.0, 25.0]);
2012
2013 let ctx = Context::default();
2015 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(500.0, 500.0));
2016 let input = RawInput {
2017 screen_rect: Some(screen),
2018 ..RawInput::default()
2019 };
2020
2021 let mut output1 = ctx.run_ui(input.clone(), |ui| {
2022 ui.add(&mut map);
2023 });
2024
2025 let segments1: Vec<[egui::Pos2; 2]> = output1
2026 .shapes
2027 .iter()
2028 .filter_map(|cs| match cs.shape {
2029 Shape::LineSegment { points, .. } => Some(points),
2030 _ => None,
2031 })
2032 .collect();
2033 output1.textures_delta.clear();
2038
2039 assert!(
2040 !segments1.is_empty(),
2041 "Frame 1: no LineSegment shapes painted (map lines did not draw)"
2042 );
2043
2044 let expected_a = egui::pos2(225.0, 225.0);
2047 let expected_b = egui::pos2(275.0, 275.0);
2048 let tolerance = 2.0;
2049 let found_on_frame1 = segments1.iter().any(|[p1, p2]| {
2050 let d_a1 = p1.distance(expected_a);
2051 let d_b1 = p2.distance(expected_b);
2052 let d_a2 = p2.distance(expected_a);
2053 let d_b2 = p1.distance(expected_b);
2054 (d_a1 < tolerance && d_b1 < tolerance) || (d_a2 < tolerance && d_b2 < tolerance)
2055 });
2056 assert!(
2057 found_on_frame1,
2058 "Frame 1: no LineSegment matches expected endpoints (~225,225 -> ~275,275); got {:?}",
2059 segments1
2060 );
2061
2062 let mut output2 = ctx.run_ui(input, |ui| {
2064 ui.add(&mut map);
2065 });
2066
2067 let segments2: Vec<[egui::Pos2; 2]> = output2
2068 .shapes
2069 .iter()
2070 .filter_map(|cs| match cs.shape {
2071 Shape::LineSegment { points, .. } => Some(points),
2072 _ => None,
2073 })
2074 .collect();
2075 output2.textures_delta.clear();
2076
2077 assert_eq!(
2078 segments1.len(),
2079 segments2.len(),
2080 "Frame 2: expected {} line segments (no duplication across frames), got {}",
2081 segments1.len(),
2082 segments2.len()
2083 );
2084 }
2085
2086 #[test]
2089 fn set_pos_and_get_pos_roundtrip() {
2090 let mut map = Map::new();
2091 map.set_pos([25.0, -35.0]);
2092 assert_eq!(map.get_pos(), [25.0, -35.0]);
2093 }
2094
2095 #[test]
2096 fn set_pos_from_nodeid_with_valid_id() {
2097 let mut map = Map::new();
2098 map.add_points(sample_points());
2099 assert!(map.set_pos_from_nodeid(2));
2100 assert_eq!(map.get_pos(), [10.0, 10.0]);
2101 }
2102
2103 #[test]
2110 fn centered_node_is_painted_at_the_middle_of_the_drawable_area() {
2111 use egui::{Context, RawInput, Shape};
2112
2113 for screen_size in [egui::vec2(500.0, 500.0), egui::vec2(800.0, 400.0)] {
2114 for zoom in [1.0, 2.0] {
2115 let mut map = Map::new();
2116 map.set_zoom(zoom);
2117 map.add_points(vec![MapPoint::new(7, [123.0, 456.0])]);
2118 map.set_pos_from_nodeid(7);
2119
2120 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, screen_size);
2121 let ctx = Context::default();
2122 let mut widget_rect = egui::Rect::NOTHING;
2123 let mut output = ctx.run_ui(
2124 RawInput {
2125 screen_rect: Some(screen),
2126 ..RawInput::default()
2127 },
2128 |ui| {
2129 widget_rect = ui.add(&mut map).rect;
2130 },
2131 );
2132
2133 assert!(
2135 screen.contains_rect(widget_rect),
2136 "{screen_size:?} zoom {zoom}: widget rect {widget_rect:?} overflows {screen:?}"
2137 );
2138
2139 let (node_center, drawable) = output
2142 .shapes
2143 .iter()
2144 .find_map(|cs| match &cs.shape {
2145 Shape::Circle(circle) => Some((circle.center, cs.clip_rect)),
2146 _ => None,
2147 })
2148 .expect("the node must be painted");
2149
2150 assert!(
2151 node_center.distance(drawable.center()) < 0.5,
2152 "{screen_size:?} zoom {zoom}: node painted at {node_center:?} but the \
2153 drawable area {drawable:?} is centred at {:?}",
2154 drawable.center()
2155 );
2156
2157 output.textures_delta.clear();
2158 }
2159 }
2160 }
2161
2162 #[test]
2163 fn set_pos_from_nodeid_with_invalid_id_keeps_position() {
2164 let mut map = Map::new();
2165 map.add_points(sample_points());
2166 let before = map.reference.pos.components;
2167 assert!(!map.set_pos_from_nodeid(999));
2169 assert_eq!(map.reference.pos.components, before);
2170 }
2171
2172 #[test]
2173 fn set_pos_from_nodeid_without_points_does_nothing() {
2174 let mut map = Map::new();
2175 assert!(!map.set_pos_from_nodeid(1));
2176 assert_eq!(map.reference.pos.components, [0.0, 0.0]);
2177 }
2178
2179 #[test]
2182 fn add_labels_stores_labels() {
2183 let mut map = Map::new();
2184 let label = MapLabel {
2185 text: "Region".to_string(),
2186 center: Pos2::new(1.0, 2.0),
2187 };
2188 map.add_labels(vec![label]);
2189 assert_eq!(map.labels.len(), 1);
2190 assert_eq!(map.labels[0].text, "Region");
2191 }
2192
2193 #[test]
2194 fn add_lines_stores_lines() {
2195 let mut map = Map::new();
2196 let lines = vec![MapSegment::new((1, 2), [0.0, 0.0], [1.0, 1.0])];
2197 map.add_lines(lines);
2198 let tree = map.segments.as_ref().unwrap();
2199 assert_eq!(tree.size(), 1);
2200 assert_eq!(
2201 tree.locate_in_envelope_intersecting(rstar::AABB::from_corners(
2202 [-1.0, -1.0],
2203 [2.0, 2.0],
2204 ))
2205 .next()
2206 .unwrap()
2207 .id,
2208 (1, 2)
2209 );
2210 }
2211
2212 #[test]
2215 fn line_at_returns_closest_line_within_tolerance() {
2216 let mut map = Map::new();
2217 map.add_points(sample_points());
2218 let lines = vec![
2219 MapSegment::new((1, 2), [0.0, 0.0], [10.0, 0.0]),
2220 MapSegment::new((3, 4), [20.0, -5.0], [20.0, 5.0]),
2221 ];
2222 map.add_lines(lines);
2223
2224 let hit = map.line_at([5.0, 1.5], 2.0).expect("line must be hit");
2226 assert_eq!(hit, (1, 2));
2227
2228 let hit = map.line_at([19.0, 0.0], 2.0).expect("line must be hit");
2230 assert_eq!(hit, (3, 4));
2231 }
2232
2233 #[test]
2234 fn line_at_returns_none_beyond_tolerance() {
2235 let mut map = Map::new();
2236 map.add_points(sample_points());
2237 let lines = vec![MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0])];
2238 map.add_lines(lines);
2239
2240 assert!(map.line_at([5.0, 4.0], 0.8).is_some());
2243 assert!(map.line_at([5.0, 4.0], 0.5).is_none());
2244 assert!(map.line_at([100.0, 100.0], 5.0).is_none());
2245 }
2246
2247 #[test]
2248 fn line_at_returns_none_without_lines() {
2249 let map = Map::new();
2250 assert!(map.line_at([0.0, 0.0], 10.0).is_none());
2251 }
2252
2253 #[test]
2254 fn line_at_negative_tolerance_behaves_like_zero() {
2255 let mut map = Map::new();
2256 map.add_points(sample_points());
2257 let lines = vec![MapSegment::new((1, 2), [0.0, 0.0], [10.0, 10.0])];
2258 map.add_lines(lines);
2259
2260 assert!(map.line_at([5.0, 5.0], -1.0).is_some());
2262 assert!(map.line_at([5.0, 5.1], -1.0).is_none());
2263 }
2264
2265 #[test]
2268 #[allow(deprecated)]
2269 fn deprecated_notify_still_records_a_pulse() {
2270 let mut map = Map::new();
2271 let t1 = Instant::now();
2272 map.notify(5, t1);
2273 let recorded = map.notifications.get(&5).expect("notify must record");
2274 assert_eq!(recorded.started, t1);
2275 assert_eq!(recorded.animation, NodeAnimation::Pulse);
2276 assert_eq!(recorded.color, None);
2277
2278 let t2 = t1 + Duration::from_secs(1);
2279 map.notify(5, t2);
2280 assert_eq!(map.notifications.get(&5).unwrap().started, t2);
2281 assert_eq!(map.notifications.len(), 1);
2282 }
2283
2284 fn map_with_nodes() -> Map {
2287 let mut map = Map::new();
2288 map.add_points(vec![
2289 MapPoint::new(1, [0.0, 0.0]),
2290 MapPoint::new(2, [10.0, 10.0]),
2291 ]);
2292 map
2293 }
2294
2295 #[test]
2296 fn node_returns_none_for_an_unknown_id() {
2297 let mut map = map_with_nodes();
2298 assert!(map.node(1).is_some());
2299 assert!(map.node(999).is_none());
2300 assert!(Map::new().node(1).is_none());
2302 }
2303
2304 #[test]
2305 fn each_event_effect_records_its_own_animation() {
2306 let now = Instant::now();
2307 for (apply, expected) in [
2308 (
2309 Box::new(|n: NodeHandle| n.pulse(now)) as Box<dyn FnOnce(NodeHandle)>,
2310 NodeAnimation::Pulse,
2311 ),
2312 (
2313 Box::new(|n: NodeHandle| n.ripple(now)),
2314 NodeAnimation::Ripple,
2315 ),
2316 (
2317 Box::new(|n: NodeHandle| n.countdown(now)),
2318 NodeAnimation::CountdownArc,
2319 ),
2320 (
2321 Box::new(|n: NodeHandle| n.scale_in(now)),
2322 NodeAnimation::ScaleIn,
2323 ),
2324 (
2325 Box::new(|n: NodeHandle| n.crosshair(now)),
2326 NodeAnimation::Crosshair,
2327 ),
2328 ] {
2329 let mut map = map_with_nodes();
2330 apply(map.node(1).unwrap());
2331 let recorded = map.notifications.get(&1).expect("effect must be recorded");
2332 assert_eq!(recorded.animation, expected);
2333 assert_eq!(recorded.started, now);
2334 assert!(map.node_states.is_empty());
2336 }
2337 }
2338
2339 #[test]
2340 fn each_steady_effect_records_lasting_state() {
2341 for (apply, expected) in [
2342 (
2343 Box::new(|n: NodeHandle| n.halo()) as Box<dyn FnOnce(NodeHandle)>,
2344 SteadyAnimation::Halo,
2345 ),
2346 (Box::new(|n: NodeHandle| n.blink()), SteadyAnimation::Blink),
2347 (Box::new(|n: NodeHandle| n.orbit()), SteadyAnimation::Orbit),
2348 ] {
2349 let mut map = map_with_nodes();
2350 apply(map.node(1).unwrap());
2351 assert_eq!(map.node_states.get(&1).unwrap().animation, expected);
2352 assert!(map.notifications.is_empty());
2354 }
2355 }
2356
2357 #[test]
2358 fn color_modifier_reaches_both_families() {
2359 let mut map = map_with_nodes();
2360 map.node(1)
2361 .unwrap()
2362 .color(Color32::RED)
2363 .pulse(Instant::now());
2364 map.node(2).unwrap().color(Color32::BLUE).halo();
2365
2366 assert_eq!(map.notifications.get(&1).unwrap().color, Some(Color32::RED));
2367 assert_eq!(map.node_states.get(&2).unwrap().color, Some(Color32::BLUE));
2368 }
2369
2370 #[test]
2371 fn a_node_can_carry_state_and_a_notification_at_once() {
2372 let mut map = map_with_nodes();
2373 map.node(1).unwrap().halo();
2374 map.node(1).unwrap().ripple(Instant::now());
2375
2376 assert!(map.node_states.contains_key(&1));
2377 assert!(map.notifications.contains_key(&1));
2378 }
2379
2380 #[test]
2381 fn clear_removes_both_families_for_that_node_only() {
2382 let mut map = map_with_nodes();
2383 map.node(1).unwrap().halo();
2384 map.node(1).unwrap().ripple(Instant::now());
2385 map.node(2).unwrap().halo();
2386
2387 map.node(1).unwrap().clear();
2388
2389 assert!(!map.node_states.contains_key(&1));
2390 assert!(!map.notifications.contains_key(&1));
2391 assert!(map.node_states.contains_key(&2), "node 2 must be untouched");
2392 }
2393
2394 #[test]
2395 fn re_triggering_replaces_the_previous_effect() {
2396 let mut map = map_with_nodes();
2397 map.node(1).unwrap().pulse(Instant::now());
2398 map.node(1).unwrap().crosshair(Instant::now());
2399
2400 assert_eq!(map.notifications.len(), 1);
2401 assert_eq!(
2402 map.notifications.get(&1).unwrap().animation,
2403 NodeAnimation::Crosshair
2404 );
2405 }
2406
2407 fn map_with_segments() -> Map {
2410 let mut map = Map::new();
2411 map.add_lines(vec![
2412 MapSegment::new((1, 2), [0.0, 0.0], [10.0, 0.0]),
2413 MapSegment::new((3, 4), [0.0, 10.0], [10.0, 10.0]),
2414 ]);
2415 map
2416 }
2417
2418 #[test]
2419 fn segment_returns_none_for_an_unknown_id() {
2420 let mut map = map_with_segments();
2421 assert!(map.segment((1, 2)).is_some());
2422 assert!(map.segment((404, 404)).is_none());
2423 assert!(Map::new().segment((1, 2)).is_none());
2425 }
2426
2427 #[test]
2428 fn flash_records_a_segment_notification() {
2429 let mut map = map_with_segments();
2430 let now = Instant::now();
2431 map.segment((1, 2)).unwrap().flash(now);
2432
2433 let recorded = map
2434 .segment_notifications
2435 .get(&(1, 2))
2436 .expect("flash must be recorded");
2437 assert_eq!(recorded.animation, SegmentAnimation::FlashDecay);
2438 assert_eq!(recorded.started, now);
2439 assert!(map.segment_states.is_empty());
2441 }
2442
2443 #[test]
2444 fn each_event_segment_effect_records_its_own_notification() {
2445 for (apply, expected) in [
2446 (
2447 Box::new(|s: SegmentHandle, at: Instant| s.flash(at))
2448 as Box<dyn FnOnce(SegmentHandle, Instant)>,
2449 SegmentAnimation::FlashDecay,
2450 ),
2451 (
2452 Box::new(|s: SegmentHandle, at: Instant| s.comet_once(at, CometDirection::Forward)),
2453 SegmentAnimation::Comet(CometDirection::Forward),
2454 ),
2455 (
2456 Box::new(|s: SegmentHandle, at: Instant| s.comet_once(at, CometDirection::Reverse)),
2457 SegmentAnimation::Comet(CometDirection::Reverse),
2458 ),
2459 (
2460 Box::new(|s: SegmentHandle, at: Instant| s.wipe(at)),
2461 SegmentAnimation::Wipe,
2462 ),
2463 ] {
2464 let mut map = map_with_segments();
2465 let now = Instant::now();
2466 apply(map.segment((1, 2)).unwrap(), now);
2467
2468 let recorded = map
2469 .segment_notifications
2470 .get(&(1, 2))
2471 .expect("the effect must be recorded");
2472 assert_eq!(recorded.animation, expected);
2473 assert_eq!(recorded.started, now);
2474 assert!(map.segment_states.is_empty());
2476 }
2477 }
2478
2479 #[test]
2480 fn each_steady_segment_effect_records_lasting_state() {
2481 for (apply, expected) in [
2482 (
2483 Box::new(|s: SegmentHandle| s.comet()) as Box<dyn FnOnce(SegmentHandle)>,
2484 SteadySegmentAnimation::Comet,
2485 ),
2486 (
2487 Box::new(|s: SegmentHandle| s.dash()),
2488 SteadySegmentAnimation::Dash,
2489 ),
2490 (
2491 Box::new(|s: SegmentHandle| s.glow_band()),
2492 SteadySegmentAnimation::GlowBand,
2493 ),
2494 (
2495 Box::new(|s: SegmentHandle| s.chevrons()),
2496 SteadySegmentAnimation::Chevrons,
2497 ),
2498 ] {
2499 let mut map = map_with_segments();
2500 apply(map.segment((1, 2)).unwrap());
2501 assert_eq!(map.segment_states.get(&(1, 2)).unwrap().animation, expected);
2502 assert!(map.segment_notifications.is_empty());
2504 }
2505 }
2506
2507 #[test]
2508 fn steady_segment_effect_alpha_tracks_the_lines_zoom_fade_with_a_head_start() {
2509 let mut map = map_with_segments();
2516 map.set_zoom(0.6);
2517 let color = Color32::from_rgba_unmultiplied(10, 20, 30, 200);
2518 map.segment((1, 2)).unwrap().color(color).comet();
2519
2520 let expected_fill = scale_alpha(color, 0.7);
2526 assert_eq!(
2527 expected_fill.a(),
2528 140,
2529 "sanity-check the hand-derived alpha"
2530 );
2531
2532 let ctx = Context::default();
2533 let screen_rect = Rect::from_min_size(Pos2::ZERO, vec2(400.0, 300.0));
2534 let mut out = ctx.run_ui(
2535 RawInput {
2536 screen_rect: Some(screen_rect),
2537 ..RawInput::default()
2538 },
2539 |ui| {
2540 ui.add(&mut map);
2541 },
2542 );
2543 out.textures_delta.clear();
2547
2548 let comet_circle = out
2549 .shapes
2550 .iter()
2551 .find_map(|cs| match &cs.shape {
2552 Shape::Circle(c) => Some(*c),
2553 _ => None,
2554 })
2555 .expect("comet must paint a filled circle");
2556
2557 assert_eq!(
2558 comet_circle.fill, expected_fill,
2559 "the comet's fill must be exactly scale_alpha(color, effect_fade) -- the line's \
2560 zoom fade with the documented head start applied"
2561 );
2562 }
2563
2564 #[test]
2565 fn color_modifier_reaches_both_segment_families() {
2566 let mut map = map_with_segments();
2567 map.segment((1, 2))
2568 .unwrap()
2569 .color(Color32::RED)
2570 .flash(Instant::now());
2571 map.segment((3, 4)).unwrap().color(Color32::BLUE).comet();
2572
2573 assert_eq!(
2574 map.segment_notifications.get(&(1, 2)).unwrap().color,
2575 Some(Color32::RED)
2576 );
2577 assert_eq!(
2578 map.segment_states.get(&(3, 4)).unwrap().color,
2579 Some(Color32::BLUE)
2580 );
2581 }
2582
2583 #[test]
2584 fn a_segment_can_carry_state_and_a_notification_at_once() {
2585 let mut map = map_with_segments();
2586 map.segment((1, 2)).unwrap().comet();
2587 map.segment((1, 2)).unwrap().flash(Instant::now());
2588
2589 assert!(map.segment_states.contains_key(&(1, 2)));
2590 assert!(map.segment_notifications.contains_key(&(1, 2)));
2591 }
2592
2593 #[test]
2594 fn clear_removes_both_segment_families_for_that_id_only() {
2595 let mut map = map_with_segments();
2596 map.segment((1, 2)).unwrap().comet();
2597 map.segment((1, 2)).unwrap().flash(Instant::now());
2598 map.segment((3, 4)).unwrap().comet();
2599
2600 map.segment((1, 2)).unwrap().clear();
2601
2602 assert!(!map.segment_states.contains_key(&(1, 2)));
2603 assert!(!map.segment_notifications.contains_key(&(1, 2)));
2604 assert!(
2605 map.segment_states.contains_key(&(3, 4)),
2606 "segment (3, 4) must be untouched"
2607 );
2608 }
2609
2610 #[test]
2611 fn re_flashing_a_segment_restarts_it() {
2612 let mut map = map_with_segments();
2613 let t1 = Instant::now();
2614 map.segment((1, 2)).unwrap().flash(t1);
2615 let t2 = t1 + Duration::from_secs(1);
2616 map.segment((1, 2)).unwrap().flash(t2);
2617
2618 assert_eq!(map.segment_notifications.len(), 1);
2619 assert_eq!(map.segment_notifications.get(&(1, 2)).unwrap().started, t2);
2620 }
2621
2622 #[test]
2623 fn update_marker_inserts_and_updates() {
2624 let mut map = Map::new();
2625 map.update_marker(1, 100);
2626 assert_eq!(map.markers.get(&1), Some(&100));
2627 map.update_marker(1, 200);
2628 assert_eq!(map.markers.get(&1), Some(&200));
2629 assert_eq!(map.markers.len(), 1);
2630 }
2631
2632 #[test]
2635 fn allocate_at_least_sets_min_size() {
2636 let mut map = Map::new();
2637 map.allocate_at_least(Some(100.0), None);
2638 assert_eq!(map.min_size, (Some(100.0), None));
2639 }
2640
2641 #[test]
2642 fn allocate_at_most_sets_max_size() {
2643 let mut map = Map::new();
2644 map.allocate_at_most(None, Some(200.0));
2645 assert_eq!(map.max_size, (None, Some(200.0)));
2646 }
2647
2648 #[test]
2651 fn adjust_bounds_scales_with_zoom() {
2652 let mut map = Map::new();
2653 map.reference.min = RawPoint::new(-10.0, -20.0);
2654 map.reference.max = RawPoint::new(10.0, 20.0);
2655 map.reference.pos = RawPoint::new(5.0, 5.0);
2656 map.reference.dist = 100.0;
2657 map.set_zoom(2.0);
2658 map.adjust_bounds();
2659
2660 assert_eq!(map.current.max.components, [20.0, 40.0]);
2661 assert_eq!(map.current.min.components, [-20.0, -40.0]);
2662 assert_eq!(map.current.pos.components, [10.0, 10.0]);
2663 assert_eq!(map.current.dist, 50.0);
2664 }
2665
2666 #[test]
2669 fn theme_colors_are_resolved_live_from_the_installed_theme() {
2670 let mut map = Map::new();
2675 map.set_theme(Rc::new(Theme::ArticCyan));
2676
2677 let light = Theme::ArticCyan.colors(ColorMode::Light);
2678 let dark = Theme::ArticCyan.colors(ColorMode::Dark);
2679
2680 assert_eq!(map.current_index, 0, "Map::new starts in light mode");
2681 assert_eq!(map.theme_colors(), light);
2682
2683 map.current_index = 1;
2686 assert_eq!(map.theme_colors(), dark);
2687 }
2688
2689 #[test]
2690 fn a_custom_map_theme_reaches_the_painted_node() {
2691 use crate::map::theme::ThemeColors;
2695 use egui::{Context, RawInput, Shape};
2696
2697 struct FixedPalette;
2698 impl MapTheme for FixedPalette {
2699 fn colors(&self, _mode: ColorMode) -> ThemeColors {
2700 ThemeColors {
2701 node: Color32::from_rgb(1, 2, 3),
2702 segment: Color32::from_rgb(4, 5, 6),
2703 selected: Color32::from_rgb(7, 8, 9),
2704 alert: Color32::from_rgb(10, 11, 12),
2705 text: Color32::from_rgb(13, 14, 15),
2706 }
2707 }
2708 }
2709
2710 let mut map = Map::new();
2711 map.set_theme(Rc::new(FixedPalette));
2712 map.add_points(vec![MapPoint::new(1, [0.0, 0.0])]);
2713
2714 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(200.0, 200.0));
2715 let ctx = Context::default();
2716 let mut output = ctx.run_ui(
2717 RawInput {
2718 screen_rect: Some(screen),
2719 ..RawInput::default()
2720 },
2721 |ui| {
2722 ui.add(&mut map);
2723 },
2724 );
2725 output.textures_delta.clear();
2727
2728 let fill = output
2729 .shapes
2730 .iter()
2731 .find_map(|cs| match &cs.shape {
2732 Shape::Circle(circle) => Some(circle.fill),
2733 _ => None,
2734 })
2735 .expect("the node must be painted");
2736
2737 assert_eq!(
2738 fill,
2739 Color32::from_rgb(1, 2, 3),
2740 "the node fill must come from the installed MapTheme, in either color mode"
2741 );
2742 }
2743
2744 #[test]
2745 fn a_custom_map_theme_reaches_the_selection_highlight() {
2746 use crate::map::theme::ThemeColors;
2752 use egui::{Context, Event, RawInput};
2753 use std::cell::RefCell;
2754
2755 struct FixedPalette;
2756 impl MapTheme for FixedPalette {
2757 fn colors(&self, _mode: ColorMode) -> ThemeColors {
2758 ThemeColors {
2759 node: Color32::from_rgb(1, 2, 3),
2760 segment: Color32::from_rgb(4, 5, 6),
2761 selected: Color32::from_rgb(7, 8, 9),
2762 alert: Color32::from_rgb(10, 11, 12),
2763 text: Color32::from_rgb(13, 14, 15),
2764 }
2765 }
2766 }
2767
2768 #[derive(Default)]
2769 struct RecordingTemplate {
2770 seen: RefCell<Option<(usize, Color32)>>,
2771 }
2772 impl NodeTemplate for RecordingTemplate {
2773 fn node_ui(&self, _ui: &mut Ui, _ctx: NodeContext) {}
2774 fn selection_ui(&self, _ui: &mut Ui, ctx: SelectionContext) {
2775 *self.seen.borrow_mut() = Some((ctx.point.get_id(), ctx.color));
2776 }
2777 fn notification_ui(&self, _ui: &mut Ui, _ctx: NotificationContext) -> bool {
2778 false
2779 }
2780 fn marker_ui(&self, _ui: &mut Ui, _ctx: MarkerContext) {}
2781 }
2782
2783 let mut map = Map::new();
2784 map.set_theme(Rc::new(FixedPalette));
2785 map.settings.node_text_visibility = VisibilitySetting::Hover;
2786 map.add_points(vec![MapPoint::new(9, [0.0, 0.0])]);
2787 let template = Rc::new(RecordingTemplate::default());
2788 map.set_node_template(template.clone());
2789
2790 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(200.0, 200.0));
2791 let ctx = Context::default();
2792 for pass in 0..2 {
2796 let events = if pass == 1 {
2797 vec![Event::PointerMoved(screen.center())]
2798 } else {
2799 Vec::new()
2800 };
2801 let mut output = ctx.run_ui(
2802 RawInput {
2803 screen_rect: Some(screen),
2804 events,
2805 ..RawInput::default()
2806 },
2807 |ui| {
2808 ui.add(&mut map);
2809 },
2810 );
2811 output.textures_delta.clear();
2813 }
2814
2815 let (id, color) = template
2816 .seen
2817 .borrow()
2818 .expect("selection_ui must be called while the pointer hovers the map");
2819 assert_eq!(
2820 id, 9,
2821 "the highlighted node must be the one under the pointer"
2822 );
2823 assert_eq!(
2824 color,
2825 Color32::from_rgb(7, 8, 9),
2826 "the highlight color must come from the installed MapTheme's `selected`"
2827 );
2828 }
2829}