use embedded_graphics::{
pixelcolor::Rgb565,
prelude::{PixelColor, Size},
};
use crate::{Constraints, DivStyle};
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Insets {
pub top: u32,
pub right: u32,
pub bottom: u32,
pub left: u32,
}
impl Insets {
pub const ZERO: Self = Self::uniform(0);
pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
Self { top, right, bottom, left }
}
pub const fn uniform(value: u32) -> Self {
Self::new(value, value, value, value)
}
pub const fn zero() -> Self {
Self::ZERO
}
pub const fn horizontal(self) -> u32 {
self.left.saturating_add(self.right)
}
pub const fn vertical(self) -> u32 {
self.top.saturating_add(self.bottom)
}
pub const fn total_size(self) -> Size {
Size::new(self.horizontal(), self.vertical())
}
pub const fn saturating_add(self, other: Self) -> Self {
Self::new(
self.top.saturating_add(other.top),
self.right.saturating_add(other.right),
self.bottom.saturating_add(other.bottom),
self.left.saturating_add(other.left),
)
}
}
macro_rules! impl_insets_from {
($type:ty, $convert:expr) => {
impl From<$type> for Insets {
fn from(value: $type) -> Self {
let convert = $convert;
Self::uniform(convert(value))
}
}
impl From<($type, $type)> for Insets {
fn from((vertical, horizontal): ($type, $type)) -> Self {
let convert = $convert;
Self::new(
convert(vertical),
convert(horizontal),
convert(vertical),
convert(horizontal),
)
}
}
impl From<($type, $type, $type)> for Insets {
fn from((top, horizontal, bottom): ($type, $type, $type)) -> Self {
let convert = $convert;
Self::new(convert(top), convert(horizontal), convert(bottom), convert(horizontal))
}
}
impl From<($type, $type, $type, $type)> for Insets {
fn from((top, right, bottom, left): ($type, $type, $type, $type)) -> Self {
let convert = $convert;
Self::new(convert(top), convert(right), convert(bottom), convert(left))
}
}
};
}
impl_insets_from!(u32, |value: u32| value);
impl_insets_from!(usize, |value: usize| u32::try_from(value).unwrap_or(u32::MAX));
impl_insets_from!(i32, |value: i32| {
assert!(value >= 0, "insets cannot be negative");
value as u32
});
pub(crate) struct BoxStyle {
pub margin: Insets,
pub border: Insets,
pub padding: Insets,
pub width: Option<u32>,
pub height: Option<u32>,
}
impl BoxStyle {
pub(crate) const fn border_constraints(&self, constraints: Constraints) -> Constraints {
constraints.deflate(self.margin.total_size())
}
pub(crate) fn content_constraints(&self, constraints: Constraints) -> Constraints {
let border_constraints = self.border_constraints(constraints);
let content_size = self.content_insets().total_size();
let border_constraints = border_constraints.with_exact_dimensions(
self.width.map(|width| width.max(content_size.width)),
self.height.map(|height| height.max(content_size.height)),
);
border_constraints.deflate(content_size).loosen()
}
pub(crate) const fn content_insets(&self) -> Insets {
self.border.saturating_add(self.padding)
}
}
#[cfg(feature = "flexbox")]
#[derive(Clone, Copy)]
pub(crate) struct FlexItemStyle {
pub flex_grow: u16,
pub flex_basis: FlexBasis,
}
#[cfg(feature = "flexbox")]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum FlexBasis {
#[default]
Auto,
Zero,
}
#[cfg(feature = "flexbox")]
impl FlexBasis {
pub(crate) const fn resolve(self, auto: u32) -> u32 {
match self {
Self::Auto => auto,
Self::Zero => 0,
}
}
pub(crate) const fn is_zero(self) -> bool {
matches!(self, Self::Zero)
}
}
#[derive(PartialEq, Eq)]
pub struct Style<S: Default, C = Rgb565> {
pub margin: Insets,
pub padding: Insets,
pub border: Insets,
pub border_color: Option<C>,
pub background: Option<C>,
pub width: Option<u32>,
pub height: Option<u32>,
#[cfg(feature = "flexbox")]
pub flex_grow: u16,
#[cfg(feature = "flexbox")]
pub flex_basis: FlexBasis,
pub specific: S,
}
impl<S: Default, C> Default for Style<S, C> {
fn default() -> Self {
Self {
margin: Insets::ZERO,
padding: Insets::ZERO,
border: Insets::ZERO,
border_color: None,
background: None,
width: None,
height: None,
#[cfg(feature = "flexbox")]
flex_grow: 0,
#[cfg(feature = "flexbox")]
flex_basis: FlexBasis::Auto,
specific: S::default(),
}
}
}
impl<S: Default, C> Style<S, C> {
pub(crate) const fn box_style(&self) -> BoxStyle {
BoxStyle {
margin: self.margin,
padding: self.padding,
border: self.border,
width: self.width,
height: self.height,
}
}
#[cfg(feature = "flexbox")]
pub(crate) const fn flex_item_style(&self) -> FlexItemStyle {
FlexItemStyle { flex_grow: self.flex_grow, flex_basis: self.flex_basis }
}
}
impl<S: Default, C> core::ops::Deref for Style<S, C> {
type Target = S;
fn deref(&self) -> &Self::Target {
&self.specific
}
}
impl<S: Default, C> core::ops::DerefMut for Style<S, C> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.specific
}
}
pub trait StyledElement: Sized {
type Color: PixelColor;
type Specific: Default;
fn style(&self) -> &Style<Self::Specific, Self::Color>;
fn style_mut(&mut self) -> &mut Style<Self::Specific, Self::Color>;
fn padding(mut self, padding: impl Into<Insets>) -> Self {
self.style_mut().padding = padding.into();
self
}
fn margin(mut self, margin: impl Into<Insets>) -> Self {
self.style_mut().margin = margin.into();
self
}
fn background(mut self, color: Self::Color) -> Self {
self.style_mut().background = Some(color);
self
}
fn bg(self, color: Self::Color) -> Self {
self.background(color)
}
fn border(mut self, border: impl Into<Insets>) -> Self {
self.style_mut().border = border.into();
self
}
fn size(mut self, size: Size) -> Self {
self.style_mut().width = Some(size.width);
self.style_mut().height = Some(size.height);
self
}
fn width(mut self, width: u32) -> Self {
self.style_mut().width = Some(width);
self
}
fn height(mut self, height: u32) -> Self {
self.style_mut().height = Some(height);
self
}
fn border_color(mut self, color: Self::Color) -> Self {
self.style_mut().border_color = Some(color);
self
}
#[cfg(feature = "flexbox")]
fn flex_grow(mut self, factor: u16) -> Self {
self.style_mut().flex_grow = factor;
self
}
#[cfg(feature = "flexbox")]
fn flex_basis(mut self, flex_basis: FlexBasis) -> Self {
self.style_mut().flex_basis = flex_basis;
self
}
#[cfg(feature = "flexbox")]
fn flex(mut self, factor: u16) -> Self {
self.style_mut().flex_grow = factor;
self.style_mut().flex_basis = FlexBasis::Zero;
self
}
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum FlexDirection {
#[default]
Row,
Column,
}
impl From<&'static str> for FlexDirection {
fn from(value: &'static str) -> Self {
match value {
"row" => Self::Row,
"column" => Self::Column,
_ => panic!("invalid flex direction: {}", value),
}
}
}
#[cfg(feature = "flexbox")]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum JustifyContent {
#[default]
Start,
End,
Center,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[cfg(feature = "flexbox")]
impl JustifyContent {
pub(crate) const fn shift(&self, free: u32, index: u32, count: u32) -> u32 {
match self {
Self::End => free,
Self::Center => free / 2,
Self::SpaceBetween if count > 1 => Self::ratio(free, index, count - 1),
Self::SpaceAround if count > 0 => Self::ratio(free, 2 * index + 1, 2 * count),
Self::SpaceEvenly => Self::ratio(free, index + 1, count + 1),
Self::Start | Self::SpaceBetween | Self::SpaceAround => 0,
}
}
const fn ratio(space: u32, num: u32, denom: u32) -> u32 {
((space as u64 * num as u64) / denom as u64) as u32
}
}
#[cfg(feature = "flexbox")]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum AlignItems {
#[default]
Stretch,
Start,
End,
Center,
}
#[cfg(feature = "flexbox")]
impl AlignItems {
pub(crate) const fn shift(&self, free: u32) -> u32 {
match self {
Self::Stretch | Self::Start => 0,
Self::End => free,
Self::Center => free / 2,
}
}
pub(crate) const fn is_stretch(&self) -> bool {
matches!(self, Self::Stretch)
}
}
#[derive(PartialEq, Eq)]
pub(crate) struct FlexLayout {
pub direction: FlexDirection,
pub gap: Size,
#[cfg(feature = "flexbox")]
pub justify_content: JustifyContent,
#[cfg(feature = "flexbox")]
pub align_items: AlignItems,
}
impl From<DivStyle> for FlexLayout {
fn from(style: DivStyle) -> Self {
Self {
direction: style.direction,
gap: style.gap,
#[cfg(feature = "flexbox")]
justify_content: style.justify_content,
#[cfg(feature = "flexbox")]
align_items: style.align_items,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn css_shorthands_expand_in_trbl_order() {
assert_eq!(Insets::from(10), Insets::new(10, 10, 10, 10));
assert_eq!(Insets::from((4, 8)), Insets::new(4, 8, 4, 8));
assert_eq!(Insets::from((4, 8, 12)), Insets::new(4, 8, 12, 8));
assert_eq!(Insets::from((4, 8, 12, 16)), Insets::new(4, 8, 12, 16));
}
#[test]
fn typed_unsigned_inputs_are_supported() {
assert_eq!(Insets::from(3_u32), Insets::uniform(3));
assert_eq!(Insets::from((1_usize, 2, 3)), Insets::new(1, 2, 3, 2));
}
#[test]
fn usize_conversion_saturates() {
assert_eq!(Insets::from(usize::MAX), Insets::uniform(u32::MAX));
}
#[test]
#[should_panic(expected = "insets cannot be negative")]
fn signed_conversion_rejects_negative_values() {
let _ = Insets::from((1, -2));
}
#[test]
fn inset_arithmetic_saturates() {
let insets = Insets::new(u32::MAX, u32::MAX, 3, 4);
assert_eq!(insets.horizontal(), u32::MAX);
assert_eq!(insets.vertical(), u32::MAX);
assert_eq!(
insets.saturating_add(Insets::new(1, 2, u32::MAX, u32::MAX)),
Insets::new(u32::MAX, u32::MAX, u32::MAX, u32::MAX),
);
}
}