Skip to main content

embedded_draw_target/
lib.rs

1#![cfg_attr(not(test), no_std)]
2#![deny(missing_docs)]
3#![doc = include_str!("../README.md")]
4
5use embedded_graphics_core::prelude::{DrawTarget, Point};
6use embedded_graphics_core::primitives::Rectangle;
7
8/// Readback capability for a [`DrawTarget`].
9///
10/// `embedded-graphics-core` models displays as write-only sinks, which is the
11/// right lowest common denominator for a bare SPI panel but rules out anything
12/// that has to look at what is already on screen: true alpha compositing,
13/// analytical anti-aliasing, screen transitions that cross-fade between two
14/// frames, and read-modify-write effects generally.
15///
16/// Any target that keeps its pixels in RAM — a framebuffer, a scanline cache, a
17/// simulator window — can implement this trait, and doing so makes it usable by
18/// every crate in the ecosystem that needs readback, with one trait identity
19/// instead of one per library.
20///
21/// # Out-of-bounds reads
22///
23/// `get_pixel` is infallible by design so it can sit in a rasterizer inner
24/// loop. Implementations must not panic on out-of-bounds coordinates; return a
25/// neutral value (typically `Default::default()`, i.e. black) instead. Callers
26/// that need to distinguish "black" from "outside" should clip against
27/// [`OriginDimensions`](embedded_graphics_core::geometry::OriginDimensions)
28/// first.
29pub trait PixelRead: DrawTarget {
30    /// Returns the color currently stored at `point`.
31    ///
32    /// Returns a neutral color for coordinates outside the target's bounding
33    /// box rather than panicking.
34    fn get_pixel(&self, point: Point) -> Self::Color;
35}
36
37/// Partial-present capability for a [`DrawTarget`].
38///
39/// Display controllers with a column/row address window (SSD1357, ST7789,
40/// ILI9341, ...) can accept a sub-rectangle of pixels and leave the rest of the
41/// panel untouched. On a slow bus that is the difference between pushing a
42/// whole frame and pushing the handful of rows that actually changed.
43///
44/// Implementors set the controller's address window; the subsequent pixel
45/// writes are expected to fill `area` in row-major order.
46pub trait WindowedDrawTarget: DrawTarget {
47    /// Restricts subsequent pixel writes to `area`.
48    fn set_window(&mut self, area: &Rectangle) -> Result<(), Self::Error>;
49}
50
51/// Change tracking for a [`DrawTarget`] that buffers pixels in RAM.
52///
53/// This is the producer side of [`WindowedDrawTarget`]: a buffer records what
54/// was touched during a frame, and the present step asks for that region so it
55/// can transmit only those pixels. Keeping the trait here means a GUI library,
56/// a 3D rasterizer and an application can all draw into the same buffer and
57/// contribute to one coalesced dirty region.
58///
59/// Implementations are free to over-report (a bounding box of the touched
60/// pixels is the usual choice) but must never under-report.
61pub trait DirtyTracking: DrawTarget {
62    /// Returns the region touched since the last [`clear_dirty`](Self::clear_dirty),
63    /// or `None` if nothing changed.
64    fn dirty_area(&self) -> Option<Rectangle>;
65
66    /// Marks the whole target as clean. Called after a successful present.
67    fn clear_dirty(&mut self);
68
69    /// Marks the whole target as dirty, forcing the next present to be a full
70    /// frame. Useful after a controller reset or a mode change that leaves the
71    /// panel's contents unknown.
72    fn mark_all_dirty(&mut self);
73}
74
75#[cfg(feature = "framebuf")]
76mod framebuf {
77    use super::PixelRead;
78    use embedded_graphics_core::prelude::{PixelColor, Point};
79    use embedded_graphics_framebuf::{FrameBuf, backends::FrameBufferBackend};
80
81    impl<C, B> PixelRead for FrameBuf<C, B>
82    where
83        C: PixelColor + Default,
84        B: FrameBufferBackend<Color = C>,
85    {
86        #[inline]
87        fn get_pixel(&self, point: Point) -> C {
88            if point.x < 0
89                || point.y < 0
90                || point.x >= self.width() as i32
91                || point.y >= self.height() as i32
92            {
93                return C::default();
94            }
95            self.get_color_at(point)
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use embedded_graphics::pixelcolor::Rgb565;
104    use embedded_graphics::prelude::*;
105    use embedded_graphics::primitives::{PrimitiveStyle, Rectangle as EgRectangle};
106    use embedded_graphics_framebuf::FrameBuf;
107
108    #[test]
109    fn framebuf_reads_back_what_embedded_graphics_drew() {
110        let mut data = [Rgb565::BLACK; 8 * 4];
111        let mut fb = FrameBuf::new(&mut data, 8, 4);
112
113        EgRectangle::new(Point::new(2, 1), Size::new(3, 2))
114            .into_styled(PrimitiveStyle::with_fill(Rgb565::RED))
115            .draw(&mut fb)
116            .unwrap();
117
118        assert_eq!(fb.get_pixel(Point::new(2, 1)), Rgb565::RED);
119        assert_eq!(fb.get_pixel(Point::new(4, 2)), Rgb565::RED);
120        assert_eq!(fb.get_pixel(Point::new(5, 2)), Rgb565::BLACK);
121    }
122
123    #[test]
124    fn out_of_bounds_reads_are_neutral_not_panics() {
125        let mut data = [Rgb565::WHITE; 4 * 4];
126        let fb = FrameBuf::new(&mut data, 4, 4);
127
128        for p in [
129            Point::new(-1, 0),
130            Point::new(0, -1),
131            Point::new(4, 0),
132            Point::new(0, 4),
133            Point::new(i32::MIN, i32::MAX),
134        ] {
135            assert_eq!(fb.get_pixel(p), Rgb565::BLACK);
136        }
137    }
138
139    /// A generic consumer written against the capability traits compiles for
140    /// any conforming target, which is the whole point of the crate.
141    fn darken<T: PixelRead<Color = Rgb565>>(target: &mut T, at: Point) -> Result<(), T::Error> {
142        let c = target.get_pixel(at);
143        let half = Rgb565::new(c.r() / 2, c.g() / 2, c.b() / 2);
144        target.draw_iter(core::iter::once(Pixel(at, half)))
145    }
146
147    #[test]
148    fn generic_readback_consumer_works_on_framebuf() {
149        let mut data = [Rgb565::WHITE; 2 * 2];
150        let mut fb = FrameBuf::new(&mut data, 2, 2);
151
152        darken(&mut fb, Point::new(1, 1)).unwrap();
153
154        assert_eq!(fb.get_pixel(Point::new(0, 0)), Rgb565::WHITE);
155        assert_eq!(
156            fb.get_pixel(Point::new(1, 1)),
157            Rgb565::new(Rgb565::MAX_R / 2, Rgb565::MAX_G / 2, Rgb565::MAX_B / 2)
158        );
159    }
160}