Archived
236 lines
6.4 KiB
Rust
236 lines
6.4 KiB
Rust
/// 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);
|
|
}
|
|
}
|
|
}
|