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