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