ferrum_flow/plugins/node/
interaction.rs1use 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}