/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Specified color values. use super::AllowQuirks; use crate::color::mix::ColorInterpolationMethod; use crate::color::{parsing, AbsoluteColor, ColorSpace}; use crate::media_queries::Device; use crate::parser::{Parse, ParserContext}; use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; use crate::values::generics::color::{ ColorMixFlags, GenericCaretColor, GenericColorMix, GenericColorOrAuto, }; use crate::values::specified::Percentage; use crate::values::{normalize, CustomIdent}; use cssparser::{BasicParseErrorKind, ParseErrorKind, Parser, Token}; use std::fmt::{self, Write}; use std::io::Write as IoWrite; use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind}; use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind}; /// A specified color-mix(). pub type ColorMix = GenericColorMix; impl ColorMix { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, preserve_authored: PreserveAuthored, ) -> Result> { input.expect_function_matching("color-mix")?; input.parse_nested_block(|input| { let interpolation = ColorInterpolationMethod::parse(context, input)?; input.expect_comma()?; let try_parse_percentage = |input: &mut Parser| -> Option { input .try_parse(|input| Percentage::parse_zero_to_a_hundred(context, input)) .ok() }; let mut left_percentage = try_parse_percentage(input); let left = Color::parse_internal(context, input, preserve_authored)?; if left_percentage.is_none() { left_percentage = try_parse_percentage(input); } input.expect_comma()?; let mut right_percentage = try_parse_percentage(input); let right = Color::parse_internal(context, input, preserve_authored)?; if right_percentage.is_none() { right_percentage = try_parse_percentage(input); } let right_percentage = right_percentage .unwrap_or_else(|| Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get()))); let left_percentage = left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get())); if left_percentage.get() + right_percentage.get() <= 0.0 { // If the percentages sum to zero, the function is invalid. return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } // Pass RESULT_IN_MODERN_SYNTAX here, because the result of the color-mix() function // should always be in the modern color syntax to allow for out of gamut results and // to preserve floating point precision. Ok(ColorMix { interpolation, left, left_percentage, right, right_percentage, flags: ColorMixFlags::NORMALIZE_WEIGHTS | ColorMixFlags::RESULT_IN_MODERN_SYNTAX, }) }) } } /// Container holding an absolute color and the text specified by an author. #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub struct Absolute { /// The specified color. pub color: AbsoluteColor, /// Authored representation. pub authored: Option>, } impl ToCss for Absolute { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { if let Some(ref authored) = self.authored { dest.write_str(authored) } else { self.color.to_css(dest) } } } /// Specified color value #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub enum Color { /// The 'currentColor' keyword CurrentColor, /// An absolute color. /// https://w3c.github.io/csswg-drafts/css-color-4/#typedef-absolute-color-function Absolute(Box), /// A system color. #[cfg(feature = "gecko")] System(SystemColor), /// A color mix. ColorMix(Box), /// A light-dark() color. LightDark(Box), /// Quirksmode-only rule for inheriting color from the body #[cfg(feature = "gecko")] InheritFromBodyQuirk, } /// A light-dark(, ) function. #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, ToCss)] #[css(function, comma)] pub struct LightDark { /// The that is returned when using a light theme. pub light: Color, /// The that is returned when using a dark theme. pub dark: Color, } impl LightDark { fn compute(&self, cx: &Context) -> ComputedColor { let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme(); let dark = cx.device().is_dark_color_scheme(&style_color_scheme); let used = if dark { &self.dark } else { &self.light }; used.to_computed_value(cx) } fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, preserve_authored: PreserveAuthored, ) -> Result> { let enabled = context.chrome_rules_enabled() || static_prefs::pref!("layout.css.light-dark.enabled"); if !enabled { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } input.expect_function_matching("light-dark")?; input.parse_nested_block(|input| { let light = Color::parse_internal(context, input, preserve_authored)?; input.expect_comma()?; let dark = Color::parse_internal(context, input, preserve_authored)?; Ok(LightDark { light, dark }) }) } } impl From for Color { #[inline] fn from(value: AbsoluteColor) -> Self { Self::from_absolute_color(value) } } /// System colors. A bunch of these are ad-hoc, others come from Windows: /// /// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor /// /// Others are HTML/CSS specific. Spec is: /// /// https://drafts.csswg.org/css-color/#css-system-colors /// https://drafts.csswg.org/css-color/#deprecated-system-colors #[allow(missing_docs)] #[cfg(feature = "gecko")] #[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] #[repr(u8)] pub enum SystemColor { Activeborder, /// Background in the (active) titlebar. Activecaption, Appworkspace, Background, Buttonface, Buttonhighlight, Buttonshadow, Buttontext, Buttonborder, /// Text color in the (active) titlebar. Captiontext, #[parse(aliases = "-moz-field")] Field, /// Used for disabled field backgrounds. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozDisabledfield, #[parse(aliases = "-moz-fieldtext")] Fieldtext, Mark, Marktext, /// Combobox widgets MozComboboxtext, MozCombobox, Graytext, Highlight, Highlighttext, Inactiveborder, /// Background in the (inactive) titlebar. Inactivecaption, /// Text color in the (inactive) titlebar. Inactivecaptiontext, Infobackground, Infotext, Menu, Menutext, Scrollbar, Threeddarkshadow, Threedface, Threedhighlight, Threedlightshadow, Threedshadow, Window, Windowframe, Windowtext, #[parse(aliases = "-moz-default-color")] Canvastext, #[parse(aliases = "-moz-default-background-color")] Canvas, MozDialog, MozDialogtext, /// Used for selected but not focused cell backgrounds. #[parse(aliases = "-moz-html-cellhighlight")] MozCellhighlight, /// Used for selected but not focused cell text. #[parse(aliases = "-moz-html-cellhighlighttext")] MozCellhighlighttext, /// Used for selected and focused html cell backgrounds. Selecteditem, /// Used for selected and focused html cell text. Selecteditemtext, /// Used to button text background when hovered. MozButtonhoverface, /// Used to button text color when hovered. MozButtonhovertext, /// Used for menu item backgrounds when hovered. MozMenuhover, /// Used for menu item backgrounds when hovered and disabled. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozMenuhoverdisabled, /// Used for menu item text when hovered. MozMenuhovertext, /// Used for menubar item text when hovered. MozMenubarhovertext, /// On platforms where these colors are the same as -moz-field, use /// -moz-fieldtext as foreground color MozEventreerow, MozOddtreerow, /// Used for button text when pressed. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozButtonactivetext, /// Used for button background when pressed. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozButtonactiveface, /// Used for button background when disabled. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozButtondisabledface, /// Colors used for the header bar (sorta like the tab bar / menubar). #[parse(condition = "ParserContext::chrome_rules_enabled")] MozHeaderbar, #[parse(condition = "ParserContext::chrome_rules_enabled")] MozHeaderbartext, #[parse(condition = "ParserContext::chrome_rules_enabled")] MozHeaderbarinactive, #[parse(condition = "ParserContext::chrome_rules_enabled")] MozHeaderbarinactivetext, /// Foreground color of default buttons. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozMacDefaultbuttontext, /// Ring color around text fields and lists. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozMacFocusring, /// Text color of disabled text on toolbars. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozMacDisabledtoolbartext, /// The background of a sidebar. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozSidebar, /// The foreground color of a sidebar. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozSidebartext, /// The border color of a sidebar. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozSidebarborder, /// Theme accent color. /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolor Accentcolor, /// Foreground for the accent color. /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolortext Accentcolortext, /// The background-color for :autofill-ed inputs. #[parse(condition = "ParserContext::chrome_rules_enabled")] MozAutofillBackground, /// Hyperlink color extracted from the system, not affected by the browser.anchor_color user /// pref. /// /// There is no OS-specified safe background color for this text, but it is used regularly /// within Windows and the Gnome DE on Dialog and Window colors. #[css(skip)] MozNativehyperlinktext, /// As above, but visited link color. #[css(skip)] MozNativevisitedhyperlinktext, #[parse(aliases = "-moz-hyperlinktext")] Linktext, #[parse(aliases = "-moz-activehyperlinktext")] Activetext, #[parse(aliases = "-moz-visitedhyperlinktext")] Visitedtext, /// Color of tree column headers #[parse(condition = "ParserContext::chrome_rules_enabled")] MozColheader, #[parse(condition = "ParserContext::chrome_rules_enabled")] MozColheadertext, #[parse(condition = "ParserContext::chrome_rules_enabled")] MozColheaderhover, #[parse(condition = "ParserContext::chrome_rules_enabled")] MozColheaderhovertext, #[parse(condition = "ParserContext::chrome_rules_enabled")] MozColheaderactive, #[parse(condition = "ParserContext::chrome_rules_enabled")] MozColheaderactivetext, #[parse(condition = "ParserContext::chrome_rules_enabled")] TextSelectDisabledBackground, #[css(skip)] TextSelectAttentionBackground, #[css(skip)] TextSelectAttentionForeground, #[css(skip)] TextHighlightBackground, #[css(skip)] TextHighlightForeground, #[css(skip)] IMERawInputBackground, #[css(skip)] IMERawInputForeground, #[css(skip)] IMERawInputUnderline, #[css(skip)] IMESelectedRawTextBackground, #[css(skip)] IMESelectedRawTextForeground, #[css(skip)] IMESelectedRawTextUnderline, #[css(skip)] IMEConvertedTextBackground, #[css(skip)] IMEConvertedTextForeground, #[css(skip)] IMEConvertedTextUnderline, #[css(skip)] IMESelectedConvertedTextBackground, #[css(skip)] IMESelectedConvertedTextForeground, #[css(skip)] IMESelectedConvertedTextUnderline, #[css(skip)] SpellCheckerUnderline, #[css(skip)] ThemedScrollbar, #[css(skip)] ThemedScrollbarInactive, #[css(skip)] ThemedScrollbarThumb, #[css(skip)] ThemedScrollbarThumbHover, #[css(skip)] ThemedScrollbarThumbActive, #[css(skip)] ThemedScrollbarThumbInactive, #[css(skip)] End, // Just for array-indexing purposes. } #[cfg(feature = "gecko")] impl SystemColor { #[inline] fn compute(&self, cx: &Context) -> ComputedColor { use crate::gecko::values::convert_nscolor_to_absolute_color; use crate::gecko_bindings::bindings; // TODO: We should avoid cloning here most likely, though it's cheap-ish. let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme(); let color = cx.device().system_nscolor(*self, &style_color_scheme); if color == bindings::NS_SAME_AS_FOREGROUND_COLOR { return ComputedColor::currentcolor(); } ComputedColor::Absolute(convert_nscolor_to_absolute_color(color)) } } /// Whether to preserve authored colors during parsing. That's useful only if we /// plan to serialize the color back. #[derive(Copy, Clone)] enum PreserveAuthored { No, Yes, } impl Parse for Color { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Self::parse_internal(context, input, PreserveAuthored::Yes) } } impl Color { fn parse_internal<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, preserve_authored: PreserveAuthored, ) -> Result> { let authored = match preserve_authored { PreserveAuthored::No => None, PreserveAuthored::Yes => { // Currently we only store authored value for color keywords, // because all browsers serialize those values as keywords for // specified value. let start = input.state(); let authored = input.expect_ident_cloned().ok(); input.reset(&start); authored }, }; match input.try_parse(|i| parsing::parse_color_with(context, i)) { Ok(mut color) => { if let Color::Absolute(ref mut absolute) = color { // Because we can't set the `authored` value at construction time, we have to set it // here. absolute.authored = authored.map(|s| s.to_ascii_lowercase().into_boxed_str()); } Ok(color) }, Err(e) => { #[cfg(feature = "gecko")] { if let Ok(system) = input.try_parse(|i| SystemColor::parse(context, i)) { return Ok(Color::System(system)); } } if let Ok(mix) = input.try_parse(|i| ColorMix::parse(context, i, preserve_authored)) { return Ok(Color::ColorMix(Box::new(mix))); } if let Ok(ld) = input.try_parse(|i| LightDark::parse(context, i, preserve_authored)) { return Ok(Color::LightDark(Box::new(ld))); } match e.kind { ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => { Err(e.location.new_custom_error(StyleParseErrorKind::ValueError( ValueParseErrorKind::InvalidColor(t), ))) }, _ => Err(e), } }, } } /// Returns whether a given color is valid for authors. pub fn is_valid(context: &ParserContext, input: &mut Parser) -> bool { input .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No)) .is_ok() } /// Tries to parse a color and compute it with a given device. pub fn parse_and_compute( context: &ParserContext, input: &mut Parser, device: Option<&Device>, ) -> Option { use crate::error_reporting::ContextualParseError; let start = input.position(); let result = input .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No)); let specified = match result { Ok(s) => s, Err(e) => { if !context.error_reporting_enabled() { return None; } // Ignore other kinds of errors that might be reported, such as // ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken), // since Gecko didn't use to report those to the error console. // // TODO(emilio): Revise whether we want to keep this at all, we // use this only for canvas, this warnings are disabled by // default and not available on OffscreenCanvas anyways... if let ParseErrorKind::Custom(StyleParseErrorKind::ValueError(..)) = e.kind { let location = e.location.clone(); let error = ContextualParseError::UnsupportedValue(input.slice_from(start), e); context.log_css_error(location, error); } return None; }, }; match device { Some(device) => { Context::for_media_query_evaluation(device, device.quirks_mode(), |context| { specified.to_computed_color(Some(&context)) }) }, None => specified.to_computed_color(None), } } } impl ToCss for Color { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { match *self { Color::CurrentColor => dest.write_str("currentcolor"), Color::Absolute(ref absolute) => absolute.to_css(dest), Color::ColorMix(ref mix) => mix.to_css(dest), Color::LightDark(ref ld) => ld.to_css(dest), #[cfg(feature = "gecko")] Color::System(system) => system.to_css(dest), #[cfg(feature = "gecko")] Color::InheritFromBodyQuirk => Ok(()), } } } impl Color { /// Returns whether this color is allowed in forced-colors mode. pub fn honored_in_forced_colors_mode(&self, allow_transparent: bool) -> bool { match *self { Self::InheritFromBodyQuirk => false, Self::CurrentColor | Color::System(..) => true, Self::Absolute(ref absolute) => allow_transparent && absolute.color.is_transparent(), Self::LightDark(ref ld) => { ld.light.honored_in_forced_colors_mode(allow_transparent) && ld.dark.honored_in_forced_colors_mode(allow_transparent) }, Self::ColorMix(ref mix) => { mix.left.honored_in_forced_colors_mode(allow_transparent) && mix.right.honored_in_forced_colors_mode(allow_transparent) }, } } /// Returns currentcolor value. #[inline] pub fn currentcolor() -> Self { Self::CurrentColor } /// Returns transparent value. #[inline] pub fn transparent() -> Self { // We should probably set authored to "transparent", but maybe it doesn't matter. Self::from_absolute_color(AbsoluteColor::TRANSPARENT_BLACK) } /// Create a color from an [`AbsoluteColor`]. pub fn from_absolute_color(color: AbsoluteColor) -> Self { Color::Absolute(Box::new(Absolute { color, authored: None, })) } /// Parse a color, with quirks. /// /// pub fn parse_quirky<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, allow_quirks: AllowQuirks, ) -> Result> { input.try_parse(|i| Self::parse(context, i)).or_else(|e| { if !allow_quirks.allowed(context.quirks_mode) { return Err(e); } Color::parse_quirky_color(input).map_err(|_| e) }) } fn parse_hash<'i>( bytes: &[u8], loc: &cssparser::SourceLocation, ) -> Result> { match cssparser::color::parse_hash_color(bytes) { Ok((r, g, b, a)) => Ok(Self::from_absolute_color(AbsoluteColor::srgb_legacy( r, g, b, a, ))), Err(()) => Err(loc.new_custom_error(StyleParseErrorKind::UnspecifiedError)), } } /// Parse a value. /// /// fn parse_quirky_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { let location = input.current_source_location(); let (value, unit) = match *input.next()? { Token::Number { int_value: Some(integer), .. } => (integer, None), Token::Dimension { int_value: Some(integer), ref unit, .. } => (integer, Some(unit)), Token::Ident(ref ident) => { if ident.len() != 3 && ident.len() != 6 { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } return Self::parse_hash(ident.as_bytes(), &location); }, ref t => { return Err(location.new_unexpected_token_error(t.clone())); }, }; if value < 0 { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let length = if value <= 9 { 1 } else if value <= 99 { 2 } else if value <= 999 { 3 } else if value <= 9999 { 4 } else if value <= 99999 { 5 } else if value <= 999999 { 6 } else { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); }; let total = length + unit.as_ref().map_or(0, |d| d.len()); if total > 6 { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let mut serialization = [b'0'; 6]; let space_padding = 6 - total; let mut written = space_padding; let mut buf = itoa::Buffer::new(); let s = buf.format(value); (&mut serialization[written..]) .write_all(s.as_bytes()) .unwrap(); written += s.len(); if let Some(unit) = unit { written += (&mut serialization[written..]) .write(unit.as_bytes()) .unwrap(); } debug_assert_eq!(written, 6); Self::parse_hash(&serialization, &location) } } impl Color { /// Converts this Color into a ComputedColor. /// /// If `context` is `None`, and the specified color requires data from /// the context to resolve, then `None` is returned. pub fn to_computed_color(&self, context: Option<&Context>) -> Option { Some(match *self { Color::CurrentColor => ComputedColor::CurrentColor, Color::Absolute(ref absolute) => { let mut color = absolute.color; // Computed lightness values can not be NaN. if matches!( color.color_space, ColorSpace::Lab | ColorSpace::Oklab | ColorSpace::Lch | ColorSpace::Oklch ) { color.components.0 = normalize(color.components.0); } // Computed RGB and XYZ components can not be NaN. if !color.is_legacy_syntax() && color.color_space.is_rgb_or_xyz_like() { color.components = color.components.map(normalize); } color.alpha = normalize(color.alpha); ComputedColor::Absolute(color) }, Color::LightDark(ref ld) => ld.compute(context?), Color::ColorMix(ref mix) => { use crate::values::computed::percentage::Percentage; let left = mix.left.to_computed_color(context)?; let right = mix.right.to_computed_color(context)?; ComputedColor::from_color_mix(GenericColorMix { interpolation: mix.interpolation, left, left_percentage: Percentage(mix.left_percentage.get()), right, right_percentage: Percentage(mix.right_percentage.get()), flags: mix.flags, }) }, #[cfg(feature = "gecko")] Color::System(system) => system.compute(context?), #[cfg(feature = "gecko")] Color::InheritFromBodyQuirk => { ComputedColor::Absolute(context?.device().body_text_color()) }, }) } } impl ToComputedValue for Color { type ComputedValue = ComputedColor; fn to_computed_value(&self, context: &Context) -> ComputedColor { self.to_computed_color(Some(context)).unwrap() } fn from_computed_value(computed: &ComputedColor) -> Self { match *computed { ComputedColor::Absolute(ref color) => Self::from_absolute_color(color.clone()), ComputedColor::CurrentColor => Color::CurrentColor, ComputedColor::ColorMix(ref mix) => { Color::ColorMix(Box::new(ToComputedValue::from_computed_value(&**mix))) }, } } } impl SpecifiedValueInfo for Color { const SUPPORTED_TYPES: u8 = CssType::COLOR; fn collect_completion_keywords(f: KeywordsCollectFn) { // We are not going to insert all the color names here. Caller and // devtools should take care of them. XXX Actually, transparent // should probably be handled that way as well. // XXX `currentColor` should really be `currentcolor`. But let's // keep it consistent with the old system for now. f(&[ "rgb", "rgba", "hsl", "hsla", "hwb", "currentColor", "transparent", "color-mix", "color", "lab", "lch", "oklab", "oklch", ]); } } /// Specified value for the "color" property, which resolves the `currentcolor` /// keyword to the parent color instead of self's color. #[cfg_attr(feature = "gecko", derive(MallocSizeOf))] #[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] pub struct ColorPropertyValue(pub Color); impl ToComputedValue for ColorPropertyValue { type ComputedValue = AbsoluteColor; #[inline] fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { let current_color = context.builder.get_parent_inherited_text().clone_color(); self.0 .to_computed_value(context) .resolve_to_absolute(¤t_color) } #[inline] fn from_computed_value(computed: &Self::ComputedValue) -> Self { ColorPropertyValue(Color::from_absolute_color(*computed).into()) } } impl Parse for ColorPropertyValue { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Color::parse_quirky(context, input, AllowQuirks::Yes).map(ColorPropertyValue) } } /// auto | pub type ColorOrAuto = GenericColorOrAuto; /// caret-color pub type CaretColor = GenericCaretColor; impl Parse for CaretColor { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { ColorOrAuto::parse(context, input).map(GenericCaretColor) } } /// Various flags to represent the color-scheme property in an efficient /// way. #[derive( Clone, Copy, Debug, Default, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(C)] #[value_info(other_values = "light,dark,only")] pub struct ColorSchemeFlags(u8); bitflags! { impl ColorSchemeFlags: u8 { /// Whether the author specified `light`. const LIGHT = 1 << 0; /// Whether the author specified `dark`. const DARK = 1 << 1; /// Whether the author specified `only`. const ONLY = 1 << 2; } } /// #[derive( Clone, Debug, Default, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(C)] #[value_info(other_values = "normal")] pub struct ColorScheme { #[ignore_malloc_size_of = "Arc"] idents: crate::ArcSlice, bits: ColorSchemeFlags, } impl ColorScheme { /// Returns the `normal` value. pub fn normal() -> Self { Self { idents: Default::default(), bits: ColorSchemeFlags::empty(), } } /// Returns the raw bitfield. pub fn raw_bits(&self) -> u8 { self.bits.bits() } } impl Parse for ColorScheme { fn parse<'i, 't>( _: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let mut idents = vec![]; let mut bits = ColorSchemeFlags::empty(); let mut location = input.current_source_location(); while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { let mut is_only = false; match_ignore_ascii_case! { &ident, "normal" => { if idents.is_empty() && bits.is_empty() { return Ok(Self::normal()); } return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); }, "light" => bits.insert(ColorSchemeFlags::LIGHT), "dark" => bits.insert(ColorSchemeFlags::DARK), "only" => { if bits.intersects(ColorSchemeFlags::ONLY) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } bits.insert(ColorSchemeFlags::ONLY); is_only = true; }, _ => {}, }; if is_only { if !idents.is_empty() { // Only is allowed either at the beginning or at the end, // but not in the middle. break; } } else { idents.push(CustomIdent::from_ident(location, &ident, &[])?); } location = input.current_source_location(); } if idents.is_empty() { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(Self { idents: crate::ArcSlice::from_iter(idents.into_iter()), bits, }) } } impl ToCss for ColorScheme { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { if self.idents.is_empty() { debug_assert!(self.bits.is_empty()); return dest.write_str("normal"); } let mut first = true; for ident in self.idents.iter() { if !first { dest.write_char(' ')?; } first = false; ident.to_css(dest)?; } if self.bits.intersects(ColorSchemeFlags::ONLY) { dest.write_str(" only")?; } Ok(()) } } /// https://drafts.csswg.org/css-color-adjust/#print-color-adjust #[derive( Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum PrintColorAdjust { /// Ignore backgrounds and darken text. Economy, /// Respect specified colors. Exact, } /// https://drafts.csswg.org/css-color-adjust-1/#forced-color-adjust-prop #[derive( Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum ForcedColorAdjust { /// Adjust colors if needed. Auto, /// Respect specified colors. None, }