Skip to main content

blockworx/tools/
multi_pin_select.rs

1use crate::theme::Style;
2use blockworx_geom::WorldPx;
3use blockworx_paint::{Canvas, Event, Interaction};
4
5use crate::edit::naming::Authoring;
6use crate::{
7    theme::Role,
8    tools::{
9        MoveMultiPin,
10        names::ToolName,
11        tool::{Action, Deletable, ToolTrait},
12    },
13    widget::{drawing::Drawing, hit_target::HitTarget},
14};
15use blockworx_doc::id::PinId;
16
17/// A group of child-block pins selected by a marquee that enclosed only pins
18/// (no whole object). Entered from [`MultiSelect`](crate::tools::MultiSelect)'s marquee. The action-bar
19/// overlay offers Copy, a tag-visibility toggle, and Delete; the group can't be
20/// moved. A plain click clears it; a drag starts a fresh marquee.
21pub enum MultiPinSelect {
22    Selected { pins: Vec<PinId> },
23}
24
25impl ToolTrait for MultiPinSelect {
26    fn name(&self) -> ToolName {
27        ToolName::MultiPinSelect
28    }
29
30    fn selection(&self) -> Option<Deletable> {
31        match self {
32            MultiPinSelect::Selected { pins } => Some(Deletable::Pins(pins.clone())),
33        }
34    }
35
36    fn widget<C: Canvas>(
37        &mut self,
38        data: &mut Drawing,
39        interaction: &Interaction,
40        painter: &mut Style<'_, C>,
41    ) -> Option<Action> {
42        let MultiPinSelect::Selected { pins } = self;
43        Self::render(data, pins, interaction, painter);
44        if interaction.delete_pressed {
45            return Some(Action::Delete(Deletable::Pins(pins.clone())));
46        }
47        match interaction.event {
48            Some(Event::DragStarted { pos }) => {
49                // Grabbing one of the selected pins starts a rigid group move;
50                // dragging another object moves it directly, and empty space
51                // re-marquees (the fallback inside `drag_to_move`).
52                let on_pin = data.authoring() == Authoring::Offered
53                    && matches!(
54                        data.resolve_at_pos(pos, painter),
55                        Some(HitTarget::Pin { anchor, .. }) if pins.contains(&anchor)
56                    );
57                if on_pin {
58                    Some(Action::SwitchTool(
59                        MoveMultiPin::new(pins.clone(), pos).into(),
60                    ))
61                } else {
62                    Some(crate::tools::select_tool::drag_to_move(data, pos, painter))
63                }
64            }
65            Some(Event::Clicked { pos }) => Some(
66                crate::tools::select_tool::click_to_select(data, pos, painter).unwrap_or_default(),
67            ),
68            _ => None,
69        }
70    }
71}
72
73impl MultiPinSelect {
74    fn render<C: Canvas>(
75        data: &Drawing,
76        pins: &[PinId],
77        interaction: &Interaction,
78        painter: &mut Style<'_, C>,
79    ) {
80        crate::widget::display::widget(data, interaction, painter);
81        draw_pin_selection_frame(data, pins, painter);
82    }
83}
84
85/// Frame the given pins with a single rectangle around the union of their stubs
86/// Corner rounding of the frame drawn around a pin selection. Shared with the
87/// drag preview in [`MoveMultiPin`](super::move_multi_pin) so the frame keeps
88/// its shape when a drag starts.
89pub(crate) const SELECTION_RING_ROUNDING: WorldPx = WorldPx::new(2.0);
90
91/// and labels, so the selection reads as one. Shared by the marquee pin group
92/// ([`MultiPinSelect`]) and the single-pin [`SelectPin`](super::select_pin) tool
93/// so both render the same way.
94pub(crate) fn draw_pin_selection_frame<C: Canvas>(
95    data: &Drawing,
96    pins: &[PinId],
97    painter: &mut Style<'_, C>,
98) {
99    let bounds = crate::tools::selection_bounds::pins_bounds(data, pins, painter);
100    if let Some(bounds) = bounds {
101        painter.rect(
102            bounds.expand(2.0),
103            SELECTION_RING_ROUNDING,
104            Role::Transparent,
105            (1.5, Role::SelectionFrame),
106        );
107    }
108}