Skip to main content

ferrum_flow/plugins/
align.rs

1use gpui::{Pixels, Point, px};
2
3use crate::{
4    NodeId,
5    plugin::{FlowEvent, Plugin, PluginContext, primary_platform_modifier},
6    plugins::node::DragNodesCommand,
7};
8
9/// Align selected nodes to their shared bounding box (⌘⇧L/R/T/B/H/V or Ctrl⇧…).
10pub 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}