1use crate::theme::Style;
2use blockworx_geom::{Pos2, Rect, Vec2, WorldPx};
3use blockworx_paint::{Canvas, Event, Interaction};
4
5use crate::edit::naming::Authoring;
6use crate::{
7 grid::{GROUP_SELECTION_PAD, snap_offset},
8 shape::ShapeId,
9 state::RenderMode,
10 theme::Role,
11 tools::{
12 MultiPinSelect, SelectTool,
13 names::ToolName,
14 resize_block::ResizeBlock,
15 tool::{Action, Deletable, Supposing, ToolTrait},
16 },
17 widget::{DrawingPasses, drawing::Drawing},
18};
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 suppose<C: Canvas>(
53 &mut self,
54 data: &mut Drawing,
55 interaction: &Interaction,
56 _painter: &mut Style<'_, C>,
57 phase: &Supposing,
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.suppose_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<Action> {
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(Action::SwitchTool(
105 MultiPinSelect::Selected { pins }.into(),
106 ));
107 }
108 return Some(Action::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(Action::SwitchTool(ResizeBlock::Selected { shape }.into()));
121 }
122 *self = MultiSelect::Selected { shapes };
123 }
124 }
125 MultiSelect::Selected { shapes } => {
126 if interaction.delete_pressed {
127 return Some(Action::Delete(Deletable::Shapes(shapes.clone())));
128 }
129 if let Some(Event::DragStarted { pos }) = interaction.event {
130 if interaction.shift {
131 return Some(Action::SwitchTool(
133 MultiSelect::Marquee {
134 start: pos,
135 current: pos,
136 base: shapes.clone(),
137 }
138 .into(),
139 ));
140 } else if data.authoring() == Authoring::Offered
141 && data
142 .shape_at_pos(pos)
143 .is_some_and(|hit| shapes.contains(&hit))
144 {
145 let shapes = std::mem::take(shapes);
147 *self = MultiSelect::Moving {
148 shapes,
149 delta_pos: Vec2::ZERO,
150 };
151 } else {
152 return Some(crate::tools::select_tool::drag_to_move(data, pos, painter));
155 }
156 } else if let Some(Event::Clicked { pos }) = interaction.event {
157 if interaction.shift {
158 if let Some(hit) = data.shape_at_pos(pos) {
161 return Some(Action::SwitchTool(
162 crate::tools::select_tool::extend_with_shape(shapes, hit),
163 ));
164 }
165 return None;
166 }
167 return Some(
170 crate::tools::select_tool::click_to_select(data, pos, painter)
171 .unwrap_or_default(),
172 );
173 }
174 }
175 MultiSelect::Moving { .. } => {
176 if let Some(Event::DragStopped { .. }) = interaction.event {
177 let MultiSelect::Moving { shapes, delta_pos } = self else {
178 unreachable!()
179 };
180 let shapes = std::mem::take(shapes);
181 data.move_shapes(&shapes, *delta_pos);
182 *self = MultiSelect::Selected { shapes };
184 }
185 }
186 }
187 None
188 }
189}
190
191impl MultiSelect {
192 fn render<C: Canvas>(
193 &self,
194 data: &Drawing,
195 interaction: &Interaction,
196 painter: &mut Style<'_, C>,
197 ) {
198 match self {
199 MultiSelect::Marquee {
200 start,
201 current,
202 base,
203 } => {
204 crate::widget::display::widget(data, interaction, painter);
205 if !base.is_empty() {
208 draw_selection_bounds(data, base, Vec2::ZERO, painter);
209 }
210 let marquee = Rect::from_two_pos(*start, *current);
211 painter.rect(
212 marquee,
213 WorldPx::ZERO,
214 Role::MarqueeFill,
215 (1.0, Role::SelectionFrame),
216 );
217 }
218 MultiSelect::Selected { shapes } => {
219 crate::widget::display::widget(data, interaction, painter);
220 draw_selection_bounds(data, shapes, Vec2::ZERO, painter);
221 }
222 MultiSelect::Moving { shapes, delta_pos } => {
223 let icons: Vec<ShapeId> = shapes
226 .iter()
227 .filter_map(|s| s.block().map(ShapeId::Icon))
228 .collect();
229 let delta = *delta_pos;
230 let mut dragged_ids: Vec<ShapeId> = shapes.clone();
231 dragged_ids.extend(icons.iter().copied());
232 let offset = snap_offset(*delta_pos);
233 let guides =
234 crate::widget::alignment::shape_drag_guides(data, &dragged_ids, offset);
235 DrawingPasses::new(data)
236 .shape_mode(move |id| {
237 if shapes.contains(&id) || icons.contains(&id) {
238 RenderMode::Moving { delta }
239 } else {
240 RenderMode::Normal
241 }
242 })
243 .draw(painter);
244 draw_selection_bounds(data, shapes, offset, painter);
246 crate::widget::alignment::draw_guides(&guides, painter);
248 }
249 }
250 }
251}
252
253fn draw_selection_bounds<C: Canvas>(
256 data: &Drawing,
257 shapes: &[ShapeId],
258 offset: Vec2,
259 painter: &mut Style<'_, C>,
260) {
261 let bounds = crate::tools::selection_bounds::shapes_bounds(data, shapes);
262 if let Some(bounds) = bounds {
263 crate::render::draw_box_outline(
265 bounds.translate(offset).expand(GROUP_SELECTION_PAD),
266 Role::Transparent,
267 (1.0, Role::SelectionFrameOutline),
268 painter,
269 );
270 }
271}