use mcu_contrast::Contrast;
use mcu_hct::Hct;
use mcu_palettes::TonalPalette;
use std::sync::Arc;
use crate::color_calculation::get_spec;
use crate::{ContrastCurve, ToneDeltaPair};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DynamicColorError {
MissingBackground { color_name: String },
MissingBackgroundForContrast { color_name: String },
MissingContrastCurve { color_name: String },
}
impl std::fmt::Display for DynamicColorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingBackground { color_name } => {
write!(
f,
"Color '{}' has second_background but no background",
color_name
)
}
Self::MissingBackgroundForContrast { color_name } => {
write!(
f,
"Color '{}' has contrast_curve but no background",
color_name
)
}
Self::MissingContrastCurve { color_name } => {
write!(
f,
"Color '{}' has background but no contrast_curve",
color_name
)
}
}
}
}
impl std::error::Error for DynamicColorError {}
pub use crate::dynamic_scheme::DynamicScheme;
#[derive(Clone)]
pub struct DynamicColor {
pub name: String,
pub palette: Arc<dyn Fn(&DynamicScheme) -> TonalPalette + Send + Sync>,
pub tone: Arc<dyn Fn(&DynamicScheme) -> f64 + Send + Sync>,
pub is_background: bool,
pub chroma_multiplier: Option<Arc<dyn Fn(&DynamicScheme) -> f64 + Send + Sync>>,
pub background: Option<Arc<dyn Fn(&DynamicScheme) -> Option<DynamicColor> + Send + Sync>>,
pub second_background:
Option<Arc<dyn Fn(&DynamicScheme) -> Option<DynamicColor> + Send + Sync>>,
pub contrast_curve: Option<Arc<dyn Fn(&DynamicScheme) -> Option<ContrastCurve> + Send + Sync>>,
pub tone_delta_pair: Option<Arc<dyn Fn(&DynamicScheme) -> Option<ToneDeltaPair> + Send + Sync>>,
}
impl DynamicColor {
#[allow(clippy::too_many_arguments)]
pub fn from_palette(
name: impl Into<String>,
palette: impl Fn(&DynamicScheme) -> TonalPalette + Send + Sync + 'static,
tone: Option<impl Fn(&DynamicScheme) -> f64 + Send + Sync + 'static>,
is_background: bool,
chroma_multiplier: Option<impl Fn(&DynamicScheme) -> f64 + Send + Sync + 'static>,
background: Option<impl Fn(&DynamicScheme) -> Option<DynamicColor> + Send + Sync + 'static>,
second_background: Option<
impl Fn(&DynamicScheme) -> Option<DynamicColor> + Send + Sync + 'static,
>,
contrast_curve: Option<
impl Fn(&DynamicScheme) -> Option<ContrastCurve> + Send + Sync + 'static,
>,
tone_delta_pair: Option<
impl Fn(&DynamicScheme) -> Option<ToneDeltaPair> + Send + Sync + 'static,
>,
) -> Self {
Self::try_from_palette(
name,
palette,
tone,
is_background,
chroma_multiplier,
background,
second_background,
contrast_curve,
tone_delta_pair,
)
.expect("invalid DynamicColor configuration")
}
#[allow(clippy::too_many_arguments)]
pub fn try_from_palette(
name: impl Into<String>,
palette: impl Fn(&DynamicScheme) -> TonalPalette + Send + Sync + 'static,
tone: Option<impl Fn(&DynamicScheme) -> f64 + Send + Sync + 'static>,
is_background: bool,
chroma_multiplier: Option<impl Fn(&DynamicScheme) -> f64 + Send + Sync + 'static>,
background: Option<impl Fn(&DynamicScheme) -> Option<DynamicColor> + Send + Sync + 'static>,
second_background: Option<
impl Fn(&DynamicScheme) -> Option<DynamicColor> + Send + Sync + 'static,
>,
contrast_curve: Option<
impl Fn(&DynamicScheme) -> Option<ContrastCurve> + Send + Sync + 'static,
>,
tone_delta_pair: Option<
impl Fn(&DynamicScheme) -> Option<ToneDeltaPair> + Send + Sync + 'static,
>,
) -> Result<Self, DynamicColorError> {
let name = name.into();
if background.is_none() && second_background.is_some() {
return Err(DynamicColorError::MissingBackground { color_name: name });
}
if background.is_none() && contrast_curve.is_some() {
return Err(DynamicColorError::MissingBackgroundForContrast { color_name: name });
}
if background.is_some() && contrast_curve.is_none() {
return Err(DynamicColorError::MissingContrastCurve { color_name: name });
}
let tone_fn: Arc<dyn Fn(&DynamicScheme) -> f64 + Send + Sync> = match tone {
Some(t) => Arc::new(t),
None => {
Arc::new(|_scheme: &DynamicScheme| 50.0)
}
};
Ok(DynamicColor {
name,
palette: Arc::new(palette),
tone: tone_fn,
is_background,
chroma_multiplier: chroma_multiplier
.map(|f| Arc::new(f) as Arc<dyn Fn(&DynamicScheme) -> f64 + Send + Sync>),
background: background.map(|f| {
Arc::new(f) as Arc<dyn Fn(&DynamicScheme) -> Option<DynamicColor> + Send + Sync>
}),
second_background: second_background.map(|f| {
Arc::new(f) as Arc<dyn Fn(&DynamicScheme) -> Option<DynamicColor> + Send + Sync>
}),
contrast_curve: contrast_curve.map(|f| {
Arc::new(f) as Arc<dyn Fn(&DynamicScheme) -> Option<ContrastCurve> + Send + Sync>
}),
tone_delta_pair: tone_delta_pair.map(|f| {
Arc::new(f) as Arc<dyn Fn(&DynamicScheme) -> Option<ToneDeltaPair> + Send + Sync>
}),
})
}
pub fn get_argb(&self, scheme: &DynamicScheme) -> u32 {
self.get_hct(scheme).to_int()
}
pub fn get_hct(&self, scheme: &DynamicScheme) -> Hct {
get_spec(scheme.spec_version).get_hct(scheme, self)
}
pub fn get_tone(&self, scheme: &DynamicScheme) -> f64 {
get_spec(scheme.spec_version).get_tone(scheme, self)
}
pub fn foreground_tone(bg_tone: f64, ratio: f64) -> f64 {
let lighter_tone = Contrast::lighter_clamped(bg_tone, ratio);
let darker_tone = Contrast::darker_clamped(bg_tone, ratio);
let lighter_ratio = Contrast::ratio_of_tones(lighter_tone, bg_tone);
let darker_ratio = Contrast::ratio_of_tones(darker_tone, bg_tone);
let prefers_lighter = Self::tone_prefers_light_foreground(bg_tone);
if prefers_lighter {
let negligible_diff = (lighter_ratio - darker_ratio).abs() < 0.1
&& lighter_ratio < ratio
&& darker_ratio < ratio;
if lighter_ratio >= ratio || lighter_ratio >= darker_ratio || negligible_diff {
lighter_tone
} else {
darker_tone
}
} else {
if darker_ratio >= ratio || darker_ratio >= lighter_ratio {
darker_tone
} else {
lighter_tone
}
}
}
pub fn tone_prefers_light_foreground(tone: f64) -> bool {
tone.round() < 60.0
}
pub fn tone_allows_light_foreground(tone: f64) -> bool {
tone.round() <= 49.0
}
pub fn enable_light_foreground(tone: f64) -> f64 {
if Self::tone_prefers_light_foreground(tone) && !Self::tone_allows_light_foreground(tone) {
49.0
} else {
tone
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_palette(_scheme: &DynamicScheme) -> TonalPalette {
TonalPalette::from_hue_and_chroma(200.0, 50.0)
}
fn test_tone(_scheme: &DynamicScheme) -> f64 {
40.0
}
fn surface_tone(_scheme: &DynamicScheme) -> f64 {
95.0
}
#[test]
fn test_dynamic_color_can_be_created() {
let color = DynamicColor::from_palette(
"test_color",
test_palette,
Some(test_tone as fn(&DynamicScheme) -> f64),
false,
None::<fn(&DynamicScheme) -> f64>,
None::<fn(&DynamicScheme) -> Option<DynamicColor>>,
None::<fn(&DynamicScheme) -> Option<DynamicColor>>,
None::<fn(&DynamicScheme) -> Option<ContrastCurve>>,
None::<fn(&DynamicScheme) -> Option<ToneDeltaPair>>,
);
assert_eq!(color.name, "test_color");
assert!(!color.is_background);
}
#[test]
fn test_dynamic_color_is_background() {
let color = DynamicColor::from_palette(
"surface",
test_palette,
Some(surface_tone as fn(&DynamicScheme) -> f64),
true, None::<fn(&DynamicScheme) -> f64>,
None::<fn(&DynamicScheme) -> Option<DynamicColor>>,
None::<fn(&DynamicScheme) -> Option<DynamicColor>>,
None::<fn(&DynamicScheme) -> Option<ContrastCurve>>,
None::<fn(&DynamicScheme) -> Option<ToneDeltaPair>>,
);
assert!(color.is_background);
}
#[test]
fn test_dynamic_color_clone() {
let color1 = DynamicColor::from_palette(
"test",
test_palette,
Some(test_tone as fn(&DynamicScheme) -> f64),
false,
None::<fn(&DynamicScheme) -> f64>,
None::<fn(&DynamicScheme) -> Option<DynamicColor>>,
None::<fn(&DynamicScheme) -> Option<DynamicColor>>,
None::<fn(&DynamicScheme) -> Option<ContrastCurve>>,
None::<fn(&DynamicScheme) -> Option<ToneDeltaPair>>,
);
let color2 = color1.clone();
assert_eq!(color1.name, color2.name);
assert_eq!(color1.is_background, color2.is_background);
}
}