Skip to main content

blockworx/render/
block.rs

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
13/// Corner rounding of a block/port frame.
14const BOX_ROUNDING: WorldPx = WorldPx::new(GRID_SIZE / 8.0);
15
16/// How far inside its outline the second line of a scope-opening block runs.
17/// A quarter cell reads as its own line at a glance yet stays outboard of
18/// everything the block draws inside itself — the pin names start half a cell
19/// in, the size readout half a cell in from the left.
20pub 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
32/// A block's stroked boundary: its outline, plus — for a block that opens a
33/// scope — a second line inset within it, the classical contains-a-sheet
34/// notation. Every mode that draws a block body draws it through here, so the
35/// inset rides whatever stroke and fill that mode chose.
36pub 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    // No title toggle exists yet, but honor `hidden` for forward-compatibility
62    // (the flag lives on the `Label` namespace).
63    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
77/// Faint "Add type" prompt at a block's empty type slot. Like the tag
78/// placeholder, but suppressed on a locked block (whose type isn't editable) —
79/// the caller gates that.
80pub 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
118/// A block rect's size in whole grid cells, the way a document records it.
119fn size_label(bbox: Rect) -> String {
120    let size = grid_rect(bbox.min, bbox.max).size;
121    format!("{} × {}", size.w, size.h)
122}
123
124/// Draw a block's size in grid cells inside its upper-left corner, where the pin
125/// gutter above the first slot leaves the one interior region no label occupies.
126/// Shown only while the block is being resized, on the rect the drop will commit.
127/// Returns the drawn extent.
128pub 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/// What a block's body is painted from beyond its rect: the accent it strokes
142/// with, whether its interface is frozen, and whether it opens a scope.
143#[derive(Clone, Copy)]
144pub struct BlockPaint {
145    pub stroke: Role,
146    pub lock: InterfaceLock,
147    pub structure: Structure,
148}
149
150/// Draw a block's body: the outline fill, then the title. Drawn before the
151/// block's pins and labels so its opaque fill sits under them, never over them.
152/// The type label is left to the caller (it is block-only; ports/areas that
153/// share this frame have no type).
154pub 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    /// The readout lives inside the block's upper-left corner, in the pin gutter
193    /// above the first slot — the one interior region no label can occupy.
194    #[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}