style/values/specified/
color.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//! Specified color values.
6
7use super::AllowQuirks;
8use crate::color::mix::ColorInterpolationMethod;
9use crate::color::{parsing, AbsoluteColor, ColorFunction, ColorSpace};
10use crate::derives::*;
11use crate::media_queries::Device;
12use crate::parser::{Parse, ParserContext};
13use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue};
14use crate::values::generics::color::{
15    ColorMixFlags, GenericCaretColor, GenericColorMix, GenericColorOrAuto, GenericLightDark,
16};
17use crate::values::specified::Percentage;
18use crate::values::{normalize, CustomIdent};
19use cssparser::{match_ignore_ascii_case, BasicParseErrorKind, ParseErrorKind, Parser, Token};
20use std::fmt::{self, Write};
21use std::io::Write as IoWrite;
22use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind};
23use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind};
24
25/// A specified color-mix().
26pub type ColorMix = GenericColorMix<Color, Percentage>;
27
28impl ColorMix {
29    fn parse<'i, 't>(
30        context: &ParserContext,
31        input: &mut Parser<'i, 't>,
32        preserve_authored: PreserveAuthored,
33    ) -> Result<Self, ParseError<'i>> {
34        input.expect_function_matching("color-mix")?;
35
36        input.parse_nested_block(|input| {
37            // If the color interpolation method is omitted, default to "in oklab".
38            // See: https://github.com/web-platform-tests/interop/issues/1166
39            let interpolation = input
40                .try_parse(|input| -> Result<_, ParseError<'i>> {
41                    let interpolation = ColorInterpolationMethod::parse(context, input)?;
42                    input.expect_comma()?;
43                    Ok(interpolation)
44                })
45                .unwrap_or_default();
46
47            let try_parse_percentage = |input: &mut Parser| -> Option<Percentage> {
48                input
49                    .try_parse(|input| Percentage::parse_zero_to_a_hundred(context, input))
50                    .ok()
51            };
52
53            let mut left_percentage = try_parse_percentage(input);
54
55            let left = Color::parse_internal(context, input, preserve_authored)?;
56            if left_percentage.is_none() {
57                left_percentage = try_parse_percentage(input);
58            }
59
60            input.expect_comma()?;
61
62            let mut right_percentage = try_parse_percentage(input);
63
64            let right = Color::parse_internal(context, input, preserve_authored)?;
65
66            if right_percentage.is_none() {
67                right_percentage = try_parse_percentage(input);
68            }
69
70            let right_percentage = right_percentage
71                .unwrap_or_else(|| Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get())));
72
73            let left_percentage =
74                left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get()));
75
76            if left_percentage.get() + right_percentage.get() <= 0.0 {
77                // If the percentages sum to zero, the function is invalid.
78                return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
79            }
80
81            // Pass RESULT_IN_MODERN_SYNTAX here, because the result of the color-mix() function
82            // should always be in the modern color syntax to allow for out of gamut results and
83            // to preserve floating point precision.
84            Ok(ColorMix {
85                interpolation,
86                left,
87                left_percentage,
88                right,
89                right_percentage,
90                flags: ColorMixFlags::NORMALIZE_WEIGHTS | ColorMixFlags::RESULT_IN_MODERN_SYNTAX,
91            })
92        })
93    }
94}
95
96/// Container holding an absolute color and the text specified by an author.
97#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
98pub struct Absolute {
99    /// The specified color.
100    pub color: AbsoluteColor,
101    /// Authored representation.
102    pub authored: Option<Box<str>>,
103}
104
105impl ToCss for Absolute {
106    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
107    where
108        W: Write,
109    {
110        if let Some(ref authored) = self.authored {
111            dest.write_str(authored)
112        } else {
113            self.color.to_css(dest)
114        }
115    }
116}
117
118/// Specified color value
119#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, ToTyped)]
120pub enum Color {
121    /// The 'currentColor' keyword
122    CurrentColor,
123    /// An absolute color.
124    /// https://w3c.github.io/csswg-drafts/css-color-4/#typedef-absolute-color-function
125    Absolute(Box<Absolute>),
126    /// A color function that could not be resolved to a [Color::Absolute] color at parse time.
127    /// Right now this is only the case for relative colors with `currentColor` as the origin.
128    ColorFunction(Box<ColorFunction<Self>>),
129    /// A system color.
130    #[cfg(feature = "gecko")]
131    System(SystemColor),
132    /// A color mix.
133    ColorMix(Box<ColorMix>),
134    /// A light-dark() color.
135    LightDark(Box<GenericLightDark<Self>>),
136    /// The contrast-color function.
137    ContrastColor(Box<Color>),
138    /// Quirksmode-only rule for inheriting color from the body
139    #[cfg(feature = "gecko")]
140    InheritFromBodyQuirk,
141}
142
143impl From<AbsoluteColor> for Color {
144    #[inline]
145    fn from(value: AbsoluteColor) -> Self {
146        Self::from_absolute_color(value)
147    }
148}
149
150/// System colors. A bunch of these are ad-hoc, others come from Windows:
151///
152///   https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor
153///
154/// Others are HTML/CSS specific. Spec is:
155///
156///   https://drafts.csswg.org/css-color/#css-system-colors
157///   https://drafts.csswg.org/css-color/#deprecated-system-colors
158#[allow(missing_docs)]
159#[cfg(feature = "gecko")]
160#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
161#[repr(u8)]
162pub enum SystemColor {
163    Activeborder,
164    /// Background in the (active) titlebar.
165    Activecaption,
166    Appworkspace,
167    Background,
168    Buttonface,
169    Buttonhighlight,
170    Buttonshadow,
171    Buttontext,
172    Buttonborder,
173    /// Text color in the (active) titlebar.
174    Captiontext,
175    #[parse(aliases = "-moz-field")]
176    Field,
177    /// Used for disabled field backgrounds.
178    #[parse(condition = "ParserContext::chrome_rules_enabled")]
179    MozDisabledfield,
180    #[parse(aliases = "-moz-fieldtext")]
181    Fieldtext,
182
183    Mark,
184    Marktext,
185
186    /// Combobox widgets
187    MozComboboxtext,
188    MozCombobox,
189
190    Graytext,
191    Highlight,
192    Highlighttext,
193    Inactiveborder,
194    /// Background in the (inactive) titlebar.
195    Inactivecaption,
196    /// Text color in the (inactive) titlebar.
197    Inactivecaptiontext,
198    Infobackground,
199    Infotext,
200    Menu,
201    Menutext,
202    Scrollbar,
203    Threeddarkshadow,
204    Threedface,
205    Threedhighlight,
206    Threedlightshadow,
207    Threedshadow,
208    Window,
209    Windowframe,
210    Windowtext,
211    #[parse(aliases = "-moz-default-color")]
212    Canvastext,
213    #[parse(aliases = "-moz-default-background-color")]
214    Canvas,
215    MozDialog,
216    MozDialogtext,
217    /// Used for selected but not focused cell backgrounds.
218    #[parse(aliases = "-moz-html-cellhighlight")]
219    MozCellhighlight,
220    /// Used for selected but not focused cell text.
221    #[parse(aliases = "-moz-html-cellhighlighttext")]
222    MozCellhighlighttext,
223    /// Used for selected and focused html cell backgrounds.
224    Selecteditem,
225    /// Used for selected and focused html cell text.
226    Selecteditemtext,
227    /// Used for menu item backgrounds when hovered.
228    MozMenuhover,
229    /// Used for menu item backgrounds when hovered and disabled.
230    #[parse(condition = "ParserContext::chrome_rules_enabled")]
231    MozMenuhoverdisabled,
232    /// Used for menu item text when hovered.
233    MozMenuhovertext,
234    /// Used for menubar item text when hovered.
235    MozMenubarhovertext,
236
237    /// On platforms where this color is the same as field, or transparent, use fieldtext as
238    /// foreground color.
239    MozOddtreerow,
240
241    /// Used for button text background when hovered.
242    #[parse(condition = "ParserContext::chrome_rules_enabled")]
243    MozButtonhoverface,
244    /// Used for button text color when hovered.
245    #[parse(condition = "ParserContext::chrome_rules_enabled")]
246    MozButtonhovertext,
247    /// Used for button border color when hovered.
248    #[parse(condition = "ParserContext::chrome_rules_enabled")]
249    MozButtonhoverborder,
250    /// Used for button background when pressed.
251    #[parse(condition = "ParserContext::chrome_rules_enabled")]
252    MozButtonactiveface,
253    /// Used for button text when pressed.
254    #[parse(condition = "ParserContext::chrome_rules_enabled")]
255    MozButtonactivetext,
256    /// Used for button border when pressed.
257    #[parse(condition = "ParserContext::chrome_rules_enabled")]
258    MozButtonactiveborder,
259
260    /// Used for button background when disabled.
261    #[parse(condition = "ParserContext::chrome_rules_enabled")]
262    MozButtondisabledface,
263    /// Used for button border when disabled.
264    #[parse(condition = "ParserContext::chrome_rules_enabled")]
265    MozButtondisabledborder,
266
267    /// Colors used for the header bar (sorta like the tab bar / menubar).
268    #[parse(condition = "ParserContext::chrome_rules_enabled")]
269    MozHeaderbar,
270    #[parse(condition = "ParserContext::chrome_rules_enabled")]
271    MozHeaderbartext,
272    #[parse(condition = "ParserContext::chrome_rules_enabled")]
273    MozHeaderbarinactive,
274    #[parse(condition = "ParserContext::chrome_rules_enabled")]
275    MozHeaderbarinactivetext,
276
277    /// Foreground color of default buttons.
278    #[parse(condition = "ParserContext::chrome_rules_enabled")]
279    MozMacDefaultbuttontext,
280    /// Ring color around text fields and lists.
281    #[parse(condition = "ParserContext::chrome_rules_enabled")]
282    MozMacFocusring,
283    /// Text color of disabled text on toolbars.
284    #[parse(condition = "ParserContext::chrome_rules_enabled")]
285    MozMacDisabledtoolbartext,
286    /// The background of a sidebar.
287    #[parse(condition = "ParserContext::chrome_rules_enabled")]
288    MozSidebar,
289    /// The foreground color of a sidebar.
290    #[parse(condition = "ParserContext::chrome_rules_enabled")]
291    MozSidebartext,
292    /// The border color of a sidebar.
293    #[parse(condition = "ParserContext::chrome_rules_enabled")]
294    MozSidebarborder,
295
296    /// Theme accent color.
297    /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolor
298    Accentcolor,
299
300    /// Foreground for the accent color.
301    /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolortext
302    Accentcolortext,
303
304    /// The background-color for :autofill-ed inputs.
305    #[parse(condition = "ParserContext::chrome_rules_enabled")]
306    MozAutofillBackground,
307
308    #[parse(aliases = "-moz-hyperlinktext")]
309    Linktext,
310    #[parse(aliases = "-moz-activehyperlinktext")]
311    Activetext,
312    #[parse(aliases = "-moz-visitedhyperlinktext")]
313    Visitedtext,
314
315    /// Color of tree column headers
316    #[parse(condition = "ParserContext::chrome_rules_enabled")]
317    MozColheader,
318    #[parse(condition = "ParserContext::chrome_rules_enabled")]
319    MozColheadertext,
320    #[parse(condition = "ParserContext::chrome_rules_enabled")]
321    MozColheaderhover,
322    #[parse(condition = "ParserContext::chrome_rules_enabled")]
323    MozColheaderhovertext,
324    #[parse(condition = "ParserContext::chrome_rules_enabled")]
325    MozColheaderactive,
326    #[parse(condition = "ParserContext::chrome_rules_enabled")]
327    MozColheaderactivetext,
328
329    #[parse(condition = "ParserContext::chrome_rules_enabled")]
330    TextSelectDisabledBackground,
331    #[css(skip)]
332    TextSelectAttentionBackground,
333    #[css(skip)]
334    TextSelectAttentionForeground,
335    #[css(skip)]
336    TextHighlightBackground,
337    #[css(skip)]
338    TextHighlightForeground,
339    #[css(skip)]
340    TargetTextBackground,
341    #[css(skip)]
342    TargetTextForeground,
343    #[css(skip)]
344    IMERawInputBackground,
345    #[css(skip)]
346    IMERawInputForeground,
347    #[css(skip)]
348    IMERawInputUnderline,
349    #[css(skip)]
350    IMESelectedRawTextBackground,
351    #[css(skip)]
352    IMESelectedRawTextForeground,
353    #[css(skip)]
354    IMESelectedRawTextUnderline,
355    #[css(skip)]
356    IMEConvertedTextBackground,
357    #[css(skip)]
358    IMEConvertedTextForeground,
359    #[css(skip)]
360    IMEConvertedTextUnderline,
361    #[css(skip)]
362    IMESelectedConvertedTextBackground,
363    #[css(skip)]
364    IMESelectedConvertedTextForeground,
365    #[css(skip)]
366    IMESelectedConvertedTextUnderline,
367    #[css(skip)]
368    SpellCheckerUnderline,
369    #[css(skip)]
370    ThemedScrollbar,
371    #[css(skip)]
372    ThemedScrollbarThumb,
373    #[css(skip)]
374    ThemedScrollbarThumbHover,
375    #[css(skip)]
376    ThemedScrollbarThumbActive,
377
378    #[css(skip)]
379    End, // Just for array-indexing purposes.
380}
381
382#[cfg(feature = "gecko")]
383impl SystemColor {
384    #[inline]
385    fn compute(&self, cx: &Context) -> ComputedColor {
386        use crate::gecko::values::convert_nscolor_to_absolute_color;
387        use crate::gecko_bindings::bindings;
388
389        let color = cx.device().system_nscolor(*self, cx.builder.color_scheme);
390        if cx.for_non_inherited_property {
391            cx.rule_cache_conditions
392                .borrow_mut()
393                .set_color_scheme_dependency(cx.builder.color_scheme);
394        }
395        if color == bindings::NS_SAME_AS_FOREGROUND_COLOR {
396            return ComputedColor::currentcolor();
397        }
398        ComputedColor::Absolute(convert_nscolor_to_absolute_color(color))
399    }
400}
401
402/// Whether to preserve authored colors during parsing. That's useful only if we
403/// plan to serialize the color back.
404#[derive(Copy, Clone)]
405enum PreserveAuthored {
406    No,
407    Yes,
408}
409
410impl Parse for Color {
411    fn parse<'i, 't>(
412        context: &ParserContext,
413        input: &mut Parser<'i, 't>,
414    ) -> Result<Self, ParseError<'i>> {
415        Self::parse_internal(context, input, PreserveAuthored::Yes)
416    }
417}
418
419impl Color {
420    fn parse_internal<'i, 't>(
421        context: &ParserContext,
422        input: &mut Parser<'i, 't>,
423        preserve_authored: PreserveAuthored,
424    ) -> Result<Self, ParseError<'i>> {
425        let authored = match preserve_authored {
426            PreserveAuthored::No => None,
427            PreserveAuthored::Yes => {
428                // Currently we only store authored value for color keywords,
429                // because all browsers serialize those values as keywords for
430                // specified value.
431                let start = input.state();
432                let authored = input.expect_ident_cloned().ok();
433                input.reset(&start);
434                authored
435            },
436        };
437
438        match input.try_parse(|i| parsing::parse_color_with(context, i)) {
439            Ok(mut color) => {
440                if let Color::Absolute(ref mut absolute) = color {
441                    // Because we can't set the `authored` value at construction time, we have to set it
442                    // here.
443                    absolute.authored = authored.map(|s| s.to_ascii_lowercase().into_boxed_str());
444                }
445                Ok(color)
446            },
447            Err(e) => {
448                #[cfg(feature = "gecko")]
449                {
450                    if let Ok(system) = input.try_parse(|i| SystemColor::parse(context, i)) {
451                        return Ok(Color::System(system));
452                    }
453                }
454
455                if let Ok(mix) = input.try_parse(|i| ColorMix::parse(context, i, preserve_authored))
456                {
457                    return Ok(Color::ColorMix(Box::new(mix)));
458                }
459
460                if let Ok(ld) = input.try_parse(|i| {
461                    GenericLightDark::parse_with(i, |i| {
462                        Self::parse_internal(context, i, preserve_authored)
463                    })
464                }) {
465                    return Ok(Color::LightDark(Box::new(ld)));
466                }
467
468                if static_prefs::pref!("layout.css.contrast-color.enabled") {
469                    if let Ok(c) = input.try_parse(|i| {
470                        i.expect_function_matching("contrast-color")?;
471                        i.parse_nested_block(|i| {
472                            Self::parse_internal(context, i, preserve_authored)
473                        })
474                    }) {
475                        return Ok(Color::ContrastColor(Box::new(c)));
476                    }
477                }
478
479                match e.kind {
480                    ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => {
481                        Err(e.location.new_custom_error(StyleParseErrorKind::ValueError(
482                            ValueParseErrorKind::InvalidColor(t),
483                        )))
484                    },
485                    _ => Err(e),
486                }
487            },
488        }
489    }
490
491    /// Returns whether a given color is valid for authors.
492    pub fn is_valid(context: &ParserContext, input: &mut Parser) -> bool {
493        input
494            .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No))
495            .is_ok()
496    }
497
498    /// Tries to parse a color and compute it with a given device.
499    pub fn parse_and_compute(
500        context: &ParserContext,
501        input: &mut Parser,
502        device: Option<&Device>,
503    ) -> Option<ComputedColor> {
504        use crate::error_reporting::ContextualParseError;
505        let start = input.position();
506        let result = input
507            .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No));
508
509        let specified = match result {
510            Ok(s) => s,
511            Err(e) => {
512                if !context.error_reporting_enabled() {
513                    return None;
514                }
515                // Ignore other kinds of errors that might be reported, such as
516                // ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken),
517                // since Gecko didn't use to report those to the error console.
518                //
519                // TODO(emilio): Revise whether we want to keep this at all, we
520                // use this only for canvas, this warnings are disabled by
521                // default and not available on OffscreenCanvas anyways...
522                if let ParseErrorKind::Custom(StyleParseErrorKind::ValueError(..)) = e.kind {
523                    let location = e.location.clone();
524                    let error = ContextualParseError::UnsupportedValue(input.slice_from(start), e);
525                    context.log_css_error(location, error);
526                }
527                return None;
528            },
529        };
530
531        match device {
532            Some(device) => {
533                Context::for_media_query_evaluation(device, device.quirks_mode(), |context| {
534                    specified.to_computed_color(Some(&context))
535                })
536            },
537            None => specified.to_computed_color(None),
538        }
539    }
540}
541
542impl ToCss for Color {
543    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
544    where
545        W: Write,
546    {
547        match *self {
548            Color::CurrentColor => dest.write_str("currentcolor"),
549            Color::Absolute(ref absolute) => absolute.to_css(dest),
550            Color::ColorFunction(ref color_function) => color_function.to_css(dest),
551            Color::ColorMix(ref mix) => mix.to_css(dest),
552            Color::LightDark(ref ld) => ld.to_css(dest),
553            Color::ContrastColor(ref c) => {
554                dest.write_str("contrast-color(")?;
555                c.to_css(dest)?;
556                dest.write_char(')')
557            },
558            #[cfg(feature = "gecko")]
559            Color::System(system) => system.to_css(dest),
560            #[cfg(feature = "gecko")]
561            Color::InheritFromBodyQuirk => dest.write_str("-moz-inherit-from-body-quirk"),
562        }
563    }
564}
565
566impl Color {
567    /// Returns whether this color is allowed in forced-colors mode.
568    pub fn honored_in_forced_colors_mode(&self, allow_transparent: bool) -> bool {
569        match *self {
570            #[cfg(feature = "gecko")]
571            Self::InheritFromBodyQuirk => false,
572            Self::CurrentColor => true,
573            #[cfg(feature = "gecko")]
574            Self::System(..) => true,
575            Self::Absolute(ref absolute) => allow_transparent && absolute.color.is_transparent(),
576            Self::ColorFunction(ref color_function) => {
577                // For now we allow transparent colors if we can resolve the color function.
578                // <https://bugzilla.mozilla.org/show_bug.cgi?id=1923053>
579                color_function
580                    .resolve_to_absolute()
581                    .map(|resolved| allow_transparent && resolved.is_transparent())
582                    .unwrap_or(false)
583            },
584            Self::LightDark(ref ld) => {
585                ld.light.honored_in_forced_colors_mode(allow_transparent)
586                    && ld.dark.honored_in_forced_colors_mode(allow_transparent)
587            },
588            Self::ColorMix(ref mix) => {
589                mix.left.honored_in_forced_colors_mode(allow_transparent)
590                    && mix.right.honored_in_forced_colors_mode(allow_transparent)
591            },
592            Self::ContrastColor(ref c) => c.honored_in_forced_colors_mode(allow_transparent),
593        }
594    }
595
596    /// Returns currentcolor value.
597    #[inline]
598    pub fn currentcolor() -> Self {
599        Self::CurrentColor
600    }
601
602    /// Returns transparent value.
603    #[inline]
604    pub fn transparent() -> Self {
605        // We should probably set authored to "transparent", but maybe it doesn't matter.
606        Self::from_absolute_color(AbsoluteColor::TRANSPARENT_BLACK)
607    }
608
609    /// Create a color from an [`AbsoluteColor`].
610    pub fn from_absolute_color(color: AbsoluteColor) -> Self {
611        Color::Absolute(Box::new(Absolute {
612            color,
613            authored: None,
614        }))
615    }
616
617    /// Resolve this Color into an AbsoluteColor if it does not use any of the
618    /// forms that are invalid in an absolute color.
619    ///   https://drafts.csswg.org/css-color-5/#absolute-color
620    /// Returns None if the specified color is not valid as an absolute color.
621    pub fn resolve_to_absolute(&self) -> Option<AbsoluteColor> {
622        use crate::values::specified::percentage::ToPercentage;
623
624        match self {
625            Self::Absolute(c) => Some(c.color),
626            Self::ColorFunction(ref color_function) => color_function.resolve_to_absolute().ok(),
627            Self::ColorMix(ref mix) => {
628                let left = mix.left.resolve_to_absolute()?;
629                let right = mix.right.resolve_to_absolute()?;
630                Some(crate::color::mix::mix(
631                    mix.interpolation,
632                    &left,
633                    mix.left_percentage.to_percentage(),
634                    &right,
635                    mix.right_percentage.to_percentage(),
636                    mix.flags,
637                ))
638            },
639            _ => None,
640        }
641    }
642
643    /// Parse a color, with quirks.
644    ///
645    /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk>
646    pub fn parse_quirky<'i, 't>(
647        context: &ParserContext,
648        input: &mut Parser<'i, 't>,
649        allow_quirks: AllowQuirks,
650    ) -> Result<Self, ParseError<'i>> {
651        input.try_parse(|i| Self::parse(context, i)).or_else(|e| {
652            if !allow_quirks.allowed(context.quirks_mode) {
653                return Err(e);
654            }
655            Color::parse_quirky_color(input).map_err(|_| e)
656        })
657    }
658
659    fn parse_hash<'i>(
660        bytes: &[u8],
661        loc: &cssparser::SourceLocation,
662    ) -> Result<Self, ParseError<'i>> {
663        match cssparser::color::parse_hash_color(bytes) {
664            Ok((r, g, b, a)) => Ok(Self::from_absolute_color(AbsoluteColor::srgb_legacy(
665                r, g, b, a,
666            ))),
667            Err(()) => Err(loc.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
668        }
669    }
670
671    /// Parse a <quirky-color> value.
672    ///
673    /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk>
674    fn parse_quirky_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
675        let location = input.current_source_location();
676        let (value, unit) = match *input.next()? {
677            Token::Number {
678                int_value: Some(integer),
679                ..
680            } => (integer, None),
681            Token::Dimension {
682                int_value: Some(integer),
683                ref unit,
684                ..
685            } => (integer, Some(unit)),
686            Token::Ident(ref ident) => {
687                if ident.len() != 3 && ident.len() != 6 {
688                    return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
689                }
690                return Self::parse_hash(ident.as_bytes(), &location);
691            },
692            ref t => {
693                return Err(location.new_unexpected_token_error(t.clone()));
694            },
695        };
696        if value < 0 {
697            return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
698        }
699        let length = if value <= 9 {
700            1
701        } else if value <= 99 {
702            2
703        } else if value <= 999 {
704            3
705        } else if value <= 9999 {
706            4
707        } else if value <= 99999 {
708            5
709        } else if value <= 999999 {
710            6
711        } else {
712            return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
713        };
714        let total = length + unit.as_ref().map_or(0, |d| d.len());
715        if total > 6 {
716            return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
717        }
718        let mut serialization = [b'0'; 6];
719        let space_padding = 6 - total;
720        let mut written = space_padding;
721        let mut buf = itoa::Buffer::new();
722        let s = buf.format(value);
723        (&mut serialization[written..])
724            .write_all(s.as_bytes())
725            .unwrap();
726        written += s.len();
727        if let Some(unit) = unit {
728            written += (&mut serialization[written..])
729                .write(unit.as_bytes())
730                .unwrap();
731        }
732        debug_assert_eq!(written, 6);
733        Self::parse_hash(&serialization, &location)
734    }
735}
736
737impl Color {
738    /// Converts this Color into a ComputedColor.
739    ///
740    /// If `context` is `None`, and the specified color requires data from
741    /// the context to resolve, then `None` is returned.
742    pub fn to_computed_color(&self, context: Option<&Context>) -> Option<ComputedColor> {
743        macro_rules! adjust_absolute_color {
744            ($color:expr) => {{
745                // Computed lightness values can not be NaN.
746                if matches!(
747                    $color.color_space,
748                    ColorSpace::Lab | ColorSpace::Oklab | ColorSpace::Lch | ColorSpace::Oklch
749                ) {
750                    $color.components.0 = normalize($color.components.0);
751                }
752
753                // Computed RGB and XYZ components can not be NaN.
754                if !$color.is_legacy_syntax() && $color.color_space.is_rgb_or_xyz_like() {
755                    $color.components = $color.components.map(normalize);
756                }
757
758                $color.alpha = normalize($color.alpha);
759            }};
760        }
761
762        Some(match *self {
763            Color::CurrentColor => ComputedColor::CurrentColor,
764            Color::Absolute(ref absolute) => {
765                let mut color = absolute.color;
766                adjust_absolute_color!(color);
767                ComputedColor::Absolute(color)
768            },
769            Color::ColorFunction(ref color_function) => {
770                debug_assert!(color_function.has_origin_color(),
771                    "no need for a ColorFunction if it doesn't contain an unresolvable origin color");
772
773                // Try to eagerly resolve the color function before making it a computed color.
774                if let Ok(absolute) = color_function.resolve_to_absolute() {
775                    ComputedColor::Absolute(absolute)
776                } else {
777                    let color_function = color_function
778                        .map_origin_color(|origin_color| origin_color.to_computed_color(context));
779                    ComputedColor::ColorFunction(Box::new(color_function))
780                }
781            },
782            Color::LightDark(ref ld) => ld.compute(context?),
783            Color::ColorMix(ref mix) => {
784                use crate::values::computed::percentage::Percentage;
785
786                let left = mix.left.to_computed_color(context)?;
787                let right = mix.right.to_computed_color(context)?;
788
789                ComputedColor::from_color_mix(GenericColorMix {
790                    interpolation: mix.interpolation,
791                    left,
792                    left_percentage: Percentage(mix.left_percentage.get()),
793                    right,
794                    right_percentage: Percentage(mix.right_percentage.get()),
795                    flags: mix.flags,
796                })
797            },
798            Color::ContrastColor(ref c) => {
799                ComputedColor::ContrastColor(Box::new(c.to_computed_color(context)?))
800            },
801            #[cfg(feature = "gecko")]
802            Color::System(system) => system.compute(context?),
803            #[cfg(feature = "gecko")]
804            Color::InheritFromBodyQuirk => {
805                ComputedColor::Absolute(context?.device().body_text_color())
806            },
807        })
808    }
809}
810
811impl ToComputedValue for Color {
812    type ComputedValue = ComputedColor;
813
814    fn to_computed_value(&self, context: &Context) -> ComputedColor {
815        self.to_computed_color(Some(context)).unwrap_or_else(|| {
816            debug_assert!(
817                false,
818                "Specified color could not be resolved to a computed color!"
819            );
820            ComputedColor::Absolute(AbsoluteColor::BLACK)
821        })
822    }
823
824    fn from_computed_value(computed: &ComputedColor) -> Self {
825        match *computed {
826            ComputedColor::Absolute(ref color) => Self::from_absolute_color(color.clone()),
827            ComputedColor::ColorFunction(ref color_function) => {
828                let color_function =
829                    color_function.map_origin_color(|o| Some(Self::from_computed_value(o)));
830                Self::ColorFunction(Box::new(color_function))
831            },
832            ComputedColor::CurrentColor => Color::CurrentColor,
833            ComputedColor::ColorMix(ref mix) => {
834                Color::ColorMix(Box::new(ToComputedValue::from_computed_value(&**mix)))
835            },
836            ComputedColor::ContrastColor(ref c) => {
837                Self::ContrastColor(Box::new(ToComputedValue::from_computed_value(&**c)))
838            },
839        }
840    }
841}
842
843impl SpecifiedValueInfo for Color {
844    const SUPPORTED_TYPES: u8 = CssType::COLOR;
845
846    fn collect_completion_keywords(f: KeywordsCollectFn) {
847        // We are not going to insert all the color names here. Caller and
848        // devtools should take care of them. XXX Actually, transparent
849        // should probably be handled that way as well.
850        // XXX `currentColor` should really be `currentcolor`. But let's
851        // keep it consistent with the old system for now.
852        f(&[
853            "currentColor",
854            "transparent",
855            "rgb",
856            "rgba",
857            "hsl",
858            "hsla",
859            "hwb",
860            "color",
861            "lab",
862            "lch",
863            "oklab",
864            "oklch",
865            "color-mix",
866            "contrast-color",
867            "light-dark",
868        ]);
869    }
870}
871
872/// Specified value for the "color" property, which resolves the `currentcolor`
873/// keyword to the parent color instead of self's color.
874#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
875#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)]
876pub struct ColorPropertyValue(pub Color);
877
878impl ToComputedValue for ColorPropertyValue {
879    type ComputedValue = AbsoluteColor;
880
881    #[inline]
882    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
883        let current_color = context.builder.get_parent_inherited_text().clone_color();
884        self.0
885            .to_computed_value(context)
886            .resolve_to_absolute(&current_color)
887    }
888
889    #[inline]
890    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
891        ColorPropertyValue(Color::from_absolute_color(*computed).into())
892    }
893}
894
895impl Parse for ColorPropertyValue {
896    fn parse<'i, 't>(
897        context: &ParserContext,
898        input: &mut Parser<'i, 't>,
899    ) -> Result<Self, ParseError<'i>> {
900        Color::parse_quirky(context, input, AllowQuirks::Yes).map(ColorPropertyValue)
901    }
902}
903
904/// auto | <color>
905pub type ColorOrAuto = GenericColorOrAuto<Color>;
906
907/// caret-color
908pub type CaretColor = GenericCaretColor<Color>;
909
910impl Parse for CaretColor {
911    fn parse<'i, 't>(
912        context: &ParserContext,
913        input: &mut Parser<'i, 't>,
914    ) -> Result<Self, ParseError<'i>> {
915        ColorOrAuto::parse(context, input).map(GenericCaretColor)
916    }
917}
918
919/// Various flags to represent the color-scheme property in an efficient
920/// way.
921#[derive(
922    Clone,
923    Copy,
924    Debug,
925    Default,
926    Eq,
927    MallocSizeOf,
928    PartialEq,
929    SpecifiedValueInfo,
930    ToComputedValue,
931    ToResolvedValue,
932    ToShmem,
933)]
934#[repr(C)]
935#[value_info(other_values = "light,dark,only")]
936pub struct ColorSchemeFlags(u8);
937bitflags! {
938    impl ColorSchemeFlags: u8 {
939        /// Whether the author specified `light`.
940        const LIGHT = 1 << 0;
941        /// Whether the author specified `dark`.
942        const DARK = 1 << 1;
943        /// Whether the author specified `only`.
944        const ONLY = 1 << 2;
945    }
946}
947
948/// <https://drafts.csswg.org/css-color-adjust/#color-scheme-prop>
949#[derive(
950    Clone,
951    Debug,
952    Default,
953    MallocSizeOf,
954    PartialEq,
955    SpecifiedValueInfo,
956    ToComputedValue,
957    ToResolvedValue,
958    ToShmem,
959    ToTyped,
960)]
961#[repr(C)]
962#[value_info(other_values = "normal")]
963pub struct ColorScheme {
964    #[ignore_malloc_size_of = "Arc"]
965    idents: crate::ArcSlice<CustomIdent>,
966    /// The computed bits for the known color schemes (plus the only keyword).
967    pub bits: ColorSchemeFlags,
968}
969
970impl ColorScheme {
971    /// Returns the `normal` value.
972    pub fn normal() -> Self {
973        Self {
974            idents: Default::default(),
975            bits: ColorSchemeFlags::empty(),
976        }
977    }
978
979    /// Returns the raw bitfield.
980    pub fn raw_bits(&self) -> u8 {
981        self.bits.bits()
982    }
983}
984
985impl Parse for ColorScheme {
986    fn parse<'i, 't>(
987        _: &ParserContext,
988        input: &mut Parser<'i, 't>,
989    ) -> Result<Self, ParseError<'i>> {
990        let mut idents = vec![];
991        let mut bits = ColorSchemeFlags::empty();
992
993        let mut location = input.current_source_location();
994        while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
995            let mut is_only = false;
996            match_ignore_ascii_case! { &ident,
997                "normal" => {
998                    if idents.is_empty() && bits.is_empty() {
999                        return Ok(Self::normal());
1000                    }
1001                    return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
1002                },
1003                "light" => bits.insert(ColorSchemeFlags::LIGHT),
1004                "dark" => bits.insert(ColorSchemeFlags::DARK),
1005                "only" => {
1006                    if bits.intersects(ColorSchemeFlags::ONLY) {
1007                        return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
1008                    }
1009                    bits.insert(ColorSchemeFlags::ONLY);
1010                    is_only = true;
1011                },
1012                _ => {},
1013            };
1014
1015            if is_only {
1016                if !idents.is_empty() {
1017                    // Only is allowed either at the beginning or at the end,
1018                    // but not in the middle.
1019                    break;
1020                }
1021            } else {
1022                idents.push(CustomIdent::from_ident(location, &ident, &[])?);
1023            }
1024            location = input.current_source_location();
1025        }
1026
1027        if idents.is_empty() {
1028            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
1029        }
1030
1031        Ok(Self {
1032            idents: crate::ArcSlice::from_iter(idents.into_iter()),
1033            bits,
1034        })
1035    }
1036}
1037
1038impl ToCss for ColorScheme {
1039    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
1040    where
1041        W: Write,
1042    {
1043        if self.idents.is_empty() {
1044            debug_assert!(self.bits.is_empty());
1045            return dest.write_str("normal");
1046        }
1047        let mut first = true;
1048        for ident in self.idents.iter() {
1049            if !first {
1050                dest.write_char(' ')?;
1051            }
1052            first = false;
1053            ident.to_css(dest)?;
1054        }
1055        if self.bits.intersects(ColorSchemeFlags::ONLY) {
1056            dest.write_str(" only")?;
1057        }
1058        Ok(())
1059    }
1060}
1061
1062/// https://drafts.csswg.org/css-color-adjust/#print-color-adjust
1063#[derive(
1064    Clone,
1065    Copy,
1066    Debug,
1067    MallocSizeOf,
1068    Parse,
1069    PartialEq,
1070    SpecifiedValueInfo,
1071    ToCss,
1072    ToComputedValue,
1073    ToResolvedValue,
1074    ToShmem,
1075    ToTyped,
1076)]
1077#[repr(u8)]
1078pub enum PrintColorAdjust {
1079    /// Ignore backgrounds and darken text.
1080    Economy,
1081    /// Respect specified colors.
1082    Exact,
1083}
1084
1085/// https://drafts.csswg.org/css-color-adjust-1/#forced-color-adjust-prop
1086#[derive(
1087    Clone,
1088    Copy,
1089    Debug,
1090    MallocSizeOf,
1091    Parse,
1092    PartialEq,
1093    SpecifiedValueInfo,
1094    ToCss,
1095    ToComputedValue,
1096    ToResolvedValue,
1097    ToShmem,
1098    ToTyped,
1099)]
1100#[repr(u8)]
1101pub enum ForcedColorAdjust {
1102    /// Adjust colors if needed.
1103    Auto,
1104    /// Respect specified colors.
1105    None,
1106}
1107
1108/// Possible values for the forced-colors media query.
1109/// <https://drafts.csswg.org/mediaqueries-5/#forced-colors>
1110#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)]
1111#[repr(u8)]
1112pub enum ForcedColors {
1113    /// Page colors are not being forced.
1114    None,
1115    /// Page colors would be forced in content.
1116    #[parse(condition = "ParserContext::chrome_rules_enabled")]
1117    Requested,
1118    /// Page colors are being forced.
1119    Active,
1120}
1121
1122impl ForcedColors {
1123    /// Returns whether forced-colors is active for this page.
1124    pub fn is_active(self) -> bool {
1125        matches!(self, Self::Active)
1126    }
1127}