ferrum_flow/plugins/port/
interaction.rs1use 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#[derive(Clone, Copy)]
17struct PendingPortLink {
18 source_port: PortId,
19 end_world: Point<Pixels>,
20}
21
22#[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 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}