Skip to main content

blockworx_tools/
new_block.rs

1use crate::theme::Style;
2use blockworx_geom::{Pos2, Rect, WorldPx};
3use blockworx_paint::{Canvas, Cursor, Event, Interaction};
4
5use crate::{
6    RenameTitle,
7    grid::GRID_SIZE,
8    names::ToolName,
9    shape::ShapeId,
10    theme::Role,
11    tool::{ToolTrait, Transition},
12    widget::drawing::Drawing,
13};
14
15/// Two ways to place a block, both ending in the same rect: press-drag-release,
16/// or click one corner and click the opposite one.
17pub enum NewBlock {
18    Idle,
19    /// The button is held; the rect follows the pointer until release.
20    Dragging {
21        start: Pos2,
22    },
23    /// A click set the first corner; the rect follows the pointer until the
24    /// second corner is clicked. Escape cancels.
25    Placing {
26        start: Pos2,
27        corner: Pos2,
28    },
29}
30
31impl ToolTrait for NewBlock {
32    fn name(&self) -> ToolName {
33        ToolName::NewBlock
34    }
35    fn widget<C: Canvas>(
36        &mut self,
37        data: &mut Drawing,
38        interaction: &Interaction,
39        painter: &mut Style<'_, C>,
40    ) -> Option<Transition> {
41        // Draw the background
42        crate::widget::display::widget(data, interaction, painter);
43        painter.set_cursor(Cursor::Crosshair);
44        if interaction.escape_pressed {
45            *self = NewBlock::Idle;
46            return None;
47        }
48        match self {
49            NewBlock::Idle => {
50                match interaction.event {
51                    Some(Event::DragStarted { pos }) => *self = NewBlock::Dragging { start: pos },
52                    Some(Event::Clicked { pos }) => {
53                        *self = NewBlock::Placing {
54                            start: pos,
55                            corner: pos,
56                        }
57                    }
58                    _ => {}
59                }
60                None
61            }
62            NewBlock::Dragging { start } => {
63                let start = *start;
64                match interaction.event {
65                    Some(Event::Dragging { pos, .. }) => {
66                        preview(painter, start, pos);
67                        None
68                    }
69                    // A drag too small to be a block just cancels.
70                    Some(Event::DragStopped { pos }) => {
71                        *self = NewBlock::Idle;
72                        create(data, start, pos)
73                    }
74                    _ => None,
75                }
76            }
77            NewBlock::Placing { start, corner } => {
78                let start = *start;
79                match interaction.event {
80                    Some(Event::HoverAt(pos) | Event::Dragging { pos, .. }) => *corner = pos,
81                    // A second click (or the release of a drag started from the
82                    // first corner) completes the block; one that would make a
83                    // degenerate block leaves the corner placed to try again.
84                    Some(Event::Clicked { pos } | Event::DragStopped { pos }) => {
85                        if let Some(action) = create(data, start, pos) {
86                            *self = NewBlock::Idle;
87                            return Some(action);
88                        }
89                        *corner = pos;
90                    }
91                    _ => {}
92                }
93                let corner = *corner;
94                preview(painter, start, corner);
95                None
96            }
97        }
98    }
99}
100
101fn preview<C: Canvas>(painter: &mut Style<'_, C>, start: Pos2, corner: Pos2) {
102    painter.rect(
103        Rect::from_two_pos(start, corner),
104        WorldPx::ZERO,
105        Role::Transparent,
106        (1.0, Role::NewBlockPreviewStroke),
107    );
108}
109
110/// Add the block spanning the two corners and drop straight into naming it.
111/// `None` when the corners are too close to make a block — a block's smallest
112/// valid height is 4 grid cells (one pin slot).
113fn create(data: &mut Drawing, start: Pos2, corner: Pos2) -> Option<Transition> {
114    let candidate = Rect::from_two_pos(start, corner);
115    if candidate.width() <= GRID_SIZE || candidate.height() < 4.0 * GRID_SIZE {
116        return None;
117    }
118    let shape = ShapeId::Rect(data.add_rect_box(start, corner));
119    Some(RenameTitle::action(data, shape))
120}