style/stylesheets/
container_rule.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! A [`@container`][container] rule.
6//!
7//! [container]: https://drafts.csswg.org/css-contain-3/#container-rule
8
9use crate::computed_value_flags::ComputedValueFlags;
10use crate::dom::TElement;
11use crate::logical_geometry::{LogicalSize, WritingMode};
12use crate::parser::ParserContext;
13use crate::properties::ComputedValues;
14use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription};
15use crate::queries::values::Orientation;
16use crate::queries::{FeatureType, QueryCondition};
17use crate::shared_lock::{
18    DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard,
19};
20use crate::str::CssStringWriter;
21use crate::stylesheets::CssRules;
22use crate::stylist::Stylist;
23use crate::values::computed::{CSSPixelLength, ContainerType, Context, Ratio};
24use crate::values::specified::ContainerName;
25use app_units::Au;
26use cssparser::{Parser, SourceLocation};
27use euclid::default::Size2D;
28#[cfg(feature = "gecko")]
29use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
30use selectors::kleene_value::KleeneValue;
31use servo_arc::Arc;
32use std::fmt::{self, Write};
33use style_traits::{CssWriter, ParseError, ToCss};
34
35/// A container rule.
36#[derive(Debug, ToShmem)]
37pub struct ContainerRule {
38    /// The container query and name.
39    pub condition: Arc<ContainerCondition>,
40    /// The nested rules inside the block.
41    pub rules: Arc<Locked<CssRules>>,
42    /// The source position where this rule was found.
43    pub source_location: SourceLocation,
44}
45
46impl ContainerRule {
47    /// Returns the query condition.
48    pub fn query_condition(&self) -> &QueryCondition {
49        &self.condition.condition
50    }
51
52    /// Returns the query name filter.
53    pub fn container_name(&self) -> &ContainerName {
54        &self.condition.name
55    }
56
57    /// Measure heap usage.
58    #[cfg(feature = "gecko")]
59    pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
60        // Measurement of other fields may be added later.
61        self.rules.unconditional_shallow_size_of(ops) +
62            self.rules.read_with(guard).size_of(guard, ops)
63    }
64}
65
66impl DeepCloneWithLock for ContainerRule {
67    fn deep_clone_with_lock(
68        &self,
69        lock: &SharedRwLock,
70        guard: &SharedRwLockReadGuard,
71    ) -> Self {
72        let rules = self.rules.read_with(guard);
73        Self {
74            condition: self.condition.clone(),
75            rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))),
76            source_location: self.source_location.clone(),
77        }
78    }
79}
80
81impl ToCssWithGuard for ContainerRule {
82    fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
83        dest.write_str("@container ")?;
84        {
85            let mut writer = CssWriter::new(dest);
86            if !self.condition.name.is_none() {
87                self.condition.name.to_css(&mut writer)?;
88                writer.write_char(' ')?;
89            }
90            self.condition.condition.to_css(&mut writer)?;
91        }
92        self.rules.read_with(guard).to_css_block(guard, dest)
93    }
94}
95
96/// A container condition and filter, combined.
97#[derive(Debug, ToShmem, ToCss)]
98pub struct ContainerCondition {
99    #[css(skip_if = "ContainerName::is_none")]
100    name: ContainerName,
101    condition: QueryCondition,
102    #[css(skip)]
103    flags: FeatureFlags,
104}
105
106/// The result of a successful container query lookup.
107pub struct ContainerLookupResult<E> {
108    /// The relevant container.
109    pub element: E,
110    /// The sizing / writing-mode information of the container.
111    pub info: ContainerInfo,
112    /// The style of the element.
113    pub style: Arc<ComputedValues>,
114}
115
116fn container_type_axes(ty_: ContainerType, wm: WritingMode) -> FeatureFlags {
117    match ty_ {
118        ContainerType::Size => FeatureFlags::all_container_axes(),
119        ContainerType::InlineSize => {
120            let physical_axis = if wm.is_vertical() {
121                FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS
122            } else {
123                FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS
124            };
125            FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS | physical_axis
126        },
127        ContainerType::Normal => FeatureFlags::empty(),
128    }
129}
130
131enum TraversalResult<T> {
132    InProgress,
133    StopTraversal,
134    Done(T),
135}
136
137fn traverse_container<E, F, R>(
138    mut e: E,
139    originating_element_style: Option<&ComputedValues>,
140    evaluator: F,
141) -> Option<(E, R)>
142where
143    E: TElement,
144    F: Fn(E, Option<&ComputedValues>) -> TraversalResult<R>,
145{
146    if originating_element_style.is_some() {
147        match evaluator(e, originating_element_style) {
148            TraversalResult::InProgress => {},
149            TraversalResult::StopTraversal => return None,
150            TraversalResult::Done(result) => return Some((e, result)),
151        }
152    }
153    while let Some(element) = e.traversal_parent() {
154        match evaluator(element, None) {
155            TraversalResult::InProgress => {},
156            TraversalResult::StopTraversal => return None,
157            TraversalResult::Done(result) => return Some((element, result)),
158        }
159        e = element;
160    }
161
162    None
163}
164
165impl ContainerCondition {
166    /// Parse a container condition.
167    pub fn parse<'a>(
168        context: &ParserContext,
169        input: &mut Parser<'a, '_>,
170    ) -> Result<Self, ParseError<'a>> {
171        let name = input
172            .try_parse(|input| ContainerName::parse_for_query(context, input))
173            .ok()
174            .unwrap_or_else(ContainerName::none);
175        let condition = QueryCondition::parse(context, input, FeatureType::Container)?;
176        let flags = condition.cumulative_flags();
177        Ok(Self {
178            name,
179            condition,
180            flags,
181        })
182    }
183
184    fn valid_container_info<E>(
185        &self,
186        potential_container: E,
187        originating_element_style: Option<&ComputedValues>,
188    ) -> TraversalResult<ContainerLookupResult<E>>
189    where
190        E: TElement,
191    {
192        let data;
193        let style = match originating_element_style {
194            Some(s) => s,
195            None => {
196                data = match potential_container.borrow_data() {
197                    Some(d) => d,
198                    None => return TraversalResult::InProgress,
199                };
200                &**data.styles.primary()
201            },
202        };
203        let wm = style.writing_mode;
204        let box_style = style.get_box();
205
206        // Filter by container-type.
207        let container_type = box_style.clone_container_type();
208        let available_axes = container_type_axes(container_type, wm);
209        if !available_axes.contains(self.flags.container_axes()) {
210            return TraversalResult::InProgress;
211        }
212
213        // Filter by container-name.
214        let container_name = box_style.clone_container_name();
215        for filter_name in self.name.0.iter() {
216            if !container_name.0.contains(filter_name) {
217                return TraversalResult::InProgress;
218            }
219        }
220
221        let size = potential_container.query_container_size(&box_style.clone_display());
222        let style = style.to_arc();
223        TraversalResult::Done(ContainerLookupResult {
224            element: potential_container,
225            info: ContainerInfo { size, wm },
226            style,
227        })
228    }
229
230    /// Performs container lookup for a given element.
231    pub fn find_container<E>(
232        &self,
233        e: E,
234        originating_element_style: Option<&ComputedValues>,
235    ) -> Option<ContainerLookupResult<E>>
236    where
237        E: TElement,
238    {
239        match traverse_container(
240            e,
241            originating_element_style,
242            |element, originating_element_style| {
243                self.valid_container_info(element, originating_element_style)
244            },
245        ) {
246            Some((_, result)) => Some(result),
247            None => None,
248        }
249    }
250
251    /// Tries to match a container query condition for a given element.
252    pub(crate) fn matches<E>(
253        &self,
254        stylist: &Stylist,
255        element: E,
256        originating_element_style: Option<&ComputedValues>,
257        invalidation_flags: &mut ComputedValueFlags,
258    ) -> KleeneValue
259    where
260        E: TElement,
261    {
262        let result = self.find_container(element, originating_element_style);
263        let (container, info) = match result {
264            Some(r) => (Some(r.element), Some((r.info, r.style))),
265            None => (None, None),
266        };
267        // Set up the lookup for the container in question, as the condition may be using container
268        // query lengths.
269        let size_query_container_lookup = ContainerSizeQuery::for_option_element(
270            container, /* known_parent_style = */ None, /* is_pseudo = */ false,
271        );
272        Context::for_container_query_evaluation(
273            stylist.device(),
274            Some(stylist),
275            info,
276            size_query_container_lookup,
277            |context| {
278                let matches = self.condition.matches(context);
279                if context
280                    .style()
281                    .flags()
282                    .contains(ComputedValueFlags::USES_VIEWPORT_UNITS)
283                {
284                    // TODO(emilio): Might need something similar to improve
285                    // invalidation of font relative container-query lengths.
286                    invalidation_flags
287                        .insert(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES);
288                }
289                matches
290            },
291        )
292    }
293}
294
295/// Information needed to evaluate an individual container query.
296#[derive(Copy, Clone)]
297pub struct ContainerInfo {
298    size: Size2D<Option<Au>>,
299    wm: WritingMode,
300}
301
302impl ContainerInfo {
303    fn size(&self) -> Option<Size2D<Au>> {
304        Some(Size2D::new(self.size.width?, self.size.height?))
305    }
306}
307
308fn eval_width(context: &Context) -> Option<CSSPixelLength> {
309    let info = context.container_info.as_ref()?;
310    Some(CSSPixelLength::new(info.size.width?.to_f32_px()))
311}
312
313fn eval_height(context: &Context) -> Option<CSSPixelLength> {
314    let info = context.container_info.as_ref()?;
315    Some(CSSPixelLength::new(info.size.height?.to_f32_px()))
316}
317
318fn eval_inline_size(context: &Context) -> Option<CSSPixelLength> {
319    let info = context.container_info.as_ref()?;
320    Some(CSSPixelLength::new(
321        LogicalSize::from_physical(info.wm, info.size)
322            .inline?
323            .to_f32_px(),
324    ))
325}
326
327fn eval_block_size(context: &Context) -> Option<CSSPixelLength> {
328    let info = context.container_info.as_ref()?;
329    Some(CSSPixelLength::new(
330        LogicalSize::from_physical(info.wm, info.size)
331            .block?
332            .to_f32_px(),
333    ))
334}
335
336fn eval_aspect_ratio(context: &Context) -> Option<Ratio> {
337    let info = context.container_info.as_ref()?;
338    Some(Ratio::new(
339        info.size.width?.0 as f32,
340        info.size.height?.0 as f32,
341    ))
342}
343
344fn eval_orientation(context: &Context, value: Option<Orientation>) -> KleeneValue {
345    let size = match context.container_info.as_ref().and_then(|info| info.size()) {
346        Some(size) => size,
347        None => return KleeneValue::Unknown,
348    };
349    KleeneValue::from(Orientation::eval(size, value))
350}
351
352/// https://drafts.csswg.org/css-contain-3/#container-features
353///
354/// TODO: Support style queries, perhaps.
355pub static CONTAINER_FEATURES: [QueryFeatureDescription; 6] = [
356    feature!(
357        atom!("width"),
358        AllowsRanges::Yes,
359        Evaluator::OptionalLength(eval_width),
360        FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS,
361    ),
362    feature!(
363        atom!("height"),
364        AllowsRanges::Yes,
365        Evaluator::OptionalLength(eval_height),
366        FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS,
367    ),
368    feature!(
369        atom!("inline-size"),
370        AllowsRanges::Yes,
371        Evaluator::OptionalLength(eval_inline_size),
372        FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS,
373    ),
374    feature!(
375        atom!("block-size"),
376        AllowsRanges::Yes,
377        Evaluator::OptionalLength(eval_block_size),
378        FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS,
379    ),
380    feature!(
381        atom!("aspect-ratio"),
382        AllowsRanges::Yes,
383        Evaluator::OptionalNumberRatio(eval_aspect_ratio),
384        // XXX from_bits_truncate is const, but the pipe operator isn't, so this
385        // works around it.
386        FeatureFlags::from_bits_truncate(
387            FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() |
388                FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits()
389        ),
390    ),
391    feature!(
392        atom!("orientation"),
393        AllowsRanges::No,
394        keyword_evaluator!(eval_orientation, Orientation),
395        FeatureFlags::from_bits_truncate(
396            FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() |
397                FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits()
398        ),
399    ),
400];
401
402/// Result of a container size query, signifying the hypothetical containment boundary in terms of physical axes.
403/// Defined by up to two size containers. Queries on logical axes are resolved with respect to the querying
404/// element's writing mode.
405#[derive(Copy, Clone, Default)]
406pub struct ContainerSizeQueryResult {
407    width: Option<Au>,
408    height: Option<Au>,
409}
410
411impl ContainerSizeQueryResult {
412    fn get_viewport_size(context: &Context) -> Size2D<Au> {
413        use crate::values::specified::ViewportVariant;
414        context.viewport_size_for_viewport_unit_resolution(ViewportVariant::Small)
415    }
416
417    fn get_logical_viewport_size(context: &Context) -> LogicalSize<Au> {
418        LogicalSize::from_physical(
419            context.builder.writing_mode,
420            Self::get_viewport_size(context),
421        )
422    }
423
424    /// Get the inline-size of the query container.
425    pub fn get_container_inline_size(&self, context: &Context) -> Au {
426        if context.builder.writing_mode.is_horizontal() {
427            if let Some(w) = self.width {
428                return w;
429            }
430        } else {
431            if let Some(h) = self.height {
432                return h;
433            }
434        }
435        Self::get_logical_viewport_size(context).inline
436    }
437
438    /// Get the block-size of the query container.
439    pub fn get_container_block_size(&self, context: &Context) -> Au {
440        if context.builder.writing_mode.is_horizontal() {
441            self.get_container_height(context)
442        } else {
443            self.get_container_width(context)
444        }
445    }
446
447    /// Get the width of the query container.
448    pub fn get_container_width(&self, context: &Context) -> Au {
449        if let Some(w) = self.width {
450            return w;
451        }
452        Self::get_viewport_size(context).width
453    }
454
455    /// Get the height of the query container.
456    pub fn get_container_height(&self, context: &Context) -> Au {
457        if let Some(h) = self.height {
458            return h;
459        }
460        Self::get_viewport_size(context).height
461    }
462
463    // Merge the result of a subsequent lookup, preferring the initial result.
464    fn merge(self, new_result: Self) -> Self {
465        let mut result = self;
466        if let Some(width) = new_result.width {
467            result.width.get_or_insert(width);
468        }
469        if let Some(height) = new_result.height {
470            result.height.get_or_insert(height);
471        }
472        result
473    }
474
475    fn is_complete(&self) -> bool {
476        self.width.is_some() && self.height.is_some()
477    }
478}
479
480/// Unevaluated lazy container size query.
481pub enum ContainerSizeQuery<'a> {
482    /// Query prior to evaluation.
483    NotEvaluated(Box<dyn Fn() -> ContainerSizeQueryResult + 'a>),
484    /// Cached evaluated result.
485    Evaluated(ContainerSizeQueryResult),
486}
487
488impl<'a> ContainerSizeQuery<'a> {
489    fn evaluate_potential_size_container<E>(
490        e: E,
491        originating_element_style: Option<&ComputedValues>,
492    ) -> TraversalResult<ContainerSizeQueryResult>
493    where
494        E: TElement,
495    {
496        let data;
497        let style = match originating_element_style {
498            Some(s) => s,
499            None => {
500                data = match e.borrow_data() {
501                    Some(d) => d,
502                    None => return TraversalResult::InProgress,
503                };
504                &**data.styles.primary()
505            },
506        };
507        if !style
508            .flags
509            .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE)
510        {
511            // We know we won't find a size container.
512            return TraversalResult::StopTraversal;
513        }
514
515        let wm = style.writing_mode;
516        let box_style = style.get_box();
517
518        let container_type = box_style.clone_container_type();
519        let size = e.query_container_size(&box_style.clone_display());
520        match container_type {
521            ContainerType::Size => TraversalResult::Done(ContainerSizeQueryResult {
522                width: size.width,
523                height: size.height,
524            }),
525            ContainerType::InlineSize => {
526                if wm.is_horizontal() {
527                    TraversalResult::Done(ContainerSizeQueryResult {
528                        width: size.width,
529                        height: None,
530                    })
531                } else {
532                    TraversalResult::Done(ContainerSizeQueryResult {
533                        width: None,
534                        height: size.height,
535                    })
536                }
537            },
538            ContainerType::Normal => TraversalResult::InProgress,
539        }
540    }
541
542    /// Find the query container size for a given element. Meant to be used as a callback for new().
543    fn lookup<E>(
544        element: E,
545        originating_element_style: Option<&ComputedValues>,
546    ) -> ContainerSizeQueryResult
547    where
548        E: TElement + 'a,
549    {
550        match traverse_container(
551            element,
552            originating_element_style,
553            |e, originating_element_style| {
554                Self::evaluate_potential_size_container(e, originating_element_style)
555            },
556        ) {
557            Some((container, result)) => {
558                if result.is_complete() {
559                    result
560                } else {
561                    // Traverse up from the found size container to see if we can get a complete containment.
562                    result.merge(Self::lookup(container, None))
563                }
564            },
565            None => ContainerSizeQueryResult::default(),
566        }
567    }
568
569    /// Create a new instance of the container size query for given element, with a deferred lookup callback.
570    pub fn for_element<E>(
571        element: E,
572        known_parent_style: Option<&'a ComputedValues>,
573        is_pseudo: bool,
574    ) -> Self
575    where
576        E: TElement + 'a,
577    {
578        let parent;
579        let data;
580        let parent_style = match known_parent_style {
581            Some(s) => Some(s),
582            None => {
583                // No need to bother if we're the top element.
584                parent = match element.traversal_parent() {
585                    Some(parent) => parent,
586                    None => return Self::none(),
587                };
588                data = parent.borrow_data();
589                data.as_ref().map(|data| &**data.styles.primary())
590            },
591        };
592
593        // If there's no style, such as being `display: none` or so, we still want to show a
594        // correct computed value, so give it a try.
595        let should_traverse = parent_style.map_or(true, |s| {
596            s.flags
597                .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE)
598        });
599        if !should_traverse {
600            return Self::none();
601        }
602        return Self::NotEvaluated(Box::new(move || {
603            Self::lookup(element, if is_pseudo { known_parent_style } else { None })
604        }));
605    }
606
607    /// Create a new instance, but with optional element.
608    pub fn for_option_element<E>(
609        element: Option<E>,
610        known_parent_style: Option<&'a ComputedValues>,
611        is_pseudo: bool,
612    ) -> Self
613    where
614        E: TElement + 'a,
615    {
616        if let Some(e) = element {
617            Self::for_element(e, known_parent_style, is_pseudo)
618        } else {
619            Self::none()
620        }
621    }
622
623    /// Create a query that evaluates to empty, for cases where container size query is not required.
624    pub fn none() -> Self {
625        ContainerSizeQuery::Evaluated(ContainerSizeQueryResult::default())
626    }
627
628    /// Get the result of the container size query, doing the lookup if called for the first time.
629    pub fn get(&mut self) -> ContainerSizeQueryResult {
630        match self {
631            Self::NotEvaluated(lookup) => {
632                *self = Self::Evaluated((lookup)());
633                match self {
634                    Self::Evaluated(info) => *info,
635                    _ => unreachable!("Just evaluated but not set?"),
636                }
637            },
638            Self::Evaluated(info) => *info,
639        }
640    }
641}