Skip to main content

dotzuki_engine/render/
painter.rs

1use crate::render::{Rgba, BracketSides, TilePos, TileRect};
2
3pub trait Painter {
4    fn clear(&mut self, color: Rgba);
5    fn draw_text_box(&mut self, rect: TileRect, color: Rgba);
6    fn draw_text(&mut self, pos: TilePos, text: &str, color: Rgba);
7    fn draw_glyph(&mut self, pos: TilePos, glyph: char, color: Rgba);
8    fn draw_pixel_rect(&mut self, px: u32, py: u32, pw: u32, ph: u32, color: Rgba);
9    /// Pixel backend draws `tile_id` via the fallback text glyph. Recording backends log only `tile_id`.
10    fn draw_gb_tile(&mut self, pos: TilePos, tile_id: u8, fallback: &str, color: Rgba);
11
12    // ── Proportional (pixel-precise) text — opt-in high-resolution path ──────
13    //
14    // The legacy methods above place every glyph on the 8×8 tile grid, which is
15    // correct for Game Boy half-width fonts but clips/overlaps proportional CJK
16    // glyphs. These methods let the layout engine render at true pixel precision
17    // with per-glyph advance. They have default impls that fall back to the tile
18    // path, so existing `Painter` implementations (and all recording/mock
19    // painters) keep compiling unchanged; only pixel backends override them.
20
21    /// Draw `text` starting at pixel `(px, py)` with proportional per-glyph
22    /// advance. Default: round to the nearest tile and use [`Painter::draw_text`].
23    fn draw_text_px(&mut self, px: u32, py: u32, text: &str, color: Rgba) {
24        self.draw_text(TilePos::new(px / 8, py / 8), text, color);
25    }
26
27    /// The pixel width `text` would occupy via [`Painter::draw_text_px`].
28    /// Default: 8px per char (the tile cell width).
29    fn measure_text_px(&self, text: &str) -> u32 {
30        text.chars().count() as u32 * 8
31    }
32
33    /// Draw `text` at pixel `(px, py)` scaled by an integer factor — every glyph
34    /// pixel becomes a `scale × scale` block. Powers big title/heading text. The
35    /// default ignores `scale` and falls back to [`Painter::draw_text_px`], so
36    /// recording/mock backends stay unchanged; pixel backends override it.
37    fn draw_text_px_scaled(&mut self, px: u32, py: u32, text: &str, scale: u32, color: Rgba) {
38        let _ = scale;
39        self.draw_text_px(px, py, text, color);
40    }
41
42    /// The pixel width `text` occupies via [`Painter::draw_text_px_scaled`].
43    /// Default: [`Painter::measure_text_px`] × `scale`.
44    fn measure_text_px_scaled(&self, text: &str, scale: u32) -> u32 {
45        self.measure_text_px(text) * scale.max(1)
46    }
47
48    /// Whether this backend renders true proportional pixel text (overrides the
49    /// two methods above). The layout engine only takes its proportional path
50    /// when this is `true`, so recording/mock backends stay on the tile path.
51    fn supports_proportional(&self) -> bool {
52        false
53    }
54
55    /// Blit a full-colour `src_w × src_h` RGBA image (row-major) into the pixel
56    /// box `(dst_px, dst_py, dst_w, dst_h)`, nearest-neighbour scaled to fill the
57    /// box. Fully-transparent (`a == 0`) source pixels are skipped; `flip_x`/
58    /// `flip_y` mirror the source. Used by the layout engine's `image` element.
59    ///
60    /// Default: per-destination-pixel via [`Painter::draw_pixel_rect`], so every
61    /// existing backend (incl. recording/mock painters) works unchanged; pixel
62    /// backends may override for speed.
63    fn draw_rgba(
64        &mut self,
65        dst_px: u32,
66        dst_py: u32,
67        dst_w: u32,
68        dst_h: u32,
69        pixels: &[Rgba],
70        src_w: u32,
71        src_h: u32,
72        flip_x: bool,
73        flip_y: bool,
74    ) {
75        if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
76            return;
77        }
78        for dy in 0..dst_h {
79            let mut sy = dy * src_h / dst_h;
80            if flip_y {
81                sy = src_h - 1 - sy;
82            }
83            for dx in 0..dst_w {
84                let mut sx = dx * src_w / dst_w;
85                if flip_x {
86                    sx = src_w - 1 - sx;
87                }
88                let idx = (sy * src_w + sx) as usize;
89                let Some(&c) = pixels.get(idx) else { continue };
90                if c.a == 0 {
91                    continue;
92                }
93                self.draw_pixel_rect(dst_px + dx, dst_py + dy, 1, 1, c);
94            }
95        }
96    }
97}
98
99pub struct Ui<'p, P: Painter> {
100    painter: &'p mut P,
101    origin_tx: u32,
102    origin_ty: u32,
103}
104
105impl<'p, P: Painter> Ui<'p, P> {
106    pub fn new(painter: &'p mut P) -> Self {
107        Self { painter, origin_tx: 0, origin_ty: 0 }
108    }
109
110    /// Returns a mutable reference to the underlying [`Painter`].
111    pub fn painter(&mut self) -> &mut P {
112        self.painter
113    }
114
115    pub fn clear(&mut self, color: impl Into<Rgba>) {
116        self.painter.clear(color.into());
117    }
118
119    pub fn text_box<F>(&mut self, rect: TileRect, color: impl Into<Rgba>, border: bool, body: F)
120    where
121        F: FnOnce(&mut Frame<'_, P>),
122    {
123        let absolute = rect.translated(self.origin_tx, self.origin_ty);
124        if border {
125            self.painter.draw_text_box(absolute, color.into());
126        }
127        let inset: u32 = if border { 1 } else { 0 };
128        let mut frame = Frame {
129            painter: self.painter,
130            origin_tx: absolute.tx + inset,
131            origin_ty: absolute.ty + inset,
132        };
133        body(&mut frame);
134    }
135}
136
137pub struct Frame<'p, P: Painter> {
138    painter: &'p mut P,
139    origin_tx: u32,
140    origin_ty: u32,
141}
142
143impl<'p, P: Painter> Frame<'p, P> {
144    pub fn label(&mut self, tx: u32, ty: u32, text: &str, color: impl Into<Rgba>) {
145        self.painter.draw_text(
146            TilePos::new(self.origin_tx + tx, self.origin_ty + ty),
147            text,
148            color.into(),
149        );
150    }
151
152    pub fn cursor_at(&mut self, tx: u32, ty: u32, color: impl Into<Rgba>) {
153        self.cursor_glyph_at(tx, ty, '\u{25B6}', color);
154    }
155
156    pub fn cursor_glyph_at(&mut self, tx: u32, ty: u32, glyph: char, color: impl Into<Rgba>) {
157        self.painter.draw_glyph(
158            TilePos::new(self.origin_tx + tx, self.origin_ty + ty),
159            glyph,
160            color.into(),
161        );
162    }
163
164    /// Draw a glyph at a screen-absolute tile position. Bypasses the frame
165    /// origin so the glyph appears at the requested column/row regardless of
166    /// where the enclosing text_box is placed. Useful for cursors that sit at
167    /// the border edge of a box (where frame-relative coordinates would be
168    /// negative and thus inexpressible as u32).
169    pub fn abs_glyph(&mut self, tx: u32, ty: u32, glyph: char, color: impl Into<Rgba>) {
170        self.painter.draw_glyph(TilePos::new(tx, ty), glyph, color.into());
171    }
172
173    pub fn menu_list(
174        &mut self,
175        tx: u32,
176        ty: u32,
177        items: &[&str],
178        cursor: usize,
179        row_step: u32,
180        color: impl Into<Rgba>,
181    ) {
182        let color = color.into();
183        for (i, item) in items.iter().enumerate() {
184            let row = ty + (i as u32) * row_step;
185            self.label(tx + 1, row, item, color);
186            if i == cursor {
187                self.cursor_at(tx, row, color);
188            }
189        }
190    }
191
192    pub fn pixel_rect(&mut self, dx_px: u32, dy_px: u32, w_px: u32, h_px: u32, color: impl Into<Rgba>) {
193        let (ox_px, oy_px) = TilePos::new(self.origin_tx, self.origin_ty).to_pixels();
194        self.painter
195            .draw_pixel_rect(ox_px + dx_px, oy_px + dy_px, w_px, h_px, color.into());
196    }
197
198    pub fn sub_text_box<F>(&mut self, rect: TileRect, color: impl Into<Rgba>, body: F)
199    where
200        F: FnOnce(&mut Frame<'_, P>),
201    {
202        let absolute = rect.translated(self.origin_tx, self.origin_ty);
203        self.painter.draw_text_box(absolute, color.into());
204        let mut child = Frame {
205            painter: self.painter,
206            origin_tx: absolute.tx + 1,
207            origin_ty: absolute.ty + 1,
208        };
209        body(&mut child);
210    }
211
212    pub fn gb_tile(&mut self, tx: u32, ty: u32, tile_id: u8, fallback: &str, color: impl Into<Rgba>) {
213        self.painter.draw_gb_tile(
214            TilePos::new(self.origin_tx + tx, self.origin_ty + ty),
215            tile_id,
216            fallback,
217            color.into(),
218        );
219    }
220
221    /// Draws a partial border (one or more sides) inside the tile rect
222    /// `rect`. Pixel offsets match the interior corner of the GB box-drawing
223    /// glyphs (`+6` on the right of a tile column, `+6` on the bottom of a
224    /// tile row), so the bracket aligns exactly with `text_box` borders.
225    ///
226    /// If `with_arrow` is true, draws a 4px halfarrow (`<`) at the
227    /// far-left of the bottom edge — matching original `DrawLineBox`
228    /// terminator. Requires `sides.bottom = true`.
229    pub fn bracket_box(
230        &mut self,
231        rect: TileRect,
232        sides: BracketSides,
233        with_arrow: bool,
234        color: impl Into<Rgba>,
235    ) {
236        let color = color.into();
237        // Interior-corner offsets: right edge x = (last_col)*8 + 6,
238        // bottom edge y = (last_row)*8 + 6 — matches box_tiles glyph
239        // pixels in pokered-renderer::embedded_font.
240        let left_px = rect.tx * 8;
241        let right_px = (rect.tx + rect.tw - 1) * 8 + 6;
242        let top_px = rect.ty * 8;
243        let bot_px = (rect.ty + rect.th - 1) * 8 + 6;
244
245        if sides.right {
246            self.pixel_rect(right_px, top_px, 1, bot_px - top_px + 1, color);
247        }
248        if sides.left {
249            self.pixel_rect(left_px, top_px, 1, bot_px - top_px + 1, color);
250        }
251        if sides.top {
252            self.pixel_rect(left_px, top_px, right_px - left_px + 1, 1, color);
253        }
254        if sides.bottom {
255            self.pixel_rect(left_px, bot_px, right_px - left_px + 1, 1, color);
256            if with_arrow && left_px >= 3 {
257                let arrow_left = left_px - 3;
258                self.pixel_rect(arrow_left, bot_px, 4, 1, color);
259                self.pixel_rect(arrow_left, bot_px - 1, 1, 1, color);
260                self.pixel_rect(arrow_left, bot_px + 1, 1, 1, color);
261            }
262        }
263    }
264
265    /// 1-pixel-wide vertical line at the right interior edge of tile column
266    /// `tx`, spanning `length_tiles` tile rows starting at `ty`.
267    pub fn vline(&mut self, tx: u32, ty: u32, length_tiles: u32, color: impl Into<Rgba>) {
268        let px = tx * 8 + 6;
269        let py = ty * 8;
270        self.pixel_rect(px, py, 1, length_tiles * 8, color);
271    }
272
273    /// 1-pixel-tall horizontal line at the bottom interior edge of tile row
274    /// `ty`, spanning `length_tiles` tile columns starting at `tx`.
275    pub fn hline(&mut self, tx: u32, ty: u32, length_tiles: u32, color: impl Into<Rgba>) {
276        let px = tx * 8;
277        let py = ty * 8 + 6;
278        self.pixel_rect(px, py, length_tiles * 8, 1, color);
279    }
280
281    /// Draws a vertical sequence of label-value pairs starting at tile
282    /// `(tx, ty)`. Each pair places `label` at `(tx, row)` and `value` at
283    /// `(tx + value_indent_x, row + 1)`, advancing by `row_step` tile rows.
284    pub fn label_value_grid(
285        &mut self,
286        tx: u32,
287        ty: u32,
288        rows: &[LabelValue<'_>],
289        value_indent_x: u32,
290        row_step: u32,
291        label_color: impl Into<Rgba>,
292        value_color: impl Into<Rgba>,
293    ) {
294        let label_color = label_color.into();
295        let value_color = value_color.into();
296        for (i, lv) in rows.iter().enumerate() {
297            let row = ty + (i as u32) * row_step;
298            self.label(tx, row, lv.label, label_color);
299            self.label(tx + value_indent_x, row + 1, &lv.value, value_color);
300        }
301    }
302}
303
304/// Label and formatted value, for [`Frame::label_value_grid`].
305#[derive(Debug, Clone)]
306pub struct LabelValue<'a> {
307    pub label: &'a str,
308    pub value: String,
309}
310
311impl<'a> LabelValue<'a> {
312    pub fn new(label: &'a str, value: impl Into<String>) -> Self {
313        Self { label, value: value.into() }
314    }
315}