use super::Gradient;
use crate::core::{Color, Font, Size};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReducedMotionPreference {
#[default]
NoPreference,
ReduceMotion,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EdgeOffsets {
pub top: u32,
pub right: u32,
pub bottom: u32,
pub left: u32,
}
impl EdgeOffsets {
pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
Self { top, right, bottom, left }
}
pub const fn all(value: u32) -> Self {
Self::new(value, value, value, value)
}
pub const fn symmetric(vertical: u32, horizontal: u32) -> Self {
Self::new(vertical, horizontal, vertical, horizontal)
}
pub fn normalized(top: i32, right: i32, bottom: i32, left: i32) -> Self {
Self::new(
normalize_side(top),
normalize_side(right),
normalize_side(bottom),
normalize_side(left),
)
}
pub const fn to_padding(&self) -> Padding {
*self
}
pub const fn horizontal_total(&self) -> u32 {
self.left.saturating_add(self.right)
}
pub const fn vertical_total(&self) -> u32 {
self.top.saturating_add(self.bottom)
}
pub const fn mirrored(&self) -> Self {
Self::new(self.top, self.left, self.bottom, self.right)
}
pub const fn grow(&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),
)
}
}
pub type Padding = EdgeOffsets;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct PaddingSpec {
uniform: Option<u32>,
vertical: Option<u32>,
horizontal: Option<u32>,
top: Option<u32>,
right: Option<u32>,
bottom: Option<u32>,
left: Option<u32>,
}
impl PaddingSpec {
pub const fn new() -> Self {
Self {
uniform: None,
vertical: None,
horizontal: None,
top: None,
right: None,
bottom: None,
left: None,
}
}
pub const fn set_uniform(mut self, value: u32) -> Self {
self.uniform = Some(value);
self
}
pub const fn set_vertical(mut self, value: u32) -> Self {
self.vertical = Some(value);
self
}
pub const fn set_horizontal(mut self, value: u32) -> Self {
self.horizontal = Some(value);
self
}
pub const fn padding(self, value: u32) -> Self {
self.set_uniform(value)
}
pub const fn horizontal(self, value: u32) -> Self {
self.set_horizontal(value)
}
pub const fn vertical(self, value: u32) -> Self {
self.set_vertical(value)
}
pub const fn left(mut self, value: u32) -> Self {
self.left = Some(value);
self
}
pub const fn right(mut self, value: u32) -> Self {
self.right = Some(value);
self
}
pub const fn top(mut self, value: u32) -> Self {
self.top = Some(value);
self
}
pub const fn bottom(mut self, value: u32) -> Self {
self.bottom = Some(value);
self
}
pub const fn resolve(&self) -> EdgeOffsets {
let base = match self.uniform {
Some(value) => value,
None => 0,
};
let vertical = match self.vertical {
Some(value) => value,
None => base,
};
let horizontal = match self.horizontal {
Some(value) => value,
None => base,
};
EdgeOffsets {
top: match self.top {
Some(value) => value,
None => vertical,
},
right: match self.right {
Some(value) => value,
None => horizontal,
},
bottom: match self.bottom {
Some(value) => value,
None => vertical,
},
left: match self.left {
Some(value) => value,
None => horizontal,
},
}
}
pub const fn is_empty(&self) -> bool {
self.uniform.is_none()
&& self.vertical.is_none()
&& self.horizontal.is_none()
&& self.top.is_none()
&& self.right.is_none()
&& self.bottom.is_none()
&& self.left.is_none()
}
}
pub type Margin = EdgeOffsets;
impl Default for EdgeOffsets {
fn default() -> Self {
Self::all(0)
}
}
const fn normalize_side(value: i32) -> u32 {
if value <= 0 {
0
} else {
value as u32
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Shadow {
pub x: i32,
pub y: i32,
pub blur: u32,
pub color: Color,
}
impl Shadow {
pub fn new() -> Self {
Self { x: 0, y: 0, blur: 0, color: Color::BLACK }
}
pub fn with_offset(mut self, x: i32, y: i32) -> Self {
self.x = x;
self.y = y;
self
}
pub fn with_blur(mut self, blur: u32) -> Self {
self.blur = blur;
self
}
pub fn with_color(mut self, c: Color) -> Self {
self.color = c;
self
}
}
crate::impl_default_via_new!(Shadow);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TouchTargetSize {
Desktop,
Tablet,
Phone,
Embedded,
#[cfg(feature = "projection")]
Projection,
}
impl TouchTargetSize {
pub const fn dimensions(self) -> Size {
match self {
Self::Desktop => Size::new(32, 32),
Self::Tablet => Size::new(44, 44),
Self::Phone => Size::new(48, 48),
Self::Embedded => Size::new(40, 40),
#[cfg(feature = "projection")]
Self::Projection => Size::new(24, 24),
}
}
pub const fn spacing(self) -> u32 {
match self {
Self::Desktop => 8,
Self::Tablet => 12,
Self::Phone => 16,
Self::Embedded => 10,
#[cfg(feature = "projection")]
Self::Projection => 6,
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct WidgetStyle {
pub background_color: Option<Color>,
pub background_gradient: Option<Gradient>,
pub text_color: Option<Color>,
pub font: Option<Font>,
pub border_color: Option<Color>,
pub border_width: Option<u32>,
pub border_radius: Option<u32>,
pub padding: Padding,
pub margin: Margin,
pub spacing: Option<u32>,
pub shadow: Option<Shadow>,
pub touch_target: Option<Size>,
pub opacity: Option<f32>,
pub theme_derived: bool,
}
impl WidgetStyle {
pub fn merge_theme(&mut self, other: &WidgetStyle) {
if self.theme_derived {
self.background_color = other.background_color;
self.background_gradient = other.background_gradient.clone();
self.text_color = other.text_color;
self.font = other.font.clone();
self.border_color = other.border_color;
self.border_width = other.border_width;
self.border_radius = other.border_radius;
self.shadow = other.shadow.clone();
self.touch_target = other.touch_target;
self.opacity = other.opacity;
self.theme_derived = true;
} else {
self.merge(other);
}
}
pub fn with_background(mut self, c: Color) -> Self {
self.background_color = Some(c);
self.theme_derived = false;
self
}
pub fn with_text_color(mut self, c: Color) -> Self {
self.text_color = Some(c);
self.theme_derived = false;
self
}
pub fn with_font(mut self, f: Font) -> Self {
self.font = Some(f);
self.theme_derived = false;
self
}
pub fn with_border(mut self, color: Color, width: u32, radius: u32) -> Self {
self.border_color = Some(color);
self.border_width = Some(width);
self.border_radius = Some(radius);
self.theme_derived = false;
self
}
pub fn with_padding(mut self, p: Padding) -> Self {
self.padding = p;
self
}
pub fn with_padding_spec(mut self, spec: PaddingSpec) -> Self {
self.padding = spec.resolve();
self
}
pub fn with_spacing(mut self, spacing: u32) -> Self {
self.spacing = Some(spacing);
self.theme_derived = false;
self
}
pub fn with_margin(mut self, m: Margin) -> Self {
self.margin = m;
self
}
pub fn with_shadow(mut self, s: Shadow) -> Self {
self.shadow = Some(s);
self
}
pub fn with_touch_target(mut self, target: Size) -> Self {
self.touch_target = Some(target);
self
}
pub fn with_gradient(mut self, g: Gradient) -> Self {
self.background_gradient = Some(g);
self
}
pub fn with_opacity(mut self, opacity: f32) -> Self {
self.opacity = Some(opacity.clamp(0.0, 1.0));
self
}
pub fn inherit_from(&self, parent: &WidgetStyle) -> WidgetStyle {
WidgetStyle {
background_color: self.background_color.or(parent.background_color),
background_gradient: self
.background_gradient
.clone()
.or(parent.background_gradient.clone()),
text_color: self.text_color.or(parent.text_color),
font: self.font.clone().or(parent.font.clone()),
border_color: self.border_color.or(parent.border_color),
border_width: self.border_width.or(parent.border_width),
border_radius: self.border_radius.or(parent.border_radius),
padding: self.padding,
margin: self.margin,
spacing: self.spacing,
shadow: self.shadow.clone().or(parent.shadow.clone()),
touch_target: self.touch_target.or(parent.touch_target),
opacity: self.opacity.or(parent.opacity),
theme_derived: false,
}
}
pub fn merge(&mut self, other: &WidgetStyle) {
if self.background_color.is_none() {
self.background_color = other.background_color;
}
if self.background_gradient.is_none() {
self.background_gradient.clone_from(&other.background_gradient);
}
if self.text_color.is_none() {
self.text_color = other.text_color;
}
if self.font.is_none() {
self.font.clone_from(&other.font);
}
if self.border_color.is_none() {
self.border_color = other.border_color;
}
if self.border_width.is_none() {
self.border_width = other.border_width;
}
if self.border_radius.is_none() {
self.border_radius = other.border_radius;
}
if self.shadow.is_none() {
self.shadow = other.shadow.clone();
}
if self.touch_target.is_none() {
self.touch_target = other.touch_target;
}
if self.opacity.is_none() {
self.opacity = other.opacity;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn padding_and_margin_normalize_negative_values() {
let padding = Padding::normalized(-1, 4, -99, 8);
let margin = Margin::normalized(-5, 0, 3, 2);
assert_eq!(padding, Padding::new(0, 4, 0, 8));
assert_eq!(margin, Margin::new(0, 0, 3, 2));
}
#[test]
fn padding_and_margin_support_symmetric_builders() {
assert_eq!(Padding::symmetric(6, 2), Padding::new(6, 2, 6, 2));
assert_eq!(Margin::symmetric(3, 5), Margin::new(3, 5, 3, 5));
}
#[test]
fn inherit_from_falls_back_to_parent() {
let parent =
WidgetStyle::default().with_background(Color::RED).with_text_color(Color::BLUE);
let child = WidgetStyle::default();
let inherited = child.inherit_from(&parent);
assert_eq!(inherited.background_color, Some(Color::RED));
assert_eq!(inherited.text_color, Some(Color::BLUE));
}
#[test]
fn inherit_from_child_takes_precedence() {
let parent = WidgetStyle::default().with_background(Color::RED);
let child = WidgetStyle::default().with_background(Color::GREEN);
let inherited = child.inherit_from(&parent);
assert_eq!(inherited.background_color, Some(Color::GREEN));
}
#[test]
fn merge_updates_missing_properties() {
let mut base = WidgetStyle::default().with_background(Color::RED);
let overlay = WidgetStyle::default().with_text_color(Color::BLUE);
base.merge(&overlay);
assert_eq!(base.background_color, Some(Color::RED));
assert_eq!(base.text_color, Some(Color::BLUE));
}
#[test]
fn merge_does_not_override_existing() {
let mut base = WidgetStyle::default().with_background(Color::RED);
let overlay = WidgetStyle::default().with_background(Color::GREEN);
base.merge(&overlay);
assert_eq!(base.background_color, Some(Color::RED)); }
#[test]
fn each_padding_layer_overrides_only_the_one_below_it() {
let resolved = PaddingSpec::new().padding(8).horizontal(12).resolve();
assert_eq!(resolved.top, 8, "the vertical pair keeps the general value");
assert_eq!(resolved.bottom, 8);
assert_eq!(resolved.left, 12);
assert_eq!(resolved.right, 12);
}
#[test]
fn a_side_overrides_its_axis_and_the_axis_overrides_the_uniform_layer() {
let resolved = PaddingSpec::new().padding(4).horizontal(10).left(20).resolve();
assert_eq!(resolved.left, 20, "the most specific layer wins");
assert_eq!(resolved.right, 10, "its sibling keeps the axis value");
assert_eq!(resolved.top, 4, "the untouched axis keeps the general value");
assert_eq!(resolved.bottom, 4);
}
#[test]
fn writing_a_side_does_not_disturb_the_other_three() {
let before = PaddingSpec::new().padding(6).resolve();
let after = PaddingSpec::new().padding(6).left(15).resolve();
assert_eq!(after.left, 15);
assert_eq!(after.top, before.top);
assert_eq!(after.right, before.right);
assert_eq!(after.bottom, before.bottom);
}
#[test]
fn a_more_specific_layer_wins_even_if_it_was_set_first() {
let resolved = PaddingSpec::new().left(20).padding(4).resolve();
assert_eq!(resolved.left, 20);
assert_eq!(resolved.right, 4);
}
#[test]
fn an_empty_spec_resolves_to_no_padding() {
let spec = PaddingSpec::new();
assert!(spec.is_empty());
assert_eq!(spec.resolve(), EdgeOffsets::all(0));
assert!(!PaddingSpec::new().padding(0).is_empty(), "an explicit 0 is still a statement");
}
#[test]
fn the_vertical_and_horizontal_axes_are_independent() {
let resolved = PaddingSpec::new().vertical(2).horizontal(9).resolve();
assert_eq!((resolved.top, resolved.bottom), (2, 2));
assert_eq!((resolved.left, resolved.right), (9, 9));
}
#[test]
fn a_style_can_be_given_a_padding_spec_and_a_spacing() {
let style = WidgetStyle::default()
.with_padding_spec(PaddingSpec::new().padding(8).horizontal(12))
.with_spacing(6);
assert_eq!(style.padding.top, 8);
assert_eq!(style.padding.horizontal_total(), 24);
assert_eq!(style.spacing, Some(6));
}
}