1#[cfg(any(
7 feature = "canvas",
8 feature = "gif",
9 feature = "apng",
10 feature = "nes",
11 feature = "png",
12 feature = "jpeg",
13 feature = "qrcode",
14 feature = "lottie",
15 feature = "fontdue",
16 test,
17))]
18use alloc::vec::Vec;
19#[cfg(feature = "fontdue")]
20use alloc::{collections::BTreeMap, vec};
21use bitflags::bitflags;
22use heapless::Vec as HVec;
23use rlvgl_core::cmd::{BlendMode, Cmd, CommandList};
24#[cfg(feature = "fontdue")]
25use rlvgl_core::fontdue::{Metrics, line_metrics, rasterize_glyph};
26use rlvgl_core::renderer::Renderer;
27use rlvgl_core::widget::{Color, Rect as WidgetRect};
28
29#[cfg(feature = "fontdue")]
30const FONT_DATA: &[u8] = include_bytes!("../assets/fonts/DejaVuSans.ttf");
31
32#[cfg(feature = "fontdue")]
33fn round_to_i32(value: f32) -> i32 {
34 if value.is_nan() {
35 0
36 } else if value >= 0.0 {
37 (value + 0.5) as i32
38 } else {
39 (value - 0.5) as i32
40 }
41}
42
43#[cfg(feature = "fontdue")]
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
45struct GlyphKey {
47 font: *const u8,
49 size: u32,
51 ch: char,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum PixelFmt {
58 Argb8888,
60 Rgb565,
62 L8,
64 A8,
66 A4,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct Rect {
73 pub x: i32,
75 pub y: i32,
77 pub w: u32,
79 pub h: u32,
81}
82
83pub struct Surface<'a> {
85 pub buf: &'a mut [u8],
87 pub stride: usize,
89 pub format: PixelFmt,
91 pub width: u32,
93 pub height: u32,
95}
96
97impl<'a> Surface<'a> {
98 pub fn new(
100 buf: &'a mut [u8],
101 stride: usize,
102 format: PixelFmt,
103 width: u32,
104 height: u32,
105 ) -> Self {
106 Self {
107 buf,
108 stride,
109 format,
110 width,
111 height,
112 }
113 }
114}
115
116bitflags! {
117 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
119 pub struct BlitCaps: u32 {
120 const FILL = 0b0001;
122 const BLIT = 0b0010;
124 const BLEND = 0b0100;
126 const PFC = 0b1000;
128 }
129}
130
131pub trait Blitter {
133 fn caps(&self) -> BlitCaps;
135
136 fn fill(&mut self, dst: &mut Surface, area: Rect, color: u32);
138
139 fn blit(&mut self, src: &Surface, src_area: Rect, dst: &mut Surface, dst_pos: (i32, i32));
141
142 fn blend(&mut self, src: &Surface, src_area: Rect, dst: &mut Surface, dst_pos: (i32, i32));
144}
145
146pub struct BlitPlanner<const N: usize> {
157 rects: HVec<Rect, N>,
158 overflowed: bool,
159}
160
161impl<const N: usize> BlitPlanner<N> {
162 pub fn new() -> Self {
164 Self {
165 rects: HVec::new(),
166 overflowed: false,
167 }
168 }
169
170 pub fn add(&mut self, rect: Rect) {
172 if self.rects.push(rect).is_err() {
173 self.overflowed = true;
174 }
175 }
176
177 pub fn rects(&self) -> &[Rect] {
179 &self.rects
180 }
181
182 pub fn overflowed(&self) -> bool {
184 self.overflowed
185 }
186
187 pub fn clear(&mut self) {
189 self.rects.clear();
190 self.overflowed = false;
191 }
192}
193
194impl<const N: usize> Default for BlitPlanner<N> {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200pub struct BlitterRenderer<'a, B: Blitter, const N: usize> {
206 blitter: &'a mut B,
207 surface: Surface<'a>,
208 planner: BlitPlanner<N>,
209 #[cfg(any(
210 feature = "canvas",
211 feature = "gif",
212 feature = "apng",
213 feature = "nes",
214 all(feature = "png", not(target_os = "none")),
215 all(feature = "jpeg", not(target_os = "none")),
216 all(feature = "qrcode", not(target_os = "none")),
217 feature = "lottie",
218 test,
219 ))]
220 scratch: Option<Vec<u8>>,
221 #[cfg(feature = "fontdue")]
222 glyph_cache: BTreeMap<GlyphKey, (Metrics, Vec<u8>)>,
223}
224
225impl<'a, B: Blitter, const N: usize> BlitterRenderer<'a, B, N> {
226 pub fn new(blitter: &'a mut B, surface: Surface<'a>) -> Self {
228 Self {
229 blitter,
230 surface,
231 planner: BlitPlanner::new(),
232 #[cfg(any(
233 feature = "canvas",
234 feature = "gif",
235 feature = "apng",
236 feature = "nes",
237 all(feature = "png", not(target_os = "none")),
238 all(feature = "jpeg", not(target_os = "none")),
239 all(feature = "qrcode", not(target_os = "none")),
240 feature = "lottie",
241 test,
242 ))]
243 scratch: None,
244 #[cfg(feature = "fontdue")]
245 glyph_cache: BTreeMap::new(),
246 }
247 }
248
249 pub fn planner(&mut self) -> &mut BlitPlanner<N> {
251 &mut self.planner
252 }
253
254 #[cfg(any(
255 feature = "canvas",
256 feature = "gif",
257 feature = "apng",
258 feature = "nes",
259 all(feature = "png", not(target_os = "none")),
260 all(feature = "jpeg", not(target_os = "none")),
261 all(feature = "qrcode", not(target_os = "none")),
262 feature = "lottie",
263 test,
264 ))]
265 fn blit_colors(&mut self, position: (i32, i32), pixels: &[Color], w: u32, h: u32) {
266 let required = (w * h * 4) as usize;
267 let buf = self.scratch.get_or_insert_with(Vec::new);
268 if buf.len() < required {
269 buf.resize(required, 0);
270 }
271 for (i, c) in pixels.iter().enumerate() {
272 buf[i * 4..i * 4 + 4].copy_from_slice(&c.to_argb8888().to_le_bytes());
273 }
274 let src = Surface::new(
275 &mut buf[..required],
276 (w * 4) as usize,
277 PixelFmt::Argb8888,
278 w,
279 h,
280 );
281 self.blitter
282 .blit(&src, Rect { x: 0, y: 0, w, h }, &mut self.surface, position);
283 self.planner.add(Rect {
284 x: position.0,
285 y: position.1,
286 w,
287 h,
288 });
289 }
290
291 #[cfg(all(feature = "png", not(target_os = "none")))]
292 pub fn draw_png(
294 &mut self,
295 position: (i32, i32),
296 data: &[u8],
297 ) -> Result<(), rlvgl_core::png::DecodingError> {
298 let (pixels, w, h) = rlvgl_core::png::decode(data)?;
299 self.blit_colors(position, &pixels, w, h);
300 Ok(())
301 }
302
303 #[cfg(all(feature = "jpeg", not(target_os = "none")))]
304 pub fn draw_jpeg(
306 &mut self,
307 position: (i32, i32),
308 data: &[u8],
309 ) -> Result<(), rlvgl_core::jpeg::Error> {
310 let (pixels, w, h) = rlvgl_core::jpeg::decode(data)?;
311 self.blit_colors(position, &pixels, w as u32, h as u32);
312 Ok(())
313 }
314
315 #[cfg(all(feature = "qrcode", not(target_os = "none")))]
316 pub fn draw_qr(
318 &mut self,
319 position: (i32, i32),
320 data: &[u8],
321 ) -> Result<(), rlvgl_core::qrcode::QrError> {
322 let (pixels, w, h) = rlvgl_core::qrcode::generate(data)?;
323 self.blit_colors(position, &pixels, w, h);
324 Ok(())
325 }
326
327 #[cfg(feature = "lottie")]
328 pub fn draw_lottie_frame(
332 &mut self,
333 position: (i32, i32),
334 json: &str,
335 frame: usize,
336 width: u32,
337 height: u32,
338 ) -> Result<(), rlvgl_core::lottie::Error> {
339 let pixels =
340 rlvgl_core::lottie::render_lottie_frame(json, frame, width as usize, height as usize)?;
341 self.blit_colors(position, &pixels, width, height);
342 Ok(())
343 }
344
345 #[cfg(feature = "canvas")]
346 pub fn draw_canvas(&mut self, position: (i32, i32), canvas: &rlvgl_core::canvas::Canvas) {
348 let (w, h) = canvas.size();
349 let pixels = canvas.pixels();
350 self.blit_colors(position, &pixels, w, h);
351 }
352
353 #[cfg(feature = "gif")]
354 pub fn draw_gif_frame(
356 &mut self,
357 position: (i32, i32),
358 data: &[u8],
359 frame: usize,
360 ) -> Result<(), rlvgl_core::gif::DecodingError> {
361 let (frames, w, h) = rlvgl_core::gif::decode(data)?;
362 if let Some(f) = frames.get(frame) {
363 self.blit_colors(position, &f.pixels, w as u32, h as u32);
364 }
365 Ok(())
366 }
367
368 #[cfg(feature = "apng")]
369 pub fn draw_apng_frame(
371 &mut self,
372 position: (i32, i32),
373 data: &[u8],
374 frame: usize,
375 ) -> Result<(), image::ImageError> {
376 let (frames, w, h) = rlvgl_core::apng::decode(data)?;
377 if let Some(f) = frames.get(frame) {
378 self.blit_colors(position, &f.pixels, w, h);
379 }
380 Ok(())
381 }
382
383 #[cfg(all(feature = "pinyin", feature = "fontdue"))]
384 pub fn draw_pinyin_candidates(
388 &mut self,
389 position: (i32, i32),
390 ime: &rlvgl_core::pinyin::PinyinInputMethod,
391 input: &str,
392 color: Color,
393 ) -> bool {
394 if let Some(chars) = ime.candidates(input) {
395 let max_w = self.surface.width as i32 - position.0;
397 let max_h = self.surface.height as i32 - position.1;
398 if max_w <= 0 || max_h < 16 {
399 return false;
400 }
401
402 let text: alloc::string::String = chars.into_iter().collect();
404 let max_chars = (max_w / 16) as usize;
405 let clipped: alloc::string::String = text.chars().take(max_chars).collect();
406 if clipped.is_empty() {
407 return false;
408 }
409 Renderer::draw_text(self, position, &clipped, color);
410 true
411 } else {
412 false
413 }
414 }
415
416 #[cfg(all(feature = "fatfs", feature = "fontdue"))]
417 pub fn draw_fatfs_dir<T>(
419 &mut self,
420 position: (i32, i32),
421 image: &mut T,
422 dir: &str,
423 color: Color,
424 ) -> Result<(), std::io::Error>
425 where
426 T: std::io::Read + std::io::Write + std::io::Seek,
427 {
428 let max_w = self.surface.width as i32 - position.0;
429 let max_h = self.surface.height as i32 - position.1;
430 if max_w <= 0 || max_h <= 0 {
431 return Ok(());
432 }
433 let line_h = 16;
434 let max_lines = (max_h / line_h) as usize;
435 let max_chars = (max_w / 16) as usize;
436 let names = rlvgl_core::fatfs::list_dir(image, dir)?;
437 for (i, name) in names.iter().take(max_lines).enumerate() {
438 let y = position.1 + (i as i32) * line_h;
439 let clipped: alloc::string::String = name.chars().take(max_chars).collect();
440 if clipped.is_empty() {
441 break;
442 }
443 Renderer::draw_text(self, (position.0, y), &clipped, color);
444 }
445 Ok(())
446 }
447
448 #[cfg(feature = "nes")]
449 pub fn draw_nes_frame(
451 &mut self,
452 position: (i32, i32),
453 pixels: &[Color],
454 width: u32,
455 height: u32,
456 ) {
457 self.blit_colors(position, pixels, width, height);
458 }
459
460 #[cfg(feature = "fontdue")]
461 pub fn draw_text(
463 &mut self,
464 position: (i32, i32),
465 text: &str,
466 color: Color,
467 font_data: &[u8],
468 px: f32,
469 ) {
470 let vm = line_metrics(font_data, px).unwrap();
471 let ascent = round_to_i32(vm.ascent);
472 let baseline = position.1 + ascent;
473 let mut x_cursor = position.0;
474 for ch in text.chars() {
475 let key = GlyphKey {
476 font: font_data.as_ptr(),
477 size: px.to_bits(),
478 ch,
479 };
480 let (metrics, bitmap) = {
481 let entry = self
482 .glyph_cache
483 .entry(key)
484 .or_insert_with(|| rasterize_glyph(font_data, ch, px).unwrap());
485 (entry.0, entry.1.clone())
486 };
487 let w = metrics.width as i32;
488 let h = metrics.height as i32;
489 if w == 0 || h == 0 {
490 x_cursor += round_to_i32(metrics.advance_width);
491 continue;
492 }
493 let mut argb = vec![0u8; (w * h * 4) as usize];
494 for y in 0..h {
495 for x in 0..w {
496 let alpha = bitmap[(y) as usize * metrics.width + x as usize];
497 let idx = ((y * w + x) * 4) as usize;
498 argb[idx] = (color.0 as u16 * alpha as u16 / 255) as u8;
499 argb[idx + 1] = (color.1 as u16 * alpha as u16 / 255) as u8;
500 argb[idx + 2] = (color.2 as u16 * alpha as u16 / 255) as u8;
501 argb[idx + 3] = alpha;
502 }
503 }
504 let src = Surface::new(
505 argb.as_mut_slice(),
506 (w * 4) as usize,
507 PixelFmt::Argb8888,
508 w as u32,
509 h as u32,
510 );
511 let dst_pos = (
512 x_cursor + metrics.xmin,
513 baseline - ascent - metrics.ymin - (h - 1),
514 );
515 self.blitter.blend(
516 &src,
517 Rect {
518 x: 0,
519 y: 0,
520 w: w as u32,
521 h: h as u32,
522 },
523 &mut self.surface,
524 dst_pos,
525 );
526 self.planner.add(Rect {
527 x: dst_pos.0,
528 y: dst_pos.1,
529 w: w as u32,
530 h: h as u32,
531 });
532 x_cursor += round_to_i32(metrics.advance_width);
533 }
534 }
535
536 #[cfg(not(feature = "fontdue"))]
537 pub fn draw_text(
539 &mut self,
540 position: (i32, i32),
541 text: &str,
542 color: Color,
543 _font_data: &[u8],
544 _px: f32,
545 ) {
546 let _ = (position, text, color);
547 }
548}
549
550impl<B: Blitter, const N: usize> Renderer for BlitterRenderer<'_, B, N> {
551 fn blend_rect(&mut self, rect: WidgetRect, color: Color) {
552 let alpha = color.3 as u16;
553 if alpha == 0 {
554 return;
555 }
556 if alpha == 255 {
557 self.fill_rect(rect, color);
558 return;
559 }
560 if self.surface.format == PixelFmt::Argb8888 {
562 let sw = self.surface.width as i32;
563 let sh = self.surface.height as i32;
564 let stride = self.surface.stride;
565 let inv = 255 - alpha;
566 let x0 = rect.x.max(0);
567 let y0 = rect.y.max(0);
568 let x1 = (rect.x + rect.width).min(sw);
569 let y1 = (rect.y + rect.height).min(sh);
570 for y in y0..y1 {
571 for x in x0..x1 {
572 let off = y as usize * stride + x as usize * 4;
573 let bg_b = self.surface.buf[off] as u16;
574 let bg_g = self.surface.buf[off + 1] as u16;
575 let bg_r = self.surface.buf[off + 2] as u16;
576 self.surface.buf[off] = ((color.2 as u16 * alpha + bg_b * inv) / 255) as u8;
577 self.surface.buf[off + 1] = ((color.1 as u16 * alpha + bg_g * inv) / 255) as u8;
578 self.surface.buf[off + 2] = ((color.0 as u16 * alpha + bg_r * inv) / 255) as u8;
579 self.surface.buf[off + 3] = 0xff;
580 }
581 }
582 self.planner.add(Rect {
583 x: rect.x,
584 y: rect.y,
585 w: rect.width as u32,
586 h: rect.height as u32,
587 });
588 } else {
589 self.fill_rect(rect, color);
591 }
592 }
593
594 fn fill_rect(&mut self, rect: WidgetRect, color: Color) {
595 let r = Rect {
596 x: rect.x,
597 y: rect.y,
598 w: rect.width as u32,
599 h: rect.height as u32,
600 };
601 self.planner.add(r);
602 self.blitter.fill(&mut self.surface, r, color.to_argb8888());
603 }
604
605 fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
606 #[cfg(feature = "fontdue")]
607 {
608 const PX: f32 = 16.0;
609 BlitterRenderer::draw_text(self, position, text, color, FONT_DATA, PX);
610 }
611 #[cfg(not(feature = "fontdue"))]
612 {
613 let _ = (position, text, color);
614 }
615 }
616
617 fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
618 if self.surface.format == PixelFmt::Argb8888 {
621 let sw = self.surface.width as i32;
622 let sh = self.surface.height as i32;
623 let stride = self.surface.stride;
624 for y in 0..height as i32 {
625 let dy = position.1 + y;
626 if dy < 0 || dy >= sh {
627 continue;
628 }
629 for x in 0..width as i32 {
630 let dx = position.0 + x;
631 if dx < 0 || dx >= sw {
632 continue;
633 }
634 let src_idx = (y as u32 * width + x as u32) as usize;
635 if let Some(&c) = pixels.get(src_idx) {
636 let off = dy as usize * stride + dx as usize * 4;
637 self.surface.buf[off..off + 4]
638 .copy_from_slice(&c.to_argb8888().to_le_bytes());
639 }
640 }
641 }
642 self.planner.add(Rect {
643 x: position.0,
644 y: position.1,
645 w: width,
646 h: height,
647 });
648 } else {
649 for y in 0..height as i32 {
651 for x in 0..width as i32 {
652 let idx = (y as u32 * width + x as u32) as usize;
653 if let Some(&c) = pixels.get(idx) {
654 self.fill_rect(
655 WidgetRect {
656 x: position.0 + x,
657 y: position.1 + y,
658 width: 1,
659 height: 1,
660 },
661 c,
662 );
663 }
664 }
665 }
666 }
667 }
668
669 fn blend_row(&mut self, x: i32, y: i32, color: Color, coverage: &[u8]) {
670 if color.3 == 0 || coverage.is_empty() {
674 return;
675 }
676 if self.surface.format != PixelFmt::Argb8888 {
677 for (i, &cov) in coverage.iter().enumerate() {
680 if cov == 0 {
681 continue;
682 }
683 let alpha = ((color.3 as u16 * cov as u16) / 255) as u8;
684 if alpha == 0 {
685 continue;
686 }
687 self.blend_rect(
688 WidgetRect {
689 x: x + i as i32,
690 y,
691 width: 1,
692 height: 1,
693 },
694 Color(color.0, color.1, color.2, alpha),
695 );
696 }
697 return;
698 }
699 let sw = self.surface.width as i32;
700 let sh = self.surface.height as i32;
701 if y < 0 || y >= sh {
702 return;
703 }
704 let stride = self.surface.stride;
705 let src_a = color.3 as u16;
706 let row_off = y as usize * stride;
707 let mut written_x0 = i32::MAX;
708 let mut written_x1 = i32::MIN;
709 for (i, &cov) in coverage.iter().enumerate() {
710 if cov == 0 {
711 continue;
712 }
713 let px = x + i as i32;
714 if px < 0 || px >= sw {
715 continue;
716 }
717 let alpha = (src_a * cov as u16) / 255;
718 if alpha == 0 {
719 continue;
720 }
721 let inv = 255 - alpha;
722 let off = row_off + (px as usize) * 4;
723 let bg_b = self.surface.buf[off] as u16;
724 let bg_g = self.surface.buf[off + 1] as u16;
725 let bg_r = self.surface.buf[off + 2] as u16;
726 self.surface.buf[off] = ((color.2 as u16 * alpha + bg_b * inv) / 255) as u8;
727 self.surface.buf[off + 1] = ((color.1 as u16 * alpha + bg_g * inv) / 255) as u8;
728 self.surface.buf[off + 2] = ((color.0 as u16 * alpha + bg_r * inv) / 255) as u8;
729 self.surface.buf[off + 3] = 0xff;
730 if px < written_x0 {
731 written_x0 = px;
732 }
733 if px > written_x1 {
734 written_x1 = px;
735 }
736 }
737 if written_x0 <= written_x1 {
738 self.planner.add(Rect {
739 x: written_x0,
740 y,
741 w: (written_x1 - written_x0 + 1) as u32,
742 h: 1,
743 });
744 }
745 }
746
747 fn submit(&mut self, list: &CommandList) {
748 submit_with_occlusion(self, list);
749 }
750}
751
752fn submit_with_occlusion<B: Blitter, const N: usize>(
790 r: &mut BlitterRenderer<'_, B, N>,
791 list: &CommandList,
792) {
793 let mut pending: Option<(WidgetRect, Color, BlendMode)> = None;
794
795 for (i, cmd) in list.iter().enumerate() {
796 let occluded = match cmd.aabb() {
798 None => false,
799 Some(bb_i) => {
800 let mut hit = false;
801 for later in list.iter().skip(i + 1) {
802 if matches!(later, Cmd::Barrier | Cmd::SetClip { .. }) {
803 break;
804 }
805 if !is_opaque_filler(later) {
806 continue;
807 }
808 let Some(bb_j) = later.aabb() else { continue };
809 if rect_contains(bb_j, bb_i) {
810 hit = true;
811 break;
812 }
813 }
814 hit
815 }
816 };
817 if occluded {
818 continue;
821 }
822
823 if let Cmd::FillRect { rect, color, blend } = cmd {
825 if let Some((prect, pcolor, pblend)) = pending.as_ref() {
826 if pcolor == color
827 && pblend == blend
828 && let Some(merged) = merge_rects_axis_aligned(*prect, *rect)
829 {
830 pending = Some((merged, *color, *blend));
831 continue;
832 }
833 flush_pending(r, pending.take());
835 }
836 pending = Some((*rect, *color, *blend));
837 } else {
838 flush_pending(r, pending.take());
840 cmd.dispatch_to(r);
841 }
842 }
843 flush_pending(r, pending.take());
844}
845
846#[inline]
847fn flush_pending<B: Blitter, const N: usize>(
848 r: &mut BlitterRenderer<'_, B, N>,
849 pending: Option<(WidgetRect, Color, BlendMode)>,
850) {
851 if let Some((rect, color, blend)) = pending {
852 Cmd::FillRect { rect, color, blend }.dispatch_to(r);
853 }
854}
855
856#[inline]
861fn merge_rects_axis_aligned(a: WidgetRect, b: WidgetRect) -> Option<WidgetRect> {
862 if a.y == b.y && a.height == b.height {
863 if a.x + a.width == b.x {
864 return Some(WidgetRect {
865 x: a.x,
866 y: a.y,
867 width: a.width + b.width,
868 height: a.height,
869 });
870 }
871 if b.x + b.width == a.x {
872 return Some(WidgetRect {
873 x: b.x,
874 y: b.y,
875 width: a.width + b.width,
876 height: a.height,
877 });
878 }
879 }
880 if a.x == b.x && a.width == b.width {
881 if a.y + a.height == b.y {
882 return Some(WidgetRect {
883 x: a.x,
884 y: a.y,
885 width: a.width,
886 height: a.height + b.height,
887 });
888 }
889 if b.y + b.height == a.y {
890 return Some(WidgetRect {
891 x: b.x,
892 y: b.y,
893 width: a.width,
894 height: a.height + b.height,
895 });
896 }
897 }
898 None
899}
900
901#[inline]
902fn is_opaque_filler(cmd: &Cmd) -> bool {
903 if let Cmd::FillRect { color, .. } = cmd {
904 color.3 == 255
905 } else {
906 false
907 }
908}
909
910#[inline]
911fn rect_contains(outer: WidgetRect, inner: WidgetRect) -> bool {
912 outer.x <= inner.x
913 && outer.y <= inner.y
914 && outer.x + outer.width >= inner.x + inner.width
915 && outer.y + outer.height >= inner.y + inner.height
916}
917
918pub struct RotatedRenderer<'a> {
928 inner: &'a mut dyn Renderer,
929 fb_width: i32,
931}
932
933impl<'a> RotatedRenderer<'a> {
934 pub fn new(inner: &'a mut dyn Renderer, fb_width: u32) -> Self {
938 Self {
939 inner,
940 fb_width: fb_width as i32,
941 }
942 }
943}
944
945impl Renderer for RotatedRenderer<'_> {
946 fn fill_rect(&mut self, rect: WidgetRect, color: Color) {
947 let mut fb_x = self.fb_width - rect.y - rect.height;
948 let fb_y = rect.x;
949 let mut fb_w = rect.height;
950 let fb_h = rect.width;
951
952 if fb_w <= 0 || fb_h <= 0 {
953 return;
954 }
955
956 if fb_x < 0 {
958 fb_w += fb_x; fb_x = 0;
960 }
961 if fb_x + fb_w > self.fb_width {
963 fb_w = self.fb_width - fb_x;
964 }
965 if fb_w <= 0 {
966 return;
967 }
968
969 self.inner.fill_rect(
970 WidgetRect {
971 x: fb_x,
972 y: fb_y,
973 width: fb_w,
974 height: fb_h,
975 },
976 color,
977 );
978 }
979
980 fn blend_rect(&mut self, rect: WidgetRect, color: Color) {
981 let mut fb_x = self.fb_width - rect.y - rect.height;
982 let fb_y = rect.x;
983 let mut fb_w = rect.height;
984 let fb_h = rect.width;
985
986 if fb_w <= 0 || fb_h <= 0 {
987 return;
988 }
989
990 if fb_x < 0 {
991 fb_w += fb_x;
992 fb_x = 0;
993 }
994 if fb_x + fb_w > self.fb_width {
995 fb_w = self.fb_width - fb_x;
996 }
997 if fb_w <= 0 {
998 return;
999 }
1000
1001 self.inner.blend_rect(
1002 WidgetRect {
1003 x: fb_x,
1004 y: fb_y,
1005 width: fb_w,
1006 height: fb_h,
1007 },
1008 color,
1009 );
1010 }
1011
1012 fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
1013 let fx = self.fb_width - 1 - position.1;
1014 if fx >= 0 {
1015 self.inner.draw_text((fx, position.0), text, color);
1016 }
1017 }
1018
1019 fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
1020 if width == 0 || height == 0 {
1038 return;
1039 }
1040
1041 let fb_x = self.fb_width - position.1 - height as i32;
1045 let fb_y = position.0;
1046 let phys_w = height; let phys_h = width; const SCRATCH_PIXELS: usize = 64 * 64;
1069 #[cfg_attr(target_os = "none", unsafe(link_section = ".rlvgl_blit_scratch"))]
1109 static mut SCRATCH: [Color; SCRATCH_PIXELS] = [Color(0, 0, 0, 0); SCRATCH_PIXELS]; let len = (width * height) as usize;
1112 if len <= SCRATCH_PIXELS {
1113 let scratch = unsafe { &mut SCRATCH[..len] };
1118 for ly in 0..height as i32 {
1123 for lx in 0..width as i32 {
1124 let src_idx = (ly as u32 * width + lx as u32) as usize;
1125 let dst_idx = (lx as u32 * phys_w + (height as i32 - 1 - ly) as u32) as usize;
1126 scratch[dst_idx] = pixels.get(src_idx).copied().unwrap_or(Color(0, 0, 0, 0));
1127 }
1128 }
1129 self.inner
1130 .draw_pixels((fb_x, fb_y), scratch, phys_w, phys_h);
1131 } else {
1132 let mut rotated: alloc::vec::Vec<Color> = alloc::vec::Vec::with_capacity(len);
1134 rotated.resize(len, Color(0, 0, 0, 0));
1135 for ly in 0..height as i32 {
1136 for lx in 0..width as i32 {
1137 let src_idx = (ly as u32 * width + lx as u32) as usize;
1138 let dst_idx = (lx as u32 * phys_w + (height as i32 - 1 - ly) as u32) as usize;
1139 if let Some(&c) = pixels.get(src_idx) {
1140 rotated[dst_idx] = c;
1141 }
1142 }
1143 }
1144 self.inner
1145 .draw_pixels((fb_x, fb_y), &rotated, phys_w, phys_h);
1146 }
1147 }
1148
1149 fn submit(&mut self, list: &CommandList) {
1150 self.inner.submit(list);
1154 }
1155}
1156
1157#[cfg(test)]
1158mod scratch_tests {
1159 use super::*;
1160 use crate::cpu_blitter::CpuBlitter;
1161
1162 #[test]
1163 fn blit_colors_reuses_scratch_buffer() {
1164 let mut buf = [0u8; 4 * 4 * 4];
1165 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1166 let mut blit = CpuBlitter;
1167 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1168 BlitterRenderer::new(&mut blit, surface);
1169 let pixels = [Color(0, 0, 0, 0)];
1170 renderer.blit_colors((0, 0), &pixels, 1, 1);
1171 let first_ptr = renderer.scratch.as_ref().unwrap().as_ptr();
1172 renderer.blit_colors((1, 1), &pixels, 1, 1);
1173 let second_ptr = renderer.scratch.as_ref().unwrap().as_ptr();
1174 assert_eq!(first_ptr, second_ptr);
1175 }
1176}
1177
1178#[cfg(all(test, feature = "fontdue"))]
1179mod text_tests {
1180 use super::*;
1181 use crate::cpu_blitter::CpuBlitter;
1182
1183 #[test]
1184 fn blitter_draws_text() {
1185 let mut buf = [0u8; 64 * 64 * 4];
1186 let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1187 let mut blit = CpuBlitter;
1188 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1189 BlitterRenderer::new(&mut blit, surface);
1190 Renderer::draw_text(&mut renderer, (0, 32), "A", Color(255, 255, 255, 255));
1191 assert!(buf.iter().any(|&p| p != 0));
1192 }
1193
1194 #[test]
1195 fn cache_accounts_for_size() {
1196 let mut buf = [0u8; 64 * 64 * 4];
1197 let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1198 let mut blit = CpuBlitter;
1199 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1200 BlitterRenderer::new(&mut blit, surface);
1201 renderer.draw_text((0, 32), "Hi", Color(255, 255, 255, 255), FONT_DATA, 16.0);
1202 let len_after_small = renderer.glyph_cache.len();
1203 renderer.draw_text((0, 32), "Hi", Color(255, 255, 255, 255), FONT_DATA, 16.0);
1204 assert_eq!(len_after_small, renderer.glyph_cache.len());
1205 renderer.draw_text((0, 32), "Hi", Color(255, 255, 255, 255), FONT_DATA, 24.0);
1206 assert!(renderer.glyph_cache.len() > len_after_small);
1207 }
1208}
1209
1210#[cfg(all(test, feature = "png", not(target_os = "none")))]
1211mod png_tests {
1212 use super::*;
1213 use crate::cpu_blitter::CpuBlitter;
1214 use base64::Engine;
1215 use rlvgl_core::png::DecodingError;
1216
1217 const RED_DOT_PNG: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC";
1218
1219 #[test]
1220 fn blitter_draws_png() {
1221 let data = base64::engine::general_purpose::STANDARD
1222 .decode(RED_DOT_PNG)
1223 .unwrap();
1224 let mut buf = [0u8; 4 * 4 * 4];
1225 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1226 let mut blit = CpuBlitter;
1227 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1228 BlitterRenderer::new(&mut blit, surface);
1229 renderer.draw_png((0, 0), &data).unwrap();
1230 assert!(buf.iter().any(|&p| p != 0));
1231 }
1232
1233 #[test]
1234 fn blitter_rejects_invalid_png() {
1235 let mut buf = [0u8; 4 * 4 * 4];
1236 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1237 let mut blit = CpuBlitter;
1238 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1239 BlitterRenderer::new(&mut blit, surface);
1240 let err = renderer.draw_png((0, 0), b"not a png").unwrap_err();
1241 assert!(matches!(err, DecodingError::Format(_)));
1242 }
1243}
1244
1245#[cfg(all(test, feature = "jpeg", not(target_os = "none")))]
1246mod jpeg_tests {
1247 use super::*;
1248 use crate::cpu_blitter::CpuBlitter;
1249 use base64::Engine;
1250 use rlvgl_core::jpeg::Error as JpegError;
1251
1252 const RED_DOT_JPEG: &str = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDi6KKK+ZP3E//Z";
1253
1254 #[test]
1255 fn blitter_draws_jpeg() {
1256 let data = base64::engine::general_purpose::STANDARD
1257 .decode(RED_DOT_JPEG)
1258 .unwrap();
1259 let mut buf = [0u8; 4 * 4 * 4];
1260 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1261 let mut blit = CpuBlitter;
1262 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1263 BlitterRenderer::new(&mut blit, surface);
1264 renderer.draw_jpeg((0, 0), &data).unwrap();
1265 assert!(buf.iter().any(|&p| p != 0));
1266 }
1267
1268 #[test]
1269 fn blitter_rejects_invalid_jpeg() {
1270 let mut buf = [0u8; 4 * 4 * 4];
1271 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1272 let mut blit = CpuBlitter;
1273 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1274 BlitterRenderer::new(&mut blit, surface);
1275 let err = renderer.draw_jpeg((0, 0), b"not a jpeg").unwrap_err();
1276 assert!(matches!(err, JpegError::Format(_)));
1277 }
1278}
1279
1280#[cfg(all(test, feature = "gif"))]
1281mod gif_tests {
1282 use super::*;
1283 use crate::cpu_blitter::CpuBlitter;
1284 use base64::Engine;
1285 use rlvgl_core::gif::DecodingError as GifDecodingError;
1286
1287 const RED_DOT_GIF: &str = "R0lGODdhAQABAPAAAP8AAP///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
1288
1289 #[test]
1290 fn blitter_draws_gif() {
1291 let data = base64::engine::general_purpose::STANDARD
1292 .decode(RED_DOT_GIF)
1293 .unwrap();
1294 let mut buf = [0u8; 4 * 4 * 4];
1295 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1296 let mut blit = CpuBlitter;
1297 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1298 BlitterRenderer::new(&mut blit, surface);
1299 renderer.draw_gif_frame((0, 0), &data, 0).unwrap();
1300 assert!(buf.iter().any(|&p| p != 0));
1301 }
1302
1303 #[test]
1304 fn blitter_rejects_invalid_gif() {
1305 let mut buf = [0u8; 4 * 4 * 4];
1306 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1307 let mut blit = CpuBlitter;
1308 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1309 BlitterRenderer::new(&mut blit, surface);
1310 let err = renderer
1311 .draw_gif_frame((0, 0), b"not a gif", 0)
1312 .unwrap_err();
1313 assert!(matches!(err, GifDecodingError::Format(_)));
1314 }
1315}
1316
1317#[cfg(all(test, feature = "apng"))]
1318mod apng_tests {
1319 use super::*;
1320 use crate::cpu_blitter::CpuBlitter;
1321 use base64::Engine;
1322
1323 const RED_DOT_APNG: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjVEwAAAABAAAAALQt6aAAAAAaZmNUTAAAAAAAAAABAAAAAQAAAAAAAAAAAGQD6AEAqmVSjAAAAA1JREFUeJxj+M/A8B8ABQAB/4mZPR0AAAAASUVORK5CYII=";
1324
1325 #[test]
1326 fn blitter_draws_apng() {
1327 let data = base64::engine::general_purpose::STANDARD
1328 .decode(RED_DOT_APNG)
1329 .unwrap();
1330 let mut buf = [0u8; 4 * 4 * 4];
1331 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1332 let mut blit = CpuBlitter;
1333 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1334 BlitterRenderer::new(&mut blit, surface);
1335 renderer.draw_apng_frame((0, 0), &data, 0).unwrap();
1336 assert!(buf.iter().any(|&p| p != 0));
1337 }
1338}
1339
1340#[cfg(all(test, feature = "canvas"))]
1341mod canvas_tests {
1342 use super::*;
1343 use crate::cpu_blitter::CpuBlitter;
1344 use embedded_graphics::prelude::Point;
1345 use rlvgl_core::canvas::Canvas;
1346
1347 #[test]
1348 fn blitter_draws_canvas() {
1349 let mut canvas = Canvas::new(1, 1);
1350 canvas.draw_pixel(Point::new(0, 0), Color(255, 0, 0, 255));
1351 let mut buf = [0u8; 4];
1352 let surface = Surface::new(&mut buf, 4, PixelFmt::Argb8888, 1, 1);
1353 let mut blit = CpuBlitter;
1354 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1355 BlitterRenderer::new(&mut blit, surface);
1356 renderer.draw_canvas((0, 0), &canvas);
1357 assert!(buf.iter().any(|&p| p != 0));
1358 }
1359}
1360
1361#[cfg(all(test, feature = "qrcode", not(target_os = "none")))]
1362mod qrcode_tests {
1363 use super::*;
1364 use crate::cpu_blitter::CpuBlitter;
1365 use rlvgl_core::qrcode::QrError;
1366
1367 #[test]
1368 fn blitter_draws_qr() {
1369 let mut buf = [0u8; 64 * 64 * 4];
1370 let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1371 let mut blit = CpuBlitter;
1372 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1373 BlitterRenderer::new(&mut blit, surface);
1374 renderer.draw_qr((0, 0), b"hi").unwrap();
1375 assert!(buf.iter().any(|&p| p != 0));
1376 }
1377
1378 #[test]
1379 fn blitter_rejects_invalid_qr_data() {
1380 let mut buf = [0u8; 64 * 64 * 4];
1381 let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1382 let mut blit = CpuBlitter;
1383 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1384 BlitterRenderer::new(&mut blit, surface);
1385 let data = vec![0u8; 3000];
1386 let err = renderer.draw_qr((0, 0), &data).unwrap_err();
1387 assert!(matches!(err, QrError::DataTooLong));
1388 }
1389}
1390
1391#[cfg(all(test, feature = "lottie"))]
1392mod lottie_tests {
1393 use super::*;
1394 use crate::cpu_blitter::CpuBlitter;
1395
1396 const SIMPLE_JSON: &str =
1397 "{\"v\":\"5.7\",\"fr\":30,\"ip\":0,\"op\":0,\"w\":1,\"h\":1,\"layers\":[]}";
1398
1399 #[test]
1400 fn blitter_draws_lottie() {
1401 let mut buf = [0u8; 4 * 4 * 4];
1402 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1403 let mut blit = CpuBlitter;
1404 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1405 BlitterRenderer::new(&mut blit, surface);
1406 renderer
1407 .draw_lottie_frame((0, 0), SIMPLE_JSON, 0, 1, 1)
1408 .unwrap();
1409 assert!(buf.iter().any(|&p| p != 0));
1410 }
1411
1412 #[test]
1413 fn blitter_rejects_invalid_lottie() {
1414 let mut buf = [0u8; 4 * 4 * 4];
1415 let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1416 let mut blit = CpuBlitter;
1417 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1418 BlitterRenderer::new(&mut blit, surface);
1419 assert!(
1420 renderer
1421 .draw_lottie_frame((0, 0), "not json", 0, 1, 1)
1422 .is_err()
1423 );
1424 }
1425}
1426
1427#[cfg(all(test, feature = "pinyin", feature = "fontdue"))]
1428mod pinyin_tests {
1429 use super::*;
1430 use crate::cpu_blitter::CpuBlitter;
1431 use rlvgl_core::pinyin::PinyinInputMethod;
1432
1433 #[test]
1434 fn blitter_draws_pinyin() {
1435 let mut buf = [0u8; 64 * 64 * 4];
1436 let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1437 let mut blit = CpuBlitter;
1438 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1439 BlitterRenderer::new(&mut blit, surface);
1440 let ime = PinyinInputMethod;
1441 assert!(renderer.draw_pinyin_candidates((0, 0), &ime, "zhong", Color(255, 255, 255, 255)));
1442 assert!(buf.iter().any(|&p| p != 0));
1443 }
1444
1445 #[test]
1446 fn pinyin_candidates_clipped_to_surface() {
1447 let mut buf = [0u8; 32 * 16 * 4];
1448 let surface = Surface::new(&mut buf, 32 * 4, PixelFmt::Argb8888, 32, 16);
1449 let mut blit = CpuBlitter;
1450 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1451 BlitterRenderer::new(&mut blit, surface);
1452 let ime = PinyinInputMethod;
1453 assert!(renderer.draw_pinyin_candidates((0, 0), &ime, "zhong", Color(255, 255, 255, 255)));
1454
1455 let mut expected = [0u8; 32 * 16 * 4];
1456 let surface_e = Surface::new(&mut expected, 32 * 4, PixelFmt::Argb8888, 32, 16);
1457 let mut blit_e = CpuBlitter;
1458 let mut renderer_e: BlitterRenderer<'_, CpuBlitter, 4> =
1459 BlitterRenderer::new(&mut blit_e, surface_e);
1460 let chars = ime.candidates("zhong").unwrap();
1461 let text: alloc::string::String = chars.into_iter().collect();
1462 let clipped: alloc::string::String = text.chars().take(2).collect();
1463 Renderer::draw_text(&mut renderer_e, (0, 0), &clipped, Color(255, 255, 255, 255));
1464 assert_eq!(buf[..], expected[..]);
1465 }
1466}
1467
1468#[cfg(all(test, feature = "fatfs", feature = "fontdue"))]
1469mod fatfs_tests {
1470 use super::*;
1471 use crate::cpu_blitter::CpuBlitter;
1472 use fatfs::{FileSystem, FormatVolumeOptions, FsOptions};
1473 use fscommon::BufStream;
1474 use std::io::{Cursor, Seek, SeekFrom, Write};
1475
1476 #[test]
1477 fn blitter_draws_fatfs_listing() {
1478 let mut img = Cursor::new(vec![0u8; 1024 * 512]);
1479 fatfs::format_volume(&mut img, FormatVolumeOptions::new()).unwrap();
1480 img.seek(SeekFrom::Start(0)).unwrap();
1481 {
1482 let buf_stream = BufStream::new(&mut img);
1483 let fs = FileSystem::new(buf_stream, FsOptions::new()).unwrap();
1484 fs.root_dir()
1485 .create_file("foo.txt")
1486 .unwrap()
1487 .write_all(b"hi")
1488 .unwrap();
1489 }
1490 img.seek(SeekFrom::Start(0)).unwrap();
1491 let mut buf = [0u8; 64 * 64 * 4];
1492 let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1493 let mut blit = CpuBlitter;
1494 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1495 BlitterRenderer::new(&mut blit, surface);
1496 renderer
1497 .draw_fatfs_dir((0, 0), &mut img, "/", Color(255, 255, 255, 255))
1498 .unwrap();
1499 assert!(buf.iter().any(|&p| p != 0));
1500 }
1501
1502 #[test]
1503 fn fatfs_listing_clipped_to_surface() {
1504 let mut img = Cursor::new(vec![0u8; 1024 * 512]);
1505 fatfs::format_volume(&mut img, FormatVolumeOptions::new()).unwrap();
1506 img.seek(SeekFrom::Start(0)).unwrap();
1507 {
1508 let buf_stream = BufStream::new(&mut img);
1509 let fs = FileSystem::new(buf_stream, FsOptions::new()).unwrap();
1510 fs.root_dir().create_file("first_long_name.txt").unwrap();
1511 fs.root_dir().create_file("second_long_name.txt").unwrap();
1512 fs.root_dir().create_file("third_long_name.txt").unwrap();
1513 }
1514 img.seek(SeekFrom::Start(0)).unwrap();
1515 let image_vec = img.get_ref().clone();
1516 let mut img_expected = Cursor::new(image_vec.clone());
1517 let mut img_actual = Cursor::new(image_vec);
1518
1519 let mut buf = [0u8; 32 * 32 * 4];
1520 let surface = Surface::new(&mut buf, 32 * 4, PixelFmt::Argb8888, 32, 32);
1521 let mut blit = CpuBlitter;
1522 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1523 BlitterRenderer::new(&mut blit, surface);
1524 renderer
1525 .draw_fatfs_dir((0, 0), &mut img_actual, "/", Color(255, 255, 255, 255))
1526 .unwrap();
1527
1528 let names = rlvgl_core::fatfs::list_dir(&mut img_expected, "/").unwrap();
1529 let mut expected = [0u8; 32 * 32 * 4];
1530 let surface_e = Surface::new(&mut expected, 32 * 4, PixelFmt::Argb8888, 32, 32);
1531 let mut blit_e = CpuBlitter;
1532 let mut renderer_e: BlitterRenderer<'_, CpuBlitter, 4> =
1533 BlitterRenderer::new(&mut blit_e, surface_e);
1534 for (i, name) in names.iter().take(2).enumerate() {
1535 let clipped: alloc::string::String = name.chars().take(2).collect();
1536 Renderer::draw_text(
1537 &mut renderer_e,
1538 (0, (i as i32) * 16),
1539 &clipped,
1540 Color(255, 255, 255, 255),
1541 );
1542 }
1543 assert_eq!(buf[..], expected[..]);
1544 }
1545}
1546
1547#[cfg(all(test, feature = "nes"))]
1548mod nes_tests {
1549 use super::*;
1550 use crate::cpu_blitter::CpuBlitter;
1551
1552 #[test]
1553 fn blitter_draws_nes_frame() {
1554 let pixels = [Color(255, 0, 0, 255)];
1555 let mut buf = [0u8; 4];
1556 let surface = Surface::new(&mut buf, 4, PixelFmt::Argb8888, 1, 1);
1557 let mut blit = CpuBlitter;
1558 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1559 BlitterRenderer::new(&mut blit, surface);
1560 renderer.draw_nes_frame((0, 0), &pixels, 1, 1);
1561 assert!(buf.iter().any(|&p| p != 0));
1562 }
1563
1564 #[test]
1565 fn blitter_draws_full_nes_frame() {
1566 let mut pixels = [Color(0, 0, 0, 255); 256 * 240];
1567 for y in 0..240 {
1568 for x in 0..256 {
1569 pixels[y * 256 + x] = Color(x as u8, y as u8, 0, 255);
1570 }
1571 }
1572 let mut buf = [0u8; 256 * 240 * 4];
1573 let surface = Surface::new(&mut buf, 256 * 4, PixelFmt::Argb8888, 256, 240);
1574 let mut blit = CpuBlitter;
1575 let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1576 BlitterRenderer::new(&mut blit, surface);
1577 renderer.draw_nes_frame((0, 0), &pixels, 256, 240);
1578 let x = 128usize;
1579 let y = 120usize;
1580 let idx = (y * 256 + x) * 4;
1581 let actual = u32::from_le_bytes(buf[idx..idx + 4].try_into().unwrap());
1582 let expected = Color(x as u8, y as u8, 0, 255).to_argb8888();
1583 assert_eq!(actual, expected);
1584 }
1585}