Layout: refactor the box tree update logic for better readability (#37390)

Refactor the box tree update logic to improve readability and facilitate
the addition of incremental update logic in the following PRs.

Testing: No logic changed, just covered by existing WPT tests
Fixes: Nothing to fix

Signed-off-by: sharpshooter_pt <ibluegalaxy_taoj@163.com>
This commit is contained in:
JoeDow 2025-06-13 16:42:59 +08:00 committed by GitHub
parent 6818d5c600
commit 5159529888
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -125,168 +125,11 @@ impl BoxTree {
/// * how intrinsic content sizes are computed eagerly makes it hard
/// to update those sizes for ancestors of the node from which we
/// made an incremental update.
pub fn update(context: &LayoutContext, mut dirty_node: ServoLayoutNode<'_>) -> bool {
#[allow(clippy::enum_variant_names)]
enum UpdatePoint {
AbsolutelyPositionedBlockLevelBox(ArcRefCell<BlockLevelBox>),
AbsolutelyPositionedInlineLevelBox(ArcRefCell<InlineItem>, usize),
AbsolutelyPositionedFlexLevelBox(ArcRefCell<FlexLevelBox>),
AbsolutelyPositionedTaffyLevelBox(ArcRefCell<TaffyItemBox>),
}
fn update_point(
node: ServoLayoutNode<'_>,
) -> Option<(Arc<ComputedValues>, DisplayInside, UpdatePoint)> {
if !node.is_element() {
return None;
}
if node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLBodyElement) {
// This can require changes to the canvas background.
return None;
}
// Don't update unstyled nodes or nodes that have pseudo-elements.
let element_data = node.style_data()?.element_data.borrow();
if !element_data.styles.pseudos.is_empty() {
return None;
}
let layout_data = NodeExt::layout_data(&node)?;
if layout_data.pseudo_before_box.borrow().is_some() {
return None;
}
if layout_data.pseudo_after_box.borrow().is_some() {
return None;
}
let primary_style = element_data.styles.primary();
let box_style = primary_style.get_box();
if !box_style.position.is_absolutely_positioned() {
return None;
}
let display_inside = match Display::from(box_style.display) {
Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { inside, .. }) => {
inside
},
_ => return None,
};
let update_point =
match &*AtomicRef::filter_map(layout_data.self_box.borrow(), Option::as_ref)? {
LayoutBox::DisplayContents(..) => return None,
LayoutBox::BlockLevel(block_level_box) => match &*block_level_box.borrow() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedBlockLevelBox(block_level_box.clone())
},
_ => return None,
},
LayoutBox::InlineLevel(inline_level_items) => {
let inline_level_box = inline_level_items.first()?;
let InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index) =
&*inline_level_box.borrow()
else {
return None;
};
UpdatePoint::AbsolutelyPositionedInlineLevelBox(
inline_level_box.clone(),
*text_offset_index,
)
},
LayoutBox::FlexLevel(flex_level_box) => match &*flex_level_box.borrow() {
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedFlexLevelBox(flex_level_box.clone())
},
_ => return None,
},
LayoutBox::TableLevelBox(..) => return None,
LayoutBox::TaffyItemBox(taffy_level_box) => match &taffy_level_box
.borrow()
.taffy_level_box
{
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
UpdatePoint::AbsolutelyPositionedTaffyLevelBox(taffy_level_box.clone())
},
_ => return None,
},
};
Some((primary_style.clone(), display_inside, update_point))
}
loop {
let Some((primary_style, display_inside, update_point)) = update_point(dirty_node)
else {
dirty_node = match dirty_node.parent_node() {
Some(parent) => parent,
None => return false,
};
continue;
};
let contents = ReplacedContents::for_element(dirty_node, context)
.map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced);
let info = NodeAndStyleInfo::new(dirty_node, Arc::clone(&primary_style));
let out_of_flow_absolutely_positioned_box = ArcRefCell::new(
AbsolutelyPositionedBox::construct(context, &info, display_inside, contents),
);
match update_point {
UpdatePoint::AbsolutelyPositionedBlockLevelBox(block_level_box) => {
*block_level_box.borrow_mut() = BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
UpdatePoint::AbsolutelyPositionedInlineLevelBox(
inline_level_box,
text_offset_index,
) => {
*inline_level_box.borrow_mut() = InlineItem::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
text_offset_index,
);
},
UpdatePoint::AbsolutelyPositionedFlexLevelBox(flex_level_box) => {
*flex_level_box.borrow_mut() = FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
UpdatePoint::AbsolutelyPositionedTaffyLevelBox(taffy_level_box) => {
taffy_level_box.borrow_mut().taffy_level_box =
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
}
break;
}
// We are going to rebuild 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 one of 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
// siblings and siblings 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.
while let Some(parent_node) = dirty_node.parent_node() {
parent_node.invalidate_cached_fragment();
dirty_node = parent_node;
}
pub fn update(context: &LayoutContext, dirty_root_from_script: ServoLayoutNode<'_>) -> bool {
let Some(box_tree_update) = IncrementalBoxTreeUpdate::find(dirty_root_from_script) else {
return false;
};
box_tree_update.update_from_dirty_root(context);
true
}
}
@ -400,3 +243,189 @@ impl BoxTree {
)
}
}
#[allow(clippy::enum_variant_names)]
enum DirtyRootBoxTreeNode {
AbsolutelyPositionedBlockLevelBox(ArcRefCell<BlockLevelBox>),
AbsolutelyPositionedInlineLevelBox(ArcRefCell<InlineItem>, usize),
AbsolutelyPositionedFlexLevelBox(ArcRefCell<FlexLevelBox>),
AbsolutelyPositionedTaffyLevelBox(ArcRefCell<TaffyItemBox>),
}
struct IncrementalBoxTreeUpdate<'dom> {
node: ServoLayoutNode<'dom>,
box_tree_node: DirtyRootBoxTreeNode,
primary_style: Arc<ComputedValues>,
display_inside: DisplayInside,
}
impl<'dom> IncrementalBoxTreeUpdate<'dom> {
fn find(dirty_root_from_script: ServoLayoutNode<'dom>) -> Option<Self> {
let mut maybe_dirty_root_node = Some(dirty_root_from_script);
while let Some(dirty_root_node) = maybe_dirty_root_node {
if let Some(dirty_root) = Self::new_if_valid(dirty_root_node) {
return Some(dirty_root);
}
maybe_dirty_root_node = dirty_root_node.parent_node();
}
None
}
fn new_if_valid(potential_dirty_root_node: ServoLayoutNode<'dom>) -> Option<Self> {
if !potential_dirty_root_node.is_element() {
return None;
}
if potential_dirty_root_node.type_id() ==
LayoutNodeType::Element(LayoutElementType::HTMLBodyElement)
{
// This can require changes to the canvas background.
return None;
}
// Don't update unstyled nodes or nodes that have pseudo-elements.
let element_data = potential_dirty_root_node
.style_data()?
.element_data
.borrow();
if !element_data.styles.pseudos.is_empty() {
return None;
}
let layout_data = NodeExt::layout_data(&potential_dirty_root_node)?;
if layout_data.pseudo_before_box.borrow().is_some() {
return None;
}
if layout_data.pseudo_after_box.borrow().is_some() {
return None;
}
let primary_style = element_data.styles.primary();
let box_style = primary_style.get_box();
if !box_style.position.is_absolutely_positioned() {
return None;
}
let display_inside = match Display::from(box_style.display) {
Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { inside, .. }) => inside,
_ => return None,
};
let box_tree_node =
match &*AtomicRef::filter_map(layout_data.self_box.borrow(), Option::as_ref)? {
LayoutBox::DisplayContents(..) => return None,
LayoutBox::BlockLevel(block_level_box) => match &*block_level_box.borrow() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
DirtyRootBoxTreeNode::AbsolutelyPositionedBlockLevelBox(
block_level_box.clone(),
)
},
_ => return None,
},
LayoutBox::InlineLevel(inline_level_items) => {
let inline_level_box = inline_level_items.first()?;
let InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index) =
&*inline_level_box.borrow()
else {
return None;
};
DirtyRootBoxTreeNode::AbsolutelyPositionedInlineLevelBox(
inline_level_box.clone(),
*text_offset_index,
)
},
LayoutBox::FlexLevel(flex_level_box) => match &*flex_level_box.borrow() {
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
DirtyRootBoxTreeNode::AbsolutelyPositionedFlexLevelBox(
flex_level_box.clone(),
)
},
_ => return None,
},
LayoutBox::TableLevelBox(..) => return None,
LayoutBox::TaffyItemBox(taffy_level_box) => {
match &taffy_level_box.borrow().taffy_level_box {
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(_)
if box_style.position.is_absolutely_positioned() =>
{
DirtyRootBoxTreeNode::AbsolutelyPositionedTaffyLevelBox(
taffy_level_box.clone(),
)
},
_ => return None,
}
},
};
Some(Self {
node: potential_dirty_root_node,
box_tree_node,
primary_style: primary_style.clone(),
display_inside,
})
}
fn update_from_dirty_root(&self, context: &LayoutContext) {
let contents = ReplacedContents::for_element(self.node, context)
.map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced);
let info = NodeAndStyleInfo::new(self.node, self.primary_style.clone());
let out_of_flow_absolutely_positioned_box = ArcRefCell::new(
AbsolutelyPositionedBox::construct(context, &info, self.display_inside, contents),
);
match &self.box_tree_node {
DirtyRootBoxTreeNode::AbsolutelyPositionedBlockLevelBox(block_level_box) => {
*block_level_box.borrow_mut() = BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
DirtyRootBoxTreeNode::AbsolutelyPositionedInlineLevelBox(
inline_level_box,
text_offset_index,
) => {
*inline_level_box.borrow_mut() = InlineItem::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
*text_offset_index,
);
},
DirtyRootBoxTreeNode::AbsolutelyPositionedFlexLevelBox(flex_level_box) => {
*flex_level_box.borrow_mut() = FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
DirtyRootBoxTreeNode::AbsolutelyPositionedTaffyLevelBox(taffy_level_box) => {
taffy_level_box.borrow_mut().taffy_level_box =
TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(
out_of_flow_absolutely_positioned_box,
);
},
}
// We are going to rebuild 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 one of 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
// siblings and siblings 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.
let mut invalidate_start_point = self.node;
while let Some(parent_node) = invalidate_start_point.parent_node() {
parent_node.invalidate_cached_fragment();
invalidate_start_point = parent_node;
}
}
}