1use crate::theme::Style;
2use blockworx_geom::{Pos2, Rect, Vec2, WorldPx};
3use blockworx_paint::{Canvas, Event, Interaction};
4
5use crate::edit::geometry::Moving;
6use crate::edit::naming::Authoring;
7use crate::{
8 MultiPinSelect, SelectTool,
9 grid::{GROUP_SELECTION_PAD, snap_offset},
10 names::ToolName,
11 resize_block::ResizeBlock,
12 shape::ShapeId,
13 state::RenderMode,
14 theme::Role,
15 tool::{Action, Deletable, PreviewPhase, ToolTrait, Transition},
16 widget::{DrawingPasses, drawing::Drawing},
17};
18use blockworx_geom::grid::px_rect;
19
20pub enum MultiSelect {
24 Marquee {
27 start: Pos2,
28 current: Pos2,
29 base: Vec<ShapeId>,
30 },
31 Selected { shapes: Vec<ShapeId> },
33 Moving {
35 shapes: Vec<ShapeId>,
36 delta_pos: Vec2,
37 },
38}
39
40impl ToolTrait for MultiSelect {
41 fn name(&self) -> ToolName {
42 ToolName::MultiSelect
43 }
44
45 fn selection(&self) -> Option<Deletable> {
46 match self {
47 MultiSelect::Selected { shapes } => Some(Deletable::Shapes(shapes.clone())),
48 _ => None,
49 }
50 }
51
52 fn preview<C: Canvas>(
53 &mut self,
54 data: &mut Drawing,
55 interaction: &Interaction,
56 _painter: &mut Style<'_, C>,
57 phase: &PreviewPhase,
58 ) {
59 let MultiSelect::Moving { shapes, delta_pos } = self else {
60 return;
61 };
62 if let Some(Event::Dragging { delta, .. }) = interaction.event {
63 *delta_pos += delta;
64 }
65 let offset = snap_offset(*delta_pos);
69 let drags: Vec<(ShapeId, Vec2)> = shapes.iter().map(|&id| (id, offset)).collect();
70 data.preview_drag(phase, &drags);
71 }
72
73 fn widget<C: Canvas>(
74 &mut self,
75 data: &mut Drawing,
76 interaction: &Interaction,
77 painter: &mut Style<'_, C>,
78 ) -> Option<Transition> {
79 self.render(data, interaction, painter);
80 match self {
81 MultiSelect::Marquee {
82 start,
83 current,
84 base,
85 } => {
86 if let Some(Event::Dragging { pos, .. }) = interaction.event {
87 *current = pos;
88 } else if let Some(Event::DragStopped { .. }) = interaction.event {
89 let marquee = Rect::from_two_pos(*start, *current);
92 let enclosed: Vec<ShapeId> = data
93 .shape_candidates(marquee)
94 .into_iter()
95 .chain(data.areas())
96 .filter(|(_, s)| marquee.contains_rect(s.gui_rect()))
97 .map(|(id, _)| id)
98 .collect();
99 if enclosed.is_empty() && base.is_empty() {
100 let pins = data.pins_in_rect(marquee);
103 if !pins.is_empty() {
104 return Some(Transition::SwitchTool(
105 MultiPinSelect::Selected { pins }.into(),
106 ));
107 }
108 return Some(Transition::SwitchTool(SelectTool.into()));
109 }
110 let mut shapes = std::mem::take(base);
112 for id in enclosed {
113 if !shapes.contains(&id) {
114 shapes.push(id);
115 }
116 }
117 if let [shape] = shapes[..] {
118 return Some(Transition::SwitchTool(
121 ResizeBlock::Selected { shape }.into(),
122 ));
123 }
124 *self = MultiSelect::Selected { shapes };
125 }
126 }
127 MultiSelect::Selected { shapes } => {
128 if interaction.delete_pressed {
129 return Some(Action::Delete(Deletable::Shapes(shapes.clone())).into());
130 }
131 if let Some(Event::DragStarted { pos }) = interaction.event {
132 if interaction.shift {
133 return Some(Transition::SwitchTool(
135 MultiSelect::Marquee {
136 start: pos,
137 current: pos,
138 base: shapes.clone(),
139 }
140 .into(),
141 ));
142 } else if data.authoring() == Authoring::Offered
143 && data
144 .shape_at_pos(pos)
145 .is_some_and(|hit| shapes.contains(&hit))
146 {
147 let shapes = std::mem::take(shapes);
149 *self = MultiSelect::Moving {
150 shapes,
151 delta_pos: Vec2::ZERO,
152 };
153 } else {
154 return Some(crate::select_tool::drag_to_move(
157 data,
158 pos,
159 painter,
160 crate::select_tool::IconGrab::NotArmed,
161 ));
162 }
163 } else if let Some(Event::Clicked { pos }) = interaction.event {
164 if interaction.shift {
165 if let Some(hit) = data.shape_at_pos(pos) {
168 return Some(Transition::SwitchTool(
169 crate::select_tool::extend_with_shape(shapes, hit),
170 ));
171 }
172 return None;
173 }
174 return Some(
177 crate::select_tool::click_to_select(data, pos, painter).unwrap_or_default(),
178 );
179 }
180 }
181 MultiSelect::Moving { .. } => {
182 if let Some(Event::DragStopped { .. }) = interaction.event {
183 let MultiSelect::Moving { shapes, delta_pos } = self else {
184 unreachable!()
185 };
186 let shapes = std::mem::take(shapes);
187 data.move_shapes(&shapes, *delta_pos);
188 *self = MultiSelect::Selected { shapes };
190 }
191 }
192 }
193 None
194 }
195}
196
197impl MultiSelect {
198 fn render<C: Canvas>(
199 &self,
200 data: &Drawing,
201 interaction: &Interaction,
202 painter: &mut Style<'_, C>,
203 ) {
204 match self {
205 MultiSelect::Marquee {
206 start,
207 current,
208 base,
209 } => {
210 crate::widget::display::widget(data, interaction, painter);
211 if !base.is_empty() {
214 draw_selection_bounds(data, base, Vec2::ZERO, painter);
215 }
216 let marquee = Rect::from_two_pos(*start, *current);
217 painter.rect(
218 marquee,
219 WorldPx::ZERO,
220 Role::MarqueeFill,
221 (1.0, Role::SelectionFrame),
222 );
223 }
224 MultiSelect::Selected { shapes } => {
225 crate::widget::display::widget(data, interaction, painter);
226 draw_selection_bounds(data, shapes, Vec2::ZERO, painter);
227 }
228 MultiSelect::Moving { shapes, delta_pos } => {
229 let icons: Vec<ShapeId> = shapes
232 .iter()
233 .filter_map(|s| s.block().map(ShapeId::Icon))
234 .collect();
235 let delta = *delta_pos;
236 let mut dragged_ids: Vec<ShapeId> = shapes.clone();
237 dragged_ids.extend(icons.iter().copied());
238 let offset = snap_offset(*delta_pos);
239 let guides =
240 crate::widget::alignment::shape_drag_guides(data, &dragged_ids, offset);
241 DrawingPasses::new(data)
242 .shape_mode(move |id| {
243 if shapes.contains(&id) || icons.contains(&id) {
244 RenderMode::Moving { delta }
245 } else {
246 RenderMode::Normal
247 }
248 })
249 .draw(painter);
250 draw_selection_bounds(data, shapes, offset, painter);
252 for overlap in data.conflicts(shapes, delta, Moving::AsAGroup) {
257 crate::render::draw_refused_conflict(px_rect(overlap), painter);
258 }
259 crate::widget::alignment::draw_guides(&guides, painter);
261 }
262 }
263 }
264}
265
266fn draw_selection_bounds<C: Canvas>(
269 data: &Drawing,
270 shapes: &[ShapeId],
271 offset: Vec2,
272 painter: &mut Style<'_, C>,
273) {
274 let bounds = crate::selection_bounds::shapes_bounds(data, shapes);
275 if let Some(bounds) = bounds {
276 crate::render::draw_box_outline(
278 bounds.translate(offset).expand(GROUP_SELECTION_PAD),
279 Role::Transparent,
280 (1.0, Role::SelectionFrameOutline),
281 painter,
282 );
283 }
284}