Skip to main content

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 euclid::Rect;
7use euclid::default::Size2D as UntypedSize2D;
8use layout_api::{AxesOverflow, LayoutElement, LayoutNode};
9use malloc_size_of_derive::MallocSizeOf;
10use paint_api::display_list::AxesScrollSensitivity;
11use script::layout_dom::ServoLayoutNode;
12use style::values::computed::Overflow;
13use style_traits::CSSPixel;
14
15use crate::cell::ArcRefCell;
16use crate::context::LayoutContext;
17use crate::dom::{LayoutBox, NodeExt};
18use crate::dom_traversal::{Contents, NodeAndStyleInfo};
19use crate::flow::float::FloatBox;
20use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
21use crate::formatting_contexts::IndependentFormattingContext;
22use crate::fragment_tree::{FragmentFlags, FragmentTree};
23use crate::geom::{LogicalVec2, PhysicalSize};
24use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
25use crate::style_ext::Display;
26use crate::{DefiniteContainingBlock, PropagatedBoxTreeData};
27
28#[derive(MallocSizeOf)]
29pub struct BoxTree {
30    /// Contains typically exactly one block-level box, which was generated by the root element.
31    /// There may be zero if that element has `display: none`.
32    root: BlockFormattingContext,
33
34    /// Whether or not the viewport should be sensitive to scrolling input events in two axes
35    pub(crate) viewport_overflow: AxesOverflow,
36}
37
38impl BoxTree {
39    #[servo_tracing::instrument(name = "Box Tree Construction", skip_all)]
40    pub(crate) fn construct(context: &LayoutContext, root_element: ServoLayoutNode<'_>) -> Self {
41        let boxes = construct_for_root_element(context, root_element);
42
43        // Zero box for `:root { display: none }`, one for the root element otherwise.
44        assert!(boxes.len() <= 1);
45
46        let viewport_overflow = Self::viewport_overflow(root_element, boxes.first());
47        let contents = BlockContainer::BlockLevelBoxes(boxes);
48        let contains_floats = contents.contains_floats();
49        Self {
50            root: BlockFormattingContext {
51                contents,
52                contains_floats,
53            },
54            // From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
55            // > If visible is applied to the viewport, it must be interpreted as auto.
56            // > If clip is applied to the viewport, it must be interpreted as hidden.
57            viewport_overflow: viewport_overflow.to_scrollable(),
58        }
59    }
60
61    fn viewport_overflow(
62        root_element: ServoLayoutNode<'_>,
63        root_box: Option<&ArcRefCell<BlockLevelBox>>,
64    ) -> AxesOverflow {
65        // From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
66        // > UAs must apply the overflow-* values set on the root element to the viewport when the
67        // > root element’s display value is not none. However, when the root element is an [HTML]
68        // > html element (including XML syntax for HTML) whose overflow value is visible (in both
69        // > axes), and that element has as a child a body element whose display value is also not
70        // > none, user agents must instead apply the overflow-* values of the first such child
71        // > element to the viewport. The element from which the value is propagated must then have a
72        // > used overflow value of visible.
73
74        // If there is no root box, the root element has `display: none`, so don't propagate.
75        // The spec isn't very clear about what value to use, but the initial value seems fine.
76        // See https://github.com/w3c/csswg-drafts/issues/12649
77        let Some(root_box) = root_box else {
78            return AxesOverflow::default();
79        };
80
81        let propagate_from_body = || {
82            // Unlike what the spec implies, we stop iterating when we find the first <body>,
83            // even if it's not suitable because it lacks a box. This matches other browsers.
84            // See https://github.com/w3c/csswg-drafts/issues/12644
85            let body = root_element.flat_tree_children().find(|child| {
86                child
87                    .as_element()
88                    .is_some_and(|element| element.is_body_element_of_html_element_root())
89            })?;
90
91            // We only propagate from the <body> if it generates a box. The spec only checks for
92            // `display: none`, but other browsers don't propagate for `display: contents` either.
93            // See https://github.com/w3c/csswg-drafts/issues/12643
94            let body_layout_data = body.inner_layout_data()?;
95            let mut body_box = body_layout_data.self_box.borrow_mut();
96            body_box.as_mut()?.with_base_mut(|base| {
97                base.base_fragment_info
98                    .flags
99                    .insert(FragmentFlags::PROPAGATED_OVERFLOW_TO_VIEWPORT);
100                AxesOverflow::from(&*base.style)
101            })
102        };
103
104        root_box.borrow_mut().with_base_mut(|base| {
105            let root_overflow = AxesOverflow::from(&*base.style);
106            if root_overflow.x == Overflow::Visible &&
107                root_overflow.y == Overflow::Visible &&
108                let Some(body_overflow) = propagate_from_body()
109            {
110                return body_overflow;
111            }
112            base.base_fragment_info
113                .flags
114                .insert(FragmentFlags::PROPAGATED_OVERFLOW_TO_VIEWPORT);
115            root_overflow
116        })
117    }
118}
119
120fn construct_for_root_element(
121    context: &LayoutContext,
122    root_element: ServoLayoutNode<'_>,
123) -> Vec<ArcRefCell<BlockLevelBox>> {
124    let info = NodeAndStyleInfo::new(root_element, root_element.style(&context.style_context));
125    let box_style = info.style.get_box();
126
127    let display_inside = match Display::from(box_style.display) {
128        Display::None => return Vec::new(),
129        Display::Contents => {
130            // Unreachable because the style crate adjusts the computed values:
131            // https://drafts.csswg.org/css-display-3/#transformations
132            // “'display' of 'contents' computes to 'block' on the root element”
133            unreachable!()
134        },
135        // The root element is blockified, ignore DisplayOutside
136        Display::GeneratingBox(display_generating_box) => display_generating_box.display_inside(),
137    };
138
139    let contents = Contents::for_element(root_element, context);
140
141    let propagated_data = PropagatedBoxTreeData::default();
142    let root_box = if box_style.position.is_absolutely_positioned() {
143        BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new(
144            AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
145        ))
146    } else if box_style.float.is_floating() {
147        BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct(
148            context,
149            &info,
150            display_inside,
151            contents,
152            propagated_data,
153        ))
154    } else {
155        BlockLevelBox::Independent(IndependentFormattingContext::construct(
156            context,
157            &info,
158            display_inside,
159            contents,
160            propagated_data,
161        ))
162    };
163
164    let root_box = ArcRefCell::new(root_box);
165    root_element
166        .box_slot()
167        .set(LayoutBox::BlockLevel(root_box.clone()));
168    vec![root_box]
169}
170
171impl BoxTree {
172    #[servo_tracing::instrument(name = "Fragment Tree Construction", skip_all)]
173    pub(crate) fn layout(
174        &self,
175        layout_context: &LayoutContext,
176        viewport: UntypedSize2D<Au>,
177    ) -> FragmentTree {
178        let style = layout_context
179            .style_context
180            .stylist
181            .device()
182            .default_computed_values();
183
184        // FIXME: use the document’s mode:
185        // https://drafts.csswg.org/css-writing-modes/#principal-flow
186        let physical_containing_block: Rect<Au, CSSPixel> =
187            PhysicalSize::from_untyped(viewport).into();
188        let initial_containing_block = DefiniteContainingBlock {
189            size: LogicalVec2 {
190                inline: physical_containing_block.size.width,
191                block: physical_containing_block.size.height,
192            },
193            style,
194        };
195
196        let mut positioning_context = PositioningContext::default();
197        let independent_layout = self.root.layout(
198            layout_context,
199            &mut positioning_context,
200            &(&initial_containing_block).into(),
201        );
202
203        let mut root_fragments = independent_layout.fragments;
204
205        // Zero box for `:root { display: none }`, one for the root element otherwise.
206        assert!(root_fragments.len() <= 1);
207
208        // There may be more fragments at the top-level
209        // (for positioned boxes whose containing is the initial containing block)
210        // but only if there was one fragment for the root element.
211        positioning_context.layout_initial_containing_block_children(
212            layout_context,
213            &initial_containing_block,
214            &mut root_fragments,
215        );
216
217        let viewport_scroll_sensitivity = AxesScrollSensitivity {
218            x: self.viewport_overflow.x.into(),
219            y: self.viewport_overflow.y.into(),
220        };
221
222        FragmentTree::new(
223            root_fragments,
224            physical_containing_block,
225            viewport_scroll_sensitivity,
226        )
227    }
228}