diff --git a/Cargo.lock b/Cargo.lock index 02d9e3a7ae1..1d4cf58a5a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6607,7 +6607,7 @@ dependencies = [ [[package]] name = "selectors" version = "0.28.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "bitflags 2.9.1", "cssparser", @@ -6902,7 +6902,7 @@ dependencies = [ [[package]] name = "servo_arc" version = "0.4.1" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "serde", "stable_deref_trait", @@ -7363,7 +7363,7 @@ dependencies = [ [[package]] name = "stylo" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "app_units", "arrayvec", @@ -7421,7 +7421,7 @@ dependencies = [ [[package]] name = "stylo_atoms" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "string_cache", "string_cache_codegen", @@ -7430,12 +7430,12 @@ dependencies = [ [[package]] name = "stylo_config" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" [[package]] name = "stylo_derive" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "darling", "proc-macro2", @@ -7447,7 +7447,7 @@ dependencies = [ [[package]] name = "stylo_dom" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "bitflags 2.9.1", "stylo_malloc_size_of", @@ -7456,7 +7456,7 @@ dependencies = [ [[package]] name = "stylo_malloc_size_of" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "app_units", "cssparser", @@ -7473,12 +7473,12 @@ dependencies = [ [[package]] name = "stylo_static_prefs" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" [[package]] name = "stylo_traits" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "app_units", "bitflags 2.9.1", @@ -7887,7 +7887,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "to_shmem" version = "0.2.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "cssparser", "servo_arc", @@ -7900,7 +7900,7 @@ dependencies = [ [[package]] name = "to_shmem_derive" version = "0.1.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/coding-joedow/stylo?branch=main#9a87b429580c4b8504b6259b8f9c674d1cb10fe6" dependencies = [ "darling", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 0fc9d90bf14..570bbb66cb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,7 +119,7 @@ rustls-pemfile = "2.0" rustls-pki-types = "1.12" script_layout_interface = { path = "components/shared/script_layout" } script_traits = { path = "components/shared/script" } -selectors = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +selectors = { git = "https://github.com/coding-joedow/stylo", branch = "main" } serde = "1.0.219" serde_bytes = "0.11" serde_json = "1.0" @@ -127,7 +127,7 @@ servo-media = { git = "https://github.com/servo/media" } servo-media-dummy = { git = "https://github.com/servo/media" } servo-media-gstreamer = { git = "https://github.com/servo/media" } servo-tracing = { path = "components/servo_tracing" } -servo_arc = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +servo_arc = { git = "https://github.com/coding-joedow/stylo", branch = "main" } smallbitvec = "2.6.0" smallvec = "1.15" snapshot = { path = "./components/shared/snapshot" } @@ -136,12 +136,12 @@ string_cache = "0.8" string_cache_codegen = "0.5" strum = "0.26" strum_macros = "0.26" -stylo = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_atoms = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_config = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_dom = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_malloc_size_of = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_traits = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +stylo = { git = "https://github.com/coding-joedow/stylo", branch = "main" } +stylo_atoms = { git = "https://github.com/coding-joedow/stylo", branch = "main" } +stylo_config = { git = "https://github.com/coding-joedow/stylo", branch = "main" } +stylo_dom = { git = "https://github.com/coding-joedow/stylo", branch = "main" } +stylo_malloc_size_of = { git = "https://github.com/coding-joedow/stylo", branch = "main" } +stylo_traits = { git = "https://github.com/coding-joedow/stylo", branch = "main" } surfman = { git = "https://github.com/servo/surfman", rev = "f7688b4585f9e0b5d4bf8ee8e4a91e82349610b1", features = ["chains"] } syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] } synstructure = "0.13" diff --git a/components/layout/cell.rs b/components/layout/cell.rs index 8b35fdf7943..72b99aa2f93 100644 --- a/components/layout/cell.rs +++ b/components/layout/cell.rs @@ -21,6 +21,10 @@ impl ArcRefCell { value: Arc::new(AtomicRefCell::new(value)), } } + + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + Arc::ptr_eq(&this.value, &other.value) + } } impl Clone for ArcRefCell { diff --git a/components/layout/dom.rs b/components/layout/dom.rs index 129f1338ffe..1666af9569d 100644 --- a/components/layout/dom.rs +++ b/components/layout/dom.rs @@ -20,7 +20,7 @@ use script_layout_interface::{ use servo_arc::Arc as ServoArc; use style::context::SharedStyleContext; use style::properties::ComputedValues; -use style::selector_parser::PseudoElement; +use style::selector_parser::{PseudoElement, RestyleDamage}; use crate::cell::ArcRefCell; use crate::flexbox::FlexLevelBox; @@ -160,7 +160,6 @@ pub struct BoxSlot<'dom> { /// A mutable reference to a `LayoutBox` stored in a DOM element. impl BoxSlot<'_> { pub(crate) fn new(slot: ArcRefCell>) -> Self { - *slot.borrow_mut() = None; let slot = Some(slot); Self { slot, @@ -216,6 +215,7 @@ pub(crate) trait NodeExt<'dom> { fn invalidate_cached_fragment(&self); fn repair_style(&self, context: &SharedStyleContext); + fn clear_restyle_damage(&self); } impl<'dom> NodeExt<'dom> for ServoLayoutNode<'dom> { @@ -404,4 +404,10 @@ impl<'dom> NodeExt<'dom> for ServoLayoutNode<'dom> { } } } + + fn clear_restyle_damage(&self) { + if let Some(data) = self.style_data() { + data.element_data.borrow_mut().damage = RestyleDamage::empty(); + } + } } diff --git a/components/layout/dom_traversal.rs b/components/layout/dom_traversal.rs index 4da6aa48b55..334c58c04d6 100644 --- a/components/layout/dom_traversal.rs +++ b/components/layout/dom_traversal.rs @@ -17,7 +17,7 @@ use selectors::Element as SelectorsElement; use servo_arc::Arc as ServoArc; use style::dom::{NodeInfo, TElement, TNode, TShadowRoot}; use style::properties::ComputedValues; -use style::selector_parser::PseudoElement; +use style::selector_parser::{PseudoElement, RestyleDamage}; use style::values::generics::counters::{Content, ContentItem}; use style::values::specified::Quotes; @@ -94,6 +94,24 @@ impl<'dom> NodeAndStyleInfo<'dom> { pub(crate) fn get_selection_range(&self) -> Option> { self.node.to_threadsafe().selection() } + + pub(crate) fn get_restyle_damage(&self) -> RestyleDamage { + self.node.style_data().unwrap().element_data.borrow().damage + } + + pub(crate) fn is_anonymous(&self) -> bool { + if matches!( + self.pseudo_element_type, + Some(PseudoElement::ServoAnonymousBox) | + Some(PseudoElement::ServoAnonymousTable) | + Some(PseudoElement::ServoAnonymousTableCell) | + Some(PseudoElement::ServoAnonymousTableRow) + ) { + return true; + } + + false + } } impl<'dom> From<&NodeAndStyleInfo<'dom>> for BaseFragmentInfo { @@ -108,13 +126,7 @@ impl<'dom> From<&NodeAndStyleInfo<'dom>> for BaseFragmentInfo { // TODO(mrobinson): It seems that anonymous boxes should take part in hit testing in some // cases, but currently this means that the order of hit test results isn't as expected for // some WPT tests. This needs more investigation. - if matches!( - pseudo, - Some(PseudoElement::ServoAnonymousBox) | - Some(PseudoElement::ServoAnonymousTable) | - Some(PseudoElement::ServoAnonymousTableCell) | - Some(PseudoElement::ServoAnonymousTableRow) - ) { + if info.is_anonymous() { return Self::anonymous(); } @@ -198,6 +210,12 @@ pub(super) trait TraversalHandler<'dom> { /// Notify the handler that we have finished a `display: contents` element. fn leave_display_contents(&mut self) {} + + /// Returns whether the pseudo element box should be cleared before traversed + /// by this handler. + fn need_clear_pseudo_element_box(&self, _node: &ServoLayoutNode<'dom>) -> bool { + true + } } fn traverse_children_of<'dom>( @@ -246,7 +264,9 @@ fn traverse_element<'dom>( ) { // Clear any existing pseudo-element box slot, because markers are not handled like // `::before`` and `::after`. They are processed during box tree creation. - element.unset_pseudo_element_box(PseudoElement::Marker); + if handler.need_clear_pseudo_element_box(&element) { + element.unset_pseudo_element_box(PseudoElement::Marker); + } let replaced = ReplacedContents::for_element(element, context); let style = element.style(context.shared_context()); @@ -288,6 +308,14 @@ fn traverse_element<'dom>( handler.handle_element(&info, display, contents, box_slot); }, } + + // Currently, we do not support storing the restyle damage caused by their + // styles changes for pseudo-element and list-marker seperately, which is + // stored at the originating element's style data. Clearing restyle damage + // for the originating element at here for keeping consistent with current + // restyle damage detecting strategy: when the originating element's box can + // be kept unchanged, the pseudo-element and list-marker will be kept unchanged. + element.clear_restyle_damage(); } fn traverse_eager_pseudo_element<'dom>( @@ -299,7 +327,9 @@ fn traverse_eager_pseudo_element<'dom>( assert!(pseudo_element_type.is_eager()); // First clear any old contents from the node. - node.unset_pseudo_element_box(pseudo_element_type); + if handler.need_clear_pseudo_element_box(&node) { + node.unset_pseudo_element_box(pseudo_element_type); + } let Some(element) = node.to_threadsafe().as_element() else { return; diff --git a/components/layout/flexbox/mod.rs b/components/layout/flexbox/mod.rs index 7f4a869a944..595d853e316 100644 --- a/components/layout/flexbox/mod.rs +++ b/components/layout/flexbox/mod.rs @@ -142,6 +142,12 @@ impl FlexContainer { self.config = FlexContainerConfig::new(new_style); self.style = new_style.clone(); } + + pub(crate) fn invalidate_subtree_caches(&self) { + for flex_level_box in &self.children { + flex_level_box.borrow().invalidate_subtree_caches(); + } + } } #[derive(Debug, MallocSizeOf)] @@ -182,6 +188,17 @@ impl FlexLevelBox { } } + pub(crate) fn invalidate_subtree_caches(&self) { + match self { + FlexLevelBox::FlexItem(flex_item_box) => flex_item_box + .independent_formatting_context + .invalidate_subtree_caches(), + FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => { + positioned_box.borrow().context.invalidate_subtree_caches() + }, + } + } + pub(crate) fn fragments(&self) -> Vec { match self { FlexLevelBox::FlexItem(flex_item_box) => flex_item_box diff --git a/components/layout/flow/construct.rs b/components/layout/flow/construct.rs index cc3fe0e6f44..ca236f961b3 100644 --- a/components/layout/flow/construct.rs +++ b/components/layout/flow/construct.rs @@ -4,12 +4,16 @@ use std::borrow::Cow; use std::convert::TryFrom; +use std::mem; +use atomic_refcell::AtomicRef; use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use script::layout_dom::ServoLayoutNode; +use script_layout_interface::wrapper_traits::LayoutNode; use servo_arc::Arc; use style::properties::ComputedValues; use style::properties::longhands::list_style_position::computed_value::T as ListStylePosition; -use style::selector_parser::PseudoElement; +use style::selector_parser::{PseudoElement, RestyleDamage}; use style::str::char_is_whitespace; use super::OutsideMarker; @@ -56,9 +60,36 @@ impl BlockFormattingContext { contains_floats, } } + + pub(crate) fn repair( + &mut self, + context: &LayoutContext, + info: &NodeAndStyleInfo<'_>, + contents: NonReplacedContents, + propagated_data: PropagatedBoxTreeData, + is_list_item: bool, + ) { + self.contents + .repair(context, info, contents, propagated_data, is_list_item); + self.contains_floats = self.contents.contains_floats(); + } } -struct BlockLevelJob<'dom> { +enum BlockLevelJob<'dom> { + Copy(ArcRefCell), + Repair(BlockLevelRepairJob<'dom>), + Create(BlockLevelCreateJob<'dom>), +} + +struct BlockLevelRepairJob<'dom> { + info: NodeAndStyleInfo<'dom>, + repaired_box: ArcRefCell, + propagated_data: PropagatedBoxTreeData, + display_inside: DisplayInside, + contents: Contents, +} + +struct BlockLevelCreateJob<'dom> { info: NodeAndStyleInfo<'dom>, box_slot: BoxSlot<'dom>, propagated_data: PropagatedBoxTreeData, @@ -104,6 +135,20 @@ enum IntermediateBlockContainer { }, } +/// Some childern node of a given DOM node do not have `REBUILD_BOX` restyle damage. +/// Thus, their boxes do not need to be rebuilt and can be retrieved from the +/// previously built children boxes of the given DOM node's repaired block container. +/// +/// This helper struct used to try to find the matched boxes in previously built +/// children boxes for a given children node. For block level node, this helper +/// is not very useful. It is more useful to find the inline formatting context +/// that the inline level node participates in, and the anonymous box that wraps +/// the inline formatting context. +struct PreviouslyBuiltChildernMatcher { + block_level_boxes: Vec>, + block_level_boxes_cursor: usize, +} + /// A builder for a block container. /// /// This builder starts from the first child of a given DOM node @@ -115,7 +160,9 @@ pub(crate) struct BlockContainerBuilder<'dom, 'style> { /// content designator, and the block container style. info: &'style NodeAndStyleInfo<'dom>, - /// The list of block-level boxes to be built for the final block container. + /// The list of block-level boxes to be repaired or copied from previous + /// block container building result, or newly built, for the final block + /// container. /// /// Contains all the block-level jobs we found traversing the tree /// so far, if this is empty at the end of the traversal and the ongoing @@ -156,6 +203,10 @@ pub(crate) struct BlockContainerBuilder<'dom, 'style> { /// ancestors that have been processed. This `Vec` allows passing those into new /// [`InlineFormattingContext`]s that we create. display_contents_shared_styles: Vec, + + /// The [`PreviouslyBuiltChildrenMatcher`] if we need to repair the current block + /// container, otherwise None. + previously_built_children_matcher: Option, } impl BlockContainer { @@ -166,26 +217,74 @@ impl BlockContainer { propagated_data: PropagatedBoxTreeData, is_list_item: bool, ) -> BlockContainer { - let mut builder = BlockContainerBuilder::new(context, info, propagated_data); + let builder = BlockContainerBuilder::new(context, info, propagated_data); + builder.build(contents, is_list_item) + } - if is_list_item { - if let Some((marker_info, marker_contents)) = crate::lists::make_marker(context, info) { - match marker_info.style.clone_list_style_position() { - ListStylePosition::Inside => { - builder.handle_list_item_marker_inside(&marker_info, info, marker_contents) - }, - ListStylePosition::Outside => builder.handle_list_item_marker_outside( - &marker_info, - info, - marker_contents, - info.style.clone(), - ), - } - } + pub fn repair( + &mut self, + context: &LayoutContext, + info: &NodeAndStyleInfo<'_>, + contents: NonReplacedContents, + propagated_data: PropagatedBoxTreeData, + is_list_item: bool, + ) { + let builder = BlockContainerBuilder::new_for_repair(context, info, propagated_data, self); + let _ = mem::replace(self, builder.build(contents, is_list_item)); + } +} + +impl PreviouslyBuiltChildernMatcher { + fn new(block_container: &mut BlockContainer) -> Self { + let previously_built_block_level_boxes = match block_container { + BlockContainer::BlockLevelBoxes(boxes) => mem::take(boxes), + BlockContainer::InlineFormattingContext(_) => vec![], + }; + + Self { + block_level_boxes: previously_built_block_level_boxes, + block_level_boxes_cursor: 0, + } + } + + fn match_and_advance( + &mut self, + info: &NodeAndStyleInfo<'_>, + ) -> Option> { + if info.is_anonymous() { + return self.find_first_anonymous_box_and_advance(); } - contents.traverse(context, info, &mut builder); - builder.finish() + let data = info.node.layout_data_mut(); + let layout_box = data.for_pseudo(info.pseudo_element_type); + + let previously_bound_box = match &*AtomicRef::filter_map(layout_box, Option::as_ref)? { + LayoutBox::BlockLevel(block_level_box) => block_level_box.clone(), + _ => return None, + }; + + // Skip the mismatch boxes, which were produced by the removed node or + // the node has `REBUILD_BOX` restyle damage, to find the box produced by + // current given node. + loop { + let curr = self.next()?; + if ArcRefCell::ptr_eq(&curr, &previously_bound_box) { + return Some(curr); + } + } + } + + fn find_first_anonymous_box_and_advance(&mut self) -> Option> { + unreachable!("Unexpected situations, and unimplemented"); + } + + fn next(&mut self) -> Option> { + if self.block_level_boxes_cursor < self.block_level_boxes.len() { + self.block_level_boxes_cursor += 1; + return Some(self.block_level_boxes[self.block_level_boxes_cursor - 1].clone()); + } + + None } } @@ -205,9 +304,48 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> { anonymous_table_content: Vec::new(), inline_formatting_context_builder: None, display_contents_shared_styles: Vec::new(), + previously_built_children_matcher: None, } } + fn new_for_repair( + context: &'style LayoutContext, + info: &'style NodeAndStyleInfo<'dom>, + propagated_data: PropagatedBoxTreeData, + repaired_block_container: &mut BlockContainer, + ) -> Self { + let mut builder = Self::new(context, info, propagated_data); + builder.previously_built_children_matcher = Some(PreviouslyBuiltChildernMatcher::new( + repaired_block_container, + )); + builder + } + + fn build(mut self, contents: NonReplacedContents, is_list_item: bool) -> BlockContainer { + if is_list_item { + if let Some((marker_info, marker_contents)) = + crate::lists::make_marker(self.context, self.info) + { + match marker_info.style.clone_list_style_position() { + ListStylePosition::Inside => self.handle_list_item_marker_inside( + &marker_info, + self.info, + marker_contents, + ), + ListStylePosition::Outside => self.handle_list_item_marker_outside( + &marker_info, + self.info, + marker_contents, + self.info.style.clone(), + ), + } + } + } + + contents.traverse(self.context, self.info, &mut self); + self.finish() + } + fn currently_processing_inline_box(&self) -> bool { self.inline_formatting_context_builder .as_ref() @@ -300,12 +438,13 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> { self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); } - self.block_level_boxes.push(BlockLevelJob { - info: table_info, - box_slot: BoxSlot::dummy(), - kind: BlockLevelCreator::AnonymousTable { table_block }, - propagated_data: self.propagated_data, - }); + self.block_level_boxes + .push(BlockLevelJob::Create(BlockLevelCreateJob { + info: table_info, + box_slot: BoxSlot::dummy(), + kind: BlockLevelCreator::AnonymousTable { table_block }, + propagated_data: self.propagated_data, + })); } // If the last element in the anonymous table content is whitespace, that @@ -400,9 +539,34 @@ impl<'dom> TraversalHandler<'dom> for BlockContainerBuilder<'dom, '_> { builder.leave_display_contents(); } } + + fn need_clear_pseudo_element_box(&self, node: &ServoLayoutNode<'dom>) -> bool { + let damage = node.style_data().unwrap().element_data.borrow().damage; + if damage.contains(RestyleDamage::REBUILD_BOX) { + return true; + } + + false + } } impl<'dom> BlockContainerBuilder<'dom, '_> { + fn try_reuse_block_level_box( + &mut self, + info: &NodeAndStyleInfo<'dom>, + ) -> Option> { + let previously_built_children_matcher = self.previously_built_children_matcher.as_mut()?; + + if info + .get_restyle_damage() + .contains(RestyleDamage::REBUILD_BOX) + { + return None; + } + + previously_built_children_matcher.match_and_advance(info) + } + fn handle_list_item_marker_inside( &mut self, marker_info: &NodeAndStyleInfo<'dom>, @@ -435,6 +599,57 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { container_info: &NodeAndStyleInfo<'dom>, contents: Vec, list_item_style: Arc, + ) { + // TODO: We do not currently support saving box slots for ::marker pseudo-elements + // that are part nested in ::before and ::after pseudo elements. For now, just + // always create new box for it. + if container_info.pseudo_element_type.is_some() { + self.create_list_item_marker_outside( + marker_info, + container_info, + contents, + list_item_style, + ); + return; + } + + let Some(reused_block_level_box) = self.try_reuse_block_level_box(marker_info) else { + self.create_list_item_marker_outside( + marker_info, + container_info, + contents, + list_item_style, + ); + return; + }; + + if marker_info + .get_restyle_damage() + .contains(RestyleDamage::REPAIR_BOX) + { + self.block_level_boxes + .push(BlockLevelJob::Repair(BlockLevelRepairJob { + info: marker_info.clone(), + repaired_box: reused_block_level_box, + propagated_data: self.propagated_data, + contents: NonReplacedContents::OfPseudoElement(contents).into(), + display_inside: DisplayInside::Flow { + is_list_item: false, + }, + })); + return; + } + + self.block_level_boxes + .push(BlockLevelJob::Copy(reused_block_level_box)); + } + + fn create_list_item_marker_outside( + &mut self, + marker_info: &NodeAndStyleInfo<'dom>, + container_info: &NodeAndStyleInfo<'dom>, + contents: Vec, + list_item_style: Arc, ) { // TODO: We do not currently support saving box slots for ::marker pseudo-elements // that are part nested in ::before and ::after pseudo elements. For now, just @@ -446,15 +661,16 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { .pseudo_element_box_slot(PseudoElement::Marker), }; - self.block_level_boxes.push(BlockLevelJob { - info: marker_info.clone(), - box_slot, - kind: BlockLevelCreator::OutsideMarker { - contents, - list_item_style, - }, - propagated_data: self.propagated_data, - }); + self.block_level_boxes + .push(BlockLevelJob::Create(BlockLevelCreateJob { + info: marker_info.clone(), + box_slot, + kind: BlockLevelCreator::OutsideMarker { + contents, + list_item_style, + }, + propagated_data: self.propagated_data, + })); } fn handle_inline_level_element( @@ -464,12 +680,12 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { contents: Contents, box_slot: BoxSlot<'dom>, ) { + let propagated_data = self.propagated_data; let (DisplayInside::Flow { is_list_item }, false) = (display_inside, contents.is_replaced()) else { // If this inline element is an atomic, handle it and return. let context = self.context; - let propagated_data = self.propagated_data; let atomic = self.ensure_inline_formatting_context_builder().push_atomic( IndependentFormattingContext::construct( context, @@ -546,6 +762,39 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); } + if let Some(reused_block_level_box) = self.try_reuse_block_level_box(info) { + if info + .get_restyle_damage() + .contains(RestyleDamage::REPAIR_BOX) + { + self.block_level_boxes + .push(BlockLevelJob::Repair(BlockLevelRepairJob { + info: info.clone(), + repaired_box: reused_block_level_box, + propagated_data: self.propagated_data, + contents, + display_inside, + })); + } else { + self.block_level_boxes + .push(BlockLevelJob::Copy(reused_block_level_box)); + } + } else { + self.create_block_level_box(info, display_inside, contents, box_slot); + } + + // Any block also counts as the first line for the purposes of text indent. Even if + // they don't actually indent. + self.have_already_seen_first_line_for_text_indent = true; + } + + fn create_block_level_box( + &mut self, + info: &NodeAndStyleInfo<'dom>, + display_inside: DisplayInside, + contents: Contents, + box_slot: BoxSlot<'dom>, + ) { let propagated_data = self.propagated_data; let kind = match contents { Contents::NonReplaced(contents) => match display_inside { @@ -577,16 +826,13 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { } }, }; - self.block_level_boxes.push(BlockLevelJob { - info: info.clone(), - box_slot, - kind, - propagated_data, - }); - - // Any block also counts as the first line for the purposes of text indent. Even if - // they don't actually indent. - self.have_already_seen_first_line_for_text_indent = true; + self.block_level_boxes + .push(BlockLevelJob::Create(BlockLevelCreateJob { + info: info.clone(), + box_slot, + kind, + propagated_data, + })); } fn handle_absolutely_positioned_element( @@ -610,16 +856,37 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { } } + if let Some(reused_block_level_box) = self.try_reuse_block_level_box(info) { + if info + .get_restyle_damage() + .contains(RestyleDamage::REPAIR_BOX) + { + self.block_level_boxes + .push(BlockLevelJob::Repair(BlockLevelRepairJob { + info: info.clone(), + repaired_box: reused_block_level_box, + propagated_data: self.propagated_data, + contents, + display_inside, + })); + } else { + self.block_level_boxes + .push(BlockLevelJob::Copy(reused_block_level_box)); + } + return; + } + let kind = BlockLevelCreator::OutOfFlowAbsolutelyPositionedBox { contents, display_inside, }; - self.block_level_boxes.push(BlockLevelJob { - info: info.clone(), - box_slot, - kind, - propagated_data: self.propagated_data, - }); + self.block_level_boxes + .push(BlockLevelJob::Create(BlockLevelCreateJob { + info: info.clone(), + box_slot, + kind, + propagated_data: self.propagated_data, + })); } fn handle_float_element( @@ -643,16 +910,37 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { } } + if let Some(reused_block_level_box) = self.try_reuse_block_level_box(info) { + if info + .get_restyle_damage() + .contains(RestyleDamage::REPAIR_BOX) + { + self.block_level_boxes + .push(BlockLevelJob::Repair(BlockLevelRepairJob { + info: info.clone(), + repaired_box: reused_block_level_box, + propagated_data: self.propagated_data, + contents, + display_inside, + })); + } else { + self.block_level_boxes + .push(BlockLevelJob::Copy(reused_block_level_box)); + } + return; + } + let kind = BlockLevelCreator::OutOfFlowFloatBox { contents, display_inside, }; - self.block_level_boxes.push(BlockLevelJob { - info: info.clone(), - box_slot, - kind, - propagated_data: self.propagated_data, - }); + self.block_level_boxes + .push(BlockLevelJob::Create(BlockLevelCreateJob { + info: info.clone(), + box_slot, + kind, + propagated_data: self.propagated_data, + })); } fn push_block_level_job_for_inline_formatting_context( @@ -669,23 +957,118 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { }) .clone(); - self.block_level_boxes.push(BlockLevelJob { - info, - // FIXME(nox): We should be storing this somewhere. - box_slot: BoxSlot::dummy(), - kind: BlockLevelCreator::SameFormattingContextBlock( - IntermediateBlockContainer::InlineFormattingContext( - BlockContainer::InlineFormattingContext(inline_formatting_context), + self.block_level_boxes + .push(BlockLevelJob::Create(BlockLevelCreateJob { + info, + // FIXME(nox): We should be storing this somewhere. + box_slot: BoxSlot::dummy(), + kind: BlockLevelCreator::SameFormattingContextBlock( + IntermediateBlockContainer::InlineFormattingContext( + BlockContainer::InlineFormattingContext(inline_formatting_context), + ), ), - ), - propagated_data: self.propagated_data, - }); + propagated_data: self.propagated_data, + })); self.have_already_seen_first_line_for_text_indent = true; } } -impl BlockLevelJob<'_> { +impl BlockLevelBox { + pub(crate) fn repair( + &mut self, + context: &LayoutContext, + info: &NodeAndStyleInfo<'_>, + repaired_contents: Contents, + display_inside: DisplayInside, + propagated_data: PropagatedBoxTreeData, + ) { + match self { + BlockLevelBox::SameFormattingContextBlock { + base, + contents, + contains_floats, + } => match display_inside { + DisplayInside::Flow { is_list_item } | DisplayInside::FlowRoot { is_list_item } => { + contents.repair( + context, + info, + repaired_contents + .try_into() + .expect("Expect NonReplacedContents, but got ReplacedContents!"), + propagated_data, + is_list_item, + ); + // Currently, the incremental layout is not fully ready. When a box is preserved + // during incremental box tree update, its cached layout result is also preserved. + // So, we have to invalidate all caches here to ensure that the layout is recomputed + // correctly. + base.invalidate_all_caches(); + base.repair_style(&info.style); + *contains_floats = contents.contains_floats(); + }, + _ => unreachable!("Expect flow display inside"), + }, + BlockLevelBox::Independent(independent_formatting_context) => { + independent_formatting_context.repair( + context, + info, + repaired_contents, + display_inside, + propagated_data, + ) + }, + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => { + positioned_box.borrow_mut().context.repair( + context, + info, + repaired_contents, + display_inside, + PropagatedBoxTreeData::default(), + ) + }, + BlockLevelBox::OutOfFlowFloatBox(float_box) => float_box.contents.repair( + context, + info, + repaired_contents, + display_inside, + propagated_data, + ), + BlockLevelBox::OutsideMarker(marker) => { + marker.block_container.repair( + context, + info, + repaired_contents + .try_into() + .expect("Expect NonReplacedContents, but got ReplacedContents!"), + propagated_data, + false, + ); + marker.repair_style(context.shared_context(), &info.node, &info.style); + // Currently, the incremental layout is not fully ready. When a box is preserved + // during incremental box tree update, its cached layout result is also preserved. + // So, we have to invalidate all caches here to ensure that the layout is recomputed + // correctly. + marker.invalidate_all_caches(); + }, + }; + } +} + +impl BlockLevelRepairJob<'_> { + fn finish(self, context: &LayoutContext) -> ArcRefCell { + self.repaired_box.borrow_mut().repair( + context, + &self.info, + self.contents, + self.display_inside, + self.propagated_data, + ); + self.repaired_box + } +} + +impl BlockLevelCreateJob<'_> { fn finish(self, context: &LayoutContext) -> ArcRefCell { let info = &self.info; let block_level_box = match self.kind { @@ -758,6 +1141,19 @@ impl BlockLevelJob<'_> { } } +impl BlockLevelJob<'_> { + fn finish(self, context: &LayoutContext) -> ArcRefCell { + match self { + BlockLevelJob::Copy(copied_box) => { + copied_box.borrow().invalidate_subtree_caches(); + copied_box + }, + BlockLevelJob::Repair(repair_job) => repair_job.finish(context), + BlockLevelJob::Create(create_job) => create_job.finish(context), + } + } +} + impl IntermediateBlockContainer { fn finish(self, context: &LayoutContext, info: &NodeAndStyleInfo<'_>) -> BlockContainer { match self { diff --git a/components/layout/flow/inline/mod.rs b/components/layout/flow/inline/mod.rs index 6fd4a51a526..fa6180170ed 100644 --- a/components/layout/flow/inline/mod.rs +++ b/components/layout/flow/inline/mod.rs @@ -278,6 +278,26 @@ impl InlineItem { } } + pub(crate) fn invalidate_subtree_caches(&self) { + match self { + InlineItem::StartInlineBox(inline_box) => { + inline_box.borrow().base.invalidate_all_caches() + }, + InlineItem::EndInlineBox | InlineItem::TextRun(..) => {}, + InlineItem::OutOfFlowAbsolutelyPositionedBox(positioned_box, ..) => { + positioned_box.borrow().context.invalidate_subtree_caches(); + }, + InlineItem::OutOfFlowFloatBox(float_box) => { + float_box.borrow().contents.invalidate_subtree_caches() + }, + InlineItem::Atomic(independent_formatting_context, ..) => { + independent_formatting_context + .borrow() + .invalidate_subtree_caches(); + }, + } + } + pub(crate) fn fragments(&self) -> Vec { match self { InlineItem::StartInlineBox(inline_box) => inline_box.borrow().base.fragments(), @@ -1713,6 +1733,12 @@ impl InlineFormattingContext { *self.shared_inline_styles.selected.borrow_mut() = node.to_threadsafe().selected_style(); } + pub(crate) fn invalidate_subtree_caches(&self) { + for inline_item in &self.inline_items { + inline_item.borrow().invalidate_subtree_caches(); + } + } + pub(super) fn layout( &self, layout_context: &LayoutContext, diff --git a/components/layout/flow/mod.rs b/components/layout/flow/mod.rs index 4776b65771c..a5f3807a4e7 100644 --- a/components/layout/flow/mod.rs +++ b/components/layout/flow/mod.rs @@ -87,6 +87,19 @@ impl BlockContainer { }, } } + + pub(crate) fn invalidate_subtree_caches(&self) { + match self { + BlockContainer::BlockLevelBoxes(block_level_boxes) => { + for block_level_box in block_level_boxes { + block_level_box.borrow().invalidate_subtree_caches(); + } + }, + BlockContainer::InlineFormattingContext(inline_formating_context) => { + inline_formating_context.invalidate_subtree_caches(); + }, + } + } } #[derive(Debug, MallocSizeOf)] @@ -138,6 +151,25 @@ impl BlockLevelBox { self.with_base(LayoutBoxBase::invalidate_cached_fragment); } + pub(crate) fn invalidate_subtree_caches(&self) { + match self { + BlockLevelBox::Independent(independent_formatting_context) => { + independent_formatting_context.invalidate_subtree_caches() + }, + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => { + positioned_box.borrow().context.invalidate_subtree_caches() + }, + BlockLevelBox::OutOfFlowFloatBox(float_box) => { + float_box.contents.invalidate_subtree_caches() + }, + BlockLevelBox::OutsideMarker(outside_marker) => outside_marker.invalidate_all_caches(), + BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => { + base.invalidate_all_caches(); + contents.invalidate_subtree_caches(); + }, + } + } + pub(crate) fn fragments(&self) -> Vec { self.with_base(LayoutBoxBase::fragments) } @@ -427,6 +459,10 @@ impl OutsideMarker { self.list_item_style = node.style(context); self.base.repair_style(new_style); } + + fn invalidate_all_caches(&self) { + self.base.invalidate_all_caches(); + } } impl BlockFormattingContext { diff --git a/components/layout/flow/root.rs b/components/layout/flow/root.rs index fe98dbc3156..3ea062e6436 100644 --- a/components/layout/flow/root.rs +++ b/components/layout/flow/root.rs @@ -12,10 +12,10 @@ use script::layout_dom::ServoLayoutNode; use script_layout_interface::wrapper_traits::{ LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, }; -use script_layout_interface::{LayoutElementType, LayoutNodeType}; 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; @@ -32,8 +32,8 @@ use crate::fragment_tree::FragmentTree; use crate::geom::{LogicalVec2, PhysicalSize}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::replaced::ReplacedContents; -use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside}; -use crate::taffy::{TaffyItemBox, TaffyItemBoxInner}; +use crate::style_ext::{Display, DisplayInside}; +use crate::taffy::TaffyItemBoxInner; use crate::{DefiniteContainingBlock, PropagatedBoxTreeData}; #[derive(MallocSizeOf)] @@ -109,185 +109,29 @@ impl BoxTree { /// 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. The only valid candidates for now are absolutely - /// positioned boxes which don't change their outside display mode (i.e. it - /// will not attempt to update from an absolutely positioned inline element - /// which became an absolutely positioned block element). The value `true` - /// is returned if an incremental update could be done, and `false` - /// otherwise. + /// 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 pain points that need to be taken care of to extend - /// the set of valid candidates: - /// * it is not obvious how to incrementally check whether a block - /// formatting context still contains floats or not; - /// * the propagation of text decorations towards node descendants is - /// hard to do incrementally with our current representation of boxes - /// * 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), - AbsolutelyPositionedInlineLevelBox(ArcRefCell, usize), - AbsolutelyPositionedFlexLevelBox(ArcRefCell), - AbsolutelyPositionedTaffyLevelBox(ArcRefCell), - } - - fn update_point( - node: ServoLayoutNode<'_>, - ) -> Option<(Arc, 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; - } - - true + /// 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() } } @@ -295,6 +139,8 @@ fn construct_for_root_element( context: &LayoutContext, root_element: ServoLayoutNode<'_>, ) -> Vec> { + root_element.clear_restyle_damage(); + let info = NodeAndStyleInfo::new(root_element, root_element.style(context.shared_context())); let box_style = info.style.get_box(); @@ -400,3 +246,201 @@ impl BoxTree { ) } } + +struct BoxTreeUpdateHelper<'style, 'dom> { + context: &'style LayoutContext<'style>, + dirty_node: ServoLayoutNode<'dom>, + primary_style: Option>, + display_inside: Option, +} + +enum UpdatePoint { + DoNotNeedUpdateTree, + RootElementPrimaryBox(ArcRefCell), + AbsolutelyPositionedBox(ArcRefCell), +} + +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 { + 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(), + ); + }, + } + } +} diff --git a/components/layout/formatting_contexts.rs b/components/layout/formatting_contexts.rs index 2b242c00361..1c6780408c2 100644 --- a/components/layout/formatting_contexts.rs +++ b/components/layout/formatting_contexts.rs @@ -2,6 +2,8 @@ * 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 std::mem; + use app_units::Au; use malloc_size_of_derive::MallocSizeOf; use script::layout_dom::{ServoLayoutElement, ServoLayoutNode}; @@ -11,7 +13,7 @@ use style::properties::ComputedValues; use style::selector_parser::PseudoElement; use crate::context::LayoutContext; -use crate::dom_traversal::{Contents, NodeAndStyleInfo}; +use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents}; use crate::flexbox::FlexContainer; use crate::flow::BlockFormattingContext; use crate::fragment_tree::{BaseFragmentInfo, FragmentFlags}; @@ -143,6 +145,39 @@ impl IndependentFormattingContext { } } + pub fn repair( + &mut self, + context: &LayoutContext, + node_and_style_info: &NodeAndStyleInfo, + contents: Contents, + display_inside: DisplayInside, + propagated_data: PropagatedBoxTreeData, + ) { + match &mut self.contents { + IndependentFormattingContextContents::NonReplaced( + independent_non_replaced_contents, + ) => { + independent_non_replaced_contents.repair( + context, + node_and_style_info, + contents + .try_into() + .expect("Expect NonReplacedContents, but got ReplacedContents!"), + display_inside, + propagated_data, + ); + }, + IndependentFormattingContextContents::Replaced(..) => {}, + } + + // Currently, the incremental layout is not fully ready. When a box is preserved + // during incremental box tree update, its cached layout result is also preserved. + // So, we have to invalidate all caches here to ensure that the layout is recomputed + // correctly. + self.base.invalidate_all_caches(); + self.base.repair_style(&node_and_style_info.style); + } + pub fn is_replaced(&self) -> bool { matches!( self.contents, @@ -234,6 +269,16 @@ impl IndependentFormattingContext { IndependentFormattingContextContents::Replaced(..) => {}, } } + + pub(crate) fn invalidate_subtree_caches(&self) { + self.base.invalidate_all_caches(); + match &self.contents { + IndependentFormattingContextContents::NonReplaced(content) => { + content.invalidate_subtree_caches(); + }, + IndependentFormattingContextContents::Replaced(..) => {}, + } + } } impl IndependentNonReplacedContents { @@ -376,6 +421,86 @@ impl IndependentNonReplacedContents { IndependentNonReplacedContents::Table(table) => table.repair_style(context, new_style), } } + + fn repair( + &mut self, + context: &LayoutContext, + node_and_style_info: &NodeAndStyleInfo, + non_replaced_contents: NonReplacedContents, + display_inside: DisplayInside, + propagated_data: PropagatedBoxTreeData, + ) { + match self { + IndependentNonReplacedContents::Flow(block_formatting_context) => { + match display_inside { + DisplayInside::Flow { is_list_item } | + DisplayInside::FlowRoot { is_list_item } => { + block_formatting_context.repair( + context, + node_and_style_info, + non_replaced_contents, + propagated_data, + is_list_item, + ); + }, + _ => unreachable!("Expect flow display inside mode"), + } + }, + IndependentNonReplacedContents::Flex(old_flex_container) => { + let new_flex_container = FlexContainer::construct( + context, + node_and_style_info, + non_replaced_contents, + propagated_data, + ); + let _ = mem::replace(old_flex_container, new_flex_container); + }, + IndependentNonReplacedContents::Grid(old_taffy_container) => { + let new_taffy_container = TaffyContainer::construct( + context, + node_and_style_info, + non_replaced_contents, + propagated_data, + ); + let _ = mem::replace(old_taffy_container, new_taffy_container); + }, + IndependentNonReplacedContents::Table(old_table) => { + let table_grid_style = context + .shared_context() + .stylist + .style_for_anonymous::( + &context.shared_context().guards, + &PseudoElement::ServoTableGrid, + &node_and_style_info.style, + ); + let new_table = Table::construct( + context, + node_and_style_info, + table_grid_style, + non_replaced_contents, + propagated_data, + ); + let _ = mem::replace(old_table, new_table); + }, + } + } + + fn invalidate_subtree_caches(&self) { + match self { + IndependentNonReplacedContents::Flow(block_formatting_context) => { + block_formatting_context + .contents + .invalidate_subtree_caches(); + }, + IndependentNonReplacedContents::Flex(flex_container) => { + flex_container.invalidate_subtree_caches() + }, + IndependentNonReplacedContents::Grid(taffy_container) => { + taffy_container.invalidate_subtree_caches() + }, + IndependentNonReplacedContents::Table(table) => table.invalidate_subtree_caches(), + } + } } impl ComputeInlineContentSizes for IndependentNonReplacedContents { diff --git a/components/layout/layout_box_base.rs b/components/layout/layout_box_base.rs index 161aee0f9bb..282ce25285f 100644 --- a/components/layout/layout_box_base.rs +++ b/components/layout/layout_box_base.rs @@ -74,6 +74,12 @@ impl LayoutBoxBase { let _ = self.cached_layout_result.borrow_mut().take(); } + pub(crate) fn invalidate_all_caches(&self) { + let _ = self.cached_inline_content_size.borrow_mut().take(); + self.invalidate_cached_fragment(); + self.clear_fragments(); + } + pub(crate) fn fragments(&self) -> Vec { self.fragments.borrow().clone() } diff --git a/components/layout/layout_impl.rs b/components/layout/layout_impl.rs index 0fbc8395c35..e5ef08e1ef9 100644 --- a/components/layout/layout_impl.rs +++ b/components/layout/layout_impl.rs @@ -778,7 +778,7 @@ impl LayoutThread { compute_damage_and_repair_style(layout_context.shared_context(), root_node); if viewport_changed { damage = RestyleDamage::REBUILD_BOX; - } else if !damage.contains(RestyleDamage::REBUILD_BOX) { + } else if !damage.will_change_box_subtree() { layout_context.style_context.stylist.rule_tree().maybe_gc(); return damage; } diff --git a/components/layout/table/mod.rs b/components/layout/table/mod.rs index 78884c377e9..c43c3f91b87 100644 --- a/components/layout/table/mod.rs +++ b/components/layout/table/mod.rs @@ -207,6 +207,35 @@ impl Table { new_style, ); } + + pub(crate) fn invalidate_subtree_caches(&self) { + for caption in &self.captions { + caption.borrow().context.invalidate_subtree_caches(); + } + for column_group in &self.column_groups { + column_group.borrow().base.invalidate_all_caches(); + } + for row_group in &self.row_groups { + row_group.borrow().base.invalidate_all_caches(); + } + for column in &self.columns { + column.borrow().base.invalidate_all_caches(); + } + for row in &self.rows { + row.borrow().base.invalidate_all_caches(); + } + for slot_cell_row in &self.slots { + for slot in slot_cell_row { + match slot { + TableSlot::Cell(cell) => { + cell.borrow().base.invalidate_all_caches(); + cell.borrow().contents.contents.invalidate_subtree_caches(); + }, + TableSlot::Spanned(..) | TableSlot::Empty => {}, + } + } + } + } } type TableSlotCoordinates = Point2D; diff --git a/components/layout/taffy/mod.rs b/components/layout/taffy/mod.rs index 05fc09c5511..07c0ca5fcde 100644 --- a/components/layout/taffy/mod.rs +++ b/components/layout/taffy/mod.rs @@ -73,6 +73,12 @@ impl TaffyContainer { pub(crate) fn repair_style(&mut self, new_style: &Arc) { self.style = new_style.clone(); } + + pub(crate) fn invalidate_subtree_caches(&self) { + for taffy_level_box in &self.children { + taffy_level_box.borrow_mut().invalidate_subtree_caches(); + } + } } #[derive(MallocSizeOf)] @@ -138,6 +144,21 @@ impl TaffyItemBox { } } + pub(crate) fn invalidate_subtree_caches(&mut self) { + self.taffy_layout = Default::default(); + self.positioning_context = PositioningContext::default(); + self.child_fragments.clear(); + + match self.taffy_level_box { + TaffyItemBoxInner::InFlowBox(ref independent_formatting_context) => { + independent_formatting_context.invalidate_subtree_caches(); + }, + TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox(ref positioned_box) => { + positioned_box.borrow().context.invalidate_subtree_caches(); + }, + } + } + pub(crate) fn fragments(&self) -> Vec { match self.taffy_level_box { TaffyItemBoxInner::InFlowBox(ref independent_formatting_context) => { diff --git a/components/layout/traversal.rs b/components/layout/traversal.rs index 22faa4b1191..4cd6a362fb7 100644 --- a/components/layout/traversal.rs +++ b/components/layout/traversal.rs @@ -99,6 +99,17 @@ pub(crate) fn compute_damage_and_repair_style( compute_damage_and_repair_style_inner(context, node, RestyleDamage::empty()) } +fn need_repair_style_before_box_tree_update(damage: RestyleDamage) -> bool { + // Repair the style at the node's layout objects only when itself has + // restyle damage but it's boxes will be kept unchanged, otherwise, + // the style will be repaired during box tree update. + if damage.is_empty() || damage.will_change_box_subtree() { + return false; + } + + true +} + pub(crate) fn compute_damage_and_repair_style_inner( context: &SharedStyleContext, node: ServoLayoutNode<'_>, @@ -106,7 +117,6 @@ pub(crate) fn compute_damage_and_repair_style_inner( ) -> RestyleDamage { let original_damage; let damage; - { let mut element_data = node .style_data() @@ -114,28 +124,42 @@ pub(crate) fn compute_damage_and_repair_style_inner( .element_data .borrow_mut(); - original_damage = std::mem::take(&mut element_data.damage); + original_damage = element_data.damage; + // The damage that can cause the box subtree to change will be cleaned + // after incremental box tree update. + if !element_data.damage.will_change_box_subtree() { + let _ = std::mem::take(&mut element_data.damage); + }; + damage = original_damage | parent_restyle_damage; if let Some(ref style) = element_data.styles.primary { if style.get_box().display == Display::None { - return damage; + return damage.propagate_up_damage(); } } - } + }; + + let mut propagated_damage = damage.propagate_up_damage(); + let propagated_down_damage = damage.propagate_down_damage(); - let mut propagated_damage = damage; for child in iter_child_nodes(node) { if child.is_element() { - propagated_damage |= compute_damage_and_repair_style_inner(context, child, damage); + propagated_damage |= + compute_damage_and_repair_style_inner(context, child, propagated_down_damage); } } - if !propagated_damage.contains(RestyleDamage::REBUILD_BOX) && - !original_damage.contains(RestyleDamage::REBUILD_BOX) + if need_repair_style_before_box_tree_update(propagated_damage) && + need_repair_style_before_box_tree_update(original_damage) { node.repair_style(context); } + if propagated_damage.contains(RestyleDamage::REPAIR_BOX) { + let mut element_data = node.style_data().unwrap().element_data.borrow_mut(); + element_data.damage |= RestyleDamage::REPAIR_BOX; + } + propagated_damage } diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 68efa07537c..e583a616d7c 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -940,7 +940,7 @@ impl Document { // FIXME(emilio): This is very inefficient, ideally the flag above would // be enough and incremental layout could figure out from there. - node.dirty(NodeDamage::OtherNodeDamage); + node.dirty(NodeDamage::NodeContentOrHeritageDamaged); } /// Remove any existing association between the provided id and any elements in this document. diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 4db03cfb3b3..008acfe96de 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -360,8 +360,9 @@ impl Element { // NodeStyleDamaged, but I'm preserving existing behavior. restyle.hint.insert(RestyleHint::RESTYLE_SELF); - if damage == NodeDamage::OtherNodeDamage { - doc.note_node_with_dirty_descendants(self.upcast()); + if damage == NodeDamage::NodeContentOrHeritageDamaged { + restyle.damage = RestyleDamage::repair(); + } else if damage == NodeDamage::OtherNodeDamage { restyle.damage = RestyleDamage::reconstruct(); } } diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index ab814bc9108..cc6d73fe1ae 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -3933,9 +3933,17 @@ impl VirtualMethods for Node { /// A summary of the changes that happened to a node. #[derive(Clone, Copy, MallocSizeOf, PartialEq)] +// FIXME(joedow): The enum-variant-name-threshold's default is 3, so add the +// `NodeContentOrHeritageDamaged` variant leads to the lint being triggered, reference: +// https://rust-lang.github.io/rust-clippy/master/index.html#enum_variant_names +// +// Allow this lint temperarily, and its fix will be done in a follow-up PR. +#[allow(clippy::enum_variant_names)] pub(crate) enum NodeDamage { /// The node's `style` attribute changed. NodeStyleDamaged, + /// The node's content or heritage changed: children removed or added, text content changed. + NodeContentOrHeritageDamaged, /// Other parts of a node changed; attributes, text content, etc. OtherNodeDamage, }