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