Skip to main content

ferrum_flow/
plugin.rs

1use std::collections::HashMap;
2
3use gpui::{
4    AnyElement, Bounds, Context, Div, KeyDownEvent, KeyUpEvent, MouseDownEvent, MouseMoveEvent,
5    MouseUpEvent, Pixels, Point, ScrollWheelEvent, Size, Styled, Window, div, px, rgb,
6};
7
8use crate::{
9    Edge, EdgeBuilder, EdgeId, FlowCanvas, FlowTheme, Graph, Node, NodeBuilder, NodeId,
10    NodeRenderer, Port, PortId, RendererRegistry, Viewport,
11    alignment_guides::AlignmentGuides,
12    canvas::{
13        Command, CommandContext, HistoryProvider, Interaction, InteractionState, PortLayoutCache,
14    },
15    copied_subgraph::CopiedSubgraph,
16};
17
18mod sync;
19mod utils;
20
21pub use sync::SyncPlugin;
22
23pub use utils::{
24    cache_all_node_port_offset, cache_node_port_offset, cache_port_offset_with_edge,
25    cache_port_offset_with_port, is_edge_visible, is_node_visible, port_offset_cached,
26    primary_platform_modifier,
27};
28
29/// Chrome for [`RenderContext::node_card_shell`]. [`NodeCardVariant::Default`] and
30/// [`NodeCardVariant::UndefinedType`] read colors from [`RenderContext::theme`]; plugins may change
31/// them via [`InitPluginContext::theme`] / [`PluginContext::theme`].
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum NodeCardVariant {
34    /// Card from [`FlowTheme::node_card_background`] and border from [`FlowTheme::node_card_border`]
35    /// / [`FlowTheme::node_card_border_selected`] when `selected`.
36    Default,
37    /// Card from [`FlowTheme::undefined_node_background`] and [`FlowTheme::undefined_node_border`]
38    /// (no selection styling).
39    UndefinedType,
40
41    /// Geometry and border width only; set `.bg` / `.border_color` yourself.
42    Custom,
43}
44
45pub trait Plugin {
46    fn name(&self) -> &'static str;
47
48    fn setup(&mut self, _ctx: &mut InitPluginContext) {}
49
50    fn on_event(&mut self, _event: &FlowEvent, _ctx: &mut PluginContext) -> EventResult {
51        EventResult::Continue
52    }
53
54    fn render(&mut self, _ctx: &mut RenderContext) -> Option<AnyElement> {
55        None
56    }
57
58    fn priority(&self) -> i32 {
59        0
60    }
61
62    fn render_layer(&self) -> RenderLayer {
63        RenderLayer::Overlay
64    }
65}
66
67pub struct InitPluginContext<'a, 'b> {
68    pub graph: &'a mut Graph,
69    pub port_offset_cache: &'a mut PortLayoutCache,
70    pub viewport: &'a mut Viewport,
71    pub renderers: &'a mut RendererRegistry,
72    pub gpui_ctx: &'a Context<'b, FlowCanvas>,
73    /// Drawable size from the `window` passed to [`FlowCanvas::builder`] (`Window::viewport_size` when `build()` runs).
74    pub drawable_size: Size<Pixels>,
75    /// Canvas colors and strokes; mutate in [`Plugin::setup`](Plugin::setup) to customize chrome.
76    pub theme: &'a mut FlowTheme,
77    // pub notify: &'a mut dyn FnMut(),
78}
79
80impl<'a, 'b> InitPluginContext<'a, 'b> {
81    pub fn create_node(&self, node_type: &str) -> NodeBuilder {
82        self.graph.create_node(node_type)
83    }
84
85    pub fn create_edge(&self) -> EdgeBuilder {
86        self.graph.create_dege()
87    }
88
89    pub fn next_node_id(&self) -> NodeId {
90        self.graph.next_node_id()
91    }
92
93    pub fn next_port_id(&self) -> PortId {
94        self.graph.next_port_id()
95    }
96
97    pub fn next_edge_id(&self) -> EdgeId {
98        self.graph.next_edge_id()
99    }
100
101    pub fn add_node(&mut self, node: Node) {
102        self.graph.add_node(node);
103    }
104
105    pub fn add_port(&mut self, port: Port) {
106        self.graph.add_port(port);
107    }
108
109    pub fn get_node(&self, id: &NodeId) -> Option<&Node> {
110        self.graph.get_node(id)
111    }
112
113    pub fn get_node_mut(&mut self, id: &NodeId) -> Option<&mut Node> {
114        self.graph.get_node_mut(id)
115    }
116    pub fn remove_node(&mut self, id: &NodeId) {
117        self.graph.remove_node(id);
118    }
119    pub fn nodes(&self) -> &HashMap<NodeId, Node> {
120        self.graph.nodes()
121    }
122    pub fn node_order(&self) -> &Vec<NodeId> {
123        self.graph.node_order()
124    }
125
126    pub fn new_edge(&self) -> Edge {
127        self.graph.new_edge()
128    }
129
130    pub fn add_edge(&mut self, edge: Edge) {
131        self.graph.add_edge(edge);
132    }
133
134    pub fn remove_edge(&mut self, edge_id: EdgeId) {
135        self.graph.remove_edge(edge_id);
136    }
137
138    pub fn add_selected_node(&mut self, id: NodeId, shift: bool) {
139        self.graph.add_selected_node(id, shift);
140    }
141    pub fn clear_selected_node(&mut self) {
142        self.graph.clear_selected_node();
143    }
144    pub fn remove_selected_node(&mut self) -> bool {
145        self.graph.remove_selected_node()
146    }
147
148    pub fn add_selected_edge(&mut self, id: EdgeId, shift: bool) {
149        self.graph.add_selected_edge(id, shift);
150    }
151    pub fn clear_selected_edge(&mut self) {
152        self.graph.clear_selected_edge();
153    }
154    pub fn remove_selected_edge(&mut self) -> bool {
155        self.graph.remove_selected_edge()
156    }
157
158    pub fn selection_bounds(&self) -> Option<Bounds<Pixels>> {
159        self.graph.selection_bounds()
160    }
161
162    pub fn selected_nodes_with_positions(&self) -> HashMap<NodeId, Point<Pixels>> {
163        self.graph.selected_nodes_with_positions()
164    }
165
166    pub fn hit_node(&self, mouse: Point<Pixels>) -> Option<NodeId> {
167        self.graph.hit_node(mouse)
168    }
169
170    pub fn bring_node_to_front(&mut self, node_id: NodeId) {
171        self.graph.bring_node_to_front(node_id);
172    }
173
174    pub fn world_to_screen(&self, p: Point<Pixels>) -> Point<Pixels> {
175        self.viewport.world_to_screen(p)
176    }
177
178    pub fn screen_to_world(&self, p: Point<Pixels>) -> Point<Pixels> {
179        self.viewport.screen_to_world(p)
180    }
181
182    pub fn is_node_visible(&self, node_id: &NodeId) -> bool {
183        is_node_visible(self.graph, self.viewport, node_id)
184    }
185
186    pub fn is_edge_visible(&self, edge: &Edge) -> bool {
187        is_edge_visible(self.graph, self.viewport, edge)
188    }
189
190    pub fn port_offset_cached(&self, node_id: &NodeId, port_id: &PortId) -> Option<Point<Pixels>> {
191        port_offset_cached(self.port_offset_cache, node_id, port_id)
192    }
193
194    pub fn cache_port_offset_with_node(&mut self, node_ids: &Vec<NodeId>) {
195        for node_id in node_ids {
196            self.cache_node_port_offset(&node_id);
197        }
198    }
199
200    pub fn cache_port_offset_with_edge(&mut self, edge_id: &EdgeId) {
201        cache_port_offset_with_edge(self.graph, self.renderers, self.port_offset_cache, edge_id)
202    }
203
204    pub fn cache_port_offset_with_port(&mut self, port_id: &PortId) {
205        cache_port_offset_with_port(self.graph, self.renderers, self.port_offset_cache, port_id)
206    }
207
208    fn cache_node_port_offset(&mut self, node_id: &NodeId) {
209        cache_node_port_offset(self.graph, self.renderers, self.port_offset_cache, node_id);
210    }
211}
212
213pub struct PluginContext<'a> {
214    pub graph: &'a mut Graph,
215    pub port_offset_cache: &'a mut PortLayoutCache,
216    pub viewport: &'a mut Viewport,
217    pub(crate) interaction: &'a mut InteractionState,
218    pub renderers: &'a mut RendererRegistry,
219
220    sync_plugin: &'a mut Option<Box<dyn SyncPlugin + 'static>>,
221
222    pub history: &'a mut dyn HistoryProvider,
223    /// Shared node subgraph for [`crate::plugins::ClipboardPlugin`] and context menu.
224    pub(crate) clipboard_subgraph: &'a mut Option<CopiedSubgraph>,
225    /// Canvas theme; change during event handling and call [`PluginContext::notify`] to redraw.
226    pub theme: &'a mut FlowTheme,
227    emit: &'a mut dyn FnMut(FlowEvent),
228    notify: &'a mut dyn FnMut(),
229}
230
231pub enum EventResult {
232    Continue,
233    Stop,
234}
235
236impl<'a> PluginContext<'a> {
237    pub fn new(
238        graph: &'a mut Graph,
239        port_offset_cache: &'a mut PortLayoutCache,
240        viewport: &'a mut Viewport,
241        interaction: &'a mut InteractionState,
242        renderers: &'a mut RendererRegistry,
243        sync_plugin: &'a mut Option<Box<dyn SyncPlugin + 'static>>,
244        history: &'a mut dyn HistoryProvider,
245        clipboard_subgraph: &'a mut Option<CopiedSubgraph>,
246        theme: &'a mut FlowTheme,
247        emit: &'a mut dyn FnMut(FlowEvent),
248        notify: &'a mut dyn FnMut(),
249    ) -> Self {
250        Self {
251            graph,
252            port_offset_cache,
253            viewport,
254            interaction,
255            renderers,
256            sync_plugin,
257            history,
258            clipboard_subgraph,
259            theme,
260            emit,
261            notify,
262        }
263    }
264
265    pub fn start_interaction(&mut self, handler: impl Interaction + 'static) {
266        self.interaction.alignment_guides = None;
267        self.interaction.handler = Some(Box::new(handler));
268    }
269
270    pub fn cancel_interaction(&mut self) {
271        self.interaction.alignment_guides = None;
272        self.interaction.handler = None;
273    }
274
275    pub fn has_interaction(&self) -> bool {
276        self.interaction.handler.is_some()
277    }
278
279    /// Tell GPUI that this entity has changed and observers of it should be notified.
280    pub fn notify(&mut self) {
281        (self.notify)();
282    }
283
284    pub fn emit(&mut self, event: FlowEvent) {
285        (self.emit)(event);
286        self.notify();
287    }
288
289    pub fn has_sync_plugin(&self) -> bool {
290        self.sync_plugin.is_some()
291    }
292
293    pub fn execute_command(&mut self, command: impl Command + 'static) {
294        let mut ctx = CommandContext {
295            graph: self.graph,
296            port_offset_cache: self.port_offset_cache,
297            viewport: self.viewport,
298            renderers: self.renderers,
299            notify: self.notify,
300        };
301        if let Some(sync) = &mut self.sync_plugin {
302            let ops = command.to_ops(&mut ctx);
303            for op in ops.into_iter() {
304                sync.process_intent(op);
305            }
306            self.notify();
307        } else {
308            self.history.push(Box::new(command), &mut ctx);
309
310            self.notify();
311        }
312    }
313
314    pub fn undo(&mut self) {
315        if let Some(sync) = &mut self.sync_plugin {
316            sync.undo();
317        } else {
318            let mut ctx = CommandContext {
319                graph: self.graph,
320                port_offset_cache: self.port_offset_cache,
321                viewport: self.viewport,
322                renderers: self.renderers,
323                notify: self.notify,
324            };
325
326            self.history.undo(&mut ctx);
327
328            self.notify();
329        }
330    }
331
332    pub fn redo(&mut self) {
333        if let Some(sync) = &mut self.sync_plugin {
334            sync.redo();
335        } else {
336            let mut ctx = CommandContext {
337                graph: self.graph,
338                port_offset_cache: self.port_offset_cache,
339                viewport: self.viewport,
340                renderers: self.renderers,
341                notify: self.notify,
342            };
343
344            self.history.redo(&mut ctx);
345
346            self.notify();
347        }
348    }
349
350    pub fn create_node(&self, node_type: &str) -> NodeBuilder {
351        self.graph.create_node(node_type)
352    }
353
354    pub fn create_edge(&self) -> EdgeBuilder {
355        self.graph.create_dege()
356    }
357
358    pub fn next_node_id(&self) -> NodeId {
359        self.graph.next_node_id()
360    }
361
362    pub fn next_port_id(&self) -> PortId {
363        self.graph.next_port_id()
364    }
365
366    pub fn next_edge_id(&self) -> EdgeId {
367        self.graph.next_edge_id()
368    }
369
370    pub fn add_node(&mut self, node: Node) {
371        self.graph.add_node(node);
372    }
373
374    pub fn add_port(&mut self, port: Port) {
375        self.graph.add_port(port);
376    }
377
378    pub fn get_node(&self, id: &NodeId) -> Option<&Node> {
379        self.graph.get_node(id)
380    }
381
382    pub fn get_node_render(&self, id: &NodeId) -> Option<&dyn NodeRenderer> {
383        let node = self.get_node(id)?;
384
385        Some(self.renderers.get(&node.node_type))
386    }
387
388    pub fn get_node_mut(&mut self, id: &NodeId) -> Option<&mut Node> {
389        self.graph.get_node_mut(id)
390    }
391    pub fn remove_node(&mut self, id: &NodeId) {
392        self.graph.remove_node(id);
393        self.port_offset_cache.clear_node(id);
394    }
395    pub fn nodes(&self) -> &HashMap<NodeId, Node> {
396        self.graph.nodes()
397    }
398    pub fn node_order(&self) -> &Vec<NodeId> {
399        self.graph.node_order()
400    }
401
402    pub fn new_edge(&self) -> Edge {
403        self.graph.new_edge()
404    }
405
406    pub fn add_edge(&mut self, edge: Edge) {
407        self.graph.add_edge(edge);
408    }
409
410    pub fn remove_edge(&mut self, edge_id: EdgeId) {
411        self.graph.remove_edge(edge_id);
412    }
413
414    pub fn add_selected_node(&mut self, id: NodeId, shift: bool) {
415        self.graph.add_selected_node(id, shift);
416    }
417    pub fn clear_selected_node(&mut self) {
418        self.graph.clear_selected_node();
419    }
420    pub fn remove_selected_node(&mut self) -> bool {
421        self.graph.remove_selected_node()
422    }
423
424    pub fn add_selected_edge(&mut self, id: EdgeId, shift: bool) {
425        self.graph.add_selected_edge(id, shift);
426    }
427    pub fn clear_selected_edge(&mut self) {
428        self.graph.clear_selected_edge();
429    }
430    pub fn remove_selected_edge(&mut self) -> bool {
431        self.graph.remove_selected_edge()
432    }
433
434    pub fn selection_bounds(&self) -> Option<Bounds<Pixels>> {
435        self.graph.selection_bounds()
436    }
437
438    pub fn selected_nodes_with_positions(&self) -> HashMap<NodeId, Point<Pixels>> {
439        self.graph.selected_nodes_with_positions()
440    }
441
442    pub fn hit_node(&self, mouse: Point<Pixels>) -> Option<NodeId> {
443        self.graph.hit_node(mouse)
444    }
445
446    pub fn bring_node_to_front(&mut self, node_id: NodeId) {
447        self.graph.bring_node_to_front(node_id);
448    }
449
450    pub fn world_to_screen(&self, p: Point<Pixels>) -> Point<Pixels> {
451        self.viewport.world_to_screen(p)
452    }
453
454    pub fn screen_to_world(&self, p: Point<Pixels>) -> Point<Pixels> {
455        self.viewport.screen_to_world(p)
456    }
457
458    pub fn is_node_visible(&self, node_id: &NodeId) -> bool {
459        is_node_visible(self.graph, self.viewport, node_id)
460    }
461
462    pub fn is_edge_visible(&self, edge: &Edge) -> bool {
463        is_edge_visible(self.graph, self.viewport, edge)
464    }
465
466    pub fn port_offset_cached(&self, node_id: &NodeId, port_id: &PortId) -> Option<Point<Pixels>> {
467        port_offset_cached(self.port_offset_cache, node_id, port_id)
468    }
469
470    pub fn cache_all_node_port_offset(&mut self) {
471        cache_all_node_port_offset(self.graph, self.renderers, self.port_offset_cache);
472    }
473
474    pub fn cache_port_offset_with_node(&mut self, node_ids: &Vec<NodeId>) {
475        for node_id in node_ids {
476            self.cache_node_port_offset(&node_id);
477        }
478    }
479
480    pub fn cache_port_offset_with_edge(&mut self, edge_id: &EdgeId) {
481        cache_port_offset_with_edge(self.graph, self.renderers, self.port_offset_cache, edge_id)
482    }
483
484    pub fn cache_port_offset_with_port(&mut self, port_id: &PortId) {
485        cache_port_offset_with_port(self.graph, self.renderers, self.port_offset_cache, port_id)
486    }
487
488    fn cache_node_port_offset(&mut self, node_id: &NodeId) {
489        cache_node_port_offset(self.graph, self.renderers, self.port_offset_cache, node_id);
490    }
491}
492
493pub enum FlowEvent {
494    Input(InputEvent),
495    Custom(Box<dyn std::any::Any + Send>),
496}
497
498impl FlowEvent {
499    pub fn custom<T: 'static + Send>(event: T) -> Self {
500        FlowEvent::Custom(Box::new(event))
501    }
502    pub fn as_custom<T: 'static>(&self) -> Option<&T> {
503        match self {
504            FlowEvent::Custom(e) => e.downcast_ref::<T>(),
505            _ => None,
506        }
507    }
508}
509
510pub enum InputEvent {
511    KeyDown(KeyDownEvent),
512    KeyUp(KeyUpEvent),
513
514    MouseDown(MouseDownEvent),
515    MouseMove(MouseMoveEvent),
516    MouseUp(MouseUpEvent),
517
518    Wheel(ScrollWheelEvent),
519}
520
521pub struct RenderContext<'a> {
522    pub graph: &'a Graph,
523    pub port_offset_cache: &'a mut PortLayoutCache,
524    pub viewport: &'a Viewport,
525    pub renderers: &'a RendererRegistry,
526
527    pub window: &'a Window,
528
529    pub layer: RenderLayer,
530    /// Populated while dragging nodes when alignment matches other nodes.
531    pub alignment_guides: Option<&'a AlignmentGuides>,
532    /// Active canvas theme (from [`FlowCanvas::theme`](crate::canvas::FlowCanvas::theme)).
533    pub theme: &'a FlowTheme,
534}
535
536impl<'a> RenderContext<'a> {
537    pub fn new(
538        graph: &'a mut Graph,
539        port_offset_cache: &'a mut PortLayoutCache,
540        viewport: &'a Viewport,
541        renderers: &'a RendererRegistry,
542        window: &'a Window,
543        layer: RenderLayer,
544        alignment_guides: Option<&'a AlignmentGuides>,
545        theme: &'a FlowTheme,
546    ) -> Self {
547        Self {
548            graph,
549            port_offset_cache,
550            viewport,
551            renderers,
552            window,
553            layer,
554            alignment_guides,
555            theme,
556        }
557    }
558
559    pub fn create_node(&self, node_type: &str) -> NodeBuilder {
560        self.graph.create_node(node_type)
561    }
562
563    pub fn create_edge(&self) -> EdgeBuilder {
564        self.graph.create_dege()
565    }
566
567    pub fn next_node_id(&self) -> NodeId {
568        self.graph.next_node_id()
569    }
570
571    pub fn next_port_id(&self) -> PortId {
572        self.graph.next_port_id()
573    }
574
575    pub fn next_edge_id(&self) -> EdgeId {
576        self.graph.next_edge_id()
577    }
578
579    pub fn get_node(&self, id: &NodeId) -> Option<&Node> {
580        self.graph.get_node(id)
581    }
582
583    pub fn get_node_render(&self, id: &NodeId) -> Option<&dyn NodeRenderer> {
584        let node = self.get_node(id)?;
585
586        Some(self.renderers.get(&node.node_type))
587    }
588
589    pub fn nodes(&self) -> &HashMap<NodeId, Node> {
590        self.graph.nodes()
591    }
592    pub fn node_order(&self) -> &Vec<NodeId> {
593        self.graph.node_order()
594    }
595
596    pub fn new_edge(&self) -> Edge {
597        self.graph.new_edge()
598    }
599
600    pub fn selection_bounds(&self) -> Option<Bounds<Pixels>> {
601        self.graph.selection_bounds()
602    }
603
604    pub fn selected_nodes_with_positions(&self) -> HashMap<NodeId, Point<Pixels>> {
605        self.graph.selected_nodes_with_positions()
606    }
607
608    pub fn hit_node(&self, mouse: Point<Pixels>) -> Option<NodeId> {
609        self.graph.hit_node(mouse)
610    }
611
612    pub fn world_to_screen(&self, p: Point<Pixels>) -> Point<Pixels> {
613        self.viewport.world_to_screen(p)
614    }
615
616    /// Absolute-positioned node card shell: screen origin, zoom-scaled size, rounded rect and border.
617    ///
618    /// Chain `.child(...)` for the inner body, then `.into_any()` (see [`gpui::Element`]).
619    pub fn node_card_shell(&self, node: &Node, selected: bool, variant: NodeCardVariant) -> Div {
620        let screen = self.world_to_screen(node.point());
621        let z = self.viewport.zoom;
622        let base = div()
623            .absolute()
624            .left(screen.x)
625            .top(screen.y)
626            .w(node.size.width * z)
627            .h(node.size.height * z)
628            .rounded(px(6.0))
629            .border(px(1.5));
630        let t = self.theme;
631        match variant {
632            NodeCardVariant::Default => {
633                base.bg(rgb(t.node_card_background))
634                    .border_color(rgb(if selected {
635                        t.node_card_border_selected
636                    } else {
637                        t.node_card_border
638                    }))
639            }
640            NodeCardVariant::UndefinedType => base
641                .bg(rgb(t.undefined_node_background))
642                .border_color(rgb(t.undefined_node_border)),
643            NodeCardVariant::Custom => base,
644        }
645    }
646
647    pub fn screen_to_world(&self, p: Point<Pixels>) -> Point<Pixels> {
648        self.viewport.screen_to_world(p)
649    }
650
651    pub fn is_node_visible(&self, node_id: &NodeId) -> bool {
652        is_node_visible(self.graph, self.viewport, node_id)
653    }
654
655    pub fn is_edge_visible(&self, edge: &Edge) -> bool {
656        is_edge_visible(self.graph, self.viewport, edge)
657    }
658
659    pub fn port_offset_cached(&self, node_id: &NodeId, port_id: &PortId) -> Option<Point<Pixels>> {
660        port_offset_cached(self.port_offset_cache, node_id, port_id)
661    }
662
663    pub fn cache_all_node_port_offset(&mut self) {
664        for (node_id, _) in self.graph.nodes() {
665            self.cache_node_port_offset(node_id);
666        }
667    }
668
669    pub fn cache_port_offset_with_nodes(&mut self, node_ids: &Vec<NodeId>) {
670        for node_id in node_ids {
671            self.cache_node_port_offset(node_id);
672        }
673    }
674
675    pub fn cache_port_offset_with_edge(&mut self, edge_id: &EdgeId) {
676        cache_port_offset_with_edge(self.graph, self.renderers, self.port_offset_cache, edge_id)
677    }
678
679    pub fn cache_port_offset_with_port(&mut self, port_id: &PortId) {
680        cache_port_offset_with_port(self.graph, self.renderers, self.port_offset_cache, port_id)
681    }
682
683    fn cache_node_port_offset(&mut self, node_id: &NodeId) {
684        cache_node_port_offset(self.graph, self.renderers, self.port_offset_cache, node_id);
685    }
686}
687
688#[derive(Debug, PartialEq, Eq, Clone, Copy)]
689pub enum RenderLayer {
690    Background,
691    Edges,
692    Nodes,
693    Selection,
694    Interaction,
695    Overlay,
696}
697
698impl RenderLayer {
699    pub const ALL: [RenderLayer; 6] = [
700        RenderLayer::Background,
701        RenderLayer::Edges,
702        RenderLayer::Nodes,
703        RenderLayer::Selection,
704        RenderLayer::Interaction,
705        RenderLayer::Overlay,
706    ];
707    pub fn index(self) -> usize {
708        match self {
709            RenderLayer::Background => 0,
710            RenderLayer::Edges => 1,
711            RenderLayer::Nodes => 2,
712            RenderLayer::Selection => 3,
713            RenderLayer::Interaction => 4,
714            RenderLayer::Overlay => 5,
715        }
716    }
717}
718
719pub struct PluginRegistry {
720    pub plugins: Vec<Box<dyn Plugin>>,
721}
722
723impl PluginRegistry {
724    pub fn new() -> Self {
725        Self { plugins: vec![] }
726    }
727
728    pub fn add(mut self, plugin: impl Plugin + 'static) -> Self {
729        self.plugins.push(Box::new(plugin));
730        self
731    }
732}