Skip to main content

rosace_theme/
typography.rs

1//! Typography tokens: font families, weights, and the type scale.
2
3/// The font family to use for a text style.
4#[derive(Debug, Clone, PartialEq)]
5pub enum FontFamily {
6    /// The operating system's default UI font.
7    System,
8    /// The operating system's default monospaced font.
9    Monospace,
10    /// A custom font loaded by name.
11    Custom(String),
12}
13
14/// Numeric font weight following the CSS/OpenType convention.
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum FontWeight {
17    Thin = 100,
18    Light = 300,
19    Regular = 400,
20    Medium = 500,
21    SemiBold = 600,
22    Bold = 700,
23    ExtraBold = 800,
24    Black = 900,
25}
26
27/// A complete description of how a piece of text should be rendered.
28#[derive(Debug, Clone)]
29pub struct TextStyle {
30    pub family: FontFamily,
31    /// Font size in logical pixels.
32    pub size: f32,
33    pub weight: FontWeight,
34    /// Line height as a multiplier of `size`. Defaults to 1.4.
35    pub line_height: f32,
36    /// Additional letter spacing in logical pixels. Defaults to 0.0.
37    pub letter_spacing: f32,
38}
39
40impl TextStyle {
41    /// Builds a `TextStyle` with system font, regular weight, and default
42    /// line-height / letter-spacing.
43    pub fn new(size: f32, weight: FontWeight) -> Self {
44        Self {
45            family: FontFamily::System,
46            size,
47            weight,
48            line_height: 1.4,
49            letter_spacing: 0.0,
50        }
51    }
52}
53
54/// The complete Material Design 3–inspired type scale, bumped +1px across
55/// every step (2026-08-03, user-requested: the MD3 baseline still read
56/// small in practice) — the whole point of routing widgets through this
57/// ONE scale (D127) instead of their own literals is that a tweak like
58/// this now takes effect everywhere at once, with zero widget-file edits.
59#[derive(Debug, Clone)]
60pub struct Typography {
61    pub display_large: TextStyle,   // 58 px
62    pub display_medium: TextStyle,  // 46 px
63    pub display_small: TextStyle,   // 37 px
64    pub headline_large: TextStyle,  // 33 px
65    pub headline_medium: TextStyle, // 29 px
66    pub headline_small: TextStyle,  // 25 px
67    pub title_large: TextStyle,     // 23 px
68    pub title_medium: TextStyle,    // 17 px, medium weight
69    pub title_small: TextStyle,     // 15 px, medium weight
70    pub body_large: TextStyle,      // 17 px
71    pub body_medium: TextStyle,     // 15 px
72    pub body_small: TextStyle,      // 13 px
73    pub label_large: TextStyle,     // 15 px, medium
74    pub label_medium: TextStyle,    // 13 px, medium
75    pub label_small: TextStyle,     // 12 px, medium
76}
77
78impl Default for Typography {
79    fn default() -> Self {
80        Self {
81            display_large:   TextStyle::new(58.0, FontWeight::Regular),
82            display_medium:  TextStyle::new(46.0, FontWeight::Regular),
83            display_small:   TextStyle::new(37.0, FontWeight::Regular),
84            headline_large:  TextStyle::new(33.0, FontWeight::Regular),
85            headline_medium: TextStyle::new(29.0, FontWeight::Regular),
86            headline_small:  TextStyle::new(25.0, FontWeight::Regular),
87            title_large:     TextStyle::new(23.0, FontWeight::Regular),
88            title_medium:    TextStyle::new(17.0, FontWeight::Medium),
89            title_small:     TextStyle::new(15.0, FontWeight::Medium),
90            body_large:      TextStyle::new(17.0, FontWeight::Regular),
91            body_medium:     TextStyle::new(15.0, FontWeight::Regular),
92            body_small:      TextStyle::new(13.0, FontWeight::Regular),
93            label_large:     TextStyle::new(15.0, FontWeight::Medium),
94            label_medium:    TextStyle::new(13.0, FontWeight::Medium),
95            label_small:     TextStyle::new(12.0, FontWeight::Medium),
96        }
97    }
98}
99
100// ---------------------------------------------------------------------------
101// Tests
102// ---------------------------------------------------------------------------
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn typography_scale_sizes_are_ordered() {
110        let t = Typography::default();
111        assert!(t.display_large.size > t.headline_large.size,
112            "display_large ({}) should be larger than headline_large ({})",
113            t.display_large.size, t.headline_large.size);
114        assert!(t.headline_large.size > t.body_large.size,
115            "headline_large ({}) should be larger than body_large ({})",
116            t.headline_large.size, t.body_large.size);
117    }
118
119    #[test]
120    fn typography_default_display_large_is_58px() {
121        let t = Typography::default();
122        assert_eq!(t.display_large.size, 58.0);
123    }
124
125    #[test]
126    fn typography_title_medium_is_medium_weight() {
127        let t = Typography::default();
128        assert_eq!(t.title_medium.weight, FontWeight::Medium);
129    }
130
131    #[test]
132    fn typography_default_line_height() {
133        let t = Typography::default();
134        assert!((t.body_large.line_height - 1.4).abs() < 1e-6);
135    }
136
137    #[test]
138    fn font_family_custom_stores_name() {
139        let f = FontFamily::Custom("Inter".to_string());
140        assert_eq!(f, FontFamily::Custom("Inter".to_string()));
141        assert_ne!(f, FontFamily::System);
142    }
143}