use std::any::Any;
use frust_core::widget::{LayoutCtx, PaintCtx};
use frust_text::TextStyle;
use crate::color::{Brightness, ColorScheme};
use crate::elevation::Elevation;
use crate::extensions::ThemeExtensions;
use crate::glass::GlassScale;
use crate::motion::MotionScheme;
use crate::shape::ShapeScale;
use crate::status::StatusPalette;
use crate::typography::TypeScale;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DesignLanguage {
#[default]
Material3,
Cupertino,
Glyph,
Custom(&'static str),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Theme {
pub light: ColorScheme,
pub dark: ColorScheme,
pub type_scale: TypeScale,
pub shape: ShapeScale,
pub elevation: Elevation,
pub motion: MotionScheme,
pub glass: GlassScale,
pub brightness: Brightness,
pub design_language: DesignLanguage,
pub extensions: ThemeExtensions,
}
impl Theme {
pub fn neutral() -> Self {
let mut extensions = ThemeExtensions::new();
extensions.insert(StatusPalette::neutral());
Self {
light: ColorScheme::neutral_light(),
dark: ColorScheme::neutral_dark(),
type_scale: TypeScale::neutral(&TextStyle::default()),
shape: ShapeScale::neutral(),
elevation: Elevation::neutral(),
motion: MotionScheme::neutral(),
glass: GlassScale::opaque_material(),
brightness: Brightness::Light,
design_language: DesignLanguage::default(),
extensions,
}
}
pub fn scheme(&self) -> &ColorScheme {
match self.brightness {
Brightness::Light => &self.light,
Brightness::Dark => &self.dark,
}
}
pub fn with_brightness(mut self, brightness: Brightness) -> Self {
self.brightness = brightness;
self
}
pub fn from_paint_ctx<'a>(ctx: &'a PaintCtx<'_>) -> Option<&'a Theme> {
ctx.theme_as::<Theme>()
}
pub fn from_layout_ctx<'a>(ctx: &'a LayoutCtx<'_>) -> Option<&'a Theme> {
ctx.theme_as::<Theme>()
}
pub fn extension<T: Any + Send + Sync>(&self) -> Option<&T> {
self.extensions.get::<T>()
}
pub fn builder(base: Theme) -> crate::builder::ThemeBuilder {
crate::builder::ThemeBuilder::new(base)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scheme_selects_by_brightness() {
let mut theme = Theme::neutral();
assert_eq!(theme.scheme(), &theme.light);
theme.brightness = Brightness::Dark;
assert_eq!(theme.scheme(), &theme.dark);
}
#[test]
fn from_paint_ctx_recovers_a_threaded_theme() {
use frust_core::widget::PaintCtx;
use peniko::kurbo::{Point, Size};
let theme = Theme::neutral();
let ctx = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0)).with_theme(&theme);
assert_eq!(Theme::from_paint_ctx(&ctx), Some(&theme));
let bare = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0));
assert!(Theme::from_paint_ctx(&bare).is_none());
}
#[test]
fn from_layout_ctx_recovers_a_threaded_theme() {
use frust_core::widget::LayoutCtx;
let theme = Theme::neutral();
let ctx = LayoutCtx::new().with_theme(&theme);
assert_eq!(Theme::from_layout_ctx(&ctx), Some(&theme));
let bare = LayoutCtx::new();
assert!(Theme::from_layout_ctx(&bare).is_none());
}
#[test]
fn design_language_defaults_to_material3() {
assert_eq!(DesignLanguage::default(), DesignLanguage::Material3);
}
#[test]
fn with_brightness_forces_design_language_without_discarding_brightness() {
let material = Theme::builder(Theme::neutral())
.design_language(DesignLanguage::Material3)
.build();
let cupertino = Theme::builder(Theme::neutral())
.design_language(DesignLanguage::Cupertino)
.build();
assert_ne!(material.design_language, cupertino.design_language);
let theme = material.clone().with_brightness(Brightness::Dark);
assert_eq!(theme.brightness, Brightness::Dark);
assert_eq!(theme.design_language, DesignLanguage::Material3);
assert_eq!(theme.scheme(), &theme.dark);
let theme = cupertino.with_brightness(Brightness::Dark);
assert_eq!(theme.brightness, Brightness::Dark);
assert_eq!(theme.design_language, DesignLanguage::Cupertino);
assert_eq!(theme.scheme(), &theme.dark);
let theme = material.with_brightness(Brightness::Light);
assert_eq!(theme.brightness, Brightness::Light);
}
#[test]
fn custom_extension_type_round_trips() {
#[derive(Debug, PartialEq)]
struct AppTokens {
brand_name: &'static str,
}
let mut theme = Theme::neutral();
assert!(theme.extension::<AppTokens>().is_none());
theme.extensions.insert(AppTokens { brand_name: "Acme" });
assert_eq!(
theme.extension::<AppTokens>(),
Some(&AppTokens { brand_name: "Acme" })
);
use crate::status::StatusPalette;
assert_eq!(
theme.extension::<StatusPalette>(),
Some(&StatusPalette::neutral())
);
}
#[test]
fn theme_extensions_field_survives_clone() {
#[derive(Debug, PartialEq)]
struct Marker;
let mut theme = Theme::neutral();
theme.extensions.insert(Marker);
let cloned = theme.clone();
assert_eq!(cloned.extension::<Marker>(), Some(&Marker));
}
#[test]
fn neutral_populates_every_role_in_both_brightnesses() {
let theme = Theme::neutral();
assert_eq!(theme.light, ColorScheme::neutral_light());
assert_eq!(theme.dark, ColorScheme::neutral_dark());
assert_eq!(theme.shape, ShapeScale::neutral());
assert_eq!(theme.elevation, Elevation::neutral());
assert_eq!(theme.motion, MotionScheme::neutral());
assert_eq!(theme.glass, GlassScale::opaque_material());
assert_eq!(theme.brightness, Brightness::Light);
}
#[test]
fn neutral_with_brightness_selects_both_schemes() {
let light = Theme::neutral();
assert_eq!(light.scheme(), &light.light);
let dark = Theme::neutral().with_brightness(Brightness::Dark);
assert_eq!(dark.scheme(), &dark.dark);
assert_ne!(dark.scheme().surface, light.scheme().surface);
}
#[test]
fn neutral_attaches_the_neutral_status_palette_extension() {
use crate::status::StatusPalette;
assert_eq!(
Theme::neutral().extension::<StatusPalette>(),
Some(&StatusPalette::neutral())
);
}
#[test]
fn neutral_design_language_stays_the_default() {
assert_eq!(Theme::neutral().design_language, DesignLanguage::default());
}
#[test]
fn neutral_type_scale_references_no_bundled_font() {
let theme = Theme::neutral();
assert!(matches!(
theme.type_scale.body_large.family,
frust_text::FontFamily::NamedWithGeneric(_)
));
}
#[test]
fn neutral_survives_clone_and_extends_independently() {
let theme = Theme::neutral();
let cloned = theme.clone();
assert_eq!(cloned, theme);
}
#[test]
fn custom_design_language_compares_by_content() {
assert_eq!(DesignLanguage::Custom("x"), DesignLanguage::Custom("x"));
assert_ne!(DesignLanguage::Custom("x"), DesignLanguage::Custom("y"));
assert_ne!(DesignLanguage::Custom("x"), DesignLanguage::Material3);
}
#[test]
fn custom_design_language_round_trips_through_the_builder() {
let theme = Theme::builder(Theme::neutral())
.design_language(DesignLanguage::Custom("sample"))
.build();
assert_eq!(theme.design_language, DesignLanguage::Custom("sample"));
}
}