An egui widget that renders an interactive 2D map and displays information about it.
- Pan with click & drag, and zoom with the mouse wheel (hold
Ctrl— orCmdon macOS — to zoom faster) or the built-in slider. - Spatial indexing via kd-tree: only the nodes inside the viewport are painted each frame.
- Node names with configurable visibility rules (always / on hover / hidden).
- Connection lines between nodes and free-floating text labels.
- Text is sized in screen pixels (
MapSettings::node_text_size,MapSettings::label_text_size), so names stay readable at any zoom level instead of shrinking away as you zoom out. - Animations attached per node through
map.node(id): one-off events that end on their own (pulse,ripple,countdown,scale_in,crosshair) and lasting state that runs untilclear()(halo,blink,orbit), each with an optionalcolor(). The effects live inmap::animation::Animationand can be reused from your ownNodeTemplate. - The same idiom for segments through
map.segment(id):flash/comet_once(at, direction)/wipe(one-off) andcomet/dash/glow_band/chevrons(lasting, untilclear()) --comet_onceis a single dot pass with the direction you choose (CometDirection::Forward/Reverse),wipedraws the line in from one endpoint to the other,dashis a "marching ants" pattern andchevronsa row of sliding arrowheads, both painted as a repeating-texture mesh (two triangles per segment, one shared texture),glow_banda soft travelling highlight that fades out past each end instead of repeating, also with an optionalcolor(). - Custom node rendering and right-click context menus through the
NodeTemplateandContextMenuManagertraits, and custom segment rendering throughSegmentTemplate. - Fourteen built-in color themes, each with a light and a dark variant, or install your own through the
MapThemetrait.
Add the dependency:
[dependencies]
egui-map = "0.0"Feed the map a set of nodes and add it to your UI:
use egui_map::map::Map;
use egui_map::map::objects::{MapPoint, RawPoint};
use std::collections::HashMap;
// Build the node set, keyed by node id.
let mut points: HashMap<usize, MapPoint> = HashMap::new();
points.insert(1, MapPoint::new(1, RawPoint::new(0.0, 0.0)));
points.insert(2, MapPoint::new(2, RawPoint::new(100.0, 50.0)));
let mut map = Map::new();
map.add_hashmap_points(points);
// Then, on every frame of your egui update loop:
// ui.add(&mut map);Lines are wired in three steps: create the nodes, register a unique connection id in the connections of both endpoints, and load the line geometry keyed by that same id:
use egui_map::map::objects::{MapPoint, RawLine, RawPoint};
use std::collections::HashMap;
let mut points: HashMap<usize, MapPoint> = HashMap::new();
points.insert(1, MapPoint::new(1, RawPoint::new(0.0, 0.0)));
points.insert(2, MapPoint::new(2, RawPoint::new(10.0, 10.0)));
// Register the connection id on both endpoints.
for id in [1, 2] {
points.get_mut(&id).unwrap().connections.push("1-2".to_string());
}
map.add_hashmap_points(points);
// Line geometry, keyed by the same connection id.
let mut lines: HashMap<String, RawLine> = HashMap::new();
lines.insert("1-2".to_string(), RawLine::new(RawPoint::new(0.0, 0.0), RawPoint::new(10.0, 10.0)));
map.add_lines(lines);A line is only drawn while the zoom level is above MapSettings::line_visible_zoom and its bounding box intersects the viewport. Segments are culled broad-phase with an R-tree, so long lines crossing the view are drawn even when both endpoints lie outside of it.
Implement NodeTemplate to take over how nodes, selection highlights, notifications and markers are drawn — including the name labels, which the widget no longer paints once a template is installed:
use egui_map::map::objects::{
MarkerContext, NodeContext, NodeTemplate, NotificationContext, SelectionContext,
};
use egui::Ui;
struct MyTemplate;
impl NodeTemplate for MyTemplate {
fn node_ui(&self, ui: &mut Ui, ctx: NodeContext) {
// `ctx.position` is the node's screen position; scale every size by `ctx.zoom`.
// `ctx.color` is already resolved: the node's own color override, or the
// active theme's node color if it doesn't have one.
ui.painter().circle_filled(ctx.position, 6.0 * ctx.zoom, ctx.color);
}
fn notification_ui(&self, ui: &mut Ui, ctx: NotificationContext) -> bool {
// `ctx.kind` is which built-in event was requested (Pulse, Ripple, ...) and
// `ctx.node_id` is which node -- dispatch on either, or reuse
// `animation::Animation::*`. Draw a time-driven effect from `ctx.initial_time`.
ui.ctx().request_repaint(); // keep the animation frames coming
ctx.initial_time.elapsed().as_secs_f32() < 2.0 // returning false removes the notification
}
fn selection_ui(&self, _ui: &mut Ui, _ctx: SelectionContext) {
// `ctx.point` is which node the highlight belongs to; `ctx.color` is the
// active theme's selection color.
}
fn marker_ui(&self, _ui: &mut Ui, _ctx: MarkerContext) {
// `ctx.kind` is Halo/Blink/Orbit for persistent node state, or the shared
// `MapSettings::marker_animation` for a `Map::update_marker` marker.
}
}
map.set_node_template(std::rc::Rc::new(MyTemplate));See the NodeTemplate rustdoc for a complete example with a custom node shape and an animated notification.
SegmentTemplate is the segment counterpart of NodeTemplate. Its methods take a bare &Painter rather than &mut Ui, since segments are visited in bulk after the R-tree viewport culling — use painter.ctx() to reach request_repaint():
use egui_map::map::objects::{MapSegment, SegmentTemplate};
use egui::{Color32, Painter, Pos2, Stroke};
use std::time::Instant;
struct MySegments;
impl SegmentTemplate for MySegments {
fn segment_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, _segment: &MapSegment) {
painter.line_segment([a, b], Stroke::new(1.5 * zoom, Color32::GRAY));
}
fn segment_notification_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, start: Instant, color: Color32) -> bool {
// ... draw a time-driven effect computed from `start.elapsed()` ...
painter.ctx().request_repaint(); // keep the animation frames coming
start.elapsed().as_secs_f32() < 1.0 // returning false removes the notification
}
fn segment_state_ui(&self, painter: &Painter, a: Pos2, b: Pos2, zoom: f32, time: f32, color: Color32) {
// `time` is the frame time (`ui.input(|i| i.time)`), shared by every element animated this frame.
painter.ctx().request_repaint();
}
}
map.set_segment_template(std::rc::Rc::new(MySegments));examples/animations.rs shows the built-in node and segment effects end to end, with no custom template at all. examples/node_template_animations.rs shows the opposite pairing: a custom NodeTemplate (its own node shape) that still reuses the built-in Animation::* functions from its notification_ui/marker_ui hooks instead of hand-rolling new ones, dispatching directly on the kind/node_id those hooks receive.
The widget ships fourteen named Theme palettes — NebulaViolet is the default — each with a light and a dark variant; see the Theme rustdoc for the full list. Switch between them, or install your own palette, with Map::set_theme and the MapTheme trait:
use egui_map::map::theme::{ColorMode, MapTheme, Theme, ThemeColors};
// A built-in theme:
map.set_theme(std::rc::Rc::new(Theme::ArticCyan));
// Or a custom palette:
struct HighContrast;
impl MapTheme for HighContrast {
fn colors(&self, mode: ColorMode) -> ThemeColors {
match mode {
ColorMode::Light => ThemeColors {
node: egui::Color32::BLACK,
segment: egui::Color32::DARK_GRAY,
selected: egui::Color32::RED,
alert: egui::Color32::RED,
text: egui::Color32::BLACK,
},
ColorMode::Dark => ThemeColors {
node: egui::Color32::WHITE,
segment: egui::Color32::LIGHT_GRAY,
selected: egui::Color32::YELLOW,
alert: egui::Color32::YELLOW,
text: egui::Color32::WHITE,
},
}
}
}
map.set_theme(std::rc::Rc::new(HighContrast));ColorMode is egui::Theme re-exported under this crate's name, so the same map picks up the right palette automatically when the surrounding app's mode changes. Non-palette visual settings (stroke width, font, background) stay on MapSettings::styles — see the theme module rustdoc.
debug_overlay: adds a read-out of the widget's internal viewport state (bounds, current position, distance, zoom, node counts, pointer position). It stays out of the way: a dimdbgtoggle in the map's top-left corner, collapsed by default and with no background of its own, that you click open when you need the numbers. egui remembers the open/closed state per widget, and the overlay never affects the map's layout.
The widget's hot paths (rendering, viewport culling, point/line loading) are instrumented with tracing spans. tracing is a normal, unconditional dependency of this crate, and the spans are cheap no-ops unless a subscriber is installed somewhere in your binary -- egui-map never installs one itself.
To see these spans in the Tracy profiler, install a tracing_tracy::TracyLayer in your own main, e.g.:
tracing_subscriber::registry()
.with(tracing_tracy::TracyLayer::default())
.init();The profile feature pulls in tracing-subscriber and tracing-tracy so examples/tracy_profile.rs can demonstrate exactly this. Run it (with a Tracy capture window already listening) with:
cargo run --example tracy_profile --features profileMIT. See LICENSE.md.