Skip to main content

oxiui_theme/
typography.rs

1//! Typographic scale: named text roles with size, line-height, weight, spacing.
2
3/// A resolved text style for one typographic role.
4#[derive(Clone, Copy, Debug, PartialEq)]
5pub struct TextStyleToken {
6    /// Font size in logical pixels.
7    pub size: f32,
8    /// Line height in logical pixels (total line box height).
9    pub line_height: f32,
10    /// Letter spacing in logical pixels (may be negative for tight display text).
11    pub letter_spacing: f32,
12    /// Font weight (100 thin … 900 black; 400 regular).
13    pub weight: u16,
14}
15
16impl TextStyleToken {
17    /// Construct a text-style token.
18    pub const fn new(size: f32, line_height: f32, letter_spacing: f32, weight: u16) -> Self {
19        Self {
20            size,
21            line_height,
22            letter_spacing,
23            weight,
24        }
25    }
26}
27
28/// The set of named typographic roles for a theme, largest to smallest.
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct TypographyScale {
31    /// Largest display text (hero headings).
32    pub display: TextStyleToken,
33    /// Section headline.
34    pub headline: TextStyleToken,
35    /// Subsection title.
36    pub title: TextStyleToken,
37    /// Body / paragraph text.
38    pub body: TextStyleToken,
39    /// Small caption / helper text.
40    pub caption: TextStyleToken,
41    /// Smallest overline / label text (often uppercased).
42    pub overline: TextStyleToken,
43}
44
45impl Default for TypographyScale {
46    /// A conventional Material-ish scale anchored at a 14-px body.
47    fn default() -> Self {
48        Self {
49            display: TextStyleToken::new(32.0, 40.0, -0.5, 700),
50            headline: TextStyleToken::new(24.0, 32.0, -0.25, 600),
51            title: TextStyleToken::new(18.0, 24.0, 0.0, 600),
52            body: TextStyleToken::new(14.0, 20.0, 0.0, 400),
53            caption: TextStyleToken::new(12.0, 16.0, 0.2, 400),
54            overline: TextStyleToken::new(10.0, 14.0, 1.0, 500),
55        }
56    }
57}
58
59impl TypographyScale {
60    /// All roles ordered largest → smallest by size.
61    pub fn roles_descending(&self) -> [TextStyleToken; 6] {
62        [
63            self.display,
64            self.headline,
65            self.title,
66            self.body,
67            self.caption,
68            self.overline,
69        ]
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn sizes_are_monotonic_descending() {
79        let s = TypographyScale::default();
80        let roles = s.roles_descending();
81        for w in roles.windows(2) {
82            assert!(
83                w[0].size >= w[1].size,
84                "scale must not increase as it descends"
85            );
86        }
87        // Strict ordering of the key roles.
88        assert!(s.caption.size < s.body.size);
89        assert!(s.body.size < s.title.size);
90        assert!(s.title.size < s.headline.size);
91        assert!(s.headline.size < s.display.size);
92    }
93
94    #[test]
95    fn line_height_at_least_size() {
96        let s = TypographyScale::default();
97        for role in s.roles_descending() {
98            assert!(
99                role.line_height >= role.size,
100                "line height must cover the glyph size"
101            );
102        }
103    }
104}