1use crate::theme::Style;
2use blockworx_geom::{Pos2, Rect, Vec2};
3use blockworx_paint::{Canvas, Event, Interaction};
4
5use crate::{
6 edit::geometry::PinMove,
7 grid::{PIN_PITCH, max_pin_slot},
8 shape::pin::PinSide,
9 state::RenderMode,
10 theme::Role,
11 tools::{
12 MultiPinSelect,
13 multi_pin_select::SELECTION_RING_ROUNDING,
14 names::ToolName,
15 tool::{Action, Supposing, ToolTrait},
16 },
17 widget::{DrawingPasses, drawing::Drawing},
18};
19use blockworx_doc::{geometry::PinSlot, id::PinId};
20
21pub struct MoveMultiPin {
28 pins: Vec<PinId>,
29 delta_pos: Vec2,
30 cursor: Pos2,
31}
32
33#[derive(Clone, Copy, PartialEq, Eq)]
35enum DropValidity {
36 Valid,
37 Invalid,
38}
39
40impl MoveMultiPin {
41 pub fn new(pins: Vec<PinId>, start: Pos2) -> Self {
42 Self {
43 pins,
44 delta_pos: Vec2::ZERO,
45 cursor: start,
46 }
47 }
48
49 fn moves(&self, data: &Drawing) -> Vec<PinMove> {
53 let mut info: Vec<(PinId, Rect, i32, PinSide)> = Vec::new();
54 let (mut lower, mut upper) = (i32::MIN, i32::MAX);
55 for &anchor in &self.pins {
56 let Some((shape, pin)) = data.pin_on_shape(anchor) else {
57 continue;
58 };
59 let rect = shape.gui_rect();
60 let slot = crate::shape::pin::slot(pin);
61 let old = slot.offset as i32;
62 lower = lower.max(-old);
63 upper = upper.min(max_pin_slot(rect.height()) as i32 - old);
64 info.push((anchor, rect, old, slot.side));
65 }
66 let raw = (self.delta_pos.y / PIN_PITCH).round() as i32;
67 let delta = if lower <= upper {
68 raw.clamp(lower, upper)
69 } else {
70 0
71 };
72 let first_side = info.first().map(|&(.., side)| side);
76 let mixed_sides = info.iter().any(|&(.., side)| Some(side) != first_side);
77 info.into_iter()
78 .map(|(pin, rect, old, side)| {
79 let side = if mixed_sides {
80 side
81 } else if self.cursor.x < rect.center().x {
82 PinSide::West
83 } else {
84 PinSide::East
85 };
86 PinMove {
87 pin,
88 to: PinSlot {
89 side,
90 offset: (old + delta).max(0) as u32,
91 },
92 }
93 })
94 .collect()
95 }
96}
97
98impl ToolTrait for MoveMultiPin {
99 fn name(&self) -> ToolName {
100 ToolName::MoveMultiPin
101 }
102
103 fn suppose<C: Canvas>(
104 &mut self,
105 data: &mut Drawing,
106 interaction: &Interaction,
107 _painter: &mut Style<'_, C>,
108 phase: &Supposing,
109 ) {
110 match interaction.event {
113 Some(Event::Dragging { pos, delta }) => {
114 self.delta_pos += delta;
115 self.cursor = pos;
116 }
117 Some(Event::DragStopped { pos }) => self.cursor = pos,
118 _ => {}
119 }
120 let moves = self.moves(data);
126 if data.can_relocate_pins(&moves) {
127 data.suppose_pin_drags(phase, &moves);
128 } else {
129 data.suppose_pin_drags(phase, &[]);
130 }
131 }
132
133 fn widget<C: Canvas>(
134 &mut self,
135 data: &mut Drawing,
136 interaction: &Interaction,
137 painter: &mut Style<'_, C>,
138 ) -> Option<Action> {
139 let moves = self.moves(data);
140 let validity = if data.can_relocate_pins(&moves) {
141 DropValidity::Valid
142 } else {
143 DropValidity::Invalid
144 };
145
146 if let Some(Event::DragStopped { .. }) = interaction.event {
147 if validity == DropValidity::Valid {
148 data.relocate_pins(&moves);
149 }
150 crate::widget::display::widget(data, interaction, painter);
153 return Some(Action::SwitchTool(
154 MultiPinSelect::Selected {
155 pins: self.pins.clone(),
156 }
157 .into(),
158 ));
159 }
160
161 self.render(data, &moves, validity, painter);
162 None
163 }
164}
165
166impl MoveMultiPin {
167 fn render<C: Canvas>(
168 &self,
169 data: &Drawing,
170 moves: &[PinMove],
171 validity: DropValidity,
172 painter: &mut Style<'_, C>,
173 ) {
174 let pins = &self.pins;
175 let guides = crate::widget::alignment::pin_drag_guides(data, moves, pins);
176 DrawingPasses::new(data)
177 .blocks(move |painter| {
180 for (_, shape) in data.blocks_layer() {
181 shape.render_ng(data.shape_accents(), RenderMode::Normal, painter);
182 }
183 })
184 .pins(move |painter| {
188 for (id, shape) in data.blocks_layer() {
189 if !pins.iter().any(|&a| data.pin_shape(a) == Some(id)) {
190 shape.render_pins_ng(data.shape_accents(), RenderMode::Normal, painter);
191 continue;
192 }
193 let rect = shape.gui_rect();
194 shape.with_pins(|pid, pin| {
195 let paint = crate::render::PinPaint {
196 accent: data.shape_accents().pin(pid),
197 ..crate::render::PinPaint::default()
198 };
199 if pins.contains(&pid) {
200 painter.with_opacity(0.5, |p| {
201 crate::render::draw_pin(rect, pin, paint, p);
202 });
203 } else {
204 crate::render::draw_pin(rect, pin, paint, painter);
205 }
206 });
207 }
208 })
209 .overlay(move |painter| {
213 let ring = if validity == DropValidity::Valid {
214 Role::SelectionFrame
215 } else {
216 Role::PinStem
217 };
218 let mut bounds: Option<Rect> = None;
219 for &PinMove { pin: anchor, to } in moves {
220 let Some((shape, pin)) = data.pin_on_shape(anchor) else {
221 continue;
222 };
223 let rect = shape.gui_rect();
224 let dy =
225 (to.offset as f32 - crate::shape::pin::slot(pin).offset as f32) * PIN_PITCH;
226 let paint = crate::render::PinPaint {
227 delta_y: dy,
228 side: Some(to.side),
229 accent: data.shape_accents().pin(anchor),
230 };
231 crate::render::draw_pin(rect, pin, paint, painter);
232 let dest = crate::render::PinExtent {
235 bbox: rect,
236 side: to.side,
237 offset: to.offset,
238 name: &pin.name,
239 type_label: &pin.type_name,
240 tag: &pin.tag,
241 }
242 .bbox(painter);
243 bounds = Some(bounds.map_or(dest, |b| b.union(dest)));
244 }
245 if let Some(bounds) = bounds {
246 painter.rect(
247 bounds.expand(2.0),
248 SELECTION_RING_ROUNDING,
249 Role::Transparent,
250 (1.5, ring),
251 );
252 }
253 })
254 .draw(painter);
255 crate::widget::alignment::draw_guides(&guides, painter);
257 }
258}