mirror of
https://github.com/servo/servo.git
synced 2025-06-20 23:28:59 +01:00
Margins should be able to collapse through floats when collapsing with parent blocks (the containing block). To properly place floats in this situation, we need to look at these subsequent floats to find out how much of the margin will collapse with the parent. This initial implementation is very basic and the second step would be to cache this in order to avoid having to constantly recalculate it. Fixes #29915. Co-authored-by: Oriol Brufau <obrufau@igalia.com>
1023 lines
42 KiB
Rust
1023 lines
42 KiB
Rust
/* 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/. */
|
|
|
|
//! Flow layout, also known as block-and-inline layout.
|
|
|
|
use crate::cell::ArcRefCell;
|
|
use crate::context::LayoutContext;
|
|
use crate::flow::float::{ClearSide, ContainingBlockPositionInfo, FloatBox, SequentialLayoutState};
|
|
use crate::flow::inline::InlineFormattingContext;
|
|
use crate::formatting_contexts::{
|
|
IndependentFormattingContext, IndependentLayout, NonReplacedFormattingContext,
|
|
};
|
|
use crate::fragment_tree::{
|
|
BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment,
|
|
};
|
|
use crate::geom::flow_relative::{Rect, Sides, Vec2};
|
|
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
|
|
use crate::replaced::ReplacedContent;
|
|
use crate::sizing::{self, ContentSizes};
|
|
use crate::style_ext::{ComputedValuesExt, PaddingBorderMargin};
|
|
use crate::ContainingBlock;
|
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
|
use servo_arc::Arc;
|
|
use style::computed_values::clear::T as Clear;
|
|
use style::computed_values::float::T as Float;
|
|
use style::logical_geometry::WritingMode;
|
|
use style::properties::ComputedValues;
|
|
use style::values::computed::{CSSPixelLength, Length, LengthOrAuto};
|
|
use style::Zero;
|
|
|
|
mod construct;
|
|
pub mod float;
|
|
pub mod inline;
|
|
mod root;
|
|
|
|
pub use root::{BoxTree, CanvasBackground};
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub(crate) struct BlockFormattingContext {
|
|
pub contents: BlockContainer,
|
|
pub contains_floats: bool,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub(crate) enum BlockContainer {
|
|
BlockLevelBoxes(Vec<ArcRefCell<BlockLevelBox>>),
|
|
InlineFormattingContext(InlineFormattingContext),
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub(crate) enum BlockLevelBox {
|
|
SameFormattingContextBlock {
|
|
base_fragment_info: BaseFragmentInfo,
|
|
#[serde(skip_serializing)]
|
|
style: Arc<ComputedValues>,
|
|
contents: BlockContainer,
|
|
},
|
|
OutOfFlowAbsolutelyPositionedBox(ArcRefCell<AbsolutelyPositionedBox>),
|
|
OutOfFlowFloatBox(FloatBox),
|
|
Independent(IndependentFormattingContext),
|
|
}
|
|
|
|
impl BlockLevelBox {
|
|
fn find_block_margin_collapsing_with_parent_for_floats(
|
|
&self,
|
|
collected_margin: &mut CollapsedMargin,
|
|
containing_block_writing_mode: WritingMode,
|
|
) -> bool {
|
|
// TODO(mrobinson,Loirooriol): Cache margins here so that we don't constantly
|
|
// have to keep looking forward when dealing with sequences of floats.
|
|
let style = match self {
|
|
BlockLevelBox::SameFormattingContextBlock { ref style, .. } => &style,
|
|
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_) |
|
|
BlockLevelBox::OutOfFlowFloatBox(_) => return true,
|
|
BlockLevelBox::Independent(ref context) => context.style(),
|
|
};
|
|
|
|
let margin = style.margin(containing_block_writing_mode);
|
|
let border = style.border_width(containing_block_writing_mode);
|
|
let padding = style.padding(containing_block_writing_mode);
|
|
|
|
if style.get_box().clear != Clear::None {
|
|
return false;
|
|
}
|
|
|
|
let start_margin = margin
|
|
.block_start
|
|
.percentage_relative_to(CSSPixelLength::zero())
|
|
.auto_is(CSSPixelLength::zero);
|
|
collected_margin.adjoin_assign(&CollapsedMargin::new(start_margin));
|
|
|
|
let start_padding_is_zero = padding.block_start.is_zero();
|
|
let start_border_is_zero = border.block_start.is_zero();
|
|
if !start_border_is_zero || !start_padding_is_zero {
|
|
return false;
|
|
}
|
|
|
|
let contents = match self {
|
|
BlockLevelBox::SameFormattingContextBlock { ref contents, .. } => contents,
|
|
_ => return false,
|
|
};
|
|
match contents {
|
|
BlockContainer::BlockLevelBoxes(boxes) => {
|
|
if !Self::find_block_margin_collapsing_with_parent_for_floats_from_slice(
|
|
&boxes,
|
|
collected_margin,
|
|
style.writing_mode,
|
|
) {
|
|
return false;
|
|
}
|
|
},
|
|
BlockContainer::InlineFormattingContext(_) => return false,
|
|
}
|
|
|
|
let block_size_zero =
|
|
style.content_block_size().is_definitely_zero() || style.content_block_size().is_auto();
|
|
if !style.min_block_size().is_definitely_zero() ||
|
|
!block_size_zero ||
|
|
!border.block_end.is_zero() ||
|
|
!padding.block_end.is_zero()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
let end_margin = margin
|
|
.block_end
|
|
.percentage_relative_to(CSSPixelLength::zero())
|
|
.auto_is(CSSPixelLength::zero);
|
|
collected_margin.adjoin_assign(&CollapsedMargin::new(end_margin));
|
|
|
|
true
|
|
}
|
|
|
|
fn find_block_margin_collapsing_with_parent_for_floats_from_slice(
|
|
boxes: &[ArcRefCell<BlockLevelBox>],
|
|
margin: &mut CollapsedMargin,
|
|
writing_mode: WritingMode,
|
|
) -> bool {
|
|
boxes.iter().all(|block_level_box| {
|
|
block_level_box
|
|
.borrow()
|
|
.find_block_margin_collapsing_with_parent_for_floats(margin, writing_mode)
|
|
})
|
|
}
|
|
}
|
|
|
|
struct FlowLayout {
|
|
pub fragments: Vec<Fragment>,
|
|
pub content_block_size: Length,
|
|
pub collapsible_margins_in_children: CollapsedBlockMargins,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct CollapsibleWithParentStartMargin(bool);
|
|
|
|
impl BlockFormattingContext {
|
|
pub(super) fn layout(
|
|
&self,
|
|
layout_context: &LayoutContext,
|
|
positioning_context: &mut PositioningContext,
|
|
containing_block: &ContainingBlock,
|
|
) -> IndependentLayout {
|
|
let mut sequential_layout_state = if self.contains_floats || !layout_context.use_rayon {
|
|
Some(SequentialLayoutState::new(containing_block.inline_size))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let flow_layout = self.contents.layout(
|
|
layout_context,
|
|
positioning_context,
|
|
containing_block,
|
|
sequential_layout_state.as_mut(),
|
|
CollapsibleWithParentStartMargin(false),
|
|
);
|
|
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.map_or(Length::zero(), |sequential_layout_state| {
|
|
sequential_layout_state.calculate_clearance(ClearSide::Both)
|
|
});
|
|
|
|
IndependentLayout {
|
|
fragments: flow_layout.fragments,
|
|
content_block_size: flow_layout.content_block_size +
|
|
flow_layout.collapsible_margins_in_children.end.solve() +
|
|
clearance,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 calculate_inline_content_size_for_block_level_boxes(
|
|
boxes: &[ArcRefCell<BlockLevelBox>],
|
|
layout_context: &LayoutContext,
|
|
writing_mode: WritingMode,
|
|
) -> ContentSizes {
|
|
let get_box_info = |box_: &ArcRefCell<BlockLevelBox>| {
|
|
let size = box_
|
|
.borrow_mut()
|
|
.inline_content_sizes(layout_context, writing_mode);
|
|
if let BlockLevelBox::OutOfFlowFloatBox(ref float_box) = *box_.borrow_mut() {
|
|
let style_box = &float_box.contents.style().get_box();
|
|
(size, style_box.float, style_box.clear)
|
|
} else {
|
|
// The element may in fact have clearance, but the logic below ignores it,
|
|
// so don't bother retrieving it from the style.
|
|
(size, Float::None, Clear::None)
|
|
}
|
|
};
|
|
|
|
/// When iterating the block-level boxes to compute the inline content sizes,
|
|
/// this struct contains the data accumulated up to the current box.
|
|
struct AccumulatedData {
|
|
/// The maximum size seen so far, not including trailing uncleared floats.
|
|
max_size: ContentSizes,
|
|
/// The size of the trailing uncleared floats with 'float: left'.
|
|
left_floats: ContentSizes,
|
|
/// The size of the trailing uncleared floats with 'float: right'.
|
|
right_floats: ContentSizes,
|
|
}
|
|
|
|
impl AccumulatedData {
|
|
fn max_size_including_uncleared_floats(&self) -> ContentSizes {
|
|
self.max_size.max(self.left_floats.add(&self.right_floats))
|
|
}
|
|
fn clear_floats(&mut self, clear: Clear) {
|
|
match clear {
|
|
Clear::Left => {
|
|
self.max_size = self.max_size_including_uncleared_floats();
|
|
self.left_floats = ContentSizes::zero();
|
|
},
|
|
Clear::Right => {
|
|
self.max_size = self.max_size_including_uncleared_floats();
|
|
self.right_floats = ContentSizes::zero();
|
|
},
|
|
Clear::Both => {
|
|
self.max_size = self.max_size_including_uncleared_floats();
|
|
self.left_floats = ContentSizes::zero();
|
|
self.right_floats = ContentSizes::zero();
|
|
},
|
|
Clear::None => {},
|
|
};
|
|
}
|
|
}
|
|
|
|
let accumulate = |mut data: AccumulatedData, (size, float, clear)| {
|
|
if float == Float::None {
|
|
// TODO: The first BFC root after a sequence of floats should appear next to them
|
|
// (if it doesn't have clearance).
|
|
data.clear_floats(Clear::Both);
|
|
data.max_size = data.max_size.max(size);
|
|
} else {
|
|
data.clear_floats(clear);
|
|
match float {
|
|
Float::Left => data.left_floats = data.left_floats.add(&size),
|
|
Float::Right => data.right_floats = data.right_floats.add(&size),
|
|
Float::None => unreachable!(),
|
|
}
|
|
}
|
|
data
|
|
};
|
|
let zero = AccumulatedData {
|
|
max_size: ContentSizes::zero(),
|
|
left_floats: ContentSizes::zero(),
|
|
right_floats: ContentSizes::zero(),
|
|
};
|
|
let data = if layout_context.use_rayon {
|
|
boxes
|
|
.par_iter()
|
|
.map(get_box_info)
|
|
.collect::<Vec<_>>()
|
|
.into_iter()
|
|
.fold(zero, accumulate)
|
|
} else {
|
|
boxes.iter().map(get_box_info).fold(zero, accumulate)
|
|
};
|
|
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,
|
|
) -> FlowLayout {
|
|
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,
|
|
),
|
|
BlockContainer::InlineFormattingContext(ifc) => ifc.layout(
|
|
layout_context,
|
|
positioning_context,
|
|
containing_block,
|
|
sequential_layout_state,
|
|
),
|
|
}
|
|
}
|
|
|
|
pub(super) fn inline_content_sizes(
|
|
&self,
|
|
layout_context: &LayoutContext,
|
|
writing_mode: WritingMode,
|
|
) -> ContentSizes {
|
|
match &self {
|
|
Self::BlockLevelBoxes(boxes) => calculate_inline_content_size_for_block_level_boxes(
|
|
boxes,
|
|
layout_context,
|
|
writing_mode,
|
|
),
|
|
Self::InlineFormattingContext(context) => {
|
|
context.inline_content_sizes(layout_context, writing_mode)
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
) -> FlowLayout {
|
|
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,
|
|
collapsible_with_parent_start_margin,
|
|
),
|
|
None => layout_block_level_children_in_parallel(
|
|
layout_context,
|
|
positioning_context,
|
|
child_boxes,
|
|
containing_block,
|
|
collapsible_with_parent_start_margin,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn layout_block_level_children_in_parallel(
|
|
layout_context: &LayoutContext,
|
|
positioning_context: &mut PositioningContext,
|
|
child_boxes: &[ArcRefCell<BlockLevelBox>],
|
|
containing_block: &ContainingBlock,
|
|
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
|
|
) -> FlowLayout {
|
|
let collects_for_nearest_positioned_ancestor =
|
|
positioning_context.collects_for_nearest_positioned_ancestor();
|
|
let layout_results: Vec<(Fragment, PositioningContext)> = child_boxes
|
|
.par_iter()
|
|
.map(|child_box| {
|
|
let mut child_positioning_context =
|
|
PositioningContext::new_for_subtree(collects_for_nearest_positioned_ancestor);
|
|
let fragment = child_box.borrow_mut().layout(
|
|
layout_context,
|
|
&mut child_positioning_context,
|
|
containing_block,
|
|
/* sequential_layout_state = */ None,
|
|
);
|
|
(fragment, child_positioning_context)
|
|
})
|
|
.collect();
|
|
|
|
let mut placement_state = PlacementState::new(collapsible_with_parent_start_margin);
|
|
let fragments = layout_results
|
|
.into_iter()
|
|
.map(|(mut fragment, mut child_positioning_context)| {
|
|
placement_state.place_fragment(&mut fragment, None);
|
|
child_positioning_context.adjust_static_position_of_hoisted_fragments(&fragment);
|
|
positioning_context.append(child_positioning_context);
|
|
fragment
|
|
})
|
|
.collect();
|
|
|
|
let (content_block_size, collapsible_margins_in_children) = placement_state.finish();
|
|
FlowLayout {
|
|
fragments,
|
|
content_block_size,
|
|
collapsible_margins_in_children,
|
|
}
|
|
}
|
|
|
|
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,
|
|
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
|
|
) -> FlowLayout {
|
|
let mut placement_state = PlacementState::new(collapsible_with_parent_start_margin);
|
|
let collects_for_nearest_positioned_ancestor =
|
|
positioning_context.collects_for_nearest_positioned_ancestor();
|
|
|
|
// 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).
|
|
let fragments = child_boxes
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, child_box)| {
|
|
let mut child_positioning_context =
|
|
PositioningContext::new_for_subtree(collects_for_nearest_positioned_ancestor);
|
|
let mut fragment = child_box.borrow_mut().layout(
|
|
layout_context,
|
|
&mut child_positioning_context,
|
|
containing_block,
|
|
Some(&mut *sequential_layout_state),
|
|
);
|
|
|
|
let sequential_info = PlacementStateSequentialInfo {
|
|
sequential_layout_state,
|
|
following_boxes: &child_boxes[index..],
|
|
};
|
|
placement_state.place_fragment(&mut fragment, Some(sequential_info));
|
|
|
|
child_positioning_context.adjust_static_position_of_hoisted_fragments(&fragment);
|
|
positioning_context.append(child_positioning_context);
|
|
|
|
fragment
|
|
})
|
|
.collect();
|
|
|
|
let (content_block_size, collapsible_margins_in_children) = placement_state.finish();
|
|
FlowLayout {
|
|
fragments,
|
|
content_block_size,
|
|
collapsible_margins_in_children,
|
|
}
|
|
}
|
|
|
|
impl BlockLevelBox {
|
|
fn layout(
|
|
&mut self,
|
|
layout_context: &LayoutContext,
|
|
positioning_context: &mut PositioningContext,
|
|
containing_block: &ContainingBlock,
|
|
sequential_layout_state: Option<&mut SequentialLayoutState>,
|
|
) -> Fragment {
|
|
match self {
|
|
BlockLevelBox::SameFormattingContextBlock {
|
|
base_fragment_info: tag,
|
|
style,
|
|
contents,
|
|
} => Fragment::Box(positioning_context.layout_maybe_position_relative_fragment(
|
|
layout_context,
|
|
containing_block,
|
|
style,
|
|
|positioning_context| {
|
|
layout_in_flow_non_replaced_block_level(
|
|
layout_context,
|
|
positioning_context,
|
|
containing_block,
|
|
*tag,
|
|
style,
|
|
NonReplacedContents::SameFormattingContextBlock(contents),
|
|
sequential_layout_state,
|
|
)
|
|
},
|
|
)),
|
|
BlockLevelBox::Independent(independent) => match independent {
|
|
IndependentFormattingContext::Replaced(replaced) => {
|
|
Fragment::Box(positioning_context.layout_maybe_position_relative_fragment(
|
|
layout_context,
|
|
containing_block,
|
|
&replaced.style,
|
|
|_positioning_context| {
|
|
layout_in_flow_replaced_block_level(
|
|
containing_block,
|
|
replaced.base_fragment_info,
|
|
&replaced.style,
|
|
&replaced.contents,
|
|
sequential_layout_state,
|
|
)
|
|
},
|
|
))
|
|
},
|
|
IndependentFormattingContext::NonReplaced(non_replaced) => {
|
|
Fragment::Box(positioning_context.layout_maybe_position_relative_fragment(
|
|
layout_context,
|
|
containing_block,
|
|
&non_replaced.style,
|
|
|positioning_context| {
|
|
layout_in_flow_non_replaced_block_level(
|
|
layout_context,
|
|
positioning_context,
|
|
containing_block,
|
|
non_replaced.base_fragment_info,
|
|
&non_replaced.style,
|
|
NonReplacedContents::EstablishesAnIndependentFormattingContext(
|
|
non_replaced,
|
|
),
|
|
sequential_layout_state,
|
|
)
|
|
},
|
|
))
|
|
},
|
|
},
|
|
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(box_) => {
|
|
let hoisted_box = AbsolutelyPositionedBox::to_hoisted(
|
|
box_.clone(),
|
|
// This is incorrect, however we do not know the
|
|
// correct positioning until later, in place_block_level_fragment,
|
|
// and this value will be adjusted there
|
|
Vec2::zero(),
|
|
containing_block,
|
|
);
|
|
let hoisted_fragment = hoisted_box.fragment.clone();
|
|
positioning_context.push(hoisted_box);
|
|
Fragment::AbsoluteOrFixedPositioned(hoisted_fragment)
|
|
},
|
|
BlockLevelBox::OutOfFlowFloatBox(float_box) => Fragment::Float(float_box.layout(
|
|
layout_context,
|
|
positioning_context,
|
|
containing_block,
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn inline_content_sizes(
|
|
&mut self,
|
|
layout_context: &LayoutContext,
|
|
containing_block_writing_mode: WritingMode,
|
|
) -> ContentSizes {
|
|
match self {
|
|
Self::SameFormattingContextBlock {
|
|
style, contents, ..
|
|
} => sizing::outer_inline(style, containing_block_writing_mode, || {
|
|
contents.inline_content_sizes(layout_context, style.writing_mode)
|
|
}),
|
|
Self::Independent(independent) => independent
|
|
.outer_inline_content_sizes(layout_context, containing_block_writing_mode),
|
|
BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => ContentSizes::zero(),
|
|
BlockLevelBox::OutOfFlowFloatBox(float_box) => float_box
|
|
.contents
|
|
.outer_inline_content_sizes(layout_context, containing_block_writing_mode),
|
|
}
|
|
}
|
|
}
|
|
|
|
enum NonReplacedContents<'a> {
|
|
SameFormattingContextBlock(&'a BlockContainer),
|
|
EstablishesAnIndependentFormattingContext(&'a NonReplacedFormattingContext),
|
|
}
|
|
|
|
/// https://drafts.csswg.org/css2/visudet.html#blockwidth
|
|
/// https://drafts.csswg.org/css2/visudet.html#normal-block
|
|
fn layout_in_flow_non_replaced_block_level(
|
|
layout_context: &LayoutContext,
|
|
positioning_context: &mut PositioningContext,
|
|
containing_block: &ContainingBlock,
|
|
base_fragment_info: BaseFragmentInfo,
|
|
style: &Arc<ComputedValues>,
|
|
block_level_kind: NonReplacedContents,
|
|
mut sequential_layout_state: Option<&mut SequentialLayoutState>,
|
|
) -> BoxFragment {
|
|
let pbm = style.padding_border_margin(containing_block);
|
|
let box_size = style.content_box_size(containing_block, &pbm);
|
|
let max_box_size = style.content_max_box_size(containing_block, &pbm);
|
|
let min_box_size = style
|
|
.content_min_box_size(containing_block, &pbm)
|
|
.auto_is(Length::zero);
|
|
|
|
// https://drafts.csswg.org/css2/visudet.html#min-max-widths
|
|
let solve_inline_margins = |inline_size| {
|
|
solve_inline_margins_for_in_flow_block_level(containing_block, &pbm, inline_size)
|
|
};
|
|
let (mut inline_size, mut inline_margins) =
|
|
if let Some(inline_size) = box_size.inline.non_auto() {
|
|
(inline_size, solve_inline_margins(inline_size))
|
|
} else {
|
|
let margin_inline_start = pbm.margin.inline_start.auto_is(Length::zero);
|
|
let margin_inline_end = pbm.margin.inline_end.auto_is(Length::zero);
|
|
let inline_size = containing_block.inline_size -
|
|
pbm.padding_border_sums.inline -
|
|
margin_inline_start -
|
|
margin_inline_end;
|
|
(inline_size, (margin_inline_start, margin_inline_end))
|
|
};
|
|
if let Some(max_inline_size) = max_box_size.inline {
|
|
if inline_size > max_inline_size {
|
|
inline_size = max_inline_size;
|
|
inline_margins = solve_inline_margins(inline_size);
|
|
}
|
|
}
|
|
if inline_size < min_box_size.inline {
|
|
inline_size = min_box_size.inline;
|
|
inline_margins = solve_inline_margins(inline_size);
|
|
}
|
|
|
|
let margin = Sides {
|
|
inline_start: inline_margins.0,
|
|
inline_end: inline_margins.1,
|
|
block_start: pbm.margin.block_start.auto_is(Length::zero),
|
|
block_end: pbm.margin.block_end.auto_is(Length::zero),
|
|
};
|
|
|
|
// https://drafts.csswg.org/css2/visudet.html#min-max-heights
|
|
let mut block_size = box_size.block;
|
|
if let LengthOrAuto::LengthPercentage(ref mut block_size) = block_size {
|
|
*block_size = block_size.clamp_between_extremums(min_box_size.block, max_box_size.block);
|
|
}
|
|
|
|
let containing_block_for_children = ContainingBlock {
|
|
inline_size,
|
|
block_size,
|
|
style,
|
|
};
|
|
|
|
// https://drafts.csswg.org/css-writing-modes/#orthogonal-flows
|
|
assert_eq!(
|
|
containing_block.style.writing_mode, containing_block_for_children.style.writing_mode,
|
|
"Mixed writing modes are not supported yet"
|
|
);
|
|
|
|
let block_is_same_formatting_context = match block_level_kind {
|
|
NonReplacedContents::SameFormattingContextBlock(_) => true,
|
|
NonReplacedContents::EstablishesAnIndependentFormattingContext(_) => false,
|
|
};
|
|
|
|
let start_margin_can_collapse_with_children = block_is_same_formatting_context &&
|
|
pbm.padding.block_start == Length::zero() &&
|
|
pbm.border.block_start == Length::zero();
|
|
let end_margin_can_collapse_with_children = block_is_same_formatting_context &&
|
|
pbm.padding.block_end == Length::zero() &&
|
|
pbm.border.block_end == Length::zero() &&
|
|
block_size == LengthOrAuto::Auto;
|
|
|
|
let mut clearance = Length::zero();
|
|
let parent_containing_block_position_info;
|
|
match sequential_layout_state {
|
|
None => parent_containing_block_position_info = None,
|
|
Some(ref mut sequential_layout_state) => {
|
|
sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin.block_start));
|
|
if !start_margin_can_collapse_with_children {
|
|
sequential_layout_state.collapse_margins();
|
|
}
|
|
|
|
// Introduce clearance if necessary.
|
|
let clear_side = ClearSide::from_style(style);
|
|
clearance = sequential_layout_state.calculate_clearance(clear_side);
|
|
|
|
// 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,
|
|
);
|
|
|
|
// 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 +
|
|
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 + inline_size,
|
|
};
|
|
parent_containing_block_position_info = Some(
|
|
sequential_layout_state.replace_containing_block_position_info(new_cb_offsets),
|
|
);
|
|
},
|
|
};
|
|
|
|
let mut block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin);
|
|
|
|
let fragments;
|
|
let mut content_block_size;
|
|
match block_level_kind {
|
|
NonReplacedContents::SameFormattingContextBlock(contents) => {
|
|
let flow_layout = contents.layout(
|
|
layout_context,
|
|
positioning_context,
|
|
&containing_block_for_children,
|
|
sequential_layout_state.as_mut().map(|x| &mut **x),
|
|
CollapsibleWithParentStartMargin(start_margin_can_collapse_with_children),
|
|
);
|
|
|
|
fragments = flow_layout.fragments;
|
|
content_block_size = flow_layout.content_block_size;
|
|
|
|
// Update margins.
|
|
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(),
|
|
));
|
|
}
|
|
}
|
|
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();
|
|
}
|
|
block_margins_collapsed_with_children.collapsed_through =
|
|
collapsible_margins_in_children.collapsed_through &&
|
|
block_is_same_formatting_context &&
|
|
pbm.padding_border_sums.block == Length::zero() &&
|
|
block_size.auto_is(|| Length::zero()) == Length::zero() &&
|
|
min_box_size.block == Length::zero();
|
|
},
|
|
NonReplacedContents::EstablishesAnIndependentFormattingContext(non_replaced) => {
|
|
let independent_layout = non_replaced.layout(
|
|
layout_context,
|
|
positioning_context,
|
|
&containing_block_for_children,
|
|
);
|
|
fragments = independent_layout.fragments;
|
|
content_block_size = independent_layout.content_block_size;
|
|
},
|
|
};
|
|
|
|
let block_size = block_size.auto_is(|| {
|
|
content_block_size.clamp_between_extremums(min_box_size.block, max_box_size.block)
|
|
});
|
|
|
|
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).
|
|
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 = Rect {
|
|
start_corner: Vec2 {
|
|
block: pbm.padding.block_start + pbm.border.block_start + clearance,
|
|
inline: pbm.padding.inline_start + pbm.border.inline_start + margin.inline_start,
|
|
},
|
|
size: Vec2 {
|
|
block: block_size,
|
|
inline: inline_size,
|
|
},
|
|
};
|
|
|
|
BoxFragment::new(
|
|
base_fragment_info,
|
|
style.clone(),
|
|
fragments,
|
|
content_rect,
|
|
pbm.padding,
|
|
pbm.border,
|
|
margin,
|
|
clearance,
|
|
block_margins_collapsed_with_children,
|
|
)
|
|
}
|
|
|
|
/// 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_replaced_block_level<'a>(
|
|
containing_block: &ContainingBlock,
|
|
base_fragment_info: BaseFragmentInfo,
|
|
style: &Arc<ComputedValues>,
|
|
replaced: &ReplacedContent,
|
|
mut sequential_layout_state: Option<&mut SequentialLayoutState>,
|
|
) -> BoxFragment {
|
|
let pbm = style.padding_border_margin(containing_block);
|
|
let size = replaced.used_size_as_if_inline_element(containing_block, style, None, &pbm);
|
|
|
|
let (margin_inline_start, margin_inline_end) =
|
|
solve_inline_margins_for_in_flow_block_level(containing_block, &pbm, size.inline);
|
|
let margin = Sides {
|
|
inline_start: margin_inline_start,
|
|
inline_end: margin_inline_end,
|
|
block_start: pbm.margin.block_start.auto_is(Length::zero),
|
|
block_end: pbm.margin.block_end.auto_is(Length::zero),
|
|
};
|
|
let fragments = replaced.make_fragments(style, size.clone());
|
|
|
|
let mut clearance = Length::zero();
|
|
if let Some(ref mut sequential_layout_state) = sequential_layout_state {
|
|
sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin.block_start));
|
|
sequential_layout_state.collapse_margins();
|
|
clearance = sequential_layout_state.calculate_clearance(ClearSide::from_style(style));
|
|
sequential_layout_state.advance_block_position(
|
|
pbm.border.block_sum() + pbm.padding.block_sum() + size.block + clearance,
|
|
);
|
|
sequential_layout_state.adjoin_assign(&CollapsedMargin::new(margin.block_end));
|
|
};
|
|
|
|
let content_rect = Rect {
|
|
start_corner: Vec2 {
|
|
block: pbm.padding.block_start + pbm.border.block_start + clearance,
|
|
inline: pbm.padding.inline_start + pbm.border.inline_start + margin.inline_start,
|
|
},
|
|
size,
|
|
};
|
|
let block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin);
|
|
|
|
BoxFragment::new(
|
|
base_fragment_info,
|
|
style.clone(),
|
|
fragments,
|
|
content_rect,
|
|
pbm.padding,
|
|
pbm.border,
|
|
margin,
|
|
clearance,
|
|
block_margins_collapsed_with_children,
|
|
)
|
|
}
|
|
|
|
fn solve_inline_margins_for_in_flow_block_level(
|
|
containing_block: &ContainingBlock,
|
|
pbm: &PaddingBorderMargin,
|
|
inline_size: Length,
|
|
) -> (Length, Length) {
|
|
let available = containing_block.inline_size - pbm.padding_border_sums.inline - inline_size;
|
|
match (pbm.margin.inline_start, pbm.margin.inline_end) {
|
|
(LengthOrAuto::Auto, LengthOrAuto::Auto) => (available / 2., available / 2.),
|
|
(LengthOrAuto::Auto, LengthOrAuto::LengthPercentage(end)) => (available - end, end),
|
|
(LengthOrAuto::LengthPercentage(start), _) => (start, available - start),
|
|
}
|
|
}
|
|
|
|
/// Information passed to [PlacementState::place_fragment] when operating in sequential
|
|
/// mode.
|
|
struct PlacementStateSequentialInfo<'a> {
|
|
/// The sequential layout state of the current layout.
|
|
sequential_layout_state: &'a mut SequentialLayoutState,
|
|
|
|
/// The boxes that follow the one currently being placed. This is used to try
|
|
/// to calculate margins after the current box that will collapse with the
|
|
/// parent, if this current box is floating.
|
|
following_boxes: &'a [ArcRefCell<BlockLevelBox>],
|
|
}
|
|
|
|
/// 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 {
|
|
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: Length,
|
|
}
|
|
|
|
impl PlacementState {
|
|
fn new(
|
|
collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin,
|
|
) -> PlacementState {
|
|
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: Length::zero(),
|
|
}
|
|
}
|
|
|
|
/// 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_info: Option<PlacementStateSequentialInfo>,
|
|
) {
|
|
match fragment {
|
|
Fragment::Box(fragment) => {
|
|
let fragment_block_margins = &fragment.block_margins_collapsed_with_children;
|
|
let fragment_block_size = fragment.clearance +
|
|
fragment.padding.block_sum() +
|
|
fragment.border.block_sum() +
|
|
fragment.content_rect.size.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 fragment.clearance != Length::zero() {
|
|
// 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.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_eq!(self.current_margin.solve(), Length::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.start_corner.block +=
|
|
self.current_margin.solve() + self.current_block_direction_position;
|
|
|
|
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.clearance;
|
|
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) => {
|
|
let offset = Vec2 {
|
|
block: self.current_margin.solve() + self.current_block_direction_position,
|
|
inline: Length::new(0.),
|
|
};
|
|
fragment.borrow_mut().adjust_offsets(offset);
|
|
},
|
|
Fragment::Float(box_fragment) => {
|
|
let info = sequential_info
|
|
.expect("Tried to lay out a float with no sequential placement state!");
|
|
|
|
let mut margins_collapsing_with_parent_containing_block = self.start_margin;
|
|
if self.next_in_flow_margin_collapses_with_parent_start_margin {
|
|
// If block start margins are still collapsing from the parent, margins of elements
|
|
// that follow this float in tree order might collapse, pushing this float down
|
|
// and changing the offset of the containing block of the float. We try to look
|
|
// ahead to determine which margins from future elements will collapse with the
|
|
// parent.
|
|
let mut future_margins = CollapsedMargin::zero();
|
|
BlockLevelBox::find_block_margin_collapsing_with_parent_for_floats_from_slice(
|
|
info.following_boxes,
|
|
&mut future_margins,
|
|
WritingMode::empty(),
|
|
);
|
|
margins_collapsing_with_parent_containing_block.adjoin_assign(&future_margins)
|
|
}
|
|
|
|
let block_offset_from_containing_block_top =
|
|
self.current_block_direction_position + self.current_margin.solve();
|
|
info.sequential_layout_state.place_float_fragment(
|
|
box_fragment,
|
|
margins_collapsing_with_parent_containing_block,
|
|
block_offset_from_containing_block_top,
|
|
);
|
|
},
|
|
Fragment::Anonymous(_) => {},
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn finish(mut self) -> (Length, CollapsedBlockMargins) {
|
|
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();
|
|
}
|
|
(
|
|
self.current_block_direction_position,
|
|
CollapsedBlockMargins {
|
|
collapsed_through: self.next_in_flow_margin_collapses_with_parent_start_margin,
|
|
start: self.start_margin,
|
|
end: self.current_margin,
|
|
},
|
|
)
|
|
}
|
|
}
|