LibWeb: Make two passes in FFC where to support percentage min/max sizes

Percentage sizes of flex items are relative to the flex container, but
even when the flex container is automatically sized, we still have to
support them.

To make this work, we first do a pass where percentage sizes are ignored
(treated as "auto", basically) in order to get a "reference" value.

Then we do a second pass where percentages can be resolved against this
reference value.
This commit is contained in:
Andreas Kling 2022-07-22 22:02:02 +02:00
parent 3def9e47f6
commit 7200b3a16c
2 changed files with 150 additions and 70 deletions

View file

@ -136,7 +136,7 @@ void FlexFormattingContext::run(Box const& run_box, LayoutMode layout_mode)
// Cross Size Determination
// 7. Determine the hypothetical cross size of each item
for (auto& flex_item : m_flex_items) {
determine_hypothetical_cross_size_of_item(flex_item);
determine_hypothetical_cross_size_of_item(flex_item, false);
}
// 8. Calculate the cross size of each flex line.
@ -176,6 +176,18 @@ void FlexFormattingContext::run(Box const& run_box, LayoutMode layout_mode)
}
}
{
// NOTE: We re-resolve cross sizes here, now that we can resolve percentages.
// 7. Determine the hypothetical cross size of each item
for (auto& flex_item : m_flex_items) {
determine_hypothetical_cross_size_of_item(flex_item, true);
}
// 11. Determine the used cross size of each flex item.
determine_used_cross_size_of_each_flex_item();
}
// 16. Align all flex lines (per align-content)
align_all_flex_lines();
@ -706,7 +718,7 @@ void FlexFormattingContext::determine_flex_base_size_and_hypothetical_main_size(
// FIXME: This is probably too naive.
// FIXME: Care about FlexBasis::Auto
if (has_definite_main_size(child_box))
return specified_main_size_of_child_box(child_box);
return resolved_definite_main_size(child_box);
// NOTE: To avoid repeated layout work, we keep a cache of flex item main sizes on the
// root LayoutState object. It's available through a full layout cycle.
@ -1026,40 +1038,55 @@ void FlexFormattingContext::resolve_flexible_lengths()
}
// https://drafts.csswg.org/css-flexbox-1/#algo-cross-item
void FlexFormattingContext::determine_hypothetical_cross_size_of_item(FlexItem& item)
void FlexFormattingContext::determine_hypothetical_cross_size_of_item(FlexItem& item, bool resolve_percentage_min_max_sizes)
{
// Determine the hypothetical cross size of each item by performing layout
// as if it were an in-flow block-level box with the used main size
// and the given available space, treating auto as fit-content.
auto const& computed_min_size = this->computed_cross_min_size(item.box);
auto const& computed_max_size = this->computed_cross_max_size(item.box);
auto clamp_min = (!computed_min_size.is_auto() && (resolve_percentage_min_max_sizes || !computed_min_size.is_percentage())) ? specified_cross_min_size(item.box) : 0;
auto clamp_max = (!computed_max_size.is_auto() && (resolve_percentage_min_max_sizes || !computed_max_size.is_percentage())) ? specified_cross_max_size(item.box) : NumericLimits<float>::max();
// If we have a definite cross size, this is easy! No need to perform layout, we can just use it as-is.
if (has_definite_cross_size(item.box)) {
auto clamp_min = has_cross_min_size(item.box) ? specified_cross_min_size(item.box) : 0;
auto clamp_max = has_cross_max_size(item.box) ? specified_cross_max_size(item.box) : NumericLimits<float>::max();
item.hypothetical_cross_size = css_clamp(resolved_definite_cross_size(item.box), clamp_min, clamp_max);
return;
}
if (has_definite_main_size(item.box)) {
// For indefinite cross sizes, we perform a throwaway layout and then measure it.
LayoutState throwaway_state(&m_state);
auto& box_state = throwaway_state.get_mutable(item.box);
// Item has definite main size, layout with that as the used main size.
auto independent_formatting_context = create_independent_formatting_context_if_needed(throwaway_state, item.box);
// NOTE: Flex items should always create an independent formatting context!
VERIFY(independent_formatting_context);
independent_formatting_context->run(item.box, LayoutMode::Normal);
if (is_row_layout())
item.hypothetical_cross_size = BlockFormattingContext::compute_theoretical_height(throwaway_state, item.box);
else
item.hypothetical_cross_size = box_state.content_width();
} else {
// Item has indefinite main size, layout with "fit-content"
item.hypothetical_cross_size = calculate_fit_content_cross_size(item);
if (computed_cross_size(item.box).is_auto()) {
// Item has automatic cross size, layout with "fit-content"
item.hypothetical_cross_size = css_clamp(calculate_fit_content_cross_size(item), clamp_min, clamp_max);
return;
}
// For indefinite cross sizes, we perform a throwaway layout and then measure it.
LayoutState throwaway_state(&m_state);
auto& containing_block_state = throwaway_state.get_mutable(flex_container());
if (is_row_layout()) {
containing_block_state.set_content_width(item.main_size);
containing_block_state.set_has_definite_width(true);
} else {
containing_block_state.set_content_height(item.main_size);
containing_block_state.set_has_definite_height(true);
}
auto& box_state = throwaway_state.get_mutable(item.box);
// Item has definite main size, layout with that as the used main size.
auto independent_formatting_context = create_independent_formatting_context_if_needed(throwaway_state, item.box);
// NOTE: Flex items should always create an independent formatting context!
VERIFY(independent_formatting_context);
independent_formatting_context->run(item.box, LayoutMode::Normal);
auto automatic_cross_size = is_row_layout() ? BlockFormattingContext::compute_theoretical_height(throwaway_state, item.box)
: box_state.content_width();
item.hypothetical_cross_size = css_clamp(automatic_cross_size, clamp_min, clamp_max);
}
// https://www.w3.org/TR/css-flexbox-1/#algo-cross-line
@ -1449,8 +1476,6 @@ float FlexFormattingContext::calculate_intrinsic_main_size_of_flex_container(Lay
m_flex_lines.last().items.append(&flex_item);
}
float largest_sum = 0;
// Within each line, find the greatest (most positive) desired flex fraction among all the flex items.
// This is the lines chosen flex fraction.
for (auto& flex_line : m_flex_lines) {
@ -1474,33 +1499,47 @@ float FlexFormattingContext::calculate_intrinsic_main_size_of_flex_container(Lay
if (chosen_flex_fraction < 0 && sum_of_flex_shrink_factors < 1)
chosen_flex_fraction *= sum_of_flex_shrink_factors;
// 4. Add each items flex base size to the product of its flex grow factor (scaled flex shrink factor, if shrinking)
// and the chosen flex fraction, then clamp that result by the max main size floored by the min main size.
float sum = 0;
for (auto& flex_item : flex_line.items) {
float product = 0;
if (flex_item->desired_flex_fraction > 0)
product = chosen_flex_fraction * flex_item->box.computed_values().flex_grow();
else if (flex_item->desired_flex_fraction < 0)
product = chosen_flex_fraction * flex_item->scaled_flex_shrink_factor;
auto result = flex_item->flex_base_size + product;
auto clamp_min = has_main_min_size(flex_item->box) ? specified_main_min_size(flex_item->box) : automatic_minimum_size(*flex_item);
auto clamp_max = has_main_max_size(flex_item->box) ? specified_main_max_size(flex_item->box) : NumericLimits<float>::max();
result = css_clamp(result, clamp_min, clamp_max);
// NOTE: The spec doesn't mention anything about the *outer* size here, but if we don't add the margin box,
// flex items with non-zero padding/border/margin in the main axis end up overflowing the container.
result = flex_item->add_main_margin_box_sizes(result);
sum += result;
}
// 5. The flex containers max-content size is the largest sum (among all the lines) of the afore-calculated sizes of all items within a single line.
largest_sum = max(largest_sum, sum);
flex_line.chosen_flex_fraction = chosen_flex_fraction;
}
return largest_sum;
auto determine_main_size = [&](bool resolve_percentage_min_max_sizes) -> float {
float largest_sum = 0;
for (auto& flex_line : m_flex_lines) {
// 4. Add each items flex base size to the product of its flex grow factor (scaled flex shrink factor, if shrinking)
// and the chosen flex fraction, then clamp that result by the max main size floored by the min main size.
float sum = 0;
for (auto& flex_item : flex_line.items) {
float product = 0;
if (flex_item->desired_flex_fraction > 0)
product = flex_line.chosen_flex_fraction * flex_item->box.computed_values().flex_grow();
else if (flex_item->desired_flex_fraction < 0)
product = flex_line.chosen_flex_fraction * flex_item->scaled_flex_shrink_factor;
auto result = flex_item->flex_base_size + product;
auto const& computed_min_size = this->computed_main_min_size(flex_item->box);
auto const& computed_max_size = this->computed_main_max_size(flex_item->box);
auto clamp_min = (!computed_min_size.is_auto() && (resolve_percentage_min_max_sizes || !computed_min_size.is_percentage())) ? specified_main_min_size(flex_item->box) : automatic_minimum_size(*flex_item);
auto clamp_max = (!computed_max_size.is_auto() && (resolve_percentage_min_max_sizes || !computed_max_size.is_percentage())) ? specified_main_max_size(flex_item->box) : NumericLimits<float>::max();
result = css_clamp(result, clamp_min, clamp_max);
// NOTE: The spec doesn't mention anything about the *outer* size here, but if we don't add the margin box,
// flex items with non-zero padding/border/margin in the main axis end up overflowing the container.
result = flex_item->add_main_margin_box_sizes(result);
sum += result;
}
largest_sum = max(largest_sum, sum);
}
// 5. The flex containers max-content size is the largest sum (among all the lines) of the afore-calculated sizes of all items within a single line.
return largest_sum;
};
auto first_pass_main_size = determine_main_size(false);
set_main_size(flex_container(), first_pass_main_size);
auto second_pass_main_size = determine_main_size(true);
return second_pass_main_size;
}
// https://drafts.csswg.org/css-flexbox-1/#intrinsic-cross-sizes
@ -1511,16 +1550,23 @@ float FlexFormattingContext::calculate_intrinsic_cross_size_of_flex_container(La
// The min-content/max-content cross size of a single-line flex container
// is the largest min-content contribution/max-content contribution (respectively) of its flex items.
if (is_single_line()) {
float largest_contribution = 0;
for (auto& flex_item : m_flex_items) {
float contribution;
if (m_flex_container_state.width_constraint == SizeConstraint::MinContent || m_flex_container_state.height_constraint == SizeConstraint::MinContent)
contribution = calculate_cross_min_content_contribution(flex_item);
else if (m_flex_container_state.width_constraint == SizeConstraint::MaxContent || m_flex_container_state.height_constraint == SizeConstraint::MaxContent)
contribution = calculate_cross_max_content_contribution(flex_item);
largest_contribution = max(largest_contribution, contribution);
}
return largest_contribution;
auto calculate_largest_contribution = [&](bool resolve_percentage_min_max_sizes) {
float largest_contribution = 0;
for (auto& flex_item : m_flex_items) {
float contribution;
if (m_flex_container_state.width_constraint == SizeConstraint::MinContent || m_flex_container_state.height_constraint == SizeConstraint::MinContent)
contribution = calculate_cross_min_content_contribution(flex_item, resolve_percentage_min_max_sizes);
else if (m_flex_container_state.width_constraint == SizeConstraint::MaxContent || m_flex_container_state.height_constraint == SizeConstraint::MaxContent)
contribution = calculate_cross_max_content_contribution(flex_item, resolve_percentage_min_max_sizes);
largest_contribution = max(largest_contribution, contribution);
}
return largest_contribution;
};
auto first_pass_largest_contribution = calculate_largest_contribution(false);
set_cross_size(flex_container(), first_pass_largest_contribution);
auto second_pass_largest_contribution = calculate_largest_contribution(true);
return second_pass_largest_contribution;
}
// For a multi-line flex container, the min-content/max-content cross size is the sum of the flex line cross sizes
@ -1577,7 +1623,7 @@ float FlexFormattingContext::calculate_main_max_content_contribution(FlexItem co
return item.add_main_margin_box_sizes(clamped_inner_size);
}
float FlexFormattingContext::calculate_cross_min_content_contribution(FlexItem const& item) const
float FlexFormattingContext::calculate_cross_min_content_contribution(FlexItem const& item, bool resolve_percentage_min_max_sizes) const
{
auto larger_size = [&] {
auto inner_min_content_size = calculate_min_content_cross_size(item);
@ -1587,14 +1633,18 @@ float FlexFormattingContext::calculate_cross_min_content_contribution(FlexItem c
return max(inner_min_content_size, inner_preferred_size);
}();
auto clamp_min = has_cross_min_size(item.box) ? specified_cross_min_size(item.box) : 0;
auto clamp_max = has_cross_max_size(item.box) ? specified_cross_max_size(item.box) : NumericLimits<float>::max();
auto const& computed_min_size = this->computed_cross_min_size(item.box);
auto const& computed_max_size = this->computed_cross_max_size(item.box);
auto clamp_min = (!computed_min_size.is_auto() && (resolve_percentage_min_max_sizes || !computed_min_size.is_percentage())) ? specified_cross_min_size(item.box) : 0;
auto clamp_max = (!computed_max_size.is_auto() && (resolve_percentage_min_max_sizes || !computed_max_size.is_percentage())) ? specified_cross_max_size(item.box) : NumericLimits<float>::max();
auto clamped_inner_size = css_clamp(larger_size, clamp_min, clamp_max);
return item.add_cross_margin_box_sizes(clamped_inner_size);
}
float FlexFormattingContext::calculate_cross_max_content_contribution(FlexItem const& item) const
float FlexFormattingContext::calculate_cross_max_content_contribution(FlexItem const& item, bool resolve_percentage_min_max_sizes) const
{
auto larger_size = [&] {
auto inner_max_content_size = calculate_max_content_cross_size(item);
@ -1604,8 +1654,12 @@ float FlexFormattingContext::calculate_cross_max_content_contribution(FlexItem c
return max(inner_max_content_size, inner_preferred_size);
}();
auto clamp_min = has_cross_min_size(item.box) ? specified_cross_min_size(item.box) : 0;
auto clamp_max = has_cross_max_size(item.box) ? specified_cross_max_size(item.box) : NumericLimits<float>::max();
auto const& computed_min_size = this->computed_cross_min_size(item.box);
auto const& computed_max_size = this->computed_cross_max_size(item.box);
auto clamp_min = (!computed_min_size.is_auto() && (resolve_percentage_min_max_sizes || !computed_min_size.is_percentage())) ? specified_cross_min_size(item.box) : 0;
auto clamp_max = (!computed_max_size.is_auto() && (resolve_percentage_min_max_sizes || !computed_max_size.is_percentage())) ? specified_cross_max_size(item.box) : NumericLimits<float>::max();
auto clamped_inner_size = css_clamp(larger_size, clamp_min, clamp_max);
return item.add_cross_margin_box_sizes(clamped_inner_size);
@ -1668,9 +1722,30 @@ CSS::LengthPercentage const& FlexFormattingContext::computed_main_size(Box const
{
return is_row_layout() ? box.computed_values().width() : box.computed_values().height();
}
CSS::LengthPercentage const& FlexFormattingContext::computed_main_min_size(Box const& box) const
{
return is_row_layout() ? box.computed_values().min_width() : box.computed_values().min_height();
}
CSS::LengthPercentage const& FlexFormattingContext::computed_main_max_size(Box const& box) const
{
return is_row_layout() ? box.computed_values().max_width() : box.computed_values().max_height();
}
CSS::LengthPercentage const& FlexFormattingContext::computed_cross_size(Box const& box) const
{
return !is_row_layout() ? box.computed_values().width() : box.computed_values().height();
}
CSS::LengthPercentage const& FlexFormattingContext::computed_cross_min_size(Box const& box) const
{
return !is_row_layout() ? box.computed_values().min_width() : box.computed_values().min_height();
}
CSS::LengthPercentage const& FlexFormattingContext::computed_cross_max_size(Box const& box) const
{
return !is_row_layout() ? box.computed_values().max_width() : box.computed_values().max_height();
}
}

View file

@ -78,6 +78,7 @@ private:
Vector<FlexItem*> items;
float cross_size { 0 };
float remaining_free_space { 0 };
float chosen_flex_fraction { 0 };
};
bool has_definite_main_size(Box const&) const;
@ -106,7 +107,11 @@ private:
Optional<float> transferred_size_suggestion(FlexItem const&) const;
float content_size_suggestion(FlexItem const&) const;
CSS::LengthPercentage const& computed_main_size(Box const&) const;
CSS::LengthPercentage const& computed_main_min_size(Box const&) const;
CSS::LengthPercentage const& computed_main_max_size(Box const&) const;
CSS::LengthPercentage const& computed_cross_size(Box const&) const;
CSS::LengthPercentage const& computed_cross_min_size(Box const&) const;
CSS::LengthPercentage const& computed_cross_max_size(Box const&) const;
float get_pixel_width(Box const& box, Optional<CSS::LengthPercentage> const& length_percentage) const;
float get_pixel_height(Box const& box, Optional<CSS::LengthPercentage> const& length_percentage) const;
@ -136,7 +141,7 @@ private:
void resolve_flexible_lengths();
void determine_hypothetical_cross_size_of_item(FlexItem&);
void determine_hypothetical_cross_size_of_item(FlexItem&, bool resolve_percentage_min_max_sizes);
void calculate_cross_size_of_each_flex_line(float cross_min_size, float cross_max_size);
@ -161,8 +166,8 @@ private:
[[nodiscard]] float calculate_intrinsic_main_size_of_flex_container(LayoutMode);
[[nodiscard]] float calculate_intrinsic_cross_size_of_flex_container(LayoutMode);
[[nodiscard]] float calculate_cross_min_content_contribution(FlexItem const&) const;
[[nodiscard]] float calculate_cross_max_content_contribution(FlexItem const&) const;
[[nodiscard]] float calculate_cross_min_content_contribution(FlexItem const&, bool resolve_percentage_min_max_sizes) const;
[[nodiscard]] float calculate_cross_max_content_contribution(FlexItem const&, bool resolve_percentage_min_max_sizes) const;
[[nodiscard]] float calculate_main_min_content_contribution(FlexItem const&) const;
[[nodiscard]] float calculate_main_max_content_contribution(FlexItem const&) const;