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