use frust_text::{FontFamily, FontWeight, GenericSlot, LineHeight, TextStyle};
#[derive(Clone, Debug, PartialEq)]
pub struct TypeScale {
pub display_large: TextStyle,
pub display_medium: TextStyle,
pub display_small: TextStyle,
pub headline_large: TextStyle,
pub headline_medium: TextStyle,
pub headline_small: TextStyle,
pub title_large: TextStyle,
pub title_medium: TextStyle,
pub title_small: TextStyle,
pub body_large: TextStyle,
pub body_medium: TextStyle,
pub body_small: TextStyle,
pub label_large: TextStyle,
pub label_medium: TextStyle,
pub label_small: TextStyle,
pub display_large_emphasized: TextStyle,
pub display_medium_emphasized: TextStyle,
pub display_small_emphasized: TextStyle,
pub headline_large_emphasized: TextStyle,
pub headline_medium_emphasized: TextStyle,
pub headline_small_emphasized: TextStyle,
pub title_large_emphasized: TextStyle,
pub title_medium_emphasized: TextStyle,
pub title_small_emphasized: TextStyle,
pub body_large_emphasized: TextStyle,
pub body_medium_emphasized: TextStyle,
pub body_small_emphasized: TextStyle,
pub label_large_emphasized: TextStyle,
pub label_medium_emphasized: TextStyle,
pub label_small_emphasized: TextStyle,
}
type Token = (f32, f32, f32, FontWeight);
const DISPLAY_LARGE: Token = (57.0, 64.0, -0.25, FontWeight::REGULAR);
const DISPLAY_MEDIUM: Token = (45.0, 52.0, 0.0, FontWeight::REGULAR);
const DISPLAY_SMALL: Token = (36.0, 44.0, 0.0, FontWeight::REGULAR);
const HEADLINE_LARGE: Token = (32.0, 40.0, 0.0, FontWeight::REGULAR);
const HEADLINE_MEDIUM: Token = (28.0, 36.0, 0.0, FontWeight::REGULAR);
const HEADLINE_SMALL: Token = (24.0, 32.0, 0.0, FontWeight::REGULAR);
const TITLE_LARGE: Token = (22.0, 28.0, 0.0, FontWeight::REGULAR);
const TITLE_MEDIUM: Token = (16.0, 24.0, 0.15, FontWeight::MEDIUM);
const TITLE_SMALL: Token = (14.0, 20.0, 0.1, FontWeight::MEDIUM);
const BODY_LARGE: Token = (16.0, 24.0, 0.5, FontWeight::REGULAR);
const BODY_MEDIUM: Token = (14.0, 20.0, 0.25, FontWeight::REGULAR);
const BODY_SMALL: Token = (12.0, 16.0, 0.4, FontWeight::REGULAR);
const LABEL_LARGE: Token = (14.0, 20.0, 0.1, FontWeight::MEDIUM);
const LABEL_MEDIUM: Token = (12.0, 16.0, 0.5, FontWeight::MEDIUM);
const LABEL_SMALL: Token = (11.0, 16.0, 0.5, FontWeight::MEDIUM);
const DISPLAY_LARGE_EMPHASIZED: Token = (57.0, 64.0, 0.0, FontWeight::MEDIUM);
const DISPLAY_MEDIUM_EMPHASIZED: Token = (45.0, 52.0, 0.0, FontWeight::MEDIUM);
const DISPLAY_SMALL_EMPHASIZED: Token = (36.0, 44.0, 0.0, FontWeight::MEDIUM);
const HEADLINE_LARGE_EMPHASIZED: Token = (32.0, 40.0, 0.0, FontWeight::MEDIUM);
const HEADLINE_MEDIUM_EMPHASIZED: Token = (28.0, 36.0, 0.0, FontWeight::MEDIUM);
const HEADLINE_SMALL_EMPHASIZED: Token = (24.0, 32.0, 0.0, FontWeight::MEDIUM);
const TITLE_LARGE_EMPHASIZED: Token = (22.0, 28.0, 0.0, FontWeight::MEDIUM);
const TITLE_MEDIUM_EMPHASIZED: Token = (16.0, 24.0, 0.15, FontWeight::BOLD);
const TITLE_SMALL_EMPHASIZED: Token = (14.0, 20.0, 0.1, FontWeight::BOLD);
const BODY_LARGE_EMPHASIZED: Token = (16.0, 24.0, 0.15, FontWeight::MEDIUM);
const BODY_MEDIUM_EMPHASIZED: Token = (14.0, 20.0, 0.25, FontWeight::MEDIUM);
const BODY_SMALL_EMPHASIZED: Token = (12.0, 16.0, 0.4, FontWeight::MEDIUM);
const LABEL_LARGE_EMPHASIZED: Token = (14.0, 20.0, 0.1, FontWeight::BOLD);
const LABEL_MEDIUM_EMPHASIZED: Token = (12.0, 16.0, 0.5, FontWeight::BOLD);
const LABEL_SMALL_EMPHASIZED: Token = (11.0, 16.0, 0.5, FontWeight::BOLD);
fn apply(base: &TextStyle, token: Token) -> TextStyle {
let (size, line_height_px, letter_spacing, weight) = token;
TextStyle {
size,
weight,
letter_spacing,
line_height: LineHeight::Absolute(line_height_px),
..base.clone()
}
}
impl TypeScale {
pub fn neutral(base: &TextStyle) -> Self {
let family =
FontFamily::stack_with_generic(std::iter::empty::<&str>(), GenericSlot::SansSerif);
let base = TextStyle {
family,
..base.clone()
};
Self {
display_large: apply(&base, DISPLAY_LARGE),
display_medium: apply(&base, DISPLAY_MEDIUM),
display_small: apply(&base, DISPLAY_SMALL),
headline_large: apply(&base, HEADLINE_LARGE),
headline_medium: apply(&base, HEADLINE_MEDIUM),
headline_small: apply(&base, HEADLINE_SMALL),
title_large: apply(&base, TITLE_LARGE),
title_medium: apply(&base, TITLE_MEDIUM),
title_small: apply(&base, TITLE_SMALL),
body_large: apply(&base, BODY_LARGE),
body_medium: apply(&base, BODY_MEDIUM),
body_small: apply(&base, BODY_SMALL),
label_large: apply(&base, LABEL_LARGE),
label_medium: apply(&base, LABEL_MEDIUM),
label_small: apply(&base, LABEL_SMALL),
display_large_emphasized: apply(&base, DISPLAY_LARGE_EMPHASIZED),
display_medium_emphasized: apply(&base, DISPLAY_MEDIUM_EMPHASIZED),
display_small_emphasized: apply(&base, DISPLAY_SMALL_EMPHASIZED),
headline_large_emphasized: apply(&base, HEADLINE_LARGE_EMPHASIZED),
headline_medium_emphasized: apply(&base, HEADLINE_MEDIUM_EMPHASIZED),
headline_small_emphasized: apply(&base, HEADLINE_SMALL_EMPHASIZED),
title_large_emphasized: apply(&base, TITLE_LARGE_EMPHASIZED),
title_medium_emphasized: apply(&base, TITLE_MEDIUM_EMPHASIZED),
title_small_emphasized: apply(&base, TITLE_SMALL_EMPHASIZED),
body_large_emphasized: apply(&base, BODY_LARGE_EMPHASIZED),
body_medium_emphasized: apply(&base, BODY_MEDIUM_EMPHASIZED),
body_small_emphasized: apply(&base, BODY_SMALL_EMPHASIZED),
label_large_emphasized: apply(&base, LABEL_LARGE_EMPHASIZED),
label_medium_emphasized: apply(&base, LABEL_MEDIUM_EMPHASIZED),
label_small_emphasized: apply(&base, LABEL_SMALL_EMPHASIZED),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use peniko::Color;
#[test]
fn display_large_matches_table() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
assert_eq!(scale.display_large.size, 57.0);
assert_eq!(scale.display_large.line_height, LineHeight::Absolute(64.0));
assert_eq!(scale.display_large.letter_spacing, -0.25);
assert_eq!(scale.display_large.weight, FontWeight::REGULAR);
}
#[test]
fn title_medium_is_medium_weight() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
assert_eq!(scale.title_medium.weight, FontWeight::MEDIUM);
assert_eq!(scale.title_medium.size, 16.0);
assert_eq!(scale.title_medium.letter_spacing, 0.15);
}
#[test]
fn title_large_is_regular_weight() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
assert_eq!(scale.title_large.weight, FontWeight::REGULAR);
}
#[test]
fn label_small_matches_table() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
assert_eq!(scale.label_small.size, 11.0);
assert_eq!(scale.label_small.line_height, LineHeight::Absolute(16.0));
assert_eq!(scale.label_small.letter_spacing, 0.5);
assert_eq!(scale.label_small.weight, FontWeight::MEDIUM);
}
#[test]
fn every_emphasized_role_differs_in_weight_from_its_base() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
let pairs: [(&TextStyle, &TextStyle); 15] = [
(&scale.display_large, &scale.display_large_emphasized),
(&scale.display_medium, &scale.display_medium_emphasized),
(&scale.display_small, &scale.display_small_emphasized),
(&scale.headline_large, &scale.headline_large_emphasized),
(&scale.headline_medium, &scale.headline_medium_emphasized),
(&scale.headline_small, &scale.headline_small_emphasized),
(&scale.title_large, &scale.title_large_emphasized),
(&scale.title_medium, &scale.title_medium_emphasized),
(&scale.title_small, &scale.title_small_emphasized),
(&scale.body_large, &scale.body_large_emphasized),
(&scale.body_medium, &scale.body_medium_emphasized),
(&scale.body_small, &scale.body_small_emphasized),
(&scale.label_large, &scale.label_large_emphasized),
(&scale.label_medium, &scale.label_medium_emphasized),
(&scale.label_small, &scale.label_small_emphasized),
];
for (base, emphasized) in pairs {
assert_ne!(
base.weight, emphasized.weight,
"expected emphasized weight to differ from base weight"
);
}
}
#[test]
fn emphasized_keeps_base_size_and_line_height() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
assert_eq!(
scale.display_large.size,
scale.display_large_emphasized.size
);
assert_eq!(
scale.display_large.line_height,
scale.display_large_emphasized.line_height
);
assert_eq!(scale.label_small.size, scale.label_small_emphasized.size);
assert_eq!(
scale.label_small.line_height,
scale.label_small_emphasized.line_height
);
}
#[test]
fn display_large_emphasized_is_medium_weight() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
assert_eq!(scale.display_large_emphasized.weight, FontWeight::MEDIUM);
assert_eq!(scale.display_large_emphasized.letter_spacing, 0.0);
}
#[test]
fn title_medium_emphasized_is_bold_weight() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
assert_eq!(scale.title_medium.weight, FontWeight::MEDIUM);
assert_eq!(scale.title_medium_emphasized.weight, FontWeight::BOLD);
}
#[test]
fn label_small_emphasized_is_bold_weight() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
assert_eq!(scale.label_small.weight, FontWeight::MEDIUM);
assert_eq!(scale.label_small_emphasized.weight, FontWeight::BOLD);
}
#[test]
fn body_large_emphasized_tracking_differs_from_base() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
assert_eq!(scale.body_large.letter_spacing, 0.5);
assert_eq!(scale.body_large_emphasized.letter_spacing, 0.15);
}
#[test]
fn neutral_uses_no_bundled_fonts() {
let scale = TypeScale::neutral(&TextStyle::new(16.0, Color::BLACK));
let expected =
FontFamily::stack_with_generic(std::iter::empty::<&str>(), GenericSlot::SansSerif);
for family in [
&scale.display_large.family,
&scale.headline_medium.family,
&scale.title_large.family,
&scale.body_large.family,
&scale.label_small.family,
&scale.body_large_emphasized.family,
] {
assert_eq!(family, &expected);
match family {
FontFamily::NamedWithGeneric(parts) => {
assert!(
!parts
.iter()
.any(|p| matches!(p, frust_text::FamilyName::Named(_)))
);
}
other => panic!("expected a generic-only stack, got {other:?}"),
}
}
}
#[test]
fn neutral_preserves_base_color_but_overrides_family() {
let base = TextStyle {
family: FontFamily::named("Roboto"),
..TextStyle::new(16.0, Color::from_rgb8(1, 2, 3))
};
let scale = TypeScale::neutral(&base);
assert_ne!(scale.body_large.family, base.family);
assert_eq!(scale.body_large.color, base.color);
}
}