Skip to main content

oxiui_theme/
tokens.rs

1//! Design tokens: spacing, border-radius, elevation, and opacity scales.
2//!
3//! These provide a consistent, theme-wide vocabulary of sizes so that widgets
4//! reference semantic steps (`spacing.md`) rather than magic numbers.
5
6/// A named step within the spacing scale.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum SpacingStep {
9    /// Extra-small spacing (tightest).
10    Xs,
11    /// Small spacing.
12    Sm,
13    /// Medium spacing.
14    Md,
15    /// Large spacing.
16    Lg,
17    /// Extra-large spacing.
18    Xl,
19    /// Double extra-large spacing.
20    Xxl,
21    /// Triple extra-large spacing (loosest).
22    Xxxl,
23}
24
25/// A named step within the border-radius scale.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum RadiusStep {
28    /// No rounding (sharp corners).
29    None,
30    /// Small radius.
31    Sm,
32    /// Medium radius.
33    Md,
34    /// Large radius.
35    Lg,
36    /// Extra-large radius.
37    Xl,
38    /// Fully rounded (pill / circle).
39    Full,
40}
41
42/// The full set of design tokens for a theme.
43///
44/// Arrays are indexed by their corresponding step enum's discriminant order.
45#[derive(Clone, Debug, PartialEq)]
46pub struct DesignTokens {
47    /// Spacing scale in logical pixels (7 steps, `Xs`..`Xxxl`).
48    pub spacing: [f32; 7],
49    /// Border-radius scale in logical pixels (6 steps, `None`..`Full`).
50    pub radius: [f32; 6],
51    /// Elevation blur radii in logical pixels (6 levels, 0..=5).
52    pub elevation: [f32; 6],
53    /// Opacity levels in `[0, 1]` (5 steps: disabled..opaque).
54    pub opacity: [f32; 5],
55}
56
57impl DesignTokens {
58    /// Spacing value (logical px) for a named step.
59    pub fn spacing(&self, step: SpacingStep) -> f32 {
60        let i = match step {
61            SpacingStep::Xs => 0,
62            SpacingStep::Sm => 1,
63            SpacingStep::Md => 2,
64            SpacingStep::Lg => 3,
65            SpacingStep::Xl => 4,
66            SpacingStep::Xxl => 5,
67            SpacingStep::Xxxl => 6,
68        };
69        self.spacing[i]
70    }
71
72    /// Border-radius value (logical px) for a named step.
73    pub fn radius(&self, step: RadiusStep) -> f32 {
74        let i = match step {
75            RadiusStep::None => 0,
76            RadiusStep::Sm => 1,
77            RadiusStep::Md => 2,
78            RadiusStep::Lg => 3,
79            RadiusStep::Xl => 4,
80            RadiusStep::Full => 5,
81        };
82        self.radius[i]
83    }
84
85    /// Elevation blur radius (logical px) for level `0..=5` (clamped).
86    pub fn elevation(&self, level: usize) -> f32 {
87        self.elevation[level.min(self.elevation.len() - 1)]
88    }
89}
90
91impl Default for DesignTokens {
92    /// The COOLJAPAN default scale: 4-px-based spacing, conventional radii.
93    fn default() -> Self {
94        Self {
95            // 4 / 8 / 12 / 16 / 24 / 32 / 48 — all multiples of 4.
96            spacing: [4.0, 8.0, 12.0, 16.0, 24.0, 32.0, 48.0],
97            // none / sm / md / lg / xl / full.
98            radius: [0.0, 2.0, 4.0, 8.0, 16.0, 9999.0],
99            // 0..5 shadow blur.
100            elevation: [0.0, 1.0, 3.0, 6.0, 12.0, 24.0],
101            // disabled / muted / secondary / high / opaque.
102            opacity: [0.38, 0.60, 0.74, 0.87, 1.0],
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn spacing_is_multiple_of_four() {
113        let t = DesignTokens::default();
114        for v in t.spacing {
115            assert_eq!(v % 4.0, 0.0, "spacing {v} not a multiple of 4");
116        }
117    }
118
119    #[test]
120    fn spacing_is_monotonic() {
121        let t = DesignTokens::default();
122        for w in t.spacing.windows(2) {
123            assert!(w[1] > w[0], "spacing scale must increase");
124        }
125    }
126
127    #[test]
128    fn radius_non_negative_and_named_lookup() {
129        let t = DesignTokens::default();
130        for v in t.radius {
131            assert!(v >= 0.0);
132        }
133        assert_eq!(t.radius(RadiusStep::None), 0.0);
134        assert!(t.radius(RadiusStep::Full) > t.radius(RadiusStep::Lg));
135    }
136
137    #[test]
138    fn named_spacing_lookup() {
139        let t = DesignTokens::default();
140        assert_eq!(t.spacing(SpacingStep::Xs), 4.0);
141        assert_eq!(t.spacing(SpacingStep::Md), 12.0);
142        assert_eq!(t.spacing(SpacingStep::Xxxl), 48.0);
143    }
144
145    #[test]
146    fn elevation_clamps() {
147        let t = DesignTokens::default();
148        assert_eq!(t.elevation(0), 0.0);
149        assert_eq!(t.elevation(5), 24.0);
150        // Out-of-range clamps to the last level.
151        assert_eq!(t.elevation(99), 24.0);
152    }
153
154    #[test]
155    fn opacity_in_range_and_monotonic() {
156        let t = DesignTokens::default();
157        for v in t.opacity {
158            assert!((0.0..=1.0).contains(&v));
159        }
160        for w in t.opacity.windows(2) {
161            assert!(w[1] > w[0]);
162        }
163    }
164}