| /* 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 layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode}; |
| use layout_api::{LayoutElementType, LayoutNodeType}; |
| use malloc_size_of_derive::MallocSizeOf; |
| use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode}; |
| use servo_arc::Arc; |
| use style::dom::{NodeInfo, TNode}; |
| use style::properties::ComputedValues; |
| use style::values::computed::Overflow; |
| use style::values::specified::box_::DisplayOutside; |
| use style_traits::CSSPixel; |
| |
| use crate::cell::ArcRefCell; |
| use crate::context::LayoutContext; |
| use crate::dom::{LayoutBox, NodeExt}; |
| use crate::dom_traversal::{Contents, NodeAndStyleInfo, NonReplacedContents}; |
| 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::{FragmentFlags, FragmentTree}; |
| use crate::geom::{LogicalVec2, PhysicalSize}; |
| use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; |
| use crate::replaced::ReplacedContents; |
| use crate::style_ext::{AxesOverflow, 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 { |
| #[servo_tracing::instrument(name = "Box Tree Construction", skip_all)] |
| pub(crate) fn construct(context: &LayoutContext, root_element: ServoLayoutNode<'_>) -> Self { |
| let root_element = root_element.to_threadsafe(); |
| |
| // 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.style_context); |
| |
| let mut viewport_overflow = AxesOverflow::from(&*root_style); |
| let mut element_propagating_overflow = root_element; |
| if viewport_overflow.x == Overflow::Visible && |
| viewport_overflow.y == Overflow::Visible && |
| !root_style.get_box().display.is_none() |
| { |
| for child in root_element.children() { |
| if !child |
| .as_element() |
| .is_some_and(|element| element.is_body_element_of_html_element_root()) |
| { |
| continue; |
| } |
| |
| let style = child.style(&context.style_context); |
| if !style.get_box().display.is_none() { |
| viewport_overflow = AxesOverflow::from(&*style); |
| element_propagating_overflow = child; |
| |
| break; |
| } |
| } |
| } |
| |
| 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); |
| |
| if let Some(layout_data) = element_propagating_overflow.inner_layout_data() { |
| if let Some(ref mut layout_box) = *layout_data.self_box.borrow_mut() { |
| layout_box.with_base_mut(|base| { |
| base.base_fragment_info |
| .flags |
| .insert(FragmentFlags::PROPAGATED_OVERFLOW_TO_VIEWPORT) |
| }); |
| } |
| } |
| |
| 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(crate) fn update( |
| context: &LayoutContext, |
| dirty_root_from_script: ServoLayoutNode<'_>, |
| ) -> bool { |
| let Some(box_tree_update) = IncrementalBoxTreeUpdate::find(dirty_root_from_script) else { |
| return false; |
| }; |
| box_tree_update.update_from_dirty_root(context); |
| true |
| } |
| } |
| |
| fn construct_for_root_element( |
| context: &LayoutContext, |
| root_element: ServoThreadSafeLayoutNode<'_>, |
| ) -> Vec<ArcRefCell<BlockLevelBox>> { |
| let info = NodeAndStyleInfo::new( |
| root_element, |
| root_element.style(&context.style_context), |
| root_element.take_restyle_damage(), |
| ); |
| 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 |
| .box_slot() |
| .set(LayoutBox::BlockLevel(root_box.clone())); |
| vec![root_box] |
| } |
| |
| impl BoxTree { |
| #[servo_tracing::instrument(name = "Fragment Tree Construction", skip_all)] |
| pub(crate) fn layout( |
| &self, |
| layout_context: &LayoutContext, |
| viewport: UntypedSize2D<Au>, |
| ) -> 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<Au, CSSPixel> = |
| 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(), |
| ); |
| |
| let mut root_fragments = independent_layout.fragments.into_iter().collect::<Vec<_>>(); |
| |
| // 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, |
| ) |
| } |
| } |
| |
| #[allow(clippy::enum_variant_names)] |
| enum DirtyRootBoxTreeNode { |
| AbsolutelyPositionedBlockLevelBox(ArcRefCell<BlockLevelBox>), |
| AbsolutelyPositionedInlineLevelBox(ArcRefCell<InlineItem>, usize), |
| AbsolutelyPositionedFlexLevelBox(ArcRefCell<FlexLevelBox>), |
| AbsolutelyPositionedTaffyLevelBox(ArcRefCell<TaffyItemBox>), |
| } |
| |
| struct IncrementalBoxTreeUpdate<'dom> { |
| node: ServoLayoutNode<'dom>, |
| box_tree_node: DirtyRootBoxTreeNode, |
| primary_style: Arc<ComputedValues>, |
| display_inside: DisplayInside, |
| } |
| |
| impl<'dom> IncrementalBoxTreeUpdate<'dom> { |
| fn find(dirty_root_from_script: ServoLayoutNode<'dom>) -> Option<Self> { |
| let mut maybe_dirty_root_node = Some(dirty_root_from_script); |
| while let Some(dirty_root_node) = maybe_dirty_root_node { |
| if let Some(dirty_root) = Self::new_if_valid(dirty_root_node) { |
| return Some(dirty_root); |
| } |
| |
| maybe_dirty_root_node = dirty_root_node.parent_node(); |
| } |
| |
| None |
| } |
| |
| fn new_if_valid(potential_dirty_root_node: ServoLayoutNode<'dom>) -> Option<Self> { |
| if !potential_dirty_root_node.is_element() { |
| return None; |
| } |
| |
| if potential_dirty_root_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 potential_thread_safe_dirty_root_node = potential_dirty_root_node.to_threadsafe(); |
| let element_data = potential_thread_safe_dirty_root_node |
| .style_data()? |
| .element_data |
| .borrow(); |
| if !element_data.styles.pseudos.is_empty() { |
| return None; |
| } |
| |
| let layout_data = NodeExt::inner_layout_data(&potential_thread_safe_dirty_root_node)?; |
| if !layout_data.pseudo_boxes.is_empty() { |
| 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 box_tree_node = |
| 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() => |
| { |
| // If the outer type of its original display changed from block to inline, |
| // a block-level abspos needs to be placed in an inline formatting context, |
| // see [`BlockContainerBuilder::handle_absolutely_positioned_element()`]. |
| if box_style.original_display.outside() == DisplayOutside::Inline { |
| return None; |
| } |
| DirtyRootBoxTreeNode::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; |
| }; |
| DirtyRootBoxTreeNode::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() => |
| { |
| DirtyRootBoxTreeNode::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() => |
| { |
| DirtyRootBoxTreeNode::AbsolutelyPositionedTaffyLevelBox( |
| taffy_level_box.clone(), |
| ) |
| }, |
| _ => return None, |
| } |
| }, |
| }; |
| |
| Some(Self { |
| node: potential_dirty_root_node, |
| box_tree_node, |
| primary_style: primary_style.clone(), |
| display_inside, |
| }) |
| } |
| |
| #[servo_tracing::instrument(name = "Box Tree Update From Dirty Root", skip_all)] |
| fn update_from_dirty_root(&self, context: &LayoutContext) { |
| let node = self.node.to_threadsafe(); |
| let contents = ReplacedContents::for_element(node, context) |
| .map_or_else(|| NonReplacedContents::OfElement.into(), Contents::Replaced); |
| |
| let info = |
| NodeAndStyleInfo::new(node, self.primary_style.clone(), node.take_restyle_damage()); |
| |
| let out_of_flow_absolutely_positioned_box = ArcRefCell::new( |
| AbsolutelyPositionedBox::construct(context, &info, self.display_inside, contents), |
| ); |
| match &self.box_tree_node { |
| DirtyRootBoxTreeNode::AbsolutelyPositionedBlockLevelBox(block_level_box) => { |
| *block_level_box.borrow_mut() = BlockLevelBox::OutOfFlowAbsolutelyPositionedBox( |
| out_of_flow_absolutely_positioned_box, |
| ); |
| }, |
| DirtyRootBoxTreeNode::AbsolutelyPositionedInlineLevelBox( |
| inline_level_box, |
| text_offset_index, |
| ) => { |
| *inline_level_box.borrow_mut() = InlineItem::OutOfFlowAbsolutelyPositionedBox( |
| out_of_flow_absolutely_positioned_box, |
| *text_offset_index, |
| ); |
| }, |
| DirtyRootBoxTreeNode::AbsolutelyPositionedFlexLevelBox(flex_level_box) => { |
| *flex_level_box.borrow_mut() = FlexLevelBox::OutOfFlowAbsolutelyPositionedBox( |
| out_of_flow_absolutely_positioned_box, |
| ); |
| }, |
| DirtyRootBoxTreeNode::AbsolutelyPositionedTaffyLevelBox(taffy_level_box) => { |
| taffy_level_box.borrow_mut().taffy_level_box = |
| TaffyItemBoxInner::OutOfFlowAbsolutelyPositionedBox( |
| out_of_flow_absolutely_positioned_box, |
| ); |
| }, |
| } |
| |
| let mut invalidate_start_point = self.node; |
| while let Some(parent_node) = invalidate_start_point.parent_node() { |
| // Box tree reconstruction doesn't need to involve these ancestors, so their |
| // damage isn't useful for us. |
| // |
| // TODO: This isn't going to be good enough for incremental fragment tree |
| // reconstruction, as fragment tree damage might extend further up the tree. |
| parent_node.to_threadsafe().take_restyle_damage(); |
| |
| invalidate_start_point = parent_node; |
| } |
| } |
| } |