Skip to main content

ferrum_flow/plugins/
snap_guides.rs

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
11/// Screen-space snap distance; world threshold = this / zoom.
12const 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
140/// While dragging nodes, draws magenta alignment lines when selection bbox matches other nodes.
141pub 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}