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

+213
View File
@@ -0,0 +1,213 @@
//! Geometry module.
mod point;
mod size;
pub use point::Point;
pub use size::Size;
use crate::primitives::Rectangle;
/// Adds the ability to get the bounding box of an item.
///
/// The exact definition of the bounding box depends on the item:
///
/// * Primitives ([`Rectangle`], [`Circle`], ...)
///
/// For unstyled [primitives] the bounding box is defined as the smallest rectangle that surrounds the entire primitive.
/// * Styled primitives and other [`Drawable`]s ([`Image`], [`Text`], ...)
///
/// The bounding box of a drawable is defined as the smallest rectangle that contains all drawn pixels.
/// While all builtin [`Drawable`]s in embedded-graphics provide an implementation of this trait, this might
/// not be true for third party drawables.
///
/// Note that a styled primitive can have a different bounding box than the underlying unstyled primitive;
/// depending on the stroke width and alignment the bounding box of the styled primitive may be larger.
/// * [`DrawTarget`]s (displays, simulator, ...)
///
/// The bounding box of a draw target is defined as the area that should be used for drawing operations.
/// For most display drivers the top left corner of the bounding box will be at the origin but other draw targets
/// can have different positions of the top left corner.
///
/// The bounding box will be returned as a [`Rectangle`]. The methods provided by [`Rectangle`] make
/// it easy to implement additional functions like hit testing (by using [`contains`]) or drawing a focus
/// rectangle around a drawable (by converting the rectangle into a [`Styled`]).
///
/// # Implementation notes
///
/// `Dimensions` should be implemented for `Drawable`s if the bounding box is known before [`Drawable::draw`] is
/// executed. The implementation must return a rectangle that contains all drawn pixels.
/// [`MockDisplay::affected_area`] can be a used in unit tests to make sure a drawable returns a bounding box with
/// the correct dimensions.
///
/// [`DrawTarget`]s (display drivers, etc) are required to implement `Dimensions`. The
/// implementation must return a rectangle representing the drawing area. For display
/// drivers it is recommended to implement [`OriginDimensions`] instead of implementing `Dimensions` directly,
/// if the top left corner of the display area is at the origin `(0, 0)`.
///
/// The bounding box of [`ImageDrawable`]s must always start at the origin, therefore [`OriginDimensions`] must be implemented instead of this trait.
///
/// [`Drawable`]: super::Drawable
/// [`Drawable::draw`]: super::Drawable::draw
/// [`DrawTarget`]: super::draw_target::DrawTarget
/// [`ImageDrawable`]: super::image::ImageDrawable
/// [`Rectangle`]: super::primitives::rectangle::Rectangle
/// [`points`]: super::primitives::PointsIter
/// [`MockDisplay::affected_area`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/mock_display/struct.MockDisplay.html#method.affected_area
/// [`contains`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/primitives/trait.ContainsPoint.html#tymethod.contains
/// [primitives]: https://docs.rs/embedded-graphics/latest/embedded_graphics/primitives/index.html
/// [`Circle`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/primitives/circle/struct.Circle.html
/// [`Image`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/image/struct.Image.html
/// [`Text`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/fonts/struct.Text.html
/// [`Styled`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/style/styled/struct.Styled.html
pub trait Dimensions {
/// Returns the bounding box.
fn bounding_box(&self) -> Rectangle;
}
/// Dimensions with `top_left` of the bounding box at `(0, 0)`.
///
/// A blanket implementation of `Dimensions` is provided for all types that implement this trait.
/// See the [`Dimensions`] trait documentation for more information about bounding boxes.
///
/// # Implementation notes
///
/// This trait should be implemented instead of [`Dimensions`] if the top left corner of the bounding box
/// will always be at the origin, which will be the case for most display drivers. Some types, like [`ImageDrawable`],
/// require a bounding box that starts at the origin and can only be used if [`OriginDimensions`] is implemented.
///
/// [`ImageDrawable`]: super::image::ImageDrawable
pub trait OriginDimensions {
/// Returns the size of the bounding box.
fn size(&self) -> Size;
}
impl<T> Dimensions for T
where
T: OriginDimensions,
{
fn bounding_box(&self) -> Rectangle {
Rectangle::new(Point::zero(), self.size())
}
}
/// Anchor point.
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub enum AnchorPoint {
/// Top left.
TopLeft,
/// Top center.
TopCenter,
/// Top right.
TopRight,
/// Center left.
CenterLeft,
/// Center.
Center,
/// Center right.
CenterRight,
/// Bottom left.
BottomLeft,
/// Bottom center.
BottomCenter,
/// Bottom right.
BottomRight,
}
impl AnchorPoint {
/// Creates an anchor point from an X and Y component.
pub fn from_xy(x: AnchorX, y: AnchorY) -> Self {
match (y, x) {
(AnchorY::Top, AnchorX::Left) => AnchorPoint::TopLeft,
(AnchorY::Top, AnchorX::Center) => AnchorPoint::TopCenter,
(AnchorY::Top, AnchorX::Right) => AnchorPoint::TopRight,
(AnchorY::Center, AnchorX::Left) => AnchorPoint::CenterLeft,
(AnchorY::Center, AnchorX::Center) => AnchorPoint::Center,
(AnchorY::Center, AnchorX::Right) => AnchorPoint::CenterRight,
(AnchorY::Bottom, AnchorX::Left) => AnchorPoint::BottomLeft,
(AnchorY::Bottom, AnchorX::Center) => AnchorPoint::BottomCenter,
(AnchorY::Bottom, AnchorX::Right) => AnchorPoint::BottomRight,
}
}
/// Returns the X axis component.
pub fn x(self) -> AnchorX {
match self {
AnchorPoint::TopLeft | AnchorPoint::CenterLeft | AnchorPoint::BottomLeft => {
AnchorX::Left
}
AnchorPoint::TopCenter | AnchorPoint::Center | AnchorPoint::BottomCenter => {
AnchorX::Center
}
AnchorPoint::TopRight | AnchorPoint::CenterRight | AnchorPoint::BottomRight => {
AnchorX::Right
}
}
}
/// Returns the Y axis component.
pub fn y(self) -> AnchorY {
match self {
AnchorPoint::TopLeft | AnchorPoint::TopCenter | AnchorPoint::TopRight => AnchorY::Top,
AnchorPoint::CenterLeft | AnchorPoint::Center | AnchorPoint::CenterRight => {
AnchorY::Center
}
AnchorPoint::BottomLeft | AnchorPoint::BottomCenter | AnchorPoint::BottomRight => {
AnchorY::Bottom
}
}
}
}
/// X axis anchor point.
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub enum AnchorX {
/// Left.
Left,
/// Center.
Center,
/// Right.
Right,
}
/// Y axis anchor point.
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub enum AnchorY {
/// Top.
Top,
/// Center.
Center,
/// Bottom.
Bottom,
}
#[cfg(test)]
mod tests {
use super::*;
#[rustfmt::skip]
const ANCHOR_TESTS: &[((AnchorY, AnchorX), AnchorPoint)] = &[
((AnchorY::Top, AnchorX::Left), AnchorPoint::TopLeft),
((AnchorY::Top, AnchorX::Center), AnchorPoint::TopCenter),
((AnchorY::Top, AnchorX::Right), AnchorPoint::TopRight),
((AnchorY::Center, AnchorX::Left), AnchorPoint::CenterLeft),
((AnchorY::Center, AnchorX::Center), AnchorPoint::Center),
((AnchorY::Center, AnchorX::Right), AnchorPoint::CenterRight),
((AnchorY::Bottom, AnchorX::Left), AnchorPoint::BottomLeft),
((AnchorY::Bottom, AnchorX::Center), AnchorPoint::BottomCenter),
((AnchorY::Bottom, AnchorX::Right), AnchorPoint::BottomRight),
];
#[test]
fn anchor_conversion() {
for ((y, x), p) in ANCHOR_TESTS.iter().copied() {
assert_eq!(p.x(), x);
assert_eq!(p.y(), y);
assert_eq!(AnchorPoint::from_xy(x, y), p);
}
}
}
+772
View File
@@ -0,0 +1,772 @@
use core::{
convert::{TryFrom, TryInto},
fmt,
ops::{Add, AddAssign, Div, DivAssign, Index, Mul, MulAssign, Neg, Sub, SubAssign},
};
use crate::geometry::Size;
/// 2D point.
///
/// A point can be used to define the position of a graphics object. For example, a [`Rectangle`]
/// may be defined that has its top left corner at `(-1, -2)`. To specify the size of an object
/// [`Size`] should be used instead.
///
/// [Nalgebra] support can be enabled with the `nalgebra_support` feature. This implements
/// `From<Vector2<N>>` and `From<&Vector2<N>>` where `N` is `Scalar + Into<i32>`. This allows use
/// of Nalgebra's [`Vector2`] with embedded-graphics where `i8`, `i16`, `i32`, `u16` or `u8` is used
/// for value storage.
///
/// # Examples
///
/// ## Create a `Point` from two integers
///
/// ```rust
/// use embedded_graphics::geometry::Point;
///
/// // Create a coord using the `new` constructor method
/// let p = Point::new(10, 20);
/// ```
///
/// ## Create a `Point` from a Nalgebra `Vector2`
///
/// _Be sure to enable the `nalgebra_support` feature to get [Nalgebra] integration._
///
/// ```rust
/// # #[cfg(feature = "nalgebra_support")] {
/// use embedded_graphics::geometry::Point;
/// use nalgebra::Vector2;
///
/// let n_coord = Vector2::new(10i32, 20);
///
/// assert_eq!(Point::from(n_coord), Point::new(10, 20));
/// # }
/// ```
///
/// ## Convert a `Vector2<u8>` into a `Point`
///
/// _Be sure to enable the `nalgebra_support` feature to get [Nalgebra] integration._
///
/// Smaller unsigned types that can be converted to `i32` are also supported in conversions.
///
/// ```rust
/// # #[cfg(feature = "nalgebra_support")] {
/// use embedded_graphics::geometry::Point;
/// use nalgebra::Vector2;
///
/// let n_coord = Vector2::new(10u8, 20);
///
/// assert_eq!(Point::from(n_coord), Point::new(10, 20));
/// # }
/// ```
///
/// [`Rectangle`]: crate::primitives::rectangle::Rectangle
/// [`Vector2<N>`]: https://docs.rs/nalgebra/0.18.0/nalgebra/base/type.Vector2.html
/// [`Vector2`]: https://docs.rs/nalgebra/0.18.0/nalgebra/base/type.Vector2.html
/// [Nalgebra]: https://docs.rs/nalgebra
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub struct Point {
/// The x coordinate.
pub x: i32,
/// The y coordinate.
pub y: i32,
}
impl Point {
/// Creates a point from X and Y coordinates.
pub const fn new(x: i32, y: i32) -> Self {
Point { x, y }
}
/// Creates a point with X and Y values set to an equal value.
///
/// # Examples
///
/// ```rust
/// use embedded_graphics::geometry::Point;
///
/// let point = Point::new_equal(11);
///
/// assert_eq!(point, Point { x: 11, y: 11 });
/// ```
pub const fn new_equal(value: i32) -> Self {
Point { x: value, y: value }
}
/// Creates a point with X and Y equal to zero.
pub const fn zero() -> Self {
Point { x: 0, y: 0 }
}
/// Returns a point with equal `x` value and `y` set to `0`.
///
/// # Examples
///
/// ## Move a `Point` along the X axis.
///
/// ```rust
/// use embedded_graphics::geometry::Point;
///
/// let translate = Point::new(20, 30);
///
/// let point = Point::new(10, 15);
///
/// let moved_x = point + translate.x_axis();
///
/// assert_eq!(moved_x, Point::new(30, 15));
/// ```
pub const fn x_axis(self) -> Self {
Self { x: self.x, y: 0 }
}
/// Returns a point with equal `y` value and `x` set to `0`.
///
/// # Examples
///
/// ## Move a `Point` along the Y axis.
///
/// ```rust
/// use embedded_graphics::geometry::Point;
///
/// let translate = Point::new(20, 30);
///
/// let point = Point::new(10, 15);
///
/// let moved_y = point + translate.y_axis();
///
/// assert_eq!(moved_y, Point::new(10, 45));
/// ```
pub const fn y_axis(self) -> Self {
Self { x: 0, y: self.y }
}
/// Remove the sign from a coordinate
///
/// # Examples
///
/// ```
/// # use embedded_graphics::geometry::Point;
/// #
/// let point = Point::new(-5, -10);
///
/// assert_eq!(point.abs(), Point::new(5, 10));
/// ```
pub const fn abs(self) -> Self {
Point::new(self.x.abs(), self.y.abs())
}
/// Offsets a point by subtracting a size.
///
/// This method provides a workaround for the `Sub` trait not being usable in `const` contexts.
///
/// # Panics
///
/// This function will panic if `width` or `height` are too large to be represented as an `i32`
/// and debug assertions are enabled.
pub(crate) const fn sub_size(self, other: Size) -> Point {
let width = other.width as i32;
let height = other.height as i32;
debug_assert!(width >= 0, "width is too large");
debug_assert!(height >= 0, "height is too large");
Point::new(self.x - width, self.y - height)
}
/// Returns the componentwise minimum of two `Point`s
///
/// # Examples
///
/// ```rust
/// use embedded_graphics::geometry::Point;
///
/// let min = Point::new(20, 30).component_min(Point::new(15, 50));
///
/// assert_eq!(min, Point::new(15, 30));
/// ```
pub fn component_min(self, other: Self) -> Self {
Self::new(self.x.min(other.x), self.y.min(other.y))
}
/// Returns the componentwise maximum of two `Point`s
///
/// # Examples
///
/// ```rust
/// use embedded_graphics::geometry::Point;
///
/// let min = Point::new(20, 30).component_max(Point::new(15, 50));
///
/// assert_eq!(min, Point::new(20, 50));
/// ```
pub fn component_max(self, other: Self) -> Self {
Self::new(self.x.max(other.x), self.y.max(other.y))
}
/// Returns the componentwise multiplication of two `Point`s.
///
/// ```rust
/// use embedded_graphics::geometry::Point;
///
/// let result = Point::new(20, 30).component_mul(Point::new(-2, 3));
///
/// assert_eq!(result, Point::new(-40, 90));
/// ```
pub const fn component_mul(self, other: Self) -> Self {
Self::new(self.x * other.x, self.y * other.y)
}
/// Returns the componentwise division of two `Points`s.
///
/// # Panics
///
/// Panics if one of the components of `other` equals zero.
///
/// ```rust
/// use embedded_graphics::geometry::Point;
///
/// let result = Point::new(20, 30).component_div(Point::new(10, -3));
///
/// assert_eq!(result, Point::new(2, -10));
/// ```
pub const fn component_div(self, other: Self) -> Self {
Self::new(self.x / other.x, self.y / other.y)
}
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point::new(self.x + other.x, self.y + other.y)
}
}
impl Add<Size> for Point {
type Output = Point;
/// Offsets a point by adding a size.
///
/// # Panics
///
/// This function will panic if `width` or `height` are too large to be represented as an `i32`
/// and debug assertions are enabled.
fn add(self, other: Size) -> Point {
let width = other.width as i32;
let height = other.height as i32;
debug_assert!(width >= 0, "width is too large");
debug_assert!(height >= 0, "height is too large");
Point::new(self.x + width, self.y + height)
}
}
impl AddAssign for Point {
fn add_assign(&mut self, other: Point) {
self.x += other.x;
self.y += other.y;
}
}
impl AddAssign<Size> for Point {
/// Offsets a point by adding a size.
///
/// # Panics
///
/// This function will panic if `width` or `height` are too large to be represented as an `i32`
/// and debug assertions are enabled.
fn add_assign(&mut self, other: Size) {
let width = other.width as i32;
let height = other.height as i32;
debug_assert!(width >= 0, "width is too large");
debug_assert!(height >= 0, "height is too large");
self.x += width;
self.y += height;
}
}
impl Sub for Point {
type Output = Point;
fn sub(self, other: Point) -> Point {
Point::new(self.x - other.x, self.y - other.y)
}
}
impl Sub<Size> for Point {
type Output = Point;
/// Offsets a point by subtracting a size.
///
/// # Panics
///
/// This function will panic if `width` or `height` are too large to be represented as an `i32`
/// and debug assertions are enabled.
fn sub(self, other: Size) -> Point {
self.sub_size(other)
}
}
impl SubAssign for Point {
fn sub_assign(&mut self, other: Point) {
self.x -= other.x;
self.y -= other.y;
}
}
impl SubAssign<Size> for Point {
/// Offsets a point by subtracting a size.
///
/// # Panics
///
/// This function will panic if `width` or `height` are too large to be represented as an `i32`
/// and debug assertions are enabled.
fn sub_assign(&mut self, other: Size) {
let width = other.width as i32;
let height = other.height as i32;
debug_assert!(width >= 0, "width is too large");
debug_assert!(height >= 0, "height is too large");
self.x -= width;
self.y -= height;
}
}
impl Mul<i32> for Point {
type Output = Point;
fn mul(self, rhs: i32) -> Point {
Point::new(self.x * rhs, self.y * rhs)
}
}
impl MulAssign<i32> for Point {
fn mul_assign(&mut self, rhs: i32) {
self.x *= rhs;
self.y *= rhs;
}
}
impl Div<i32> for Point {
type Output = Point;
fn div(self, rhs: i32) -> Point {
Point::new(self.x / rhs, self.y / rhs)
}
}
impl DivAssign<i32> for Point {
fn div_assign(&mut self, rhs: i32) {
self.x /= rhs;
self.y /= rhs;
}
}
impl Index<usize> for Point {
type Output = i32;
fn index(&self, idx: usize) -> &i32 {
match idx {
0 => &self.x,
1 => &self.y,
_ => panic!("index out of bounds: the len is 2 but the index is {}", idx),
}
}
}
impl Neg for Point {
type Output = Point;
fn neg(self) -> Self::Output {
Point::new(-self.x, -self.y)
}
}
impl From<(i32, i32)> for Point {
fn from(other: (i32, i32)) -> Self {
Point::new(other.0, other.1)
}
}
impl From<[i32; 2]> for Point {
fn from(other: [i32; 2]) -> Self {
Point::new(other[0], other[1])
}
}
impl From<&[i32; 2]> for Point {
fn from(other: &[i32; 2]) -> Self {
Point::new(other[0], other[1])
}
}
impl From<Point> for (i32, i32) {
fn from(other: Point) -> (i32, i32) {
(other.x, other.y)
}
}
impl From<Point> for [i32; 2] {
fn from(other: Point) -> [i32; 2] {
[other.x, other.y]
}
}
impl From<&Point> for (i32, i32) {
fn from(other: &Point) -> (i32, i32) {
(other.x, other.y)
}
}
impl TryFrom<Point> for (u32, u32) {
type Error = core::num::TryFromIntError;
fn try_from(point: Point) -> Result<Self, Self::Error> {
Ok((point.x.try_into()?, point.y.try_into()?))
}
}
impl TryFrom<(u32, u32)> for Point {
type Error = core::num::TryFromIntError;
fn try_from(point: (u32, u32)) -> Result<Self, Self::Error> {
let x = point.0.try_into()?;
let y = point.1.try_into()?;
Ok(Point::new(x, y))
}
}
impl TryFrom<Point> for [u32; 2] {
type Error = core::num::TryFromIntError;
fn try_from(point: Point) -> Result<Self, Self::Error> {
Ok([point.x.try_into()?, point.y.try_into()?])
}
}
impl TryFrom<[u32; 2]> for Point {
type Error = core::num::TryFromIntError;
fn try_from(point: [u32; 2]) -> Result<Self, Self::Error> {
let x = point[0].try_into()?;
let y = point[1].try_into()?;
Ok(Point::new(x, y))
}
}
impl TryFrom<&[u32; 2]> for Point {
type Error = core::num::TryFromIntError;
fn try_from(point: &[u32; 2]) -> Result<Self, Self::Error> {
let x = point[0].try_into()?;
let y = point[1].try_into()?;
Ok(Point::new(x, y))
}
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}, {}", self.x, self.y)
}
}
#[cfg(feature = "nalgebra_support")]
use nalgebra::{base::Scalar, Vector2};
#[cfg(feature = "nalgebra_support")]
impl<N> From<Vector2<N>> for Point
where
N: Into<i32> + Scalar + Copy,
{
fn from(other: Vector2<N>) -> Self {
Self::new(other[0].into(), other[1].into())
}
}
#[cfg(feature = "nalgebra_support")]
impl<N> From<&Vector2<N>> for Point
where
N: Into<i32> + Scalar + Copy,
{
fn from(other: &Vector2<N>) -> Self {
Self::new(other[0].into(), other[1].into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::fmt::Write;
#[test]
fn convert_positive_to_u32_tuple() {
let p = Point::new(10, 20);
let tuple: (u32, u32) = p.try_into().unwrap();
let array: [u32; 2] = p.try_into().unwrap();
assert_eq!(tuple, (10, 20));
assert_eq!(array, [10, 20]);
}
#[test]
fn convert_i32_max_to_u32_tuple() {
let p = Point::new(i32::max_value(), i32::max_value());
let tuple: (u32, u32) = p.try_into().unwrap();
let array: [u32; 2] = p.try_into().unwrap();
// Literal value taken from https://doc.rust-lang.org/std/primitive.i32.html#method.max_value
assert_eq!(tuple, (2147483647, 2147483647));
assert_eq!(array, [2147483647, 2147483647]);
}
#[test]
fn convert_negative_to_u32_tuple() {
let p = Point::new(-50, -10);
let tuple: Result<(u32, u32), _> = p.try_into();
let array: Result<[u32; 2], _> = p.try_into();
assert!(tuple.is_err());
assert!(array.is_err());
}
#[test]
fn convert_i32_min_to_u32_tuple() {
let p = Point::new(i32::min_value(), i32::min_value());
let tuple: Result<(u32, u32), _> = p.try_into();
let array: Result<[u32; 2], _> = p.try_into();
assert!(tuple.is_err());
assert!(array.is_err());
}
#[test]
fn points_can_be_added() {
let mut left = Point::new(10, 20);
let right = Point::new(30, 40);
assert_eq!(left + right, Point::new(40, 60));
left += right;
assert_eq!(left, Point::new(40, 60));
}
#[test]
fn point_and_size_can_be_added() {
let mut left = Point::new(11, 21);
let right = Size::new(30, 40);
assert_eq!(left + right, Point::new(41, 61));
left += right;
assert_eq!(left, Point::new(41, 61));
}
#[test]
fn points_can_be_subtracted() {
let mut left = Point::new(30, 50);
let right = Point::new(10, 20);
assert_eq!(left - right, Point::new(20, 30));
left -= right;
assert_eq!(left, Point::new(20, 30));
}
#[test]
fn point_and_size_can_be_subtracted() {
let mut left = Point::new(30, 40);
let right = Size::new(11, 22);
assert_eq!(left - right, Point::new(19, 18));
left -= right;
assert_eq!(left, Point::new(19, 18));
}
#[test]
fn points_can_be_negative_after_subtraction() {
let left = Point::new(10, 20);
let right = Point::new(30, 50);
assert_eq!(left - right, Point::new(-20, -30));
let left = Point::new(10, 20);
let right = Size::new(31, 42);
assert_eq!(left - right, Point::new(-21, -22));
}
#[test]
fn points_can_be_multiplied_by_scalar() {
let p = Point::new(1, 2);
assert_eq!(p * 3, Point::new(3, 6));
let mut p = Point::new(3, 4);
p *= -5;
assert_eq!(p, Point::new(-15, -20));
}
#[test]
fn points_can_be_divided_by_scalar() {
let p = Point::new(10, 20);
assert_eq!(p / 2, Point::new(5, 10));
let mut p = Point::new(-10, 10);
p /= -5;
assert_eq!(p, Point::new(2, -2));
}
#[test]
#[should_panic(expected = "width is too large")]
#[cfg(debug_assertions)]
fn too_large_width_can_not_be_added() {
let p = Point::zero();
let _ = p + Size::new(u32::max_value(), 0);
}
#[test]
#[should_panic(expected = "width is too large")]
#[cfg(debug_assertions)]
fn too_large_width_can_not_be_add_assigned() {
let mut p = Point::zero();
p += Size::new(u32::max_value(), 0);
}
#[test]
#[should_panic(expected = "height is too large")]
#[cfg(debug_assertions)]
fn too_large_height_can_not_be_added() {
let p = Point::zero();
let _ = p + Size::new(0, 0x80000000);
}
#[test]
#[should_panic(expected = "height is too large")]
#[cfg(debug_assertions)]
fn too_large_height_can_not_be_add_assigned() {
let mut p = Point::zero();
p += Size::new(0, 0x80000000);
}
#[test]
#[should_panic(expected = "width is too large")]
#[cfg(debug_assertions)]
fn too_large_width_can_not_be_subtracted() {
let p = Point::zero();
let _ = p - Size::new(u32::max_value(), 0);
}
#[test]
#[should_panic(expected = "width is too large")]
#[cfg(debug_assertions)]
fn too_large_width_can_not_be_sub_assigned() {
let mut p = Point::zero();
p -= Size::new(u32::max_value(), 0);
}
#[test]
#[should_panic(expected = "height is too large")]
#[cfg(debug_assertions)]
fn too_large_height_can_not_be_subtracted() {
let p = Point::zero();
let _ = p - Size::new(0, 0x80000000);
}
#[test]
#[should_panic(expected = "height is too large")]
#[cfg(debug_assertions)]
fn too_large_height_can_not_be_sub_assigned() {
let mut p = Point::zero();
p -= Size::new(0, 0x80000000);
}
#[test]
fn from_tuple() {
assert_eq!(Point::from((20i32, 30i32)), Point::new(20, 30));
assert_eq!(Point::from((20i32, 30i32)), Point::new(20, 30));
}
#[test]
fn from_array() {
assert_eq!(Point::from([20i32, 30i32]), Point::new(20, 30));
assert_eq!(Point::from([20i32, 30i32]), Point::new(20, 30));
}
#[test]
fn from_array_ref() {
assert_eq!(Point::from(&[20i32, 30i32]), Point::new(20, 30));
assert_eq!(Point::from(&[20i32, 30i32]), Point::new(20, 30));
}
#[test]
fn neg() {
assert_eq!(-Point::new(10, 20), Point::new(-10, -20));
assert_eq!(-Point::new(-40, -50), Point::new(40, 50));
}
#[test]
fn index() {
let point = Point::new(12, -34);
assert_eq!(point.x, point[0]);
assert_eq!(point.y, point[1]);
}
#[test]
#[should_panic]
fn index_out_of_bounds() {
let point = Point::new(1, 2);
let _ = point[2];
}
#[test]
#[cfg(feature = "nalgebra_support")]
fn nalgebra_support() {
let left = Vector2::new(30, 40);
let right = Vector2::new(10, 20);
assert_eq!(Point::from(left - right), Point::new(20, 20));
}
#[test]
#[cfg(feature = "nalgebra_support")]
fn convert_ref() {
let left = Vector2::new(30, 40);
let right = Vector2::new(10, 20);
let c = left - right;
assert_eq!(Point::from(&c), Point::new(20, 20));
}
#[test]
fn component_min_max() {
let a = Point::new(20, 30);
let b = Point::new(15, 50);
assert_eq!(a.component_min(b), Point::new(15, 30));
assert_eq!(a.component_max(b), Point::new(20, 50));
}
#[test]
fn display() {
let mut buffer = arrayvec::ArrayString::<32>::new();
write!(buffer, "{}", Point::new(123, -456)).unwrap();
assert_eq!(&buffer, "123, -456");
}
}
+501
View File
@@ -0,0 +1,501 @@
use core::{
fmt,
ops::{Add, AddAssign, Div, DivAssign, Index, Mul, MulAssign, Sub, SubAssign},
};
use crate::geometry::Point;
/// 2D size.
///
/// `Size` is used to define the width and height of an object.
///
/// [Nalgebra] support can be enabled with the `nalgebra_support` feature. This implements
/// `From<Vector2<N>>` and `From<&Vector2<N>>` where `N` is `Scalar + Into<u32>`. This allows use
/// of Nalgebra's [`Vector2`] with embedded-graphics where `u32`, `u16` or `u8` is used for value
/// storage.
///
/// # Examples
///
/// ## Create a `Size` from two integers
///
///
/// ```rust
/// use embedded_graphics::geometry::Size;
///
/// // Create a size using the `new` constructor method
/// let s = Size::new(10, 20);
/// ```
///
/// ## Create a `Size` from a Nalgebra `Vector2`
///
/// _Be sure to enable the `nalgebra_support` feature to get [Nalgebra] integration._
///
/// Any `Vector2<N>` can be used where `N: Into<u32> + nalgebra::Scalar`. This includes the primitive types `u32`, `u16` and `u8`.
///
/// ```rust
/// # #[cfg(feature = "nalgebra_support")] {
/// use embedded_graphics::geometry::Size;
/// use nalgebra::Vector2;
///
/// assert_eq!(Size::from(Vector2::new(10u32, 20)), Size::new(10u32, 20));
/// assert_eq!(Size::from(Vector2::new(10u16, 20)), Size::new(10u32, 20));
/// assert_eq!(Size::from(Vector2::new(10u8, 20)), Size::new(10u32, 20));
/// # }
/// ```
///
/// `.into()` can also be used, but may require more type annotations:
///
/// ```rust
/// # #[cfg(feature = "nalgebra_support")] {
/// use embedded_graphics::geometry::Size;
/// use nalgebra::Vector2;
///
/// let c: Size = Vector2::new(10u32, 20).into();
///
/// assert_eq!(c, Size::new(10u32, 20));
/// # }
/// ```
///
/// [`Drawable`]: crate::Drawable
/// [`Vector2<N>`]: https://docs.rs/nalgebra/0.18.0/nalgebra/base/type.Vector2.html
/// [`Vector2`]: https://docs.rs/nalgebra/0.18.0/nalgebra/base/type.Vector2.html
/// [Nalgebra]: https://docs.rs/nalgebra
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
pub struct Size {
/// The width.
pub width: u32,
/// The height.
pub height: u32,
}
impl Size {
/// Creates a size from a width and a height.
pub const fn new(width: u32, height: u32) -> Self {
Size { width, height }
}
/// Creates a size with width and height set to an equal value.
///
/// ```rust
/// use embedded_graphics::geometry::Size;
///
/// let size = Size::new_equal(11);
///
/// assert_eq!(
/// size,
/// Size {
/// width: 11,
/// height: 11
/// }
/// );
/// ```
pub const fn new_equal(value: u32) -> Self {
Size {
width: value,
height: value,
}
}
/// Creates a size with width and height equal to zero.
pub const fn zero() -> Self {
Size {
width: 0,
height: 0,
}
}
/// Returns a size with equal `width` value and `height` set to `0`.
///
/// # Examples
///
/// ## Move a `Point` along the X axis.
///
/// ```rust
/// use embedded_graphics::geometry::{Point, Size};
///
/// let size = Size::new(20, 30);
///
/// let point = Point::new(10, 15);
///
/// let moved_x = point + size.x_axis();
///
/// assert_eq!(moved_x, Point::new(30, 15));
/// ```
pub const fn x_axis(self) -> Self {
Self {
width: self.width,
height: 0,
}
}
/// Returns a size with equal `height` value and `width` set to `0`.
///
/// # Examples
///
/// ## Move a `Point` along the Y axis.
///
/// ```rust
/// use embedded_graphics::geometry::{Point, Size};
///
/// let size = Size::new(20, 30);
///
/// let point = Point::new(10, 15);
///
/// let moved_y = point + size.y_axis();
///
/// assert_eq!(moved_y, Point::new(10, 45));
/// ```
pub const fn y_axis(self) -> Self {
Self {
width: 0,
height: self.height,
}
}
/// Saturating addition.
///
/// Returns `u32::max_value()` for `width` and/or `height` instead of overflowing.
pub const fn saturating_add(self, other: Self) -> Self {
Self {
width: self.width.saturating_add(other.width),
height: self.height.saturating_add(other.height),
}
}
/// Saturating subtraction.
///
/// Returns `0` for `width` and/or `height` instead of overflowing, if the
/// value in `other` is larger then in `self`.
pub const fn saturating_sub(self, other: Self) -> Self {
Self {
width: self.width.saturating_sub(other.width),
height: self.height.saturating_sub(other.height),
}
}
/// Division.
///
/// This method provides a workaround for the `Div` trait not being usable in `const` contexts.
pub(crate) const fn div_u32(self, rhs: u32) -> Size {
Size::new(self.width / rhs, self.height / rhs)
}
/// Creates a size from two corner points of a bounding box.
pub(crate) const fn from_bounding_box(corner_1: Point, corner_2: Point) -> Self {
let width = (corner_1.x - corner_2.x).unsigned_abs() + 1;
let height = (corner_1.y - corner_2.y).unsigned_abs() + 1;
Self { width, height }
}
/// Returns the componentwise minimum of two `Size`s.
///
/// ```rust
/// use embedded_graphics::geometry::Size;
///
/// let min = Size::new(20, 30).component_min(Size::new(15, 50));
///
/// assert_eq!(min, Size::new(15, 30));
/// ```
pub fn component_min(self, other: Self) -> Self {
Self::new(self.width.min(other.width), self.height.min(other.height))
}
/// Returns the componentwise maximum of two `Size`s.
///
/// ```rust
/// use embedded_graphics::geometry::Size;
///
/// let min = Size::new(20, 30).component_max(Size::new(15, 50));
///
/// assert_eq!(min, Size::new(20, 50));
/// ```
pub fn component_max(self, other: Self) -> Self {
Self::new(self.width.max(other.width), self.height.max(other.height))
}
/// Returns the componentwise multiplication of two `Size`s.
///
/// ```rust
/// use embedded_graphics::geometry::Size;
///
/// let result = Size::new(20, 30).component_mul(Size::new(2, 3));
///
/// assert_eq!(result, Size::new(40, 90));
/// ```
pub const fn component_mul(self, other: Self) -> Self {
Self::new(self.width * other.width, self.height * other.height)
}
/// Returns the componentwise division of two `Size`s.
///
/// # Panics
///
/// Panics if one of the components of `other` equals zero.
///
/// ```rust
/// use embedded_graphics::geometry::Size;
///
/// let result = Size::new(20, 30).component_div(Size::new(5, 10));
///
/// assert_eq!(result, Size::new(4, 3));
/// ```
pub const fn component_div(self, other: Self) -> Self {
Self::new(self.width / other.width, self.height / other.height)
}
}
impl Add for Size {
type Output = Size;
fn add(self, other: Size) -> Size {
Size::new(self.width + other.width, self.height + other.height)
}
}
impl AddAssign for Size {
fn add_assign(&mut self, other: Size) {
self.width += other.width;
self.height += other.height;
}
}
impl Sub for Size {
type Output = Size;
fn sub(self, other: Size) -> Size {
Size::new(self.width - other.width, self.height - other.height)
}
}
impl SubAssign for Size {
fn sub_assign(&mut self, other: Size) {
self.width -= other.width;
self.height -= other.height;
}
}
impl Mul<u32> for Size {
type Output = Size;
fn mul(self, rhs: u32) -> Size {
Size::new(self.width * rhs, self.height * rhs)
}
}
impl MulAssign<u32> for Size {
fn mul_assign(&mut self, rhs: u32) {
self.width *= rhs;
self.height *= rhs;
}
}
impl Div<u32> for Size {
type Output = Size;
fn div(self, rhs: u32) -> Size {
self.div_u32(rhs)
}
}
impl DivAssign<u32> for Size {
fn div_assign(&mut self, rhs: u32) {
self.width /= rhs;
self.height /= rhs;
}
}
impl Index<usize> for Size {
type Output = u32;
fn index(&self, idx: usize) -> &u32 {
match idx {
0 => &self.width,
1 => &self.height,
_ => panic!("index out of bounds: the len is 2 but the index is {}", idx),
}
}
}
impl From<(u32, u32)> for Size {
fn from(other: (u32, u32)) -> Self {
Size::new(other.0, other.1)
}
}
impl From<[u32; 2]> for Size {
fn from(other: [u32; 2]) -> Self {
Size::new(other[0], other[1])
}
}
impl From<&[u32; 2]> for Size {
fn from(other: &[u32; 2]) -> Self {
Size::new(other[0], other[1])
}
}
impl From<Size> for (u32, u32) {
fn from(other: Size) -> (u32, u32) {
(other.width, other.height)
}
}
impl From<Size> for [u32; 2] {
fn from(other: Size) -> [u32; 2] {
[other.width, other.height]
}
}
impl From<&Size> for (u32, u32) {
fn from(other: &Size) -> (u32, u32) {
(other.width, other.height)
}
}
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} x {}", self.width, self.height)
}
}
#[cfg(feature = "nalgebra_support")]
use nalgebra::{base::Scalar, Vector2};
#[cfg(feature = "nalgebra_support")]
impl<N> From<Vector2<N>> for Size
where
N: Into<u32> + Scalar + Copy,
{
fn from(other: Vector2<N>) -> Self {
Self::new(other[0].into(), other[1].into())
}
}
#[cfg(feature = "nalgebra_support")]
impl<N> From<&Vector2<N>> for Size
where
N: Into<u32> + Scalar + Copy,
{
fn from(other: &Vector2<N>) -> Self {
Self::new(other[0].into(), other[1].into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::fmt::Write;
#[test]
fn sizes_can_be_added() {
let left = Size::new(10, 20);
let right = Size::new(30, 40);
assert_eq!(left + right, Size::new(40, 60));
}
#[test]
fn sizes_can_be_subtracted() {
let left = Size::new(30, 40);
let right = Size::new(10, 20);
assert_eq!(left - right, Size::new(20, 20));
}
#[test]
fn saturating_sub() {
let p = Size::new(10, 20);
assert_eq!(p.saturating_sub(Size::new(9, 18)), Size::new(1, 2));
assert_eq!(p.saturating_sub(Size::new(11, 18)), Size::new(0, 2));
assert_eq!(p.saturating_sub(Size::new(9, 21)), Size::new(1, 0));
assert_eq!(p.saturating_sub(Size::new(11, 21)), Size::new(0, 0));
}
#[test]
fn sizes_can_be_multiplied_by_scalar() {
let s = Size::new(1, 2);
assert_eq!(s * 3, Size::new(3, 6));
let mut s = Size::new(2, 3);
s *= 4;
assert_eq!(s, Size::new(8, 12));
}
#[test]
fn sizes_can_be_divided_by_scalar() {
let s = Size::new(10, 20);
assert_eq!(s / 2, Size::new(5, 10));
let mut s = Size::new(20, 30);
s /= 5;
assert_eq!(s, Size::new(4, 6));
}
#[test]
fn from_tuple() {
assert_eq!(Size::from((20, 30)), Size::new(20, 30));
}
#[test]
fn from_array() {
assert_eq!(Size::from([20, 30]), Size::new(20, 30));
}
#[test]
fn to_array() {
let array: [u32; 2] = Size::new(20, 30).into();
assert_eq!(array, [20, 30]);
}
#[test]
fn from_array_ref() {
assert_eq!(Size::from(&[20, 30]), Size::new(20, 30));
}
#[test]
fn index() {
let size = Size::new(1, 2);
assert_eq!(size.width, size[0]);
assert_eq!(size.height, size[1]);
}
#[test]
#[should_panic]
fn index_out_of_bounds() {
let size = Size::new(1, 2);
let _ = size[2];
}
#[test]
#[cfg(feature = "nalgebra_support")]
fn nalgebra_support() {
let left = Vector2::new(30u32, 40);
let right = Vector2::new(10, 20);
assert_eq!(Size::from(left - right), Size::new(20, 20));
}
#[test]
fn component_min_max() {
let a = Size::new(20, 30);
let b = Size::new(15, 50);
assert_eq!(a.component_min(b), Size::new(15, 30));
assert_eq!(a.component_max(b), Size::new(20, 50));
}
#[test]
fn display() {
let mut buffer = arrayvec::ArrayString::<32>::new();
write!(buffer, "{}", Size::new(123, 456)).unwrap();
assert_eq!(&buffer, "123 x 456");
}
}