Skip to main content

blockworx_tools/
move_block.rs

1use crate::theme::Style;
2use blockworx_geom::Vec2;
3use blockworx_paint::{Canvas, Cursor, Event, Interaction};
4
5use crate::{
6    edit::geometry::Moving,
7    grid::snap_offset,
8    names::ToolName,
9    resize_block::ResizeBlock,
10    shape::ShapeId,
11    state::RenderMode,
12    tool::{PreviewPhase, ToolTrait, Transition},
13    widget::{DrawingPasses, drawing::Drawing},
14};
15use blockworx_geom::grid::px_rect;
16
17pub enum MoveBlock {
18    Idle,
19    Dragging { shape: ShapeId, delta_pos: Vec2 },
20}
21
22impl ToolTrait for MoveBlock {
23    fn name(&self) -> ToolName {
24        ToolName::MoveBlock
25    }
26
27    fn preview<C: Canvas>(
28        &mut self,
29        data: &mut Drawing,
30        interaction: &Interaction,
31        _painter: &mut Style<'_, C>,
32        phase: &PreviewPhase,
33    ) {
34        let MoveBlock::Dragging { shape, delta_pos } = self else {
35            return;
36        };
37        if let Some(Event::Dragging { delta, .. }) = interaction.event {
38            *delta_pos += delta;
39        }
40        // Suppose the block at its previewed position so the frame draws — and
41        // routes — against it. Snap the offset to the grid so this matches the
42        // geometry the on-drop commit produces (no release flash).
43        data.preview_drag(phase, &[(*shape, snap_offset(*delta_pos))]);
44    }
45
46    fn widget<C: Canvas>(
47        &mut self,
48        data: &mut Drawing,
49        interaction: &Interaction,
50        painter: &mut Style<'_, C>,
51    ) -> Option<Transition> {
52        self.render(data, interaction, painter);
53        match self {
54            MoveBlock::Idle => {
55                if let Some(Event::HoverAt(hover_pos)) = interaction.event
56                    && data.shape_at_pos(hover_pos).is_some()
57                {
58                    painter.set_cursor(Cursor::Move);
59                }
60                if let Some(Event::DragStarted { pos }) = interaction.event
61                    && let Some(shape) = data.shape_at_pos(pos)
62                {
63                    *self = MoveBlock::Dragging {
64                        shape,
65                        delta_pos: Vec2::ZERO,
66                    };
67                }
68            }
69            MoveBlock::Dragging { .. } => {
70                if let Some(Event::DragStopped { .. }) = interaction.event {
71                    let MoveBlock::Dragging { shape, delta_pos } = self else {
72                        unreachable!()
73                    };
74                    // Free shapes (images/icons) snap magnetically to alignment,
75                    // then an icon is contained to its block — both must match the
76                    // preview so there's no release flash.
77                    let magnetic = if shape.is_artwork() {
78                        crate::widget::alignment::snap_drag_offset(data, &[*shape], *delta_pos)
79                    } else {
80                        *delta_pos
81                    };
82                    let commit = data.constrain_move(*shape, magnetic);
83                    data.move_shape(*shape, commit);
84                    if shape.affects_routing() {
85                        // Drop the stale approach corners the drag left on routes
86                        // ending at the moved shape; the gesture's solve rider
87                        // rebuilds a clean approach in the same commit.
88                        data.trim_partial_route_approaches(&[*shape]);
89                    }
90                    // Keep the just-moved shape selected.
91                    return Some(Transition::SwitchTool(
92                        ResizeBlock::Selected { shape: *shape }.into(),
93                    ));
94                }
95            }
96        }
97        None
98    }
99}
100
101impl MoveBlock {
102    fn render<C: Canvas>(
103        &self,
104        data: &Drawing,
105        interaction: &Interaction,
106        painter: &mut Style<'_, C>,
107    ) {
108        match self {
109            MoveBlock::Idle => {
110                crate::widget::display::widget(data, interaction, painter);
111            }
112            MoveBlock::Dragging {
113                shape: dragging_id,
114                delta_pos,
115            } => {
116                // The dragged block's icon travels with it, so preview it moving
117                // alongside the block (they commit together in `move_block`).
118                let icon_id = dragging_id.block().map(ShapeId::Icon);
119                let raw = *delta_pos;
120                let dragging_id = *dragging_id;
121                let mut dragged_ids = vec![dragging_id];
122                dragged_ids.extend(icon_id);
123                // Free shapes (images/icons) snap magnetically to alignment so the
124                // guide lands; blocks/others preview at the raw delta and their
125                // guides use the grid-snapped offset (they snap on commit).
126                let is_free = dragging_id.is_artwork();
127                let magnetic = if is_free {
128                    crate::widget::alignment::snap_drag_offset(data, &dragged_ids, raw)
129                } else {
130                    raw
131                };
132                // Contain an icon to its block live, so the preview matches the
133                // clamped commit instead of leaving the rect and snapping back.
134                let delta = data.constrain_move(dragging_id, magnetic);
135                let guide_offset = if is_free { delta } else { snap_offset(raw) };
136                let guides =
137                    crate::widget::alignment::shape_drag_guides(data, &dragged_ids, guide_offset);
138                // A block previews at the grid-snapped position (it snaps on
139                // commit), so its icon must follow at that same snapped delta —
140                // not the raw `delta` — or the icon drifts off the block.
141                let icon_delta = snap_offset(raw);
142                DrawingPasses::new(data)
143                    .shape_mode(move |id| {
144                        if id == dragging_id {
145                            RenderMode::Moving { delta }
146                        } else if Some(id) == icon_id {
147                            RenderMode::Moving { delta: icon_delta }
148                        } else {
149                            RenderMode::Normal
150                        }
151                    })
152                    .draw(painter);
153                // A destination the emitter would refuse says so while the
154                // drag is still in the hand, rather than undoing itself in
155                // silence on release.
156                // The cells it would have to share are what is marked — one
157                // drag can conflict with several shapes, and an outline
158                // round the mover could not say which.
159                for overlap in data.conflicts(&[dragging_id], delta, Moving::Alone) {
160                    crate::render::draw_refused_conflict(px_rect(overlap), painter);
161                }
162                // Guides are drawn on top so a center guide (which runs through the
163                // opaque shapes it aligns) stays visible.
164                crate::widget::alignment::draw_guides(&guides, painter);
165            }
166        }
167    }
168}