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