1use std::ops::ControlFlow;
2
3use blockworx_geom::{Pos2, Vec2};
4
5use crate::render::{LabelPass, RouteRenderMode, render_route};
6use crate::theme::Style;
7use crate::{
8 shape::ShapeId,
9 tools::{
10 EditRoute, EditTextBox, MoveBlock, MoveBlockType, MoveLabel, MovePin, MoveTitle,
11 MultiSelect, RenameBlockType, RenamePin, RenameRoute, RenameTitle, RetypePin,
12 names::ToolName,
13 rename_pin::Field,
14 resize_block::ResizeBlock,
15 select_pin::SelectPin,
16 tool::{Action, Tool, ToolTrait},
17 },
18 widget::{
19 drawing::Drawing,
20 hit_target::{HitTarget, PinPart},
21 },
22};
23use blockworx_paint::{Canvas, Cursor, Event, Interaction, Renderer};
24pub struct SelectTool;
25
26impl ToolTrait for SelectTool {
27 fn name(&self) -> ToolName {
28 ToolName::Select
29 }
30
31 fn widget<C: Canvas>(
32 &mut self,
33 data: &mut Drawing,
34 interaction: &Interaction,
35 painter: &mut Style<'_, C>,
36 ) -> Option<Action> {
37 crate::widget::display::widget(data, interaction, painter);
38 if let ControlFlow::Break(action) =
41 crate::tools::route_start::widget(data, interaction, painter)
42 {
43 return action;
44 }
45 painter.set_cursor(Cursor::Default);
46 match interaction.event {
47 Some(Event::HoverAt(pos)) => {
48 let _span = tracing::info_span!("hit_test_hover").entered();
49 match data.resolve_at_pos(pos, painter) {
52 Some(
53 HitTarget::Title(_)
54 | HitTarget::BlockType(_)
55 | HitTarget::Pin { .. }
56 | HitTarget::RouteLabel { .. },
57 ) => painter.set_cursor(Cursor::PointingHand),
58 Some(HitTarget::Route(rid)) => {
59 if let Some((wire, geometry)) =
60 data.auto_route(rid).zip(data.route_geometry(rid))
61 {
62 painter.set_cursor(Cursor::PointingHand);
63 render_route(
64 painter,
65 &wire,
66 geometry,
67 RouteRenderMode::Highlighted,
68 LabelPass::Draw,
69 );
70 }
71 }
72 Some(HitTarget::Port(_) | HitTarget::Shape(_)) | None => {}
73 }
74 }
75 Some(Event::DoubleClicked { pos }) => {
76 if let Some(tool) = editor_at_pos(data, pos, painter) {
77 return Some(Action::SwitchTool(tool));
78 }
79 if editor_target_at_pos(data, pos, painter) {
83 return None;
84 }
85 return Some(Action::ResetView);
86 }
87 Some(Event::DragStarted { pos }) => {
88 return Some(drag_to_move(data, pos, painter));
89 }
90 Some(Event::Clicked { pos }) => {
91 let _span = tracing::info_span!("hit_test_click").entered();
92 return click_to_select(data, pos, painter);
96 }
97 _ => {}
98 }
99
100 None
101 }
102}
103
104pub(crate) fn editor_at_pos(
110 data: &Drawing,
111 pos: Pos2,
112 painter: &Style<'_, impl Renderer>,
113) -> Option<Tool> {
114 if data.authoring().is_withheld() {
115 return None;
116 }
117 match data.resolve_at_pos(pos, painter)? {
118 HitTarget::Title(shape) => RenameTitle::new_with_shape(data, shape).map(Into::into),
119 HitTarget::BlockType(rect) => RenameBlockType::new_with_rect(data, rect).map(Into::into),
120 HitTarget::Pin { anchor, part, .. } => match part {
121 PinPart::Name => RenamePin::new_with_anchor(data, anchor, Field::Name).map(Into::into),
122 PinPart::Type => RetypePin::new_with_anchor(data, anchor).map(Into::into),
123 PinPart::Tag => RenamePin::new_with_anchor(data, anchor, Field::Tag).map(Into::into),
124 PinPart::Stub => None,
125 },
126 HitTarget::RouteLabel { route, label } => {
127 RenameRoute::new_with_route_and_label(data, route, label, painter).map(Into::into)
128 }
129 HitTarget::Shape(ShapeId::Text(tid)) => EditTextBox::new_for(data, tid).map(Into::into),
130 HitTarget::Port(_) | HitTarget::Route(_) | HitTarget::Shape(_) => None,
131 }
132}
133
134fn editor_target_at_pos<C: Canvas>(data: &Drawing, pos: Pos2, painter: &Style<'_, C>) -> bool {
139 matches!(
140 data.resolve_at_pos(pos, painter),
141 Some(
142 HitTarget::Title(_)
143 | HitTarget::BlockType(_)
144 | HitTarget::Pin {
145 part: PinPart::Name | PinPart::Type | PinPart::Tag,
146 ..
147 }
148 | HitTarget::RouteLabel { .. }
149 | HitTarget::Shape(ShapeId::Text(_))
150 )
151 )
152}
153
154pub(crate) fn click_to_select(
161 data: &Drawing,
162 pos: Pos2,
163 painter: &Style<'_, impl Renderer>,
164) -> Option<Action> {
165 let select_shape = |shape| Some(Action::SwitchTool(ResizeBlock::Selected { shape }.into()));
167 match data.resolve_at_pos(pos, painter)? {
168 HitTarget::Title(shape) | HitTarget::Shape(shape) => select_shape(shape),
169 HitTarget::BlockType(rect) => select_shape(ShapeId::Rect(rect)),
170 HitTarget::Port(port) => select_shape(ShapeId::Port(port)),
171 HitTarget::Pin { anchor, .. } => match data.pin_shape(anchor) {
174 Some(port @ ShapeId::Port(_)) => select_shape(port),
175 _ => Some(Action::SwitchTool(SelectPin::Selected { anchor }.into())),
176 },
177 HitTarget::RouteLabel { .. } => None,
179 HitTarget::Route(id) => Some(Action::SwitchTool(
180 EditRoute::Selected { id, anchor: pos }.into(),
181 )),
182 }
183}
184
185pub(crate) fn drag_to_move<C: Canvas>(
191 data: &mut Drawing,
192 pos: Pos2,
193 painter: &Style<'_, C>,
194) -> Action {
195 if data.authoring().is_withheld() {
198 return Action::SwitchTool(
199 MultiSelect::Marquee {
200 start: pos,
201 current: pos,
202 base: Vec::new(),
203 }
204 .into(),
205 );
206 }
207 let move_shape = |shape| {
209 MoveBlock::Dragging {
210 shape,
211 delta_pos: Vec2::ZERO,
212 }
213 .into()
214 };
215 let tool: Tool = match data.resolve_at_pos(pos, painter) {
216 Some(HitTarget::Title(shape)) => MoveTitle::Dragging {
217 shape,
218 delta_pos: Vec2::ZERO,
219 }
220 .into(),
221 Some(HitTarget::BlockType(rect)) => MoveBlockType {
222 rect,
223 delta_pos: Vec2::ZERO,
224 }
225 .into(),
226 Some(HitTarget::Port(port)) => move_shape(ShapeId::Port(port)),
227 Some(HitTarget::Pin {
228 anchor, location, ..
229 }) => match data.pin_shape(anchor) {
230 Some(port @ ShapeId::Port(_)) => move_shape(port),
233 _ => MovePin::Dragging {
234 anchor,
235 location,
236 delta_pos: Vec2::ZERO,
237 }
238 .into(),
239 },
240 Some(HitTarget::RouteLabel { route, label }) => {
241 match MoveLabel::drag_from(data, route, label) {
242 Some(dragging) => dragging.into(),
243 None => EditRoute::Selected {
244 id: route,
245 anchor: pos,
246 }
247 .into(),
248 }
249 }
250 Some(HitTarget::Route(id)) => {
251 EditRoute::drag_from(data, id, pos)
255 .unwrap_or(EditRoute::Selected { id, anchor: pos })
256 .into()
257 }
258 Some(HitTarget::Shape(shape)) => move_shape(shape),
259 None => MultiSelect::Marquee {
261 start: pos,
262 current: pos,
263 base: Vec::new(),
264 }
265 .into(),
266 };
267 Action::SwitchTool(tool)
268}
269
270pub(crate) fn extend_with_shape(base: &[ShapeId], shape: ShapeId) -> Tool {
274 let mut shapes: Vec<ShapeId> = base.to_vec();
275 if let Some(i) = shapes.iter().position(|&s| s == shape) {
276 shapes.remove(i);
277 } else {
278 shapes.push(shape);
279 }
280 match shapes.as_slice() {
281 [] => SelectTool.into(),
282 [only] => ResizeBlock::Selected { shape: *only }.into(),
283 _ => MultiSelect::Selected { shapes }.into(),
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::canvas::Painter;
291 use crate::path::Scope;
292 use crate::{
293 grid::GRID_SIZE,
294 shape::pin::PinSide,
295 theme::Theme,
296 widget::test_fixtures::{self as fx, Scene},
297 };
298 use blockworx_doc::fixtures::block_id;
299 use blockworx_geom::{Rect, pos2};
300
301 fn cells(min: (f32, f32), max: (f32, f32)) -> Rect {
303 Rect::from_min_max(
304 pos2(min.0 * GRID_SIZE, min.1 * GRID_SIZE),
305 pos2(max.0 * GRID_SIZE, max.1 * GRID_SIZE),
306 )
307 }
308
309 #[test]
319 fn every_editor_opens_over_the_label_it_was_summoned_from() {
320 let mut scene = Scene::new(vec![
323 fx::block_in(1, Scope::Root, cells((0.0, 0.0), (34.0, 30.0))),
324 fx::pin_at(
325 2,
326 Scope::Block(block_id(1)),
327 "po",
328 fx::slot(PinSide::East, 2),
329 cells((2.0, 2.0), (7.0, 4.0)),
330 ),
331 fx::pin_at(
332 3,
333 Scope::Block(block_id(1)),
334 "pi",
335 fx::slot(PinSide::West, 1),
336 cells((2.0, 5.0), (7.0, 7.0)),
337 ),
338 fx::block_in(
340 4,
341 Scope::Block(block_id(1)),
342 cells((8.0, 8.0), (22.0, 20.0)),
343 ),
344 fx::titled(4, "core"),
345 fx::typed(4, "Widget"),
346 fx::pin_at(
347 5,
348 Scope::Block(block_id(4)),
349 "out",
350 fx::slot(PinSide::East, 1),
351 Rect::ZERO,
352 ),
353 fx::pin_typed(5, "u8"),
354 fx::pin_tagged(5, "t1"),
355 fx::pin_tag_shown(5),
356 fx::pin_at(
357 6,
358 Scope::Block(block_id(4)),
359 "in",
360 fx::slot(PinSide::West, 2),
361 Rect::ZERO,
362 ),
363 fx::pin_typed(6, "u8"),
364 fx::pin_tagged(6, "t2"),
365 fx::pin_tag_shown(6),
366 fx::block_in(
369 7,
370 Scope::Block(block_id(1)),
371 cells((26.0, 8.0), (30.0, 14.0)),
372 ),
373 fx::titled(7, "a rather long title"),
374 fx::title_offset(7, 400.0),
375 fx::area(
376 8,
377 Scope::Block(block_id(1)),
378 cells((2.0, 24.0), (14.0, 28.0)),
379 ),
380 fx::text(
381 9,
382 Scope::Block(block_id(1)),
383 "note",
384 pos2(24.0 * GRID_SIZE, 24.0 * GRID_SIZE),
385 ),
386 ])
387 .inside(block_id(1));
388
389 let idle = Interaction {
390 event: None,
391 press: None,
392 lost_focus: false,
393 enter_pressed: false,
394 tab_pressed: false,
395 escape_pressed: false,
396 delete_pressed: false,
397 shift: false,
398 };
399 let ctx = egui::Context::default();
400 ctx.set_fonts(crate::canvas::build_fonts(
401 blockworx_paint::FontChoice::default(),
402 ));
403 ctx.run_ui(egui::RawInput::default(), |_| {})
404 .drop_without_applying_deltas();
405 let mut editors = 0;
406 ctx.run_ui(egui::RawInput::default(), |ui| {
407 let theme = Theme::default();
408 let mut painter = Painter::headless(ui.painter().clone(), theme.palette().clone());
409 let mut drawing = scene.drawing();
410 for i in 0..=68 {
412 for j in 0..=60 {
413 let pos = pos2(i as f32 * GRID_SIZE * 0.5, j as f32 * GRID_SIZE * 0.5);
414 let armed = {
415 let style = Style::new(&theme, &mut painter);
416 editor_at_pos(&drawing, pos, &style)
417 };
418 let Some(mut tool) = armed else { continue };
419 {
420 let mut style = Style::new(&theme, &mut painter);
421 let _ =
422 crate::tools::tool::frame(&mut tool, &mut drawing, &idle, &mut style);
423 }
424 let edit = painter
425 .take_edit_text()
426 .unwrap_or_else(|| panic!("editor armed at {pos:?} published no EditText"));
427 assert!(
428 edit.position.expand(GRID_SIZE).contains(pos),
429 "editor at {:?} opened away from the {:?} that summoned it",
430 edit.position,
431 pos
432 );
433 editors += 1;
434 }
435 }
436 })
437 .drop_without_applying_deltas();
438 assert!(editors > 60, "only {editors} editor points found");
440 }
441}