feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+211
View File
@@ -0,0 +1,211 @@
use crate::{
draw_target::DrawTarget, geometry::Dimensions, iterator::contiguous::Cropped,
primitives::Rectangle, transform::Transform, Pixel,
};
/// Clipped draw target.
///
/// Created by calling [`clipped`] on any [`DrawTarget`].
/// See the [`clipped`] method documentation for more.
///
/// [`clipped`]: crate::draw_target::DrawTargetExt::clipped
#[derive(Debug)]
pub struct Clipped<'a, T>
where
T: DrawTarget,
{
parent: &'a mut T,
clip_area: Rectangle,
}
impl<'a, T> Clipped<'a, T>
where
T: DrawTarget,
{
pub(super) fn new(parent: &'a mut T, clip_area: &Rectangle) -> Self {
let clip_area = clip_area.intersection(&parent.bounding_box());
Self { parent, clip_area }
}
}
impl<T> DrawTarget for Clipped<'_, T>
where
T: DrawTarget,
{
type Color = T::Color;
type Error = T::Error;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
let pixels = pixels
.into_iter()
.filter(|Pixel(p, _)| self.clip_area.contains(*p));
self.parent.draw_iter(pixels)
}
fn fill_contiguous<I>(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Self::Color>,
{
let intersection = self.bounding_box().intersection(area);
if &intersection == area {
self.parent.fill_contiguous(area, colors)
} else {
let crop_area = intersection.translate(-area.top_left);
let cropped = Cropped::new(colors.into_iter(), area.size, &crop_area);
self.parent.fill_contiguous(&intersection, cropped)
}
}
fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
let area = area.intersection(&self.clip_area);
self.parent.fill_solid(&area, color)
}
}
impl<T> Dimensions for Clipped<'_, T>
where
T: DrawTarget,
{
fn bounding_box(&self) -> Rectangle {
self.clip_area
}
}
#[cfg(test)]
mod tests {
use crate::{
draw_target::{DrawTarget, DrawTargetExt},
geometry::Dimensions,
geometry::{Point, Size},
mock_display::MockDisplay,
pixelcolor::BinaryColor,
primitives::{Primitive, PrimitiveStyle, Rectangle},
Drawable, Pixel,
};
#[test]
fn draw_iter() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(2, 1), Size::new(2, 4));
let mut clipped = display.clipped(&area);
let pixels = [
Pixel(Point::new(0, 1), BinaryColor::On),
Pixel(Point::new(1, 1), BinaryColor::On),
Pixel(Point::new(2, 1), BinaryColor::On),
Pixel(Point::new(3, 1), BinaryColor::On),
Pixel(Point::new(4, 1), BinaryColor::On),
Pixel(Point::new(2, 0), BinaryColor::Off),
Pixel(Point::new(2, 2), BinaryColor::Off),
Pixel(Point::new(2, 3), BinaryColor::Off),
Pixel(Point::new(2, 4), BinaryColor::Off),
Pixel(Point::new(2, 5), BinaryColor::Off),
];
clipped.draw_iter(pixels.iter().copied()).unwrap();
display.assert_pattern(&[
" ", //
" ##", //
" . ", //
" . ", //
" . ", //
]);
}
#[test]
fn fill_contiguous() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(3, 2), Size::new(2, 3));
let mut clipped = display.clipped(&area);
let colors = [
1, 1, 1, 1, 1, //
0, 0, 0, 0, 1, //
0, 1, 0, 1, 1, //
1, 0, 1, 0, 1, //
];
let area = Rectangle::new(Point::new(1, 2), Size::new(5, 4));
clipped
.fill_contiguous(&area, colors.iter().map(|c| BinaryColor::from(*c != 0)))
.unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ##", //
" ..", //
" .#", //
]);
}
#[test]
fn fill_solid() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(3, 2), Size::new(4, 2));
let mut clipped = display.clipped(&area);
let area = Rectangle::new(Point::new(2, 1), Size::new(6, 4));
clipped.fill_solid(&area, BinaryColor::On).unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ####", //
" ####", //
]);
}
#[test]
fn clear() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(1, 3), Size::new(3, 4));
let mut clipped = display.clipped(&area);
clipped.clear(BinaryColor::On).unwrap();
let mut expected = MockDisplay::new();
area.into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
.draw(&mut expected)
.unwrap();
display.assert_eq(&expected);
}
#[test]
fn bounding_box() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
let area = Rectangle::new(Point::new(1, 3), Size::new(2, 4));
let clipped = display.clipped(&area);
assert_eq!(clipped.bounding_box(), area);
}
#[test]
fn bounding_box_is_clipped() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
let display_bb = display.bounding_box();
let top_left = Point::new(10, 20);
let size = Size::new(1000, 1000);
let area = Rectangle::new(top_left, size);
let clipped = display.clipped(&area);
let expected_size = display_bb.size - Size::new(top_left.x as u32, top_left.y as u32);
assert_eq!(
clipped.bounding_box(),
Rectangle::new(top_left, expected_size),
);
}
}
@@ -0,0 +1,75 @@
use crate::{
draw_target::DrawTarget, geometry::Dimensions, pixelcolor::PixelColor, primitives::Rectangle,
Pixel,
};
use core::marker::PhantomData;
/// Color conversion draw target.
///
/// Created by calling [`color_converted`] on any [`DrawTarget`].
/// See the [`color_converted`] method documentation for more information.
///
/// [`color_converted`]: crate::draw_target::DrawTargetExt::color_converted
#[derive(Debug)]
pub struct ColorConverted<'a, T, C> {
/// The parent draw target.
parent: &'a mut T,
/// The input color type.
color_type: PhantomData<C>,
}
impl<'a, T, C> ColorConverted<'a, T, C>
where
T: DrawTarget,
C: PixelColor + Into<T::Color>,
{
pub(super) fn new(parent: &'a mut T) -> Self {
Self {
parent,
color_type: PhantomData,
}
}
}
impl<T, C> DrawTarget for ColorConverted<'_, T, C>
where
T: DrawTarget,
C: PixelColor + Into<T::Color>,
{
type Color = C;
type Error = T::Error;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
self.parent
.draw_iter(pixels.into_iter().map(|Pixel(p, c)| Pixel(p, c.into())))
}
fn fill_contiguous<I>(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Self::Color>,
{
self.parent
.fill_contiguous(area, colors.into_iter().map(|c| c.into()))
}
fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
self.parent.fill_solid(area, color.into())
}
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
self.parent.clear(color.into())
}
}
impl<T, C> Dimensions for ColorConverted<'_, T, C>
where
T: DrawTarget,
{
fn bounding_box(&self) -> Rectangle {
self.parent.bounding_box()
}
}
+203
View File
@@ -0,0 +1,203 @@
use crate::{
draw_target::{DrawTarget, DrawTargetExt, Translated},
geometry::{OriginDimensions, Size},
primitives::Rectangle,
Pixel,
};
/// Cropped draw target.
///
/// Created by calling [`cropped`] on any [`DrawTarget`].
/// See the [`cropped`] method documentation for more.
///
/// [`cropped`]: DrawTargetExt::cropped
#[derive(Debug)]
pub struct Cropped<'a, T>
where
T: DrawTarget,
{
parent: Translated<'a, T>,
size: Size,
}
impl<'a, T> Cropped<'a, T>
where
T: DrawTarget,
{
pub(super) fn new(parent: &'a mut T, area: &Rectangle) -> Self {
let area = area.intersection(&parent.bounding_box());
Self {
parent: parent.translated(area.top_left),
size: area.size,
}
}
}
impl<T> DrawTarget for Cropped<'_, T>
where
T: DrawTarget,
{
type Color = T::Color;
type Error = T::Error;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
self.parent.draw_iter(pixels)
}
fn fill_contiguous<I>(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Self::Color>,
{
self.parent.fill_contiguous(area, colors)
}
fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
self.parent.fill_solid(area, color)
}
}
impl<T> OriginDimensions for Cropped<'_, T>
where
T: DrawTarget,
{
fn size(&self) -> Size {
self.size
}
}
#[cfg(test)]
mod tests {
use crate::{
draw_target::{DrawTarget, DrawTargetExt},
geometry::Dimensions,
geometry::{Point, Size},
mock_display::MockDisplay,
pixelcolor::BinaryColor,
primitives::{Primitive, PrimitiveStyle, Rectangle},
Drawable, Pixel,
};
#[test]
fn draw_iter() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(2, 3), Size::new(10, 10));
let mut cropped = display.cropped(&area);
let pixels = [
Pixel(Point::new(0, 0), BinaryColor::On),
Pixel(Point::new(1, 2), BinaryColor::Off),
];
cropped.draw_iter(pixels.iter().copied()).unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ", //
" # ", //
" ", //
" .", //
]);
}
#[test]
fn fill_contiguous() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(3, 2), Size::new(10, 10));
let mut cropped = display.cropped(&area);
let colors = [
1, 1, 1, 1, 1, //
0, 0, 0, 0, 1, //
0, 1, 0, 1, 1, //
1, 0, 1, 0, 1, //
];
let area = Rectangle::new(Point::new(1, 2), Size::new(5, 4));
cropped
.fill_contiguous(&area, colors.iter().map(|c| BinaryColor::from(*c != 0)))
.unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ", //
" ", //
" #####", //
" ....#", //
" .#.##", //
" #.#.#", //
]);
}
#[test]
fn fill_solid() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(1, 3), Size::new(10, 10));
let mut cropped = display.cropped(&area);
let area = Rectangle::new(Point::new(2, 1), Size::new(3, 4));
cropped.fill_solid(&area, BinaryColor::On).unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ", //
" ", //
" ###", //
" ###", //
" ###", //
" ###", //
]);
}
#[test]
fn clear() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(1, 3), Size::new(3, 4));
let mut cropped = display.cropped(&area);
cropped.clear(BinaryColor::On).unwrap();
let mut expected = MockDisplay::new();
area.into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
.draw(&mut expected)
.unwrap();
display.assert_eq(&expected);
}
#[test]
fn bounding_box() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
let size = Size::new(3, 4);
let area = Rectangle::new(Point::new(1, 3), size);
let cropped = display.cropped(&area);
assert_eq!(cropped.bounding_box(), Rectangle::new(Point::zero(), size));
}
#[test]
fn bounding_box_is_clipped() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
let display_bb = display.bounding_box();
let top_left = Point::new(10, 20);
let size = Size::new(1000, 1000);
let area = Rectangle::new(top_left, size);
let cropped = display.cropped(&area);
let expected_size = display_bb.size - Size::new(top_left.x as u32, top_left.y as u32);
assert_eq!(
cropped.bounding_box(),
Rectangle::new(Point::zero(), expected_size),
);
}
}
+368
View File
@@ -0,0 +1,368 @@
//! A target for embedded-graphics drawing operations.
mod clipped;
mod color_converted;
mod cropped;
mod translated;
use crate::{geometry::Point, pixelcolor::PixelColor, primitives::Rectangle};
pub use clipped::Clipped;
pub use color_converted::ColorConverted;
pub use cropped::Cropped;
pub use translated::Translated;
pub use embedded_graphics_core::draw_target::DrawTarget;
/// Extension trait for `DrawTarget`s.
pub trait DrawTargetExt: DrawTarget + Sized {
/// Creates a translated draw target based on this draw target.
///
/// All drawing operations are translated by `offset` pixels, before being passed to the parent
/// draw target.
///
/// # Examples
///
/// ```
/// use embedded_graphics::{
/// mock_display::MockDisplay,
/// mono_font::{ascii::FONT_6X9, MonoTextStyle},
/// pixelcolor::BinaryColor,
/// prelude::*,
/// text::Text,
/// };
///
/// let mut display = MockDisplay::new();
/// let mut translated_display = display.translated(Point::new(5, 10));
///
/// let style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On);
///
/// // Draws text at position (5, 10) in the display coordinate system
/// Text::new("Text", Point::zero(), style).draw(&mut translated_display)?;
/// #
/// # let mut expected = MockDisplay::new();
/// #
/// # Text::new("Text", Point::new(5, 10), style).draw(&mut expected)?;
/// #
/// # display.assert_eq(&expected);
/// #
/// # Ok::<(), core::convert::Infallible>(())
/// ```
fn translated(&mut self, offset: Point) -> Translated<'_, Self>;
/// Creates a cropped draw target based on this draw target.
///
/// A cropped draw target is a draw target for a rectangular subregion of the parent draw target.
/// Its coordinate system is shifted so that the origin coincides with `area.top_left` in the
/// parent draw target's coordinate system.
///
/// The bounding box of the returned target will always be contained inside the bounding box
/// of the parent target. If any of the requested `area` lies outside the parent target's bounding
/// box the intersection of the parent target's bounding box and `area` will be used.
///
/// Drawing operations outside the bounding box will not be clipped.
///
/// # Examples
///
/// ```
/// use embedded_graphics::{
/// mock_display::MockDisplay,
/// mono_font::{ascii::FONT_6X9, MonoTextStyle},
/// pixelcolor::Rgb565,
/// prelude::*,
/// primitives::Rectangle,
/// text::{Text, Alignment, Baseline, TextStyleBuilder},
/// };
///
/// /// Fills a draw target with a blue background and prints centered yellow text.
/// fn draw_text<T>(target: &mut T, text: &str) -> Result<(), T::Error>
/// where
/// T: DrawTarget<Color = Rgb565>,
/// {
/// target.clear(Rgb565::BLUE)?;
///
/// let text_position = target.bounding_box().center();
///
/// let character_style = MonoTextStyle::new(&FONT_6X9, Rgb565::YELLOW);
/// let text_style = TextStyleBuilder::new()
/// .alignment(Alignment::Center)
/// .baseline(Baseline::Middle)
/// .build();
///
/// Text::with_text_style(text, text_position, character_style, text_style).draw(target)?;
///
/// Ok(())
/// }
///
/// let mut display = MockDisplay::new();
/// display.set_allow_overdraw(true);
///
/// let area = Rectangle::new(Point::new(5, 10), Size::new(40, 15));
/// let mut cropped_display = display.cropped(&area);
///
/// draw_text(&mut cropped_display, "Text")?;
/// #
/// # Ok::<(), core::convert::Infallible>(())
/// ```
fn cropped(&mut self, area: &Rectangle) -> Cropped<'_, Self>;
/// Creates a clipped draw target based on this draw target.
///
/// A clipped draw target is a draw target for a rectangular subregion of the parent draw target.
/// The coordinate system of the created draw target is equal to the parent target's coordinate
/// system. All drawing operations outside the bounding box will be clipped.
///
/// The bounding box of the returned target will always be contained inside the bounding box
/// of the parent target. If any of the requested `area` lies outside the parent target's bounding
/// box the intersection of the parent target's bounding box and `area` will be used.
///
/// # Examples
///
/// ```
/// use embedded_graphics::{
/// mock_display::MockDisplay,
/// mono_font::{ascii::FONT_10X20, MonoTextStyle},
/// pixelcolor::BinaryColor,
/// prelude::*,
/// primitives::Rectangle,
/// text::Text,
/// };
///
/// let mut display = MockDisplay::new();
///
/// let area = Rectangle::new(Point::zero(), Size::new(4 * 10, 20));
/// let mut clipped_display = display.clipped(&area);
///
/// let style = MonoTextStyle::new(&FONT_10X20, BinaryColor::On);
///
/// // Only the first 4 characters will be drawn, because the others are outside
/// // the clipping area
/// Text::new("Clipped", Point::new(0, 15), style).draw(&mut clipped_display)?;
/// #
/// # let mut expected = MockDisplay::new();
/// #
/// # Text::new("Clip", Point::new(0, 15), style).draw(&mut expected)?;
/// #
/// # display.assert_eq(&expected);
/// #
/// # Ok::<(), core::convert::Infallible>(())
/// ```
fn clipped(&mut self, area: &Rectangle) -> Clipped<'_, Self>;
/// Creates a color conversion draw target.
///
/// A color conversion draw target is used to draw drawables with a different color type to a
/// draw target. The drawable color type must implement `Into<C>`, where `C` is the draw
/// target color type.
///
/// # Performance
///
/// Color conversion can be expensive on embedded hardware and should be avoided if possible.
/// Using the same color type for drawables and the draw target makes sure that no unnecessary
/// color conversion is used. But in some cases color conversion will be required, for example,
/// to draw images with a color format only known at runtime.
///
/// # Examples
///
/// This example draws a `BinaryColor` image to an `Rgb888` display.
///
/// ```
/// use embedded_graphics::{
/// image::{Image, ImageRaw},
/// mock_display::MockDisplay,
/// pixelcolor::{BinaryColor, Rgb888},
/// prelude::*,
/// };
///
/// /// The image data.
/// const DATA: &[u8] = &[
/// 0b11110000, //
/// 0b10010000, //
/// 0b10010000, //
/// 0b11110000, //
/// ];
///
/// // Create a `BinaryColor` image from the image data.
/// let raw_image = ImageRaw::<BinaryColor>::new(DATA, 4);
/// let image = Image::new(&raw_image, Point::zero());
///
/// // Create a `Rgb888` display.
/// let mut display = MockDisplay::<Rgb888>::new();
///
/// // The image can't directly be drawn to the draw target because they use different
/// // color type. This will fail to compile:
/// // image.draw(&mut display)?;
///
/// // To fix this `color_converted` is added to enable color conversion.
/// image.draw(&mut display.color_converted())?;
/// #
/// # let mut expected = MockDisplay::from_pattern(&[
/// # "WWWW", //
/// # "WKKW", //
/// # "WKKW", //
/// # "WWWW", //
/// # ]);
/// #
/// # display.assert_eq(&expected);
/// #
/// # Ok::<(), core::convert::Infallible>(())
/// ```
fn color_converted<C>(&mut self) -> ColorConverted<'_, Self, C>
where
C: PixelColor + Into<Self::Color>;
}
impl<T> DrawTargetExt for T
where
T: DrawTarget,
{
fn translated(&mut self, offset: Point) -> Translated<'_, Self> {
Translated::new(self, offset)
}
fn cropped(&mut self, area: &Rectangle) -> Cropped<'_, Self> {
Cropped::new(self, area)
}
fn clipped(&mut self, area: &Rectangle) -> Clipped<'_, Self> {
Clipped::new(self, area)
}
fn color_converted<C>(&mut self) -> ColorConverted<'_, Self, C>
where
C: PixelColor + Into<Self::Color>,
{
ColorConverted::new(self)
}
}
#[cfg(test)]
mod tests {
use crate::{
draw_target::{DrawTarget, DrawTargetExt},
geometry::{Dimensions, Point, Size},
mock_display::MockDisplay,
pixelcolor::BinaryColor,
primitives::{Primitive, PrimitiveStyle, Rectangle},
Drawable, Pixel,
};
#[test]
fn draw_iter() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(2, 1), Size::new(2, 4));
let mut clipped = display.clipped(&area);
let pixels = [
Pixel(Point::new(0, 1), BinaryColor::On),
Pixel(Point::new(1, 1), BinaryColor::On),
Pixel(Point::new(2, 1), BinaryColor::On),
Pixel(Point::new(3, 1), BinaryColor::On),
Pixel(Point::new(4, 1), BinaryColor::On),
Pixel(Point::new(2, 0), BinaryColor::Off),
Pixel(Point::new(2, 2), BinaryColor::Off),
Pixel(Point::new(2, 3), BinaryColor::Off),
Pixel(Point::new(2, 4), BinaryColor::Off),
Pixel(Point::new(2, 5), BinaryColor::Off),
];
clipped.draw_iter(pixels.iter().copied()).unwrap();
display.assert_pattern(&[
" ", //
" ##", //
" . ", //
" . ", //
" . ", //
]);
}
#[test]
fn fill_contiguous() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(3, 2), Size::new(2, 3));
let mut clipped = display.clipped(&area);
let colors = [
1, 1, 1, 1, 1, //
0, 0, 0, 0, 1, //
0, 1, 0, 1, 1, //
1, 0, 1, 0, 1, //
];
let area = Rectangle::new(Point::new(1, 2), Size::new(5, 4));
clipped
.fill_contiguous(&area, colors.iter().map(|c| BinaryColor::from(*c != 0)))
.unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ##", //
" ..", //
" .#", //
]);
}
#[test]
fn fill_solid() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(3, 2), Size::new(4, 2));
let mut clipped = display.clipped(&area);
let area = Rectangle::new(Point::new(2, 1), Size::new(6, 4));
clipped.fill_solid(&area, BinaryColor::On).unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ####", //
" ####", //
]);
}
#[test]
fn clear() {
let mut display = MockDisplay::new();
let area = Rectangle::new(Point::new(1, 3), Size::new(3, 4));
let mut clipped = display.clipped(&area);
clipped.clear(BinaryColor::On).unwrap();
let mut expected = MockDisplay::new();
area.into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
.draw(&mut expected)
.unwrap();
display.assert_eq(&expected);
}
#[test]
fn bounding_box() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
let area = Rectangle::new(Point::new(1, 3), Size::new(2, 4));
let clipped = display.clipped(&area);
assert_eq!(clipped.bounding_box(), area);
}
#[test]
fn bounding_box_is_clipped() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
let display_bb = display.bounding_box();
let top_left = Point::new(10, 20);
let size = Size::new(1000, 1000);
let area = Rectangle::new(top_left, size);
let clipped = display.clipped(&area);
let expected_size = display_bb.size - Size::new(top_left.x as u32, top_left.y as u32);
assert_eq!(
clipped.bounding_box(),
Rectangle::new(top_left, expected_size),
);
}
}
+186
View File
@@ -0,0 +1,186 @@
use crate::{
draw_target::DrawTarget,
geometry::{Dimensions, Point},
iterator::PixelIteratorExt,
primitives::Rectangle,
transform::Transform,
Pixel,
};
/// Translated draw target.
///
/// Created by calling [`translated`] on any [`DrawTarget`].
/// See the [`translated`] method documentation for more.
///
/// [`translated`]: crate::draw_target::DrawTargetExt::translated
#[derive(Debug)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub struct Translated<'a, T>
where
T: DrawTarget,
{
parent: &'a mut T,
offset: Point,
}
impl<'a, T> Translated<'a, T>
where
T: DrawTarget,
{
pub(super) fn new(parent: &'a mut T, offset: Point) -> Self {
Self { parent, offset }
}
}
impl<T> DrawTarget for Translated<'_, T>
where
T: DrawTarget,
{
type Color = T::Color;
type Error = T::Error;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
self.parent
.draw_iter(pixels.into_iter().translated(self.offset))
}
fn fill_contiguous<I>(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Self::Color>,
{
let area = area.translate(self.offset);
self.parent.fill_contiguous(&area, colors)
}
fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
let area = area.translate(self.offset);
self.parent.fill_solid(&area, color)
}
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
self.parent.clear(color)
}
}
impl<T> Dimensions for Translated<'_, T>
where
T: DrawTarget,
{
fn bounding_box(&self) -> Rectangle {
self.parent.bounding_box().translate(-self.offset)
}
}
#[cfg(test)]
mod tests {
use crate::{
draw_target::{DrawTarget, DrawTargetExt},
geometry::Dimensions,
geometry::{Point, Size},
mock_display::MockDisplay,
pixelcolor::BinaryColor,
primitives::Rectangle,
transform::Transform,
Pixel,
};
#[test]
fn draw_iter() {
let mut display = MockDisplay::new();
let mut translated = display.translated(Point::new(2, 3));
let pixels = [
Pixel(Point::new(0, 0), BinaryColor::On),
Pixel(Point::new(1, 2), BinaryColor::Off),
];
translated.draw_iter(pixels.iter().copied()).unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ", //
" # ", //
" ", //
" .", //
]);
}
#[test]
fn fill_contiguous() {
let mut display = MockDisplay::new();
let mut translated = display.translated(Point::new(3, 2));
let colors = [
1, 1, 1, 1, 1, //
0, 0, 0, 0, 1, //
0, 1, 0, 1, 1, //
1, 0, 1, 0, 1, //
];
let area = Rectangle::new(Point::new(1, 2), Size::new(5, 4));
translated
.fill_contiguous(&area, colors.iter().map(|c| BinaryColor::from(*c != 0)))
.unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ", //
" ", //
" #####", //
" ....#", //
" .#.##", //
" #.#.#", //
]);
}
#[test]
fn fill_solid() {
let mut display = MockDisplay::new();
let mut translated = display.translated(Point::new(1, 3));
let area = Rectangle::new(Point::new(2, 1), Size::new(3, 4));
translated.fill_solid(&area, BinaryColor::On).unwrap();
display.assert_pattern(&[
" ", //
" ", //
" ", //
" ", //
" ###", //
" ###", //
" ###", //
" ###", //
]);
}
#[test]
fn clear() {
let mut display = MockDisplay::new();
let mut translated = display.translated(Point::new(1, 3));
translated.clear(BinaryColor::On).unwrap();
let mut expected = MockDisplay::new();
expected.clear(BinaryColor::On).unwrap();
display.assert_eq(&expected);
}
#[test]
fn bounding_box() {
let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
let display_bb = display.bounding_box();
let translated = display.translated(Point::new(1, 3));
assert_eq!(
display_bb.translate(-Point::new(1, 3)),
translated.bounding_box()
);
}
}