Skip to main content

dotzuki_renderer/layout_engine/elements/
primitives.rs

1//! Primitive shape elements — `bracket`, `pixel_rect`.
2//!
3//! These compose from the painter's `draw_pixel_rect` and reproduce the
4//! pokered-ui `Frame` primitives (bracket box, raw rect) so layouts can be
5//! expressed declaratively in `.gui`. Game-specific primitives (e.g. the
6//! Gen-I HP bar) are NOT built in — games register them as `custom:*`
7//! elements via the `ElementRegistry`.
8
9use dotzuki_engine::render::painter::Painter;
10use dotzuki_engine::render::Rgba;
11
12use crate::layout_engine::elements::text::parse_color;
13use crate::layout_engine::types::{BracketParams, DataContext, LayoutElement, PixelRectParams};
14
15const TILE: u32 = 8;
16
17/// Partial box border (1px edges) at the element rect. Matches
18/// `Frame::bracket_box` (interior-corner pixel offsets).
19pub fn render_bracket(
20    element: &LayoutElement,
21    params: &BracketParams,
22    ctx: &DataContext,
23    painter: &mut dyn Painter,
24) {
25    let tx = element.rect.tx.resolve(ctx);
26    let ty = element.rect.ty.resolve(ctx);
27    let tw = element.rect.tw.unwrap_or(1);
28    let th = element.rect.th.unwrap_or(1);
29    let color = params
30        .color
31        .as_deref()
32        .map(parse_color)
33        .unwrap_or(Rgba::INK_BLACK);
34
35    let left_px = tx * TILE;
36    let right_px = (tx + tw - 1) * TILE + 6;
37    let top_px = ty * TILE;
38    let bot_px = (ty + th - 1) * TILE + 6;
39
40    if params.right {
41        painter.draw_pixel_rect(right_px, top_px, 1, bot_px - top_px + 1, color);
42    }
43    if params.left {
44        painter.draw_pixel_rect(left_px, top_px, 1, bot_px - top_px + 1, color);
45    }
46    if params.top {
47        painter.draw_pixel_rect(left_px, top_px, right_px - left_px + 1, 1, color);
48    }
49    if params.bottom {
50        painter.draw_pixel_rect(left_px, bot_px, right_px - left_px + 1, 1, color);
51        if params.with_arrow && left_px >= 3 {
52            let arrow_left = left_px - 3;
53            painter.draw_pixel_rect(arrow_left, bot_px, 4, 1, color);
54            painter.draw_pixel_rect(arrow_left, bot_px - 1, 1, 1, color);
55            painter.draw_pixel_rect(arrow_left, bot_px + 1, 1, 1, color);
56        }
57    }
58}
59
60/// Raw filled rectangle in pixel coordinates.
61pub fn render_pixel_rect(params: &PixelRectParams, painter: &mut dyn Painter) {
62    let color = params
63        .color
64        .as_deref()
65        .map(parse_color)
66        .unwrap_or(Rgba::INK_BLACK);
67    painter.draw_pixel_rect(params.px, params.py, params.pw, params.ph, color);
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::layout_engine::types::ElementParams;
74    use crate::layout_engine::types::{Coord, ElementRect, LayoutElement, Visibility};
75    use dotzuki_engine::render::{TilePos, TileRect};
76    use std::cell::RefCell;
77
78    #[derive(Default)]
79    struct PxRec {
80        rects: RefCell<Vec<(u32, u32, u32, u32, Rgba)>>,
81    }
82    impl Painter for PxRec {
83        fn clear(&mut self, _c: Rgba) {}
84        fn draw_text_box(&mut self, _r: TileRect, _c: Rgba) {}
85        fn draw_text(&mut self, _p: TilePos, _t: &str, _c: Rgba) {}
86        fn draw_glyph(&mut self, _p: TilePos, _g: char, _c: Rgba) {}
87        fn draw_pixel_rect(&mut self, x: u32, y: u32, w: u32, h: u32, c: Rgba) {
88            self.rects.borrow_mut().push((x, y, w, h, c));
89        }
90        fn draw_gb_tile(&mut self, _p: TilePos, _t: u8, _f: &str, _c: Rgba) {}
91    }
92
93    #[test]
94    fn bracket_draws_requested_sides_only() {
95        let e = LayoutElement {
96            id: String::new(),
97            element_type: "bracket".into(),
98            rect: ElementRect {
99                tx: Coord::Literal(2),
100                ty: Coord::Literal(3),
101                tw: Some(4),
102                th: Some(2),
103            },
104            visible: Visibility::Static(true),
105            z_index: 0,
106            params: ElementParams::Bracket(BracketParams {
107                color: None,
108                left: true,
109                right: false,
110                top: false,
111                bottom: true,
112                with_arrow: false,
113            }),
114        };
115        let ElementParams::Bracket(ref p) = e.params else { unreachable!() };
116        let mut painter = PxRec::default();
117        render_bracket(&e, p, &DataContext::new(), &mut painter);
118        let rects = painter.rects.borrow();
119        // one vertical (left) + one horizontal (bottom) line
120        assert_eq!(rects.len(), 2);
121        assert_eq!(rects[0].2, 1, "left edge is 1px wide");
122        assert_eq!(rects[1].3, 1, "bottom edge is 1px tall");
123    }
124
125    #[test]
126    fn pixel_rect_draws_raw_rect() {
127        let params = PixelRectParams {
128            color: Some("white".into()),
129            px: 10,
130            py: 20,
131            pw: 30,
132            ph: 2,
133        };
134        let mut painter = PxRec::default();
135        render_pixel_rect(&params, &mut painter);
136        assert_eq!(
137            *painter.rects.borrow(),
138            vec![(10, 20, 30, 2, Rgba::INK_WHITE)]
139        );
140    }
141}