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 <percentage> syntax, not the whole <color-adjuster>
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
This commit is contained in:
Oriol Brufau 2023-05-16 07:26:09 +02:00
parent 252b50931d
commit 980d10fc17
3 changed files with 109 additions and 39 deletions

View file

@ -379,13 +379,15 @@ where
type DeclarationsToApplyUnlessOverriden = SmallVec<[PropertyDeclaration; 2]>; type DeclarationsToApplyUnlessOverriden = SmallVec<[PropertyDeclaration; 2]>;
fn tweak_when_ignoring_colors( fn tweak_when_ignoring_colors(
builder: &StyleBuilder, context: &computed::Context,
longhand_id: LonghandId, longhand_id: LonghandId,
origin: Origin, origin: Origin,
declaration: &mut Cow<PropertyDeclaration>, declaration: &mut Cow<PropertyDeclaration>,
declarations_to_apply_unless_overriden: &mut DeclarationsToApplyUnlessOverriden, declarations_to_apply_unless_overriden: &mut DeclarationsToApplyUnlessOverriden,
) { ) {
use crate::values::specified::Color; use crate::values::specified::Color;
use crate::values::computed::ToComputedValue;
use cssparser::RGBA;
if !longhand_id.ignored_when_document_colors_disabled() { if !longhand_id.ignored_when_document_colors_disabled() {
return; return;
@ -399,34 +401,16 @@ fn tweak_when_ignoring_colors(
// Don't override background-color on ::-moz-color-swatch. It is set as an // 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 // author style (via the style attribute), but it's pretty important for it
// to show up for obvious reasons :) // 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 longhand_id == LonghandId::BackgroundColor
{ {
return; return;
} }
fn alpha_channel(color: &Color) -> u8 { fn alpha_channel(color: &Color, context: &computed::Context) -> u8 {
match *color { // We assume here currentColor is opaque.
// Seems safe enough to assume that the default color and system let color = color.to_computed_value(context).to_rgba(RGBA::new(0, 0, 0, 255));
// colors are opaque in HCM, though maybe we shouldn't asume the color.alpha
// 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,
}
} }
// A few special-cases ahead. // 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 // should consider not doing that even if it causes some issues like
// bug 1625036, or finding a performant way to preserve the original // bug 1625036, or finding a performant way to preserve the original
// widget background color's rgb channels but not alpha... // widget background color's rgb channels but not alpha...
let alpha = alpha_channel(color); let alpha = alpha_channel(color, context);
if alpha != 0 { if alpha != 0 {
let mut color = builder.device.default_background_color(); let mut color = context.builder.device.default_background_color();
color.alpha = alpha; color.alpha = alpha;
declarations_to_apply_unless_overriden declarations_to_apply_unless_overriden
.push(PropertyDeclaration::BackgroundColor(color.into())) .push(PropertyDeclaration::BackgroundColor(color.into()))
@ -450,14 +434,14 @@ fn tweak_when_ignoring_colors(
}, },
PropertyDeclaration::Color(ref color) => { PropertyDeclaration::Color(ref color) => {
// We honor color: transparent, and "revert-or-initial" otherwise. // We honor color: transparent, and "revert-or-initial" otherwise.
if alpha_channel(&color.0) == 0 { if alpha_channel(&color.0, context) == 0 {
return; return;
} }
// If the inherited color would be transparent, but we would // If the inherited color would be transparent, but we would
// override this with a non-transparent color, then override it with // override this with a non-transparent color, then override it with
// the default color. Otherwise just let it inherit through. // the default color. Otherwise just let it inherit through.
if builder.get_parent_inherited_text().clone_color().alpha == 0 { if context.builder.get_parent_inherited_text().clone_color().alpha == 0 {
let color = builder.device.default_color(); let color = context.builder.device.default_color();
declarations_to_apply_unless_overriden.push(PropertyDeclaration::Color( declarations_to_apply_unless_overriden.push(PropertyDeclaration::Color(
specified::ColorPropertyValue(color.into()), specified::ColorPropertyValue(color.into()),
)) ))
@ -631,7 +615,7 @@ impl<'a, 'b: 'a> Cascade<'a, 'b> {
// properties that are marked as ignored in that mode. // properties that are marked as ignored in that mode.
if ignore_colors { if ignore_colors {
tweak_when_ignoring_colors( tweak_when_ignoring_colors(
&self.context.builder, &self.context,
longhand_id, longhand_id,
origin, origin,
&mut declaration, &mut declaration,

View file

@ -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 { fn scaled_rgba(&self) -> RGBA {
if self.ratios.bg == 0. { if self.ratios.bg == 0. {
return RGBA::transparent(); return RGBA::transparent();

View file

@ -9,8 +9,9 @@ use super::AllowQuirks;
use crate::gecko_bindings::structs::nscolor; use crate::gecko_bindings::structs::nscolor;
use crate::parser::{Parse, ParserContext}; use crate::parser::{Parse, ParserContext};
use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; 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::calc::CalcNode;
use crate::values::specified::Percentage;
use cssparser::{AngleOrNumber, Color as CSSParserColor, Parser, Token, RGBA}; use cssparser::{AngleOrNumber, Color as CSSParserColor, Parser, Token, RGBA};
use cssparser::{BasicParseErrorKind, NumberOrPercentage, ParseErrorKind}; use cssparser::{BasicParseErrorKind, NumberOrPercentage, ParseErrorKind};
use itoa; use itoa;
@ -19,6 +20,74 @@ use std::io::Write as IoWrite;
use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind}; use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind};
use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind}; 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<Self, ParseError<'i>> {
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<W>(&self, dest: &mut CssWriter<W>) -> 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 /// Specified color value
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
pub enum Color { pub enum Color {
@ -36,6 +105,8 @@ pub enum Color {
/// A system color /// A system color
#[cfg(feature = "gecko")] #[cfg(feature = "gecko")]
System(SystemColor), System(SystemColor),
/// A color mix.
ColorMix(Box<ColorMix>),
/// Quirksmode-only rule for inheriting color from the body /// Quirksmode-only rule for inheriting color from the body
#[cfg(feature = "gecko")] #[cfg(feature = "gecko")]
InheritFromBodyQuirk, 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<f32, ParseError<'i>> { fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> {
use crate::values::specified::Percentage;
Ok(Percentage::parse(self.0, input)?.get()) 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 { match e.kind {
ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => { ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => {
Err(e.location.new_custom_error(StyleParseErrorKind::ValueError( Err(e.location.new_custom_error(StyleParseErrorKind::ValueError(
@ -425,7 +498,9 @@ impl ToCss for Color {
Color::Numeric { Color::Numeric {
parsed: ref rgba, .. parsed: ref rgba, ..
} => rgba.to_css(dest), } => rgba.to_css(dest),
// TODO: Could represent this as a color-mix() instead.
Color::Complex(_) => Ok(()), Color::Complex(_) => Ok(()),
Color::ColorMix(ref mix) => mix.to_css(dest),
#[cfg(feature = "gecko")] #[cfg(feature = "gecko")]
Color::System(system) => system.to_css(dest), Color::System(system) => system.to_css(dest),
#[cfg(feature = "gecko")] #[cfg(feature = "gecko")]
@ -562,17 +637,23 @@ impl Color {
/// ///
/// If `context` is `None`, and the specified color requires data from /// If `context` is `None`, and the specified color requires data from
/// the context to resolve, then `None` is returned. /// the context to resolve, then `None` is returned.
pub fn to_computed_color(&self, _context: Option<&Context>) -> Option<ComputedColor> { pub fn to_computed_color(&self, context: Option<&Context>) -> Option<ComputedColor> {
Some(match *self { Some(match *self {
Color::CurrentColor => ComputedColor::currentcolor(), Color::CurrentColor => ComputedColor::currentcolor(),
Color::Numeric { ref parsed, .. } => ComputedColor::rgba(*parsed), Color::Numeric { ref parsed, .. } => ComputedColor::rgba(*parsed),
Color::Complex(ref complex) => *complex, Color::Complex(ref complex) => *complex,
#[cfg(feature = "gecko")] Color::ColorMix(ref mix) => {
Color::System(system) => system.compute(_context?), use crate::values::animated::color::Color as AnimatedColor;
#[cfg(feature = "gecko")] use crate::values::animated::ToAnimatedValue;
Color::InheritFromBodyQuirk => {
ComputedColor::rgba(_context?.device().body_text_color()) 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()),
}) })
} }
} }