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::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::{CssStringWriter, CssWriter, ParseError, StyleParseErrorKind, 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, if any.
48    pub fn query_condition(&self) -> Option<&QueryCondition> {
49        self.condition.condition.as_ref()
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(&self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard) -> Self {
68        let rules = self.rules.read_with(guard);
69        Self {
70            condition: self.condition.clone(),
71            rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))),
72            source_location: self.source_location.clone(),
73        }
74    }
75}
76
77impl ToCssWithGuard for ContainerRule {
78    fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
79        dest.write_str("@container ")?;
80        {
81            let mut writer = CssWriter::new(dest);
82            if !self.condition.name.is_none() {
83                self.condition.name.to_css(&mut writer)?;
84                if self.condition.condition.is_some() {
85                    writer.write_char(' ')?;
86                }
87            }
88            if let Some(ref condition) = self.condition.condition {
89                condition.to_css(&mut writer)?;
90            }
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: Option<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    if ty_.intersects(ContainerType::SIZE) {
118        FeatureFlags::all_container_axes()
119    } else if ty_.intersects(ContainerType::INLINE_SIZE) {
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    } else {
127        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 = 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(crate) 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        let (container, info) = match result {
289            Some(r) => (r.element, (r.info, r.style)),
290            None => {
291                // If we did not find the named (or any) container,
292                // the query must fail to match.
293                return KleeneValue::False;
294            },
295        };
296        // Set up the lookup for the container in question, as the condition may be using container
297        // query lengths.
298        let size_query_container_lookup = ContainerSizeQuery::for_element(
299            container, /* known_parent_style = */ None, /* is_pseudo = */ false,
300        );
301        Context::for_container_query_evaluation(
302            stylist.device(),
303            Some(stylist),
304            Some(info),
305            size_query_container_lookup,
306            |context| {
307                let matches = condition.matches(context, &mut CustomMediaEvaluator::none());
308                if context
309                    .style()
310                    .flags()
311                    .contains(ComputedValueFlags::USES_VIEWPORT_UNITS)
312                {
313                    // TODO(emilio): Might need something similar to improve
314                    // invalidation of font relative container-query lengths.
315                    invalidation_flags
316                        .insert(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES);
317                }
318                matches
319            },
320        )
321    }
322}
323
324/// Information needed to evaluate an individual container query.
325#[derive(Clone)]
326pub struct ContainerInfo {
327    size: Size2D<Option<Au>>,
328    wm: WritingMode,
329    inherited_style: Option<Arc<ComputedValues>>,
330}
331
332impl ContainerInfo {
333    fn size(&self) -> Option<Size2D<Au>> {
334        Some(Size2D::new(self.size.width?, self.size.height?))
335    }
336
337    /// Get a reference to the container's inherited style, if any.
338    pub fn inherited_style(&self) -> Option<&ComputedValues> {
339        self.inherited_style.as_deref()
340    }
341}
342
343fn eval_width(context: &Context) -> Option<CSSPixelLength> {
344    let info = context.container_info.as_ref()?;
345    Some(CSSPixelLength::new(info.size.width?.to_f32_px()))
346}
347
348fn eval_height(context: &Context) -> Option<CSSPixelLength> {
349    let info = context.container_info.as_ref()?;
350    Some(CSSPixelLength::new(info.size.height?.to_f32_px()))
351}
352
353fn eval_inline_size(context: &Context) -> Option<CSSPixelLength> {
354    let info = context.container_info.as_ref()?;
355    Some(CSSPixelLength::new(
356        LogicalSize::from_physical(info.wm, info.size)
357            .inline?
358            .to_f32_px(),
359    ))
360}
361
362fn eval_block_size(context: &Context) -> Option<CSSPixelLength> {
363    let info = context.container_info.as_ref()?;
364    Some(CSSPixelLength::new(
365        LogicalSize::from_physical(info.wm, info.size)
366            .block?
367            .to_f32_px(),
368    ))
369}
370
371fn eval_aspect_ratio(context: &Context) -> Option<Ratio> {
372    let info = context.container_info.as_ref()?;
373    Some(Ratio::new(
374        info.size.width?.0 as f32,
375        info.size.height?.0 as f32,
376    ))
377}
378
379fn eval_orientation(context: &Context, value: Option<Orientation>) -> KleeneValue {
380    let size = match context.container_info.as_ref().and_then(|info| info.size()) {
381        Some(size) => size,
382        None => return KleeneValue::Unknown,
383    };
384    KleeneValue::from(Orientation::eval(size, value))
385}
386
387/// https://drafts.csswg.org/css-contain-3/#container-features
388///
389/// TODO: Support style queries, perhaps.
390pub static CONTAINER_FEATURES: [QueryFeatureDescription; 6] = [
391    feature!(
392        atom!("width"),
393        AllowsRanges::Yes,
394        Evaluator::OptionalLength(eval_width),
395        FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS,
396    ),
397    feature!(
398        atom!("height"),
399        AllowsRanges::Yes,
400        Evaluator::OptionalLength(eval_height),
401        FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS,
402    ),
403    feature!(
404        atom!("inline-size"),
405        AllowsRanges::Yes,
406        Evaluator::OptionalLength(eval_inline_size),
407        FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS,
408    ),
409    feature!(
410        atom!("block-size"),
411        AllowsRanges::Yes,
412        Evaluator::OptionalLength(eval_block_size),
413        FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS,
414    ),
415    feature!(
416        atom!("aspect-ratio"),
417        AllowsRanges::Yes,
418        Evaluator::OptionalNumberRatio(eval_aspect_ratio),
419        // XXX from_bits_truncate is const, but the pipe operator isn't, so this
420        // works around it.
421        FeatureFlags::from_bits_truncate(
422            FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits()
423                | FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits()
424        ),
425    ),
426    feature!(
427        atom!("orientation"),
428        AllowsRanges::No,
429        keyword_evaluator!(eval_orientation, Orientation),
430        FeatureFlags::from_bits_truncate(
431            FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits()
432                | FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits()
433        ),
434    ),
435];
436
437/// Result of a container size query, signifying the hypothetical containment boundary in terms of physical axes.
438/// Defined by up to two size containers. Queries on logical axes are resolved with respect to the querying
439/// element's writing mode.
440#[derive(Copy, Clone, Default)]
441pub struct ContainerSizeQueryResult {
442    width: Option<Au>,
443    height: Option<Au>,
444}
445
446impl ContainerSizeQueryResult {
447    fn get_viewport_size(context: &Context) -> Size2D<Au> {
448        use crate::values::specified::ViewportVariant;
449        context.viewport_size_for_viewport_unit_resolution(ViewportVariant::Small)
450    }
451
452    fn get_logical_viewport_size(context: &Context) -> LogicalSize<Au> {
453        LogicalSize::from_physical(
454            context.builder.writing_mode,
455            Self::get_viewport_size(context),
456        )
457    }
458
459    /// Get the inline-size of the query container.
460    pub fn get_container_inline_size(&self, context: &Context) -> Au {
461        if context.builder.writing_mode.is_horizontal() {
462            if let Some(w) = self.width {
463                return w;
464            }
465        } else {
466            if let Some(h) = self.height {
467                return h;
468            }
469        }
470        Self::get_logical_viewport_size(context).inline
471    }
472
473    /// Get the block-size of the query container.
474    pub fn get_container_block_size(&self, context: &Context) -> Au {
475        if context.builder.writing_mode.is_horizontal() {
476            self.get_container_height(context)
477        } else {
478            self.get_container_width(context)
479        }
480    }
481
482    /// Get the width of the query container.
483    pub fn get_container_width(&self, context: &Context) -> Au {
484        if let Some(w) = self.width {
485            return w;
486        }
487        Self::get_viewport_size(context).width
488    }
489
490    /// Get the height of the query container.
491    pub fn get_container_height(&self, context: &Context) -> Au {
492        if let Some(h) = self.height {
493            return h;
494        }
495        Self::get_viewport_size(context).height
496    }
497
498    // Merge the result of a subsequent lookup, preferring the initial result.
499    fn merge(self, new_result: Self) -> Self {
500        let mut result = self;
501        if let Some(width) = new_result.width {
502            result.width.get_or_insert(width);
503        }
504        if let Some(height) = new_result.height {
505            result.height.get_or_insert(height);
506        }
507        result
508    }
509
510    fn is_complete(&self) -> bool {
511        self.width.is_some() && self.height.is_some()
512    }
513}
514
515/// Unevaluated lazy container size query.
516pub enum ContainerSizeQuery<'a> {
517    /// Query prior to evaluation.
518    NotEvaluated(Box<dyn Fn() -> ContainerSizeQueryResult + 'a>),
519    /// Cached evaluated result.
520    Evaluated(ContainerSizeQueryResult),
521}
522
523impl<'a> ContainerSizeQuery<'a> {
524    fn evaluate_potential_size_container<E>(
525        e: E,
526        originating_element_style: Option<&ComputedValues>,
527    ) -> TraversalResult<ContainerSizeQueryResult>
528    where
529        E: TElement,
530    {
531        let data;
532        let style = match originating_element_style {
533            Some(s) => s,
534            None => {
535                data = match e.borrow_data() {
536                    Some(d) => d,
537                    None => return TraversalResult::InProgress,
538                };
539                &**data.styles.primary()
540            },
541        };
542        if !style
543            .flags
544            .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE)
545        {
546            // We know we won't find a size container.
547            return TraversalResult::StopTraversal;
548        }
549
550        let wm = style.writing_mode;
551        let box_style = style.get_box();
552
553        let container_type = box_style.clone_container_type();
554        let size = e.query_container_size(&box_style.clone_display());
555        if container_type.intersects(ContainerType::SIZE) {
556            TraversalResult::Done(ContainerSizeQueryResult {
557                width: size.width,
558                height: size.height,
559            })
560        } else if container_type.intersects(ContainerType::INLINE_SIZE) {
561            if wm.is_horizontal() {
562                TraversalResult::Done(ContainerSizeQueryResult {
563                    width: size.width,
564                    height: None,
565                })
566            } else {
567                TraversalResult::Done(ContainerSizeQueryResult {
568                    width: None,
569                    height: size.height,
570                })
571            }
572        } else {
573            TraversalResult::InProgress
574        }
575    }
576
577    /// Find the query container size for a given element. Meant to be used as a callback for new().
578    fn lookup<E>(
579        element: E,
580        originating_element_style: Option<&ComputedValues>,
581    ) -> ContainerSizeQueryResult
582    where
583        E: TElement + 'a,
584    {
585        match traverse_container(
586            element,
587            originating_element_style,
588            |e, originating_element_style| {
589                Self::evaluate_potential_size_container(e, originating_element_style)
590            },
591        ) {
592            Some((container, result)) => {
593                if result.is_complete() {
594                    result
595                } else {
596                    // Traverse up from the found size container to see if we can get a complete containment.
597                    result.merge(Self::lookup(container, None))
598                }
599            },
600            None => ContainerSizeQueryResult::default(),
601        }
602    }
603
604    /// Create a new instance of the container size query for given element, with a deferred lookup callback.
605    pub fn for_element<E>(
606        element: E,
607        known_parent_style: Option<&'a ComputedValues>,
608        is_pseudo: bool,
609    ) -> Self
610    where
611        E: TElement + 'a,
612    {
613        let parent;
614        let data;
615        let parent_style = match known_parent_style {
616            Some(s) => Some(s),
617            None => {
618                // No need to bother if we're the top element.
619                parent = match element.traversal_parent() {
620                    Some(parent) => parent,
621                    None => return Self::none(),
622                };
623                data = parent.borrow_data();
624                data.as_ref().map(|data| &**data.styles.primary())
625            },
626        };
627
628        // If there's no style, such as being `display: none` or so, we still want to show a
629        // correct computed value, so give it a try.
630        let should_traverse = parent_style.map_or(true, |s| {
631            s.flags
632                .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE)
633        });
634        if !should_traverse {
635            return Self::none();
636        }
637        return Self::NotEvaluated(Box::new(move || {
638            Self::lookup(element, if is_pseudo { known_parent_style } else { None })
639        }));
640    }
641
642    /// Create a new instance, but with optional element.
643    pub fn for_option_element<E>(
644        element: Option<E>,
645        known_parent_style: Option<&'a ComputedValues>,
646        is_pseudo: bool,
647    ) -> Self
648    where
649        E: TElement + 'a,
650    {
651        if let Some(e) = element {
652            Self::for_element(e, known_parent_style, is_pseudo)
653        } else {
654            Self::none()
655        }
656    }
657
658    /// Create a query that evaluates to empty, for cases where container size query is not required.
659    pub fn none() -> Self {
660        ContainerSizeQuery::Evaluated(ContainerSizeQueryResult::default())
661    }
662
663    /// Get the result of the container size query, doing the lookup if called for the first time.
664    pub fn get(&mut self) -> ContainerSizeQueryResult {
665        match self {
666            Self::NotEvaluated(lookup) => {
667                *self = Self::Evaluated((lookup)());
668                match self {
669                    Self::Evaluated(info) => *info,
670                    _ => unreachable!("Just evaluated but not set?"),
671                }
672            },
673            Self::Evaluated(info) => *info,
674        }
675    }
676}