layout/flow/
root.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;
6use atomic_refcell::AtomicRef;
7use euclid::Rect;
8use euclid::default::Size2D as UntypedSize2D;
9use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode};
10use layout_api::{AxesOverflow, LayoutElementType, LayoutNodeType};
11use malloc_size_of_derive::MallocSizeOf;
12use paint_api::display_list::AxesScrollSensitivity;
13use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode};
14use servo_arc::Arc;
15use style::dom::{NodeInfo, TNode};
16use style::properties::ComputedValues;
17use style::values::computed::Overflow;
18use style::values::specified::box_::DisplayOutside;
19use style_traits::CSSPixel;
20
21use crate::cell::ArcRefCell;
22use crate::context::LayoutContext;
23use crate::dom::{LayoutBox, NodeExt};
24use crate::dom_traversal::{Contents, NodeAndStyleInfo};
25use crate::flexbox::FlexLevelBox;
26use crate::flow::float::FloatBox;
27use crate::flow::inline::InlineItem;
28use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
29use crate::formatting_contexts::IndependentFormattingContext;
30use crate::fragment_tree::{FragmentFlags, FragmentTree};
31use crate::geom::{LogicalVec2, PhysicalSize};
32use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
33use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside};
34use crate::taffy::{TaffyItemBox, TaffyItemBoxInner};
35use crate::{DefiniteContainingBlock, PropagatedBoxTreeData};
36
37#[derive(MallocSizeOf)]
38pub struct BoxTree {
39    /// Contains typically exactly one block-level box, which was generated by the root element.
40    /// There may be zero if that element has `display: none`.
41    root: BlockFormattingContext,
42
43    /// Whether or not the viewport should be sensitive to scrolling input events in two axes
44    pub(crate) viewport_overflow: AxesOverflow,
45}
46
47impl BoxTree {
48    #[servo_tracing::instrument(name = "Box Tree Construction", skip_all)]
49    pub(crate) fn construct(context: &LayoutContext, root_element: ServoLayoutNode<'_>) -> Self {
50        let root_element = root_element.to_threadsafe();
51        let boxes = construct_for_root_element(context, root_element);
52
53        // Zero box for `:root { display: none }`, one for the root element otherwise.
54        assert!(boxes.len() <= 1);
55
56        let viewport_overflow = Self::viewport_overflow(root_element, boxes.first());
57        let contents = BlockContainer::BlockLevelBoxes(boxes);
58        let contains_floats = contents.contains_floats();
59        Self {
60            root: BlockFormattingContext {
61                contents,
62                contains_floats,
63            },
64            // From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
65            // > If visible is applied to the viewport, it must be interpreted as auto.
66            // > If clip is applied to the viewport, it must be interpreted as hidden.
67            viewport_overflow: viewport_overflow.to_scrollable(),
68        }
69    }
70
71    fn viewport_overflow(
72        root_element: ServoThreadSafeLayoutNode<'_>,
73        root_box: Option<&ArcRefCell<BlockLevelBox>>,
74    ) -> AxesOverflow {
75        // From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
76        // > UAs must apply the overflow-* values set on the root element to the viewport when the
77        // > root element’s display value is not none. However, when the root element is an [HTML]
78        // > html element (including XML syntax for HTML) whose overflow value is visible (in both
79        // > axes), and that element has as a child a body element whose display value is also not
80        // > none, user agents must instead apply the overflow-* values of the first such child
81        // > element to the viewport. The element from which the value is propagated must then have a
82        // > used overflow value of visible.
83
84        // If there is no root box, the root element has `display: none`, so don't propagate.
85        // The spec isn't very clear about what value to use, but the initial value seems fine.
86        // See https://github.com/w3c/csswg-drafts/issues/12649
87        let Some(root_box) = root_box else {
88            return AxesOverflow::default();
89        };
90
91        let propagate_from_body = || {
92            // Unlike what the spec implies, we stop iterating when we find the first <body>,
93            // even if it's not suitable because it lacks a box. This matches other browsers.
94            // See https://github.com/w3c/csswg-drafts/issues/12644
95            let body = root_element.children().find(|child| {
96                child
97                    .as_element()
98                    .is_some_and(|element| element.is_body_element_of_html_element_root())
99            })?;
100
101            // We only propagate from the <body> if it generates a box. The spec only checks for
102            // `display: none`, but other browsers don't propagate for `display: contents` either.
103            // See https://github.com/w3c/csswg-drafts/issues/12643
104            let body_layout_data = body.inner_layout_data()?;
105            let mut body_box = body_layout_data.self_box.borrow_mut();
106            body_box.as_mut()?.with_base_mut(|base| {
107                base.base_fragment_info
108                    .flags
109                    .insert(FragmentFlags::PROPAGATED_OVERFLOW_TO_VIEWPORT);
110                AxesOverflow::from(&*base.style)
111            })
112        };
113
114        root_box.borrow_mut().with_base_mut(|base| {
115            let root_overflow = AxesOverflow::from(&*base.style);
116            if root_overflow.x == Overflow::Visible && root_overflow.y == Overflow::Visible {
117                if let Some(body_overflow) = propagate_from_body() {
118                    return body_overflow;
119                }
120            }
121            base.base_fragment_info
122                .flags
123                .insert(FragmentFlags::PROPAGATED_OVERFLOW_TO_VIEWPORT);
124            root_overflow
125        })
126    }
127
128    /// This method attempts to incrementally update the box tree from an
129    /// arbitrary node that is not necessarily the document's root element.
130    ///
131    /// If the node is not a valid candidate for incremental update, the method
132    /// loops over its parent. The only valid candidates for now are absolutely
133    /// positioned boxes which don't change their outside display mode (i.e. it
134    /// will not attempt to update from an absolutely positioned inline element
135    /// which became an absolutely positioned block element). The value `true`
136    /// is returned if an incremental update could be done, and `false`
137    /// otherwise.
138    ///
139    /// There are various pain points that need to be taken care of to extend
140    /// the set of valid candidates:
141    /// * it is not obvious how to incrementally check whether a block
142    ///   formatting context still contains floats or not;
143    /// * the propagation of text decorations towards node descendants is
144    ///   hard to do incrementally with our current representation of boxes
145    /// * how intrinsic content sizes are computed eagerly makes it hard
146    ///   to update those sizes for ancestors of the node from which we
147    ///   made an incremental update.
148    pub(crate) fn update(
149        context: &LayoutContext,
150        dirty_root_from_script: ServoLayoutNode<'_>,
151    ) -> bool {
152        let Some(box_tree_update) = IncrementalBoxTreeUpdate::find(dirty_root_from_script) else {
153            return false;
154        };
155        box_tree_update.update_from_dirty_root(context);
156        true
157    }
158}
159
160fn construct_for_root_element(
161    context: &LayoutContext,
162    root_element: ServoThreadSafeLayoutNode<'_>,
163) -> Vec<ArcRefCell<BlockLevelBox>> {
164    let info = NodeAndStyleInfo::new(root_element, root_element.style(&context.style_context));
165    let box_style = info.style.get_box();
166
167    let display_inside = match Display::from(box_style.display) {
168        Display::None => return Vec::new(),
169        Display::Contents => {
170            // Unreachable because the style crate adjusts the computed values:
171            // https://drafts.csswg.org/css-display-3/#transformations
172            // “'display' of 'contents' computes to 'block' on the root element”
173            unreachable!()
174        },
175        // The root element is blockified, ignore DisplayOutside
176        Display::GeneratingBox(display_generating_box) => display_generating_box.display_inside(),
177    };
178
179    let contents = Contents::for_element(root_element, context);
180
181    let propagated_data = PropagatedBoxTreeData::default();
182    let root_box = if box_style.position.is_absolutely_positioned() {
183        BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new(
184            AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
185        ))
186    } else if box_style.float.is_floating() {
187        BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct(
188            context,
189            &info,
190            display_inside,
191            contents,
192            propagated_data,
193        ))
194    } else {
195        BlockLevelBox::Independent(IndependentFormattingContext::construct(
196            context,
197            &info,
198            display_inside,
199            contents,
200            propagated_data,
201        ))
202    };
203
204    let root_box = ArcRefCell::new(root_box);
205    root_element
206        .box_slot()
207        .set(LayoutBox::BlockLevel(root_box.clone()));
208    vec![root_box]
209}
210
211impl BoxTree {
212    #[servo_tracing::instrument(name = "Fragment Tree Construction", skip_all)]
213    pub(crate) fn layout(
214        &self,
215        layout_context: &LayoutContext,
216        viewport: UntypedSize2D<Au>,
217    ) -> FragmentTree {
218        let style = layout_context
219            .style_context
220            .stylist
221            .device()
222            .default_computed_values();
223
224        // FIXME: use the document’s mode:
225        // https://drafts.csswg.org/css-writing-modes/#principal-flow
226        let physical_containing_block: Rect<Au, CSSPixel> =
227            PhysicalSize::from_untyped(viewport).into();
228        let initial_containing_block = DefiniteContainingBlock {
229            size: LogicalVec2 {
230                inline: physical_containing_block.size.width,
231                block: physical_containing_block.size.height,
232            },
233            style,
234        };
235
236        let mut positioning_context = PositioningContext::default();
237        let independent_layout = self.root.layout(
238            layout_context,
239            &mut positioning_context,
240            &(&initial_containing_block).into(),
241        );
242
243        let mut root_fragments = independent_layout.fragments;
244
245        // Zero box for `:root { display: none }`, one for the root element otherwise.
246        assert!(root_fragments.len() <= 1);
247
248        // There may be more fragments at the top-level
249        // (for positioned boxes whose containing is the initial containing block)
250        // but only if there was one fragment for the root element.
251        positioning_context.layout_initial_containing_block_children(
252            layout_context,
253            &initial_containing_block,
254            &mut root_fragments,
255        );
256
257        let viewport_scroll_sensitivity = AxesScrollSensitivity {
258            x: self.viewport_overflow.x.into(),
259            y: self.viewport_overflow.y.into(),
260        };
261
262        FragmentTree::new(
263            layout_context,
264            root_fragments,
265            physical_containing_block,
266            viewport_scroll_sensitivity,
267        )
268    }
269}
270
271#[expect(clippy::enum_variant_names)]
272enum DirtyRootBoxTreeNode {
273    AbsolutelyPositionedBlockLevelBox(ArcRefCell<BlockLevelBox>),
274    AbsolutelyPositionedInlineLevelBox(InlineItem, usize),
275    AbsolutelyPositionedFlexLevelBox(ArcRefCell<FlexLevelBox>),
276    AbsolutelyPositionedTaffyLevelBox(ArcRefCell<TaffyItemBox>),
277}
278
279struct IncrementalBoxTreeUpdate<'dom> {
280    node: ServoLayoutNode<'dom>,
281    box_tree_node: DirtyRootBoxTreeNode,
282    primary_style: Arc<ComputedValues>,
283    display_inside: DisplayInside,
284}
285
286impl<'dom> IncrementalBoxTreeUpdate<'dom> {
287    fn find(dirty_root_from_script: ServoLayoutNode<'dom>) -> Option<Self> {
288        let mut maybe_dirty_root_node = Some(dirty_root_from_script);
289        while let Some(dirty_root_node) = maybe_dirty_root_node {
290            if let Some(dirty_root) = Self::new_if_valid(dirty_root_node) {
291                return Some(dirty_root);
292            }
293
294            maybe_dirty_root_node = dirty_root_node.parent_node();
295        }
296
297        None
298    }
299
300    fn new_if_valid(potential_dirty_root_node: ServoLayoutNode<'dom>) -> Option<Self> {
301        if !potential_dirty_root_node.is_element() {
302            return None;
303        }
304
305        if potential_dirty_root_node.type_id() ==
306            LayoutNodeType::Element(LayoutElementType::HTMLBodyElement)
307        {
308            // This can require changes to the canvas background.
309            return None;
310        }
311
312        // Don't update unstyled nodes or nodes that have pseudo-elements.
313        let potential_thread_safe_dirty_root_node = potential_dirty_root_node.to_threadsafe();
314        let element_data = potential_thread_safe_dirty_root_node
315            .style_data()?
316            .element_data
317            .borrow();
318        if !element_data.styles.pseudos.is_empty() {
319            return None;
320        }
321
322        let layout_data = NodeExt::inner_layout_data(&potential_thread_safe_dirty_root_node)?;
323        if !layout_data.pseudo_boxes.is_empty() {
324            return None;
325        }
326
327        let primary_style = element_data.styles.primary();
328        let box_style = primary_style.get_box();
329
330        if !box_style.position.is_absolutely_positioned() {
331            return None;
332        }
333
334        let display_inside = match Display::from(box_style.display) {
335            Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { inside, .. }) => inside,
336            _ => return None,
337        };
338
339        let box_tree_node =
340            match &*AtomicRef::filter_map(layout_data.self_box.borrow(), Option::as_ref)? {
341                LayoutBox::DisplayContents(..) => return None,
342                LayoutBox::BlockLevel(block_level_box) => match &*block_level_box.borrow() {
343                    BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
344                        if box_style.position.is_absolutely_positioned() =>
345                    {
346                        // If the outer type of its original display changed from block to inline,
347                        // a block-level abspos needs to be placed in an inline formatting context,
348                        // see [`BlockContainerBuilder::handle_absolutely_positioned_element()`].
349                        if box_style.original_display.outside() == DisplayOutside::Inline {
350                            return None;
351                        }
352                        DirtyRootBoxTreeNode::AbsolutelyPositionedBlockLevelBox(
353                            block_level_box.clone(),
354                        )
355                    },
356                    _ => return None,
357                },
358                LayoutBox::InlineLevel(inline_level_box) => {
359                    let InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index) =
360                        inline_level_box
361                    else {
362                        return None;
363                    };
364                    DirtyRootBoxTreeNode::AbsolutelyPositionedInlineLevelBox(
365                        inline_level_box.clone(),
366                        *text_offset_index,
367                    )
368                },
369                LayoutBox::FlexLevel(flex_level_box) => match &*flex_level_box.borrow() {
370                    FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
371                        if box_style.position.is_absolutely_positioned() =>
372                    {
373                        DirtyRootBoxTreeNode::AbsolutelyPositionedFlexLevelBox(
374                            flex_level_box.clone(),
375                        )
376                    },
377                    _ => return None,
378                },
379                LayoutBox::TableLevelBox(..) => return None,
380                LayoutBox::TaffyItemBox(taffy_level_box) => {
381                    match &taffy_level_box.borrow().taffy_level_box {
382                        TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(_)
383                            if box_style.position.is_absolutely_positioned() =>
384                        {
385                            DirtyRootBoxTreeNode::AbsolutelyPositionedTaffyLevelBox(
386                                taffy_level_box.clone(),
387                            )
388                        },
389                        _ => return None,
390                    }
391                },
392            };
393
394        Some(Self {
395            node: potential_dirty_root_node,
396            box_tree_node,
397            primary_style: primary_style.clone(),
398            display_inside,
399        })
400    }
401
402    #[servo_tracing::instrument(name = "Box Tree Update From Dirty Root", skip_all)]
403    fn update_from_dirty_root(&self, context: &LayoutContext) {
404        let node = self.node.to_threadsafe();
405        let contents = Contents::for_element(node, context);
406
407        let info = NodeAndStyleInfo::new(node, self.primary_style.clone());
408
409        let build_new_box = |old_parent| {
410            let mut out_of_flow_absolutely_positioned_box =
411                AbsolutelyPositionedBox::construct(context, &info, self.display_inside, contents);
412            if let Some(old_parent) = old_parent {
413                out_of_flow_absolutely_positioned_box
414                    .context
415                    .base
416                    .parent_box
417                    .replace(old_parent);
418            }
419            out_of_flow_absolutely_positioned_box
420        };
421
422        match &self.box_tree_node {
423            DirtyRootBoxTreeNode::AbsolutelyPositionedBlockLevelBox(block_level_box) => {
424                let mut block_level_box = block_level_box.borrow_mut();
425                let old_parent = block_level_box.with_base(|base| base.parent_box.clone());
426                *block_level_box = BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(
427                    ArcRefCell::new(build_new_box(old_parent)),
428                );
429            },
430            DirtyRootBoxTreeNode::AbsolutelyPositionedInlineLevelBox(
431                inline_level_box,
432                text_offset_index,
433            ) => match inline_level_box {
434                InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, offset_in_text) => {
435                    let mut positioned_box = positioned_box.borrow_mut();
436                    let old_parent = positioned_box.context.base.parent_box.clone();
437                    *positioned_box = build_new_box(old_parent);
438                    assert_eq!(
439                        *offset_in_text, *text_offset_index,
440                        "The offset of the dirty root shouldn't have changed"
441                    );
442                },
443                _ => unreachable!("The dirty root should be absolutely positioned"),
444            },
445            DirtyRootBoxTreeNode::AbsolutelyPositionedFlexLevelBox(flex_level_box) => {
446                let mut flex_level_box = flex_level_box.borrow_mut();
447                let old_parent = flex_level_box.with_base(|base| base.parent_box.clone());
448                *flex_level_box = FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new(
449                    build_new_box(old_parent),
450                ));
451            },
452            DirtyRootBoxTreeNode::AbsolutelyPositionedTaffyLevelBox(taffy_level_box) => {
453                let mut taffy_level_box = taffy_level_box.borrow_mut();
454                let old_parent = taffy_level_box.with_base(|base| base.parent_box.clone());
455                taffy_level_box.taffy_level_box =
456                    TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new(
457                        build_new_box(old_parent),
458                    ));
459            },
460        }
461    }
462}