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