mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Bug 1695376 - Implement basic color-mix() functionality, behind a pref, but exposed to chrome code. r=boris
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:
parent
dee15c3162
commit
8cc760e83a
@ -6203,6 +6203,13 @@
|
||||
mirror: always
|
||||
rust: true
|
||||
|
||||
# Is support for color-mix on content enabled?
|
||||
- name: layout.css.color-mix.enabled
|
||||
type: RelaxedAtomicBool
|
||||
value: @IS_NIGHTLY_BUILD@
|
||||
mirror: always
|
||||
rust: true
|
||||
|
||||
# Is support for DOMMatrix enabled?
|
||||
- name: layout.css.DOMMatrix.enabled
|
||||
type: RelaxedAtomicBool
|
||||
|
@ -386,13 +386,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<PropertyDeclaration>,
|
||||
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;
|
||||
@ -406,34 +408,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.
|
||||
@ -447,9 +431,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()))
|
||||
@ -457,14 +441,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()),
|
||||
))
|
||||
@ -638,7 +622,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,
|
||||
|
@ -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();
|
||||
|
@ -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<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
|
||||
#[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<ColorMix>),
|
||||
/// 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<f32, ParseError<'i>> {
|
||||
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<ComputedColor> {
|
||||
pub fn to_computed_color(&self, context: Option<&Context>) -> Option<ComputedColor> {
|
||||
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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
[color-mix-basic-001.tentative.html]
|
||||
prefs: [layout.css.color-mix.enabled:true]
|
@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<!-- Tentative pending potential syntax changes in https://github.com/w3c/csswg-drafts/issues/6066 -->
|
||||
<link rel="help" href="https://drafts.csswg.org/css-color-5/#color-mix">
|
||||
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1695376">
|
||||
<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
|
||||
<link rel="author" href="https://mozilla.org" title="Mozilla">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<style>
|
||||
div { color: black }
|
||||
</style>
|
||||
<div id="test"></div>
|
||||
<div id="ref"></div>
|
||||
<script>
|
||||
const TEST_CASES = [
|
||||
["blue", "red"],
|
||||
["blue", "green"],
|
||||
["rgb(255, 0, 0, .2)", "red"],
|
||||
["blue", "red", 0.9],
|
||||
["blue", "red", 0],
|
||||
["currentColor", "white"],
|
||||
["currentColor", "rgba(0, 0, 0, .5)"],
|
||||
];
|
||||
|
||||
const testElement = document.getElementById("test");
|
||||
const refElement = document.getElementById("ref");
|
||||
const testStyle = getComputedStyle(testElement);
|
||||
const refStyle = getComputedStyle(refElement);
|
||||
|
||||
let animation = null;
|
||||
|
||||
for (let [from, to, animationProgress] of TEST_CASES) {
|
||||
const animationProgressExplicit = animationProgress !== undefined;
|
||||
animationProgress = animationProgressExplicit ? animationProgress : 0.5;
|
||||
test(function() {
|
||||
// Set up the ref.
|
||||
if (animation) {
|
||||
animation.cancel();
|
||||
}
|
||||
animation = refElement.animate({
|
||||
backgroundColor: [from, to],
|
||||
}, { duration: 1000 });
|
||||
animation.pause();
|
||||
animation.currentTime = 1000 * animationProgress;
|
||||
|
||||
let value = "color-mix(in srgb, " + from;
|
||||
if (animationProgressExplicit) {
|
||||
value += " " + (animationProgress * 100) + "%";
|
||||
}
|
||||
value += ", " + to + ")";
|
||||
testElement.style.backgroundColor = "";
|
||||
testElement.style.backgroundColor = value;
|
||||
|
||||
assert_not_equals(testElement.style.backgroundColor, "", "Should parse " + value);
|
||||
assert_equals(testStyle.backgroundColor, refStyle.backgroundColor, "Colors should match");
|
||||
}, `From ${from} to ${to} at ${animationProgress}`);
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user