1use crate::layout_engine::types::{
15 DataContext, ImageParams, LayoutElement, RenderContext, RenderError,
16};
17use dotzuki_engine::render::painter::Painter;
18use dotzuki_engine::render::Rgba;
19use dotzuki_engine::render::TilePos;
20
21pub fn render_image(
37 element: &LayoutElement,
38 params: &ImageParams,
39 ctx: &DataContext,
40 render_ctx: &RenderContext,
41 painter: &mut dyn Painter,
42) -> Result<(), RenderError> {
43 let rect = &element.rect;
44 let tw = rect.tw.unwrap_or(1);
45 let th = rect.th.unwrap_or(1);
46 let source_key = ctx.resolve(¶ms.source);
47 let tx = rect.tx.resolve(ctx);
48 let ty = rect.ty.resolve(ctx);
49
50 if source_key.is_empty() || source_key == "?" {
51 draw_placeholder(tx, ty, tw, th, painter);
52 return Ok(());
53 }
54
55 match render_ctx.images.get(&source_key) {
58 Some(img) if !img.is_empty() => {
59 let (box_px, box_py) = TilePos::new(tx, ty).to_pixels();
61 let box_w = tw * 8;
62 let box_h = th * 8;
63 let scale =
64 (box_w as f32 / img.width as f32).min(box_h as f32 / img.height as f32);
65 let dst_w = ((img.width as f32 * scale).round() as u32).max(1);
66 let dst_h = ((img.height as f32 * scale).round() as u32).max(1);
67 let ox = box_px + (box_w.saturating_sub(dst_w)) / 2;
68 let oy = box_py + (box_h.saturating_sub(dst_h)) / 2;
69 painter.draw_rgba(
70 ox,
71 oy,
72 dst_w,
73 dst_h,
74 &img.pixels,
75 img.width,
76 img.height,
77 params.flip_x,
78 params.flip_y,
79 );
80 }
81 _ => draw_placeholder(tx, ty, tw, th, painter),
83 }
84 Ok(())
85}
86
87fn draw_placeholder(tx: u32, ty: u32, tw: u32, th: u32, painter: &mut dyn Painter) {
88 for row in 0..th {
89 for col in 0..tw {
90 let color = if (col + row) % 2 == 0 {
91 Rgba::INK_LIGHT_GRAY
92 } else {
93 Rgba::INK_DARK_GRAY
94 };
95 let (px, py) = TilePos::new(tx + col, ty + row).to_pixels();
96 painter.draw_pixel_rect(px, py, 8, 8, color);
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::layout_engine::types::{Coord, ElementParams, ElementRect};
105 use crate::layout_engine::types::DataValue;
106 use dotzuki_engine::render::Rgba as EngineRgba;
107
108 #[derive(Debug, Default)]
109 struct RecordingPainter {
110 pixel_rects: Vec<(u32, u32, u32, u32, EngineRgba)>,
111 tile_calls: Vec<(u32, u32, u8)>,
112 }
113
114 impl Painter for RecordingPainter {
115 fn clear(&mut self, _color: EngineRgba) {}
116
117 fn draw_text_box(
118 &mut self,
119 _rect: dotzuki_engine::render::TileRect,
120 _color: EngineRgba,
121 ) {
122 }
123
124 fn draw_text(&mut self, _pos: TilePos, _text: &str, _color: EngineRgba) {}
125
126 fn draw_glyph(&mut self, _pos: TilePos, _glyph: char, _color: EngineRgba) {}
127
128 fn draw_pixel_rect(
129 &mut self,
130 px: u32,
131 py: u32,
132 pw: u32,
133 ph: u32,
134 color: EngineRgba,
135 ) {
136 self.pixel_rects.push((px, py, pw, ph, color));
137 }
138
139 fn draw_gb_tile(
140 &mut self,
141 pos: TilePos,
142 tile_id: u8,
143 _fallback: &str,
144 _color: EngineRgba,
145 ) {
146 self.tile_calls.push((pos.tx, pos.ty, tile_id));
147 }
148 }
149
150 fn make_element(rect: ElementRect, params: ImageParams) -> LayoutElement {
151 LayoutElement {
152 id: String::new(),
153 element_type: "image".to_string(),
154 rect,
155 visible: crate::layout_engine::types::Visibility::Static(true),
156 z_index: 0,
157 params: ElementParams::Image(params),
158 }
159 }
160
161 fn make_render_ctx() -> (
162 RenderContext<'static>,
163 std::collections::HashMap<String, ()>,
164 std::collections::HashMap<String, ()>,
165 ) {
166 let fonts: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
167 let tilesets: std::collections::HashMap<String, ()> = std::collections::HashMap::new();
168 let theme = crate::layout_engine::types::Theme::default();
169 let fonts_ref: &'static std::collections::HashMap<String, ()> =
171 Box::leak(Box::new(fonts.clone()));
172 let tilesets_ref: &'static std::collections::HashMap<String, ()> =
173 Box::leak(Box::new(tilesets.clone()));
174 let theme_ref: &'static crate::layout_engine::types::Theme =
175 Box::leak(Box::new(theme));
176 (
177 RenderContext {
178 screen: "test",
179 theme: theme_ref,
180 fonts: fonts_ref,
181 tilesets: tilesets_ref,
182 images: crate::layout_engine::types::empty_image_registry(),
183 },
184 fonts,
185 tilesets,
186 )
187 }
188
189 #[test]
190 fn resolves_source_from_datacontext() {
191 let rect = ElementRect {
192 tx: Coord::Literal(0),
193 ty: Coord::Literal(0),
194 tw: Some(1),
195 th: Some(1),
196 };
197 let params = ImageParams {
198 source: "{sprite}".to_string(),
199 flip_x: false,
200 flip_y: false,
201 palette: None,
202 };
203 let elem = make_element(rect, params);
204 let mut ctx = DataContext::new();
205 ctx.set("sprite", "leafkit_front");
206 let (rc, _fonts, _tilesets) = make_render_ctx();
207
208 let mut painter = RecordingPainter::default();
209 render_image(
210 &elem,
211 if let ElementParams::Image(ref p) = elem.params {
212 p
213 } else {
214 unreachable!()
215 },
216 &ctx,
217 &rc,
218 &mut painter,
219 )
220 .unwrap();
221
222 assert!(!painter.pixel_rects.is_empty());
223 }
224
225 #[test]
226 fn unknown_source_draws_placeholder() {
227 let rect = ElementRect {
228 tx: Coord::Literal(0),
229 ty: Coord::Literal(0),
230 tw: Some(1),
231 th: Some(1),
232 };
233 let params = ImageParams {
234 source: "{missing}".to_string(),
235 flip_x: false,
236 flip_y: false,
237 palette: None,
238 };
239 let elem = make_element(rect, params);
240 let ctx = DataContext::new();
241 let (rc, _fonts, _tilesets) = make_render_ctx();
242
243 let mut painter = RecordingPainter::default();
244 render_image(
245 &elem,
246 if let ElementParams::Image(ref p) = elem.params {
247 p
248 } else {
249 unreachable!()
250 },
251 &ctx,
252 &rc,
253 &mut painter,
254 )
255 .unwrap();
256
257 assert!(!painter.pixel_rects.is_empty());
258 }
259
260 #[test]
261 fn placeholder_spans_full_rect() {
262 let rect = ElementRect {
263 tx: Coord::Literal(2),
264 ty: Coord::Literal(1),
265 tw: Some(3),
266 th: Some(2),
267 };
268 let params = ImageParams {
269 source: "{sprite}".to_string(),
270 flip_x: false,
271 flip_y: false,
272 palette: None,
273 };
274 let elem = make_element(rect, params);
275 let mut ctx = DataContext::new();
276 ctx.set("sprite", DataValue::Str("unknown".to_string()));
277 let (rc, _fonts, _tilesets) = make_render_ctx();
278
279 let mut painter = RecordingPainter::default();
280 render_image(
281 &elem,
282 if let ElementParams::Image(ref p) = elem.params {
283 p
284 } else {
285 unreachable!()
286 },
287 &ctx,
288 &rc,
289 &mut painter,
290 )
291 .unwrap();
292
293 assert_eq!(painter.pixel_rects.len(), 6);
294 for (px, py, pw, ph, _) in &painter.pixel_rects {
295 assert_eq!(*pw, 8);
296 assert_eq!(*ph, 8);
297 assert!(*px >= 16 && *px < 40);
298 assert!(*py >= 8 && *py < 24);
299 }
300 }
301
302 #[test]
303 fn placeholder_alternates_colors() {
304 let rect = ElementRect {
305 tx: Coord::Literal(0),
306 ty: Coord::Literal(0),
307 tw: Some(2),
308 th: Some(2),
309 };
310 let params = ImageParams {
311 source: "{sprite}".to_string(),
312 flip_x: false,
313 flip_y: false,
314 palette: None,
315 };
316 let elem = make_element(rect, params);
317 let mut ctx = DataContext::new();
318 ctx.set("sprite", "missing");
319 let (rc, _fonts, _tilesets) = make_render_ctx();
320
321 let mut painter = RecordingPainter::default();
322 render_image(
323 &elem,
324 if let ElementParams::Image(ref p) = elem.params {
325 p
326 } else {
327 unreachable!()
328 },
329 &ctx,
330 &rc,
331 &mut painter,
332 )
333 .unwrap();
334
335 assert_eq!(painter.pixel_rects.len(), 4);
336 let colors: Vec<EngineRgba> = painter.pixel_rects.iter().map(|r| r.4).collect();
337 assert_eq!(colors[0], EngineRgba::INK_LIGHT_GRAY);
338 assert_eq!(colors[1], EngineRgba::INK_DARK_GRAY);
339 assert_eq!(colors[2], EngineRgba::INK_DARK_GRAY);
340 assert_eq!(colors[3], EngineRgba::INK_LIGHT_GRAY);
341 }
342
343 #[test]
344 fn empty_resolved_source_draws_placeholder() {
345 let rect = ElementRect {
346 tx: Coord::Literal(0),
347 ty: Coord::Literal(0),
348 tw: Some(2),
349 th: Some(2),
350 };
351 let params = ImageParams {
352 source: "{empty}".to_string(),
353 flip_x: false,
354 flip_y: false,
355 palette: None,
356 };
357 let elem = make_element(rect, params);
358 let mut ctx = DataContext::new();
359 ctx.set("empty", "");
360 let (rc, _fonts, _tilesets) = make_render_ctx();
361
362 let mut painter = RecordingPainter::default();
363 render_image(
364 &elem,
365 if let ElementParams::Image(ref p) = elem.params {
366 p
367 } else {
368 unreachable!()
369 },
370 &ctx,
371 &rc,
372 &mut painter,
373 )
374 .unwrap();
375
376 assert!(!painter.pixel_rects.is_empty());
377 }
378
379 #[test]
380 fn flip_params_are_accepted() {
381 let rect = ElementRect {
382 tx: Coord::Literal(0),
383 ty: Coord::Literal(0),
384 tw: Some(1),
385 th: Some(1),
386 };
387 let params = ImageParams {
388 source: "{sprite}".to_string(),
389 flip_x: true,
390 flip_y: true,
391 palette: Some("pal1".to_string()),
392 };
393 let elem = make_element(rect, params);
394 let mut ctx = DataContext::new();
395 ctx.set("sprite", "leafkit");
396 let (rc, _fonts, _tilesets) = make_render_ctx();
397
398 let mut painter = RecordingPainter::default();
399 let result = render_image(
400 &elem,
401 if let ElementParams::Image(ref p) = elem.params {
402 p
403 } else {
404 unreachable!()
405 },
406 &ctx,
407 &rc,
408 &mut painter,
409 );
410 assert!(result.is_ok());
411 }
412
413 #[test]
414 fn default_tw_th_is_1() {
415 let rect = ElementRect {
416 tx: Coord::Literal(5),
417 ty: Coord::Literal(3),
418 tw: None,
419 th: None,
420 };
421 let params = ImageParams {
422 source: "{sprite}".to_string(),
423 flip_x: false,
424 flip_y: false,
425 palette: None,
426 };
427 let elem = make_element(rect, params);
428 let mut ctx = DataContext::new();
429 ctx.set("sprite", "leafkit");
430 let (rc, _fonts, _tilesets) = make_render_ctx();
431
432 let mut painter = RecordingPainter::default();
433 render_image(
434 &elem,
435 if let ElementParams::Image(ref p) = elem.params {
436 p
437 } else {
438 unreachable!()
439 },
440 &ctx,
441 &rc,
442 &mut painter,
443 )
444 .unwrap();
445
446 assert_eq!(painter.pixel_rects.len(), 1);
447 assert_eq!(painter.pixel_rects[0].0, 40);
448 assert_eq!(painter.pixel_rects[0].1, 24);
449 assert_eq!(painter.pixel_rects[0].2, 8);
450 assert_eq!(painter.pixel_rects[0].3, 8);
451 }
452
453 #[test]
454 fn registered_image_is_blitted_not_placeholder() {
455 use crate::layout_engine::types::{ImageData, ImageRegistry, Theme};
456 let rect = ElementRect {
457 tx: Coord::Literal(0),
458 ty: Coord::Literal(0),
459 tw: Some(1),
460 th: Some(1),
461 };
462 let params = ImageParams {
463 source: "{sprite}".to_string(),
464 flip_x: false,
465 flip_y: false,
466 palette: None,
467 };
468 let elem = make_element(rect, params);
469 let mut ctx = DataContext::new();
470 ctx.set("sprite", "hero");
471
472 let red = EngineRgba::new(255, 0, 0, 255);
474 let mut images = ImageRegistry::new();
475 images.insert("hero".to_string(), ImageData::new(1, 1, vec![red]));
476
477 let theme = Theme::default();
478 let fonts = std::collections::HashMap::<String, ()>::new();
479 let tilesets = std::collections::HashMap::<String, ()>::new();
480 let rc = RenderContext {
481 screen: "t",
482 theme: &theme,
483 fonts: &fonts,
484 tilesets: &tilesets,
485 images: &images,
486 };
487
488 let mut painter = RecordingPainter::default();
489 render_image(
490 &elem,
491 if let ElementParams::Image(ref p) = elem.params {
492 p
493 } else {
494 unreachable!()
495 },
496 &ctx,
497 &rc,
498 &mut painter,
499 )
500 .unwrap();
501
502 assert_eq!(painter.pixel_rects.len(), 64, "8×8 box filled");
505 assert!(
506 painter.pixel_rects.iter().all(|r| r.4 == red),
507 "every blitted pixel is the image colour, not a placeholder"
508 );
509 assert!(painter.pixel_rects.iter().all(|r| r.2 == 1 && r.3 == 1));
510 }
511
512 #[test]
513 fn unknown_key_with_nonempty_registry_still_placeholders() {
514 use crate::layout_engine::types::{ImageData, ImageRegistry, Theme};
515 let rect = ElementRect {
516 tx: Coord::Literal(0),
517 ty: Coord::Literal(0),
518 tw: Some(2),
519 th: Some(2),
520 };
521 let params = ImageParams {
522 source: "{sprite}".to_string(),
523 flip_x: false,
524 flip_y: false,
525 palette: None,
526 };
527 let elem = make_element(rect, params);
528 let mut ctx = DataContext::new();
529 ctx.set("sprite", "missing");
530
531 let mut images = ImageRegistry::new();
532 images.insert(
533 "other".to_string(),
534 ImageData::new(1, 1, vec![EngineRgba::new(0, 255, 0, 255)]),
535 );
536 let theme = Theme::default();
537 let fonts = std::collections::HashMap::<String, ()>::new();
538 let tilesets = std::collections::HashMap::<String, ()>::new();
539 let rc = RenderContext {
540 screen: "t",
541 theme: &theme,
542 fonts: &fonts,
543 tilesets: &tilesets,
544 images: &images,
545 };
546
547 let mut painter = RecordingPainter::default();
548 render_image(
549 &elem,
550 if let ElementParams::Image(ref p) = elem.params {
551 p
552 } else {
553 unreachable!()
554 },
555 &ctx,
556 &rc,
557 &mut painter,
558 )
559 .unwrap();
560
561 assert_eq!(painter.pixel_rects.len(), 4);
563 assert!(painter.pixel_rects.iter().all(|r| r.2 == 8 && r.3 == 8));
564 }
565}