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}