Skip to main content

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