Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Output of parsing a color function, e.g. rgb(..), hsl(..), color(..)
use std::fmt::Write;
use super::{
component::ColorComponent,
convert::normalize_hue,
parsing::{NumberOrAngleComponent, NumberOrPercentageComponent},
AbsoluteColor, ColorFlags, ColorSpace,
};
use crate::values::{
computed::color::Color as ComputedColor, generics::Optional, normalize,
specified::color::Color as SpecifiedColor,
};
use cssparser::color::{clamp_floor_256_f32, OPAQUE};
/// Represents a specified color function.
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
#[repr(u8)]
pub enum ColorFunction<OriginColor> {
Rgb(
Optional<OriginColor>, // origin
ColorComponent<NumberOrPercentageComponent>, // red
ColorComponent<NumberOrPercentageComponent>, // green
ColorComponent<NumberOrPercentageComponent>, // blue
ColorComponent<NumberOrPercentageComponent>, // alpha
),
Hsl(
Optional<OriginColor>, // origin
ColorComponent<NumberOrAngleComponent>, // hue
ColorComponent<NumberOrPercentageComponent>, // saturation
ColorComponent<NumberOrPercentageComponent>, // lightness
ColorComponent<NumberOrPercentageComponent>, // alpha
),
Hwb(
Optional<OriginColor>, // origin
ColorComponent<NumberOrAngleComponent>, // hue
ColorComponent<NumberOrPercentageComponent>, // whiteness
ColorComponent<NumberOrPercentageComponent>, // blackness
ColorComponent<NumberOrPercentageComponent>, // alpha
),
Lab(
Optional<OriginColor>, // origin
ColorComponent<NumberOrPercentageComponent>, // lightness
ColorComponent<NumberOrPercentageComponent>, // a
ColorComponent<NumberOrPercentageComponent>, // b
ColorComponent<NumberOrPercentageComponent>, // alpha
),
Lch(
Optional<OriginColor>, // origin
ColorComponent<NumberOrPercentageComponent>, // lightness
ColorComponent<NumberOrPercentageComponent>, // chroma
ColorComponent<NumberOrAngleComponent>, // hue
ColorComponent<NumberOrPercentageComponent>, // alpha
),
Oklab(
Optional<OriginColor>, // origin
ColorComponent<NumberOrPercentageComponent>, // lightness
ColorComponent<NumberOrPercentageComponent>, // a
ColorComponent<NumberOrPercentageComponent>, // b
ColorComponent<NumberOrPercentageComponent>, // alpha
),
Oklch(
Optional<OriginColor>, // origin
ColorComponent<NumberOrPercentageComponent>, // lightness
ColorComponent<NumberOrPercentageComponent>, // chroma
ColorComponent<NumberOrAngleComponent>, // hue
ColorComponent<NumberOrPercentageComponent>, // alpha
),
Color(
Optional<OriginColor>, // origin
ColorComponent<NumberOrPercentageComponent>, // red / x
ColorComponent<NumberOrPercentageComponent>, // green / y
ColorComponent<NumberOrPercentageComponent>, // blue / z
ColorComponent<NumberOrPercentageComponent>, // alpha
ColorSpace,
),
}
impl ColorFunction<AbsoluteColor> {
/// Try to resolve into a valid absolute color.
pub fn resolve_to_absolute(&self) -> Result<AbsoluteColor, ()> {
macro_rules! alpha {
($alpha:expr, $origin_color:expr) => {{
$alpha
.resolve($origin_color)?
.map(|value| normalize(value.to_number(1.0)).clamp(0.0, OPAQUE))
}};
}
Ok(match self {
ColorFunction::Rgb(origin_color, r, g, b, alpha) => {
#[inline]
fn resolve(
component: &ColorComponent<NumberOrPercentageComponent>,
origin_color: Option<&AbsoluteColor>,
) -> Result<u8, ()> {
Ok(clamp_floor_256_f32(
component
.resolve(origin_color)?
.map(|value| value.to_number(u8::MAX as f32))
.unwrap_or(0.0),
))
}
let origin_color = origin_color
.as_ref()
.map(|o| o.to_color_space(ColorSpace::Srgb).into_srgb_legacy());
let r = resolve(r, origin_color.as_ref())?;
let g = resolve(g, origin_color.as_ref())?;
let b = resolve(b, origin_color.as_ref())?;
let alpha = alpha!(alpha, origin_color.as_ref()).unwrap_or(0.0);
if origin_color.is_some() {
AbsoluteColor::new(ColorSpace::Srgb, r, g, b, alpha)
} else {
AbsoluteColor::srgb_legacy(r, g, b, alpha)
}
},
ColorFunction::Hsl(origin_color, h, s, l, alpha) => {
// Percent reference range for S and L: 0% = 0.0, 100% = 100.0
const LIGHTNESS_RANGE: f32 = 100.0;
const SATURATION_RANGE: f32 = 100.0;
// If the origin color:
// - was *NOT* specified, then we stick with the old way of serializing the
// value to rgb(..).
// - was specified, we don't use the rgb(..) syntax, because we should allow the
// color to be out of gamut and not clamp.
let use_rgb_sytax = origin_color.is_none();
let origin_color = origin_color
.as_ref()
.map(|o| o.to_color_space(ColorSpace::Hsl));
let mut result = AbsoluteColor::new(
ColorSpace::Hsl,
h.resolve(origin_color.as_ref())?
.map(|angle| normalize_hue(angle.degrees())),
s.resolve(origin_color.as_ref())?.map(|s| {
if use_rgb_sytax {
s.to_number(SATURATION_RANGE).clamp(0.0, SATURATION_RANGE)
} else {
s.to_number(SATURATION_RANGE)
}
}),
l.resolve(origin_color.as_ref())?.map(|l| {
if use_rgb_sytax {
l.to_number(LIGHTNESS_RANGE).clamp(0.0, LIGHTNESS_RANGE)
} else {
l.to_number(LIGHTNESS_RANGE)
}
}),
alpha!(alpha, origin_color.as_ref()),
);
if use_rgb_sytax {
result.flags.insert(ColorFlags::IS_LEGACY_SRGB);
}
result
},
ColorFunction::Hwb(origin_color, h, w, b, alpha) => {
// If the origin color:
// - was *NOT* specified, then we stick with the old way of serializing the
// value to rgb(..).
// - was specified, we don't use the rgb(..) syntax, because we should allow the
// color to be out of gamut and not clamp.
let use_rgb_sytax = origin_color.is_none();
// Percent reference range for W and B: 0% = 0.0, 100% = 100.0
const WHITENESS_RANGE: f32 = 100.0;
const BLACKNESS_RANGE: f32 = 100.0;
let origin_color = origin_color
.as_ref()
.map(|o| o.to_color_space(ColorSpace::Hwb));
let mut result = AbsoluteColor::new(
ColorSpace::Hwb,
h.resolve(origin_color.as_ref())?
.map(|angle| normalize_hue(angle.degrees())),
w.resolve(origin_color.as_ref())?.map(|w| {
if use_rgb_sytax {
w.to_number(WHITENESS_RANGE).clamp(0.0, WHITENESS_RANGE)
} else {
w.to_number(WHITENESS_RANGE)
}
}),
b.resolve(origin_color.as_ref())?.map(|b| {
if use_rgb_sytax {
b.to_number(BLACKNESS_RANGE).clamp(0.0, BLACKNESS_RANGE)
} else {
b.to_number(BLACKNESS_RANGE)
}
}),
alpha!(alpha, origin_color.as_ref()),
);
if use_rgb_sytax {
result.flags.insert(ColorFlags::IS_LEGACY_SRGB);
}
result
},
ColorFunction::Lab(origin_color, l, a, b, alpha) => {
// for L: 0% = 0.0, 100% = 100.0
// for a and b: -100% = -125, 100% = 125
const LIGHTNESS_RANGE: f32 = 100.0;
const A_B_RANGE: f32 = 125.0;
let origin_color = origin_color
.as_ref()
.map(|o| o.to_color_space(ColorSpace::Lab));
AbsoluteColor::new(
ColorSpace::Lab,
l.resolve(origin_color.as_ref())?
.map(|l| l.to_number(LIGHTNESS_RANGE)),
a.resolve(origin_color.as_ref())?
.map(|a| a.to_number(A_B_RANGE)),
b.resolve(origin_color.as_ref())?
.map(|b| b.to_number(A_B_RANGE)),
alpha!(alpha, origin_color.as_ref()),
)
},
ColorFunction::Lch(origin_color, l, c, h, alpha) => {
// for L: 0% = 0.0, 100% = 100.0
// for C: 0% = 0, 100% = 150
const LIGHTNESS_RANGE: f32 = 100.0;
const CHROMA_RANGE: f32 = 150.0;
let origin_color = origin_color
.as_ref()
.map(|o| o.to_color_space(ColorSpace::Lch));
AbsoluteColor::new(
ColorSpace::Lch,
l.resolve(origin_color.as_ref())?
.map(|l| l.to_number(LIGHTNESS_RANGE)),
c.resolve(origin_color.as_ref())?
.map(|c| c.to_number(CHROMA_RANGE)),
h.resolve(origin_color.as_ref())?
.map(|angle| normalize_hue(angle.degrees())),
alpha!(alpha, origin_color.as_ref()),
)
},
ColorFunction::Oklab(origin_color, l, a, b, alpha) => {
// for L: 0% = 0.0, 100% = 1.0
// for a and b: -100% = -0.4, 100% = 0.4
const LIGHTNESS_RANGE: f32 = 1.0;
const A_B_RANGE: f32 = 0.4;
let origin_color = origin_color
.as_ref()
.map(|o| o.to_color_space(ColorSpace::Oklab));
AbsoluteColor::new(
ColorSpace::Oklab,
l.resolve(origin_color.as_ref())?
.map(|l| l.to_number(LIGHTNESS_RANGE)),
a.resolve(origin_color.as_ref())?
.map(|a| a.to_number(A_B_RANGE)),
b.resolve(origin_color.as_ref())?
.map(|b| b.to_number(A_B_RANGE)),
alpha!(alpha, origin_color.as_ref()),
)
},
ColorFunction::Oklch(origin_color, l, c, h, alpha) => {
// for L: 0% = 0.0, 100% = 1.0
// for C: 0% = 0.0 100% = 0.4
const LIGHTNESS_RANGE: f32 = 1.0;
const CHROMA_RANGE: f32 = 0.4;
let origin_color = origin_color
.as_ref()
.map(|o| o.to_color_space(ColorSpace::Oklch));
AbsoluteColor::new(
ColorSpace::Oklch,
l.resolve(origin_color.as_ref())?
.map(|l| l.to_number(LIGHTNESS_RANGE)),
c.resolve(origin_color.as_ref())?
.map(|c| c.to_number(CHROMA_RANGE)),
h.resolve(origin_color.as_ref())?
.map(|angle| normalize_hue(angle.degrees())),
alpha!(alpha, origin_color.as_ref()),
)
},
ColorFunction::Color(origin_color, r, g, b, alpha, color_space) => {
let origin_color = origin_color.as_ref().map(|o| {
let mut result = o.to_color_space(*color_space);
// If the origin color was a `rgb(..)` function, we should
// make sure it doesn't have the legacy flag any more so
// that it is recognized as a `color(srgb ..)` function.
result.flags.set(ColorFlags::IS_LEGACY_SRGB, false);
result
});
AbsoluteColor::new(
*color_space,
r.resolve(origin_color.as_ref())?.map(|c| c.to_number(1.0)),
g.resolve(origin_color.as_ref())?.map(|c| c.to_number(1.0)),
b.resolve(origin_color.as_ref())?.map(|c| c.to_number(1.0)),
alpha!(alpha, origin_color.as_ref()),
)
},
})
}
}
impl ColorFunction<SpecifiedColor> {
/// Return true if the color funciton has an origin color specified.
pub fn has_origin_color(&self) -> bool {
match self {
Self::Rgb(origin_color, ..) |
Self::Hsl(origin_color, ..) |
Self::Hwb(origin_color, ..) |
Self::Lab(origin_color, ..) |
Self::Lch(origin_color, ..) |
Self::Oklab(origin_color, ..) |
Self::Oklch(origin_color, ..) |
Self::Color(origin_color, ..) => origin_color.is_some(),
}
}
/// Try to resolve the color function to an [`AbsoluteColor`] that does not
/// contain any variables (currentcolor, color components, etc.).
pub fn resolve_to_absolute(&self) -> Result<AbsoluteColor, ()> {
// Map the color function to one with an absolute origin color.
let resolvable = self.map_origin_color(|o| o.resolve_to_absolute());
resolvable.resolve_to_absolute()
}
}
impl<Color> ColorFunction<Color> {
/// Map the origin color to another type. Return None from `f` if the conversion fails.
pub fn map_origin_color<U>(&self, f: impl FnOnce(&Color) -> Option<U>) -> ColorFunction<U> {
macro_rules! map {
($f:ident, $o:expr, $c0:expr, $c1:expr, $c2:expr, $alpha:expr) => {{
ColorFunction::$f(
$o.as_ref().and_then(f).into(),
$c0.clone(),
$c1.clone(),
$c2.clone(),
$alpha.clone(),
)
}};
}
match self {
ColorFunction::Rgb(o, c0, c1, c2, alpha) => map!(Rgb, o, c0, c1, c2, alpha),
ColorFunction::Hsl(o, c0, c1, c2, alpha) => map!(Hsl, o, c0, c1, c2, alpha),
ColorFunction::Hwb(o, c0, c1, c2, alpha) => map!(Hwb, o, c0, c1, c2, alpha),
ColorFunction::Lab(o, c0, c1, c2, alpha) => map!(Lab, o, c0, c1, c2, alpha),
ColorFunction::Lch(o, c0, c1, c2, alpha) => map!(Lch, o, c0, c1, c2, alpha),
ColorFunction::Oklab(o, c0, c1, c2, alpha) => map!(Oklab, o, c0, c1, c2, alpha),
ColorFunction::Oklch(o, c0, c1, c2, alpha) => map!(Oklch, o, c0, c1, c2, alpha),
ColorFunction::Color(o, c0, c1, c2, alpha, color_space) => ColorFunction::Color(
o.as_ref().and_then(f).into(),
c0.clone(),
c1.clone(),
c2.clone(),
alpha.clone(),
color_space.clone(),
),
}
}
}
impl ColorFunction<ComputedColor> {
/// Resolve a computed color function to an absolute computed color.
pub fn resolve_to_absolute(&self, current_color: &AbsoluteColor) -> AbsoluteColor {
// Map the color function to one with an absolute origin color.
let resolvable = self.map_origin_color(|o| Some(o.resolve_to_absolute(current_color)));
match resolvable.resolve_to_absolute() {
Ok(color) => color,
Err(..) => {
debug_assert!(
false,
"the color could not be resolved even with a currentcolor specified?"
);
AbsoluteColor::TRANSPARENT_BLACK
},
}
}
}
impl<C: style_traits::ToCss> style_traits::ToCss for ColorFunction<C> {
fn to_css<W>(&self, dest: &mut style_traits::CssWriter<W>) -> std::fmt::Result
where
W: std::fmt::Write,
{
let (origin_color, alpha) = match self {
Self::Rgb(origin_color, _, _, _, alpha) => {
dest.write_str("rgb(")?;
(origin_color, alpha)
},
Self::Hsl(origin_color, _, _, _, alpha) => {
dest.write_str("hsl(")?;
(origin_color, alpha)
},
Self::Hwb(origin_color, _, _, _, alpha) => {
dest.write_str("hwb(")?;
(origin_color, alpha)
},
Self::Lab(origin_color, _, _, _, alpha) => {
dest.write_str("lab(")?;
(origin_color, alpha)
},
Self::Lch(origin_color, _, _, _, alpha) => {
dest.write_str("lch(")?;
(origin_color, alpha)
},
Self::Oklab(origin_color, _, _, _, alpha) => {
dest.write_str("oklab(")?;
(origin_color, alpha)
},
Self::Oklch(origin_color, _, _, _, alpha) => {
dest.write_str("oklch(")?;
(origin_color, alpha)
},
Self::Color(origin_color, _, _, _, alpha, _) => {
dest.write_str("color(")?;
(origin_color, alpha)
},
};
if let Optional::Some(origin_color) = origin_color {
dest.write_str("from ")?;
origin_color.to_css(dest)?;
dest.write_str(" ")?;
}
let is_opaque = if let ColorComponent::Value(value) = *alpha {
value.to_number(OPAQUE) == OPAQUE
} else {
false
};
macro_rules! serialize_alpha {
($alpha_component:expr) => {{
if !is_opaque && !matches!($alpha_component, ColorComponent::AlphaOmitted) {
dest.write_str(" / ")?;
$alpha_component.to_css(dest)?;
}
}};
}
macro_rules! serialize_components {
($c0:expr, $c1:expr, $c2:expr) => {{
debug_assert!(!matches!($c0, ColorComponent::AlphaOmitted));
debug_assert!(!matches!($c1, ColorComponent::AlphaOmitted));
debug_assert!(!matches!($c2, ColorComponent::AlphaOmitted));
$c0.to_css(dest)?;
dest.write_str(" ")?;
$c1.to_css(dest)?;
dest.write_str(" ")?;
$c2.to_css(dest)?;
}};
}
match self {
Self::Rgb(_, c0, c1, c2, alpha) => {
serialize_components!(c0, c1, c2);
serialize_alpha!(alpha);
},
Self::Hsl(_, c0, c1, c2, alpha) => {
serialize_components!(c0, c1, c2);
serialize_alpha!(alpha);
},
Self::Hwb(_, c0, c1, c2, alpha) => {
serialize_components!(c0, c1, c2);
serialize_alpha!(alpha);
},
Self::Lab(_, c0, c1, c2, alpha) => {
serialize_components!(c0, c1, c2);
serialize_alpha!(alpha);
},
Self::Lch(_, c0, c1, c2, alpha) => {
serialize_components!(c0, c1, c2);
serialize_alpha!(alpha);
},
Self::Oklab(_, c0, c1, c2, alpha) => {
serialize_components!(c0, c1, c2);
serialize_alpha!(alpha);
},
Self::Oklch(_, c0, c1, c2, alpha) => {
serialize_components!(c0, c1, c2);
serialize_alpha!(alpha);
},
Self::Color(_, c0, c1, c2, alpha, color_space) => {
color_space.to_css(dest)?;
dest.write_str(" ")?;
serialize_components!(c0, c1, c2);
serialize_alpha!(alpha);
},
}
dest.write_str(")")
}
}