Bug 1744231: Part 4 - Enable lookup and computation of container size queries. r=emilio

Differential Revision: https://phabricator.services.mozilla.com/D158057
This commit is contained in:
David Shin 2022-10-17 17:26:13 +00:00
parent 3e04e32ba0
commit 5ee4d158da
15 changed files with 335 additions and 179 deletions

View File

@ -23,6 +23,7 @@ use crate::selector_parser::PseudoElement;
use crate::shared_lock::StylesheetGuards;
use crate::style_adjuster::StyleAdjuster;
use crate::stylesheets::{Origin, layer_rule::LayerOrder};
use crate::stylesheets::container_rule::ContainerSizeQuery;
use crate::values::{computed, specified};
use fxhash::FxHashMap;
use servo_arc::Arc;
@ -276,6 +277,7 @@ where
};
let is_root_element = pseudo.is_none() && element.map_or(false, |e| e.is_root());
let container_size_query = ContainerSizeQuery::for_option_element(element);
let mut context = computed::Context::new(
// We'd really like to own the rules here to avoid refcount traffic, but
@ -292,6 +294,7 @@ where
),
quirks_mode,
rule_cache_conditions,
container_size_query,
);
let using_cached_reset_properties;

View File

@ -6,6 +6,7 @@
//!
//! [container]: https://drafts.csswg.org/css-contain-3/#container-rule
use crate::computed_value_flags::ComputedValueFlags;
use crate::dom::TElement;
use crate::logical_geometry::{LogicalSize, WritingMode};
use crate::media_queries::Device;
@ -19,7 +20,7 @@ use crate::shared_lock::{
};
use crate::str::CssStringWriter;
use crate::stylesheets::CssRules;
use crate::values::computed::{CSSPixelLength, Context, Ratio};
use crate::values::computed::{ContainerType, CSSPixelLength, Context, Ratio};
use crate::values::specified::ContainerName;
use app_units::Au;
use cssparser::{Parser, SourceLocation};
@ -112,6 +113,44 @@ pub struct ContainerLookupResult<E> {
pub style: Arc<ComputedValues>,
}
fn container_type_axes(ty_: ContainerType, wm: WritingMode) -> FeatureFlags {
if ty_.contains(ContainerType::SIZE) {
return FeatureFlags::all_container_axes();
}
if ty_.contains(ContainerType::INLINE_SIZE) {
let physical_axis = if wm.is_vertical() {
FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS
} else {
FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS
};
return FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS | physical_axis;
}
FeatureFlags::empty()
}
enum TraversalResult<T> {
InProgress,
StopTraversal,
Done(T),
}
fn traverse_container<E, F, R>(mut e: E, evaluator: F) -> Option<(E, R)>
where
E: TElement,
F: Fn(E) -> TraversalResult<R>
{
while let Some(element) = e.traversal_parent() {
match evaluator(element) {
TraversalResult::InProgress => {},
TraversalResult::StopTraversal => break,
TraversalResult::Done(result) => return Some((element, result)),
}
e = element;
}
None
}
impl ContainerCondition {
/// Parse a container condition.
pub fn parse<'a>(
@ -135,30 +174,16 @@ impl ContainerCondition {
})
}
fn valid_container_info<E>(&self, potential_container: E) -> Option<ContainerLookupResult<E>>
fn valid_container_info<E>(
&self,
potential_container: E
) -> TraversalResult<ContainerLookupResult<E>>
where
E: TElement,
{
use crate::values::computed::ContainerType;
fn container_type_axes(ty_: ContainerType, wm: WritingMode) -> FeatureFlags {
if ty_.contains(ContainerType::SIZE) {
return FeatureFlags::all_container_axes();
}
if ty_.contains(ContainerType::INLINE_SIZE) {
let physical_axis = if wm.is_vertical() {
FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS
} else {
FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS
};
return FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS | physical_axis;
}
FeatureFlags::empty()
}
let data = match potential_container.borrow_data() {
Some(data) => data,
None => return None,
None => return TraversalResult::InProgress,
};
let style = data.styles.primary();
let wm = style.writing_mode;
@ -168,20 +193,20 @@ impl ContainerCondition {
let container_type = box_style.clone_container_type();
let available_axes = container_type_axes(container_type, wm);
if !available_axes.contains(self.flags.container_axes()) {
return None;
return TraversalResult::InProgress;
}
// Filter by container-name.
let container_name = box_style.clone_container_name();
for filter_name in self.name.0.iter() {
if !container_name.0.contains(filter_name) {
return None;
return TraversalResult::InProgress;
}
}
let size = potential_container.primary_box_size();
let style = style.clone();
Some(ContainerLookupResult {
TraversalResult::Done(ContainerLookupResult {
element: potential_container,
info: ContainerInfo { size, wm },
style,
@ -189,18 +214,14 @@ impl ContainerCondition {
}
/// Performs container lookup for a given element.
pub fn find_container<E>(&self, mut e: E) -> Option<ContainerLookupResult<E>>
pub fn find_container<E>(&self, e: E) -> Option<ContainerLookupResult<E>>
where
E: TElement,
{
while let Some(element) = e.traversal_parent() {
if let Some(result) = self.valid_container_info(element) {
return Some(result);
}
e = element;
match traverse_container(e, |element| self.valid_container_info(element)) {
Some((_, result)) => Some(result),
None => None,
}
None
}
/// Tries to match a container query condition for a given element.
@ -209,10 +230,18 @@ impl ContainerCondition {
E: TElement,
{
let result = self.find_container(element);
let info = result.map(|r| (r.info, r.style));
Context::for_container_query_evaluation(device, info, |context| {
self.condition.matches(context)
})
let (container, info) = match result {
Some(r) => (Some(r.element), Some((r.info, r.style))),
None => (None, None),
};
// Set up the lookup for the container in question, as the condition may be using container query lengths.
let size_query_container_lookup = ContainerSizeQuery::for_option_element(container);
Context::for_container_query_evaluation(
device,
info,
size_query_container_lookup,
|context| self.condition.matches(context),
)
}
}
@ -320,3 +349,220 @@ pub static CONTAINER_FEATURES: [QueryFeatureDescription; 6] = [
),
),
];
/// Result of a container size query, signifying the hypothetical containment boundary in terms of physical axes.
/// Defined by up to two size containers. Queries on logical axes are resolved with respect to the querying
/// element's writing mode.
#[derive(Copy, Clone, Default)]
pub struct ContainerSizeQueryResult {
width: Option<Au>,
height: Option<Au>,
}
impl ContainerSizeQueryResult {
fn get_viewport_size(context: &Context) -> Size2D<Au> {
use crate::values::specified::ViewportVariant;
context
.device()
.au_viewport_size_for_viewport_unit_resolution(ViewportVariant::Small)
}
fn get_logical_viewport_size(context: &Context) -> LogicalSize<Au> {
LogicalSize::from_physical(
context.builder.writing_mode,
Self::get_viewport_size(context),
)
}
/// Get the inline-size of the query container.
pub fn get_container_inline_size(&self, context: &Context) -> Au {
if context.builder.writing_mode.is_horizontal() {
if let Some(w) = self.width {
return w;
}
} else {
if let Some(h) = self.height {
return h;
}
}
Self::get_logical_viewport_size(context).inline
}
/// Get the block-size of the query container.
pub fn get_container_block_size(&self, context: &Context) -> Au {
if context.builder.writing_mode.is_horizontal() {
self.get_container_height(context)
} else {
self.get_container_width(context)
}
}
/// Get the width of the query container.
pub fn get_container_width(&self, context: &Context) -> Au {
if let Some(w) = self.width {
return w;
}
Self::get_viewport_size(context).width
}
/// Get the height of the query container.
pub fn get_container_height(&self, context: &Context) -> Au {
if let Some(h) = self.height {
return h;
}
Self::get_viewport_size(context).height
}
// Merge the result of a subsequent lookup, preferring the initial result.
fn merge(self, new_result: Self) -> Self {
let mut result = self;
if let Some(width) = new_result.width {
result.width.get_or_insert(width);
}
if let Some(height) = new_result.height {
result.height.get_or_insert(height);
}
result
}
fn is_complete(&self) -> bool {
self.width.is_some() && self.height.is_some()
}
}
/// Unevaluated lazy container size query.
pub enum ContainerSizeQuery<'a> {
/// Query prior to evaluation.
NotEvaluated(Box<dyn Fn() -> ContainerSizeQueryResult + 'a>),
/// Cached evaluated result.
Evaluated(ContainerSizeQueryResult),
}
impl<'a> ContainerSizeQuery<'a> {
fn evaluate_potential_size_container<E>(
e: E
) -> TraversalResult<ContainerSizeQueryResult>
where
E: TElement
{
let data = match e.borrow_data() {
Some(data) => data,
None => return TraversalResult::InProgress,
};
let style = data.styles.primary();
if !style
.flags
.contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE)
{
// We know we won't find a size container.
return TraversalResult::StopTraversal;
}
let wm = style.writing_mode;
let box_style = style.get_box();
let container_type = box_style.clone_container_type();
let size = e.primary_box_size();
match container_type {
ContainerType::SIZE => {
TraversalResult::Done(
ContainerSizeQueryResult {
width: Some(size.width),
height: Some(size.height)
}
)
},
ContainerType::INLINE_SIZE => {
if wm.is_horizontal() {
TraversalResult::Done(
ContainerSizeQueryResult {
width: Some(size.width),
height: None,
}
)
} else {
TraversalResult::Done(
ContainerSizeQueryResult {
width: None,
height: Some(size.height),
}
)
}
},
_ => TraversalResult::InProgress,
}
}
/// Find the query container size for a given element. Meant to be used as a callback for new().
fn lookup<E>(element: E) -> ContainerSizeQueryResult
where
E: TElement + 'a,
{
match traverse_container(element, |e| { Self::evaluate_potential_size_container(e) }) {
Some((container, result)) => if result.is_complete() {
result
} else {
// Traverse up from the found size container to see if we can get a complete containment.
result.merge(Self::lookup(container))
},
None => ContainerSizeQueryResult::default(),
}
}
/// Create a new instance of the container size query for given element, with a deferred lookup callback.
pub fn for_element<E>(element: E) -> Self
where
E: TElement + 'a,
{
// No need to bother if we're the top element.
if let Some(parent) = element.traversal_parent() {
let should_traverse = match parent.borrow_data() {
Some(data) => {
let style = data.styles.primary();
style
.flags
.contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE)
}
None => true, // `display: none`, still want to show a correct computed value, so give it a try.
};
if should_traverse {
return Self::NotEvaluated(Box::new(move || {
Self::lookup(element)
}));
}
}
Self::none()
}
/// Create a new instance, but with optional element.
pub fn for_option_element<E>(element: Option<E>) -> Self
where
E: TElement + 'a,
{
if let Some(e) = element {
Self::for_element(e)
} else {
Self::none()
}
}
/// Create a query that evaluates to empty, for cases where container size query is not required.
pub fn none() -> Self {
ContainerSizeQuery::Evaluated(ContainerSizeQueryResult::default())
}
/// Get the result of the container size query, doing the lookup if called for the first time.
pub fn get(&mut self) -> ContainerSizeQueryResult {
match self {
Self::NotEvaluated(lookup) => {
*self = Self::Evaluated((lookup)());
match self {
Self::Evaluated(info) => *info,
_ => unreachable!("Just evaluated but not set?"),
}
},
Self::Evaluated(info) => *info,
}
}
}

View File

@ -16,6 +16,7 @@ use crate::rule_cache::RuleCacheConditions;
use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards, ToCssWithGuard};
use crate::str::CssStringWriter;
use crate::stylesheets::cascading_at_rule::DescriptorDeclaration;
use crate::stylesheets::container_rule::ContainerSizeQuery;
use crate::stylesheets::{Origin, StylesheetInDocument};
use crate::values::computed::{Context, ToComputedValue};
use crate::values::generics::length::LengthPercentageOrAuto;
@ -678,6 +679,7 @@ impl MaybeNew for ViewportConstraints {
StyleBuilder::for_inheritance(device, None, None),
quirks_mode,
&mut conditions,
ContainerSizeQuery::none(),
);
// DEVICE-ADAPT § 9.3 Resolving 'extend-to-zoom'

View File

@ -22,7 +22,9 @@ use crate::media_queries::Device;
use crate::properties;
use crate::properties::{ComputedValues, LonghandId, StyleBuilder};
use crate::rule_cache::RuleCacheConditions;
use crate::stylesheets::container_rule::ContainerInfo;
use crate::stylesheets::container_rule::{
ContainerInfo, ContainerSizeQuery, ContainerSizeQueryResult,
};
use crate::values::specified::length::FontBaseSize;
use crate::{ArcSlice, Atom, One};
use euclid::{default, Point2D, Rect, Size2D};
@ -189,9 +191,18 @@ pub struct Context<'a> {
///
/// FIXME(emilio): Drop the refcell.
pub rule_cache_conditions: RefCell<&'a mut RuleCacheConditions>,
/// Container size query for this context.
container_size_query: RefCell<ContainerSizeQuery<'a>>,
}
impl<'a> Context<'a> {
/// Lazily evaluate the container size query, returning the result.
pub fn get_container_size_query(&self) -> ContainerSizeQueryResult {
let mut resolved = self.container_size_query.borrow_mut();
resolved.get().clone()
}
/// Creates a suitable context for media query evaluation, in which
/// font-relative units compute against the system_font, and executes `f`
/// with it.
@ -209,6 +220,7 @@ impl<'a> Context<'a> {
container_info: None,
for_non_inherited_property: None,
rule_cache_conditions: RefCell::new(&mut conditions),
container_size_query: RefCell::new(ContainerSizeQuery::none()),
};
f(&context)
}
@ -218,6 +230,7 @@ impl<'a> Context<'a> {
pub fn for_container_query_evaluation<F, R>(
device: &Device,
container_info_and_style: Option<(ContainerInfo, Arc<ComputedValues>)>,
container_size_query: ContainerSizeQuery,
f: F,
) -> R
where
@ -241,6 +254,7 @@ impl<'a> Context<'a> {
container_info,
for_non_inherited_property: None,
rule_cache_conditions: RefCell::new(&mut conditions),
container_size_query: RefCell::new(container_size_query),
};
f(&context)
@ -251,6 +265,7 @@ impl<'a> Context<'a> {
builder: StyleBuilder<'a>,
quirks_mode: QuirksMode,
rule_cache_conditions: &'a mut RuleCacheConditions,
container_size_query: ContainerSizeQuery<'a>,
) -> Self {
Self {
builder,
@ -261,6 +276,7 @@ impl<'a> Context<'a> {
for_smil_animation: false,
for_non_inherited_property: None,
rule_cache_conditions: RefCell::new(rule_cache_conditions),
container_size_query: RefCell::new(container_size_query),
}
}
@ -270,6 +286,7 @@ impl<'a> Context<'a> {
for_smil_animation: bool,
quirks_mode: QuirksMode,
rule_cache_conditions: &'a mut RuleCacheConditions,
container_size_query: ContainerSizeQuery<'a>,
) -> Self {
Self {
builder,
@ -280,6 +297,7 @@ impl<'a> Context<'a> {
for_smil_animation,
for_non_inherited_property: None,
rule_cache_conditions: RefCell::new(rule_cache_conditions),
container_size_query: RefCell::new(container_size_query),
}
}

View File

@ -717,16 +717,28 @@ impl ContainerRelativeLength {
/// Computes the given container-relative length.
pub fn to_computed_value(&self, context: &Context) -> CSSPixelLength {
// TODO(dshin): For now, use the small viewport size.
let small_viewport_size = match *self {
ContainerRelativeLength::Cqw(v) => ViewportPercentageLength::Svw(v),
ContainerRelativeLength::Cqh(v) => ViewportPercentageLength::Svh(v),
ContainerRelativeLength::Cqi(v) => ViewportPercentageLength::Svi(v),
ContainerRelativeLength::Cqb(v) => ViewportPercentageLength::Svb(v),
ContainerRelativeLength::Cqmin(v) => ViewportPercentageLength::Svmin(v),
ContainerRelativeLength::Cqmax(v) => ViewportPercentageLength::Svmax(v),
let size = context.get_container_size_query();
let (factor, container_length) = match *self {
ContainerRelativeLength::Cqw(v) => (v, size.get_container_width(context)),
ContainerRelativeLength::Cqh(v) => (v, size.get_container_height(context)),
ContainerRelativeLength::Cqi(v) => (v, size.get_container_inline_size(context)),
ContainerRelativeLength::Cqb(v) => (v, size.get_container_block_size(context)),
ContainerRelativeLength::Cqmin(v) => (
v,
cmp::min(
size.get_container_inline_size(context),
size.get_container_block_size(context),
),
),
ContainerRelativeLength::Cqmax(v) => (
v,
cmp::max(
size.get_container_inline_size(context),
size.get_container_block_size(context),
),
),
};
small_viewport_size.to_computed_value(context)
CSSPixelLength::new(((container_length.to_f64_px()) * factor as f64 / 100.0) as f32)
}
}

View File

@ -115,6 +115,7 @@ use style::selector_parser::PseudoElementCascadeType;
use style::shared_lock::{Locked, SharedRwLockReadGuard, StylesheetGuards, ToCssWithGuard};
use style::string_cache::{Atom, WeakAtom};
use style::style_adjuster::StyleAdjuster;
use style::stylesheets::container_rule::ContainerSizeQuery;
use style::stylesheets::import_rule::ImportSheet;
use style::stylesheets::keyframes_rule::{Keyframe, KeyframeSelector, KeyframesStepValue};
use style::stylesheets::layer_rule::LayerOrder;
@ -5914,12 +5915,14 @@ fn create_context_for_animation<'a>(
parent_style: Option<&'a ComputedValues>,
for_smil_animation: bool,
rule_cache_conditions: &'a mut RuleCacheConditions,
container_size_query: ContainerSizeQuery<'a>,
) -> Context<'a> {
Context::new_for_animation(
StyleBuilder::for_animation(per_doc_data.stylist.device(), style, parent_style),
for_smil_animation,
per_doc_data.stylist.quirks_mode(),
rule_cache_conditions,
container_size_query,
)
}
@ -5997,6 +6000,7 @@ pub extern "C" fn Servo_GetComputedKeyframeValues(
.map(|d| d.styles.primary())
.map(|x| &**x);
let container_size_query = ContainerSizeQuery::for_element(element);
let mut conditions = Default::default();
let mut context = create_context_for_animation(
&data,
@ -6004,6 +6008,7 @@ pub extern "C" fn Servo_GetComputedKeyframeValues(
parent_style,
/* for_smil_animation = */ false,
&mut conditions,
container_size_query,
);
let restriction = pseudo.and_then(|p| p.property_restriction());
@ -6120,6 +6125,7 @@ pub extern "C" fn Servo_GetAnimationValues(
.map(|d| d.styles.primary())
.map(|x| &**x);
let container_size_query = ContainerSizeQuery::for_element(element);
let mut conditions = Default::default();
let mut context = create_context_for_animation(
&data,
@ -6127,6 +6133,7 @@ pub extern "C" fn Servo_GetAnimationValues(
parent_style,
/* for_smil_animation = */ true,
&mut conditions,
container_size_query,
);
let default_values = data.default_computed_values();
@ -6171,6 +6178,7 @@ pub extern "C" fn Servo_AnimationValue_Compute(
.map(|d| d.styles.primary())
.map(|x| &**x);
let container_size_query = ContainerSizeQuery::for_element(element);
let mut conditions = Default::default();
let mut context = create_context_for_animation(
&data,
@ -6178,6 +6186,7 @@ pub extern "C" fn Servo_AnimationValue_Compute(
parent_style,
/* for_smil_animation = */ false,
&mut conditions,
container_size_query,
);
let default_values = data.default_computed_values();

View File

@ -1,38 +0,0 @@
[container-units-animation.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Animation using cqw unit]
expected: FAIL
[Animation using cqw unit responds to changing container size]
expected: FAIL
[Animation using cqh unit]
expected: FAIL
[Animation using cqh unit responds to changing container size]
expected: FAIL
[Animation using cqi unit]
expected: FAIL
[Animation using cqi unit responds to changing container size]
expected: FAIL
[Animation using cqb unit]
expected: FAIL
[Animation using cqb unit responds to changing container size]
expected: FAIL
[Animation using cqmin unit]
expected: FAIL
[Animation using cqmin unit responds to changing container size]
expected: FAIL
[Animation using cqmax unit]
expected: FAIL
[Animation using cqmax unit responds to changing container size]
expected: FAIL

View File

@ -1,8 +0,0 @@
[container-units-basic.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Container relative units]
expected: FAIL
[Container relative units in math functions]
expected: FAIL

View File

@ -1,2 +0,0 @@
[container-units-gradient-invalidation.html]
expected: FAIL

View File

@ -1,2 +0,0 @@
[container-units-gradient.html]
expected: FAIL

View File

@ -1,5 +0,0 @@
[container-units-in-at-container-dynamic.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Query with container-relative units are responsive to changes]
expected: FAIL

View File

@ -1,56 +0,0 @@
[container-units-in-at-container.html]
expected:
if (os == "android") and fission: [TIMEOUT, OK]
[cqw unit resolves against appropriate container]
expected: FAIL
[cqh unit resolves against appropriate container]
expected: FAIL
[cqi unit resolves against appropriate container]
expected: FAIL
[cqb unit resolves against appropriate container]
expected: FAIL
[cqmin unit resolves against appropriate container]
expected: FAIL
[cqmax unit resolves against appropriate container]
expected: FAIL
[cqw unit resolves against appropriate container (vertical writing-mode on subject)]
expected: FAIL
[cqh unit resolves against appropriate container (vertical writing-mode on subject)]
expected: FAIL
[cqi unit resolves against appropriate container (vertical writing-mode on subject)]
expected: FAIL
[cqb unit resolves against appropriate container (vertical writing-mode on subject)]
expected: FAIL
[cqmin unit resolves against appropriate container (vertical writing-mode on subject)]
expected: FAIL
[cqmax unit resolves against appropriate container (vertical writing-mode on subject)]
expected: FAIL
[cqw unit resolves against appropriate container (vertical writing-mode on container)]
expected: FAIL
[cqh unit resolves against appropriate container (vertical writing-mode on container)]
expected: FAIL
[cqi unit resolves against appropriate container (vertical writing-mode on container)]
expected: FAIL
[cqb unit resolves against appropriate container (vertical writing-mode on container)]
expected: FAIL
[cqmin unit resolves against appropriate container (vertical writing-mode on container)]
expected: FAIL
[cqmax unit resolves against appropriate container (vertical writing-mode on container)]
expected: FAIL

View File

@ -1,17 +1,5 @@
[container-units-invalidation.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[cqb respond when intermediate container changes type (inline-size -> size)]
expected: FAIL
[cqi respond when selected container changes inline-size]
expected: FAIL
[cqb respond when selected container changes block-size]
expected: FAIL
[cqi respond when selected container changes type (inline-size -> normal)]
expected: FAIL
[cqb respond when selected container changes type (size -> normal)]
expected: FAIL

View File

@ -1,6 +0,0 @@
[container-units-selection.html]
[Container units select the proper container]
expected: FAIL
[Units respond to the writing-mode of the element]
expected: FAIL

View File

@ -1,5 +0,0 @@
[container-units-small-viewport-fallback.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Use small viewport size as fallback]
expected: FAIL