ferrum_flow/plugins/selection/
mod.rs1use 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}