Skip to main content

ferrum_flow/plugins/port/
interaction.rs

1use gpui::{Element, MouseButton, Pixels, Point, canvas, px, rgb};
2
3use crate::{
4    PortId, PortKind, PortPosition,
5    canvas::Interaction,
6    plugin::{FlowEvent, InputEvent, Plugin, RenderContext},
7    plugins::port::{
8        edge_bezier, filled_disc_path, port_screen_big_bounds, port_screen_bounds,
9        port_screen_position,
10    },
11};
12
13use super::command::CreateEdge;
14
15/// Dangling link from a port to a world-space endpoint (shown with a dot until the user clicks it).
16#[derive(Clone, Copy)]
17struct PendingPortLink {
18    source_port: PortId,
19    end_world: Point<Pixels>,
20}
21
22/// Internal: interaction finished on empty canvas — queue for [`PortInteractionPlugin`].
23#[derive(Clone, Copy)]
24struct PendingLinkCommitted {
25    source_port: PortId,
26    end_world: Point<Pixels>,
27}
28
29pub struct PortInteractionPlugin {
30    pending: Option<PendingPortLink>,
31}
32
33impl PortInteractionPlugin {
34    pub fn new() -> Self {
35        Self { pending: None }
36    }
37
38    fn facing_position(p: PortPosition) -> PortPosition {
39        match p {
40            PortPosition::Left => PortPosition::Right,
41            PortPosition::Right => PortPosition::Left,
42            PortPosition::Top => PortPosition::Bottom,
43            PortPosition::Bottom => PortPosition::Top,
44        }
45    }
46
47    fn pending_dot_contains_screen(
48        ctx: &crate::plugin::PluginContext,
49        end_world: Point<Pixels>,
50        screen: Point<Pixels>,
51    ) -> bool {
52        let c = ctx.world_to_screen(end_world);
53        let dx: f32 = (screen.x - c.x).into();
54        let dy: f32 = (screen.y - c.y).into();
55        let rf: f32 = px(10.0).into();
56        dx * dx + dy * dy <= rf * rf
57    }
58
59    fn finish_pending_link(&mut self, ctx: &mut crate::plugin::PluginContext, p: PendingPortLink) {
60        let Some(source) = ctx.graph.ports.get(&p.source_port).cloned() else {
61            return;
62        };
63
64        let x: f32 = p.end_world.x.into();
65        let y: f32 = p.end_world.y.into();
66
67        let mut builder = ctx.create_node("");
68        builder = builder.position(x, y);
69        builder = match source.kind {
70            PortKind::Output => builder.input(),
71            PortKind::Input => builder.output(),
72        };
73
74        let (new_node, new_ports) = builder.only_build(ctx.graph);
75
76        let edge = match source.kind {
77            PortKind::Output => {
78                let Some(in_port) = new_node.inputs.first().copied() else {
79                    return;
80                };
81                ctx.new_edge().source(p.source_port).target(in_port)
82            }
83            PortKind::Input => {
84                let Some(out_port) = new_node.outputs.first().copied() else {
85                    return;
86                };
87                ctx.new_edge().source(out_port).target(p.source_port)
88            }
89        };
90
91        ctx.execute_command(super::command::CreateNode::new(new_node));
92        for port in new_ports {
93            ctx.execute_command(super::command::CreatePort::new(port));
94        }
95
96        ctx.execute_command(CreateEdge::new(edge));
97    }
98
99    fn paint_wire_and_dot(
100        win: &mut gpui::Window,
101        start: Point<Pixels>,
102        end: Point<Pixels>,
103        start_position: PortPosition,
104        target_position: PortPosition,
105        viewport: &crate::Viewport,
106        line_rgb: u32,
107        dot_rgb: u32,
108    ) {
109        if let Ok(path) = edge_bezier(start, start_position, target_position, end, viewport) {
110            win.paint_path(path, rgb(line_rgb));
111        }
112        if let Ok(dot) = filled_disc_path(end, px(6.0)) {
113            win.paint_path(dot, rgb(dot_rgb));
114        }
115    }
116}
117
118impl Plugin for PortInteractionPlugin {
119    fn name(&self) -> &'static str {
120        "port_interaction"
121    }
122    fn setup(&mut self, _ctx: &mut crate::plugin::InitPluginContext) {}
123    fn on_event(
124        &mut self,
125        event: &FlowEvent,
126        ctx: &mut crate::plugin::PluginContext,
127    ) -> crate::plugin::EventResult {
128        if let Some(p) = event.as_custom::<PendingLinkCommitted>() {
129            self.pending = Some(PendingPortLink {
130                source_port: p.source_port,
131                end_world: p.end_world,
132            });
133            return crate::plugin::EventResult::Stop;
134        }
135
136        if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event {
137            if ev.button != MouseButton::Left {
138                return crate::plugin::EventResult::Continue;
139            }
140            if let Some(pend) = self.pending {
141                if Self::pending_dot_contains_screen(ctx, pend.end_world, ev.position) {
142                    self.pending = None;
143                    self.finish_pending_link(ctx, pend);
144                    return crate::plugin::EventResult::Stop;
145                }
146            }
147
148            let mouse_world = ctx.viewport.screen_to_world(ev.position);
149            let port_hit = ctx
150                .graph
151                .ports
152                .iter()
153                .filter(|(_, port)| ctx.is_node_visible(&port.node_id))
154                .find(|(id, _)| match port_screen_bounds(**id, ctx) {
155                    Some(b) => b.contains(&mouse_world),
156                    None => false,
157                })
158                .map(|(_, p)| (p.id, p.position));
159
160            if let Some((port_id, position)) = port_hit {
161                self.pending = None;
162                ctx.start_interaction(PortConnecting {
163                    port_id,
164                    position,
165                    target_position: PortPosition::Left,
166                    mouse: Some(ev.position),
167                });
168                return crate::plugin::EventResult::Stop;
169            }
170
171            if self.pending.take().is_some() {
172                ctx.notify();
173            }
174        }
175
176        crate::plugin::EventResult::Continue
177    }
178    fn priority(&self) -> i32 {
179        125
180    }
181    fn render(&mut self, ctx: &mut RenderContext) -> Option<gpui::AnyElement> {
182        let p = self.pending.as_ref()?;
183        let start = port_screen_position(p.source_port, ctx)?;
184        let end = ctx.world_to_screen(p.end_world);
185        let source_port = ctx.graph.ports.get(&p.source_port)?;
186        let start_position = source_port.position;
187        let target_position = Self::facing_position(start_position);
188        let viewport = ctx.viewport.clone();
189        let line_rgb = ctx.theme.port_preview_line;
190        let dot_rgb = ctx.theme.port_preview_dot;
191
192        Some(
193            canvas(
194                move |_, _, _| (start_position, target_position, viewport, line_rgb, dot_rgb),
195                move |_, (sp, tp, vp, lr, dr), win, _| {
196                    Self::paint_wire_and_dot(win, start, end, sp, tp, &vp, lr, dr);
197                },
198            )
199            .into_any(),
200        )
201    }
202    fn render_layer(&self) -> crate::plugin::RenderLayer {
203        crate::plugin::RenderLayer::Interaction
204    }
205}
206
207struct PortConnecting {
208    port_id: PortId,
209    position: PortPosition,
210    target_position: PortPosition,
211    /// Cursor in **screen** space (matches port_screen_position / bezier end).
212    mouse: Option<Point<Pixels>>,
213}
214
215impl Interaction for PortConnecting {
216    fn on_mouse_move(
217        &mut self,
218        event: &gpui::MouseMoveEvent,
219        ctx: &mut crate::plugin::PluginContext,
220    ) -> crate::canvas::InteractionResult {
221        self.mouse = Some(event.position);
222        let mouse_world = ctx.screen_to_world(event.position);
223        if let Some(port) = ctx
224            .graph
225            .ports
226            .iter()
227            .filter(|(_, port)| ctx.is_node_visible(&port.node_id))
228            .find(|(id, _)| match port_screen_big_bounds(**id, ctx) {
229                Some(b) => b.contains(&mouse_world),
230                None => false,
231            })
232            .map(|(_, p)| p)
233        {
234            if port.id != self.port_id {
235                self.target_position = port.position;
236            }
237        }
238        ctx.notify();
239        crate::canvas::InteractionResult::Continue
240    }
241    fn on_mouse_up(
242        &mut self,
243        ev: &gpui::MouseUpEvent,
244        ctx: &mut crate::plugin::PluginContext,
245    ) -> crate::canvas::InteractionResult {
246        let mouse_world = ctx.screen_to_world(ev.position);
247        if let Some((node_id, port_id)) = ctx
248            .graph
249            .ports
250            .iter()
251            .filter(|(_, port)| ctx.is_node_visible(&port.node_id))
252            .find(|(id, _)| match port_screen_bounds(**id, ctx) {
253                Some(b) => b.contains(&mouse_world),
254                None => false,
255            })
256            .map(|(_, p)| (p.node_id, p.id))
257        {
258            let source_node = ctx.graph.ports[&self.port_id].node_id;
259            if node_id == source_node {
260                ctx.cancel_interaction();
261                ctx.notify();
262                return crate::canvas::InteractionResult::End;
263            }
264            let connecting_port = &ctx.graph.ports[&self.port_id];
265            let target_port = &ctx.graph.ports[&port_id];
266            if connecting_port.kind == target_port.kind {
267                ctx.cancel_interaction();
268                ctx.notify();
269                return crate::canvas::InteractionResult::End;
270            }
271            let edge = match (connecting_port.kind, target_port.kind) {
272                (PortKind::Output, PortKind::Input) => {
273                    ctx.new_edge().source(self.port_id).target(port_id)
274                }
275                (PortKind::Input, PortKind::Output) => {
276                    ctx.new_edge().source(port_id).target(self.port_id)
277                }
278                _ => {
279                    ctx.cancel_interaction();
280                    ctx.notify();
281                    return crate::canvas::InteractionResult::End;
282                }
283            };
284
285            ctx.execute_command(CreateEdge::new(edge));
286            return crate::canvas::InteractionResult::End;
287        }
288
289        ctx.emit(FlowEvent::custom(PendingLinkCommitted {
290            source_port: self.port_id,
291            end_world: mouse_world,
292        }));
293        crate::canvas::InteractionResult::End
294    }
295    fn render(&self, ctx: &mut RenderContext) -> Option<gpui::AnyElement> {
296        let mouse = self.mouse?;
297        let start = port_screen_position(self.port_id, ctx)?;
298        let position = self.position;
299        let target_position = self.target_position;
300        let viewport = ctx.viewport.clone();
301        let line_rgb = ctx.theme.port_preview_line;
302        let dot_rgb = ctx.theme.port_preview_dot;
303
304        Some(
305            canvas(
306                move |_, _, _| (position, target_position, viewport, line_rgb, dot_rgb),
307                move |_, (position, target_position, viewport, lr, dr), win, _| {
308                    PortInteractionPlugin::paint_wire_and_dot(
309                        win,
310                        start,
311                        mouse,
312                        position,
313                        target_position,
314                        &viewport,
315                        lr,
316                        dr,
317                    );
318                },
319            )
320            .into_any(),
321        )
322    }
323}