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

@@ -0,0 +1,50 @@
use crate::image::SubImage;
use embedded_graphics_core::{image::ImageDrawable, primitives::Rectangle};
/// Extension trait for image drawables.
pub trait ImageDrawableExt: Sized {
/// Returns a sub image of this image drawable.
///
/// If any of the given `area` lies outside the bounding box of the parent image, the
/// intersection of `area` and the bounding box will be used.
///
/// # Examples
///
/// This example loads a raw image containing multiple 16x16px sprites and draws two of them to
/// a display, with their top-left corners positioned at `(0, 0)` and `(32, 8)`.
///
/// ```rust
/// use embedded_graphics::{
/// image::{Image, ImageRaw, ImageRawBE},
/// pixelcolor::Rgb565,
/// prelude::*,
/// primitives::Rectangle,
/// };
/// # use embedded_graphics::mock_display::MockDisplay as Display;
/// # let mut display: Display<Rgb565> = Display::default();
///
/// let data = [ 0xF8, 0x00, 0x07, 0xE0, 0xFF, 0xE0, /* ... */ ];
/// // or: let data = include_bytes!("sprite_atlas.raw");
///
/// # let data = [0u8; 32 * 16 * 2];
/// let sprite_atlas = ImageRawBE::<Rgb565>::new(&data, 32);
///
/// let sprite_1 = sprite_atlas.sub_image(&Rectangle::new(Point::new(0, 0), Size::new(16, 16)));
/// let sprite_2 = sprite_atlas.sub_image(&Rectangle::new(Point::new(16, 0), Size::new(16, 16)));
///
/// Image::new(&sprite_1, Point::new(0, 0)).draw(&mut display)?;
/// Image::new(&sprite_2, Point::new(32, 8)).draw(&mut display)?;
///
/// # Ok::<(), core::convert::Infallible>(())
/// ```
fn sub_image(&self, area: &Rectangle) -> SubImage<Self>;
}
impl<T> ImageDrawableExt for T
where
T: ImageDrawable,
{
fn sub_image(&self, area: &Rectangle) -> SubImage<T> {
SubImage::new(self, area)
}
}
+703
View File
@@ -0,0 +1,703 @@
use core::marker::PhantomData;
use crate::{
draw_target::DrawTarget,
geometry::{Dimensions, OriginDimensions, Point, Size},
image::{GetPixel, ImageDrawable},
iterator::raw::RawDataSlice,
pixelcolor::{
raw::{BigEndian, ByteOrder, LittleEndian, RawData},
PixelColor,
},
primitives::Rectangle,
};
/// Image with little endian data.
pub type ImageRawLE<'a, C> = ImageRaw<'a, C, LittleEndian>;
/// Image with big endian data.
pub type ImageRawBE<'a, C> = ImageRaw<'a, C, BigEndian>;
/// An image constructed from a slice of raw pixel data.
///
/// The `ImageRaw` struct can be used to construct an image from a slice
/// of raw image data. The storage format is determined by the [`PixelColor`]
/// type `C` and the [`ByteOrder`] `BO`. The byteorder doesn't need to be
/// specified for colors which aren't stored in multiple bytes.
///
/// For color types with less than 8 bits per pixels the start of each row is
/// aligned to the next whole byte.
///
/// Details about the conversion of raw data to color types are explained in the
/// [`raw` module documentation].
///
/// To draw an `ImageRaw` object it needs to be wrapped in an [`Image`] object.
///
/// # Examples
///
/// ## Draw a 1BPP image
///
/// This example creates an image from 1 bit per pixel data.
///
/// ```
/// use embedded_graphics::{
/// image::{Image, ImageRaw},
/// pixelcolor::BinaryColor,
/// prelude::*,
/// };
/// # use embedded_graphics::mock_display::MockDisplay as Display;
///
/// /// 12 x 5 pixel image with 1 bit per pixel.
/// /// The data for each row is 12 bits long and is padded with zeros on the
/// /// end because each row needs to contain a whole number of bytes.
/// #[rustfmt::skip]
/// const DATA: &[u8] = &[
/// 0b11101111, 0b0101_0000,
/// 0b10001000, 0b0101_0000,
/// 0b11101011, 0b0101_0000,
/// 0b10001001, 0b0101_0000,
/// 0b11101111, 0b0101_0000,
/// ];
///
/// // The image dimensions and the format of the stored raw data must be specified
/// // when the `new` function is called. The data format can, for example, be specified
/// // by using the turbofish syntax. For the image dimensions only the width must be
/// // passed to the `new` function. The image height will be calculated based on the
/// // length of the image data and the data format.
/// let raw_image = ImageRaw::<BinaryColor>::new(DATA, 12);
///
/// let image = Image::new(&raw_image, Point::zero());
///
/// let mut display = Display::default();
///
/// image.draw(&mut display)?;
/// # Ok::<(), core::convert::Infallible>(())
/// ```
///
/// ## Draw an image that uses multibyte pixel encoding
///
/// Colors with more than one byte per pixel need an additional type annotation for the byte order.
/// For convenience, the [`ImageRawBE`] and [`ImageRawLE`] type aliases can be used to abbreviate
/// the type.
///
/// ```
/// use embedded_graphics::{
/// image::{Image, ImageRaw, ImageRawBE, ImageRawLE},
/// pixelcolor::{
/// raw::{BigEndian, LittleEndian},
/// Rgb565, Rgb888,
/// },
/// prelude::*,
/// };
/// # const DATA: &[u8] = &[0x55; 8 * 8 * 3];
///
/// // Rgb888 image with 24 bits per pixel and big endian byte order
/// let image1 = ImageRawBE::<Rgb888>::new(DATA, 8);
/// // or:
/// let image2 = ImageRaw::<Rgb888, BigEndian>::new(DATA, 8);
/// # assert_eq!(image1, image2);
///
/// // Rgb565 image with 16 bits per pixel and little endian byte order
/// let image1 = ImageRawLE::<Rgb565>::new(DATA, 16);
/// // or:
/// let image2 = ImageRaw::<Rgb565, LittleEndian>::new(DATA, 16);
/// # assert_eq!(image1, image2);
/// ```
///
/// [`raw` module documentation]: crate::pixelcolor::raw
/// [`Image`]: crate::image::Image
/// [`Drawable`]: crate::Drawable
/// [`PixelColor`]: crate::pixelcolor::PixelColor
/// [`ByteOrder`]: crate::pixelcolor::raw::ByteOrder
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub struct ImageRaw<'a, C, BO = BigEndian>
where
C: PixelColor + From<<C as PixelColor>::Raw>,
BO: ByteOrder,
{
/// Image data, packed as dictated by raw data type `C::Raw`
data: &'a [u8],
/// Image size in pixels
size: Size,
pixel_type: PhantomData<C>,
byte_order: PhantomData<BO>,
}
impl<'a, C, BO> ImageRaw<'a, C, BO>
where
C: PixelColor + From<<C as PixelColor>::Raw>,
BO: ByteOrder,
{
/// Creates a new image.
///
/// Only the width of the image needs to be specified. The height of the image will be
/// calculated based on the length of the given image data. If the length of the image data
/// isn't an integer multiple of the data length for a single row the last partial row will
/// be ignored.
pub const fn new(data: &'a [u8], width: u32) -> Self {
// Prevent panic for `width == 0` by returning a zero sized image.
if width == 0 {
return Self {
data: &[],
size: Size::zero(),
pixel_type: PhantomData,
byte_order: PhantomData,
};
}
let height = data.len() / bytes_per_row(width, C::Raw::BITS_PER_PIXEL);
Self {
data,
size: Size::new(width, height as u32),
pixel_type: PhantomData,
byte_order: PhantomData,
}
}
/// Returns the actual row width in pixels.
///
/// For images with less than 8 bits per pixel each row is padded to contain an integer number
/// of bytes. This method returns the width of each row including the padding pixels.
const fn data_width(&self) -> u32 {
if C::Raw::BITS_PER_PIXEL < 8 {
let pixels_per_byte = 8 / C::Raw::BITS_PER_PIXEL as u32;
bytes_per_row(self.size.width, C::Raw::BITS_PER_PIXEL) as u32 * pixels_per_byte
} else {
self.size.width
}
}
}
/// Returns the length of each row in bytes.
const fn bytes_per_row(width: u32, bits_per_pixel: usize) -> usize {
(width as usize * bits_per_pixel + 7) / 8
}
impl<'a, C, BO> ImageDrawable for ImageRaw<'a, C, BO>
where
C: PixelColor + From<<C as PixelColor>::Raw>,
BO: ByteOrder,
RawDataSlice<'a, C::Raw, BO>: IntoIterator<Item = C::Raw>,
{
type Color = C;
fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
where
D: DrawTarget<Color = C>,
{
let row_skip = self.data_width() - self.size.width;
target.fill_contiguous(
&self.bounding_box(),
ContiguousPixels::new(self, self.size, 0, row_skip as usize),
)
}
fn draw_sub_image<D>(&self, target: &mut D, area: &Rectangle) -> Result<(), D::Error>
where
D: DrawTarget<Color = Self::Color>,
{
// Don't draw anything if `area` is zero sized or partially outside the image.
if area.is_zero_sized()
|| area.top_left.x < 0
|| area.top_left.y < 0
|| area.top_left.x as u32 + area.size.width > self.size.width
|| area.top_left.y as u32 + area.size.height > self.size.height
{
return Ok(());
}
let data_width = self.data_width() as usize;
let initial_skip = area.top_left.y as usize * data_width + area.top_left.x as usize;
let row_skip = data_width - area.size.width as usize;
target.fill_contiguous(
&Rectangle::new(Point::zero(), area.size),
ContiguousPixels::new(self, area.size, initial_skip, row_skip),
)
}
}
impl<C, BO> OriginDimensions for ImageRaw<'_, C, BO>
where
C: PixelColor + From<<C as PixelColor>::Raw>,
BO: ByteOrder,
{
fn size(&self) -> Size {
self.size
}
}
impl<'a, C, BO> GetPixel for ImageRaw<'a, C, BO>
where
C: PixelColor + From<<C as PixelColor>::Raw>,
BO: ByteOrder,
RawDataSlice<'a, C::Raw, BO>: IntoIterator<Item = C::Raw>,
{
type Color = C;
fn pixel(&self, p: Point) -> Option<Self::Color> {
if p.x < 0 || p.y < 0 || p.x >= self.size.width as i32 || p.y >= self.size.height as i32 {
return None;
}
RawDataSlice::new(self.data)
.into_iter()
.nth(p.x as usize + p.y as usize * self.data_width() as usize)
.map(|r| r.into())
}
}
struct ContiguousPixels<'a, C, BO>
where
C: PixelColor + From<<C as PixelColor>::Raw>,
BO: ByteOrder,
RawDataSlice<'a, C::Raw, BO>: IntoIterator<Item = C::Raw>,
{
iter: <RawDataSlice<'a, C::Raw, BO> as IntoIterator>::IntoIter,
remaining_x: u32,
width: u32,
remaining_y: u32,
row_skip: usize,
}
impl<'a, C, BO> ContiguousPixels<'a, C, BO>
where
C: PixelColor + From<<C as PixelColor>::Raw>,
BO: ByteOrder,
RawDataSlice<'a, C::Raw, BO>: IntoIterator<Item = C::Raw>,
{
fn new(image: &ImageRaw<'a, C, BO>, size: Size, initial_skip: usize, row_skip: usize) -> Self {
let mut iter = RawDataSlice::new(image.data).into_iter();
if initial_skip > 0 {
iter.nth(initial_skip - 1);
}
// Set `remaining_y` to `0` if `width == 0` to prevent integer underflow in `next`.
let remaining_y = if size.width > 0 { size.height } else { 0 };
Self {
iter,
remaining_x: size.width,
width: size.width,
remaining_y,
row_skip,
}
}
}
impl<'a, C, BO> Iterator for ContiguousPixels<'a, C, BO>
where
C: PixelColor + From<<C as PixelColor>::Raw>,
BO: ByteOrder,
RawDataSlice<'a, C::Raw, BO>: IntoIterator<Item = C::Raw>,
{
type Item = C;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining_x > 0 {
self.remaining_x -= 1;
self.iter.next()
} else {
if self.remaining_y == 0 {
return None;
}
self.remaining_y -= 1;
self.remaining_x = self.width - 1;
self.iter.nth(self.row_skip)
}
.map(|c| c.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
draw_target::DrawTarget,
geometry::Point,
image::Image,
iterator::PixelIteratorExt,
mock_display::{ColorMapping, MockDisplay},
pixelcolor::{raw::RawU32, *},
Drawable, Pixel,
};
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
struct TestColorU32(RawU32);
impl PixelColor for TestColorU32 {
type Raw = RawU32;
}
impl From<RawU32> for TestColorU32 {
fn from(data: RawU32) -> Self {
Self(data)
}
}
/// Tests if the given image data matches an excepted `MockDisplay` pattern.
fn assert_pattern<C, BO>(image_data: ImageRaw<C, BO>, expected_pattern: &[&str])
where
C: PixelColor + From<<C as PixelColor>::Raw> + ColorMapping,
BO: ByteOrder,
for<'a> RawDataSlice<'a, C::Raw, BO>: IntoIterator<Item = C::Raw>,
{
let image = Image::new(&image_data, Point::zero());
let mut display = MockDisplay::new();
image.draw(&mut display).unwrap();
display.assert_pattern(expected_pattern);
}
#[test]
fn image_dimensions() {
let data = [
0xAA, 0x00, //
0x55, 0xFF, //
0xAA, 0x80, //
];
let image_data: ImageRaw<BinaryColor> = ImageRaw::new(&data, 9);
assert_eq!(image_data.size(), Size::new(9, 3));
}
#[test]
fn truncated_data() {
let data = [
0xAA, 0x00, //
0x55, 0xFF, //
0xAA, //
];
let image_data: ImageRaw<BinaryColor> = ImageRaw::new(&data, 9);
assert_pattern(
image_data,
&[
"#.#.#.#..", //
".#.#.#.##", //
],
);
}
#[test]
fn bpp1_new() {
let data = [
0xAA, 0x00, //
0x55, 0xFF, //
0xAA, 0x80, //
];
let image_data: ImageRaw<BinaryColor> = ImageRaw::new(&data, 9);
assert_pattern(
image_data,
&[
"#.#.#.#..", //
".#.#.#.##", //
"#.#.#.#.#", //
],
);
}
#[test]
fn bpp1_get_pixel() {
let data = [
0xAA, 0x00, //
0x55, 0xFF, //
0xAA, 0x80, //
];
let image_data: ImageRaw<BinaryColor> = ImageRaw::new(&data, 9);
assert_eq!(image_data.pixel(Point::new(0, 0)), Some(BinaryColor::On));
assert_eq!(image_data.pixel(Point::new(8, 0)), Some(BinaryColor::Off));
assert_eq!(image_data.pixel(Point::new(0, 1)), Some(BinaryColor::Off));
assert_eq!(image_data.pixel(Point::new(8, 1)), Some(BinaryColor::On));
assert_eq!(image_data.pixel(Point::new(0, 2)), Some(BinaryColor::On));
assert_eq!(image_data.pixel(Point::new(8, 2)), Some(BinaryColor::On));
}
#[test]
fn bpp2() {
let data = [
0b00_01_10_11, //
0b00_00_00_00, //
0b11_10_01_00, //
0b11_11_11_11, //
];
let image_data: ImageRaw<Gray2> = ImageRaw::new(&data, 5);
assert_pattern(
image_data,
&[
"01230", //
"32103", //
],
);
}
#[test]
fn bpp4() {
let data = [
0b0001_1000, //
0b1111_0000, //
0b0101_1010, //
0b0000_0000, //
];
let image_data: ImageRaw<Gray4> = ImageRaw::new(&data, 3);
assert_pattern(
image_data,
&[
"18F", //
"5A0", //
],
);
}
#[test]
fn bpp8_1() {
let data = [
0x11, 0x22, //
0x33, 0x44, //
0x55, 0x66, //
];
let image_data: ImageRaw<Gray8> = ImageRaw::new(&data, 2);
assert_pattern(
image_data,
&[
"12", //
"34", //
"56", //
],
);
}
/// Additional test for luma values with different low and high nibbles,
/// which are not supported by `MockDisplay` patterns.
#[test]
fn bpp8_2() {
let data = [0x01, 0x08, 0x10, 0x80];
let image_data: ImageRaw<Gray8> = ImageRaw::new(&data, 4);
let mut display = MockDisplay::new();
Image::new(&image_data, Point::zero())
.draw(&mut display)
.unwrap();
let mut expected = MockDisplay::new();
expected
.fill_contiguous(
&expected.bounding_box(),
data.iter().copied().map(Gray8::new),
)
.unwrap();
display.assert_eq(&expected);
}
#[test]
fn bpp16_little_endian() {
let data = [
0x00, 0xF8, //
0xE0, 0x07, //
0x1F, 0x00, //
0x00, 0x00, //
];
let image_data: ImageRawLE<Rgb565> = ImageRaw::new(&data, 1);
assert_pattern(
image_data,
&[
"R", //
"G", //
"B", //
"K", //
],
);
}
#[test]
fn bpp16_big_endian() {
let data = [
0xF8, 0x00, //
0x07, 0xE0, //
0x00, 0x1F, //
0x00, 0x00, //
];
let image_data: ImageRawBE<Rgb565> = ImageRaw::new(&data, 2);
assert_pattern(
image_data,
&[
"RG", //
"BK", //
],
);
}
#[test]
fn bpp16_big_endian_get_pixel() {
let data = [
0xF8, 0x00, //
0x07, 0xE0, //
0x00, 0x1F, //
0x00, 0x00, //
];
let image_data: ImageRawBE<Rgb565> = ImageRaw::new(&data, 2);
assert_eq!(image_data.pixel(Point::new(0, 0)), Some(Rgb565::RED));
assert_eq!(image_data.pixel(Point::new(1, 0)), Some(Rgb565::GREEN));
assert_eq!(image_data.pixel(Point::new(0, 1)), Some(Rgb565::BLUE));
assert_eq!(image_data.pixel(Point::new(1, 1)), Some(Rgb565::BLACK));
}
#[test]
fn bpp24_little_endian() {
let data = [
0xFF, 0x00, 0x00, //
0x00, 0xFF, 0x00, //
0x00, 0x00, 0xFF, //
0x00, 0x00, 0x00, //
];
let image_data: ImageRawLE<Bgr888> = ImageRaw::new(&data, 1);
assert_pattern(
image_data,
&[
"R", //
"G", //
"B", //
"K", //
],
);
}
#[test]
fn bpp24_big_endian() {
let data = [
0xFF, 0x00, 0x00, //
0x00, 0xFF, 0x00, //
0x00, 0x00, 0xFF, //
0x00, 0x00, 0x00, //
];
let image_data: ImageRawBE<Rgb888> = ImageRaw::new(&data, 4);
assert_pattern(image_data, &["RGBK"]);
}
#[test]
fn bpp32_little_endian() {
let data = [
0x12, 0x34, 0x56, 0x78, //
0x9A, 0xBC, 0xDE, 0xF0, //
0x00, 0x00, 0x00, 0x00, //
0xFF, 0xFF, 0xFF, 0xFF, //
];
let image_data: ImageRawLE<TestColorU32> = ImageRaw::new(&data, 2);
let mut display = MockDisplay::new();
Image::new(&image_data, Point::zero())
.draw(&mut display)
.unwrap();
let expected = [
Pixel(Point::new(0, 0), TestColorU32(RawU32::new(0x78563412))),
Pixel(Point::new(1, 0), TestColorU32(RawU32::new(0xF0DEBC9A))),
Pixel(Point::new(0, 1), TestColorU32(RawU32::new(0x00000000))),
Pixel(Point::new(1, 1), TestColorU32(RawU32::new(0xFFFFFFFF))),
];
let mut expected_display = MockDisplay::new();
expected
.iter()
.copied()
.draw(&mut expected_display)
.unwrap();
// assert_eq can't be used here because ColorMapping isn't implemented for TestColorU32
assert!(display.eq(&expected_display));
}
#[test]
fn bpp32_big_endian() {
let data = [
0x12, 0x34, 0x56, 0x78, //
0x9A, 0xBC, 0xDE, 0xF0, //
0x00, 0x00, 0x00, 0x00, //
0xFF, 0xFF, 0xFF, 0xFF, //
];
let image_data: ImageRawBE<TestColorU32> = ImageRaw::new(&data, 4);
let mut display = MockDisplay::new();
Image::new(&image_data, Point::zero())
.draw(&mut display)
.unwrap();
let expected = [
Pixel(Point::new(0, 0), TestColorU32(RawU32::new(0x12345678))),
Pixel(Point::new(1, 0), TestColorU32(RawU32::new(0x9ABCDEF0))),
Pixel(Point::new(2, 0), TestColorU32(RawU32::new(0x00000000))),
Pixel(Point::new(3, 0), TestColorU32(RawU32::new(0xFFFFFFFF))),
];
let mut expected_display = MockDisplay::new();
expected
.iter()
.copied()
.draw(&mut expected_display)
.unwrap();
// assert_eq can't be used here because ColorMapping isn't implemented for TestColorU32
assert!(display.eq(&expected_display));
}
#[test]
fn calculated_height() {
let data = [0u8; 1];
assert_eq!(ImageRaw::<BinaryColor>::new(&data, 12).size().height, 0);
let data = [0u8; 2];
assert_eq!(ImageRaw::<BinaryColor>::new(&data, 12).size().height, 1);
let data = [0u8; 3];
assert_eq!(ImageRaw::<BinaryColor>::new(&data, 12).size().height, 1);
let data = [0u8; 4];
assert_eq!(ImageRaw::<BinaryColor>::new(&data, 12).size().height, 2);
}
#[test]
fn binary_image_with_zero_width() {
let image = ImageRaw::<BinaryColor>::new(&[], 0);
assert_eq!(image.size, Size::zero());
}
#[test]
fn pixel_out_of_bounds() {
let data = [
0xAA, 0x00, //
0x55, 0xFF, //
0xAA, 0x80, //
];
let image_data = ImageRaw::<BinaryColor>::new(&data, 9);
assert_eq!(image_data.pixel(Point::new(-1, 0)), None);
assert_eq!(image_data.pixel(Point::new(0, -1)), None);
assert_eq!(image_data.pixel(Point::new(9, 0)), None);
assert_eq!(image_data.pixel(Point::new(9, 3)), None);
}
}
+315
View File
@@ -0,0 +1,315 @@
//! Image support for embedded-graphics
//!
//! The two main types used to draw images are [`ImageDrawable`] and [`Image`].
//!
//! [`ImageDrawable`] is implemented to add support for different image formats. This crate includes
//! an implementation for [raw pixel data]. Additional implementations for other image formats are
//! provided by external crates like [tinybmp] and [tinytga].
//!
//! The [`Image`] object is used to specify the location at which an [`ImageDrawable`] is drawn.
//! Images are drawn relative to their top-left corner.
//!
//! # Examples
//!
//! ## Display an RGB565 raw data image
//!
//! This example displays a small image created from a raw data array. The image is RGB565 encoded,
//! so it uses the `Rgb565` color type.
//!
//! ```rust
//! use embedded_graphics::{
//! image::{Image, ImageRaw, ImageRawBE},
//! pixelcolor::Rgb565,
//! prelude::*,
//! };
//! # use embedded_graphics::mock_display::MockDisplay as Display;
//!
//! let mut display: Display<Rgb565> = Display::default();
//!
//! // Raw big endian image data for demonstration purposes. A real image would likely be much
//! // larger.
//! let data = [
//! 0x00, 0x00, 0xF8, 0x00, 0x07, 0xE0, 0xFF, 0xE0, //
//! 0x00, 0x1F, 0x07, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, //
//! ];
//!
//! // Create a raw image instance. Other image formats will require different code to load them.
//! // All code after loading is the same for any image format.
//! let raw: ImageRawBE<Rgb565> = ImageRaw::new(&data, 4);
//!
//! // Create an `Image` object to position the image at `Point::zero()`.
//! let image = Image::new(&raw, Point::zero());
//!
//! // Draw the image to the display.
//! image.draw(&mut display)?;
//!
//! # Ok::<(), core::convert::Infallible>(())
//! ```
//!
//! ## Sub images
//!
//! [`SubImage`]s are used to split a larger image drawables into multiple parts, e.g. to draw a
//! single sprite from a sprite atlas as in this example. Use the [`sub_image`] method provided by
//! [`ImageDrawableExt`] to get a sub image from an image drawable. [`ImageDrawableExt`] is included
//! in the [`prelude`], which this example takes advantage of.
//!
//! ```rust
//! use embedded_graphics::{
//! image::{Image, ImageRaw, ImageRawBE},
//! pixelcolor::Rgb565,
//! prelude::*,
//! primitives::Rectangle,
//! };
//! # use embedded_graphics::mock_display::MockDisplay as Display;
//!
//! let mut display: Display<Rgb565> = Display::default();
//!
//! let data = [ 0xF8, 0x00, 0x07, 0xE0, 0xFF, 0xE0, /* ... */ ];
//! // or: let data = include_bytes!("sprite_atlas.raw");
//!
//! # let data = [0u8; 32 * 16 * 2];
//! let sprite_atlas: ImageRawBE<Rgb565> = ImageRaw::new(&data, 32);
//!
//! // Create individual sub images for each sprite in the sprite atlas.
//! // The position and size of the sub images is defined by a `Rectangle`.
//! let sprite_1 = sprite_atlas.sub_image(&Rectangle::new(Point::new(0, 0), Size::new(16, 16)));
//! let sprite_2 = sprite_atlas.sub_image(&Rectangle::new(Point::new(16, 0), Size::new(16, 16)));
//!
//! // Create `Image` objects to draw the sprites at different positions on the display.
//! Image::new(&sprite_1, Point::new(10, 10)).draw(&mut display)?;
//! Image::new(&sprite_2, Point::new(40, 30)).draw(&mut display)?;
//!
//! # Ok::<(), core::convert::Infallible>(())
//! ```
//!
//! # Implementing new image formats
//!
//! To add embedded-graphics support for an new image format the [`ImageDrawable`] and
//! [`OriginDimensions`] traits must be implemented. See the [`ImageDrawable`] documentation
//! for more information.
//!
//! [tinytga]: https://crates.io/crates/tinytga
//! [tinybmp]: https://crates.io/crates/tinybmp
//! [raw pixel data]: ImageRaw
//! [`sub_image`]: ImageDrawableExt::sub_image
//! [`OriginDimensions`]: super::geometry::OriginDimensions
//! [`prelude`]: super::prelude
mod image_drawable_ext;
mod image_raw;
mod sub_image;
pub use embedded_graphics_core::image::{GetPixel, ImageDrawable};
pub use image_drawable_ext::ImageDrawableExt;
pub use image_raw::{ImageRaw, ImageRawBE, ImageRawLE};
pub use sub_image::SubImage;
use crate::{
draw_target::{DrawTarget, DrawTargetExt},
geometry::{Dimensions, OriginDimensions, Point},
primitives::Rectangle,
transform::Transform,
Drawable,
};
use core::fmt::Debug;
/// Image object.
///
/// The `Image` struct serves as a wrapper around an [`ImageDrawable`] that provides support for
/// an image format (raw bytes, BMP, TGA, etc). It allows an image to be repositioned using
/// [`Transform::translate`] or [`Transform::translate_mut`] and drawn to a display that
/// implements the [`DrawTarget`] trait.
///
/// Refer to the [module documentation] for examples.
///
/// [module documentation]: self
/// [`Transform::translate`]: super::transform::Transform::translate
/// [`Transform::translate_mut`]: super::transform::Transform::translate_mut
/// [`DrawTarget`]: super::draw_target::DrawTarget
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub struct Image<'a, T> {
image_drawable: &'a T,
offset: Point,
}
impl<'a, T> Image<'a, T>
where
T: ImageDrawable,
{
/// Creates a new `Image`.
pub const fn new(image_drawable: &'a T, position: Point) -> Self {
Self {
image_drawable,
offset: position,
}
}
/// Creates a new `Image` centered around a given point.
pub fn with_center(image_drawable: &'a T, center: Point) -> Self {
let offset = Rectangle::with_center(center, image_drawable.size()).top_left;
Self {
image_drawable,
offset,
}
}
}
impl<T> Transform for Image<'_, T> {
/// Translate the image by a given delta, returning a new image
///
/// # Examples
///
/// ## Move an image around
///
/// This examples moves a 4x4 black and white image by `(10, 20)` pixels without mutating the
/// original image
///
/// ```rust
/// use embedded_graphics::{
/// geometry::Point,
/// image::{Image, ImageRaw},
/// pixelcolor::BinaryColor,
/// prelude::*,
/// };
///
/// let image: ImageRaw<BinaryColor> = ImageRaw::new(&[0xff, 0x00, 0xff, 0x00], 4);
///
/// let image = Image::new(&image, Point::zero());
///
/// let image_moved = image.translate(Point::new(10, 20));
///
/// assert_eq!(image.bounding_box().top_left, Point::zero());
/// assert_eq!(image_moved.bounding_box().top_left, Point::new(10, 20));
/// ```
fn translate(&self, by: Point) -> Self {
Self {
image_drawable: self.image_drawable,
offset: self.offset + by,
}
}
/// Translate the image by a given delta, modifying the original object
///
/// # Examples
///
/// ## Move an image around
///
/// This examples moves a 4x4 black and white image by `(10, 20)` pixels by mutating the
/// original image
///
/// ```rust
/// use embedded_graphics::{
/// geometry::Point,
/// image::{Image, ImageRaw},
/// pixelcolor::BinaryColor,
/// prelude::*,
/// };
///
/// let image: ImageRaw<BinaryColor> = ImageRaw::new(&[0xff, 0x00, 0xff, 0x00], 4);
///
/// let mut image = Image::new(&image, Point::zero());
///
/// image.translate_mut(Point::new(10, 20));
///
/// assert_eq!(image.bounding_box().top_left, Point::new(10, 20));
/// ```
fn translate_mut(&mut self, by: Point) -> &mut Self {
self.offset += by;
self
}
}
impl<'a, T> Drawable for Image<'a, T>
where
T: ImageDrawable,
{
type Color = T::Color;
type Output = ();
fn draw<D>(&self, display: &mut D) -> Result<Self::Output, D::Error>
where
D: DrawTarget<Color = Self::Color>,
{
self.image_drawable
.draw(&mut display.translated(self.offset))
}
}
impl<'a, T> Dimensions for Image<'a, T>
where
T: OriginDimensions,
{
fn bounding_box(&self) -> Rectangle {
self.image_drawable.bounding_box().translate(self.offset)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{geometry::Size, mock_display::MockDisplay, pixelcolor::BinaryColor};
#[test]
fn negative_top_left() {
let image: ImageRaw<BinaryColor> = ImageRaw::new(&[0xff, 0x00, 0xff, 0x00], 4);
let image = Image::new(&image, Point::zero()).translate(Point::new(-1, -1));
assert_eq!(
image.bounding_box(),
Rectangle::new(Point::new(-1, -1), Size::new(4, 4))
);
}
#[test]
fn dimensions() {
let image: ImageRaw<BinaryColor> = ImageRaw::new(&[0xff, 0x00, 0xFF, 0x00], 4);
let image = Image::new(&image, Point::zero()).translate(Point::new(100, 200));
assert_eq!(
image.bounding_box(),
Rectangle::new(Point::new(100, 200), Size::new(4, 4))
);
}
#[test]
fn position() {
let image_raw: ImageRaw<BinaryColor> = ImageRaw::new(&[0xAA, 0x55, 0xAA, 0x55], 4);
let mut display = MockDisplay::new();
Image::new(&image_raw, Point::new(1, 2))
.draw(&mut display)
.unwrap();
display.assert_pattern(&[
" ", //
" ", //
" #.#.", //
" .#.#", //
" #.#.", //
" .#.#", //
]);
}
#[test]
fn with_center() {
let image_raw: ImageRaw<BinaryColor> = ImageRaw::new(&[0xAA, 0x55, 0xAA, 0x55], 4);
let mut display = MockDisplay::new();
Image::with_center(&image_raw, Point::new(1, 2))
.draw(&mut display)
.unwrap();
display.assert_pattern(&[
" ", //
"#.#.", //
".#.#", //
"#.#.", //
".#.#", //
]);
}
}
+167
View File
@@ -0,0 +1,167 @@
use crate::{
draw_target::DrawTarget,
geometry::{Dimensions, OriginDimensions},
image::ImageDrawable,
primitives::Rectangle,
transform::Transform,
};
/// Sub image.
///
/// A sub image is rectangular subsection of an [`ImageDrawable`]. It can, for example, be used to
/// draw individual sprites from a larger sprite atlas.
///
/// To create a sub image call the [`sub_image`] method on the parent [`ImageDrawable`]. See the
/// [module-level documentation] for an example.
///
/// [`sub_image`]: trait.ImageDrawableExt.html#tymethod.sub_image
/// [module-level documentation]: super#sub-images
#[derive(Debug)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub struct SubImage<'a, T> {
parent: &'a T,
area: Rectangle,
}
impl<'a, T> SubImage<'a, T>
where
T: ImageDrawable,
{
pub(super) fn new(parent: &'a T, area: &Rectangle) -> Self {
let area = parent.bounding_box().intersection(area);
Self { parent, area }
}
pub(crate) const fn new_unchecked(parent: &'a T, area: Rectangle) -> Self {
Self { parent, area }
}
}
impl<T> OriginDimensions for SubImage<'_, T> {
fn size(&self) -> crate::prelude::Size {
self.area.size
}
}
impl<'a, T> ImageDrawable for SubImage<'a, T>
where
T: ImageDrawable,
{
type Color = T::Color;
fn draw<DT>(&self, target: &mut DT) -> Result<(), DT::Error>
where
DT: DrawTarget<Color = Self::Color>,
{
self.parent.draw_sub_image(target, &self.area)
}
fn draw_sub_image<DT>(&self, target: &mut DT, area: &Rectangle) -> Result<(), DT::Error>
where
DT: DrawTarget<Color = Self::Color>,
{
let area = area.translate(self.area.top_left);
self.parent.draw_sub_image(target, &area)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
geometry::{Point, Size},
image::ImageDrawableExt,
mock_display::MockDisplay,
pixelcolor::BinaryColor,
};
struct MockImageDrawable {
expected_area: Rectangle,
}
impl ImageDrawable for MockImageDrawable {
type Color = BinaryColor;
fn draw<DT>(&self, _target: &mut DT) -> Result<(), DT::Error>
where
DT: DrawTarget<Color = BinaryColor>,
{
panic!("draw shouldn't have been called on MockImageDrawable")
}
fn draw_sub_image<DT>(&self, _target: &mut DT, area: &Rectangle) -> Result<(), DT::Error>
where
DT: DrawTarget<Color = BinaryColor>,
{
assert_eq!(area, &self.expected_area);
Ok(())
}
}
impl OriginDimensions for MockImageDrawable {
fn size(&self) -> Size {
Size::new(8, 10)
}
}
#[test]
fn sub_image() {
let area = Rectangle::new(Point::new(2, 3), Size::new(3, 4));
let image = MockImageDrawable {
expected_area: area,
};
let mut display = MockDisplay::new();
image.sub_image(&area).draw(&mut display).unwrap();
}
#[test]
fn area_larger_than_parent() {
let area = Rectangle::new(Point::new(-5, -5), Size::new(20, 20));
let image = MockImageDrawable {
expected_area: Rectangle::new(Point::zero(), Size::new(8, 10)),
};
let mut display = MockDisplay::new();
image.sub_image(&area).draw(&mut display).unwrap();
}
#[test]
fn sub_image_of_sub_image() {
let area1 = Rectangle::new(Point::new(2, 3), Size::new(3, 4));
let area2 = Rectangle::new(Point::new(1, 1), Size::new(2, 2));
let image = MockImageDrawable {
expected_area: Rectangle::new(area1.top_left + area2.top_left, area2.size),
};
let mut display = MockDisplay::new();
image
.sub_image(&area1)
.sub_image(&area2)
.draw(&mut display)
.unwrap();
}
#[test]
fn sub_image_of_sub_image_area_larger_than_parent() {
let area1 = Rectangle::new(Point::new(2, 3), Size::new(3, 4));
let area2 = Rectangle::new(Point::new(-10, -10), Size::new(20, 20));
let image = MockImageDrawable {
expected_area: area1,
};
let mut display = MockDisplay::new();
image
.sub_image(&area1)
.sub_image(&area2)
.draw(&mut display)
.unwrap();
}
}