Skip to main content

core_s3/display/
sprite.rs

1use core::convert::Infallible;
2
3use embedded_graphics::{
4    Pixel,
5    draw_target::DrawTarget,
6    geometry::{OriginDimensions, Point, Size},
7    pixelcolor::PixelColor,
8    primitives::Rectangle,
9};
10use heapless::Vec;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum DirtySpriteError {
14    BufferTooSmall,
15    TooManyRegions,
16}
17
18/// Small fixed-capacity dirty rectangle set.
19pub struct RegionSet<const MAX_REGIONS: usize> {
20    regions: Vec<Rectangle, MAX_REGIONS>,
21}
22
23impl<const MAX_REGIONS: usize> RegionSet<MAX_REGIONS> {
24    pub const fn new() -> Self {
25        Self {
26            regions: Vec::new(),
27        }
28    }
29
30    pub fn clear(&mut self) {
31        self.regions.clear();
32    }
33
34    pub fn is_empty(&self) -> bool {
35        self.regions.is_empty()
36    }
37
38    pub fn iter(&self) -> impl Iterator<Item = Rectangle> + '_ {
39        self.regions.iter().copied()
40    }
41
42    pub fn add(&mut self, rect: Rectangle) -> Result<(), DirtySpriteError> {
43        if rect.is_zero_sized() {
44            return Ok(());
45        }
46
47        if let Some(existing) = self
48            .regions
49            .iter_mut()
50            .find(|existing| intersects_or_touches(**existing, rect))
51        {
52            *existing = bounding_rect(*existing, rect);
53            self.compact();
54            return Ok(());
55        }
56
57        self.regions
58            .push(rect)
59            .map_err(|_| DirtySpriteError::TooManyRegions)
60    }
61
62    fn compact(&mut self) {
63        let mut i = 0;
64        while i < self.regions.len() {
65            let mut j = i + 1;
66            while j < self.regions.len() {
67                if intersects_or_touches(self.regions[i], self.regions[j]) {
68                    let merged = bounding_rect(self.regions[i], self.regions[j]);
69                    self.regions[i] = merged;
70                    self.regions.swap_remove(j);
71                } else {
72                    j += 1;
73                }
74            }
75            i += 1;
76        }
77    }
78}
79
80impl<const MAX_REGIONS: usize> Default for RegionSet<MAX_REGIONS> {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86/// Off-screen framebuffer that tracks the rectangles touched by draw calls.
87///
88/// Use a full-screen sprite (`W=320`, `H=240`) when RAM is available, or create
89/// smaller sprites per widget. `N` must be at least `W * H`; it is separate from
90/// `W`/`H` to stay on stable Rust without generic-const arithmetic.
91pub struct DirtySprite<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize>
92where
93    C: PixelColor + Copy + Default,
94{
95    pixels: [C; N],
96    dirty: RegionSet<MAX_REGIONS>,
97}
98
99impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize>
100    DirtySprite<C, W, H, N, MAX_REGIONS>
101where
102    C: PixelColor + Copy + Default,
103{
104    pub fn new(clear: C) -> Result<Self, DirtySpriteError> {
105        if N < usize::from(W) * usize::from(H) {
106            return Err(DirtySpriteError::BufferTooSmall);
107        }
108
109        Ok(Self {
110            pixels: [clear; N],
111            dirty: RegionSet::new(),
112        })
113    }
114
115    pub fn dirty_regions(&self) -> impl Iterator<Item = Rectangle> + '_ {
116        self.dirty.iter()
117    }
118
119    pub fn clear_dirty(&mut self) {
120        self.dirty.clear();
121    }
122
123    pub fn pixel(&self, point: Point) -> Option<C> {
124        self.index(point).map(|idx| self.pixels[idx])
125    }
126
127    pub fn set_pixel(&mut self, point: Point, color: C) -> Result<(), DirtySpriteError> {
128        if let Some(idx) = self.index(point)
129            && self.pixels[idx] != color
130        {
131            self.pixels[idx] = color;
132            self.dirty.add(Rectangle::new(point, Size::new(1, 1)))?;
133        }
134        Ok(())
135    }
136
137    /// Repaint only dirty rectangles into a concrete display draw target.
138    pub fn flush_dirty<T>(&mut self, target: &mut T) -> Result<(), T::Error>
139    where
140        T: DrawTarget<Color = C>,
141    {
142        for rect in self.dirty.iter() {
143            let clipped = clip_to_bounds(rect, W, H);
144            target.draw_iter(
145                points_in(clipped).filter_map(|point| {
146                    self.index(point).map(|idx| Pixel(point, self.pixels[idx]))
147                }),
148            )?;
149        }
150        self.clear_dirty();
151        Ok(())
152    }
153
154    fn index(&self, point: Point) -> Option<usize> {
155        if point.x < 0 || point.y < 0 || point.x >= i32::from(W) || point.y >= i32::from(H) {
156            return None;
157        }
158
159        Some(point.y as usize * usize::from(W) + point.x as usize)
160    }
161}
162
163impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize> OriginDimensions
164    for DirtySprite<C, W, H, N, MAX_REGIONS>
165where
166    C: PixelColor + Copy + Default,
167{
168    fn size(&self) -> Size {
169        Size::new(u32::from(W), u32::from(H))
170    }
171}
172
173impl<C, const W: u16, const H: u16, const N: usize, const MAX_REGIONS: usize> DrawTarget
174    for DirtySprite<C, W, H, N, MAX_REGIONS>
175where
176    C: PixelColor + Copy + Default,
177{
178    type Color = C;
179    type Error = Infallible;
180
181    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
182    where
183        I: IntoIterator<Item = Pixel<Self::Color>>,
184    {
185        for Pixel(point, color) in pixels {
186            let _ = self.set_pixel(point, color);
187        }
188        Ok(())
189    }
190
191    fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
192        self.pixels[..usize::from(W) * usize::from(H)].fill(color);
193        let _ = self.dirty.add(Rectangle::new(Point::zero(), self.size()));
194        Ok(())
195    }
196}
197
198fn clip_to_bounds(rect: Rectangle, width: u16, height: u16) -> Rectangle {
199    let bounds = Rectangle::new(
200        Point::zero(),
201        Size::new(u32::from(width), u32::from(height)),
202    );
203    rect.intersection(&bounds)
204}
205
206fn points_in(rect: Rectangle) -> impl Iterator<Item = Point> {
207    let top_left = rect.top_left;
208    let bottom_right = rect.bottom_right().unwrap_or(top_left);
209    (top_left.y..=bottom_right.y)
210        .flat_map(move |y| (top_left.x..=bottom_right.x).map(move |x| Point::new(x, y)))
211}
212
213fn intersects_or_touches(a: Rectangle, b: Rectangle) -> bool {
214    let a_br = a.bottom_right().unwrap_or(a.top_left);
215    let b_br = b.bottom_right().unwrap_or(b.top_left);
216
217    a.top_left.x <= b_br.x + 1
218        && a_br.x + 1 >= b.top_left.x
219        && a.top_left.y <= b_br.y + 1
220        && a_br.y + 1 >= b.top_left.y
221}
222
223fn bounding_rect(a: Rectangle, b: Rectangle) -> Rectangle {
224    let a_br = a.bottom_right().unwrap_or(a.top_left);
225    let b_br = b.bottom_right().unwrap_or(b.top_left);
226    let min_x = a.top_left.x.min(b.top_left.x);
227    let min_y = a.top_left.y.min(b.top_left.y);
228    let max_x = a_br.x.max(b_br.x);
229    let max_y = a_br.y.max(b_br.y);
230
231    Rectangle::new(
232        Point::new(min_x, min_y),
233        Size::new((max_x - min_x + 1) as u32, (max_y - min_y + 1) as u32),
234    )
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use embedded_graphics::{pixelcolor::Rgb565, prelude::*, primitives::PrimitiveStyle};
241
242    #[test]
243    fn tracks_dirty_regions_for_drawn_shapes() {
244        let mut sprite = DirtySprite::<Rgb565, 8, 8, 64, 8>::new(Rgb565::BLACK).unwrap();
245        Rectangle::new(Point::new(1, 2), Size::new(3, 4))
246            .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE))
247            .draw(&mut sprite)
248            .unwrap();
249
250        let regions: std::vec::Vec<_> = sprite.dirty_regions().collect();
251        assert_eq!(
252            regions,
253            std::vec![Rectangle::new(Point::new(1, 2), Size::new(3, 4))]
254        );
255    }
256
257    #[test]
258    fn rejects_too_small_buffer() {
259        let err = match DirtySprite::<Rgb565, 8, 8, 63, 8>::new(Rgb565::BLACK) {
260            Ok(_) => panic!("expected buffer validation to fail"),
261            Err(err) => err,
262        };
263        assert_eq!(err, DirtySpriteError::BufferTooSmall);
264    }
265}