layout/fragment_tree/
box_fragment.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
5use app_units::{Au, MAX_AU, MIN_AU};
6use atomic_refcell::AtomicRefCell;
7use base::id::ScrollTreeNodeId;
8use base::print_tree::PrintTree;
9use euclid::Rect;
10use malloc_size_of_derive::MallocSizeOf;
11use servo_arc::Arc as ServoArc;
12use servo_geometry::f32_rect_to_au_rect;
13use style::Zero;
14use style::computed_values::border_collapse::T as BorderCollapse;
15use style::computed_values::overflow_x::T as ComputedOverflow;
16use style::computed_values::position::T as ComputedPosition;
17use style::logical_geometry::WritingMode;
18use style::properties::ComputedValues;
19use style::values::specified::box_::DisplayOutside;
20
21use super::{BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment, FragmentFlags};
22use crate::SharedStyle;
23use crate::display_list::ToWebRender;
24use crate::formatting_contexts::Baselines;
25use crate::fragment_tree::BaseFragmentStyleRef;
26use crate::geom::{
27    AuOrAuto, LengthPercentageOrAuto, PhysicalPoint, PhysicalRect, PhysicalSides, ToLogical,
28};
29use crate::style_ext::ComputedValuesExt;
30use crate::table::SpecificTableGridInfo;
31use crate::taffy::SpecificTaffyGridInfo;
32
33/// Describes how a [`BoxFragment`] paints its background.
34#[derive(MallocSizeOf)]
35pub(crate) enum BackgroundMode {
36    /// Draw the normal [`BoxFragment`] background as well as the extra backgrounds
37    /// based on the style and positioning rectangles in this data structure.
38    Extra(Vec<ExtraBackground>),
39    /// Do not draw a background for this Fragment. This is used for elements like
40    /// table tracks and table track groups, which rely on cells to paint their
41    /// backgrounds.
42    None,
43    /// Draw the background normally, getting information from the Fragment style.
44    Normal,
45}
46#[derive(MallocSizeOf)]
47pub(crate) struct ExtraBackground {
48    pub style: SharedStyle,
49    pub rect: PhysicalRect<Au>,
50}
51
52#[derive(Clone, Debug, MallocSizeOf)]
53pub(crate) enum SpecificLayoutInfo {
54    Grid(Box<SpecificTaffyGridInfo>),
55    TableCellWithCollapsedBorders,
56    TableGridWithCollapsedBorders(Box<SpecificTableGridInfo>),
57    TableWrapper,
58}
59
60#[derive(MallocSizeOf)]
61pub(crate) struct BlockLevelLayoutInfo {
62    /// When the `clear` property is not set to `none`, it may introduce clearance.
63    /// Clearance is some extra spacing that is added above the top margin,
64    /// so that the element doesn't overlap earlier floats in the same BFC.
65    /// The presence of clearance prevents the top margin from collapsing with
66    /// earlier margins or with the bottom margin of the parent block.
67    /// <https://drafts.csswg.org/css2/#clearance>
68    pub clearance: Option<Au>,
69
70    pub block_margins_collapsed_with_children: CollapsedBlockMargins,
71}
72
73#[derive(MallocSizeOf)]
74pub(crate) struct BoxFragmentRareData {
75    /// Information that is specific to a layout system (e.g., grid, table, etc.).
76    pub specific_layout_info: Option<SpecificLayoutInfo>,
77}
78
79impl BoxFragmentRareData {
80    /// Create a new rare data based on information given to the fragment. Ideally, We should
81    /// avoid creating rare data as much as possible to reduce the memory cost.
82    fn try_boxed_from(specific_layout_info: Option<SpecificLayoutInfo>) -> Option<Box<Self>> {
83        specific_layout_info.map(|info| {
84            Box::new(BoxFragmentRareData {
85                specific_layout_info: Some(info),
86            })
87        })
88    }
89}
90
91#[derive(MallocSizeOf)]
92pub(crate) struct BoxFragment {
93    pub base: BaseFragment,
94
95    pub children: Vec<Fragment>,
96
97    /// This [`BoxFragment`]'s containing block rectangle in coordinates relative to
98    /// the initial containing block, but not taking into account any transforms.
99    pub cumulative_containing_block_rect: PhysicalRect<Au>,
100
101    pub padding: PhysicalSides<Au>,
102    pub border: PhysicalSides<Au>,
103    pub margin: PhysicalSides<Au>,
104
105    /// When this [`BoxFragment`] is for content that has a baseline, this tracks
106    /// the first and last baselines of that content. This is used to propagate baselines
107    /// to things such as tables and inline formatting contexts.
108    baselines: Baselines,
109
110    /// The scrollable overflow of this box fragment in the same coordiante system as
111    /// [`Self::content_rect`] ie a rectangle within the parent fragment's content
112    /// rectangle. This does not take into account any transforms this fragment applies.
113    /// This is handled when calling [`Self::scrollable_overflow_for_parent`].
114    scrollable_overflow: Option<PhysicalRect<Au>>,
115
116    /// The resolved box insets if this box is `position: sticky`. These are calculated
117    /// during `StackingContextTree` construction because they rely on the size of the
118    /// scroll container.
119    pub(crate) resolved_sticky_insets: AtomicRefCell<Option<PhysicalSides<AuOrAuto>>>,
120
121    pub background_mode: BackgroundMode,
122
123    /// Rare data that not all kinds of [`BoxFragment`] would have.
124    pub rare_data: Option<Box<BoxFragmentRareData>>,
125
126    /// Additional information for block-level boxes.
127    pub block_level_layout_info: Option<Box<BlockLevelLayoutInfo>>,
128
129    /// The containing spatial tree node of this [`BoxFragment`]. This is assigned during
130    /// `StackingContextTree` construction, so isn't available before that time. This is
131    /// used to for determining final viewport size and position of this node and will
132    /// also be used in the future for hit testing.
133    pub spatial_tree_node: AtomicRefCell<Option<ScrollTreeNodeId>>,
134}
135
136impl BoxFragment {
137    #[allow(clippy::too_many_arguments)]
138    pub fn new(
139        base_fragment_info: BaseFragmentInfo,
140        style: ServoArc<ComputedValues>,
141        children: Vec<Fragment>,
142        content_rect: PhysicalRect<Au>,
143        padding: PhysicalSides<Au>,
144        border: PhysicalSides<Au>,
145        margin: PhysicalSides<Au>,
146        specific_layout_info: Option<SpecificLayoutInfo>,
147    ) -> BoxFragment {
148        let rare_data = BoxFragmentRareData::try_boxed_from(specific_layout_info);
149
150        BoxFragment {
151            base: BaseFragment::new(base_fragment_info, style.into(), content_rect),
152            children,
153            cumulative_containing_block_rect: Default::default(),
154            padding,
155            border,
156            margin,
157            baselines: Baselines::default(),
158            scrollable_overflow: None,
159            resolved_sticky_insets: AtomicRefCell::default(),
160            background_mode: BackgroundMode::Normal,
161            rare_data,
162            block_level_layout_info: None,
163            spatial_tree_node: AtomicRefCell::default(),
164        }
165    }
166
167    pub fn with_baselines(mut self, baselines: Baselines) -> Self {
168        self.baselines = baselines;
169        self
170    }
171
172    pub(crate) fn style<'a>(&'a self) -> BaseFragmentStyleRef<'a> {
173        self.base.style()
174    }
175
176    /// Get the baselines for this [`BoxFragment`] if they are compatible with the given [`WritingMode`].
177    /// If they are not compatible, [`Baselines::default()`] is returned.
178    pub fn baselines(&self, writing_mode: WritingMode) -> Baselines {
179        let style = self.style();
180        let mut baselines = if writing_mode.is_horizontal() == style.writing_mode.is_horizontal() {
181            self.baselines
182        } else {
183            // If the writing mode of the container requesting baselines is not
184            // compatible, ensure that the baselines established by this fragment are
185            // not used.
186            Baselines::default()
187        };
188
189        // From the https://drafts.csswg.org/css-align-3/#baseline-export section on "block containers":
190        // > However, for legacy reasons if its baseline-source is auto (the initial
191        // > value) a block-level or inline-level block container that is a scroll container
192        // > always has a last baseline set, whose baselines all correspond to its block-end
193        // > margin edge.
194        //
195        // This applies even if there is no baseline set, so we unconditionally set the value here
196        // and ignore anything that is set via [`Self::with_baselines`].
197        if style.establishes_scroll_container(self.base.flags) {
198            let content_rect_size = self.content_rect().size.to_logical(writing_mode);
199            let padding = self.padding.to_logical(writing_mode);
200            let border = self.border.to_logical(writing_mode);
201            let margin = self.margin.to_logical(writing_mode);
202            baselines.last = Some(
203                content_rect_size.block + padding.block_end + border.block_end + margin.block_end,
204            )
205        }
206        baselines
207    }
208
209    pub fn add_extra_background(&mut self, extra_background: ExtraBackground) {
210        match self.background_mode {
211            BackgroundMode::Extra(ref mut backgrounds) => backgrounds.push(extra_background),
212            _ => self.background_mode = BackgroundMode::Extra(vec![extra_background]),
213        }
214    }
215
216    pub fn set_does_not_paint_background(&mut self) {
217        self.background_mode = BackgroundMode::None;
218    }
219
220    pub fn specific_layout_info(&self) -> Option<&SpecificLayoutInfo> {
221        self.rare_data.as_ref()?.specific_layout_info.as_ref()
222    }
223
224    pub fn with_block_level_layout_info(
225        mut self,
226        block_margins_collapsed_with_children: CollapsedBlockMargins,
227        clearance: Option<Au>,
228    ) -> Self {
229        self.block_level_layout_info = Some(Box::new(BlockLevelLayoutInfo {
230            block_margins_collapsed_with_children,
231            clearance,
232        }));
233        self
234    }
235
236    /// Get the scrollable overflow for this [`BoxFragment`] relative to its
237    /// containing block.
238    pub fn scrollable_overflow(&self) -> PhysicalRect<Au> {
239        self.scrollable_overflow
240            .expect("Should only call `scrollable_overflow()` after calculating overflow")
241    }
242
243    /// This is an implementation of:
244    /// - <https://drafts.csswg.org/css-overflow-3/#scrollable>.
245    /// - <https://drafts.csswg.org/cssom-view/#scrolling-area>
246    pub(crate) fn calculate_scrollable_overflow(&mut self) {
247        let physical_padding_rect = self.padding_rect();
248        let content_origin = self.base.rect.origin.to_vector();
249
250        // > The scrollable overflow area is the union of:
251        // > * The scroll container’s own padding box.
252        // > * All line boxes directly contained by the scroll container.
253        // > * The border boxes of all boxes for which it is the containing block and
254        // >   whose border boxes are positioned not wholly in the unreachable
255        // >   scrollable overflow region, accounting for transforms by projecting
256        // >   each box onto the plane of the element that establishes its 3D
257        // >   rendering context.
258        // > * The margin areas of grid item and flex item boxes for which the box
259        // >   establishes a containing block.
260        // > * The scrollable overflow areas of all of the above boxes (including zero-area
261        // >   boxes and accounting for transforms as described above), provided they
262        // >   themselves have overflow: visible (i.e. do not themselves trap the overflow)
263        // >   and that scrollable overflow is not already clipped (e.g. by the clip property
264        // >   or the contain property).
265        // > * Additional padding added to the scrollable overflow rectangle as necessary
266        //     to enable scroll positions that satisfy the requirements of both place-content:
267        //     start and place-content: end alignment.
268        //
269        // TODO(mrobinson): Below we are handling the border box and the scrollable
270        // overflow together, but from the specification it seems that if the border
271        // box of an item is in the "wholly unreachable scrollable overflow region", but
272        // its scrollable overflow is not, it should also be excluded.
273        let scrollable_overflow = self
274            .children
275            .iter()
276            .fold(physical_padding_rect, |acc, child| {
277                let scrollable_overflow_from_child = child
278                    .calculate_scrollable_overflow_for_parent()
279                    .translate(content_origin);
280
281                // Note that this doesn't just exclude scrollable overflow outside the
282                // wholly unrechable scrollable overflow area, but also clips it. This
283                // makes the resulting value more like the "scroll area" rather than the
284                // "scrollable overflow."
285                let scrollable_overflow_from_child = self
286                    .clip_wholly_unreachable_scrollable_overflow(
287                        scrollable_overflow_from_child,
288                        physical_padding_rect,
289                    );
290                acc.union(&scrollable_overflow_from_child)
291            });
292
293        // Fragments with `IS_COLLAPSED` (currently only table cells that are part of
294        // table tracks with `visibility: collapse`) should not contribute to scrollable
295        // overflow. This behavior matches Chrome, but not Firefox.
296        // See https://github.com/w3c/csswg-drafts/issues/12689
297        if self.base.flags.contains(FragmentFlags::IS_COLLAPSED) {
298            self.scrollable_overflow = Some(Rect::zero());
299            return;
300        }
301
302        self.scrollable_overflow = Some(scrollable_overflow)
303    }
304
305    pub(crate) fn set_containing_block(&mut self, containing_block: &PhysicalRect<Au>) {
306        self.cumulative_containing_block_rect = *containing_block;
307    }
308
309    pub fn offset_by_containing_block(&self, rect: &PhysicalRect<Au>) -> PhysicalRect<Au> {
310        rect.translate(self.cumulative_containing_block_rect.origin.to_vector())
311    }
312
313    pub(crate) fn cumulative_content_box_rect(&self) -> PhysicalRect<Au> {
314        self.offset_by_containing_block(&self.base.rect)
315    }
316
317    pub(crate) fn cumulative_padding_box_rect(&self) -> PhysicalRect<Au> {
318        self.offset_by_containing_block(&self.padding_rect())
319    }
320
321    pub(crate) fn cumulative_border_box_rect(&self) -> PhysicalRect<Au> {
322        self.offset_by_containing_block(&self.border_rect())
323    }
324
325    pub(crate) fn content_rect(&self) -> PhysicalRect<Au> {
326        self.base.rect
327    }
328
329    pub(crate) fn padding_rect(&self) -> PhysicalRect<Au> {
330        self.content_rect().outer_rect(self.padding)
331    }
332
333    pub(crate) fn border_rect(&self) -> PhysicalRect<Au> {
334        self.padding_rect().outer_rect(self.border)
335    }
336
337    pub(crate) fn margin_rect(&self) -> PhysicalRect<Au> {
338        self.border_rect().outer_rect(self.margin)
339    }
340
341    pub(crate) fn padding_border_margin(&self) -> PhysicalSides<Au> {
342        self.margin + self.border + self.padding
343    }
344
345    pub(crate) fn is_root_element(&self) -> bool {
346        self.base.flags.intersects(FragmentFlags::IS_ROOT_ELEMENT)
347    }
348
349    pub(crate) fn is_body_element_of_html_element_root(&self) -> bool {
350        self.base
351            .flags
352            .intersects(FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT)
353    }
354
355    pub fn print(&self, tree: &mut PrintTree) {
356        tree.new_level(format!(
357            "Box\
358                \nbase={:?}\
359                \ncontent={:?}\
360                \npadding rect={:?}\
361                \nborder rect={:?}\
362                \nmargin={:?}\
363                \nscrollable_overflow={:?}\
364                \nbaselines={:?}\
365                \noverflow={:?}",
366            self.base,
367            self.content_rect(),
368            self.padding_rect(),
369            self.border_rect(),
370            self.margin,
371            self.scrollable_overflow(),
372            self.baselines,
373            self.style().effective_overflow(self.base.flags),
374        ));
375
376        for child in &self.children {
377            child.print(tree);
378        }
379        tree.end_level();
380    }
381
382    pub(crate) fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> {
383        let style = self.style();
384        let mut overflow = self.border_rect();
385        if !style.establishes_scroll_container(self.base.flags) {
386            // https://www.w3.org/TR/css-overflow-3/#scrollable
387            // Only include the scrollable overflow of a child box if it has overflow: visible.
388            let scrollable_overflow = self.scrollable_overflow();
389            let bottom_right = PhysicalPoint::new(
390                overflow.max_x().max(scrollable_overflow.max_x()),
391                overflow.max_y().max(scrollable_overflow.max_y()),
392            );
393
394            let overflow_style = style.effective_overflow(self.base.flags);
395            if overflow_style.y == ComputedOverflow::Visible {
396                overflow.origin.y = overflow.origin.y.min(scrollable_overflow.origin.y);
397                overflow.size.height = bottom_right.y - overflow.origin.y;
398            }
399
400            if overflow_style.x == ComputedOverflow::Visible {
401                overflow.origin.x = overflow.origin.x.min(scrollable_overflow.origin.x);
402                overflow.size.width = bottom_right.x - overflow.origin.x;
403            }
404        }
405
406        if !style.has_effective_transform_or_perspective(self.base.flags) {
407            return overflow;
408        }
409
410        // <https://drafts.csswg.org/css-overflow-3/#scrollable-overflow-region>
411        // > ...accounting for transforms by projecting each box onto the plane of
412        // > the element that establishes its 3D rendering context. [CSS3-TRANSFORMS]
413        // Both boxes and its scrollable overflow (if it is included) should be transformed accordingly.
414        //
415        // TODO(stevennovaryo): We are supposed to handle perspective transform and 3d
416        // contexts, but it is yet to happen.
417        self.calculate_transform_matrix(&self.border_rect())
418            .and_then(|transform| {
419                transform.outer_transformed_rect(&overflow.to_webrender().to_rect())
420            })
421            .map(|transformed_rect| f32_rect_to_au_rect(transformed_rect).cast_unit())
422            .unwrap_or(overflow)
423    }
424
425    /// Return the clipped the scrollable overflow based on its scroll origin, determined
426    /// by overflow direction. For an element, the clip rect is the padding rect and for
427    /// viewport, it is the initial containing block.
428    pub(crate) fn clip_wholly_unreachable_scrollable_overflow(
429        &self,
430        scrollable_overflow: PhysicalRect<Au>,
431        clipping_rect: PhysicalRect<Au>,
432    ) -> PhysicalRect<Au> {
433        // From <https://drafts.csswg.org/css-overflow/#unreachable-scrollable-overflow-region>:
434        // > Unless otherwise adjusted (e.g. by content alignment [css-align-3]), the area
435        // > beyond the scroll origin in either axis is considered the unreachable scrollable
436        // > overflow region: content rendered here is not accessible to the reader, see § 2.2
437        // > Scrollable Overflow. A scroll container is said to be scrolled to its scroll
438        // > origin when its scroll origin coincides with the corresponding corner of its
439        // > scrollport. This scroll position, the scroll origin position, usually, but not
440        // > always, coincides with the initial scroll position.
441        let scrolling_direction = self.style().overflow_direction();
442        let mut clipping_box = clipping_rect.to_box2d();
443        if scrolling_direction.rightward {
444            clipping_box.max.x = MAX_AU;
445        } else {
446            clipping_box.min.x = MIN_AU;
447        }
448
449        if scrolling_direction.downward {
450            clipping_box.max.y = MAX_AU;
451        } else {
452            clipping_box.min.y = MIN_AU;
453        }
454
455        let scrollable_overflow_box = scrollable_overflow
456            .to_box2d()
457            .intersection_unchecked(&clipping_box);
458
459        match scrollable_overflow_box.is_negative() {
460            true => PhysicalRect::zero(),
461            false => scrollable_overflow_box.to_rect(),
462        }
463    }
464
465    pub(crate) fn calculate_resolved_insets_if_positioned(&self) -> PhysicalSides<AuOrAuto> {
466        let style = self.style();
467        let position = style.get_box().position;
468        debug_assert_ne!(
469            position,
470            ComputedPosition::Static,
471            "Should not call this method on statically positioned box."
472        );
473
474        if let Some(resolved_sticky_insets) = *self.resolved_sticky_insets.borrow() {
475            return resolved_sticky_insets;
476        }
477
478        let convert_to_au_or_auto = |sides: PhysicalSides<Au>| {
479            PhysicalSides::new(
480                AuOrAuto::LengthPercentage(sides.top),
481                AuOrAuto::LengthPercentage(sides.right),
482                AuOrAuto::LengthPercentage(sides.bottom),
483                AuOrAuto::LengthPercentage(sides.left),
484            )
485        };
486
487        // "A resolved value special case property like top defined in another
488        // specification If the property applies to a positioned element and the
489        // resolved value of the display property is not none or contents, and
490        // the property is not over-constrained, then the resolved value is the
491        // used value. Otherwise the resolved value is the computed value."
492        // https://drafts.csswg.org/cssom/#resolved-values
493        let insets = style.physical_box_offsets();
494        let (cb_width, cb_height) = (
495            self.cumulative_containing_block_rect.width(),
496            self.cumulative_containing_block_rect.height(),
497        );
498        if position == ComputedPosition::Relative {
499            let get_resolved_axis = |start: &LengthPercentageOrAuto,
500                                     end: &LengthPercentageOrAuto,
501                                     container_length: Au| {
502                let start = start.map(|value| value.to_used_value(container_length));
503                let end = end.map(|value| value.to_used_value(container_length));
504                match (start.non_auto(), end.non_auto()) {
505                    (None, None) => (Au::zero(), Au::zero()),
506                    (None, Some(end)) => (-end, end),
507                    (Some(start), None) => (start, -start),
508                    // This is the overconstrained case, for which the resolved insets will
509                    // simply be the computed insets.
510                    (Some(start), Some(end)) => (start, end),
511                }
512            };
513            let (left, right) = get_resolved_axis(&insets.left, &insets.right, cb_width);
514            let (top, bottom) = get_resolved_axis(&insets.top, &insets.bottom, cb_height);
515            return convert_to_au_or_auto(PhysicalSides::new(top, right, bottom, left));
516        }
517
518        debug_assert!(position.is_absolutely_positioned());
519
520        let margin_rect = self.margin_rect();
521        let (top, bottom) = match (&insets.top, &insets.bottom) {
522            (
523                LengthPercentageOrAuto::LengthPercentage(top),
524                LengthPercentageOrAuto::LengthPercentage(bottom),
525            ) => (
526                top.to_used_value(cb_height),
527                bottom.to_used_value(cb_height),
528            ),
529            _ => (margin_rect.origin.y, cb_height - margin_rect.max_y()),
530        };
531        let (left, right) = match (&insets.left, &insets.right) {
532            (
533                LengthPercentageOrAuto::LengthPercentage(left),
534                LengthPercentageOrAuto::LengthPercentage(right),
535            ) => (left.to_used_value(cb_width), right.to_used_value(cb_width)),
536            _ => (margin_rect.origin.x, cb_width - margin_rect.max_x()),
537        };
538
539        convert_to_au_or_auto(PhysicalSides::new(top, right, bottom, left))
540    }
541
542    /// Whether this is a non-replaced inline-level box whose inner display type is `flow`.
543    /// <https://drafts.csswg.org/css-display-3/#inline-box>
544    pub(crate) fn is_inline_box(&self) -> bool {
545        self.style().is_inline_box(self.base.flags)
546    }
547
548    /// Whether this is an atomic inline-level box.
549    /// <https://drafts.csswg.org/css-display-3/#atomic-inline>
550    pub(crate) fn is_atomic_inline_level(&self) -> bool {
551        self.style().get_box().display.outside() == DisplayOutside::Inline && !self.is_inline_box()
552    }
553
554    /// Whether this is a table wrapper box.
555    /// <https://www.w3.org/TR/css-tables-3/#table-wrapper-box>
556    pub(crate) fn is_table_wrapper(&self) -> bool {
557        matches!(
558            self.specific_layout_info(),
559            Some(SpecificLayoutInfo::TableWrapper)
560        )
561    }
562
563    pub(crate) fn has_collapsed_borders(&self) -> bool {
564        match self.specific_layout_info() {
565            Some(SpecificLayoutInfo::TableCellWithCollapsedBorders) => true,
566            Some(SpecificLayoutInfo::TableGridWithCollapsedBorders(_)) => true,
567            Some(SpecificLayoutInfo::TableWrapper) => {
568                self.style().get_inherited_table().border_collapse == BorderCollapse::Collapse
569            },
570            _ => false,
571        }
572    }
573
574    pub(crate) fn spatial_tree_node(&self) -> Option<ScrollTreeNodeId> {
575        *self.spatial_tree_node.borrow()
576    }
577}