/// Typography scale — named text styles with semantic meaning. /// /// Don't hardcode font sizes. Use the named scale. The theme maps each /// style to the right size, weight, and line height for the platform. /// Named text style levels. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum TextStyle { /// Largest — hero headings, splash screens. Display, /// Page-level headings. Headline, /// Section headings, dialog titles. Title, /// Default readable body copy. Body, /// UI labels, button text, captions. Label, /// Monospaced — code, terminal output. Code, /// Fine print, footnotes, timestamps. Caption, } /// Font weight. #[derive(Debug, Clone, PartialEq, Eq)] pub enum FontWeight { Thin, // 100 Light, // 300 Regular, // 400 Medium, // 500 SemiBold, // 600 Bold, // 700 ExtraBold, // 800 Black, // 900 } impl FontWeight { /// Numeric CSS/platform font weight value. pub fn value(&self) -> u32 { match self { FontWeight::Thin => 100, FontWeight::Light => 300, FontWeight::Regular => 400, FontWeight::Medium => 500, FontWeight::SemiBold => 600, FontWeight::Bold => 700, FontWeight::ExtraBold => 800, FontWeight::Black => 900, } } } /// Text alignment — logical (RTL-aware), not physical. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TextAlign { /// Aligns to the start of reading direction (left in LTR, right in RTL). Start, /// Center. Center, /// Aligns to the end of reading direction. End, /// Justify. Justify, } /// Text decoration. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TextDecoration { None, Underline, LineThrough, Overline, } /// Overflow behavior for text that doesn't fit. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TextOverflow { Clip, Ellipsis, Wrap, } /// Fully-resolved spec for a single text style level. #[derive(Debug, Clone)] pub struct TextSpec { /// Font size in density-independent pixels. pub size: f32, /// Line height multiplier (1.5 = 150% of font size). pub line_height: f32, /// Letter spacing in em units. pub letter_spacing: f32, pub weight: FontWeight, } /// Maps each TextStyle level to a concrete TextSpec. #[derive(Debug, Clone)] pub struct TypographyScheme { pub display: TextSpec, pub headline: TextSpec, pub title: TextSpec, pub body: TextSpec, pub label: TextSpec, pub code: TextSpec, pub caption: TextSpec, } impl TypographyScheme { /// Default typography scale (4px base grid, 16px body). pub fn default() -> Self { Self { display: TextSpec { size: 57.0, line_height: 1.12, letter_spacing: -0.025, weight: FontWeight::Regular, }, headline: TextSpec { size: 32.0, line_height: 1.25, letter_spacing: -0.015, weight: FontWeight::SemiBold, }, title: TextSpec { size: 22.0, line_height: 1.3, letter_spacing: -0.01, weight: FontWeight::SemiBold, }, body: TextSpec { size: 16.0, line_height: 1.5, letter_spacing: 0.0, weight: FontWeight::Regular, }, label: TextSpec { size: 14.0, line_height: 1.4, letter_spacing: 0.005, weight: FontWeight::Medium, }, code: TextSpec { size: 14.0, line_height: 1.6, letter_spacing: 0.0, weight: FontWeight::Regular, }, caption: TextSpec { size: 12.0, line_height: 1.33, letter_spacing: 0.01, weight: FontWeight::Regular, }, } } /// Resolve a TextStyle variant to its TextSpec. pub fn resolve(&self, style: &TextStyle) -> &TextSpec { match style { TextStyle::Display => &self.display, TextStyle::Headline => &self.headline, TextStyle::Title => &self.title, TextStyle::Body => &self.body, TextStyle::Label => &self.label, TextStyle::Code => &self.code, TextStyle::Caption => &self.caption, } } } #[cfg(test)] mod tests { use super::*; #[test] fn typography_scheme_body_size() { let scheme = TypographyScheme::default(); let spec = scheme.resolve(&TextStyle::Body); assert_eq!(spec.size, 16.0); } #[test] fn typography_scheme_display_largest() { let scheme = TypographyScheme::default(); let display = scheme.resolve(&TextStyle::Display); let caption = scheme.resolve(&TextStyle::Caption); assert!(display.size > caption.size); } #[test] fn font_weight_values() { assert_eq!(FontWeight::Regular.value(), 400); assert_eq!(FontWeight::Bold.value(), 700); assert_eq!(FontWeight::SemiBold.value(), 600); } #[test] fn typography_minimum_size() { // All text styles must be >= 11pt (accessibility minimum) let scheme = TypographyScheme::default(); for style in [ TextStyle::Display, TextStyle::Headline, TextStyle::Title, TextStyle::Body, TextStyle::Label, TextStyle::Code, TextStyle::Caption, ] { let spec = scheme.resolve(&style); assert!( spec.size >= 11.0, "{:?} size {} is below 11pt minimum", style, spec.size ); } } #[test] fn typography_code_is_readable_size() { let scheme = TypographyScheme::default(); let spec = scheme.resolve(&TextStyle::Code); assert!(spec.size >= 12.0); } #[test] fn typography_line_heights_positive() { let scheme = TypographyScheme::default(); for style in [TextStyle::Body, TextStyle::Caption, TextStyle::Title] { let spec = scheme.resolve(&style); assert!(spec.line_height > 1.0); } } }