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}