Bug 1945342 - Prototype style() container queries. r=dshin

Not intended to be a full implementation, but more of an off-by-default
prototype with the right pieces in place.

This parses and evaluates the queries, but:

 * They don't evaluate against the right style (I didn't change the
   container query lookup yet).

 * They're not invalidated properly (this changes selector matching as a
   result of style changes which is not currently dealt with).

Still I want to get this in since it's kinda straight-forward and needed
for the eventual full implementation.

There are some tests that start running now that I'd need to update, try
in progress.

Differential Revision: https://phabricator.services.mozilla.com/D236468
This commit is contained in:
Emilio Cobos Álvarez
2025-02-07 18:42:26 +00:00
parent ad0d4ca3f6
commit 0052469531
23 changed files with 336 additions and 45 deletions

View File

@@ -9319,6 +9319,13 @@
mirror: always
rust: true
# Whether style() container queries are enabled
- name: layout.css.style-queries.enabled
type: RelaxedAtomicBool
value: false
mirror: always
rust: true
# Should we look for counter ancestor scopes first?
- name: layout.css.counter-ancestor-scope.enabled
type: bool

View File

@@ -238,6 +238,23 @@ pub struct VariableValue {
trivial_to_computed_value!(VariableValue);
/// Given a potentially registered variable value turn it into a computed custom property value.
pub fn compute_variable_value(
value: &Arc<VariableValue>,
registration: &PropertyRegistrationData,
computed_context: &computed::Context,
) -> Option<ComputedRegisteredValue> {
if registration.syntax.is_universal() {
return Some(ComputedRegisteredValue::universal(Arc::clone(value)));
}
compute_value(
&value.css,
&value.url_data,
registration,
computed_context,
).ok()
}
// For all purposes, we want values to be considered equal if their css text is equal.
impl PartialEq for VariableValue {
fn eq(&self, other: &Self) -> bool {

View File

@@ -8,10 +8,12 @@
//! https://drafts.csswg.org/css-contain-3/#typedef-container-condition
use super::{FeatureFlags, FeatureType, QueryFeatureExpression};
use crate::custom_properties;
use crate::values::computed;
use crate::{error_reporting::ContextualParseError, parser::ParserContext};
use cssparser::{Parser, Token};
use cssparser::{Parser, SourcePosition, Token};
use selectors::kleene_value::KleeneValue;
use servo_arc::Arc;
use std::fmt::{self, Write};
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
@@ -30,6 +32,81 @@ enum AllowOr {
No,
}
/// A style query feature:
/// https://drafts.csswg.org/css-conditional-5/#typedef-style-feature
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
pub struct StyleFeature {
name: custom_properties::Name,
// TODO: This is a "primary" reference, probably should be unconditionally measured.
#[ignore_malloc_size_of = "Arc"]
value: Option<Arc<custom_properties::SpecifiedValue>>,
}
impl ToCss for StyleFeature {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: fmt::Write,
{
dest.write_str("--")?;
crate::values::serialize_atom_identifier(&self.name, dest)?;
if let Some(ref v) = self.value {
dest.write_str(": ")?;
v.to_css(dest)?;
}
Ok(())
}
}
impl StyleFeature {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
feature_type: FeatureType,
) -> Result<Self, ParseError<'i>> {
if !static_prefs::pref!("layout.css.style-queries.enabled") ||
feature_type != FeatureType::Container
{
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
// TODO: Allow parsing nested style feature queries.
let ident = input.expect_ident()?;
// TODO(emilio): Maybe support non-custom properties?
let name = match custom_properties::parse_name(ident.as_ref()) {
Ok(name) => custom_properties::Name::from(name),
Err(()) => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
};
let value = if input.try_parse(|i| i.expect_colon()).is_ok() {
input.skip_whitespace();
Some(Arc::new(custom_properties::SpecifiedValue::parse(
input,
&context.url_data,
)?))
} else {
None
};
Ok(Self { name, value })
}
fn matches(&self, ctx: &computed::Context) -> KleeneValue {
// FIXME(emilio): Confirm this is the right style to query.
let registration = ctx
.builder
.stylist
.expect("container queries should have a stylist around")
.get_custom_property_registration(&self.name);
let current_value = ctx
.inherited_custom_properties()
.get(registration, &self.name);
KleeneValue::from(match self.value {
Some(ref v) => current_value.is_some_and(|cur| {
custom_properties::compute_variable_value(v, registration, ctx)
.is_some_and(|v| v == *cur)
}),
None => current_value.is_some(),
})
}
}
/// Represents a condition.
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
pub enum QueryCondition {
@@ -41,6 +118,8 @@ pub enum QueryCondition {
Operation(Box<[QueryCondition]>, Operator),
/// A condition wrapped in parenthesis.
InParens(Box<QueryCondition>),
/// A <style> query.
Style(StyleFeature),
/// [ <function-token> <any-value>? ) ] | [ ( <any-value>? ) ]
GeneralEnclosed(String),
}
@@ -63,6 +142,11 @@ impl ToCss for QueryCondition {
c.to_css(dest)?;
dest.write_char(')')
},
QueryCondition::Style(ref c) => {
dest.write_str("style(")?;
c.to_css(dest)?;
dest.write_char(')')
},
QueryCondition::Operation(ref list, op) => {
let mut iter = list.iter();
iter.next().unwrap().to_css(dest)?;
@@ -100,8 +184,7 @@ impl QueryCondition {
{
visitor(self);
match *self {
Self::Feature(..) => {},
Self::GeneralEnclosed(..) => {},
Self::Feature(..) | Self::GeneralEnclosed(..) | Self::Style(..) => {},
Self::Not(ref cond) => cond.visit(visitor),
Self::Operation(ref conds, _op) => {
for cond in conds.iter() {
@@ -117,6 +200,9 @@ impl QueryCondition {
pub fn cumulative_flags(&self) -> FeatureFlags {
let mut result = FeatureFlags::empty();
self.visit(&mut |condition| {
if let Self::Style(..) = condition {
result.insert(FeatureFlags::STYLE);
}
if let Self::Feature(ref f) = condition {
result.insert(f.feature_flags())
}
@@ -200,6 +286,29 @@ impl QueryCondition {
Err(feature_error)
}
fn try_parse_block<'i, T, F>(
context: &ParserContext,
input: &mut Parser<'i, '_>,
start: SourcePosition,
parse: F,
) -> Option<T>
where
F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i>>,
{
let nested = input.try_parse(|input| input.parse_nested_block(parse));
match nested {
Ok(nested) => Some(nested),
Err(e) => {
// We're about to swallow the error in a `<general-enclosed>`
// condition, so report it while we can.
let loc = e.location;
let error = ContextualParseError::InvalidMediaRule(input.slice_from(start), e);
context.log_css_error(loc, error);
None
},
}
}
/// Parse a condition in parentheses, or `<general-enclosed>`.
///
/// https://drafts.csswg.org/mediaqueries/#typedef-media-in-parens
@@ -213,25 +322,22 @@ impl QueryCondition {
let start_location = input.current_source_location();
match *input.next()? {
Token::ParenthesisBlock => {
let nested = input.try_parse(|input| {
input.parse_nested_block(|input| {
Self::parse_in_parenthesis_block(context, input, feature_type)
})
let nested = Self::try_parse_block(context, input, start, |input| {
Self::parse_in_parenthesis_block(context, input, feature_type)
});
match nested {
Ok(nested) => return Ok(nested),
Err(e) => {
// We're about to swallow the error in a `<general-enclosed>`
// condition, so report it while we can.
let loc = e.location;
let error =
ContextualParseError::InvalidMediaRule(input.slice_from(start), e);
context.log_css_error(loc, error);
},
if let Some(nested) = nested {
return Ok(nested);
}
},
Token::Function(..) => {
// TODO: handle `style()` queries, etc.
Token::Function(ref name) => {
if name.eq_ignore_ascii_case("style") {
let feature = Self::try_parse_block(context, input, start, |input| {
StyleFeature::parse(context, input, feature_type)
});
if let Some(feature) = feature {
return Ok(Self::Style(feature));
}
}
},
ref t => return Err(start_location.new_unexpected_token_error(t.clone())),
}
@@ -250,6 +356,7 @@ impl QueryCondition {
QueryCondition::GeneralEnclosed(_) => KleeneValue::Unknown,
QueryCondition::InParens(ref c) => c.matches(context),
QueryCondition::Not(ref c) => !c.matches(context),
QueryCondition::Style(ref c) => c.matches(context),
QueryCondition::Operation(ref conditions, op) => {
debug_assert!(!conditions.is_empty(), "We never create an empty op");
match op {

View File

@@ -123,6 +123,8 @@ bitflags! {
const CONTAINER_REQUIRES_HEIGHT_AXIS = 1 << 5;
/// The feature evaluation depends on the viewport size.
const VIEWPORT_DEPENDENT = 1 << 6;
/// The feature evaluation depends on style queries.
const STYLE = 1 << 7;
}
}

View File

@@ -1 +1 @@
prefs: [dom.customHighlightAPI.enabled:true,layout.css.transition-behavior.enabled:true,layout.css.basic-shape-shape.enabled:true]
prefs: [dom.customHighlightAPI.enabled:true,layout.css.transition-behavior.enabled:true,layout.css.basic-shape-shape.enabled:true,layout.css.style-queries.enabled:true]

View File

@@ -1,2 +1,9 @@
[at-container-style-parsing.html]
expected: ERROR
[Query condition should be valid: style(not ((--foo: calc(10px + 2em)) and ((--foo: url(x)))))]
expected: FAIL
[Query condition should be valid: style((--foo: bar) or (--bar: 10px))]
expected: FAIL
[Query condition should be valid: style(--foo: bar !important)]
expected: FAIL

View File

@@ -1,2 +1,6 @@
[at-container-style-serialization.html]
expected: ERROR
[Unknown CSS property after 'or']
expected: FAIL
[Original string number in custom property value]
expected: FAIL

View File

@@ -1,2 +1,9 @@
[container-selection-unknown-features.html]
expected: ERROR
[width query with (foo: bar)]
expected: FAIL
[width query with foo(bar)]
expected: FAIL
[style query with (foo: bar)]
expected: FAIL

View File

@@ -1,2 +1,102 @@
[custom-property-style-queries.html]
expected: ERROR
[ style(--inner: true)]
expected: FAIL
[ style(--inner:true)]
expected: FAIL
[ style(--inner:true )]
expected: FAIL
[ style(--inner: true )]
expected: FAIL
[ style(--inner-no-space: true)]
expected: FAIL
[ style(--inner-no-space:true)]
expected: FAIL
[ style(--inner-no-space:true )]
expected: FAIL
[ style(--inner-no-space: true )]
expected: FAIL
[ style(--inner-space-after: true)]
expected: FAIL
[ style(--inner-space-after:true)]
expected: FAIL
[ style(--inner-space-after:true )]
expected: FAIL
[ style(--inner-space-after: true )]
expected: FAIL
[outer style(--outer: true)]
expected: FAIL
[outer style(--outer:true)]
expected: FAIL
[outer style(--outer:true )]
expected: FAIL
[outer style(--outer: true )]
expected: FAIL
[outer style(--outer-no-space: true)]
expected: FAIL
[outer style(--outer-no-space:true)]
expected: FAIL
[outer style(--outer-no-space:true )]
expected: FAIL
[outer style(--outer-no-space: true )]
expected: FAIL
[outer style(--outer-space-after: true)]
expected: FAIL
[outer style(--outer-space-after:true)]
expected: FAIL
[outer style(--outer-space-after:true )]
expected: FAIL
[outer style(--outer-space-after: true )]
expected: FAIL
[Query custom property with !important declaration]
expected: FAIL
[Query custom property using var()]
expected: FAIL
[Query custom property including unknown var() reference with matching fallback]
expected: FAIL
[Query custom property matching guaranteed-invalid values]
expected: FAIL
[Style query 'initial' matching]
expected: FAIL
[Style query 'initial' matching (with explicit 'initial' value)]
expected: FAIL
[Style query 'inherit' matching]
expected: FAIL
[Style query 'unset' matching]
expected: FAIL
[Match registered <length> custom property with px via initial keyword.]
expected: FAIL
[Match registered <length> custom property with initial value via initial keyword.]
expected: FAIL

View File

@@ -1,2 +1,12 @@
[custom-property-style-query-change.html]
expected: ERROR
[Target child]
expected: FAIL
[Target grandchild]
expected: FAIL
[Registered property query child]
expected: FAIL
[Registered property query grandchild]
expected: FAIL

View File

@@ -1,2 +1,3 @@
[display-contents-dynamic-style-queries.html]
expected: ERROR
[After display and --foo changes, style() query causes the color to be green]
expected: FAIL

View File

@@ -1,2 +1,6 @@
[multiple-style-containers-comma-separated-queries.html]
expected: ERROR
[Should match the named outer container for --foo:bar]
expected: FAIL
[Match the #combined container for --foo:qux which is also a size container]
expected: FAIL

View File

@@ -1,2 +1,3 @@
[nested-size-style-container-invalidation.html]
expected: ERROR
[Green after reducing width]
expected: FAIL

View File

@@ -1,2 +0,0 @@
[pseudo-elements-005.html]
expected: ERROR

View File

@@ -1,2 +0,0 @@
[registered-color-style-queries.html]
expected: ERROR

View File

@@ -1,2 +1,36 @@
[style-container-for-shadow-dom.html]
expected: ERROR
[Match container in outer tree]
expected: FAIL
[Match container in the shadow tree for a host child in the host child's tree scope]
expected: FAIL
[Match <slot> as a container for ::slotted element]
expected: FAIL
[Match container in outer tree for :host]
expected: FAIL
[Match ::part's parent in the shadow tree as the container for ::part]
expected: FAIL
[Match ::slotted as a container for its ::before]
expected: FAIL
[Match container in outer tree for :host::before]
expected: FAIL
[Match the ::part as a container for ::before on ::part elements]
expected: FAIL
[Match container for ::part selector in inner shadow tree for exportparts]
expected: FAIL
[Match container for slot light tree child fallback]
expected: FAIL
[Should match parent container inside shadow tree for ::part()]
expected: FAIL
[A :host::part rule matching a container in the shadow tree]
expected: FAIL

View File

@@ -1,2 +1,3 @@
[style-container-invalidation-inheritance.html]
expected: ERROR
[Changed --match inherits down descendants and affects container query]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[style-query-document-element.html]
[style query should evaluate to true]
expected: FAIL

View File

@@ -1,2 +0,0 @@
[style-query-guaranteed-invalid.tentative.html]
expected: ERROR

View File

@@ -1,2 +1,3 @@
[style-query-no-cycle.html]
expected: ERROR
[style(--foo: var(--foo)) should match]
expected: FAIL

View File

@@ -1,2 +0,0 @@
[style-query-registered-custom-invalid.tentative.html]
expected: ERROR

View File

@@ -1,2 +1,3 @@
[style-query-unset-on-root.html]
expected: ERROR
[Match style(--foo: unset) on :root element]
expected: FAIL

View File

@@ -1,2 +0,0 @@
[style-query-with-unknown-width.html]
expected: ERROR