Skip to main content

frust_material/tokens/
metrics.rs

1//! Material 3 Expressive elevation, spacing, and dimension tokens.
2//!
3//! Elevation, spacing, and radius values sourced from Material 3 Expressive design tokens:
4//! - Elevation: <https://m3.material.io/styles/elevation> (verified 2026-07-17, reference
5//!   v0.192 dp values with two-layer shadow model)
6//! - Spacing: Material 3 Expressive 6-step scale (xs=4…xxl=32dp)
7//! - Dimensions: Material 3 Expressive spacing (0–48dp) and radius tokens
8
9use frust::{Elevation, ElevationLevel, ShadowSpec, SurfaceRole};
10
11// ---- Elevation ----------------------------------------------------------
12
13/// dp source: <https://m3.material.io/styles/elevation> (verified
14/// 2026-07-17): L0 0dp, L1 1dp, L2 3dp, L3 6dp, L4 8dp, L5 12dp. Component
15/// mapping per the same source: L1 = elevated cards/bottom sheets, L2 = nav
16/// bar/menus, L3 = FAB/dialogs.
17///
18/// **Shadow math is TUNABLE, not an M3-published spec.** Material 3's 2023
19/// direction replaced tonal-overlay tinting with *static surface-container
20/// roles* for most elevated surfaces — the opposite of "primarily tonal
21/// overlay post-2023" — but shadows still exist alongside; M3 does not
22/// publish exact shadow blur/offset math, so this module defines a
23/// documented v1 mapping: `y_offset = dp / 2.0 + 1.0`, `blur_std_dev = dp`,
24/// shadow color = `ColorScheme::shadow` at `color_alpha` ~0.3. Treat it as
25/// adjustable, not load-bearing, Frust-specific policy.
26///
27/// The reference's Dart implementation uses a two-layer shadow model:
28/// - Key shadow: 30% alpha, offset = dp/2, blur = dp
29/// - Ambient shadow: 15% alpha, offset = dp, blur = dp*2
30///
31/// This Rust mapping uses a single shadow per the `ElevationLevel` type contract.
32const fn elevation_level(dp: f64, surface_role: SurfaceRole) -> ElevationLevel {
33    let shadow = ShadowSpec {
34        y_offset: dp / 2.0 + 1.0,
35        blur_std_dev: dp,
36        color_alpha: 0.3,
37    };
38    ElevationLevel {
39        dp,
40        shadow_light: shadow,
41        shadow_dark: shadow,
42        surface_role,
43    }
44}
45
46/// The Material 3 baseline elevation table (dp values verified; shadow math
47/// and surface-role assignment are this crate's documented v1 mapping — see
48/// [`elevation_level`]'s doc comment).
49pub const fn elevation() -> Elevation {
50    Elevation {
51        level0: elevation_level(0.0, SurfaceRole::Surface),
52        level1: elevation_level(1.0, SurfaceRole::SurfaceContainerLow),
53        level2: elevation_level(3.0, SurfaceRole::SurfaceContainer),
54        level3: elevation_level(6.0, SurfaceRole::SurfaceContainerHigh),
55        level4: elevation_level(8.0, SurfaceRole::SurfaceContainerHigh),
56        level5: elevation_level(12.0, SurfaceRole::SurfaceContainerHighest),
57    }
58}
59
60// ---- Spacing ---------------------------------------------------------------
61
62/// Material 3 Expressive spacing scale (6-step tokens). Values are in logical pixels (dp).
63/// Reference: Material 3 Expressive design tokens (verified 2026-08-19, matching
64/// `material_3_expressive/lib/foundations/m3e_spacing.dart`).
65///
66/// These are static token values; components import them directly without theme plumbing.
67pub struct MaterialSpacing;
68
69impl MaterialSpacing {
70    /// Extra-small gap: 4dp.
71    pub const XS: f64 = 4.0;
72
73    /// Small gap: 8dp.
74    pub const SM: f64 = 8.0;
75
76    /// Medium gap: 12dp.
77    pub const MD: f64 = 12.0;
78
79    /// Large gap: 16dp.
80    pub const LG: f64 = 16.0;
81
82    /// Extra-large gap: 24dp.
83    pub const XL: f64 = 24.0;
84
85    /// Extra-extra-large gap: 32dp.
86    pub const XXL: f64 = 32.0;
87}
88
89// ---- Dimensions ---------------------------------------------------------
90
91/// Material 3 Expressive dimension tokens: spacing and radius scales.
92/// Reference: Material 3 Expressive design tokens (verified 2026-08-19,
93/// `material_3_expressive/lib/foundations/m3e_dimensions.dart`).
94///
95/// Spacing follows a 4dp grid; radius scales match shape tokens.
96/// These are static token values; components import them directly.
97pub struct MaterialDimensions;
98
99impl MaterialDimensions {
100    // ---- Spacing scale (0–48dp on 4dp grid) ----
101
102    /// 0dp spacing.
103    pub const SPACE_0: f64 = 0.0;
104
105    /// 4dp spacing.
106    pub const SPACE_4: f64 = 4.0;
107
108    /// 8dp spacing.
109    pub const SPACE_8: f64 = 8.0;
110
111    /// 12dp spacing.
112    pub const SPACE_12: f64 = 12.0;
113
114    /// 16dp spacing.
115    pub const SPACE_16: f64 = 16.0;
116
117    /// 20dp spacing.
118    pub const SPACE_20: f64 = 20.0;
119
120    /// 24dp spacing.
121    pub const SPACE_24: f64 = 24.0;
122
123    /// 28dp spacing.
124    pub const SPACE_28: f64 = 28.0;
125
126    /// 32dp spacing.
127    pub const SPACE_32: f64 = 32.0;
128
129    /// 40dp spacing.
130    pub const SPACE_40: f64 = 40.0;
131
132    /// 48dp spacing.
133    pub const SPACE_48: f64 = 48.0;
134
135    // ---- Theme spacing aliases ----
136
137    /// Theme extra-small gap (`MaterialSpacing::XS`).
138    pub const SPACE_XS: f64 = Self::SPACE_4;
139
140    /// Theme small gap (`MaterialSpacing::SM`).
141    pub const SPACE_SM: f64 = Self::SPACE_8;
142
143    /// Theme medium gap (`MaterialSpacing::MD`).
144    pub const SPACE_MD: f64 = Self::SPACE_12;
145
146    /// Theme large gap (`MaterialSpacing::LG`).
147    pub const SPACE_LG: f64 = Self::SPACE_16;
148
149    /// Theme extra-large gap (`MaterialSpacing::XL`).
150    pub const SPACE_XL: f64 = Self::SPACE_24;
151
152    /// Theme extra-extra-large gap (`MaterialSpacing::XXL`).
153    pub const SPACE_XXL: f64 = Self::SPACE_32;
154
155    // ---- Radius scale ----
156
157    /// 0dp — sharp corners.
158    pub const RADIUS_NONE: f64 = 0.0;
159
160    /// 4dp corner radius.
161    pub const RADIUS_EXTRA_SMALL: f64 = 4.0;
162
163    /// 8dp corner radius.
164    pub const RADIUS_SMALL: f64 = 8.0;
165
166    /// 12dp corner radius.
167    pub const RADIUS_MEDIUM: f64 = 12.0;
168
169    /// 16dp corner radius.
170    pub const RADIUS_LARGE: f64 = 16.0;
171
172    /// 20dp corner radius (expressive large+).
173    pub const RADIUS_LARGE_INCREASED: f64 = 20.0;
174
175    /// 28dp corner radius.
176    pub const RADIUS_EXTRA_LARGE: f64 = 28.0;
177
178    /// 32dp corner radius (expressive XL+).
179    pub const RADIUS_EXTRA_LARGE_INCREASED: f64 = 32.0;
180
181    /// 48dp corner radius.
182    pub const RADIUS_EXTRA_EXTRA_LARGE: f64 = 48.0;
183
184    /// Sentinel radius that reads as a stadium for typical heights.
185    pub const RADIUS_FULL: f64 = f64::INFINITY;
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn elevation_dp_matches_reference() {
194        let e = elevation();
195        assert_eq!(e.level0.dp, 0.0);
196        assert_eq!(e.level1.dp, 1.0);
197        assert_eq!(e.level2.dp, 3.0);
198        assert_eq!(e.level3.dp, 6.0);
199        assert_eq!(e.level4.dp, 8.0);
200        assert_eq!(e.level5.dp, 12.0);
201    }
202
203    #[test]
204    fn elevation_shadow_is_identical_on_both_brightnesses() {
205        let e = elevation();
206        assert_eq!(e.level0.shadow_light, e.level0.shadow_dark);
207        assert_eq!(e.level1.shadow_light, e.level1.shadow_dark);
208        assert_eq!(e.level2.shadow_light, e.level2.shadow_dark);
209        assert_eq!(e.level3.shadow_light, e.level3.shadow_dark);
210        assert_eq!(e.level4.shadow_light, e.level4.shadow_dark);
211        assert_eq!(e.level5.shadow_light, e.level5.shadow_dark);
212    }
213
214    #[test]
215    fn spacing_scale_matches_reference() {
216        assert_eq!(MaterialSpacing::XS, 4.0);
217        assert_eq!(MaterialSpacing::SM, 8.0);
218        assert_eq!(MaterialSpacing::MD, 12.0);
219        assert_eq!(MaterialSpacing::LG, 16.0);
220        assert_eq!(MaterialSpacing::XL, 24.0);
221        assert_eq!(MaterialSpacing::XXL, 32.0);
222    }
223
224    #[test]
225    fn spacing_tokens_match_reference() {
226        assert_eq!(MaterialDimensions::SPACE_0, 0.0);
227        assert_eq!(MaterialDimensions::SPACE_4, 4.0);
228        assert_eq!(MaterialDimensions::SPACE_8, 8.0);
229        assert_eq!(MaterialDimensions::SPACE_12, 12.0);
230        assert_eq!(MaterialDimensions::SPACE_16, 16.0);
231        assert_eq!(MaterialDimensions::SPACE_20, 20.0);
232        assert_eq!(MaterialDimensions::SPACE_24, 24.0);
233        assert_eq!(MaterialDimensions::SPACE_28, 28.0);
234        assert_eq!(MaterialDimensions::SPACE_32, 32.0);
235        assert_eq!(MaterialDimensions::SPACE_40, 40.0);
236        assert_eq!(MaterialDimensions::SPACE_48, 48.0);
237    }
238
239    #[test]
240    fn spacing_aliases_match_reference() {
241        assert_eq!(MaterialDimensions::SPACE_XS, MaterialDimensions::SPACE_4);
242        assert_eq!(MaterialDimensions::SPACE_SM, MaterialDimensions::SPACE_8);
243        assert_eq!(MaterialDimensions::SPACE_MD, MaterialDimensions::SPACE_12);
244        assert_eq!(MaterialDimensions::SPACE_LG, MaterialDimensions::SPACE_16);
245        assert_eq!(MaterialDimensions::SPACE_XL, MaterialDimensions::SPACE_24);
246        assert_eq!(MaterialDimensions::SPACE_XXL, MaterialDimensions::SPACE_32);
247    }
248
249    #[test]
250    fn radius_tokens_match_reference() {
251        assert_eq!(MaterialDimensions::RADIUS_NONE, 0.0);
252        assert_eq!(MaterialDimensions::RADIUS_EXTRA_SMALL, 4.0);
253        assert_eq!(MaterialDimensions::RADIUS_SMALL, 8.0);
254        assert_eq!(MaterialDimensions::RADIUS_MEDIUM, 12.0);
255        assert_eq!(MaterialDimensions::RADIUS_LARGE, 16.0);
256        assert_eq!(MaterialDimensions::RADIUS_LARGE_INCREASED, 20.0);
257        assert_eq!(MaterialDimensions::RADIUS_EXTRA_LARGE, 28.0);
258        assert_eq!(MaterialDimensions::RADIUS_EXTRA_LARGE_INCREASED, 32.0);
259        assert_eq!(MaterialDimensions::RADIUS_EXTRA_EXTRA_LARGE, 48.0);
260        assert!(MaterialDimensions::RADIUS_FULL.is_infinite());
261    }
262}