mirror of
https://github.com/servo/servo
synced 2026-05-11 01:22:19 +02:00
Replace the old dirty root box tree layout approach with one that works
based on independent formatting contexts. Instead of just allowing a
single dirty root to be the source of box tree reconstruction, allow box
tree reconstruction to happen anywhere in the tree that can isolate box
tree damage from ancestors. This essentially combines damage propagation
and box tree construction into a single step.
There is currently one downside to this approach compared to the dirty
root approach which is that we currently cannot detect and start box
tree layout when the dirty has a compatible `display` and `position`
value. This can mean the scope of box tree layout extends further up the
tree. We will address this in a followup -- but have not noticed any
major performance implications (currently fragment tree layout is much
more expensive than box tree layout).
Benefits:
1. Damage propagation now only happens under the dirty root.
2. Future changes can limit the scope of damage up the tree and perhaps
preserve the inline content size cache between box tree rebuilds.
Testing: This should not change behavior in a testable way (we currently
do
not have robust performance tests), so WPT test results should not
change.
---------
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
Co-authored-by: Luke Warlow <lwarlow@igalia.com>
231 lines
9.4 KiB
Rust
231 lines
9.4 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||
|
||
use app_units::Au;
|
||
use euclid::Rect;
|
||
use euclid::default::Size2D as UntypedSize2D;
|
||
use layout_api::AxesOverflow;
|
||
use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode};
|
||
use malloc_size_of_derive::MallocSizeOf;
|
||
use paint_api::display_list::AxesScrollSensitivity;
|
||
use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode};
|
||
use style::values::computed::Overflow;
|
||
use style_traits::CSSPixel;
|
||
|
||
use crate::cell::ArcRefCell;
|
||
use crate::context::LayoutContext;
|
||
use crate::dom::{LayoutBox, NodeExt};
|
||
use crate::dom_traversal::{Contents, NodeAndStyleInfo};
|
||
use crate::flow::float::FloatBox;
|
||
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
|
||
use crate::formatting_contexts::IndependentFormattingContext;
|
||
use crate::fragment_tree::{FragmentFlags, FragmentTree};
|
||
use crate::geom::{LogicalVec2, PhysicalSize};
|
||
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
|
||
use crate::style_ext::Display;
|
||
use crate::{DefiniteContainingBlock, PropagatedBoxTreeData};
|
||
|
||
#[derive(MallocSizeOf)]
|
||
pub struct BoxTree {
|
||
/// Contains typically exactly one block-level box, which was generated by the root element.
|
||
/// There may be zero if that element has `display: none`.
|
||
root: BlockFormattingContext,
|
||
|
||
/// Whether or not the viewport should be sensitive to scrolling input events in two axes
|
||
pub(crate) viewport_overflow: AxesOverflow,
|
||
}
|
||
|
||
impl BoxTree {
|
||
#[servo_tracing::instrument(name = "Box Tree Construction", skip_all)]
|
||
pub(crate) fn construct(context: &LayoutContext, root_element: ServoLayoutNode<'_>) -> Self {
|
||
let root_element = root_element.to_threadsafe();
|
||
let boxes = construct_for_root_element(context, root_element);
|
||
|
||
// Zero box for `:root { display: none }`, one for the root element otherwise.
|
||
assert!(boxes.len() <= 1);
|
||
|
||
let viewport_overflow = Self::viewport_overflow(root_element, boxes.first());
|
||
let contents = BlockContainer::BlockLevelBoxes(boxes);
|
||
let contains_floats = contents.contains_floats();
|
||
Self {
|
||
root: BlockFormattingContext {
|
||
contents,
|
||
contains_floats,
|
||
},
|
||
// From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
|
||
// > If visible is applied to the viewport, it must be interpreted as auto.
|
||
// > If clip is applied to the viewport, it must be interpreted as hidden.
|
||
viewport_overflow: viewport_overflow.to_scrollable(),
|
||
}
|
||
}
|
||
|
||
fn viewport_overflow(
|
||
root_element: ServoThreadSafeLayoutNode<'_>,
|
||
root_box: Option<&ArcRefCell<BlockLevelBox>>,
|
||
) -> AxesOverflow {
|
||
// From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
|
||
// > UAs must apply the overflow-* values set on the root element to the viewport when the
|
||
// > root element’s display value is not none. However, when the root element is an [HTML]
|
||
// > html element (including XML syntax for HTML) whose overflow value is visible (in both
|
||
// > axes), and that element has as a child a body element whose display value is also not
|
||
// > none, user agents must instead apply the overflow-* values of the first such child
|
||
// > element to the viewport. The element from which the value is propagated must then have a
|
||
// > used overflow value of visible.
|
||
|
||
// If there is no root box, the root element has `display: none`, so don't propagate.
|
||
// The spec isn't very clear about what value to use, but the initial value seems fine.
|
||
// See https://github.com/w3c/csswg-drafts/issues/12649
|
||
let Some(root_box) = root_box else {
|
||
return AxesOverflow::default();
|
||
};
|
||
|
||
let propagate_from_body = || {
|
||
// Unlike what the spec implies, we stop iterating when we find the first <body>,
|
||
// even if it's not suitable because it lacks a box. This matches other browsers.
|
||
// See https://github.com/w3c/csswg-drafts/issues/12644
|
||
let body = root_element.children().find(|child| {
|
||
child
|
||
.as_element()
|
||
.is_some_and(|element| element.is_body_element_of_html_element_root())
|
||
})?;
|
||
|
||
// We only propagate from the <body> if it generates a box. The spec only checks for
|
||
// `display: none`, but other browsers don't propagate for `display: contents` either.
|
||
// See https://github.com/w3c/csswg-drafts/issues/12643
|
||
let body_layout_data = body.inner_layout_data()?;
|
||
let mut body_box = body_layout_data.self_box.borrow_mut();
|
||
body_box.as_mut()?.with_base_mut(|base| {
|
||
base.base_fragment_info
|
||
.flags
|
||
.insert(FragmentFlags::PROPAGATED_OVERFLOW_TO_VIEWPORT);
|
||
AxesOverflow::from(&*base.style)
|
||
})
|
||
};
|
||
|
||
root_box.borrow_mut().with_base_mut(|base| {
|
||
let root_overflow = AxesOverflow::from(&*base.style);
|
||
if root_overflow.x == Overflow::Visible && root_overflow.y == Overflow::Visible {
|
||
if let Some(body_overflow) = propagate_from_body() {
|
||
return body_overflow;
|
||
}
|
||
}
|
||
base.base_fragment_info
|
||
.flags
|
||
.insert(FragmentFlags::PROPAGATED_OVERFLOW_TO_VIEWPORT);
|
||
root_overflow
|
||
})
|
||
}
|
||
}
|
||
|
||
fn construct_for_root_element(
|
||
context: &LayoutContext,
|
||
root_element: ServoThreadSafeLayoutNode<'_>,
|
||
) -> Vec<ArcRefCell<BlockLevelBox>> {
|
||
let info = NodeAndStyleInfo::new(root_element, root_element.style(&context.style_context));
|
||
let box_style = info.style.get_box();
|
||
|
||
let display_inside = match Display::from(box_style.display) {
|
||
Display::None => return Vec::new(),
|
||
Display::Contents => {
|
||
// Unreachable because the style crate adjusts the computed values:
|
||
// https://drafts.csswg.org/css-display-3/#transformations
|
||
// “'display' of 'contents' computes to 'block' on the root element”
|
||
unreachable!()
|
||
},
|
||
// The root element is blockified, ignore DisplayOutside
|
||
Display::GeneratingBox(display_generating_box) => display_generating_box.display_inside(),
|
||
};
|
||
|
||
let contents = Contents::for_element(root_element, context);
|
||
|
||
let propagated_data = PropagatedBoxTreeData::default();
|
||
let root_box = if box_style.position.is_absolutely_positioned() {
|
||
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new(
|
||
AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
|
||
))
|
||
} else if box_style.float.is_floating() {
|
||
BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct(
|
||
context,
|
||
&info,
|
||
display_inside,
|
||
contents,
|
||
propagated_data,
|
||
))
|
||
} else {
|
||
BlockLevelBox::Independent(IndependentFormattingContext::construct(
|
||
context,
|
||
&info,
|
||
display_inside,
|
||
contents,
|
||
propagated_data,
|
||
))
|
||
};
|
||
|
||
let root_box = ArcRefCell::new(root_box);
|
||
root_element
|
||
.box_slot()
|
||
.set(LayoutBox::BlockLevel(root_box.clone()));
|
||
vec![root_box]
|
||
}
|
||
|
||
impl BoxTree {
|
||
#[servo_tracing::instrument(name = "Fragment Tree Construction", skip_all)]
|
||
pub(crate) fn layout(
|
||
&self,
|
||
layout_context: &LayoutContext,
|
||
viewport: UntypedSize2D<Au>,
|
||
) -> FragmentTree {
|
||
let style = layout_context
|
||
.style_context
|
||
.stylist
|
||
.device()
|
||
.default_computed_values();
|
||
|
||
// FIXME: use the document’s mode:
|
||
// https://drafts.csswg.org/css-writing-modes/#principal-flow
|
||
let physical_containing_block: Rect<Au, CSSPixel> =
|
||
PhysicalSize::from_untyped(viewport).into();
|
||
let initial_containing_block = DefiniteContainingBlock {
|
||
size: LogicalVec2 {
|
||
inline: physical_containing_block.size.width,
|
||
block: physical_containing_block.size.height,
|
||
},
|
||
style,
|
||
};
|
||
|
||
let mut positioning_context = PositioningContext::default();
|
||
let independent_layout = self.root.layout(
|
||
layout_context,
|
||
&mut positioning_context,
|
||
&(&initial_containing_block).into(),
|
||
);
|
||
|
||
let mut root_fragments = independent_layout.fragments;
|
||
|
||
// Zero box for `:root { display: none }`, one for the root element otherwise.
|
||
assert!(root_fragments.len() <= 1);
|
||
|
||
// There may be more fragments at the top-level
|
||
// (for positioned boxes whose containing is the initial containing block)
|
||
// but only if there was one fragment for the root element.
|
||
positioning_context.layout_initial_containing_block_children(
|
||
layout_context,
|
||
&initial_containing_block,
|
||
&mut root_fragments,
|
||
);
|
||
|
||
let viewport_scroll_sensitivity = AxesScrollSensitivity {
|
||
x: self.viewport_overflow.x.into(),
|
||
y: self.viewport_overflow.y.into(),
|
||
};
|
||
|
||
FragmentTree::new(
|
||
layout_context,
|
||
root_fragments,
|
||
physical_containing_block,
|
||
viewport_scroll_sensitivity,
|
||
)
|
||
}
|
||
}
|