Skip to main content

blockworx_tools/
move_pin.rs

1use crate::render::{LabelPass, RouteRenderMode, render_route};
2use crate::theme::Style;
3use blockworx_geom::Vec2;
4use blockworx_paint::{Canvas, Cursor, Event, Interaction};
5
6use crate::{
7    edit::geometry::PinMove,
8    grid::{PIN_PITCH, max_pin_slot},
9    names::ToolName,
10    shape::{PinLocation, ShapeId, pin::PinSide},
11    state::RenderMode,
12    tool::{PreviewPhase, ToolTrait, Transition, select_tool_for_anchor},
13    widget::{DrawingPasses, drawing::Drawing},
14};
15use blockworx_doc::{geometry::PinSlot, id::PinId};
16
17/// The slot the pin would snap to if released now. One definition, so the
18/// preview, the guides, and the drop cannot disagree about where the drag
19/// promised to land — `None` when no slot is free.
20fn drop_candidate(data: &Drawing, anchor: PinId, side: PinSide, offset: f32) -> Option<u32> {
21    data.pin_shape(anchor)
22        .and_then(|id| data.shape(id))
23        .and_then(|s| s.pin_drop_candidate(anchor, side, offset))
24}
25
26pub enum MovePin {
27    Idle,
28    Dragging {
29        anchor: PinId,
30        location: PinLocation,
31        delta_pos: Vec2,
32    },
33}
34
35impl ToolTrait for MovePin {
36    fn name(&self) -> ToolName {
37        ToolName::MovePin
38    }
39
40    fn preview<C: Canvas>(
41        &mut self,
42        data: &mut Drawing,
43        interaction: &Interaction,
44        _painter: &mut Style<'_, C>,
45        phase: &PreviewPhase,
46    ) {
47        let MovePin::Dragging {
48            anchor,
49            location,
50            delta_pos,
51        } = self
52        else {
53            return;
54        };
55        // The release advances the drag like any other frame: it paints before
56        // it commits, so the side it drops on has to be the side it draws.
57        let pointer = match interaction.event {
58            Some(Event::Dragging { pos, delta }) => {
59                *delta_pos += delta;
60                Some(pos)
61            }
62            Some(Event::DragStopped { pos }) => Some(pos),
63            _ => None,
64        };
65        if let Some(pos) = pointer {
66            let Some(rect) = data
67                .pin_on_shape(*anchor)
68                .map(|(shape, _)| shape.gui_rect())
69            else {
70                return;
71            };
72            // Saturate the dragged offset to the block's bounds so the pin
73            // stays inside its containing rect: the lowest slot that keeps
74            // two grid cells of bottom clearance is `max_pin_slot` (see its
75            // docs), in offset pixels `max_pin_slot * PIN_PITCH`.
76            let max_offset = max_pin_slot(rect.height()) as f32 * PIN_PITCH;
77            delta_pos.y = delta_pos
78                .y
79                .clamp(-location.offset, max_offset - location.offset);
80            location.side = if pos.x < rect.center().x {
81                PinSide::West
82            } else {
83                PinSide::East
84            };
85        }
86        // Re-route the connected routes live so they track the candidate.
87        // Only a stub slides along an edge; a port body stays put, so its
88        // routes are left on their committed geometry.
89        let candidate = drop_candidate(data, *anchor, location.side, location.offset + delta_pos.y);
90        if let (Some(offset), Some(ShapeId::Rect(_))) = (candidate, data.pin_shape(*anchor)) {
91            data.preview_pin_drag(phase, *anchor, location.side, offset);
92        }
93    }
94
95    fn widget<C: Canvas>(
96        &mut self,
97        data: &mut Drawing,
98        interaction: &Interaction,
99        painter: &mut Style<'_, C>,
100    ) -> Option<Transition> {
101        match self {
102            MovePin::Idle => {
103                crate::widget::display::widget(data, interaction, painter);
104                if let Some(Event::HoverAt(hover_pos)) = interaction.event {
105                    if data.pin_text_at_pos(hover_pos, painter).is_some() {
106                        painter.set_cursor(Cursor::PointingHand);
107                    }
108                } else if let Some(Event::DragStarted { pos }) = interaction.event
109                    && let Some((anchor, location)) = data.pin_text_at_pos(pos, painter)
110                {
111                    *self = MovePin::Dragging {
112                        anchor,
113                        location,
114                        delta_pos: Vec2::ZERO,
115                    };
116                }
117            }
118            MovePin::Dragging {
119                anchor,
120                location,
121                delta_pos,
122            } => {
123                if let Some(Event::DragStopped { .. }) = interaction.event {
124                    // The drop resolves its slot through the same candidate
125                    // search the preview drew, so the pin lands where the drag
126                    // promised (or nowhere, if no slot was free).
127                    if let Some(offset) =
128                        drop_candidate(data, *anchor, location.side, location.offset + delta_pos.y)
129                    {
130                        data.move_pin_snapped(
131                            *anchor,
132                            blockworx_doc::geometry::PinSlot {
133                                side: location.side,
134                                offset,
135                            },
136                        );
137                    }
138                    // Paint the committed state this frame. The tool switches
139                    // away next frame, so without rendering here the frame goes
140                    // blank and the pin flashes. Drop the stale approach corners
141                    // on the moved pin's routes, then re-route first so they land
142                    // in their final spot with a clean approach.
143                    data.trim_anchor_approach(*anchor);
144                    crate::widget::display::widget(data, interaction, painter);
145                    // Stay on the moved pin so it remains the standing target for
146                    // further edits (block pin → SelectPin, port → ResizeBlock).
147                    return Some(Transition::SwitchTool(select_tool_for_anchor(
148                        data, *anchor,
149                    )));
150                }
151                let owner = data.pin_shape(*anchor);
152                let candidate =
153                    drop_candidate(data, *anchor, location.side, location.offset + delta_pos.y);
154                let data: &Drawing = data;
155                let anchor = *anchor;
156                let side = location.side;
157                let delta = delta_pos.y;
158                let guides = candidate.map(|slot| {
159                    crate::widget::alignment::pin_drag_guides(
160                        data,
161                        &[PinMove {
162                            pin: anchor,
163                            to: PinSlot { side, offset: slot },
164                        }],
165                        &[anchor],
166                    )
167                });
168                DrawingPasses::new(data)
169                    .shape_mode(move |id| {
170                        if Some(id) == owner {
171                            RenderMode::PinDragged {
172                                pin: anchor,
173                                delta,
174                                side,
175                                candidate,
176                            }
177                        } else {
178                            RenderMode::Normal
179                        }
180                    })
181                    // Draw every route; one touching the dragged pin shows only
182                    // when the drop would be valid (then it tracks the candidate
183                    // slot), otherwise it stays hidden until a valid slot is
184                    // reached.
185                    .routes(move |painter| {
186                        let drawn: Vec<_> = data
187                            .auto_routes()
188                            .filter(|(_, wire)| {
189                                let touching = wire.route.from == anchor || wire.route.to == anchor;
190                                !touching || candidate.is_some()
191                            })
192                            .collect();
193                        let ids: Vec<_> = drawn.iter().map(|&(id, _)| id).collect();
194                        let hops = data.hops(&ids);
195                        for (id, wire) in drawn {
196                            let Some(course) = data.course(id, hops.of(id)) else {
197                                continue;
198                            };
199                            render_route(
200                                painter,
201                                &wire,
202                                course,
203                                RouteRenderMode::Normal,
204                                LabelPass::Draw,
205                            );
206                        }
207                    })
208                    .draw(painter);
209                // Guides on top so they stay visible over the diagram.
210                if let Some(guides) = &guides {
211                    crate::widget::alignment::draw_guides(guides, painter);
212                }
213            }
214        }
215        None
216    }
217}