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