mod color;
mod extension;
mod fonts;
mod hct;
mod metrics;
mod motion;
mod type_scale;
pub use color::{MaterialSemanticColors, color_scheme_dark, color_scheme_light};
pub use extension::MaterialTokens;
pub use fonts::font_data;
pub use hct::{CorePalette, Hct, TonalPalette, from_seed, theme_from_seed};
pub use metrics::{MaterialDimensions, MaterialSpacing, elevation};
pub use motion::{MaterialMotion, MaterialSpring};
pub use type_scale::type_scale;
use frust::authoring::text::TextStyle;
use frust::{
Brightness, CosmeticLoopRate, DesignLanguage, EasingSet, FontFace, GlassScale, MotionDurations,
MotionScheme, MotionSpring, NativeTypefaces, ShapeScale, StatusColors, StatusPalette, Theme,
ThemeExtensions,
};
use peniko::Color;
pub const ROBOTO_FLEX_FAMILY: &str = "Roboto Flex";
#[allow(dead_code)]
pub const ROBOTO_MONO_FAMILY: &str = "Roboto Mono";
pub fn baseline() -> Theme {
let mut extensions = ThemeExtensions::new();
extensions.insert(status_palette());
extensions.insert(MaterialTokens::material());
extensions.insert(native_typefaces());
Theme {
light: color_scheme_light(),
dark: color_scheme_dark(),
type_scale: type_scale(&TextStyle::default()),
shape: shape_scale(),
elevation: elevation(),
motion: motion_scheme(),
glass: GlassScale::opaque_material(),
brightness: Brightness::Light,
design_language: DesignLanguage::Material3,
extensions,
}
}
pub fn status_palette() -> StatusPalette {
StatusPalette {
light: StatusColors {
success: Color::from_rgb8(0x2E, 0x7D, 0x32),
on_success: Color::from_rgb8(0xFF, 0xFF, 0xFF),
success_container: Color::from_rgb8(0xC8, 0xE6, 0xC9),
on_success_container: Color::from_rgb8(0x1B, 0x5E, 0x20),
warning: Color::from_rgb8(0x8A, 0x53, 0x00),
on_warning: Color::from_rgb8(0xFF, 0xFF, 0xFF),
warning_container: Color::from_rgb8(0xFF, 0xDD, 0xB0),
on_warning_container: Color::from_rgb8(0x2B, 0x17, 0x00),
info: Color::from_rgb8(0x00, 0x61, 0xA4),
on_info: Color::from_rgb8(0xFF, 0xFF, 0xFF),
info_container: Color::from_rgb8(0xD1, 0xE4, 0xFF),
on_info_container: Color::from_rgb8(0x00, 0x1D, 0x36),
},
dark: StatusColors {
success: Color::from_rgb8(0xA6, 0xF1, 0xA1),
on_success: Color::from_rgb8(0x00, 0x39, 0x0F),
success_container: Color::from_rgb8(0x20, 0x57, 0x23),
on_success_container: Color::from_rgb8(0xC8, 0xE6, 0xC9),
warning: Color::from_rgb8(0xFF, 0xC4, 0x6B),
on_warning: Color::from_rgb8(0x45, 0x2B, 0x00),
warning_container: Color::from_rgb8(0x6F, 0x49, 0x00),
on_warning_container: Color::from_rgb8(0xFF, 0xDD, 0xB0),
info: Color::from_rgb8(0x9F, 0xCA, 0xFF),
on_info: Color::from_rgb8(0x00, 0x32, 0x50),
info_container: Color::from_rgb8(0x00, 0x4A, 0x76),
on_info_container: Color::from_rgb8(0xD1, 0xE4, 0xFF),
},
}
}
pub const fn shape_scale() -> ShapeScale {
ShapeScale {
none: 0.0,
extra_small: 4.0,
small: 8.0,
medium: 12.0,
large: 16.0,
large_increased: 20.0,
extra_large: 28.0,
extra_large_increased: 32.0,
extra_extra_large: 48.0,
full: f64::INFINITY,
}
}
pub const fn motion_scheme() -> MotionScheme {
MotionScheme {
fast_spatial: MotionSpring {
damping_ratio: MaterialSpring::SPATIAL_FAST.damping_ratio,
stiffness: MaterialSpring::SPATIAL_FAST.stiffness,
},
fast_effects: MotionSpring {
damping_ratio: MaterialSpring::EFFECTS_FAST.damping_ratio,
stiffness: MaterialSpring::EFFECTS_FAST.stiffness,
},
default_spatial: MotionSpring {
damping_ratio: MaterialSpring::SPATIAL_DEFAULT.damping_ratio,
stiffness: MaterialSpring::SPATIAL_DEFAULT.stiffness,
},
default_effects: MotionSpring {
damping_ratio: MaterialSpring::EFFECTS_DEFAULT.damping_ratio,
stiffness: MaterialSpring::EFFECTS_DEFAULT.stiffness,
},
slow_spatial: MotionSpring {
damping_ratio: MaterialSpring::SPATIAL_SLOW.damping_ratio,
stiffness: MaterialSpring::SPATIAL_SLOW.stiffness,
},
slow_effects: MotionSpring {
damping_ratio: MaterialSpring::EFFECTS_SLOW.damping_ratio,
stiffness: MaterialSpring::EFFECTS_SLOW.stiffness,
},
durations: MotionDurations {
instant: MaterialMotion::SHORT_1.as_millis() as f64,
fast: MaterialMotion::SHORT_3.as_millis() as f64,
base: MaterialMotion::MEDIUM_2.as_millis() as f64,
slow: MaterialMotion::LONG_2.as_millis() as f64,
deliberate: MaterialMotion::EXTRA_LONG_1.as_millis() as f64,
},
easing: EasingSet {
spatial: MaterialMotion::EMPHASIZED_DECELERATE,
effects: MaterialMotion::STANDARD,
exit: MaterialMotion::STANDARD_ACCELERATE,
},
reduce_motion: false,
cosmetic_loop_rate: CosmeticLoopRate::new(30.0),
}
}
pub fn native_typefaces() -> NativeTypefaces {
match fonts::font_data()
.get(fonts::ROBOTO_FLEX_VARIABLE_INDEX)
.copied()
{
Some(bytes) => NativeTypefaces::uniform(FontFace::new(ROBOTO_FLEX_FAMILY, bytes)),
None => NativeTypefaces::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_scale_matches_table() {
let s = shape_scale();
assert_eq!(s.none, 0.0);
assert_eq!(s.extra_small, 4.0);
assert_eq!(s.small, 8.0);
assert_eq!(s.medium, 12.0);
assert_eq!(s.large, 16.0);
assert_eq!(s.large_increased, 20.0);
assert_eq!(s.extra_large, 28.0);
assert_eq!(s.extra_large_increased, 32.0);
assert_eq!(s.extra_extra_large, 48.0);
assert!(s.full.is_infinite());
}
#[test]
fn elevation_dp_matches_table() {
let e = elevation();
assert_eq!(e.level0.dp, 0.0);
assert_eq!(e.level1.dp, 1.0);
assert_eq!(e.level2.dp, 3.0);
assert_eq!(e.level3.dp, 6.0);
assert_eq!(e.level4.dp, 8.0);
assert_eq!(e.level5.dp, 12.0);
}
#[test]
fn elevation_shadow_is_identical_on_both_brightnesses() {
let e = elevation();
assert_eq!(e.level3.shadow_light, e.level3.shadow_dark);
}
#[test]
fn motion_scheme_spring_presets_are_exact() {
let m = motion_scheme();
assert_eq!(
m.fast_spatial,
MotionSpring {
damping_ratio: 0.9,
stiffness: 1400.0
}
);
assert_eq!(
m.fast_effects,
MotionSpring {
damping_ratio: 1.0,
stiffness: 3800.0
}
);
}
#[test]
fn motion_scheme_effects_springs_are_critically_damped() {
let m = motion_scheme();
assert_eq!(m.fast_effects.damping_ratio, 1.0);
assert_eq!(m.default_effects.damping_ratio, 1.0);
assert_eq!(m.slow_effects.damping_ratio, 1.0);
}
#[test]
fn motion_scheme_cosmetic_loop_rate_is_30hz() {
assert_eq!(motion_scheme().cosmetic_loop_rate.hz(), 30.0);
}
#[test]
fn baseline_composes_the_m3_scales() {
let theme = baseline();
assert_eq!(theme.shape, shape_scale());
assert_eq!(theme.elevation, elevation());
assert_eq!(theme.motion, motion_scheme());
}
#[test]
fn baseline_attaches_all_three_extensions() {
let theme = baseline();
assert_eq!(theme.extension::<StatusPalette>(), Some(&status_palette()));
assert_eq!(
theme.extension::<MaterialTokens>(),
Some(&MaterialTokens::material())
);
assert_eq!(
theme.extension::<NativeTypefaces>(),
Some(&native_typefaces())
);
}
#[test]
fn two_semantic_vocabularies_diverge_by_design() {
let material = MaterialTokens::material();
let status = status_palette();
assert_eq!(material.light.warning, Color::from_rgb8(0xEF, 0x6C, 0x00));
assert_eq!(status.light.warning, Color::from_rgb8(0x8A, 0x53, 0x00));
assert_ne!(material.light.warning, status.light.warning);
assert_eq!(material.light.info, Color::from_rgb8(0x7D, 0x52, 0x60));
assert_eq!(status.light.info, Color::from_rgb8(0x00, 0x61, 0xA4));
assert_ne!(material.light.info, status.light.info);
assert_eq!(material.dark.warning, Color::from_rgb8(0xFF, 0xB7, 0x4D));
assert_eq!(status.dark.warning, Color::from_rgb8(0xFF, 0xC4, 0x6B));
assert_ne!(material.dark.warning, status.dark.warning);
assert_eq!(material.dark.info, Color::from_rgb8(0xEF, 0xB8, 0xC8));
assert_eq!(status.dark.info, Color::from_rgb8(0x9F, 0xCA, 0xFF));
assert_ne!(material.dark.info, status.dark.info);
assert_eq!(material.light.success, Color::from_rgb8(0x2E, 0x7D, 0x32));
assert_eq!(status.light.success, Color::from_rgb8(0x2E, 0x7D, 0x32));
assert_eq!(material.light.success, status.light.success);
assert_eq!(material.dark.success, Color::from_rgb8(0x81, 0xC7, 0x84));
assert_eq!(status.dark.success, Color::from_rgb8(0xA6, 0xF1, 0xA1));
assert_ne!(material.dark.success, status.dark.success);
}
}