ferrum_flow/plugins/selection/
mod.rs1use std::collections::HashMap;
2use std::time::{Duration, Instant};
3
4use gpui::{
5 AnyElement, Bounds, Element, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, Point, Size,
6 Styled, div, px, rgb, rgba,
7};
8
9use crate::{
10 FlowTheme, Graph, Node, NodeId,
11 canvas::{Interaction, InteractionResult},
12 plugin::{
13 EventResult, FlowEvent, InputEvent, Plugin, PluginContext, RenderContext, RenderLayer,
14 },
15};
16
17const DRAG_THRESHOLD: Pixels = px(2.0);
18const DRAG_COMMAND_INTERVAL: Duration = Duration::from_millis(50);
19
20pub struct SelectionPlugin {
21 selected: Option<Selected>,
22}
23
24struct Selected {
25 bounds: Bounds<Pixels>,
26 nodes: HashMap<NodeId, Point<Pixels>>,
27}
28
29impl SelectionPlugin {
30 pub fn new() -> Self {
31 Self { selected: None }
32 }
33}
34
35impl Plugin for SelectionPlugin {
36 fn name(&self) -> &'static str {
37 "selection"
38 }
39 fn setup(&mut self, _ctx: &mut crate::plugin::InitPluginContext) {}
40 fn on_event(
41 &mut self,
42 event: &FlowEvent,
43 ctx: &mut crate::plugin::PluginContext,
44 ) -> EventResult {
45 if let FlowEvent::Input(InputEvent::MouseDown(ev)) = event {
46 if ev.button != MouseButton::Left {
47 return EventResult::Continue;
48 }
49 if !ev.modifiers.shift {
50 let start = ctx.viewport.screen_to_world(ev.position);
51 if let Some(Selected { bounds, nodes }) = self.selected.take() {
52 if bounds.contains(&start) {
53 ctx.start_interaction(SelectionInteraction::start_move(
54 start, bounds, nodes,
55 ));
56
57 return EventResult::Stop;
58 }
59 }
60
61 ctx.start_interaction(SelectionInteraction::new(start));
62 return EventResult::Stop;
63 }
64 } else if let Some(SelectedEvent { bounds, nodes }) = event.as_custom() {
65 self.selected = Some(Selected {
66 bounds: *bounds,
67 nodes: nodes.clone(),
68 });
69 return EventResult::Stop;
70 }
71
72 EventResult::Continue
73 }
74 fn priority(&self) -> i32 {
75 100
76 }
77 fn render_layer(&self) -> RenderLayer {
78 RenderLayer::Selection
79 }
80 fn render(&mut self, ctx: &mut RenderContext) -> Option<AnyElement> {
81 self.selected.as_ref().map(|Selected { bounds, .. }| {
82 let top_left = ctx.world_to_screen(bounds.origin);
83
84 let size = Size::new(
85 bounds.size.width * ctx.viewport.zoom,
86 bounds.size.height * ctx.viewport.zoom,
87 );
88 render_rect(Bounds::new(top_left, size), ctx.theme)
89 })
90 }
91}
92
93pub struct SelectionInteraction {
94 state: SelectionState,
95 last_drag_command_at: Option<Instant>,
96}
97
98enum SelectionState {
99 Pending {
100 start: Point<Pixels>,
101 },
102 Selecting {
103 start: Point<Pixels>,
104 end: Point<Pixels>,
105 },
106 Moving {
107 start_mouse: Point<Pixels>,
108 start_bounds: Bounds<Pixels>,
109 bounds: Bounds<Pixels>,
110 nodes: HashMap<NodeId, Point<Pixels>>,
111 },
112}
113struct SelectedEvent {
114 bounds: Bounds<Pixels>,
115 nodes: HashMap<NodeId, Point<Pixels>>,
116}
117
118impl SelectionInteraction {
119 pub fn new(start: Point<Pixels>) -> Self {
120 Self {
121 state: SelectionState::Pending { start },
122 last_drag_command_at: None,
123 }
124 }
125 pub fn start_move(
126 mouse: Point<Pixels>,
127 bounds: Bounds<Pixels>,
128 nodes: HashMap<NodeId, Point<Pixels>>,
129 ) -> Self {
130 Self {
131 state: SelectionState::Moving {
132 start_mouse: mouse,
133 start_bounds: bounds.clone(),
134 bounds: bounds,
135 nodes,
136 },
137 last_drag_command_at: None,
138 }
139 }
140}
141
142impl Interaction for SelectionInteraction {
143 fn on_mouse_move(&mut self, ev: &MouseMoveEvent, ctx: &mut PluginContext) -> InteractionResult {
144 let mouse_world = ctx.screen_to_world(ev.position);
145 match &mut self.state {
146 SelectionState::Pending { start } => {
147 let delta = mouse_world - *start;
148
149 if delta.x.abs() > DRAG_THRESHOLD && delta.y.abs() > DRAG_THRESHOLD {
150 self.state = SelectionState::Selecting {
151 start: *start,
152 end: mouse_world,
153 };
154
155 ctx.notify();
156 }
157 }
158
159 SelectionState::Selecting { end, .. } => {
160 *end = mouse_world;
161 ctx.notify();
162 }
163
164 SelectionState::Moving {
165 start_mouse,
166 start_bounds,
167 bounds,
168 nodes,
169 } => {
170 let delta = mouse_world - *start_mouse;
171
172 for (id, start_pos) in nodes.iter_mut() {
173 if let Some(node) = ctx.get_node_mut(id) {
174 node.x = start_pos.x + delta.x;
175 node.y = start_pos.y + delta.y;
176 }
177 }
178 *bounds = Bounds::new(start_bounds.origin + delta, start_bounds.size);
179
180 if ctx.has_sync_plugin() {
181 let now = Instant::now();
182 let should_command = self
183 .last_drag_command_at
184 .map(|t| now.duration_since(t) >= DRAG_COMMAND_INTERVAL)
185 .unwrap_or(true);
186 if should_command {
187 let start_position: Vec<_> = nodes
188 .iter()
189 .map(|(id, point)| (id.clone(), point.clone()))
190 .collect();
191 ctx.execute_command(super::node::DragNodesCommand::new(
192 &start_position,
193 &ctx,
194 ));
195 self.last_drag_command_at = Some(now);
196 }
197 }
198
199 ctx.notify();
200 }
201 }
202
203 InteractionResult::Continue
204 }
205 fn on_mouse_up(&mut self, _ev: &MouseUpEvent, ctx: &mut PluginContext) -> InteractionResult {
206 match &mut self.state {
207 SelectionState::Pending { .. } => {
208 return InteractionResult::End;
209 }
210
211 SelectionState::Selecting { start, end } => {
212 let rect = normalize_rect(*start, *end);
213
214 let mut selected = HashMap::new();
215
216 ctx.clear_selected_node();
217
218 for node in ctx.graph.nodes().values() {
219 let bounds = node_world_bounds(node);
220
221 if rect.intersects(&bounds) {
222 selected.insert(node.id, node.point());
223 }
224 }
225
226 for (id, _) in selected.iter() {
227 ctx.add_selected_node(*id, true);
228 }
229
230 let bounds = compute_nodes_bounds(&selected, ctx.graph);
231
232 ctx.cancel_interaction();
233 ctx.emit(FlowEvent::custom(SelectedEvent {
234 bounds,
235 nodes: selected,
236 }));
237
238 return InteractionResult::End;
239 }
240
241 SelectionState::Moving { bounds, nodes, .. } => {
242 let bounds = *bounds;
243
244 let mut new_nodes = HashMap::new();
245 for (id, _) in nodes.iter() {
246 ctx.add_selected_node(*id, true);
247 if let Some(node) = ctx.graph.nodes().get(id) {
248 new_nodes.insert(id.clone(), node.point());
249 }
250 }
251
252 let start_position: Vec<_> = nodes
253 .iter()
254 .map(|(id, point)| (id.clone(), point.clone()))
255 .collect();
256
257 ctx.execute_command(super::node::DragNodesCommand::new(&start_position, ctx));
258
259 ctx.emit(FlowEvent::custom(SelectedEvent {
260 bounds,
261 nodes: new_nodes,
262 }));
263
264 return InteractionResult::End;
265 }
266 }
267 }
268 fn render(&self, ctx: &mut RenderContext) -> Option<AnyElement> {
269 match &self.state {
270 SelectionState::Selecting { start, end } => {
271 let rect = normalize_rect(*start, *end);
272
273 let top_left = ctx.world_to_screen(rect.origin);
274
275 let size = Size::new(
276 rect.size.width * ctx.viewport.zoom,
277 rect.size.height * ctx.viewport.zoom,
278 );
279
280 Some(render_rect(Bounds::new(top_left, size), ctx.theme))
281 }
282
283 SelectionState::Moving { bounds, .. } => {
284 let top_left = ctx.world_to_screen(bounds.origin);
285
286 let size = Size::new(
287 bounds.size.width * ctx.viewport.zoom,
288 bounds.size.height * ctx.viewport.zoom,
289 );
290 Some(render_rect(Bounds::new(top_left, size), ctx.theme))
291 }
292
293 _ => None,
294 }
295 }
296}
297
298fn normalize_rect(start: Point<Pixels>, end: Point<Pixels>) -> Bounds<Pixels> {
299 let x = start.x.min(end.x);
300 let y = start.y.min(end.y);
301
302 let w = (end.x - start.x).abs();
303 let h = (end.y - start.y).abs();
304
305 Bounds::new(Point::new(x, y), Size::new(w, h))
306}
307
308fn render_rect(bounds: Bounds<Pixels>, theme: &FlowTheme) -> AnyElement {
309 div()
310 .absolute()
311 .left(bounds.origin.x)
312 .top(bounds.origin.y)
313 .w(bounds.size.width)
314 .h(bounds.size.height)
315 .border(px(1.0))
316 .border_color(rgb(theme.selection_rect_border))
317 .bg(rgba(theme.selection_rect_fill_rgba))
318 .into_any()
319}
320
321fn node_world_bounds(node: &Node) -> Bounds<Pixels> {
322 Bounds::new(Point::new(node.x, node.y), node.size)
323}
324
325fn compute_nodes_bounds(nodes: &HashMap<NodeId, Point<Pixels>>, graph: &Graph) -> Bounds<Pixels> {
326 let mut min_x = f32::MAX;
327 let mut min_y = f32::MAX;
328 let mut max_x = f32::MIN;
329 let mut max_y = f32::MIN;
330
331 for id in nodes.keys() {
332 let Some(node) = &graph.nodes().get(id) else {
333 continue;
334 };
335
336 min_x = min_x.min(node.x.into());
337 min_y = min_y.min(node.y.into());
338
339 max_x = max_x.max((node.x + node.size.width).into());
340 max_y = max_y.max((node.y + node.size.height).into());
341 }
342
343 Bounds::new(
344 Point::new(px(min_x), px(min_y)),
345 Size::new(px(max_x - min_x), px(max_y - min_y)),
346 )
347}