servo/components/layout/flow/root.rs
coding-joedow 94a662193e Layout: begin support incremental box tree update
Currently, we just rebuild boxes for all nodes from the update point
downward, and the unique valid candidates as update point is just the
absolutely positioned ancestor of the style recalc dirty dom root node.
It is quite crude way for incremental box tree update and incremental
layout, because it will lead to a lot of boxes to be rebuilt even though
their originating nodes have no style change, i.e. only some child nodes
are newly added or removed. Meanwhile, all cached fragments need to be
invalidated from the update point downward, even though there is no any
change of the layout constraits and containing block for some of those
rebuilt boxes.

To preserve more boxes and cached fragments as much as possible, this PR
try to rebuild those boxes whose originating node has `REBUILD_BOX`
restyle damage and try to repair those boxes whose originating node has
`REPAIR_BOX` damage. It is a relative big task. To implement it step by
step, this PR only repair and reuse the block level boxes. In the future,
the others kind of boxes will be repaired or reused.

Signed-off-by: coding-joedow <ibluegalaxy_taoj@163.com>
2025-06-03 10:45:12 +08:00

446 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 atomic_refcell::AtomicRef;
use compositing_traits::display_list::AxesScrollSensitivity;
use euclid::Rect;
use euclid::default::Size2D as UntypedSize2D;
use malloc_size_of_derive::MallocSizeOf;
use script::layout_dom::ServoLayoutNode;
use script_layout_interface::wrapper_traits::{
LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
};
use servo_arc::Arc;
use style::dom::{NodeInfo, TNode};
use style::properties::ComputedValues;
use style::selector_parser::RestyleDamage;
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, NonReplacedContents, iter_child_nodes};
use crate::flexbox::FlexLevelBox;
use crate::flow::float::FloatBox;
use crate::flow::inline::InlineItem;
use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox};
use crate::formatting_contexts::IndependentFormattingContext;
use crate::fragment_tree::FragmentTree;
use crate::geom::{LogicalVec2, PhysicalSize};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
use crate::replaced::ReplacedContents;
use crate::style_ext::{Display, DisplayInside};
use crate::taffy::TaffyItemBoxInner;
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
viewport_scroll_sensitivity: AxesScrollSensitivity,
}
impl BoxTree {
pub fn construct(context: &LayoutContext, root_element: ServoLayoutNode<'_>) -> Self {
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);
// 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 elements 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.
let root_style = root_element.style(context.shared_context());
let mut viewport_overflow_x = root_style.clone_overflow_x();
let mut viewport_overflow_y = root_style.clone_overflow_y();
if viewport_overflow_x == Overflow::Visible &&
viewport_overflow_y == Overflow::Visible &&
!root_style.get_box().display.is_none()
{
for child in iter_child_nodes(root_element) {
if !child
.to_threadsafe()
.as_element()
.is_some_and(|element| element.is_body_element_of_html_element_root())
{
continue;
}
let style = child.style(context.shared_context());
if !style.get_box().display.is_none() {
viewport_overflow_x = style.clone_overflow_x();
viewport_overflow_y = style.clone_overflow_y();
break;
}
}
}
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_scroll_sensitivity: AxesScrollSensitivity {
x: viewport_overflow_x.to_scrollable().into(),
y: viewport_overflow_y.to_scrollable().into(),
},
}
}
/// This method attempts to incrementally update the box tree from an
/// arbitrary node that is not necessarily the document's root element.
///
/// If the node is not a valid candidate for incremental update, the method
/// loops over its parent. Currently, the only valid candidates for now
/// are absolutely positioned boxes and their originating nodes must have
/// restyle damage that is `REPAIR_BOX` rather than `REBUILD_BOX`. In the
/// future, this may be extended to other types of boxes if the new restyle
/// damage types for incremental layout is ready.
///
/// There are various reasons why only absolutely positioned boxes can be
/// selected as the update point:
/// * it needs a bit of trick to incrementally update the `contains_floats`
/// for a block formatting context and `SameFormattingContext`, which make
/// the incremental update less eligible.
/// * the propagation of box tree data towards node descendants is hard to do
/// incrementally with our current representation of boxes. To support
/// incremental update, we have to store it at the `LayoutBoxBase`.
/// * We have to clear all layout caches for ancestors of the update point
/// when the incremental layout is not fully ready. However, it is really hard
/// to do that incrementally because a box does not hold a reference to its
/// parent box with current representation of boxes. The anonymous ancestor
/// boxes can not be visited. Thus, select the absolutely positioned boxes
/// as the update point, this is not a problem.
pub fn update(context: &LayoutContext, dirty_node: ServoLayoutNode<'_>) -> bool {
let mut helper = BoxTreeUpdateHelper::new(context, dirty_node);
helper.try_update_box_tree()
}
}
fn construct_for_root_element(
context: &LayoutContext,
root_element: ServoLayoutNode<'_>,
) -> Vec<ArcRefCell<BlockLevelBox>> {
root_element.clear_restyle_damage();
let info = NodeAndStyleInfo::new(root_element, root_element.style(context.shared_context()));
let box_style = info.style.get_box();
let display_inside = match Display::from(box_style.display) {
Display::None => {
root_element.unset_all_boxes();
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 = ReplacedContents::for_element(root_element, context)
.map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced);
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
.element_box_slot()
.set(LayoutBox::BlockLevel(root_box.clone()));
vec![root_box]
}
impl BoxTree {
pub fn layout(
&self,
layout_context: &LayoutContext,
viewport: UntypedSize2D<Au>,
) -> FragmentTree {
let style = layout_context
.style_context
.stylist
.device()
.default_computed_values();
// FIXME: use the documents 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(),
false, /* depends_on_block_constraints */
);
let mut root_fragments = independent_layout.fragments.into_iter().collect::<Vec<_>>();
// 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,
);
FragmentTree::new(
layout_context,
root_fragments,
physical_containing_block,
self.viewport_scroll_sensitivity,
)
}
}
struct BoxTreeUpdateHelper<'style, 'dom> {
context: &'style LayoutContext<'style>,
dirty_node: ServoLayoutNode<'dom>,
primary_style: Option<Arc<ComputedValues>>,
display_inside: Option<DisplayInside>,
}
enum UpdatePoint {
DoNotNeedUpdateTree,
RootElementPrimaryBox(ArcRefCell<BlockLevelBox>),
AbsolutelyPositionedBox(ArcRefCell<AbsolutelyPositionedBox>),
}
impl<'style, 'dom> BoxTreeUpdateHelper<'style, 'dom> {
fn new(context: &'style LayoutContext<'style>, dirty_node: ServoLayoutNode<'dom>) -> Self {
Self {
context,
dirty_node,
primary_style: None,
display_inside: None,
}
}
fn try_update_box_tree(&mut self) -> bool {
loop {
let Some(update_point) = self.update_point() else {
self.dirty_node = match self.dirty_node.parent_node() {
Some(parent) => parent,
None => return false,
};
continue;
};
match update_point {
UpdatePoint::DoNotNeedUpdateTree => {},
_ => {
self.update_box_tree(update_point);
self.invalidate_layout_cache();
self.clear_ancestor_restyle_damage();
},
};
break;
}
true
}
fn update_point(&mut self) -> Option<UpdatePoint> {
if !self.dirty_node.is_element() {
return None;
}
let element_data = self.dirty_node.style_data()?.element_data.borrow();
if !element_data.damage.contains(RestyleDamage::REPAIR_BOX) {
return Some(UpdatePoint::DoNotNeedUpdateTree);
}
if element_data.damage.contains(RestyleDamage::REBUILD_BOX) {
return None;
}
let primary_style = element_data.styles.primary();
let box_style = primary_style.get_box();
let display_inside = match Display::from(box_style.display) {
Display::GeneratingBox(generating_box) => generating_box.display_inside(),
_ => return None,
};
let layout_data = NodeExt::layout_data(&self.dirty_node)?;
let layout_box = &*AtomicRef::filter_map(layout_data.self_box.borrow(), Option::as_ref)?;
if self.dirty_node.to_threadsafe().as_element()?.is_root() {
match layout_box {
LayoutBox::BlockLevel(block_level_box) => {
self.primary_style = Some(primary_style.clone());
self.display_inside = Some(display_inside);
return Some(UpdatePoint::RootElementPrimaryBox(block_level_box.clone()));
},
// 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!("Root element should only has display: block"),
}
}
if !box_style.position.is_absolutely_positioned() {
return None;
}
let update_point = match layout_box {
LayoutBox::DisplayContents(..) => return None,
LayoutBox::BlockLevel(block_level_box) => match &*block_level_box.borrow() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => {
UpdatePoint::AbsolutelyPositionedBox(positioned_box.clone())
},
_ => return None,
},
LayoutBox::InlineLevel(inline_level_items) => {
let inline_level_item = inline_level_items.first()?;
match &*inline_level_item.borrow() {
InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, _) => {
UpdatePoint::AbsolutelyPositionedBox(positioned_box.clone())
},
_ => return None,
}
},
LayoutBox::FlexLevel(flex_level_box) => match &*flex_level_box.borrow() {
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => {
UpdatePoint::AbsolutelyPositionedBox(positioned_box.clone())
},
_ => return None,
},
LayoutBox::TableLevelBox(..) => return None,
LayoutBox::TaffyItemBox(taffy_item_box) => {
match &taffy_item_box.borrow().taffy_level_box {
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(positioned_box) => {
UpdatePoint::AbsolutelyPositionedBox(positioned_box.clone())
},
_ => return None,
}
},
};
self.primary_style = Some(primary_style.clone());
self.display_inside = Some(display_inside);
Some(update_point)
}
/// We are going to repair the box tree from the update point downward, but this update
/// point is an absolute, which means that it needs to be laid out again in the containing
/// block for absolutes, which is established by on if its ancestors. In addition,
/// absolutes, when laid out, can produce more absolutes (either fixed or absolutely
/// positioned) elements, so there may be yet more layout that has to happen in this
/// ancestor.
///
/// We do not know which ancestor is the one that established the containing block for this
/// update point, so just invalidate the fragment cache of all ancestors, meaning that even
/// though the box tree is preserved, the fragment tree from the root to the update point and
/// all of its descendants will need to be rebuilt. This isn't as bad as it seems, because
/// slibings and slibings of ancestors of this path through the tree will still have cached
/// fragments.
///
/// TODO: Do better. This is still a very crude way to do incremental layout.
fn invalidate_layout_cache(&self) {
let mut invalidation_start_point = self.dirty_node;
while let Some(parent_node) = invalidation_start_point.parent_node() {
parent_node.invalidate_cached_fragment();
invalidation_start_point = parent_node;
}
}
/// We have already propagate up some restyle damage from descendants to
/// the ancestors of update point, but we are just going to traverse the
/// subtree from the update point rather than the root element during
/// incremental update. Thus, clear all ancestor's restyle damage now.
fn clear_ancestor_restyle_damage(&self) {
let mut inclusive_ancestor = Some(self.dirty_node);
while let Some(node) = inclusive_ancestor {
node.clear_restyle_damage();
inclusive_ancestor = node.parent_node();
}
}
fn update_box_tree(&self, update_point: UpdatePoint) {
let context = self.context;
let display_inside = self.display_inside.unwrap();
let info = NodeAndStyleInfo::new(self.dirty_node, self.primary_style.clone().unwrap());
let contents = ReplacedContents::for_element(self.dirty_node, context)
.map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced);
match update_point {
UpdatePoint::DoNotNeedUpdateTree => unreachable!("Should have been filtered out"),
UpdatePoint::RootElementPrimaryBox(block_level_box) => {
block_level_box.borrow_mut().repair(
context,
&info,
contents,
display_inside,
PropagatedBoxTreeData::default(),
);
},
UpdatePoint::AbsolutelyPositionedBox(positioned_box) => {
positioned_box.borrow_mut().context.repair(
context,
&info,
contents,
display_inside,
PropagatedBoxTreeData::default(),
);
},
}
}
}