diff --git a/tests/reftests/blocks.html b/tests/reftests/blocks.html index c71d5a4..4ca1d72 100644 --- a/tests/reftests/blocks.html +++ b/tests/reftests/blocks.html @@ -1,9 +1,9 @@ -
+

\ No newline at end of file +p { background-color: blue; width: 200px; height: 100px; margin: 10px 5px 5px } + diff --git a/tests/reftests/blocks.png b/tests/reftests/blocks.png index 9eb0c8f..60023f2 100644 Binary files a/tests/reftests/blocks.png and b/tests/reftests/blocks.png differ diff --git a/victor/src/layout/flow/inline.rs b/victor/src/layout/flow/inline.rs index 3072fd9..8086332 100644 --- a/victor/src/layout/flow/inline.rs +++ b/victor/src/layout/flow/inline.rs @@ -71,7 +71,7 @@ impl InlineFormattingContext { &self, containing_block: &ContainingBlock, tree_rank: usize, - ) -> (Vec, Vec, Length) { + ) -> FlowChildren { let mut ifc = InlineFormattingContextState { containing_block, absolutely_positioned_fragments: Vec::new(), @@ -137,11 +137,12 @@ impl InlineFormattingContext { } else { ifc.line_boxes .finish_line(&mut ifc.current_nesting_level, containing_block); - return ( - ifc.line_boxes.boxes, - ifc.absolutely_positioned_fragments, - ifc.line_boxes.next_line_block_position, - ); + return FlowChildren { + fragments: ifc.line_boxes.boxes, + absolutely_positioned_fragments: ifc.absolutely_positioned_fragments, + block_size: ifc.line_boxes.next_line_block_position, + collapsible_margins_in_children: CollapsedBlockMargins::zero(), + }; } } } @@ -242,6 +243,7 @@ impl<'box_tree> PartialInlineBoxFragment<'box_tree> { padding: self.padding.clone(), border: self.border.clone(), margin: self.margin.clone(), + block_margins_collapsed_with_children: CollapsedBlockMargins::zero(), }; let last_fragment = self.last_box_tree_fragment && !at_line_break; if last_fragment { diff --git a/victor/src/layout/flow/mod.rs b/victor/src/layout/flow/mod.rs index 844ca41..1f47fb0 100644 --- a/victor/src/layout/flow/mod.rs +++ b/victor/src/layout/flow/mod.rs @@ -39,12 +39,22 @@ pub(super) enum BlockLevelBox { }, } +pub(super) struct FlowChildren<'a> { + pub fragments: Vec, + pub absolutely_positioned_fragments: Vec>, + pub block_size: Length, + pub collapsible_margins_in_children: CollapsedBlockMargins, +} + +#[derive(Clone, Copy)] +struct CollapsibleWithParentStartMargin(bool); + impl BlockFormattingContext { pub(super) fn layout( &self, containing_block: &ContainingBlock, tree_rank: usize, - ) -> (Vec, Vec, Length) { + ) -> FlowChildren { let mut float_context; let float_context = if self.contains_floats { float_context = FloatContext::new(); @@ -52,22 +62,34 @@ impl BlockFormattingContext { } else { None }; - self.contents - .layout(containing_block, float_context, tree_rank) + let mut flow_children = self.contents.layout( + containing_block, + float_context, + tree_rank, + CollapsibleWithParentStartMargin(false), + ); + flow_children.block_size += flow_children.collapsible_margins_in_children.end.solve(); + flow_children.collapsible_margins_in_children.end = CollapsedMargin::zero(); + flow_children } } impl BlockContainer { - pub fn layout( + fn layout( &self, containing_block: &ContainingBlock, float_context: Option<&mut FloatContext>, tree_rank: usize, - ) -> (Vec, Vec, Length) { + collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, + ) -> FlowChildren { match self { - BlockContainer::BlockLevelBoxes(child_boxes) => { - layout_block_level_children(containing_block, float_context, tree_rank, child_boxes) - } + BlockContainer::BlockLevelBoxes(child_boxes) => layout_block_level_children( + containing_block, + float_context, + tree_rank, + collapsible_with_parent_start_margin, + child_boxes, + ), BlockContainer::InlineFormattingContext(ifc) => ifc.layout(containing_block, tree_rank), } } @@ -77,16 +99,71 @@ fn layout_block_level_children<'a>( containing_block: &ContainingBlock, float_context: Option<&mut FloatContext>, tree_rank: usize, + collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, child_boxes: &'a [BlockLevelBox], -) -> (Vec, Vec>, Length) { +) -> FlowChildren<'a> { + fn place_block_level_fragment(fragment: &mut Fragment, placement_state: &mut PlacementState) { + match fragment { + Fragment::Box(fragment) => { + let fragment_block_size = fragment.padding.block_sum() + + fragment.border.block_sum() + + fragment.content_rect.size.block; + + let current_margin = std::mem::replace( + &mut placement_state.current_margin, + fragment.block_margins_collapsed_with_children.end, + ); + if placement_state + .next_in_flow_margin_collapses_with_parent_start_margin + .take() + { + debug_assert_eq!(current_margin.solve(), Length::zero()); + debug_assert_eq!(placement_state.start_margin.solve(), Length::zero()); + placement_state.start_margin = + fragment.block_margins_collapsed_with_children.start; + } else { + placement_state.current_block_direction_position += current_margin + .adjoin(&fragment.block_margins_collapsed_with_children.start) + .solve() + } + + fragment.content_rect.start_corner.block += + placement_state.current_block_direction_position; + placement_state.current_block_direction_position += fragment_block_size; + } + Fragment::Anonymous(fragment) => { + // FIXME(nox): Margin collapsing for hypothetical boxes of + // abspos elements is probably wrong. + assert!(fragment.children.is_empty()); + assert_eq!(fragment.rect.size.block, Length::zero()); + fragment.rect.start_corner.block += + placement_state.current_block_direction_position; + } + _ => unreachable!(), + } + } + + struct PlacementState { + next_in_flow_margin_collapses_with_parent_start_margin: bool, + start_margin: CollapsedMargin, + current_margin: CollapsedMargin, + current_block_direction_position: Length, + } + let mut absolutely_positioned_fragments = vec![]; - let mut current_block_direction_position = Length::zero(); - let mut child_fragments: Vec; + let mut placement_state = PlacementState { + next_in_flow_margin_collapses_with_parent_start_margin: + collapsible_with_parent_start_margin.0, + start_margin: CollapsedMargin::zero(), + current_margin: CollapsedMargin::zero(), + current_block_direction_position: Length::zero(), + }; + let mut fragments: Vec<_>; if let Some(float_context) = float_context { // Because floats are involved, we do layout for this block formatting context // in tree order without parallelism. This enables mutable access // to a `FloatContext` that tracks every float encountered so far (again in tree order). - child_fragments = child_boxes + fragments = child_boxes .iter() .enumerate() .map(|(tree_rank, box_)| { @@ -96,12 +173,12 @@ fn layout_block_level_children<'a>( tree_rank, &mut absolutely_positioned_fragments, ); - place_block_level_fragment(&mut fragment, &mut current_block_direction_position); + place_block_level_fragment(&mut fragment, &mut placement_state); fragment }) .collect() } else { - child_fragments = child_boxes + fragments = child_boxes .par_iter() .enumerate() .mapfold_reduce_into( @@ -119,39 +196,26 @@ fn layout_block_level_children<'a>( }, ) .collect(); - for fragment in &mut child_fragments { - place_block_level_fragment(fragment, &mut current_block_direction_position) + for fragment in &mut fragments { + place_block_level_fragment(fragment, &mut placement_state) } } - let content_block_size = current_block_direction_position; - let block_size = containing_block.block_size.auto_is(|| content_block_size); adjust_static_positions( &mut absolutely_positioned_fragments, - &mut child_fragments, + &mut fragments, tree_rank, ); - (child_fragments, absolutely_positioned_fragments, block_size) -} - -fn place_block_level_fragment( - fragment: &mut Fragment, - current_block_direction_position: &mut Length, -) { - let (bpm, rect) = match fragment { - Fragment::Box(fragment) => ( - fragment.padding.block_sum() - + fragment.border.block_sum() - + fragment.margin.block_sum(), - &mut fragment.content_rect, - ), - Fragment::Anonymous(fragment) => (Length::zero(), &mut fragment.rect), - _ => unreachable!(), - }; - // FIXME: margin collapsing - rect.start_corner.block += *current_block_direction_position; - *current_block_direction_position += bpm + rect.size.block; + FlowChildren { + fragments, + absolutely_positioned_fragments, + block_size: placement_state.current_block_direction_position, + collapsible_margins_in_children: CollapsedBlockMargins { + start: placement_state.start_margin, + end: placement_state.current_margin, + }, + } } impl BlockLevelBox { @@ -164,24 +228,33 @@ impl BlockLevelBox { ) -> Fragment { match self { BlockLevelBox::SameFormattingContextBlock { style, contents } => { - layout_in_flow_non_replaced_block_level( + Fragment::Box(layout_in_flow_non_replaced_block_level( containing_block, absolutely_positioned_fragments, style, - |containing_block| contents.layout(containing_block, float_context, tree_rank), - ) + BlockLevelKind::SameFormattingContextBlock, + |containing_block, collapsible_with_parent_start_margin| { + contents.layout( + containing_block, + float_context, + tree_rank, + collapsible_with_parent_start_margin, + ) + }, + )) } BlockLevelBox::Independent { style, contents } => match contents.as_replaced() { Ok(replaced) => { // FIXME match *replaced {} } - Err(contents) => layout_in_flow_non_replaced_block_level( + Err(contents) => Fragment::Box(layout_in_flow_non_replaced_block_level( containing_block, absolutely_positioned_fragments, style, - |containing_block| contents.layout(containing_block, tree_rank), - ), + BlockLevelKind::EstablishesAnIndependentFormattingContext, + |containing_block, _| contents.layout(containing_block, tree_rank), + )), }, BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(box_) => { absolutely_positioned_fragments.push(box_.layout(Vec2::zero(), tree_rank)); @@ -195,16 +268,21 @@ impl BlockLevelBox { } } +#[derive(PartialEq)] +enum BlockLevelKind { + SameFormattingContextBlock, + EstablishesAnIndependentFormattingContext, +} + /// 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<'a>( containing_block: &ContainingBlock, absolutely_positioned_fragments: &mut Vec>, style: &Arc, - layout_contents: impl FnOnce( - &ContainingBlock, - ) -> (Vec, Vec>, Length), -) -> Fragment { + block_level_kind: BlockLevelKind, + layout_contents: impl FnOnce(&ContainingBlock, CollapsibleWithParentStartMargin) -> FlowChildren<'a>, +) -> BoxFragment { let cbis = containing_block.inline_size; let padding = style.padding().percentages_relative_to(cbis); let border = style.border_width().percentages_relative_to(cbis); @@ -237,8 +315,8 @@ fn layout_in_flow_non_replaced_block_level<'a>( } } let margin = computed_margin.auto_is(Length::zero); - let pbm = &pb + &margin; - let inline_size = inline_size.auto_is(|| cbis - pbm.inline_sum()); + let mut block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin); + let inline_size = inline_size.auto_is(|| cbis - pb.inline_sum() - margin.inline_sum()); let block_size = match box_size.block { LengthOrPercentageOrAuto::Length(l) => LengthOrAuto::Length(l), LengthOrPercentageOrAuto::Percentage(p) => containing_block.block_size.map(|cbbs| cbbs * p), @@ -254,12 +332,39 @@ fn layout_in_flow_non_replaced_block_level<'a>( containing_block.mode, containing_block_for_children.mode, "Mixed writing modes are not supported yet" ); - let (mut children, nested_abspos, content_block_size) = - layout_contents(&containing_block_for_children); + let this_start_margin_can_collapse_with_children = CollapsibleWithParentStartMargin( + block_level_kind == BlockLevelKind::SameFormattingContextBlock + && pb.block_start == Length::zero(), + ); + let mut flow_children = layout_contents( + &containing_block_for_children, + this_start_margin_can_collapse_with_children, + ); + if this_start_margin_can_collapse_with_children.0 { + block_margins_collapsed_with_children + .start + .adjoin_assign(&flow_children.collapsible_margins_in_children.start); + } + let this_end_margin_can_collapse_with_children = (block_level_kind, pb.block_end, block_size) + == ( + BlockLevelKind::SameFormattingContextBlock, + Length::zero(), + LengthOrAuto::Auto, + ); + if this_end_margin_can_collapse_with_children { + block_margins_collapsed_with_children + .end + .adjoin_assign(&flow_children.collapsible_margins_in_children.end); + } else { + flow_children.block_size += flow_children.collapsible_margins_in_children.end.solve(); + } let relative_adjustement = relative_adjustement(style, inline_size, block_size); - let block_size = block_size.auto_is(|| content_block_size); + let block_size = block_size.auto_is(|| flow_children.block_size); let content_rect = Rect { - start_corner: &pbm.start_corner() + &relative_adjustement, + start_corner: Vec2 { + block: pb.block_start + relative_adjustement.block, + inline: pb.inline_start + relative_adjustement.inline + margin.inline_start, + }, size: Vec2 { block: block_size, inline: inline_size, @@ -267,21 +372,22 @@ fn layout_in_flow_non_replaced_block_level<'a>( }; if style.box_.position.is_relatively_positioned() { AbsolutelyPositionedFragment::in_positioned_containing_block( - &nested_abspos, - &mut children, + &flow_children.absolutely_positioned_fragments, + &mut flow_children.fragments, &content_rect.size, &padding, containing_block_for_children.mode, ) } else { - absolutely_positioned_fragments.extend(nested_abspos); + absolutely_positioned_fragments.extend(flow_children.absolutely_positioned_fragments); }; - Fragment::Box(BoxFragment { + BoxFragment { style: style.clone(), - children, + children: flow_children.fragments, content_rect, padding, border, margin, - }) + block_margins_collapsed_with_children, + } } diff --git a/victor/src/layout/flow/root.rs b/victor/src/layout/flow/root.rs index bf044a3..489f4a0 100644 --- a/victor/src/layout/flow/root.rs +++ b/victor/src/layout/flow/root.rs @@ -101,18 +101,18 @@ impl BoxTreeRoot { mode: (WritingMode::HorizontalTb, Direction::Ltr), }; let dummy_tree_rank = 0; - let (mut fragments, abspos, _) = self.0.layout(&initial_containing_block, dummy_tree_rank); + let mut flow_children = self.0.layout(&initial_containing_block, dummy_tree_rank); let initial_containing_block = DefiniteContainingBlock { size: initial_containing_block_size, mode: initial_containing_block.mode, }; - fragments.par_extend( - abspos + flow_children.fragments.par_extend( + flow_children + .absolutely_positioned_fragments .par_iter() .map(|a| a.layout(&initial_containing_block)), ); - - fragments + flow_children.fragments } } diff --git a/victor/src/layout/fragments.rs b/victor/src/layout/fragments.rs index 29d831e..1d6034a 100644 --- a/victor/src/layout/fragments.rs +++ b/victor/src/layout/fragments.rs @@ -19,6 +19,19 @@ pub(crate) struct BoxFragment { pub padding: Sides, pub border: Sides, pub margin: Sides, + + pub block_margins_collapsed_with_children: CollapsedBlockMargins, +} + +pub(crate) struct CollapsedBlockMargins { + pub start: CollapsedMargin, + pub end: CollapsedMargin, +} + +#[derive(Clone, Copy)] +pub(crate) struct CollapsedMargin { + max_positive: Length, + min_negative: Length, } /// Can contain child fragments with relative coordinates, but does not contribute to painting itself. @@ -51,3 +64,50 @@ impl BoxFragment { .inflate(&self.border) } } + +impl CollapsedBlockMargins { + pub fn from_margin(margin: &Sides) -> Self { + Self { + start: CollapsedMargin::new(margin.block_start), + end: CollapsedMargin::new(margin.block_end), + } + } + + pub fn zero() -> Self { + Self { + start: CollapsedMargin::zero(), + end: CollapsedMargin::zero(), + } + } +} + +impl CollapsedMargin { + pub fn zero() -> Self { + Self { + max_positive: Length::zero(), + min_negative: Length::zero(), + } + } + + pub fn new(margin: Length) -> Self { + Self { + max_positive: margin.max(Length::zero()), + min_negative: margin.min(Length::zero()), + } + } + + pub fn adjoin(&self, other: &Self) -> Self { + Self { + max_positive: self.max_positive.max(other.max_positive), + min_negative: self.min_negative.min(other.min_negative), + } + } + + pub fn adjoin_assign(&mut self, other: &Self) { + *self = self.adjoin(other); + } + + pub fn solve(&self) -> Length { + self.max_positive + self.min_negative + } +} diff --git a/victor/src/layout/mod.rs b/victor/src/layout/mod.rs index bfa637e..1d7beb8 100644 --- a/victor/src/layout/mod.rs +++ b/victor/src/layout/mod.rs @@ -59,11 +59,7 @@ impl IndependentFormattingContext { } } - fn layout( - &self, - containing_block: &ContainingBlock, - tree_rank: usize, - ) -> (Vec, Vec, Length) { + fn layout(&self, containing_block: &ContainingBlock, tree_rank: usize) -> FlowChildren { match self.as_replaced() { Ok(replaced) => match *replaced {}, Err(ifc) => ifc.layout(containing_block, tree_rank), @@ -72,11 +68,7 @@ impl IndependentFormattingContext { } impl<'a> NonReplacedIFC<'a> { - fn layout( - &self, - containing_block: &ContainingBlock, - tree_rank: usize, - ) -> (Vec, Vec>, Length) { + fn layout(&self, containing_block: &ContainingBlock, tree_rank: usize) -> FlowChildren<'a> { match self { NonReplacedIFC::Flow(bfc) => bfc.layout(containing_block, tree_rank), } diff --git a/victor/src/layout/positioned.rs b/victor/src/layout/positioned.rs index f1b8388..3702f0f 100644 --- a/victor/src/layout/positioned.rs +++ b/victor/src/layout/positioned.rs @@ -254,7 +254,7 @@ impl<'a> AbsolutelyPositionedFragment<'a> { "Mixed writing modes are not supported yet" ); let dummy_tree_rank = 0; - let (mut children, nested_abspos, block_size) = self + let mut flow_children = self .absolutely_positioned_box .contents .layout(&containing_block_for_children, dummy_tree_rank); @@ -264,6 +264,7 @@ impl<'a> AbsolutelyPositionedFragment<'a> { Anchor::End(end) => cbbs - end - pb.inline_end - margin.inline_end - inline_size, }; + let block_size = block_size.auto_is(|| flow_children.block_size); let block_start = match block_anchor { Anchor::Start(start) => start + pb.block_start + margin.block_start, Anchor::End(end) => cbbs - end - pb.block_end - margin.block_end - block_size, @@ -281,8 +282,8 @@ impl<'a> AbsolutelyPositionedFragment<'a> { }; AbsolutelyPositionedFragment::in_positioned_containing_block( - &nested_abspos, - &mut children, + &flow_children.absolutely_positioned_fragments, + &mut flow_children.fragments, &content_rect.size, &padding, containing_block_for_children.mode, @@ -290,11 +291,12 @@ impl<'a> AbsolutelyPositionedFragment<'a> { Fragment::Box(BoxFragment { style: style.clone(), - children, + children: flow_children.fragments, content_rect, padding, border, margin, + block_margins_collapsed_with_children: CollapsedBlockMargins::zero(), }) } } diff --git a/victor/src/style/values/length.rs b/victor/src/style/values/length.rs index 37881bd..531e7b9 100644 --- a/victor/src/style/values/length.rs +++ b/victor/src/style/values/length.rs @@ -50,7 +50,7 @@ pub(crate) enum LengthOrPercentageOrAuto { Auto, } -#[derive(Debug, Copy, Clone, FromVariants)] +#[derive(Copy, Clone, Debug, FromVariants, PartialEq)] pub(crate) enum LengthOrAuto { Length(Length), Auto, @@ -110,6 +110,12 @@ impl Length { } } + pub fn min(self, other: Self) -> Self { + Length { + px: self.px.min(other.px), + } + } + pub fn max_assign(&mut self, other: Self) { *self = self.max(other) } @@ -146,6 +152,12 @@ impl ops::AddAssign for Length { } } +impl ops::SubAssign for Length { + fn sub_assign(&mut self, other: Self) { + self.px -= other.px + } +} + impl ops::Mul for Length { type Output = Self;