This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-ui-retired/crates/el-style/src/typography.rs
T

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);
}
}
}