LibWeb: Let parent formatting context determine size of flex containers

Until now, we had implemented flex container sizing by awkwardly doing
exactly what the spec said (basically having FFC size the container)
despite that not really making sense in the big picture. (Parent
formatting contexts should be responsible for sizing and placing their
children)

This patch moves us away from the Flexbox spec text a little bit, by
removing the logic for sizing the flex container in FFC, and instead
making sure that all formatting contexts can set both width and height
of flex container children.

This required changes in BFC and IFC, but it's actually quite simple!
Width was already not a problem, and it turns out height isn't either,
since the automatic height of a flex container is max-content.
With this in mind, we can simply determine the height of flex containers
before transferring control to FFC, and everything flows nicely.

With this change, we can remove all the virtuals and FFC logic for
negotiating container size with the parent formatting context.
We also don't need the "available space for flex container" stuff
anymore either, so that's gone as well.

There are some minor diffs in layout test results from this, but the
new results actually match other browsers more closely, so that's fine.

This should make flex layout, and indeed layout in general, easier to
understand, since this was the main weird special case outside of
BFC/IFC where a formatting context delegates work to its parent instead
of the other way around. :^)
This commit is contained in:
Andreas Kling 2024-01-10 14:52:21 +01:00
parent 5f0230a57e
commit 5af02a914c
Notes: sideshowbarker 2024-07-17 02:28:18 +09:00
9 changed files with 40 additions and 196 deletions

View file

@ -2,7 +2,7 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (1,1) content-size 798x39.46875 [BFC] children: not-inline
BlockContainer <body> at (10,10) content-size 780x21.46875 children: not-inline
Box <div.flexrow> at (11,11) content-size 778x19.46875 flex-container(row) [FFC] children: not-inline
Box <div.project> at (12,12) content-size 44.03125x17.46875 flex-container(column) flex-item [FFC] children: not-inline
Box <div.project> at (12,12) content-size 44.03125x19.46875 flex-container(column) flex-item [FFC] children: not-inline
BlockContainer <(anonymous)> at (12,12) content-size 44.03125x17.46875 flex-item [BFC] children: inline
line 0 width: 44.03125, height: 17.46875, bottom: 17.46875, baseline: 13.53125
frag 0 from TextNode start: 0, length: 6, rect: [12,12 44.03125x17.46875]
@ -11,8 +11,8 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x41.46875]
PaintableWithLines (BlockContainer<BODY>) [9,9 782x23.46875]
PaintableBox (Box<DIV>.flexrow) [10,10 780x21.46875]
PaintableBox (Box<DIV>.project) [11,11 46.03125x19.46875]
PaintableWithLines (BlockContainer<BODY>) [9,9 782x23.46875] overflow: [10,10 780x22.46875]
PaintableBox (Box<DIV>.flexrow) [10,10 780x21.46875] overflow: [11,11 778x21.46875]
PaintableBox (Box<DIV>.project) [11,11 46.03125x21.46875]
PaintableWithLines (BlockContainer(anonymous)) [12,12 44.03125x17.46875]
TextPaintable (TextNode<#text>)

View file

@ -1,11 +1,11 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (1,1) content-size 798x69.96875 [BFC] children: not-inline
Box <body> at (10,10) content-size 780x51.96875 flex-container(row) [FFC] children: not-inline
ImageBox <img> at (11,11) content-size 66.65625x49.984375 flex-item children: not-inline
BlockContainer <html> at (1,1) content-size 798x69.984375 [BFC] children: not-inline
Box <body> at (10,10) content-size 780x51.984375 flex-container(row) [FFC] children: not-inline
ImageBox <img> at (11,11) content-size 64x49.984375 flex-item children: not-inline
BlockContainer <(anonymous)> (not painted) [BFC] children: inline
TextNode <#text>
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x71.96875]
PaintableBox (Box<BODY>) [9,9 782x53.96875] overflow: [10,10 780x51.984375]
ImagePaintable (ImageBox<IMG>) [10,10 68.65625x51.984375]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x71.984375]
PaintableBox (Box<BODY>) [9,9 782x53.984375]
ImagePaintable (ImageBox<IMG>) [10,10 66x51.984375]

View file

@ -680,7 +680,8 @@ void BlockFormattingContext::layout_block_level_box(Box const& box, BlockContain
place_block_level_element_in_normal_flow_horizontally(box, available_space);
if (box.is_replaced_box())
// NOTE: Flex containers with `auto` height are treated as `max-content`, so we can compute their height early.
if (box.is_replaced_box() || box.display().is_flex_inside())
compute_height(box, available_space);
if (independent_formatting_context) {
@ -1247,15 +1248,4 @@ CSSPixels BlockFormattingContext::greatest_child_width(Box const& box) const
return max_width;
}
void BlockFormattingContext::determine_width_of_child(Box const&, AvailableSpace const&)
{
// NOTE: We don't do anything here, since we'll have already determined the width of the child
// before recursing into nested layout within the child.
}
void BlockFormattingContext::determine_height_of_child(Box const& box, AvailableSpace const& available_space)
{
compute_height(box, available_space);
}
}

View file

@ -50,10 +50,6 @@ public:
void layout_block_level_box(Box const&, BlockContainer const&, LayoutMode, CSSPixels& bottom_of_lowest_margin_box, AvailableSpace const&);
virtual bool can_determine_size_of_child() const override { return true; }
virtual void determine_width_of_child(Box const&, AvailableSpace const&) override;
virtual void determine_height_of_child(Box const&, AvailableSpace const&) override;
void resolve_vertical_box_model_metrics(Box const&);
enum class DidIntroduceClearance {

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2023, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021-2024, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
@ -56,35 +56,17 @@ CSSPixels FlexFormattingContext::automatic_content_height() const
return m_flex_container_state.content_height();
}
void FlexFormattingContext::run(Box const& run_box, LayoutMode, AvailableSpace const& available_content_space)
void FlexFormattingContext::run(Box const& run_box, LayoutMode, AvailableSpace const& available_space)
{
VERIFY(&run_box == &flex_container());
// NOTE: The available space provided by the parent context is basically our *content box*.
// FFC is currently written in a way that expects that to include padding and border as well,
// so we pad out the available space here to accommodate that.
// FIXME: Refactor the necessary parts of FFC so we don't need this hack!
auto available_width = available_content_space.width;
if (available_width.is_definite())
available_width = AvailableSize::make_definite(available_width.to_px_or_zero() + m_flex_container_state.border_box_left() + m_flex_container_state.border_box_right());
auto available_height = available_content_space.height;
if (available_height.is_definite())
available_height = AvailableSize::make_definite(available_height.to_px_or_zero() + m_flex_container_state.border_box_top() + m_flex_container_state.border_box_bottom());
m_available_space_for_flex_container = AxisAgnosticAvailableSpace {
.main = is_row_layout() ? available_width : available_height,
.cross = !is_row_layout() ? available_width : available_height,
.space = { available_width, available_height },
};
// This implements https://www.w3.org/TR/css-flexbox-1/#layout-algorithm
// 1. Generate anonymous flex items
generate_anonymous_flex_items();
// 2. Determine the available main and cross space for the flex items
determine_available_space_for_items(AvailableSpace(available_width, available_height));
determine_available_space_for_items(available_space);
{
// https://drafts.csswg.org/css-flexbox-1/#definite-sizes
@ -114,12 +96,16 @@ void FlexFormattingContext::run(Box const& run_box, LayoutMode, AvailableSpace c
determine_flex_base_size_and_hypothetical_main_size(item);
}
if (available_width.is_intrinsic_sizing_constraint() || available_height.is_intrinsic_sizing_constraint()) {
if (available_space.width.is_intrinsic_sizing_constraint() || available_space.height.is_intrinsic_sizing_constraint()) {
// We're computing intrinsic size for the flex container. This happens at the end of run().
} else {
// 4. Determine the main size of the flex container
determine_main_size_of_flex_container();
// Determine the main size of the flex container using the rules of the formatting context in which it participates.
// NOTE: The automatic block size of a block-level flex container is its max-content size.
// NOTE: We've already handled this in the parent formatting context.
// Specifically, all formatting contexts will have assigned width & height to the flex container
// before this formatting context runs.
}
// 5. Collect flex items into flex lines:
@ -186,7 +172,7 @@ void FlexFormattingContext::run(Box const& run_box, LayoutMode, AvailableSpace c
// 16. Align all flex lines (per align-content)
align_all_flex_lines();
if (available_width.is_intrinsic_sizing_constraint() || available_height.is_intrinsic_sizing_constraint()) {
if (available_space.width.is_intrinsic_sizing_constraint() || available_space.height.is_intrinsic_sizing_constraint()) {
// We're computing intrinsic size for the flex container.
determine_intrinsic_size_of_flex_container();
} else {
@ -195,7 +181,7 @@ void FlexFormattingContext::run(Box const& run_box, LayoutMode, AvailableSpace c
copy_dimensions_from_flex_items_to_boxes();
for (auto& item : m_flex_items) {
auto& box_state = m_state.get(item.box);
if (auto independent_formatting_context = layout_inside(item.box, LayoutMode::Normal, box_state.available_inner_space_or_constraints_from(m_available_space_for_flex_container->space)))
if (auto independent_formatting_context = layout_inside(item.box, LayoutMode::Normal, box_state.available_inner_space_or_constraints_from(m_available_space_for_items->space)))
independent_formatting_context->parent_context_did_dimension_child_root_box();
compute_inset(item.box);
@ -443,66 +429,17 @@ void FlexFormattingContext::set_main_axis_second_margin(FlexItem& item, CSSPixel
// https://drafts.csswg.org/css-flexbox-1/#algo-available
void FlexFormattingContext::determine_available_space_for_items(AvailableSpace const& available_space)
{
// For each dimension, if that dimension of the flex containers content box is a definite size, use that;
// if that dimension of the flex container is being sized under a min or max-content constraint, the available space in that dimension is that constraint;
// otherwise, subtract the flex containers margin, border, and padding from the space available to the flex container in that dimension and use that value.
// This might result in an infinite value.
Optional<AvailableSize> available_width_for_items;
if (m_flex_container_state.has_definite_width()) {
available_width_for_items = AvailableSize::make_definite(m_flex_container_state.content_width());
} else {
if (available_space.width.is_intrinsic_sizing_constraint()) {
available_width_for_items = available_space.width;
} else {
if (available_space.width.is_definite()) {
auto remaining = available_space.width.to_px_or_zero()
- m_flex_container_state.margin_left
- m_flex_container_state.margin_right
- m_flex_container_state.border_left
- m_flex_container_state.padding_right
- m_flex_container_state.padding_left
- m_flex_container_state.padding_right;
available_width_for_items = AvailableSize::make_definite(remaining);
} else {
available_width_for_items = AvailableSize::make_indefinite();
}
}
}
Optional<AvailableSize> available_height_for_items;
if (m_flex_container_state.has_definite_height()) {
available_height_for_items = AvailableSize::make_definite(m_flex_container_state.content_height());
} else {
if (available_space.height.is_intrinsic_sizing_constraint()) {
available_height_for_items = available_space.height;
} else {
if (available_space.height.is_definite()) {
auto remaining = available_space.height.to_px_or_zero()
- m_flex_container_state.margin_top
- m_flex_container_state.margin_bottom
- m_flex_container_state.border_top
- m_flex_container_state.padding_bottom
- m_flex_container_state.padding_top
- m_flex_container_state.padding_bottom;
available_height_for_items = AvailableSize::make_definite(remaining);
} else {
available_height_for_items = AvailableSize::make_indefinite();
}
}
}
if (is_row_layout()) {
m_available_space_for_items = AxisAgnosticAvailableSpace {
.main = *available_width_for_items,
.cross = *available_height_for_items,
.space = { *available_width_for_items, *available_height_for_items },
.main = available_space.width,
.cross = available_space.height,
.space = { available_space.width, available_space.height },
};
} else {
m_available_space_for_items = AxisAgnosticAvailableSpace {
.main = *available_height_for_items,
.cross = *available_width_for_items,
.space = { *available_width_for_items, *available_height_for_items },
.main = available_space.height,
.cross = available_space.width,
.space = { available_space.width, available_space.height },
};
}
}
@ -785,59 +722,6 @@ CSSPixels FlexFormattingContext::content_based_minimum_size(FlexItem const& item
return unclamped_size;
}
bool FlexFormattingContext::can_determine_size_of_child() const
{
return true;
}
void FlexFormattingContext::determine_width_of_child(Box const&, AvailableSpace const&)
{
// NOTE: For now, we simply do nothing here. If a child context is calling up to us
// and asking us to determine its width, we've already done so as part of the
// flex layout algorithm.
}
void FlexFormattingContext::determine_height_of_child(Box const&, AvailableSpace const&)
{
// NOTE: For now, we simply do nothing here. If a child context is calling up to us
// and asking us to determine its height, we've already done so as part of the
// flex layout algorithm.
}
// https://drafts.csswg.org/css-flexbox-1/#algo-main-container
void FlexFormattingContext::determine_main_size_of_flex_container()
{
// Determine the main size of the flex container using the rules of the formatting context in which it participates.
// NOTE: The automatic block size of a block-level flex container is its max-content size.
// FIXME: The code below doesn't know how to size absolutely positioned flex containers at all.
// We just leave it alone for now and let the parent context deal with it.
if (flex_container().is_absolutely_positioned())
return;
// FIXME: Once all parent contexts now how to size a given child, we can remove
// `can_determine_size_of_child()`.
if (parent()->can_determine_size_of_child()) {
if (is_row_layout()) {
parent()->determine_width_of_child(flex_container(), m_available_space_for_flex_container->space);
} else {
parent()->determine_height_of_child(flex_container(), m_available_space_for_flex_container->space);
}
return;
}
if (is_row_layout()) {
if (!flex_container().is_out_of_flow(*parent()) && m_state.get(*flex_container().containing_block()).has_definite_width()) {
set_main_size(flex_container(), calculate_stretch_fit_width(flex_container(), m_available_space_for_flex_container->space.width));
} else {
set_main_size(flex_container(), calculate_max_content_width(flex_container()));
}
} else {
if (!has_definite_main_size(flex_container()))
set_main_size(flex_container(), calculate_max_content_height(flex_container(), m_available_space_for_flex_container->space.width.to_px_or_zero()));
}
}
// https://www.w3.org/TR/css-flexbox-1/#algo-line-break
void FlexFormattingContext::collect_flex_items_into_flex_lines()
{
@ -1229,7 +1113,7 @@ void FlexFormattingContext::calculate_cross_size_of_each_flex_line()
// If the flex container is single-line, then clamp the lines cross-size to be within the containers computed min and max cross sizes.
// Note that if CSS 2.1s definition of min/max-width/height applied more generally, this behavior would fall out automatically.
// AD-HOC: We don't do this when the flex container is being sized under a min-content or max-content constraint.
if (is_single_line() && !m_available_space_for_flex_container->cross.is_intrinsic_sizing_constraint()) {
if (is_single_line() && !m_available_space_for_items->cross.is_intrinsic_sizing_constraint()) {
auto const& computed_min_size = this->computed_cross_min_size(flex_container());
auto const& computed_max_size = this->computed_cross_max_size(flex_container());
auto cross_min_size = (!computed_min_size.is_auto() && !computed_min_size.contains_percentage()) ? specified_cross_min_size(flex_container()) : 0;
@ -1553,7 +1437,7 @@ void FlexFormattingContext::determine_flex_container_used_cross_size()
}
// AD-HOC: We don't apply min/max cross size constraints when sizing the flex container under an intrinsic sizing constraint.
if (!m_available_space_for_flex_container->cross.is_intrinsic_sizing_constraint()) {
if (!m_available_space_for_items->cross.is_intrinsic_sizing_constraint()) {
auto const& computed_min_size = this->computed_cross_min_size(flex_container());
auto const& computed_max_size = this->computed_cross_max_size(flex_container());
auto cross_min_size = (!computed_min_size.is_auto() && !computed_min_size.contains_percentage()) ? specified_cross_min_size(flex_container()) : 0;
@ -1689,7 +1573,7 @@ void FlexFormattingContext::copy_dimensions_from_flex_items_to_boxes()
// https://drafts.csswg.org/css-flexbox-1/#intrinsic-sizes
void FlexFormattingContext::determine_intrinsic_size_of_flex_container()
{
if (m_available_space_for_flex_container->main.is_intrinsic_sizing_constraint()) {
if (m_available_space_for_items->main.is_intrinsic_sizing_constraint()) {
CSSPixels main_size = calculate_intrinsic_main_size_of_flex_container();
set_main_size(flex_container(), main_size);
}
@ -2051,7 +1935,7 @@ CSSPixels FlexFormattingContext::calculate_fit_content_cross_size(FlexItem const
CSSPixels FlexFormattingContext::calculate_min_content_cross_size(FlexItem const& item) const
{
if (is_row_layout()) {
auto available_space = m_state.get(item.box).available_inner_space_or_constraints_from(m_available_space_for_flex_container->space);
auto available_space = m_state.get(item.box).available_inner_space_or_constraints_from(m_available_space_for_items->space);
if (available_space.width.is_indefinite()) {
available_space.width = AvailableSize::make_definite(calculate_width_to_use_when_determining_intrinsic_height_of_item(item));
}
@ -2063,7 +1947,7 @@ CSSPixels FlexFormattingContext::calculate_min_content_cross_size(FlexItem const
CSSPixels FlexFormattingContext::calculate_max_content_cross_size(FlexItem const& item) const
{
if (is_row_layout()) {
auto available_space = m_state.get(item.box).available_inner_space_or_constraints_from(m_available_space_for_flex_container->space);
auto available_space = m_state.get(item.box).available_inner_space_or_constraints_from(m_available_space_for_items->space);
if (available_space.width.is_indefinite()) {
available_space.width = AvailableSize::make_definite(calculate_width_to_use_when_determining_intrinsic_height_of_item(item));
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021-2024, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -24,10 +24,6 @@ public:
Box const& flex_container() const { return context_box(); }
virtual bool can_determine_size_of_child() const override;
virtual void determine_width_of_child(Box const&, AvailableSpace const&) override;
virtual void determine_height_of_child(Box const&, AvailableSpace const&) override;
virtual CSSPixelPoint calculate_static_position(Box const&) const override;
private:
@ -162,8 +158,6 @@ private:
void determine_flex_base_size_and_hypothetical_main_size(FlexItem&);
void determine_main_size_of_flex_container();
void collect_flex_items_into_flex_lines();
void resolve_flexible_lengths();
@ -229,7 +223,6 @@ private:
AvailableSpace space;
};
Optional<AxisAgnosticAvailableSpace> m_available_space_for_items;
Optional<AxisAgnosticAvailableSpace> m_available_space_for_flex_container;
};
}

View file

@ -89,10 +89,6 @@ public:
[[nodiscard]] CSSPixels calculate_stretch_fit_width(Box const&, AvailableSize const&) const;
[[nodiscard]] CSSPixels calculate_stretch_fit_height(Box const&, AvailableSize const&) const;
virtual bool can_determine_size_of_child() const { return false; }
virtual void determine_width_of_child(Box const&, AvailableSpace const&) { }
virtual void determine_height_of_child(Box const&, AvailableSpace const&) { }
virtual CSSPixelPoint calculate_static_position(Box const&) const;
bool can_skip_is_anonymous_text_run(Box&);

View file

@ -172,6 +172,10 @@ void InlineFormattingContext::dimension_box_on_line(Box const& box, LayoutMode l
box_state.set_content_width(width);
// NOTE: Flex containers with `auto` height are treated as `max-content`, so we can compute their height early.
if (box_state.has_definite_height() || box.display().is_flex_inside())
parent().compute_height(box, AvailableSpace(AvailableSize::make_definite(width), AvailableSize::make_definite(m_containing_block_state.content_height())));
auto independent_formatting_context = layout_inside(box, layout_mode, box_state.available_inner_space_or_constraints_from(*m_available_space));
auto const& height_value = box.computed_values().height();
@ -389,21 +393,6 @@ bool InlineFormattingContext::can_fit_new_line_at_y(CSSPixels y) const
return true;
}
bool InlineFormattingContext::can_determine_size_of_child() const
{
return parent().can_determine_size_of_child();
}
void InlineFormattingContext::determine_width_of_child(Box const& box, AvailableSpace const& available_space)
{
return parent().determine_width_of_child(box, available_space);
}
void InlineFormattingContext::determine_height_of_child(Box const& box, AvailableSpace const& available_space)
{
return parent().determine_height_of_child(box, available_space);
}
CSSPixels InlineFormattingContext::vertical_float_clearance() const
{
return m_vertical_float_clearance;

View file

@ -34,10 +34,6 @@ public:
bool any_floats_intrude_at_y(CSSPixels y) const;
bool can_fit_new_line_at_y(CSSPixels y) const;
virtual bool can_determine_size_of_child() const override;
virtual void determine_width_of_child(Box const&, AvailableSpace const&) override;
virtual void determine_height_of_child(Box const&, AvailableSpace const&) override;
CSSPixels vertical_float_clearance() const;
void set_vertical_float_clearance(CSSPixels);