Skip to main content

rlvgl_platform/
blit.rs

1//! Basic graphics types and blitter traits for platform backends.
2//!
3//! These types describe pixel surfaces and operations that can be
4//! accelerated by different platform implementations.
5
6#[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;
23#[cfg(feature = "fontdue")]
24use rlvgl_core::fontdue::{Metrics, line_metrics, rasterize_glyph};
25use rlvgl_core::renderer::Renderer;
26use rlvgl_core::widget::{Color, Rect as WidgetRect};
27
28#[cfg(feature = "fontdue")]
29const FONT_DATA: &[u8] = include_bytes!("../../assets/fonts/DejaVuSans.ttf");
30
31#[cfg(feature = "fontdue")]
32fn round_to_i32(value: f32) -> i32 {
33    if value.is_nan() {
34        0
35    } else if value >= 0.0 {
36        (value + 0.5) as i32
37    } else {
38        (value - 0.5) as i32
39    }
40}
41
42#[cfg(feature = "fontdue")]
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
44/// Key identifying a cached glyph by font, size, and character.
45struct GlyphKey {
46    /// Pointer to the font data used to rasterize the glyph.
47    font: *const u8,
48    /// Font size in pixels, stored as raw bits for ordering.
49    size: u32,
50    /// Unicode codepoint of the glyph.
51    ch: char,
52}
53
54/// Supported pixel formats.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum PixelFmt {
57    /// 32-bit ARGB8888 format.
58    Argb8888,
59    /// 16-bit RGB565 format.
60    Rgb565,
61    /// 8-bit grayscale format.
62    L8,
63    /// 8-bit alpha-only format.
64    A8,
65    /// 4-bit alpha-only format.
66    A4,
67}
68
69/// Rectangular region within a surface.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct Rect {
72    /// Left coordinate of the rectangle.
73    pub x: i32,
74    /// Top coordinate of the rectangle.
75    pub y: i32,
76    /// Width of the rectangle in pixels.
77    pub w: u32,
78    /// Height of the rectangle in pixels.
79    pub h: u32,
80}
81
82/// A pixel buffer with dimension and format metadata.
83pub struct Surface<'a> {
84    /// Underlying pixel storage.
85    pub buf: &'a mut [u8],
86    /// Number of bytes between consecutive lines.
87    pub stride: usize,
88    /// Pixel format used by the buffer.
89    pub format: PixelFmt,
90    /// Width of the surface in pixels.
91    pub width: u32,
92    /// Height of the surface in pixels.
93    pub height: u32,
94}
95
96impl<'a> Surface<'a> {
97    /// Create a new surface from raw parts.
98    pub fn new(
99        buf: &'a mut [u8],
100        stride: usize,
101        format: PixelFmt,
102        width: u32,
103        height: u32,
104    ) -> Self {
105        Self {
106            buf,
107            stride,
108            format,
109            width,
110            height,
111        }
112    }
113}
114
115bitflags! {
116    /// Capabilities supported by a blitter implementation.
117    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
118    pub struct BlitCaps: u32 {
119        /// Ability to fill regions with a solid color.
120        const FILL = 0b0001;
121        /// Ability to copy pixels between surfaces.
122        const BLIT = 0b0010;
123        /// Ability to blend a source over a destination.
124        const BLEND = 0b0100;
125        /// Ability to convert between pixel formats.
126        const PFC = 0b1000;
127    }
128}
129
130/// Trait implemented by types capable of transferring pixel data.
131pub trait Blitter {
132    /// Return the capabilities supported by this blitter.
133    fn caps(&self) -> BlitCaps;
134
135    /// Fill `area` within `dst` with a solid `color`.
136    fn fill(&mut self, dst: &mut Surface, area: Rect, color: u32);
137
138    /// Copy pixels from `src` within `src_area` to `dst` at `dst_pos`.
139    fn blit(&mut self, src: &Surface, src_area: Rect, dst: &mut Surface, dst_pos: (i32, i32));
140
141    /// Blend pixels from `src` over `dst`.
142    fn blend(&mut self, src: &Surface, src_area: Rect, dst: &mut Surface, dst_pos: (i32, i32));
143}
144
145/// Collects dirty rectangles for a frame and optionally coalesces them.
146///
147/// The planner stores up to `N` rectangles in a stack-allocated buffer. Call
148/// [`Self::add`] to register a region that changed during rendering and
149/// [`Self::rects`] to obtain the batched list for flushing. After presenting
150/// the frame, call [`Self::clear`] to reuse the planner for the next frame.
151pub struct BlitPlanner<const N: usize> {
152    rects: HVec<Rect, N>,
153}
154
155impl<const N: usize> BlitPlanner<N> {
156    /// Create an empty planner.
157    pub fn new() -> Self {
158        Self { rects: HVec::new() }
159    }
160
161    /// Record a dirty rectangle.
162    pub fn add(&mut self, rect: Rect) {
163        let _ = self.rects.push(rect);
164    }
165
166    /// Return all accumulated rectangles.
167    pub fn rects(&self) -> &[Rect] {
168        &self.rects
169    }
170
171    /// Remove all stored rectangles.
172    pub fn clear(&mut self) {
173        self.rects.clear();
174    }
175}
176
177impl<const N: usize> Default for BlitPlanner<N> {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183/// Renderer implementation backed by a [`Blitter`].
184///
185/// A `BlitterRenderer` owns a target [`Surface`] and batches dirty regions
186/// using a [`BlitPlanner`]. Widgets interact with the generic [`Renderer`] trait
187/// without being aware of the underlying blitter.
188pub struct BlitterRenderer<'a, B: Blitter, const N: usize> {
189    blitter: &'a mut B,
190    surface: Surface<'a>,
191    planner: BlitPlanner<N>,
192    #[cfg(any(
193        feature = "canvas",
194        feature = "gif",
195        feature = "apng",
196        feature = "nes",
197        all(feature = "png", not(target_os = "none")),
198        all(feature = "jpeg", not(target_os = "none")),
199        all(feature = "qrcode", not(target_os = "none")),
200        feature = "lottie",
201        test,
202    ))]
203    scratch: Option<Vec<u8>>,
204    #[cfg(feature = "fontdue")]
205    glyph_cache: BTreeMap<GlyphKey, (Metrics, Vec<u8>)>,
206}
207
208impl<'a, B: Blitter, const N: usize> BlitterRenderer<'a, B, N> {
209    /// Create a new renderer targeting `surface` using `blitter`.
210    pub fn new(blitter: &'a mut B, surface: Surface<'a>) -> Self {
211        Self {
212            blitter,
213            surface,
214            planner: BlitPlanner::new(),
215            #[cfg(any(
216                feature = "canvas",
217                feature = "gif",
218                feature = "apng",
219                feature = "nes",
220                all(feature = "png", not(target_os = "none")),
221                all(feature = "jpeg", not(target_os = "none")),
222                all(feature = "qrcode", not(target_os = "none")),
223                feature = "lottie",
224                test,
225            ))]
226            scratch: None,
227            #[cfg(feature = "fontdue")]
228            glyph_cache: BTreeMap::new(),
229        }
230    }
231
232    /// Access the internal dirty-rectangle planner.
233    pub fn planner(&mut self) -> &mut BlitPlanner<N> {
234        &mut self.planner
235    }
236
237    #[cfg(any(
238        feature = "canvas",
239        feature = "gif",
240        feature = "apng",
241        feature = "nes",
242        all(feature = "png", not(target_os = "none")),
243        all(feature = "jpeg", not(target_os = "none")),
244        all(feature = "qrcode", not(target_os = "none")),
245        feature = "lottie",
246        test,
247    ))]
248    fn blit_colors(&mut self, position: (i32, i32), pixels: &[Color], w: u32, h: u32) {
249        let required = (w * h * 4) as usize;
250        let buf = self.scratch.get_or_insert_with(Vec::new);
251        if buf.len() < required {
252            buf.resize(required, 0);
253        }
254        for (i, c) in pixels.iter().enumerate() {
255            buf[i * 4..i * 4 + 4].copy_from_slice(&c.to_argb8888().to_le_bytes());
256        }
257        let src = Surface::new(
258            &mut buf[..required],
259            (w * 4) as usize,
260            PixelFmt::Argb8888,
261            w,
262            h,
263        );
264        self.blitter
265            .blit(&src, Rect { x: 0, y: 0, w, h }, &mut self.surface, position);
266        self.planner.add(Rect {
267            x: position.0,
268            y: position.1,
269            w,
270            h,
271        });
272    }
273
274    #[cfg(all(feature = "png", not(target_os = "none")))]
275    /// Decode a PNG image and blit it onto the target surface.
276    pub fn draw_png(
277        &mut self,
278        position: (i32, i32),
279        data: &[u8],
280    ) -> Result<(), rlvgl_core::png::DecodingError> {
281        let (pixels, w, h) = rlvgl_core::png::decode(data)?;
282        self.blit_colors(position, &pixels, w, h);
283        Ok(())
284    }
285
286    #[cfg(all(feature = "jpeg", not(target_os = "none")))]
287    /// Decode a JPEG image and blit it onto the target surface.
288    pub fn draw_jpeg(
289        &mut self,
290        position: (i32, i32),
291        data: &[u8],
292    ) -> Result<(), rlvgl_core::jpeg::Error> {
293        let (pixels, w, h) = rlvgl_core::jpeg::decode(data)?;
294        self.blit_colors(position, &pixels, w as u32, h as u32);
295        Ok(())
296    }
297
298    #[cfg(all(feature = "qrcode", not(target_os = "none")))]
299    /// Generate a QR code from `data` and blit it onto the target surface.
300    pub fn draw_qr(
301        &mut self,
302        position: (i32, i32),
303        data: &[u8],
304    ) -> Result<(), rlvgl_core::qrcode::QrError> {
305        let (pixels, w, h) = rlvgl_core::qrcode::generate(data)?;
306        self.blit_colors(position, &pixels, w, h);
307        Ok(())
308    }
309
310    #[cfg(feature = "lottie")]
311    /// Render a Lottie JSON animation frame and blit it onto the target surface.
312    ///
313    /// Returns an error if the JSON data is invalid.
314    pub fn draw_lottie_frame(
315        &mut self,
316        position: (i32, i32),
317        json: &str,
318        frame: usize,
319        width: u32,
320        height: u32,
321    ) -> Result<(), rlvgl_core::lottie::Error> {
322        let pixels =
323            rlvgl_core::lottie::render_lottie_frame(json, frame, width as usize, height as usize)?;
324        self.blit_colors(position, &pixels, width, height);
325        Ok(())
326    }
327
328    #[cfg(feature = "canvas")]
329    /// Blit an [`rlvgl_core::canvas::Canvas`] onto the target surface.
330    pub fn draw_canvas(&mut self, position: (i32, i32), canvas: &rlvgl_core::canvas::Canvas) {
331        let (w, h) = canvas.size();
332        let pixels = canvas.pixels();
333        self.blit_colors(position, &pixels, w, h);
334    }
335
336    #[cfg(feature = "gif")]
337    /// Decode a GIF and blit the selected frame onto the target surface.
338    pub fn draw_gif_frame(
339        &mut self,
340        position: (i32, i32),
341        data: &[u8],
342        frame: usize,
343    ) -> Result<(), rlvgl_core::gif::DecodingError> {
344        let (frames, w, h) = rlvgl_core::gif::decode(data)?;
345        if let Some(f) = frames.get(frame) {
346            self.blit_colors(position, &f.pixels, w as u32, h as u32);
347        }
348        Ok(())
349    }
350
351    #[cfg(feature = "apng")]
352    /// Decode an APNG and blit the selected frame onto the target surface.
353    pub fn draw_apng_frame(
354        &mut self,
355        position: (i32, i32),
356        data: &[u8],
357        frame: usize,
358    ) -> Result<(), image::ImageError> {
359        let (frames, w, h) = rlvgl_core::apng::decode(data)?;
360        if let Some(f) = frames.get(frame) {
361            self.blit_colors(position, &f.pixels, w, h);
362        }
363        Ok(())
364    }
365
366    #[cfg(all(feature = "pinyin", feature = "fontdue"))]
367    /// Render Pinyin IME candidate characters via the blitter.
368    ///
369    /// Returns `true` if any candidates were rendered for `input`.
370    pub fn draw_pinyin_candidates(
371        &mut self,
372        position: (i32, i32),
373        ime: &rlvgl_core::pinyin::PinyinInputMethod,
374        input: &str,
375        color: Color,
376    ) -> bool {
377        if let Some(chars) = ime.candidates(input) {
378            // Determine remaining space on the surface.
379            let max_w = self.surface.width as i32 - position.0;
380            let max_h = self.surface.height as i32 - position.1;
381            if max_w <= 0 || max_h < 16 {
382                return false;
383            }
384
385            // Truncate the candidate string to fit within the surface width.
386            let text: alloc::string::String = chars.into_iter().collect();
387            let max_chars = (max_w / 16) as usize;
388            let clipped: alloc::string::String = text.chars().take(max_chars).collect();
389            if clipped.is_empty() {
390                return false;
391            }
392            Renderer::draw_text(self, position, &clipped, color);
393            true
394        } else {
395            false
396        }
397    }
398
399    #[cfg(all(feature = "fatfs", feature = "fontdue"))]
400    /// List a FAT directory and render the entries line by line.
401    pub fn draw_fatfs_dir<T>(
402        &mut self,
403        position: (i32, i32),
404        image: &mut T,
405        dir: &str,
406        color: Color,
407    ) -> Result<(), std::io::Error>
408    where
409        T: std::io::Read + std::io::Write + std::io::Seek,
410    {
411        let max_w = self.surface.width as i32 - position.0;
412        let max_h = self.surface.height as i32 - position.1;
413        if max_w <= 0 || max_h <= 0 {
414            return Ok(());
415        }
416        let line_h = 16;
417        let max_lines = (max_h / line_h) as usize;
418        let max_chars = (max_w / 16) as usize;
419        let names = rlvgl_core::fatfs::list_dir(image, dir)?;
420        for (i, name) in names.iter().take(max_lines).enumerate() {
421            let y = position.1 + (i as i32) * line_h;
422            let clipped: alloc::string::String = name.chars().take(max_chars).collect();
423            if clipped.is_empty() {
424                break;
425            }
426            Renderer::draw_text(self, (position.0, y), &clipped, color);
427        }
428        Ok(())
429    }
430
431    #[cfg(feature = "nes")]
432    /// Blit an NES frame represented as ARGB8888 [`Color`] pixels.
433    pub fn draw_nes_frame(
434        &mut self,
435        position: (i32, i32),
436        pixels: &[Color],
437        width: u32,
438        height: u32,
439    ) {
440        self.blit_colors(position, pixels, width, height);
441    }
442
443    #[cfg(feature = "fontdue")]
444    /// Draw UTF-8 text using the supplied font and size.
445    pub fn draw_text(
446        &mut self,
447        position: (i32, i32),
448        text: &str,
449        color: Color,
450        font_data: &[u8],
451        px: f32,
452    ) {
453        let vm = line_metrics(font_data, px).unwrap();
454        let ascent = round_to_i32(vm.ascent);
455        let baseline = position.1 + ascent;
456        let mut x_cursor = position.0;
457        for ch in text.chars() {
458            let key = GlyphKey {
459                font: font_data.as_ptr(),
460                size: px.to_bits(),
461                ch,
462            };
463            let (metrics, bitmap) = {
464                let entry = self
465                    .glyph_cache
466                    .entry(key)
467                    .or_insert_with(|| rasterize_glyph(font_data, ch, px).unwrap());
468                (entry.0, entry.1.clone())
469            };
470            let w = metrics.width as i32;
471            let h = metrics.height as i32;
472            if w == 0 || h == 0 {
473                x_cursor += round_to_i32(metrics.advance_width);
474                continue;
475            }
476            let mut argb = vec![0u8; (w * h * 4) as usize];
477            for y in 0..h {
478                for x in 0..w {
479                    let alpha = bitmap[(y) as usize * metrics.width + x as usize];
480                    let idx = ((y * w + x) * 4) as usize;
481                    argb[idx] = (color.0 as u16 * alpha as u16 / 255) as u8;
482                    argb[idx + 1] = (color.1 as u16 * alpha as u16 / 255) as u8;
483                    argb[idx + 2] = (color.2 as u16 * alpha as u16 / 255) as u8;
484                    argb[idx + 3] = alpha;
485                }
486            }
487            let src = Surface::new(
488                argb.as_mut_slice(),
489                (w * 4) as usize,
490                PixelFmt::Argb8888,
491                w as u32,
492                h as u32,
493            );
494            let dst_pos = (
495                x_cursor + metrics.xmin,
496                baseline - ascent - metrics.ymin - (h - 1),
497            );
498            self.blitter.blend(
499                &src,
500                Rect {
501                    x: 0,
502                    y: 0,
503                    w: w as u32,
504                    h: h as u32,
505                },
506                &mut self.surface,
507                dst_pos,
508            );
509            self.planner.add(Rect {
510                x: dst_pos.0,
511                y: dst_pos.1,
512                w: w as u32,
513                h: h as u32,
514            });
515            x_cursor += round_to_i32(metrics.advance_width);
516        }
517    }
518
519    #[cfg(not(feature = "fontdue"))]
520    /// Stub text renderer when fontdue is disabled.
521    pub fn draw_text(
522        &mut self,
523        position: (i32, i32),
524        text: &str,
525        color: Color,
526        _font_data: &[u8],
527        _px: f32,
528    ) {
529        let _ = (position, text, color);
530    }
531}
532
533impl<B: Blitter, const N: usize> Renderer for BlitterRenderer<'_, B, N> {
534    fn blend_rect(&mut self, rect: WidgetRect, color: Color) {
535        let alpha = color.3 as u16;
536        if alpha == 0 {
537            return;
538        }
539        if alpha == 255 {
540            self.fill_rect(rect, color);
541            return;
542        }
543        // Inline source-over blending for ARGB8888 surfaces.
544        if self.surface.format == PixelFmt::Argb8888 {
545            let sw = self.surface.width as i32;
546            let sh = self.surface.height as i32;
547            let stride = self.surface.stride;
548            let inv = 255 - alpha;
549            let x0 = rect.x.max(0);
550            let y0 = rect.y.max(0);
551            let x1 = (rect.x + rect.width).min(sw);
552            let y1 = (rect.y + rect.height).min(sh);
553            for y in y0..y1 {
554                for x in x0..x1 {
555                    let off = y as usize * stride + x as usize * 4;
556                    let bg_b = self.surface.buf[off] as u16;
557                    let bg_g = self.surface.buf[off + 1] as u16;
558                    let bg_r = self.surface.buf[off + 2] as u16;
559                    self.surface.buf[off] = ((color.2 as u16 * alpha + bg_b * inv) / 255) as u8;
560                    self.surface.buf[off + 1] = ((color.1 as u16 * alpha + bg_g * inv) / 255) as u8;
561                    self.surface.buf[off + 2] = ((color.0 as u16 * alpha + bg_r * inv) / 255) as u8;
562                    self.surface.buf[off + 3] = 0xff;
563                }
564            }
565            self.planner.add(Rect {
566                x: rect.x,
567                y: rect.y,
568                w: rect.width as u32,
569                h: rect.height as u32,
570            });
571        } else {
572            // Fallback for non-ARGB8888 surfaces: just overwrite.
573            self.fill_rect(rect, color);
574        }
575    }
576
577    fn fill_rect(&mut self, rect: WidgetRect, color: Color) {
578        let r = Rect {
579            x: rect.x,
580            y: rect.y,
581            w: rect.width as u32,
582            h: rect.height as u32,
583        };
584        self.planner.add(r);
585        self.blitter.fill(&mut self.surface, r, color.to_argb8888());
586    }
587
588    fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
589        #[cfg(feature = "fontdue")]
590        {
591            const PX: f32 = 16.0;
592            BlitterRenderer::draw_text(self, position, text, color, FONT_DATA, PX);
593        }
594        #[cfg(not(feature = "fontdue"))]
595        {
596            let _ = (position, text, color);
597        }
598    }
599
600    fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
601        // Fast path: write ARGB8888 pixels directly into the surface buffer,
602        // then record the dirty rectangle. Avoids per-pixel fill_rect overhead.
603        if self.surface.format == PixelFmt::Argb8888 {
604            let sw = self.surface.width as i32;
605            let sh = self.surface.height as i32;
606            let stride = self.surface.stride;
607            for y in 0..height as i32 {
608                let dy = position.1 + y;
609                if dy < 0 || dy >= sh {
610                    continue;
611                }
612                for x in 0..width as i32 {
613                    let dx = position.0 + x;
614                    if dx < 0 || dx >= sw {
615                        continue;
616                    }
617                    let src_idx = (y as u32 * width + x as u32) as usize;
618                    if let Some(&c) = pixels.get(src_idx) {
619                        let off = dy as usize * stride + dx as usize * 4;
620                        self.surface.buf[off..off + 4]
621                            .copy_from_slice(&c.to_argb8888().to_le_bytes());
622                    }
623                }
624            }
625            self.planner.add(Rect {
626                x: position.0,
627                y: position.1,
628                w: width,
629                h: height,
630            });
631        } else {
632            // Fallback: per-pixel fill_rect for non-ARGB8888 surfaces
633            for y in 0..height as i32 {
634                for x in 0..width as i32 {
635                    let idx = (y as u32 * width + x as u32) as usize;
636                    if let Some(&c) = pixels.get(idx) {
637                        self.fill_rect(
638                            WidgetRect {
639                                x: position.0 + x,
640                                y: position.1 + y,
641                                width: 1,
642                                height: 1,
643                            },
644                            c,
645                        );
646                    }
647                }
648            }
649        }
650    }
651}
652
653/// Renderer wrapper that applies 90° CCW rotation for platforms where the
654/// physical display is landscape but the framebuffer is portrait.
655///
656/// Maps logical coordinates (800×480 landscape) to framebuffer coordinates
657/// (480×800 portrait):
658///   fb_x = fb_width - logical_y - logical_height
659///   fb_y = logical_x
660///   fb_w = logical_height
661///   fb_h = logical_width
662pub struct RotatedRenderer<'a> {
663    inner: &'a mut dyn Renderer,
664    /// Portrait framebuffer width (the short dimension, e.g. 480).
665    fb_width: i32,
666}
667
668impl<'a> RotatedRenderer<'a> {
669    /// Create a rotated renderer wrapping `inner`.
670    ///
671    /// `fb_width` is the portrait framebuffer width (480 on STM32H747I-DISCO).
672    pub fn new(inner: &'a mut dyn Renderer, fb_width: u32) -> Self {
673        Self {
674            inner,
675            fb_width: fb_width as i32,
676        }
677    }
678}
679
680impl Renderer for RotatedRenderer<'_> {
681    fn fill_rect(&mut self, rect: WidgetRect, color: Color) {
682        let mut fb_x = self.fb_width - rect.y - rect.height;
683        let fb_y = rect.x;
684        let mut fb_w = rect.height;
685        let fb_h = rect.width;
686
687        if fb_w <= 0 || fb_h <= 0 {
688            return;
689        }
690
691        // Clamp left edge: rect extends off-screen left
692        if fb_x < 0 {
693            fb_w += fb_x; // shrink width by the overshoot
694            fb_x = 0;
695        }
696        // Clamp right edge
697        if fb_x + fb_w > self.fb_width {
698            fb_w = self.fb_width - fb_x;
699        }
700        if fb_w <= 0 {
701            return;
702        }
703
704        self.inner.fill_rect(
705            WidgetRect {
706                x: fb_x,
707                y: fb_y,
708                width: fb_w,
709                height: fb_h,
710            },
711            color,
712        );
713    }
714
715    fn blend_rect(&mut self, rect: WidgetRect, color: Color) {
716        let mut fb_x = self.fb_width - rect.y - rect.height;
717        let fb_y = rect.x;
718        let mut fb_w = rect.height;
719        let fb_h = rect.width;
720
721        if fb_w <= 0 || fb_h <= 0 {
722            return;
723        }
724
725        if fb_x < 0 {
726            fb_w += fb_x;
727            fb_x = 0;
728        }
729        if fb_x + fb_w > self.fb_width {
730            fb_w = self.fb_width - fb_x;
731        }
732        if fb_w <= 0 {
733            return;
734        }
735
736        self.inner.blend_rect(
737            WidgetRect {
738                x: fb_x,
739                y: fb_y,
740                width: fb_w,
741                height: fb_h,
742            },
743            color,
744        );
745    }
746
747    fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
748        let fx = self.fb_width - 1 - position.1;
749        if fx >= 0 {
750            self.inner.draw_text((fx, position.0), text, color);
751        }
752    }
753
754    fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
755        for py in 0..height as i32 {
756            for px in 0..width as i32 {
757                let idx = (py as u32 * width + px as u32) as usize;
758                if let Some(&c) = pixels.get(idx) {
759                    self.fill_rect(
760                        WidgetRect {
761                            x: position.0 + px,
762                            y: position.1 + py,
763                            width: 1,
764                            height: 1,
765                        },
766                        c,
767                    );
768                }
769            }
770        }
771    }
772}
773
774#[cfg(test)]
775mod scratch_tests {
776    use super::*;
777    use crate::cpu_blitter::CpuBlitter;
778
779    #[test]
780    fn blit_colors_reuses_scratch_buffer() {
781        let mut buf = [0u8; 4 * 4 * 4];
782        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
783        let mut blit = CpuBlitter;
784        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
785            BlitterRenderer::new(&mut blit, surface);
786        let pixels = [Color(0, 0, 0, 0)];
787        renderer.blit_colors((0, 0), &pixels, 1, 1);
788        let first_ptr = renderer.scratch.as_ref().unwrap().as_ptr();
789        renderer.blit_colors((1, 1), &pixels, 1, 1);
790        let second_ptr = renderer.scratch.as_ref().unwrap().as_ptr();
791        assert_eq!(first_ptr, second_ptr);
792    }
793}
794
795#[cfg(all(test, feature = "fontdue"))]
796mod text_tests {
797    use super::*;
798    use crate::cpu_blitter::CpuBlitter;
799
800    #[test]
801    fn blitter_draws_text() {
802        let mut buf = [0u8; 64 * 64 * 4];
803        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
804        let mut blit = CpuBlitter;
805        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
806            BlitterRenderer::new(&mut blit, surface);
807        Renderer::draw_text(&mut renderer, (0, 32), "A", Color(255, 255, 255, 255));
808        assert!(buf.iter().any(|&p| p != 0));
809    }
810
811    #[test]
812    fn cache_accounts_for_size() {
813        let mut buf = [0u8; 64 * 64 * 4];
814        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
815        let mut blit = CpuBlitter;
816        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
817            BlitterRenderer::new(&mut blit, surface);
818        renderer.draw_text((0, 32), "Hi", Color(255, 255, 255, 255), FONT_DATA, 16.0);
819        let len_after_small = renderer.glyph_cache.len();
820        renderer.draw_text((0, 32), "Hi", Color(255, 255, 255, 255), FONT_DATA, 16.0);
821        assert_eq!(len_after_small, renderer.glyph_cache.len());
822        renderer.draw_text((0, 32), "Hi", Color(255, 255, 255, 255), FONT_DATA, 24.0);
823        assert!(renderer.glyph_cache.len() > len_after_small);
824    }
825}
826
827#[cfg(all(test, feature = "png", not(target_os = "none")))]
828mod png_tests {
829    use super::*;
830    use crate::cpu_blitter::CpuBlitter;
831    use base64::Engine;
832    use rlvgl_core::png::DecodingError;
833
834    const RED_DOT_PNG: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC";
835
836    #[test]
837    fn blitter_draws_png() {
838        let data = base64::engine::general_purpose::STANDARD
839            .decode(RED_DOT_PNG)
840            .unwrap();
841        let mut buf = [0u8; 4 * 4 * 4];
842        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
843        let mut blit = CpuBlitter;
844        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
845            BlitterRenderer::new(&mut blit, surface);
846        renderer.draw_png((0, 0), &data).unwrap();
847        assert!(buf.iter().any(|&p| p != 0));
848    }
849
850    #[test]
851    fn blitter_rejects_invalid_png() {
852        let mut buf = [0u8; 4 * 4 * 4];
853        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
854        let mut blit = CpuBlitter;
855        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
856            BlitterRenderer::new(&mut blit, surface);
857        let err = renderer.draw_png((0, 0), b"not a png").unwrap_err();
858        assert!(matches!(err, DecodingError::Format(_)));
859    }
860}
861
862#[cfg(all(test, feature = "jpeg", not(target_os = "none")))]
863mod jpeg_tests {
864    use super::*;
865    use crate::cpu_blitter::CpuBlitter;
866    use base64::Engine;
867    use rlvgl_core::jpeg::Error as JpegError;
868
869    const RED_DOT_JPEG: &str = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDi6KKK+ZP3E//Z";
870
871    #[test]
872    fn blitter_draws_jpeg() {
873        let data = base64::engine::general_purpose::STANDARD
874            .decode(RED_DOT_JPEG)
875            .unwrap();
876        let mut buf = [0u8; 4 * 4 * 4];
877        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
878        let mut blit = CpuBlitter;
879        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
880            BlitterRenderer::new(&mut blit, surface);
881        renderer.draw_jpeg((0, 0), &data).unwrap();
882        assert!(buf.iter().any(|&p| p != 0));
883    }
884
885    #[test]
886    fn blitter_rejects_invalid_jpeg() {
887        let mut buf = [0u8; 4 * 4 * 4];
888        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
889        let mut blit = CpuBlitter;
890        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
891            BlitterRenderer::new(&mut blit, surface);
892        let err = renderer.draw_jpeg((0, 0), b"not a jpeg").unwrap_err();
893        assert!(matches!(err, JpegError::Format(_)));
894    }
895}
896
897#[cfg(all(test, feature = "gif"))]
898mod gif_tests {
899    use super::*;
900    use crate::cpu_blitter::CpuBlitter;
901    use base64::Engine;
902    use rlvgl_core::gif::DecodingError as GifDecodingError;
903
904    const RED_DOT_GIF: &str = "R0lGODdhAQABAPAAAP8AAP///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
905
906    #[test]
907    fn blitter_draws_gif() {
908        let data = base64::engine::general_purpose::STANDARD
909            .decode(RED_DOT_GIF)
910            .unwrap();
911        let mut buf = [0u8; 4 * 4 * 4];
912        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
913        let mut blit = CpuBlitter;
914        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
915            BlitterRenderer::new(&mut blit, surface);
916        renderer.draw_gif_frame((0, 0), &data, 0).unwrap();
917        assert!(buf.iter().any(|&p| p != 0));
918    }
919
920    #[test]
921    fn blitter_rejects_invalid_gif() {
922        let mut buf = [0u8; 4 * 4 * 4];
923        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
924        let mut blit = CpuBlitter;
925        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
926            BlitterRenderer::new(&mut blit, surface);
927        let err = renderer
928            .draw_gif_frame((0, 0), b"not a gif", 0)
929            .unwrap_err();
930        assert!(matches!(err, GifDecodingError::Format(_)));
931    }
932}
933
934#[cfg(all(test, feature = "apng"))]
935mod apng_tests {
936    use super::*;
937    use crate::cpu_blitter::CpuBlitter;
938    use base64::Engine;
939
940    const RED_DOT_APNG: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjVEwAAAABAAAAALQt6aAAAAAaZmNUTAAAAAAAAAABAAAAAQAAAAAAAAAAAGQD6AEAqmVSjAAAAA1JREFUeJxj+M/A8B8ABQAB/4mZPR0AAAAASUVORK5CYII=";
941
942    #[test]
943    fn blitter_draws_apng() {
944        let data = base64::engine::general_purpose::STANDARD
945            .decode(RED_DOT_APNG)
946            .unwrap();
947        let mut buf = [0u8; 4 * 4 * 4];
948        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
949        let mut blit = CpuBlitter;
950        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
951            BlitterRenderer::new(&mut blit, surface);
952        renderer.draw_apng_frame((0, 0), &data, 0).unwrap();
953        assert!(buf.iter().any(|&p| p != 0));
954    }
955}
956
957#[cfg(all(test, feature = "canvas"))]
958mod canvas_tests {
959    use super::*;
960    use crate::cpu_blitter::CpuBlitter;
961    use embedded_graphics::prelude::Point;
962    use rlvgl_core::canvas::Canvas;
963
964    #[test]
965    fn blitter_draws_canvas() {
966        let mut canvas = Canvas::new(1, 1);
967        canvas.draw_pixel(Point::new(0, 0), Color(255, 0, 0, 255));
968        let mut buf = [0u8; 4];
969        let surface = Surface::new(&mut buf, 4, PixelFmt::Argb8888, 1, 1);
970        let mut blit = CpuBlitter;
971        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
972            BlitterRenderer::new(&mut blit, surface);
973        renderer.draw_canvas((0, 0), &canvas);
974        assert!(buf.iter().any(|&p| p != 0));
975    }
976}
977
978#[cfg(all(test, feature = "qrcode", not(target_os = "none")))]
979mod qrcode_tests {
980    use super::*;
981    use crate::cpu_blitter::CpuBlitter;
982    use rlvgl_core::qrcode::QrError;
983
984    #[test]
985    fn blitter_draws_qr() {
986        let mut buf = [0u8; 64 * 64 * 4];
987        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
988        let mut blit = CpuBlitter;
989        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
990            BlitterRenderer::new(&mut blit, surface);
991        renderer.draw_qr((0, 0), b"hi").unwrap();
992        assert!(buf.iter().any(|&p| p != 0));
993    }
994
995    #[test]
996    fn blitter_rejects_invalid_qr_data() {
997        let mut buf = [0u8; 64 * 64 * 4];
998        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
999        let mut blit = CpuBlitter;
1000        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1001            BlitterRenderer::new(&mut blit, surface);
1002        let data = vec![0u8; 3000];
1003        let err = renderer.draw_qr((0, 0), &data).unwrap_err();
1004        assert!(matches!(err, QrError::DataTooLong));
1005    }
1006}
1007
1008#[cfg(all(test, feature = "lottie"))]
1009mod lottie_tests {
1010    use super::*;
1011    use crate::cpu_blitter::CpuBlitter;
1012
1013    const SIMPLE_JSON: &str =
1014        "{\"v\":\"5.7\",\"fr\":30,\"ip\":0,\"op\":0,\"w\":1,\"h\":1,\"layers\":[]}";
1015
1016    #[test]
1017    fn blitter_draws_lottie() {
1018        let mut buf = [0u8; 4 * 4 * 4];
1019        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1020        let mut blit = CpuBlitter;
1021        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1022            BlitterRenderer::new(&mut blit, surface);
1023        renderer
1024            .draw_lottie_frame((0, 0), SIMPLE_JSON, 0, 1, 1)
1025            .unwrap();
1026        assert!(buf.iter().any(|&p| p != 0));
1027    }
1028
1029    #[test]
1030    fn blitter_rejects_invalid_lottie() {
1031        let mut buf = [0u8; 4 * 4 * 4];
1032        let surface = Surface::new(&mut buf, 4 * 4, PixelFmt::Argb8888, 4, 4);
1033        let mut blit = CpuBlitter;
1034        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1035            BlitterRenderer::new(&mut blit, surface);
1036        assert!(
1037            renderer
1038                .draw_lottie_frame((0, 0), "not json", 0, 1, 1)
1039                .is_err()
1040        );
1041    }
1042}
1043
1044#[cfg(all(test, feature = "pinyin", feature = "fontdue"))]
1045mod pinyin_tests {
1046    use super::*;
1047    use crate::cpu_blitter::CpuBlitter;
1048    use rlvgl_core::pinyin::PinyinInputMethod;
1049
1050    #[test]
1051    fn blitter_draws_pinyin() {
1052        let mut buf = [0u8; 64 * 64 * 4];
1053        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1054        let mut blit = CpuBlitter;
1055        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1056            BlitterRenderer::new(&mut blit, surface);
1057        let ime = PinyinInputMethod;
1058        assert!(renderer.draw_pinyin_candidates((0, 0), &ime, "zhong", Color(255, 255, 255, 255)));
1059        assert!(buf.iter().any(|&p| p != 0));
1060    }
1061
1062    #[test]
1063    fn pinyin_candidates_clipped_to_surface() {
1064        let mut buf = [0u8; 32 * 16 * 4];
1065        let surface = Surface::new(&mut buf, 32 * 4, PixelFmt::Argb8888, 32, 16);
1066        let mut blit = CpuBlitter;
1067        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1068            BlitterRenderer::new(&mut blit, surface);
1069        let ime = PinyinInputMethod;
1070        assert!(renderer.draw_pinyin_candidates((0, 0), &ime, "zhong", Color(255, 255, 255, 255)));
1071
1072        let mut expected = [0u8; 32 * 16 * 4];
1073        let surface_e = Surface::new(&mut expected, 32 * 4, PixelFmt::Argb8888, 32, 16);
1074        let mut blit_e = CpuBlitter;
1075        let mut renderer_e: BlitterRenderer<'_, CpuBlitter, 4> =
1076            BlitterRenderer::new(&mut blit_e, surface_e);
1077        let chars = ime.candidates("zhong").unwrap();
1078        let text: alloc::string::String = chars.into_iter().collect();
1079        let clipped: alloc::string::String = text.chars().take(2).collect();
1080        Renderer::draw_text(&mut renderer_e, (0, 0), &clipped, Color(255, 255, 255, 255));
1081        assert_eq!(buf[..], expected[..]);
1082    }
1083}
1084
1085#[cfg(all(test, feature = "fatfs", feature = "fontdue"))]
1086mod fatfs_tests {
1087    use super::*;
1088    use crate::cpu_blitter::CpuBlitter;
1089    use fatfs::{FileSystem, FormatVolumeOptions, FsOptions};
1090    use fscommon::BufStream;
1091    use std::io::{Cursor, Seek, SeekFrom, Write};
1092
1093    #[test]
1094    fn blitter_draws_fatfs_listing() {
1095        let mut img = Cursor::new(vec![0u8; 1024 * 512]);
1096        fatfs::format_volume(&mut img, FormatVolumeOptions::new()).unwrap();
1097        img.seek(SeekFrom::Start(0)).unwrap();
1098        {
1099            let buf_stream = BufStream::new(&mut img);
1100            let fs = FileSystem::new(buf_stream, FsOptions::new()).unwrap();
1101            fs.root_dir()
1102                .create_file("foo.txt")
1103                .unwrap()
1104                .write_all(b"hi")
1105                .unwrap();
1106        }
1107        img.seek(SeekFrom::Start(0)).unwrap();
1108        let mut buf = [0u8; 64 * 64 * 4];
1109        let surface = Surface::new(&mut buf, 64 * 4, PixelFmt::Argb8888, 64, 64);
1110        let mut blit = CpuBlitter;
1111        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1112            BlitterRenderer::new(&mut blit, surface);
1113        renderer
1114            .draw_fatfs_dir((0, 0), &mut img, "/", Color(255, 255, 255, 255))
1115            .unwrap();
1116        assert!(buf.iter().any(|&p| p != 0));
1117    }
1118
1119    #[test]
1120    fn fatfs_listing_clipped_to_surface() {
1121        let mut img = Cursor::new(vec![0u8; 1024 * 512]);
1122        fatfs::format_volume(&mut img, FormatVolumeOptions::new()).unwrap();
1123        img.seek(SeekFrom::Start(0)).unwrap();
1124        {
1125            let buf_stream = BufStream::new(&mut img);
1126            let fs = FileSystem::new(buf_stream, FsOptions::new()).unwrap();
1127            fs.root_dir().create_file("first_long_name.txt").unwrap();
1128            fs.root_dir().create_file("second_long_name.txt").unwrap();
1129            fs.root_dir().create_file("third_long_name.txt").unwrap();
1130        }
1131        img.seek(SeekFrom::Start(0)).unwrap();
1132        let image_vec = img.get_ref().clone();
1133        let mut img_expected = Cursor::new(image_vec.clone());
1134        let mut img_actual = Cursor::new(image_vec);
1135
1136        let mut buf = [0u8; 32 * 32 * 4];
1137        let surface = Surface::new(&mut buf, 32 * 4, PixelFmt::Argb8888, 32, 32);
1138        let mut blit = CpuBlitter;
1139        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1140            BlitterRenderer::new(&mut blit, surface);
1141        renderer
1142            .draw_fatfs_dir((0, 0), &mut img_actual, "/", Color(255, 255, 255, 255))
1143            .unwrap();
1144
1145        let names = rlvgl_core::fatfs::list_dir(&mut img_expected, "/").unwrap();
1146        let mut expected = [0u8; 32 * 32 * 4];
1147        let surface_e = Surface::new(&mut expected, 32 * 4, PixelFmt::Argb8888, 32, 32);
1148        let mut blit_e = CpuBlitter;
1149        let mut renderer_e: BlitterRenderer<'_, CpuBlitter, 4> =
1150            BlitterRenderer::new(&mut blit_e, surface_e);
1151        for (i, name) in names.iter().take(2).enumerate() {
1152            let clipped: alloc::string::String = name.chars().take(2).collect();
1153            Renderer::draw_text(
1154                &mut renderer_e,
1155                (0, (i as i32) * 16),
1156                &clipped,
1157                Color(255, 255, 255, 255),
1158            );
1159        }
1160        assert_eq!(buf[..], expected[..]);
1161    }
1162}
1163
1164#[cfg(all(test, feature = "nes"))]
1165mod nes_tests {
1166    use super::*;
1167    use crate::cpu_blitter::CpuBlitter;
1168
1169    #[test]
1170    fn blitter_draws_nes_frame() {
1171        let pixels = [Color(255, 0, 0, 255)];
1172        let mut buf = [0u8; 4];
1173        let surface = Surface::new(&mut buf, 4, PixelFmt::Argb8888, 1, 1);
1174        let mut blit = CpuBlitter;
1175        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1176            BlitterRenderer::new(&mut blit, surface);
1177        renderer.draw_nes_frame((0, 0), &pixels, 1, 1);
1178        assert!(buf.iter().any(|&p| p != 0));
1179    }
1180
1181    #[test]
1182    fn blitter_draws_full_nes_frame() {
1183        let mut pixels = [Color(0, 0, 0, 255); 256 * 240];
1184        for y in 0..240 {
1185            for x in 0..256 {
1186                pixels[y * 256 + x] = Color(x as u8, y as u8, 0, 255);
1187            }
1188        }
1189        let mut buf = [0u8; 256 * 240 * 4];
1190        let surface = Surface::new(&mut buf, 256 * 4, PixelFmt::Argb8888, 256, 240);
1191        let mut blit = CpuBlitter;
1192        let mut renderer: BlitterRenderer<'_, CpuBlitter, 4> =
1193            BlitterRenderer::new(&mut blit, surface);
1194        renderer.draw_nes_frame((0, 0), &pixels, 256, 240);
1195        let x = 128usize;
1196        let y = 120usize;
1197        let idx = (y * 256 + x) * 4;
1198        let actual = u32::from_le_bytes(buf[idx..idx + 4].try_into().unwrap());
1199        let expected = Color(x as u8, y as u8, 0, 255).to_argb8888();
1200        assert_eq!(actual, expected);
1201    }
1202}