use frust::authoring::text::{FontFamily, FontWeight, GenericSlot, LineHeight, TextStyle};
use frust::{
CosmeticLoopRate, Curve, EasingSet, Elevation, ElevationLevel, GlassMaterial, GlassScale,
MotionDurations, MotionScheme, MotionSpring, ShadowSpec, ShapeScale, SurfaceRole, TypeScale,
};
#[derive(Clone, Copy)]
enum Face {
Display,
Ui,
}
impl Face {
fn family(self) -> FontFamily {
match self {
Face::Display => FontFamily::stack_with_generic(["Space Mono"], GenericSlot::Monospace),
Face::Ui => FontFamily::stack_with_generic(["IBM Plex Mono"], GenericSlot::Monospace),
}
}
}
type GlyphToken = (f32, f32, f32, FontWeight, Face);
const GLYPH_DISPLAY: GlyphToken = (32.0, 36.0, 0.0, FontWeight::BOLD, Face::Display); const GLYPH_HEADING: GlyphToken = (20.0, 24.0, 0.0, FontWeight::BOLD, Face::Display); const GLYPH_TITLE: GlyphToken = (15.0, 22.0, 0.0, FontWeight::MEDIUM, Face::Ui); const GLYPH_BODY: GlyphToken = (13.0, 20.0, 0.0, FontWeight::REGULAR, Face::Ui); const GLYPH_CAPTION: GlyphToken = (11.0, 17.0, 0.0, FontWeight::REGULAR, Face::Ui); const GLYPH_LABEL: GlyphToken = (12.5, 18.0, 0.125, FontWeight::MEDIUM, Face::Ui); const GLYPH_MICRO: GlyphToken = (9.5, 14.0, 0.475, FontWeight::MEDIUM, Face::Ui);
fn apply_glyph(base: &TextStyle, token: GlyphToken) -> TextStyle {
let (size, line_height_px, letter_spacing, weight, face) = token;
TextStyle {
family: face.family(),
size,
weight,
letter_spacing,
line_height: LineHeight::Absolute(line_height_px),
..base.clone()
}
}
fn apply_glyph_emphasized(base: &TextStyle, token: GlyphToken) -> TextStyle {
let (size, lh, ls, weight, face) = token;
let up = if weight == FontWeight::REGULAR {
FontWeight::MEDIUM
} else if weight == FontWeight::MEDIUM {
FontWeight::SEMI_BOLD
} else {
weight };
apply_glyph(base, (size, lh, ls, up, face))
}
pub fn type_scale(base: &TextStyle) -> TypeScale {
TypeScale {
display_large: apply_glyph(base, GLYPH_DISPLAY),
display_medium: apply_glyph(base, GLYPH_DISPLAY),
display_small: apply_glyph(base, GLYPH_DISPLAY),
headline_large: apply_glyph(base, GLYPH_HEADING),
headline_medium: apply_glyph(base, GLYPH_HEADING),
headline_small: apply_glyph(base, GLYPH_HEADING),
title_large: apply_glyph(base, GLYPH_TITLE),
title_medium: apply_glyph(base, GLYPH_TITLE),
title_small: apply_glyph(base, GLYPH_TITLE),
body_large: apply_glyph(base, GLYPH_BODY),
body_medium: apply_glyph(base, GLYPH_BODY),
body_small: apply_glyph(base, GLYPH_CAPTION),
label_large: apply_glyph(base, GLYPH_LABEL),
label_medium: apply_glyph(base, GLYPH_LABEL),
label_small: apply_glyph(base, GLYPH_MICRO),
display_large_emphasized: apply_glyph_emphasized(base, GLYPH_DISPLAY),
display_medium_emphasized: apply_glyph_emphasized(base, GLYPH_DISPLAY),
display_small_emphasized: apply_glyph_emphasized(base, GLYPH_DISPLAY),
headline_large_emphasized: apply_glyph_emphasized(base, GLYPH_HEADING),
headline_medium_emphasized: apply_glyph_emphasized(base, GLYPH_HEADING),
headline_small_emphasized: apply_glyph_emphasized(base, GLYPH_HEADING),
title_large_emphasized: apply_glyph_emphasized(base, GLYPH_TITLE),
title_medium_emphasized: apply_glyph_emphasized(base, GLYPH_TITLE),
title_small_emphasized: apply_glyph_emphasized(base, GLYPH_TITLE),
body_large_emphasized: apply_glyph_emphasized(base, GLYPH_BODY),
body_medium_emphasized: apply_glyph_emphasized(base, GLYPH_BODY),
body_small_emphasized: apply_glyph_emphasized(base, GLYPH_CAPTION),
label_large_emphasized: apply_glyph_emphasized(base, GLYPH_LABEL),
label_medium_emphasized: apply_glyph_emphasized(base, GLYPH_LABEL),
label_small_emphasized: apply_glyph_emphasized(base, GLYPH_MICRO),
}
}
pub const fn shape() -> ShapeScale {
ShapeScale {
none: 0.0,
extra_small: 4.0, small: 6.0, medium: 10.0, large: 16.0, large_increased: 22.0, extra_large: 22.0,
extra_large_increased: 22.0,
extra_extra_large: 22.0,
full: f64::INFINITY, }
}
const fn glyph_level(
dp: f64,
y: f64,
blur: f64,
dark_alpha: f32,
light_alpha: f32,
surface_role: SurfaceRole,
) -> ElevationLevel {
ElevationLevel {
dp,
shadow_light: ShadowSpec {
y_offset: y,
blur_std_dev: blur,
color_alpha: light_alpha,
},
shadow_dark: ShadowSpec {
y_offset: y,
blur_std_dev: blur,
color_alpha: dark_alpha,
},
surface_role,
}
}
pub const fn elevation() -> Elevation {
Elevation {
level0: glyph_level(0.0, 0.0, 0.0, 0.0, 0.0, SurfaceRole::Surface),
level1: glyph_level(1.0, 1.0, 3.0, 0.10, 0.04, SurfaceRole::SurfaceContainerLow),
level2: glyph_level(3.0, 4.0, 12.0, 0.22, 0.06, SurfaceRole::SurfaceContainer),
level3: glyph_level(
6.0,
8.0,
20.0,
0.30,
0.09,
SurfaceRole::SurfaceContainerHigh,
),
level4: glyph_level(
8.0,
12.0,
32.0,
0.40,
0.12,
SurfaceRole::SurfaceContainerHigh,
),
level5: glyph_level(
12.0,
24.0,
60.0,
0.50,
0.18,
SurfaceRole::SurfaceContainerHighest,
),
}
}
pub const fn motion() -> MotionScheme {
const SPATIAL_ZETA: f64 = 0.6;
MotionScheme {
fast_spatial: MotionSpring {
damping_ratio: SPATIAL_ZETA,
stiffness: 900.0,
},
fast_effects: MotionSpring {
damping_ratio: 1.0,
stiffness: 1400.0,
},
default_spatial: MotionSpring {
damping_ratio: SPATIAL_ZETA,
stiffness: 500.0,
},
default_effects: MotionSpring {
damping_ratio: 1.0,
stiffness: 900.0,
},
slow_spatial: MotionSpring {
damping_ratio: SPATIAL_ZETA,
stiffness: 250.0,
},
slow_effects: MotionSpring {
damping_ratio: 1.0,
stiffness: 500.0,
},
durations: MotionDurations {
instant: 100.0,
fast: 150.0,
base: 220.0,
slow: 340.0,
deliberate: 600.0,
},
easing: EasingSet {
spatial: Curve::Cubic(0.34, 1.35, 0.64, 1.0),
effects: Curve::Cubic(0.16, 1.0, 0.3, 1.0),
exit: Curve::Cubic(0.4, 0.0, 1.0, 1.0),
},
reduce_motion: false,
cosmetic_loop_rate: CosmeticLoopRate::new(30.0),
}
}
pub fn glass() -> GlassScale {
let elev = elevation();
const HAIRLINE: f32 = 0.18;
let opaque = |shadow: ShadowSpec, hairline: f32| GlassMaterial {
blur_radius_intent: 0.0,
fills_light: Vec::new(),
fills_dark: Vec::new(),
hairline_alpha: hairline,
shadow,
};
GlassScale {
chrome: opaque(elev.level3.shadow_dark, HAIRLINE),
bar: opaque(elev.level2.shadow_dark, HAIRLINE),
control: opaque(elev.level1.shadow_dark, HAIRLINE),
}
}
#[cfg(test)]
mod tests {
use super::*;
use frust::Color;
fn base() -> TextStyle {
TextStyle::new(16.0, Color::BLACK)
}
#[test]
fn glyph_type_sizes_match_source_roles() {
let s = type_scale(&base());
assert_eq!(s.display_large.size, 32.0); assert_eq!(s.headline_large.size, 20.0); assert_eq!(s.title_large.size, 15.0); assert_eq!(s.body_large.size, 13.0); assert_eq!(s.body_small.size, 11.0); assert_eq!(s.label_small.size, 9.5); }
#[test]
fn glyph_type_weights_and_faces_split_by_role() {
let s = type_scale(&base());
let display = FontFamily::stack_with_generic(["Space Mono"], GenericSlot::Monospace);
let ui = FontFamily::stack_with_generic(["IBM Plex Mono"], GenericSlot::Monospace);
assert_eq!(s.display_large.family, display);
assert_eq!(s.display_large.weight, FontWeight::BOLD);
assert_eq!(s.headline_large.family, display);
assert_eq!(s.title_large.family, ui);
assert_eq!(s.title_large.weight, FontWeight::MEDIUM);
assert_eq!(s.body_large.family, ui);
assert_eq!(s.body_large.weight, FontWeight::REGULAR);
}
#[test]
fn glyph_micro_carries_tracking() {
let s = type_scale(&base());
assert!((s.label_small.letter_spacing - 0.475).abs() < 1e-6);
}
#[test]
fn glyph_emphasized_steps_weight_up_where_it_can() {
let s = type_scale(&base());
assert_eq!(s.body_large_emphasized.weight, FontWeight::MEDIUM);
assert_eq!(s.title_large_emphasized.weight, FontWeight::SEMI_BOLD);
assert_eq!(s.display_large_emphasized.weight, FontWeight::BOLD);
assert_eq!(s.body_large.size, s.body_large_emphasized.size);
assert_eq!(
s.body_large.line_height,
s.body_large_emphasized.line_height
);
}
#[test]
fn glyph_type_preserves_base_color_but_overrides_family() {
let b = TextStyle {
family: FontFamily::named("Roboto"),
..TextStyle::new(16.0, Color::from_rgb8(1, 2, 3))
};
let s = type_scale(&b);
assert_ne!(s.body_large.family, b.family); assert_eq!(s.body_large.color, b.color);
}
#[test]
fn glyph_shape_matches_documented_scale() {
let s = shape();
assert_eq!(s.none, 0.0);
assert_eq!(s.extra_small, 4.0); assert_eq!(s.small, 6.0);
assert_eq!(s.medium, 10.0);
assert_eq!(s.large, 16.0);
assert_eq!(s.large_increased, 22.0);
assert_eq!(s.extra_large, 22.0);
assert_eq!(s.extra_extra_large, 22.0);
assert!(s.full.is_infinite());
}
#[test]
fn glyph_elevation_is_per_brightness_and_source_anchored() {
let e = elevation();
assert_eq!(e.level4.shadow_dark.y_offset, 12.0);
assert_eq!(e.level4.shadow_dark.blur_std_dev, 32.0);
assert_eq!(e.level4.shadow_dark.color_alpha, 0.40);
assert_eq!(e.level4.shadow_light.color_alpha, 0.12);
assert_eq!(e.level5.shadow_dark.y_offset, 24.0);
assert_eq!(e.level5.shadow_dark.blur_std_dev, 60.0);
assert_eq!(e.level5.shadow_dark.color_alpha, 0.50);
assert_eq!(e.level5.shadow_light.color_alpha, 0.18);
}
#[test]
fn glyph_elevation_low_levels_are_borders_first() {
let e = elevation();
assert_eq!(e.level0.shadow_dark.color_alpha, 0.0);
assert!(e.level1.shadow_dark.color_alpha < e.level4.shadow_dark.color_alpha);
for lvl in [&e.level2, &e.level3, &e.level4, &e.level5] {
assert!(lvl.shadow_dark.color_alpha > lvl.shadow_light.color_alpha);
}
}
#[test]
fn glyph_motion_durations_and_easings_are_exact_source_values() {
let m = motion();
assert_eq!(
m.durations,
MotionDurations {
instant: 100.0,
fast: 150.0,
base: 220.0,
slow: 340.0,
deliberate: 600.0,
}
);
assert_eq!(m.easing.spatial, Curve::Cubic(0.34, 1.35, 0.64, 1.0));
assert_eq!(m.easing.effects, Curve::Cubic(0.16, 1.0, 0.3, 1.0));
assert_eq!(m.easing.exit, Curve::Cubic(0.4, 0.0, 1.0, 1.0));
}
#[test]
fn glyph_cosmetic_loop_rate_defaults_to_30hz() {
assert_eq!(motion().cosmetic_loop_rate.hz(), 30.0);
}
#[test]
fn glyph_motion_springs_match_the_curve_overshoot_split() {
let m = motion();
for s in [&m.fast_spatial, &m.default_spatial, &m.slow_spatial] {
assert!(s.damping_ratio < 1.0 && s.damping_ratio > 0.0);
}
for s in [&m.fast_effects, &m.default_effects, &m.slow_effects] {
assert_eq!(s.damping_ratio, 1.0);
}
}
#[test]
fn glyph_glass_is_opaque_with_a_hairline_and_glyph_shadows() {
let g = glass();
let e = elevation();
for tier in [&g.chrome, &g.bar, &g.control] {
assert!(tier.is_opaque());
assert!(tier.fills_light.is_empty());
assert!(tier.fills_dark.is_empty());
assert_eq!(tier.hairline_alpha, 0.18);
}
assert_eq!(g.chrome.shadow, e.level3.shadow_dark);
assert_eq!(g.bar.shadow, e.level2.shadow_dark);
assert_eq!(g.control.shadow, e.level1.shadow_dark);
}
#[test]
fn glyph_scales_are_const_where_expected() {
const SHAPE: ShapeScale = shape();
const ELEV: Elevation = elevation();
const MOTION: MotionScheme = motion();
assert_eq!(SHAPE.large, 16.0);
assert_eq!(ELEV.level5.shadow_dark.color_alpha, 0.50);
assert_eq!(MOTION.durations.base, 220.0);
}
}