style/color/
color_function.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Output of parsing a color function, e.g. rgb(..), hsl(..), color(..)
6
7use std::fmt::Write;
8
9use super::{
10    component::ColorComponent,
11    convert::normalize_hue,
12    parsing::{NumberOrAngleComponent, NumberOrPercentageComponent},
13    AbsoluteColor, ColorFlags, ColorSpace,
14};
15use crate::values::{
16    computed::color::Color as ComputedColor, generics::Optional, normalize,
17    specified::color::Color as SpecifiedColor,
18};
19use cssparser::color::{clamp_floor_256_f32, OPAQUE};
20
21/// Represents a specified color function.
22#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
23#[repr(u8)]
24pub enum ColorFunction<OriginColor> {
25    /// <https://drafts.csswg.org/css-color-4/#rgb-functions>
26    Rgb(
27        Optional<OriginColor>,                       // origin
28        ColorComponent<NumberOrPercentageComponent>, // red
29        ColorComponent<NumberOrPercentageComponent>, // green
30        ColorComponent<NumberOrPercentageComponent>, // blue
31        ColorComponent<NumberOrPercentageComponent>, // alpha
32    ),
33    /// <https://drafts.csswg.org/css-color-4/#the-hsl-notation>
34    Hsl(
35        Optional<OriginColor>,                       // origin
36        ColorComponent<NumberOrAngleComponent>,      // hue
37        ColorComponent<NumberOrPercentageComponent>, // saturation
38        ColorComponent<NumberOrPercentageComponent>, // lightness
39        ColorComponent<NumberOrPercentageComponent>, // alpha
40    ),
41    /// <https://drafts.csswg.org/css-color-4/#the-hwb-notation>
42    Hwb(
43        Optional<OriginColor>,                       // origin
44        ColorComponent<NumberOrAngleComponent>,      // hue
45        ColorComponent<NumberOrPercentageComponent>, // whiteness
46        ColorComponent<NumberOrPercentageComponent>, // blackness
47        ColorComponent<NumberOrPercentageComponent>, // alpha
48    ),
49    /// <https://drafts.csswg.org/css-color-4/#specifying-lab-lch>
50    Lab(
51        Optional<OriginColor>,                       // origin
52        ColorComponent<NumberOrPercentageComponent>, // lightness
53        ColorComponent<NumberOrPercentageComponent>, // a
54        ColorComponent<NumberOrPercentageComponent>, // b
55        ColorComponent<NumberOrPercentageComponent>, // alpha
56    ),
57    /// <https://drafts.csswg.org/css-color-4/#specifying-lab-lch>
58    Lch(
59        Optional<OriginColor>,                       // origin
60        ColorComponent<NumberOrPercentageComponent>, // lightness
61        ColorComponent<NumberOrPercentageComponent>, // chroma
62        ColorComponent<NumberOrAngleComponent>,      // hue
63        ColorComponent<NumberOrPercentageComponent>, // alpha
64    ),
65    /// <https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch>
66    Oklab(
67        Optional<OriginColor>,                       // origin
68        ColorComponent<NumberOrPercentageComponent>, // lightness
69        ColorComponent<NumberOrPercentageComponent>, // a
70        ColorComponent<NumberOrPercentageComponent>, // b
71        ColorComponent<NumberOrPercentageComponent>, // alpha
72    ),
73    /// <https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch>
74    Oklch(
75        Optional<OriginColor>,                       // origin
76        ColorComponent<NumberOrPercentageComponent>, // lightness
77        ColorComponent<NumberOrPercentageComponent>, // chroma
78        ColorComponent<NumberOrAngleComponent>,      // hue
79        ColorComponent<NumberOrPercentageComponent>, // alpha
80    ),
81    /// <https://drafts.csswg.org/css-color-4/#color-function>
82    Color(
83        Optional<OriginColor>,                       // origin
84        ColorComponent<NumberOrPercentageComponent>, // red / x
85        ColorComponent<NumberOrPercentageComponent>, // green / y
86        ColorComponent<NumberOrPercentageComponent>, // blue / z
87        ColorComponent<NumberOrPercentageComponent>, // alpha
88        ColorSpace,
89    ),
90}
91
92impl ColorFunction<AbsoluteColor> {
93    /// Try to resolve into a valid absolute color.
94    pub fn resolve_to_absolute(&self) -> Result<AbsoluteColor, ()> {
95        macro_rules! alpha {
96            ($alpha:expr, $origin_color:expr) => {{
97                $alpha
98                    .resolve($origin_color)?
99                    .map(|value| normalize(value.to_number(1.0)).clamp(0.0, OPAQUE))
100            }};
101        }
102
103        Ok(match self {
104            ColorFunction::Rgb(origin_color, r, g, b, alpha) => {
105                // Use `color(srgb ...)` to serialize `rgb(...)` if an origin color is available;
106                // this is the only reason for now.
107                let use_color_syntax = origin_color.is_some();
108
109                if use_color_syntax {
110                    let origin_color = origin_color.as_ref().map(|origin| {
111                        let origin = origin.to_color_space(ColorSpace::Srgb);
112                        // Because rgb(..) syntax have components in range [0..255), we have to
113                        // map them.
114                        // NOTE: The IS_LEGACY_SRGB flag is not added back to the color, because
115                        //       we're going to return the modern color(srgb ..) syntax.
116                        AbsoluteColor::new(
117                            ColorSpace::Srgb,
118                            origin.c0().map(|v| v * 255.0),
119                            origin.c1().map(|v| v * 255.0),
120                            origin.c2().map(|v| v * 255.0),
121                            origin.alpha(),
122                        )
123                    });
124
125                    // We have to map all the components back to [0..1) range after all the
126                    // calculations.
127                    AbsoluteColor::new(
128                        ColorSpace::Srgb,
129                        r.resolve(origin_color.as_ref())?
130                            .map(|c| c.to_number(255.0) / 255.0),
131                        g.resolve(origin_color.as_ref())?
132                            .map(|c| c.to_number(255.0) / 255.0),
133                        b.resolve(origin_color.as_ref())?
134                            .map(|c| c.to_number(255.0) / 255.0),
135                        alpha!(alpha, origin_color.as_ref()),
136                    )
137                } else {
138                    #[inline]
139                    fn resolve(
140                        component: &ColorComponent<NumberOrPercentageComponent>,
141                        origin_color: Option<&AbsoluteColor>,
142                    ) -> Result<u8, ()> {
143                        Ok(clamp_floor_256_f32(
144                            component
145                                .resolve(origin_color)?
146                                .map_or(0.0, |value| value.to_number(u8::MAX as f32)),
147                        ))
148                    }
149
150                    let origin_color = origin_color.as_ref().map(|o| o.into_srgb_legacy());
151
152                    AbsoluteColor::srgb_legacy(
153                        resolve(r, origin_color.as_ref())?,
154                        resolve(g, origin_color.as_ref())?,
155                        resolve(b, origin_color.as_ref())?,
156                        alpha!(alpha, origin_color.as_ref()).unwrap_or(0.0),
157                    )
158                }
159            },
160            ColorFunction::Hsl(origin_color, h, s, l, alpha) => {
161                // Percent reference range for S and L: 0% = 0.0, 100% = 100.0
162                const LIGHTNESS_RANGE: f32 = 100.0;
163                const SATURATION_RANGE: f32 = 100.0;
164
165                // If the origin color:
166                // - was *NOT* specified, then we stick with the old way of serializing the
167                //   value to rgb(..).
168                // - was specified, we don't use the rgb(..) syntax, because we should allow the
169                //   color to be out of gamut and not clamp.
170                let use_rgb_sytax = origin_color.is_none();
171
172                let origin_color = origin_color
173                    .as_ref()
174                    .map(|o| o.to_color_space(ColorSpace::Hsl));
175
176                let mut result = AbsoluteColor::new(
177                    ColorSpace::Hsl,
178                    h.resolve(origin_color.as_ref())?
179                        .map(|angle| normalize_hue(angle.degrees())),
180                    s.resolve(origin_color.as_ref())?.map(|s| {
181                        if use_rgb_sytax {
182                            s.to_number(SATURATION_RANGE).clamp(0.0, SATURATION_RANGE)
183                        } else {
184                            s.to_number(SATURATION_RANGE)
185                        }
186                    }),
187                    l.resolve(origin_color.as_ref())?.map(|l| {
188                        if use_rgb_sytax {
189                            l.to_number(LIGHTNESS_RANGE).clamp(0.0, LIGHTNESS_RANGE)
190                        } else {
191                            l.to_number(LIGHTNESS_RANGE)
192                        }
193                    }),
194                    alpha!(alpha, origin_color.as_ref()),
195                );
196
197                if use_rgb_sytax {
198                    result.flags.insert(ColorFlags::IS_LEGACY_SRGB);
199                }
200
201                result
202            },
203            ColorFunction::Hwb(origin_color, h, w, b, alpha) => {
204                // If the origin color:
205                // - was *NOT* specified, then we stick with the old way of serializing the
206                //   value to rgb(..).
207                // - was specified, we don't use the rgb(..) syntax, because we should allow the
208                //   color to be out of gamut and not clamp.
209                let use_rgb_sytax = origin_color.is_none();
210
211                // Percent reference range for W and B: 0% = 0.0, 100% = 100.0
212                const WHITENESS_RANGE: f32 = 100.0;
213                const BLACKNESS_RANGE: f32 = 100.0;
214
215                let origin_color = origin_color
216                    .as_ref()
217                    .map(|o| o.to_color_space(ColorSpace::Hwb));
218
219                let mut result = AbsoluteColor::new(
220                    ColorSpace::Hwb,
221                    h.resolve(origin_color.as_ref())?
222                        .map(|angle| normalize_hue(angle.degrees())),
223                    w.resolve(origin_color.as_ref())?.map(|w| {
224                        if use_rgb_sytax {
225                            w.to_number(WHITENESS_RANGE).clamp(0.0, WHITENESS_RANGE)
226                        } else {
227                            w.to_number(WHITENESS_RANGE)
228                        }
229                    }),
230                    b.resolve(origin_color.as_ref())?.map(|b| {
231                        if use_rgb_sytax {
232                            b.to_number(BLACKNESS_RANGE).clamp(0.0, BLACKNESS_RANGE)
233                        } else {
234                            b.to_number(BLACKNESS_RANGE)
235                        }
236                    }),
237                    alpha!(alpha, origin_color.as_ref()),
238                );
239
240                if use_rgb_sytax {
241                    result.flags.insert(ColorFlags::IS_LEGACY_SRGB);
242                }
243
244                result
245            },
246            ColorFunction::Lab(origin_color, l, a, b, alpha) => {
247                // for L: 0% = 0.0, 100% = 100.0
248                // for a and b: -100% = -125, 100% = 125
249                const LIGHTNESS_RANGE: f32 = 100.0;
250                const A_B_RANGE: f32 = 125.0;
251
252                let origin_color = origin_color
253                    .as_ref()
254                    .map(|o| o.to_color_space(ColorSpace::Lab));
255
256                AbsoluteColor::new(
257                    ColorSpace::Lab,
258                    l.resolve(origin_color.as_ref())?
259                        .map(|l| l.to_number(LIGHTNESS_RANGE)),
260                    a.resolve(origin_color.as_ref())?
261                        .map(|a| a.to_number(A_B_RANGE)),
262                    b.resolve(origin_color.as_ref())?
263                        .map(|b| b.to_number(A_B_RANGE)),
264                    alpha!(alpha, origin_color.as_ref()),
265                )
266            },
267            ColorFunction::Lch(origin_color, l, c, h, alpha) => {
268                // for L: 0% = 0.0, 100% = 100.0
269                // for C: 0% = 0, 100% = 150
270                const LIGHTNESS_RANGE: f32 = 100.0;
271                const CHROMA_RANGE: f32 = 150.0;
272
273                let origin_color = origin_color
274                    .as_ref()
275                    .map(|o| o.to_color_space(ColorSpace::Lch));
276
277                AbsoluteColor::new(
278                    ColorSpace::Lch,
279                    l.resolve(origin_color.as_ref())?
280                        .map(|l| l.to_number(LIGHTNESS_RANGE)),
281                    c.resolve(origin_color.as_ref())?
282                        .map(|c| c.to_number(CHROMA_RANGE)),
283                    h.resolve(origin_color.as_ref())?
284                        .map(|angle| normalize_hue(angle.degrees())),
285                    alpha!(alpha, origin_color.as_ref()),
286                )
287            },
288            ColorFunction::Oklab(origin_color, l, a, b, alpha) => {
289                // for L: 0% = 0.0, 100% = 1.0
290                // for a and b: -100% = -0.4, 100% = 0.4
291                const LIGHTNESS_RANGE: f32 = 1.0;
292                const A_B_RANGE: f32 = 0.4;
293
294                let origin_color = origin_color
295                    .as_ref()
296                    .map(|o| o.to_color_space(ColorSpace::Oklab));
297
298                AbsoluteColor::new(
299                    ColorSpace::Oklab,
300                    l.resolve(origin_color.as_ref())?
301                        .map(|l| l.to_number(LIGHTNESS_RANGE)),
302                    a.resolve(origin_color.as_ref())?
303                        .map(|a| a.to_number(A_B_RANGE)),
304                    b.resolve(origin_color.as_ref())?
305                        .map(|b| b.to_number(A_B_RANGE)),
306                    alpha!(alpha, origin_color.as_ref()),
307                )
308            },
309            ColorFunction::Oklch(origin_color, l, c, h, alpha) => {
310                // for L: 0% = 0.0, 100% = 1.0
311                // for C: 0% = 0.0 100% = 0.4
312                const LIGHTNESS_RANGE: f32 = 1.0;
313                const CHROMA_RANGE: f32 = 0.4;
314
315                let origin_color = origin_color
316                    .as_ref()
317                    .map(|o| o.to_color_space(ColorSpace::Oklch));
318
319                AbsoluteColor::new(
320                    ColorSpace::Oklch,
321                    l.resolve(origin_color.as_ref())?
322                        .map(|l| l.to_number(LIGHTNESS_RANGE)),
323                    c.resolve(origin_color.as_ref())?
324                        .map(|c| c.to_number(CHROMA_RANGE)),
325                    h.resolve(origin_color.as_ref())?
326                        .map(|angle| normalize_hue(angle.degrees())),
327                    alpha!(alpha, origin_color.as_ref()),
328                )
329            },
330            ColorFunction::Color(origin_color, r, g, b, alpha, color_space) => {
331                let origin_color = origin_color.as_ref().map(|o| {
332                    let mut result = o.to_color_space(*color_space);
333
334                    // If the origin color was a `rgb(..)` function, we should
335                    // make sure it doesn't have the legacy flag any more so
336                    // that it is recognized as a `color(srgb ..)` function.
337                    result.flags.set(ColorFlags::IS_LEGACY_SRGB, false);
338
339                    result
340                });
341
342                AbsoluteColor::new(
343                    *color_space,
344                    r.resolve(origin_color.as_ref())?.map(|c| c.to_number(1.0)),
345                    g.resolve(origin_color.as_ref())?.map(|c| c.to_number(1.0)),
346                    b.resolve(origin_color.as_ref())?.map(|c| c.to_number(1.0)),
347                    alpha!(alpha, origin_color.as_ref()),
348                )
349            },
350        })
351    }
352}
353
354impl ColorFunction<SpecifiedColor> {
355    /// Return true if the color funciton has an origin color specified.
356    pub fn has_origin_color(&self) -> bool {
357        match self {
358            Self::Rgb(origin_color, ..)
359            | Self::Hsl(origin_color, ..)
360            | Self::Hwb(origin_color, ..)
361            | Self::Lab(origin_color, ..)
362            | Self::Lch(origin_color, ..)
363            | Self::Oklab(origin_color, ..)
364            | Self::Oklch(origin_color, ..)
365            | Self::Color(origin_color, ..) => origin_color.is_some(),
366        }
367    }
368
369    /// Try to resolve the color function to an [`AbsoluteColor`] that does not
370    /// contain any variables (currentcolor, color components, etc.).
371    pub fn resolve_to_absolute(&self) -> Result<AbsoluteColor, ()> {
372        // Map the color function to one with an absolute origin color.
373        let resolvable = self.map_origin_color(|o| o.resolve_to_absolute());
374        resolvable.resolve_to_absolute()
375    }
376}
377
378impl<Color> ColorFunction<Color> {
379    /// Map the origin color to another type.  Return None from `f` if the conversion fails.
380    pub fn map_origin_color<U>(&self, f: impl FnOnce(&Color) -> Option<U>) -> ColorFunction<U> {
381        macro_rules! map {
382            ($f:ident, $o:expr, $c0:expr, $c1:expr, $c2:expr, $alpha:expr) => {{
383                ColorFunction::$f(
384                    $o.as_ref().and_then(f).into(),
385                    $c0.clone(),
386                    $c1.clone(),
387                    $c2.clone(),
388                    $alpha.clone(),
389                )
390            }};
391        }
392        match self {
393            ColorFunction::Rgb(o, c0, c1, c2, alpha) => map!(Rgb, o, c0, c1, c2, alpha),
394            ColorFunction::Hsl(o, c0, c1, c2, alpha) => map!(Hsl, o, c0, c1, c2, alpha),
395            ColorFunction::Hwb(o, c0, c1, c2, alpha) => map!(Hwb, o, c0, c1, c2, alpha),
396            ColorFunction::Lab(o, c0, c1, c2, alpha) => map!(Lab, o, c0, c1, c2, alpha),
397            ColorFunction::Lch(o, c0, c1, c2, alpha) => map!(Lch, o, c0, c1, c2, alpha),
398            ColorFunction::Oklab(o, c0, c1, c2, alpha) => map!(Oklab, o, c0, c1, c2, alpha),
399            ColorFunction::Oklch(o, c0, c1, c2, alpha) => map!(Oklch, o, c0, c1, c2, alpha),
400            ColorFunction::Color(o, c0, c1, c2, alpha, color_space) => ColorFunction::Color(
401                o.as_ref().and_then(f).into(),
402                c0.clone(),
403                c1.clone(),
404                c2.clone(),
405                alpha.clone(),
406                color_space.clone(),
407            ),
408        }
409    }
410}
411
412impl ColorFunction<ComputedColor> {
413    /// Resolve a computed color function to an absolute computed color.
414    pub fn resolve_to_absolute(&self, current_color: &AbsoluteColor) -> AbsoluteColor {
415        // Map the color function to one with an absolute origin color.
416        let resolvable = self.map_origin_color(|o| Some(o.resolve_to_absolute(current_color)));
417        match resolvable.resolve_to_absolute() {
418            Ok(color) => color,
419            Err(..) => {
420                debug_assert!(
421                    false,
422                    "the color could not be resolved even with a currentcolor specified?"
423                );
424                AbsoluteColor::TRANSPARENT_BLACK
425            },
426        }
427    }
428}
429
430impl<C: style_traits::ToCss> style_traits::ToCss for ColorFunction<C> {
431    fn to_css<W>(&self, dest: &mut style_traits::CssWriter<W>) -> std::fmt::Result
432    where
433        W: std::fmt::Write,
434    {
435        let (origin_color, alpha) = match self {
436            Self::Rgb(origin_color, _, _, _, alpha) => {
437                dest.write_str("rgb(")?;
438                (origin_color, alpha)
439            },
440            Self::Hsl(origin_color, _, _, _, alpha) => {
441                dest.write_str("hsl(")?;
442                (origin_color, alpha)
443            },
444            Self::Hwb(origin_color, _, _, _, alpha) => {
445                dest.write_str("hwb(")?;
446                (origin_color, alpha)
447            },
448            Self::Lab(origin_color, _, _, _, alpha) => {
449                dest.write_str("lab(")?;
450                (origin_color, alpha)
451            },
452            Self::Lch(origin_color, _, _, _, alpha) => {
453                dest.write_str("lch(")?;
454                (origin_color, alpha)
455            },
456            Self::Oklab(origin_color, _, _, _, alpha) => {
457                dest.write_str("oklab(")?;
458                (origin_color, alpha)
459            },
460            Self::Oklch(origin_color, _, _, _, alpha) => {
461                dest.write_str("oklch(")?;
462                (origin_color, alpha)
463            },
464            Self::Color(origin_color, _, _, _, alpha, _) => {
465                dest.write_str("color(")?;
466                (origin_color, alpha)
467            },
468        };
469
470        if let Optional::Some(origin_color) = origin_color {
471            dest.write_str("from ")?;
472            origin_color.to_css(dest)?;
473            dest.write_str(" ")?;
474        }
475
476        let is_opaque = if let ColorComponent::Value(value) = *alpha {
477            value.to_number(OPAQUE) == OPAQUE
478        } else {
479            false
480        };
481
482        macro_rules! serialize_alpha {
483            ($alpha_component:expr) => {{
484                if !is_opaque && !matches!($alpha_component, ColorComponent::AlphaOmitted) {
485                    dest.write_str(" / ")?;
486                    $alpha_component.to_css(dest)?;
487                }
488            }};
489        }
490
491        macro_rules! serialize_components {
492            ($c0:expr, $c1:expr, $c2:expr) => {{
493                debug_assert!(!matches!($c0, ColorComponent::AlphaOmitted));
494                debug_assert!(!matches!($c1, ColorComponent::AlphaOmitted));
495                debug_assert!(!matches!($c2, ColorComponent::AlphaOmitted));
496
497                $c0.to_css(dest)?;
498                dest.write_str(" ")?;
499                $c1.to_css(dest)?;
500                dest.write_str(" ")?;
501                $c2.to_css(dest)?;
502            }};
503        }
504
505        match self {
506            Self::Rgb(_, c0, c1, c2, alpha) => {
507                serialize_components!(c0, c1, c2);
508                serialize_alpha!(alpha);
509            },
510            Self::Hsl(_, c0, c1, c2, alpha) => {
511                serialize_components!(c0, c1, c2);
512                serialize_alpha!(alpha);
513            },
514            Self::Hwb(_, c0, c1, c2, alpha) => {
515                serialize_components!(c0, c1, c2);
516                serialize_alpha!(alpha);
517            },
518            Self::Lab(_, c0, c1, c2, alpha) => {
519                serialize_components!(c0, c1, c2);
520                serialize_alpha!(alpha);
521            },
522            Self::Lch(_, c0, c1, c2, alpha) => {
523                serialize_components!(c0, c1, c2);
524                serialize_alpha!(alpha);
525            },
526            Self::Oklab(_, c0, c1, c2, alpha) => {
527                serialize_components!(c0, c1, c2);
528                serialize_alpha!(alpha);
529            },
530            Self::Oklch(_, c0, c1, c2, alpha) => {
531                serialize_components!(c0, c1, c2);
532                serialize_alpha!(alpha);
533            },
534            Self::Color(_, c0, c1, c2, alpha, color_space) => {
535                color_space.to_css(dest)?;
536                dest.write_str(" ")?;
537                serialize_components!(c0, c1, c2);
538                serialize_alpha!(alpha);
539            },
540        }
541
542        dest.write_str(")")
543    }
544}