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