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