Skip to main content

embedded_gui/render/
task.rs

1use embedded_graphics_core::{draw_target::DrawTarget, geometry::Point, pixelcolor::Rgb565};
2use heapless::Vec;
3
4use crate::{
5    geometry::Rect,
6    image::{ImageFit, ImageRef},
7    render::{RenderCtx, TextStyle},
8    style::{Border, LinearGradient, Shadow},
9};
10
11/// Discrete atomic 2D drawing task generated during UI traversal.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub enum DrawTask<'a> {
14    Fill {
15        rect: Rect,
16        color: Rgb565,
17        radius: u8,
18        opacity: u8,
19    },
20    Gradient {
21        rect: Rect,
22        gradient: LinearGradient,
23        radius: u8,
24        opacity: u8,
25    },
26    Border {
27        rect: Rect,
28        border: Border,
29        radius: u8,
30    },
31    Label {
32        rect: Rect,
33        text: &'a str,
34        style: TextStyle,
35    },
36    Image {
37        rect: Rect,
38        image: ImageRef<'a>,
39        tint: Option<Rgb565>,
40    },
41    Arc {
42        center: Point,
43        radius: u16,
44        start_angle: i16,
45        end_angle: i16,
46        stroke_width: u8,
47        color: Rgb565,
48    },
49    Line {
50        start: Point,
51        end: Point,
52        color: Rgb565,
53        width: u8,
54    },
55    BoxShadow {
56        rect: Rect,
57        shadow: Shadow,
58        radius: u8,
59    },
60}
61
62impl<'a> DrawTask<'a> {
63    /// Returns the bounding box of the draw task.
64    pub fn bounds(&self) -> Rect {
65        match self {
66            DrawTask::Fill { rect, .. } => *rect,
67            DrawTask::Gradient { rect, .. } => *rect,
68            DrawTask::Border { rect, border, .. } => {
69                let w = border.width as i32;
70                Rect::new(
71                    rect.x - w,
72                    rect.y - w,
73                    rect.w + (w as u32 * 2),
74                    rect.h + (w as u32 * 2),
75                )
76            }
77            DrawTask::Label { rect, .. } => *rect,
78            DrawTask::Image { rect, .. } => *rect,
79            DrawTask::Arc {
80                center,
81                radius,
82                stroke_width,
83                ..
84            } => {
85                let r = *radius as i32 + (*stroke_width as i32 / 2) + 1;
86                Rect::new(center.x - r, center.y - r, (r * 2) as u32, (r * 2) as u32)
87            }
88            DrawTask::Line {
89                start, end, width, ..
90            } => {
91                let min_x = start.x.min(end.x) - (*width as i32 / 2);
92                let min_y = start.y.min(end.y) - (*width as i32 / 2);
93                let max_x = start.x.max(end.x) + (*width as i32 / 2);
94                let max_y = start.y.max(end.y) + (*width as i32 / 2);
95                Rect::new(
96                    min_x,
97                    min_y,
98                    (max_x - min_x).max(1) as u32,
99                    (max_y - min_y).max(1) as u32,
100                )
101            }
102            DrawTask::BoxShadow { rect, shadow, .. } => {
103                let s = shadow.spread as i32;
104                Rect::new(
105                    rect.x + shadow.offset_x as i32 - s,
106                    rect.y + shadow.offset_y as i32 - s,
107                    (rect.w as i32 + s * 2).max(1) as u32,
108                    (rect.h as i32 + s * 2).max(1) as u32,
109                )
110            }
111        }
112    }
113}
114
115/// A fixed-capacity queue of draw tasks (strictly zero-allocation).
116#[derive(Debug, Clone)]
117pub struct DrawTaskQueue<'a, const CAPACITY: usize> {
118    tasks: Vec<DrawTask<'a>, CAPACITY>,
119}
120
121impl<'a, const CAPACITY: usize> Default for DrawTaskQueue<'a, CAPACITY> {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl<'a, const CAPACITY: usize> DrawTaskQueue<'a, CAPACITY> {
128    pub const fn new() -> Self {
129        Self { tasks: Vec::new() }
130    }
131
132    pub fn push(&mut self, task: DrawTask<'a>) -> Result<(), DrawTask<'a>> {
133        self.tasks.push(task)
134    }
135
136    pub fn is_empty(&self) -> bool {
137        self.tasks.is_empty()
138    }
139
140    pub fn len(&self) -> usize {
141        self.tasks.len()
142    }
143
144    pub fn clear(&mut self) {
145        self.tasks.clear();
146    }
147
148    pub fn as_slice(&self) -> &[DrawTask<'a>] {
149        &self.tasks
150    }
151}
152
153/// A pluggable hardware or software rendering unit.
154pub trait DrawUnit<D: DrawTarget<Color = Rgb565>> {
155    /// Returns whether this draw unit can accelerate or handle the given task.
156    fn can_handle(&self, task: &DrawTask) -> bool;
157
158    /// Executes the task on the target buffer.
159    fn execute(&mut self, task: &DrawTask, target: &mut D) -> Result<(), D::Error>;
160}
161
162/// Default software draw unit implementing rasterization via [`RenderCtx`].
163#[derive(Debug, Default, Clone, Copy)]
164pub struct SoftwareDrawUnit;
165
166impl<D: DrawTarget<Color = Rgb565>> DrawUnit<D> for SoftwareDrawUnit {
167    fn can_handle(&self, _task: &DrawTask) -> bool {
168        true
169    }
170
171    fn execute(&mut self, task: &DrawTask, target: &mut D) -> Result<(), D::Error> {
172        let b = task.bounds();
173        let mut ctx = RenderCtx::new(target, b);
174        match task {
175            DrawTask::Fill {
176                rect,
177                color,
178                radius,
179                opacity,
180            } => {
181                if *opacity == 0 {
182                    return Ok(());
183                }
184                if *radius == 0 && *opacity == 255 {
185                    ctx.fill_rect(*rect, *color)?;
186                } else {
187                    ctx.fill_rounded_rect_alpha(*rect, *radius, *color, *opacity)?;
188                }
189            }
190            DrawTask::Gradient {
191                rect,
192                gradient,
193                radius,
194                opacity,
195            } => {
196                if *opacity == 0 {
197                    return Ok(());
198                }
199                ctx.fill_rounded_rect_gradient_alpha(*rect, *radius, *gradient, *opacity)?;
200            }
201            DrawTask::Border {
202                rect,
203                border,
204                radius,
205            } => {
206                if border.width > 0 {
207                    if *radius == 0 {
208                        ctx.stroke_rect(*rect, *border)?;
209                    } else {
210                        ctx.stroke_rounded_rect(*rect, *radius, *border)?;
211                    }
212                }
213            }
214            DrawTask::Label { rect, text, style } => {
215                ctx.draw_text_in(*rect, text, *style)?;
216            }
217            DrawTask::Image {
218                rect,
219                image,
220                tint: _,
221            } => {
222                ctx.draw_image(*rect, *image, ImageFit::Stretch)?;
223            }
224            DrawTask::Arc {
225                center,
226                radius,
227                start_angle,
228                end_angle,
229                stroke_width: _,
230                color,
231            } => {
232                ctx.stroke_arc(
233                    center.x,
234                    center.y,
235                    *radius as u32,
236                    *start_angle as i32,
237                    *end_angle as i32,
238                    *color,
239                )?;
240            }
241            DrawTask::Line {
242                start,
243                end,
244                color,
245                width: _,
246            } => {
247                ctx.draw_line(start.x, start.y, end.x, end.y, *color)?;
248            }
249            DrawTask::BoxShadow {
250                rect,
251                shadow,
252                radius: _,
253            } => {
254                if shadow.opacity > 0 {
255                    let s = shadow.spread as i32;
256                    let shadow_rect = Rect::new(
257                        rect.x + shadow.offset_x as i32 - s,
258                        rect.y + shadow.offset_y as i32 - s,
259                        (rect.w as i32 + s * 2).max(1) as u32,
260                        (rect.h as i32 + s * 2).max(1) as u32,
261                    );
262                    ctx.fill_rect_alpha(shadow_rect, shadow.color, shadow.opacity)?;
263                }
264            }
265        }
266        Ok(())
267    }
268}
269
270/// Dispatches a queue of draw tasks through a list of draw units with a fallback unit.
271pub fn dispatch_draw_tasks<D: DrawTarget<Color = Rgb565>, const CAP: usize>(
272    queue: &DrawTaskQueue<'_, CAP>,
273    target: &mut D,
274    units: &mut [&mut dyn DrawUnit<D>],
275    fallback: &mut SoftwareDrawUnit,
276) -> Result<(), D::Error> {
277    for task in queue.as_slice() {
278        let mut handled = false;
279        for unit in units.iter_mut() {
280            if unit.can_handle(task) {
281                unit.execute(task, target)?;
282                handled = true;
283                break;
284            }
285        }
286        if !handled {
287            fallback.execute(task, target)?;
288        }
289    }
290    Ok(())
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::framebuffer::Framebuffer;
297    use embedded_graphics_core::pixelcolor::RgbColor;
298
299    #[test]
300    fn test_draw_task_bounds() {
301        let fill = DrawTask::Fill {
302            rect: Rect::new(10, 20, 30, 40),
303            color: Rgb565::RED,
304            radius: 0,
305            opacity: 255,
306        };
307        assert_eq!(fill.bounds(), Rect::new(10, 20, 30, 40));
308
309        let border = DrawTask::Border {
310            rect: Rect::new(10, 20, 30, 40),
311            border: Border::one(Rgb565::WHITE),
312            radius: 0,
313        };
314        assert_eq!(border.bounds(), Rect::new(9, 19, 32, 42));
315    }
316
317    #[test]
318    fn test_draw_task_queue_operations() {
319        let mut queue = DrawTaskQueue::<4>::new();
320        assert!(queue.is_empty());
321        assert_eq!(queue.len(), 0);
322
323        let task1 = DrawTask::Fill {
324            rect: Rect::new(0, 0, 10, 10),
325            color: Rgb565::RED,
326            radius: 0,
327            opacity: 255,
328        };
329        let task2 = DrawTask::Fill {
330            rect: Rect::new(10, 10, 10, 10),
331            color: Rgb565::BLUE,
332            radius: 0,
333            opacity: 255,
334        };
335
336        assert!(queue.push(task1).is_ok());
337        assert!(queue.push(task2).is_ok());
338        assert_eq!(queue.len(), 2);
339        assert_eq!(queue.as_slice().len(), 2);
340
341        queue.clear();
342        assert!(queue.is_empty());
343    }
344
345    struct MockHwFillUnit {
346        fill_count: usize,
347    }
348
349    impl<D: DrawTarget<Color = Rgb565>> DrawUnit<D> for MockHwFillUnit {
350        fn can_handle(&self, task: &DrawTask) -> bool {
351            matches!(task, DrawTask::Fill { .. })
352        }
353
354        fn execute(&mut self, task: &DrawTask, target: &mut D) -> Result<(), D::Error> {
355            if let DrawTask::Fill { rect, color, .. } = task {
356                self.fill_count += 1;
357                let mut ctx = RenderCtx::new(target, *rect);
358                ctx.fill_rect(*rect, *color)?;
359            }
360            Ok(())
361        }
362    }
363
364    #[test]
365    fn test_draw_unit_dispatch() {
366        let mut fb = Framebuffer::<400>::new(20, 20);
367        let mut queue = DrawTaskQueue::<4>::new();
368
369        queue
370            .push(DrawTask::Fill {
371                rect: Rect::new(0, 0, 5, 5),
372                color: Rgb565::RED,
373                radius: 0,
374                opacity: 255,
375            })
376            .unwrap();
377
378        let mut hw_unit = MockHwFillUnit { fill_count: 0 };
379        let mut fallback = SoftwareDrawUnit;
380
381        let mut units: [&mut dyn DrawUnit<Framebuffer<400>>; 1] = [&mut hw_unit];
382        dispatch_draw_tasks(&queue, &mut fb, &mut units, &mut fallback).unwrap();
383
384        assert_eq!(hw_unit.fill_count, 1);
385        assert_eq!(fb.pixels()[0], Rgb565::RED);
386    }
387}