/* 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 script_layout_interface::wrapper_traits::{ LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, }; use script_layout_interface::{LayoutElementType, LayoutNodeType}; use servo_arc::Arc; use style::dom::OpaqueNode; 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, PhysicalPoint, PhysicalRect, PhysicalSize}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::replaced::ReplacedContents; use crate::style_ext::{ComputedValuesExt, Display, DisplayGeneratingBox, DisplayInside}; use crate::taffy::{TaffyItemBox, TaffyItemBoxInner}; use crate::{DefiniteContainingBlock, PropagatedBoxTreeData}; 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, /// canvas_background: CanvasBackground, /// Whether or not the viewport should be sensitive to scrolling input events in two axes viewport_scroll_sensitivity: AxesScrollSensitivity, } impl BoxTree { pub fn construct<'dom, Node>(context: &LayoutContext, root_element: Node) -> Self where Node: 'dom + Copy + LayoutNode<'dom> + Send + Sync, { 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); 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); 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, }, canvas_background: CanvasBackground::for_root_element(context, root_element), // 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<'dom, Node>(context: &LayoutContext, mut dirty_node: Node) -> bool where Node: 'dom + Copy + LayoutNode<'dom> + Send + Sync, { #[allow(clippy::enum_variant_names)] enum UpdatePoint { AbsolutelyPositionedBlockLevelBox(ArcRefCell), AbsolutelyPositionedInlineLevelBox(ArcRefCell, usize), AbsolutelyPositionedFlexLevelBox(ArcRefCell), AbsolutelyPositionedTaffyLevelBox(ArcRefCell), } fn update_point<'dom, Node>( node: Node, ) -> Option<(Arc, DisplayInside, UpdatePoint)> where Node: NodeExt<'dom>, { 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 = node.layout_data()?; 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_box) => match &*inline_level_box.borrow() { InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index) if box_style.position.is_absolutely_positioned() => { UpdatePoint::AbsolutelyPositionedInlineLevelBox( inline_level_box.clone(), *text_offset_index, ) }, _ => return None, }, 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::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<'dom>( context: &LayoutContext, root_element: impl NodeExt<'dom>, ) -> Vec> { let info = NodeAndStyleInfo::new(root_element, root_element.style(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().union(&info.style); 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: euclid::Size2D, ) -> 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 = PhysicalRect::new( PhysicalPoint::zero(), PhysicalSize::new( Au::from_f32_px(viewport.width), Au::from_f32_px(viewport.height), ), ); 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::new_for_containing_block_for_all_descendants(); 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(); // 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 { root_fragments, scrollable_overflow, initial_containing_block: physical_containing_block, canvas_background: self.canvas_background.clone(), viewport_scroll_sensitivity: self.viewport_scroll_sensitivity, } } } /// #[derive(Clone)] pub struct CanvasBackground { /// DOM node for the root element pub root_element: OpaqueNode, /// The element whose style the canvas takes background properties from (see next field). /// This can be the root element (same as the previous field), or the HTML `` element. /// See pub from_element: OpaqueNode, /// The computed styles to take background properties from. pub style: Option>, } impl CanvasBackground { fn for_root_element<'dom>(context: &LayoutContext, root_element: impl NodeExt<'dom>) -> Self { let root_style = root_element.style(context); let mut style = root_style; let mut from_element = root_element; // https://drafts.csswg.org/css-backgrounds/#body-background // “if the computed value of background-image on the root element is none // and its background-color is transparent” if style.background_is_transparent() && // “For documents whose root element is an HTML `HTML` element // or an XHTML `html` element” root_element.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLHtmlElement) && // Don’t try to access styles for an unstyled subtree !matches!(style.clone_display().into(), Display::None) { // “that element’s first HTML `BODY` or XHTML `body` child element” if let Some(body) = iter_child_nodes(root_element).find(|child| { child.is_element() && child.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLBodyElement) }) { style = body.style(context); from_element = body; } } Self { root_element: root_element.opaque(), from_element: from_element.opaque(), // “However, if no boxes are generated for the element // whose background would be used for the canvas // (for example, if the root element has display: none), // then the canvas background is transparent.” style: if let Display::GeneratingBox(_) = style.clone_display().into() { Some(style) } else { None }, } } }