From b6e8088e8e6116633f42f6de4583e95f42dde1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Tue, 15 Aug 2023 01:15:16 +0200 Subject: [PATCH] style: Use ColorMix for interpolated colors in the computed style rather than ComplexColorRatios This among other things preserves the right color-space when interpolating currentColor. Differential Revision: https://phabricator.services.mozilla.com/D147512 --- components/style/properties/cascade.rs | 2 +- .../style/properties/longhands/svg.mako.rs | 6 +- .../style/properties/properties.mako.rs | 2 +- components/style/values/animated/color.rs | 336 ++++++------------ components/style/values/computed/color.rs | 88 ++--- .../style/values/computed/percentage.rs | 7 + components/style/values/computed/svg.rs | 4 +- components/style/values/generics/color.rs | 293 ++++++++++++--- components/style/values/specified/color.rs | 236 +++--------- .../style/values/specified/percentage.rs | 23 +- 10 files changed, 460 insertions(+), 537 deletions(-) diff --git a/components/style/properties/cascade.rs b/components/style/properties/cascade.rs index e3383118709..3d3c6b833b8 100644 --- a/components/style/properties/cascade.rs +++ b/components/style/properties/cascade.rs @@ -418,7 +418,7 @@ fn tweak_when_ignoring_colors( fn alpha_channel(color: &Color, context: &computed::Context) -> u8 { // We assume here currentColor is opaque. - let color = color.to_computed_value(context).to_rgba(RGBA::new(0, 0, 0, 255)); + let color = color.to_computed_value(context).into_rgba(RGBA::new(0, 0, 0, 255)); color.alpha } diff --git a/components/style/properties/longhands/svg.mako.rs b/components/style/properties/longhands/svg.mako.rs index 3d711462ff3..13704e5953b 100644 --- a/components/style/properties/longhands/svg.mako.rs +++ b/components/style/properties/longhands/svg.mako.rs @@ -20,7 +20,7 @@ ${helpers.single_keyword( ${helpers.predefined_type( "stop-color", "Color", - "RGBA::new(0, 0, 0, 255).into()", + "computed::Color::black()", engines="gecko", animation_value_type="AnimatedRGBA", spec="https://www.w3.org/TR/SVGTiny12/painting.html#StopColorProperty", @@ -40,7 +40,7 @@ ${helpers.predefined_type( ${helpers.predefined_type( "flood-color", "Color", - "RGBA::new(0, 0, 0, 255).into()", + "computed::Color::black()", engines="gecko", animation_value_type="AnimatedColor", spec="https://www.w3.org/TR/SVG/filters.html#FloodColorProperty", @@ -58,7 +58,7 @@ ${helpers.predefined_type( ${helpers.predefined_type( "lighting-color", "Color", - "RGBA::new(255, 255, 255, 255).into()", + "computed::Color::white()", engines="gecko", animation_value_type="AnimatedColor", spec="https://www.w3.org/TR/SVG/filters.html#LightingColorProperty", diff --git a/components/style/properties/properties.mako.rs b/components/style/properties/properties.mako.rs index 4f430559fca..4327a739001 100644 --- a/components/style/properties/properties.mako.rs +++ b/components/style/properties/properties.mako.rs @@ -3219,7 +3219,7 @@ impl ComputedValues { /// style.resolve_color(style.get_border().clone_border_top_color()); #[inline] pub fn resolve_color(&self, color: computed::Color) -> RGBA { - color.to_rgba(self.get_inherited_text().clone_color()) + color.into_rgba(self.get_inherited_text().clone_color()) } /// Returns which longhand properties have different values in the two diff --git a/components/style/values/animated/color.rs b/components/style/values/animated/color.rs index c34bba939cd..cdfa45dc4e7 100644 --- a/components/style/values/animated/color.rs +++ b/components/style/values/animated/color.rs @@ -5,9 +5,11 @@ //! Animated types for CSS colors. use crate::values::animated::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::Percentage; use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; -use crate::values::generics::color::{Color as GenericColor, ComplexColorRatios}; -use crate::values::specified::color::{ColorInterpolationMethod, ColorSpace, HueInterpolationMethod}; +use crate::values::generics::color::{ + GenericColor, GenericColorMix, ColorInterpolationMethod, ColorSpace, HueInterpolationMethod, +}; use euclid::default::{Transform3D, Vector3D}; use std::f32::consts::PI; @@ -15,7 +17,7 @@ use std::f32::consts::PI; /// /// Unlike in computed values, each component value may exceed the /// range `[0.0, 1.0]`. -#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedZero)] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedZero, ToAnimatedValue)] #[repr(C)] pub struct RGBA { /// The red component. @@ -48,44 +50,20 @@ impl RGBA { alpha, } } - - /// Returns whether or not the colour is in gamut for sRGB. - pub fn in_gamut(&self) -> bool { - 0. <= self.red && - self.red <= 1. && - 0. <= self.green && - self.green <= 1. && - 0. <= self.blue && - self.blue <= 1. - } - - /// Returns the colour with coordinates clamped to the sRGB range. - pub fn clamp(&self) -> Self { - Self { - red: self.red.max(0.).min(1.), - green: self.green.max(0.).min(1.), - blue: self.blue.max(0.).min(1.), - alpha: self.alpha, - } - } } impl Animate for RGBA { #[inline] fn animate(&self, other: &Self, procedure: Procedure) -> Result { - let mut alpha = self.alpha.animate(&other.alpha, procedure)?; - if alpha <= 0. { - // Ideally we should return color value that only alpha component is - // 0, but this is what current gecko does. - return Ok(RGBA::transparent()); - } - - alpha = alpha.min(1.); - let red = (self.red * self.alpha).animate(&(other.red * other.alpha), procedure)?; - let green = (self.green * self.alpha).animate(&(other.green * other.alpha), procedure)?; - let blue = (self.blue * self.alpha).animate(&(other.blue * other.alpha), procedure)?; - let inv = 1. / alpha; - Ok(RGBA::new(red * inv, green * inv, blue * inv, alpha)) + let (left_weight, right_weight) = procedure.weights(); + Ok(Color::mix( + &ColorInterpolationMethod::srgb(), + self, + left_weight as f32, + other, + right_weight as f32, + /* normalize_weights = */ false, + )) } } @@ -113,41 +91,38 @@ impl ComputeSquaredDistance for RGBA { } /// An animated value for ``. -pub type Color = GenericColor; +pub type Color = GenericColor; + +/// An animated value for ``. +pub type ColorMix = GenericColorMix; impl Color { - fn effective_intermediate_rgba(&self) -> RGBA { - if self.ratios.bg == 0. { - return RGBA::transparent(); - } - - if self.ratios.bg == 1. { - return self.color; - } - - RGBA { - alpha: self.color.alpha * self.ratios.bg, - ..self.color - } + fn to_rgba(&self, current_color: RGBA) -> RGBA { + let mut clone = self.clone(); + clone.simplify(Some(¤t_color)); + *clone.as_numeric().unwrap() } /// Mix two colors into one. pub fn mix( interpolation: &ColorInterpolationMethod, - left_color: &Color, + left_color: &RGBA, mut left_weight: f32, - right_color: &Color, + right_color: &RGBA, mut right_weight: f32, - ) -> Self { + normalize_weights: bool, + ) -> RGBA { // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm - let sum = left_weight + right_weight; let mut alpha_multiplier = 1.0; - if sum != 1.0 { - let scale = 1.0 / sum; - left_weight *= scale; - right_weight *= scale; - if sum < 1.0 { - alpha_multiplier = sum; + if normalize_weights { + let sum = left_weight + right_weight; + if sum != 1.0 { + let scale = 1.0 / sum; + left_weight *= scale; + right_weight *= scale; + if sum < 1.0 { + alpha_multiplier = sum; + } } } @@ -161,202 +136,77 @@ impl Color { ColorSpace::Hsl => Self::mix_in::, ColorSpace::Lch => Self::mix_in::, }; - mix_function(left_color, left_weight, right_color, right_weight, interpolation.hue, alpha_multiplier) + mix_function( + left_color, + left_weight, + right_color, + right_weight, + interpolation.hue, + alpha_multiplier, + ) } fn mix_in( - left_color: &Color, + left_color: &RGBA, left_weight: f32, - right_color: &Color, + right_color: &RGBA, right_weight: f32, hue_interpolation: HueInterpolationMethod, alpha_multiplier: f32, - ) -> Self + ) -> RGBA where S: ModelledColor, { - let left_bg = S::from(left_color.scaled_rgba()); - let right_bg = S::from(right_color.scaled_rgba()); - - let color = S::lerp(&left_bg, left_weight, &right_bg, right_weight, hue_interpolation); - let rgba: RGBA = color.into(); - let mut rgba = if !rgba.in_gamut() { - // TODO: Better gamut mapping. - rgba.clamp() - } else { - rgba - }; + let left = S::from(*left_color); + let right = S::from(*right_color); + let color = S::lerp( + &left, + left_weight, + &right, + right_weight, + hue_interpolation, + ); + let mut rgba = RGBA::from(color.into()); if alpha_multiplier != 1.0 { rgba.alpha *= alpha_multiplier; } - let fg = left_color.ratios.fg * left_weight + right_color.ratios.fg * right_weight; - Self::new(rgba, ComplexColorRatios { bg: 1., fg }) - } - - fn scaled_rgba(&self) -> RGBA { - if self.ratios.bg == 0. { - return RGBA::transparent(); - } - - if self.ratios.bg == 1. { - return self.color; - } - - RGBA { - red: self.color.red * self.ratios.bg, - green: self.color.green * self.ratios.bg, - blue: self.color.blue * self.ratios.bg, - alpha: self.color.alpha * self.ratios.bg, - } + rgba } } impl Animate for Color { #[inline] fn animate(&self, other: &Self, procedure: Procedure) -> Result { - let self_numeric = self.is_numeric(); - let other_numeric = other.is_numeric(); - - if self_numeric && other_numeric { - return Ok(Self::rgba(self.color.animate(&other.color, procedure)?)); - } - - let self_currentcolor = self.is_currentcolor(); - let other_currentcolor = other.is_currentcolor(); - - if self_currentcolor && other_currentcolor { - let (self_weight, other_weight) = procedure.weights(); - return Ok(Self::new( - RGBA::transparent(), - ComplexColorRatios { - bg: 0., - fg: (self_weight + other_weight) as f32, - }, - )); - } - - // FIXME(emilio): Without these special cases tests fail, looks fairly - // sketchy! - if (self_currentcolor && other_numeric) || (self_numeric && other_currentcolor) { - let (self_weight, other_weight) = procedure.weights(); - return Ok(if self_numeric { - Self::new( - self.color, - ComplexColorRatios { - bg: self_weight as f32, - fg: other_weight as f32, - }, - ) - } else { - Self::new( - other.color, - ComplexColorRatios { - bg: other_weight as f32, - fg: self_weight as f32, - }, - ) - }); - } - - // Compute the "scaled" contribution for `color`. - // Each `Color`, represents a complex combination of foreground color and - // background color where fg and bg represent the overall - // contributions. ie: - // - // color = { bg * mColor, fg * foreground } - // = { bg_color , fg_color } - // = bg_color + fg_color - // - // where `foreground` is `currentcolor`, and `bg_color`, - // `fg_color` are the scaled background and foreground - // contributions. - // - // Each operation, lerp, addition, or accumulate, can be - // represented as a scaled-addition each complex color. ie: - // - // p * col1 + q * col2 - // - // where p = (1 - a), q = a for lerp(a), p = 1, q = 1 for - // addition, etc. - // - // Therefore: - // - // col1 op col2 - // = p * col1 + q * col2 - // = p * { bg_color1, fg_color1 } + q * { bg_color2, fg_color2 } - // = p * (bg_color1 + fg_color1) + q * (bg_color2 + fg_color2) - // = p * bg_color1 + p * fg_color1 + q * bg_color2 + p * fg_color2 - // = (p * bg_color1 + q * bg_color2) + (p * fg_color1 + q * fg_color2) - // = (bg_color1 op bg_color2) + (fg_color1 op fg_color2) - // - // fg_color1 op fg_color2 is equivalent to (fg1 op fg2) * foreground, - // so the final color is: - // - // = { bg_color, fg_color } - // = { 1 * (bg_color1 op bg_color2), (fg1 op fg2) * foreground } - // - // To perform the operation on two complex colors, we need to - // generate the scaled contributions of each background color - // component. - let bg_color1 = self.scaled_rgba(); - let bg_color2 = other.scaled_rgba(); - - // Perform bg_color1 op bg_color2 - let bg_color = bg_color1.animate(&bg_color2, procedure)?; - - // Calculate the final foreground color ratios; perform - // animation on effective fg ratios. - let fg = self.ratios.fg.animate(&other.ratios.fg, procedure)?; - - Ok(Self::new(bg_color, ComplexColorRatios { bg: 1., fg })) + let (left_weight, right_weight) = procedure.weights(); + let mut color = Color::ColorMix(Box::new(ColorMix { + interpolation: ColorInterpolationMethod::srgb(), + left: self.clone(), + left_percentage: Percentage(left_weight as f32), + right: other.clone(), + right_percentage: Percentage(right_weight as f32), + // See https://github.com/w3c/csswg-drafts/issues/7324 + normalize_weights: false, + })); + color.simplify(None); + Ok(color) } } impl ComputeSquaredDistance for Color { #[inline] fn compute_squared_distance(&self, other: &Self) -> Result { - // All comments from the Animate impl also apply here. - let self_numeric = self.is_numeric(); - let other_numeric = other.is_numeric(); - - if self_numeric && other_numeric { - return self.color.compute_squared_distance(&other.color); - } - - let self_currentcolor = self.is_currentcolor(); - let other_currentcolor = other.is_currentcolor(); - if self_currentcolor && other_currentcolor { - return Ok(SquaredDistance::from_sqrt(0.)); - } - - if (self_currentcolor && other_numeric) || (self_numeric && other_currentcolor) { - let color = if self_numeric { - &self.color - } else { - &other.color - }; - // `computed_squared_distance` is symmetric. - return Ok(color.compute_squared_distance(&RGBA::transparent())? + - SquaredDistance::from_sqrt(1.)); - } - - let self_color = self.effective_intermediate_rgba(); - let other_color = other.effective_intermediate_rgba(); - let self_ratios = self.ratios; - let other_ratios = other.ratios; - - Ok(self_color.compute_squared_distance(&other_color)? + - self_ratios.bg.compute_squared_distance(&other_ratios.bg)? + - self_ratios.fg.compute_squared_distance(&other_ratios.fg)?) + let current_color = RGBA::transparent(); + self.to_rgba(current_color) + .compute_squared_distance(&other.to_rgba(current_color)) } } impl ToAnimatedZero for Color { #[inline] fn to_animated_zero(&self) -> Result { - Ok(RGBA::transparent().into()) + Ok(Color::rgba(RGBA::transparent())) } } @@ -488,9 +338,23 @@ fn interpolate_premultiplied( for i in 0..3 { let is_hue = hue_index == Some(i); result[i] = if is_hue { - interpolate_hue(left[i], left_weight, right[i], right_weight, hue_interpolation) + interpolate_hue( + left[i], + left_weight, + right[i], + right_weight, + hue_interpolation, + ) } else { - interpolate_premultiplied_component(left[i], left_weight, left_alpha, right[i], right_weight, right_alpha, inverse_of_result_alpha) + interpolate_premultiplied_component( + left[i], + left_weight, + left_alpha, + right[i], + right_weight, + right_alpha, + inverse_of_result_alpha, + ) }; } result[3] = result_alpha; @@ -524,7 +388,7 @@ macro_rules! impl_lerp { } } } - } + }; } impl_lerp!(RGBA, None); @@ -614,7 +478,12 @@ impl_lerp!(HSLA, Some(0)); // // We also return min/max for the hwb conversion. fn rgb_to_hsl(rgba: RGBA) -> (HSLA, f32, f32) { - let RGBA { red, green, blue, alpha } = rgba; + let RGBA { + red, + green, + blue, + alpha, + } = rgba; let max = red.max(green).max(blue); let min = red.min(green).min(blue); let mut hue = std::f32::NAN; @@ -640,7 +509,16 @@ fn rgb_to_hsl(rgba: RGBA) -> (HSLA, f32, f32) { hue *= 60.; } - (HSLA { hue, sat, light, alpha }, min, max) + ( + HSLA { + hue, + sat, + light, + alpha, + }, + min, + max, + ) } impl From for HSLA { @@ -750,7 +628,7 @@ impl From for XYZD65A { 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0., 0., 0., 0., 1., ); - let d65 = BRADFORD_INVERSE.transform_vector3d(Vector3D::new(d50.x, d50.y, d50.z)); + let d65 = BRADFORD_INVERSE.transform_vector3d(Vector3D::new(d50.x, d50.y, d50.z)); Self { x: d65.x, y: d65.y, diff --git a/components/style/values/computed/color.rs b/components/style/values/computed/color.rs index 7610bfbba3b..573cb6fe500 100644 --- a/components/style/values/computed/color.rs +++ b/components/style/values/computed/color.rs @@ -7,6 +7,7 @@ use crate::values::animated::color::RGBA as AnimatedRGBA; use crate::values::animated::ToAnimatedValue; use crate::values::generics::color::{GenericCaretColor, GenericColor, GenericColorOrAuto}; +use crate::values::computed::percentage::Percentage; use cssparser::{Color as CSSParserColor, RGBA}; use std::fmt; use style_traits::{CssWriter, ToCss}; @@ -20,7 +21,20 @@ pub type ColorPropertyValue = RGBA; pub type MozFontSmoothingBackgroundColor = RGBA; /// A computed value for ``. -pub type Color = GenericColor; +pub type Color = GenericColor; + +impl ToCss for Color { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + match *self { + Self::Numeric(ref c) => c.to_css(dest), + Self::CurrentColor => CSSParserColor::CurrentColor.to_css(dest), + Self::ColorMix(ref m) => m.to_css(dest), + } + } +} impl Color { /// Returns a complex color value representing transparent. @@ -28,67 +42,21 @@ impl Color { Color::rgba(RGBA::transparent()) } - /// Combine this complex color with the given foreground color into - /// a numeric RGBA color. It currently uses linear blending. - pub fn to_rgba(&self, fg_color: RGBA) -> RGBA { - // Common cases that the complex color is either pure numeric color or - // pure currentcolor. - if self.is_numeric() { - return self.color; - } - - if self.is_currentcolor() { - return fg_color; - } - - let ratios = &self.ratios; - let color = &self.color; - - // For the more complicated case that the alpha value differs, - // we use the following formula to compute the components: - // alpha = self_alpha * bg_ratio + fg_alpha * fg_ratio - // color = (self_color * self_alpha * bg_ratio + - // fg_color * fg_alpha * fg_ratio) / alpha - - let p1 = ratios.bg; - let a1 = color.alpha_f32(); - let r1 = a1 * color.red_f32(); - let g1 = a1 * color.green_f32(); - let b1 = a1 * color.blue_f32(); - - let p2 = ratios.fg; - let a2 = fg_color.alpha_f32(); - let r2 = a2 * fg_color.red_f32(); - let g2 = a2 * fg_color.green_f32(); - let b2 = a2 * fg_color.blue_f32(); - - let a = p1 * a1 + p2 * a2; - if a <= 0. { - return RGBA::transparent(); - } - let a = a.min(1.); - - let inv = 1. / a; - - let r = (p1 * r1 + p2 * r2) * inv; - let g = (p1 * g1 + p2 * g2) * inv; - let b = (p1 * b1 + p2 * b2) * inv; - RGBA::from_floats(r, g, b, a) + /// Returns opaque black. + pub fn black() -> Color { + Color::rgba(RGBA::new(0, 0, 0, 255)) } -} -impl ToCss for Color { - fn to_css(&self, dest: &mut CssWriter) -> fmt::Result - where - W: fmt::Write, - { - if self.is_currentcolor() { - return CSSParserColor::CurrentColor.to_css(dest); - } - if self.is_numeric() { - return self.color.to_css(dest); - } - Ok(()) + /// Returns opaque white. + pub fn white() -> Color { + Color::rgba(RGBA::new(255, 255, 255, 255)) + } + + /// Combine this complex color with the given foreground color into + /// a numeric RGBA color. + pub fn into_rgba(mut self, current_color: RGBA) -> RGBA { + self.simplify(Some(¤t_color)); + *self.as_numeric().unwrap() } } diff --git a/components/style/values/computed/percentage.rs b/components/style/values/computed/percentage.rs index 7430d82d471..4e9732ade2c 100644 --- a/components/style/values/computed/percentage.rs +++ b/components/style/values/computed/percentage.rs @@ -6,6 +6,7 @@ use crate::values::animated::ToAnimatedValue; use crate::values::generics::NonNegative; +use crate::values::specified::percentage::ToPercentage; use crate::values::{serialize_percentage, CSSFloat}; use crate::Zero; use std::fmt; @@ -64,6 +65,12 @@ impl Zero for Percentage { } } +impl ToPercentage for Percentage { + fn to_percentage(&self) -> CSSFloat { + self.0 + } +} + impl std::ops::AddAssign for Percentage { fn add_assign(&mut self, other: Self) { self.0 += other.0 diff --git a/components/style/values/computed/svg.rs b/components/style/values/computed/svg.rs index 344f1d83518..640c3bfda70 100644 --- a/components/style/values/computed/svg.rs +++ b/components/style/values/computed/svg.rs @@ -8,7 +8,6 @@ use crate::values::computed::color::Color; use crate::values::computed::url::ComputedUrl; use crate::values::computed::{LengthPercentage, NonNegativeLengthPercentage, Opacity}; use crate::values::generics::svg as generic; -use crate::values::RGBA; use crate::Zero; pub use crate::values::specified::{DProperty, MozContextProperties, SVGPaintOrder}; @@ -22,9 +21,8 @@ pub type SVGPaintKind = generic::GenericSVGPaintKind; impl SVGPaint { /// Opaque black color pub fn black() -> Self { - let rgba = RGBA::from_floats(0., 0., 0., 1.).into(); SVGPaint { - kind: generic::SVGPaintKind::Color(rgba), + kind: generic::SVGPaintKind::Color(Color::black()), fallback: generic::SVGPaintFallback::Unset, } } diff --git a/components/style/values/generics/color.rs b/components/style/values/generics/color.rs index 5b477dee60d..7ab3880bc1a 100644 --- a/components/style/values/generics/color.rs +++ b/components/style/values/generics/color.rs @@ -4,81 +4,268 @@ //! Generic types for color properties. -/// Ratios representing the contribution of color and currentcolor to -/// the final color value. -/// -/// NOTE(emilio): For animated colors, the sum of these two might be more than -/// one (because the background color would've been scaled down already). So -/// beware that it is not generally safe to assume that if bg is 1 then fg is 0, -/// for example. -#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] -#[repr(C)] -pub struct ComplexColorRatios { - /// Numeric color contribution. - pub bg: f32, - /// currentcolor contribution. - pub fg: f32, -} - -impl ComplexColorRatios { - /// Ratios representing a `Numeric` color. - pub const NUMERIC: ComplexColorRatios = ComplexColorRatios { bg: 1., fg: 0. }; - /// Ratios representing the `CurrentColor` color. - pub const CURRENT_COLOR: ComplexColorRatios = ComplexColorRatios { bg: 0., fg: 1. }; -} +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; +use crate::values::{Parse, ParserContext, Parser}; +use crate::values::specified::percentage::ToPercentage; +use crate::values::animated::ToAnimatedValue; +use crate::values::animated::color::RGBA as AnimatedRGBA; /// This struct represents a combined color from a numeric color and /// the current foreground color (currentcolor keyword). -#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] #[repr(C)] -pub struct GenericColor { +pub enum GenericColor { /// The actual numeric color. - pub color: RGBA, - /// The ratios of mixing between numeric and currentcolor. - /// The formula is: `color * ratios.bg + currentcolor * ratios.fg`. - pub ratios: ComplexColorRatios, + Numeric(RGBA), + /// The `CurrentColor` keyword. + CurrentColor, + /// The color-mix() function. + ColorMix(Box>), +} + +/// A color space as defined in [1]. +/// +/// [1]: https://drafts.csswg.org/css-color-4/#typedef-color-space +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToAnimatedValue, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +pub enum ColorSpace { + /// The sRGB color space. + Srgb, + /// The linear-sRGB color space. + LinearSrgb, + /// The CIEXYZ color space. + #[parse(aliases = "xyz-d65")] + Xyz, + /// https://drafts.csswg.org/css-color-4/#valdef-color-xyz + XyzD50, + /// The CIELAB color space. + Lab, + /// https://drafts.csswg.org/css-color-4/#valdef-hsl-hsl + Hsl, + /// https://drafts.csswg.org/css-color-4/#valdef-hwb-hwb + Hwb, + /// The CIELAB color space, expressed in cylindrical coordinates. + Lch, + // TODO: Oklab, Lch +} + +impl ColorSpace { + /// Returns whether this is a ``. + pub fn is_polar(self) -> bool { + match self { + Self::Srgb | Self::LinearSrgb | Self::Xyz | Self::XyzD50 | Self::Lab => false, + Self::Hsl | Self::Hwb | Self::Lch => true, + } + } +} + +/// A hue-interpolation-method as defined in [1]. +/// +/// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToAnimatedValue, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +pub enum HueInterpolationMethod { + /// https://drafts.csswg.org/css-color-4/#shorter + Shorter, + /// https://drafts.csswg.org/css-color-4/#longer + Longer, + /// https://drafts.csswg.org/css-color-4/#increasing + Increasing, + /// https://drafts.csswg.org/css-color-4/#decreasing + Decreasing, + /// https://drafts.csswg.org/css-color-4/#specified + Specified, +} + +/// https://drafts.csswg.org/css-color-4/#color-interpolation-method +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem, ToAnimatedValue, ToComputedValue, ToResolvedValue)] +#[repr(C)] +pub struct ColorInterpolationMethod { + /// The color-space the interpolation should be done in. + pub space: ColorSpace, + /// The hue interpolation method. + pub hue: HueInterpolationMethod, +} + +impl ColorInterpolationMethod { + /// Returns the srgb interpolation method. + pub fn srgb() -> Self { + Self { + space: ColorSpace::Srgb, + hue: HueInterpolationMethod::Shorter, + } + } +} + +impl Parse for ColorInterpolationMethod { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_ident_matching("in")?; + let space = ColorSpace::parse(input)?; + // https://drafts.csswg.org/css-color-4/#hue-interpolation + // Unless otherwise specified, if no specific hue interpolation + // algorithm is selected by the host syntax, the default is shorter. + let hue = if space.is_polar() { + input.try_parse(|input| -> Result<_, ParseError<'i>> { + let hue = HueInterpolationMethod::parse(input)?; + input.expect_ident_matching("hue")?; + Ok(hue) + }).unwrap_or(HueInterpolationMethod::Shorter) + } else { + HueInterpolationMethod::Shorter + }; + Ok(Self { space, hue }) + } +} + +impl ToCss for ColorInterpolationMethod { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + dest.write_str("in ")?; + self.space.to_css(dest)?; + if self.hue != HueInterpolationMethod::Shorter { + dest.write_char(' ')?; + self.hue.to_css(dest)?; + dest.write_str(" hue")?; + } + Ok(()) + } +} + +/// A restricted version of the css `color-mix()` function, which only supports +/// percentages. +/// +/// https://drafts.csswg.org/css-color-5/#color-mix +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToComputedValue, ToResolvedValue, ToShmem)] +#[allow(missing_docs)] +#[repr(C)] +pub struct GenericColorMix { + pub interpolation: ColorInterpolationMethod, + pub left: Color, + pub left_percentage: Percentage, + pub right: Color, + pub right_percentage: Percentage, + pub normalize_weights: bool, +} + +pub use self::GenericColorMix as ColorMix; + +impl ToCss for ColorMix { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + fn can_omit(percent: &Percentage, other: &Percentage, is_left: bool) -> bool { + if percent.is_calc() { + return false; + } + if percent.to_percentage() == 0.5 { + return other.to_percentage() == 0.5; + } + if is_left { + return false; + } + (1.0 - percent.to_percentage() - other.to_percentage()).abs() <= f32::EPSILON + } + + dest.write_str("color-mix(")?; + self.interpolation.to_css(dest)?; + dest.write_str(", ")?; + self.left.to_css(dest)?; + if !can_omit(&self.left_percentage, &self.right_percentage, true) { + dest.write_str(" ")?; + self.left_percentage.to_css(dest)?; + } + dest.write_str(", ")?; + self.right.to_css(dest)?; + if !can_omit(&self.right_percentage, &self.left_percentage, false) { + dest.write_str(" ")?; + self.right_percentage.to_css(dest)?; + } + dest.write_str(")") + } +} + +impl ColorMix, Percentage> { + fn to_rgba(&self) -> Option + where + RGBA: Clone + ToAnimatedValue, + Percentage: ToPercentage, + { + use crate::values::animated::color::Color as AnimatedColor; + let left = self.left.as_numeric()?.clone().to_animated_value(); + let right = self.right.as_numeric()?.clone().to_animated_value(); + Some(ToAnimatedValue::from_animated_value(AnimatedColor::mix( + &self.interpolation, + &left, + self.left_percentage.to_percentage(), + &right, + self.right_percentage.to_percentage(), + self.normalize_weights, + ))) + } } pub use self::GenericColor as Color; -impl Color { - /// Returns a color value representing currentcolor. - pub fn currentcolor() -> Self { - Color { - color: cssparser::RGBA::transparent(), - ratios: ComplexColorRatios::CURRENT_COLOR, +impl Color { + /// Returns the numeric rgba value if this color is numeric, or None + /// otherwise. + pub fn as_numeric(&self) -> Option<&RGBA> { + match *self { + Self::Numeric(ref rgba) => Some(rgba), + _ => None, } } -} -impl Color { - /// Create a color based upon the specified ratios. - pub fn new(color: RGBA, ratios: ComplexColorRatios) -> Self { - Self { color, ratios } + /// Simplifies the color-mix()es to the extent possible given a current + /// color (or not). + pub fn simplify(&mut self, current_color: Option<&RGBA>) + where + RGBA: Clone + ToAnimatedValue, + Percentage: ToPercentage, + { + match *self { + Self::Numeric(..) => {}, + Self::CurrentColor => { + if let Some(c) = current_color { + *self = Self::Numeric(c.clone()); + } + }, + Self::ColorMix(ref mut mix) => { + mix.left.simplify(current_color); + mix.right.simplify(current_color); + + if let Some(mix) = mix.to_rgba() { + *self = Self::Numeric(mix); + } + }, + } + } + + /// Returns a color value representing currentcolor. + pub fn currentcolor() -> Self { + Self::CurrentColor } /// Returns a numeric color representing the given RGBA value. pub fn rgba(color: RGBA) -> Self { - Self { - color, - ratios: ComplexColorRatios::NUMERIC, - } - } - - /// Whether it is a numeric color (no currentcolor component). - pub fn is_numeric(&self) -> bool { - self.ratios == ComplexColorRatios::NUMERIC + Self::Numeric(color) } /// Whether it is a currentcolor value (no numeric color component). pub fn is_currentcolor(&self) -> bool { - self.ratios == ComplexColorRatios::CURRENT_COLOR + matches!(*self, Self::CurrentColor) } -} -impl From for Color { - fn from(color: RGBA) -> Self { - Self::rgba(color) + /// Whether it is a numeric color (no currentcolor component). + pub fn is_numeric(&self) -> bool { + matches!(*self, Self::Numeric(..)) } } diff --git a/components/style/values/specified/color.rs b/components/style/values/specified/color.rs index f751ede9a2e..493c2cf8c85 100644 --- a/components/style/values/specified/color.rs +++ b/components/style/values/specified/color.rs @@ -7,7 +7,7 @@ use super::AllowQuirks; use crate::parser::{Parse, ParserContext}; use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; -use crate::values::generics::color::{GenericCaretColor, GenericColorOrAuto}; +use crate::values::generics::color::{ColorInterpolationMethod, GenericColorMix, GenericCaretColor, GenericColorOrAuto}; use crate::values::specified::calc::CalcNode; use crate::values::specified::Percentage; use crate::values::CustomIdent; @@ -19,119 +19,8 @@ use std::io::Write as IoWrite; use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind}; use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind}; -/// A color space as defined in [1]. -/// -/// [1]: https://drafts.csswg.org/css-color-4/#typedef-color-space -#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] -pub enum ColorSpace { - /// The sRGB color space. - Srgb, - /// The linear-sRGB color space. - LinearSrgb, - /// The CIEXYZ color space. - #[parse(aliases = "xyz-d65")] - Xyz, - /// https://drafts.csswg.org/css-color-4/#valdef-color-xyz - XyzD50, - /// The CIELAB color space. - Lab, - /// https://drafts.csswg.org/css-color-4/#valdef-hsl-hsl - Hsl, - /// https://drafts.csswg.org/css-color-4/#valdef-hwb-hwb - Hwb, - /// The CIELAB color space, expressed in cylindrical coordinates. - Lch, - // TODO: Oklab, Lch -} - -impl ColorSpace { - /// Returns whether this is a ``. - pub fn is_polar(self) -> bool { - match self { - Self::Srgb | Self::LinearSrgb | Self::Xyz | Self::XyzD50 | Self::Lab => false, - Self::Hsl | Self::Hwb | Self::Lch => true, - } - } -} - -/// A hue-interpolation-method as defined in [1]. -/// -/// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method -#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] -pub enum HueInterpolationMethod { - /// https://drafts.csswg.org/css-color-4/#shorter - Shorter, - /// https://drafts.csswg.org/css-color-4/#longer - Longer, - /// https://drafts.csswg.org/css-color-4/#increasing - Increasing, - /// https://drafts.csswg.org/css-color-4/#decreasing - Decreasing, - /// https://drafts.csswg.org/css-color-4/#specified - Specified, -} - -/// https://drafts.csswg.org/css-color-4/#color-interpolation-method -#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] -pub struct ColorInterpolationMethod { - /// The color-space the interpolation should be done in. - pub space: ColorSpace, - /// The hue interpolation method. - pub hue: HueInterpolationMethod, -} - -impl Parse for ColorInterpolationMethod { - fn parse<'i, 't>( - _: &ParserContext, - input: &mut Parser<'i, 't>, - ) -> Result> { - input.expect_ident_matching("in")?; - let space = ColorSpace::parse(input)?; - // https://drafts.csswg.org/css-color-4/#hue-interpolation - // Unless otherwise specified, if no specific hue interpolation - // algorithm is selected by the host syntax, the default is shorter. - let hue = if space.is_polar() { - input.try_parse(|input| -> Result<_, ParseError<'i>> { - let hue = HueInterpolationMethod::parse(input)?; - input.expect_ident_matching("hue")?; - Ok(hue) - }).unwrap_or(HueInterpolationMethod::Shorter) - } else { - HueInterpolationMethod::Shorter - }; - Ok(Self { space, hue }) - } -} - -impl ToCss for ColorInterpolationMethod { - fn to_css(&self, dest: &mut CssWriter) -> fmt::Result - where - W: Write, - { - dest.write_str("in ")?; - self.space.to_css(dest)?; - if self.hue != HueInterpolationMethod::Shorter { - dest.write_char(' ')?; - self.hue.to_css(dest)?; - dest.write_str(" hue")?; - } - Ok(()) - } -} - -/// A restricted version of the css `color-mix()` function, which only supports -/// percentages. -/// -/// https://drafts.csswg.org/css-color-5/#color-mix -#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] -#[allow(missing_docs)] -pub struct ColorMix { - pub interpolation: ColorInterpolationMethod, - pub left: Color, - pub left_percentage: Percentage, - pub right: Color, - pub right_percentage: Percentage, -} +/// A specified color-mix(). +pub type ColorMix = GenericColorMix; #[inline] fn allow_color_mix() -> bool { @@ -160,7 +49,9 @@ impl Parse for ColorMix { input.expect_comma()?; let try_parse_percentage = |input: &mut Parser| -> Option { - input.try_parse(|input| Percentage::parse_zero_to_a_hundred(context, input)).ok() + input + .try_parse(|input| Percentage::parse_zero_to_a_hundred(context, input)) + .ok() }; let mut left_percentage = try_parse_percentage(input); @@ -180,9 +71,8 @@ impl Parse for ColorMix { right_percentage = try_parse_percentage(input); } - let right_percentage = right_percentage.unwrap_or_else(|| { - Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get())) - }); + let right_percentage = right_percentage + .unwrap_or_else(|| Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get()))); let left_percentage = left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get())); @@ -198,47 +88,12 @@ impl Parse for ColorMix { left_percentage, right, right_percentage, + normalize_weights: true, }) }) } } -impl ToCss for ColorMix { - fn to_css(&self, dest: &mut CssWriter) -> fmt::Result - where - W: Write, - { - fn can_omit(percent: &Percentage, other: &Percentage, is_left: bool) -> bool { - if percent.is_calc() { - return false; - } - if percent.get() == 0.5 { - return other.get() == 0.5; - } - if is_left { - return false; - } - (1.0 - percent.get() - other.get()).abs() <= f32::EPSILON - } - - dest.write_str("color-mix(")?; - self.interpolation.to_css(dest)?; - dest.write_str(", ")?; - self.left.to_css(dest)?; - if !can_omit(&self.left_percentage, &self.right_percentage, true) { - dest.write_str(" ")?; - self.left_percentage.to_css(dest)?; - } - dest.write_str(", ")?; - self.right.to_css(dest)?; - if !can_omit(&self.right_percentage, &self.left_percentage, false) { - dest.write_str(" ")?; - self.right_percentage.to_css(dest)?; - } - dest.write_str(")") - } -} - /// Specified color value #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub enum Color { @@ -251,8 +106,6 @@ pub enum Color { /// Authored representation authored: Option>, }, - /// A complex color value from computed value - Complex(ComputedColor), /// A system color. #[cfg(feature = "gecko")] System(SystemColor), @@ -640,8 +493,6 @@ impl ToCss for Color { Color::Numeric { parsed: ref rgba, .. } => rgba.to_css(dest), - // TODO: Could represent this as a color-mix() instead. - Color::Complex(_) => Ok(()), Color::ColorMix(ref mix) => mix.to_css(dest), #[cfg(feature = "gecko")] Color::System(system) => system.to_css(dest), @@ -787,22 +638,23 @@ impl Color { /// the context to resolve, then `None` is returned. pub fn to_computed_color(&self, context: Option<&Context>) -> Option { Some(match *self { - Color::CurrentColor => ComputedColor::currentcolor(), - Color::Numeric { ref parsed, .. } => ComputedColor::rgba(*parsed), - Color::Complex(ref complex) => *complex, + Color::CurrentColor => ComputedColor::CurrentColor, + Color::Numeric { ref parsed, .. } => ComputedColor::Numeric(*parsed), Color::ColorMix(ref mix) => { - use crate::values::animated::color::Color as AnimatedColor; - use crate::values::animated::ToAnimatedValue; + use crate::values::computed::percentage::Percentage; - let left = mix.left.to_computed_color(context)?.to_animated_value(); - let right = mix.right.to_computed_color(context)?.to_animated_value(); - ToAnimatedValue::from_animated_value(AnimatedColor::mix( - &mix.interpolation, - &left, - mix.left_percentage.get(), - &right, - mix.right_percentage.get(), - )) + let left = mix.left.to_computed_color(context)?; + let right = mix.right.to_computed_color(context)?; + let mut color = ComputedColor::ColorMix(Box::new(GenericColorMix { + interpolation: mix.interpolation, + left, + left_percentage: Percentage(mix.left_percentage.get()), + right, + right_percentage: Percentage(mix.right_percentage.get()), + normalize_weights: mix.normalize_weights, + })); + color.simplify(None); + color }, #[cfg(feature = "gecko")] Color::System(system) => system.compute(context?), @@ -820,13 +672,13 @@ impl ToComputedValue for Color { } fn from_computed_value(computed: &ComputedColor) -> Self { - if computed.is_numeric() { - return Color::rgba(computed.color); + match *computed { + ComputedColor::Numeric(ref color) => Color::rgba(*color), + ComputedColor::CurrentColor => Color::CurrentColor, + ComputedColor::ColorMix(ref mix) => { + Color::ColorMix(Box::new(ToComputedValue::from_computed_value(&**mix))) + }, } - if computed.is_currentcolor() { - return Color::currentcolor(); - } - Color::Complex(*computed) } } @@ -854,7 +706,7 @@ impl ToComputedValue for MozFontSmoothingBackgroundColor { fn to_computed_value(&self, context: &Context) -> RGBA { self.0 .to_computed_value(context) - .to_rgba(RGBA::transparent()) + .into_rgba(RGBA::transparent()) } fn from_computed_value(computed: &RGBA) -> Self { @@ -871,7 +723,15 @@ impl SpecifiedValueInfo for Color { // should probably be handled that way as well. // XXX `currentColor` should really be `currentcolor`. But let's // keep it consistent with the old system for now. - f(&["rgb", "rgba", "hsl", "hsla", "hwb", "currentColor", "transparent"]); + f(&[ + "rgb", + "rgba", + "hsl", + "hsla", + "hwb", + "currentColor", + "transparent", + ]); } } @@ -888,7 +748,7 @@ impl ToComputedValue for ColorPropertyValue { fn to_computed_value(&self, context: &Context) -> RGBA { self.0 .to_computed_value(context) - .to_rgba(context.builder.get_parent_inherited_text().clone_color()) + .into_rgba(context.builder.get_parent_inherited_text().clone_color()) } #[inline] @@ -1050,7 +910,19 @@ impl ToCss for ColorScheme { } /// https://drafts.csswg.org/css-color-adjust/#print-color-adjust -#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem)] +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] #[repr(u8)] pub enum PrintColorAdjust { /// Ignore backgrounds and darken text. diff --git a/components/style/values/specified/percentage.rs b/components/style/values/specified/percentage.rs index d1086aae300..1f92c6c3e6a 100644 --- a/components/style/values/specified/percentage.rs +++ b/components/style/values/specified/percentage.rs @@ -92,11 +92,6 @@ impl Percentage { Number::new_with_clamping_mode(self.value, self.calc_clamping_mode) } - /// Returns whether this percentage is a `calc()` value. - pub fn is_calc(&self) -> bool { - self.calc_clamping_mode.is_some() - } - /// Returns the calc() clamping mode for this percentage. pub fn calc_clamping_mode(&self) -> Option { self.calc_clamping_mode @@ -188,6 +183,24 @@ impl ToComputedValue for Percentage { impl SpecifiedValueInfo for Percentage {} +/// Turns the percentage into a plain float. +pub trait ToPercentage { + /// Returns whether this percentage used to be a calc(). + fn is_calc(&self) -> bool { false } + /// Turns the percentage into a plain float. + fn to_percentage(&self) -> CSSFloat; +} + +impl ToPercentage for Percentage { + fn is_calc(&self) -> bool { + self.calc_clamping_mode.is_some() + } + + fn to_percentage(&self) -> CSSFloat { + self.get() + } +} + /// A wrapper of Percentage, whose value must be >= 0. pub type NonNegativePercentage = NonNegative;