Skip to main content

embedded_gui/render/
band.rs

1use core::convert::Infallible;
2use embedded_graphics_core::{
3    Pixel,
4    draw_target::DrawTarget,
5    geometry::{OriginDimensions, Point, Size},
6    pixelcolor::{Rgb565, RgbColor},
7};
8
9use crate::{
10    geometry::Rect,
11    render::{
12        PixelRead, WindowedDrawTarget,
13        task::{DrawTaskQueue, SoftwareDrawUnit, dispatch_draw_tasks},
14    },
15};
16
17/// A fixed-capacity row-band buffer for partial rendering.
18/// Allows rendering large displays in narrow horizontal slices (e.g. 10-20 lines),
19/// requiring only a small fraction of the SRAM (e.g., 2–8 KB instead of 150 KB).
20#[derive(Clone, Debug)]
21pub struct PartialBandBuffer<const N: usize> {
22    buffer: [Rgb565; N],
23    width: usize,
24    height: usize,
25}
26
27impl<const N: usize> PartialBandBuffer<N> {
28    pub const fn new(width: usize, height: usize) -> Self {
29        Self {
30            buffer: [Rgb565::BLACK; N],
31            width,
32            height,
33        }
34    }
35
36    pub const fn width(&self) -> usize {
37        self.width
38    }
39
40    pub const fn height(&self) -> usize {
41        self.height
42    }
43
44    pub fn buffer(&self) -> &[Rgb565] {
45        &self.buffer[..self.width * self.height]
46    }
47
48    pub fn buffer_mut(&mut self) -> &mut [Rgb565] {
49        let len = self.width * self.height;
50        &mut self.buffer[..len]
51    }
52
53    pub fn clear_color(&mut self, color: Rgb565) {
54        let len = self.width * self.height;
55        self.buffer[..len].fill(color);
56    }
57
58    /// Renders draw tasks overlapping `dirty_rect` in horizontal slices of height `self.height`,
59    /// flushing each rendered band directly to a [`WindowedDrawTarget`].
60    pub fn render_tasks_banded<D, const CAP: usize>(
61        &mut self,
62        _viewport: Rect,
63        dirty_rect: Rect,
64        tasks: &DrawTaskQueue<'_, CAP>,
65        target: &mut D,
66        clear_color: Option<Rgb565>,
67    ) -> Result<(), D::Error>
68    where
69        D: WindowedDrawTarget<Color = Rgb565>,
70    {
71        if dirty_rect.w == 0 || dirty_rect.h == 0 {
72            return Ok(());
73        }
74
75        let mut y = dirty_rect.y;
76        let y_end = dirty_rect.y + dirty_rect.h as i32;
77
78        while y < y_end {
79            let band_h = ((y_end - y) as u32).min(self.height as u32);
80            let band_rect = Rect::new(dirty_rect.x, y, dirty_rect.w, band_h);
81
82            let len = self.width * self.height;
83            if let Some(c) = clear_color {
84                self.buffer[..len].fill(c);
85            } else {
86                self.buffer[..len].fill(Rgb565::BLACK);
87            }
88
89            // Render tasks that intersect this band
90            for task in tasks.as_slice() {
91                let tb = task.bounds();
92                let overlap = tb.intersection(band_rect);
93                if !overlap.is_empty() {
94                    // Create task clipped to band buffer local coordinates
95                    let mut local_target = BandTargetWrapper {
96                        parent: self,
97                        band_origin: Point::new(band_rect.x, band_rect.y),
98                    };
99                    let mut fallback = SoftwareDrawUnit;
100                    let mut queue = DrawTaskQueue::<1>::new();
101                    let _ = queue.push(*task);
102                    let mut units = [];
103                    let _ =
104                        dispatch_draw_tasks(&queue, &mut local_target, &mut units, &mut fallback);
105                }
106            }
107
108            // Set window on hardware controller and flush band slice
109            let eg_rect = embedded_graphics_core::primitives::Rectangle::new(
110                Point::new(band_rect.x, band_rect.y),
111                Size::new(band_rect.w, band_rect.h),
112            );
113            target.set_window(&eg_rect)?;
114
115            // Stream active pixels
116            let count = (band_rect.w * band_rect.h) as usize;
117            target.fill_contiguous(&eg_rect, self.buffer[..count].iter().copied())?;
118
119            y += band_h as i32;
120        }
121
122        Ok(())
123    }
124}
125
126impl<const N: usize> OriginDimensions for PartialBandBuffer<N> {
127    fn size(&self) -> Size {
128        Size::new(self.width as u32, self.height as u32)
129    }
130}
131
132impl<const N: usize> DrawTarget for PartialBandBuffer<N> {
133    type Color = Rgb565;
134    type Error = Infallible;
135
136    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
137    where
138        I: IntoIterator<Item = Pixel<Self::Color>>,
139    {
140        for Pixel(coord, color) in pixels {
141            if coord.x >= 0
142                && coord.y >= 0
143                && (coord.x as usize) < self.width
144                && (coord.y as usize) < self.height
145            {
146                let idx = (coord.y as usize) * self.width + (coord.x as usize);
147                if idx < N {
148                    self.buffer[idx] = color;
149                }
150            }
151        }
152        Ok(())
153    }
154
155    fn fill_solid(
156        &mut self,
157        area: &embedded_graphics_core::primitives::Rectangle,
158        color: Self::Color,
159    ) -> Result<(), Self::Error> {
160        let intersect = area.intersection(&embedded_graphics_core::primitives::Rectangle::new(
161            Point::zero(),
162            self.size(),
163        ));
164        if intersect.is_zero_sized() {
165            return Ok(());
166        }
167        let x0 = intersect.top_left.x as usize;
168        let y0 = intersect.top_left.y as usize;
169        let w = intersect.size.width as usize;
170        let h = intersect.size.height as usize;
171        let stride = self.width;
172
173        for row in 0..h {
174            let start = (y0 + row) * stride + x0;
175            if start + w <= N {
176                self.buffer[start..start + w].fill(color);
177            }
178        }
179        Ok(())
180    }
181
182    fn fill_contiguous<I>(
183        &mut self,
184        area: &embedded_graphics_core::primitives::Rectangle,
185        colors: I,
186    ) -> Result<(), Self::Error>
187    where
188        I: IntoIterator<Item = Self::Color>,
189    {
190        let mut colors = colors.into_iter();
191        let stride = self.width;
192        let x_end = area.top_left.x + area.size.width as i32;
193        let y_end = area.top_left.y + area.size.height as i32;
194
195        for y in area.top_left.y..y_end {
196            for x in area.top_left.x..x_end {
197                if let Some(c) = colors.next() {
198                    if x >= 0 && y >= 0 && (x as usize) < self.width && (y as usize) < self.height {
199                        let idx = (y as usize) * stride + (x as usize);
200                        if idx < N {
201                            self.buffer[idx] = c;
202                        }
203                    }
204                } else {
205                    return Ok(());
206                }
207            }
208        }
209        Ok(())
210    }
211
212    fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
213        self.clear_color(color);
214        Ok(())
215    }
216}
217
218impl<const N: usize> PixelRead for PartialBandBuffer<N> {
219    fn get_pixel(&self, point: Point) -> Self::Color {
220        if point.x >= 0
221            && point.y >= 0
222            && (point.x as usize) < self.width
223            && (point.y as usize) < self.height
224        {
225            let idx = (point.y as usize) * self.width + (point.x as usize);
226            if idx < N {
227                return self.buffer[idx];
228            }
229        }
230        Rgb565::BLACK
231    }
232}
233
234/// Helper wrapper that translates global coordinates into local band buffer coordinates.
235struct BandTargetWrapper<'a, const N: usize> {
236    parent: &'a mut PartialBandBuffer<N>,
237    band_origin: Point,
238}
239
240impl<'a, const N: usize> OriginDimensions for BandTargetWrapper<'a, N> {
241    fn size(&self) -> Size {
242        self.parent.size()
243    }
244}
245
246impl<'a, const N: usize> DrawTarget for BandTargetWrapper<'a, N> {
247    type Color = Rgb565;
248    type Error = Infallible;
249
250    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
251    where
252        I: IntoIterator<Item = Pixel<Self::Color>>,
253    {
254        let ox = self.band_origin.x;
255        let oy = self.band_origin.y;
256        let w = self.parent.width;
257        let h = self.parent.height;
258        for Pixel(coord, color) in pixels {
259            let lx = coord.x - ox;
260            let ly = coord.y - oy;
261            if lx >= 0 && ly >= 0 && (lx as usize) < w && (ly as usize) < h {
262                let idx = (ly as usize) * w + (lx as usize);
263                if idx < N {
264                    self.parent.buffer[idx] = color;
265                }
266            }
267        }
268        Ok(())
269    }
270
271    fn fill_solid(
272        &mut self,
273        area: &embedded_graphics_core::primitives::Rectangle,
274        color: Self::Color,
275    ) -> Result<(), Self::Error> {
276        let ox = self.band_origin.x;
277        let oy = self.band_origin.y;
278        let local_rect = embedded_graphics_core::primitives::Rectangle::new(
279            Point::new(area.top_left.x - ox, area.top_left.y - oy),
280            area.size,
281        );
282        self.parent.fill_solid(&local_rect, color)
283    }
284
285    fn fill_contiguous<I>(
286        &mut self,
287        area: &embedded_graphics_core::primitives::Rectangle,
288        colors: I,
289    ) -> Result<(), Self::Error>
290    where
291        I: IntoIterator<Item = Self::Color>,
292    {
293        let ox = self.band_origin.x;
294        let oy = self.band_origin.y;
295        let local_rect = embedded_graphics_core::primitives::Rectangle::new(
296            Point::new(area.top_left.x - ox, area.top_left.y - oy),
297            area.size,
298        );
299        self.parent.fill_contiguous(&local_rect, colors)
300    }
301}
302
303impl<'a, const N: usize> PixelRead for BandTargetWrapper<'a, N> {
304    fn get_pixel(&self, point: Point) -> Rgb565 {
305        let lx = point.x - self.band_origin.x;
306        let ly = point.y - self.band_origin.y;
307        self.parent.get_pixel(Point::new(lx, ly))
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::render::task::DrawTask;
315    use embedded_graphics_core::primitives::Rectangle;
316
317    struct MockWindowedTarget {
318        pixels: [Rgb565; 400],
319        window: Option<Rectangle>,
320    }
321
322    impl OriginDimensions for MockWindowedTarget {
323        fn size(&self) -> Size {
324            Size::new(20, 20)
325        }
326    }
327
328    impl DrawTarget for MockWindowedTarget {
329        type Color = Rgb565;
330        type Error = Infallible;
331
332        fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
333        where
334            I: IntoIterator<Item = Pixel<Self::Color>>,
335        {
336            for Pixel(pt, color) in pixels {
337                if pt.x >= 0 && pt.y >= 0 && pt.x < 20 && pt.y < 20 {
338                    self.pixels[(pt.y * 20 + pt.x) as usize] = color;
339                }
340            }
341            Ok(())
342        }
343    }
344
345    impl WindowedDrawTarget for MockWindowedTarget {
346        fn set_window(&mut self, rect: &Rectangle) -> Result<(), Self::Error> {
347            self.window = Some(*rect);
348            Ok(())
349        }
350    }
351
352    #[test]
353    fn test_partial_band_buffer_render() {
354        let mut band = PartialBandBuffer::<100>::new(20, 5);
355        assert_eq!(band.width(), 20);
356        assert_eq!(band.height(), 5);
357
358        let mut queue = DrawTaskQueue::<2>::new();
359        queue
360            .push(DrawTask::Fill {
361                rect: Rect::new(0, 0, 20, 10),
362                color: Rgb565::GREEN,
363                radius: 0,
364                opacity: 255,
365            })
366            .unwrap();
367
368        let mut target = MockWindowedTarget {
369            pixels: [Rgb565::BLACK; 400],
370            window: None,
371        };
372
373        band.render_tasks_banded(
374            Rect::new(0, 0, 20, 20),
375            Rect::new(0, 0, 20, 10),
376            &queue,
377            &mut target,
378            Some(Rgb565::BLACK),
379        )
380        .unwrap();
381
382        assert_eq!(target.pixels[0], Rgb565::GREEN);
383        assert_eq!(target.pixels[20 * 9 + 5], Rgb565::GREEN);
384        assert_eq!(target.pixels[20 * 11 + 5], Rgb565::BLACK);
385    }
386}