Skip to main content

autom8/ui/gui/
typography.rs

1//! Typography configuration for the GUI.
2//!
3//! This module provides custom font embedding and type scale definitions
4//! for consistent visual hierarchy throughout the application.
5
6use eframe::egui::{self, FontData, FontDefinitions, FontFamily, FontId, TextStyle};
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10/// Embedded Geist Sans Regular font data.
11const GEIST_REGULAR: &[u8] = include_bytes!("fonts/Geist-Regular.ttf");
12
13/// Embedded Geist Sans Medium font data.
14const GEIST_MEDIUM: &[u8] = include_bytes!("fonts/Geist-Medium.ttf");
15
16/// Embedded Geist Sans SemiBold font data.
17const GEIST_SEMIBOLD: &[u8] = include_bytes!("fonts/Geist-SemiBold.ttf");
18
19/// Embedded Geist Mono Regular font data.
20const GEIST_MONO_REGULAR: &[u8] = include_bytes!("fonts/GeistMono-Regular.ttf");
21
22/// Font family identifier for Geist Sans Regular.
23pub const FAMILY_GEIST_REGULAR: &str = "Geist-Regular";
24
25/// Font family identifier for Geist Sans Medium.
26pub const FAMILY_GEIST_MEDIUM: &str = "Geist-Medium";
27
28/// Font family identifier for Geist Sans SemiBold.
29pub const FAMILY_GEIST_SEMIBOLD: &str = "Geist-SemiBold";
30
31/// Font family identifier for Geist Mono.
32pub const FAMILY_GEIST_MONO: &str = "Geist-Mono";
33
34/// Standard font sizes for the type scale.
35///
36/// This defines a consistent set of sizes used throughout the application
37/// to maintain visual hierarchy.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum FontSize {
40    /// Extra small text for captions or tertiary information (10px).
41    Caption,
42    /// Small text for labels and secondary information (12px).
43    Small,
44    /// Standard body text size (14px).
45    Body,
46    /// Slightly larger text for emphasis (16px).
47    Large,
48    /// Section headings (18px).
49    Heading,
50    /// Page or view titles (24px).
51    Title,
52    /// Large display text (32px).
53    Display,
54}
55
56impl FontSize {
57    /// Returns the pixel size for this font size.
58    pub fn pixels(self) -> f32 {
59        match self {
60            FontSize::Caption => 10.0,
61            FontSize::Small => 12.0,
62            FontSize::Body => 14.0,
63            FontSize::Large => 16.0,
64            FontSize::Heading => 18.0,
65            FontSize::Title => 24.0,
66            FontSize::Display => 32.0,
67        }
68    }
69}
70
71/// Font weight variants available in the application.
72#[derive(Debug, Clone, Copy, PartialEq)]
73pub enum FontWeight {
74    /// Regular weight (400).
75    Regular,
76    /// Medium weight (500).
77    Medium,
78    /// SemiBold weight (600).
79    SemiBold,
80}
81
82impl FontWeight {
83    /// Returns the FontFamily for this weight.
84    pub fn family(self) -> FontFamily {
85        match self {
86            FontWeight::Regular => FontFamily::Name(FAMILY_GEIST_REGULAR.into()),
87            FontWeight::Medium => FontFamily::Name(FAMILY_GEIST_MEDIUM.into()),
88            FontWeight::SemiBold => FontFamily::Name(FAMILY_GEIST_SEMIBOLD.into()),
89        }
90    }
91}
92
93/// Create a FontId with the specified size and weight.
94pub fn font(size: FontSize, weight: FontWeight) -> FontId {
95    FontId::new(size.pixels(), weight.family())
96}
97
98/// Create a FontId for monospace text at the specified size.
99pub fn mono(size: FontSize) -> FontId {
100    FontId::new(size.pixels(), FontFamily::Name(FAMILY_GEIST_MONO.into()))
101}
102
103/// Returns the approximate line height for a given font size.
104///
105/// Uses a standard 1.4x multiplier on the font pixel size to account
106/// for line spacing.
107pub fn line_height(size: FontSize) -> f32 {
108    size.pixels() * 1.4
109}
110
111/// Create FontDefinitions with embedded Geist fonts.
112///
113/// This function configures egui to use the Geist font family as the default
114/// for all text rendering. It sets up:
115/// - Geist Sans (Regular, Medium, SemiBold) as custom font families
116/// - Geist Mono as the monospace font
117/// - Default text styles mapped to appropriate sizes and weights
118pub fn configure_fonts() -> FontDefinitions {
119    let mut fonts = FontDefinitions::default();
120
121    // Insert font data wrapped in Arc
122    fonts.font_data.insert(
123        FAMILY_GEIST_REGULAR.to_owned(),
124        Arc::new(FontData::from_static(GEIST_REGULAR)),
125    );
126    fonts.font_data.insert(
127        FAMILY_GEIST_MEDIUM.to_owned(),
128        Arc::new(FontData::from_static(GEIST_MEDIUM)),
129    );
130    fonts.font_data.insert(
131        FAMILY_GEIST_SEMIBOLD.to_owned(),
132        Arc::new(FontData::from_static(GEIST_SEMIBOLD)),
133    );
134    fonts.font_data.insert(
135        FAMILY_GEIST_MONO.to_owned(),
136        Arc::new(FontData::from_static(GEIST_MONO_REGULAR)),
137    );
138
139    // Create custom font families
140    fonts.families.insert(
141        FontFamily::Name(FAMILY_GEIST_REGULAR.into()),
142        vec![FAMILY_GEIST_REGULAR.to_owned()],
143    );
144    fonts.families.insert(
145        FontFamily::Name(FAMILY_GEIST_MEDIUM.into()),
146        vec![FAMILY_GEIST_MEDIUM.to_owned()],
147    );
148    fonts.families.insert(
149        FontFamily::Name(FAMILY_GEIST_SEMIBOLD.into()),
150        vec![FAMILY_GEIST_SEMIBOLD.to_owned()],
151    );
152    fonts.families.insert(
153        FontFamily::Name(FAMILY_GEIST_MONO.into()),
154        vec![FAMILY_GEIST_MONO.to_owned()],
155    );
156
157    // Set Geist Regular as the default proportional font
158    fonts
159        .families
160        .entry(FontFamily::Proportional)
161        .or_default()
162        .insert(0, FAMILY_GEIST_REGULAR.to_owned());
163
164    // Set Geist Mono as the default monospace font
165    fonts
166        .families
167        .entry(FontFamily::Monospace)
168        .or_default()
169        .insert(0, FAMILY_GEIST_MONO.to_owned());
170
171    fonts
172}
173
174/// Configure default text styles with the custom type scale.
175///
176/// This maps egui's built-in TextStyle variants to appropriate font sizes
177/// and weights from our type system.
178pub fn configure_text_styles(ctx: &egui::Context) {
179    let mut style = (*ctx.style()).clone();
180
181    // Map TextStyles to our type scale
182    let mut text_styles = BTreeMap::new();
183    text_styles.insert(
184        TextStyle::Small,
185        FontId::new(FontSize::Small.pixels(), FontFamily::Proportional),
186    );
187    text_styles.insert(
188        TextStyle::Body,
189        FontId::new(FontSize::Body.pixels(), FontFamily::Proportional),
190    );
191    text_styles.insert(
192        TextStyle::Button,
193        FontId::new(FontSize::Body.pixels(), FontWeight::Medium.family()),
194    );
195    text_styles.insert(
196        TextStyle::Heading,
197        FontId::new(FontSize::Heading.pixels(), FontWeight::SemiBold.family()),
198    );
199    text_styles.insert(
200        TextStyle::Monospace,
201        FontId::new(FontSize::Body.pixels(), FontFamily::Monospace),
202    );
203
204    style.text_styles = text_styles;
205    ctx.set_style(style);
206}
207
208/// Initialize typography for the application.
209///
210/// Call this during app initialization (in the CreationContext callback)
211/// to set up custom fonts and text styles.
212pub fn init(ctx: &egui::Context) {
213    ctx.set_fonts(configure_fonts());
214    configure_text_styles(ctx);
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_font_size_pixels() {
223        assert_eq!(FontSize::Caption.pixels(), 10.0);
224        assert_eq!(FontSize::Small.pixels(), 12.0);
225        assert_eq!(FontSize::Body.pixels(), 14.0);
226        assert_eq!(FontSize::Large.pixels(), 16.0);
227        assert_eq!(FontSize::Heading.pixels(), 18.0);
228        assert_eq!(FontSize::Title.pixels(), 24.0);
229        assert_eq!(FontSize::Display.pixels(), 32.0);
230    }
231
232    #[test]
233    fn test_font_weight_family() {
234        assert_eq!(
235            FontWeight::Regular.family(),
236            FontFamily::Name(FAMILY_GEIST_REGULAR.into())
237        );
238        assert_eq!(
239            FontWeight::Medium.family(),
240            FontFamily::Name(FAMILY_GEIST_MEDIUM.into())
241        );
242        assert_eq!(
243            FontWeight::SemiBold.family(),
244            FontFamily::Name(FAMILY_GEIST_SEMIBOLD.into())
245        );
246    }
247
248    #[test]
249    fn test_font_helper() {
250        let font_id = font(FontSize::Body, FontWeight::Regular);
251        assert_eq!(font_id.size, 14.0);
252        assert_eq!(
253            font_id.family,
254            FontFamily::Name(FAMILY_GEIST_REGULAR.into())
255        );
256    }
257
258    #[test]
259    fn test_mono_helper() {
260        let font_id = mono(FontSize::Body);
261        assert_eq!(font_id.size, 14.0);
262        assert_eq!(font_id.family, FontFamily::Name(FAMILY_GEIST_MONO.into()));
263    }
264
265    #[test]
266    fn test_configure_fonts_has_all_families() {
267        let fonts = configure_fonts();
268
269        // Check that all font data is loaded
270        assert!(fonts.font_data.contains_key(FAMILY_GEIST_REGULAR));
271        assert!(fonts.font_data.contains_key(FAMILY_GEIST_MEDIUM));
272        assert!(fonts.font_data.contains_key(FAMILY_GEIST_SEMIBOLD));
273        assert!(fonts.font_data.contains_key(FAMILY_GEIST_MONO));
274
275        // Check that custom families are defined
276        assert!(fonts
277            .families
278            .contains_key(&FontFamily::Name(FAMILY_GEIST_REGULAR.into())));
279        assert!(fonts
280            .families
281            .contains_key(&FontFamily::Name(FAMILY_GEIST_MEDIUM.into())));
282        assert!(fonts
283            .families
284            .contains_key(&FontFamily::Name(FAMILY_GEIST_SEMIBOLD.into())));
285        assert!(fonts
286            .families
287            .contains_key(&FontFamily::Name(FAMILY_GEIST_MONO.into())));
288
289        // Check that default families have Geist fonts as primary
290        let proportional = fonts.families.get(&FontFamily::Proportional).unwrap();
291        assert_eq!(proportional.first().unwrap(), FAMILY_GEIST_REGULAR);
292
293        let monospace = fonts.families.get(&FontFamily::Monospace).unwrap();
294        assert_eq!(monospace.first().unwrap(), FAMILY_GEIST_MONO);
295    }
296}