Skip to main content

ferrum_flow/plugins/edge/
mod.rs

1use gpui::{Bounds, Element, MouseButton, PathBuilder, Pixels, Point, canvas, px, rgb};
2
3use crate::{
4    Edge, EdgeId, PortId, PortPosition, RenderContext, Viewport,
5    plugin::{FlowEvent, Plugin, PluginContext},
6    plugins::edge::command::ClearEdgeCommand,
7};
8
9mod command;
10
11use command::SelectEdgeCommand;
12
13pub struct EdgePlugin;
14
15impl EdgePlugin {
16    pub fn new() -> Self {
17        Self {}
18    }
19}
20
21impl Plugin for EdgePlugin {
22    fn name(&self) -> &'static str {
23        "edge"
24    }
25    fn setup(&mut self, _ctx: &mut crate::plugin::InitPluginContext) {}
26    fn on_event(
27        &mut self,
28        event: &FlowEvent,
29        ctx: &mut crate::plugin::PluginContext,
30    ) -> crate::plugin::EventResult {
31        if let FlowEvent::Input(crate::plugin::InputEvent::MouseDown(ev)) = event {
32            if ev.button != MouseButton::Left {
33                return crate::plugin::EventResult::Continue;
34            }
35            let shift = ev.modifiers.shift;
36            if let Some(id) = hit_test_get_edge(ev.position, &ctx) {
37                ctx.cache_port_offset_with_edge(&id);
38                ctx.execute_command(SelectEdgeCommand::new(id, shift, &ctx));
39                return crate::plugin::EventResult::Stop;
40            } else {
41                if !shift {
42                    ctx.execute_command(ClearEdgeCommand::new(&ctx));
43                }
44            }
45        }
46        crate::plugin::EventResult::Continue
47    }
48    fn priority(&self) -> i32 {
49        120
50    }
51    fn render_layer(&self) -> crate::plugin::RenderLayer {
52        crate::plugin::RenderLayer::Edges
53    }
54    fn render(&mut self, ctx: &mut crate::RenderContext) -> Option<gpui::AnyElement> {
55        let edges: Vec<_> = ctx
56            .graph
57            .edges
58            .iter()
59            .filter(|(_, edge)| ctx.is_edge_visible(edge))
60            .map(|(k, v)| (*k, edge_geometry2(v, &ctx)))
61            .collect();
62
63        let edge_ids: Vec<_> = edges.iter().map(|(id, _)| *id).collect();
64        for edge_id in edge_ids {
65            ctx.cache_port_offset_with_edge(&edge_id);
66        }
67
68        let selected_edges = ctx.graph.selected_edge.clone();
69        let stroke = ctx.theme.edge_stroke;
70        let stroke_sel = ctx.theme.edge_stroke_selected;
71
72        Some(
73            canvas(
74                move |_, _, _| (edges, selected_edges, stroke, stroke_sel),
75                move |_, (edges, selected_edges, stroke, stroke_sel), win, _| {
76                    for (id, geometry) in edges.iter() {
77                        let Some(EdgeGeometry { start, c1, c2, end }) = geometry else {
78                            return;
79                        };
80                        let mut line = PathBuilder::stroke(px(1.0));
81                        line.move_to(*start);
82                        line.cubic_bezier_to(*end, *c1, *c2);
83
84                        let selected = selected_edges.iter().find(|i| **i == *id).is_some();
85
86                        if let Ok(line) = line.build() {
87                            win.paint_path(
88                                line,
89                                rgb(if selected { stroke_sel } else { stroke }),
90                            );
91                        }
92                    }
93                },
94            )
95            .into_any(),
96        )
97    }
98}
99
100pub struct EdgeGeometry {
101    pub start: Point<Pixels>,
102    pub c1: Point<Pixels>,
103    pub c2: Point<Pixels>,
104    pub end: Point<Pixels>,
105}
106
107fn edge_geometry(edge: &Edge, ctx: &PluginContext) -> Option<EdgeGeometry> {
108    let Edge {
109        source_port: source_id,
110        target_port: target_id,
111        ..
112    } = edge;
113
114    let start = port_screen_position(*source_id, &ctx)?;
115    let end = port_screen_position(*target_id, &ctx)?;
116
117    let source_port = ctx.graph.ports.get(source_id)?;
118    let target_port = ctx.graph.ports.get(target_id)?;
119
120    let c1 = get_control_point(start, source_port.position, ctx.viewport);
121    let c2 = get_control_point(end, target_port.position, ctx.viewport);
122
123    Some(EdgeGeometry { start, c1, c2, end })
124}
125
126fn edge_geometry2(edge: &Edge, ctx: &RenderContext) -> Option<EdgeGeometry> {
127    let Edge {
128        source_port: source_id,
129        target_port: target_id,
130        ..
131    } = edge;
132
133    let start = port_screen_position2(*source_id, &ctx)?;
134    let end = port_screen_position2(*target_id, &ctx)?;
135
136    let source_port = ctx.graph.ports.get(source_id)?;
137    let target_port = ctx.graph.ports.get(target_id)?;
138
139    let c1 = get_control_point(start, source_port.position, ctx.viewport);
140    let c2 = get_control_point(end, target_port.position, ctx.viewport);
141
142    Some(EdgeGeometry { start, c1, c2, end })
143}
144
145fn port_screen_position(port_id: PortId, ctx: &PluginContext) -> Option<Point<Pixels>> {
146    let port = &ctx.graph.ports.get(&port_id)?;
147    let node = &ctx.nodes().get(&port.node_id)?;
148
149    let node_pos = node.point();
150
151    let offset = ctx.port_offset_cached(&port.node_id, &port_id)?;
152
153    Some(ctx.world_to_screen(node_pos + offset))
154}
155fn port_screen_position2(port_id: PortId, ctx: &RenderContext) -> Option<Point<Pixels>> {
156    let port = &ctx.graph.ports.get(&port_id)?;
157    let node = &ctx.nodes().get(&port.node_id)?;
158
159    let node_pos = node.point();
160
161    let offset = ctx.port_offset_cached(&port.node_id, &port_id)?;
162
163    Some(ctx.world_to_screen(node_pos + offset))
164}
165
166fn hit_test_get_edge(mouse: Point<Pixels>, ctx: &PluginContext) -> Option<EdgeId> {
167    let edges = ctx
168        .graph
169        .edges
170        .values()
171        .filter(|edge| ctx.is_edge_visible(edge));
172    for edge in edges {
173        let Some(geom) = edge_geometry(edge, ctx) else {
174            continue;
175        };
176
177        let bound = edge_bounds(&geom);
178        if !bound.contains(&mouse) {
179            continue;
180        }
181
182        if hit_test_edge(mouse, edge, ctx) {
183            return Some(edge.id);
184        }
185    }
186
187    None
188}
189
190pub fn edge_bounds(geom: &EdgeGeometry) -> Bounds<Pixels> {
191    let min_x = geom.start.x.min(geom.end.x).min(geom.c1.x).min(geom.c2.x);
192    let max_x = geom.start.x.max(geom.end.x).max(geom.c1.x).max(geom.c2.x);
193
194    let min_y = geom.start.y.min(geom.end.y).min(geom.c1.y).min(geom.c2.y);
195    let max_y = geom.start.y.max(geom.end.y).max(geom.c1.y).max(geom.c2.y);
196
197    Bounds::from_corners(
198        Point::new(min_x - px(10.0), min_y - px(10.0)),
199        Point::new(max_x + px(10.0), max_y + px(10.0)),
200    )
201}
202
203fn hit_test_edge(mouse: Point<Pixels>, edge: &Edge, ctx: &PluginContext) -> bool {
204    let Some(geom) = edge_geometry(edge, ctx) else {
205        return false;
206    };
207
208    let points = sample_bezier(&geom, 20);
209
210    for segment in points.windows(2) {
211        let d = distance_to_segment(mouse, segment[0], segment[1]);
212
213        if d < 8.0 {
214            return true;
215        }
216    }
217
218    false
219}
220
221fn sample_bezier(geom: &EdgeGeometry, steps: usize) -> Vec<Point<Pixels>> {
222    let mut points = Vec::new();
223
224    for i in 0..=steps {
225        let t = i as f32 / steps as f32;
226
227        let x = (1.0 - t).powi(3) * geom.start.x
228            + 3.0 * (1.0 - t).powi(2) * t * geom.c1.x
229            + 3.0 * (1.0 - t) * t * t * geom.c2.x
230            + t.powi(3) * geom.end.x;
231
232        let y = (1.0 - t).powi(3) * geom.start.y
233            + 3.0 * (1.0 - t).powi(2) * t * geom.c1.y
234            + 3.0 * (1.0 - t) * t * t * geom.c2.y
235            + t.powi(3) * geom.end.y;
236
237        points.push(Point::new(x, y));
238    }
239
240    points
241}
242pub fn distance_to_segment(p: Point<Pixels>, a: Point<Pixels>, b: Point<Pixels>) -> f32 {
243    let ap = vec_sub(p, a);
244    let ab = vec_sub(b, a);
245
246    let ab_len2 = ab.0 * ab.0 + ab.1 * ab.1;
247
248    if ab_len2 == 0.0 {
249        return vec_length(ap);
250    }
251
252    let t = (vec_dot(ap, ab) / ab_len2).clamp(0.0, 1.0);
253
254    let closest = Point::new(f32::from(a.x) + ab.0 * t, f32::from(a.y) + ab.1 * t);
255
256    let dx = f32::from(p.x) - closest.x;
257    let dy = f32::from(p.y) - closest.y;
258
259    (dx * dx + dy * dy).sqrt()
260}
261
262fn vec_sub(a: Point<Pixels>, b: Point<Pixels>) -> (f32, f32) {
263    (f32::from(a.x - b.x), f32::from(a.y - b.y))
264}
265
266fn vec_dot(a: (f32, f32), b: (f32, f32)) -> f32 {
267    a.0 * b.0 + a.1 * b.1
268}
269
270fn vec_length(v: (f32, f32)) -> f32 {
271    (v.0 * v.0 + v.1 * v.1).sqrt()
272}
273
274pub fn get_control_point(
275    source: Point<Pixels>,
276    position: PortPosition,
277    viewport: &Viewport,
278) -> Point<Pixels> {
279    match position {
280        PortPosition::Top => source - Point::new(px(0.0), px(50.0 * viewport.zoom)),
281        PortPosition::Left => source - Point::new(px(50.0 * viewport.zoom), px(0.0)),
282        PortPosition::Right => source + Point::new(px(50.0 * viewport.zoom), px(0.0)),
283        PortPosition::Bottom => source + Point::new(px(0.0), px(50.0 * viewport.zoom)),
284    }
285}