Skip to main content

dotzuki_renderer/layout_engine/elements/
cursor.rs

1//! Cursor element — draws a selection glyph (▶) at a position computed from a
2//! base + grid offset.
3//!
4//! The element's `rect.tx`/`rect.ty` is the base (origin) tile. The final
5//! position is `base_tx + col*col_step` / `base_ty + row*row_step`, where
6//! `col`/`row` are data bindings. This expresses:
7//! - a 1-D list cursor (`row_step` set, `row = "{cursor}"`),
8//! - a 2-D grid (battle FIGHT/PKMN/ITEM/RUN: `col_step`+`row_step`),
9//! - an enum-offset selector (options: `col_step = 1`, `col = "{opt_index}"`).
10//!
11//! Multi-cursor screens (options rows, party ▶ + ◆) place several cursor
12//! elements, each with its own `visible` condition and bindings.
13
14use dotzuki_engine::render::painter::Painter;
15use dotzuki_engine::render::TilePos;
16
17use crate::layout_engine::elements::text::parse_color;
18use crate::layout_engine::types::{CursorParams, DataContext, LayoutElement, Theme};
19
20/// Render a cursor glyph into the framebuffer via `painter`.
21pub fn render_cursor(
22    element: &LayoutElement,
23    params: &CursorParams,
24    ctx: &DataContext,
25    theme: &Theme,
26    painter: &mut dyn Painter,
27) {
28    let base_tx = element.rect.tx.resolve(ctx);
29    let base_ty = element.rect.ty.resolve(ctx);
30    let col = params.col.resolve(ctx);
31    let row = params.row.resolve(ctx);
32
33    let tx = base_tx + col * params.col_step;
34    let ty = base_ty + row * params.row_step;
35
36    // Explicit colour wins; else the theme cursor ink (→ ink → INK_BLACK).
37    let color = match params.color.as_deref() {
38        Some(c) => parse_color(c),
39        None => theme.cursor_ink(),
40    };
41
42    // Proportional screens place the glyph at pixel precision; the legacy tile
43    // path is preserved byte-for-byte for pokered.
44    if theme.proportional(painter.supports_proportional()) {
45        let mut buf = [0u8; 4];
46        painter.draw_text_px(tx * 8, ty * 8, params.glyph_char().encode_utf8(&mut buf), color);
47    } else {
48        painter.draw_glyph(TilePos::new(tx, ty), params.glyph_char(), color);
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::layout_engine::types::{Coord, ElementParams, ElementRect, Visibility};
56    use dotzuki_engine::render::{Rgba, TileRect};
57    use std::cell::RefCell;
58
59    #[derive(Default)]
60    struct Rec {
61        glyphs: RefCell<Vec<(u32, u32, char)>>,
62    }
63    impl Painter for Rec {
64        fn clear(&mut self, _c: Rgba) {}
65        fn draw_text_box(&mut self, _r: TileRect, _c: Rgba) {}
66        fn draw_text(&mut self, _p: TilePos, _t: &str, _c: Rgba) {}
67        fn draw_glyph(&mut self, p: TilePos, g: char, _c: Rgba) {
68            self.glyphs.borrow_mut().push((p.tx, p.ty, g));
69        }
70        fn draw_pixel_rect(&mut self, _x: u32, _y: u32, _w: u32, _h: u32, _c: Rgba) {}
71        fn draw_gb_tile(&mut self, _p: TilePos, _t: u8, _f: &str, _c: Rgba) {}
72    }
73
74    fn elem(tx: u32, ty: u32, params: CursorParams) -> LayoutElement {
75        LayoutElement {
76            id: String::new(),
77            element_type: "cursor".into(),
78            rect: ElementRect {
79                tx: Coord::Literal(tx),
80                ty: Coord::Literal(ty),
81                tw: Some(1),
82                th: Some(1),
83            },
84            visible: Visibility::Static(true),
85            z_index: 0,
86            params: ElementParams::Cursor(params),
87        }
88    }
89
90    fn cparams(col: Coord, row: Coord, col_step: u32, row_step: u32) -> CursorParams {
91        CursorParams { glyph: None, color: None, col, row, col_step, row_step }
92    }
93
94    #[test]
95    fn grid_position_computed_from_col_row() {
96        // base (1,12), 2x2 grid: col_step 9, row_step 2, at col=1,row=1 → (10,14)
97        let mut ctx = DataContext::new();
98        ctx.set("c", 1i64);
99        ctx.set("r", 1i64);
100        let e = elem(1, 12, cparams(Coord::Template("{c}".into()), Coord::Template("{r}".into()), 9, 2));
101        let ElementParams::Cursor(ref p) = e.params else { unreachable!() };
102        let mut painter = Rec::default();
103        render_cursor(&e, p, &ctx, &Theme::default(), &mut painter);
104        assert_eq!(painter.glyphs.borrow()[0], (10, 14, '\u{25B6}'));
105    }
106
107    #[test]
108    fn defaults_to_base_and_triangle_glyph() {
109        let e = elem(3, 5, cparams(Coord::Literal(0), Coord::Literal(0), 0, 0));
110        let ElementParams::Cursor(ref p) = e.params else { unreachable!() };
111        let mut painter = Rec::default();
112        render_cursor(&e, p, &DataContext::new(), &Theme::default(), &mut painter);
113        assert_eq!(painter.glyphs.borrow()[0], (3, 5, '\u{25B6}'));
114    }
115}