From 980d10fc172566a702efc66704a9b17e0545877a Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Tue, 16 May 2023 07:26:09 +0200 Subject: [PATCH] style: Implement basic color-mix() functionality, behind a pref, but exposed to chrome code This is straight-forward and builds on the color animation code. This implements only the syntax, not the whole syntax, which seems fairly more complex. Of course, this only uses sRGB because that's all the colors we support, but it should be feasible to extend to lab() / lch() colors once we support those. I believe this subset of syntax is useful and worth implementing, so people can play with it and say if it's useful. Differential Revision: https://phabricator.services.mozilla.com/D106698 --- components/style/properties/cascade.rs | 44 +++------- components/style/values/animated/color.rs | 5 ++ components/style/values/specified/color.rs | 99 ++++++++++++++++++++-- 3 files changed, 109 insertions(+), 39 deletions(-) diff --git a/components/style/properties/cascade.rs b/components/style/properties/cascade.rs index f7ab9f95fde..275ed7afc38 100644 --- a/components/style/properties/cascade.rs +++ b/components/style/properties/cascade.rs @@ -379,13 +379,15 @@ where type DeclarationsToApplyUnlessOverriden = SmallVec<[PropertyDeclaration; 2]>; fn tweak_when_ignoring_colors( - builder: &StyleBuilder, + context: &computed::Context, longhand_id: LonghandId, origin: Origin, declaration: &mut Cow, declarations_to_apply_unless_overriden: &mut DeclarationsToApplyUnlessOverriden, ) { use crate::values::specified::Color; + use crate::values::computed::ToComputedValue; + use cssparser::RGBA; if !longhand_id.ignored_when_document_colors_disabled() { return; @@ -399,34 +401,16 @@ fn tweak_when_ignoring_colors( // Don't override background-color on ::-moz-color-swatch. It is set as an // author style (via the style attribute), but it's pretty important for it // to show up for obvious reasons :) - if builder.pseudo.map_or(false, |p| p.is_color_swatch()) && + if context.builder.pseudo.map_or(false, |p| p.is_color_swatch()) && longhand_id == LonghandId::BackgroundColor { return; } - fn alpha_channel(color: &Color) -> u8 { - match *color { - // Seems safe enough to assume that the default color and system - // colors are opaque in HCM, though maybe we shouldn't asume the - // later? - #[cfg(feature = "gecko")] - Color::InheritFromBodyQuirk | Color::System(..) => 255, - // We don't have the actual color here, but since except for color: - // transparent we force opaque text colors, it seems sane to do - // this. You can technically fool this bit of code with: - // - // color: transparent; background-color: currentcolor; - // - // but this is best-effort, and that seems unlikely to happen in - // practice. - Color::CurrentColor => 255, - // Complex colors are results of interpolation only and probably - // shouldn't show up around here in HCM, but we've always treated - // them as opaque effectively so keep doing it. - Color::Complex { .. } => 255, - Color::Numeric { ref parsed, .. } => parsed.alpha, - } + 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)); + color.alpha } // A few special-cases ahead. @@ -440,9 +424,9 @@ fn tweak_when_ignoring_colors( // should consider not doing that even if it causes some issues like // bug 1625036, or finding a performant way to preserve the original // widget background color's rgb channels but not alpha... - let alpha = alpha_channel(color); + let alpha = alpha_channel(color, context); if alpha != 0 { - let mut color = builder.device.default_background_color(); + let mut color = context.builder.device.default_background_color(); color.alpha = alpha; declarations_to_apply_unless_overriden .push(PropertyDeclaration::BackgroundColor(color.into())) @@ -450,14 +434,14 @@ fn tweak_when_ignoring_colors( }, PropertyDeclaration::Color(ref color) => { // We honor color: transparent, and "revert-or-initial" otherwise. - if alpha_channel(&color.0) == 0 { + if alpha_channel(&color.0, context) == 0 { return; } // If the inherited color would be transparent, but we would // override this with a non-transparent color, then override it with // the default color. Otherwise just let it inherit through. - if builder.get_parent_inherited_text().clone_color().alpha == 0 { - let color = builder.device.default_color(); + if context.builder.get_parent_inherited_text().clone_color().alpha == 0 { + let color = context.builder.device.default_color(); declarations_to_apply_unless_overriden.push(PropertyDeclaration::Color( specified::ColorPropertyValue(color.into()), )) @@ -631,7 +615,7 @@ impl<'a, 'b: 'a> Cascade<'a, 'b> { // properties that are marked as ignored in that mode. if ignore_colors { tweak_when_ignoring_colors( - &self.context.builder, + &self.context, longhand_id, origin, &mut declaration, diff --git a/components/style/values/animated/color.rs b/components/style/values/animated/color.rs index 4ef73dda276..77091da01ec 100644 --- a/components/style/values/animated/color.rs +++ b/components/style/values/animated/color.rs @@ -104,6 +104,11 @@ impl Color { } } + /// Mix two colors into one. + pub fn mix(left: &Color, right: &Color, progress: f32) -> Self { + left.animate(right, Procedure::Interpolate { progress: progress as f64 }).unwrap() + } + fn scaled_rgba(&self) -> RGBA { if self.ratios.bg == 0. { return RGBA::transparent(); diff --git a/components/style/values/specified/color.rs b/components/style/values/specified/color.rs index 71b1c5b077c..903b867bf73 100644 --- a/components/style/values/specified/color.rs +++ b/components/style/values/specified/color.rs @@ -9,8 +9,9 @@ use super::AllowQuirks; use crate::gecko_bindings::structs::nscolor; use crate::parser::{Parse, ParserContext}; use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; -use crate::values::generics::color::{ColorOrAuto as GenericColorOrAuto}; +use crate::values::generics::color::ColorOrAuto as GenericColorOrAuto; use crate::values::specified::calc::CalcNode; +use crate::values::specified::Percentage; use cssparser::{AngleOrNumber, Color as CSSParserColor, Parser, Token, RGBA}; use cssparser::{BasicParseErrorKind, NumberOrPercentage, ParseErrorKind}; use itoa; @@ -19,6 +20,74 @@ use std::io::Write as IoWrite; use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind}; use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind}; +/// A restricted version of the css `color-mix()` function, which only supports +/// percentages and sRGB color-space interpolation. +/// +/// https://drafts.csswg.org/css-color-5/#color-mix +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +#[allow(missing_docs)] +pub struct ColorMix { + pub left: Color, + pub right: Color, + pub percentage: Percentage, +} + +// NOTE(emilio): Syntax is still a bit in-flux, since [1] doesn't seem +// particularly complete, and disagrees with the examples. +// +// [1]: https://github.com/w3c/csswg-drafts/commit/a4316446112f9e814668c2caff7f826f512f8fed +impl Parse for ColorMix { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let enabled = + context.chrome_rules_enabled() || static_prefs::pref!("layout.css.color-mix.enabled"); + + if !enabled { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + input.expect_function_matching("color-mix")?; + + // NOTE(emilio): This implements the syntax described here for now, + // might need to get updated in the future. + // + // https://github.com/w3c/csswg-drafts/issues/6066#issuecomment-789836765 + input.parse_nested_block(|input| { + input.expect_ident_matching("in")?; + // TODO: support multiple interpolation spaces. + input.expect_ident_matching("srgb")?; + input.expect_comma()?; + let left = Color::parse(context, input)?; + let percentage = input.try_parse(|input| { + Percentage::parse(context, input) + }).unwrap_or_else(|_| Percentage::new(0.5)); + input.expect_comma()?; + let right = Color::parse(context, input)?; + + Ok(ColorMix { left, right, percentage }) + }) + } +} + +impl ToCss for ColorMix { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + dest.write_str("color-mix(in srgb, ")?; + self.left.to_css(dest)?; + if self.percentage.get() != 0.5 || self.percentage.is_calc() { + dest.write_str(" ")?; + self.percentage.to_css(dest)?; + } + dest.write_str(", ")?; + self.right.to_css(dest)?; + dest.write_str(")") + } +} + /// Specified color value #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] pub enum Color { @@ -36,6 +105,8 @@ pub enum Color { /// A system color #[cfg(feature = "gecko")] System(SystemColor), + /// A color mix. + ColorMix(Box), /// Quirksmode-only rule for inheriting color from the body #[cfg(feature = "gecko")] InheritFromBodyQuirk, @@ -338,8 +409,6 @@ impl<'a, 'b: 'a, 'i: 'a> ::cssparser::ColorComponentParser<'i> for ColorComponen } fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result> { - use crate::values::specified::Percentage; - Ok(Percentage::parse(self.0, input)?.get()) } @@ -398,6 +467,10 @@ impl Parse for Color { } } + if let Ok(mix) = input.try_parse(|i| ColorMix::parse(context, i)) { + return Ok(Color::ColorMix(Box::new(mix))); + } + match e.kind { ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => { Err(e.location.new_custom_error(StyleParseErrorKind::ValueError( @@ -425,7 +498,9 @@ 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), #[cfg(feature = "gecko")] @@ -562,17 +637,23 @@ impl Color { /// /// If `context` is `None`, and the specified color requires data from /// the context to resolve, then `None` is returned. - pub fn to_computed_color(&self, _context: Option<&Context>) -> Option { + 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, - #[cfg(feature = "gecko")] - Color::System(system) => system.compute(_context?), - #[cfg(feature = "gecko")] - Color::InheritFromBodyQuirk => { - ComputedColor::rgba(_context?.device().body_text_color()) + Color::ColorMix(ref mix) => { + use crate::values::animated::color::Color as AnimatedColor; + use crate::values::animated::ToAnimatedValue; + + 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(&left, &right, mix.percentage.get())) }, + #[cfg(feature = "gecko")] + Color::System(system) => system.compute(context?), + #[cfg(feature = "gecko")] + Color::InheritFromBodyQuirk => ComputedColor::rgba(context?.device().body_text_color()), }) } }