layout: Initial implementation of flex-direction: column and column-reverse (#33031)

This change removes restrictions on using the column layout mode of
flexbox and adds an initial implementation of sizing for that flex
direction. There's a lot of missing pieces still, but in some cases this
does render column flexbox.

In particular, there are now two code paths for preferred widths
(intrinsic size) calcuation: one in the main axis (row) and one in
the cross axis (column) corresponding to the flex direciton with
horizontal writing modes.

In addition, `FlexItemBox::inline_content_sizes` is removed in favor of
making `sizing::outer_inline` /
`IndependentFormattingContext::outer_inline_content_sizes` generic
enough to handle using a different value for auto minimum sizes, which
flexbox needs.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Martin Robinson 2024-08-14 16:25:09 +02:00 committed by GitHub
parent c059bab6f4
commit 7633bdccd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
124 changed files with 541 additions and 605 deletions

View file

@ -204,15 +204,62 @@ impl FlexContainer {
&mut self,
layout_context: &LayoutContext,
writing_mode: WritingMode,
) -> ContentSizes {
let flex_axis = FlexAxis::from(used_flex_direction(&*self.style));
match flex_axis {
FlexAxis::Row => self.main_content_sizes(layout_context, flex_axis, writing_mode),
FlexAxis::Column => self.cross_content_sizes(layout_context, flex_axis),
}
}
fn cross_content_sizes(
&mut self,
layout_context: &LayoutContext,
flex_axis: FlexAxis,
) -> ContentSizes {
// <https://drafts.csswg.org/css-flexbox/#intrinsic-cross-sizes>
assert_eq!(
flex_axis,
FlexAxis::Column,
"The cross axis should be the inline one"
);
let mut content_sizes = ContentSizes::zero();
for kid in self.children.iter() {
let kid = &mut *kid.borrow_mut();
match kid {
FlexLevelBox::FlexItem(item) => {
// TODO: For the max-content size we should distribute items into
// columns, and sum the column sizes and gaps.
content_sizes.max_assign(
item.independent_formatting_context
.inline_content_sizes(layout_context),
);
},
FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => {},
}
}
content_sizes
}
fn main_content_sizes(
&mut self,
layout_context: &LayoutContext,
flex_axis: FlexAxis,
writing_mode: WritingMode,
) -> ContentSizes {
// - TODO: calculate intrinsic cross sizes when container is a column
// (and check for writing-mode?)
// - TODO: Collapsed flex items need to be skipped for intrinsic size calculation.
// <https://drafts.csswg.org/css-flexbox/#intrinsic-cross-sizes>
// <https://drafts.csswg.org/css-flexbox-1/#intrinsic-main-sizes>
// > It is calculated, considering only non-collapsed flex items, by:
// > 1. For each flex item, subtract its outer flex base size from its max-content
// > contribution size.
assert_eq!(
flex_axis,
FlexAxis::Row,
"The main axis should be the inline one"
);
let mut chosen_max_flex_fraction = f32::NEG_INFINITY;
let mut chosen_min_flex_fraction = f32::NEG_INFINITY;
let mut sum_of_flex_grow_factors = 0.0;
@ -236,7 +283,7 @@ impl FlexContainer {
sum_of_flex_grow_factors += item.style().get_position().flex_grow.0;
sum_of_flex_shrink_factors += item.style().get_position().flex_shrink.0;
let info = item.inline_content_size_info(
let info = item.main_content_size_info(
layout_context,
writing_mode,
container_is_horizontal,
@ -455,7 +502,7 @@ impl FlexContainer {
// https://github.com/w3c/csswg-drafts/issues/4905
// Gecko reportedly uses `block-size: fit-content` in this case
// (which requires running another pass of the "full" layout algorithm)
todo!()
containing_block.block_size.auto_is(Au::zero)
// Note: this panic shouldnt happen since the start of `FlexContainer::layout`
// forces `FlexAxis::Row`.
},
@ -842,9 +889,11 @@ impl<'a> FlexItem<'a> {
box_.automatic_min_size(
flex_context.layout_context,
cross_axis_is_item_block_axis,
content_box_size,
min_size,
max_size,
flex_context
.flex_axis
.vec2_to_flex_relative(content_box_size),
flex_context.flex_axis.vec2_to_flex_relative(min_size),
flex_context.flex_axis.vec2_to_flex_relative(max_size),
)
}),
block: min_size.block.auto_is(Au::zero),
@ -969,7 +1018,7 @@ fn flex_base_size(
} else {
// FIXME: block-axis content sizing requires another pass
// of "full" layout
todo!()
Au::zero()
// Note: this panic shouldnt happen since the start of `FlexContainer::layout`
// forces `FlexAxis::Row` and the `writing-mode` property is disabled.
}
@ -989,13 +1038,7 @@ fn cross_axis_is_item_block_axis(
}
fn used_flex_direction(container_style: &ComputedValues) -> FlexDirection {
// Column flex containers are not fully implemented yet,
// so give a different layout instead of panicking.
// FIXME: implement `todo!`s for FlexAxis::Column below, and remove this
match container_style.clone_flex_direction() {
FlexDirection::Row | FlexDirection::Column => FlexDirection::Row,
FlexDirection::RowReverse | FlexDirection::ColumnReverse => FlexDirection::RowReverse,
}
container_style.clone_flex_direction()
}
// “Collect flex items into flex lines”
@ -1596,111 +1639,139 @@ impl FlexItem<'_> {
.positioning_context
.collects_for_nearest_positioned_ancestor(),
);
match flex_context.flex_axis {
FlexAxis::Row => {
// The main axis is the containers inline axis
// https://drafts.csswg.org/css-writing-modes/#orthogonal-flows
assert_eq!(
flex_context.containing_block.style.writing_mode,
self.box_.style().writing_mode,
"Mixed writing modes are not supported yet"
// https://drafts.csswg.org/css-writing-modes/#orthogonal-flows
let container_writing_mode = flex_context.containing_block.style.writing_mode;
assert_eq!(
container_writing_mode,
self.box_.style().writing_mode,
"Mixed writing modes are not supported yet"
);
// … and also the items inline axis.
match self.box_ {
IndependentFormattingContext::Replaced(replaced) => {
let pbm = replaced
.style
.padding_border_margin(flex_context.containing_block);
let box_size = used_cross_size_override.map(|size| LogicalVec2 {
inline: replaced
.style
.content_box_size(flex_context.containing_block, &pbm)
.inline
.map(Au::from),
block: AuOrAuto::LengthPercentage(size),
});
let size = replaced.contents.used_size_as_if_inline_element(
flex_context.containing_block,
&replaced.style,
box_size,
&pbm,
);
let cross_size = flex_context.vec2_to_flex_relative(size).cross;
let container_writing_mode = flex_context.containing_block.style.writing_mode;
let fragments = replaced.contents.make_fragments(
&replaced.style,
size.to_physical_size(container_writing_mode),
);
// … and also the items inline axis.
match self.box_ {
IndependentFormattingContext::Replaced(replaced) => {
let pbm = replaced
.style
.padding_border_margin(flex_context.containing_block);
let box_size = used_cross_size_override.map(|size| LogicalVec2 {
inline: replaced
.style
.content_box_size(flex_context.containing_block, &pbm)
.inline
.map(Au::from),
block: AuOrAuto::LengthPercentage(size),
});
let size = replaced.contents.used_size_as_if_inline_element(
flex_context.containing_block,
&replaced.style,
box_size,
&pbm,
);
let cross_size = flex_context.vec2_to_flex_relative(size).cross;
let container_writing_mode =
flex_context.containing_block.style.writing_mode;
let fragments = replaced.contents.make_fragments(
&replaced.style,
size.to_physical_size(container_writing_mode),
);
FlexItemLayoutResult {
hypothetical_cross_size: cross_size,
fragments,
positioning_context,
FlexItemLayoutResult {
hypothetical_cross_size: cross_size,
fragments,
positioning_context,
// We will need to synthesize the baseline, but since the used cross
// size can differ from the hypothetical cross size, we should defer
// synthesizing until needed.
baseline_relative_to_margin_box: None,
}
},
IndependentFormattingContext::NonReplaced(non_replaced) => {
let block_size = match used_cross_size_override {
Some(s) => AuOrAuto::LengthPercentage(s),
None => self.content_box_size.cross.map(|t| t),
};
let item_as_containing_block = ContainingBlock {
inline_size: used_main_size,
block_size,
style: &non_replaced.style,
};
let IndependentLayout {
fragments,
content_block_size,
baselines: content_box_baselines,
..
} = non_replaced.layout(
flex_context.layout_context,
&mut positioning_context,
&item_as_containing_block,
flex_context.containing_block,
);
let baselines_relative_to_margin_box =
self.layout_baselines_relative_to_margin_box(&content_box_baselines);
let baseline_relative_to_margin_box = match self.align_self.0.value() {
// baseline computes to first baseline.
AlignFlags::BASELINE => baselines_relative_to_margin_box.first,
AlignFlags::LAST_BASELINE => baselines_relative_to_margin_box.last,
_ => None,
};
let hypothetical_cross_size = self
.content_box_size
.cross
.auto_is(|| content_block_size)
.clamp_between_extremums(
self.content_min_size.cross,
self.content_max_size.cross,
);
FlexItemLayoutResult {
hypothetical_cross_size,
fragments,
positioning_context,
baseline_relative_to_margin_box,
}
},
// We will need to synthesize the baseline, but since the used cross
// size can differ from the hypothetical cross size, we should defer
// synthesizing until needed.
baseline_relative_to_margin_box: None,
}
},
FlexAxis::Column => {
todo!()
// Note: this panic shouldnt happen since the start of `FlexContainer::layout`
// forces `FlexAxis::Row`.
IndependentFormattingContext::NonReplaced(non_replaced) => {
let cross_size = match used_cross_size_override {
Some(s) => AuOrAuto::LengthPercentage(s),
None => self.content_box_size.cross.map(|t| t),
};
let item_writing_mode = non_replaced.style.writing_mode;
let item_is_horizontal = item_writing_mode.is_horizontal();
let cross_axis_is_item_block_axis = cross_axis_is_item_block_axis(
flex_context
.containing_block
.style
.writing_mode
.is_horizontal(),
item_is_horizontal,
flex_context.flex_axis,
);
let (inline_size, block_size) = if cross_axis_is_item_block_axis {
(used_main_size, cross_size)
} else {
(
cross_size.auto_is(|| {
let content_contributions = non_replaced.outer_inline_content_sizes(
flex_context.layout_context,
container_writing_mode,
Au::zero,
);
flex_context
.containing_block
.inline_size
.min(content_contributions.max_content)
.max(content_contributions.min_content) -
self.pbm_auto_is_zero.cross
}),
AuOrAuto::LengthPercentage(used_main_size),
)
};
let item_as_containing_block = ContainingBlock {
inline_size,
block_size,
style: &non_replaced.style,
};
let IndependentLayout {
fragments,
content_block_size,
baselines: content_box_baselines,
..
} = non_replaced.layout(
flex_context.layout_context,
&mut positioning_context,
&item_as_containing_block,
flex_context.containing_block,
);
let baselines_relative_to_margin_box =
self.layout_baselines_relative_to_margin_box(&content_box_baselines);
let baseline_relative_to_margin_box = match self.align_self.0.value() {
// baseline computes to first baseline.
AlignFlags::BASELINE => baselines_relative_to_margin_box.first,
AlignFlags::LAST_BASELINE => baselines_relative_to_margin_box.last,
_ => None,
};
let hypothetical_cross_size = self
.content_box_size
.cross
.auto_is(|| {
if cross_axis_is_item_block_axis {
content_block_size
} else {
inline_size
}
})
.clamp_between_extremums(
self.content_min_size.cross,
self.content_max_size.cross,
);
FlexItemLayoutResult {
hypothetical_cross_size,
fragments,
positioning_context,
baseline_relative_to_margin_box,
}
},
}
}
@ -1841,7 +1912,7 @@ impl FlexItem<'_> {
}
impl FlexItemBox {
fn inline_content_size_info(
fn main_content_size_info(
&mut self,
layout_context: &LayoutContext,
container_writing_mode: WritingMode,
@ -1877,9 +1948,9 @@ impl FlexItemBox {
let automatic_min_size = self.automatic_min_size(
layout_context,
cross_axis_is_item_block_axis,
content_box_size,
content_min_size,
content_max_size,
flex_axis.vec2_to_flex_relative(content_box_size),
flex_axis.vec2_to_flex_relative(content_min_size),
flex_axis.vec2_to_flex_relative(content_max_size),
);
let content_box_size = flex_axis.vec2_to_flex_relative(content_box_size);
@ -1913,14 +1984,19 @@ impl FlexItemBox {
padding_border,
);
let content_contribution_sizes = self.inline_content_sizes(
layout_context,
container_writing_mode,
content_box_size,
content_min_size_no_auto,
content_max_size,
pbm_auto_is_zero,
// Compute the min-content and max-content contributions of the item.
// <https://drafts.csswg.org/css-flexbox/#intrinsic-item-contributions>
assert_eq!(
flex_axis,
FlexAxis::Row,
"The main axis should be the inline one"
);
let content_contribution_sizes = self
.independent_formatting_context
.outer_inline_content_sizes(layout_context, container_writing_mode, || {
automatic_min_size
});
let outer_flex_base_size = flex_base_size + pbm_auto_is_zero.main;
let max_flex_factors = self.desired_flex_factors_for_preferred_width(
content_contribution_sizes.max_content,
@ -2012,69 +2088,14 @@ impl FlexItemBox {
}
}
fn inline_content_sizes(
&mut self,
layout_context: &LayoutContext,
writing_mode: WritingMode,
content_box_size: FlexRelativeVec2<GenericLengthPercentageOrAuto<Au>>,
content_min_size: FlexRelativeVec2<Au>,
content_max_size: FlexRelativeVec2<Option<Au>>,
pbm_auto_is_zero: FlexRelativeVec2<Au>,
) -> ContentSizes {
// TODO: use cross sizes when container is a column
// (and check for writing-mode?)
// <https://drafts.csswg.org/css-flexbox/#intrinsic-item-contributions>
let outer_inline_content_sizes = self
.independent_formatting_context
.outer_inline_content_sizes(layout_context, writing_mode);
let outer_preferred_size = content_box_size
.main
.non_auto()
.map(|preferred_size| preferred_size + pbm_auto_is_zero.main);
let outer_min_main_size = content_min_size.main + pbm_auto_is_zero.main;
let outer_max_main_size = content_max_size
.main
.map(|max_main_size| max_main_size + pbm_auto_is_zero.main);
// > The main-size min-content contribution of a flex item is the larger of its
// > outer min-content size and outer preferred size if that is not auto, clamped by
// > its min/max main size.
let min_content_contribution = outer_preferred_size
.map_or(
outer_inline_content_sizes.min_content,
|outer_preferred_size| {
outer_preferred_size.max(outer_inline_content_sizes.min_content)
},
)
.clamp_between_extremums(outer_min_main_size, outer_max_main_size);
// > The main-size max-content contribution of a flex item is the larger of its
// > outer max-content size and outer preferred size if that is not auto, clamped by
// > its min/max main size.
let max_content_contribution = outer_preferred_size
.map_or(
outer_inline_content_sizes.max_content,
|outer_preferred_size| {
outer_preferred_size.max(outer_inline_content_sizes.max_content)
},
)
.clamp_between_extremums(outer_min_main_size, outer_max_main_size);
ContentSizes {
min_content: min_content_contribution,
max_content: max_content_contribution,
}
}
/// This is an implementation of <https://drafts.csswg.org/css-flexbox/#min-size-auto>.
fn automatic_min_size(
&mut self,
layout_context: &LayoutContext,
cross_axis_is_item_block_axis: bool,
content_box_size: LogicalVec2<AuOrAuto>,
min_size: LogicalVec2<GenericLengthPercentageOrAuto<Au>>,
max_size: LogicalVec2<Option<Au>>,
content_box_size: FlexRelativeVec2<AuOrAuto>,
min_size: FlexRelativeVec2<GenericLengthPercentageOrAuto<Au>>,
max_size: FlexRelativeVec2<Option<Au>>,
) -> Au {
// FIXME(stshine): Consider more situations when auto min size is not needed.
if self
@ -2085,80 +2106,78 @@ impl FlexItemBox {
return Au::zero();
}
if cross_axis_is_item_block_axis {
// > **specified size suggestion**
// > If the items preferred main size is definite and not automatic, then the specified
// > size suggestion is that size. It is otherwise undefined.
let specified_size_suggestion = content_box_size.inline.non_auto();
// > **specified size suggestion**
// > If the items preferred main size is definite and not automatic, then the specified
// > size suggestion is that size. It is otherwise undefined.
let specified_size_suggestion = content_box_size.main.non_auto();
// > **transferred size suggestion**
// > If the item has a preferred aspect ratio and its preferred cross size is definite, then the
// > transferred size suggestion is that size (clamped by its minimum and maximum cross sizes if they
// > are definite), converted through the aspect ratio. It is otherwise undefined.
let transferred_size_suggestion = match self.independent_formatting_context {
IndependentFormattingContext::NonReplaced(_) => None,
IndependentFormattingContext::Replaced(ref bfc) => {
match (
bfc.contents.inline_size_over_block_size_intrinsic_ratio(
self.independent_formatting_context.style(),
),
content_box_size.block,
) {
(Some(ratio), AuOrAuto::LengthPercentage(block_size)) => {
let block_size = block_size.clamp_between_extremums(
min_size.block.auto_is(Au::zero),
max_size.block,
);
Some(block_size.scale_by(ratio))
},
_ => None,
}
},
};
// > **content size suggestion**
// > The content size suggestion is the min-content size in the main axis, clamped, if it has a
// > preferred aspect ratio, by any definite minimum and maximum cross sizes converted through the
// > aspect ratio.
let inline_content_size = self
.independent_formatting_context
.inline_content_sizes(layout_context)
.min_content;
let (is_replaced, aspect_ratio) = match self.independent_formatting_context {
let (is_replaced, main_size_over_cross_size_intrinsic_ratio) =
match self.independent_formatting_context {
IndependentFormattingContext::NonReplaced(_) => (false, None),
IndependentFormattingContext::Replaced(ref replaced) => (
true,
replaced
IndependentFormattingContext::Replaced(ref replaced) => {
let ratio = replaced
.contents
.inline_size_over_block_size_intrinsic_ratio(
self.independent_formatting_context.style(),
),
),
)
.map(|ratio| {
if cross_axis_is_item_block_axis {
ratio
} else {
1.0 / ratio
}
});
(true, ratio)
},
};
let content_size_suggestion = aspect_ratio
.map(|aspect_ratio| {
inline_content_size.clamp_between_extremums(
min_size.block.auto_is(Au::zero).scale_by(aspect_ratio),
max_size.block.map(|l| l.scale_by(aspect_ratio)),
)
})
.unwrap_or(inline_content_size);
// > The content-based minimum size of a flex item is the smaller of its specified size
// > suggestion and its content size suggestion if its specified size suggestion exists;
// > otherwise, the smaller of its transferred size suggestion and its content size
// > suggestion if the element is replaced and its transferred size suggestion exists;
// > otherwise its content size suggestion. In all cases, the size is clamped by the maximum
// > main size if its definite.
match (specified_size_suggestion, transferred_size_suggestion) {
(Some(specified), _) => specified.min(content_size_suggestion),
(_, Some(transferred)) if is_replaced => transferred.min(content_size_suggestion),
_ => content_size_suggestion,
}
.clamp_below_max(max_size.inline)
// > **transferred size suggestion**
// > If the item has a preferred aspect ratio and its preferred cross size is definite, then the
// > transferred size suggestion is that size (clamped by its minimum and maximum cross sizes if they
// > are definite), converted through the aspect ratio. It is otherwise undefined.
let transferred_size_suggestion = match (
main_size_over_cross_size_intrinsic_ratio,
content_box_size.cross,
) {
(Some(ratio), AuOrAuto::LengthPercentage(cross_size)) => {
let cross_size = cross_size
.clamp_between_extremums(min_size.cross.auto_is(Au::zero), max_size.cross);
Some(cross_size.scale_by(ratio))
},
_ => None,
};
// > **content size suggestion**
// > The content size suggestion is the min-content size in the main axis, clamped, if it has a
// > preferred aspect ratio, by any definite minimum and maximum cross sizes converted through the
// > aspect ratio.
let main_content_size = if cross_axis_is_item_block_axis {
self.independent_formatting_context
.inline_content_sizes(layout_context)
.min_content
} else {
// FIXME(stshine): Implement this when main axis is item's block axis.
Au::zero()
};
let content_size_suggestion = main_size_over_cross_size_intrinsic_ratio
.map(|ratio| {
main_content_size.clamp_between_extremums(
min_size.cross.auto_is(Au::zero).scale_by(ratio),
max_size.cross.map(|l| l.scale_by(ratio)),
)
})
.unwrap_or(main_content_size);
// > The content-based minimum size of a flex item is the smaller of its specified size
// > suggestion and its content size suggestion if its specified size suggestion exists;
// > otherwise, the smaller of its transferred size suggestion and its content size
// > suggestion if the element is replaced and its transferred size suggestion exists;
// > otherwise its content size suggestion. In all cases, the size is clamped by the maximum
// > main size if its definite.
match (specified_size_suggestion, transferred_size_suggestion) {
(Some(specified), _) => specified.min(content_size_suggestion),
(_, Some(transferred)) if is_replaced => transferred.min(content_size_suggestion),
_ => content_size_suggestion,
}
.clamp_below_max(max_size.main)
}
}