diff options
Diffstat (limited to 'servo/components/style')
32 files changed, 1332 insertions, 1343 deletions
diff --git a/servo/components/style/build_gecko.rs b/servo/components/style/build_gecko.rs index a83c5dbc6d..1574f7b6b1 100644 --- a/servo/components/style/build_gecko.rs +++ b/servo/components/style/build_gecko.rs @@ -127,17 +127,7 @@ impl BuilderExt for Builder { .size_t_is_usize(true) .disable_untagged_union(); - let rustfmt_path = env::var_os("RUSTFMT") - // This can be replaced with - // > .filter(|p| !p.is_empty()).map(PathBuf::from) - // once we can use 1.27+. - .and_then(|p| { - if p.is_empty() { - None - } else { - Some(PathBuf::from(p)) - } - }); + let rustfmt_path = env::var_os("RUSTFMT").filter(|p| !p.is_empty()).map(PathBuf::from); if let Some(path) = rustfmt_path { builder = builder.with_rustfmt(path); } diff --git a/servo/components/style/color/component.rs b/servo/components/style/color/component.rs new file mode 100644 index 0000000000..9f101a460c --- /dev/null +++ b/servo/components/style/color/component.rs @@ -0,0 +1,48 @@ +/* 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/. */ + +//! Parse/serialize and resolve a single color component. + +/// A single color component. +#[derive(Clone, MallocSizeOf, PartialEq, ToShmem)] +pub enum ColorComponent<ValueType> { + /// The "none" keyword. + None, + /// A absolute value. + Value(ValueType), +} + +impl<ValueType> ColorComponent<ValueType> { + /// Return true if the component is "none". + #[inline] + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + /// If the component contains a value, map it to another value. + pub fn map_value<OutType>( + self, + f: impl FnOnce(ValueType) -> OutType, + ) -> ColorComponent<OutType> { + match self { + Self::None => ColorComponent::None, + Self::Value(value) => ColorComponent::Value(f(value)), + } + } + /// Return the component as its value. + pub fn into_value(self) -> ValueType { + match self { + Self::None => panic!("value not available when component is None"), + Self::Value(value) => value, + } + } + + /// Return the component as its value or a default value. + pub fn into_value_or(self, default: ValueType) -> ValueType { + match self { + Self::None => default, + Self::Value(value) => value, + } + } +} diff --git a/servo/components/style/color/mod.rs b/servo/components/style/color/mod.rs index 797a1cb00f..6f35daaa8a 100644 --- a/servo/components/style/color/mod.rs +++ b/servo/components/style/color/mod.rs @@ -4,14 +4,15 @@ //! Color support functions. +pub mod component; /// cbindgen:ignore pub mod convert; pub mod mix; pub mod parsing; +mod to_css; +use component::ColorComponent; use cssparser::color::PredefinedColorSpace; -use std::fmt::{self, Write}; -use style_traits::{CssWriter, ToCss}; /// The 3 components that make up a color. (Does not include the alpha component) #[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] @@ -260,6 +261,22 @@ impl From<Option<f32>> for ComponentDetails { } } +impl From<ColorComponent<f32>> for ComponentDetails { + fn from(value: ColorComponent<f32>) -> Self { + if let ColorComponent::Value(value) = value { + Self { + value, + is_none: false, + } + } else { + Self { + value: 0.0, + is_none: true, + } + } + } +} + impl AbsoluteColor { /// A fully transparent color in the legacy syntax. pub const TRANSPARENT_BLACK: Self = Self { @@ -540,74 +557,3 @@ impl From<PredefinedColorSpace> for ColorSpace { } } } - -impl ToCss for AbsoluteColor { - fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result - where - W: Write, - { - match self.color_space { - ColorSpace::Srgb if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) => { - // The "none" keyword is not supported in the rgb/rgba legacy syntax. - cssparser::ToCss::to_css( - &parsing::RgbaLegacy::from_floats( - self.components.0, - self.components.1, - self.components.2, - self.alpha, - ), - dest, - ) - }, - ColorSpace::Hsl | ColorSpace::Hwb => self.into_srgb_legacy().to_css(dest), - ColorSpace::Lab => cssparser::ToCss::to_css( - &parsing::Lab::new(self.c0(), self.c1(), self.c2(), self.alpha()), - dest, - ), - ColorSpace::Lch => cssparser::ToCss::to_css( - &parsing::Lch::new(self.c0(), self.c1(), self.c2(), self.alpha()), - dest, - ), - ColorSpace::Oklab => cssparser::ToCss::to_css( - &parsing::Oklab::new(self.c0(), self.c1(), self.c2(), self.alpha()), - dest, - ), - ColorSpace::Oklch => cssparser::ToCss::to_css( - &parsing::Oklch::new(self.c0(), self.c1(), self.c2(), self.alpha()), - dest, - ), - _ => { - let color_space = match self.color_space { - ColorSpace::Srgb => { - debug_assert!( - !self.flags.contains(ColorFlags::IS_LEGACY_SRGB), - "legacy srgb is not a color function" - ); - PredefinedColorSpace::Srgb - }, - ColorSpace::SrgbLinear => PredefinedColorSpace::SrgbLinear, - ColorSpace::DisplayP3 => PredefinedColorSpace::DisplayP3, - ColorSpace::A98Rgb => PredefinedColorSpace::A98Rgb, - ColorSpace::ProphotoRgb => PredefinedColorSpace::ProphotoRgb, - ColorSpace::Rec2020 => PredefinedColorSpace::Rec2020, - ColorSpace::XyzD50 => PredefinedColorSpace::XyzD50, - ColorSpace::XyzD65 => PredefinedColorSpace::XyzD65, - - _ => { - unreachable!("other color spaces do not support color() syntax") - }, - }; - - let color_function = parsing::ColorFunction { - color_space, - c1: self.c0(), - c2: self.c1(), - c3: self.c2(), - alpha: self.alpha(), - }; - let color = parsing::Color::ColorFunction(color_function); - cssparser::ToCss::to_css(&color, dest) - }, - } - } -} diff --git a/servo/components/style/color/parsing.rs b/servo/components/style/color/parsing.rs index f60b44c5b6..1ae32c4ad7 100644 --- a/servo/components/style/color/parsing.rs +++ b/servo/components/style/color/parsing.rs @@ -8,21 +8,20 @@ //! Relative colors, color-mix, system colors, and other such things require better calc() support //! and integration. -use super::{ - convert::{hsl_to_rgb, hwb_to_rgb, normalize_hue}, - ColorComponents, -}; -use crate::values::normalize; +use crate::color::component::ColorComponent; use cssparser::color::{ - clamp_floor_256_f32, clamp_unit_f32, parse_hash_color, serialize_color_alpha, - PredefinedColorSpace, OPAQUE, + clamp_floor_256_f32, clamp_unit_f32, parse_hash_color, PredefinedColorSpace, OPAQUE, }; -use cssparser::{match_ignore_ascii_case, CowRcStr, ParseError, Parser, ToCss, Token}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::f32::consts::PI; -use std::fmt; +use cssparser::{match_ignore_ascii_case, CowRcStr, Parser, Token}; use std::str::FromStr; +use style_traits::{ParseError, StyleParseErrorKind}; + +impl From<u8> for ColorComponent<u8> { + #[inline] + fn from(value: u8) -> Self { + ColorComponent::Value(value) + } +} /// Return the named color with the given name. /// @@ -34,13 +33,23 @@ pub fn parse_color_keyword<Output>(ident: &str) -> Result<Output, ()> where Output: FromParsedColor, { - Ok(match_ignore_ascii_case! { ident , - "transparent" => Output::from_rgba(0, 0, 0, 0.0), + Ok(match_ignore_ascii_case! { ident, + "transparent" => Output::from_rgba( + 0u8.into(), + 0u8.into(), + 0u8.into(), + ColorComponent::Value(NumberOrPercentage::Number { value: 0.0 }), + ), "currentcolor" => Output::from_current_color(), _ => { let (r, g, b) = cssparser::color::parse_named_color(ident)?; - Output::from_rgba(r, g, b, OPAQUE) - } + Output::from_rgba( + r.into(), + g.into(), + b.into(), + ColorComponent::Value(NumberOrPercentage::Number { value: OPAQUE }), + ) + }, }) } @@ -49,16 +58,22 @@ where pub fn parse_color_with<'i, 't, P>( color_parser: &P, input: &mut Parser<'i, 't>, -) -> Result<P::Output, ParseError<'i, P::Error>> +) -> Result<P::Output, ParseError<'i>> where P: ColorParser<'i>, { let location = input.current_source_location(); let token = input.next()?; match *token { - Token::Hash(ref value) | Token::IDHash(ref value) => { - parse_hash_color(value.as_bytes()).map(|(r, g, b, a)| P::Output::from_rgba(r, g, b, a)) - }, + Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes()) + .map(|(r, g, b, a)| { + P::Output::from_rgba( + r.into(), + g.into(), + b.into(), + ColorComponent::Value(NumberOrPercentage::Number { value: a }), + ) + }), Token::Ident(ref value) => parse_color_keyword(value), Token::Function(ref name) => { let name = name.clone(); @@ -77,35 +92,19 @@ fn parse_color_function<'i, 't, P>( color_parser: &P, name: CowRcStr<'i>, arguments: &mut Parser<'i, 't>, -) -> Result<P::Output, ParseError<'i, P::Error>> +) -> Result<P::Output, ParseError<'i>> where P: ColorParser<'i>, { let color = match_ignore_ascii_case! { &name, "rgb" | "rgba" => parse_rgb(color_parser, arguments), - "hsl" | "hsla" => parse_hsl(color_parser, arguments), - "hwb" => parse_hwb(color_parser, arguments), - - // for L: 0% = 0.0, 100% = 100.0 - // for a and b: -100% = -125, 100% = 125 - "lab" => parse_lab_like(color_parser, arguments, 100.0, 125.0, P::Output::from_lab), - - // for L: 0% = 0.0, 100% = 100.0 - // for C: 0% = 0, 100% = 150 - "lch" => parse_lch_like(color_parser, arguments, 100.0, 150.0, P::Output::from_lch), - - // for L: 0% = 0.0, 100% = 1.0 - // for a and b: -100% = -0.4, 100% = 0.4 - "oklab" => parse_lab_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklab), - - // for L: 0% = 0.0, 100% = 1.0 - // for C: 0% = 0.0 100% = 0.4 - "oklch" => parse_lch_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklch), - + "lab" => parse_lab_like(color_parser, arguments, P::Output::from_lab), + "lch" => parse_lch_like(color_parser, arguments, P::Output::from_lch), + "oklab" => parse_lab_like(color_parser, arguments, P::Output::from_oklab), + "oklch" => parse_lch_like(color_parser, arguments, P::Output::from_oklch), "color" => parse_color_with_color_space(color_parser, arguments), - _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))), }?; @@ -114,50 +113,47 @@ where Ok(color) } -/// Parse the alpha component by itself from either number or percentage, -/// clipping the result to [0.0..1.0]. -#[inline] -fn parse_alpha_component<'i, 't, P>( - color_parser: &P, - arguments: &mut Parser<'i, 't>, -) -> Result<f32, ParseError<'i, P::Error>> -where - P: ColorParser<'i>, -{ - // Percent reference range for alpha: 0% = 0.0, 100% = 1.0 - let alpha = color_parser - .parse_number_or_percentage(arguments)? - .to_number(1.0); - Ok(normalize(alpha).clamp(0.0, OPAQUE)) -} - fn parse_legacy_alpha<'i, 't, P>( color_parser: &P, arguments: &mut Parser<'i, 't>, -) -> Result<f32, ParseError<'i, P::Error>> +) -> Result<ColorComponent<NumberOrPercentage>, ParseError<'i>> where P: ColorParser<'i>, { - Ok(if !arguments.is_exhausted() { + if !arguments.is_exhausted() { arguments.expect_comma()?; - parse_alpha_component(color_parser, arguments)? + color_parser.parse_number_or_percentage(arguments, false) } else { - OPAQUE - }) + Ok(ColorComponent::Value(NumberOrPercentage::Number { + value: OPAQUE, + })) + } } fn parse_modern_alpha<'i, 't, P>( color_parser: &P, arguments: &mut Parser<'i, 't>, -) -> Result<Option<f32>, ParseError<'i, P::Error>> +) -> Result<ColorComponent<NumberOrPercentage>, ParseError<'i>> where P: ColorParser<'i>, { if !arguments.is_exhausted() { arguments.expect_delim('/')?; - parse_none_or(arguments, |p| parse_alpha_component(color_parser, p)) + color_parser.parse_number_or_percentage(arguments, true) } else { - Ok(Some(OPAQUE)) + Ok(ColorComponent::Value(NumberOrPercentage::Number { + value: OPAQUE, + })) + } +} + +impl ColorComponent<NumberOrPercentage> { + /// Return true if the component contains a percentage. + pub fn is_percentage(&self) -> Result<bool, ()> { + Ok(match self { + Self::Value(NumberOrPercentage::Percentage { .. }) => true, + _ => false, + }) } } @@ -165,58 +161,58 @@ where fn parse_rgb<'i, 't, P>( color_parser: &P, arguments: &mut Parser<'i, 't>, -) -> Result<P::Output, ParseError<'i, P::Error>> +) -> Result<P::Output, ParseError<'i>> where P: ColorParser<'i>, { - let maybe_red = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?; + let location = arguments.current_source_location(); + + let maybe_red = color_parser.parse_number_or_percentage(arguments, true)?; // If the first component is not "none" and is followed by a comma, then we // are parsing the legacy syntax. - let is_legacy_syntax = maybe_red.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); + let is_legacy_syntax = + !maybe_red.is_none() && arguments.try_parse(|p| p.expect_comma()).is_ok(); let (red, green, blue, alpha) = if is_legacy_syntax { - let (red, green, blue) = match maybe_red.unwrap() { - NumberOrPercentage::Number { value } => { - let red = clamp_floor_256_f32(value); - let green = clamp_floor_256_f32(color_parser.parse_number(arguments)?); - arguments.expect_comma()?; - let blue = clamp_floor_256_f32(color_parser.parse_number(arguments)?); - (red, green, blue) - }, - NumberOrPercentage::Percentage { unit_value } => { - let red = clamp_unit_f32(unit_value); - let green = clamp_unit_f32(color_parser.parse_percentage(arguments)?); - arguments.expect_comma()?; - let blue = clamp_unit_f32(color_parser.parse_percentage(arguments)?); - (red, green, blue) - }, + let Ok(is_percentage) = maybe_red.is_percentage() else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }; + let (red, green, blue) = if is_percentage { + let red = maybe_red.map_value(|v| clamp_unit_f32(v.to_number(1.0))); + let green = color_parser + .parse_percentage(arguments, false)? + .map_value(clamp_unit_f32); + arguments.expect_comma()?; + let blue = color_parser + .parse_percentage(arguments, false)? + .map_value(clamp_unit_f32); + (red, green, blue) + } else { + let red = maybe_red.map_value(|v| clamp_floor_256_f32(v.to_number(255.0))); + let green = color_parser + .parse_number(arguments, false)? + .map_value(clamp_floor_256_f32); + arguments.expect_comma()?; + let blue = color_parser + .parse_number(arguments, false)? + .map_value(clamp_floor_256_f32); + (red, green, blue) }; let alpha = parse_legacy_alpha(color_parser, arguments)?; (red, green, blue, alpha) } else { - #[inline] - fn get_component_value(c: Option<NumberOrPercentage>) -> u8 { - c.map(|c| match c { - NumberOrPercentage::Number { value } => clamp_floor_256_f32(value), - NumberOrPercentage::Percentage { unit_value } => clamp_unit_f32(unit_value), - }) - .unwrap_or(0) - } - - let red = get_component_value(maybe_red); + let red = maybe_red.map_value(|v| clamp_floor_256_f32(v.to_number(255.0))); + let green = color_parser + .parse_number_or_percentage(arguments, true)? + .map_value(|v| clamp_floor_256_f32(v.to_number(255.0))); + let blue = color_parser + .parse_number_or_percentage(arguments, true)? + .map_value(|v| clamp_floor_256_f32(v.to_number(255.0))); - let green = get_component_value(parse_none_or(arguments, |p| { - color_parser.parse_number_or_percentage(p) - })?); - - let blue = get_component_value(parse_none_or(arguments, |p| { - color_parser.parse_number_or_percentage(p) - })?); - - let alpha = parse_modern_alpha(color_parser, arguments)?.unwrap_or(0.0); + let alpha = parse_modern_alpha(color_parser, arguments)?; (red, green, blue, alpha) }; @@ -231,40 +227,39 @@ where fn parse_hsl<'i, 't, P>( color_parser: &P, arguments: &mut Parser<'i, 't>, -) -> Result<P::Output, ParseError<'i, P::Error>> +) -> Result<P::Output, ParseError<'i>> where P: ColorParser<'i>, { - // Percent reference range for S and L: 0% = 0.0, 100% = 100.0 - const LIGHTNESS_RANGE: f32 = 100.0; - const SATURATION_RANGE: f32 = 100.0; - - let maybe_hue = parse_none_or(arguments, |p| color_parser.parse_angle_or_number(p))?; + let hue = color_parser.parse_number_or_angle(arguments, true)?; // If the hue is not "none" and is followed by a comma, then we are parsing // the legacy syntax. - let is_legacy_syntax = maybe_hue.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); - - let saturation: Option<f32>; - let lightness: Option<f32>; + let is_legacy_syntax = !hue.is_none() && arguments.try_parse(|p| p.expect_comma()).is_ok(); - let alpha = if is_legacy_syntax { - saturation = Some(color_parser.parse_percentage(arguments)? * SATURATION_RANGE); + let (saturation, lightness, alpha) = if is_legacy_syntax { + let saturation = color_parser + .parse_percentage(arguments, false)? + .map_value(|unit_value| NumberOrPercentage::Percentage { unit_value }); arguments.expect_comma()?; - lightness = Some(color_parser.parse_percentage(arguments)? * LIGHTNESS_RANGE); - Some(parse_legacy_alpha(color_parser, arguments)?) + let lightness = color_parser + .parse_percentage(arguments, false)? + .map_value(|unit_value| NumberOrPercentage::Percentage { unit_value }); + ( + saturation, + lightness, + parse_legacy_alpha(color_parser, arguments)?, + ) } else { - saturation = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))? - .map(|v| v.to_number(SATURATION_RANGE)); - lightness = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))? - .map(|v| v.to_number(LIGHTNESS_RANGE)); - parse_modern_alpha(color_parser, arguments)? + let saturation = color_parser.parse_number_or_percentage(arguments, true)?; + let lightness = color_parser.parse_number_or_percentage(arguments, true)?; + ( + saturation, + lightness, + parse_modern_alpha(color_parser, arguments)?, + ) }; - let hue = maybe_hue.map(|h| normalize_hue(h.degrees())); - let saturation = saturation.map(|s| s.clamp(0.0, SATURATION_RANGE)); - let lightness = lightness.map(|l| l.clamp(0.0, LIGHTNESS_RANGE)); - Ok(P::Output::from_hsl(hue, saturation, lightness, alpha)) } @@ -275,40 +270,34 @@ where fn parse_hwb<'i, 't, P>( color_parser: &P, arguments: &mut Parser<'i, 't>, -) -> Result<P::Output, ParseError<'i, P::Error>> +) -> Result<P::Output, ParseError<'i>> where P: ColorParser<'i>, { - // Percent reference range for W and B: 0% = 0.0, 100% = 100.0 - const WHITENESS_RANGE: f32 = 100.0; - const BLACKNESS_RANGE: f32 = 100.0; - let (hue, whiteness, blackness, alpha) = parse_components( color_parser, arguments, - P::parse_angle_or_number, + P::parse_number_or_angle, P::parse_number_or_percentage, P::parse_number_or_percentage, )?; - let hue = hue.map(|h| normalize_hue(h.degrees())); - let whiteness = whiteness.map(|w| w.to_number(WHITENESS_RANGE).clamp(0.0, WHITENESS_RANGE)); - let blackness = blackness.map(|b| b.to_number(BLACKNESS_RANGE).clamp(0.0, BLACKNESS_RANGE)); - Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha)) } -type IntoColorFn<Output> = - fn(l: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) -> Output; +type IntoLabFn<Output> = fn( + l: ColorComponent<NumberOrPercentage>, + a: ColorComponent<NumberOrPercentage>, + b: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, +) -> Output; #[inline] fn parse_lab_like<'i, 't, P>( color_parser: &P, arguments: &mut Parser<'i, 't>, - lightness_range: f32, - a_b_range: f32, - into_color: IntoColorFn<P::Output>, -) -> Result<P::Output, ParseError<'i, P::Error>> + into_color: IntoLabFn<P::Output>, +) -> Result<P::Output, ParseError<'i>> where P: ColorParser<'i>, { @@ -320,21 +309,22 @@ where P::parse_number_or_percentage, )?; - let lightness = lightness.map(|l| l.to_number(lightness_range)); - let a = a.map(|a| a.to_number(a_b_range)); - let b = b.map(|b| b.to_number(a_b_range)); - Ok(into_color(lightness, a, b, alpha)) } +type IntoLchFn<Output> = fn( + l: ColorComponent<NumberOrPercentage>, + a: ColorComponent<NumberOrPercentage>, + b: ColorComponent<NumberOrAngle>, + alpha: ColorComponent<NumberOrPercentage>, +) -> Output; + #[inline] fn parse_lch_like<'i, 't, P>( color_parser: &P, arguments: &mut Parser<'i, 't>, - lightness_range: f32, - chroma_range: f32, - into_color: IntoColorFn<P::Output>, -) -> Result<P::Output, ParseError<'i, P::Error>> + into_color: IntoLchFn<P::Output>, +) -> Result<P::Output, ParseError<'i>> where P: ColorParser<'i>, { @@ -343,13 +333,9 @@ where arguments, P::parse_number_or_percentage, P::parse_number_or_percentage, - P::parse_angle_or_number, + P::parse_number_or_angle, )?; - let lightness = lightness.map(|l| l.to_number(lightness_range)); - let chroma = chroma.map(|c| c.to_number(chroma_range)); - let hue = hue.map(|h| normalize_hue(h.degrees())); - Ok(into_color(lightness, chroma, hue, alpha)) } @@ -358,7 +344,7 @@ where fn parse_color_with_color_space<'i, 't, P>( color_parser: &P, arguments: &mut Parser<'i, 't>, -) -> Result<P::Output, ParseError<'i, P::Error>> +) -> Result<P::Output, ParseError<'i>> where P: ColorParser<'i>, { @@ -378,10 +364,6 @@ where P::parse_number_or_percentage, )?; - let c1 = c1.map(|c| c.to_number(1.0)); - let c2 = c2.map(|c| c.to_number(1.0)); - let c3 = c3.map(|c| c.to_number(1.0)); - Ok(P::Output::from_color_function( color_space, c1, @@ -391,8 +373,15 @@ where )) } -type ComponentParseResult<'i, R1, R2, R3, Error> = - Result<(Option<R1>, Option<R2>, Option<R3>, Option<f32>), ParseError<'i, Error>>; +type ComponentParseResult<'i, R1, R2, R3> = Result< + ( + ColorComponent<R1>, + ColorComponent<R2>, + ColorComponent<R3>, + ColorComponent<NumberOrPercentage>, + ), + ParseError<'i>, +>; /// Parse the color components and alpha with the modern [color-4] syntax. pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>( @@ -401,567 +390,22 @@ pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>( f1: F1, f2: F2, f3: F3, -) -> ComponentParseResult<'i, R1, R2, R3, P::Error> +) -> ComponentParseResult<'i, R1, R2, R3> where P: ColorParser<'i>, - F1: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R1, ParseError<'i, P::Error>>, - F2: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R2, ParseError<'i, P::Error>>, - F3: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R3, ParseError<'i, P::Error>>, + F1: FnOnce(&P, &mut Parser<'i, 't>, bool) -> Result<ColorComponent<R1>, ParseError<'i>>, + F2: FnOnce(&P, &mut Parser<'i, 't>, bool) -> Result<ColorComponent<R2>, ParseError<'i>>, + F3: FnOnce(&P, &mut Parser<'i, 't>, bool) -> Result<ColorComponent<R3>, ParseError<'i>>, { - let r1 = parse_none_or(input, |p| f1(color_parser, p))?; - let r2 = parse_none_or(input, |p| f2(color_parser, p))?; - let r3 = parse_none_or(input, |p| f3(color_parser, p))?; + let r1 = f1(color_parser, input, true)?; + let r2 = f2(color_parser, input, true)?; + let r3 = f3(color_parser, input, true)?; let alpha = parse_modern_alpha(color_parser, input)?; Ok((r1, r2, r3, alpha)) } -fn parse_none_or<'i, 't, F, T, E>(input: &mut Parser<'i, 't>, thing: F) -> Result<Option<T>, E> -where - F: FnOnce(&mut Parser<'i, 't>) -> Result<T, E>, -{ - match input.try_parse(|p| p.expect_ident_matching("none")) { - Ok(_) => Ok(None), - Err(_) => Ok(Some(thing(input)?)), - } -} - -/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and -/// floating point values. -struct ModernComponent<'a>(&'a Option<f32>); - -impl<'a> ToCss for ModernComponent<'a> { - fn to_css<W>(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - if let Some(value) = self.0 { - if value.is_finite() { - value.to_css(dest) - } else if value.is_nan() { - dest.write_str("calc(NaN)") - } else { - debug_assert!(value.is_infinite()); - if value.is_sign_negative() { - dest.write_str("calc(-infinity)") - } else { - dest.write_str("calc(infinity)") - } - } - } else { - dest.write_str("none") - } - } -} - -/// A color with red, green, blue, and alpha components, in a byte each. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct RgbaLegacy { - /// The red component. - pub red: u8, - /// The green component. - pub green: u8, - /// The blue component. - pub blue: u8, - /// The alpha component. - pub alpha: f32, -} - -impl RgbaLegacy { - /// Constructs a new RGBA value from float components. It expects the red, - /// green, blue and alpha channels in that order, and all values will be - /// clamped to the 0.0 ... 1.0 range. - #[inline] - pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self { - Self::new( - clamp_unit_f32(red), - clamp_unit_f32(green), - clamp_unit_f32(blue), - alpha.clamp(0.0, OPAQUE), - ) - } - - /// Same thing, but with `u8` values instead of floats in the 0 to 1 range. - #[inline] - pub const fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self { - Self { - red, - green, - blue, - alpha, - } - } -} - -#[cfg(feature = "serde")] -impl Serialize for RgbaLegacy { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - (self.red, self.green, self.blue, self.alpha).serialize(serializer) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for RgbaLegacy { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let (r, g, b, a) = Deserialize::deserialize(deserializer)?; - Ok(RgbaLegacy::new(r, g, b, a)) - } -} - -impl ToCss for RgbaLegacy { - fn to_css<W>(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - let has_alpha = self.alpha != OPAQUE; - - dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?; - self.red.to_css(dest)?; - dest.write_str(", ")?; - self.green.to_css(dest)?; - dest.write_str(", ")?; - self.blue.to_css(dest)?; - - // Legacy syntax does not allow none components. - serialize_color_alpha(dest, Some(self.alpha), true)?; - - dest.write_char(')') - } -} - -/// Color specified by hue, saturation and lightness components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Hsl { - /// The hue component. - pub hue: Option<f32>, - /// The saturation component. - pub saturation: Option<f32>, - /// The lightness component. - pub lightness: Option<f32>, - /// The alpha component. - pub alpha: Option<f32>, -} - -impl Hsl { - /// Construct a new HSL color from it's components. - pub fn new( - hue: Option<f32>, - saturation: Option<f32>, - lightness: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Self { - hue, - saturation, - lightness, - alpha, - } - } -} - -impl ToCss for Hsl { - fn to_css<W>(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - // HSL serializes to RGB, so we have to convert it. - let ColorComponents(red, green, blue) = hsl_to_rgb(&ColorComponents( - self.hue.unwrap_or(0.0) / 360.0, - self.saturation.unwrap_or(0.0), - self.lightness.unwrap_or(0.0), - )); - - RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) - } -} - -#[cfg(feature = "serde")] -impl Serialize for Hsl { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - (self.hue, self.saturation, self.lightness, self.alpha).serialize(serializer) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for Hsl { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; - Ok(Self::new(lightness, a, b, alpha)) - } -} - -/// Color specified by hue, whiteness and blackness components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Hwb { - /// The hue component. - pub hue: Option<f32>, - /// The whiteness component. - pub whiteness: Option<f32>, - /// The blackness component. - pub blackness: Option<f32>, - /// The alpha component. - pub alpha: Option<f32>, -} - -impl Hwb { - /// Construct a new HWB color from it's components. - pub fn new( - hue: Option<f32>, - whiteness: Option<f32>, - blackness: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Self { - hue, - whiteness, - blackness, - alpha, - } - } -} - -impl ToCss for Hwb { - fn to_css<W>(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - // HWB serializes to RGB, so we have to convert it. - let ColorComponents(red, green, blue) = hwb_to_rgb(&ColorComponents( - self.hue.unwrap_or(0.0) / 360.0, - self.whiteness.unwrap_or(0.0), - self.blackness.unwrap_or(0.0), - )); - - RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) - } -} - -#[cfg(feature = "serde")] -impl Serialize for Hwb { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - (self.hue, self.whiteness, self.blackness, self.alpha).serialize(serializer) - } -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for Hwb { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let (lightness, whiteness, blackness, alpha) = Deserialize::deserialize(deserializer)?; - Ok(Self::new(lightness, whiteness, blackness, alpha)) - } -} - -// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro, -// because it causes cbindgen to ignore them. - -/// Color specified by lightness, a- and b-axis components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Lab { - /// The lightness component. - pub lightness: Option<f32>, - /// The a-axis component. - pub a: Option<f32>, - /// The b-axis component. - pub b: Option<f32>, - /// The alpha component. - pub alpha: Option<f32>, -} - -/// Color specified by lightness, a- and b-axis components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Oklab { - /// The lightness component. - pub lightness: Option<f32>, - /// The a-axis component. - pub a: Option<f32>, - /// The b-axis component. - pub b: Option<f32>, - /// The alpha component. - pub alpha: Option<f32>, -} - -macro_rules! impl_lab_like { - ($cls:ident, $fname:literal) => { - impl $cls { - /// Construct a new Lab color format with lightness, a, b and alpha components. - pub fn new( - lightness: Option<f32>, - a: Option<f32>, - b: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Self { - lightness, - a, - b, - alpha, - } - } - } - - #[cfg(feature = "serde")] - impl Serialize for $cls { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - (self.lightness, self.a, self.b, self.alpha).serialize(serializer) - } - } - - #[cfg(feature = "serde")] - impl<'de> Deserialize<'de> for $cls { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; - Ok(Self::new(lightness, a, b, alpha)) - } - } - - impl ToCss for $cls { - fn to_css<W>(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - dest.write_str($fname)?; - dest.write_str("(")?; - ModernComponent(&self.lightness).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.a).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.b).to_css(dest)?; - serialize_color_alpha(dest, self.alpha, false)?; - dest.write_char(')') - } - } - }; -} - -impl_lab_like!(Lab, "lab"); -impl_lab_like!(Oklab, "oklab"); - -// NOTE: LCH and OKLCH is not declared inside the [impl_lch_like] macro, -// because it causes cbindgen to ignore them. - -/// Color specified by lightness, chroma and hue components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Lch { - /// The lightness component. - pub lightness: Option<f32>, - /// The chroma component. - pub chroma: Option<f32>, - /// The hue component. - pub hue: Option<f32>, - /// The alpha component. - pub alpha: Option<f32>, -} - -/// Color specified by lightness, chroma and hue components. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct Oklch { - /// The lightness component. - pub lightness: Option<f32>, - /// The chroma component. - pub chroma: Option<f32>, - /// The hue component. - pub hue: Option<f32>, - /// The alpha component. - pub alpha: Option<f32>, -} - -macro_rules! impl_lch_like { - ($cls:ident, $fname:literal) => { - impl $cls { - /// Construct a new color with lightness, chroma and hue components. - pub fn new( - lightness: Option<f32>, - chroma: Option<f32>, - hue: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Self { - lightness, - chroma, - hue, - alpha, - } - } - } - - #[cfg(feature = "serde")] - impl Serialize for $cls { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - (self.lightness, self.chroma, self.hue, self.alpha).serialize(serializer) - } - } - - #[cfg(feature = "serde")] - impl<'de> Deserialize<'de> for $cls { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let (lightness, chroma, hue, alpha) = Deserialize::deserialize(deserializer)?; - Ok(Self::new(lightness, chroma, hue, alpha)) - } - } - - impl ToCss for $cls { - fn to_css<W>(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - dest.write_str($fname)?; - dest.write_str("(")?; - ModernComponent(&self.lightness).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.chroma).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.hue).to_css(dest)?; - serialize_color_alpha(dest, self.alpha, false)?; - dest.write_char(')') - } - } - }; -} - -impl_lch_like!(Lch, "lch"); -impl_lch_like!(Oklch, "oklch"); - -/// A color specified by the color() function. -/// <https://drafts.csswg.org/css-color-4/#color-function> -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct ColorFunction { - /// The color space for this color. - pub color_space: PredefinedColorSpace, - /// The first component of the color. Either red or x. - pub c1: Option<f32>, - /// The second component of the color. Either green or y. - pub c2: Option<f32>, - /// The third component of the color. Either blue or z. - pub c3: Option<f32>, - /// The alpha component of the color. - pub alpha: Option<f32>, -} - -impl ColorFunction { - /// Construct a new color function definition with the given color space and - /// color components. - pub fn new( - color_space: PredefinedColorSpace, - c1: Option<f32>, - c2: Option<f32>, - c3: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Self { - color_space, - c1, - c2, - c3, - alpha, - } - } -} - -impl ToCss for ColorFunction { - fn to_css<W>(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - dest.write_str("color(")?; - self.color_space.to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.c1).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.c2).to_css(dest)?; - dest.write_char(' ')?; - ModernComponent(&self.c3).to_css(dest)?; - - serialize_color_alpha(dest, self.alpha, false)?; - - dest.write_char(')') - } -} - -/// Describes one of the value <color> values according to the CSS -/// specification. -/// -/// Most components are `Option<_>`, so when the value is `None`, that component -/// serializes to the "none" keyword. -/// -/// <https://drafts.csswg.org/css-color-4/#color-type> -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum Color { - /// The 'currentcolor' keyword. - CurrentColor, - /// Specify sRGB colors directly by their red/green/blue/alpha chanels. - Rgba(RgbaLegacy), - /// Specifies a color in sRGB using hue, saturation and lightness components. - Hsl(Hsl), - /// Specifies a color in sRGB using hue, whiteness and blackness components. - Hwb(Hwb), - /// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue - /// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB - /// rectangular coordinate model. - Lab(Lab), - /// Specifies a CIELAB color by CIE Lightness, Chroma, and hue using the - /// CIE LCH cylindrical coordinate model. - Lch(Lch), - /// Specifies an Oklab color by Oklab Lightness and its a- and b-axis hue - /// coordinates (red/green-ness, and yellow/blue-ness) using the Oklab - /// rectangular coordinate model. - Oklab(Oklab), - /// Specifies an Oklab color by Oklab Lightness, Chroma, and hue using - /// the OKLCH cylindrical coordinate model. - Oklch(Oklch), - /// Specifies a color in a predefined color space. - ColorFunction(ColorFunction), -} - -impl ToCss for Color { - fn to_css<W>(&self, dest: &mut W) -> fmt::Result - where - W: fmt::Write, - { - match *self { - Color::CurrentColor => dest.write_str("currentcolor"), - Color::Rgba(rgba) => rgba.to_css(dest), - Color::Hsl(hsl) => hsl.to_css(dest), - Color::Hwb(hwb) => hwb.to_css(dest), - Color::Lab(lab) => lab.to_css(dest), - Color::Lch(lch) => lch.to_css(dest), - Color::Oklab(lab) => lab.to_css(dest), - Color::Oklch(lch) => lch.to_css(dest), - Color::ColorFunction(color_function) => color_function.to_css(dest), - } - } -} - /// Either a number or a percentage. pub enum NumberOrPercentage { /// `<number>`. @@ -989,7 +433,7 @@ impl NumberOrPercentage { } /// Either an angle or a number. -pub enum AngleOrNumber { +pub enum NumberOrAngle { /// `<number>`. Number { /// The numeric value parsed, as a float. @@ -1002,13 +446,13 @@ pub enum AngleOrNumber { }, } -impl AngleOrNumber { - /// Return the angle in degrees. `AngleOrNumber::Number` is returned as +impl NumberOrAngle { + /// Return the angle in degrees. `NumberOrAngle::Number` is returned as /// degrees, because it is the canonical unit. pub fn degrees(&self) -> f32 { match *self { - AngleOrNumber::Number { value } => value, - AngleOrNumber::Angle { degrees } => degrees, + Self::Number { value } => value, + Self::Angle { degrees } => degrees, } } } @@ -1021,37 +465,14 @@ pub trait ColorParser<'i> { /// The type that the parser will construct on a successful parse. type Output: FromParsedColor; - /// A custom error type that can be returned from the parsing functions. - type Error: 'i; - /// Parse an `<angle>` or `<number>`. /// /// Returns the result in degrees. - fn parse_angle_or_number<'t>( + fn parse_number_or_angle<'t>( &self, input: &mut Parser<'i, 't>, - ) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> { - let location = input.current_source_location(); - Ok(match *input.next()? { - Token::Number { value, .. } => AngleOrNumber::Number { value }, - Token::Dimension { - value: v, ref unit, .. - } => { - let degrees = match_ignore_ascii_case! { unit, - "deg" => v, - "grad" => v * 360. / 400., - "rad" => v * 360. / (2. * PI), - "turn" => v * 360., - _ => { - return Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))) - } - }; - - AngleOrNumber::Angle { degrees } - }, - ref t => return Err(location.new_unexpected_token_error(t.clone())), - }) - } + allow_none: bool, + ) -> Result<ColorComponent<NumberOrAngle>, ParseError<'i>>; /// Parse a `<percentage>` value. /// @@ -1059,47 +480,22 @@ pub trait ColorParser<'i> { fn parse_percentage<'t>( &self, input: &mut Parser<'i, 't>, - ) -> Result<f32, ParseError<'i, Self::Error>> { - input.expect_percentage().map_err(From::from) - } + allow_none: bool, + ) -> Result<ColorComponent<f32>, ParseError<'i>>; /// Parse a `<number>` value. fn parse_number<'t>( &self, input: &mut Parser<'i, 't>, - ) -> Result<f32, ParseError<'i, Self::Error>> { - input.expect_number().map_err(From::from) - } + allow_none: bool, + ) -> Result<ColorComponent<f32>, ParseError<'i>>; /// Parse a `<number>` value or a `<percentage>` value. fn parse_number_or_percentage<'t>( &self, input: &mut Parser<'i, 't>, - ) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> { - let location = input.current_source_location(); - Ok(match *input.next()? { - Token::Number { value, .. } => NumberOrPercentage::Number { value }, - Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value }, - ref t => return Err(location.new_unexpected_token_error(t.clone())), - }) - } -} - -/// Default implementation of a [`ColorParser`] -pub struct DefaultColorParser; - -impl<'i> ColorParser<'i> for DefaultColorParser { - type Output = Color; - type Error = (); -} - -impl Color { - /// Parse a <color> value, per CSS Color Module Level 3. - /// - /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet. - pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i, ()>> { - parse_color_with(&DefaultColorParser, input) - } + allow_none: bool, + ) -> Result<ColorComponent<NumberOrPercentage>, ParseError<'i>>; } /// This trait is used by the [`ColorParser`] to construct colors of any type. @@ -1108,139 +504,67 @@ pub trait FromParsedColor { fn from_current_color() -> Self; /// Construct a new color from red, green, blue and alpha components. - fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self; + fn from_rgba( + red: ColorComponent<u8>, + green: ColorComponent<u8>, + blue: ColorComponent<u8>, + alpha: ColorComponent<NumberOrPercentage>, + ) -> Self; /// Construct a new color from hue, saturation, lightness and alpha components. fn from_hsl( - hue: Option<f32>, - saturation: Option<f32>, - lightness: Option<f32>, - alpha: Option<f32>, + hue: ColorComponent<NumberOrAngle>, + saturation: ColorComponent<NumberOrPercentage>, + lightness: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self; /// Construct a new color from hue, blackness, whiteness and alpha components. fn from_hwb( - hue: Option<f32>, - whiteness: Option<f32>, - blackness: Option<f32>, - alpha: Option<f32>, + hue: ColorComponent<NumberOrAngle>, + whiteness: ColorComponent<NumberOrPercentage>, + blackness: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self; /// Construct a new color from the `lab` notation. - fn from_lab(lightness: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) - -> Self; + fn from_lab( + lightness: ColorComponent<NumberOrPercentage>, + a: ColorComponent<NumberOrPercentage>, + b: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, + ) -> Self; /// Construct a new color from the `lch` notation. fn from_lch( - lightness: Option<f32>, - chroma: Option<f32>, - hue: Option<f32>, - alpha: Option<f32>, + lightness: ColorComponent<NumberOrPercentage>, + chroma: ColorComponent<NumberOrPercentage>, + hue: ColorComponent<NumberOrAngle>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self; /// Construct a new color from the `oklab` notation. fn from_oklab( - lightness: Option<f32>, - a: Option<f32>, - b: Option<f32>, - alpha: Option<f32>, + lightness: ColorComponent<NumberOrPercentage>, + a: ColorComponent<NumberOrPercentage>, + b: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self; /// Construct a new color from the `oklch` notation. fn from_oklch( - lightness: Option<f32>, - chroma: Option<f32>, - hue: Option<f32>, - alpha: Option<f32>, + lightness: ColorComponent<NumberOrPercentage>, + chroma: ColorComponent<NumberOrPercentage>, + hue: ColorComponent<NumberOrAngle>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self; /// Construct a new color with a predefined color space. fn from_color_function( color_space: PredefinedColorSpace, - c1: Option<f32>, - c2: Option<f32>, - c3: Option<f32>, - alpha: Option<f32>, + c1: ColorComponent<NumberOrPercentage>, + c2: ColorComponent<NumberOrPercentage>, + c3: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self; } - -impl FromParsedColor for Color { - #[inline] - fn from_current_color() -> Self { - Color::CurrentColor - } - - #[inline] - fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self { - Color::Rgba(RgbaLegacy::new(red, green, blue, alpha)) - } - - fn from_hsl( - hue: Option<f32>, - saturation: Option<f32>, - lightness: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Color::Hsl(Hsl::new(hue, saturation, lightness, alpha)) - } - - fn from_hwb( - hue: Option<f32>, - blackness: Option<f32>, - whiteness: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Color::Hwb(Hwb::new(hue, blackness, whiteness, alpha)) - } - - #[inline] - fn from_lab( - lightness: Option<f32>, - a: Option<f32>, - b: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Color::Lab(Lab::new(lightness, a, b, alpha)) - } - - #[inline] - fn from_lch( - lightness: Option<f32>, - chroma: Option<f32>, - hue: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Color::Lch(Lch::new(lightness, chroma, hue, alpha)) - } - - #[inline] - fn from_oklab( - lightness: Option<f32>, - a: Option<f32>, - b: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Color::Oklab(Oklab::new(lightness, a, b, alpha)) - } - - #[inline] - fn from_oklch( - lightness: Option<f32>, - chroma: Option<f32>, - hue: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Color::Oklch(Oklch::new(lightness, chroma, hue, alpha)) - } - - #[inline] - fn from_color_function( - color_space: PredefinedColorSpace, - c1: Option<f32>, - c2: Option<f32>, - c3: Option<f32>, - alpha: Option<f32>, - ) -> Self { - Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha)) - } -} diff --git a/servo/components/style/color/to_css.rs b/servo/components/style/color/to_css.rs new file mode 100644 index 0000000000..350d8386f1 --- /dev/null +++ b/servo/components/style/color/to_css.rs @@ -0,0 +1,251 @@ +/* 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/. */ + +//! Write colors into CSS strings. + +use super::{AbsoluteColor, ColorFlags, ColorSpace}; +use crate::values::normalize; +use cssparser::color::{clamp_unit_f32, serialize_color_alpha, OPAQUE}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and +/// floating point values. +struct ModernComponent<'a>(&'a Option<f32>); + +impl<'a> ToCss for ModernComponent<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + if let Some(value) = self.0 { + if value.is_finite() { + value.to_css(dest) + } else if value.is_nan() { + dest.write_str("calc(NaN)") + } else { + debug_assert!(value.is_infinite()); + if value.is_sign_negative() { + dest.write_str("calc(-infinity)") + } else { + dest.write_str("calc(infinity)") + } + } + } else { + dest.write_str("none") + } + } +} + +impl ToCss for AbsoluteColor { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match self.color_space { + ColorSpace::Srgb if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) => { + // The "none" keyword is not supported in the rgb/rgba legacy syntax. + let has_alpha = self.alpha != OPAQUE; + + dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?; + clamp_unit_f32(self.components.0).to_css(dest)?; + dest.write_str(", ")?; + clamp_unit_f32(self.components.1).to_css(dest)?; + dest.write_str(", ")?; + clamp_unit_f32(self.components.2).to_css(dest)?; + + // Legacy syntax does not allow none components. + serialize_color_alpha(dest, Some(self.alpha), true)?; + + dest.write_char(')') + }, + ColorSpace::Hsl | ColorSpace::Hwb => self.into_srgb_legacy().to_css(dest), + ColorSpace::Oklab | ColorSpace::Lab | ColorSpace::Oklch | ColorSpace::Lch => { + if let ColorSpace::Oklab | ColorSpace::Oklch = self.color_space { + dest.write_str("ok")?; + } + if let ColorSpace::Oklab | ColorSpace::Lab = self.color_space { + dest.write_str("lab(")?; + } else { + dest.write_str("lch(")?; + } + ModernComponent(&self.c0()).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c1()).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c2()).to_css(dest)?; + serialize_color_alpha(dest, self.alpha(), false)?; + dest.write_char(')') + }, + _ => { + #[cfg(debug_assertions)] + match self.color_space { + ColorSpace::Srgb => { + debug_assert!( + !self.flags.contains(ColorFlags::IS_LEGACY_SRGB), + "legacy srgb is not a color function" + ); + }, + ColorSpace::SrgbLinear | + ColorSpace::DisplayP3 | + ColorSpace::A98Rgb | + ColorSpace::ProphotoRgb | + ColorSpace::Rec2020 | + ColorSpace::XyzD50 | + ColorSpace::XyzD65 => { + // These color spaces are allowed. + }, + _ => { + unreachable!("other color spaces do not support color() syntax") + }, + }; + + dest.write_str("color(")?; + self.color_space.to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c0()).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c1()).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c2()).to_css(dest)?; + + serialize_color_alpha(dest, self.alpha(), false)?; + + dest.write_char(')') + }, + } + } +} + +impl AbsoluteColor { + /// Write a string to `dest` that represents a color as an author would + /// enter it. + /// NOTE: The format of the output is NOT according to any specification, + /// but makes assumptions about the best ways that authors would want to + /// enter color values in style sheets, devtools, etc. + pub fn write_author_preferred_value<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + macro_rules! precision { + ($v:expr) => {{ + ($v * 100.0).round() / 100.0 + }}; + } + macro_rules! number { + ($c:expr) => {{ + if let Some(v) = $c.map(normalize) { + precision!(v).to_css(dest)?; + } else { + write!(dest, "none")?; + } + }}; + } + macro_rules! percentage { + ($c:expr) => {{ + if let Some(v) = $c.map(normalize) { + precision!(v).to_css(dest)?; + dest.write_char('%')?; + } else { + write!(dest, "none")?; + } + }}; + } + macro_rules! unit_percentage { + ($c:expr) => {{ + if let Some(v) = $c.map(normalize) { + precision!(v * 100.0).to_css(dest)?; + dest.write_char('%')?; + } else { + write!(dest, "none")?; + } + }}; + } + macro_rules! angle { + ($c:expr) => {{ + if let Some(v) = $c.map(normalize) { + precision!(v).to_css(dest)?; + dest.write_str("deg")?; + } else { + write!(dest, "none")?; + } + }}; + } + + match self.color_space { + ColorSpace::Srgb => { + write!(dest, "rgb(")?; + unit_percentage!(self.c0()); + dest.write_char(' ')?; + unit_percentage!(self.c1()); + dest.write_char(' ')?; + unit_percentage!(self.c2()); + serialize_color_alpha(dest, self.alpha(), false)?; + dest.write_char(')') + }, + ColorSpace::Hsl | ColorSpace::Hwb => { + dest.write_str(if self.color_space == ColorSpace::Hsl { + "hsl(" + } else { + "hwb(" + })?; + angle!(self.c0()); + dest.write_char(' ')?; + percentage!(self.c1()); + dest.write_char(' ')?; + percentage!(self.c2()); + serialize_color_alpha(dest, self.alpha(), false)?; + dest.write_char(')') + }, + ColorSpace::Lab | ColorSpace::Oklab => { + if self.color_space == ColorSpace::Oklab { + dest.write_str("ok")?; + } + dest.write_str("lab(")?; + if self.color_space == ColorSpace::Lab { + percentage!(self.c0()) + } else { + unit_percentage!(self.c0()) + } + dest.write_char(' ')?; + number!(self.c1()); + dest.write_char(' ')?; + number!(self.c2()); + serialize_color_alpha(dest, self.alpha(), false)?; + dest.write_char(')') + }, + ColorSpace::Lch | ColorSpace::Oklch => { + if self.color_space == ColorSpace::Oklch { + dest.write_str("ok")?; + } + dest.write_str("lch(")?; + number!(self.c0()); + dest.write_char(' ')?; + number!(self.c1()); + dest.write_char(' ')?; + angle!(self.c2()); + serialize_color_alpha(dest, self.alpha(), false)?; + dest.write_char(')') + }, + ColorSpace::SrgbLinear | + ColorSpace::DisplayP3 | + ColorSpace::A98Rgb | + ColorSpace::ProphotoRgb | + ColorSpace::Rec2020 | + ColorSpace::XyzD50 | + ColorSpace::XyzD65 => { + dest.write_str("color(")?; + self.color_space.to_css(dest)?; + dest.write_char(' ')?; + number!(self.c0()); + dest.write_char(' ')?; + number!(self.c1()); + dest.write_char(' ')?; + number!(self.c2()); + serialize_color_alpha(dest, self.alpha(), false)?; + dest.write_char(')') + }, + } + } +} diff --git a/servo/components/style/custom_properties.rs b/servo/components/style/custom_properties.rs index cb3b9685ae..b6523dd489 100644 --- a/servo/components/style/custom_properties.rs +++ b/servo/components/style/custom_properties.rs @@ -15,7 +15,10 @@ use crate::properties::{ }; use crate::properties_and_values::{ registry::PropertyRegistrationData, - value::{AllowComputationallyDependent, SpecifiedValue as SpecifiedRegisteredValue}, + value::{ + AllowComputationallyDependent, ComputedValue as ComputedRegisteredValue, + SpecifiedValue as SpecifiedRegisteredValue, + }, }; use crate::selector_map::{PrecomputedHashMap, PrecomputedHashSet}; use crate::stylesheets::UrlExtraData; @@ -122,7 +125,12 @@ macro_rules! lnf_int_variable { }}; } -static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 7] = [ +static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 8] = [ + lnf_int_variable!( + atom!("-moz-gtk-csd-titlebar-button-spacing"), + TitlebarButtonSpacing, + int_pixels + ), lnf_int_variable!( atom!("-moz-gtk-csd-titlebar-radius"), TitlebarRadius, @@ -245,7 +253,7 @@ impl ComputedCustomProperties { } /// Return the name and value of the property at specified index, if any. - pub fn property_at(&self, index: usize) -> Option<(&Name, &Option<Arc<VariableValue>>)> { + pub fn property_at(&self, index: usize) -> Option<(&Name, &Option<ComputedRegisteredValue>)> { // Just expose the custom property items from custom_properties.inherited, followed // by custom property items from custom_properties.non_inherited. self.inherited @@ -259,7 +267,7 @@ impl ComputedCustomProperties { &mut self, registration: &PropertyRegistrationData, name: &Name, - value: Arc<VariableValue>, + value: ComputedRegisteredValue, ) { self.map_mut(registration).insert(name, value) } @@ -288,7 +296,7 @@ impl ComputedCustomProperties { &self, registration: &PropertyRegistrationData, name: &Name, - ) -> Option<&Arc<VariableValue>> { + ) -> Option<&ComputedRegisteredValue> { if registration.inherits() { self.inherited.get(name) } else { @@ -669,7 +677,9 @@ fn parse_declaration_value_block<'i, 't>( let mut prev_reference_index: Option<usize> = None; loop { let token_start = input.position(); - let Ok(token) = input.next_including_whitespace_and_comments() else { break }; + let Ok(token) = input.next_including_whitespace_and_comments() else { + break; + }; let prev_token_type = last_token_type; let serialization_type = token.serialization_type(); @@ -809,7 +819,8 @@ fn parse_declaration_value_block<'i, 't>( check_closed!(")"); prev_reference_index = Some(our_ref_index); let reference = &mut references.refs[our_ref_index]; - reference.end = input.position().byte_index() - input_start.byte_index() + missing_closing_characters.len(); + reference.end = input.position().byte_index() - input_start.byte_index() + + missing_closing_characters.len(); reference.fallback = fallback; if is_var { references.any_var = true; @@ -885,7 +896,11 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { /// Create a new builder, inheriting from a given custom properties map. /// /// We expose this publicly mostly for @keyframe blocks. - pub fn new_with_properties(stylist: &'a Stylist, custom_properties: ComputedCustomProperties, computed_context: &'a mut computed::Context<'b>) -> Self { + pub fn new_with_properties( + stylist: &'a Stylist, + custom_properties: ComputedCustomProperties, + computed_context: &'a mut computed::Context<'b>, + ) -> Self { Self { seen: PrecomputedHashSet::default(), reverted: Default::default(), @@ -915,7 +930,9 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { // Reuse flags from computing registered custom properties initial values, such as // whether they depend on viewport units. - context.style().add_flags(stylist.get_custom_property_initial_values_flags()); + context + .style() + .add_flags(stylist.get_custom_property_initial_values_flags()); Self::new_with_properties(stylist, properties, context) } @@ -943,8 +960,8 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { let map = &mut self.custom_properties; let registration = self.stylist.get_custom_property_registration(&name); - match *value { - CustomDeclarationValue::Value(ref unparsed_value) => { + match value { + CustomDeclarationValue::Value(unparsed_value) => { let has_custom_property_references = unparsed_value.references.any_var; let registered_length_property = registration.syntax.may_reference_font_relative_length(); @@ -970,21 +987,19 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { self.computed_context, ); } - map.insert(registration, name, Arc::clone(unparsed_value)); + let value = ComputedRegisteredValue::universal(Arc::clone(unparsed_value)); + map.insert(registration, name, value); }, CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword { CSSWideKeyword::RevertLayer | CSSWideKeyword::Revert => { - let origin_revert = keyword == CSSWideKeyword::Revert; + let origin_revert = matches!(keyword, CSSWideKeyword::Revert); self.seen.remove(name); self.reverted.insert(name, (priority, origin_revert)); }, CSSWideKeyword::Initial => { // For non-inherited custom properties, 'initial' was handled in value_may_affect_style. debug_assert!(registration.inherits(), "Should've been handled earlier"); - map.remove(registration, name); - if let Some(ref initial_value) = registration.initial_value { - map.insert(registration, name, initial_value.clone()); - } + remove_and_insert_initial_value(name, registration, map); }, CSSWideKeyword::Inherit => { // For inherited custom properties, 'inherit' was handled in value_may_affect_style. @@ -1006,7 +1021,11 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { /// Note a non-custom property with variable reference that may in turn depend on that property. /// e.g. `font-size` depending on a custom property that may be a registered property using `em`. - pub fn note_potentially_cyclic_non_custom_dependency(&mut self, id: LonghandId, decl: &VariableDeclaration) { + pub fn note_potentially_cyclic_non_custom_dependency( + &mut self, + id: LonghandId, + decl: &VariableDeclaration, + ) { // With unit algebra in `calc()`, references aren't limited to `font-size`. // For example, `--foo: 100ex; font-weight: calc(var(--foo) / 1ex);`, // or `--foo: 1em; zoom: calc(var(--foo) * 30px / 2em);` @@ -1020,8 +1039,7 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { }, LonghandId::LineHeight => { if self.computed_context.is_root_element() { - NonCustomReferences::ROOT_LH_UNITS | - NonCustomReferences::ROOT_FONT_UNITS + NonCustomReferences::ROOT_LH_UNITS | NonCustomReferences::ROOT_FONT_UNITS } else { NonCustomReferences::LH_UNITS | NonCustomReferences::FONT_UNITS } @@ -1033,15 +1051,24 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { return; } - let variables: Vec<Atom> = refs.refs.iter().filter_map(|reference| { - if !reference.is_var { - return None; - } - if !self.stylist.get_custom_property_registration(&reference.name).syntax.may_compute_length() { - return None; - } - Some(reference.name.clone()) - }).collect(); + let variables: Vec<Atom> = refs + .refs + .iter() + .filter_map(|reference| { + if !reference.is_var { + return None; + } + if !self + .stylist + .get_custom_property_registration(&reference.name) + .syntax + .may_compute_length() + { + return None; + } + Some(reference.name.clone()) + }) + .collect(); references.for_each(|idx| { let entry = &mut self.references_from_non_custom_properties[idx]; let was_none = entry.is_none(); @@ -1098,8 +1125,12 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { debug_assert!(registration.inherits(), "Should've been handled earlier"); // Don't bother overwriting an existing value with the initial value specified in // the registration. - if Some(existing_value) == registration.initial_value.as_ref() { - return false; + if let Some(initial_value) = self + .stylist + .get_custom_property_initial_values() + .get(registration, name) + { + return existing_value != initial_value; } }, (Some(_), &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit)) => { @@ -1120,8 +1151,16 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { (Some(existing_value), &CustomDeclarationValue::Value(ref value)) => { // Don't bother overwriting an existing value with the same // specified value. - if existing_value == value { - return false; + if let Some(existing_value) = existing_value.as_universal() { + return existing_value != value; + } + if let Ok(value) = compute_value( + &value.css, + &value.url_data, + registration, + self.computed_context, + ) { + return existing_value.v != value.v; } }, _ => {}, @@ -1164,7 +1203,8 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { self.stylist, self.computed_context, ); - self.computed_context.builder.invalid_non_custom_properties = invalid_non_custom_properties; + self.computed_context.builder.invalid_non_custom_properties = + invalid_non_custom_properties; } self.custom_properties.shrink_to_fit(); @@ -1178,7 +1218,8 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { inherited: if self .computed_context .inherited_custom_properties() - .inherited == self.custom_properties.inherited + .inherited == + self.custom_properties.inherited { self.computed_context .inherited_custom_properties() @@ -1217,6 +1258,9 @@ impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { // have to worry about resolving in a wrong order. for (k, v) in deferred.iter() { let Some(v) = v else { continue }; + let Some(v) = v.as_universal() else { + unreachable!("Computing should have been deferred!") + }; substitute_references_if_needed_and_apply( k, v, @@ -1340,6 +1384,7 @@ fn substitute_all( VarType::Custom(ref name) => { let registration = context.stylist.get_custom_property_registration(name); let value = context.map.get(registration, name)?; + let value = value.as_universal()?; let non_custom_references = value .references @@ -1476,22 +1521,16 @@ fn substitute_all( .invalid_non_custom_properties .insert(LonghandId::FontSize); } - if context.non_custom_references.intersects( - NonCustomReferences::LH_UNITS | - NonCustomReferences::ROOT_LH_UNITS, - ) { + if context + .non_custom_references + .intersects(NonCustomReferences::LH_UNITS | NonCustomReferences::ROOT_LH_UNITS) + { context .invalid_non_custom_properties .insert(LonghandId::LineHeight); } // This variable is in loop. Resolve to invalid. - handle_invalid_at_computed_value_time( - name, - context.map, - context.computed_context.inherited_custom_properties(), - context.stylist, - context.computed_context.is_root_element(), - ); + handle_invalid_at_computed_value_time(name, context.map, context.computed_context); }; loop { let var_index = context @@ -1534,7 +1573,7 @@ fn substitute_all( return None; } - if let Some(ref v) = value.as_ref() { + if let Some(ref v) = value { let registration = context.stylist.get_custom_property_registration(&name); let registered_length_property = registration.syntax.may_reference_font_relative_length(); @@ -1542,7 +1581,8 @@ fn substitute_all( if !context.non_custom_references.is_empty() && registered_length_property { if let Some(deferred) = &mut context.deferred_properties { // This property directly depends on a non-custom property, defer resolving it. - deferred.insert(registration, &name, (*v).clone()); + let deferred_property = ComputedRegisteredValue::universal(Arc::clone(v)); + deferred.insert(registration, &name, deferred_property); context.map.remove(registration, &name); defer = true; } @@ -1553,11 +1593,14 @@ fn substitute_all( continue; } if let Some(deferred) = &mut context.deferred_properties { - let registration = - context.stylist.get_custom_property_registration(&reference.name); + let registration = context + .stylist + .get_custom_property_registration(&reference.name); if deferred.get(registration, &reference.name).is_some() { // This property depends on a custom property that depends on a non-custom property, defer. - deferred.insert(registration, &name, Arc::clone(v)); + let deferred_property = + ComputedRegisteredValue::universal(Arc::clone(v)); + deferred.insert(registration, &name, deferred_property); context.map.remove(registration, &name); defer = true; break; @@ -1610,22 +1653,27 @@ fn substitute_all( fn handle_invalid_at_computed_value_time( name: &Name, custom_properties: &mut ComputedCustomProperties, - inherited: &ComputedCustomProperties, - stylist: &Stylist, - is_root_element: bool, + computed_context: &computed::Context, ) { + let stylist = computed_context.style().stylist.unwrap(); let registration = stylist.get_custom_property_registration(&name); if !registration.syntax.is_universal() { // For the root element, inherited maps are empty. We should just // use the initial value if any, rather than removing the name. - if registration.inherits() && !is_root_element { + if registration.inherits() && !computed_context.is_root_element() { + let inherited = computed_context.inherited_custom_properties(); if let Some(value) = inherited.get(registration, name) { - custom_properties.insert(registration, name, Arc::clone(value)); + custom_properties.insert(registration, name, value.clone()); return; } - } else { - if let Some(ref initial_value) = registration.initial_value { - custom_properties.insert(registration, name, Arc::clone(initial_value)); + } else if let Some(ref initial_value) = registration.initial_value { + if let Ok(initial_value) = compute_value( + &initial_value.css, + &initial_value.url_data, + registration, + computed_context, + ) { + custom_properties.insert(registration, name, initial_value); return; } } @@ -1644,35 +1692,43 @@ fn substitute_references_if_needed_and_apply( let registration = stylist.get_custom_property_registration(&name); if !value.has_references() && registration.syntax.is_universal() { // Trivial path: no references and no need to compute the value, just apply it directly. - custom_properties.insert(registration, name, Arc::clone(value)); + let computed_value = ComputedRegisteredValue::universal(Arc::clone(value)); + custom_properties.insert(registration, name, computed_value); return; } let inherited = computed_context.inherited_custom_properties(); - let value = match substitute_internal(value, custom_properties, stylist, registration, computed_context) { + let url_data = &value.url_data; + let value = match substitute_internal( + value, + custom_properties, + stylist, + registration, + computed_context, + ) { Ok(v) => v, Err(..) => { - handle_invalid_at_computed_value_time( - name, - custom_properties, - inherited, - stylist, - computed_context.is_root_element(), - ); + handle_invalid_at_computed_value_time(name, custom_properties, computed_context); return; }, - }.into_value(&value.url_data); + } + .into_value(url_data); // If variable fallback results in a wide keyword, deal with it now. { - let mut input = ParserInput::new(&value.css); + let css = value.to_variable_value().css; + let mut input = ParserInput::new(&css); let mut input = Parser::new(&mut input); if let Ok(kw) = input.try_parse(CSSWideKeyword::parse) { // TODO: It's unclear what this should do for revert / revert-layer, see // https://github.com/w3c/csswg-drafts/issues/9131. For now treating as unset // seems fine? - match (kw, registration.inherits(), computed_context.is_root_element()) { + match ( + kw, + registration.inherits(), + computed_context.is_root_element(), + ) { (CSSWideKeyword::Initial, _, _) | (CSSWideKeyword::Revert, false, _) | (CSSWideKeyword::RevertLayer, false, _) | @@ -1681,10 +1737,7 @@ fn substitute_references_if_needed_and_apply( (CSSWideKeyword::RevertLayer, true, true) | (CSSWideKeyword::Unset, true, true) | (CSSWideKeyword::Inherit, _, true) => { - custom_properties.remove(registration, name); - if let Some(ref initial_value) = registration.initial_value { - custom_properties.insert(registration, name, Arc::clone(initial_value)); - } + remove_and_insert_initial_value(name, registration, custom_properties); }, (CSSWideKeyword::Revert, true, false) | (CSSWideKeyword::RevertLayer, true, false) | @@ -1692,7 +1745,7 @@ fn substitute_references_if_needed_and_apply( (CSSWideKeyword::Unset, true, false) => { match inherited.get(registration, name) { Some(value) => { - custom_properties.insert(registration, name, Arc::clone(value)); + custom_properties.insert(registration, name, value.clone()); }, None => { custom_properties.remove(registration, name); @@ -1704,45 +1757,81 @@ fn substitute_references_if_needed_and_apply( } } - custom_properties.insert(registration, name, Arc::new(value)); + custom_properties.insert(registration, name, value); +} + +enum Substitution<'a> { + Universal(UniversalSubstitution<'a>), + Computed(ComputedRegisteredValue), +} + +impl<'a> Default for Substitution<'a> { + fn default() -> Self { + Self::Universal(UniversalSubstitution::default()) + } } #[derive(Default)] -struct Substitution<'a> { +struct UniversalSubstitution<'a> { css: Cow<'a, str>, first_token_type: TokenSerializationType, last_token_type: TokenSerializationType, } +impl<'a> UniversalSubstitution<'a> { + fn from_value(v: VariableValue) -> Self { + UniversalSubstitution { + css: Cow::from(v.css), + first_token_type: v.first_token_type, + last_token_type: v.last_token_type, + } + } +} + impl<'a> Substitution<'a> { fn new( css: &'a str, first_token_type: TokenSerializationType, last_token_type: TokenSerializationType, ) -> Self { - Self { + Self::Universal(UniversalSubstitution { css: Cow::Borrowed(css), first_token_type, last_token_type, - } + }) } - fn from_value(v: VariableValue) -> Substitution<'static> { - debug_assert!(!v.has_references(), "Computed values shouldn't have references"); - Substitution { - css: Cow::from(v.css), - first_token_type: v.first_token_type, - last_token_type: v.last_token_type, + fn into_universal(self) -> UniversalSubstitution<'a> { + match self { + Substitution::Universal(substitution) => substitution, + Substitution::Computed(computed) => { + UniversalSubstitution::from_value(computed.to_variable_value()) + }, } } - fn into_value(self, url_data: &UrlExtraData) -> VariableValue { - VariableValue { - css: self.css.into_owned(), - first_token_type: self.first_token_type, - last_token_type: self.last_token_type, - url_data: url_data.clone(), - references: Default::default(), + fn from_value(v: VariableValue) -> Self { + debug_assert!( + !v.has_references(), + "Computed values shouldn't have references" + ); + let substitution = UniversalSubstitution::from_value(v); + Self::Universal(substitution) + } + + fn into_value(self, url_data: &UrlExtraData) -> ComputedRegisteredValue { + match self { + Substitution::Universal(substitution) => { + let value = Arc::new(VariableValue { + css: substitution.css.into_owned(), + first_token_type: substitution.first_token_type, + last_token_type: substitution.last_token_type, + url_data: url_data.clone(), + references: Default::default(), + }); + ComputedRegisteredValue::universal(value) + }, + Substitution::Computed(computed) => computed, } } } @@ -1752,20 +1841,32 @@ fn compute_value( url_data: &UrlExtraData, registration: &PropertyRegistrationData, computed_context: &computed::Context, -) -> Result<Substitution<'static>, ()> { +) -> Result<ComputedRegisteredValue, ()> { debug_assert!(!registration.syntax.is_universal()); let mut input = ParserInput::new(&css); let mut input = Parser::new(&mut input); - let value = SpecifiedRegisteredValue::compute( + SpecifiedRegisteredValue::compute( &mut input, registration, url_data, computed_context, AllowComputationallyDependent::Yes, - )?; - Ok(Substitution::from_value(value)) + ) +} + +/// Removes the named registered custom property and inserts its uncomputed initial value. +fn remove_and_insert_initial_value( + name: &Name, + registration: &PropertyRegistrationData, + custom_properties: &mut ComputedCustomProperties, +) { + custom_properties.remove(registration, name); + if let Some(ref initial_value) = registration.initial_value { + let value = ComputedRegisteredValue::universal(Arc::clone(initial_value)); + custom_properties.insert(registration, name, value); + } } fn do_substitute_chunk<'a>( @@ -1792,7 +1893,8 @@ fn do_substitute_chunk<'a>( { let result = &css[start..end]; if !registration.syntax.is_universal() { - return compute_value(result, url_data, registration, computed_context); + let computed_value = compute_value(result, url_data, registration, computed_context)?; + return Ok(Substitution::Computed(computed_value)); } return Ok(Substitution::new(result, first_token_type, last_token_type)); } @@ -1824,6 +1926,7 @@ fn do_substitute_chunk<'a>( return Ok(substitution); } + let substitution = substitution.into_universal(); substituted.push( &substitution.css, substitution.first_token_type, @@ -1837,7 +1940,9 @@ fn do_substitute_chunk<'a>( substituted.push(&css[cur_pos..end], next_token_type, last_token_type)?; } if !registration.syntax.is_universal() { - return compute_value(&substituted.css, url_data, registration, computed_context); + let computed_value = + compute_value(&substituted.css, url_data, registration, computed_context)?; + return Ok(Substitution::Computed(computed_value)); } Ok(Substitution::from_value(substituted)) } @@ -1855,7 +1960,8 @@ fn substitute_one_reference<'a>( if reference.is_var { registration = stylist.get_custom_property_registration(&reference.name); if let Some(v) = custom_properties.get(registration, &reference.name) { - debug_assert!(!v.has_references(), "Should be already computed"); + #[cfg(debug_assertions)] + debug_assert!(v.is_parsed(registration), "Should be already computed"); if registration.syntax.is_universal() { // Skip references that are inside the outer variable (in fallback for example). while references @@ -1881,11 +1987,7 @@ fn substitute_one_reference<'a>( )?; } } - return Ok(Substitution { - css: Cow::from(&v.css), - first_token_type: v.first_token_type, - last_token_type: v.last_token_type, - }); + return Ok(Substitution::Computed(v.clone())); } } else { registration = PropertyRegistrationData::unregistered(); @@ -1899,7 +2001,9 @@ fn substitute_one_reference<'a>( } } - let Some(ref fallback) = reference.fallback else { return Err(()) }; + let Some(ref fallback) = reference.fallback else { + return Err(()); + }; do_substitute_chunk( css, @@ -1955,5 +2059,6 @@ pub fn substitute<'a>( PropertyRegistrationData::unregistered(), computed_context, )?; + let v = v.into_universal(); Ok(v.css) } diff --git a/servo/components/style/custom_properties_map.rs b/servo/components/style/custom_properties_map.rs index 04ca8e1b3d..13c5374308 100644 --- a/servo/components/style/custom_properties_map.rs +++ b/servo/components/style/custom_properties_map.rs @@ -4,7 +4,8 @@ //! The structure that contains the custom properties of a given element. -use crate::custom_properties::{Name, VariableValue}; +use crate::custom_properties::Name; +use crate::properties_and_values::value::ComputedValue as ComputedRegisteredValue; use crate::selector_map::PrecomputedHasher; use indexmap::IndexMap; use servo_arc::Arc; @@ -22,7 +23,8 @@ impl Default for CustomPropertiesMap { } /// We use None in the value to represent a removed entry. -type OwnMap = IndexMap<Name, Option<Arc<VariableValue>>, BuildHasherDefault<PrecomputedHasher>>; +type OwnMap = + IndexMap<Name, Option<ComputedRegisteredValue>, BuildHasherDefault<PrecomputedHasher>>; // IndexMap equality doesn't consider ordering, which we want to account for. Also, for the same // reason, IndexMap equality comparisons are slower than needed. @@ -69,12 +71,12 @@ const ANCESTOR_COUNT_LIMIT: usize = 4; /// An iterator over the custom properties. pub struct Iter<'a> { current: &'a Inner, - current_iter: indexmap::map::Iter<'a, Name, Option<Arc<VariableValue>>>, + current_iter: indexmap::map::Iter<'a, Name, Option<ComputedRegisteredValue>>, descendants: smallvec::SmallVec<[&'a Inner; ANCESTOR_COUNT_LIMIT]>, } impl<'a> Iterator for Iter<'a> { - type Item = (&'a Name, &'a Option<Arc<VariableValue>>); + type Item = (&'a Name, &'a Option<ComputedRegisteredValue>); fn next(&mut self) -> Option<Self::Item> { loop { @@ -141,14 +143,14 @@ impl Inner { self.len } - fn get(&self, name: &Name) -> Option<&Arc<VariableValue>> { + fn get(&self, name: &Name) -> Option<&ComputedRegisteredValue> { if let Some(p) = self.own_properties.get(name) { return p.as_ref(); } self.parent.as_ref()?.get(name) } - fn insert(&mut self, name: &Name, value: Option<Arc<VariableValue>>) { + fn insert(&mut self, name: &Name, value: Option<ComputedRegisteredValue>) { let new = self.own_properties.insert(name.clone(), value).is_none(); if new && self.parent.as_ref().map_or(true, |p| p.get(name).is_none()) { self.len += 1; @@ -177,7 +179,7 @@ impl CustomPropertiesMap { } /// Returns the property name and value at a given index. - pub fn get_index(&self, index: usize) -> Option<(&Name, &Option<Arc<VariableValue>>)> { + pub fn get_index(&self, index: usize) -> Option<(&Name, &Option<ComputedRegisteredValue>)> { if index >= self.len() { return None; } @@ -186,11 +188,11 @@ impl CustomPropertiesMap { } /// Returns a given property value by name. - pub fn get(&self, name: &Name) -> Option<&Arc<VariableValue>> { + pub fn get(&self, name: &Name) -> Option<&ComputedRegisteredValue> { self.0.get(name) } - fn do_insert(&mut self, name: &Name, value: Option<Arc<VariableValue>>) { + fn do_insert(&mut self, name: &Name, value: Option<ComputedRegisteredValue>) { if let Some(inner) = Arc::get_mut(&mut self.0) { return inner.insert(name, value); } @@ -214,7 +216,7 @@ impl CustomPropertiesMap { } /// Inserts an element in the map. - pub fn insert(&mut self, name: &Name, value: Arc<VariableValue>) { + pub fn insert(&mut self, name: &Name, value: ComputedRegisteredValue) { self.do_insert(name, Some(value)) } diff --git a/servo/components/style/dom.rs b/servo/components/style/dom.rs index 554d79fdb3..358d788845 100644 --- a/servo/components/style/dom.rs +++ b/servo/components/style/dom.rs @@ -207,6 +207,17 @@ pub trait TNode: Sized + Copy + Clone + Debug + NodeInfo + PartialEq { } } + /// Returns the depth of this node in the DOM. + fn depth(&self) -> usize { + let mut depth = 0; + let mut curr = *self; + while let Some(parent) = curr.traversal_parent() { + depth += 1; + curr = parent.as_node(); + } + depth + } + /// Get this node's parent element from the perspective of a restyle /// traversal. fn traversal_parent(&self) -> Option<Self::ConcreteElement>; @@ -398,18 +409,6 @@ pub trait TElement: true } - /// Returns the depth of this element in the DOM. - fn depth(&self) -> usize { - let mut depth = 0; - let mut curr = *self; - while let Some(parent) = curr.traversal_parent() { - depth += 1; - curr = parent; - } - - depth - } - /// Get this node's parent element from the perspective of a restyle /// traversal. fn traversal_parent(&self) -> Option<Self> { diff --git a/servo/components/style/driver.rs b/servo/components/style/driver.rs index 95447ce08e..a2407cb209 100644 --- a/servo/components/style/driver.rs +++ b/servo/components/style/driver.rs @@ -46,16 +46,28 @@ fn report_statistics(stats: &PerThreadTraversalStatistics) { gecko_stats.mStylesReused += stats.styles_reused; } -fn with_pool_in_place_scope<'scope, R>( +fn with_pool_in_place_scope<'scope>( work_unit_max: usize, pool: Option<&rayon::ThreadPool>, - closure: impl FnOnce(Option<&rayon::ScopeFifo<'scope>>) -> R, -) -> R { + closure: impl FnOnce(Option<&rayon::ScopeFifo<'scope>>) + Send + 'scope, +) { if work_unit_max == 0 || pool.is_none() { - closure(None) + closure(None); } else { - pool.unwrap() - .in_place_scope_fifo(|scope| closure(Some(scope))) + let pool = pool.unwrap(); + pool.in_place_scope_fifo(|scope| { + #[cfg(feature = "gecko")] + debug_assert_eq!( + pool.current_thread_index(), + Some(0), + "Main thread should be the first thread" + ); + if cfg!(feature = "gecko") || pool.current_thread_index().is_some() { + closure(Some(scope)); + } else { + scope.spawn_fifo(|scope| closure(Some(scope))); + } + }); } } @@ -108,6 +120,8 @@ where // Process the nodes breadth-first. This helps keep similar traversal characteristics for the // style sharing cache. let work_unit_max = work_unit_max(); + + let send_root = unsafe { SendNode::new(root.as_node()) }; with_pool_in_place_scope(work_unit_max, pool, |maybe_scope| { let mut tlc = scoped_tls.ensure(parallel::create_thread_local_context); let mut context = StyleContext { @@ -115,22 +129,16 @@ where thread_local: &mut tlc, }; - debug_assert_eq!( - scoped_tls.current_thread_index(), - 0, - "Main thread should be the first thread" - ); - let mut discovered = VecDeque::with_capacity(work_unit_max * 2); - discovered.push_back(unsafe { SendNode::new(root.as_node()) }); + let current_dom_depth = send_root.depth(); + let opaque_root = send_root.opaque(); + discovered.push_back(send_root); parallel::style_trees( &mut context, discovered, - root.as_node().opaque(), + opaque_root, work_unit_max, - PerLevelTraversalData { - current_dom_depth: root.depth(), - }, + PerLevelTraversalData { current_dom_depth }, maybe_scope, traversal, &scoped_tls, diff --git a/servo/components/style/gecko/media_features.rs b/servo/components/style/gecko/media_features.rs index c9ad30b28b..8de45d95c2 100644 --- a/servo/components/style/gecko/media_features.rs +++ b/servo/components/style/gecko/media_features.rs @@ -169,8 +169,7 @@ fn eval_color_gamut(context: &Context, query_value: Option<ColorGamut>) -> bool // Match if our color gamut is at least as wide as the query value query_value <= match color_gamut { - // EndGuard_ is not a valid color gamut, so the default color-gamut is used. - ScreenColorGamut::Srgb | ScreenColorGamut::EndGuard_ => ColorGamut::Srgb, + ScreenColorGamut::Srgb => ColorGamut::Srgb, ScreenColorGamut::P3 => ColorGamut::P3, ScreenColorGamut::Rec2020 => ColorGamut::Rec2020, } @@ -566,6 +565,8 @@ pub enum Platform { /// platforms and they already use the "linux" string elsewhere (e.g., /// toolkit/themes/linux). Linux, + /// Matches any iOS version. + Ios, /// Matches any macOS version. Macos, /// Matches any Windows version. diff --git a/servo/components/style/gecko/regen_atoms.py b/servo/components/style/gecko/regen_atoms.py index 61f2fc4c63..9a3bb48271 100755 --- a/servo/components/style/gecko/regen_atoms.py +++ b/servo/components/style/gecko/regen_atoms.py @@ -18,7 +18,7 @@ import build # Matches lines like `GK_ATOM(foo, "foo", 0x12345678, true, nsStaticAtom, PseudoElementAtom)`. PATTERN = re.compile( - '^GK_ATOM\(([^,]*),[^"]*"([^"]*)",\s*(0x[0-9a-f]+),\s*[^,]*,\s*([^,]*),\s*([^)]*)\)', + r'^GK_ATOM\(([^,]*),[^"]*"([^"]*)",\s*(0x[0-9a-f]+),\s*[^,]*,\s*([^,]*),\s*([^)]*)\)', re.MULTILINE, ) FILE = "include/nsGkAtomList.h" diff --git a/servo/components/style/gecko/wrapper.rs b/servo/components/style/gecko/wrapper.rs index 61352ef9c0..43cf6e7941 100644 --- a/servo/components/style/gecko/wrapper.rs +++ b/servo/components/style/gecko/wrapper.rs @@ -865,7 +865,6 @@ impl<'le> GeckoElement<'le> { after_change_style: &ComputedValues, existing_transitions: &FxHashMap<OwnedPropertyDeclarationId, Arc<AnimationValue>>, ) -> bool { - use crate::values::animated::{Animate, Procedure}; debug_assert!(!property_declaration_id.is_logical()); // If there is an existing transition, update only if the end value @@ -882,20 +881,17 @@ impl<'le> GeckoElement<'le> { return ***existing != after_value; } + if combined_duration_seconds <= 0.0f32 { + return false; + } + let from = AnimationValue::from_computed_values(property_declaration_id, before_change_style); let to = AnimationValue::from_computed_values(property_declaration_id, after_change_style); debug_assert_eq!(to.is_some(), from.is_some()); - combined_duration_seconds > 0.0f32 && - from != to && - from.unwrap() - .animate( - to.as_ref().unwrap(), - Procedure::Interpolate { progress: 0.5 }, - ) - .is_ok() + from != to } /// Get slow selector flags required for nth-of invalidation. diff --git a/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl b/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl Binary files differdeleted file mode 100644 index 9593025a47..0000000000 --- a/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl +++ /dev/null diff --git a/servo/components/style/properties/build.py b/servo/components/style/properties/build.py index 6c3ee0cf66..42121a4eae 100644 --- a/servo/components/style/properties/build.py +++ b/servo/components/style/properties/build.py @@ -8,7 +8,6 @@ import re import sys BASE = os.path.dirname(__file__.replace("\\", "/")) -sys.path.insert(0, os.path.join(BASE, "Mako-1.1.2-py2.py3-none-any.whl")) sys.path.insert(0, BASE) # For importing `data.py` from mako import exceptions diff --git a/servo/components/style/properties/cascade.rs b/servo/components/style/properties/cascade.rs index 59a8a65876..d4d48eaeb8 100644 --- a/servo/components/style/properties/cascade.rs +++ b/servo/components/style/properties/cascade.rs @@ -448,6 +448,8 @@ fn tweak_when_ignoring_colors( // A few special-cases ahead. match **declaration { + // Honor CSS-wide keywords like unset / revert / initial... + PropertyDeclaration::CSSWideKeyword(..) => return, PropertyDeclaration::BackgroundColor(ref color) => { // We honor system colors and transparent colors unconditionally. // diff --git a/servo/components/style/properties/gecko.mako.rs b/servo/components/style/properties/gecko.mako.rs index f5ae0cade3..8d3f90f8f5 100644 --- a/servo/components/style/properties/gecko.mako.rs +++ b/servo/components/style/properties/gecko.mako.rs @@ -1673,12 +1673,13 @@ mask-mode mask-repeat mask-clip mask-origin mask-composite mask-position-x mask- animation-direction animation-fill-mode animation-play-state animation-iteration-count animation-timing-function animation-composition animation-timeline - transition-duration transition-delay + transition-behavior transition-duration transition-delay transition-timing-function transition-property scroll-timeline-name scroll-timeline-axis view-timeline-name view-timeline-axis view-timeline-inset""" %> <%self:impl_trait style_struct_name="UI" skip_longhands="${skip_ui_longhands}"> + ${impl_coordinated_property('transition', 'behavior', 'Behavior')} ${impl_coordinated_property('transition', 'delay', 'Delay')} ${impl_coordinated_property('transition', 'duration', 'Duration')} ${impl_coordinated_property('transition', 'timing_function', 'TimingFunction')} diff --git a/servo/components/style/properties/longhands/font.mako.rs b/servo/components/style/properties/longhands/font.mako.rs index f188af5b1f..4583de0bd7 100644 --- a/servo/components/style/properties/longhands/font.mako.rs +++ b/servo/components/style/properties/longhands/font.mako.rs @@ -305,10 +305,7 @@ ${helpers.predefined_type( "MathDepth", "0", engines="gecko", - gecko_pref="layout.css.math-depth.enabled", - has_effect_on_gecko_scrollbars=False, animation_value_type="none", - enabled_in="ua", spec="https://mathml-refresh.github.io/mathml-core/#the-math-script-level-property", affects="", )} @@ -318,11 +315,8 @@ ${helpers.single_keyword( "normal compact", engines="gecko", gecko_enum_prefix="StyleMathStyle", - gecko_pref="layout.css.math-style.enabled", spec="https://mathml-refresh.github.io/mathml-core/#the-math-style-property", - has_effect_on_gecko_scrollbars=False, animation_value_type="none", - enabled_in="ua", needs_conversion=True, affects="layout", )} diff --git a/servo/components/style/properties/longhands/ui.mako.rs b/servo/components/style/properties/longhands/ui.mako.rs index 1150816ac0..58006e0d65 100644 --- a/servo/components/style/properties/longhands/ui.mako.rs +++ b/servo/components/style/properties/longhands/ui.mako.rs @@ -199,6 +199,20 @@ ${helpers.predefined_type( affects="", )} +${helpers.predefined_type( + "transition-behavior", + "TransitionBehavior", + "computed::TransitionBehavior::normal()", + engines="gecko", + initial_specified_value="specified::TransitionBehavior::normal()", + vector=True, + need_index=True, + animation_value_type="none", + gecko_pref="layout.css.transition-behavior.enabled", + spec="https://drafts.csswg.org/css-transitions-2/#transition-behavior-property", + affects="", +)} + <% animation_extra_prefixes = "moz:layout.css.prefixes.animations webkit" %> ${helpers.predefined_type( diff --git a/servo/components/style/properties/shorthands/svg.mako.rs b/servo/components/style/properties/shorthands/svg.mako.rs index cf34b116ee..195427dad5 100644 --- a/servo/components/style/properties/shorthands/svg.mako.rs +++ b/servo/components/style/properties/shorthands/svg.mako.rs @@ -144,8 +144,8 @@ // <mask-reference> || // <position> [ / <bg-size> ]? || // <repeat-style> || - // <geometry-box> || - // [ <geometry-box> | no-clip ] || + // <coord-box> || + // [ <coord-box> | no-clip ] || // <compositing-operator> || // <masking-mode> // https://drafts.fxtf.org/css-masking-1/#the-mask @@ -198,12 +198,21 @@ writer.item(repeat)?; } - // <geometry-box> - if has_origin { + // <coord-box> + // Note: + // Even if 'mask-origin' is at its initial value 'border-box', + // we still have to serialize it to avoid ambiguity iF the + // 'mask-clip' longhand has some other <coord-box> value + // (i.e. neither 'border-box' nor 'no-clip'). (If we naively + // declined to serialize the 'mask-origin' value in this + // situation, then whatever value we serialize for 'mask-clip' + // would implicitly also represent 'mask-origin' and would be + // providing the wrong value for that longhand.) + if has_origin || (has_clip && *clip != Clip::NoClip) { writer.item(origin)?; } - // [ <geometry-box> | no-clip ] + // [ <coord-box> | no-clip ] if has_clip && *clip != From::from(*origin) { writer.item(clip)?; } diff --git a/servo/components/style/properties/shorthands/ui.mako.rs b/servo/components/style/properties/shorthands/ui.mako.rs index 1fdb5965fc..4a27d5e003 100644 --- a/servo/components/style/properties/shorthands/ui.mako.rs +++ b/servo/components/style/properties/shorthands/ui.mako.rs @@ -22,10 +22,10 @@ macro_rules! try_parse_one { extra_prefixes="moz:layout.css.prefixes.transitions webkit" sub_properties="transition-property transition-duration transition-timing-function - transition-delay" + transition-delay transition-behavior" spec="https://drafts.csswg.org/css-transitions/#propdef-transition"> use crate::parser::Parse; - % for prop in "delay duration property timing_function".split(): + % for prop in "delay duration property timing_function behavior".split(): use crate::properties::longhands::transition_${prop}; % endfor use crate::values::specified::TransitionProperty; @@ -35,7 +35,7 @@ macro_rules! try_parse_one { input: &mut Parser<'i, 't>, ) -> Result<Longhands, ParseError<'i>> { struct SingleTransition { - % for prop in "property duration timing_function delay".split(): + % for prop in "property duration timing_function delay behavior".split(): transition_${prop}: transition_${prop}::SingleSpecifiedValue, % endfor } @@ -45,7 +45,7 @@ macro_rules! try_parse_one { input: &mut Parser<'i, 't>, first: bool, ) -> Result<SingleTransition,ParseError<'i>> { - % for prop in "property duration timing_function delay".split(): + % for prop in "property duration timing_function delay behavior".split(): let mut ${prop} = None; % endfor @@ -56,6 +56,9 @@ macro_rules! try_parse_one { try_parse_one!(context, input, duration, transition_duration); try_parse_one!(context, input, timing_function, transition_timing_function); try_parse_one!(context, input, delay, transition_delay); + if static_prefs::pref!("layout.css.transition-behavior.enabled") { + try_parse_one!(context, input, behavior, transition_behavior); + } // Must check 'transition-property' after 'transition-timing-function' since // 'transition-property' accepts any keyword. if property.is_none() { @@ -78,7 +81,7 @@ macro_rules! try_parse_one { if parsed != 0 { Ok(SingleTransition { - % for prop in "property duration timing_function delay".split(): + % for prop in "property duration timing_function delay behavior".split(): transition_${prop}: ${prop}.unwrap_or_else(transition_${prop}::single_value ::get_initial_specified_value), % endfor @@ -88,7 +91,7 @@ macro_rules! try_parse_one { } } - % for prop in "property duration timing_function delay".split(): + % for prop in "property duration timing_function delay behavior".split(): let mut ${prop}s = Vec::new(); % endfor @@ -105,13 +108,13 @@ macro_rules! try_parse_one { Ok(transition) })?; for result in results { - % for prop in "property duration timing_function delay".split(): + % for prop in "property duration timing_function delay behavior".split(): ${prop}s.push(result.transition_${prop}); % endfor } Ok(expanded! { - % for prop in "property duration timing_function delay".split(): + % for prop in "property duration timing_function delay behavior".split(): transition_${prop}: transition_${prop}::SpecifiedValue(${prop}s.into()), % endfor }) @@ -133,12 +136,24 @@ macro_rules! try_parse_one { return Ok(()); } % endfor + + if let Some(behavior) = self.transition_behavior { + if behavior.0.len() != 1 { + return Ok(()); + } + } } else { % for name in "duration delay timing_function".split(): if self.transition_${name}.0.len() != property_len { return Ok(()); } % endfor + + if let Some(behavior) = self.transition_behavior { + if behavior.0.len() != property_len { + return Ok(()); + } + } } // Representative length. @@ -152,7 +167,11 @@ macro_rules! try_parse_one { let has_duration = !self.transition_duration.0[i].is_zero(); let has_timing = !self.transition_timing_function.0[i].is_ease(); let has_delay = !self.transition_delay.0[i].is_zero(); - let has_any = has_duration || has_timing || has_delay; + let has_behavior = match self.transition_behavior { + Some(behavior) => !behavior.0[i].is_normal(), + _ => false, + }; + let has_any = has_duration || has_timing || has_delay || has_behavior; let mut writer = SequenceWriter::new(dest, " "); @@ -174,6 +193,10 @@ macro_rules! try_parse_one { if has_delay { writer.item(&self.transition_delay.0[i])?; } + + if has_behavior { + writer.item(&self.transition_behavior.unwrap().0[i])?; + } } Ok(()) } diff --git a/servo/components/style/properties_and_values/rule.rs b/servo/components/style/properties_and_values/rule.rs index 96617eccce..08693e6dd5 100644 --- a/servo/components/style/properties_and_values/rule.rs +++ b/servo/components/style/properties_and_values/rule.rs @@ -9,7 +9,10 @@ use super::{ registry::{PropertyRegistration, PropertyRegistrationData}, syntax::Descriptor, - value::{AllowComputationallyDependent, SpecifiedValue as SpecifiedRegisteredValue}, + value::{ + AllowComputationallyDependent, ComputedValue as ComputedRegisteredValue, + SpecifiedValue as SpecifiedRegisteredValue, + }, }; use crate::custom_properties::{Name as CustomPropertyName, SpecifiedValue}; use crate::error_reporting::ContextualParseError; @@ -216,13 +219,13 @@ impl PropertyRegistration { pub fn compute_initial_value( &self, computed_context: &computed::Context, - ) -> Result<InitialValue, ()> { + ) -> Result<ComputedRegisteredValue, ()> { let Some(ref initial) = self.data.initial_value else { return Err(()); }; if self.data.syntax.is_universal() { - return Ok(Arc::clone(initial)); + return Ok(ComputedRegisteredValue::universal(Arc::clone(initial))); } let mut input = ParserInput::new(initial.css_text()); @@ -236,7 +239,7 @@ impl PropertyRegistration { computed_context, AllowComputationallyDependent::No, ) { - Ok(computed) => Ok(Arc::new(computed)), + Ok(computed) => Ok(computed), Err(_) => Err(()), } } diff --git a/servo/components/style/properties_and_values/value.rs b/servo/components/style/properties_and_values/value.rs index 8e9d78b8cc..b66e268421 100644 --- a/servo/components/style/properties_and_values/value.rs +++ b/servo/components/style/properties_and_values/value.rs @@ -131,11 +131,13 @@ pub enum GenericValueComponent< /// A <resolution> value Resolution(Resolution), /// A <transform-function> value + /// TODO(bug 1884606): <transform-function> `none` should not interpolate. TransformFunction(TransformFunction), /// A <custom-ident> value #[animation(error)] CustomIdent(CustomIdent), /// A <transform-list> value, equivalent to <transform-function>+ + /// TODO(bug 1884606): <transform-list> `none` should not interpolate. TransformList(ComponentList<Self>), /// A <string> value #[animation(error)] @@ -156,7 +158,11 @@ impl<Component: Animate> Animate for ComponentList<Component> { if self.multiplier != other.multiplier { return Err(()); } - let components = animated::lists::by_computed_value::animate(&self.components, &other.components, procedure)?; + let components = animated::lists::by_computed_value::animate( + &self.components, + &other.components, + procedure, + )?; Ok(Self { multiplier: self.multiplier, components, @@ -190,9 +196,45 @@ impl<Component: ToCss> ToCss for ComponentList<Component> { } } +/// A struct for a single specified registered custom property value that includes its original URL +// data so the value can be uncomputed later. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss)] +pub struct Value<Component> { + /// The registered custom property value. + pub(crate) v: ValueInner<Component>, + /// The URL data of the registered custom property from before it was computed. This is + /// necessary to uncompute registered custom properties. + #[css(skip)] + url_data: UrlExtraData, +} + +impl<Component: Animate> Animate for Value<Component> { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let v = self.v.animate(&other.v, procedure)?; + Ok(Value { + v, + url_data: self.url_data.clone(), + }) + } +} + +impl<Component> Value<Component> { + /// Creates a new registered custom property value. + pub fn new(v: ValueInner<Component>, url_data: UrlExtraData) -> Self { + Self { v, url_data } + } + + /// Creates a new registered custom property value presumed to have universal syntax. + pub fn universal(var: Arc<ComputedPropertyValue>) -> Self { + let url_data = var.url_data.clone(); + let v = ValueInner::Universal(var); + Self { v, url_data } + } +} + /// A specified registered custom property value. #[derive(Animate, ToComputedValue, ToCss, Clone, Debug, MallocSizeOf, PartialEq)] -pub enum Value<Component> { +pub enum ValueInner<Component> { /// A single specified component value whose syntax descriptor component did not have a /// multiplier. Component(Component), @@ -206,31 +248,31 @@ pub enum Value<Component> { /// Specified custom property value. pub type SpecifiedValue = Value<SpecifiedValueComponent>; +impl ToComputedValue for SpecifiedValue { + type ComputedValue = ComputedValue; + + fn to_computed_value(&self, context: &computed::Context) -> Self::ComputedValue { + Self::ComputedValue { + v: self.v.to_computed_value(context), + url_data: self.url_data.clone(), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self { + v: ToComputedValue::from_computed_value(&computed.v), + url_data: computed.url_data.clone(), + } + } +} + /// Computed custom property value. pub type ComputedValue = Value<ComputedValueComponent>; impl SpecifiedValue { - /// Convert a Computed custom property value to a VariableValue. - pub fn compute<'i, 't>( - input: &mut CSSParser<'i, 't>, - registration: &PropertyRegistrationData, - url_data: &UrlExtraData, - context: &computed::Context, - allow_computationally_dependent: AllowComputationallyDependent, - ) -> Result<ComputedPropertyValue, ()> { - let value = Self::get_computed_value( - input, - registration, - url_data, - context, - allow_computationally_dependent, - )?; - Ok(value.to_variable_value(url_data)) - } - /// Convert a registered custom property to a Computed custom property value, given input and a /// property registration. - fn get_computed_value<'i, 't>( + pub fn compute<'i, 't>( input: &mut CSSParser<'i, 't>, registration: &PropertyRegistrationData, url_data: &UrlExtraData, @@ -259,9 +301,11 @@ impl SpecifiedValue { allow_computationally_dependent: AllowComputationallyDependent, ) -> Result<Self, StyleParseError<'i>> { if syntax.is_universal() { - return Ok(Self::Universal(Arc::new(ComputedPropertyValue::parse( - &mut input, url_data, - )?))); + let parsed = ComputedPropertyValue::parse(&mut input, url_data)?; + return Ok(SpecifiedValue { + v: ValueInner::Universal(Arc::new(parsed)), + url_data: url_data.clone(), + }); } let mut values = SmallComponentVec::new(); @@ -270,45 +314,64 @@ impl SpecifiedValue { let mut parser = Parser::new(syntax, &mut values, &mut multiplier); parser.parse(&mut input, url_data, allow_computationally_dependent)?; } - let computed_value = if let Some(multiplier) = multiplier { - Self::List(ComponentList { + let v = if let Some(multiplier) = multiplier { + ValueInner::List(ComponentList { multiplier, components: values.to_vec().into(), }) } else { - Self::Component(values[0].clone()) + ValueInner::Component(values[0].clone()) }; - Ok(computed_value) + Ok(Self { + v, + url_data: url_data.clone(), + }) } } impl ComputedValue { fn serialization_types(&self) -> (TokenSerializationType, TokenSerializationType) { - match self { - Self::Component(component) => component.serialization_types(), - Self::Universal(_) => unreachable!(), - Self::List(list) => list + match &self.v { + ValueInner::Component(component) => component.serialization_types(), + ValueInner::Universal(_) => unreachable!(), + ValueInner::List(list) => list .components .first() .map_or(Default::default(), |f| f.serialization_types()), } } - fn to_declared_value(&self, url_data: &UrlExtraData) -> Arc<ComputedPropertyValue> { - if let Self::Universal(var) = self { + fn to_declared_value(&self) -> Arc<ComputedPropertyValue> { + if let ValueInner::Universal(ref var) = self.v { return Arc::clone(var); } - Arc::new(self.to_variable_value(url_data)) + Arc::new(self.to_variable_value()) + } + + /// Returns the contained variable value if it exists, otherwise `None`. + pub fn as_universal(&self) -> Option<&Arc<ComputedPropertyValue>> { + if let ValueInner::Universal(ref var) = self.v { + Some(var) + } else { + None + } } - fn to_variable_value(&self, url_data: &UrlExtraData) -> ComputedPropertyValue { - debug_assert!(!matches!(self, Self::Universal(..)), "Shouldn't be needed"); - // TODO(zrhoffman, 1864736): Preserve the computed type instead of converting back to a - // string. + /// Returns whether the the property is computed. + #[cfg(debug_assertions)] + pub fn is_parsed(&self, registration: &PropertyRegistrationData) -> bool { + registration.syntax.is_universal() || !matches!(self.v, ValueInner::Universal(_)) + } + + /// Convert to an untyped variable value. + pub fn to_variable_value(&self) -> ComputedPropertyValue { + if let ValueInner::Universal(ref value) = self.v { + return (**value).clone(); + } let serialization_types = self.serialization_types(); ComputedPropertyValue::new( self.to_css_string(), - url_data, + &self.url_data, serialization_types.0, serialization_types.1, ) @@ -530,7 +593,6 @@ impl<'a> Parser<'a> { } } - /// An animated value for custom property. #[derive(Clone, Debug, MallocSizeOf, PartialEq)] pub struct CustomAnimatedValue { @@ -538,25 +600,17 @@ pub struct CustomAnimatedValue { pub(crate) name: crate::custom_properties::Name, /// The computed value of the custom property. value: ComputedValue, - /// The url data where the value came from. - /// FIXME: This seems like it should not be needed: registered properties don't need it, and - /// unregistered properties animate discretely. But we need it so far because the computed - /// value representation isn't typed. - url_data: UrlExtraData, } impl Animate for CustomAnimatedValue { fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { if self.name != other.name { - return Err(()) + return Err(()); } let value = self.value.animate(&other.value, procedure)?; Ok(Self { name: self.name.clone(), value, - // NOTE: This is sketchy AF, but it's ~fine, since values that can animate (non-universal) - // don't need it. - url_data: self.url_data.clone(), }) } } @@ -564,13 +618,11 @@ impl Animate for CustomAnimatedValue { impl CustomAnimatedValue { pub(crate) fn from_computed( name: &crate::custom_properties::Name, - value: &Arc<ComputedPropertyValue>, + value: &ComputedValue, ) -> Self { Self { name: name.clone(), - // FIXME: Should probably preserve type-ness in ComputedPropertyValue. - value: ComputedValue::Universal(value.clone()), - url_data: value.url_data.clone(), + value: value.clone(), } } @@ -590,8 +642,11 @@ impl CustomAnimatedValue { context.builder.stylist.is_some(), "Need a Stylist to get property registration!" ); - let registration = - context.builder.stylist.unwrap().get_custom_property_registration(&declaration.name); + let registration = context + .builder + .stylist + .unwrap() + .get_custom_property_registration(&declaration.name); // FIXME: Do we need to perform substitution here somehow? let computed_value = if registration.syntax.is_universal() { @@ -599,20 +654,22 @@ impl CustomAnimatedValue { } else { let mut input = cssparser::ParserInput::new(&value.css); let mut input = CSSParser::new(&mut input); - SpecifiedValue::get_computed_value( + SpecifiedValue::compute( &mut input, registration, &value.url_data, context, AllowComputationallyDependent::Yes, - ).ok() + ) + .ok() }; - let url_data = value.url_data.clone(); - let value = computed_value.unwrap_or_else(|| ComputedValue::Universal(Arc::clone(value))); + let value = computed_value.unwrap_or_else(|| ComputedValue { + v: ValueInner::Universal(Arc::clone(value)), + url_data: value.url_data.clone(), + }); Some(Self { name: declaration.name.clone(), - url_data, value, }) } @@ -620,7 +677,7 @@ impl CustomAnimatedValue { pub(crate) fn to_declaration(&self) -> properties::PropertyDeclaration { properties::PropertyDeclaration::Custom(properties::CustomDeclaration { name: self.name.clone(), - value: properties::CustomDeclarationValue::Value(self.value.to_declared_value(&self.url_data)), + value: properties::CustomDeclarationValue::Value(self.value.to_declared_value()), }) } } diff --git a/servo/components/style/sharing/mod.rs b/servo/components/style/sharing/mod.rs index eeea135c06..28592a02c5 100644 --- a/servo/components/style/sharing/mod.rs +++ b/servo/components/style/sharing/mod.rs @@ -908,12 +908,7 @@ impl<E: TElement> StyleSharingCache<E> { // RELEVANT_LINK_VISITED flag, so we can't share by rule node between visited and // unvisited styles. We don't check for visitedness and just refuse to share for links // entirely, so that visitedness doesn't affect timing. - debug_assert_eq!( - target.is_link(), - candidate.element.is_link(), - "Linkness mismatch" - ); - if target.is_link() { + if target.is_link() || candidate.element.is_link() { return None; } diff --git a/servo/components/style/values/computed/animation.rs b/servo/components/style/values/computed/animation.rs index 626dbe5347..b626687a36 100644 --- a/servo/components/style/values/computed/animation.rs +++ b/servo/components/style/values/computed/animation.rs @@ -11,8 +11,8 @@ use std::fmt::{self, Write}; use style_traits::{CssWriter, ToCss}; pub use crate::values::specified::animation::{ - AnimationName, ScrollAxis, ScrollTimelineName, TransitionProperty, AnimationComposition, - AnimationDirection, AnimationFillMode, AnimationPlayState, + AnimationComposition, AnimationDirection, AnimationFillMode, AnimationName, AnimationPlayState, + ScrollAxis, ScrollTimelineName, TransitionBehavior, TransitionProperty, }; /// A computed value for the `animation-iteration-count` property. diff --git a/servo/components/style/values/computed/color.rs b/servo/components/style/values/computed/color.rs index 9b5185d923..8aa50ba5ad 100644 --- a/servo/components/style/values/computed/color.rs +++ b/servo/components/style/values/computed/color.rs @@ -4,14 +4,13 @@ //! Computed color values. -use crate::color::parsing::Color as CSSParserColor; use crate::color::AbsoluteColor; use crate::values::animated::ToAnimatedZero; use crate::values::computed::percentage::Percentage; use crate::values::generics::color::{ GenericCaretColor, GenericColor, GenericColorMix, GenericColorOrAuto, }; -use std::fmt; +use std::fmt::{self, Write}; use style_traits::{CssWriter, ToCss}; pub use crate::values::specified::color::{ColorScheme, ForcedColorAdjust, PrintColorAdjust}; @@ -32,7 +31,7 @@ impl ToCss for Color { { match *self { Self::Absolute(ref c) => c.to_css(dest), - Self::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest), + Self::CurrentColor => dest.write_str("currentcolor"), Self::ColorMix(ref m) => m.to_css(dest), } } diff --git a/servo/components/style/values/computed/length_percentage.rs b/servo/components/style/values/computed/length_percentage.rs index 898281a7ef..0dbd2de76d 100644 --- a/servo/components/style/values/computed/length_percentage.rs +++ b/servo/components/style/values/computed/length_percentage.rs @@ -443,6 +443,19 @@ impl LengthPercentage { } } + /// Converts to a `<percentage>` with given basis. Returns None if the basis is 0. + #[inline] + pub fn to_percentage_of(&self, basis: Length) -> Option<Percentage> { + if basis.px() == 0. { + return None; + } + Some(match self.unpack() { + Unpacked::Length(l) => Percentage(l.px() / basis.px()), + Unpacked::Percentage(p) => p, + Unpacked::Calc(ref c) => Percentage(c.resolve(basis).px() / basis.px()), + }) + } + /// Returns the used value. #[inline] pub fn to_used_value(&self, containing_length: Au) -> Au { diff --git a/servo/components/style/values/computed/mod.rs b/servo/components/style/values/computed/mod.rs index de5db2cdab..85aadb401f 100644 --- a/servo/components/style/values/computed/mod.rs +++ b/servo/components/style/values/computed/mod.rs @@ -47,7 +47,7 @@ pub use self::angle::Angle; pub use self::animation::{ AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState, AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis, - ScrollTimelineName, TransitionProperty, ViewTimelineInset + ScrollTimelineName, TransitionBehavior, TransitionProperty, ViewTimelineInset }; pub use self::background::{BackgroundRepeat, BackgroundSize}; pub use self::basic_shape::FillRule; diff --git a/servo/components/style/values/specified/animation.rs b/servo/components/style/values/specified/animation.rs index e7bbf26fb3..5a1f5003f3 100644 --- a/servo/components/style/values/specified/animation.rs +++ b/servo/components/style/values/specified/animation.rs @@ -112,6 +112,44 @@ impl TransitionProperty { } } +/// A specified value for <transition-behavior-value>. +/// +/// https://drafts.csswg.org/css-transitions-2/#transition-behavior-property +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TransitionBehavior { + /// Transitions will not be started for discrete properties, only for interpolable properties. + Normal, + /// Transitions will be started for discrete properties as well as interpolable properties. + AllowDiscrete, +} + +impl TransitionBehavior { + /// Return normal, the initial value. + #[inline] + pub fn normal() -> Self { + Self::Normal + } + + /// Return true if it is normal. + #[inline] + pub fn is_normal(&self) -> bool { + matches!(*self, Self::Normal) + } +} + /// https://drafts.csswg.org/css-animations/#animation-iteration-count #[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem)] pub enum AnimationIterationCount { diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs index 2660864319..17f043ac58 100644 --- a/servo/components/style/values/specified/calc.rs +++ b/servo/components/style/values/specified/calc.rs @@ -6,7 +6,6 @@ //! //! [calc]: https://drafts.csswg.org/css-values/#calc-notation -use crate::color::parsing::{AngleOrNumber, NumberOrPercentage}; use crate::parser::ParserContext; use crate::values::generics::calc::{ self as generic, CalcNodeLeaf, CalcUnits, MinMaxOp, ModRemOp, PositivePercentageBasis, @@ -185,7 +184,11 @@ impl generic::CalcNodeLeaf for Leaf { let self_negative = self.is_negative(); if self_negative != other.is_negative() { - return Some(if self_negative { cmp::Ordering::Less } else { cmp::Ordering::Greater }); + return Some(if self_negative { + cmp::Ordering::Less + } else { + cmp::Ordering::Greater + }); } match (self, other) { @@ -482,7 +485,7 @@ impl CalcNode { /// Parse a top-level `calc` expression, with all nested sub-expressions. /// /// This is in charge of parsing, for example, `2 + 3 * 100%`. - fn parse<'i, 't>( + pub fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, function: MathFunction, @@ -524,13 +527,18 @@ impl CalcNode { } let value = Self::parse_argument(context, input, allowed_units)?; - input.expect_comma()?; - let step = Self::parse_argument(context, input, allowed_units)?; + + // <step> defaults to the number 1 if not provided + // https://drafts.csswg.org/css-values-4/#funcdef-round + let step = input.try_parse(|input| { + input.expect_comma()?; + Self::parse_argument(context, input, allowed_units) + }); Ok(Self::Round { strategy: strategy.unwrap_or(RoundingStrategy::Nearest), value: Box::new(value), - step: Box::new(step), + step: Box::new(step.unwrap_or(Self::Leaf(Leaf::Number(1.0)))), }) }, MathFunction::Mod | MathFunction::Rem => { @@ -1046,41 +1054,4 @@ impl CalcNode { .to_resolution() .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } - - /// Convenience parsing function for `<number>` or `<percentage>`. - pub fn parse_number_or_percentage<'i, 't>( - context: &ParserContext, - input: &mut Parser<'i, 't>, - function: MathFunction, - ) -> Result<NumberOrPercentage, ParseError<'i>> { - let node = Self::parse(context, input, function, CalcUnits::PERCENTAGE)?; - - if let Ok(value) = node.to_number() { - return Ok(NumberOrPercentage::Number { value }); - } - - match node.to_percentage() { - Ok(unit_value) => Ok(NumberOrPercentage::Percentage { unit_value }), - Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), - } - } - - /// Convenience parsing function for `<number>` or `<angle>`. - pub fn parse_angle_or_number<'i, 't>( - context: &ParserContext, - input: &mut Parser<'i, 't>, - function: MathFunction, - ) -> Result<AngleOrNumber, ParseError<'i>> { - let node = Self::parse(context, input, function, CalcUnits::ANGLE)?; - - if let Ok(angle) = node.to_angle() { - let degrees = angle.degrees(); - return Ok(AngleOrNumber::Angle { degrees }); - } - - match node.to_number() { - Ok(value) => Ok(AngleOrNumber::Number { value }), - Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), - } - } } diff --git a/servo/components/style/values/specified/color.rs b/servo/components/style/values/specified/color.rs index 3a19a2f4a3..3694b4e9bc 100644 --- a/servo/components/style/values/specified/color.rs +++ b/servo/components/style/values/specified/color.rs @@ -5,19 +5,21 @@ //! Specified color values. use super::AllowQuirks; -use crate::color::parsing::{ - self, AngleOrNumber, Color as CSSParserColor, FromParsedColor, NumberOrPercentage, -}; +use crate::color::component::ColorComponent; +use crate::color::convert::normalize_hue; +use crate::color::parsing::{self, FromParsedColor, NumberOrAngle, 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::calc::CalcUnits; use crate::values::generics::color::{ ColorMixFlags, GenericCaretColor, GenericColorMix, GenericColorOrAuto, }; -use crate::values::specified::calc::CalcNode; +use crate::values::specified::calc::{CalcNode, Leaf}; use crate::values::specified::Percentage; use crate::values::{normalize, CustomIdent}; +use cssparser::color::OPAQUE; use cssparser::{color::PredefinedColorSpace, BasicParseErrorKind, ParseErrorKind, Parser, Token}; use itoa; use std::fmt::{self, Write}; @@ -428,94 +430,217 @@ impl SystemColor { } } +impl<T> From<ColorComponent<T>> for Option<T> { + fn from(value: ColorComponent<T>) -> Self { + match value { + ColorComponent::None => None, + ColorComponent::Value(value) => Some(value), + } + } +} + +impl ColorComponent<NumberOrPercentage> { + #[inline] + fn into_alpha(self) -> Option<f32> { + match self { + ColorComponent::None => None, + ColorComponent::Value(number_or_percentage) => { + Some(normalize(number_or_percentage.to_number(1.0)).clamp(0.0, OPAQUE)) + }, + } + } +} + 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_rgba( + red: ColorComponent<u8>, + green: ColorComponent<u8>, + blue: ColorComponent<u8>, + alpha: ColorComponent<NumberOrPercentage>, + ) -> Self { + macro_rules! c { + ($c:expr) => {{ + match $c { + ColorComponent::None => 0u8, + ColorComponent::Value(value) => value, + } + }}; + } + + // Legacy rgb() doesn't support "none" alpha values and falls back to 0. + let alpha = alpha.into_alpha().unwrap_or(0.0); + + AbsoluteColor::srgb_legacy(c!(red), c!(green), c!(blue), alpha).into() } fn from_hsl( - hue: Option<f32>, - saturation: Option<f32>, - lightness: Option<f32>, - alpha: Option<f32>, + hue: ColorComponent<NumberOrAngle>, + saturation: ColorComponent<NumberOrPercentage>, + lightness: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self { - AbsoluteColor::new(ColorSpace::Hsl, hue, saturation, lightness, alpha).into() + // Percent reference range for S and L: 0% = 0.0, 100% = 100.0 + const LIGHTNESS_RANGE: f32 = 100.0; + const SATURATION_RANGE: f32 = 100.0; + + let hue = hue.map_value(|angle| normalize_hue(angle.degrees())); + let saturation = + saturation.map_value(|s| s.to_number(SATURATION_RANGE).clamp(0.0, SATURATION_RANGE)); + let lightness = + lightness.map_value(|l| l.to_number(LIGHTNESS_RANGE).clamp(0.0, LIGHTNESS_RANGE)); + + AbsoluteColor::new( + ColorSpace::Hsl, + hue, + saturation, + lightness, + alpha.into_alpha(), + ) + .into() } fn from_hwb( - hue: Option<f32>, - whiteness: Option<f32>, - blackness: Option<f32>, - alpha: Option<f32>, + hue: ColorComponent<NumberOrAngle>, + whiteness: ColorComponent<NumberOrPercentage>, + blackness: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self { - AbsoluteColor::new(ColorSpace::Hwb, hue, whiteness, blackness, alpha).into() + // Percent reference range for W and B: 0% = 0.0, 100% = 100.0 + const WHITENESS_RANGE: f32 = 100.0; + const BLACKNESS_RANGE: f32 = 100.0; + + let hue = hue.map_value(|angle| normalize_hue(angle.degrees())); + let whiteness = + whiteness.map_value(|w| w.to_number(WHITENESS_RANGE).clamp(0.0, WHITENESS_RANGE)); + let blackness = + blackness.map_value(|b| b.to_number(BLACKNESS_RANGE).clamp(0.0, BLACKNESS_RANGE)); + + AbsoluteColor::new( + ColorSpace::Hwb, + hue, + whiteness, + blackness, + alpha.into_alpha(), + ) + .into() } fn from_lab( - lightness: Option<f32>, - a: Option<f32>, - b: Option<f32>, - alpha: Option<f32>, + lightness: ColorComponent<NumberOrPercentage>, + a: ColorComponent<NumberOrPercentage>, + b: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self { - AbsoluteColor::new(ColorSpace::Lab, lightness, a, b, alpha).into() + // for L: 0% = 0.0, 100% = 100.0 + // for a and b: -100% = -125, 100% = 125 + const LIGHTNESS_RANGE: f32 = 100.0; + const A_B_RANGE: f32 = 125.0; + + let lightness = lightness.map_value(|l| l.to_number(LIGHTNESS_RANGE)); + let a = a.map_value(|a| a.to_number(A_B_RANGE)); + let b = b.map_value(|b| b.to_number(A_B_RANGE)); + + AbsoluteColor::new(ColorSpace::Lab, lightness, a, b, alpha.into_alpha()).into() } fn from_lch( - lightness: Option<f32>, - chroma: Option<f32>, - hue: Option<f32>, - alpha: Option<f32>, + lightness: ColorComponent<NumberOrPercentage>, + chroma: ColorComponent<NumberOrPercentage>, + hue: ColorComponent<NumberOrAngle>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self { - AbsoluteColor::new(ColorSpace::Lch, lightness, chroma, hue, alpha).into() + // for L: 0% = 0.0, 100% = 100.0 + // for C: 0% = 0, 100% = 150 + const LIGHTNESS_RANGE: f32 = 100.0; + const CHROMA_RANGE: f32 = 150.0; + + let lightness = lightness.map_value(|l| l.to_number(LIGHTNESS_RANGE)); + let chroma = chroma.map_value(|c| c.to_number(CHROMA_RANGE)); + let hue = hue.map_value(|angle| normalize_hue(angle.degrees())); + + AbsoluteColor::new(ColorSpace::Lch, lightness, chroma, hue, alpha.into_alpha()).into() } fn from_oklab( - lightness: Option<f32>, - a: Option<f32>, - b: Option<f32>, - alpha: Option<f32>, + lightness: ColorComponent<NumberOrPercentage>, + a: ColorComponent<NumberOrPercentage>, + b: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self { - AbsoluteColor::new(ColorSpace::Oklab, lightness, a, b, alpha).into() + // for L: 0% = 0.0, 100% = 1.0 + // for a and b: -100% = -0.4, 100% = 0.4 + const LIGHTNESS_RANGE: f32 = 1.0; + const A_B_RANGE: f32 = 0.4; + + let lightness = lightness.map_value(|l| l.to_number(LIGHTNESS_RANGE)); + let a = a.map_value(|a| a.to_number(A_B_RANGE)); + let b = b.map_value(|b| b.to_number(A_B_RANGE)); + + AbsoluteColor::new(ColorSpace::Oklab, lightness, a, b, alpha.into_alpha()).into() } fn from_oklch( - lightness: Option<f32>, - chroma: Option<f32>, - hue: Option<f32>, - alpha: Option<f32>, + lightness: ColorComponent<NumberOrPercentage>, + chroma: ColorComponent<NumberOrPercentage>, + hue: ColorComponent<NumberOrAngle>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self { - AbsoluteColor::new(ColorSpace::Oklch, lightness, chroma, hue, alpha).into() + // for L: 0% = 0.0, 100% = 1.0 + // for C: 0% = 0.0 100% = 0.4 + const LIGHTNESS_RANGE: f32 = 1.0; + const CHROMA_RANGE: f32 = 0.4; + + let lightness = lightness.map_value(|l| l.to_number(LIGHTNESS_RANGE)); + let chroma = chroma.map_value(|c| c.to_number(CHROMA_RANGE)); + let hue = hue.map_value(|angle| normalize_hue(angle.degrees())); + + AbsoluteColor::new( + ColorSpace::Oklch, + lightness, + chroma, + hue, + alpha.into_alpha(), + ) + .into() } fn from_color_function( color_space: PredefinedColorSpace, - c1: Option<f32>, - c2: Option<f32>, - c3: Option<f32>, - alpha: Option<f32>, + c1: ColorComponent<NumberOrPercentage>, + c2: ColorComponent<NumberOrPercentage>, + c3: ColorComponent<NumberOrPercentage>, + alpha: ColorComponent<NumberOrPercentage>, ) -> Self { - AbsoluteColor::new(color_space.into(), c1, c2, c3, alpha).into() + let c1 = c1.map_value(|c| c.to_number(1.0)); + let c2 = c2.map_value(|c| c.to_number(1.0)); + let c3 = c3.map_value(|c| c.to_number(1.0)); + + AbsoluteColor::new(color_space.into(), c1, c2, c3, alpha.into_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>( + fn parse_number_or_angle<'t>( &self, input: &mut Parser<'i, 't>, - ) -> Result<AngleOrNumber, ParseError<'i>> { + allow_none: bool, + ) -> Result<ColorComponent<NumberOrAngle>, ParseError<'i>> { use crate::values::specified::Angle; let location = input.current_source_location(); let token = input.next()?.clone(); - match token { + Ok(match token { + Token::Ident(ref value) if allow_none && value.eq_ignore_ascii_case("none") => { + ColorComponent::None + }, Token::Dimension { value, ref unit, .. } => { @@ -526,44 +651,125 @@ impl<'a, 'b: 'a, 'i: 'a> parsing::ColorParser<'i> for ColorParser<'a, 'b> { Err(()) => return Err(location.new_unexpected_token_error(token.clone())), }; - Ok(AngleOrNumber::Angle { degrees }) + ColorComponent::Value(NumberOrAngle::Angle { degrees }) }, - Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }), + Token::Number { value, .. } => ColorComponent::Value(NumberOrAngle::Number { value }), Token::Function(ref name) => { let function = CalcNode::math_function(self.0, name, location)?; - CalcNode::parse_angle_or_number(self.0, input, function) + let node = CalcNode::parse(self.0, input, function, CalcUnits::ANGLE)?; + + // If we can resolve the calc node, then use the value. + match node.resolve() { + Ok(Leaf::Number(value)) => { + ColorComponent::Value(NumberOrAngle::Number { value }) + }, + Ok(Leaf::Angle(angle)) => ColorComponent::Value(NumberOrAngle::Angle { + degrees: angle.degrees(), + }), + _ => { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } }, t => return Err(location.new_unexpected_token_error(t)), - } + }) } - fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> { - Ok(Percentage::parse(self.0, input)?.get()) + fn parse_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + allow_none: bool, + ) -> Result<ColorComponent<f32>, ParseError<'i>> { + let location = input.current_source_location(); + + Ok(match *input.next()? { + Token::Ident(ref value) if allow_none && value.eq_ignore_ascii_case("none") => { + ColorComponent::None + }, + Token::Percentage { unit_value, .. } => ColorComponent::Value(unit_value), + Token::Function(ref name) => { + let function = CalcNode::math_function(self.0, name, location)?; + let node = CalcNode::parse(self.0, input, function, CalcUnits::PERCENTAGE)?; + + // If we can resolve the calc node, then use the value. + let Ok(resolved_leaf) = node.resolve() else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }; + if let Leaf::Percentage(value) = resolved_leaf { + ColorComponent::Value(value) + } else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) } - fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> { - use crate::values::specified::Number; + fn parse_number<'t>( + &self, + input: &mut Parser<'i, 't>, + allow_none: bool, + ) -> Result<ColorComponent<f32>, ParseError<'i>> { + let location = input.current_source_location(); + + Ok(match *input.next()? { + Token::Ident(ref value) if allow_none && value.eq_ignore_ascii_case("none") => { + ColorComponent::None + }, + Token::Number { value, .. } => ColorComponent::Value(value), + Token::Function(ref name) => { + let function = CalcNode::math_function(self.0, name, location)?; + let node = CalcNode::parse(self.0, input, function, CalcUnits::empty())?; - Ok(Number::parse(self.0, input)?.get()) + // If we can resolve the calc node, then use the value. + let Ok(resolved_leaf) = node.resolve() else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }; + if let Leaf::Number(value) = resolved_leaf { + ColorComponent::Value(value) + } else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) } fn parse_number_or_percentage<'t>( &self, input: &mut Parser<'i, 't>, - ) -> Result<NumberOrPercentage, ParseError<'i>> { + allow_none: bool, + ) -> Result<ColorComponent<NumberOrPercentage>, ParseError<'i>> { let location = input.current_source_location(); - match *input.next()? { - Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }), + Ok(match *input.next()? { + Token::Ident(ref value) if allow_none && value.eq_ignore_ascii_case("none") => { + ColorComponent::None + }, + Token::Number { value, .. } => { + ColorComponent::Value(NumberOrPercentage::Number { value }) + }, Token::Percentage { unit_value, .. } => { - Ok(NumberOrPercentage::Percentage { unit_value }) + ColorComponent::Value(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) + let node = CalcNode::parse(self.0, input, function, CalcUnits::PERCENTAGE)?; + + // If we can resolve the calc node, then use the value. + let Ok(resolved_leaf) = node.resolve() else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }; + if let Leaf::Percentage(unit_value) = resolved_leaf { + ColorComponent::Value(NumberOrPercentage::Percentage { unit_value }) + } else if let Leaf::Number(value) = resolved_leaf { + ColorComponent::Value(NumberOrPercentage::Number { value }) + } else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } }, ref t => return Err(location.new_unexpected_token_error(t.clone())), - } + }) } } @@ -700,7 +906,7 @@ impl ToCss for Color { W: Write, { match *self { - Color::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest), + 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), @@ -772,7 +978,12 @@ impl Color { loc: &cssparser::SourceLocation, ) -> Result<Self, ParseError<'i>> { match cssparser::color::parse_hash_color(bytes) { - Ok((r, g, b, a)) => Ok(Self::from_rgba(r, g, b, a)), + Ok((r, g, b, a)) => Ok(Self::from_rgba( + r.into(), + g.into(), + b.into(), + ColorComponent::Value(NumberOrPercentage::Number { value: a }), + )), Err(()) => Err(loc.new_custom_error(StyleParseErrorKind::UnspecifiedError)), } } diff --git a/servo/components/style/values/specified/font.rs b/servo/components/style/values/specified/font.rs index 2435682ce3..db3d871a0f 100644 --- a/servo/components/style/values/specified/font.rs +++ b/servo/components/style/values/specified/font.rs @@ -450,16 +450,6 @@ impl ToComputedValue for FontStretch { } } -#[cfg(feature = "gecko")] -fn math_depth_enabled(_context: &ParserContext) -> bool { - static_prefs::pref!("layout.css.math-depth.enabled") -} - -#[cfg(feature = "servo")] -fn math_depth_enabled(_context: &ParserContext) -> bool { - false -} - /// CSS font keywords #[derive( Animate, @@ -496,7 +486,7 @@ pub enum FontSizeKeyword { XXXLarge, /// Indicate whether to apply font-size: math is specified so that extra /// scaling due to math-depth changes is applied during the cascade. - #[parse(condition = "math_depth_enabled")] + #[cfg(feature="gecko")] Math, #[css(skip)] None, @@ -1018,7 +1008,7 @@ impl FontSize { return Ok(FontSize::Length(lp)); } - if let Ok(kw) = input.try_parse(|i| FontSizeKeyword::parse(context, i)) { + if let Ok(kw) = input.try_parse(|i| FontSizeKeyword::parse(i)) { return Ok(FontSize::Keyword(KeywordInfo::new(kw))); } diff --git a/servo/components/style/values/specified/mod.rs b/servo/components/style/values/specified/mod.rs index 7fc76b3c07..1a12ca56e7 100644 --- a/servo/components/style/values/specified/mod.rs +++ b/servo/components/style/values/specified/mod.rs @@ -33,7 +33,7 @@ pub use self::angle::{AllowUnitlessZeroAngle, Angle}; pub use self::animation::{ AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState, AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis, - ScrollTimelineName, TransitionProperty, ViewTimelineInset + ScrollTimelineName, TransitionBehavior, TransitionProperty, ViewTimelineInset }; pub use self::background::{BackgroundRepeat, BackgroundSize}; pub use self::basic_shape::FillRule; |