/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use app_units::Au; use atomic_refcell::AtomicRef; use compositing_traits::display_list::AxesScrollSensitivity; use euclid::Rect; use euclid::default::Size2D as UntypedSize2D; use malloc_size_of_derive::MallocSizeOf; use script::layout_dom::ServoLayoutNode; use script_layout_interface::wrapper_traits::{ LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, }; use servo_arc::Arc; use style::dom::{NodeInfo, TNode}; use style::properties::ComputedValues; use style::selector_parser::RestyleDamage; use style::values::computed::Overflow; use style_traits::CSSPixel; use crate::cell::ArcRefCell; use crate::context::LayoutContext; use crate::dom::{LayoutBox, NodeExt}; use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents, iter_child_nodes}; use crate::flexbox::FlexLevelBox; use crate::flow::float::FloatBox; use crate::flow::inline::InlineItem; use crate::flow::{BlockContainer, BlockFormattingContext, BlockLevelBox}; use crate::formatting_contexts::IndependentFormattingContext; use crate::fragment_tree::FragmentTree; use crate::geom::{LogicalVec2, PhysicalSize}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::replaced::ReplacedContents; use crate::style_ext::{Display, DisplayInside}; use crate::taffy::TaffyItemBoxInner; use crate::{DefiniteContainingBlock, PropagatedBoxTreeData}; #[derive(MallocSizeOf)] pub struct BoxTree { /// Contains typically exactly one block-level box, which was generated by the root element. /// There may be zero if that element has `display: none`. root: BlockFormattingContext, /// Whether or not the viewport should be sensitive to scrolling input events in two axes viewport_scroll_sensitivity: AxesScrollSensitivity, } impl BoxTree { pub fn construct(context: &LayoutContext, root_element: ServoLayoutNode<'_>) -> Self { let boxes = construct_for_root_element(context, root_element); // Zero box for `:root { display: none }`, one for the root element otherwise. assert!(boxes.len() <= 1); // From https://www.w3.org/TR/css-overflow-3/#overflow-propagation: // > UAs must apply the overflow-* values set on the root element to the viewport when the // > root element’s display value is not none. However, when the root element is an [HTML] // > html element (including XML syntax for HTML) whose overflow value is visible (in both // > axes), and that element has as a child a body element whose display value is also not // > none, user agents must instead apply the overflow-* values of the first such child // > element to the viewport. The element from which the value is propagated must then have a // > used overflow value of visible. let root_style = root_element.style(context.shared_context()); let mut viewport_overflow_x = root_style.clone_overflow_x(); let mut viewport_overflow_y = root_style.clone_overflow_y(); if viewport_overflow_x == Overflow::Visible && viewport_overflow_y == Overflow::Visible && !root_style.get_box().display.is_none() { for child in iter_child_nodes(root_element) { if !child .to_threadsafe() .as_element() .is_some_and(|element| element.is_body_element_of_html_element_root()) { continue; } let style = child.style(context.shared_context()); if !style.get_box().display.is_none() { viewport_overflow_x = style.clone_overflow_x(); viewport_overflow_y = style.clone_overflow_y(); break; } } } let contents = BlockContainer::BlockLevelBoxes(boxes); let contains_floats = contents.contains_floats(); Self { root: BlockFormattingContext { contents, contains_floats, }, // From https://www.w3.org/TR/css-overflow-3/#overflow-propagation: // > If visible is applied to the viewport, it must be interpreted as auto. // > If clip is applied to the viewport, it must be interpreted as hidden. viewport_scroll_sensitivity: AxesScrollSensitivity { x: viewport_overflow_x.to_scrollable().into(), y: viewport_overflow_y.to_scrollable().into(), }, } } /// This method attempts to incrementally update the box tree from an /// arbitrary node that is not necessarily the document's root element. /// /// If the node is not a valid candidate for incremental update, the method /// loops over its parent. Currently, the only valid candidates for now /// are absolutely positioned boxes and their originating nodes must have /// restyle damage that is `REPAIR_BOX` rather than `REBUILD_BOX`. In the /// future, this may be extended to other types of boxes if the new restyle /// damage types for incremental layout is ready. /// /// There are various reasons why only absolutely positioned boxes can be /// selected as the update point: /// * it needs a bit of trick to incrementally update the `contains_floats` /// for a block formatting context and `SameFormattingContext`, which make /// the incremental update less eligible. /// * the propagation of box tree data towards node descendants is hard to do /// incrementally with our current representation of boxes. To support /// incremental update, we have to store it at the `LayoutBoxBase`. /// * We have to clear all layout caches for ancestors of the update point /// when the incremental layout is not fully ready. However, it is really hard /// to do that incrementally because a box does not hold a reference to its /// parent box with current representation of boxes. The anonymous ancestor /// boxes can not be visited. Thus, select the absolutely positioned boxes /// as the update point, this is not a problem. pub fn update(context: &LayoutContext, dirty_node: ServoLayoutNode<'_>) -> bool { let mut helper = BoxTreeUpdateHelper::new(context, dirty_node); helper.try_update_box_tree() } } fn construct_for_root_element( context: &LayoutContext, root_element: ServoLayoutNode<'_>, ) -> Vec> { root_element.clear_restyle_damage(); let info = NodeAndStyleInfo::new(root_element, root_element.style(context.shared_context())); let box_style = info.style.get_box(); let display_inside = match Display::from(box_style.display) { Display::None => { root_element.unset_all_boxes(); return Vec::new(); }, Display::Contents => { // Unreachable because the style crate adjusts the computed values: // https://drafts.csswg.org/css-display-3/#transformations // “'display' of 'contents' computes to 'block' on the root element” unreachable!() }, // The root element is blockified, ignore DisplayOutside Display::GeneratingBox(display_generating_box) => display_generating_box.display_inside(), }; let contents = ReplacedContents::for_element(root_element, context) .map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced); let propagated_data = PropagatedBoxTreeData::default(); let root_box = if box_style.position.is_absolutely_positioned() { BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(ArcRefCell::new( AbsolutelyPositionedBox::construct(context, &info, display_inside, contents), )) } else if box_style.float.is_floating() { BlockLevelBox::OutOfFlowFloatBox(FloatBox::construct( context, &info, display_inside, contents, propagated_data, )) } else { BlockLevelBox::Independent(IndependentFormattingContext::construct( context, &info, display_inside, contents, propagated_data, )) }; let root_box = ArcRefCell::new(root_box); root_element .element_box_slot() .set(LayoutBox::BlockLevel(root_box.clone())); vec![root_box] } impl BoxTree { pub fn layout( &self, layout_context: &LayoutContext, viewport: UntypedSize2D, ) -> FragmentTree { let style = layout_context .style_context .stylist .device() .default_computed_values(); // FIXME: use the document’s mode: // https://drafts.csswg.org/css-writing-modes/#principal-flow let physical_containing_block: Rect = PhysicalSize::from_untyped(viewport).into(); let initial_containing_block = DefiniteContainingBlock { size: LogicalVec2 { inline: physical_containing_block.size.width, block: physical_containing_block.size.height, }, style, }; let mut positioning_context = PositioningContext::default(); let independent_layout = self.root.layout( layout_context, &mut positioning_context, &(&initial_containing_block).into(), false, /* depends_on_block_constraints */ ); let mut root_fragments = independent_layout.fragments.into_iter().collect::>(); // Zero box for `:root { display: none }`, one for the root element otherwise. assert!(root_fragments.len() <= 1); // There may be more fragments at the top-level // (for positioned boxes whose containing is the initial containing block) // but only if there was one fragment for the root element. positioning_context.layout_initial_containing_block_children( layout_context, &initial_containing_block, &mut root_fragments, ); FragmentTree::new( layout_context, root_fragments, physical_containing_block, self.viewport_scroll_sensitivity, ) } } struct BoxTreeUpdateHelper<'style, 'dom> { context: &'style LayoutContext<'style>, dirty_node: ServoLayoutNode<'dom>, primary_style: Option>, 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(), ); }, } } }