Skip to main content

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