summaryrefslogtreecommitdiffstats
path: root/servo/components/style/stylesheets/container_rule.rs
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components/style/stylesheets/container_rule.rs')
-rw-r--r--servo/components/style/stylesheets/container_rule.rs632
1 files changed, 632 insertions, 0 deletions
diff --git a/servo/components/style/stylesheets/container_rule.rs b/servo/components/style/stylesheets/container_rule.rs
new file mode 100644
index 0000000000..f9d488b9b4
--- /dev/null
+++ b/servo/components/style/stylesheets/container_rule.rs
@@ -0,0 +1,632 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! A [`@container`][container] rule.
+//!
+//! [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;
+use crate::parser::ParserContext;
+use crate::properties::ComputedValues;
+use crate::queries::condition::KleeneValue;
+use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription};
+use crate::queries::values::Orientation;
+use crate::queries::{FeatureType, QueryCondition};
+use crate::shared_lock::{
+ DeepCloneParams, DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard,
+};
+use crate::str::CssStringWriter;
+use crate::stylesheets::CssRules;
+use crate::values::computed::{CSSPixelLength, ContainerType, Context, Ratio};
+use crate::values::specified::ContainerName;
+use app_units::Au;
+use cssparser::{Parser, SourceLocation};
+use euclid::default::Size2D;
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A container rule.
+#[derive(Debug, ToShmem)]
+pub struct ContainerRule {
+ /// The container query and name.
+ pub condition: Arc<ContainerCondition>,
+ /// The nested rules inside the block.
+ pub rules: Arc<Locked<CssRules>>,
+ /// The source position where this rule was found.
+ pub source_location: SourceLocation,
+}
+
+impl ContainerRule {
+ /// Returns the query condition.
+ pub fn query_condition(&self) -> &QueryCondition {
+ &self.condition.condition
+ }
+
+ /// Returns the query name filter.
+ pub fn container_name(&self) -> &ContainerName {
+ &self.condition.name
+ }
+
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ // Measurement of other fields may be added later.
+ self.rules.unconditional_shallow_size_of(ops) +
+ self.rules.read_with(guard).size_of(guard, ops)
+ }
+}
+
+impl DeepCloneWithLock for ContainerRule {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ let rules = self.rules.read_with(guard);
+ Self {
+ condition: self.condition.clone(),
+ rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
+
+impl ToCssWithGuard for ContainerRule {
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@container ")?;
+ {
+ let mut writer = CssWriter::new(dest);
+ if !self.condition.name.is_none() {
+ self.condition.name.to_css(&mut writer)?;
+ writer.write_char(' ')?;
+ }
+ self.condition.condition.to_css(&mut writer)?;
+ }
+ self.rules.read_with(guard).to_css_block(guard, dest)
+ }
+}
+
+/// A container condition and filter, combined.
+#[derive(Debug, ToShmem, ToCss)]
+pub struct ContainerCondition {
+ #[css(skip_if = "ContainerName::is_none")]
+ name: ContainerName,
+ condition: QueryCondition,
+ #[css(skip)]
+ flags: FeatureFlags,
+}
+
+/// The result of a successful container query lookup.
+pub struct ContainerLookupResult<E> {
+ /// The relevant container.
+ pub element: E,
+ /// The sizing / writing-mode information of the container.
+ pub info: ContainerInfo,
+ /// The style of the element.
+ pub style: Arc<ComputedValues>,
+}
+
+fn container_type_axes(ty_: ContainerType, wm: WritingMode) -> FeatureFlags {
+ match ty_ {
+ ContainerType::Size => FeatureFlags::all_container_axes(),
+ ContainerType::InlineSize => {
+ let physical_axis = if wm.is_vertical() {
+ FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS
+ } else {
+ FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS
+ };
+ FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS | physical_axis
+ },
+ ContainerType::Normal => FeatureFlags::empty(),
+ }
+}
+
+enum TraversalResult<T> {
+ InProgress,
+ StopTraversal,
+ Done(T),
+}
+
+fn traverse_container<E, F, R>(
+ mut e: E,
+ originating_element_style: Option<&ComputedValues>,
+ evaluator: F,
+) -> Option<(E, R)>
+where
+ E: TElement,
+ F: Fn(E, Option<&ComputedValues>) -> TraversalResult<R>,
+{
+ if originating_element_style.is_some() {
+ match evaluator(e, originating_element_style) {
+ TraversalResult::InProgress => {},
+ TraversalResult::StopTraversal => return None,
+ TraversalResult::Done(result) => return Some((e, result)),
+ }
+ }
+ while let Some(element) = e.traversal_parent() {
+ match evaluator(element, None) {
+ TraversalResult::InProgress => {},
+ TraversalResult::StopTraversal => return None,
+ TraversalResult::Done(result) => return Some((element, result)),
+ }
+ e = element;
+ }
+
+ None
+}
+
+impl ContainerCondition {
+ /// Parse a container condition.
+ pub fn parse<'a>(
+ context: &ParserContext,
+ input: &mut Parser<'a, '_>,
+ ) -> Result<Self, ParseError<'a>> {
+ let name = input
+ .try_parse(|input| ContainerName::parse_for_query(context, input))
+ .ok()
+ .unwrap_or_else(ContainerName::none);
+ let condition = QueryCondition::parse(context, input, FeatureType::Container)?;
+ let flags = condition.cumulative_flags();
+ Ok(Self {
+ name,
+ condition,
+ flags,
+ })
+ }
+
+ fn valid_container_info<E>(
+ &self,
+ potential_container: E,
+ originating_element_style: Option<&ComputedValues>,
+ ) -> TraversalResult<ContainerLookupResult<E>>
+ where
+ E: TElement,
+ {
+ let data;
+ let style = match originating_element_style {
+ Some(s) => s,
+ None => {
+ data = match potential_container.borrow_data() {
+ Some(d) => d,
+ None => return TraversalResult::InProgress,
+ };
+ &**data.styles.primary()
+ },
+ };
+ let wm = style.writing_mode;
+ let box_style = style.get_box();
+
+ // Filter by container-type.
+ 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 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 TraversalResult::InProgress;
+ }
+ }
+
+ let size = potential_container.query_container_size(&box_style.clone_display());
+ let style = style.to_arc();
+ TraversalResult::Done(ContainerLookupResult {
+ element: potential_container,
+ info: ContainerInfo { size, wm },
+ style,
+ })
+ }
+
+ /// Performs container lookup for a given element.
+ pub fn find_container<E>(
+ &self,
+ e: E,
+ originating_element_style: Option<&ComputedValues>,
+ ) -> Option<ContainerLookupResult<E>>
+ where
+ E: TElement,
+ {
+ match traverse_container(
+ e,
+ originating_element_style,
+ |element, originating_element_style| {
+ self.valid_container_info(element, originating_element_style)
+ },
+ ) {
+ Some((_, result)) => Some(result),
+ None => None,
+ }
+ }
+
+ /// Tries to match a container query condition for a given element.
+ pub(crate) fn matches<E>(
+ &self,
+ device: &Device,
+ element: E,
+ originating_element_style: Option<&ComputedValues>,
+ invalidation_flags: &mut ComputedValueFlags,
+ ) -> KleeneValue
+ where
+ E: TElement,
+ {
+ let result = self.find_container(element, originating_element_style);
+ 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, None);
+ Context::for_container_query_evaluation(
+ device,
+ info,
+ size_query_container_lookup,
+ |context| {
+ let matches = self.condition.matches(context);
+ if context
+ .style()
+ .flags()
+ .contains(ComputedValueFlags::USES_VIEWPORT_UNITS)
+ {
+ // TODO(emilio): Might need something similar to improve
+ // invalidation of font relative container-query lengths.
+ invalidation_flags
+ .insert(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES);
+ }
+ matches
+ },
+ )
+ }
+}
+
+/// Information needed to evaluate an individual container query.
+#[derive(Copy, Clone)]
+pub struct ContainerInfo {
+ size: Size2D<Option<Au>>,
+ wm: WritingMode,
+}
+
+impl ContainerInfo {
+ fn size(&self) -> Option<Size2D<Au>> {
+ Some(Size2D::new(self.size.width?, self.size.height?))
+ }
+}
+
+fn eval_width(context: &Context) -> Option<CSSPixelLength> {
+ let info = context.container_info.as_ref()?;
+ Some(CSSPixelLength::new(info.size.width?.to_f32_px()))
+}
+
+fn eval_height(context: &Context) -> Option<CSSPixelLength> {
+ let info = context.container_info.as_ref()?;
+ Some(CSSPixelLength::new(info.size.height?.to_f32_px()))
+}
+
+fn eval_inline_size(context: &Context) -> Option<CSSPixelLength> {
+ let info = context.container_info.as_ref()?;
+ Some(CSSPixelLength::new(
+ LogicalSize::from_physical(info.wm, info.size)
+ .inline?
+ .to_f32_px(),
+ ))
+}
+
+fn eval_block_size(context: &Context) -> Option<CSSPixelLength> {
+ let info = context.container_info.as_ref()?;
+ Some(CSSPixelLength::new(
+ LogicalSize::from_physical(info.wm, info.size)
+ .block?
+ .to_f32_px(),
+ ))
+}
+
+fn eval_aspect_ratio(context: &Context) -> Option<Ratio> {
+ let info = context.container_info.as_ref()?;
+ Some(Ratio::new(
+ info.size.width?.0 as f32,
+ info.size.height?.0 as f32,
+ ))
+}
+
+fn eval_orientation(context: &Context, value: Option<Orientation>) -> KleeneValue {
+ let size = match context.container_info.as_ref().and_then(|info| info.size()) {
+ Some(size) => size,
+ None => return KleeneValue::Unknown,
+ };
+ KleeneValue::from(Orientation::eval(size, value))
+}
+
+/// https://drafts.csswg.org/css-contain-3/#container-features
+///
+/// TODO: Support style queries, perhaps.
+pub static CONTAINER_FEATURES: [QueryFeatureDescription; 6] = [
+ feature!(
+ atom!("width"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalLength(eval_width),
+ FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS,
+ ),
+ feature!(
+ atom!("height"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalLength(eval_height),
+ FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS,
+ ),
+ feature!(
+ atom!("inline-size"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalLength(eval_inline_size),
+ FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS,
+ ),
+ feature!(
+ atom!("block-size"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalLength(eval_block_size),
+ FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS,
+ ),
+ feature!(
+ atom!("aspect-ratio"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalNumberRatio(eval_aspect_ratio),
+ // XXX from_bits_truncate is const, but the pipe operator isn't, so this
+ // works around it.
+ FeatureFlags::from_bits_truncate(
+ FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() |
+ FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits()
+ ),
+ ),
+ feature!(
+ atom!("orientation"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_orientation, Orientation),
+ FeatureFlags::from_bits_truncate(
+ FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() |
+ FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits()
+ ),
+ ),
+];
+
+/// 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.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,
+ originating_element_style: Option<&ComputedValues>,
+ ) -> TraversalResult<ContainerSizeQueryResult>
+ where
+ E: TElement,
+ {
+ let data;
+ let style = match originating_element_style {
+ Some(s) => s,
+ None => {
+ data = match e.borrow_data() {
+ Some(d) => d,
+ None => return TraversalResult::InProgress,
+ };
+ &**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.query_container_size(&box_style.clone_display());
+ match container_type {
+ ContainerType::Size => TraversalResult::Done(ContainerSizeQueryResult {
+ width: size.width,
+ height: size.height,
+ }),
+ ContainerType::InlineSize => {
+ if wm.is_horizontal() {
+ TraversalResult::Done(ContainerSizeQueryResult {
+ width: size.width,
+ height: None,
+ })
+ } else {
+ TraversalResult::Done(ContainerSizeQueryResult {
+ width: None,
+ height: size.height,
+ })
+ }
+ },
+ ContainerType::Normal => 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,
+ originating_element_style: Option<&ComputedValues>,
+ ) -> ContainerSizeQueryResult
+ where
+ E: TElement + 'a,
+ {
+ match traverse_container(
+ element,
+ originating_element_style,
+ |e, originating_element_style| {
+ Self::evaluate_potential_size_container(e, originating_element_style)
+ },
+ ) {
+ 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))
+ }
+ },
+ 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, originating_element_style: Option<&'a ComputedValues>) -> Self
+ where
+ E: TElement + 'a,
+ {
+ let parent;
+ let data;
+ let style = match originating_element_style {
+ Some(s) => Some(s),
+ None => {
+ // No need to bother if we're the top element.
+ parent = match element.traversal_parent() {
+ Some(parent) => parent,
+ None => return Self::none(),
+ };
+ data = parent.borrow_data();
+ data.as_ref().map(|data| &**data.styles.primary())
+ },
+ };
+ let should_traverse = match style {
+ Some(style) => 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, originating_element_style)
+ }));
+ }
+ Self::none()
+ }
+
+ /// Create a new instance, but with optional element.
+ pub fn for_option_element<E>(
+ element: Option<E>,
+ originating_element_style: Option<&'a ComputedValues>,
+ ) -> Self
+ where
+ E: TElement + 'a,
+ {
+ if let Some(e) = element {
+ Self::for_element(e, originating_element_style)
+ } 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,
+ }
+ }
+}