blob: 5b116c23bb51d924af5e69ccd6674caa83f1774a [file] [edit]
/* 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 http://mozilla.org/MPL/2.0/. */
#![deny(missing_docs)]
//! Fairly complete css-color implementation.
//! Relative colors, color-mix, system colors, and other such things require better calc() support
//! and integration.
#[cfg(test)]
mod tests;
use cssparser::color::{
clamp_floor_256_f32, clamp_unit_f32, parse_hash_color, serialize_color_alpha,
PredefinedColorSpace, OPAQUE,
};
use cssparser::{match_ignore_ascii_case, CowRcStr, ParseError, Parser, ToCss, Token};
use std::f32::consts::PI;
use std::fmt;
/// Return the named color with the given name.
///
/// Matching is case-insensitive in the ASCII range.
/// CSS escaping (if relevant) should be resolved before calling this function.
/// (For example, the value of an `Ident` token is fine.)
#[allow(clippy::result_unit_err)]
#[inline]
pub fn parse_color_keyword<Output>(ident: &str) -> Result<Output, ()>
where
Output: FromParsedColor,
{
Ok(match_ignore_ascii_case! { ident ,
"transparent" => Output::from_rgba(0, 0, 0, 0.0),
"currentcolor" => Output::from_current_color(),
_ => {
let (r, g, b) = cssparser::color::parse_named_color(ident)?;
Output::from_rgba(r, g, b, OPAQUE)
}
})
}
/// Parse a CSS color using the specified [`ColorParser`] and return a new color
/// value on success.
pub fn parse_color_with<'i, 't, P>(
color_parser: &P,
input: &mut Parser<'i, 't>,
) -> Result<P::Output, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
let location = input.current_source_location();
let token = input.next()?;
match *token {
Token::Hash(ref value) | Token::IDHash(ref value) => {
parse_hash_color(value.as_bytes()).map(|(r, g, b, a)| P::Output::from_rgba(r, g, b, a))
}
Token::Ident(ref value) => parse_color_keyword(value),
Token::Function(ref name) => {
let name = name.clone();
return input.parse_nested_block(|arguments| {
parse_color_function(color_parser, name, arguments)
});
}
_ => Err(()),
}
.map_err(|()| location.new_unexpected_token_error(token.clone()))
}
/// Parse one of the color functions: rgba(), lab(), color(), etc.
#[inline]
fn parse_color_function<'i, 't, P>(
color_parser: &P,
name: CowRcStr<'i>,
arguments: &mut Parser<'i, 't>,
) -> Result<P::Output, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
let color = match_ignore_ascii_case! { &name,
"rgb" | "rgba" => parse_rgb(color_parser, arguments),
"hsl" | "hsla" => parse_hsl(color_parser, arguments),
"hwb" => parse_hwb(color_parser, arguments),
// for L: 0% = 0.0, 100% = 100.0
// for a and b: -100% = -125, 100% = 125
"lab" => parse_lab_like(color_parser, arguments, 100.0, 125.0, P::Output::from_lab),
// for L: 0% = 0.0, 100% = 100.0
// for C: 0% = 0, 100% = 150
"lch" => parse_lch_like(color_parser, arguments, 100.0, 150.0, P::Output::from_lch),
// for L: 0% = 0.0, 100% = 1.0
// for a and b: -100% = -0.4, 100% = 0.4
"oklab" => parse_lab_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklab),
// for L: 0% = 0.0, 100% = 1.0
// for C: 0% = 0.0 100% = 0.4
"oklch" => parse_lch_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklch),
"color" => parse_color_with_color_space(color_parser, arguments),
_ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))),
}?;
arguments.expect_exhausted()?;
Ok(color)
}
/// Parse the alpha component by itself from either number or percentage,
/// clipping the result to [0.0..1.0].
#[inline]
fn parse_alpha_component<'i, 't, P>(
color_parser: &P,
arguments: &mut Parser<'i, 't>,
) -> Result<f32, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
Ok(color_parser
.parse_number_or_percentage(arguments)?
.unit_value()
.clamp(0.0, OPAQUE))
}
fn parse_legacy_alpha<'i, 't, P>(
color_parser: &P,
arguments: &mut Parser<'i, 't>,
) -> Result<f32, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
Ok(if !arguments.is_exhausted() {
arguments.expect_comma()?;
parse_alpha_component(color_parser, arguments)?
} else {
OPAQUE
})
}
fn parse_modern_alpha<'i, 't, P>(
color_parser: &P,
arguments: &mut Parser<'i, 't>,
) -> Result<Option<f32>, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
if !arguments.is_exhausted() {
arguments.expect_delim('/')?;
parse_none_or(arguments, |p| parse_alpha_component(color_parser, p))
} else {
Ok(Some(OPAQUE))
}
}
#[inline]
fn parse_rgb<'i, 't, P>(
color_parser: &P,
arguments: &mut Parser<'i, 't>,
) -> Result<P::Output, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
let maybe_red = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?;
// If the first component is not "none" and is followed by a comma, then we
// are parsing the legacy syntax.
let is_legacy_syntax = maybe_red.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok();
let (red, green, blue, alpha) = if is_legacy_syntax {
let (red, green, blue) = match maybe_red.unwrap() {
NumberOrPercentage::Number { value } => {
let red = clamp_floor_256_f32(value);
let green = clamp_floor_256_f32(color_parser.parse_number(arguments)?);
arguments.expect_comma()?;
let blue = clamp_floor_256_f32(color_parser.parse_number(arguments)?);
(red, green, blue)
}
NumberOrPercentage::Percentage { unit_value } => {
let red = clamp_unit_f32(unit_value);
let green = clamp_unit_f32(color_parser.parse_percentage(arguments)?);
arguments.expect_comma()?;
let blue = clamp_unit_f32(color_parser.parse_percentage(arguments)?);
(red, green, blue)
}
};
let alpha = parse_legacy_alpha(color_parser, arguments)?;
(red, green, blue, alpha)
} else {
#[inline]
fn get_component_value(c: Option<NumberOrPercentage>) -> u8 {
c.map(|c| match c {
NumberOrPercentage::Number { value } => clamp_floor_256_f32(value),
NumberOrPercentage::Percentage { unit_value } => clamp_unit_f32(unit_value),
})
.unwrap_or(0)
}
let red = get_component_value(maybe_red);
let green = get_component_value(parse_none_or(arguments, |p| {
color_parser.parse_number_or_percentage(p)
})?);
let blue = get_component_value(parse_none_or(arguments, |p| {
color_parser.parse_number_or_percentage(p)
})?);
let alpha = parse_modern_alpha(color_parser, arguments)?.unwrap_or(0.0);
(red, green, blue, alpha)
};
Ok(P::Output::from_rgba(red, green, blue, alpha))
}
/// Parses hsl syntax.
///
/// <https://drafts.csswg.org/css-color/#the-hsl-notation>
#[inline]
fn parse_hsl<'i, 't, P>(
color_parser: &P,
arguments: &mut Parser<'i, 't>,
) -> Result<P::Output, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
let maybe_hue = parse_none_or(arguments, |p| color_parser.parse_angle_or_number(p))?;
// If the hue is not "none" and is followed by a comma, then we are parsing
// the legacy syntax.
let is_legacy_syntax = maybe_hue.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok();
let saturation: Option<f32>;
let lightness: Option<f32>;
let alpha = if is_legacy_syntax {
saturation = Some(color_parser.parse_percentage(arguments)?);
arguments.expect_comma()?;
lightness = Some(color_parser.parse_percentage(arguments)?);
Some(parse_legacy_alpha(color_parser, arguments)?)
} else {
saturation = parse_none_or(arguments, |p| color_parser.parse_percentage(p))?;
lightness = parse_none_or(arguments, |p| color_parser.parse_percentage(p))?;
parse_modern_alpha(color_parser, arguments)?
};
let hue = maybe_hue.map(|h| normalize_hue(h.degrees()));
let saturation = saturation.map(|s| s.clamp(0.0, 1.0));
let lightness = lightness.map(|l| l.clamp(0.0, 1.0));
Ok(P::Output::from_hsl(hue, saturation, lightness, alpha))
}
/// Parses hwb syntax.
///
/// <https://drafts.csswg.org/css-color/#the-hbw-notation>
#[inline]
fn parse_hwb<'i, 't, P>(
color_parser: &P,
arguments: &mut Parser<'i, 't>,
) -> Result<P::Output, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
let (hue, whiteness, blackness, alpha) = parse_components(
color_parser,
arguments,
P::parse_angle_or_number,
P::parse_percentage,
P::parse_percentage,
)?;
let hue = hue.map(|h| normalize_hue(h.degrees()));
let whiteness = whiteness.map(|w| w.clamp(0.0, 1.0));
let blackness = blackness.map(|b| b.clamp(0.0, 1.0));
Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha))
}
/// <https://drafts.csswg.org/css-color-4/#hwb-to-rgb>
#[inline]
pub fn hwb_to_rgb(h: f32, w: f32, b: f32) -> (f32, f32, f32) {
if w + b >= 1.0 {
let gray = w / (w + b);
return (gray, gray, gray);
}
// hue is expected in the range [0..1].
let (mut red, mut green, mut blue) = hsl_to_rgb(h, 1.0, 0.5);
let x = 1.0 - w - b;
red = red * x + w;
green = green * x + w;
blue = blue * x + w;
(red, green, blue)
}
/// <https://drafts.csswg.org/css-color/#hsl-color>
/// except with h pre-multiplied by 3, to avoid some rounding errors.
#[inline]
pub fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> (f32, f32, f32) {
debug_assert!((0.0..=1.0).contains(&hue));
fn hue_to_rgb(m1: f32, m2: f32, mut h3: f32) -> f32 {
if h3 < 0. {
h3 += 3.
}
if h3 > 3. {
h3 -= 3.
}
if h3 * 2. < 1. {
m1 + (m2 - m1) * h3 * 2.
} else if h3 * 2. < 3. {
m2
} else if h3 < 2. {
m1 + (m2 - m1) * (2. - h3) * 2.
} else {
m1
}
}
let m2 = if lightness <= 0.5 {
lightness * (saturation + 1.)
} else {
lightness + saturation - lightness * saturation
};
let m1 = lightness * 2. - m2;
let hue_times_3 = hue * 3.;
let red = hue_to_rgb(m1, m2, hue_times_3 + 1.);
let green = hue_to_rgb(m1, m2, hue_times_3);
let blue = hue_to_rgb(m1, m2, hue_times_3 - 1.);
(red, green, blue)
}
type IntoColorFn<Output> =
fn(l: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) -> Output;
#[inline]
fn parse_lab_like<'i, 't, P>(
color_parser: &P,
arguments: &mut Parser<'i, 't>,
lightness_range: f32,
a_b_range: f32,
into_color: IntoColorFn<P::Output>,
) -> Result<P::Output, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
let (lightness, a, b, alpha) = parse_components(
color_parser,
arguments,
P::parse_number_or_percentage,
P::parse_number_or_percentage,
P::parse_number_or_percentage,
)?;
let lightness = lightness.map(|l| l.value(lightness_range));
let a = a.map(|a| a.value(a_b_range));
let b = b.map(|b| b.value(a_b_range));
Ok(into_color(lightness, a, b, alpha))
}
#[inline]
fn parse_lch_like<'i, 't, P>(
color_parser: &P,
arguments: &mut Parser<'i, 't>,
lightness_range: f32,
chroma_range: f32,
into_color: IntoColorFn<P::Output>,
) -> Result<P::Output, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
let (lightness, chroma, hue, alpha) = parse_components(
color_parser,
arguments,
P::parse_number_or_percentage,
P::parse_number_or_percentage,
P::parse_angle_or_number,
)?;
let lightness = lightness.map(|l| l.value(lightness_range));
let chroma = chroma.map(|c| c.value(chroma_range));
let hue = hue.map(|h| normalize_hue(h.degrees()));
Ok(into_color(lightness, chroma, hue, alpha))
}
/// Parse the color() function.
#[inline]
fn parse_color_with_color_space<'i, 't, P>(
color_parser: &P,
arguments: &mut Parser<'i, 't>,
) -> Result<P::Output, ParseError<'i, P::Error>>
where
P: ColorParser<'i>,
{
let color_space = PredefinedColorSpace::parse(arguments)?;
let (c1, c2, c3, alpha) = parse_components(
color_parser,
arguments,
P::parse_number_or_percentage,
P::parse_number_or_percentage,
P::parse_number_or_percentage,
)?;
let c1 = c1.map(|c| c.unit_value());
let c2 = c2.map(|c| c.unit_value());
let c3 = c3.map(|c| c.unit_value());
Ok(P::Output::from_color_function(
color_space,
c1,
c2,
c3,
alpha,
))
}
type ComponentParseResult<'i, R1, R2, R3, Error> =
Result<(Option<R1>, Option<R2>, Option<R3>, Option<f32>), ParseError<'i, Error>>;
/// Parse the color components and alpha with the modern [color-4] syntax.
pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>(
color_parser: &P,
input: &mut Parser<'i, 't>,
f1: F1,
f2: F2,
f3: F3,
) -> ComponentParseResult<'i, R1, R2, R3, P::Error>
where
P: ColorParser<'i>,
F1: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R1, ParseError<'i, P::Error>>,
F2: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R2, ParseError<'i, P::Error>>,
F3: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R3, ParseError<'i, P::Error>>,
{
let r1 = parse_none_or(input, |p| f1(color_parser, p))?;
let r2 = parse_none_or(input, |p| f2(color_parser, p))?;
let r3 = parse_none_or(input, |p| f3(color_parser, p))?;
let alpha = parse_modern_alpha(color_parser, input)?;
Ok((r1, r2, r3, alpha))
}
fn parse_none_or<'i, 't, F, T, E>(input: &mut Parser<'i, 't>, thing: F) -> Result<Option<T>, E>
where
F: FnOnce(&mut Parser<'i, 't>) -> Result<T, E>,
{
match input.try_parse(|p| p.expect_ident_matching("none")) {
Ok(_) => Ok(None),
Err(_) => Ok(Some(thing(input)?)),
}
}
/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and
/// floating point values.
struct ModernComponent<'a>(&'a Option<f32>);
impl<'a> ToCss for ModernComponent<'a> {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
if let Some(value) = self.0 {
if value.is_finite() {
value.to_css(dest)
} else if value.is_nan() {
dest.write_str("calc(NaN)")
} else {
debug_assert!(value.is_infinite());
if value.is_sign_negative() {
dest.write_str("calc(-infinity)")
} else {
dest.write_str("calc(infinity)")
}
}
} else {
dest.write_str("none")
}
}
}
// Guaratees hue in [0..360)
fn normalize_hue(hue: f32) -> f32 {
// <https://drafts.csswg.org/css-values/#angles>
// Subtract an integer before rounding, to avoid some rounding errors:
hue - 360.0 * (hue / 360.0).floor()
}
/// A color with red, green, blue, and alpha components, in a byte each.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct RgbaLegacy {
/// The red component.
pub red: u8,
/// The green component.
pub green: u8,
/// The blue component.
pub blue: u8,
/// The alpha component.
pub alpha: f32,
}
impl RgbaLegacy {
/// Constructs a new RGBA value from float components. It expects the red,
/// green, blue and alpha channels in that order, and all values will be
/// clamped to the 0.0 ... 1.0 range.
#[inline]
pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
Self::new(
clamp_unit_f32(red),
clamp_unit_f32(green),
clamp_unit_f32(blue),
alpha.clamp(0.0, OPAQUE),
)
}
/// Same thing, but with `u8` values instead of floats in the 0 to 1 range.
#[inline]
pub const fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
Self {
red,
green,
blue,
alpha,
}
}
}
impl ToCss for RgbaLegacy {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
let has_alpha = self.alpha != OPAQUE;
dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?;
self.red.to_css(dest)?;
dest.write_str(", ")?;
self.green.to_css(dest)?;
dest.write_str(", ")?;
self.blue.to_css(dest)?;
// Legacy syntax does not allow none components.
serialize_color_alpha(dest, Some(self.alpha), true)?;
dest.write_char(')')
}
}
/// Color specified by hue, saturation and lightness components.
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Hsl {
/// The hue component.
pub hue: Option<f32>,
/// The saturation component.
pub saturation: Option<f32>,
/// The lightness component.
pub lightness: Option<f32>,
/// The alpha component.
pub alpha: Option<f32>,
}
impl Hsl {
/// Construct a new HSL color from it's components.
pub fn new(
hue: Option<f32>,
saturation: Option<f32>,
lightness: Option<f32>,
alpha: Option<f32>,
) -> Self {
Self {
hue,
saturation,
lightness,
alpha,
}
}
}
impl ToCss for Hsl {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
// HSL serializes to RGB, so we have to convert it.
let (red, green, blue) = hsl_to_rgb(
self.hue.unwrap_or(0.0) / 360.0,
self.saturation.unwrap_or(0.0),
self.lightness.unwrap_or(0.0),
);
RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest)
}
}
/// Color specified by hue, whiteness and blackness components.
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Hwb {
/// The hue component.
pub hue: Option<f32>,
/// The whiteness component.
pub whiteness: Option<f32>,
/// The blackness component.
pub blackness: Option<f32>,
/// The alpha component.
pub alpha: Option<f32>,
}
impl Hwb {
/// Construct a new HWB color from it's components.
pub fn new(
hue: Option<f32>,
whiteness: Option<f32>,
blackness: Option<f32>,
alpha: Option<f32>,
) -> Self {
Self {
hue,
whiteness,
blackness,
alpha,
}
}
}
impl ToCss for Hwb {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
// HWB serializes to RGB, so we have to convert it.
let (red, green, blue) = hwb_to_rgb(
self.hue.unwrap_or(0.0) / 360.0,
self.whiteness.unwrap_or(0.0),
self.blackness.unwrap_or(0.0),
);
RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest)
}
}
// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro,
// because it causes cbindgen to ignore them.
/// Color specified by lightness, a- and b-axis components.
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Lab {
/// The lightness component.
pub lightness: Option<f32>,
/// The a-axis component.
pub a: Option<f32>,
/// The b-axis component.
pub b: Option<f32>,
/// The alpha component.
pub alpha: Option<f32>,
}
/// Color specified by lightness, a- and b-axis components.
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Oklab {
/// The lightness component.
pub lightness: Option<f32>,
/// The a-axis component.
pub a: Option<f32>,
/// The b-axis component.
pub b: Option<f32>,
/// The alpha component.
pub alpha: Option<f32>,
}
macro_rules! impl_lab_like {
($cls:ident, $fname:literal) => {
impl $cls {
/// Construct a new Lab color format with lightness, a, b and alpha components.
pub fn new(
lightness: Option<f32>,
a: Option<f32>,
b: Option<f32>,
alpha: Option<f32>,
) -> Self {
Self {
lightness,
a,
b,
alpha,
}
}
}
impl ToCss for $cls {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
dest.write_str($fname)?;
dest.write_str("(")?;
ModernComponent(&self.lightness).to_css(dest)?;
dest.write_char(' ')?;
ModernComponent(&self.a).to_css(dest)?;
dest.write_char(' ')?;
ModernComponent(&self.b).to_css(dest)?;
serialize_color_alpha(dest, self.alpha, false)?;
dest.write_char(')')
}
}
};
}
impl_lab_like!(Lab, "lab");
impl_lab_like!(Oklab, "oklab");
// NOTE: LCH and OKLCH is not declared inside the [impl_lch_like] macro,
// because it causes cbindgen to ignore them.
/// Color specified by lightness, chroma and hue components.
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Lch {
/// The lightness component.
pub lightness: Option<f32>,
/// The chroma component.
pub chroma: Option<f32>,
/// The hue component.
pub hue: Option<f32>,
/// The alpha component.
pub alpha: Option<f32>,
}
/// Color specified by lightness, chroma and hue components.
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Oklch {
/// The lightness component.
pub lightness: Option<f32>,
/// The chroma component.
pub chroma: Option<f32>,
/// The hue component.
pub hue: Option<f32>,
/// The alpha component.
pub alpha: Option<f32>,
}
macro_rules! impl_lch_like {
($cls:ident, $fname:literal) => {
impl $cls {
/// Construct a new color with lightness, chroma and hue components.
pub fn new(
lightness: Option<f32>,
chroma: Option<f32>,
hue: Option<f32>,
alpha: Option<f32>,
) -> Self {
Self {
lightness,
chroma,
hue,
alpha,
}
}
}
impl ToCss for $cls {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
dest.write_str($fname)?;
dest.write_str("(")?;
ModernComponent(&self.lightness).to_css(dest)?;
dest.write_char(' ')?;
ModernComponent(&self.chroma).to_css(dest)?;
dest.write_char(' ')?;
ModernComponent(&self.hue).to_css(dest)?;
serialize_color_alpha(dest, self.alpha, false)?;
dest.write_char(')')
}
}
};
}
impl_lch_like!(Lch, "lch");
impl_lch_like!(Oklch, "oklch");
/// A color specified by the color() function.
/// <https://drafts.csswg.org/css-color-4/#color-function>
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ColorFunction {
/// The color space for this color.
pub color_space: PredefinedColorSpace,
/// The first component of the color. Either red or x.
pub c1: Option<f32>,
/// The second component of the color. Either green or y.
pub c2: Option<f32>,
/// The third component of the color. Either blue or z.
pub c3: Option<f32>,
/// The alpha component of the color.
pub alpha: Option<f32>,
}
impl ColorFunction {
/// Construct a new color function definition with the given color space and
/// color components.
pub fn new(
color_space: PredefinedColorSpace,
c1: Option<f32>,
c2: Option<f32>,
c3: Option<f32>,
alpha: Option<f32>,
) -> Self {
Self {
color_space,
c1,
c2,
c3,
alpha,
}
}
}
impl ToCss for ColorFunction {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
dest.write_str("color(")?;
self.color_space.to_css(dest)?;
dest.write_char(' ')?;
ModernComponent(&self.c1).to_css(dest)?;
dest.write_char(' ')?;
ModernComponent(&self.c2).to_css(dest)?;
dest.write_char(' ')?;
ModernComponent(&self.c3).to_css(dest)?;
serialize_color_alpha(dest, self.alpha, false)?;
dest.write_char(')')
}
}
/// Describes one of the value <color> values according to the CSS
/// specification.
///
/// Most components are `Option<_>`, so when the value is `None`, that component
/// serializes to the "none" keyword.
///
/// <https://drafts.csswg.org/css-color-4/#color-type>
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
pub enum Color {
/// The 'currentcolor' keyword.
CurrentColor,
/// Specify sRGB colors directly by their red/green/blue/alpha chanels.
Rgba(RgbaLegacy),
/// Specifies a color in sRGB using hue, saturation and lightness components.
Hsl(Hsl),
/// Specifies a color in sRGB using hue, whiteness and blackness components.
Hwb(Hwb),
/// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue
/// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB
/// rectangular coordinate model.
Lab(Lab),
/// Specifies a CIELAB color by CIE Lightness, Chroma, and hue using the
/// CIE LCH cylindrical coordinate model.
Lch(Lch),
/// Specifies an Oklab color by Oklab Lightness and its a- and b-axis hue
/// coordinates (red/green-ness, and yellow/blue-ness) using the Oklab
/// rectangular coordinate model.
Oklab(Oklab),
/// Specifies an Oklab color by Oklab Lightness, Chroma, and hue using
/// the OKLCH cylindrical coordinate model.
Oklch(Oklch),
/// Specifies a color in a predefined color space.
ColorFunction(ColorFunction),
}
impl ToCss for Color {
fn to_css<W>(&self, dest: &mut W) -> fmt::Result
where
W: fmt::Write,
{
match *self {
Color::CurrentColor => dest.write_str("currentcolor"),
Color::Rgba(rgba) => rgba.to_css(dest),
Color::Hsl(hsl) => hsl.to_css(dest),
Color::Hwb(hwb) => hwb.to_css(dest),
Color::Lab(lab) => lab.to_css(dest),
Color::Lch(lch) => lch.to_css(dest),
Color::Oklab(lab) => lab.to_css(dest),
Color::Oklch(lch) => lch.to_css(dest),
Color::ColorFunction(color_function) => color_function.to_css(dest),
}
}
}
/// Either a number or a percentage.
pub enum NumberOrPercentage {
/// `<number>`.
Number {
/// The numeric value parsed, as a float.
value: f32,
},
/// `<percentage>`
Percentage {
/// The value as a float, divided by 100 so that the nominal range is
/// 0.0 to 1.0.
unit_value: f32,
},
}
impl NumberOrPercentage {
/// Return the value as a percentage.
pub fn unit_value(&self) -> f32 {
match *self {
NumberOrPercentage::Number { value } => value,
NumberOrPercentage::Percentage { unit_value } => unit_value,
}
}
/// Return the value as a number with a percentage adjusted to the
/// `percentage_basis`.
pub fn value(&self, percentage_basis: f32) -> f32 {
match *self {
Self::Number { value } => value,
Self::Percentage { unit_value } => unit_value * percentage_basis,
}
}
}
/// Either an angle or a number.
pub enum AngleOrNumber {
/// `<number>`.
Number {
/// The numeric value parsed, as a float.
value: f32,
},
/// `<angle>`
Angle {
/// The value as a number of degrees.
degrees: f32,
},
}
impl AngleOrNumber {
/// Return the angle in degrees. `AngleOrNumber::Number` is returned as
/// degrees, because it is the canonical unit.
pub fn degrees(&self) -> f32 {
match *self {
AngleOrNumber::Number { value } => value,
AngleOrNumber::Angle { degrees } => degrees,
}
}
}
/// A trait that can be used to hook into how `cssparser` parses color
/// components, with the intention of implementing more complicated behavior.
///
/// For example, this is used by Servo to support calc() in color.
pub trait ColorParser<'i> {
/// The type that the parser will construct on a successful parse.
type Output: FromParsedColor;
/// A custom error type that can be returned from the parsing functions.
type Error: 'i;
/// Parse an `<angle>` or `<number>`.
///
/// Returns the result in degrees.
fn parse_angle_or_number<'t>(
&self,
input: &mut Parser<'i, 't>,
) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> {
let location = input.current_source_location();
Ok(match *input.next()? {
Token::Number { value, .. } => AngleOrNumber::Number { value },
Token::Dimension {
value: v, ref unit, ..
} => {
let degrees = match_ignore_ascii_case! { unit,
"deg" => v,
"grad" => v * 360. / 400.,
"rad" => v * 360. / (2. * PI),
"turn" => v * 360.,
_ => {
return Err(location.new_unexpected_token_error(Token::Ident(unit.clone())))
}
};
AngleOrNumber::Angle { degrees }
}
ref t => return Err(location.new_unexpected_token_error(t.clone())),
})
}
/// Parse a `<percentage>` value.
///
/// Returns the result in a number from 0.0 to 1.0.
fn parse_percentage<'t>(
&self,
input: &mut Parser<'i, 't>,
) -> Result<f32, ParseError<'i, Self::Error>> {
input.expect_percentage().map_err(From::from)
}
/// Parse a `<number>` value.
fn parse_number<'t>(
&self,
input: &mut Parser<'i, 't>,
) -> Result<f32, ParseError<'i, Self::Error>> {
input.expect_number().map_err(From::from)
}
/// Parse a `<number>` value or a `<percentage>` value.
fn parse_number_or_percentage<'t>(
&self,
input: &mut Parser<'i, 't>,
) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> {
let location = input.current_source_location();
Ok(match *input.next()? {
Token::Number { value, .. } => NumberOrPercentage::Number { value },
Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value },
ref t => return Err(location.new_unexpected_token_error(t.clone())),
})
}
}
/// Default implementation of a [`ColorParser`]
pub struct DefaultColorParser;
impl<'i> ColorParser<'i> for DefaultColorParser {
type Output = Color;
type Error = ();
}
impl Color {
/// Parse a <color> value, per CSS Color Module Level 3.
///
/// FIXME(#2) Deprecated CSS2 System Colors are not supported yet.
pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i, ()>> {
parse_color_with(&DefaultColorParser, input)
}
}
/// This trait is used by the [`ColorParser`] to construct colors of any type.
pub trait FromParsedColor {
/// Construct a new color from the CSS `currentcolor` keyword.
fn from_current_color() -> Self;
/// Construct a new color from red, green, blue and alpha components.
fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self;
/// Construct a new color from hue, saturation, lightness and alpha components.
fn from_hsl(
hue: Option<f32>,
saturation: Option<f32>,
lightness: Option<f32>,
alpha: Option<f32>,
) -> Self;
/// Construct a new color from hue, blackness, whiteness and alpha components.
fn from_hwb(
hue: Option<f32>,
whiteness: Option<f32>,
blackness: Option<f32>,
alpha: Option<f32>,
) -> Self;
/// Construct a new color from the `lab` notation.
fn from_lab(lightness: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>)
-> Self;
/// Construct a new color from the `lch` notation.
fn from_lch(
lightness: Option<f32>,
chroma: Option<f32>,
hue: Option<f32>,
alpha: Option<f32>,
) -> Self;
/// Construct a new color from the `oklab` notation.
fn from_oklab(
lightness: Option<f32>,
a: Option<f32>,
b: Option<f32>,
alpha: Option<f32>,
) -> Self;
/// Construct a new color from the `oklch` notation.
fn from_oklch(
lightness: Option<f32>,
chroma: Option<f32>,
hue: Option<f32>,
alpha: Option<f32>,
) -> Self;
/// Construct a new color with a predefined color space.
fn from_color_function(
color_space: PredefinedColorSpace,
c1: Option<f32>,
c2: Option<f32>,
c3: Option<f32>,
alpha: Option<f32>,
) -> Self;
}
impl FromParsedColor for Color {
#[inline]
fn from_current_color() -> Self {
Color::CurrentColor
}
#[inline]
fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
Color::Rgba(RgbaLegacy::new(red, green, blue, alpha))
}
fn from_hsl(
hue: Option<f32>,
saturation: Option<f32>,
lightness: Option<f32>,
alpha: Option<f32>,
) -> Self {
Color::Hsl(Hsl::new(hue, saturation, lightness, alpha))
}
fn from_hwb(
hue: Option<f32>,
blackness: Option<f32>,
whiteness: Option<f32>,
alpha: Option<f32>,
) -> Self {
Color::Hwb(Hwb::new(hue, blackness, whiteness, alpha))
}
#[inline]
fn from_lab(
lightness: Option<f32>,
a: Option<f32>,
b: Option<f32>,
alpha: Option<f32>,
) -> Self {
Color::Lab(Lab::new(lightness, a, b, alpha))
}
#[inline]
fn from_lch(
lightness: Option<f32>,
chroma: Option<f32>,
hue: Option<f32>,
alpha: Option<f32>,
) -> Self {
Color::Lch(Lch::new(lightness, chroma, hue, alpha))
}
#[inline]
fn from_oklab(
lightness: Option<f32>,
a: Option<f32>,
b: Option<f32>,
alpha: Option<f32>,
) -> Self {
Color::Oklab(Oklab::new(lightness, a, b, alpha))
}
#[inline]
fn from_oklch(
lightness: Option<f32>,
chroma: Option<f32>,
hue: Option<f32>,
alpha: Option<f32>,
) -> Self {
Color::Oklch(Oklch::new(lightness, chroma, hue, alpha))
}
#[inline]
fn from_color_function(
color_space: PredefinedColorSpace,
c1: Option<f32>,
c2: Option<f32>,
c3: Option<f32>,
alpha: Option<f32>,
) -> Self {
Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha))
}
}