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