embedded-draw-target 0.1.1

Readback and windowed-present capability traits for embedded-graphics draw targets
Documentation
#![cfg_attr(not(test), no_std)]
#![deny(missing_docs)]
#![doc = include_str!("../README.md")]

use embedded_graphics_core::prelude::{DrawTarget, Point};
use embedded_graphics_core::primitives::Rectangle;

#[cfg(feature = "completion")]
pub mod completion;

/// Readback capability for a [`DrawTarget`].
///
/// `embedded-graphics-core` models displays as write-only sinks, which is the
/// right lowest common denominator for a bare SPI panel but rules out anything
/// that has to look at what is already on screen: true alpha compositing,
/// analytical anti-aliasing, screen transitions that cross-fade between two
/// frames, and read-modify-write effects generally.
///
/// Any target that keeps its pixels in RAM — a framebuffer, a scanline cache, a
/// simulator window — can implement this trait, and doing so makes it usable by
/// every crate in the ecosystem that needs readback, with one trait identity
/// instead of one per library.
///
/// # Out-of-bounds reads
///
/// `get_pixel` is infallible by design so it can sit in a rasterizer inner
/// loop. Implementations must not panic on out-of-bounds coordinates; return a
/// neutral value (typically `Default::default()`, i.e. black) instead. Callers
/// that need to distinguish "black" from "outside" should clip against
/// [`OriginDimensions`](embedded_graphics_core::geometry::OriginDimensions)
/// first.
pub trait PixelRead: DrawTarget {
    /// Returns the color currently stored at `point`.
    ///
    /// Returns a neutral color for coordinates outside the target's bounding
    /// box rather than panicking.
    fn get_pixel(&self, point: Point) -> Self::Color;
}

/// Partial-present capability for a [`DrawTarget`].
///
/// Display controllers with a column/row address window (SSD1357, ST7789,
/// ILI9341, ...) can accept a sub-rectangle of pixels and leave the rest of the
/// panel untouched. On a slow bus that is the difference between pushing a
/// whole frame and pushing the handful of rows that actually changed.
///
/// Implementors set the controller's address window; the subsequent pixel
/// writes are expected to fill `area` in row-major order.
pub trait WindowedDrawTarget: DrawTarget {
    /// Restricts subsequent pixel writes to `area`.
    fn set_window(&mut self, area: &Rectangle) -> Result<(), Self::Error>;
}

/// Change tracking for a [`DrawTarget`] that buffers pixels in RAM.
///
/// This is the producer side of [`WindowedDrawTarget`]: a buffer records what
/// was touched during a frame, and the present step asks for that region so it
/// can transmit only those pixels. Keeping the trait here means a GUI library,
/// a 3D rasterizer and an application can all draw into the same buffer and
/// contribute to one coalesced dirty region.
///
/// Implementations are free to over-report (a bounding box of the touched
/// pixels is the usual choice) but must never under-report.
pub trait DirtyTracking: DrawTarget {
    /// Returns the region touched since the last [`clear_dirty`](Self::clear_dirty),
    /// or `None` if nothing changed.
    fn dirty_area(&self) -> Option<Rectangle>;

    /// Marks the whole target as clean. Called after a successful present.
    fn clear_dirty(&mut self);

    /// Marks the whole target as dirty, forcing the next present to be a full
    /// frame. Useful after a controller reset or a mode change that leaves the
    /// panel's contents unknown.
    fn mark_all_dirty(&mut self);
}

#[cfg(feature = "framebuf")]
mod framebuf {
    use super::PixelRead;
    use embedded_graphics_core::prelude::{PixelColor, Point};
    use embedded_graphics_framebuf::{FrameBuf, backends::FrameBufferBackend};

    impl<C, B> PixelRead for FrameBuf<C, B>
    where
        C: PixelColor + Default,
        B: FrameBufferBackend<Color = C>,
    {
        #[inline]
        fn get_pixel(&self, point: Point) -> C {
            if point.x < 0
                || point.y < 0
                || point.x >= self.width() as i32
                || point.y >= self.height() as i32
            {
                return C::default();
            }
            self.get_color_at(point)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use embedded_graphics::pixelcolor::Rgb565;
    use embedded_graphics::prelude::*;
    use embedded_graphics::primitives::{PrimitiveStyle, Rectangle as EgRectangle};
    use embedded_graphics_framebuf::FrameBuf;

    #[test]
    fn framebuf_reads_back_what_embedded_graphics_drew() {
        let mut data = [Rgb565::BLACK; 8 * 4];
        let mut fb = FrameBuf::new(&mut data, 8, 4);

        EgRectangle::new(Point::new(2, 1), Size::new(3, 2))
            .into_styled(PrimitiveStyle::with_fill(Rgb565::RED))
            .draw(&mut fb)
            .unwrap();

        assert_eq!(fb.get_pixel(Point::new(2, 1)), Rgb565::RED);
        assert_eq!(fb.get_pixel(Point::new(4, 2)), Rgb565::RED);
        assert_eq!(fb.get_pixel(Point::new(5, 2)), Rgb565::BLACK);
    }

    #[test]
    fn out_of_bounds_reads_are_neutral_not_panics() {
        let mut data = [Rgb565::WHITE; 4 * 4];
        let fb = FrameBuf::new(&mut data, 4, 4);

        for p in [
            Point::new(-1, 0),
            Point::new(0, -1),
            Point::new(4, 0),
            Point::new(0, 4),
            Point::new(i32::MIN, i32::MAX),
        ] {
            assert_eq!(fb.get_pixel(p), Rgb565::BLACK);
        }
    }

    /// A generic consumer written against the capability traits compiles for
    /// any conforming target, which is the whole point of the crate.
    fn darken<T: PixelRead<Color = Rgb565>>(target: &mut T, at: Point) -> Result<(), T::Error> {
        let c = target.get_pixel(at);
        let half = Rgb565::new(c.r() / 2, c.g() / 2, c.b() / 2);
        target.draw_iter(core::iter::once(Pixel(at, half)))
    }

    #[test]
    fn generic_readback_consumer_works_on_framebuf() {
        let mut data = [Rgb565::WHITE; 2 * 2];
        let mut fb = FrameBuf::new(&mut data, 2, 2);

        darken(&mut fb, Point::new(1, 1)).unwrap();

        assert_eq!(fb.get_pixel(Point::new(0, 0)), Rgb565::WHITE);
        assert_eq!(
            fb.get_pixel(Point::new(1, 1)),
            Rgb565::new(Rgb565::MAX_R / 2, Rgb565::MAX_G / 2, Rgb565::MAX_B / 2)
        );
    }
}