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