Skip to main content

ferrum_flow/
canvas.rs

1use futures::{StreamExt, channel::mpsc};
2use gpui::*;
3
4use crate::{
5    BackgroundPlugin, DeletePlugin, EdgePlugin, FlowTheme, GraphChange, HistoryPlugin,
6    NodeInteractionPlugin, NodePlugin, PortInteractionPlugin, SelectionPlugin, SyncPlugin,
7    ViewportPlugin,
8    copied_subgraph::CopiedSubgraph,
9    graph::Graph,
10    plugin::{
11        EventResult, FlowEvent, InitPluginContext, InputEvent, Plugin, PluginContext,
12        PluginRegistry, RenderContext, RenderLayer,
13    },
14    viewport::Viewport,
15};
16
17mod node_renderer;
18mod port_cache;
19mod types;
20mod undo;
21
22pub use port_cache::PortLayoutCache;
23
24pub use undo::{Command, CommandContext, CompositeCommand, HistoryProvider, LocalHistory};
25
26pub use types::{Interaction, InteractionResult, InteractionState};
27
28pub use node_renderer::{
29    NodeRenderer, RendererRegistry, default_node_caption, port_screen_position,
30};
31
32pub struct FlowCanvas {
33    pub graph: Graph,
34
35    pub(crate) viewport: Viewport,
36
37    pub(crate) plugins_registry: PluginRegistry,
38
39    pub(crate) sync_plugin: Option<Box<dyn SyncPlugin + 'static>>,
40
41    renderers: RendererRegistry,
42
43    pub(crate) focus_handle: FocusHandle,
44
45    pub(crate) interaction: InteractionState,
46
47    pub history: Box<dyn HistoryProvider>,
48
49    pub event_queue: Vec<FlowEvent>,
50    pub port_offset_cache: PortLayoutCache,
51
52    /// Shared with [`crate::plugins::ClipboardPlugin`] and context menu.
53    pub(crate) clipboard_subgraph: Option<CopiedSubgraph>,
54
55    /// Visual tokens for canvas chrome; plugins adjust via [`InitPluginContext::theme`](crate::plugin::InitPluginContext::theme).
56    pub theme: FlowTheme,
57}
58
59// // TODO
60// impl Clone for FlowCanvas {
61//     fn clone(&self) -> Self {
62//         Self {
63//             graph: self.graph.clone(),
64//             viewport: self.viewport.clone(),
65//             plugins_registry: PluginRegistry::new(),
66//             focus_handle: self.focus_handle.clone(),
67//             interaction: InteractionState::new(),
68//             event_queue: vec![],
69//         }
70//     }
71// }
72
73impl FlowCanvas {
74    pub fn new(graph: Graph, cx: &mut Context<Self>) -> Self {
75        let focus_handle = cx.focus_handle();
76        Self {
77            graph,
78            viewport: Viewport::new(),
79            plugins_registry: PluginRegistry::new(),
80            sync_plugin: None,
81            renderers: RendererRegistry::new(),
82            focus_handle,
83            interaction: InteractionState::new(),
84            history: Box::new(LocalHistory::new()),
85            event_queue: vec![],
86            port_offset_cache: PortLayoutCache::new(),
87            clipboard_subgraph: None,
88            theme: FlowTheme::default(),
89        }
90    }
91
92    pub fn builder<'a, 'b>(
93        graph: Graph,
94        ctx: &'a mut Context<'b, Self>,
95        window: &'a Window,
96    ) -> FlowCanvasBuilder<'a, 'b> {
97        FlowCanvasBuilder {
98            graph,
99            ctx,
100            window,
101            plugins: PluginRegistry::new(),
102            sync_plugin: None,
103            renderers: RendererRegistry::new(),
104            theme: FlowTheme::default(),
105        }
106    }
107
108    pub fn handle_event(&mut self, event: FlowEvent, cx: &mut Context<Self>) {
109        let event_queue = &mut self.event_queue;
110
111        let mut emit = |event: FlowEvent| {
112            event_queue.push(event);
113        };
114
115        let mut notify = || {
116            cx.notify();
117        };
118
119        // if has interaction
120        if let Some(mut handler) = self.interaction.handler.take() {
121            let mut ctx = PluginContext::new(
122                &mut self.graph,
123                &mut self.port_offset_cache,
124                &mut self.viewport,
125                &mut self.interaction,
126                &mut self.renderers,
127                &mut self.sync_plugin,
128                self.history.as_mut(),
129                &mut self.clipboard_subgraph,
130                &mut self.theme,
131                &mut emit,
132                &mut notify,
133            );
134            let mut fast_return = false;
135            let result = match &event {
136                FlowEvent::Input(InputEvent::MouseMove(ev)) => {
137                    fast_return = true;
138                    handler.on_mouse_move(ev, &mut ctx)
139                }
140
141                FlowEvent::Input(InputEvent::MouseUp(ev)) => {
142                    fast_return = true;
143                    handler.on_mouse_up(ev, &mut ctx)
144                }
145
146                _ => InteractionResult::Continue,
147            };
148
149            if fast_return {
150                match result {
151                    InteractionResult::Continue => self.interaction.handler = Some(handler),
152
153                    InteractionResult::End => {
154                        self.interaction.handler = None;
155                    }
156
157                    InteractionResult::Replace(h) => {
158                        self.interaction.handler = Some(h);
159                    }
160                }
161                return;
162            }
163        }
164
165        let mut ctx = PluginContext::new(
166            &mut self.graph,
167            &mut self.port_offset_cache,
168            &mut self.viewport,
169            &mut self.interaction,
170            &mut self.renderers,
171            &mut self.sync_plugin,
172            self.history.as_mut(),
173            &mut self.clipboard_subgraph,
174            &mut self.theme,
175            &mut emit,
176            &mut notify,
177        );
178
179        for plugin in &mut self.plugins_registry.plugins {
180            let result = plugin.on_event(&event, &mut ctx);
181            match result {
182                EventResult::Continue => {}
183                EventResult::Stop => break,
184            }
185        }
186    }
187
188    fn process_event_queue(&mut self, cx: &mut Context<Self>) {
189        while let Some(event) = self.event_queue.pop() {
190            let mut emit = |e| self.event_queue.push(e);
191
192            let mut notify = || {
193                cx.notify();
194            };
195
196            let mut ctx = PluginContext::new(
197                &mut self.graph,
198                &mut self.port_offset_cache,
199                &mut self.viewport,
200                &mut self.interaction,
201                &mut self.renderers,
202                &mut self.sync_plugin,
203                self.history.as_mut(),
204                &mut self.clipboard_subgraph,
205                &mut self.theme,
206                &mut emit,
207                &mut notify,
208            );
209
210            for plugin in &mut self.plugins_registry.plugins {
211                let result = plugin.on_event(&event, &mut ctx);
212                match result {
213                    EventResult::Continue => {}
214                    EventResult::Stop => break,
215                }
216            }
217        }
218    }
219
220    fn on_key_down(&mut self, ev: &KeyDownEvent, _: &mut Window, cx: &mut Context<Self>) {
221        self.handle_event(FlowEvent::Input(InputEvent::KeyDown(ev.clone())), cx);
222        self.process_event_queue(cx);
223    }
224
225    fn on_key_up(&mut self, ev: &KeyUpEvent, _: &mut Window, cx: &mut Context<Self>) {
226        self.handle_event(FlowEvent::Input(InputEvent::KeyUp(ev.clone())), cx);
227        self.process_event_queue(cx);
228    }
229
230    fn on_mouse_down(&mut self, ev: &MouseDownEvent, _: &mut Window, cx: &mut Context<Self>) {
231        self.handle_event(FlowEvent::Input(InputEvent::MouseDown(ev.clone())), cx);
232        self.process_event_queue(cx);
233    }
234
235    fn on_mouse_move(&mut self, ev: &MouseMoveEvent, _: &mut Window, cx: &mut Context<Self>) {
236        self.handle_event(FlowEvent::Input(InputEvent::MouseMove(ev.clone())), cx);
237        if let Some(sync_plugin) = &mut self.sync_plugin {
238            let world = self.viewport.screen_to_world(ev.position);
239            sync_plugin.on_mouse_move(ev, world);
240        }
241        self.process_event_queue(cx);
242    }
243
244    fn on_mouse_up(&mut self, ev: &MouseUpEvent, _: &mut Window, cx: &mut Context<Self>) {
245        self.handle_event(FlowEvent::Input(InputEvent::MouseUp(ev.clone())), cx);
246        self.process_event_queue(cx);
247    }
248
249    fn on_scroll_wheel(&mut self, ev: &ScrollWheelEvent, _: &mut Window, cx: &mut Context<Self>) {
250        self.handle_event(FlowEvent::Input(InputEvent::Wheel(ev.clone())), cx);
251        self.process_event_queue(cx);
252    }
253
254    fn on_canvas_hover(&mut self, hovered: &bool, _: &mut Window, cx: &mut Context<Self>) {
255        if !*hovered {
256            if let Some(sync_plugin) = &mut self.sync_plugin {
257                sync_plugin.on_mouse_leave();
258            }
259            cx.notify();
260        }
261    }
262}
263
264impl Render for FlowCanvas {
265    fn render(&mut self, window: &mut Window, this_cx: &mut Context<Self>) -> impl IntoElement {
266        self.viewport.sync_drawable_bounds(window);
267
268        let entity = this_cx.entity();
269
270        let graph = &mut self.graph;
271        let viewport = &self.viewport;
272        let renderder = &self.renderers;
273        let port_offset_cache = &mut self.port_offset_cache;
274        let theme = &self.theme;
275
276        let mut layers: Vec<Vec<AnyElement>> =
277            (0..RenderLayer::ALL.len()).map(|_| Vec::new()).collect();
278
279        for plugin in self.plugins_registry.plugins.iter_mut() {
280            let layer = plugin.render_layer();
281
282            let mut ctx = RenderContext::new(
283                graph,
284                port_offset_cache,
285                viewport,
286                renderder,
287                window,
288                layer,
289                theme,
290            );
291
292            if let Some(el) = plugin.render(&mut ctx) {
293                layers[layer.index()].push(el);
294            }
295        }
296
297        if let Some(i) = self.interaction.handler.as_ref() {
298            let mut ctx = RenderContext::new(
299                graph,
300                port_offset_cache,
301                viewport,
302                renderder,
303                window,
304                RenderLayer::Interaction,
305                theme,
306            );
307
308            if let Some(el) = i.render(&mut ctx) {
309                layers[RenderLayer::Interaction.index()].push(el);
310            }
311        }
312
313        if let Some(sync_plugin) = &mut self.sync_plugin {
314            let mut ctx = RenderContext::new(
315                graph,
316                port_offset_cache,
317                viewport,
318                renderder,
319                window,
320                RenderLayer::Overlay,
321                theme,
322            );
323            let els = sync_plugin.render(&mut ctx);
324            for el in els {
325                layers[RenderLayer::Overlay.index()].push(el);
326            }
327        }
328
329        div()
330            .id("ferrum_flow_canvas")
331            .size_full()
332            .track_focus(&self.focus_handle)
333            .on_key_down(window.listener_for(&entity, Self::on_key_down))
334            .on_key_up(window.listener_for(&entity, Self::on_key_up))
335            .on_mouse_down(
336                MouseButton::Left,
337                window.listener_for(&entity, Self::on_mouse_down),
338            )
339            .on_mouse_down(
340                MouseButton::Right,
341                window.listener_for(&entity, Self::on_mouse_down),
342            )
343            .on_mouse_move(window.listener_for(&entity, Self::on_mouse_move))
344            .on_hover(window.listener_for(&entity, Self::on_canvas_hover))
345            .on_mouse_up(
346                MouseButton::Left,
347                window.listener_for(&entity, Self::on_mouse_up),
348            )
349            .on_scroll_wheel(window.listener_for(&entity, Self::on_scroll_wheel))
350            .children(RenderLayer::ALL.iter().map(|layer| {
351                div()
352                    .absolute()
353                    .size_full()
354                    .children(layers[layer.index()].drain(..))
355            }))
356    }
357}
358
359pub struct FlowCanvasBuilder<'a, 'b> {
360    graph: Graph,
361    ctx: &'a mut Context<'b, FlowCanvas>,
362    window: &'a Window,
363
364    plugins: PluginRegistry,
365    renderers: RendererRegistry,
366    sync_plugin: Option<Box<dyn SyncPlugin + 'static>>,
367    theme: FlowTheme,
368}
369
370impl<'a, 'b> FlowCanvasBuilder<'a, 'b> {
371    /// register plugin
372    pub fn plugin(mut self, plugin: impl Plugin + 'static) -> Self {
373        self.plugins = self.plugins.add(plugin);
374        self
375    }
376
377    /// Registers the **core** plugin set for editing a node graph on the canvas: background,
378    /// selection, node drag, pan/zoom, node/edge rendering, port wiring, delete, and undo/redo
379    /// ([`BackgroundPlugin`], [`SelectionPlugin`], [`NodeInteractionPlugin`], [`ViewportPlugin`],
380    /// [`NodePlugin`], [`PortInteractionPlugin`], [`EdgePlugin`], [`DeletePlugin`], [`HistoryPlugin`]).
381    ///
382    /// Event order is determined by each plugin’s [`Plugin::priority`] when [`FlowCanvas::build`]
383    /// runs (not by the order of calls to [`.plugin`](Self::plugin)). Add minimap, clipboard,
384    /// context menu, etc. with [`.plugin`](Self::plugin) before or after this call.
385    pub fn plugins_core(mut self) -> Self {
386        self.plugins = self
387            .plugins
388            .add(BackgroundPlugin::new())
389            .add(SelectionPlugin::new())
390            .add(NodeInteractionPlugin::new())
391            .add(ViewportPlugin::new())
392            .add(NodePlugin::new())
393            .add(PortInteractionPlugin::new())
394            .add(EdgePlugin::new())
395            .add(DeletePlugin::new())
396            .add(HistoryPlugin::new());
397        self
398    }
399
400    pub fn sync_plugin(mut self, plugin: impl SyncPlugin + 'static) -> Self {
401        self.sync_plugin = Some(Box::new(plugin));
402        self
403    }
404
405    /// register node renderer
406    pub fn node_renderer<R>(mut self, name: impl Into<String>, renderer: R) -> Self
407    where
408        R: node_renderer::NodeRenderer + 'static,
409    {
410        self.renderers.register(name, renderer);
411        self
412    }
413
414    /// Replace the default [`FlowTheme`] before plugins run [`Plugin::setup`](crate::plugin::Plugin::setup).
415    pub fn theme(mut self, theme: FlowTheme) -> Self {
416        self.theme = theme;
417        self
418    }
419
420    pub fn build(self) -> FlowCanvas {
421        let focus_handle = self.ctx.focus_handle();
422        let drawable_size = self.window.viewport_size();
423
424        let mut canvas = FlowCanvas {
425            graph: self.graph,
426            viewport: Viewport::new(),
427            plugins_registry: self.plugins,
428            sync_plugin: self.sync_plugin,
429            renderers: self.renderers,
430            focus_handle,
431            interaction: InteractionState::new(),
432            history: Box::new(LocalHistory::new()),
433            event_queue: vec![],
434            port_offset_cache: PortLayoutCache::new(),
435            clipboard_subgraph: None,
436            theme: self.theme,
437        };
438
439        if let Some(sync_plugin) = &mut canvas.sync_plugin {
440            let (change_sender, mut change_receiver) = mpsc::unbounded::<GraphChange>();
441
442            self.ctx
443                .spawn(async move |this, ctx| {
444                    while let Some(change) = change_receiver.next().await {
445                        let _ = this.update(ctx, |this, cx| {
446                            this.graph.apply(change.kind);
447                            cx.notify();
448                        });
449                    }
450                })
451                .detach();
452            sync_plugin.setup(change_sender);
453        }
454
455        canvas
456            .plugins_registry
457            .plugins
458            .sort_by_key(|p| -p.priority());
459
460        {
461            let mut ctx = InitPluginContext {
462                graph: &mut canvas.graph,
463                port_offset_cache: &mut canvas.port_offset_cache,
464                viewport: &mut canvas.viewport,
465                renderers: &mut canvas.renderers,
466                gpui_ctx: &self.ctx,
467                drawable_size,
468                theme: &mut canvas.theme,
469                //notify: &mut notify,
470            };
471
472            for plugin in &mut canvas.plugins_registry.plugins {
473                plugin.setup(&mut ctx);
474            }
475        }
476
477        canvas
478    }
479}