1use gpui::{Bounds, Element, MouseButton, PathBuilder, Pixels, Point, Size, canvas, px, rgb};
4
5use crate::{
6 Graph, Viewport,
7 canvas::{Command, CommandContext},
8 plugin::{
9 EventResult, FlowEvent, InputEvent, Plugin, PluginContext, RenderContext, RenderLayer,
10 },
11};
12
13const MAP_W: f32 = 200.0;
14const MAP_H: f32 = 140.0;
15const OUTER_MARGIN: f32 = 16.0;
16const INNER_INSET: f32 = 3.0;
17const WORLD_PAD: f32 = 96.0;
18
19#[derive(Clone)]
21struct MinimapLayout {
22 chrome: Bounds<Pixels>,
23 inner: Bounds<Pixels>,
24 world_x0: f32,
25 world_y0: f32,
26 world_w: f32,
27 world_h: f32,
28}
29
30impl MinimapLayout {
31 fn contains_chrome(&self, p: Point<Pixels>) -> bool {
32 self.chrome.contains(&p)
33 }
34
35 fn screen_to_world(&self, screen: Point<Pixels>) -> Point<Pixels> {
37 let ix: f32 = self.inner.origin.x.into();
38 let iy: f32 = self.inner.origin.y.into();
39 let iw: f32 = self.inner.size.width.into();
40 let ih: f32 = self.inner.size.height.into();
41 let sx: f32 = screen.x.into();
42 let sy: f32 = screen.y.into();
43 let u = ((sx - ix) / iw.max(1.0)).clamp(0.0, 1.0);
44 let v = ((sy - iy) / ih.max(1.0)).clamp(0.0, 1.0);
45 let wx = self.world_x0 + u * self.world_w;
46 let wy = self.world_y0 + v * self.world_h;
47 Point::new(px(wx), px(wy))
48 }
49}
50
51fn graph_world_extent(graph: &Graph, viewport: &Viewport) -> (f32, f32, f32, f32) {
52 let nodes: Vec<_> = graph
53 .nodes()
54 .values()
55 .filter(|n| viewport.is_node_visible(n))
56 .collect();
57 if nodes.is_empty() {
58 return (0.0, 0.0, 640.0, 480.0);
59 }
60 let mut min_x = f32::MAX;
61 let mut min_y = f32::MAX;
62 let mut max_x = f32::MIN;
63 let mut max_y = f32::MIN;
64 for n in nodes {
65 let x: f32 = n.x.into();
66 let y: f32 = n.y.into();
67 let w: f32 = n.size.width.into();
68 let h: f32 = n.size.height.into();
69 min_x = min_x.min(x);
70 min_y = min_y.min(y);
71 max_x = max_x.max(x + w);
72 max_y = max_y.max(y + h);
73 }
74 let w = (max_x - min_x + 2.0 * WORLD_PAD).max(120.0);
75 let h = (max_y - min_y + 2.0 * WORLD_PAD).max(120.0);
76 (min_x - WORLD_PAD, min_y - WORLD_PAD, w, h)
77}
78
79fn visible_world_aabb(viewport: &Viewport, win: &Bounds<Pixels>) -> (f32, f32, f32, f32) {
80 let w: f32 = win.size.width.into();
81 let h: f32 = win.size.height.into();
82 let corners = [
83 viewport.screen_to_world(Point::new(px(0.0), px(0.0))),
84 viewport.screen_to_world(Point::new(px(w), px(0.0))),
85 viewport.screen_to_world(Point::new(px(w), px(h))),
86 viewport.screen_to_world(Point::new(px(0.0), px(h))),
87 ];
88 let mut min_x = f32::MAX;
89 let mut min_y = f32::MAX;
90 let mut max_x = f32::MIN;
91 let mut max_y = f32::MIN;
92 for c in corners {
93 let x: f32 = c.x.into();
94 let y: f32 = c.y.into();
95 min_x = min_x.min(x);
96 min_y = min_y.min(y);
97 max_x = max_x.max(x);
98 max_y = max_y.max(y);
99 }
100 (
101 min_x,
102 min_y,
103 (max_x - min_x).max(1.0),
104 (max_y - min_y).max(1.0),
105 )
106}
107
108fn build_layout(viewport: &Viewport, graph: &Graph) -> Option<MinimapLayout> {
109 let win = viewport.window_bounds?;
110 let ww: f32 = win.size.width.into();
111 let wh: f32 = win.size.height.into();
112 if ww < MAP_W + OUTER_MARGIN || wh < MAP_H + OUTER_MARGIN {
113 return None;
114 }
115
116 let map_w = px(MAP_W);
117 let map_h = px(MAP_H);
118 let ox = win.size.width - map_w - px(OUTER_MARGIN);
119 let oy = win.size.height - map_h - px(OUTER_MARGIN);
120 let chrome = Bounds::new(Point::new(ox, oy), Size::new(map_w, map_h));
121
122 let inset = px(INNER_INSET);
123 let inner = Bounds::new(
124 chrome.origin + Point::new(inset, inset),
125 Size::new(
126 chrome.size.width - inset * 2.0,
127 chrome.size.height - inset * 2.0,
128 ),
129 );
130
131 let (wx0, wy0, ww, wh) = graph_world_extent(graph, viewport);
132
133 Some(MinimapLayout {
134 chrome,
135 inner,
136 world_x0: wx0,
137 world_y0: wy0,
138 world_w: ww.max(1.0),
139 world_h: wh.max(1.0),
140 })
141}
142
143fn world_to_inner_pt(wx: f32, wy: f32, layout: &MinimapLayout) -> Point<Pixels> {
144 let u = ((wx - layout.world_x0) / layout.world_w).clamp(0.0, 1.0);
145 let v = ((wy - layout.world_y0) / layout.world_h).clamp(0.0, 1.0);
146 let ix: f32 = layout.inner.origin.x.into();
147 let iy: f32 = layout.inner.origin.y.into();
148 let iw: f32 = layout.inner.size.width.into();
149 let ih: f32 = layout.inner.size.height.into();
150 Point::new(px(ix + u * iw), px(iy + v * ih))
151}
152
153fn center_viewport_on_world(ctx: &mut PluginContext, world: Point<Pixels>) {
154 let Some(wb) = ctx.viewport.window_bounds else {
155 return;
156 };
157 let cx: f32 = (wb.size.width / 2.0).into();
158 let cy: f32 = (wb.size.height / 2.0).into();
159 let z = ctx.viewport.zoom;
160 let wx: f32 = world.x.into();
161 let wy: f32 = world.y.into();
162 let from = ctx.viewport.offset;
163 ctx.viewport.offset.x = px(cx - wx * z);
164 ctx.viewport.offset.y = px(cy - wy * z);
165 let to = ctx.viewport.offset;
166 ctx.execute_command(MinimapPanCommand { from, to });
167}
168
169struct MinimapPanCommand {
170 from: Point<Pixels>,
171 to: Point<Pixels>,
172}
173
174impl Command for MinimapPanCommand {
175 fn name(&self) -> &'static str {
176 "minimap_pan"
177 }
178
179 fn execute(&mut self, ctx: &mut CommandContext) {
180 ctx.viewport.offset.x = self.to.x;
181 ctx.viewport.offset.y = self.to.y;
182 }
183
184 fn undo(&mut self, ctx: &mut CommandContext) {
185 ctx.viewport.offset.x = self.from.x;
186 ctx.viewport.offset.y = self.from.y;
187 }
188
189 fn to_ops(&self, _ctx: &mut crate::CommandContext) -> Vec<crate::GraphOp> {
190 vec![]
191 }
192}
193
194pub struct MinimapPlugin {
199 last_layout: Option<MinimapLayout>,
200}
201
202impl MinimapPlugin {
203 pub fn new() -> Self {
204 Self { last_layout: None }
205 }
206}
207
208impl Plugin for MinimapPlugin {
209 fn name(&self) -> &'static str {
210 "minimap"
211 }
212
213 fn setup(&mut self, _ctx: &mut crate::plugin::InitPluginContext) {}
214
215 fn on_event(&mut self, event: &FlowEvent, ctx: &mut PluginContext) -> EventResult {
216 if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event {
217 if let Some(ref layout) = self.last_layout {
218 if layout.contains_chrome(ev.position) {
219 if ev.button == MouseButton::Right {
220 return EventResult::Stop;
221 }
222 if ev.button == MouseButton::Left {
223 let world = layout.screen_to_world(ev.position);
224 center_viewport_on_world(ctx, world);
225 ctx.notify();
226 return EventResult::Stop;
227 }
228 }
229 }
230 }
231 EventResult::Continue
232 }
233
234 fn priority(&self) -> i32 {
235 135
236 }
237
238 fn render_layer(&self) -> RenderLayer {
239 RenderLayer::Overlay
240 }
241
242 fn render(&mut self, ctx: &mut RenderContext) -> Option<gpui::AnyElement> {
243 let layout = build_layout(ctx.viewport, ctx.graph)?;
244 self.last_layout = Some(layout.clone());
245
246 let inner = layout.inner;
247
248 let nodes: Vec<_> = ctx
249 .graph
250 .nodes()
251 .values()
252 .filter(|n| ctx.is_node_visible(&n.id))
253 .map(|n| {
254 let x: f32 = n.x.into();
255 let y: f32 = n.y.into();
256 let w: f32 = n.size.width.into();
257 let h: f32 = n.size.height.into();
258 (x, y, w, h)
259 })
260 .collect();
261
262 let edges: Vec<_> = ctx
263 .graph
264 .edges
265 .values()
266 .filter(|e| ctx.is_edge_visible(e))
267 .filter_map(|e| {
268 let s = ctx.graph.ports.get(&e.source_port)?;
269 let t = ctx.graph.ports.get(&e.target_port)?;
270 let sn = ctx.graph.nodes.get(&s.node_id)?;
271 let tn = ctx.graph.nodes.get(&t.node_id)?;
272 let sx: f32 = f32::from(sn.x) + f32::from(sn.size.width) * 0.5;
273 let sy: f32 = f32::from(sn.y) + f32::from(sn.size.height) * 0.5;
274 let tx: f32 = f32::from(tn.x) + f32::from(tn.size.width) * 0.5;
275 let ty: f32 = f32::from(tn.y) + f32::from(tn.size.height) * 0.5;
276 Some((sx, sy, tx, ty))
277 })
278 .collect();
279
280 let win_bounds = ctx.viewport.window_bounds?;
281 let (vx0, vy0, vw, vh) = visible_world_aabb(ctx.viewport, &win_bounds);
282 let v_tl = world_to_inner_pt(vx0, vy0, &layout);
283 let v_br = world_to_inner_pt(vx0 + vw, vy0 + vh, &layout);
284
285 let minimap_background = ctx.theme.minimap_background;
286 let minimap_border = ctx.theme.minimap_border;
287 let minimap_edge = ctx.theme.minimap_edge;
288 let minimap_node_fill = ctx.theme.minimap_node_fill;
289 let minimap_node_stroke = ctx.theme.minimap_node_stroke;
290 let minimap_viewport_stroke = ctx.theme.minimap_viewport_stroke;
291
292 Some(
293 canvas(
294 move |_, _, _| (),
295 move |_, _, win, _| {
296 if let Ok(p) = rect_fill_path(inner) {
298 win.paint_path(p, rgb(minimap_background));
299 }
300 if let Ok(p) = rect_stroke_path(inner, px(1.0)) {
301 win.paint_path(p, rgb(minimap_border));
302 }
303
304 for (sx, sy, tx, ty) in edges {
306 let a = world_to_inner_pt(sx, sy, &layout);
307 let b = world_to_inner_pt(tx, ty, &layout);
308 let mut line = PathBuilder::stroke(px(1.0));
309 line.move_to(a);
310 line.line_to(b);
311 if let Ok(p) = line.build() {
312 win.paint_path(p, rgb(minimap_edge));
313 }
314 }
315
316 for (x, y, nw, nh) in nodes {
317 let p0 = world_to_inner_pt(x, y, &layout);
318 let p1 = world_to_inner_pt(x + nw, y + nh, &layout);
319 let min_x = f32::min(f32::from(p0.x), f32::from(p1.x));
320 let max_x = f32::max(f32::from(p0.x), f32::from(p1.x));
321 let min_y = f32::min(f32::from(p0.y), f32::from(p1.y));
322 let max_y = f32::max(f32::from(p0.y), f32::from(p1.y));
323 let rw = (max_x - min_x).max(2.0);
324 let rh = (max_y - min_y).max(2.0);
325 let o = Point::new(px(min_x), px(min_y));
326 let s = Size::new(px(rw), px(rh));
327 if let Ok(p) = rect_fill_bounds(o, s) {
328 win.paint_path(p, rgb(minimap_node_fill));
329 }
330 if let Ok(p) = rect_stroke_bounds(o, s, px(1.0)) {
331 win.paint_path(p, rgb(minimap_node_stroke));
332 }
333 }
334
335 let min_x = f32::min(f32::from(v_tl.x), f32::from(v_br.x));
337 let max_x = f32::max(f32::from(v_tl.x), f32::from(v_br.x));
338 let min_y = f32::min(f32::from(v_tl.y), f32::from(v_br.y));
339 let max_y = f32::max(f32::from(v_tl.y), f32::from(v_br.y));
340 let vo = Point::new(px(min_x), px(min_y));
341 let vs = Size::new(px((max_x - min_x).max(2.0)), px((max_y - min_y).max(2.0)));
342 if let Ok(p) = rect_stroke_bounds(vo, vs, px(1.5)) {
343 win.paint_path(p, rgb(minimap_viewport_stroke));
344 }
345 },
346 )
347 .into_any(),
348 )
349 }
350}
351
352fn rect_fill_path(b: Bounds<Pixels>) -> Result<gpui::Path<Pixels>, anyhow::Error> {
353 rect_fill_bounds(b.origin, b.size)
354}
355
356fn rect_fill_bounds(
357 o: Point<Pixels>,
358 s: Size<Pixels>,
359) -> Result<gpui::Path<Pixels>, anyhow::Error> {
360 let x0: f32 = o.x.into();
361 let y0: f32 = o.y.into();
362 let w: f32 = s.width.into();
363 let h: f32 = s.height.into();
364 let pts = [
365 Point::new(px(x0), px(y0)),
366 Point::new(px(x0 + w), px(y0)),
367 Point::new(px(x0 + w), px(y0 + h)),
368 Point::new(px(x0), px(y0 + h)),
369 ];
370 let mut pb = PathBuilder::fill();
371 pb.add_polygon(&pts, true);
372 pb.build()
373}
374
375fn rect_stroke_path(b: Bounds<Pixels>, width: Pixels) -> Result<gpui::Path<Pixels>, anyhow::Error> {
376 rect_stroke_bounds(b.origin, b.size, width)
377}
378
379fn rect_stroke_bounds(
380 o: Point<Pixels>,
381 s: Size<Pixels>,
382 width: Pixels,
383) -> Result<gpui::Path<Pixels>, anyhow::Error> {
384 let x0: f32 = o.x.into();
385 let y0: f32 = o.y.into();
386 let w: f32 = s.width.into();
387 let h: f32 = s.height.into();
388 let mut line = PathBuilder::stroke(width);
389 line.move_to(Point::new(px(x0), px(y0)));
390 line.line_to(Point::new(px(x0 + w), px(y0)));
391 line.line_to(Point::new(px(x0 + w), px(y0 + h)));
392 line.line_to(Point::new(px(x0), px(y0 + h)));
393 line.close();
394 line.build()
395}