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}