/// Spacing scale — 4px base grid. /// /// Use named scale values, not raw numbers. This ensures visual consistency /// and makes it easy to tweak the entire system by changing the base unit. /// Named spacing scale values. /// /// The base unit is 4dp/pt. All values are multiples of 4. /// Use these for padding, margin, gap, and any other spatial measurement. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Spacing { /// 0 — no spacing None, /// 4dp — hairline gap, tight list items Xs, /// 8dp — default tight padding, icon margins Sm, /// 12dp — compact component padding Md, /// 16dp — standard component padding (the workhorse) Lg, /// 24dp — generous padding, card internal spacing Xl, /// 32dp — section separation Xxl, /// 48dp — hero sections, major layout gaps Xxxl, /// 64dp — page-level margins, maximum separation Max, /// Custom value in dp (escape hatch) Custom(u32), } impl Spacing { /// Resolve to a concrete dp/pt value. pub fn dp(&self) -> u32 { match self { Spacing::None => 0, Spacing::Xs => 4, Spacing::Sm => 8, Spacing::Md => 12, Spacing::Lg => 16, Spacing::Xl => 24, Spacing::Xxl => 32, Spacing::Xxxl => 48, Spacing::Max => 64, Spacing::Custom(v) => *v, } } /// Resolve to a concrete CSS pixel string. pub fn to_css(&self) -> String { format!("{}px", self.dp()) } } /// The spacing scale exposed by a theme. /// Provides the mapping from scale names to concrete values. #[derive(Debug, Clone)] pub struct SpacingScale { /// Base unit in dp (default: 4). pub base: u32, } impl SpacingScale { pub fn default() -> Self { Self { base: 4 } } /// Resolve a Spacing token to dp. pub fn resolve(&self, spacing: &Spacing) -> u32 { match spacing { Spacing::Custom(v) => *v, other => { let multiplier = match other { Spacing::None => 0, Spacing::Xs => 1, Spacing::Sm => 2, Spacing::Md => 3, Spacing::Lg => 4, Spacing::Xl => 6, Spacing::Xxl => 8, Spacing::Xxxl => 12, Spacing::Max => 16, Spacing::Custom(_) => unreachable!(), }; self.base * multiplier } } } } #[cfg(test)] mod tests { use super::*; #[test] fn spacing_none_is_zero() { assert_eq!(Spacing::None.dp(), 0); } #[test] fn spacing_lg_is_16() { assert_eq!(Spacing::Lg.dp(), 16); } #[test] fn spacing_max_is_64() { assert_eq!(Spacing::Max.dp(), 64); } #[test] fn spacing_custom() { assert_eq!(Spacing::Custom(20).dp(), 20); } #[test] fn spacing_scale_resolves_base_unit() { let scale = SpacingScale { base: 4 }; assert_eq!(scale.resolve(&Spacing::Xs), 4); assert_eq!(scale.resolve(&Spacing::Sm), 8); assert_eq!(scale.resolve(&Spacing::Lg), 16); } #[test] fn spacing_to_css() { assert_eq!(Spacing::Lg.to_css(), "16px"); assert_eq!(Spacing::None.to_css(), "0px"); } #[test] fn spacing_scale_custom_scale() { let scale = SpacingScale { base: 8 }; assert_eq!(scale.resolve(&Spacing::Xs), 8); assert_eq!(scale.resolve(&Spacing::Sm), 16); } }