1use crate::render::{BracketSides, Rgba, 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 {
108 painter,
109 origin_tx: 0,
110 origin_ty: 0,
111 }
112 }
113
114 pub fn painter(&mut self) -> &mut P {
116 self.painter
117 }
118
119 pub fn clear(&mut self, color: impl Into<Rgba>) {
120 self.painter.clear(color.into());
121 }
122
123 pub fn text_box<F>(&mut self, rect: TileRect, color: impl Into<Rgba>, border: bool, body: F)
124 where
125 F: FnOnce(&mut Frame<'_, P>),
126 {
127 let absolute = rect.translated(self.origin_tx, self.origin_ty);
128 if border {
129 self.painter.draw_text_box(absolute, color.into());
130 }
131 let inset: u32 = if border { 1 } else { 0 };
132 let mut frame = Frame {
133 painter: self.painter,
134 origin_tx: absolute.tx + inset,
135 origin_ty: absolute.ty + inset,
136 };
137 body(&mut frame);
138 }
139}
140
141pub struct Frame<'p, P: Painter> {
142 painter: &'p mut P,
143 origin_tx: u32,
144 origin_ty: u32,
145}
146
147impl<'p, P: Painter> Frame<'p, P> {
148 pub fn label(&mut self, tx: u32, ty: u32, text: &str, color: impl Into<Rgba>) {
149 self.painter.draw_text(
150 TilePos::new(self.origin_tx + tx, self.origin_ty + ty),
151 text,
152 color.into(),
153 );
154 }
155
156 pub fn cursor_at(&mut self, tx: u32, ty: u32, color: impl Into<Rgba>) {
157 self.cursor_glyph_at(tx, ty, '\u{25B6}', color);
158 }
159
160 pub fn cursor_glyph_at(&mut self, tx: u32, ty: u32, glyph: char, color: impl Into<Rgba>) {
161 self.painter.draw_glyph(
162 TilePos::new(self.origin_tx + tx, self.origin_ty + ty),
163 glyph,
164 color.into(),
165 );
166 }
167
168 pub fn abs_glyph(&mut self, tx: u32, ty: u32, glyph: char, color: impl Into<Rgba>) {
174 self.painter
175 .draw_glyph(TilePos::new(tx, ty), glyph, color.into());
176 }
177
178 pub fn menu_list(
179 &mut self,
180 tx: u32,
181 ty: u32,
182 items: &[&str],
183 cursor: usize,
184 row_step: u32,
185 color: impl Into<Rgba>,
186 ) {
187 let color = color.into();
188 for (i, item) in items.iter().enumerate() {
189 let row = ty + (i as u32) * row_step;
190 self.label(tx + 1, row, item, color);
191 if i == cursor {
192 self.cursor_at(tx, row, color);
193 }
194 }
195 }
196
197 pub fn pixel_rect(
198 &mut self,
199 dx_px: u32,
200 dy_px: u32,
201 w_px: u32,
202 h_px: u32,
203 color: impl Into<Rgba>,
204 ) {
205 let (ox_px, oy_px) = TilePos::new(self.origin_tx, self.origin_ty).to_pixels();
206 self.painter
207 .draw_pixel_rect(ox_px + dx_px, oy_px + dy_px, w_px, h_px, color.into());
208 }
209
210 pub fn sub_text_box<F>(&mut self, rect: TileRect, color: impl Into<Rgba>, body: F)
211 where
212 F: FnOnce(&mut Frame<'_, P>),
213 {
214 let absolute = rect.translated(self.origin_tx, self.origin_ty);
215 self.painter.draw_text_box(absolute, color.into());
216 let mut child = Frame {
217 painter: self.painter,
218 origin_tx: absolute.tx + 1,
219 origin_ty: absolute.ty + 1,
220 };
221 body(&mut child);
222 }
223
224 pub fn gb_tile(
225 &mut self,
226 tx: u32,
227 ty: u32,
228 tile_id: u8,
229 fallback: &str,
230 color: impl Into<Rgba>,
231 ) {
232 self.painter.draw_gb_tile(
233 TilePos::new(self.origin_tx + tx, self.origin_ty + ty),
234 tile_id,
235 fallback,
236 color.into(),
237 );
238 }
239
240 pub fn bracket_box(
249 &mut self,
250 rect: TileRect,
251 sides: BracketSides,
252 with_arrow: bool,
253 color: impl Into<Rgba>,
254 ) {
255 let color = color.into();
256 let left_px = rect.tx * 8;
260 let right_px = (rect.tx + rect.tw - 1) * 8 + 6;
261 let top_px = rect.ty * 8;
262 let bot_px = (rect.ty + rect.th - 1) * 8 + 6;
263
264 if sides.right {
265 self.pixel_rect(right_px, top_px, 1, bot_px - top_px + 1, color);
266 }
267 if sides.left {
268 self.pixel_rect(left_px, top_px, 1, bot_px - top_px + 1, color);
269 }
270 if sides.top {
271 self.pixel_rect(left_px, top_px, right_px - left_px + 1, 1, color);
272 }
273 if sides.bottom {
274 self.pixel_rect(left_px, bot_px, right_px - left_px + 1, 1, color);
275 if with_arrow && left_px >= 3 {
276 let arrow_left = left_px - 3;
277 self.pixel_rect(arrow_left, bot_px, 4, 1, color);
278 self.pixel_rect(arrow_left, bot_px - 1, 1, 1, color);
279 self.pixel_rect(arrow_left, bot_px + 1, 1, 1, color);
280 }
281 }
282 }
283
284 pub fn vline(&mut self, tx: u32, ty: u32, length_tiles: u32, color: impl Into<Rgba>) {
287 let px = tx * 8 + 6;
288 let py = ty * 8;
289 self.pixel_rect(px, py, 1, length_tiles * 8, color);
290 }
291
292 pub fn hline(&mut self, tx: u32, ty: u32, length_tiles: u32, color: impl Into<Rgba>) {
295 let px = tx * 8;
296 let py = ty * 8 + 6;
297 self.pixel_rect(px, py, length_tiles * 8, 1, color);
298 }
299
300 pub fn label_value_grid(
304 &mut self,
305 tx: u32,
306 ty: u32,
307 rows: &[LabelValue<'_>],
308 value_indent_x: u32,
309 row_step: u32,
310 label_color: impl Into<Rgba>,
311 value_color: impl Into<Rgba>,
312 ) {
313 let label_color = label_color.into();
314 let value_color = value_color.into();
315 for (i, lv) in rows.iter().enumerate() {
316 let row = ty + (i as u32) * row_step;
317 self.label(tx, row, lv.label, label_color);
318 self.label(tx + value_indent_x, row + 1, &lv.value, value_color);
319 }
320 }
321}
322
323#[derive(Debug, Clone)]
325pub struct LabelValue<'a> {
326 pub label: &'a str,
327 pub value: String,
328}
329
330impl<'a> LabelValue<'a> {
331 pub fn new(label: &'a str, value: impl Into<String>) -> Self {
332 Self {
333 label,
334 value: value.into(),
335 }
336 }
337}