/* 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 script_layout_interface::{LayoutElementType, LayoutNodeType}; use servo_arc::Arc; use style::dom::{NodeInfo, TNode}; use style::properties::ComputedValues; 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, PhysicalRect, PhysicalSize}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::replaced::ReplacedContents; use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside}; use crate::taffy::{TaffyItemBox, 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. 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. /// /// 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 } } fn construct_for_root_element( context: &LayoutContext, root_element: ServoLayoutNode<'_>, ) -> Vec> { 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, ); let scrollable_overflow = root_fragments .iter() .fold(PhysicalRect::zero(), |acc, child| { let child_overflow = child.scrollable_overflow_for_parent(); // https://drafts.csswg.org/css-overflow/#scrolling-direction // We want to clip scrollable overflow on box-start and inline-start // sides of the scroll container. // // FIXME(mrobinson, bug 25564): This should take into account writing // mode. let child_overflow = PhysicalRect::new( euclid::Point2D::zero(), euclid::Size2D::new( child_overflow.size.width + child_overflow.origin.x, child_overflow.size.height + child_overflow.origin.y, ), ); acc.union(&child_overflow) }); FragmentTree::new( layout_context, root_fragments, scrollable_overflow, physical_containing_block, self.viewport_scroll_sensitivity, ) } }