Skip to main content

embedded_gui/
test_buffer.rs

1use core::convert::Infallible;
2
3use embedded_graphics_core::{
4    Pixel,
5    draw_target::DrawTarget,
6    geometry::{OriginDimensions, Size},
7    pixelcolor::{Rgb565, RgbColor},
8};
9
10use heapless::Vec;
11
12use crate::{geometry::Rect, present::PresentRegion, render::BlendMode};
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct TestBuffer {
16    size: Size,
17    pixels: std::vec::Vec<Rgb565>,
18}
19
20impl TestBuffer {
21    pub fn new(width: u32, height: u32) -> Self {
22        Self {
23            size: Size::new(width, height),
24            pixels: std::vec![Rgb565::BLACK; width.saturating_mul(height) as usize],
25        }
26    }
27
28    pub fn pixel_at(&self, x: i32, y: i32) -> Option<Rgb565> {
29        if x < 0 || y < 0 || x >= self.size.width as i32 || y >= self.size.height as i32 {
30            return None;
31        }
32        self.pixels
33            .get(y as usize * self.size.width as usize + x as usize)
34            .copied()
35    }
36
37    pub fn clear_color(&mut self, color: Rgb565) {
38        self.pixels.fill(color);
39    }
40
41    pub fn count_color(&self, color: Rgb565) -> usize {
42        self.pixels.iter().filter(|&&pixel| pixel == color).count()
43    }
44
45    pub fn has_non_background_pixel(&self) -> bool {
46        self.pixels.iter().any(|&pixel| pixel != Rgb565::BLACK)
47    }
48
49    pub fn assert_non_empty_rect(&self, rect: Rect) {
50        let clipped = rect.intersection(Rect::new(0, 0, self.size.width, self.size.height));
51        for y in clipped.y..clipped.bottom() {
52            for x in clipped.x..clipped.right() {
53                if self
54                    .pixel_at(x, y)
55                    .is_some_and(|pixel| pixel != Rgb565::BLACK)
56                {
57                    return;
58                }
59            }
60        }
61        panic!("expected non-background pixel in {:?}", rect);
62    }
63
64    pub fn digest(&self) -> u64 {
65        self.pixels.iter().enumerate().fold(0u64, |acc, (idx, &c)| {
66            acc.wrapping_mul(16_777_619)
67                ^ idx as u64
68                ^ ((c.r() as u64) << 32)
69                ^ ((c.g() as u64) << 40)
70                ^ ((c.b() as u64) << 48)
71        })
72    }
73
74    pub fn assert_digest_eq(&self, expected: u64, label: &str) {
75        let actual = self.digest();
76        assert_eq!(
77            actual, expected,
78            "visual digest mismatch for {}: expected {expected:#x}, got {actual:#x}",
79            label
80        );
81    }
82
83    pub fn diff_bounding_region(&self, previous: &Self) -> Option<PresentRegion> {
84        if self.size != previous.size {
85            return Some(PresentRegion::new(
86                0,
87                0,
88                self.size.width as usize,
89                self.size.height as usize,
90            ));
91        }
92
93        let mut bounds = Rect::empty();
94        for y in 0..self.size.height as i32 {
95            for x in 0..self.size.width as i32 {
96                if self.pixel_at(x, y) != previous.pixel_at(x, y) {
97                    let pixel = Rect::new(x, y, 1, 1);
98                    bounds = if bounds.is_empty() {
99                        pixel
100                    } else {
101                        bounds.union(pixel)
102                    };
103                }
104            }
105        }
106
107        (!bounds.is_empty()).then_some(bounds.into())
108    }
109
110    pub fn diff_regions<const N: usize>(&self, previous: &Self) -> Vec<PresentRegion, N> {
111        let mut regions = Vec::new();
112        let Some(bounds) = self.diff_bounding_region(previous) else {
113            return regions;
114        };
115
116        if self.size != previous.size {
117            let _ = regions.push(bounds);
118            return regions;
119        }
120
121        for y in 0..self.size.height as i32 {
122            let mut min_x = self.size.width as i32;
123            let mut max_x = -1;
124            for x in 0..self.size.width as i32 {
125                if self.pixel_at(x, y) != previous.pixel_at(x, y) {
126                    min_x = min_x.min(x);
127                    max_x = max_x.max(x);
128                }
129            }
130
131            if max_x >= min_x {
132                let row =
133                    PresentRegion::new(min_x as usize, y as usize, (max_x - min_x + 1) as usize, 1);
134                if regions.push(row).is_err() {
135                    regions.clear();
136                    let _ = regions.push(bounds);
137                    return regions;
138                }
139            }
140        }
141
142        regions
143    }
144
145    pub fn composite_from(&mut self, overlay: &Self, mode: BlendMode, opacity: u8) {
146        if self.size != overlay.size {
147            return;
148        }
149        if opacity == 0 {
150            return;
151        }
152        for (idx, src) in overlay.pixels.iter().copied().enumerate() {
153            if src == Rgb565::BLACK {
154                continue;
155            }
156            let dst = self.pixels[idx];
157            let blended = blend_pixel(src, dst, mode);
158            self.pixels[idx] = lerp_pixel(dst, blended, opacity);
159        }
160    }
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
164pub struct LayerCanvas {
165    inner: TestBuffer,
166}
167
168impl LayerCanvas {
169    pub fn new(width: u32, height: u32) -> Self {
170        Self {
171            inner: TestBuffer::new(width, height),
172        }
173    }
174
175    pub fn clear(&mut self, color: Rgb565) {
176        self.inner.clear_color(color);
177    }
178
179    pub fn target_mut(&mut self) -> &mut TestBuffer {
180        &mut self.inner
181    }
182
183    pub fn composite_into(&self, target: &mut TestBuffer, mode: BlendMode, opacity: u8) {
184        target.composite_from(&self.inner, mode, opacity);
185    }
186}
187
188fn lerp_pixel(a: Rgb565, b: Rgb565, t: u8) -> Rgb565 {
189    let t = t as u16;
190    let inv = 255u16.saturating_sub(t);
191    let r = ((a.r() as u16 * inv) + (b.r() as u16 * t)) / 255;
192    let g = ((a.g() as u16 * inv) + (b.g() as u16 * t)) / 255;
193    let bb = ((a.b() as u16 * inv) + (b.b() as u16 * t)) / 255;
194    Rgb565::new(r as u8, g as u8, bb as u8)
195}
196
197fn blend_pixel(src: Rgb565, dst: Rgb565, mode: BlendMode) -> Rgb565 {
198    match mode {
199        BlendMode::Normal => src,
200        BlendMode::Add => Rgb565::new(
201            src.r().saturating_add(dst.r()),
202            src.g().saturating_add(dst.g()),
203            src.b().saturating_add(dst.b()),
204        ),
205        BlendMode::Multiply => Rgb565::new(
206            ((src.r() as u16 * dst.r() as u16) / 31) as u8,
207            ((src.g() as u16 * dst.g() as u16) / 63) as u8,
208            ((src.b() as u16 * dst.b() as u16) / 31) as u8,
209        ),
210        BlendMode::Screen => Rgb565::new(
211            (31 - ((31 - src.r() as u16) * (31 - dst.r() as u16) / 31)) as u8,
212            (63 - ((63 - src.g() as u16) * (63 - dst.g() as u16) / 63)) as u8,
213            (31 - ((31 - src.b() as u16) * (31 - dst.b() as u16) / 31)) as u8,
214        ),
215    }
216}
217
218impl DrawTarget for TestBuffer {
219    type Color = Rgb565;
220    type Error = Infallible;
221
222    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
223    where
224        I: IntoIterator<Item = Pixel<Self::Color>>,
225    {
226        for Pixel(point, color) in pixels {
227            if point.x < 0
228                || point.y < 0
229                || point.x >= self.size.width as i32
230                || point.y >= self.size.height as i32
231            {
232                continue;
233            }
234            let idx = point.y as usize * self.size.width as usize + point.x as usize;
235            if let Some(pixel) = self.pixels.get_mut(idx) {
236                *pixel = color;
237            }
238        }
239        Ok(())
240    }
241}
242
243impl OriginDimensions for TestBuffer {
244    fn size(&self) -> Size {
245        self.size
246    }
247}