Skip to main content

ferrum_flow/plugins/node/
interaction.rs

1use std::collections::HashSet;
2use std::time::{Duration, Instant};
3
4use gpui::{MouseButton, Pixels, Point, px};
5
6use crate::{
7    NodeId,
8    canvas::{Interaction, InteractionResult},
9    plugin::{EventResult, FlowEvent, InitPluginContext, InputEvent, Plugin, PluginContext},
10    plugins::{
11        node::command::{DragNodesCommand, SelecteNodeCommand},
12        snap_guides::compute_alignment_guides,
13    },
14};
15
16const DRAG_THRESHOLD: Pixels = px(2.0);
17const DRAG_COMMAND_INTERVAL: Duration = Duration::from_millis(50);
18
19pub struct NodeInteractionPlugin;
20
21impl NodeInteractionPlugin {
22    pub fn new() -> Self {
23        Self
24    }
25}
26
27impl Plugin for NodeInteractionPlugin {
28    fn name(&self) -> &'static str {
29        "node_interaction"
30    }
31
32    fn setup(&mut self, _ctx: &mut InitPluginContext) {}
33
34    fn on_event(&mut self, event: &FlowEvent, ctx: &mut PluginContext) -> EventResult {
35        if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event {
36            if ev.button != MouseButton::Left {
37                return EventResult::Continue;
38            }
39            let mouse_world = ctx.screen_to_world(ev.position);
40
41            if let Some(node_id) = ctx.hit_node(mouse_world) {
42                ctx.start_interaction(NodeDragInteraction::start(
43                    node_id,
44                    mouse_world,
45                    ev.modifiers.shift,
46                ));
47
48                return EventResult::Stop;
49            } else {
50                ctx.clear_selected_node();
51            }
52        }
53
54        EventResult::Continue
55    }
56
57    fn priority(&self) -> i32 {
58        120
59    }
60}
61
62pub struct NodeDragInteraction {
63    state: NodeDragState,
64    last_drag_command_at: Option<Instant>,
65}
66
67enum NodeDragState {
68    Pending {
69        node_id: NodeId,
70        start_mouse: Point<Pixels>,
71        shift: bool,
72    },
73    Draging {
74        start_mouse: Point<Pixels>,
75        start_positions: Vec<(NodeId, Point<Pixels>)>,
76    },
77}
78
79impl NodeDragInteraction {
80    fn start(node_id: NodeId, start_mouse: Point<Pixels>, shift: bool) -> Self {
81        Self {
82            state: NodeDragState::Pending {
83                node_id,
84                start_mouse,
85                shift,
86            },
87            last_drag_command_at: None,
88        }
89    }
90}
91
92impl Interaction for NodeDragInteraction {
93    fn on_mouse_move(
94        &mut self,
95        ev: &gpui::MouseMoveEvent,
96        ctx: &mut PluginContext,
97    ) -> crate::canvas::InteractionResult {
98        match &self.state {
99            NodeDragState::Pending {
100                node_id,
101                start_mouse,
102                ..
103            } => {
104                ctx.interaction.alignment_guides = None;
105                let delta = ctx.screen_to_world(ev.position) - *start_mouse;
106                if delta.x.abs() > DRAG_THRESHOLD || delta.y.abs() > DRAG_THRESHOLD {
107                    let mut nodes = vec![];
108
109                    if ctx.graph.selected_node.contains(&node_id) {
110                        for id in &ctx.graph.selected_node {
111                            if let Some(node) = ctx.nodes().get(&id) {
112                                nodes.push((id.clone(), node.point()));
113                            }
114                        }
115                    } else {
116                        if let Some(node) = ctx.nodes().get(&node_id) {
117                            nodes.push((node_id.clone(), node.point()));
118                        }
119                    }
120                    self.state = NodeDragState::Draging {
121                        start_mouse: ev.position,
122                        start_positions: nodes,
123                    };
124
125                    ctx.notify();
126                }
127            }
128            NodeDragState::Draging {
129                start_mouse,
130                start_positions,
131            } => {
132                let dx = (ev.position.x - start_mouse.x) / ctx.viewport.zoom;
133                let dy = (ev.position.y - start_mouse.y) / ctx.viewport.zoom;
134                for (id, point) in start_positions.iter() {
135                    if let Some(node) = ctx.get_node_mut(id) {
136                        node.x = point.x + dx;
137                        node.y = point.y + dy;
138                    }
139                }
140
141                let dragged: HashSet<NodeId> =
142                    start_positions.iter().map(|(id, _)| *id).collect();
143                let guides = compute_alignment_guides(ctx.graph, ctx.viewport, &dragged);
144                ctx.interaction.alignment_guides =
145                    if guides.vertical_x.is_empty() && guides.horizontal_y.is_empty() {
146                        None
147                    } else {
148                        Some(guides)
149                    };
150
151                if ctx.has_sync_plugin() {
152                    let now = Instant::now();
153                    let should_command = self
154                        .last_drag_command_at
155                        .map(|t| now.duration_since(t) >= DRAG_COMMAND_INTERVAL)
156                        .unwrap_or(true);
157                    if should_command {
158                        ctx.execute_command(DragNodesCommand::new(start_positions, &ctx));
159                        self.last_drag_command_at = Some(now);
160                    }
161                }
162                ctx.notify();
163            }
164        }
165        InteractionResult::Continue
166    }
167    fn on_mouse_up(
168        &mut self,
169        _ev: &gpui::MouseUpEvent,
170        ctx: &mut PluginContext,
171    ) -> crate::canvas::InteractionResult {
172        ctx.interaction.alignment_guides = None;
173        match &self.state {
174            NodeDragState::Pending { node_id, shift, .. } => {
175                ctx.execute_command(SelecteNodeCommand::new(*node_id, *shift, ctx));
176                InteractionResult::End
177            }
178            NodeDragState::Draging {
179                start_positions, ..
180            } => {
181                ctx.execute_command(DragNodesCommand::new(start_positions, &ctx));
182                InteractionResult::End
183            }
184        }
185    }
186    fn render(&self, _ctx: &mut crate::plugin::RenderContext) -> Option<gpui::AnyElement> {
187        None
188    }
189}