1use crate::layout_engine::elements::group::{Group, GroupLayout};
2use crate::layout_engine::registry::ElementRegistry;
3use crate::layout_engine::types::{
4 Coord, DataContext, ElementParams, LayoutElement, RenderContext, RenderError, ScreenLayout,
5};
6use dotzuki_engine::render::painter::Painter;
7use dotzuki_engine::render::{Rgba, TileRect};
8
9pub fn render_layout(
12 layout: &ScreenLayout,
13 ctx: &DataContext,
14 render_ctx: &RenderContext,
15 registry: &ElementRegistry,
16 painter: &mut dyn Painter,
17) -> Result<(), RenderError> {
18 let bg = parse_bg_color(&layout.theme.bg_color);
19 painter.clear(bg);
20 render_elements(layout, ctx, render_ctx, registry, painter)
21}
22
23pub fn render_layout_no_clear(
26 layout: &ScreenLayout,
27 ctx: &DataContext,
28 render_ctx: &RenderContext,
29 registry: &ElementRegistry,
30 painter: &mut dyn Painter,
31) -> Result<(), RenderError> {
32 render_elements(layout, ctx, render_ctx, registry, painter)
33}
34
35fn render_elements(
36 layout: &ScreenLayout,
37 ctx: &DataContext,
38 render_ctx: &RenderContext,
39 registry: &ElementRegistry,
40 painter: &mut dyn Painter,
41) -> Result<(), RenderError> {
42 let mut elements: Vec<&LayoutElement> = layout.elements.iter().collect();
43 elements.sort_by_key(|e| e.z_index);
44
45 for element in &elements {
46 if !element.visible.eval(ctx) {
47 continue;
48 }
49 dispatch_element(element, ctx, render_ctx, registry, painter)?;
50 }
51
52 Ok(())
53}
54
55fn dispatch_element(
58 element: &LayoutElement,
59 ctx: &DataContext,
60 render_ctx: &RenderContext,
61 registry: &ElementRegistry,
62 painter: &mut dyn Painter,
63) -> Result<(), RenderError> {
64 match element.element_type.as_str() {
65 "border" => render_border_element(element, ctx, render_ctx, registry, painter),
66 "text" => render_text_element(element, ctx, render_ctx, painter),
67 "tile" => render_tile_element(element, ctx, render_ctx, painter),
68 "divider" => render_divider_element(element, ctx, render_ctx, painter),
69 "image" => render_image_element(element, ctx, render_ctx, painter),
70 "list" => render_list_element(element, ctx, render_ctx, painter),
71 "flex_list" => render_flex_list_element(element, ctx, render_ctx, painter),
72 "cursor" => render_cursor_element(element, ctx, render_ctx, painter),
73 "bracket" => render_bracket_element(element, ctx, painter),
74 "pixel_rect" => render_pixel_rect_element(element, painter),
75 "group" => render_group_element(element, ctx, render_ctx, registry, painter),
76 t if t.starts_with("custom:") => {
77 if let Some(custom) = registry.get(t) {
78 custom.render(element, ctx, render_ctx, painter)
79 } else {
80 log::warn!("Custom element type '{}' not registered — skipping", t);
81 Ok(())
82 }
83 }
84 _ => {
85 log::warn!("Unknown element type '{}' — skipping", element.element_type);
86 Ok(())
87 }
88 }
89}
90
91fn render_border_element(
94 element: &LayoutElement,
95 ctx: &DataContext,
96 render_ctx: &RenderContext,
97 registry: &ElementRegistry,
98 painter: &mut dyn Painter,
99) -> Result<(), RenderError> {
100 let ElementParams::Border(ref params) = element.params else {
101 return Ok(());
102 };
103
104 let rect = TileRect::new(
105 element.rect.tx.resolve(ctx),
106 element.rect.ty.resolve(ctx),
107 element.rect.tw.unwrap_or(1),
108 element.rect.th.unwrap_or(1),
109 );
110
111 painter.draw_text_box(rect, Rgba::INK_BLACK);
115 for child in ¶ms.children {
121 if !child.visible.eval(ctx) {
122 continue;
123 }
124 dispatch_element(child, ctx, render_ctx, registry, painter)?;
125 }
126 Ok(())
127}
128
129fn render_text_element(
130 element: &LayoutElement,
131 ctx: &DataContext,
132 render_ctx: &RenderContext,
133 painter: &mut dyn Painter,
134) -> Result<(), RenderError> {
135 let ElementParams::Text(ref params) = element.params else {
136 return Ok(());
137 };
138 crate::layout_engine::elements::text::render_text(
139 element, params, ctx, render_ctx, painter,
140 )
141}
142
143fn render_tile_element(
144 element: &LayoutElement,
145 ctx: &DataContext,
146 render_ctx: &RenderContext,
147 painter: &mut dyn Painter,
148) -> Result<(), RenderError> {
149 let ElementParams::Tile(ref params) = element.params else {
150 return Ok(());
151 };
152 crate::layout_engine::elements::tile::render_tile(
153 element, params, ctx, render_ctx, painter,
154 )
155}
156
157fn render_divider_element(
158 element: &LayoutElement,
159 ctx: &DataContext,
160 render_ctx: &RenderContext,
161 painter: &mut dyn Painter,
162) -> Result<(), RenderError> {
163 let ElementParams::Divider(ref params) = element.params else {
164 return Ok(());
165 };
166 crate::layout_engine::elements::divider::render_divider(
167 element, params, ctx, render_ctx, painter,
168 )
169}
170
171fn render_image_element(
172 element: &LayoutElement,
173 ctx: &DataContext,
174 render_ctx: &RenderContext,
175 painter: &mut dyn Painter,
176) -> Result<(), RenderError> {
177 let ElementParams::Image(ref params) = element.params else {
178 return Ok(());
179 };
180 crate::layout_engine::elements::image::render_image(
181 element, params, ctx, render_ctx, painter,
182 )
183}
184
185fn render_list_element(
186 element: &LayoutElement,
187 ctx: &DataContext,
188 render_ctx: &RenderContext,
189 painter: &mut dyn Painter,
190) -> Result<(), RenderError> {
191 let ElementParams::List(ref params) = element.params else {
192 return Ok(());
193 };
194 crate::layout_engine::elements::list::render_list(
195 params,
196 element.rect.tx.resolve(ctx),
197 element.rect.ty.resolve(ctx),
198 ctx,
199 render_ctx.theme,
200 painter,
201 );
202 Ok(())
203}
204
205fn render_flex_list_element(
206 element: &LayoutElement,
207 ctx: &DataContext,
208 render_ctx: &RenderContext,
209 painter: &mut dyn Painter,
210) -> Result<(), RenderError> {
211 let ElementParams::FlexList(ref params) = element.params else {
212 return Ok(());
213 };
214 crate::layout_engine::elements::flex_list::render_flex_list(
215 params,
216 element.rect.tx.resolve(ctx),
217 element.rect.ty.resolve(ctx),
218 ctx,
219 render_ctx.theme,
220 painter,
221 );
222 Ok(())
223}
224
225fn render_cursor_element(
226 element: &LayoutElement,
227 ctx: &DataContext,
228 render_ctx: &RenderContext,
229 painter: &mut dyn Painter,
230) -> Result<(), RenderError> {
231 let ElementParams::Cursor(ref params) = element.params else {
232 return Ok(());
233 };
234 crate::layout_engine::elements::cursor::render_cursor(
235 element,
236 params,
237 ctx,
238 render_ctx.theme,
239 painter,
240 );
241 Ok(())
242}
243
244fn render_bracket_element(
245 element: &LayoutElement,
246 ctx: &DataContext,
247 painter: &mut dyn Painter,
248) -> Result<(), RenderError> {
249 let ElementParams::Bracket(ref params) = element.params else {
250 return Ok(());
251 };
252 crate::layout_engine::elements::primitives::render_bracket(element, params, ctx, painter);
253 Ok(())
254}
255
256fn render_pixel_rect_element(
257 element: &LayoutElement,
258 painter: &mut dyn Painter,
259) -> Result<(), RenderError> {
260 let ElementParams::PixelRect(ref params) = element.params else {
261 return Ok(());
262 };
263 crate::layout_engine::elements::primitives::render_pixel_rect(params, painter);
264 Ok(())
265}
266
267fn render_group_element(
268 element: &LayoutElement,
269 ctx: &DataContext,
270 render_ctx: &RenderContext,
271 registry: &ElementRegistry,
272 painter: &mut dyn Painter,
273) -> Result<(), RenderError> {
274 let ElementParams::Group(ref params) = element.params else {
275 return Ok(());
276 };
277
278 let rect = TileRect::new(
279 element.rect.tx.resolve(ctx),
280 element.rect.ty.resolve(ctx),
281 element.rect.tw.unwrap_or(20),
282 element.rect.th.unwrap_or(18),
283 );
284
285 let layout = GroupLayout::from_config(¶ms.layout);
286 let group = Group::new(rect)
287 .with_layout(layout)
288 .with_clip(params.clip);
289
290 let resolved = group.resolve_children(¶ms.children, ctx);
292
293 let mut indices: Vec<usize> = (0..resolved.len()).collect();
295 indices.sort_by_key(|&i| resolved[i].z_index);
296
297 for &child_idx in &indices {
299 let child_rect = &resolved[child_idx];
300 if !child_rect.visible {
301 continue;
302 }
303 let child = ¶ms.children[child_idx];
304
305 let mut resolved_child = child.clone();
307 resolved_child.rect.tx = Coord::Literal(child_rect.rect.tx);
308 resolved_child.rect.ty = Coord::Literal(child_rect.rect.ty);
309
310 dispatch_element(&resolved_child, ctx, render_ctx, registry, painter)?;
311 }
312
313 Ok(())
314}
315
316fn parse_bg_color(hex: &str) -> Rgba {
319 match hex.to_lowercase().as_str() {
320 "#000000" | "black" => Rgba::INK_BLACK,
323 "#606060" | "#808080" | "darkgray" | "dark_gray" => Rgba::INK_DARK_GRAY,
324 "#a0a0a0" | "#c0c0c0" | "lightgray" | "light_gray" => Rgba::INK_LIGHT_GRAY,
325 "#ffffff" | "#e0e0e0" | "white" => Rgba::INK_WHITE,
326 other if other.starts_with('#') => {
329 crate::layout_engine::elements::text::parse_color(other)
330 }
331 other => {
333 log::warn!("Unknown bg_color '{}' — falling back to White", other);
334 Rgba::INK_WHITE
335 }
336 }
337}
338
339#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::layout_engine::types::Theme;
345 use dotzuki_engine::render::Rgba as EngineRgba;
346 use dotzuki_engine::render::TilePos;
347 use std::collections::HashMap;
348
349 #[derive(Debug, Clone)]
352 enum DrawOp {
353 Clear(EngineRgba),
354 TextBox(TileRect, EngineRgba),
355 Text(TilePos, String, EngineRgba),
356 Glyph(TilePos, char, EngineRgba),
357 PixelRect(u32, u32, u32, u32, EngineRgba),
358 GbTile(TilePos, u8, String, EngineRgba),
359 }
360
361 #[derive(Debug, Default)]
362 struct RecordingPainter {
363 ops: Vec<DrawOp>,
364 }
365
366 impl RecordingPainter {
367 fn new() -> Self {
368 Self::default()
369 }
370
371 fn has_op<F: Fn(&DrawOp) -> bool>(&self, pred: F) -> bool {
372 self.ops.iter().any(pred)
373 }
374
375 fn gb_tiles(&self) -> Vec<(TilePos, u8)> {
376 self.ops
377 .iter()
378 .filter_map(|op| match op {
379 DrawOp::GbTile(pos, id, _, _) => Some((*pos, *id)),
380 _ => None,
381 })
382 .collect()
383 }
384
385 fn glyphs(&self) -> Vec<(TilePos, char)> {
386 self.ops
387 .iter()
388 .filter_map(|op| match op {
389 DrawOp::Glyph(pos, ch, _) => Some((*pos, *ch)),
390 _ => None,
391 })
392 .collect()
393 }
394 }
395
396 impl Painter for RecordingPainter {
397 fn clear(&mut self, color: EngineRgba) {
398 self.ops.push(DrawOp::Clear(color));
399 }
400
401 fn draw_text_box(&mut self, rect: TileRect, color: EngineRgba) {
402 self.ops.push(DrawOp::TextBox(rect, color));
403 }
404
405 fn draw_text(&mut self, pos: TilePos, text: &str, color: EngineRgba) {
406 self.ops.push(DrawOp::Text(pos, text.to_string(), color));
407 }
408
409 fn draw_glyph(&mut self, pos: TilePos, glyph: char, color: EngineRgba) {
410 self.ops.push(DrawOp::Glyph(pos, glyph, color));
411 }
412
413 fn draw_pixel_rect(
414 &mut self,
415 px: u32,
416 py: u32,
417 pw: u32,
418 ph: u32,
419 color: EngineRgba,
420 ) {
421 self.ops.push(DrawOp::PixelRect(px, py, pw, ph, color));
422 }
423
424 fn draw_gb_tile(
425 &mut self,
426 pos: TilePos,
427 tile_id: u8,
428 fallback: &str,
429 color: EngineRgba,
430 ) {
431 self.ops
432 .push(DrawOp::GbTile(pos, tile_id, fallback.to_string(), color));
433 }
434 }
435
436 fn make_theme() -> Theme {
439 Theme {
440 bg_color: "#FFFFFF".to_string(),
441 default_font: "default".to_string(),
442 ..Default::default()
443 }
444 }
445
446 fn make_fonts() -> HashMap<String, ()> {
447 HashMap::new()
448 }
449
450 fn make_tilesets() -> HashMap<String, ()> {
451 HashMap::new()
452 }
453
454 fn make_render_ctx<'a>(
455 theme: &'a Theme,
456 fonts: &'a HashMap<String, ()>,
457 tilesets: &'a HashMap<String, ()>,
458 ) -> RenderContext<'a> {
459 RenderContext {
460 screen: "test",
461 theme,
462 fonts,
463 tilesets,
464 images: crate::layout_engine::types::empty_image_registry(),
465 }
466 }
467
468 #[test]
471 fn test_dispatch_border() {
472 let json = r##"{
473 "schema_version": 1,
474 "screen": "test",
475 "elements": [
476 { "type": "border", "rect": { "tx": 1, "ty": 1, "tw": 10, "th": 5 } }
477 ]
478 }"##;
479 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
480 let ctx = DataContext::new();
481 let theme = make_theme();
482 let fonts = make_fonts();
483 let tilesets = make_tilesets();
484 let rc = make_render_ctx(&theme, &fonts, &tilesets);
485 let registry = ElementRegistry::new();
486 let mut painter = RecordingPainter::new();
487
488 render_layout(&layout, &ctx, &rc, ®istry, &mut painter).unwrap();
489
490 assert!(
492 painter.has_op(|op| matches!(op, DrawOp::TextBox(..))),
493 "border should draw a text box"
494 );
495 }
496
497 #[test]
500 fn test_dispatch_text() {
501 let json = r##"{
502 "schema_version": 1,
503 "screen": "test",
504 "elements": [
505 { "type": "text", "rect": { "tx": 2, "ty": 3, "tw": 10, "th": 2 }, "value": "Hello" }
506 ]
507 }"##;
508 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
509 let ctx = DataContext::new();
510 let theme = make_theme();
511 let fonts = make_fonts();
512 let tilesets = make_tilesets();
513 let rc = make_render_ctx(&theme, &fonts, &tilesets);
514 let registry = ElementRegistry::new();
515 let mut painter = RecordingPainter::new();
516
517 render_layout(&layout, &ctx, &rc, ®istry, &mut painter).unwrap();
518
519 let glyphs = painter.glyphs();
520 assert!(!glyphs.is_empty(), "text should draw glyphs");
521 assert_eq!(glyphs[0].0, TilePos::new(2, 3));
522 assert_eq!(glyphs[0].1, 'H');
523 }
524
525 #[test]
528 fn test_dispatch_tile() {
529 let json = r##"{
530 "schema_version": 1,
531 "screen": "test",
532 "elements": [
533 { "type": "tile", "rect": { "tx": 5, "ty": 5, "tw": 1, "th": 1 }, "tile_id": 42 }
534 ]
535 }"##;
536 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
537 let ctx = DataContext::new();
538 let theme = make_theme();
539 let fonts = make_fonts();
540 let tilesets = make_tilesets();
541 let rc = make_render_ctx(&theme, &fonts, &tilesets);
542 let registry = ElementRegistry::new();
543 let mut painter = RecordingPainter::new();
544
545 render_layout(&layout, &ctx, &rc, ®istry, &mut painter).unwrap();
546
547 let tiles = painter.gb_tiles();
548 assert_eq!(tiles.len(), 1);
549 assert_eq!(tiles[0], (TilePos::new(5, 5), 42));
550 }
551
552 #[test]
555 fn test_dispatch_group() {
556 let json = r##"{
557 "schema_version": 1,
558 "screen": "test",
559 "elements": [
560 {
561 "type": "group",
562 "rect": { "tx": 2, "ty": 2, "tw": 16, "th": 14 },
563 "layout": { "gap": 0 },
564 "clip": false,
565 "children": [
566 { "type": "text", "rect": { "tx": 0, "ty": 0, "tw": 5, "th": 1 }, "value": "A" },
567 { "type": "text", "rect": { "tx": 1, "ty": 1, "tw": 5, "th": 1 }, "value": "B" }
568 ]
569 }
570 ]
571 }"##;
572 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
573 let ctx = DataContext::new();
574 let theme = make_theme();
575 let fonts = make_fonts();
576 let tilesets = make_tilesets();
577 let rc = make_render_ctx(&theme, &fonts, &tilesets);
578 let registry = ElementRegistry::new();
579 let mut painter = RecordingPainter::new();
580
581 render_layout(&layout, &ctx, &rc, ®istry, &mut painter).unwrap();
582
583 let glyphs = painter.glyphs();
584
585 let a_glyph = glyphs.iter().find(|(_, ch)| *ch == 'A');
587 assert!(a_glyph.is_some(), "'A' should be rendered");
588 assert_eq!(a_glyph.unwrap().0, TilePos::new(2, 2));
589
590 let b_glyph = glyphs.iter().find(|(_, ch)| *ch == 'B');
592 assert!(b_glyph.is_some(), "'B' should be rendered");
593 assert_eq!(b_glyph.unwrap().0, TilePos::new(3, 3));
594 }
595
596 #[test]
599 fn test_dispatch_invisible_element() {
600 let json = r##"{
601 "schema_version": 1,
602 "screen": "test",
603 "elements": [
604 { "type": "text", "rect": { "tx": 0, "ty": 0 }, "value": "hidden", "visible": false },
605 { "type": "text", "rect": { "tx": 0, "ty": 1 }, "value": "visible" }
606 ]
607 }"##;
608 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
609 let ctx = DataContext::new();
610 let theme = make_theme();
611 let fonts = make_fonts();
612 let tilesets = make_tilesets();
613 let rc = make_render_ctx(&theme, &fonts, &tilesets);
614 let registry = ElementRegistry::new();
615 let mut painter = RecordingPainter::new();
616
617 render_layout(&layout, &ctx, &rc, ®istry, &mut painter).unwrap();
618
619 let glyphs = painter.glyphs();
620 let hidden = glyphs.iter().any(|(_, ch)| *ch == 'h');
621 assert!(!hidden, "invisible element should not render");
622 let visible = glyphs.iter().any(|(_, ch)| *ch == 'v');
623 assert!(visible, "visible element should render");
624 }
625
626 #[test]
629 fn test_dispatch_unknown_type() {
630 let json = r##"{
631 "schema_version": 1,
632 "screen": "test",
633 "elements": [
634 { "type": "fantasy_element", "rect": { "tx": 0, "ty": 0 } }
635 ]
636 }"##;
637 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
638 let ctx = DataContext::new();
639 let theme = make_theme();
640 let fonts = make_fonts();
641 let tilesets = make_tilesets();
642 let rc = make_render_ctx(&theme, &fonts, &tilesets);
643 let registry = ElementRegistry::new();
644 let mut painter = RecordingPainter::new();
645
646 let result = render_layout(&layout, &ctx, &rc, ®istry, &mut painter);
647 assert!(result.is_ok(), "unknown type should not cause error");
648
649 let non_clear_ops: Vec<_> = painter
651 .ops
652 .iter()
653 .filter(|op| !matches!(op, DrawOp::Clear(_)))
654 .collect();
655 assert!(non_clear_ops.is_empty(), "unknown type should skip rendering");
656 }
657
658 #[test]
661 fn test_dispatch_custom() {
662 use crate::layout_engine::registry::CustomElement;
663
664 #[derive(Debug)]
665 struct TestCustom;
666
667 impl CustomElement for TestCustom {
668 fn element_type(&self) -> &'static str {
669 "custom:test"
670 }
671
672 fn render(
673 &self,
674 _element: &LayoutElement,
675 _ctx: &DataContext,
676 _render_ctx: &RenderContext,
677 painter: &mut dyn Painter,
678 ) -> Result<(), RenderError> {
679 painter.draw_glyph(TilePos::new(99, 99), 'X', EngineRgba::INK_BLACK);
680 Ok(())
681 }
682 }
683
684 let json = r##"{
685 "schema_version": 1,
686 "screen": "test",
687 "elements": [
688 { "type": "custom:test", "rect": { "tx": 0, "ty": 0 } }
689 ]
690 }"##;
691 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
692 let ctx = DataContext::new();
693 let theme = make_theme();
694 let fonts = make_fonts();
695 let tilesets = make_tilesets();
696 let rc = make_render_ctx(&theme, &fonts, &tilesets);
697 let mut registry = ElementRegistry::new();
698 registry.register(Box::new(TestCustom));
699 let mut painter = RecordingPainter::new();
700
701 render_layout(&layout, &ctx, &rc, ®istry, &mut painter).unwrap();
702
703 let glyphs = painter.glyphs();
704 assert_eq!(glyphs.len(), 1);
705 assert_eq!(glyphs[0], (TilePos::new(99, 99), 'X'));
706 }
707
708 #[test]
711 fn test_dispatch_z_order() {
712 let json = r##"{
715 "schema_version": 1,
716 "screen": "test",
717 "elements": [
718 { "type": "tile", "rect": { "tx": 0, "ty": 0, "tw": 1, "th": 1 }, "tile_id": 99, "z_index": 0 },
719 { "type": "tile", "rect": { "tx": 0, "ty": 0, "tw": 1, "th": 1 }, "tile_id": 1, "z_index": 10 },
720 { "type": "tile", "rect": { "tx": 0, "ty": 0, "tw": 1, "th": 1 }, "tile_id": 50, "z_index": 5 }
721 ]
722 }"##;
723 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
724 let ctx = DataContext::new();
725 let theme = make_theme();
726 let fonts = make_fonts();
727 let tilesets = make_tilesets();
728 let rc = make_render_ctx(&theme, &fonts, &tilesets);
729 let registry = ElementRegistry::new();
730 let mut painter = RecordingPainter::new();
731
732 render_layout(&layout, &ctx, &rc, ®istry, &mut painter).unwrap();
733
734 let tiles = painter.gb_tiles();
735 assert_eq!(tiles.len(), 3);
737 assert_eq!(tiles[0].1, 99);
738 assert_eq!(tiles[1].1, 50);
739 assert_eq!(tiles[2].1, 1);
740 }
741
742 #[test]
745 fn test_render_title_screen() {
746 let json = r##"{
747 "schema_version": 1,
748 "screen": "title_screen",
749 "theme": { "bg_color": "#FFFFFF", "default_font": "default" },
750 "elements": [
751 {
752 "type": "border",
753 "rect": { "tx": 1, "ty": 5, "tw": 18, "th": 7 },
754 "style": "Single"
755 },
756 {
757 "type": "text",
758 "rect": { "tx": 5, "ty": 7, "tw": 10, "th": 1 },
759 "value": "MONSTER RED",
760 "color": "black",
761 "align": "Center"
762 },
763 {
764 "type": "text",
765 "rect": { "tx": 6, "ty": 9, "tw": 8, "th": 1 },
766 "value": "Press START",
767 "color": "darkgray",
768 "align": "Center"
769 }
770 ]
771 }"##;
772 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
773 let ctx = DataContext::new();
774 let theme = layout.theme.clone();
775 let fonts = make_fonts();
776 let tilesets = make_tilesets();
777 let rc = make_render_ctx(&theme, &fonts, &tilesets);
778 let registry = ElementRegistry::new();
779 let mut painter = RecordingPainter::new();
780
781 render_layout(&layout, &ctx, &rc, ®istry, &mut painter).unwrap();
782
783 assert!(painter.has_op(|op| matches!(op, DrawOp::Clear(Rgba::INK_WHITE))));
785
786 assert!(painter.has_op(|op| matches!(op, DrawOp::TextBox(..))));
788
789 let glyphs = painter.glyphs();
791 assert!(!glyphs.is_empty(), "title screen should have text glyphs");
792 }
793
794 #[test]
797 fn test_dispatch_divider() {
798 let json = r##"{
799 "schema_version": 1,
800 "screen": "test",
801 "elements": [
802 {
803 "type": "divider",
804 "rect": { "tx": 0, "ty": 0, "tw": 5, "th": 1 },
805 "tiles": [120, 121],
806 "repeat": 3
807 }
808 ]
809 }"##;
810 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
811 let ctx = DataContext::new();
812 let theme = make_theme();
813 let fonts = make_fonts();
814 let tilesets = make_tilesets();
815 let rc = make_render_ctx(&theme, &fonts, &tilesets);
816 let registry = ElementRegistry::new();
817 let mut painter = RecordingPainter::new();
818
819 render_layout(&layout, &ctx, &rc, ®istry, &mut painter).unwrap();
820
821 let tiles = painter.gb_tiles();
822 assert_eq!(tiles.len(), 5);
823 assert_eq!(tiles[0].1, 120);
824 assert_eq!(tiles[1].1, 121);
825 assert_eq!(tiles[2].1, 121);
827 assert_eq!(tiles[3].1, 121);
828 assert_eq!(tiles[4].1, 121);
829 }
830
831 #[test]
834 fn test_dispatch_image() {
835 let json = r##"{
836 "schema_version": 1,
837 "screen": "test",
838 "elements": [
839 {
840 "type": "image",
841 "rect": { "tx": 1, "ty": 1, "tw": 3, "th": 3 },
842 "source": "sparkit"
843 }
844 ]
845 }"##;
846 let layout: ScreenLayout = serde_json::from_str(json).unwrap();
847 let ctx = DataContext::new();
848 let theme = make_theme();
849 let fonts = make_fonts();
850 let tilesets = make_tilesets();
851 let rc = make_render_ctx(&theme, &fonts, &tilesets);
852 let registry = ElementRegistry::new();
853 let mut painter = RecordingPainter::new();
854
855 let result = render_layout(&layout, &ctx, &rc, ®istry, &mut painter);
856 assert!(result.is_ok(), "image dispatch should not error");
857
858 assert!(
860 painter.has_op(|op| matches!(op, DrawOp::PixelRect(..))),
861 "image should draw placeholder"
862 );
863 }
864
865 #[test]
868 fn test_parse_bg_color_white() {
869 assert_eq!(parse_bg_color("#FFFFFF"), Rgba::INK_WHITE);
870 assert_eq!(parse_bg_color("white"), Rgba::INK_WHITE);
871 }
872
873 #[test]
874 fn test_parse_bg_color_black() {
875 assert_eq!(parse_bg_color("#000000"), Rgba::INK_BLACK);
876 assert_eq!(parse_bg_color("black"), Rgba::INK_BLACK);
877 }
878
879 #[test]
880 fn test_parse_bg_color_darkgray() {
881 assert_eq!(parse_bg_color("#808080"), Rgba::INK_DARK_GRAY);
882 assert_eq!(parse_bg_color("darkgray"), Rgba::INK_DARK_GRAY);
883 }
884
885 #[test]
886 fn test_parse_bg_color_unknown() {
887 assert_eq!(parse_bg_color("pink"), Rgba::INK_WHITE);
888 }
889}