Skip to main content

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