/* 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 atomic_refcell::AtomicRef; use script_layout_interface::wrapper_traits::{ LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, }; use script_layout_interface::{LayoutElementType, LayoutNodeType}; use serde::Serialize; use servo_arc::Arc; use style::dom::OpaqueNode; use style::properties::ComputedValues; use style::values::computed::{Length, Overflow}; use style_traits::CSSPixel; use webrender_traits::display_list::ScrollSensitivity; use crate::cell::ArcRefCell; use crate::context::LayoutContext; use crate::dom::{LayoutBox, NodeExt}; use crate::dom_traversal::{iter_child_nodes, Contents, NodeAndStyleInfo}; 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::ReplacedContent; use crate::style_ext::{ComputedValuesExt, Display, DisplayGeneratingBox, DisplayInside}; use crate::DefiniteContainingBlock; #[derive(Serialize)] 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 root element should be sensitive to scrolling input events. sensitive_to_scroll_input: bool, } 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://drafts.csswg.org/css-overflow/#propdef-overflow: // > 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. // // TODO: This should handle when different overflow is set multiple axes, which requires the // compositor scroll tree to allow setting a value per axis. let root_style = root_element.style(context); let mut root_overflow = root_style.get_box().overflow_y; if root_overflow == Overflow::Visible && !root_style.get_box().display.is_none() { for child in iter_child_nodes(root_element) { if !child.to_threadsafe().as_element().map_or(false, |element| { element.is_body_element_of_html_element_root() }) { continue; } let style = child.style(context); if !style.get_box().display.is_none() { root_overflow = style.get_box().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), sensitive_to_scroll_input: root_overflow != Overflow::Hidden, } } /// 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), AbsolutelyPositionedFlexLevelBox(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::InlineBox(_) => return None, LayoutBox::InlineLevel(inline_level_box) => match &*inline_level_box.borrow() { InlineItem::OutOfFlowAbsolutelyPositionedBox(_) if box_style.position.is_absolutely_positioned() => { UpdatePoint::AbsolutelyPositionedInlineLevelBox( inline_level_box.clone(), ) }, _ => 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, }, }; Some((primary_style.clone(), display_inside, update_point)) } loop { if let Some((primary_style, display_inside, update_point)) = update_point(dirty_node) { let contents = ReplacedContent::for_element(dirty_node, context) .map_or(Contents::OfElement, 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) => { *inline_level_box.borrow_mut() = InlineItem::OutOfFlowAbsolutelyPositionedBox( out_of_flow_absolutely_positioned_box, ); }, UpdatePoint::AbsolutelyPositionedFlexLevelBox(flex_level_box) => { *flex_level_box.borrow_mut() = FlexLevelBox::OutOfFlowAbsolutelyPositionedBox( out_of_flow_absolutely_positioned_box, ); }, } return true; } dirty_node = match dirty_node.parent_node() { Some(parent) => parent, None => return false, }; } } } 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 = ReplacedContent::for_element(root_element, context) .map_or(Contents::OfElement, Contents::Replaced); 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, )) } else { let propagated_text_decoration_line = info.style.clone_text_decoration_line(); BlockLevelBox::Independent(IndependentFormattingContext::construct( context, &info, display_inside, contents, propagated_text_decoration_line, )) }; 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 = ComputedValues::initial_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(Length::new(viewport.width), Length::new(viewport.height)), ); let initial_containing_block = DefiniteContainingBlock { size: LogicalVec2 { inline: physical_containing_block.size.width.into(), block: physical_containing_block.size.height.into(), }, 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(), ); let mut root_fragments = independent_layout .fragments .into_iter() .map(ArcRefCell::new) .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 .borrow() .scrollable_overflow(&physical_containing_block); // 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) }); let root_scroll_sensitivity = if self.sensitive_to_scroll_input { ScrollSensitivity::ScriptAndInputEvents } else { ScrollSensitivity::Script }; FragmentTree { root_fragments, scrollable_overflow, initial_containing_block: physical_containing_block, canvas_background: self.canvas_background.clone(), root_scroll_sensitivity, } } } /// #[derive(Clone, Serialize)] 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. #[serde(skip)] 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 }, } } }