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}