use crate::dynamic::color_spec::{Platform, SpecVersion};
use crate::dynamic::dynamic_scheme::DynamicScheme;
use crate::dynamic::material_dynamic_colors::MaterialDynamicColors;
use crate::dynamic::variant::Variant;
use crate::helpers::{MaterializedScheme, MaterializedSchemeGroup, MaterializedTheme};
use crate::scheme::{
SchemeCmf, SchemeContent, SchemeExpressive, SchemeFidelity, SchemeFruitSalad, SchemeMonochrome,
SchemeNeutral, SchemeRainbow, SchemeTonalSpot, SchemeVibrant,
};
use crate::utils::color_utils::Argb;
#[bon::builder]
pub fn theme_from_color(
#[builder(start_fn)]
source_color: Argb,
#[builder(default = Variant::Vibrant)]
variant: Variant,
#[builder(default = 0.0)]
contrast_level: f64,
#[builder(default = SpecVersion::Spec2026)]
spec_version: SpecVersion,
#[builder(default = Platform::Phone)]
platform: Platform,
) -> MaterializedTheme {
let light_scheme = create_dynamic_scheme(
source_color,
variant,
false,
contrast_level,
spec_version,
platform,
);
let dark_scheme = create_dynamic_scheme(
source_color,
variant,
true,
contrast_level,
spec_version,
platform,
);
let mdc = MaterialDynamicColors::new_with_spec(spec_version);
#[cfg(feature = "rayon")]
let (light, dark) = rayon::join(
|| materialize(&light_scheme, &mdc),
|| materialize(&dark_scheme, &mdc),
);
#[cfg(not(feature = "rayon"))]
let light = materialize(&light_scheme, &mdc);
#[cfg(not(feature = "rayon"))]
let dark = materialize(&dark_scheme, &mdc);
MaterializedTheme {
source_color,
variant,
contrast_level,
platform,
spec_version,
schemes: MaterializedSchemeGroup { light, dark },
}
}
fn create_dynamic_scheme(
source_color: Argb,
variant: Variant,
is_dark: bool,
contrast_level: f64,
spec_version: SpecVersion,
platform: Platform,
) -> DynamicScheme {
match variant {
Variant::TonalSpot => SchemeTonalSpot::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
Variant::Vibrant => SchemeVibrant::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
Variant::Expressive => SchemeExpressive::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
Variant::Content => SchemeContent::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
Variant::Fidelity => SchemeFidelity::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
Variant::Monochrome => SchemeMonochrome::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
Variant::Neutral => SchemeNeutral::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
Variant::Rainbow => SchemeRainbow::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
Variant::FruitSalad => SchemeFruitSalad::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
Variant::Cmf => SchemeCmf::builder(source_color, is_dark, contrast_level)
.spec_version(spec_version)
.platform(platform)
.build(),
}
}
fn materialize(scheme: &DynamicScheme, mdc: &MaterialDynamicColors) -> MaterializedScheme {
#[cfg(feature = "rayon")]
let (
(
(
background,
on_background,
surface,
surface_dim,
surface_bright,
surface_container_lowest,
surface_container_low,
surface_container,
surface_container_high,
surface_container_highest,
on_surface,
surface_variant,
on_surface_variant,
inverse_surface,
inverse_on_surface,
),
(outline, outline_variant, shadow, scrim, surface_tint),
),
(
(
(primary, on_primary, primary_container, on_primary_container, inverse_primary),
(secondary, on_secondary, secondary_container, on_secondary_container),
(tertiary, on_tertiary, tertiary_container, on_tertiary_container),
(error, on_error, error_container, on_error_container),
),
(
(primary_fixed, primary_fixed_dim, on_primary_fixed, on_primary_fixed_variant),
(
secondary_fixed,
secondary_fixed_dim,
on_secondary_fixed,
on_secondary_fixed_variant,
),
(tertiary_fixed, tertiary_fixed_dim, on_tertiary_fixed, on_tertiary_fixed_variant),
),
),
) = rayon::join(
|| {
rayon::join(
|| {
(
scheme.get_argb(&mdc.background()),
scheme.get_argb(&mdc.on_background()),
scheme.get_argb(&mdc.surface()),
scheme.get_argb(&mdc.surface_dim()),
scheme.get_argb(&mdc.surface_bright()),
scheme.get_argb(&mdc.surface_container_lowest()),
scheme.get_argb(&mdc.surface_container_low()),
scheme.get_argb(&mdc.surface_container()),
scheme.get_argb(&mdc.surface_container_high()),
scheme.get_argb(&mdc.surface_container_highest()),
scheme.get_argb(&mdc.on_surface()),
scheme.get_argb(&mdc.surface_variant()),
scheme.get_argb(&mdc.on_surface_variant()),
scheme.get_argb(&mdc.inverse_surface()),
scheme.get_argb(&mdc.inverse_on_surface()),
)
},
|| {
(
scheme.get_argb(&mdc.outline()),
scheme.get_argb(&mdc.outline_variant()),
scheme.get_argb(&mdc.shadow()),
scheme.get_argb(&mdc.scrim()),
scheme.get_argb(&mdc.surface_tint()),
)
},
)
},
|| {
rayon::join(
|| {
(
(
scheme.get_argb(&mdc.primary()),
scheme.get_argb(&mdc.on_primary()),
scheme.get_argb(&mdc.primary_container()),
scheme.get_argb(&mdc.on_primary_container()),
scheme.get_argb(&mdc.inverse_primary()),
),
(
scheme.get_argb(&mdc.secondary()),
scheme.get_argb(&mdc.on_secondary()),
scheme.get_argb(&mdc.secondary_container()),
scheme.get_argb(&mdc.on_secondary_container()),
),
(
scheme.get_argb(&mdc.tertiary()),
scheme.get_argb(&mdc.on_tertiary()),
scheme.get_argb(&mdc.tertiary_container()),
scheme.get_argb(&mdc.on_tertiary_container()),
),
(
scheme.get_argb(&mdc.error()),
scheme.get_argb(&mdc.on_error()),
scheme.get_argb(&mdc.error_container()),
scheme.get_argb(&mdc.on_error_container()),
),
)
},
|| {
(
(
scheme.get_argb(&mdc.primary_fixed()),
scheme.get_argb(&mdc.primary_fixed_dim()),
scheme.get_argb(&mdc.on_primary_fixed()),
scheme.get_argb(&mdc.on_primary_fixed_variant()),
),
(
scheme.get_argb(&mdc.secondary_fixed()),
scheme.get_argb(&mdc.secondary_fixed_dim()),
scheme.get_argb(&mdc.on_secondary_fixed()),
scheme.get_argb(&mdc.on_secondary_fixed_variant()),
),
(
scheme.get_argb(&mdc.tertiary_fixed()),
scheme.get_argb(&mdc.tertiary_fixed_dim()),
scheme.get_argb(&mdc.on_tertiary_fixed()),
scheme.get_argb(&mdc.on_tertiary_fixed_variant()),
),
)
},
)
},
);
#[cfg(not(feature = "rayon"))]
let (
background,
on_background,
surface,
surface_dim,
surface_bright,
surface_container_lowest,
surface_container_low,
surface_container,
surface_container_high,
surface_container_highest,
on_surface,
surface_variant,
on_surface_variant,
inverse_surface,
inverse_on_surface,
) = (
scheme.get_argb(&mdc.background()),
scheme.get_argb(&mdc.on_background()),
scheme.get_argb(&mdc.surface()),
scheme.get_argb(&mdc.surface_dim()),
scheme.get_argb(&mdc.surface_bright()),
scheme.get_argb(&mdc.surface_container_lowest()),
scheme.get_argb(&mdc.surface_container_low()),
scheme.get_argb(&mdc.surface_container()),
scheme.get_argb(&mdc.surface_container_high()),
scheme.get_argb(&mdc.surface_container_highest()),
scheme.get_argb(&mdc.on_surface()),
scheme.get_argb(&mdc.surface_variant()),
scheme.get_argb(&mdc.on_surface_variant()),
scheme.get_argb(&mdc.inverse_surface()),
scheme.get_argb(&mdc.inverse_on_surface()),
);
#[cfg(not(feature = "rayon"))]
let (outline, outline_variant, shadow, scrim, surface_tint) = (
scheme.get_argb(&mdc.outline()),
scheme.get_argb(&mdc.outline_variant()),
scheme.get_argb(&mdc.shadow()),
scheme.get_argb(&mdc.scrim()),
scheme.get_argb(&mdc.surface_tint()),
);
#[cfg(not(feature = "rayon"))]
let (primary, on_primary, primary_container, on_primary_container, inverse_primary) = (
scheme.get_argb(&mdc.primary()),
scheme.get_argb(&mdc.on_primary()),
scheme.get_argb(&mdc.primary_container()),
scheme.get_argb(&mdc.on_primary_container()),
scheme.get_argb(&mdc.inverse_primary()),
);
#[cfg(not(feature = "rayon"))]
let (secondary, on_secondary, secondary_container, on_secondary_container) = (
scheme.get_argb(&mdc.secondary()),
scheme.get_argb(&mdc.on_secondary()),
scheme.get_argb(&mdc.secondary_container()),
scheme.get_argb(&mdc.on_secondary_container()),
);
#[cfg(not(feature = "rayon"))]
let (tertiary, on_tertiary, tertiary_container, on_tertiary_container) = (
scheme.get_argb(&mdc.tertiary()),
scheme.get_argb(&mdc.on_tertiary()),
scheme.get_argb(&mdc.tertiary_container()),
scheme.get_argb(&mdc.on_tertiary_container()),
);
#[cfg(not(feature = "rayon"))]
let (error, on_error, error_container, on_error_container) = (
scheme.get_argb(&mdc.error()),
scheme.get_argb(&mdc.on_error()),
scheme.get_argb(&mdc.error_container()),
scheme.get_argb(&mdc.on_error_container()),
);
#[cfg(not(feature = "rayon"))]
let (primary_fixed, primary_fixed_dim, on_primary_fixed, on_primary_fixed_variant) = (
scheme.get_argb(&mdc.primary_fixed()),
scheme.get_argb(&mdc.primary_fixed_dim()),
scheme.get_argb(&mdc.on_primary_fixed()),
scheme.get_argb(&mdc.on_primary_fixed_variant()),
);
#[cfg(not(feature = "rayon"))]
let (secondary_fixed, secondary_fixed_dim, on_secondary_fixed, on_secondary_fixed_variant) = (
scheme.get_argb(&mdc.secondary_fixed()),
scheme.get_argb(&mdc.secondary_fixed_dim()),
scheme.get_argb(&mdc.on_secondary_fixed()),
scheme.get_argb(&mdc.on_secondary_fixed_variant()),
);
#[cfg(not(feature = "rayon"))]
let (tertiary_fixed, tertiary_fixed_dim, on_tertiary_fixed, on_tertiary_fixed_variant) = (
scheme.get_argb(&mdc.tertiary_fixed()),
scheme.get_argb(&mdc.tertiary_fixed_dim()),
scheme.get_argb(&mdc.on_tertiary_fixed()),
scheme.get_argb(&mdc.on_tertiary_fixed_variant()),
);
MaterializedScheme {
is_dark: scheme.is_dark,
source_color: scheme.source_color_argb(),
variant: scheme.variant,
contrast_level: scheme.contrast_level,
platform: scheme.platform,
spec_version: scheme.spec_version,
primary_palette: scheme.primary_palette.clone(),
secondary_palette: scheme.secondary_palette.clone(),
tertiary_palette: scheme.tertiary_palette.clone(),
neutral_palette: scheme.neutral_palette.clone(),
neutral_variant_palette: scheme.neutral_variant_palette.clone(),
error_palette: scheme.error_palette.clone(),
background,
on_background,
surface,
surface_dim,
surface_bright,
surface_container_lowest,
surface_container_low,
surface_container,
surface_container_high,
surface_container_highest,
on_surface,
surface_variant,
on_surface_variant,
inverse_surface,
inverse_on_surface,
outline,
outline_variant,
shadow,
scrim,
surface_tint,
primary,
on_primary,
primary_container,
on_primary_container,
inverse_primary,
secondary,
on_secondary,
secondary_container,
on_secondary_container,
tertiary,
on_tertiary,
tertiary_container,
on_tertiary_container,
error,
on_error,
error_container,
on_error_container,
primary_fixed,
primary_fixed_dim,
on_primary_fixed,
on_primary_fixed_variant,
secondary_fixed,
secondary_fixed_dim,
on_secondary_fixed,
on_secondary_fixed_variant,
tertiary_fixed,
tertiary_fixed_dim,
on_tertiary_fixed,
on_tertiary_fixed_variant,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dynamic::variant::Variant;
use crate::hct::Hct;
use crate::utils::color_utils::Argb;
const GOOGLE_BLUE: Argb = Argb(0xFF4285F4);
#[test]
fn test_theme_generation_basic() {
let theme = theme_from_color(GOOGLE_BLUE).call();
assert_eq!(theme.source_color, GOOGLE_BLUE);
assert_eq!(theme.variant, Variant::Vibrant);
assert!(!theme.schemes.light.is_dark);
assert!(theme.schemes.dark.is_dark);
assert_ne!(theme.schemes.light.primary.0, 0);
assert_ne!(theme.schemes.dark.primary.0, 0);
}
#[test]
fn test_theme_variant_monochrome() {
let theme = theme_from_color(GOOGLE_BLUE)
.variant(Variant::Monochrome)
.call();
assert_eq!(theme.variant, Variant::Monochrome);
let primary_hct = Hct::from_argb(theme.schemes.light.primary);
assert!(primary_hct.chroma() < 2.0);
let secondary_hct = Hct::from_argb(theme.schemes.light.secondary);
assert!(secondary_hct.chroma() < 2.0);
}
#[test]
fn test_contrast_levels_affect_output() {
let low_contrast = theme_from_color(GOOGLE_BLUE).contrast_level(-1.0).call();
let high_contrast = theme_from_color(GOOGLE_BLUE).contrast_level(1.0).call();
assert_ne!(
low_contrast.schemes.light.primary,
high_contrast.schemes.light.primary
);
let on_primary_low = Hct::from_argb(low_contrast.schemes.light.on_primary).tone();
let on_primary_high = Hct::from_argb(high_contrast.schemes.light.on_primary).tone();
assert!(on_primary_high < on_primary_low);
}
#[test]
fn test_light_dark_contrast_polarization() {
let theme = theme_from_color(GOOGLE_BLUE).call();
let light_bg = Hct::from_argb(theme.schemes.light.background).tone();
let dark_bg = Hct::from_argb(theme.schemes.dark.background).tone();
assert!(light_bg > 80.0);
assert!(dark_bg < 20.0);
}
#[test]
fn test_fixed_colors_presence() {
let theme = theme_from_color(GOOGLE_BLUE).call();
let light = &theme.schemes.light;
assert_ne!(light.primary_fixed.0, 0);
assert_ne!(light.secondary_fixed_dim.0, 0);
let tone_fixed = Hct::from_argb(light.primary_fixed).tone();
assert!(tone_fixed > 40. && tone_fixed < 70.);
}
#[test]
fn test_spec_version_consistency() {
let theme_2021 = theme_from_color(GOOGLE_BLUE)
.spec_version(SpecVersion::Spec2021)
.call();
let theme_2026 = theme_from_color(GOOGLE_BLUE)
.spec_version(SpecVersion::Spec2026)
.call();
assert_ne!(
theme_2021.schemes.light.surface_container,
theme_2026.schemes.light.surface_container
);
}
}