facett-core 0.1.4

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **Typography** (§8) — a modular type scale mapped onto egui `text_styles`,
//! plus the UI + console font *family selection* policy. We never bundle San
//! Francisco (anti-pattern §27); the OS font is loaded at runtime by the host.
//! Here we describe the scale + which family each preset *wants*; M1 ships the
//! UI-font policy and the scale, M2 wires the bundled console mono.

use std::collections::BTreeMap;

use egui::{FontFamily, FontId, TextStyle};
use serde::{Deserialize, Serialize};

/// Which UI font family a preset prefers. The host loads the actual face; the
/// theme only states intent so it stays serialisable + bundle-free.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum UiFont {
    /// macOS system San Francisco (loaded at runtime; never bundled).
    SanFrancisco,
    /// Windows Segoe UI Variable.
    SegoeUi,
    /// Bundled Inter fallback (feature-flagged in the host).
    Inter,
    /// A crisp legible default (Device / unknown OS).
    System,
}

impl UiFont {
    pub fn as_str(self) -> &'static str {
        match self {
            UiFont::SanFrancisco => "San Francisco",
            UiFont::SegoeUi => "Segoe UI Variable",
            UiFont::Inter => "Inter",
            UiFont::System => "System",
        }
    }
}

/// Typographic scale + font intent. `base` is the body size in points; sizes are
/// a modular scale (`ratio`-spaced steps). `line_height` is a multiplier on the
/// font size. Maps onto egui's `Style.text_styles`.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Typography {
    pub ui_font: UiFont,
    /// Body font size in points.
    pub base: f32,
    /// Modular scale ratio (~1.2 minor third .. 1.25 major third).
    pub ratio: f32,
    /// Line-height multiplier for body text (~1.4–1.5).
    pub line_height: f32,
}

impl Default for Typography {
    fn default() -> Self {
        Self { ui_font: UiFont::System, base: 14.0, ratio: 1.22, line_height: 1.45 }
    }
}

impl Typography {
    pub fn with_font(mut self, f: UiFont) -> Self {
        self.ui_font = f;
        self
    }

    /// Size of step `n` on the modular scale (n=0 is body; negative shrinks).
    pub fn step(&self, n: i32) -> f32 {
        (self.base * self.ratio.powi(n)).round()
    }

    /// The size for each [`TextStyle`] derived from the modular scale:
    /// Heading = +2 steps, Body = base, Button = base, Monospace = base,
    /// Small = −1 step.
    pub fn text_styles(&self) -> BTreeMap<TextStyle, FontId> {
        let mut m = BTreeMap::new();
        m.insert(TextStyle::Heading, FontId::new(self.step(2), FontFamily::Proportional));
        m.insert(TextStyle::Body, FontId::new(self.step(0), FontFamily::Proportional));
        m.insert(TextStyle::Button, FontId::new(self.step(0), FontFamily::Proportional));
        m.insert(TextStyle::Small, FontId::new(self.step(-1), FontFamily::Proportional));
        m.insert(TextStyle::Monospace, FontId::new(self.step(0), FontFamily::Monospace));
        m
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn scale_steps_grow_and_shrink_monotonically() {
        let t = Typography::default();
        assert!(t.step(-1) < t.step(0));
        assert!(t.step(0) < t.step(1));
        assert!(t.step(1) < t.step(2));
        assert_eq!(t.step(0), t.base.round());
    }

    #[test]
    fn text_styles_cover_the_core_roles() {
        let m = Typography::default().text_styles();
        for s in [TextStyle::Heading, TextStyle::Body, TextStyle::Button, TextStyle::Small, TextStyle::Monospace] {
            assert!(m.contains_key(&s), "missing {s:?}");
        }
        // Heading is bigger than body; small is smaller.
        assert!(m[&TextStyle::Heading].size > m[&TextStyle::Body].size);
        assert!(m[&TextStyle::Small].size < m[&TextStyle::Body].size);
    }
}