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