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        let alignment_guides = self.interaction.alignment_guides.as_ref();
280
281        for plugin in self.plugins_registry.plugins.iter_mut() {
282            let layer = plugin.render_layer();
283
284            let mut ctx = RenderContext::new(
285                graph,
286                port_offset_cache,
287                viewport,
288                renderder,
289                window,
290                layer,
291                alignment_guides,
292                theme,
293            );
294
295            if let Some(el) = plugin.render(&mut ctx) {
296                layers[layer.index()].push(el);
297            }
298        }
299
300        if let Some(i) = self.interaction.handler.as_ref() {
301            let mut ctx = RenderContext::new(
302                graph,
303                port_offset_cache,
304                viewport,
305                renderder,
306                window,
307                RenderLayer::Interaction,
308                alignment_guides,
309                theme,
310            );
311
312            if let Some(el) = i.render(&mut ctx) {
313                layers[RenderLayer::Interaction.index()].push(el);
314            }
315        }
316
317        if let Some(sync_plugin) = &mut self.sync_plugin {
318            let mut ctx = RenderContext::new(
319                graph,
320                port_offset_cache,
321                viewport,
322                renderder,
323                window,
324                RenderLayer::Overlay,
325                alignment_guides,
326                theme,
327            );
328            let els = sync_plugin.render(&mut ctx);
329            for el in els {
330                layers[RenderLayer::Overlay.index()].push(el);
331            }
332        }
333
334        div()
335            .id("ferrum_flow_canvas")
336            .size_full()
337            .track_focus(&self.focus_handle)
338            .on_key_down(window.listener_for(&entity, Self::on_key_down))
339            .on_key_up(window.listener_for(&entity, Self::on_key_up))
340            .on_mouse_down(
341                MouseButton::Left,
342                window.listener_for(&entity, Self::on_mouse_down),
343            )
344            .on_mouse_down(
345                MouseButton::Right,
346                window.listener_for(&entity, Self::on_mouse_down),
347            )
348            .on_mouse_move(window.listener_for(&entity, Self::on_mouse_move))
349            .on_hover(window.listener_for(&entity, Self::on_canvas_hover))
350            .on_mouse_up(
351                MouseButton::Left,
352                window.listener_for(&entity, Self::on_mouse_up),
353            )
354            .on_scroll_wheel(window.listener_for(&entity, Self::on_scroll_wheel))
355            .children(RenderLayer::ALL.iter().map(|layer| {
356                div()
357                    .absolute()
358                    .size_full()
359                    .children(layers[layer.index()].drain(..))
360            }))
361    }
362}
363
364pub struct FlowCanvasBuilder<'a, 'b> {
365    graph: Graph,
366    ctx: &'a mut Context<'b, FlowCanvas>,
367    window: &'a Window,
368
369    plugins: PluginRegistry,
370    renderers: RendererRegistry,
371    sync_plugin: Option<Box<dyn SyncPlugin + 'static>>,
372    theme: FlowTheme,
373}
374
375impl<'a, 'b> FlowCanvasBuilder<'a, 'b> {
376    /// register plugin
377    pub fn plugin(mut self, plugin: impl Plugin + 'static) -> Self {
378        self.plugins = self.plugins.add(plugin);
379        self
380    }
381
382    /// Registers the **core** plugin set for editing a node graph on the canvas: background,
383    /// selection, node drag, pan/zoom, node/edge rendering, port wiring, delete, and undo/redo
384    /// ([`BackgroundPlugin`], [`SelectionPlugin`], [`NodeInteractionPlugin`], [`ViewportPlugin`],
385    /// [`NodePlugin`], [`PortInteractionPlugin`], [`EdgePlugin`], [`DeletePlugin`], [`HistoryPlugin`]).
386    ///
387    /// Event order is determined by each plugin’s [`Plugin::priority`] when [`FlowCanvas::build`]
388    /// runs (not by the order of calls to [`.plugin`](Self::plugin)). Add minimap, clipboard,
389    /// context menu, etc. with [`.plugin`](Self::plugin) before or after this call.
390    pub fn plugins_core(mut self) -> Self {
391        self.plugins = self
392            .plugins
393            .add(BackgroundPlugin::new())
394            .add(SelectionPlugin::new())
395            .add(NodeInteractionPlugin::new())
396            .add(ViewportPlugin::new())
397            .add(NodePlugin::new())
398            .add(PortInteractionPlugin::new())
399            .add(EdgePlugin::new())
400            .add(DeletePlugin::new())
401            .add(HistoryPlugin::new());
402        self
403    }
404
405    pub fn sync_plugin(mut self, plugin: impl SyncPlugin + 'static) -> Self {
406        self.sync_plugin = Some(Box::new(plugin));
407        self
408    }
409
410    /// register node renderer
411    pub fn node_renderer<R>(mut self, name: impl Into<String>, renderer: R) -> Self
412    where
413        R: node_renderer::NodeRenderer + 'static,
414    {
415        self.renderers.register(name, renderer);
416        self
417    }
418
419    /// Replace the default [`FlowTheme`] before plugins run [`Plugin::setup`](crate::plugin::Plugin::setup).
420    pub fn theme(mut self, theme: FlowTheme) -> Self {
421        self.theme = theme;
422        self
423    }
424
425    pub fn build(self) -> FlowCanvas {
426        let focus_handle = self.ctx.focus_handle();
427        let drawable_size = self.window.viewport_size();
428
429        let mut canvas = FlowCanvas {
430            graph: self.graph,
431            viewport: Viewport::new(),
432            plugins_registry: self.plugins,
433            sync_plugin: self.sync_plugin,
434            renderers: self.renderers,
435            focus_handle,
436            interaction: InteractionState::new(),
437            history: Box::new(LocalHistory::new()),
438            event_queue: vec![],
439            port_offset_cache: PortLayoutCache::new(),
440            clipboard_subgraph: None,
441            theme: self.theme,
442        };
443
444        if let Some(sync_plugin) = &mut canvas.sync_plugin {
445            let (change_sender, mut change_receiver) = mpsc::unbounded::<GraphChange>();
446
447            self.ctx
448                .spawn(async move |this, ctx| {
449                    while let Some(change) = change_receiver.next().await {
450                        let _ = this.update(ctx, |this, cx| {
451                            this.graph.apply(change.kind);
452                            cx.notify();
453                        });
454                    }
455                })
456                .detach();
457            sync_plugin.setup(change_sender);
458        }
459
460        canvas
461            .plugins_registry
462            .plugins
463            .sort_by_key(|p| -p.priority());
464
465        {
466            let mut ctx = InitPluginContext {
467                graph: &mut canvas.graph,
468                port_offset_cache: &mut canvas.port_offset_cache,
469                viewport: &mut canvas.viewport,
470                renderers: &mut canvas.renderers,
471                gpui_ctx: &self.ctx,
472                drawable_size,
473                theme: &mut canvas.theme,
474                //notify: &mut notify,
475            };
476
477            for plugin in &mut canvas.plugins_registry.plugins {
478                plugin.setup(&mut ctx);
479            }
480        }
481
482        canvas
483    }
484}