Skip to main content

blockworx/tools/
add_port.rs

1use crate::render::draw_port_outline;
2use crate::theme::{Role, Style};
3use blockworx_geom::{Pos2, Rect, pos2, vec2};
4use blockworx_paint::{Canvas, Cursor, Event, Interaction};
5
6use crate::{
7    grid::snap_to_grid,
8    shape::pin::PinSide,
9    tools::{
10        names::ToolName,
11        rename_pin::{Field, RenamePin},
12        tool::{Action, ToolTrait},
13    },
14    widget::drawing::Drawing,
15};
16
17/// Drag-to-place a boundary port. A ghost outline previews the port under the
18/// cursor and its width follows the drag; on release the port is stamped and
19/// its name opens for editing straight away.
20///
21/// The bare click used to stamp one, which made this the one creator that
22/// needed no drag — *"It should require a drag to create (like the other
23/// tools) if the tool is just clicked"* (playbook R40). What the click did
24/// now lives on the drag-out path, where every creator's default does
25/// ([`crate::tools::stamp`]).
26pub enum AddPort {
27    Idle,
28    Dragging { start: Pos2 },
29}
30
31impl ToolTrait for AddPort {
32    fn name(&self) -> ToolName {
33        ToolName::AddPort
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        crate::widget::display::widget(data, interaction, painter);
43        painter.set_cursor(Cursor::Crosshair);
44
45        // A locked level keeps its port interface frozen: preview nothing and
46        // create nothing, just fall back to select when a placement gesture
47        // ends — which is a drag's release and nothing else (R40).
48        if data.current_locked() {
49            let placed = matches!(interaction.event, Some(Event::DragStopped { .. }));
50            return placed.then(Action::default);
51        }
52
53        match self {
54            AddPort::Idle => match interaction.event {
55                Some(Event::HoverAt(pos)) => preview_port(painter, port_box(pos, pos)),
56                Some(Event::DragStarted { pos }) => *self = AddPort::Dragging { start: pos },
57                _ => {}
58            },
59            AddPort::Dragging { start } => {
60                let start = *start;
61                if let Some(Event::Dragging { pos, .. }) = interaction.event {
62                    preview_port(painter, port_box(start, pos));
63                } else if let Some(Event::DragStopped { pos }) = interaction.event {
64                    *self = AddPort::Idle;
65                    return Some(create_port(data, port_box(start, pos)));
66                }
67            }
68        }
69        None
70    }
71}
72
73/// The grid-snapped box a port occupies for a placement spanning `a`..`b`: the
74/// width follows the horizontal drag (never below the default a drop takes),
75/// the height is fixed and the top edge sits at the higher of the two points.
76fn port_box(a: Pos2, b: Pos2) -> Rect {
77    let min = pos2(a.x.min(b.x), a.y.min(b.y));
78    let default = crate::edit::create::stamped_port(min);
79    let width = round_to_grid_len((a.x - b.x).abs()).max(default.width());
80    Rect::from_min_size(snap_to_grid(min), vec2(width, default.height()))
81}
82
83fn round_to_grid_len(len: f32) -> f32 {
84    snap_to_grid(pos2(len, 0.0)).x
85}
86
87/// Draw the ghost port outline the placement will produce. The facing is
88/// cosmetic here — the real slot side is resolved when the port is created — so
89/// the preview always points East.
90fn preview_port<C: Canvas>(painter: &mut Style<'_, C>, bbox: Rect) {
91    draw_port_outline(
92        bbox,
93        PinSide::East,
94        Role::Transparent,
95        (1.0, Role::NewPinPreviewStroke),
96        painter,
97    );
98}
99
100/// Stamp the port and hand straight off to editing its name, so a fresh port
101/// is ready to be labelled without a second gesture.
102fn create_port(data: &mut Drawing, bbox: Rect) -> Action {
103    let anchor = data.add_port_auto_named(bbox);
104    RenamePin::action(data, anchor, Field::Name)
105}