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}