Skip to main content

ferrum_flow/plugins/selection/
mod.rs

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