From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- servo/components/style/values/specified/color.rs | 1175 ++++++++++++++++++++++ 1 file changed, 1175 insertions(+) create mode 100644 servo/components/style/values/specified/color.rs (limited to 'servo/components/style/values/specified/color.rs') diff --git a/servo/components/style/values/specified/color.rs b/servo/components/style/values/specified/color.rs new file mode 100644 index 0000000000..3a19a2f4a3 --- /dev/null +++ b/servo/components/style/values/specified/color.rs @@ -0,0 +1,1175 @@ +/* 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::parsing::{ + self, AngleOrNumber, Color as CSSParserColor, FromParsedColor, NumberOrPercentage, +}; +use crate::color::{mix::ColorInterpolationMethod, 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::calc::CalcNode; +use crate::values::specified::Percentage; +use crate::values::{normalize, CustomIdent}; +use cssparser::{color::PredefinedColorSpace, BasicParseErrorKind, ParseErrorKind, Parser, Token}; +use itoa; +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)) + } +} + +impl FromParsedColor for Color { + fn from_current_color() -> Self { + Color::CurrentColor + } + + fn from_rgba(r: u8, g: u8, b: u8, a: f32) -> Self { + AbsoluteColor::srgb_legacy(r, g, b, a).into() + } + + fn from_hsl( + hue: Option, + saturation: Option, + lightness: Option, + alpha: Option, + ) -> Self { + AbsoluteColor::new(ColorSpace::Hsl, hue, saturation, lightness, alpha).into() + } + + fn from_hwb( + hue: Option, + whiteness: Option, + blackness: Option, + alpha: Option, + ) -> Self { + AbsoluteColor::new(ColorSpace::Hwb, hue, whiteness, blackness, alpha).into() + } + + fn from_lab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + AbsoluteColor::new(ColorSpace::Lab, lightness, a, b, alpha).into() + } + + fn from_lch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + AbsoluteColor::new(ColorSpace::Lch, lightness, chroma, hue, alpha).into() + } + + fn from_oklab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + AbsoluteColor::new(ColorSpace::Oklab, lightness, a, b, alpha).into() + } + + fn from_oklch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + AbsoluteColor::new(ColorSpace::Oklch, lightness, chroma, hue, alpha).into() + } + + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, + ) -> Self { + AbsoluteColor::new(color_space.into(), c1, c2, c3, alpha).into() + } +} + +struct ColorParser<'a, 'b: 'a>(&'a ParserContext<'b>); +impl<'a, 'b: 'a, 'i: 'a> parsing::ColorParser<'i> for ColorParser<'a, 'b> { + type Output = Color; + type Error = StyleParseErrorKind<'i>; + + fn parse_angle_or_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + use crate::values::specified::Angle; + + let location = input.current_source_location(); + let token = input.next()?.clone(); + match token { + Token::Dimension { + value, ref unit, .. + } => { + let angle = Angle::parse_dimension(value, unit, /* from_calc = */ false); + + let degrees = match angle { + Ok(angle) => angle.degrees(), + Err(()) => return Err(location.new_unexpected_token_error(token.clone())), + }; + + Ok(AngleOrNumber::Angle { degrees }) + }, + Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }), + Token::Function(ref name) => { + let function = CalcNode::math_function(self.0, name, location)?; + CalcNode::parse_angle_or_number(self.0, input, function) + }, + t => return Err(location.new_unexpected_token_error(t)), + } + } + + fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result> { + Ok(Percentage::parse(self.0, input)?.get()) + } + + fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result> { + use crate::values::specified::Number; + + Ok(Number::parse(self.0, input)?.get()) + } + + fn parse_number_or_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + + match *input.next()? { + Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }), + Token::Percentage { unit_value, .. } => { + Ok(NumberOrPercentage::Percentage { unit_value }) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(self.0, name, location)?; + CalcNode::parse_number_or_percentage(self.0, input, function) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +/// 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 + }, + }; + + let color_parser = ColorParser(&*context); + match input.try_parse(|i| parsing::parse_color_with(&color_parser, 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 => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest), + 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_rgba(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, +} -- cgit v1.2.3