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>
This commit is contained in:
coding-joedow 2025-05-23 13:12:25 +08:00 committed by sharpshooter_pt
parent 8d086b9fe5
commit 94a662193e
19 changed files with 1068 additions and 295 deletions

24
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -21,6 +21,10 @@ impl<T> ArcRefCell<T> {
value: Arc::new(AtomicRefCell::new(value)),
}
}
pub fn ptr_eq(this: &Self, other: &Self) -> bool {
Arc::ptr_eq(&this.value, &other.value)
}
}
impl<T> Clone for ArcRefCell<T> {

View file

@ -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<Option<LayoutBox>>) -> 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();
}
}
}

View file

@ -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<Range<ByteIndex>> {
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.
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.
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;

View file

@ -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<Fragment> {
match self {
FlexLevelBox::FlexItem(flex_item_box) => flex_item_box

View file

@ -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<BlockLevelBox>),
Repair(BlockLevelRepairJob<'dom>),
Create(BlockLevelCreateJob<'dom>),
}
struct BlockLevelRepairJob<'dom> {
info: NodeAndStyleInfo<'dom>,
repaired_box: ArcRefCell<BlockLevelBox>,
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<ArcRefCell<BlockLevelBox>>,
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<SharedInlineStyles>,
/// The [`PreviouslyBuiltChildrenMatcher`] if we need to repair the current block
/// container, otherwise None.
previously_built_children_matcher: Option<PreviouslyBuiltChildernMatcher>,
}
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<ArcRefCell<BlockLevelBox>> {
if info.is_anonymous() {
return self.find_first_anonymous_box_and_advance();
}
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);
}
}
}
contents.traverse(context, info, &mut builder);
builder.finish()
fn find_first_anonymous_box_and_advance(&mut self) -> Option<ArcRefCell<BlockLevelBox>> {
unreachable!("Unexpected situations, and unimplemented");
}
fn next(&mut self) -> Option<ArcRefCell<BlockLevelBox>> {
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 {
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<ArcRefCell<BlockLevelBox>> {
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<crate::dom_traversal::PseudoElementContentItem>,
list_item_style: Arc<ComputedValues>,
) {
// 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<crate::dom_traversal::PseudoElementContentItem>,
list_item_style: Arc<ComputedValues>,
) {
// 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,7 +661,8 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
.pseudo_element_box_slot(PseudoElement::Marker),
};
self.block_level_boxes.push(BlockLevelJob {
self.block_level_boxes
.push(BlockLevelJob::Create(BlockLevelCreateJob {
info: marker_info.clone(),
box_slot,
kind: BlockLevelCreator::OutsideMarker {
@ -454,7 +670,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
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 {
self.block_level_boxes
.push(BlockLevelJob::Create(BlockLevelCreateJob {
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;
}));
}
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 {
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 {
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,7 +957,8 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
})
.clone();
self.block_level_boxes.push(BlockLevelJob {
self.block_level_boxes
.push(BlockLevelJob::Create(BlockLevelCreateJob {
info,
// FIXME(nox): We should be storing this somewhere.
box_slot: BoxSlot::dummy(),
@ -679,13 +968,107 @@ impl<'dom> BlockContainerBuilder<'dom, '_> {
),
),
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<BlockLevelBox> {
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<BlockLevelBox> {
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<BlockLevelBox> {
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 {

View file

@ -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<Fragment> {
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,

View file

@ -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<Fragment> {
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 {

View file

@ -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<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;
}
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<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();
@ -400,3 +246,201 @@ impl BoxTree {
)
}
}
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(),
);
},
}
}
}

View file

@ -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::<ServoLayoutElement>(
&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 {

View file

@ -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<Fragment> {
self.fragments.borrow().clone()
}

View file

@ -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;
}

View file

@ -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<usize, UnknownUnit>;

View file

@ -73,6 +73,12 @@ impl TaffyContainer {
pub(crate) fn repair_style(&mut self, new_style: &Arc<ComputedValues>) {
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<Fragment> {
match self.taffy_level_box {
TaffyItemBoxInner::InFlowBox(ref independent_formatting_context) => {

View file

@ -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
}

View file

@ -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.

View file

@ -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();
}
}

View file

@ -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,
}