Skip to main content

egui_map/map/
objects.rs

1//! Data types consumed by the [`Map`](super::Map) widget.
2//!
3//! This module contains the geometry primitives ([`RawPoint`], [`RawLine`]),
4//! the map content types ([`MapPoint`], [`MapSegment`], [`MapLabel`]) and the
5//! customization points of the widget: [`MapSettings`],
6//! [`Style`], [`VisibilitySetting`],
7//! [`ContextMenuManager`] and [`NodeTemplate`]. The color palette a `Style`
8//! paints with lives in [`super::theme`], via [`MapTheme`](super::theme::MapTheme).
9
10use crate::map::theme::Style;
11use egui::{Align2, Color32, FontFamily, FontId, Painter, Pos2, Ui};
12use rstar::AABB;
13use std::convert::{From, Into};
14use std::ops::{Add, Div, DivAssign, Mul, MulAssign, Sub};
15use std::time::Instant;
16
17/// A point (or vector) in 2D map coordinates.
18///
19/// `RawPoint` supports component-wise arithmetic: [`Mul`], [`Div`],
20/// [`MulAssign`] and [`DivAssign`] with `f32` and the common integer types, and
21/// [`Add`]/[`Sub`] with other points (by value or by reference). It also
22/// converts from and to `[f32; 2]`, integer arrays and [`egui::Pos2`].
23///
24/// # Examples
25///
26/// ```
27/// use egui_map::map::objects::RawPoint;
28///
29/// let a = RawPoint::new(1.0, 2.0);
30/// let b = RawPoint::new(3.0, -1.0);
31///
32/// assert_eq!((a + b).components, [4.0, 1.0]);
33/// assert_eq!((a * 2.0f32).components, [2.0, 4.0]);
34/// ```
35#[derive(Copy, Clone, Debug, PartialEq)]
36pub struct RawPoint {
37    /// The `x` and `y` components of the point.
38    pub components: [f32; 2],
39}
40
41impl RawPoint {
42    /// Creates a point from its `x` and `y` coordinates.
43    pub fn new(x: f32, y: f32) -> Self {
44        Self { components: [x, y] }
45    }
46}
47
48impl rstar::Point for RawPoint {
49    type Scalar = f32;
50    const DIMENSIONS: usize = 2;
51
52    fn generate(mut generator: impl FnMut(usize) -> Self::Scalar) -> Self {
53        let mut components = [0.0; 2];
54        for (i, component) in components.iter_mut().enumerate() {
55            *component = generator(i);
56        }
57        Self { components }
58    }
59
60    fn nth(&self, index: usize) -> Self::Scalar {
61        self.components[index]
62    }
63
64    fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar {
65        &mut self.components[index]
66    }
67}
68
69impl Default for RawPoint {
70    fn default() -> Self {
71        Self::new(0.00, 0.00)
72    }
73}
74
75impl Mul<i64> for RawPoint {
76    type Output = Self;
77
78    fn mul(self, rhs: i64) -> Self::Output {
79        Self {
80            components: [
81                self.components[0] * rhs as f32,
82                self.components[1] * rhs as f32,
83            ],
84        }
85    }
86}
87
88impl Mul<i32> for RawPoint {
89    type Output = Self;
90
91    fn mul(self, rhs: i32) -> Self::Output {
92        Self {
93            components: [
94                self.components[0] * rhs as f32,
95                self.components[1] * rhs as f32,
96            ],
97        }
98    }
99}
100
101impl Mul<u64> for RawPoint {
102    type Output = Self;
103
104    fn mul(self, rhs: u64) -> Self::Output {
105        Self {
106            components: [
107                self.components[0] * rhs as f32,
108                self.components[1] * rhs as f32,
109            ],
110        }
111    }
112}
113
114impl Mul<u32> for RawPoint {
115    type Output = Self;
116
117    fn mul(self, rhs: u32) -> Self::Output {
118        Self {
119            components: [
120                self.components[0] * rhs as f32,
121                self.components[1] * rhs as f32,
122            ],
123        }
124    }
125}
126
127impl Mul<f32> for RawPoint {
128    type Output = Self;
129
130    fn mul(self, rhs: f32) -> Self::Output {
131        Self {
132            components: [self.components[0] * rhs, self.components[1] * rhs],
133        }
134    }
135}
136
137impl MulAssign<i64> for RawPoint {
138    fn mul_assign(&mut self, rhs: i64) {
139        self.components[0] = self.components[0] * rhs as f32;
140        self.components[1] = self.components[1] * rhs as f32;
141    }
142}
143
144impl MulAssign<i32> for RawPoint {
145    fn mul_assign(&mut self, rhs: i32) {
146        self.components[0] = self.components[0] * rhs as f32;
147        self.components[1] = self.components[1] * rhs as f32;
148    }
149}
150
151impl MulAssign<u64> for RawPoint {
152    fn mul_assign(&mut self, rhs: u64) {
153        self.components[0] = self.components[0] * rhs as f32;
154        self.components[1] = self.components[1] * rhs as f32;
155    }
156}
157
158impl MulAssign<u32> for RawPoint {
159    fn mul_assign(&mut self, rhs: u32) {
160        self.components[0] = self.components[0] * rhs as f32;
161        self.components[1] = self.components[1] * rhs as f32;
162    }
163}
164
165impl MulAssign<f32> for RawPoint {
166    fn mul_assign(&mut self, rhs: f32) {
167        self.components[0] = self.components[0] * rhs;
168        self.components[1] = self.components[1] * rhs;
169    }
170}
171
172impl Div<i64> for RawPoint {
173    type Output = Self;
174
175    fn div(self, rhs: i64) -> Self::Output {
176        Self {
177            components: [
178                self.components[0] / rhs as f32,
179                self.components[1] / rhs as f32,
180            ],
181        }
182    }
183}
184
185impl Div<i32> for RawPoint {
186    type Output = Self;
187
188    fn div(self, rhs: i32) -> Self::Output {
189        Self {
190            components: [
191                self.components[0] / rhs as f32,
192                self.components[1] / rhs as f32,
193            ],
194        }
195    }
196}
197
198impl Div<u64> for RawPoint {
199    type Output = Self;
200
201    fn div(self, rhs: u64) -> Self::Output {
202        Self {
203            components: [
204                self.components[0] / rhs as f32,
205                self.components[1] / rhs as f32,
206            ],
207        }
208    }
209}
210
211impl Div<u32> for RawPoint {
212    type Output = Self;
213
214    fn div(self, rhs: u32) -> Self::Output {
215        Self {
216            components: [
217                self.components[0] / rhs as f32,
218                self.components[1] / rhs as f32,
219            ],
220        }
221    }
222}
223
224impl Div<f32> for RawPoint {
225    type Output = Self;
226
227    fn div(self, rhs: f32) -> Self::Output {
228        Self {
229            components: [self.components[0] / rhs, self.components[1] / rhs],
230        }
231    }
232}
233
234impl DivAssign<i64> for RawPoint {
235    fn div_assign(&mut self, rhs: i64) {
236        self.components[0] = self.components[0] / rhs as f32;
237        self.components[1] = self.components[1] / rhs as f32;
238    }
239}
240
241impl DivAssign<i32> for RawPoint {
242    fn div_assign(&mut self, rhs: i32) {
243        self.components[0] = self.components[0] / rhs as f32;
244        self.components[1] = self.components[1] / rhs as f32;
245    }
246}
247
248impl DivAssign<u64> for RawPoint {
249    fn div_assign(&mut self, rhs: u64) {
250        self.components[0] = self.components[0] / rhs as f32;
251        self.components[1] = self.components[1] / rhs as f32;
252    }
253}
254
255impl DivAssign<u32> for RawPoint {
256    fn div_assign(&mut self, rhs: u32) {
257        self.components[0] = self.components[0] / rhs as f32;
258        self.components[1] = self.components[1] / rhs as f32;
259    }
260}
261
262impl DivAssign<f32> for RawPoint {
263    fn div_assign(&mut self, rhs: f32) {
264        self.components[0] = self.components[0] / rhs;
265        self.components[1] = self.components[1] / rhs;
266    }
267}
268
269impl Add<RawPoint> for RawPoint {
270    type Output = RawPoint;
271    fn add(self, rhs: RawPoint) -> Self::Output {
272        Self {
273            components: [
274                self.components[0] + rhs.components[0],
275                self.components[1] + rhs.components[1],
276            ],
277        }
278    }
279}
280
281impl Sub<RawPoint> for RawPoint {
282    type Output = RawPoint;
283    fn sub(self, rhs: RawPoint) -> Self::Output {
284        Self {
285            components: [
286                self.components[0] - rhs.components[0],
287                self.components[1] - rhs.components[1],
288            ],
289        }
290    }
291}
292
293impl Add<&RawPoint> for RawPoint {
294    type Output = RawPoint;
295    fn add(self, rhs: &RawPoint) -> Self::Output {
296        Self {
297            components: [
298                self.components[0] + rhs.components[0],
299                self.components[1] + rhs.components[1],
300            ],
301        }
302    }
303}
304
305impl Sub<&RawPoint> for RawPoint {
306    type Output = RawPoint;
307    fn sub(self, rhs: &RawPoint) -> Self::Output {
308        Self {
309            components: [
310                self.components[0] - rhs.components[0],
311                self.components[1] - rhs.components[1],
312            ],
313        }
314    }
315}
316
317impl From<[f32; 2]> for RawPoint {
318    fn from(value: [f32; 2]) -> Self {
319        Self { components: value }
320    }
321}
322
323impl From<Pos2> for RawPoint {
324    fn from(value: Pos2) -> Self {
325        Self {
326            components: [value.x, value.y],
327        }
328    }
329}
330
331impl From<[i64; 2]> for RawPoint {
332    fn from(value: [i64; 2]) -> Self {
333        Self {
334            components: [value[0] as f32, value[1] as f32],
335        }
336    }
337}
338
339impl From<[i32; 2]> for RawPoint {
340    fn from(value: [i32; 2]) -> Self {
341        Self {
342            components: [value[0] as f32, value[1] as f32],
343        }
344    }
345}
346
347impl From<[i16; 2]> for RawPoint {
348    fn from(value: [i16; 2]) -> Self {
349        Self {
350            components: [value[0] as f32, value[1] as f32],
351        }
352    }
353}
354
355impl From<[i8; 2]> for RawPoint {
356    fn from(value: [i8; 2]) -> Self {
357        Self {
358            components: [value[0] as f32, value[1] as f32],
359        }
360    }
361}
362
363impl From<RawPoint> for [f32; 2] {
364    fn from(val: RawPoint) -> Self {
365        [val.components[0], val.components[1]]
366    }
367}
368
369impl From<RawPoint> for Pos2 {
370    fn from(val: RawPoint) -> Self {
371        Pos2::from(val.components)
372    }
373}
374
375/// A straight line segment between two [`RawPoint`]s.
376#[derive(Copy, Clone, Debug)]
377pub struct RawLine {
378    /// The two end points of the segment.
379    pub points: [RawPoint; 2],
380}
381
382impl RawLine {
383    /// Creates a segment between `a` and `b`.
384    pub fn new(a: RawPoint, b: RawPoint) -> Self {
385        Self { points: [a, b] }
386    }
387
388    /// Returns the Euclidean distance between the two end points.
389    pub fn distance(self) -> f32 {
390        let x = self.points[0].components[0] - self.points[1].components[0];
391        let y = self.points[0].components[1] - self.points[1].components[1];
392        (x.powi(2) + y.powi(2)).sqrt()
393    }
394
395    /// Returns the point halfway between the two end points.
396    pub fn midpoint(self) -> RawPoint {
397        let x = (self.points[0].components[0] + self.points[1].components[0]) / 2.0;
398        let y = (self.points[0].components[1] + self.points[1].components[1]) / 2.0;
399        RawPoint::new(x, y)
400    }
401
402    /// Returns the Euclidean distance from `point` to the closest point on
403    /// this segment.
404    ///
405    /// The closest point is the perpendicular projection of `point` onto the
406    /// segment's supporting line, clamped to the segment itself; for a
407    /// zero-length segment it is simply the distance to the endpoint.
408    ///
409    /// # Examples
410    ///
411    /// ```
412    /// use egui_map::map::objects::{RawLine, RawPoint};
413    ///
414    /// let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
415    /// assert_eq!(line.distance_to_point(RawPoint::new(5.0, 3.0)), 3.0);
416    /// // Beyond the end of the segment, the endpoint is the closest point.
417    /// assert_eq!(line.distance_to_point(RawPoint::new(14.0, 0.0)), 4.0);
418    /// ```
419    pub fn distance_to_point(self, point: RawPoint) -> f32 {
420        let [a, b] = self.points;
421        let ab = b - a;
422        let ap = point - a;
423        let len_sq = ab.components[0].powi(2) + ab.components[1].powi(2);
424        if len_sq == 0.0 {
425            // Degenerate segment: both endpoints coincide.
426            return (ap.components[0].powi(2) + ap.components[1].powi(2)).sqrt();
427        }
428        let t = ((ap.components[0] * ab.components[0] + ap.components[1] * ab.components[1])
429            / len_sq)
430            .clamp(0.0, 1.0);
431        let closest = a + ab * t;
432        let d = point - closest;
433        (d.components[0].powi(2) + d.components[1].powi(2)).sqrt()
434    }
435}
436
437impl From<RawLine> for [Pos2; 2] {
438    fn from(val: RawLine) -> Self {
439        let position1 = val.points[0].into();
440        let position2 = val.points[1].into();
441        [position1, position2]
442    }
443}
444
445impl From<[[i64; 2]; 2]> for RawLine {
446    fn from(value: [[i64; 2]; 2]) -> Self {
447        Self {
448            points: [RawPoint::from(value[0]), RawPoint::from(value[1])],
449        }
450    }
451}
452
453/// A free-floating text label drawn on the map.
454///
455/// Labels are installed with [`Map::add_labels`](super::Map::add_labels).
456#[derive(Clone, Debug)]
457pub struct MapLabel {
458    /// The text to display.
459    pub text: String,
460    /// The position of the label's center.
461    pub center: Pos2,
462}
463
464impl Default for MapLabel {
465    fn default() -> Self {
466        MapLabel::new()
467    }
468}
469
470impl MapLabel {
471    /// Creates an empty label centered at the origin.
472    pub fn new() -> Self {
473        MapLabel {
474            text: String::new(),
475            center: Pos2::new(0.00, 0.00),
476        }
477    }
478}
479
480/// A connection line between two points on the map, ready to be stored in an
481/// [`rstar::RTree`].
482///
483/// Mirrors `sde::objects::SdeSegment`'s shape (`id`, `point1`, `point2`) so
484/// callers that already hold `sde` connection data can build one with a
485/// straight field-for-field copy; the only structural difference is `f32`
486/// instead of `f64` for the coordinates, matching `egui`'s own coordinate
487/// type (`egui` — and therefore this widget — doesn't work in `f64`).
488///
489/// Lines are installed with [`Map::add_hashmap_lines`](super::Map::add_hashmap_lines),
490/// keyed by an id that nodes reference through [`MapPoint::connections`].
491/// The bounding box the R-tree uses for broad-phase viewport culling and
492/// hit-testing is computed on demand from `point1`/`point2` in
493/// [`envelope`](rstar::RTreeObject::envelope) rather than cached on the
494/// struct, same as `SdeSegment`.
495#[derive(Clone, Copy, Debug, PartialEq)]
496pub struct MapSegment {
497    /// Identifier shared with the line key (and with the
498    /// [`MapPoint::connections`] of the endpoint nodes).
499    pub id: (usize, usize),
500    /// One endpoint of the segment, in map coordinates.
501    pub point1: [f32; 2],
502    /// The other endpoint of the segment, in map coordinates.
503    pub point2: [f32; 2],
504}
505
506impl MapSegment {
507    /// Creates a segment for `id` between `point1` and `point2`.
508    pub fn new(id: (usize, usize), point1: [f32; 2], point2: [f32; 2]) -> Self {
509        Self { id, point1, point2 }
510    }
511
512    /// The segment geometry as a [`RawLine`], for the distance/midpoint math
513    /// callers already get from that type.
514    pub(crate) fn raw_line(&self) -> RawLine {
515        RawLine::new(RawPoint::from(self.point1), RawPoint::from(self.point2))
516    }
517}
518
519impl rstar::RTreeObject for MapSegment {
520    type Envelope = AABB<[f32; 2]>;
521
522    fn envelope(&self) -> Self::Envelope {
523        AABB::from_corners(
524            [
525                self.point1[0].min(self.point2[0]),
526                self.point1[1].min(self.point2[1]),
527            ],
528            [
529                self.point1[0].max(self.point2[0]),
530                self.point1[1].max(self.point2[1]),
531            ],
532        )
533    }
534}
535
536/// A node on the map: an id, a 2D position and an optional display name.
537///
538/// Mirrors `sde::objects::SdePoint`'s shape (public `coords`/`id`/`name`/
539/// `connections` fields) so callers that already hold `sde` map-query data
540/// can build one with a straight field-for-field copy. Structural
541/// differences from `SdePoint`:
542///
543/// - `f32` instead of `f64` for `coords`, matching `egui`'s own coordinate
544///   type.
545/// - 2 components instead of 3: this widget only ever renders a 2D map, so
546///   there's no third axis to carry.
547/// - `id` is a plain `usize`, not `Option<usize>`. `SdePoint` uses `None`
548///   for a bare coordinate with no entity behind it (e.g. a bounding-box
549///   corner); every `MapPoint` loaded into the widget represents a real,
550///   placed node whose id is used directly as the point-set `HashMap` key
551///   and the kd-tree payload (see
552///   [`Map::add_hashmap_points`](super::Map::add_hashmap_points)), so an
553///   optional id would just push an `.unwrap()` (or a silently dropped
554///   node) into those call sites with no caller ever passing `None`.
555///
556/// Nodes are loaded into the widget through
557/// [`Map::add_hashmap_points`](super::Map::add_hashmap_points), keyed by their
558/// id.
559#[derive(Clone, Debug, PartialEq)]
560pub struct MapPoint {
561    /// Position of the node, in map coordinates.
562    pub coords: [f32; 2],
563    /// Node identifier, used for lookups, notifications and markers.
564    pub id: usize,
565    /// Display name shown next to the node; `None` if it was never set.
566    pub name: Option<String>,
567    /// Ids of the lines connecting this node with others.
568    ///
569    /// Each entry must match a key of the map passed to
570    /// [`Map::add_hashmap_lines`](super::Map::add_hashmap_lines) (and
571    /// [`MapSegment::id`]). The usual pattern is to push the same pair into
572    /// the `connections` of **both** endpoint nodes. Line visibility is
573    /// computed from the segment bounding boxes (R-tree), not from node
574    /// visibility, so a line is drawn whenever its bounding box intersects
575    /// the viewport.
576    pub connections: Vec<(usize, usize)>,
577    /// Persistent color override for this node's default circle. `None`
578    /// (the default) falls back to the active
579    /// [`MapTheme`](super::theme::MapTheme)'s
580    /// [`ThemeColors::node`](super::theme::ThemeColors::node) for the
581    /// current color mode -- see [`Map::set_theme`](super::Map::set_theme).
582    ///
583    /// Only consulted by the built-in circle drawn when no
584    /// [`NodeTemplate`] is installed -- a custom template receives this
585    /// same `MapPoint` via [`NodeContext::point`], with
586    /// [`NodeContext::color`] already carrying the resolved fallback.
587    pub color: Option<Color32>,
588}
589
590impl MapPoint {
591    /// Creates a node with the given `id` at the given map coordinates.
592    pub fn new(id: usize, coords: [f32; 2]) -> MapPoint {
593        MapPoint {
594            coords,
595            id,
596            connections: Vec::new(),
597            name: None,
598            color: None,
599        }
600    }
601
602    /// Returns the node identifier.
603    pub fn get_id(&self) -> usize {
604        self.id
605    }
606
607    /// Returns the node display name (empty if it was never set).
608    pub fn get_name(&self) -> String {
609        self.name.clone().unwrap_or_default()
610    }
611
612    /// Sets the node display name.
613    pub fn set_name(&mut self, value: String) {
614        self.name = Some(value);
615    }
616}
617
618impl From<std::collections::hash_map::OccupiedEntry<'_, usize, MapPoint>> for MapPoint {
619    fn from(value: std::collections::hash_map::OccupiedEntry<'_, usize, MapPoint>) -> Self {
620        let k = value.get();
621        k.clone()
622    }
623}
624
625#[derive(Clone)]
626pub(crate) struct MapBounds {
627    pub min: RawPoint,
628    pub max: RawPoint,
629    pub pos: RawPoint,
630    pub dist: f32,
631}
632
633impl MapBounds {
634    pub fn new() -> Self {
635        MapBounds {
636            min: RawPoint::default(),
637            max: RawPoint::default(),
638            pos: RawPoint::default(),
639            dist: 0.0,
640        }
641    }
642}
643
644impl Default for MapBounds {
645    fn default() -> Self {
646        MapBounds::new()
647    }
648}
649
650pub(crate) struct TextSettings {
651    pub position: RawPoint,
652    pub anchor: Align2,
653    pub text: String,
654    pub size: f32,
655    pub family: FontFamily,
656    pub text_color: Color32,
657}
658
659/// Configuration of a [`Map`](super::Map) widget.
660///
661/// [`MapSettings::default()`] provides sensible zoom limits plus a light and a
662/// dark theme; the widget picks the style to apply based on
663/// [`egui::Visuals::dark_mode`], using `styles[0]` in light mode and
664/// `styles[1]` in dark mode.
665#[derive(Clone, Debug)]
666pub struct MapSettings {
667    /// Maximum zoom factor.
668    pub max_zoom: f32,
669    /// Minimum zoom factor.
670    pub min_zoom: f32,
671    /// Zoom threshold above which connection lines become visible.
672    pub line_visible_zoom: f32,
673    /// Zoom threshold above which node names become visible when
674    /// [`node_text_visibility`](Self::node_text_visibility) is
675    /// [`VisibilitySetting::Always`].
676    pub label_visible_zoom: f32,
677    /// Controls when node names are displayed.
678    pub node_text_visibility: VisibilitySetting,
679    /// Effect drawn on nodes registered with
680    /// [`Map::update_marker`](super::Map::update_marker).
681    ///
682    /// Persistent, so it keeps the app repainting for as long as a marker
683    /// exists. Ignored when a [`NodeTemplate`] is installed.
684    ///
685    /// Node *state* set through [`NodeHandle`](super::NodeHandle) picks its own
686    /// effect per node and does not read this field.
687    pub marker_animation: SteadyAnimation,
688    /// Font size, **in screen pixels**, of the node names.
689    ///
690    /// This is a screen-space size: it deliberately does *not* scale with the
691    /// zoom factor, so a name stays exactly as readable when the map is zoomed
692    /// all the way out as when it is zoomed in. Because the nodes pack closer
693    /// together as you zoom out while the names keep their size, names take up
694    /// proportionally more of the view down there — use
695    /// [`label_visible_zoom`](Self::label_visible_zoom) or
696    /// [`node_text_visibility`](Self::node_text_visibility) to control when
697    /// they are worth showing at all.
698    pub node_text_size: f32,
699    /// Font size, **in screen pixels**, of the free-floating [`MapLabel`]s.
700    ///
701    /// Screen-space, exactly like [`node_text_size`](Self::node_text_size).
702    pub label_text_size: f32,
703    /// Per-mode styles; index `0` is used in light mode, index `1` in dark
704    /// mode. Their colors are kept in sync with the active
705    /// [`MapTheme`](super::theme::MapTheme) -- see
706    /// [`Map::set_theme`](super::Map::set_theme) -- rather than set here.
707    pub styles: Vec<Style>,
708}
709
710impl MapSettings {
711    /// Creates settings with all zoom thresholds set to `0.0` and a single
712    /// transparent style.
713    ///
714    /// Prefer [`MapSettings::default()`] unless you really need to build the
715    /// configuration from scratch.
716    pub fn new() -> Self {
717        MapSettings {
718            max_zoom: 0.0,
719            min_zoom: 0.0,
720            line_visible_zoom: 0.0,
721            label_visible_zoom: 0.0,
722            node_text_visibility: VisibilitySetting::Always,
723            marker_animation: SteadyAnimation::Blink,
724            node_text_size: 12.0,
725            label_text_size: 24.0,
726            styles: vec![Style::new()],
727        }
728    }
729}
730
731impl Default for MapSettings {
732    /// Returns the default configuration: zoom from `0.1` to `2.0`, connection
733    /// lines visible above `0.2`, node names above `0.58`, and built-in light
734    /// and dark themes.
735    fn default() -> Self {
736        let mut obj = MapSettings {
737            max_zoom: 2.0,
738            min_zoom: 0.1,
739            line_visible_zoom: 0.2,
740            label_visible_zoom: 0.58,
741            node_text_visibility: VisibilitySetting::Always,
742            marker_animation: SteadyAnimation::Blink,
743            node_text_size: 12.0,
744            label_text_size: 24.0,
745            styles: Vec::new(),
746        };
747
748        // The background color below is a placeholder, overwritten by
749        // `Map::assign_visual_style` from egui's own visuals on the first
750        // frame. `Style` carries no color of its own -- every color the
751        // widget paints with comes live from the default `MapTheme` (see
752        // `Map::set_theme`/`Map::theme_colors`), so there is nothing here to
753        // keep in sync with a `Theme`.
754
755        // light style
756        obj.styles.push(Style {
757            line_width: Some(2.0),
758            font: Some(FontId::new(12.00, FontFamily::Proportional)),
759            background_color: Color32::WHITE,
760        });
761
762        // dark style
763        obj.styles.push(Style {
764            line_width: Some(2.0),
765            font: Some(FontId::new(12.00, FontFamily::Proportional)),
766            background_color: Color32::DARK_GRAY,
767        });
768        obj
769    }
770}
771
772/// A built-in effect that plays once and ends.
773///
774/// Anchored to the [`Instant`] an event happened, these are the animations
775/// reached through [`NodeHandle`](super::NodeHandle): `map.node(id)?.ripple(t)`.
776/// The widget drops the notification and stops repainting once the effect
777/// finishes. See [`crate::map::animation`] for what each looks like and how
778/// long it runs.
779///
780/// Ignored when a [`NodeTemplate`] is installed — the template's
781/// `notification_ui` takes over. The effects stay reachable there through
782/// [`Animation`](crate::map::animation::Animation).
783#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
784pub enum NodeAnimation {
785    /// Expanding, fading disc. Reads as "one thing happened here".
786    #[default]
787    Pulse,
788    /// Three staggered expanding rings. Reads as "activity is ongoing".
789    Ripple,
790    /// A ring that empties clockwise. Reads as "how old is this information".
791    CountdownArc,
792    /// A disc that overshoots its size and settles. For nodes that just appeared.
793    ScaleIn,
794    /// Four ticks converging on the node. Reads as "target acquired".
795    Crosshair,
796}
797
798/// A built-in effect that runs until it is cleared.
799///
800/// Named after how long it lasts rather than after who uses it, because it has
801/// two consumers: node state set through [`NodeHandle`](super::NodeHandle)
802/// (`map.node(id)?.halo()`), and markers registered with
803/// [`Map::update_marker`](super::Map::update_marker), which pick their look
804/// with [`MapSettings::marker_animation`].
805///
806/// These never end, so the widget keeps requesting repaints for as long as one
807/// is active. That is fine for the handful of elements they are meant for, but
808/// it does keep the app redrawing — see [`crate::map::animation`].
809#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
810pub enum SteadyAnimation {
811    /// Thick ring blinking on and off. The long-standing marker look.
812    #[default]
813    Blink,
814    /// Ring whose opacity breathes in and out. Calmer than [`Self::Blink`].
815    Halo,
816    /// A dot circling the node. Reads as "under observation".
817    Orbit,
818}
819
820/// Which endpoint a [`SegmentAnimation::Comet`] pass starts from.
821///
822/// A segment's own endpoint order (`a`, `b` as loaded through
823/// [`Map::add_lines`](super::Map::add_lines)) is not usually meaningful to a
824/// caller — naming the two ends [`Self::Forward`]/[`Self::Reverse`] instead
825/// keeps the choice about the animation's direction, not about internal
826/// storage order.
827#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
828pub enum CometDirection {
829    /// From the segment's first endpoint to its second.
830    #[default]
831    Forward,
832    /// From the segment's second endpoint to its first.
833    Reverse,
834}
835
836/// A built-in effect that plays once and ends, for a segment.
837///
838/// Anchored to the [`Instant`] an event happened, these are the animations
839/// reached through [`SegmentHandle`](super::SegmentHandle):
840/// `map.segment(id)?.flash(t)`. The widget drops the notification and stops
841/// repainting once the effect finishes. See [`crate::map::animation`] for
842/// what each looks like and how long it runs.
843///
844/// Ignored when a [`SegmentTemplate`] is installed — the template's
845/// `segment_notification_ui` takes over. The effect stays reachable there
846/// through [`Animation`](crate::map::animation::Animation).
847#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
848pub enum SegmentAnimation {
849    /// A brief bright flash on the line that fades back out. The segment
850    /// analogue of [`NodeAnimation::Pulse`] — reads as "something happened on
851    /// this route".
852    #[default]
853    FlashDecay,
854    /// A single dot pass from one endpoint to the other, then gone — the
855    /// event-driven counterpart to [`SteadySegmentAnimation::Comet`]. Reads
856    /// as "one thing moved along this route just now", direction included,
857    /// rather than "traffic keeps flowing this way".
858    Comet(CometDirection),
859    /// The line drawing itself in from the first endpoint to the second,
860    /// then gone. Reads as "this route was just established" rather than
861    /// "something travelled along it".
862    Wipe,
863}
864
865/// A built-in effect that runs until it is cleared, for a segment.
866///
867/// Reached through node state set on [`SegmentHandle`](super::SegmentHandle)
868/// (`map.segment(id)?.comet()` / `.dash()`). Like [`SteadyAnimation`], these
869/// never end, so the widget keeps requesting repaints for as long as one is
870/// active — fine for a handful of highlighted routes, not for every segment
871/// on the map.
872#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
873pub enum SteadySegmentAnimation {
874    /// A dot travelling from one endpoint to the other and looping. Reads as
875    /// "this is the direction of flow".
876    #[default]
877    Comet,
878    /// A dashed line whose pattern slides along the segment ("marching
879    /// ants"). Reads as "route", classic for a path someone might follow.
880    Dash,
881    /// A localized band of brightness travelling the length of the segment
882    /// and looping, fading out before it reaches either end rather than
883    /// snapping back. Reads as "flow", softer and less busy than [`Self::Dash`].
884    GlowBand,
885    /// A row of arrow shapes sliding along the segment, pointing the way.
886    /// Reads as "direction of travel", more explicit than [`Self::Comet`]'s
887    /// single dot.
888    Chevrons,
889}
890
891/// Controls when the name of a node is displayed next to it.
892#[derive(Clone, Debug, PartialEq)]
893pub enum VisibilitySetting {
894    /// Never show node names.
895    Hidden,
896    /// Only show the name of the node closest to the mouse pointer.
897    Hover,
898    /// Always show node names, subject to [`MapSettings::label_visible_zoom`].
899    Always,
900}
901
902/// Provides the contents of the widget's right-click context menu.
903///
904/// Install an implementation with
905/// [`Map::set_context_manager`](super::Map::set_context_manager).
906///
907/// # Examples
908///
909/// ```
910/// use egui_map::map::objects::ContextMenuManager;
911///
912/// struct MyMenu;
913///
914/// impl ContextMenuManager for MyMenu {
915///     fn ui(&self, ui: &mut egui::Ui) {
916///         ui.label("Hello from the map!");
917///     }
918/// }
919/// ```
920pub trait ContextMenuManager {
921    /// Builds the menu contents; called every frame while the menu is open.
922    fn ui(&self, ui: &mut Ui);
923}
924
925/// Customizes how nodes and their visual effects are rendered.
926///
927/// When a template is installed with
928/// [`Map::set_node_template`](super::Map::set_node_template), the widget
929/// delegates all node painting to it instead of using the built-in shapes and
930/// animations — including the node name labels, so draw the name yourself in
931/// [`NodeTemplate::node_ui`] if you need it.
932///
933/// The positions passed to these methods are in screen coordinates: already
934/// scaled by `zoom` and translated to the viewport origin. Multiply every size
935/// by `zoom` so your shapes scale together with the map.
936///
937/// # Animation idioms
938///
939/// egui only repaints on demand, so any method that animates (a blinking
940/// marker, a fading notification, ...) must call
941/// [`ui.ctx().request_repaint()`](egui::Context::request_repaint) to keep the
942/// frames coming. Time-driven effects are usually computed from
943/// [`Instant::now()`] (see `initial_time` in
944/// [`NodeTemplate::notification_ui`]) or from the system clock.
945///
946/// # Examples
947///
948/// A node drawn as a rounded box with its name inside, plus a notification
949/// animation that expands and fades out over two seconds:
950///
951/// ```
952/// use egui_map::map::objects::{MapPoint, NodeContext, NodeTemplate, NotificationContext, MarkerContext, SelectionContext};
953/// use egui::{Align2, Color32, CornerRadius, FontId, Pos2, Rect, Stroke, Ui, Vec2};
954/// use std::time::Instant;
955///
956/// struct BoxedNodes;
957///
958/// impl NodeTemplate for BoxedNodes {
959///     fn node_ui(&self, ui: &mut Ui, ctx: NodeContext) {
960///         // Multiply every size by `ctx.zoom` so the node scales with the map.
961///         let rect = Rect::from_center_size(ctx.position, Vec2::new(90.0 * ctx.zoom, 35.0 * ctx.zoom));
962///         let rounding = CornerRadius::same((10.0 * ctx.zoom) as u8);
963///         let painter = ui.painter();
964///         // `ctx.color` is already resolved: `ctx.point.color` if the node has
965///         // its own override, otherwise the active theme's node color.
966///         painter.rect_filled(rect, rounding, ctx.color);
967///         painter.rect_stroke(
968///             rect,
969///             rounding,
970///             Stroke::new(4.0 * ctx.zoom, Color32::WHITE),
971///             egui::StrokeKind::Middle,
972///         );
973///         painter.text(
974///             ctx.position,
975///             Align2::CENTER_CENTER,
976///             ctx.point.get_name(),
977///             FontId::proportional(12.0 * ctx.zoom),
978///             Color32::WHITE,
979///         );
980///     }
981///
982///     fn notification_ui(&self, ui: &mut Ui, ctx: NotificationContext) -> bool {
983///         let secs = Instant::now().duration_since(ctx.initial_time).as_secs_f32();
984///         // Expand the stroke and fade the color out over 2 seconds.
985///         let alpha = (1.0 - secs / 2.0).clamp(0.0, 1.0);
986///         let fading = Color32::from_rgba_unmultiplied(
987///             ctx.color.r(),
988///             ctx.color.g(),
989///             ctx.color.b(),
990///             (255.0 * alpha) as u8,
991///         );
992///         let rect = Rect::from_center_size(ctx.position, Vec2::new(90.0 * ctx.zoom, 35.0 * ctx.zoom));
993///         ui.painter().rect_stroke(
994///             rect,
995///             CornerRadius::same((10.0 * ctx.zoom) as u8),
996///             Stroke::new((4.0 + 25.0 * secs) * ctx.zoom, fading),
997///             egui::StrokeKind::Middle,
998///         );
999///         // Keep the animation frames coming.
1000///         ui.ctx().request_repaint();
1001///         // Returning `false` removes the notification.
1002///         secs < 2.0
1003///     }
1004///     # fn selection_ui(&self, ui: &mut Ui, ctx: SelectionContext) {
1005///     #     let rect = Rect::from_center_size(ctx.position, Vec2::new(94.0 * ctx.zoom, 39.0 * ctx.zoom));
1006///     #     ui.painter().rect_stroke(
1007///     #         rect,
1008///     #         CornerRadius::same((10.0 * ctx.zoom) as u8),
1009///     #         Stroke::new(3.0 * ctx.zoom, ctx.color),
1010///     #         egui::StrokeKind::Middle,
1011///     #     );
1012///     # }
1013///     # fn marker_ui(&self, ui: &mut Ui, ctx: MarkerContext) {
1014///     #     ui.painter().circle_stroke(ctx.position, 6.0 * ctx.zoom, Stroke::new(2.0 * ctx.zoom, Color32::LIGHT_GREEN));
1015///     #     ui.ctx().request_repaint();
1016///     # }
1017/// }
1018/// ```
1019///
1020/// # Note on `NodeAnimation`/`SteadyAnimation` in the examples above
1021///
1022/// Every method here takes a context struct -- [`NodeContext`],
1023/// [`SelectionContext`], [`NotificationContext`] or [`MarkerContext`] -- each
1024/// `#[non_exhaustive]` so a future field can be added without another
1025/// breaking change to `NodeTemplate` itself.
1026pub trait NodeTemplate {
1027    /// Draws a node, replacing the default filled circle.
1028    ///
1029    /// Called every frame for each visible node. The widget no longer draws
1030    /// the node name once a template is installed, so render it here (e.g.
1031    /// with [`Painter::text`](egui::Painter::text)) if you need it. See
1032    /// [`NodeContext`] for the fields available, in particular `ctx.color`
1033    /// -- the color already resolved for this node, so you don't have to
1034    /// repeat the `point.color.unwrap_or(...)` fallback (or reach for the
1035    /// active theme yourself) to honor a per-node color override.
1036    fn node_ui(&self, ui: &mut Ui, ctx: NodeContext);
1037
1038    /// Draws the highlight over the node closest to the mouse pointer.
1039    ///
1040    /// The nearest node is only computed while the pointer is over the map and
1041    /// [`MapSettings::node_text_visibility`] is [`VisibilitySetting::Hover`].
1042    /// See [`SelectionContext`] for the fields available, in particular
1043    /// `ctx.point` (which node is being highlighted) and `ctx.color` (the
1044    /// active theme's selection color, resolved for you).
1045    fn selection_ui(&self, ui: &mut Ui, ctx: SelectionContext);
1046
1047    /// Draws the notification effect of a node notified at
1048    /// `ctx.initial_time`.
1049    ///
1050    /// Called every frame for each node passed to
1051    /// [`Map::notify`](super::Map::notify) or animated through
1052    /// [`Map::node`](super::Map::node)'s event methods (`pulse`, `ripple`,
1053    /// ...). `ctx.kind` is which of those was requested and `ctx.node_id` is
1054    /// the id of the node it belongs to -- use them to dispatch to the
1055    /// matching built-in [`Animation`](crate::map::animation::Animation)
1056    /// function (or your own effect) instead of reimplementing every
1057    /// animation by hand. See [`NotificationContext`] for the rest of the
1058    /// fields. Should return `true` while the animation is still playing —
1059    /// remember to call
1060    /// [`ui.ctx().request_repaint()`](egui::Context::request_repaint) —;
1061    /// once it returns `false` the notification is discarded.
1062    fn notification_ui(&self, ui: &mut Ui, ctx: NotificationContext) -> bool;
1063
1064    /// Draws a marker over the given node.
1065    ///
1066    /// Called every frame for two different things -- see [`MarkerContext`]
1067    /// for what `ctx.kind`/`ctx.node_id` mean in each case. For animated
1068    /// markers (e.g. a blinking light), drive the effect from the system
1069    /// clock and call
1070    /// [`ui.ctx().request_repaint()`](egui::Context::request_repaint).
1071    fn marker_ui(&self, ui: &mut Ui, ctx: MarkerContext);
1072}
1073
1074/// The context passed to [`NodeTemplate::node_ui`].
1075///
1076/// `#[non_exhaustive]`, like [`NotificationContext`]/[`MarkerContext`], so a
1077/// future field can be added here without another breaking change.
1078#[derive(Clone, Copy, Debug)]
1079#[non_exhaustive]
1080pub struct NodeContext<'a> {
1081    /// The node's screen position: already scaled by `zoom` and translated
1082    /// to the viewport origin.
1083    pub position: Pos2,
1084    /// Multiply every size you draw by this so it scales with the map.
1085    pub zoom: f32,
1086    /// The node being painted -- its id, name, coordinates and connections.
1087    pub point: &'a MapPoint,
1088    /// The color requested for this node: [`point.color`](MapPoint::color)
1089    /// if the node has its own override, otherwise the active
1090    /// [`MapTheme`](super::theme::MapTheme)'s
1091    /// [`ThemeColors::node`](super::theme::ThemeColors::node) for the
1092    /// current color mode -- the same fallback the built-in circle uses when
1093    /// no template is installed, resolved once here so every `NodeTemplate`
1094    /// doesn't need to repeat it.
1095    pub color: Color32,
1096}
1097
1098/// The context passed to [`NodeTemplate::selection_ui`].
1099///
1100/// `#[non_exhaustive]`, like [`NodeContext`]/[`NotificationContext`]/
1101/// [`MarkerContext`], so a future field can be added here without another
1102/// breaking change.
1103#[derive(Clone, Copy, Debug)]
1104#[non_exhaustive]
1105pub struct SelectionContext<'a> {
1106    /// The node's screen position: already scaled by `zoom` and translated
1107    /// to the viewport origin.
1108    pub position: Pos2,
1109    /// Multiply every size you draw by this so it scales with the map.
1110    pub zoom: f32,
1111    /// The node the highlight belongs to -- the one closest to the mouse
1112    /// pointer. Its id, name, coordinates and connections.
1113    pub point: &'a MapPoint,
1114    /// The active [`MapTheme`](super::theme::MapTheme)'s
1115    /// [`ThemeColors::selected`](super::theme::ThemeColors::selected) for
1116    /// the current color mode -- resolved once here so every `NodeTemplate`
1117    /// doesn't need to reach for the theme itself.
1118    pub color: Color32,
1119}
1120
1121/// The context passed to [`NodeTemplate::notification_ui`].
1122///
1123/// `#[non_exhaustive]` so a future field can be added here without another
1124/// breaking change to [`NodeTemplate`].
1125#[derive(Clone, Copy, Debug)]
1126#[non_exhaustive]
1127pub struct NotificationContext {
1128    /// The node's screen position: already scaled by `zoom` and translated
1129    /// to the viewport origin.
1130    pub position: Pos2,
1131    /// Multiply every size you draw by this so it scales with the map.
1132    pub zoom: f32,
1133    /// When the notification started -- usually fed into a progress
1134    /// computation like `Instant::now().duration_since(initial_time)`.
1135    pub initial_time: Instant,
1136    /// The color requested for this notification (the node's own color, or
1137    /// the current style's `alert_color` if none was set).
1138    pub color: Color32,
1139    /// Which built-in event effect was requested (`pulse`, `ripple`, ...).
1140    /// Match on this to dispatch to the corresponding
1141    /// [`Animation`](crate::map::animation::Animation) function instead of
1142    /// reimplementing the lookup yourself.
1143    pub kind: NodeAnimation,
1144    /// The id of the node this notification belongs to.
1145    pub node_id: usize,
1146}
1147
1148/// The context passed to [`NodeTemplate::marker_ui`].
1149///
1150/// `#[non_exhaustive]`, like [`NotificationContext`], so a future field can
1151/// be added here without another breaking change.
1152#[derive(Clone, Copy, Debug)]
1153#[non_exhaustive]
1154pub struct MarkerContext {
1155    /// The node's screen position: already scaled by `zoom` and translated
1156    /// to the viewport origin.
1157    pub position: Pos2,
1158    /// Multiply every size you draw by this so it scales with the map.
1159    pub zoom: f32,
1160    /// Which persistent effect to draw. For a node's own lasting state (set
1161    /// through [`Map::node`](super::Map::node)'s `halo`/`blink`/`orbit`)
1162    /// this is whichever of those was requested; for a plain marker
1163    /// (registered with [`Map::update_marker`](super::Map::update_marker))
1164    /// it is always [`MapSettings::marker_animation`], since every marker
1165    /// shares that one setting. There is no way from inside this hook to
1166    /// tell the two *cases* apart -- only which `SteadyAnimation` to draw
1167    /// for whichever one it is.
1168    pub kind: SteadyAnimation,
1169    /// The id of the node the state/marker belongs to (for a marker: the id
1170    /// it points at, not the marker's own id).
1171    pub node_id: usize,
1172}
1173
1174/// Customizes how segments and their visual effects are rendered.
1175///
1176/// When a template is installed with
1177/// [`Map::set_segment_template`](super::Map::set_segment_template), the widget
1178/// delegates all segment painting to it instead of using the built-in stroke
1179/// and animations.
1180///
1181/// Unlike [`NodeTemplate`], these methods receive a bare [`&Painter`](Painter)
1182/// rather than `&mut Ui`. Segments are visited in bulk, every frame, after the
1183/// R-tree viewport culling in `paint_map_lines`; going through `Ui` would cost
1184/// a layout pass per segment, on top of what the culling already had to
1185/// discard. Use [`Painter::ctx`] to reach the [`egui::Context`] — for example
1186/// to call `request_repaint()`.
1187///
1188/// The positions passed to these methods are in screen coordinates: already
1189/// scaled by `zoom` and translated to the viewport origin, same as
1190/// [`NodeTemplate`]'s. Multiply every size by `zoom` so your shapes scale
1191/// together with the map.
1192///
1193/// # Examples
1194///
1195/// A segment drawn as a dashed line, plus a notification that briefly
1196/// thickens and brightens it:
1197///
1198/// ```
1199/// use egui_map::map::objects::{MapSegment, SegmentTemplate};
1200/// use egui::{Color32, Painter, Pos2, Stroke};
1201/// use std::time::Instant;
1202///
1203/// struct DashedRoutes;
1204///
1205/// impl SegmentTemplate for DashedRoutes {
1206///     fn segment_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, _segment: &MapSegment) {
1207///         // A crude dash: short strokes along the segment, spaced in screen
1208///         // pixels so they don't stretch as the map zooms.
1209///         let dir = b - a;
1210///         let len = dir.length();
1211///         let step = 10.0 * zoom;
1212///         let mut travelled = 0.0;
1213///         while travelled < len {
1214///             let start = a + dir * (travelled / len);
1215///             let end = a + dir * ((travelled + step * 0.6).min(len) / len);
1216///             painter.line_segment([start, end], Stroke::new(2.0 * zoom, Color32::GRAY));
1217///             travelled += step;
1218///         }
1219///     }
1220///
1221///     fn segment_notification_ui(
1222///         &self,
1223///         painter: &Painter,
1224///         a: Pos2,
1225///         b: Pos2,
1226///         zoom: f32,
1227///         initial_time: Instant,
1228///         color: Color32,
1229///     ) -> bool {
1230///         let secs = Instant::now().duration_since(initial_time).as_secs_f32();
1231///         let alpha = (1.0 - secs).clamp(0.0, 1.0);
1232///         let fading =
1233///             Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), (255.0 * alpha) as u8);
1234///         painter.line_segment([a, b], Stroke::new(5.0 * zoom, fading));
1235///         painter.ctx().request_repaint();
1236///         secs < 1.0
1237///     }
1238///
1239///     fn segment_state_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, time: f32, color: Color32) {
1240///         let t = (time / 1.6).rem_euclid(1.0);
1241///         painter.circle_filled(a + (b - a) * t, 4.0 * zoom, color);
1242///         painter.ctx().request_repaint();
1243///     }
1244/// }
1245/// ```
1246pub trait SegmentTemplate {
1247    /// Draws a segment, replacing the default stroked line.
1248    ///
1249    /// Called every frame for each segment that survives the R-tree viewport
1250    /// culling in `paint_map_lines`.
1251    fn segment_ui(
1252        &self,
1253        painter: &Painter,
1254        pos_a: Pos2,
1255        pos_b: Pos2,
1256        zoom: f32,
1257        segment: &MapSegment,
1258    );
1259
1260    /// Draws the notification effect of a segment notified through
1261    /// [`Map::segment`](super::Map::segment).
1262    ///
1263    /// Called every frame for each segment carrying an event-driven effect
1264    /// (see [`SegmentHandle`](super::SegmentHandle)). Should return `true`
1265    /// while the animation is still playing — remember to call
1266    /// [`Painter::ctx`]`().request_repaint()` — once it returns `false` the
1267    /// notification is discarded.
1268    fn segment_notification_ui(
1269        &self,
1270        painter: &Painter,
1271        pos_a: Pos2,
1272        pos_b: Pos2,
1273        zoom: f32,
1274        initial_time: Instant,
1275        color: Color32,
1276    ) -> bool;
1277
1278    /// Draws the lasting state effect of a segment (e.g. a travelling dot).
1279    ///
1280    /// Called every frame for each segment with lasting state set through
1281    /// [`Map::segment`](super::Map::segment). `time` is the frame time in
1282    /// seconds (`ui.input(|i| i.time)`), so every element animated this frame
1283    /// shares one clock. For animated state, remember to call
1284    /// [`Painter::ctx`]`().request_repaint()`.
1285    fn segment_state_ui(
1286        &self,
1287        painter: &Painter,
1288        pos_a: Pos2,
1289        pos_b: Pos2,
1290        zoom: f32,
1291        time: f32,
1292        color: Color32,
1293    );
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298    use super::*;
1299    use std::collections::HashMap;
1300
1301    // ---------- RawPoint ----------
1302
1303    #[test]
1304    fn raw_point_new() {
1305        let p = RawPoint::new(3.5, -2.0);
1306        assert_eq!(p.components, [3.5, -2.0]);
1307    }
1308
1309    // ---------- MapSegment ----------
1310
1311    #[test]
1312    fn map_segment_new_computes_tight_aabb() {
1313        let seg = MapSegment::new((1, 2), [10.0, -5.0], [-2.0, 7.0]);
1314        assert_eq!(seg.id, (1, 2));
1315        let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1316        assert_eq!(envelope.lower(), [-2.0, -5.0]);
1317        assert_eq!(envelope.upper(), [10.0, 7.0]);
1318        assert_eq!(seg.raw_line().points[0].components, [10.0, -5.0]);
1319        assert_eq!(seg.raw_line().points[1].components, [-2.0, 7.0]);
1320    }
1321
1322    #[test]
1323    fn map_segment_envelope_returns_its_aabb() {
1324        let seg = MapSegment::new((1, 2), [0.0, 0.0], [4.0, 2.0]);
1325        let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1326        assert_eq!(envelope.lower(), [0.0, 0.0]);
1327        assert_eq!(envelope.upper(), [4.0, 2.0]);
1328    }
1329
1330    #[test]
1331    fn map_segment_degenerate_line_has_point_aabb() {
1332        // A zero-length segment must still produce a valid (empty-area) AABB.
1333        let seg = MapSegment::new((1, 2), [3.0, 3.0], [3.0, 3.0]);
1334        let envelope: AABB<[f32; 2]> = rstar::RTreeObject::envelope(&seg);
1335        assert_eq!(envelope.lower(), [3.0, 3.0]);
1336        assert_eq!(envelope.upper(), [3.0, 3.0]);
1337    }
1338
1339    #[test]
1340    fn raw_point_default() {
1341        let p = RawPoint::default();
1342        assert_eq!(p.components, [0.0, 0.0]);
1343    }
1344
1345    #[test]
1346    fn raw_point_mul_i64() {
1347        let p = RawPoint::new(2.0, -3.0) * 3i64;
1348        assert_eq!(p.components, [6.0, -9.0]);
1349    }
1350
1351    #[test]
1352    fn raw_point_mul_i32() {
1353        let p = RawPoint::new(2.0, -3.0) * 3i32;
1354        assert_eq!(p.components, [6.0, -9.0]);
1355    }
1356
1357    #[test]
1358    fn raw_point_mul_u64() {
1359        let p = RawPoint::new(2.0, -3.0) * 3u64;
1360        assert_eq!(p.components, [6.0, -9.0]);
1361    }
1362
1363    #[test]
1364    fn raw_point_mul_u32() {
1365        let p = RawPoint::new(2.0, -3.0) * 3u32;
1366        assert_eq!(p.components, [6.0, -9.0]);
1367    }
1368
1369    #[test]
1370    fn raw_point_mul_f32() {
1371        let p = RawPoint::new(2.0, -3.0) * 0.5f32;
1372        assert_eq!(p.components, [1.0, -1.5]);
1373    }
1374
1375    #[test]
1376    fn raw_point_mul_assign_i64() {
1377        let mut p = RawPoint::new(2.0, -3.0);
1378        p *= 3i64;
1379        assert_eq!(p.components, [6.0, -9.0]);
1380    }
1381
1382    #[test]
1383    fn raw_point_mul_assign_i32() {
1384        let mut p = RawPoint::new(2.0, -3.0);
1385        p *= 3i32;
1386        assert_eq!(p.components, [6.0, -9.0]);
1387    }
1388
1389    #[test]
1390    fn raw_point_mul_assign_u64() {
1391        let mut p = RawPoint::new(2.0, -3.0);
1392        p *= 3u64;
1393        assert_eq!(p.components, [6.0, -9.0]);
1394    }
1395
1396    #[test]
1397    fn raw_point_mul_assign_u32() {
1398        let mut p = RawPoint::new(2.0, -3.0);
1399        p *= 3u32;
1400        assert_eq!(p.components, [6.0, -9.0]);
1401    }
1402
1403    #[test]
1404    fn raw_point_mul_assign_f32() {
1405        let mut p = RawPoint::new(2.0, -3.0);
1406        p *= 0.5f32;
1407        assert_eq!(p.components, [1.0, -1.5]);
1408    }
1409
1410    #[test]
1411    fn raw_point_div_i64() {
1412        let p = RawPoint::new(6.0, -9.0) / 3i64;
1413        assert_eq!(p.components, [2.0, -3.0]);
1414    }
1415
1416    #[test]
1417    fn raw_point_div_i32() {
1418        let p = RawPoint::new(6.0, -9.0) / 3i32;
1419        assert_eq!(p.components, [2.0, -3.0]);
1420    }
1421
1422    #[test]
1423    fn raw_point_div_u64() {
1424        let p = RawPoint::new(6.0, -9.0) / 3u64;
1425        assert_eq!(p.components, [2.0, -3.0]);
1426    }
1427
1428    #[test]
1429    fn raw_point_div_u32() {
1430        let p = RawPoint::new(6.0, -9.0) / 3u32;
1431        assert_eq!(p.components, [2.0, -3.0]);
1432    }
1433
1434    #[test]
1435    fn raw_point_div_f32() {
1436        let p = RawPoint::new(1.0, -1.5) / 0.5f32;
1437        assert_eq!(p.components, [2.0, -3.0]);
1438    }
1439
1440    #[test]
1441    fn raw_point_div_assign_i64() {
1442        let mut p = RawPoint::new(6.0, -9.0);
1443        p /= 3i64;
1444        assert_eq!(p.components, [2.0, -3.0]);
1445    }
1446
1447    #[test]
1448    fn raw_point_div_assign_i32() {
1449        let mut p = RawPoint::new(6.0, -9.0);
1450        p /= 3i32;
1451        assert_eq!(p.components, [2.0, -3.0]);
1452    }
1453
1454    #[test]
1455    fn raw_point_div_assign_u64() {
1456        let mut p = RawPoint::new(6.0, -9.0);
1457        p /= 3u64;
1458        assert_eq!(p.components, [2.0, -3.0]);
1459    }
1460
1461    #[test]
1462    fn raw_point_div_assign_u32() {
1463        let mut p = RawPoint::new(6.0, -9.0);
1464        p /= 3u32;
1465        assert_eq!(p.components, [2.0, -3.0]);
1466    }
1467
1468    #[test]
1469    fn raw_point_div_assign_f32() {
1470        let mut p = RawPoint::new(1.0, -1.5);
1471        p /= 0.5f32;
1472        assert_eq!(p.components, [2.0, -3.0]);
1473    }
1474
1475    #[test]
1476    fn raw_point_add() {
1477        let a = RawPoint::new(1.0, 2.0);
1478        let b = RawPoint::new(3.0, -4.0);
1479        let c = a + b;
1480        assert_eq!(c.components, [4.0, -2.0]);
1481    }
1482
1483    #[test]
1484    fn raw_point_sub() {
1485        let a = RawPoint::new(1.0, 2.0);
1486        let b = RawPoint::new(3.0, -4.0);
1487        let c = a - b;
1488        assert_eq!(c.components, [-2.0, 6.0]);
1489    }
1490
1491    #[test]
1492    #[allow(clippy::op_ref)] // se prueba a propósito la impl Add<&RawPoint>
1493    fn raw_point_add_ref() {
1494        let a = RawPoint::new(1.0, 2.0);
1495        let b = RawPoint::new(3.0, -4.0);
1496        let c = a + &b;
1497        assert_eq!(c.components, [4.0, -2.0]);
1498        // b sigue siendo usable tras la suma por referencia
1499        assert_eq!(b.components, [3.0, -4.0]);
1500    }
1501
1502    #[test]
1503    #[allow(clippy::op_ref)] // se prueba a propósito la impl Sub<&RawPoint>
1504    fn raw_point_sub_ref() {
1505        let a = RawPoint::new(1.0, 2.0);
1506        let b = RawPoint::new(3.0, -4.0);
1507        let c = a - &b;
1508        assert_eq!(c.components, [-2.0, 6.0]);
1509        assert_eq!(b.components, [3.0, -4.0]);
1510    }
1511
1512    #[test]
1513    fn raw_point_from_f32_array() {
1514        let p = RawPoint::from([1.5f32, -2.5f32]);
1515        assert_eq!(p.components, [1.5, -2.5]);
1516    }
1517
1518    #[test]
1519    fn raw_point_from_i64_array() {
1520        let p = RawPoint::from([3i64, -4i64]);
1521        assert_eq!(p.components, [3.0, -4.0]);
1522    }
1523
1524    #[test]
1525    fn raw_point_from_i32_array() {
1526        let p = RawPoint::from([3i32, -4i32]);
1527        assert_eq!(p.components, [3.0, -4.0]);
1528    }
1529
1530    #[test]
1531    fn raw_point_from_i16_array() {
1532        let p = RawPoint::from([3i16, -4i16]);
1533        assert_eq!(p.components, [3.0, -4.0]);
1534    }
1535
1536    #[test]
1537    fn raw_point_from_i8_array() {
1538        let p = RawPoint::from([3i8, -4i8]);
1539        assert_eq!(p.components, [3.0, -4.0]);
1540    }
1541
1542    #[test]
1543    fn raw_point_from_pos2() {
1544        let p = RawPoint::from(Pos2::new(7.0, 8.0));
1545        assert_eq!(p.components, [7.0, 8.0]);
1546    }
1547
1548    #[test]
1549    fn raw_point_into_f32_array() {
1550        let arr: [f32; 2] = RawPoint::new(7.0, 8.0).into();
1551        assert_eq!(arr, [7.0, 8.0]);
1552    }
1553
1554    #[test]
1555    fn raw_point_into_pos2() {
1556        let pos: Pos2 = RawPoint::new(7.0, 8.0).into();
1557        assert_eq!(pos, Pos2::new(7.0, 8.0));
1558    }
1559
1560    // ---------- RawLine ----------
1561
1562    #[test]
1563    fn raw_line_new() {
1564        let a = RawPoint::new(1.0, 2.0);
1565        let b = RawPoint::new(3.0, 4.0);
1566        let line = RawLine::new(a, b);
1567        assert_eq!(line.points[0].components, [1.0, 2.0]);
1568        assert_eq!(line.points[1].components, [3.0, 4.0]);
1569    }
1570
1571    #[test]
1572    fn raw_line_distance() {
1573        // triángulo 3-4-5
1574        let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(3.0, 4.0));
1575        assert_eq!(line.distance(), 5.0);
1576    }
1577
1578    #[test]
1579    fn raw_line_distance_zero() {
1580        let line = RawLine::new(RawPoint::new(2.0, 2.0), RawPoint::new(2.0, 2.0));
1581        assert_eq!(line.distance(), 0.0);
1582    }
1583
1584    #[test]
1585    fn raw_line_midpoint() {
1586        let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(4.0, 6.0));
1587        let mid = line.midpoint();
1588        assert_eq!(mid.components, [2.0, 3.0]);
1589    }
1590
1591    #[test]
1592    fn raw_line_distance_to_point_on_segment() {
1593        let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
1594        assert_eq!(line.distance_to_point(RawPoint::new(5.0, 3.0)), 3.0);
1595        assert_eq!(line.distance_to_point(RawPoint::new(5.0, 0.0)), 0.0);
1596    }
1597
1598    #[test]
1599    fn raw_line_distance_to_point_beyond_endpoints() {
1600        let line = RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 0.0));
1601        // Past the end of the segment, the closest point is the endpoint.
1602        assert_eq!(line.distance_to_point(RawPoint::new(14.0, 0.0)), 4.0);
1603        assert_eq!(line.distance_to_point(RawPoint::new(-3.0, -4.0)), 5.0);
1604    }
1605
1606    #[test]
1607    fn raw_line_distance_to_point_degenerate_segment() {
1608        let line = RawLine::new(RawPoint::new(1.0, 1.0), RawPoint::new(1.0, 1.0));
1609        assert_eq!(line.distance_to_point(RawPoint::new(4.0, 5.0)), 5.0);
1610    }
1611
1612    #[test]
1613    fn raw_line_into_pos2_array() {
1614        let line = RawLine::new(RawPoint::new(1.0, 2.0), RawPoint::new(3.0, 4.0));
1615        let arr: [Pos2; 2] = line.into();
1616        assert_eq!(arr, [Pos2::new(1.0, 2.0), Pos2::new(3.0, 4.0)]);
1617    }
1618
1619    #[test]
1620    fn raw_line_from_i64_arrays() {
1621        let line = RawLine::from([[1i64, 2i64], [3i64, 4i64]]);
1622        assert_eq!(line.points[0].components, [1.0, 2.0]);
1623        assert_eq!(line.points[1].components, [3.0, 4.0]);
1624    }
1625
1626    // ---------- MapStyle ----------
1627
1628    fn full_style() -> Style {
1629        Style {
1630            line_width: Some(4.0),
1631            font: Some(FontId::new(10.0, FontFamily::Proportional)),
1632            background_color: Color32::BLACK,
1633        }
1634    }
1635
1636    #[test]
1637    fn map_style_new() {
1638        let s = Style::new();
1639        assert!(s.line_width.is_none());
1640        assert!(s.font.is_none());
1641        assert_eq!(s.background_color, Color32::TRANSPARENT);
1642    }
1643
1644    #[test]
1645    fn map_style_default_equals_new() {
1646        let s = Style::default();
1647        assert!(s.line_width.is_none());
1648        assert!(s.font.is_none());
1649    }
1650
1651    #[test]
1652    fn map_style_mul_i64() {
1653        let s = full_style() * 2i64;
1654        assert_eq!(s.line_width.unwrap(), 8.0);
1655        assert_eq!(s.font.unwrap().size, 20.0);
1656    }
1657
1658    #[test]
1659    fn map_style_mul_i32() {
1660        let s = full_style() * 2i32;
1661        assert_eq!(s.line_width.unwrap(), 8.0);
1662        assert_eq!(s.font.unwrap().size, 20.0);
1663    }
1664
1665    #[test]
1666    fn map_style_mul_f32() {
1667        let s = full_style() * 0.5f32;
1668        assert_eq!(s.line_width.unwrap(), 2.0);
1669        assert_eq!(s.font.unwrap().size, 5.0);
1670    }
1671
1672    #[test]
1673    fn map_style_mul_f64() {
1674        let s = full_style() * 0.5f64;
1675        assert_eq!(s.line_width.unwrap(), 2.0);
1676        assert_eq!(s.font.unwrap().size, 5.0);
1677    }
1678
1679    #[test]
1680    fn map_style_div_i64() {
1681        let s = full_style() / 2i64;
1682        assert_eq!(s.line_width.unwrap(), 2.0);
1683        assert_eq!(s.font.unwrap().size, 5.0);
1684    }
1685
1686    #[test]
1687    fn map_style_div_i32() {
1688        let s = full_style() / 2i32;
1689        assert_eq!(s.line_width.unwrap(), 2.0);
1690        assert_eq!(s.font.unwrap().size, 5.0);
1691    }
1692
1693    #[test]
1694    fn map_style_div_f32() {
1695        let s = full_style() / 0.5f32;
1696        assert_eq!(s.line_width.unwrap(), 8.0);
1697        assert_eq!(s.font.unwrap().size, 20.0);
1698    }
1699
1700    #[test]
1701    fn map_style_div_f64() {
1702        let s = full_style() / 0.5f64;
1703        assert_eq!(s.line_width.unwrap(), 8.0);
1704        assert_eq!(s.font.unwrap().size, 20.0);
1705    }
1706
1707    // ---------- MapLabel ----------
1708
1709    #[test]
1710    fn map_label_new() {
1711        let l = MapLabel::new();
1712        assert_eq!(l.text, String::new());
1713        assert_eq!(l.center, Pos2::new(0.0, 0.0));
1714    }
1715
1716    #[test]
1717    fn map_label_default_equals_new() {
1718        let l = MapLabel::default();
1719        assert_eq!(l.text, String::new());
1720        assert_eq!(l.center, Pos2::new(0.0, 0.0));
1721    }
1722
1723    // ---------- MapPoint ----------
1724
1725    #[test]
1726    fn map_point_new() {
1727        let p = MapPoint::new(42, [1.0, 2.0]);
1728        assert_eq!(p.get_id(), 42);
1729        assert_eq!(p.coords, [1.0, 2.0]);
1730        assert!(p.connections.is_empty());
1731        assert_eq!(p.name, None);
1732        assert_eq!(p.get_name(), String::new());
1733    }
1734
1735    #[test]
1736    fn map_point_set_and_get_name() {
1737        let mut p = MapPoint::new(1, [0.0, 0.0]);
1738        p.set_name("Jita".to_string());
1739        assert_eq!(p.name, Some("Jita".to_string()));
1740        assert_eq!(p.get_name(), "Jita");
1741    }
1742
1743    #[test]
1744    fn map_point_from_occupied_entry() {
1745        let mut map: HashMap<usize, MapPoint> = HashMap::new();
1746        let mut original = MapPoint::new(7, [5.0, 6.0]);
1747        original.set_name("Amarr".to_string());
1748        map.insert(7, original);
1749
1750        use std::collections::hash_map::Entry;
1751        if let Entry::Occupied(entry) = map.entry(7) {
1752            let cloned = MapPoint::from(entry);
1753            assert_eq!(cloned.get_id(), 7);
1754            assert_eq!(cloned.get_name(), "Amarr");
1755            assert_eq!(cloned.coords, [5.0, 6.0]);
1756        } else {
1757            panic!("se esperaba una entrada ocupada");
1758        }
1759    }
1760
1761    // ---------- MapBounds ----------
1762
1763    #[test]
1764    fn map_bounds_new() {
1765        let b = MapBounds::new();
1766        assert_eq!(b.min.components, [0.0, 0.0]);
1767        assert_eq!(b.max.components, [0.0, 0.0]);
1768        assert_eq!(b.pos.components, [0.0, 0.0]);
1769        assert_eq!(b.dist, 0.0);
1770    }
1771
1772    #[test]
1773    fn map_bounds_default_equals_new() {
1774        let b = MapBounds::default();
1775        assert_eq!(b.dist, 0.0);
1776        assert_eq!(b.pos.components, [0.0, 0.0]);
1777    }
1778
1779    // ---------- MapSettings ----------
1780
1781    #[test]
1782    fn map_settings_new() {
1783        let s = MapSettings::new();
1784        assert_eq!(s.max_zoom, 0.0);
1785        assert_eq!(s.min_zoom, 0.0);
1786        assert_eq!(s.line_visible_zoom, 0.0);
1787        assert_eq!(s.label_visible_zoom, 0.0);
1788        assert_eq!(s.node_text_visibility, VisibilitySetting::Always);
1789        assert_eq!(s.marker_animation, SteadyAnimation::Blink);
1790        assert_eq!(s.node_text_size, 12.0);
1791        assert_eq!(s.label_text_size, 24.0);
1792        assert_eq!(s.styles.len(), 1);
1793    }
1794
1795    #[test]
1796    fn map_settings_default() {
1797        let s = MapSettings::default();
1798        assert_eq!(s.max_zoom, 2.0);
1799        assert_eq!(s.min_zoom, 0.1);
1800        assert_eq!(s.line_visible_zoom, 0.2);
1801        assert_eq!(s.label_visible_zoom, 0.58);
1802        assert_eq!(s.node_text_visibility, VisibilitySetting::Always);
1803        assert_eq!(s.marker_animation, SteadyAnimation::Blink);
1804        assert_eq!(s.node_text_size, 12.0);
1805        assert_eq!(s.label_text_size, 24.0);
1806        // light + dark themes
1807        assert_eq!(s.styles.len(), 2);
1808        // light theme
1809        assert_eq!(s.styles[0].background_color, Color32::WHITE);
1810        assert!(s.styles[0].line_width.is_some());
1811        assert!(s.styles[0].font.is_some());
1812        // dark theme
1813        assert_eq!(s.styles[1].background_color, Color32::DARK_GRAY);
1814        assert!(s.styles[1].line_width.is_some());
1815        assert!(s.styles[1].font.is_some());
1816    }
1817
1818    // ---------- VisibilitySetting ----------
1819
1820    #[test]
1821    fn visibility_setting_equality() {
1822        assert_eq!(VisibilitySetting::Hidden, VisibilitySetting::Hidden);
1823        assert_eq!(VisibilitySetting::Hover, VisibilitySetting::Hover);
1824        assert_eq!(VisibilitySetting::Always, VisibilitySetting::Always);
1825        assert_ne!(VisibilitySetting::Hidden, VisibilitySetting::Hover);
1826        assert_ne!(VisibilitySetting::Hover, VisibilitySetting::Always);
1827        assert_ne!(VisibilitySetting::Hidden, VisibilitySetting::Always);
1828    }
1829}