ferrum_flow/plugins/edge/
mod.rs1use 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}