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