use std::any::Any;
use crate::color::{Brightness, ColorScheme};
use crate::elevation::Elevation;
use crate::glass::GlassScale;
use crate::motion::MotionScheme;
use crate::shape::ShapeScale;
use crate::theme::{DesignLanguage, Theme};
use crate::typography::TypeScale;
#[derive(Clone, Debug)]
pub struct ThemeBuilder {
theme: Theme,
}
impl ThemeBuilder {
pub fn new(base: Theme) -> Self {
Self { theme: base }
}
pub fn colors_light(mut self, scheme: ColorScheme) -> Self {
self.theme.light = scheme;
self
}
pub fn map_colors_light(mut self, f: impl FnOnce(ColorScheme) -> ColorScheme) -> Self {
self.theme.light = f(self.theme.light);
self
}
pub fn colors_dark(mut self, scheme: ColorScheme) -> Self {
self.theme.dark = scheme;
self
}
pub fn map_colors_dark(mut self, f: impl FnOnce(ColorScheme) -> ColorScheme) -> Self {
self.theme.dark = f(self.theme.dark);
self
}
pub fn type_scale(mut self, scale: TypeScale) -> Self {
self.theme.type_scale = scale;
self
}
pub fn map_type_scale(mut self, f: impl FnOnce(TypeScale) -> TypeScale) -> Self {
self.theme.type_scale = f(self.theme.type_scale);
self
}
pub fn shape(mut self, shape: ShapeScale) -> Self {
self.theme.shape = shape;
self
}
pub fn map_shape(mut self, f: impl FnOnce(ShapeScale) -> ShapeScale) -> Self {
self.theme.shape = f(self.theme.shape);
self
}
pub fn elevation(mut self, elevation: Elevation) -> Self {
self.theme.elevation = elevation;
self
}
pub fn map_elevation(mut self, f: impl FnOnce(Elevation) -> Elevation) -> Self {
self.theme.elevation = f(self.theme.elevation);
self
}
pub fn motion(mut self, motion: MotionScheme) -> Self {
self.theme.motion = motion;
self
}
pub fn map_motion(mut self, f: impl FnOnce(MotionScheme) -> MotionScheme) -> Self {
self.theme.motion = f(self.theme.motion);
self
}
pub fn glass(mut self, glass: GlassScale) -> Self {
self.theme.glass = glass;
self
}
pub fn map_glass(mut self, f: impl FnOnce(GlassScale) -> GlassScale) -> Self {
self.theme.glass = f(self.theme.glass);
self
}
pub fn brightness(mut self, brightness: Brightness) -> Self {
self.theme.brightness = brightness;
self
}
pub fn design_language(mut self, design_language: DesignLanguage) -> Self {
self.theme.design_language = design_language;
self
}
pub fn extension<T: Any + Send + Sync>(mut self, ext: T) -> Self {
self.theme.extensions.insert(ext);
self
}
pub fn build(self) -> Theme {
self.theme
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::status::StatusPalette;
const BRAND: peniko::Color = peniko::Color::from_rgb8(0xFF, 0x6A, 0x00);
#[test]
fn round_trip_is_field_for_field_identical() {
let base = Theme::neutral();
let rebuilt = Theme::builder(base.clone()).build();
assert_eq!(rebuilt, base);
let design_system = Theme::builder(Theme::neutral())
.design_language(DesignLanguage::Cupertino)
.map_colors_light(|c| ColorScheme {
primary: BRAND,
..c
})
.build();
assert_ne!(design_system, base);
let rebuilt = Theme::builder(design_system.clone()).build();
assert_eq!(rebuilt, design_system);
}
#[test]
fn whole_group_swap_replaces_only_that_group() {
let base = Theme::neutral();
let swapped_dark = ColorScheme {
primary: BRAND,
..ColorScheme::neutral_dark()
};
let theme = Theme::builder(base.clone())
.colors_dark(swapped_dark)
.build();
assert_eq!(theme.dark.primary, BRAND);
assert_eq!(theme.light, base.light);
assert_eq!(theme.shape, base.shape);
assert_eq!(theme.motion, base.motion);
assert_eq!(theme.elevation, base.elevation);
assert_eq!(theme.type_scale, base.type_scale);
assert_eq!(theme.glass, base.glass);
assert_eq!(theme.brightness, base.brightness);
assert_eq!(theme.design_language, base.design_language);
}
#[test]
fn closure_edit_changes_only_the_named_token() {
let base = Theme::neutral();
let theme = Theme::builder(base.clone())
.map_shape(|s| ShapeScale { medium: 8.0, ..s })
.build();
assert_eq!(theme.shape.medium, 8.0);
assert_eq!(theme.shape.large, base.shape.large);
assert_eq!(theme.shape.none, base.shape.none);
assert_eq!(theme.light, base.light);
assert_eq!(theme.motion, base.motion);
}
#[test]
fn extension_insert_round_trips() {
#[derive(Debug, Clone, PartialEq)]
struct AppTokens {
brand_name: &'static str,
}
let theme = Theme::builder(Theme::neutral())
.extension(AppTokens { brand_name: "Acme" })
.build();
assert_eq!(
theme.extension::<AppTokens>(),
Some(&AppTokens { brand_name: "Acme" })
);
assert_eq!(
theme.extension::<StatusPalette>(),
Some(&StatusPalette::neutral())
);
}
#[test]
fn extension_replace_by_type_id_last_write_wins() {
#[derive(Debug, Clone, PartialEq)]
struct Marker(u32);
let theme = Theme::builder(Theme::neutral())
.extension(Marker(1))
.extension(Marker(2))
.build();
assert_eq!(theme.extension::<Marker>(), Some(&Marker(2)));
}
#[test]
fn application_order_is_last_write_wins_per_group() {
let first = ShapeScale {
medium: 8.0,
..ShapeScale::neutral()
};
let second = ShapeScale {
medium: 16.0,
..ShapeScale::neutral()
};
let theme = Theme::builder(Theme::neutral())
.shape(first)
.shape(second)
.build();
assert_eq!(theme.shape.medium, 16.0);
let theme = Theme::builder(Theme::neutral())
.shape(first)
.map_shape(|s| ShapeScale { large: 99.0, ..s })
.build();
assert_eq!(theme.shape.medium, 8.0);
assert_eq!(theme.shape.large, 99.0);
}
#[test]
fn brightness_and_design_language_setters_apply() {
let theme = Theme::builder(Theme::neutral())
.brightness(Brightness::Dark)
.design_language(DesignLanguage::Cupertino)
.build();
assert_eq!(theme.brightness, Brightness::Dark);
assert_eq!(theme.design_language, DesignLanguage::Cupertino);
assert_eq!(theme.scheme(), &theme.dark);
}
}