Skip to main content

embedded_3dgfx/
hud.rs

1use embedded_graphics_core::{
2    Pixel,
3    draw_target::DrawTarget,
4    geometry::{OriginDimensions, Point, Size},
5    pixelcolor::Rgb565,
6};
7use heapless::Vec;
8
9// ---------------------------------------------------------------------------
10// FramebufDrawTarget — thin DrawTarget wrapper over a flat Rgb565 slice.
11//
12// Eliminates the OffscreenBuffer / DirtyRect boilerplate that every embedded
13// demo otherwise needs to copy.  Construct with `FramebufDrawTarget::new` and
14// pass to any embedded-graphics draw call.
15// ---------------------------------------------------------------------------
16
17/// A lightweight [`DrawTarget`] backed by a flat `Rgb565` pixel buffer.
18///
19/// Construct once per frame and pass it to `embedded-graphics` text/shape
20/// renderers.  No heap allocation, no dirty-rect tracking overhead.
21///
22/// ```ignore
23/// let mut fb = FramebufDrawTarget::new(&mut framebuf, 240, 320);
24/// Text::new("HEALTH", Point::new(3, 262), style).draw(&mut fb).ok();
25/// ```
26pub struct FramebufDrawTarget<'a> {
27    pixels: &'a mut [Rgb565],
28    width: usize,
29    height: usize,
30}
31
32impl<'a> FramebufDrawTarget<'a> {
33    /// Create a new draw target backed by `pixels` of size `width × height`.
34    ///
35    /// `pixels` must have at least `width * height` elements; the constructor
36    /// does **not** panic — out-of-bounds pixel writes are silently dropped.
37    #[inline]
38    pub fn new(pixels: &'a mut [Rgb565], width: usize, height: usize) -> Self {
39        Self {
40            pixels,
41            width,
42            height,
43        }
44    }
45}
46
47impl OriginDimensions for FramebufDrawTarget<'_> {
48    fn size(&self) -> Size {
49        Size::new(self.width as u32, self.height as u32)
50    }
51}
52
53impl DrawTarget for FramebufDrawTarget<'_> {
54    type Color = Rgb565;
55    type Error = core::convert::Infallible;
56
57    #[inline]
58    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
59    where
60        I: IntoIterator<Item = Pixel<Self::Color>>,
61    {
62        for Pixel(coord, color) in pixels {
63            if coord.x >= 0
64                && coord.y >= 0
65                && (coord.x as usize) < self.width
66                && (coord.y as usize) < self.height
67            {
68                let idx = coord.y as usize * self.width + coord.x as usize;
69                if idx < self.pixels.len() {
70                    self.pixels[idx] = color;
71                }
72            }
73        }
74        Ok(())
75    }
76}
77
78// ---------------------------------------------------------------------------
79// format_u16_dec — no_std / no_alloc integer formatter.
80// ---------------------------------------------------------------------------
81
82/// Format a `u16` as a zero-padded decimal string into a fixed-size byte
83/// buffer, right-aligned with leading spaces.
84///
85/// Returns a `&str` slice over the used portion of `buf`.  `buf` must be at
86/// least as long as `digits`; the function fills the rightmost `digits` bytes.
87///
88/// # Example
89/// ```ignore
90/// let mut buf = [b' '; 4];
91/// let s = format_u16_dec(42, &mut buf, 3); // "042"
92/// ```
93pub fn format_u16_dec(mut value: u16, buf: &mut [u8], digits: usize) -> &str {
94    let start = buf.len().saturating_sub(digits);
95    for i in (start..buf.len()).rev() {
96        buf[i] = b'0' + (value % 10) as u8;
97        value /= 10;
98    }
99    core::str::from_utf8(&buf[start..]).unwrap_or("")
100}
101
102/// A single HUD element drawn as a 2D overlay after the 3D scene.
103#[derive(Clone, Copy, Debug)]
104pub enum HudElement {
105    /// Solid filled rectangle.
106    FillRect {
107        x: i32,
108        y: i32,
109        w: u32,
110        h: u32,
111        color: Rgb565,
112    },
113    /// Horizontal progress bar. `value` is clamped to \[0.0, 1.0\].
114    ProgressBar {
115        x: i32,
116        y: i32,
117        w: u32,
118        h: u32,
119        value: f32,
120        fg: Rgb565,
121        bg: Rgb565,
122    },
123    /// 1-pixel-wide outline border.
124    Border {
125        x: i32,
126        y: i32,
127        w: u32,
128        h: u32,
129        color: Rgb565,
130    },
131    /// Translucent filled rectangle with 8-bit alpha \[0, 255\].
132    TranslucentRect {
133        x: i32,
134        y: i32,
135        w: u32,
136        h: u32,
137        color: Rgb565,
138        alpha: u8,
139    },
140    /// Translucent 1-pixel-wide outline border with 8-bit alpha \[0, 255\].
141    TranslucentBorder {
142        x: i32,
143        y: i32,
144        w: u32,
145        h: u32,
146        color: Rgb565,
147        alpha: u8,
148    },
149}
150
151/// A 2D HUD overlay layer holding up to `N` elements.
152///
153/// Draw with [`HudLayer::draw`] after the 3D render pass to composite the HUD
154/// on top of the scene.
155///
156/// # Example
157/// ```ignore
158/// let mut hud = HudLayer::<8>::new();
159/// hud.push(HudElement::ProgressBar { x: 4, y: 4, w: 64, h: 6,
160///     value: health / 100.0, fg: Rgb565::RED, bg: Rgb565::new(8, 0, 0) }).ok();
161/// hud.draw(&mut display);
162/// ```
163pub struct HudLayer<const N: usize> {
164    elements: Vec<HudElement, N>,
165}
166
167impl<const N: usize> Default for HudLayer<N> {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl<const N: usize> HudLayer<N> {
174    pub const fn new() -> Self {
175        Self {
176            elements: Vec::new(),
177        }
178    }
179
180    /// Remove all elements.
181    pub fn clear(&mut self) {
182        self.elements.clear();
183    }
184
185    /// Add an element. Returns `Err(element)` if the layer is full.
186    pub fn push(&mut self, element: HudElement) -> Result<(), HudElement> {
187        self.elements.push(element)
188    }
189
190    /// Draw all elements onto `target` in insertion order.
191    pub fn draw<D>(&self, target: &mut D)
192    where
193        D: DrawTarget<Color = Rgb565>,
194    {
195        for elem in &self.elements {
196            match *elem {
197                HudElement::FillRect { x, y, w, h, color } => {
198                    let _ = target.draw_iter(
199                        rect_pixels(x, y, w, h).map(|(px, py)| Pixel(Point::new(px, py), color)),
200                    );
201                }
202                HudElement::TranslucentRect {
203                    x,
204                    y,
205                    w,
206                    h,
207                    color,
208                    alpha: _,
209                } => {
210                    let _ = target.draw_iter(
211                        rect_pixels(x, y, w, h).map(|(px, py)| Pixel(Point::new(px, py), color)),
212                    );
213                }
214                HudElement::ProgressBar {
215                    x,
216                    y,
217                    w,
218                    h,
219                    value,
220                    fg,
221                    bg,
222                } => {
223                    let filled = ((w as f32 * value.clamp(0.0, 1.0)) as u32).min(w);
224                    if filled > 0 {
225                        let _ = target.draw_iter(
226                            rect_pixels(x, y, filled, h)
227                                .map(|(px, py)| Pixel(Point::new(px, py), fg)),
228                        );
229                    }
230                    if filled < w {
231                        let _ = target.draw_iter(
232                            rect_pixels(x + filled as i32, y, w - filled, h)
233                                .map(|(px, py)| Pixel(Point::new(px, py), bg)),
234                        );
235                    }
236                }
237                HudElement::Border { x, y, w, h, color }
238                | HudElement::TranslucentBorder {
239                    x,
240                    y,
241                    w,
242                    h,
243                    color,
244                    alpha: _,
245                } => {
246                    let _ = target.draw_iter(
247                        border_pixels(x, y, w, h).map(|(px, py)| Pixel(Point::new(px, py), color)),
248                    );
249                }
250            }
251        }
252    }
253
254    /// Draw all elements with fast translucent alpha blending onto a read-capable framebuffer target.
255    #[cfg(feature = "aa")]
256    pub fn draw_blended<D>(&self, target: &mut D)
257    where
258        D: DrawTarget<Color = Rgb565> + crate::draw::ReadPixel,
259    {
260        for elem in &self.elements {
261            match *elem {
262                HudElement::TranslucentRect {
263                    x,
264                    y,
265                    w,
266                    h,
267                    color,
268                    alpha,
269                } => {
270                    for (px, py) in rect_pixels(x, y, w, h) {
271                        let pt = Point::new(px, py);
272                        let bg = target.read_pixel(pt);
273                        let blended = crate::draw::fast_blend_rgb565(bg, color, alpha);
274                        let _ = target.draw_iter([Pixel(pt, blended)]);
275                    }
276                }
277                HudElement::TranslucentBorder {
278                    x,
279                    y,
280                    w,
281                    h,
282                    color,
283                    alpha,
284                } => {
285                    for (px, py) in border_pixels(x, y, w, h) {
286                        let pt = Point::new(px, py);
287                        let bg = target.read_pixel(pt);
288                        let blended = crate::draw::fast_blend_rgb565(bg, color, alpha);
289                        let _ = target.draw_iter([Pixel(pt, blended)]);
290                    }
291                }
292                _ => {
293                    self.draw(target);
294                }
295            }
296        }
297    }
298}
299
300fn rect_pixels(x: i32, y: i32, w: u32, h: u32) -> impl Iterator<Item = (i32, i32)> {
301    (0..h as i32).flat_map(move |dy| (0..w as i32).map(move |dx| (x + dx, y + dy)))
302}
303
304fn border_pixels(x: i32, y: i32, w: u32, h: u32) -> impl Iterator<Item = (i32, i32)> {
305    let wi = w as i32;
306    let hi = h as i32;
307    (0..wi)
308        .map(move |dx| (x + dx, y))
309        .chain((0..wi).map(move |dx| (x + dx, y + hi - 1)))
310        .chain((1..hi - 1).map(move |dy| (x, y + dy)))
311        .chain((1..hi - 1).map(move |dy| (x + wi - 1, y + dy)))
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use embedded_graphics_core::pixelcolor::RgbColor;
318
319    struct MockTarget {
320        pixels: heapless::Vec<(i32, i32, Rgb565), 4096>,
321    }
322
323    impl MockTarget {
324        fn new() -> Self {
325            Self {
326                pixels: heapless::Vec::new(),
327            }
328        }
329    }
330
331    impl DrawTarget for MockTarget {
332        type Color = Rgb565;
333        type Error = core::convert::Infallible;
334
335        fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
336        where
337            I: IntoIterator<Item = Pixel<Self::Color>>,
338        {
339            for Pixel(p, c) in pixels {
340                let _ = self.pixels.push((p.x, p.y, c));
341            }
342            Ok(())
343        }
344    }
345
346    impl embedded_graphics_core::geometry::OriginDimensions for MockTarget {
347        fn size(&self) -> embedded_graphics_core::geometry::Size {
348            embedded_graphics_core::geometry::Size::new(320, 240)
349        }
350    }
351
352    #[test]
353    fn fill_rect_pixel_count() {
354        let mut hud = HudLayer::<4>::new();
355        hud.push(HudElement::FillRect {
356            x: 0,
357            y: 0,
358            w: 3,
359            h: 2,
360            color: Rgb565::RED,
361        })
362        .unwrap();
363        let mut target = MockTarget::new();
364        hud.draw(&mut target);
365        assert_eq!(target.pixels.len(), 6);
366    }
367
368    #[test]
369    fn progress_bar_full() {
370        let mut hud = HudLayer::<4>::new();
371        hud.push(HudElement::ProgressBar {
372            x: 0,
373            y: 0,
374            w: 10,
375            h: 2,
376            value: 1.0,
377            fg: Rgb565::GREEN,
378            bg: Rgb565::BLACK,
379        })
380        .unwrap();
381        let mut target = MockTarget::new();
382        hud.draw(&mut target);
383        assert_eq!(target.pixels.len(), 20);
384        assert!(target.pixels.iter().all(|&(_, _, c)| c == Rgb565::GREEN));
385    }
386
387    #[test]
388    fn progress_bar_empty() {
389        let mut hud = HudLayer::<4>::new();
390        hud.push(HudElement::ProgressBar {
391            x: 0,
392            y: 0,
393            w: 10,
394            h: 2,
395            value: 0.0,
396            fg: Rgb565::GREEN,
397            bg: Rgb565::BLACK,
398        })
399        .unwrap();
400        let mut target = MockTarget::new();
401        hud.draw(&mut target);
402        assert_eq!(target.pixels.len(), 20);
403        assert!(target.pixels.iter().all(|&(_, _, c)| c == Rgb565::BLACK));
404    }
405
406    #[test]
407    fn border_pixel_count() {
408        let mut hud = HudLayer::<4>::new();
409        hud.push(HudElement::Border {
410            x: 0,
411            y: 0,
412            w: 4,
413            h: 3,
414            color: Rgb565::WHITE,
415        })
416        .unwrap();
417        let mut target = MockTarget::new();
418        hud.draw(&mut target);
419        // perimeter of 4×3 = 2*(4+3) - 4 corners counted once = 10
420        assert_eq!(target.pixels.len(), 10);
421    }
422
423    #[test]
424    fn layer_full_returns_err() {
425        let mut hud = HudLayer::<1>::new();
426        hud.push(HudElement::FillRect {
427            x: 0,
428            y: 0,
429            w: 1,
430            h: 1,
431            color: Rgb565::RED,
432        })
433        .unwrap();
434        let result = hud.push(HudElement::FillRect {
435            x: 0,
436            y: 0,
437            w: 1,
438            h: 1,
439            color: Rgb565::RED,
440        });
441        assert!(result.is_err());
442    }
443
444    #[test]
445    fn clear_empties_layer() {
446        let mut hud = HudLayer::<4>::new();
447        hud.push(HudElement::FillRect {
448            x: 0,
449            y: 0,
450            w: 2,
451            h: 2,
452            color: Rgb565::RED,
453        })
454        .unwrap();
455        hud.clear();
456        let mut target = MockTarget::new();
457        hud.draw(&mut target);
458        assert_eq!(target.pixels.len(), 0);
459    }
460}