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;