1use dioxus::prelude::*;
4
5use crate::state::{use_overlay_inset, FlowCore, Interaction};
6use crate::types::{Point, Rect, Side};
7
8#[component]
11pub fn MiniMap(
12 #[props(default = 200.0)] width: f64,
13 #[props(default = 140.0)] height: f64,
14 class: Option<String>,
15) -> Element {
16 let core = use_context::<FlowCore>();
17 use_overlay_inset(Side::Bottom, height + 26.0);
20 let content = use_memo(move || {
25 let geoms = core.geoms.read();
26 geoms
27 .iter()
28 .map(|geom| geom.rect)
29 .reduce(|acc, rect| acc.union(&rect))
30 });
31 let vp = *core.viewport.read();
32 let container = *core.container.read();
33
34 let Some(content) = *content.read() else {
36 return rsx! {};
37 };
38
39 let visible = Rect::from_points(
41 vp.screen_to_flow(Point::ZERO),
42 (container.width / vp.zoom, container.height / vp.zoom).into(),
43 );
44 let world = content.union(&visible);
45 let pad = (world.width.max(world.height) * 0.05).max(10.0);
47 let world = Rect::new(
48 world.x - pad,
49 world.y - pad,
50 world.width + 2.0 * pad,
51 world.height + 2.0 * pad,
52 );
53
54 let class = format!(
55 "df-minimap{}",
56 class
57 .as_deref()
58 .map(|c| format!(" {c}"))
59 .unwrap_or_default()
60 );
61
62 let on_pointer_down = move |evt: Event<PointerData>| {
63 evt.stop_propagation();
64 core.interaction.clone().set(Interaction::Pressed);
65 let p = evt.element_coordinates();
68 let scale = (width / world.width).min(height / world.height);
69 let dx = (width - world.width * scale) / 2.0;
70 let dy = (height - world.height * scale) / 2.0;
71 let flow = Point::new(world.x + (p.x - dx) / scale, world.y + (p.y - dy) / scale);
72 core.center_on(flow, 250);
73 };
74 let on_pointer_up = move |_| {
75 let mut interaction = core.interaction;
76 if *interaction.peek() == Interaction::Pressed {
77 interaction.set(Interaction::None);
78 }
79 };
80
81 rsx! {
82 svg {
83 class,
84 width,
85 height,
86 view_box: "{world.x} {world.y} {world.width} {world.height}",
87 preserve_aspect_ratio: "xMidYMid meet",
88 "role": "img",
89 "aria-label": "Graph overview; click to move the view",
90 onpointerdown: on_pointer_down,
91 onpointerup: on_pointer_up,
92 MiniMapNodes {}
93 rect {
94 class: "df-minimap-viewport",
95 x: visible.x,
96 y: visible.y,
97 width: visible.width,
98 height: visible.height,
99 }
100 }
101 }
102}
103
104#[component]
117fn MiniMapNodes() -> Element {
118 let core = use_context::<FlowCore>();
119 let paths = use_memo(move || {
120 let geoms = core.geoms.read();
121 let mut plain = String::new();
122 let mut selected = String::new();
123 for geom in geoms.iter() {
124 let r = geom.rect;
125 if !(r.x.is_finite() && r.y.is_finite() && r.width.is_finite() && r.height.is_finite())
126 {
127 continue;
128 }
129 let into = if geom.selected {
130 &mut selected
131 } else {
132 &mut plain
133 };
134 use std::fmt::Write;
137 let _ = write!(
138 into,
139 "M{} {}h{}v{}h{}z",
140 r.x, r.y, r.width, r.height, -r.width
141 );
142 }
143 (plain, selected)
144 });
145 let (plain, selected) = paths.read().clone();
146 rsx! {
147 if !plain.is_empty() {
148 path { class: "df-minimap-node", d: plain }
149 }
150 if !selected.is_empty() {
151 path { class: "df-minimap-node df-selected", d: selected }
152 }
153 }
154}