1use std::collections::HashSet;
2
3use gpui::{Element as _, PathBuilder, Point, canvas, px, rgb};
4
5use crate::{
6 Graph, Node, NodeId, Viewport,
7 alignment_guides::AlignmentGuides,
8 plugin::{Plugin, RenderContext, RenderLayer},
9};
10
11const SNAP_SCREEN_PX: f32 = 5.0;
13const GUIDE_COLOR: u32 = 0xff4081;
14
15fn visible_world_bounds(viewport: &Viewport) -> Option<(f32, f32, f32, f32)> {
16 let wb = viewport.window_bounds?;
17 let w: f32 = wb.size.width.into();
18 let h: f32 = wb.size.height.into();
19 let corners = [
20 viewport.screen_to_world(Point::new(px(0.0), px(0.0))),
21 viewport.screen_to_world(Point::new(px(w), px(0.0))),
22 viewport.screen_to_world(Point::new(px(w), px(h))),
23 viewport.screen_to_world(Point::new(px(0.0), px(h))),
24 ];
25 let mut min_x = f32::MAX;
26 let mut min_y = f32::MAX;
27 let mut max_x = f32::MIN;
28 let mut max_y = f32::MIN;
29 for c in corners {
30 let x: f32 = c.x.into();
31 let y: f32 = c.y.into();
32 min_x = min_x.min(x);
33 min_y = min_y.min(y);
34 max_x = max_x.max(x);
35 max_y = max_y.max(y);
36 }
37 Some((min_x, min_y, max_x.max(min_x + 1.0), max_y.max(min_y + 1.0)))
38}
39
40fn node_edges(n: &Node) -> (f32, f32, f32, f32) {
41 let x: f32 = n.x.into();
42 let y: f32 = n.y.into();
43 let w: f32 = n.size.width.into();
44 let h: f32 = n.size.height.into();
45 (x, y, x + w, y + h)
46}
47
48fn union_bbox(graph: &Graph, ids: &HashSet<NodeId>) -> Option<(f32, f32, f32, f32)> {
49 let mut min_x = f32::MAX;
50 let mut min_y = f32::MAX;
51 let mut max_x = f32::MIN;
52 let mut max_y = f32::MIN;
53 let mut any = false;
54 for id in ids {
55 let n = graph.nodes.get(id)?;
56 let (l, t, r, b) = node_edges(n);
57 min_x = min_x.min(l);
58 min_y = min_y.min(t);
59 max_x = max_x.max(r);
60 max_y = max_y.max(b);
61 any = true;
62 }
63 any.then_some((min_x, min_y, max_x, max_y))
64}
65
66fn dedup_axis(v: &mut Vec<f32>) {
67 v.sort_by(|a, b| a.partial_cmp(b).unwrap());
68 v.dedup_by(|a, b| (*a - *b).abs() < 0.25);
69}
70
71pub(crate) fn compute_alignment_guides(
72 graph: &Graph,
73 viewport: &Viewport,
74 dragged_ids: &HashSet<NodeId>,
75) -> AlignmentGuides {
76 if dragged_ids.is_empty() {
77 return AlignmentGuides::default();
78 }
79
80 let Some((wx0, wy0, wx1, wy1)) = visible_world_bounds(viewport) else {
81 return AlignmentGuides::default();
82 };
83
84 let Some(db) = union_bbox(graph, dragged_ids) else {
85 return AlignmentGuides::default();
86 };
87
88 let thr = SNAP_SCREEN_PX / viewport.zoom.max(0.01);
89
90 let mut ref_x: Vec<f32> = Vec::new();
91 let mut ref_y: Vec<f32> = Vec::new();
92 for (id, n) in graph.nodes() {
93 if dragged_ids.contains(id) {
94 continue;
95 }
96 let (l, t, r, b) = node_edges(n);
97 let cx = (l + r) * 0.5;
98 let cy = (t + b) * 0.5;
99 ref_x.extend([l, cx, r]);
100 ref_y.extend([t, cy, b]);
101 }
102
103 dedup_axis(&mut ref_x);
104 dedup_axis(&mut ref_y);
105
106 let (dl, dt, dr, db) = db;
107 let dcx = (dl + dr) * 0.5;
108 let dcy = (dt + db) * 0.5;
109
110 let mut vertical_candidates: Vec<f32> = Vec::new();
111 let xs = [dl, dcx, dr];
112 for rx in ref_x {
113 for dx in xs {
114 if (dx - rx).abs() <= thr {
115 vertical_candidates.push((dx + rx) * 0.5);
116 }
117 }
118 }
119 dedup_axis(&mut vertical_candidates);
120
121 let mut horizontal_candidates: Vec<f32> = Vec::new();
122 let ys = [dt, dcy, db];
123 for ry in ref_y {
124 for dy in ys {
125 if (dy - ry).abs() <= thr {
126 horizontal_candidates.push((dy + ry) * 0.5);
127 }
128 }
129 }
130 dedup_axis(&mut horizontal_candidates);
131
132 let _ = (wx0, wy0, wx1, wy1);
133
134 AlignmentGuides {
135 vertical_x: vertical_candidates,
136 horizontal_y: horizontal_candidates,
137 }
138}
139
140pub struct SnapGuidesPlugin;
142
143impl SnapGuidesPlugin {
144 pub fn new() -> Self {
145 Self
146 }
147}
148
149impl Plugin for SnapGuidesPlugin {
150 fn name(&self) -> &'static str {
151 "snap_guides"
152 }
153
154 fn setup(&mut self, _ctx: &mut crate::plugin::InitPluginContext) {}
155
156 fn priority(&self) -> i32 {
157 118
158 }
159
160 fn render_layer(&self) -> RenderLayer {
161 RenderLayer::Interaction
162 }
163
164 fn render(&mut self, ctx: &mut RenderContext) -> Option<gpui::AnyElement> {
165 let guides = ctx.alignment_guides.as_ref()?;
166 if guides.vertical_x.is_empty() && guides.horizontal_y.is_empty() {
167 return None;
168 }
169
170 let vxs = guides.vertical_x.clone();
171 let hys = guides.horizontal_y.clone();
172 let vp = ctx.viewport.clone();
173 let (wx0, wy0, wx1, wy1) = visible_world_bounds(&vp)?;
174
175 Some(
176 canvas(
177 move |_, _, _| (),
178 move |_, _, win, _| {
179 for x in &vxs {
180 let a = vp.world_to_screen(Point::new(px(*x), px(wy0)));
181 let b = vp.world_to_screen(Point::new(px(*x), px(wy1)));
182 let mut line = PathBuilder::stroke(px(1.0));
183 line.move_to(a);
184 line.line_to(b);
185 if let Ok(p) = line.build() {
186 win.paint_path(p, rgb(GUIDE_COLOR));
187 }
188 }
189 for y in &hys {
190 let a = vp.world_to_screen(Point::new(px(wx0), px(*y)));
191 let b = vp.world_to_screen(Point::new(px(wx1), px(*y)));
192 let mut line = PathBuilder::stroke(px(1.0));
193 line.move_to(a);
194 line.line_to(b);
195 if let Ok(p) = line.build() {
196 win.paint_path(p, rgb(GUIDE_COLOR));
197 }
198 }
199 },
200 )
201 .into_any(),
202 )
203 }
204}