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