Skip to main content

blockworx/tools/
move_multi_pin.rs

1use crate::theme::Style;
2use blockworx_geom::{Pos2, Rect, Vec2};
3use blockworx_paint::{Canvas, Event, Interaction};
4
5use crate::{
6    edit::geometry::PinMove,
7    grid::{PIN_PITCH, max_pin_slot},
8    shape::pin::PinSide,
9    state::RenderMode,
10    theme::Role,
11    tools::{
12        MultiPinSelect,
13        multi_pin_select::SELECTION_RING_ROUNDING,
14        names::ToolName,
15        tool::{Action, Supposing, ToolTrait},
16    },
17    widget::{DrawingPasses, drawing::Drawing},
18};
19use blockworx_doc::{geometry::PinSlot, id::PinId};
20
21/// Rigidly relocate a group of selected child-block pins along their block
22/// edges, entered by dragging any pin of a [`MultiPinSelect`]. The whole group
23/// shifts by the same number of slots (clamped so it stays in-bounds) and each
24/// pin takes the side under the cursor. The drop commits only if the new
25/// placement is collision-free; either way it returns to `MultiPinSelect` with
26/// the same pins still selected.
27pub struct MoveMultiPin {
28    pins: Vec<PinId>,
29    delta_pos: Vec2,
30    cursor: Pos2,
31}
32
33/// Whether the pins under the cursor can land where they are being dragged.
34#[derive(Clone, Copy, PartialEq, Eq)]
35enum DropValidity {
36    Valid,
37    Invalid,
38}
39
40impl MoveMultiPin {
41    pub fn new(pins: Vec<PinId>, start: Pos2) -> Self {
42        Self {
43            pins,
44            delta_pos: Vec2::ZERO,
45            cursor: start,
46        }
47    }
48
49    /// The destination slot for each pin at the current drag. `delta_pos.y`
50    /// becomes a uniform slot shift, clamped to the tightest range that keeps
51    /// every pin on its block, so the group moves rigidly.
52    fn moves(&self, data: &Drawing) -> Vec<PinMove> {
53        let mut info: Vec<(PinId, Rect, i32, PinSide)> = Vec::new();
54        let (mut lower, mut upper) = (i32::MIN, i32::MAX);
55        for &anchor in &self.pins {
56            let Some((shape, pin)) = data.pin_on_shape(anchor) else {
57                continue;
58            };
59            let rect = shape.gui_rect();
60            let slot = crate::shape::pin::slot(pin);
61            let old = slot.offset as i32;
62            lower = lower.max(-old);
63            upper = upper.min(max_pin_slot(rect.height()) as i32 - old);
64            info.push((anchor, rect, old, slot.side));
65        }
66        let raw = (self.delta_pos.y / PIN_PITCH).round() as i32;
67        let delta = if lower <= upper {
68            raw.clamp(lower, upper)
69        } else {
70            0
71        };
72        // A group spanning both edges can't coherently pick one side from a
73        // single cursor x, so its drag is vertical only: each pin keeps its own
74        // side. A single-side group still flips to the side under the cursor.
75        let first_side = info.first().map(|&(.., side)| side);
76        let mixed_sides = info.iter().any(|&(.., side)| Some(side) != first_side);
77        info.into_iter()
78            .map(|(pin, rect, old, side)| {
79                let side = if mixed_sides {
80                    side
81                } else if self.cursor.x < rect.center().x {
82                    PinSide::West
83                } else {
84                    PinSide::East
85                };
86                PinMove {
87                    pin,
88                    to: PinSlot {
89                        side,
90                        offset: (old + delta).max(0) as u32,
91                    },
92                }
93            })
94            .collect()
95    }
96}
97
98impl ToolTrait for MoveMultiPin {
99    fn name(&self) -> ToolName {
100        ToolName::MoveMultiPin
101    }
102
103    fn suppose<C: Canvas>(
104        &mut self,
105        data: &mut Drawing,
106        interaction: &Interaction,
107        _painter: &mut Style<'_, C>,
108        phase: &Supposing,
109    ) {
110        // The release advances the drag like any other frame: it paints before
111        // it commits, so the placement it drops on has to be the one it draws.
112        match interaction.event {
113            Some(Event::Dragging { pos, delta }) => {
114                self.delta_pos += delta;
115                self.cursor = pos;
116            }
117            Some(Event::DragStopped { pos }) => self.cursor = pos,
118            _ => {}
119        }
120        // Preview the connected routes live so they track the group. When the
121        // placement is invalid, put the routes back on the pins' real positions —
122        // matching the single-pin drag, which only re-routes to a valid slot. An
123        // override-free preview restores the geometry without a commit pass, so
124        // the drag never writes the document.
125        let moves = self.moves(data);
126        if data.can_relocate_pins(&moves) {
127            data.suppose_pin_drags(phase, &moves);
128        } else {
129            data.suppose_pin_drags(phase, &[]);
130        }
131    }
132
133    fn widget<C: Canvas>(
134        &mut self,
135        data: &mut Drawing,
136        interaction: &Interaction,
137        painter: &mut Style<'_, C>,
138    ) -> Option<Action> {
139        let moves = self.moves(data);
140        let validity = if data.can_relocate_pins(&moves) {
141            DropValidity::Valid
142        } else {
143            DropValidity::Invalid
144        };
145
146        if let Some(Event::DragStopped { .. }) = interaction.event {
147            if validity == DropValidity::Valid {
148                data.relocate_pins(&moves);
149            }
150            // Paint the committed state this frame so the pins don't flash before
151            // the tool switches away next frame.
152            crate::widget::display::widget(data, interaction, painter);
153            return Some(Action::SwitchTool(
154                MultiPinSelect::Selected {
155                    pins: self.pins.clone(),
156                }
157                .into(),
158            ));
159        }
160
161        self.render(data, &moves, validity, painter);
162        None
163    }
164}
165
166impl MoveMultiPin {
167    fn render<C: Canvas>(
168        &self,
169        data: &Drawing,
170        moves: &[PinMove],
171        validity: DropValidity,
172        painter: &mut Style<'_, C>,
173    ) {
174        let pins = &self.pins;
175        let guides = crate::widget::alignment::pin_drag_guides(data, moves, pins);
176        DrawingPasses::new(data)
177            // Bodies only; the pin layer is drawn in `.pins` below so the group's
178            // lifted pins land over any block icons.
179            .blocks(move |painter| {
180                for (_, shape) in data.blocks_layer() {
181                    shape.render_ng(data.shape_accents(), RenderMode::Normal, painter);
182                }
183            })
184            // On the blocks owning the group, fade the selected pins in place —
185            // like a single dragged pin's faded original — so they read as lifted
186            // while their solid copies preview at the destination.
187            .pins(move |painter| {
188                for (id, shape) in data.blocks_layer() {
189                    if !pins.iter().any(|&a| data.pin_shape(a) == Some(id)) {
190                        shape.render_pins_ng(data.shape_accents(), RenderMode::Normal, painter);
191                        continue;
192                    }
193                    let rect = shape.gui_rect();
194                    shape.with_pins(|pid, pin| {
195                        let paint = crate::render::PinPaint {
196                            accent: data.shape_accents().pin(pid),
197                            ..crate::render::PinPaint::default()
198                        };
199                        if pins.contains(&pid) {
200                            painter.with_opacity(0.5, |p| {
201                                crate::render::draw_pin(rect, pin, paint, p);
202                            });
203                        } else {
204                            crate::render::draw_pin(rect, pin, paint, painter);
205                        }
206                    });
207                }
208            })
209            // Preview each pin (stub, name, and tag) at its destination, then
210            // frame the whole group with one rectangle, tinted red while the
211            // placement collides.
212            .overlay(move |painter| {
213                let ring = if validity == DropValidity::Valid {
214                    Role::SelectionFrame
215                } else {
216                    Role::PinStem
217                };
218                let mut bounds: Option<Rect> = None;
219                for &PinMove { pin: anchor, to } in moves {
220                    let Some((shape, pin)) = data.pin_on_shape(anchor) else {
221                        continue;
222                    };
223                    let rect = shape.gui_rect();
224                    let dy =
225                        (to.offset as f32 - crate::shape::pin::slot(pin).offset as f32) * PIN_PITCH;
226                    let paint = crate::render::PinPaint {
227                        delta_y: dy,
228                        side: Some(to.side),
229                        accent: data.shape_accents().pin(anchor),
230                    };
231                    crate::render::draw_pin(rect, pin, paint, painter);
232                    // Frame the destination over the same parts as the idle
233                    // selection, so the rectangle keeps its shape on drag start.
234                    let dest = crate::render::PinExtent {
235                        bbox: rect,
236                        side: to.side,
237                        offset: to.offset,
238                        name: &pin.name,
239                        type_label: &pin.type_name,
240                        tag: &pin.tag,
241                    }
242                    .bbox(painter);
243                    bounds = Some(bounds.map_or(dest, |b| b.union(dest)));
244                }
245                if let Some(bounds) = bounds {
246                    painter.rect(
247                        bounds.expand(2.0),
248                        SELECTION_RING_ROUNDING,
249                        Role::Transparent,
250                        (1.5, ring),
251                    );
252                }
253            })
254            .draw(painter);
255        // Guides on top so they stay visible over the diagram.
256        crate::widget::alignment::draw_guides(&guides, painter);
257    }
258}