use crate::contrast::contrast_utils::Contrast;
use crate::dynamic::color_spec::SpecVersion;
use crate::dynamic::contrast_curve::ContrastCurve;
use crate::dynamic::dynamic_scheme::DynamicScheme;
use crate::dynamic::tone_delta_pair::ToneDeltaPair;
use crate::hct::hct_color::Hct;
use crate::palettes::tonal_palette::TonalPalette;
use crate::utils::color_utils::Argb;
use std::fmt;
use std::fmt::Debug;
use std::sync::Arc;
pub type DynamicColorFunction<T> = Arc<dyn Fn(&DynamicScheme) -> T + Send + Sync>;
pub struct ContrastConstraints {
pub background: DynamicColorFunction<Option<Arc<DynamicColor>>>,
pub contrast_curve: DynamicColorFunction<Option<ContrastCurve>>,
pub second_background: Option<DynamicColorFunction<Option<Arc<DynamicColor>>>>,
}
pub struct DynamicColor {
pub name: String,
pub palette: DynamicColorFunction<TonalPalette>,
pub is_background: bool,
pub tone: DynamicColorFunction<f64>,
pub chroma_multiplier: Option<DynamicColorFunction<f64>>,
pub tone_delta_pair: Option<DynamicColorFunction<Option<ToneDeltaPair>>>,
pub opacity: Option<DynamicColorFunction<Option<f64>>>,
pub contrast: Option<ContrastConstraints>,
}
impl Debug for DynamicColor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DynamicColor")
.field("name", &self.name)
.field("is_background", &self.is_background)
.field("palette", &"<function>")
.field("tone", &"<function>")
.field("has_contrast_constraints", &self.contrast.is_some())
.field(
"chroma_multiplier",
&self.chroma_multiplier.as_ref().map(|_| "<function>"),
)
.field(
"tone_delta_pair",
&self.tone_delta_pair.as_ref().map(|_| "<function>"),
)
.field("opacity", &self.opacity.as_ref().map(|_| "<function>"))
.finish()
}
}
impl DynamicColor {
pub fn new(
name: String,
palette: DynamicColorFunction<TonalPalette>,
is_background: bool,
tone: Option<DynamicColorFunction<f64>>,
chroma_multiplier: Option<DynamicColorFunction<f64>>,
tone_delta_pair: Option<DynamicColorFunction<Option<ToneDeltaPair>>>,
opacity: Option<DynamicColorFunction<Option<f64>>>,
contrast: Option<ContrastConstraints>,
) -> Self {
let tone = tone.unwrap_or_else(|| {
let bg_func = contrast.as_ref().map(|c| Arc::clone(&c.background));
Arc::new(move |scheme| {
if let Some(ref bg) = bg_func
&& let Some(bg_color) = bg(scheme)
{
return bg_color.get_tone(scheme);
}
50.0
})
});
Self {
name,
palette,
is_background,
tone,
chroma_multiplier,
tone_delta_pair,
opacity,
contrast,
}
}
#[must_use]
pub fn get_argb(&self, scheme: &DynamicScheme) -> Argb {
scheme.get_argb(self)
}
#[must_use]
pub fn get_hct(&self, scheme: &DynamicScheme) -> Hct {
scheme.get_hct(self)
}
#[must_use]
pub fn get_tone(&self, scheme: &DynamicScheme) -> f64 {
scheme.get_tone(self)
}
#[must_use]
pub fn from_argb(name: &str, argb: Argb) -> Self {
let hct = Hct::from_argb(argb);
let palette = TonalPalette::from_argb(argb);
Self::new(
name.to_string(),
Arc::new(move |_| palette.clone()),
false,
Some(Arc::new(move |_| hct.tone())),
None, None,
None,
None,
)
}
#[must_use]
pub fn extend_spec_version(
&self,
spec_version: SpecVersion,
extended_color: &Self,
) -> Arc<Self> {
Self::validate_extended_color(self, spec_version, extended_color);
let this_palette = self.palette.clone();
let ext_palette = extended_color.palette.clone();
let palette = Arc::new(move |scheme: &DynamicScheme| {
if scheme.spec_version >= spec_version {
(ext_palette)(scheme)
} else {
(this_palette)(scheme)
}
});
let this_tone = self.tone.clone();
let ext_tone = extended_color.tone.clone();
let tone = Arc::new(move |scheme: &DynamicScheme| {
if scheme.spec_version >= spec_version {
(ext_tone)(scheme)
} else {
(this_tone)(scheme)
}
});
let background = {
let this_bg = self.contrast.as_ref().map(|c| c.background.clone());
let ext_bg = extended_color
.contrast
.as_ref()
.map(|c| c.background.clone());
Arc::new(move |scheme: &DynamicScheme| {
if scheme.spec_version >= spec_version {
ext_bg.as_ref().and_then(|f| f(scheme))
} else {
this_bg.as_ref().and_then(|f| f(scheme))
}
})
};
let contrast_curve = {
let this_curve = self.contrast.as_ref().map(|c| c.contrast_curve.clone());
let ext_curve = extended_color
.contrast
.as_ref()
.map(|c| c.contrast_curve.clone());
Arc::new(move |scheme: &DynamicScheme| {
if scheme.spec_version >= spec_version {
ext_curve.as_ref().and_then(|f| f(scheme))
} else {
this_curve.as_ref().and_then(|f| f(scheme))
}
})
};
let second_background = {
let this_bg2 = self
.contrast
.as_ref()
.and_then(|c| c.second_background.clone());
let ext_bg2 = extended_color
.contrast
.as_ref()
.and_then(|c| c.second_background.clone());
Arc::new(move |scheme: &DynamicScheme| {
if scheme.spec_version >= spec_version {
ext_bg2.as_ref().and_then(|f| f(scheme))
} else {
this_bg2.as_ref().and_then(|f| f(scheme))
}
})
};
let contrast = Some(ContrastConstraints {
background,
contrast_curve,
second_background: Some(second_background),
});
let this_chroma = self.chroma_multiplier.clone();
let ext_chroma = extended_color.chroma_multiplier.clone();
let chroma_multiplier = Arc::new(move |scheme: &DynamicScheme| {
if scheme.spec_version >= spec_version {
ext_chroma.as_ref().map_or(1.0, |f| f(scheme))
} else {
this_chroma.as_ref().map_or(1.0, |f| f(scheme))
}
});
let this_delta = self.tone_delta_pair.clone();
let ext_delta = extended_color.tone_delta_pair.clone();
let tone_delta_pair = Arc::new(move |scheme: &DynamicScheme| {
if scheme.spec_version >= spec_version {
ext_delta.as_ref().and_then(|f| f(scheme))
} else {
this_delta.as_ref().and_then(|f| f(scheme))
}
});
let this_opacity = self.opacity.clone();
let ext_opacity = extended_color.opacity.clone();
let opacity = Arc::new(move |scheme: &DynamicScheme| {
if scheme.spec_version >= spec_version {
ext_opacity.as_ref().and_then(|f| f(scheme))
} else {
this_opacity.as_ref().and_then(|f| f(scheme))
}
});
Arc::new(Self::new(
self.name.clone(),
palette,
self.is_background,
Some(tone),
Some(chroma_multiplier),
Some(tone_delta_pair),
Some(opacity),
contrast,
))
}
fn validate_extended_color(&self, spec_version: SpecVersion, extended_color: &Self) {
assert!(
self.name == extended_color.name,
"Attempting to extend color {} with color {} of different name for spec version {:?}.",
self.name,
extended_color.name,
spec_version
);
assert!(
self.is_background == extended_color.is_background,
"Attempting to extend color {} as a {} with color {} as a {} for spec version {:?}.",
self.name,
if self.is_background {
"background"
} else {
"foreground"
},
extended_color.name,
if extended_color.is_background {
"background"
} else {
"foreground"
},
spec_version
);
}
#[must_use]
pub fn foreground_tone(bg_tone: f64, ratio: f64) -> f64 {
let lighter_tone = Contrast::lighter_unsafe(bg_tone, ratio);
let darker_tone = Contrast::darker_unsafe(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 prefer_lighter = Self::tone_prefers_light_foreground(bg_tone);
if prefer_lighter {
let negligible_difference = (lighter_ratio - darker_ratio).abs() < 0.1
&& lighter_ratio < ratio
&& darker_ratio < ratio;
if lighter_ratio >= ratio || lighter_ratio >= darker_ratio || negligible_difference {
lighter_tone
} else {
darker_tone
}
} else if darker_ratio >= ratio || darker_ratio >= lighter_ratio {
darker_tone
} else {
lighter_tone
}
}
#[must_use]
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
}
}
#[must_use]
pub fn tone_prefers_light_foreground(tone: f64) -> bool {
tone.round() < 60.0
}
#[must_use]
pub fn tone_allows_light_foreground(tone: f64) -> bool {
tone.round() <= 49.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_argb() {
let color = DynamicColor::from_argb("test", Argb(0xff00ff00));
assert_eq!(color.name, "test");
let _hct = Hct::from_argb(Argb(0xff00ff00));
}
#[test]
fn test_foreground_tone() {
let fg = DynamicColor::foreground_tone(90.0, 4.5);
assert!(fg < 45.0 && fg > 40.0);
let fg = DynamicColor::foreground_tone(10.0, 4.5);
assert!(fg > 50.0 && fg < 60.0);
}
#[test]
fn test_tone_preferences() {
assert!(DynamicColor::tone_prefers_light_foreground(59.0));
assert!(!DynamicColor::tone_prefers_light_foreground(61.0));
assert!(DynamicColor::tone_allows_light_foreground(49.0));
assert!(!DynamicColor::tone_allows_light_foreground(50.0));
}
}