servo/components/layout/flow/mod.rs
Martin Robinson 573663d502
layout: Correct damage propagation and style repair for repaint-only layout (#37004)
When making last-minute changes to the repaint-only layout pass, damage
propagation was broken, meaning that full layout was always done. This
change fixes that, meaning that times in the `blaster.html` test case
now reflect those described in the original commit message from #36978.

In addition, some style repair is now fixed:
- `InlineFormattingContext`s now keep a `SharedInlineStyles` for the
root of the IFC
    which is updated during style repair.
 - `BlockFormattingContext`s now properly update their style.

These changes are verified by turning on repaint only layout for more
properties
in Stylo via servo/stylo#183.

Testing: Manual performance testing via `blaster.html`.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
2025-05-19 10:17:49 +00:00

2504 lines
106 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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/. */
#![allow(rustdoc::private_intra_doc_links)]
//! Flow layout, also known as block-and-inline layout.
use app_units::{Au, MAX_AU};
use inline::InlineFormattingContext;
use malloc_size_of_derive::MallocSizeOf;
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use script::layout_dom::ServoLayoutNode;
use servo_arc::Arc;
use style::Zero;
use style::computed_values::clear::T as StyleClear;
use style::context::SharedStyleContext;
use style::logical_geometry::Direction;
use style::properties::ComputedValues;
use style::servo::selector_parser::PseudoElement;
use style::values::computed::Size as StyleSize;
use style::values::specified::align::AlignFlags;
use style::values::specified::{Display, TextAlignKeyword};
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::dom::NodeExt;
use crate::flow::float::{
Clear, ContainingBlockPositionInfo, FloatBox, FloatSide, PlacementAmongFloats,
SequentialLayoutState,
};
use crate::formatting_contexts::{
Baselines, IndependentFormattingContext, IndependentFormattingContextContents,
IndependentNonReplacedContents,
};
use crate::fragment_tree::{
BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, FragmentFlags,
};
use crate::geom::{
AuOrAuto, LazySize, LogicalRect, LogicalSides, LogicalSides1D, LogicalVec2, PhysicalPoint,
PhysicalRect, PhysicalSides, Size, Sizes, ToLogical, ToLogicalWithContainingBlock,
};
use crate::layout_box_base::{CacheableLayoutResult, LayoutBoxBase};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext, PositioningContextLength};
use crate::replaced::ReplacedContents;
use crate::sizing::{self, ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult};
use crate::style_ext::{ContentBoxSizesAndPBM, LayoutStyle, PaddingBorderMargin};
use crate::{
ConstraintSpace, ContainingBlock, ContainingBlockSize, IndefiniteContainingBlock,
SizeConstraint,
};
mod construct;
pub mod float;
pub mod inline;
mod root;
pub(crate) use construct::BlockContainerBuilder;
pub use root::BoxTree;
#[derive(Debug, MallocSizeOf)]
pub(crate) struct BlockFormattingContext {
pub contents: BlockContainer,
pub contains_floats: bool,
}
#[derive(Debug, MallocSizeOf)]
pub(crate) enum BlockContainer {
BlockLevelBoxes(Vec<ArcRefCell<BlockLevelBox>>),
InlineFormattingContext(InlineFormattingContext),
}
impl BlockContainer {
fn contains_floats(&self) -> bool {
match self {
BlockContainer::BlockLevelBoxes(boxes) => boxes
.iter()
.any(|block_level_box| block_level_box.borrow().contains_floats()),
BlockContainer::InlineFormattingContext(context) => context.contains_floats,
}
}
pub(crate) fn repair_style(&mut self, node: &ServoLayoutNode, new_style: &Arc<ComputedValues>) {
match self {
BlockContainer::BlockLevelBoxes(..) => {},
BlockContainer::InlineFormattingContext(inline_formatting_context) => {
inline_formatting_context.repair_style(node, new_style)
},
}
}
}
#[derive(Debug, MallocSizeOf)]
pub(crate) enum BlockLevelBox {
Independent(IndependentFormattingContext),
OutOfFlowAbsolutelyPositionedBox(ArcRefCell<AbsolutelyPositionedBox>),
OutOfFlowFloatBox(FloatBox),
OutsideMarker(OutsideMarker),
SameFormattingContextBlock {
base: LayoutBoxBase,
contents: BlockContainer,
contains_floats: bool,
},
}
impl BlockLevelBox {
pub(crate) fn repair_style(
&mut self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>,
) {
self.with_base_mut(|base| {
base.repair_style(new_style);
});
match self {
BlockLevelBox::Independent(independent_formatting_context) => {
independent_formatting_context.repair_style(context, node, new_style)
},
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => positioned_box
.borrow_mut()
.context
.repair_style(context, node, new_style),
BlockLevelBox::OutOfFlowFloatBox(float_box) => {
float_box.contents.repair_style(context, node, new_style)
},
BlockLevelBox::OutsideMarker(outside_marker) => {
outside_marker.repair_style(context, node, new_style)
},
BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => {
base.repair_style(new_style);
contents.repair_style(node, new_style);
},
}
}
pub(crate) fn invalidate_cached_fragment(&self) {
self.with_base(LayoutBoxBase::invalidate_cached_fragment);
}
pub(crate) fn fragments(&self) -> Vec<Fragment> {
self.with_base(LayoutBoxBase::fragments)
}
pub(crate) fn with_base<T>(&self, callback: impl Fn(&LayoutBoxBase) -> T) -> T {
match self {
BlockLevelBox::Independent(independent_formatting_context) => {
callback(&independent_formatting_context.base)
},
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => {
callback(&positioned_box.borrow().context.base)
},
BlockLevelBox::OutOfFlowFloatBox(float_box) => callback(&float_box.contents.base),
BlockLevelBox::OutsideMarker(outside_marker) => callback(&outside_marker.base),
BlockLevelBox::SameFormattingContextBlock { base, .. } => callback(base),
}
}
pub(crate) fn with_base_mut<T>(&mut self, callback: impl Fn(&mut LayoutBoxBase) -> T) -> T {
match self {
BlockLevelBox::Independent(independent_formatting_context) => {
callback(&mut independent_formatting_context.base)
},
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(positioned_box) => {
callback(&mut positioned_box.borrow_mut().context.base)
},
BlockLevelBox::OutOfFlowFloatBox(float_box) => callback(&mut float_box.contents.base),
BlockLevelBox::OutsideMarker(outside_marker) => callback(&mut outside_marker.base),
BlockLevelBox::SameFormattingContextBlock { base, .. } => callback(base),
}
}
fn contains_floats(&self) -> bool {
match self {
BlockLevelBox::SameFormattingContextBlock {
contains_floats, ..
} => *contains_floats,
BlockLevelBox::OutOfFlowFloatBox { .. } => true,
_ => false,
}
}
fn find_block_margin_collapsing_with_parent(
&self,
layout_context: &LayoutContext,
collected_margin: &mut CollapsedMargin,
containing_block: &ContainingBlock,
) -> bool {
let layout_style = match self {
BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => {
contents.layout_style(base)
},
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_) |
BlockLevelBox::OutOfFlowFloatBox(_) => return true,
BlockLevelBox::OutsideMarker(_) => return false,
BlockLevelBox::Independent(context) => {
// FIXME: If the element doesn't fit next to floats, it will get clearance.
// In that case this should be returning false.
context.layout_style()
},
};
// FIXME: This should only return false when 'clear' causes clearance.
let style = layout_style.style();
if style.get_box().clear != StyleClear::None {
return false;
}
let ContentBoxSizesAndPBM {
content_box_sizes,
pbm,
..
} = layout_style.content_box_sizes_and_padding_border_margin(&containing_block.into());
let margin = pbm.margin.auto_is(Au::zero);
collected_margin.adjoin_assign(&CollapsedMargin::new(margin.block_start));
let child_boxes = match self {
BlockLevelBox::SameFormattingContextBlock { contents, .. } => match contents {
BlockContainer::BlockLevelBoxes(boxes) => boxes,
BlockContainer::InlineFormattingContext(_) => return false,
},
_ => return false,
};
if !pbm.padding.block_start.is_zero() || !pbm.border.block_start.is_zero() {
return false;
}
let available_inline_size =
containing_block.size.inline - pbm.padding_border_sums.inline - margin.inline_sum();
let available_block_size = containing_block.size.block.to_definite().map(|block_size| {
Au::zero().max(block_size - pbm.padding_border_sums.block - margin.block_sum())
});
let tentative_block_size = content_box_sizes.block.resolve_extrinsic(
Size::FitContent,
Au::zero(),
available_block_size,
);
let get_inline_content_sizes = || {
let constraint_space = ConstraintSpace::new(
tentative_block_size,
style.writing_mode,
None, /* TODO: support preferred aspect ratios on non-replaced boxes */
);
self.inline_content_sizes(layout_context, &constraint_space)
.sizes
};
let inline_size = content_box_sizes.inline.resolve(
Direction::Inline,
Size::Stretch,
Au::zero,
Some(available_inline_size),
get_inline_content_sizes,
false, /* is_table */
);
let containing_block_for_children = ContainingBlock {
size: ContainingBlockSize {
inline: inline_size,
block: tentative_block_size,
},
style,
};
if !Self::find_block_margin_collapsing_with_parent_from_slice(
layout_context,
child_boxes,
collected_margin,
&containing_block_for_children,
) {
return false;
}
if !block_size_is_zero_or_intrinsic(style.content_block_size(), containing_block) ||
!block_size_is_zero_or_intrinsic(style.min_block_size(), containing_block) ||
!pbm.padding_border_sums.block.is_zero()
{
return false;
}
collected_margin.adjoin_assign(&CollapsedMargin::new(margin.block_end));
true
}
fn find_block_margin_collapsing_with_parent_from_slice(
layout_context: &LayoutContext,
boxes: &[ArcRefCell<BlockLevelBox>],
margin: &mut CollapsedMargin,
containing_block: &ContainingBlock,
) -> bool {
boxes.iter().all(|block_level_box| {
block_level_box
.borrow()
.find_block_margin_collapsing_with_parent(layout_context, margin, containing_block)
})
}
}
#[derive(Clone, Copy)]
pub(crate) struct CollapsibleWithParentStartMargin(bool);
/// The contentes of a BlockContainer created to render a list marker
/// for a list that has `list-style-position: outside`.
#[derive(Debug, MallocSizeOf)]
pub(crate) struct OutsideMarker {
pub list_item_style: Arc<ComputedValues>,
pub base: LayoutBoxBase,
pub block_container: BlockContainer,
}
impl OutsideMarker {
fn inline_content_sizes(
&self,
layout_context: &LayoutContext,
constraint_space: &ConstraintSpace,
) -> InlineContentSizesResult {
self.base
.inline_content_sizes(layout_context, constraint_space, &self.block_container)
}
fn layout(
&self,
layout_context: &LayoutContext<'_>,
containing_block: &ContainingBlock<'_>,
positioning_context: &mut PositioningContext,
sequential_layout_state: Option<&mut SequentialLayoutState>,
collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>,
) -> Fragment {
let constraint_space = ConstraintSpace::new_for_style_and_ratio(
&self.base.style,
None, /* TODO: support preferred aspect ratios on non-replaced boxes */
);
let content_sizes = self.inline_content_sizes(layout_context, &constraint_space);
let containing_block_for_children = ContainingBlock {
size: ContainingBlockSize {
inline: content_sizes.sizes.max_content,
block: SizeConstraint::default(),
},
style: &self.base.style,
};
// A ::marker can't have a stretch size (must be auto), so this doesn't matter.
// https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing
let ignore_block_margins_for_stretch = LogicalSides1D::new(false, false);
let flow_layout = self.block_container.layout(
layout_context,
positioning_context,
&containing_block_for_children,
sequential_layout_state,
collapsible_with_parent_start_margin.unwrap_or(CollapsibleWithParentStartMargin(false)),
ignore_block_margins_for_stretch,
);
let max_inline_size =
flow_layout
.fragments
.iter()
.fold(Au::zero(), |current_max, fragment| {
current_max.max(
match fragment {
Fragment::Text(text) => text.borrow().rect,
Fragment::Image(image) => image.borrow().rect,
Fragment::Positioning(positioning) => positioning.borrow().rect,
Fragment::Box(_) |
Fragment::Float(_) |
Fragment::AbsoluteOrFixedPositioned(_) |
Fragment::IFrame(_) => {
unreachable!(
"Found unexpected fragment type in outside list marker!"
);
},
}
.to_logical(&containing_block_for_children)
.max_inline_position(),
)
});
// Position the marker beyond the inline start of the border box list item. This needs to
// take into account the border and padding of the item.
//
// TODO: This is the wrong containing block, as it should be the containing block of the
// parent of this list item. What this means in practice is that the writing mode could be
// wrong and padding defined as a percentage will be resolved incorrectly.
//
// TODO: This should use the LayoutStyle of the list item, not the default one. Currently
// they are the same, but this could change in the future.
let pbm_of_list_item =
LayoutStyle::Default(&self.list_item_style).padding_border_margin(containing_block);
let content_rect = LogicalRect {
start_corner: LogicalVec2 {
inline: -max_inline_size -
(pbm_of_list_item.border.inline_start +
pbm_of_list_item.padding.inline_start),
block: Zero::zero(),
},
size: LogicalVec2 {
inline: max_inline_size,
block: flow_layout.content_block_size,
},
};
let mut base_fragment_info = BaseFragmentInfo::anonymous();
base_fragment_info.flags |= FragmentFlags::IS_OUTSIDE_LIST_ITEM_MARKER;
Fragment::Box(ArcRefCell::new(BoxFragment::new(
base_fragment_info,
self.base.style.clone(),
flow_layout.fragments,
content_rect.as_physical(Some(containing_block)),
PhysicalSides::zero(),
PhysicalSides::zero(),
PhysicalSides::zero(),
None,
)))
}
fn repair_style(
&mut self,
context: &SharedStyleContext,
node: &ServoLayoutNode,
new_style: &Arc<ComputedValues>,
) {
self.list_item_style = node.style(context);
self.base.repair_style(new_style);
}
}
impl BlockFormattingContext {
pub(super) fn layout(
&self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
depends_on_block_constraints: bool,
) -> CacheableLayoutResult {
let mut sequential_layout_state = if self.contains_floats || !layout_context.use_rayon {
Some(SequentialLayoutState::new(containing_block.size.inline))
} else {
None
};
// Since this is an independent formatting context, we don't ignore block margins when
// resolving a stretch block size of the children.
// https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing
let ignore_block_margins_for_stretch = LogicalSides1D::new(false, false);
let flow_layout = self.contents.layout(
layout_context,
positioning_context,
containing_block,
sequential_layout_state.as_mut(),
CollapsibleWithParentStartMargin(false),
ignore_block_margins_for_stretch,
);
debug_assert!(
!flow_layout
.collapsible_margins_in_children
.collapsed_through
);
// The content height of a BFC root should include any float participating in that BFC
// (https://drafts.csswg.org/css2/#root-height), we implement this by imagining there is
// an element with `clear: both` after the actual contents.
let clearance = sequential_layout_state.and_then(|sequential_layout_state| {
sequential_layout_state.calculate_clearance(Clear::Both, &CollapsedMargin::zero())
});
CacheableLayoutResult {
fragments: flow_layout.fragments,
content_block_size: flow_layout.content_block_size +
flow_layout.collapsible_margins_in_children.end.solve() +
clearance.unwrap_or_default(),
content_inline_size_for_table: None,
baselines: flow_layout.baselines,
depends_on_block_constraints: depends_on_block_constraints ||
flow_layout.depends_on_block_constraints,
specific_layout_info: None,
collapsible_margins_in_children: CollapsedBlockMargins::zero(),
}
}
#[inline]
pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
LayoutStyle::Default(&base.style)
}
pub(crate) fn repair_style(&mut self, node: &ServoLayoutNode, new_style: &Arc<ComputedValues>) {
self.contents.repair_style(node, new_style);
}
}
/// Finds the min/max-content inline size of the block-level children of a block container.
/// The in-flow boxes will stack vertically, so we only need to consider the maximum size.
/// But floats can flow horizontally depending on 'clear', so we may need to sum their sizes.
/// CSS 2 does not define the exact algorithm, this logic is based on the behavior observed
/// on Gecko and Blink.
fn compute_inline_content_sizes_for_block_level_boxes(
boxes: &[ArcRefCell<BlockLevelBox>],
layout_context: &LayoutContext,
containing_block: &IndefiniteContainingBlock,
) -> InlineContentSizesResult {
let get_box_info = |box_: &ArcRefCell<BlockLevelBox>| {
match &*box_.borrow() {
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_) |
BlockLevelBox::OutsideMarker { .. } => None,
BlockLevelBox::OutOfFlowFloatBox(float_box) => {
let inline_content_sizes_result = float_box.contents.outer_inline_content_sizes(
layout_context,
containing_block,
&LogicalVec2::zero(),
false, /* auto_block_size_stretches_to_containing_block */
);
let style = &float_box.contents.style();
Some((
inline_content_sizes_result,
FloatSide::from_style_and_container_writing_mode(
style,
containing_block.writing_mode,
),
Clear::from_style_and_container_writing_mode(
style,
containing_block.writing_mode,
),
))
},
BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => {
let inline_content_sizes_result = sizing::outer_inline(
&contents.layout_style(base),
containing_block,
&LogicalVec2::zero(),
false, /* auto_block_size_stretches_to_containing_block */
false, /* is_replaced */
!matches!(base.style.pseudo(), Some(PseudoElement::ServoAnonymousBox)),
|_| None, /* TODO: support preferred aspect ratios on non-replaced boxes */
|constraint_space| {
base.inline_content_sizes(layout_context, constraint_space, contents)
},
);
// A block in the same BFC can overlap floats, it's not moved next to them,
// so we shouldn't add its size to the size of the floats.
// Instead, we treat it like an independent block with 'clear: both'.
Some((inline_content_sizes_result, None, Clear::Both))
},
BlockLevelBox::Independent(independent) => {
let inline_content_sizes_result = independent.outer_inline_content_sizes(
layout_context,
containing_block,
&LogicalVec2::zero(),
false, /* auto_block_size_stretches_to_containing_block */
);
Some((
inline_content_sizes_result,
None,
Clear::from_style_and_container_writing_mode(
independent.style(),
containing_block.writing_mode,
),
))
},
}
};
/// When iterating the block-level boxes to compute the inline content sizes,
/// this struct contains the data accumulated up to the current box.
#[derive(Default)]
struct AccumulatedData {
/// Whether the inline size depends on the block one.
depends_on_block_constraints: bool,
/// The maximum size seen so far, not including trailing uncleared floats.
max_size: ContentSizes,
/// The size of the trailing uncleared floats on the inline-start side
/// of the containing block.
start_floats: ContentSizes,
/// The size of the trailing uncleared floats on the inline-end side
/// of the containing block.
end_floats: ContentSizes,
}
impl AccumulatedData {
fn max_size_including_uncleared_floats(&self) -> ContentSizes {
self.max_size.max(self.start_floats.union(&self.end_floats))
}
fn clear_floats(&mut self, clear: Clear) {
match clear {
Clear::InlineStart => {
self.max_size = self.max_size_including_uncleared_floats();
self.start_floats = ContentSizes::zero();
},
Clear::InlineEnd => {
self.max_size = self.max_size_including_uncleared_floats();
self.end_floats = ContentSizes::zero();
},
Clear::Both => {
self.max_size = self.max_size_including_uncleared_floats();
self.start_floats = ContentSizes::zero();
self.end_floats = ContentSizes::zero();
},
Clear::None => {},
};
}
}
let accumulate =
|mut data: AccumulatedData,
(inline_content_sizes_result, float, clear): (InlineContentSizesResult, _, _)| {
let size = inline_content_sizes_result.sizes.max(ContentSizes::zero());
let depends_on_block_constraints =
inline_content_sizes_result.depends_on_block_constraints;
data.depends_on_block_constraints |= depends_on_block_constraints;
data.clear_floats(clear);
match float {
Some(FloatSide::InlineStart) => data.start_floats = data.start_floats.union(&size),
Some(FloatSide::InlineEnd) => data.end_floats = data.end_floats.union(&size),
None => {
data.max_size = data
.max_size
.max(data.start_floats.union(&data.end_floats).union(&size));
data.start_floats = ContentSizes::zero();
data.end_floats = ContentSizes::zero();
},
}
data
};
let data = if layout_context.use_rayon {
boxes
.par_iter()
.filter_map(get_box_info)
.collect::<Vec<_>>()
.into_iter()
.fold(AccumulatedData::default(), accumulate)
} else {
boxes
.iter()
.filter_map(get_box_info)
.fold(AccumulatedData::default(), accumulate)
};
InlineContentSizesResult {
depends_on_block_constraints: data.depends_on_block_constraints,
sizes: data.max_size_including_uncleared_floats(),
}
}
impl BlockContainer {
fn layout(
&self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
sequential_layout_state: Option<&mut SequentialLayoutState>,
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> CacheableLayoutResult {
match self {
BlockContainer::BlockLevelBoxes(child_boxes) => layout_block_level_children(
layout_context,
positioning_context,
child_boxes,
containing_block,
sequential_layout_state,
collapsible_with_parent_start_margin,
ignore_block_margins_for_stretch,
),
BlockContainer::InlineFormattingContext(ifc) => ifc.layout(
layout_context,
positioning_context,
containing_block,
sequential_layout_state,
collapsible_with_parent_start_margin,
),
}
}
#[inline]
pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
LayoutStyle::Default(&base.style)
}
}
impl ComputeInlineContentSizes for BlockContainer {
fn compute_inline_content_sizes(
&self,
layout_context: &LayoutContext,
constraint_space: &ConstraintSpace,
) -> InlineContentSizesResult {
match &self {
Self::BlockLevelBoxes(boxes) => compute_inline_content_sizes_for_block_level_boxes(
boxes,
layout_context,
&constraint_space.into(),
),
Self::InlineFormattingContext(context) => {
context.compute_inline_content_sizes(layout_context, constraint_space)
},
}
}
}
fn layout_block_level_children(
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
child_boxes: &[ArcRefCell<BlockLevelBox>],
containing_block: &ContainingBlock,
mut sequential_layout_state: Option<&mut SequentialLayoutState>,
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> CacheableLayoutResult {
let mut placement_state =
PlacementState::new(collapsible_with_parent_start_margin, containing_block);
let fragments = match sequential_layout_state {
Some(ref mut sequential_layout_state) => layout_block_level_children_sequentially(
layout_context,
positioning_context,
child_boxes,
containing_block,
sequential_layout_state,
&mut placement_state,
ignore_block_margins_for_stretch,
),
None => layout_block_level_children_in_parallel(
layout_context,
positioning_context,
child_boxes,
containing_block,
&mut placement_state,
ignore_block_margins_for_stretch,
),
};
let depends_on_block_constraints = fragments.iter().any(|fragment| {
fragment.base().is_some_and(|base| {
base.flags.contains(
FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM,
)
})
});
let (content_block_size, collapsible_margins_in_children, baselines) = placement_state.finish();
CacheableLayoutResult {
fragments,
content_block_size,
collapsible_margins_in_children,
baselines,
depends_on_block_constraints,
content_inline_size_for_table: None,
specific_layout_info: None,
}
}
fn layout_block_level_children_in_parallel(
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
child_boxes: &[ArcRefCell<BlockLevelBox>],
containing_block: &ContainingBlock,
placement_state: &mut PlacementState,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> Vec<Fragment> {
let mut layout_results: Vec<(Fragment, PositioningContext)> =
Vec::with_capacity(child_boxes.len());
child_boxes
.par_iter()
.map(|child_box| {
let mut child_positioning_context = PositioningContext::default();
let fragment = child_box.borrow().layout(
layout_context,
&mut child_positioning_context,
containing_block,
/* sequential_layout_state = */ None,
/* collapsible_with_parent_start_margin = */ None,
ignore_block_margins_for_stretch,
);
(fragment, child_positioning_context)
})
.collect_into_vec(&mut layout_results);
layout_results
.into_iter()
.map(|(mut fragment, mut child_positioning_context)| {
placement_state.place_fragment_and_update_baseline(&mut fragment, None);
child_positioning_context.adjust_static_position_of_hoisted_fragments(
&fragment,
PositioningContextLength::zero(),
);
positioning_context.append(child_positioning_context);
fragment
})
.collect()
}
fn layout_block_level_children_sequentially(
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
child_boxes: &[ArcRefCell<BlockLevelBox>],
containing_block: &ContainingBlock,
sequential_layout_state: &mut SequentialLayoutState,
placement_state: &mut PlacementState,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> Vec<Fragment> {
// Because floats are involved, we do layout for this block formatting context in tree
// order without parallelism. This enables mutable access to a `SequentialLayoutState` that
// tracks every float encountered so far (again in tree order).
child_boxes
.iter()
.map(|child_box| {
let positioning_context_length_before_layout = positioning_context.len();
let mut fragment = child_box.borrow().layout(
layout_context,
positioning_context,
containing_block,
Some(&mut *sequential_layout_state),
Some(CollapsibleWithParentStartMargin(
placement_state.next_in_flow_margin_collapses_with_parent_start_margin,
)),
ignore_block_margins_for_stretch,
);
placement_state
.place_fragment_and_update_baseline(&mut fragment, Some(sequential_layout_state));
positioning_context.adjust_static_position_of_hoisted_fragments(
&fragment,
positioning_context_length_before_layout,
);
fragment
})
.collect()
}
impl BlockLevelBox {
fn layout(
&self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
sequential_layout_state: Option<&mut SequentialLayoutState>,
collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> Fragment {
let fragment = match self {
BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => Fragment::Box(
ArcRefCell::new(positioning_context.layout_maybe_position_relative_fragment(
layout_context,
containing_block,
base,
|positioning_context| {
layout_in_flow_non_replaced_block_level_same_formatting_context(
layout_context,
positioning_context,
containing_block,
base,
contents,
sequential_layout_state,
collapsible_with_parent_start_margin,
ignore_block_margins_for_stretch,
)
},
)),
),
BlockLevelBox::Independent(independent) => Fragment::Box(ArcRefCell::new(
positioning_context.layout_maybe_position_relative_fragment(
layout_context,
containing_block,
&independent.base,
|positioning_context| {
independent.layout_in_flow_block_level(
layout_context,
positioning_context,
containing_block,
sequential_layout_state,
ignore_block_margins_for_stretch,
)
},
),
)),
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(box_) => {
// The static position of zero here is incorrect, however we do not know
// the correct positioning until later, in place_block_level_fragment, and
// this value will be adjusted there.
let hoisted_box = AbsolutelyPositionedBox::to_hoisted(
box_.clone(),
// This is incorrect, however we do not know the correct positioning
// until later, in PlacementState::place_fragment, and this value will be
// adjusted there
PhysicalRect::zero(),
LogicalVec2 {
inline: AlignFlags::START,
block: AlignFlags::START,
},
containing_block.style.writing_mode,
);
let hoisted_fragment = hoisted_box.fragment.clone();
positioning_context.push(hoisted_box);
Fragment::AbsoluteOrFixedPositioned(hoisted_fragment)
},
BlockLevelBox::OutOfFlowFloatBox(float_box) => Fragment::Float(ArcRefCell::new(
float_box.layout(layout_context, positioning_context, containing_block),
)),
BlockLevelBox::OutsideMarker(outside_marker) => outside_marker.layout(
layout_context,
containing_block,
positioning_context,
sequential_layout_state,
collapsible_with_parent_start_margin,
),
};
self.with_base(|base| base.set_fragment(fragment.clone()));
fragment
}
fn inline_content_sizes(
&self,
layout_context: &LayoutContext,
constraint_space: &ConstraintSpace,
) -> InlineContentSizesResult {
let independent_formatting_context = match self {
BlockLevelBox::Independent(independent) => independent,
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(box_) => &box_.borrow().context,
BlockLevelBox::OutOfFlowFloatBox(float_box) => &float_box.contents,
BlockLevelBox::OutsideMarker(outside_marker) => {
return outside_marker.inline_content_sizes(layout_context, constraint_space);
},
BlockLevelBox::SameFormattingContextBlock { base, contents, .. } => {
return base.inline_content_sizes(layout_context, constraint_space, contents);
},
};
independent_formatting_context.inline_content_sizes(layout_context, constraint_space)
}
}
/// Lay out a normal flow non-replaced block that does not establish a new formatting
/// context.
///
/// - <https://drafts.csswg.org/css2/visudet.html#blockwidth>
/// - <https://drafts.csswg.org/css2/visudet.html#normal-block>
#[allow(clippy::too_many_arguments)]
fn layout_in_flow_non_replaced_block_level_same_formatting_context(
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
base: &LayoutBoxBase,
contents: &BlockContainer,
mut sequential_layout_state: Option<&mut SequentialLayoutState>,
collapsible_with_parent_start_margin: Option<CollapsibleWithParentStartMargin>,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> BoxFragment {
let style = &base.style;
let layout_style = contents.layout_style(base);
let containing_block_writing_mode = containing_block.style.writing_mode;
let get_inline_content_sizes = |constraint_space: &ConstraintSpace| {
base.inline_content_sizes(layout_context, constraint_space, contents)
.sizes
};
let ContainingBlockPaddingAndBorder {
containing_block: containing_block_for_children,
pbm,
block_sizes,
depends_on_block_constraints,
available_block_size,
justify_self,
} = solve_containing_block_padding_and_border_for_in_flow_box(
containing_block,
&layout_style,
get_inline_content_sizes,
ignore_block_margins_for_stretch,
);
let ResolvedMargins {
margin,
effective_margin_inline_start,
} = solve_margins(
containing_block,
&pbm,
containing_block_for_children.size.inline,
justify_self,
);
let computed_block_size = style.content_block_size();
let start_margin_can_collapse_with_children =
pbm.padding.block_start.is_zero() && pbm.border.block_start.is_zero();
let mut clearance = None;
let parent_containing_block_position_info;
match sequential_layout_state {
None => parent_containing_block_position_info = None,
Some(ref mut sequential_layout_state) => {
let clear =
Clear::from_style_and_container_writing_mode(style, containing_block_writing_mode);
let mut block_start_margin = CollapsedMargin::new(margin.block_start);
// The block start margin may collapse with content margins,
// compute the resulting one in order to place floats correctly.
// Only need to do this if the element isn't also collapsing with its parent,
// otherwise we should have already included the margin in an ancestor.
// Note this lookahead stops when finding a descendant whose `clear` isn't `none`
// (since clearance prevents collapsing margins with the parent).
// But then we have to decide whether to actually add clearance or not,
// so look forward again regardless of `collapsible_with_parent_start_margin`.
// TODO: This isn't completely right: if we don't add actual clearance,
// the margin should have been included in the parent (or some ancestor).
// The lookahead should stop for actual clearance, not just for `clear`.
let collapsible_with_parent_start_margin = collapsible_with_parent_start_margin.expect(
"We should know whether we are collapsing the block start margin with the parent \
when laying out sequentially",
).0 && clear == Clear::None;
if !collapsible_with_parent_start_margin && start_margin_can_collapse_with_children {
if let BlockContainer::BlockLevelBoxes(child_boxes) = contents {
BlockLevelBox::find_block_margin_collapsing_with_parent_from_slice(
layout_context,
child_boxes,
&mut block_start_margin,
&containing_block_for_children,
);
}
}
// Introduce clearance if necessary.
clearance = sequential_layout_state.calculate_clearance(clear, &block_start_margin);
if clearance.is_some() {
sequential_layout_state.collapse_margins();
}
sequential_layout_state.adjoin_assign(&block_start_margin);
if !start_margin_can_collapse_with_children {
sequential_layout_state.collapse_margins();
}
// NB: This will be a no-op if we're collapsing margins with our children since that
// can only happen if we have no block-start padding and border.
sequential_layout_state.advance_block_position(
pbm.padding.block_start +
pbm.border.block_start +
clearance.unwrap_or_else(Au::zero),
);
// We are about to lay out children. Update the offset between the block formatting
// context and the containing block that we create for them. This offset is used to
// ajust BFC relative coordinates to coordinates that are relative to our content box.
// Our content box establishes the containing block for non-abspos children, including
// floats.
let inline_start = sequential_layout_state
.floats
.containing_block_info
.inline_start +
pbm.padding.inline_start +
pbm.border.inline_start +
effective_margin_inline_start;
let new_cb_offsets = ContainingBlockPositionInfo {
block_start: sequential_layout_state.bfc_relative_block_position,
block_start_margins_not_collapsed: sequential_layout_state.current_margin,
inline_start,
inline_end: inline_start + containing_block_for_children.size.inline,
};
parent_containing_block_position_info = Some(
sequential_layout_state.replace_containing_block_position_info(new_cb_offsets),
);
},
};
// https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing
// > If this is a block axis size, and the element is in a Block Layout formatting context,
// > and the parent element does not have a block-start border or padding and is not an
// > independent formatting context, treat the elements block-start margin as zero
// > for the purpose of calculating this size. Do the same for the block-end margin.
let ignore_block_margins_for_stretch = LogicalSides1D::new(
pbm.border.block_start.is_zero() && pbm.padding.block_start.is_zero(),
pbm.border.block_end.is_zero() && pbm.padding.block_end.is_zero(),
);
let flow_layout = contents.layout(
layout_context,
positioning_context,
&containing_block_for_children,
sequential_layout_state.as_deref_mut(),
CollapsibleWithParentStartMargin(start_margin_can_collapse_with_children),
ignore_block_margins_for_stretch,
);
let mut content_block_size: Au = flow_layout.content_block_size;
// Update margins.
let mut block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin);
let mut collapsible_margins_in_children = flow_layout.collapsible_margins_in_children;
if start_margin_can_collapse_with_children {
block_margins_collapsed_with_children
.start
.adjoin_assign(&collapsible_margins_in_children.start);
if collapsible_margins_in_children.collapsed_through {
block_margins_collapsed_with_children
.start
.adjoin_assign(&std::mem::replace(
&mut collapsible_margins_in_children.end,
CollapsedMargin::zero(),
));
}
}
let collapsed_through = collapsible_margins_in_children.collapsed_through &&
pbm.padding_border_sums.block.is_zero() &&
block_size_is_zero_or_intrinsic(computed_block_size, containing_block) &&
block_size_is_zero_or_intrinsic(style.min_block_size(), containing_block);
block_margins_collapsed_with_children.collapsed_through = collapsed_through;
let end_margin_can_collapse_with_children = collapsed_through ||
(pbm.padding.block_end.is_zero() &&
pbm.border.block_end.is_zero() &&
!containing_block_for_children.size.block.is_definite());
if end_margin_can_collapse_with_children {
block_margins_collapsed_with_children
.end
.adjoin_assign(&collapsible_margins_in_children.end);
} else {
content_block_size += collapsible_margins_in_children.end.solve();
}
let block_size = block_sizes.resolve(
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
|| content_block_size.into(),
false, /* is_table */
);
if let Some(ref mut sequential_layout_state) = sequential_layout_state {
// Now that we're done laying out our children, we can restore the
// parent's containing block position information.
sequential_layout_state
.replace_containing_block_position_info(parent_containing_block_position_info.unwrap());
// Account for padding and border. We also might have to readjust the
// `bfc_relative_block_position` if it was different from the content size (i.e. was
// non-`auto` and/or was affected by min/max block size).
//
// If this adjustment is positive, that means that a block size was specified, but
// the content inside had a smaller block size. If this adjustment is negative, a
// block size was specified, but the content inside overflowed this container in
// the block direction. In that case, the ceiling for floats is effectively raised
// as long as no floats in the overflowing content lowered it.
sequential_layout_state.advance_block_position(
block_size - content_block_size + pbm.padding.block_end + pbm.border.block_end,
);
if !end_margin_can_collapse_with_children {
sequential_layout_state.collapse_margins();
}
sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin.block_end));
}
let content_rect = LogicalRect {
start_corner: LogicalVec2 {
block: (pbm.padding.block_start +
pbm.border.block_start +
clearance.unwrap_or_else(Au::zero)),
inline: pbm.padding.inline_start +
pbm.border.inline_start +
effective_margin_inline_start,
},
size: LogicalVec2 {
block: block_size,
inline: containing_block_for_children.size.inline,
},
};
let mut base_fragment_info = base.base_fragment_info;
if depends_on_block_constraints {
base_fragment_info
.flags
.insert(FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM);
}
BoxFragment::new(
base_fragment_info,
style.clone(),
flow_layout.fragments,
content_rect.as_physical(Some(containing_block)),
pbm.padding.to_physical(containing_block_writing_mode),
pbm.border.to_physical(containing_block_writing_mode),
margin.to_physical(containing_block_writing_mode),
clearance,
)
.with_baselines(flow_layout.baselines)
.with_block_margins_collapsed_with_children(block_margins_collapsed_with_children)
}
impl IndependentNonReplacedContents {
/// Lay out a normal in flow non-replaced block that establishes an independent
/// formatting context in its containing formatting context.
///
/// - <https://drafts.csswg.org/css2/visudet.html#blockwidth>
/// - <https://drafts.csswg.org/css2/visudet.html#normal-block>
pub(crate) fn layout_in_flow_block_level(
&self,
base: &LayoutBoxBase,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
sequential_layout_state: Option<&mut SequentialLayoutState>,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> BoxFragment {
if let Some(sequential_layout_state) = sequential_layout_state {
return self.layout_in_flow_block_level_sequentially(
base,
layout_context,
positioning_context,
containing_block,
sequential_layout_state,
ignore_block_margins_for_stretch,
);
}
let get_inline_content_sizes = |constraint_space: &ConstraintSpace| {
base.inline_content_sizes(layout_context, constraint_space, self)
.sizes
};
let layout_style = self.layout_style(base);
let ContainingBlockPaddingAndBorder {
containing_block: containing_block_for_children,
pbm,
block_sizes,
depends_on_block_constraints,
available_block_size,
justify_self,
} = solve_containing_block_padding_and_border_for_in_flow_box(
containing_block,
&layout_style,
get_inline_content_sizes,
ignore_block_margins_for_stretch,
);
let lazy_block_size = LazySize::new(
&block_sizes,
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
layout_style.is_table(),
);
let layout = self.layout(
layout_context,
positioning_context,
&containing_block_for_children,
containing_block,
base,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
let inline_size = layout
.content_inline_size_for_table
.unwrap_or(containing_block_for_children.size.inline);
let block_size = lazy_block_size.resolve(|| layout.content_block_size);
let ResolvedMargins {
margin,
effective_margin_inline_start,
} = solve_margins(containing_block, &pbm, inline_size, justify_self);
let content_rect = LogicalRect {
start_corner: LogicalVec2 {
block: pbm.padding.block_start + pbm.border.block_start,
inline: pbm.padding.inline_start +
pbm.border.inline_start +
effective_margin_inline_start,
},
size: LogicalVec2 {
block: block_size,
inline: inline_size,
},
};
let block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin);
let containing_block_writing_mode = containing_block.style.writing_mode;
let mut base_fragment_info = base.base_fragment_info;
if depends_on_block_constraints {
base_fragment_info.flags.insert(
FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM,
);
}
BoxFragment::new(
base_fragment_info,
base.style.clone(),
layout.fragments,
content_rect.as_physical(Some(containing_block)),
pbm.padding.to_physical(containing_block_writing_mode),
pbm.border.to_physical(containing_block_writing_mode),
margin.to_physical(containing_block_writing_mode),
None, /* clearance */
)
.with_baselines(layout.baselines)
.with_specific_layout_info(layout.specific_layout_info)
.with_block_margins_collapsed_with_children(block_margins_collapsed_with_children)
}
/// Lay out a normal in flow non-replaced block that establishes an independent
/// formatting context in its containing formatting context but handling sequential
/// layout concerns, such clearing and placing the content next to floats.
fn layout_in_flow_block_level_sequentially(
&self,
base: &LayoutBoxBase,
layout_context: &LayoutContext<'_>,
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock<'_>,
sequential_layout_state: &mut SequentialLayoutState,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> BoxFragment {
let style = &base.style;
let containing_block_writing_mode = containing_block.style.writing_mode;
let ContentBoxSizesAndPBM {
content_box_sizes,
pbm,
depends_on_block_constraints,
..
} = self
.layout_style(base)
.content_box_sizes_and_padding_border_margin(&containing_block.into());
let (margin_block_start, margin_block_end) =
solve_block_margins_for_in_flow_block_level(&pbm);
let collapsed_margin_block_start = CollapsedMargin::new(margin_block_start);
// From https://drafts.csswg.org/css2/#floats:
// "The border box of a table, a block-level replaced element, or an element in
// the normal flow that establishes a new block formatting context (such as an
// element with overflow other than visible) must not overlap the margin box of
// any floats in the same block formatting context as the element itself. If
// necessary, implementations should clear the said element by placing it below
// any preceding floats, but may place it adjacent to such floats if there is
// sufficient space. They may even make the border box of said element narrower
// than defined by section 10.3.3. CSS 2 does not define when a UA may put said
// element next to the float or by how much said element may become narrower."
let mut content_size;
let mut layout;
let mut placement_rect;
// First compute the clear position required by the 'clear' property.
// The code below may then add extra clearance when the element can't fit
// next to floats not covered by 'clear'.
let clear_position = sequential_layout_state.calculate_clear_position(
Clear::from_style_and_container_writing_mode(style, containing_block_writing_mode),
&collapsed_margin_block_start,
);
let ceiling = clear_position.unwrap_or_else(|| {
sequential_layout_state.position_without_clearance(&collapsed_margin_block_start)
});
// Then compute a tentative block size, only taking extrinsic values into account.
let pbm_sums = pbm.sums_auto_is_zero(ignore_block_margins_for_stretch);
let available_block_size = containing_block
.size
.block
.to_definite()
.map(|block_size| Au::zero().max(block_size - pbm_sums.block));
let (preferred_block_size, min_block_size, max_block_size) = content_box_sizes
.block
.resolve_each_extrinsic(Size::FitContent, Au::zero(), available_block_size);
let tentative_block_size =
SizeConstraint::new(preferred_block_size, min_block_size, max_block_size);
// With the tentative block size we can compute the inline min/max-content sizes.
let get_inline_content_sizes = || {
let constraint_space = ConstraintSpace::new(
tentative_block_size,
style.writing_mode,
self.preferred_aspect_ratio(),
);
base.inline_content_sizes(layout_context, &constraint_space, self)
.sizes
};
let justify_self = resolve_justify_self(style, containing_block.style);
let is_table = self.is_table();
let compute_inline_size = |stretch_size| {
content_box_sizes.inline.resolve(
Direction::Inline,
automatic_inline_size(justify_self, is_table),
Au::zero,
Some(stretch_size),
get_inline_content_sizes,
is_table,
)
};
let lazy_block_size = LazySize::new(
&content_box_sizes.block,
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
is_table,
);
// The final inline size can depend on the available space, which depends on where
// we are placing the box, since floats reduce the available space.
// Here we assume that `compute_inline_size()` is a monotonically increasing function
// with respect to the available space. Therefore, if we get the same result for 0
// and for MAX_AU, it means that the function is constant.
// TODO: `compute_inline_size()` may not be monotonic with `calc-size()`. For example,
// `calc-size(stretch, (1px / (size + 1px) + sign(size)) * 1px)` would result in 1px
// both when the available space is zero and infinity, but it's not constant.
let inline_size_with_no_available_space = compute_inline_size(Au::zero());
if inline_size_with_no_available_space == compute_inline_size(MAX_AU) {
// If the inline size doesn't depend on the available inline space, we can just
// compute it with an available inline space of zero. Then, after layout we can
// compute the block size, and finally place among floats.
let inline_size = inline_size_with_no_available_space;
layout = self.layout(
layout_context,
positioning_context,
&ContainingBlock {
size: ContainingBlockSize {
inline: inline_size,
block: tentative_block_size,
},
style,
},
containing_block,
base,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
content_size = LogicalVec2 {
block: lazy_block_size.resolve(|| layout.content_block_size),
inline: layout.content_inline_size_for_table.unwrap_or(inline_size),
};
let mut placement = PlacementAmongFloats::new(
&sequential_layout_state.floats,
ceiling,
content_size + pbm.padding_border_sums,
&pbm,
);
placement_rect = placement.place();
} else {
// If the inline size depends on the available space, then we need to iterate
// the various placement candidates, resolve both the inline and block sizes
// on each one placement area, and then check if the box actually fits it.
// As an optimization, we first compute a lower bound of the final box size,
// and skip placement candidates where not even the lower bound would fit.
let minimum_size_of_block = LogicalVec2 {
// For the lower bound of the inline size, simply assume no available space.
// TODO: this won't work for things like `calc-size(stretch, 100px - size)`,
// which should result in a bigger size when the available space gets smaller.
inline: inline_size_with_no_available_space,
block: match tentative_block_size {
// If we were able to resolve the preferred and maximum block sizes,
// use the tentative block size (it takes the 3 sizes into account).
SizeConstraint::Definite(size) if max_block_size.is_some() => size,
// Oherwise the preferred or maximum block size might end up being zero,
// so can only rely on the minimum block size.
_ => min_block_size,
},
} + pbm.padding_border_sums;
let mut placement = PlacementAmongFloats::new(
&sequential_layout_state.floats,
ceiling,
minimum_size_of_block,
&pbm,
);
loop {
// First try to place the block using the minimum size as the object size.
placement_rect = placement.place();
let available_inline_size =
placement_rect.size.inline - pbm.padding_border_sums.inline;
let proposed_inline_size = compute_inline_size(available_inline_size);
// Now lay out the block using the inline size we calculated from the placement.
// Later we'll check to see if the resulting block size is compatible with the
// placement.
let positioning_context_length = positioning_context.len();
layout = self.layout(
layout_context,
positioning_context,
&ContainingBlock {
size: ContainingBlockSize {
inline: proposed_inline_size,
block: tentative_block_size,
},
style,
},
containing_block,
base,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
let inline_size = if let Some(inline_size) = layout.content_inline_size_for_table {
// This is a table that ended up being smaller than predicted because of
// collapsed columns. Note we don't backtrack to consider areas that we
// previously thought weren't big enough.
// TODO: Should `minimum_size_of_block.inline` be zero for tables?
debug_assert!(inline_size < proposed_inline_size);
inline_size
} else {
proposed_inline_size
};
content_size = LogicalVec2 {
block: lazy_block_size.resolve(|| layout.content_block_size),
inline: inline_size,
};
// Now we know the block size of this attempted layout of a box with block
// size of auto. Try to fit it into our precalculated placement among the
// floats. If it fits, then we can stop trying layout candidates.
if placement.try_to_expand_for_auto_block_size(
content_size.block + pbm.padding_border_sums.block,
&placement_rect.size,
) {
break;
}
// The previous attempt to lay out this independent formatting context
// among the floats did not work, so we must unhoist any boxes from that
// attempt.
positioning_context.truncate(&positioning_context_length);
}
}
// Only set clearance if we would have cleared or the placement among floats moves
// the block further in the block direction. These two situations are the ones that
// prevent margin collapse.
let has_clearance = clear_position.is_some() || placement_rect.start_corner.block > ceiling;
let clearance = has_clearance.then(|| {
placement_rect.start_corner.block -
sequential_layout_state
.position_with_zero_clearance(&collapsed_margin_block_start)
});
let ((margin_inline_start, margin_inline_end), effective_margin_inline_start) =
solve_inline_margins_avoiding_floats(
sequential_layout_state,
containing_block,
&pbm,
content_size.inline + pbm.padding_border_sums.inline,
placement_rect,
justify_self,
);
let margin = LogicalSides {
inline_start: margin_inline_start,
inline_end: margin_inline_end,
block_start: margin_block_start,
block_end: margin_block_end,
};
// Clearance prevents margin collapse between this block and previous ones,
// so in that case collapse margins before adjoining them below.
if clearance.is_some() {
sequential_layout_state.collapse_margins();
}
sequential_layout_state.adjoin_assign(&collapsed_margin_block_start);
// Margins can never collapse into independent formatting contexts.
sequential_layout_state.collapse_margins();
sequential_layout_state.advance_block_position(
pbm.padding_border_sums.block + content_size.block + clearance.unwrap_or_else(Au::zero),
);
sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin.block_end));
let content_rect = LogicalRect {
start_corner: LogicalVec2 {
block: pbm.padding.block_start +
pbm.border.block_start +
clearance.unwrap_or_else(Au::zero),
inline: pbm.padding.inline_start +
pbm.border.inline_start +
effective_margin_inline_start,
},
size: content_size,
};
let mut base_fragment_info = base.base_fragment_info;
if depends_on_block_constraints {
base_fragment_info.flags.insert(
FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM,
);
}
BoxFragment::new(
base_fragment_info,
style.clone(),
layout.fragments,
content_rect.as_physical(Some(containing_block)),
pbm.padding.to_physical(containing_block_writing_mode),
pbm.border.to_physical(containing_block_writing_mode),
margin.to_physical(containing_block_writing_mode),
clearance,
)
.with_baselines(layout.baselines)
.with_specific_layout_info(layout.specific_layout_info)
.with_block_margins_collapsed_with_children(CollapsedBlockMargins::from_margin(&margin))
}
}
impl ReplacedContents {
/// <https://drafts.csswg.org/css2/visudet.html#block-replaced-width>
/// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-width>
/// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-height>
fn layout_in_flow_block_level(
&self,
base: &LayoutBoxBase,
layout_context: &LayoutContext,
containing_block: &ContainingBlock,
mut sequential_layout_state: Option<&mut SequentialLayoutState>,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> BoxFragment {
let content_box_sizes_and_pbm = self
.layout_style(base)
.content_box_sizes_and_padding_border_margin(&containing_block.into());
let pbm = &content_box_sizes_and_pbm.pbm;
let content_size = self.used_size_as_if_inline_element(
containing_block,
&base.style,
&content_box_sizes_and_pbm,
ignore_block_margins_for_stretch,
);
let margin_inline_start;
let margin_inline_end;
let effective_margin_inline_start;
let (margin_block_start, margin_block_end) =
solve_block_margins_for_in_flow_block_level(pbm);
let justify_self = resolve_justify_self(&base.style, containing_block.style);
let containing_block_writing_mode = containing_block.style.writing_mode;
let physical_content_size = content_size.to_physical_size(containing_block_writing_mode);
let fragments = self.make_fragments(layout_context, &base.style, physical_content_size);
let clearance;
if let Some(ref mut sequential_layout_state) = sequential_layout_state {
// From https://drafts.csswg.org/css2/#floats:
// "The border box of a table, a block-level replaced element, or an element in
// the normal flow that establishes a new block formatting context (such as an
// element with overflow other than visible) must not overlap the margin box of
// any floats in the same block formatting context as the element itself. If
// necessary, implementations should clear the said element by placing it below
// any preceding floats, but may place it adjacent to such floats if there is
// sufficient space. They may even make the border box of said element narrower
// than defined by section 10.3.3. CSS 2 does not define when a UA may put said
// element next to the float or by how much said element may become narrower."
let collapsed_margin_block_start = CollapsedMargin::new(margin_block_start);
let size = content_size + pbm.padding_border_sums;
let placement_rect;
(clearance, placement_rect) = sequential_layout_state
.calculate_clearance_and_inline_adjustment(
Clear::from_style_and_container_writing_mode(
&base.style,
containing_block.style.writing_mode,
),
&collapsed_margin_block_start,
pbm,
size,
);
(
(margin_inline_start, margin_inline_end),
effective_margin_inline_start,
) = solve_inline_margins_avoiding_floats(
sequential_layout_state,
containing_block,
pbm,
size.inline,
placement_rect,
justify_self,
);
// Clearance prevents margin collapse between this block and previous ones,
// so in that case collapse margins before adjoining them below.
if clearance.is_some() {
sequential_layout_state.collapse_margins();
}
sequential_layout_state.adjoin_assign(&collapsed_margin_block_start);
// Margins can never collapse into replaced elements.
sequential_layout_state.collapse_margins();
sequential_layout_state
.advance_block_position(size.block + clearance.unwrap_or_else(Au::zero));
sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin_block_end));
} else {
clearance = None;
(
(margin_inline_start, margin_inline_end),
effective_margin_inline_start,
) = solve_inline_margins_for_in_flow_block_level(
containing_block,
pbm,
content_size.inline,
justify_self,
);
};
let margin = LogicalSides {
inline_start: margin_inline_start,
inline_end: margin_inline_end,
block_start: margin_block_start,
block_end: margin_block_end,
};
let start_corner = LogicalVec2 {
block: pbm.padding.block_start +
pbm.border.block_start +
clearance.unwrap_or_else(Au::zero),
inline: pbm.padding.inline_start +
pbm.border.inline_start +
effective_margin_inline_start,
};
let content_rect = LogicalRect {
start_corner,
size: content_size,
}
.as_physical(Some(containing_block));
let mut base_fragment_info = base.base_fragment_info;
if content_box_sizes_and_pbm.depends_on_block_constraints {
base_fragment_info.flags.insert(
FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM,
);
}
BoxFragment::new(
base_fragment_info,
base.style.clone(),
fragments,
content_rect,
pbm.padding.to_physical(containing_block_writing_mode),
pbm.border.to_physical(containing_block_writing_mode),
margin.to_physical(containing_block_writing_mode),
clearance,
)
.with_block_margins_collapsed_with_children(CollapsedBlockMargins::from_margin(&margin))
}
}
struct ContainingBlockPaddingAndBorder<'a> {
containing_block: ContainingBlock<'a>,
pbm: PaddingBorderMargin,
block_sizes: Sizes,
depends_on_block_constraints: bool,
available_block_size: Option<Au>,
justify_self: AlignFlags,
}
struct ResolvedMargins {
/// Used value for the margin properties, as exposed in getComputedStyle().
pub margin: LogicalSides<Au>,
/// Distance between the border box and the containing block on the inline-start side.
/// This is typically the same as the inline-start margin, but can be greater when
/// the box is justified within the free space in the containing block.
/// The reason we aren't just adjusting the used margin-inline-start is that
/// this shouldn't be observable via getComputedStyle().
/// <https://drafts.csswg.org/css-align/#justify-self-property>
pub effective_margin_inline_start: Au,
}
/// Given the style for an in-flow box and its containing block, determine the containing
/// block for its children.
/// Note that in the presence of floats, this shouldn't be used for a block-level box
/// that establishes an independent formatting context (or is replaced), since the
/// inline size could then be incorrect.
fn solve_containing_block_padding_and_border_for_in_flow_box<'a>(
containing_block: &ContainingBlock<'_>,
layout_style: &'a LayoutStyle,
get_inline_content_sizes: impl FnOnce(&ConstraintSpace) -> ContentSizes,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> ContainingBlockPaddingAndBorder<'a> {
let style = layout_style.style();
if matches!(style.pseudo(), Some(PseudoElement::ServoAnonymousBox)) {
// <https://drafts.csswg.org/css2/#anonymous-block-level>
// > Anonymous block boxes are ignored when resolving percentage values that would
// > refer to it: the closest non-anonymous ancestor box is used instead.
let containing_block_for_children = ContainingBlock {
size: ContainingBlockSize {
inline: containing_block.size.inline,
block: containing_block.size.block,
},
style,
};
// <https://drafts.csswg.org/css2/#anonymous-block-level>
// > Non-inherited properties have their initial value.
return ContainingBlockPaddingAndBorder {
containing_block: containing_block_for_children,
pbm: PaddingBorderMargin::zero(),
block_sizes: Sizes::default(),
depends_on_block_constraints: false,
// The available block size may actually be definite, but it should be irrelevant
// since the sizing properties are set to their initial value.
available_block_size: None,
// The initial `justify-self` is `auto`, but use `normal` (behaving as `stretch`).
// This is being discussed in <https://github.com/w3c/csswg-drafts/issues/11461>.
justify_self: AlignFlags::NORMAL,
};
}
let ContentBoxSizesAndPBM {
content_box_sizes,
pbm,
depends_on_block_constraints,
..
} = layout_style.content_box_sizes_and_padding_border_margin(&containing_block.into());
let pbm_sums = pbm.sums_auto_is_zero(ignore_block_margins_for_stretch);
let writing_mode = style.writing_mode;
let available_inline_size = Au::zero().max(containing_block.size.inline - pbm_sums.inline);
let available_block_size = containing_block
.size
.block
.to_definite()
.map(|block_size| Au::zero().max(block_size - pbm_sums.block));
// https://drafts.csswg.org/css2/#the-height-property
// https://drafts.csswg.org/css2/visudet.html#min-max-heights
let tentative_block_size = content_box_sizes.block.resolve_extrinsic(
Size::FitContent,
Au::zero(),
available_block_size,
);
// https://drafts.csswg.org/css2/#the-width-property
// https://drafts.csswg.org/css2/visudet.html#min-max-widths
let get_inline_content_sizes = || {
get_inline_content_sizes(&ConstraintSpace::new(
tentative_block_size,
writing_mode,
None, /* TODO: support preferred aspect ratios on non-replaced boxes */
))
};
let justify_self = resolve_justify_self(style, containing_block.style);
let is_table = layout_style.is_table();
let inline_size = content_box_sizes.inline.resolve(
Direction::Inline,
automatic_inline_size(justify_self, is_table),
Au::zero,
Some(available_inline_size),
get_inline_content_sizes,
is_table,
);
let containing_block_for_children = ContainingBlock {
size: ContainingBlockSize {
inline: inline_size,
block: tentative_block_size,
},
style,
};
// https://drafts.csswg.org/css-writing-modes/#orthogonal-flows
assert_eq!(
containing_block.style.writing_mode.is_horizontal(),
containing_block_for_children
.style
.writing_mode
.is_horizontal(),
"Vertical writing modes are not supported yet"
);
ContainingBlockPaddingAndBorder {
containing_block: containing_block_for_children,
pbm,
block_sizes: content_box_sizes.block,
depends_on_block_constraints,
available_block_size,
justify_self,
}
}
/// Given the containing block and size of an in-flow box, determine the margins.
/// Note that in the presence of floats, this shouldn't be used for a block-level box
/// that establishes an independent formatting context (or is replaced), since the
/// margins could then be incorrect.
fn solve_margins(
containing_block: &ContainingBlock<'_>,
pbm: &PaddingBorderMargin,
inline_size: Au,
justify_self: AlignFlags,
) -> ResolvedMargins {
let (inline_margins, effective_margin_inline_start) =
solve_inline_margins_for_in_flow_block_level(
containing_block,
pbm,
inline_size,
justify_self,
);
let block_margins = solve_block_margins_for_in_flow_block_level(pbm);
ResolvedMargins {
margin: LogicalSides {
inline_start: inline_margins.0,
inline_end: inline_margins.1,
block_start: block_margins.0,
block_end: block_margins.1,
},
effective_margin_inline_start,
}
}
/// Resolves 'auto' margins of an in-flow block-level box in the block axis.
/// <https://drafts.csswg.org/css2/#normal-block>
/// <https://drafts.csswg.org/css2/#block-root-margin>
fn solve_block_margins_for_in_flow_block_level(pbm: &PaddingBorderMargin) -> (Au, Au) {
(
pbm.margin.block_start.auto_is(Au::zero),
pbm.margin.block_end.auto_is(Au::zero),
)
}
/// Resolves the `justify-self` value, preserving flags.
fn resolve_justify_self(style: &ComputedValues, parent_style: &ComputedValues) -> AlignFlags {
let is_ltr = |style: &ComputedValues| style.writing_mode.line_left_is_inline_start();
let alignment = match style.clone_justify_self().0.0 {
AlignFlags::AUTO => parent_style.clone_justify_items().computed.0,
alignment => alignment,
};
let alignment_value = match alignment.value() {
AlignFlags::LEFT if is_ltr(parent_style) => AlignFlags::START,
AlignFlags::LEFT => AlignFlags::END,
AlignFlags::RIGHT if is_ltr(parent_style) => AlignFlags::END,
AlignFlags::RIGHT => AlignFlags::START,
AlignFlags::SELF_START if is_ltr(parent_style) == is_ltr(style) => AlignFlags::START,
AlignFlags::SELF_START => AlignFlags::END,
AlignFlags::SELF_END if is_ltr(parent_style) == is_ltr(style) => AlignFlags::END,
AlignFlags::SELF_END => AlignFlags::START,
alignment_value => alignment_value,
};
alignment.flags() | alignment_value
}
/// Determines the automatic size for the inline axis of a block-level box.
/// <https://drafts.csswg.org/css-sizing-3/#automatic-size>
#[inline]
fn automatic_inline_size<T>(justify_self: AlignFlags, is_table: bool) -> Size<T> {
match justify_self {
AlignFlags::STRETCH => Size::Stretch,
AlignFlags::NORMAL if !is_table => Size::Stretch,
_ => Size::FitContent,
}
}
/// Justifies a block-level box, distributing the free space according to `justify-self`.
/// Note `<center>` and `<div align>` are implemented via internal 'text-align' values,
/// which are also handled here.
/// The provided free space should already take margins into account. In particular,
/// it should be zero if there is an auto margin.
/// <https://drafts.csswg.org/css-align/#justify-block>
fn justify_self_alignment(
containing_block: &ContainingBlock,
free_space: Au,
justify_self: AlignFlags,
) -> Au {
let mut alignment = justify_self.value();
let is_safe = justify_self.flags() == AlignFlags::SAFE || alignment == AlignFlags::NORMAL;
if is_safe && free_space <= Au::zero() {
alignment = AlignFlags::START
}
match alignment {
AlignFlags::NORMAL => {},
AlignFlags::CENTER => return free_space / 2,
AlignFlags::END => return free_space,
_ => return Au::zero(),
}
// For `justify-self: normal`, fall back to the special 'text-align' values.
let style = containing_block.style;
match style.clone_text_align() {
TextAlignKeyword::MozCenter => free_space / 2,
TextAlignKeyword::MozLeft if !style.writing_mode.line_left_is_inline_start() => free_space,
TextAlignKeyword::MozRight if style.writing_mode.line_left_is_inline_start() => free_space,
_ => Au::zero(),
}
}
/// Resolves 'auto' margins of an in-flow block-level box in the inline axis,
/// distributing the free space in the containing block.
///
/// This is based on CSS2.1 § 10.3.3 <https://drafts.csswg.org/css2/#blockwidth>
/// but without adjusting the margins in "over-contrained" cases, as mandated by
/// <https://drafts.csswg.org/css-align/#justify-block>.
///
/// Note that in the presence of floats, this shouldn't be used for a block-level box
/// that establishes an independent formatting context (or is replaced).
///
/// In addition to the used margins, it also returns the effective margin-inline-start
/// (see ContainingBlockPaddingAndBorder).
fn solve_inline_margins_for_in_flow_block_level(
containing_block: &ContainingBlock,
pbm: &PaddingBorderMargin,
inline_size: Au,
justify_self: AlignFlags,
) -> ((Au, Au), Au) {
let free_space = containing_block.size.inline - pbm.padding_border_sums.inline - inline_size;
let mut justification = Au::zero();
let inline_margins = match (pbm.margin.inline_start, pbm.margin.inline_end) {
(AuOrAuto::Auto, AuOrAuto::Auto) => {
let start = Au::zero().max(free_space / 2);
(start, free_space - start)
},
(AuOrAuto::Auto, AuOrAuto::LengthPercentage(end)) => {
(Au::zero().max(free_space - end), end)
},
(AuOrAuto::LengthPercentage(start), AuOrAuto::Auto) => (start, free_space - start),
(AuOrAuto::LengthPercentage(start), AuOrAuto::LengthPercentage(end)) => {
// In the cases above, the free space is zero after taking 'auto' margins into account.
// But here we may still have some free space to perform 'justify-self' alignment.
// This aligns the margin box within the containing block, or in other words,
// aligns the border box within the margin-shrunken containing block.
justification =
justify_self_alignment(containing_block, free_space - start - end, justify_self);
(start, end)
},
};
let effective_margin_inline_start = inline_margins.0 + justification;
(inline_margins, effective_margin_inline_start)
}
/// Resolves 'auto' margins of an in-flow block-level box in the inline axis
/// similarly to |solve_inline_margins_for_in_flow_block_level|. However,
/// they align within the provided rect (instead of the containing block),
/// to avoid overlapping floats.
/// In addition to the used margins, it also returns the effective
/// margin-inline-start (see ContainingBlockPaddingAndBorder).
/// It may differ from the used inline-start margin if the computed value
/// wasn't 'auto' and there are floats to avoid or the box is justified.
/// See <https://github.com/w3c/csswg-drafts/issues/9174>
fn solve_inline_margins_avoiding_floats(
sequential_layout_state: &SequentialLayoutState,
containing_block: &ContainingBlock,
pbm: &PaddingBorderMargin,
inline_size: Au,
placement_rect: LogicalRect<Au>,
justify_self: AlignFlags,
) -> ((Au, Au), Au) {
let free_space = placement_rect.size.inline - inline_size;
debug_assert!(free_space >= Au::zero());
let cb_info = &sequential_layout_state.floats.containing_block_info;
let start_adjustment = placement_rect.start_corner.inline - cb_info.inline_start;
let end_adjustment = cb_info.inline_end - placement_rect.max_inline_position();
let mut justification = Au::zero();
let inline_margins = match (pbm.margin.inline_start, pbm.margin.inline_end) {
(AuOrAuto::Auto, AuOrAuto::Auto) => {
let half = free_space / 2;
(start_adjustment + half, end_adjustment + free_space - half)
},
(AuOrAuto::Auto, AuOrAuto::LengthPercentage(end)) => (start_adjustment + free_space, end),
(AuOrAuto::LengthPercentage(start), AuOrAuto::Auto) => (start, end_adjustment + free_space),
(AuOrAuto::LengthPercentage(start), AuOrAuto::LengthPercentage(end)) => {
// The spec says 'justify-self' aligns the margin box within the float-shrunken
// containing block. That's wrong (https://github.com/w3c/csswg-drafts/issues/9963),
// and Blink and WebKit are broken anyways. So we match Gecko instead: this aligns
// the border box within the instersection of the float-shrunken containing-block
// and the margin-shrunken containing-block.
justification = justify_self_alignment(containing_block, free_space, justify_self);
(start, end)
},
};
let effective_margin_inline_start = inline_margins.0.max(start_adjustment) + justification;
(inline_margins, effective_margin_inline_start)
}
/// State that we maintain when placing blocks.
///
/// In parallel mode, this placement is done after all child blocks are laid out. In
/// sequential mode, this is done right after each block is laid out.
struct PlacementState<'container> {
next_in_flow_margin_collapses_with_parent_start_margin: bool,
last_in_flow_margin_collapses_with_parent_end_margin: bool,
start_margin: CollapsedMargin,
current_margin: CollapsedMargin,
current_block_direction_position: Au,
inflow_baselines: Baselines,
is_inline_block_context: bool,
/// If this [`PlacementState`] is laying out a list item with an outside marker. Record the
/// block size of that marker, because the content block size of the list item needs to be at
/// least as tall as the marker size -- even though the marker doesn't advance the block
/// position of the placement.
marker_block_size: Option<Au>,
/// The [`ContainingBlock`] of the container into which this [`PlacementState`] is laying out
/// fragments. This is used to convert between physical and logical geometry.
containing_block: &'container ContainingBlock<'container>,
}
impl<'container> PlacementState<'container> {
fn new(
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
containing_block: &'container ContainingBlock<'container>,
) -> PlacementState<'container> {
let is_inline_block_context =
containing_block.style.get_box().clone_display() == Display::InlineBlock;
PlacementState {
next_in_flow_margin_collapses_with_parent_start_margin:
collapsible_with_parent_start_margin.0,
last_in_flow_margin_collapses_with_parent_end_margin: true,
start_margin: CollapsedMargin::zero(),
current_margin: CollapsedMargin::zero(),
current_block_direction_position: Au::zero(),
inflow_baselines: Baselines::default(),
is_inline_block_context,
marker_block_size: None,
containing_block,
}
}
fn place_fragment_and_update_baseline(
&mut self,
fragment: &mut Fragment,
sequential_layout_state: Option<&mut SequentialLayoutState>,
) {
self.place_fragment(fragment, sequential_layout_state);
let box_fragment = match fragment {
Fragment::Box(box_fragment) => box_fragment,
_ => return,
};
let box_fragment = box_fragment.borrow();
// From <https://drafts.csswg.org/css-align-3/#baseline-export>:
// > When finding the first/last baseline set of an inline-block, any baselines
// > contributed by table boxes must be skipped. (This quirk is a legacy behavior from
// > [CSS2].)
if self.is_inline_block_context && box_fragment.is_table_wrapper() {
return;
}
let box_block_offset = box_fragment
.content_rect
.origin
.to_logical(self.containing_block)
.block;
let box_fragment_baselines =
box_fragment.baselines(self.containing_block.style.writing_mode);
if let (None, Some(first)) = (self.inflow_baselines.first, box_fragment_baselines.first) {
self.inflow_baselines.first = Some(first + box_block_offset);
}
if let Some(last) = box_fragment_baselines.last {
self.inflow_baselines.last = Some(last + box_block_offset);
}
}
/// Place a single [Fragment] in a block level context using the state so far and
/// information gathered from the [Fragment] itself.
fn place_fragment(
&mut self,
fragment: &mut Fragment,
sequential_layout_state: Option<&mut SequentialLayoutState>,
) {
match fragment {
Fragment::Box(fragment) => {
// If this child is a marker positioned outside of a list item, then record its
// size, but also ensure that it doesn't advance the block position of the placment.
// This ensures item content is placed next to the marker.
//
// This is a pretty big hack because it doesn't properly handle all interactions
// between the marker and the item. For instance the marker should be positioned at
// the baseline of list item content and the first line of the item content should
// be at least as tall as the marker -- not the entire list item itself.
let fragment = &mut *fragment.borrow_mut();
let is_outside_marker = fragment
.base
.flags
.contains(FragmentFlags::IS_OUTSIDE_LIST_ITEM_MARKER);
if is_outside_marker {
assert!(self.marker_block_size.is_none());
self.marker_block_size = Some(
fragment
.content_rect
.size
.to_logical(self.containing_block.style.writing_mode)
.block,
);
return;
}
let fragment_block_margins = fragment.block_margins_collapsed_with_children();
let mut fragment_block_size = fragment
.border_rect()
.size
.to_logical(self.containing_block.style.writing_mode)
.block;
// We use `last_in_flow_margin_collapses_with_parent_end_margin` to implement
// this quote from https://drafts.csswg.org/css2/#collapsing-margins
// > If the top and bottom margins of an element with clearance are adjoining,
// > its margins collapse with the adjoining margins of following siblings but that
// > resulting margin does not collapse with the bottom margin of the parent block.
if let Some(clearance) = fragment.clearance {
fragment_block_size += clearance;
// Margins can't be adjoining if they are separated by clearance.
// Setting `next_in_flow_margin_collapses_with_parent_start_margin` to false
// prevents collapsing with the start margin of the parent, and will set
// `collapsed_through` to false, preventing the parent from collapsing through.
self.current_block_direction_position += self.current_margin.solve();
self.current_margin = CollapsedMargin::zero();
self.next_in_flow_margin_collapses_with_parent_start_margin = false;
if fragment_block_margins.collapsed_through {
self.last_in_flow_margin_collapses_with_parent_end_margin = false;
}
} else if !fragment_block_margins.collapsed_through {
self.last_in_flow_margin_collapses_with_parent_end_margin = true;
}
if self.next_in_flow_margin_collapses_with_parent_start_margin {
debug_assert!(self.current_margin.solve().is_zero());
self.start_margin
.adjoin_assign(&fragment_block_margins.start);
if fragment_block_margins.collapsed_through {
self.start_margin.adjoin_assign(&fragment_block_margins.end);
return;
}
self.next_in_flow_margin_collapses_with_parent_start_margin = false;
} else {
self.current_margin
.adjoin_assign(&fragment_block_margins.start);
}
fragment.content_rect.origin += LogicalVec2 {
inline: Au::zero(),
block: self.current_margin.solve() + self.current_block_direction_position,
}
.to_physical_size(self.containing_block.style.writing_mode);
if fragment_block_margins.collapsed_through {
// `fragment_block_size` is typically zero when collapsing through,
// but we still need to consider it in case there is clearance.
self.current_block_direction_position += fragment_block_size;
self.current_margin
.adjoin_assign(&fragment_block_margins.end);
} else {
self.current_block_direction_position +=
self.current_margin.solve() + fragment_block_size;
self.current_margin = fragment_block_margins.end;
}
},
Fragment::AbsoluteOrFixedPositioned(fragment) => {
// The alignment of absolutes in block flow layout is always "start", so the size of
// the static position rectangle does not matter.
fragment.borrow_mut().static_position_rect = LogicalRect {
start_corner: LogicalVec2 {
block: (self.current_margin.solve() +
self.current_block_direction_position),
inline: Au::zero(),
},
size: LogicalVec2::zero(),
}
.as_physical(Some(self.containing_block));
},
Fragment::Float(box_fragment) => {
let sequential_layout_state = sequential_layout_state
.expect("Found float fragment without SequentialLayoutState");
let block_offset_from_containing_block_top =
self.current_block_direction_position + self.current_margin.solve();
let box_fragment = &mut *box_fragment.borrow_mut();
sequential_layout_state.place_float_fragment(
box_fragment,
self.containing_block,
self.start_margin,
block_offset_from_containing_block_top,
);
},
Fragment::Positioning(_) => {},
_ => unreachable!(),
}
}
fn finish(mut self) -> (Au, CollapsedBlockMargins, Baselines) {
if !self.last_in_flow_margin_collapses_with_parent_end_margin {
self.current_block_direction_position += self.current_margin.solve();
self.current_margin = CollapsedMargin::zero();
}
let (total_block_size, collapsed_through) = match self.marker_block_size {
Some(marker_block_size) => (
self.current_block_direction_position.max(marker_block_size),
// If this is a list item (even empty) with an outside marker, then it
// should not collapse through.
false,
),
None => (
self.current_block_direction_position,
self.next_in_flow_margin_collapses_with_parent_start_margin,
),
};
(
total_block_size,
CollapsedBlockMargins {
collapsed_through,
start: self.start_margin,
end: self.current_margin,
},
self.inflow_baselines,
)
}
}
fn block_size_is_zero_or_intrinsic(size: &StyleSize, containing_block: &ContainingBlock) -> bool {
match size {
StyleSize::Auto |
StyleSize::MinContent |
StyleSize::MaxContent |
StyleSize::FitContent |
StyleSize::FitContentFunction(_) => true,
StyleSize::Stretch => {
// TODO: Should this return true when the containing block has a definite size of 0px?
!containing_block.size.block.is_definite()
},
StyleSize::LengthPercentage(lp) => {
// TODO: Should this resolve definite percentages? Blink does it, Gecko and WebKit don't.
lp.is_definitely_zero() ||
(lp.0.has_percentage() && !containing_block.size.block.is_definite())
},
StyleSize::AnchorSizeFunction(_) | StyleSize::AnchorContainingCalcFunction(_) => {
unreachable!("anchor-size() should be disabled")
},
}
}
pub(crate) struct IndependentFloatOrAtomicLayoutResult {
pub fragment: BoxFragment,
pub baselines: Option<Baselines>,
pub pbm_sums: LogicalSides<Au>,
}
impl IndependentFormattingContext {
pub(crate) fn layout_in_flow_block_level(
&self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
sequential_layout_state: Option<&mut SequentialLayoutState>,
ignore_block_margins_for_stretch: LogicalSides1D<bool>,
) -> BoxFragment {
match &self.contents {
IndependentFormattingContextContents::NonReplaced(contents) => contents
.layout_in_flow_block_level(
&self.base,
layout_context,
positioning_context,
containing_block,
sequential_layout_state,
ignore_block_margins_for_stretch,
),
IndependentFormattingContextContents::Replaced(contents) => contents
.layout_in_flow_block_level(
&self.base,
layout_context,
containing_block,
sequential_layout_state,
ignore_block_margins_for_stretch,
),
}
}
pub(crate) fn layout_float_or_atomic_inline(
&self,
layout_context: &LayoutContext,
child_positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
) -> IndependentFloatOrAtomicLayoutResult {
let style = self.style();
let container_writing_mode = containing_block.style.writing_mode;
let layout_style = self.layout_style();
let content_box_sizes_and_pbm =
layout_style.content_box_sizes_and_padding_border_margin(&containing_block.into());
let pbm = &content_box_sizes_and_pbm.pbm;
let margin = pbm.margin.auto_is(Au::zero);
let pbm_sums = pbm.padding + pbm.border + margin;
let (fragments, content_rect, baselines) = match &self.contents {
IndependentFormattingContextContents::Replaced(replaced) => {
// Floats and atomic inlines can't collapse margins with their parent,
// so don't ignore block margins when resolving a stretch block size.
// https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing
let ignore_block_margins_for_stretch = LogicalSides1D::new(false, false);
// https://drafts.csswg.org/css2/visudet.html#float-replaced-width
// https://drafts.csswg.org/css2/visudet.html#inline-replaced-height
let content_size = replaced
.used_size_as_if_inline_element(
containing_block,
style,
&content_box_sizes_and_pbm,
ignore_block_margins_for_stretch,
)
.to_physical_size(container_writing_mode);
let fragments = replaced.make_fragments(layout_context, style, content_size);
let content_rect = PhysicalRect::new(PhysicalPoint::zero(), content_size);
(fragments, content_rect, None)
},
IndependentFormattingContextContents::NonReplaced(non_replaced) => {
let writing_mode = self.style().writing_mode;
let available_inline_size =
Au::zero().max(containing_block.size.inline - pbm_sums.inline_sum());
let available_block_size = containing_block
.size
.block
.to_definite()
.map(|block_size| Au::zero().max(block_size - pbm_sums.block_sum()));
let tentative_block_size = content_box_sizes_and_pbm
.content_box_sizes
.block
.resolve_extrinsic(Size::FitContent, Au::zero(), available_block_size);
let get_content_size = || {
let constraint_space = ConstraintSpace::new(
tentative_block_size,
writing_mode,
non_replaced.preferred_aspect_ratio(),
);
self.inline_content_sizes(layout_context, &constraint_space)
.sizes
};
let is_table = layout_style.is_table();
let inline_size = content_box_sizes_and_pbm.content_box_sizes.inline.resolve(
Direction::Inline,
Size::FitContent,
Au::zero,
Some(available_inline_size),
get_content_size,
is_table,
);
let containing_block_for_children = ContainingBlock {
size: ContainingBlockSize {
inline: inline_size,
block: tentative_block_size,
},
style: self.style(),
};
assert_eq!(
container_writing_mode.is_horizontal(),
writing_mode.is_horizontal(),
"Mixed horizontal and vertical writing modes are not supported yet"
);
let lazy_block_size = LazySize::new(
&content_box_sizes_and_pbm.content_box_sizes.block,
Direction::Block,
Size::FitContent,
Au::zero,
available_block_size,
is_table,
);
let independent_layout = non_replaced.layout(
layout_context,
child_positioning_context,
&containing_block_for_children,
containing_block,
&self.base,
false, /* depends_on_block_constraints */
&lazy_block_size,
);
let inline_size = independent_layout
.content_inline_size_for_table
.unwrap_or(inline_size);
let block_size = lazy_block_size.resolve(|| independent_layout.content_block_size);
let content_size = LogicalVec2 {
block: block_size,
inline: inline_size,
}
.to_physical_size(container_writing_mode);
let content_rect = PhysicalRect::new(PhysicalPoint::zero(), content_size);
(
independent_layout.fragments,
content_rect,
Some(independent_layout.baselines),
)
},
};
let mut base_fragment_info = self.base_fragment_info();
if content_box_sizes_and_pbm.depends_on_block_constraints {
base_fragment_info.flags.insert(
FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM,
);
}
let fragment = BoxFragment::new(
base_fragment_info,
self.style().clone(),
fragments,
content_rect,
pbm.padding.to_physical(container_writing_mode),
pbm.border.to_physical(container_writing_mode),
margin.to_physical(container_writing_mode),
// Floats can have clearance, but it's handled internally by the float placement logic,
// so there's no need to store it explicitly in the fragment.
// And atomic inlines don't have clearance.
None, /* clearance */
);
IndependentFloatOrAtomicLayoutResult {
fragment,
baselines,
pbm_sums,
}
}
}