From 5cd57f9dba26d62c18d220bb1607fa0dfc90f025 Mon Sep 17 00:00:00 2001 From: JoeDow Date: Fri, 25 Jul 2025 19:49:46 +0800 Subject: [PATCH] layout: Add incremental box tree construction for inline boxes (#38084) This changes extend the incremental box tree construction for inline boxes. Since an `InlineItem` can be split into multiple `InlineItem`s by a block element, the reason such an inline item is marked as damaged may simply be the removal of the block element or the need to reconstruct its box tree. Therefore, under the current LayoutDamage design, theoretically, even damaged inline items might still have some of their splits reusable. However, based on the principle of simplicity and effectiveness, this PR only considers reusing undamaged inline boxes. Testing: This should not change observable behavior and is thus covered by existing WPT tests. Signed-off-by: sharpshooter_pt --- components/layout/flow/construct.rs | 9 +- components/layout/flow/inline/construct.rs | 102 +++++++++++++++++--- components/layout/flow/inline/inline_box.rs | 11 +-- 3 files changed, 99 insertions(+), 23 deletions(-) diff --git a/components/layout/flow/construct.rs b/components/layout/flow/construct.rs index f11efb7c559..7537c98065d 100644 --- a/components/layout/flow/construct.rs +++ b/components/layout/flow/construct.rs @@ -466,6 +466,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { contents: Contents, box_slot: BoxSlot<'dom>, ) { + let old_layout_box = box_slot.take_layout_box_if_undamaged(info.damage); let (is_list_item, non_replaced_contents) = match (display_inside, contents) { ( DisplayInside::Flow { is_list_item }, @@ -485,7 +486,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { propagated_data, )) }; - let old_layout_box = box_slot.take_layout_box_if_undamaged(info.damage); + let atomic = self .ensure_inline_formatting_context_builder() .push_atomic(construction_callback, old_layout_box); @@ -497,7 +498,11 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { // Otherwise, this is just a normal inline box. Whatever happened before, all we need to do // before recurring is to remember this ongoing inline level box. self.ensure_inline_formatting_context_builder() - .start_inline_box(InlineBox::new(info), None); + .start_inline_box( + || ArcRefCell::new(InlineBox::new(info)), + None, + old_layout_box, + ); if is_list_item { if let Some((marker_info, marker_contents)) = diff --git a/components/layout/flow/inline/construct.rs b/components/layout/flow/inline/construct.rs index b7c124819b8..91353272361 100644 --- a/components/layout/flow/inline/construct.rs +++ b/components/layout/flow/inline/construct.rs @@ -85,6 +85,15 @@ pub(crate) struct InlineFormattingContextBuilder { /// [to be split]: https://www.w3.org/TR/CSS2/visuren.html#anonymous-block-level block_in_inline_splits: Vec>>, + /// If the [`InlineBox`] of an inline-level element is not damaged, it can be reused + /// to support incremental layout. An [`InlineBox`] can be split by block elements + /// into multiple [`InlineBox`]es, all inserted into different + /// [`InlineFormattingContext`]s. Therefore, [`Self::old_block_in_inline_splits`] is + /// used to hold all these split inline boxes from the previous box tree construction + /// that are about to be reused, ensuring they can be sequentially inserted into each + /// newly built [`InlineFormattingContext`]. + old_block_in_inline_splits: Vec>>, + /// Whether or not the inline formatting context under construction has any /// uncollapsible text content. pub has_uncollapsible_text_content: bool, @@ -233,20 +242,60 @@ impl InlineFormattingContextBuilder { pub(crate) fn start_inline_box( &mut self, - inline_box: InlineBox, + inline_box_creator: impl FnOnce() -> ArcRefCell, block_in_inline_splits: Option>>, + old_layout_box: Option, ) { - self.push_control_character_string(inline_box.base.style.bidi_control_chars().0); + // If there is an existing undamaged layout box that's compatible, use the `InlineBox` within it. + if let Some(LayoutBox::InlineLevel(inline_level_box)) = old_layout_box { + let old_block_in_inline_splits: Vec> = inline_level_box + .iter() + .rev() // reverse to facilate the `Vec::pop` operation + .filter_map(|inline_item| match &*inline_item.borrow() { + InlineItem::StartInlineBox(inline_box) => Some(inline_box.clone()), + _ => None, + }) + .collect(); + + debug_assert!( + old_block_in_inline_splits.is_empty() || + old_block_in_inline_splits.len() == inline_level_box.len(), + "Create inline box with incompatible `old_layout_box`" + ); + + self.start_inline_box_internal( + inline_box_creator, + block_in_inline_splits, + old_block_in_inline_splits, + ); + } else { + self.start_inline_box_internal(inline_box_creator, block_in_inline_splits, vec![]); + } + } + + pub fn start_inline_box_internal( + &mut self, + inline_box_creator: impl FnOnce() -> ArcRefCell, + block_in_inline_splits: Option>>, + mut old_block_in_inline_splits: Vec>, + ) { + let inline_box = old_block_in_inline_splits + .pop() + .unwrap_or_else(inline_box_creator); + + let borrowed_inline_box = inline_box.borrow(); + self.push_control_character_string(borrowed_inline_box.base.style.bidi_control_chars().0); // Don't push a `SharedInlineStyles` if we are pushing this box when splitting // an IFC for a block-in-inline split. Shared styles are pushed as part of setting // up the second split of the IFC. - if inline_box.is_first_split { + if borrowed_inline_box.is_first_split { self.shared_inline_styles_stack - .push(inline_box.shared_inline_styles.clone()); + .push(borrowed_inline_box.shared_inline_styles.clone()); } + std::mem::drop(borrowed_inline_box); - let (identifier, inline_box) = self.inline_boxes.start_inline_box(inline_box); + let identifier = self.inline_boxes.start_inline_box(inline_box.clone()); let inline_level_box = ArcRefCell::new(InlineItem::StartInlineBox(inline_box)); self.inline_items.push(inline_level_box.clone()); self.inline_box_stack.push(identifier); @@ -254,6 +303,9 @@ impl InlineFormattingContextBuilder { let mut block_in_inline_splits = block_in_inline_splits.unwrap_or_default(); block_in_inline_splits.push(inline_level_box); self.block_in_inline_splits.push(block_in_inline_splits); + + self.old_block_in_inline_splits + .push(old_block_in_inline_splits); } /// End the ongoing inline box in this [`InlineFormattingContextBuilder`], returning @@ -271,6 +323,14 @@ impl InlineFormattingContextBuilder { self.push_control_character_string(inline_level_box.base.style.bidi_control_chars().1); } + debug_assert!( + self.old_block_in_inline_splits + .last() + .is_some_and(|inline_boxes| inline_boxes.is_empty()), + "Reuse incompatible `old_block_in_inline_splits` for inline boxes", + ); + let _ = self.old_block_in_inline_splits.pop(); + block_in_inline_splits.unwrap_or_default() } @@ -394,20 +454,31 @@ impl InlineFormattingContextBuilder { let mut new_builder = Self::new_for_shared_styles(self.shared_inline_styles_stack.clone()); let block_in_inline_splits = std::mem::take(&mut self.block_in_inline_splits); - for (identifier, historical_inline_boxes) in - izip!(self.inline_box_stack.iter(), block_in_inline_splits) - { + let old_block_in_inline_splits = std::mem::take(&mut self.old_block_in_inline_splits); + for (identifier, already_collected_inline_boxes, being_recollected_inline_boxes) in izip!( + self.inline_box_stack.iter(), + block_in_inline_splits, + old_block_in_inline_splits + ) { // Start a new inline box for every ongoing inline box in this // InlineFormattingContext once we are done processing this block element, // being sure to give the block-in-inline-split to the new // InlineFormattingContext. These will finally be inserted into the DOM's - // BoxSlot once the inline box has been fully processed. - new_builder.start_inline_box( - self.inline_boxes - .get(identifier) - .borrow() - .split_around_block(), - Some(historical_inline_boxes), + // BoxSlot once the inline box has been fully processed. Meanwhile, being + // sure to give the old-block-in-inline-split to new InlineFormattingContext, + // so that them will be inserted into each following InlineFormattingContext. + let split_inline_box_callback = || { + ArcRefCell::new( + self.inline_boxes + .get(identifier) + .borrow() + .split_around_block(), + ) + }; + new_builder.start_inline_box_internal( + split_inline_box_callback, + Some(already_collected_inline_boxes), + being_recollected_inline_boxes, ); } let mut inline_builder_from_before_split = std::mem::replace(self, new_builder); @@ -440,6 +511,7 @@ impl InlineFormattingContextBuilder { } assert!(self.inline_box_stack.is_empty()); + debug_assert!(self.old_block_in_inline_splits.is_empty()); Some(InlineFormattingContext::new_with_builder( self, layout_context, diff --git a/components/layout/flow/inline/inline_box.rs b/components/layout/flow/inline/inline_box.rs index 0db6dd72d59..e9d68dd12c7 100644 --- a/components/layout/flow/inline/inline_box.rs +++ b/components/layout/flow/inline/inline_box.rs @@ -114,8 +114,8 @@ impl InlineBoxes { pub(super) fn start_inline_box( &mut self, - mut inline_box: InlineBox, - ) -> (InlineBoxIdentifier, ArcRefCell) { + inline_box: ArcRefCell, + ) -> InlineBoxIdentifier { assert!(self.inline_boxes.len() <= u32::MAX as usize); assert!(self.inline_box_tree.len() <= u32::MAX as usize); @@ -126,14 +126,13 @@ impl InlineBoxes { index_of_start_in_tree, index_in_inline_boxes, }; - inline_box.identifier = identifier; - let inline_box = ArcRefCell::new(inline_box); + inline_box.borrow_mut().identifier = identifier; - self.inline_boxes.push(inline_box.clone()); + self.inline_boxes.push(inline_box); self.inline_box_tree .push(InlineBoxTreePathToken::Start(identifier)); - (identifier, inline_box) + identifier } pub(super) fn get_path(