1use crate::{
2 edit::naming::InterfaceLock,
3 grid::{BLOCK_STROKE_WIDTH, GRID_SIZE, PIN_TOP_MARGIN, grid_rect},
4 path::Structure,
5 shape::ShapeLabel,
6 theme::{Role, RoleStroke, Style},
7};
8use blockworx_geom::{Align2, Pos2, Rect, WorldPx, pos2};
9use blockworx_paint::Renderer;
10
11use super::{block_type_position, clamped_block_title_position, clamped_block_type_position};
12
13const BOX_ROUNDING: WorldPx = WorldPx::new(GRID_SIZE / 8.0);
15
16pub const SHEET_INSET: WorldPx = WorldPx::new(GRID_SIZE / 4.0);
21
22pub fn draw_box_outline(
23 bbox: Rect,
24 fill: Role,
25 stroke: impl Into<RoleStroke>,
26 painter: &mut Style<'_, impl Renderer>,
27) {
28 let stroke = stroke.into();
29 painter.rect(bbox, BOX_ROUNDING, fill, stroke);
30}
31
32const HATCH_SPACING: f32 = GRID_SIZE / 2.0;
34
35fn hatch(rect: Rect, stroke: impl Into<RoleStroke>, painter: &mut Style<'_, impl Renderer>) {
42 let stroke = stroke.into();
43 let (w, h) = (rect.width(), rect.height());
44 if w <= 0.0 || h <= 0.0 {
45 return;
46 }
47 let mut t = -h;
48 while t < w {
49 let from = (-t).max(0.0);
50 let to = h.min(w - t);
51 if to > from {
52 painter.line_segment(
53 [
54 Pos2::new(rect.min.x + t + from, rect.min.y + from),
55 Pos2::new(rect.min.x + t + to, rect.min.y + to),
56 ],
57 stroke,
58 );
59 }
60 t += HATCH_SPACING;
61 }
62}
63
64pub fn draw_refused_conflict(overlap: Rect, painter: &mut Style<'_, impl Renderer>) {
73 hatch(overlap, (2.0, Role::MoveRefusedStroke), painter);
74 painter.rect(
75 overlap,
76 WorldPx::ZERO,
77 Role::Transparent,
78 (1.5, Role::MoveRefusedStroke),
79 );
80}
81
82pub fn draw_standing_conflict(overlap: Rect, painter: &mut Style<'_, impl Renderer>) {
90 hatch(overlap, (1.5, Role::StandingConflict), painter);
91}
92
93pub fn draw_block_boundary(
98 bbox: Rect,
99 fill: Role,
100 stroke: impl Into<RoleStroke>,
101 structure: Structure,
102 painter: &mut Style<'_, impl Renderer>,
103) {
104 let stroke = stroke.into();
105 draw_box_outline(bbox, fill, stroke, painter);
106 if structure.opens_a_scope() {
107 draw_box_outline(
108 bbox.shrink(SHEET_INSET.get()),
109 Role::Transparent,
110 stroke,
111 painter,
112 );
113 }
114}
115
116pub fn draw_block_title(
117 bbox: Rect,
118 title: &ShapeLabel<'_>,
119 lock: InterfaceLock,
120 painter: &mut Style<'_, impl Renderer>,
121) {
122 if title.hidden {
125 return;
126 }
127 let font = painter.theme().title_font.clone();
128 let text_width = painter.text_size(title.name, &font).x;
129 let (pos, align) = clamped_block_title_position(bbox, title, text_width);
130 let title_role = if lock.is_locked() {
131 Role::LockedShapeTitle
132 } else {
133 Role::ShapeTitle
134 };
135 painter.text(pos, align, title.name, &font, title_role);
136}
137
138pub fn draw_block_type_placeholder(
142 bbox: Rect,
143 type_label: &ShapeLabel<'_>,
144 painter: &mut Style<'_, impl Renderer>,
145) {
146 if type_label.hidden || !type_label.name.is_empty() {
147 return;
148 }
149 let (pos, align) = block_type_position(bbox, type_label);
150 painter.text(
151 pos,
152 align,
153 crate::render::ADD_TYPE_PLACEHOLDER,
154 &painter.theme().type_font,
155 Role::PinLabelPlaceholder,
156 );
157}
158
159pub fn draw_block_type(
160 bbox: Rect,
161 type_label: &ShapeLabel<'_>,
162 lock: InterfaceLock,
163 painter: &mut Style<'_, impl Renderer>,
164) {
165 if type_label.hidden || type_label.name.is_empty() {
166 return;
167 }
168 let font = painter.theme().type_font.clone();
169 let text_width = painter.text_size(type_label.name, &font).x;
170 let (pos, align) = clamped_block_type_position(bbox, type_label, text_width);
171 let type_role = if lock.is_locked() {
172 Role::LockedShapeType
173 } else {
174 Role::ShapeType
175 };
176 painter.text(pos, align, type_label.name, &font, type_role);
177}
178
179fn size_label(bbox: Rect) -> String {
181 let size = grid_rect(bbox.min, bbox.max).size;
182 format!("{} × {}", size.w, size.h)
183}
184
185pub fn draw_size_readout(bbox: Rect, painter: &mut Style<'_, impl Renderer>) -> Rect {
190 painter.text(
191 pos2(
192 bbox.left() + GRID_SIZE / 2.0,
193 bbox.top() + PIN_TOP_MARGIN / 2.0,
194 ),
195 Align2::LEFT_CENTER,
196 size_label(bbox),
197 &painter.theme().tag_font,
198 Role::SizeReadout,
199 )
200}
201
202#[derive(Clone, Copy)]
205pub struct BlockPaint {
206 pub stroke: Role,
207 pub lock: InterfaceLock,
208 pub structure: Structure,
209}
210
211pub fn draw_block_frame(
216 bbox: Rect,
217 title: &ShapeLabel<'_>,
218 paint: BlockPaint,
219 painter: &mut Style<'_, impl Renderer>,
220) {
221 let BlockPaint {
222 stroke,
223 lock,
224 structure,
225 } = paint;
226 let fill = if lock.is_locked() {
227 Role::LockedShapeFill
228 } else {
229 Role::ShapeFill
230 };
231 draw_block_boundary(bbox, fill, (BLOCK_STROKE_WIDTH, stroke), structure, painter);
232 draw_block_title(bbox, title, lock, painter);
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::grid::pin_offset_y;
239 use crate::theme::Theme;
240 use blockworx_geom::vec2;
241 use blockworx_paint::FontChoice;
242 use blockworx_text::measure::Measured;
243
244 #[test]
245 fn the_size_readout_reads_in_whole_grid_cells() {
246 let bbox = Rect::from_min_size(
247 pos2(3.0 * GRID_SIZE, GRID_SIZE),
248 vec2(20.0 * GRID_SIZE, 16.0 * GRID_SIZE),
249 );
250 assert_eq!(size_label(bbox), "20 × 16");
251 }
252
253 #[test]
256 fn the_size_readout_sits_in_the_upper_left_pin_gutter() {
257 let bbox = Rect::from_min_size(pos2(0.0, 0.0), vec2(20.0 * GRID_SIZE, 16.0 * GRID_SIZE));
258 let theme = Theme::default();
259 let canvas = Measured::new(FontChoice::Sketchy, theme.palette().clone());
260 let drawn = canvas.frame(|painter| {
261 let mut style = Style::new(&theme, painter);
262 draw_size_readout(bbox, &mut style)
263 });
264 assert!(bbox.contains_rect(drawn), "{drawn:?} escaped the block");
265 assert!(
266 drawn.max.y <= pin_offset_y(bbox.top(), 0),
267 "{drawn:?} reaches the first pin slot"
268 );
269 assert!(
270 drawn.center().x < bbox.center().x,
271 "{drawn:?} is not in the left half"
272 );
273 }
274}