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 fn draw_gb_tile(&mut self, pos: TilePos, tile_id: u8, fallback: &str, color: Rgba);
11
12 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 fn measure_text_px(&self, text: &str) -> u32 {
30 text.chars().count() as u32 * 8
31 }
32
33 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 fn measure_text_px_scaled(&self, text: &str, scale: u32) -> u32 {
45 self.measure_text_px(text) * scale.max(1)
46 }
47
48 fn supports_proportional(&self) -> bool {
52 false
53 }
54
55 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 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 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 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 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 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 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 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#[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}