facett-core/src/look/typography.rs
//! **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);
}
}