From 27958b191a2084f1ff7962ffa66b22f8e7d215eb Mon Sep 17 00:00:00 2001 From: David Shin Date: Mon, 17 Oct 2022 17:26:13 +0000 Subject: [PATCH] style: Enable lookup and computation of container size queries Differential Revision: https://phabricator.services.mozilla.com/D158057 --- components/style/properties/cascade.rs | 3 + .../style/stylesheets/container_rule.rs | 316 ++++++++++++++++-- components/style/stylesheets/viewport_rule.rs | 2 + components/style/values/computed/mod.rs | 20 +- components/style/values/specified/length.rs | 30 +- 5 files changed, 326 insertions(+), 45 deletions(-) diff --git a/components/style/properties/cascade.rs b/components/style/properties/cascade.rs index 54fcda7481a..ce928c1c692 100644 --- a/components/style/properties/cascade.rs +++ b/components/style/properties/cascade.rs @@ -21,6 +21,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; @@ -275,6 +276,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 @@ -291,6 +293,7 @@ where ), quirks_mode, rule_cache_conditions, + container_size_query, ); let using_cached_reset_properties; diff --git a/components/style/stylesheets/container_rule.rs b/components/style/stylesheets/container_rule.rs index cc86be32ae3..6850fa2f853 100644 --- a/components/style/stylesheets/container_rule.rs +++ b/components/style/stylesheets/container_rule.rs @@ -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 { pub style: Arc, } +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 { + InProgress, + StopTraversal, + Done(T), +} + +fn traverse_container(mut e: E, evaluator: F) -> Option<(E, R)> +where + E: TElement, + F: Fn(E) -> TraversalResult +{ + 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(&self, potential_container: E) -> Option> + fn valid_container_info( + &self, + potential_container: E + ) -> TraversalResult> 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(&self, mut e: E) -> Option> + pub fn find_container(&self, e: E) -> Option> 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, + height: Option, +} + +impl ContainerSizeQueryResult { + fn get_viewport_size(context: &Context) -> Size2D { + use crate::values::specified::ViewportVariant; + context + .device() + .au_viewport_size_for_viewport_unit_resolution(ViewportVariant::Small) + } + + fn get_logical_viewport_size(context: &Context) -> LogicalSize { + 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 ContainerSizeQueryResult + 'a>), + /// Cached evaluated result. + Evaluated(ContainerSizeQueryResult), +} + +impl<'a> ContainerSizeQuery<'a> { + fn evaluate_potential_size_container( + e: E + ) -> TraversalResult + 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(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(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(element: Option) -> 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, + } + } +} diff --git a/components/style/stylesheets/viewport_rule.rs b/components/style/stylesheets/viewport_rule.rs index f112aff17ab..42a29a4b6ec 100644 --- a/components/style/stylesheets/viewport_rule.rs +++ b/components/style/stylesheets/viewport_rule.rs @@ -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; @@ -670,6 +671,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' diff --git a/components/style/values/computed/mod.rs b/components/style/values/computed/mod.rs index af0e30c93ad..0ef6568c669 100644 --- a/components/style/values/computed/mod.rs +++ b/components/style/values/computed/mod.rs @@ -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>, } 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( device: &Device, container_info_and_style: Option<(ContainerInfo, Arc)>, + 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), } } diff --git a/components/style/values/specified/length.rs b/components/style/values/specified/length.rs index f9b22352b8e..523ae05354a 100644 --- a/components/style/values/specified/length.rs +++ b/components/style/values/specified/length.rs @@ -716,16 +716,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) } }