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