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#[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 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#[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
153pub trait DrawUnit<D: DrawTarget<Color = Rgb565>> {
155 fn can_handle(&self, task: &DrawTask) -> bool;
157
158 fn execute(&mut self, task: &DrawTask, target: &mut D) -> Result<(), D::Error>;
160}
161
162#[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
270pub 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}