ferrum_flow/plugins/
align.rs1use gpui::{Pixels, Point, px};
2
3use crate::{
4 NodeId,
5 plugin::{FlowEvent, Plugin, PluginContext, primary_platform_modifier},
6 plugins::node::DragNodesCommand,
7};
8
9pub struct AlignPlugin;
11
12#[derive(Clone, Copy)]
13enum AlignKind {
14 Left,
15 Right,
16 Top,
17 Bottom,
18 CenterH,
19 CenterV,
20}
21
22impl AlignPlugin {
23 pub fn new() -> Self {
24 Self
25 }
26}
27
28fn align_shortcut(ev: &gpui::KeyDownEvent) -> bool {
29 primary_platform_modifier(ev) && ev.keystroke.modifiers.shift
30}
31
32fn px_to_f32(p: Pixels) -> f32 {
33 p.into()
34}
35
36fn f32_neq(a: f32, b: f32) -> bool {
37 (a - b).abs() > 0.01
38}
39
40fn selected_nodes_ordered(ctx: &PluginContext) -> Vec<NodeId> {
41 ctx.graph
42 .node_order()
43 .iter()
44 .filter(|id| ctx.graph.selected_node.contains(id))
45 .copied()
46 .collect()
47}
48
49fn build_aligned_positions(
50 ctx: &PluginContext,
51 kind: AlignKind,
52) -> Option<(Vec<(NodeId, Point<Pixels>)>, Vec<(NodeId, Point<Pixels>)>)> {
53 let ids = selected_nodes_ordered(ctx);
54 if ids.len() < 2 {
55 return None;
56 }
57
58 let mut min_left = f32::INFINITY;
59 let mut max_right = f32::NEG_INFINITY;
60 let mut min_top = f32::INFINITY;
61 let mut max_bottom = f32::NEG_INFINITY;
62
63 for id in &ids {
64 let n = ctx.get_node(id)?;
65 let x = px_to_f32(n.x);
66 let y = px_to_f32(n.y);
67 let w = px_to_f32(n.size.width);
68 let h = px_to_f32(n.size.height);
69 min_left = min_left.min(x);
70 max_right = max_right.max(x + w);
71 min_top = min_top.min(y);
72 max_bottom = max_bottom.max(y + h);
73 }
74
75 let center_x = (min_left + max_right) / 2.0;
76 let center_y = (min_top + max_bottom) / 2.0;
77
78 let mut from = Vec::with_capacity(ids.len());
79 let mut to = Vec::with_capacity(ids.len());
80
81 for id in ids {
82 let n = ctx.get_node(&id)?;
83 let p = n.point();
84 from.push((id, p));
85
86 let x = px_to_f32(n.x);
87 let y = px_to_f32(n.y);
88 let w = px_to_f32(n.size.width);
89 let h = px_to_f32(n.size.height);
90
91 let (nx, ny) = match kind {
92 AlignKind::Left => (min_left, y),
93 AlignKind::Right => (max_right - w, y),
94 AlignKind::Top => (x, min_top),
95 AlignKind::Bottom => (x, max_bottom - h),
96 AlignKind::CenterH => (center_x - w / 2.0, y),
97 AlignKind::CenterV => (x, center_y - h / 2.0),
98 };
99 to.push((id, Point::new(px(nx), px(ny))));
100 }
101
102 let changed = from.iter().zip(to.iter()).any(|((_, pf), (_, pt))| {
103 f32_neq(px_to_f32(pf.x), px_to_f32(pt.x)) || f32_neq(px_to_f32(pf.y), px_to_f32(pt.y))
104 });
105 if !changed {
106 return None;
107 }
108
109 Some((from, to))
110}
111
112fn apply_align(ctx: &mut PluginContext, kind: AlignKind) {
113 let Some((from, to)) = build_aligned_positions(ctx, kind) else {
114 return;
115 };
116 ctx.execute_command(DragNodesCommand::from_positions(from, to));
117 ctx.cache_all_node_port_offset();
118}
119
120impl Plugin for AlignPlugin {
121 fn name(&self) -> &'static str {
122 "align"
123 }
124
125 fn setup(&mut self, _ctx: &mut crate::plugin::InitPluginContext) {}
126
127 fn priority(&self) -> i32 {
128 91
129 }
130
131 fn on_event(
132 &mut self,
133 event: &FlowEvent,
134 ctx: &mut PluginContext,
135 ) -> crate::plugin::EventResult {
136 if let FlowEvent::Input(crate::plugin::InputEvent::KeyDown(ev)) = event {
137 if !align_shortcut(ev) {
138 return crate::plugin::EventResult::Continue;
139 }
140 let kind = match ev.keystroke.key.as_str() {
141 "l" => Some(AlignKind::Left),
142 "r" => Some(AlignKind::Right),
143 "t" => Some(AlignKind::Top),
144 "b" => Some(AlignKind::Bottom),
145 "h" => Some(AlignKind::CenterH),
146 "v" => Some(AlignKind::CenterV),
147 _ => None,
148 };
149 if let Some(kind) = kind {
150 apply_align(ctx, kind);
151 return crate::plugin::EventResult::Stop;
152 }
153 }
154 crate::plugin::EventResult::Continue
155 }
156}