/* 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 gfx_traits::print_tree::PrintTree; use serde::Serialize; use servo_arc::Arc as ServoArc; use style::computed_values::overflow_x::T as ComputedOverflow; use style::computed_values::position::T as ComputedPosition; use style::properties::ComputedValues; use style::values::computed::{CSSPixelLength, Length, LengthPercentage, LengthPercentageOrAuto}; use style::Zero; use super::{BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment}; use crate::cell::ArcRefCell; use crate::geom::{ LengthOrAuto, LogicalRect, LogicalSides, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, }; use crate::style_ext::ComputedValuesExt; #[derive(Serialize)] pub(crate) struct BoxFragment { pub base: BaseFragment, #[serde(skip_serializing)] pub style: ServoArc, pub children: Vec>, /// From the containing block’s start corner…? /// This might be broken when the containing block is in a different writing mode: /// pub content_rect: LogicalRect, pub padding: LogicalSides, pub border: LogicalSides, pub margin: LogicalSides, /// When the `clear` property is not set to `none`, it may introduce clearance. /// Clearance is some extra spacing that is added above the top margin, /// so that the element doesn't overlap earlier floats in the same BFC. /// The presence of clearance prevents the top margin from collapsing with /// earlier margins or with the bottom margin of the parent block. /// pub clearance: Option, /// When this box contains an inline formatting context, this tracks the baseline /// offset of the last inflow line. This offset is used to propagate baselines to /// ancestors of `display: inline-block` ancestors. pub last_baseline_offset: Option, pub block_margins_collapsed_with_children: CollapsedBlockMargins, /// The scrollable overflow of this box fragment. pub scrollable_overflow_from_children: PhysicalRect, /// Whether or not this box was overconstrained in the given dimension. overconstrained: PhysicalSize, /// The resolved box insets if this box is `position: sticky`. These are calculated /// during stacking context tree construction because they rely on the size of the /// scroll container. pub(crate) resolved_sticky_insets: Option>, } impl BoxFragment { pub fn new( base_fragment_info: BaseFragmentInfo, style: ServoArc, children: Vec, content_rect: LogicalRect, padding: LogicalSides, border: LogicalSides, margin: LogicalSides, clearance: Option, last_inflow_baseline_offset: Option, block_margins_collapsed_with_children: CollapsedBlockMargins, ) -> BoxFragment { let position = style.get_box().position; let insets = style.get_position(); let width_overconstrained = position == ComputedPosition::Relative && !insets.left.is_auto() && !insets.right.is_auto(); let height_overconstrained = position == ComputedPosition::Relative && !insets.left.is_auto() && !insets.bottom.is_auto(); Self::new_with_overconstrained( base_fragment_info, style, children, content_rect, padding, border, margin, clearance, last_inflow_baseline_offset, block_margins_collapsed_with_children, PhysicalSize::new(width_overconstrained, height_overconstrained), ) } pub fn new_with_overconstrained( base_fragment_info: BaseFragmentInfo, style: ServoArc, children: Vec, content_rect: LogicalRect, padding: LogicalSides, border: LogicalSides, margin: LogicalSides, clearance: Option, mut last_inflow_baseline_offset: Option, block_margins_collapsed_with_children: CollapsedBlockMargins, overconstrained: PhysicalSize, ) -> BoxFragment { // FIXME(mrobinson, bug 25564): We should be using the containing block // here to properly convert scrollable overflow to physical geometry. let containing_block = PhysicalRect::zero(); let scrollable_overflow_from_children = children.iter().fold(PhysicalRect::zero(), |acc, child| { acc.union(&child.scrollable_overflow(&containing_block)) }); // From the https://drafts.csswg.org/css-align-3/#baseline-export section on "block containers": // > However, for legacy reasons if its baseline-source is auto (the initial // > value) a block-level or inline-level block container that is a scroll container // > always has a last baseline set, whose baselines all correspond to its block-end // > margin edge. if style.establishes_scroll_container() { last_inflow_baseline_offset = Some( content_rect.size.block + padding.block_end + border.block_end + margin.block_end, ); } BoxFragment { base: base_fragment_info.into(), style, children: children .into_iter() .map(|fragment| ArcRefCell::new(fragment)) .collect(), content_rect, padding, border, margin, clearance, last_baseline_offset: last_inflow_baseline_offset, block_margins_collapsed_with_children, scrollable_overflow_from_children, overconstrained, resolved_sticky_insets: None, } } pub fn scrollable_overflow( &self, containing_block: &PhysicalRect, ) -> PhysicalRect { let physical_padding_rect = self .padding_rect() .to_physical(self.style.writing_mode, containing_block); let content_origin = self .content_rect .start_corner .to_physical(self.style.writing_mode); physical_padding_rect.union( &self .scrollable_overflow_from_children .translate(content_origin.to_vector()), ) } pub fn padding_rect(&self) -> LogicalRect { self.content_rect.inflate(&self.padding) } pub fn border_rect(&self) -> LogicalRect { self.padding_rect().inflate(&self.border) } pub fn print(&self, tree: &mut PrintTree) { tree.new_level(format!( "Box\ \nbase={:?}\ \ncontent={:?}\ \npadding rect={:?}\ \nborder rect={:?}\ \nmargin={:?}\ \nclearance={:?}\ \nscrollable_overflow={:?}\ \noverflow={:?} / {:?}", self.base, self.content_rect, self.padding_rect(), self.border_rect(), self.margin, self.clearance, self.scrollable_overflow(&PhysicalRect::zero()), self.style.get_box().overflow_x, self.style.get_box().overflow_y, )); for child in &self.children { child.borrow().print(tree); } tree.end_level(); } pub fn scrollable_overflow_for_parent( &self, containing_block: &PhysicalRect, ) -> PhysicalRect { let mut overflow = self .border_rect() .to_physical(self.style.writing_mode, containing_block); if self.style.establishes_scroll_container() { return overflow; } // https://www.w3.org/TR/css-overflow-3/#scrollable // Only include the scrollable overflow of a child box if it has overflow: visible. let scrollable_overflow = self.scrollable_overflow(&containing_block); let bottom_right = PhysicalPoint::new( overflow.max_x().max(scrollable_overflow.max_x()), overflow.max_y().max(scrollable_overflow.max_y()), ); if self.style.get_box().overflow_y == ComputedOverflow::Visible { overflow.origin.y = overflow.origin.y.min(scrollable_overflow.origin.y); overflow.size.height = bottom_right.y - overflow.origin.y; } if self.style.get_box().overflow_x == ComputedOverflow::Visible { overflow.origin.x = overflow.origin.x.min(scrollable_overflow.origin.x); overflow.size.width = bottom_right.x - overflow.origin.x; } overflow } pub(crate) fn calculate_resolved_insets_if_positioned( &self, containing_block: &PhysicalRect, ) -> PhysicalSides { let position = self.style.get_box().position; debug_assert_ne!( position, ComputedPosition::Static, "Should not call this method on statically positioned box." ); let (cb_width, cb_height) = (containing_block.width(), containing_block.height()); let content_rect = self .content_rect .to_physical(self.style.writing_mode, &containing_block); if let Some(resolved_sticky_insets) = self.resolved_sticky_insets { return resolved_sticky_insets; } let convert_to_length_or_auto = |sides: PhysicalSides| { PhysicalSides::new( LengthOrAuto::LengthPercentage(sides.top), LengthOrAuto::LengthPercentage(sides.right), LengthOrAuto::LengthPercentage(sides.bottom), LengthOrAuto::LengthPercentage(sides.left), ) }; // "A resolved value special case property like top defined in another // specification If the property applies to a positioned element and the // resolved value of the display property is not none or contents, and // the property is not over-constrained, then the resolved value is the // used value. Otherwise the resolved value is the computed value." // https://drafts.csswg.org/cssom/#resolved-values let insets = self.style.get_position(); if position == ComputedPosition::Relative { let get_resolved_axis = |start: &LengthPercentageOrAuto, end: &LengthPercentageOrAuto, container_length: CSSPixelLength| { let start = start.map(|v| v.percentage_relative_to(container_length)); let end = end.map(|v| v.percentage_relative_to(container_length)); match (start.non_auto(), end.non_auto()) { (None, None) => (Length::zero(), Length::zero()), (None, Some(end)) => (-end, end), (Some(start), None) => (start, -start), // This is the overconstrained case, for which the resolved insets will // simply be the computed insets. (Some(start), Some(end)) => (start, end), } }; let (left, right) = get_resolved_axis(&insets.left, &insets.right, cb_width); let (top, bottom) = get_resolved_axis(&insets.top, &insets.bottom, cb_height); return convert_to_length_or_auto(PhysicalSides::new(top, right, bottom, left)); } debug_assert!( position == ComputedPosition::Fixed || position == ComputedPosition::Absolute ); let resolve = |value: &LengthPercentageOrAuto, container_length| { value .auto_is(LengthPercentage::zero) .percentage_relative_to(container_length) }; let (top, bottom) = if self.overconstrained.height { ( resolve(&insets.top, cb_height), resolve(&insets.bottom, cb_height), ) } else { (content_rect.origin.y, cb_height - content_rect.max_y()) }; let (left, right) = if self.overconstrained.width { ( resolve(&insets.left, cb_width), resolve(&insets.right, cb_width), ) } else { (content_rect.origin.x, cb_width - content_rect.max_x()) }; convert_to_length_or_auto(PhysicalSides::new(top, right, bottom, left)) } }