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, 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
32pub fn draw_block_boundary(
37 bbox: Rect,
38 fill: Role,
39 stroke: impl Into<RoleStroke>,
40 structure: Structure,
41 painter: &mut Style<'_, impl Renderer>,
42) {
43 let stroke = stroke.into();
44 draw_box_outline(bbox, fill, stroke, painter);
45 if structure.opens_a_scope() {
46 draw_box_outline(
47 bbox.shrink(SHEET_INSET.get()),
48 Role::Transparent,
49 stroke,
50 painter,
51 );
52 }
53}
54
55pub fn draw_block_title(
56 bbox: Rect,
57 title: &ShapeLabel<'_>,
58 lock: InterfaceLock,
59 painter: &mut Style<'_, impl Renderer>,
60) {
61 if title.hidden {
64 return;
65 }
66 let font = painter.theme().title_font.clone();
67 let text_width = painter.text_size(title.name, &font).x;
68 let (pos, align) = clamped_block_title_position(bbox, title, text_width);
69 let title_role = if lock.is_locked() {
70 Role::LockedShapeTitle
71 } else {
72 Role::ShapeTitle
73 };
74 painter.text(pos, align, title.name, &font, title_role);
75}
76
77pub fn draw_block_type_placeholder(
81 bbox: Rect,
82 type_label: &ShapeLabel<'_>,
83 painter: &mut Style<'_, impl Renderer>,
84) {
85 if type_label.hidden || !type_label.name.is_empty() {
86 return;
87 }
88 let (pos, align) = block_type_position(bbox, type_label);
89 painter.text(
90 pos,
91 align,
92 crate::render::ADD_TYPE_PLACEHOLDER,
93 &painter.theme().type_font,
94 Role::PinLabelPlaceholder,
95 );
96}
97
98pub fn draw_block_type(
99 bbox: Rect,
100 type_label: &ShapeLabel<'_>,
101 lock: InterfaceLock,
102 painter: &mut Style<'_, impl Renderer>,
103) {
104 if type_label.hidden || type_label.name.is_empty() {
105 return;
106 }
107 let font = painter.theme().type_font.clone();
108 let text_width = painter.text_size(type_label.name, &font).x;
109 let (pos, align) = clamped_block_type_position(bbox, type_label, text_width);
110 let type_role = if lock.is_locked() {
111 Role::LockedShapeType
112 } else {
113 Role::ShapeType
114 };
115 painter.text(pos, align, type_label.name, &font, type_role);
116}
117
118fn size_label(bbox: Rect) -> String {
120 let size = grid_rect(bbox.min, bbox.max).size;
121 format!("{} × {}", size.w, size.h)
122}
123
124pub fn draw_size_readout(bbox: Rect, painter: &mut Style<'_, impl Renderer>) -> Rect {
129 painter.text(
130 pos2(
131 bbox.left() + GRID_SIZE / 2.0,
132 bbox.top() + PIN_TOP_MARGIN / 2.0,
133 ),
134 Align2::LEFT_CENTER,
135 size_label(bbox),
136 &painter.theme().tag_font,
137 Role::SizeReadout,
138 )
139}
140
141#[derive(Clone, Copy)]
144pub struct BlockPaint {
145 pub stroke: Role,
146 pub lock: InterfaceLock,
147 pub structure: Structure,
148}
149
150pub fn draw_block_frame(
155 bbox: Rect,
156 title: &ShapeLabel<'_>,
157 paint: BlockPaint,
158 painter: &mut Style<'_, impl Renderer>,
159) {
160 let BlockPaint {
161 stroke,
162 lock,
163 structure,
164 } = paint;
165 let fill = if lock.is_locked() {
166 Role::LockedShapeFill
167 } else {
168 Role::ShapeFill
169 };
170 draw_block_boundary(bbox, fill, (BLOCK_STROKE_WIDTH, stroke), structure, painter);
171 draw_block_title(bbox, title, lock, painter);
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::export::svg::SvgRenderer;
178 use crate::grid::pin_offset_y;
179 use crate::theme::Theme;
180 use blockworx_geom::vec2;
181 use blockworx_paint::FontChoice;
182
183 #[test]
184 fn the_size_readout_reads_in_whole_grid_cells() {
185 let bbox = Rect::from_min_size(
186 pos2(3.0 * GRID_SIZE, GRID_SIZE),
187 vec2(20.0 * GRID_SIZE, 16.0 * GRID_SIZE),
188 );
189 assert_eq!(size_label(bbox), "20 × 16");
190 }
191
192 #[test]
195 fn the_size_readout_sits_in_the_upper_left_pin_gutter() {
196 let bbox = Rect::from_min_size(pos2(0.0, 0.0), vec2(20.0 * GRID_SIZE, 16.0 * GRID_SIZE));
197 let theme = Theme::default();
198 let mut svg = SvgRenderer::new(theme.palette().clone(), FontChoice::Sketchy);
199 let mut style = Style::new(&theme, &mut svg);
200 let drawn = draw_size_readout(bbox, &mut style);
201 assert!(bbox.contains_rect(drawn), "{drawn:?} escaped the block");
202 assert!(
203 drawn.max.y <= pin_offset_y(bbox.top(), 0),
204 "{drawn:?} reaches the first pin slot"
205 );
206 assert!(
207 drawn.center().x < bbox.center().x,
208 "{drawn:?} is not in the left half"
209 );
210 }
211}