style/stylesheets/
container_rule.rs

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