device-envoy-rp 0.1.1

Build Pico applications with LED panels, easy Wi-Fi, and composable device abstractions
Documentation
use core::convert::Infallible;

use device_envoy_core::cyd::display::RectanglePixels;
use embedded_graphics::{
    Pixel,
    pixelcolor::{IntoStorage, Rgb565},
    prelude::{DrawTarget, OriginDimensions, Size},
};
use static_cell::StaticCell;

/// A fixed `WIDTH`x`HEIGHT` RGB565 pixel buffer, usable directly as an
/// [`embedded_graphics::draw_target::DrawTarget`].
pub struct RegionBuffer<const WIDTH: usize, const HEIGHT: usize, const PIXEL_COUNT: usize> {
    pixels: [u16; PIXEL_COUNT],
}

/// A `PIXEL_COUNT`-sized RGB565 pixel workspace a [`super::CydRp`] can own and
/// hand out [`RegionView`]s from, sized smaller than the full screen for
/// tiled drawing.
pub struct PixelBuffer<const PIXEL_COUNT: usize> {
    pixels: [u16; PIXEL_COUNT],
}

/// A borrowed `width`x`height` view into a [`PixelBuffer`].
pub struct RegionView<'a> {
    width: usize,
    height: usize,
    pixels: &'a mut [u16],
}

impl<const WIDTH: usize, const HEIGHT: usize, const PIXEL_COUNT: usize>
    RegionBuffer<WIDTH, HEIGHT, PIXEL_COUNT>
{
    /// Create a zeroed buffer. Panics if `PIXEL_COUNT != WIDTH * HEIGHT`.
    #[must_use]
    pub fn new() -> Self {
        assert!(
            PIXEL_COUNT == WIDTH * HEIGHT,
            "PIXEL_COUNT must equal WIDTH * HEIGHT"
        );
        Self {
            pixels: [0; PIXEL_COUNT],
        }
    }

    /// Initialize this buffer into `'static` storage.
    pub fn init_static(
        storage: &'static StaticCell<Self>,
    ) -> &'static mut RegionBuffer<WIDTH, HEIGHT, PIXEL_COUNT> {
        storage.init_with(Self::new)
    }

    /// Fill every pixel with `color`.
    pub fn fill(&mut self, color: Rgb565) {
        self.pixels.fill(color.into_storage());
    }

    /// Borrow the raw RGB565 pixels, row-major.
    #[must_use]
    pub fn raw_pixels(&self) -> &[u16; PIXEL_COUNT] {
        &self.pixels
    }
}

impl<const WIDTH: usize, const HEIGHT: usize, const PIXEL_COUNT: usize> Default
    for RegionBuffer<WIDTH, HEIGHT, PIXEL_COUNT>
{
    fn default() -> Self {
        Self::new()
    }
}

impl<const WIDTH: usize, const HEIGHT: usize, const PIXEL_COUNT: usize> RectanglePixels
    for RegionBuffer<WIDTH, HEIGHT, PIXEL_COUNT>
{
    fn width(&self) -> usize {
        WIDTH
    }

    fn height(&self) -> usize {
        HEIGHT
    }

    fn raw_pixels(&self) -> &[u16] {
        &self.pixels
    }
}

impl<const WIDTH: usize, const HEIGHT: usize, const PIXEL_COUNT: usize> DrawTarget
    for RegionBuffer<WIDTH, HEIGHT, PIXEL_COUNT>
{
    type Color = Rgb565;
    type Error = Infallible;

    fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
        self.fill(color);
        Ok(())
    }

    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
    where
        I: IntoIterator<Item = Pixel<Self::Color>>,
    {
        for Pixel(point, color) in pixels {
            if point.x < 0 || point.y < 0 {
                continue;
            }
            let point_x = point.x as usize;
            let point_y = point.y as usize;
            if point_x >= WIDTH || point_y >= HEIGHT {
                continue;
            }
            self.pixels[point_y * WIDTH + point_x] = color.into_storage();
        }
        Ok(())
    }
}

impl<const PIXEL_COUNT: usize> PixelBuffer<PIXEL_COUNT> {
    /// Create a zeroed, `PIXEL_COUNT`-sized pixel workspace.
    #[must_use]
    pub fn new() -> Self {
        Self {
            pixels: [0; PIXEL_COUNT],
        }
    }

    /// Initialize this workspace into `'static` storage.
    pub fn init_static(
        storage: &'static StaticCell<Self>,
    ) -> &'static mut PixelBuffer<PIXEL_COUNT> {
        storage.init_with(Self::new)
    }

    /// Borrow a `width`x`height` view out of the workspace (must fit the capacity).
    pub fn view_mut(&mut self, width: usize, height: usize) -> RegionView<'_> {
        let pixel_count = width * height;
        assert!(pixel_count <= PIXEL_COUNT, "view must fit in workspace");
        RegionView {
            width,
            height,
            pixels: &mut self.pixels[..pixel_count],
        }
    }
}

impl<const PIXEL_COUNT: usize> Default for PixelBuffer<PIXEL_COUNT> {
    fn default() -> Self {
        Self::new()
    }
}

/// Draw buffer that a [`CydRp`](super::CydRp) can own and use to create
/// [`RegionView`]s. It works with any [`PixelBuffer<PIXEL_COUNT>`] size, while
/// the app chooses the capacity through `PIXEL_COUNT` on its
/// [`CydStaticRp`](super::CydStaticRp).
// TODO Consider replacing this trait-object capacity erasure with a concrete
// `PixelBufferView` over `&mut [u16]`, produced by `PixelBuffer::view`. Region
// borrowing can then live on the view without dynamic dispatch.
pub(crate) trait DynPixelBuffer: 'static {
    /// Borrow a `width`×`height` view out of the buffer (must fit the capacity).
    fn view_mut(&mut self, width: usize, height: usize) -> RegionView<'_>;
}

impl<const PIXEL_COUNT: usize> DynPixelBuffer for PixelBuffer<PIXEL_COUNT> {
    fn view_mut(&mut self, width: usize, height: usize) -> RegionView<'_> {
        PixelBuffer::view_mut(self, width, height)
    }
}

impl RegionView<'_> {
    /// Fill every pixel with `color`.
    pub fn fill(&mut self, color: Rgb565) {
        self.pixels.fill(color.into_storage());
    }

    /// Borrow the raw RGB565 pixels, row-major.
    pub fn raw_pixels_mut(&mut self) -> &mut [u16] {
        self.pixels
    }
}

impl RectanglePixels for RegionView<'_> {
    fn width(&self) -> usize {
        self.width
    }

    fn height(&self) -> usize {
        self.height
    }

    fn raw_pixels(&self) -> &[u16] {
        self.pixels
    }
}

impl DrawTarget for RegionView<'_> {
    type Color = Rgb565;
    type Error = Infallible;

    fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
        self.fill(color);
        Ok(())
    }

    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
    where
        I: IntoIterator<Item = Pixel<Self::Color>>,
    {
        for Pixel(point, color) in pixels {
            if point.x < 0 || point.y < 0 {
                continue;
            }
            let point_x = point.x as usize;
            let point_y = point.y as usize;
            if point_x >= self.width || point_y >= self.height {
                continue;
            }
            self.pixels[point_y * self.width + point_x] = color.into_storage();
        }
        Ok(())
    }
}

impl OriginDimensions for RegionView<'_> {
    fn size(&self) -> Size {
        Size::new(self.width as u32, self.height as u32)
    }
}

impl<const WIDTH: usize, const HEIGHT: usize, const PIXEL_COUNT: usize> OriginDimensions
    for RegionBuffer<WIDTH, HEIGHT, PIXEL_COUNT>
{
    fn size(&self) -> Size {
        Size::new(WIDTH as u32, HEIGHT as u32)
    }
}