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