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