diff options
author | Andreas Kling <kling@serenityos.org> | 2023-03-08 21:18:35 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2023-03-10 13:38:34 +0100 |
commit | 109ed27423959cce1a0b50b1b6f883f06a19736d (patch) | |
tree | 6b1c2f9d0ec9c01ca7bcd978932a090f504b51d2 | |
parent | 1cd61723f07872bb82b990a9d1c730d204bd2d20 (diff) | |
download | serenity-109ed27423959cce1a0b50b1b6f883f06a19736d.zip |
LibWeb: Rewrite FFC "resolve flexible lengths" algorithm from draft spec
The draft CSS-FLEXBOX-1 spec had a more detailed description of this
algorithm, so let's use that as our basis for the implementation.
Test by Aliaksandr. :^)
4 files changed, 264 insertions, 145 deletions
diff --git a/Tests/LibWeb/Layout/expected/flex-column-height-unconstrained.txt b/Tests/LibWeb/Layout/expected/flex-column-height-unconstrained.txt new file mode 100644 index 0000000000..cbeb91b8b4 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/flex-column-height-unconstrained.txt @@ -0,0 +1,29 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer <html> at (0,0) content-size 800x324 children: not-inline + BlockContainer <body> at (8,8) content-size 784x308 children: not-inline + Box <div.my-container.column> at (9,9) content-size 782x306 flex-container(column) children: not-inline + BlockContainer <(anonymous)> at (9,9) content-size 0x0 children: inline + TextNode <#text> + BlockContainer <div.box> at (10,10) content-size 100x100 flex-item children: inline + line 0 width: 6.34375, height: 17.46875, bottom: 17.46875, baseline: 13.53125 + frag 0 from TextNode start: 0, length: 1, rect: [10,10 6.34375x17.46875] + "1" + TextNode <#text> + BlockContainer <(anonymous)> at (9,9) content-size 0x0 children: inline + TextNode <#text> + BlockContainer <div.box> at (10,112) content-size 100x100 flex-item children: inline + line 0 width: 8.8125, height: 17.46875, bottom: 17.46875, baseline: 13.53125 + frag 0 from TextNode start: 0, length: 1, rect: [10,112 8.8125x17.46875] + "2" + TextNode <#text> + BlockContainer <(anonymous)> at (9,9) content-size 0x0 children: inline + TextNode <#text> + BlockContainer <div.box> at (10,214) content-size 100x100 flex-item children: inline + line 0 width: 9.09375, height: 17.46875, bottom: 17.46875, baseline: 13.53125 + frag 0 from TextNode start: 0, length: 1, rect: [10,214 9.09375x17.46875] + "3" + TextNode <#text> + BlockContainer <(anonymous)> at (9,9) content-size 0x0 children: inline + TextNode <#text> + BlockContainer <(anonymous)> at (8,316) content-size 784x0 children: inline + TextNode <#text> diff --git a/Tests/LibWeb/Layout/input/flex-column-height-unconstrained.html b/Tests/LibWeb/Layout/input/flex-column-height-unconstrained.html new file mode 100644 index 0000000000..8a98c3901b --- /dev/null +++ b/Tests/LibWeb/Layout/input/flex-column-height-unconstrained.html @@ -0,0 +1,25 @@ +<style> + body { + font-family: 'SerenitySans'; + } + + .my-container { + display: flex; + border: 1px solid salmon; + } + + .column { + flex-direction: column; + } + + .box { + width: 100px; + height: 100px; + border: 1px solid black; + } +</style> +<div class="my-container column"> + <div class="box">1</div> + <div class="box">2</div> + <div class="box">3</div> +</div> diff --git a/Userland/Libraries/LibWeb/Layout/FlexFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/FlexFormattingContext.cpp index 8db71fc121..2dd053c07c 100644 --- a/Userland/Libraries/LibWeb/Layout/FlexFormattingContext.cpp +++ b/Userland/Libraries/LibWeb/Layout/FlexFormattingContext.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org> + * Copyright (c) 2021-2023, Andreas Kling <kling@serenityos.org> * Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause @@ -869,176 +869,203 @@ void FlexFormattingContext::collect_flex_items_into_flex_lines() m_flex_lines.append(move(line)); } -// https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths -void FlexFormattingContext::resolve_flexible_lengths() +// https://drafts.csswg.org/css-flexbox-1/#resolve-flexible-lengths +void FlexFormattingContext::resolve_flexible_lengths_for_line(FlexLine& line) { + // 1. Determine the used flex factor. + + // Sum the outer hypothetical main sizes of all items on the line. + // If the sum is less than the flex container’s inner main size, + // use the flex grow factor for the rest of this algorithm; otherwise, use the flex shrink factor enum FlexFactor { FlexGrowFactor, FlexShrinkFactor }; + auto used_flex_factor = [&]() -> FlexFactor { + CSSPixels sum = 0; + for (auto const& item : line.items) { + sum += item.outer_hypothetical_main_size(); + } + if (sum < inner_main_size(flex_container())) + return FlexFactor::FlexGrowFactor; + return FlexFactor::FlexShrinkFactor; + }(); - FlexFactor used_flex_factor; - // 6.1. Determine used flex factor - for (auto& flex_line : m_flex_lines) { - size_t number_of_unfrozen_items_on_line = flex_line.items.size(); + // 2. Each item in the flex line has a target main size, initially set to its flex base size. + // Each item is initially unfrozen and may become frozen. + for (auto& item : line.items) { + item.target_main_size = item.flex_base_size; + item.frozen = false; + } - CSSPixels sum_of_hypothetical_main_sizes = 0; - for (auto& item : flex_line.items) { - sum_of_hypothetical_main_sizes += (item.hypothetical_main_size + item.margins.main_before + item.margins.main_after + item.borders.main_before + item.borders.main_after + item.padding.main_before + item.padding.main_after); + // 3. Size inflexible items. + + for (FlexItem& item : line.items) { + if (used_flex_factor == FlexFactor::FlexGrowFactor) { + item.flex_factor = item.box.computed_values().flex_grow(); + } else if (used_flex_factor == FlexFactor::FlexShrinkFactor) { + item.flex_factor = item.box.computed_values().flex_shrink(); + } + // Freeze, setting its target main size to its hypothetical main size… + // - any item that has a flex factor of zero + // - if using the flex grow factor: any item that has a flex base size greater than its hypothetical main size + // - if using the flex shrink factor: any item that has a flex base size smaller than its hypothetical main size + if (item.flex_factor.value() == 0 + || (used_flex_factor == FlexFactor::FlexGrowFactor && item.flex_base_size > item.hypothetical_main_size) + || (used_flex_factor == FlexFactor::FlexShrinkFactor && item.flex_base_size < item.hypothetical_main_size)) { + item.frozen = true; + item.target_main_size = item.hypothetical_main_size; } - if (sum_of_hypothetical_main_sizes < m_available_space_for_items->main.to_px_or_zero()) - used_flex_factor = FlexFactor::FlexGrowFactor; - else - used_flex_factor = FlexFactor::FlexShrinkFactor; + } - for (auto& item : flex_line.items) { - if (used_flex_factor == FlexFactor::FlexGrowFactor) - item.flex_factor = item.box.computed_values().flex_grow(); - else if (used_flex_factor == FlexFactor::FlexShrinkFactor) - item.flex_factor = item.box.computed_values().flex_shrink(); + // 4. Calculate initial free space + + // Sum the outer sizes of all items on the line, and subtract this from the flex container’s inner main size. + // For frozen items, use their outer target main size; for other items, use their outer flex base size. + auto calculate_remaining_free_space = [&]() -> CSSPixels { + CSSPixels sum = 0; + for (auto const& item : line.items) { + if (item.frozen) + sum += item.outer_target_main_size(); + else + sum += item.outer_flex_base_size(); } + return inner_main_size(flex_container()) - sum; + }; + auto const initial_free_space = calculate_remaining_free_space(); - // 6.2. Size inflexible items - auto freeze_item_setting_target_main_size_to_hypothetical_main_size = [&number_of_unfrozen_items_on_line](FlexItem& item) { - item.target_main_size = item.hypothetical_main_size; - number_of_unfrozen_items_on_line--; - item.frozen = true; - }; - for (auto& item : flex_line.items) { - if (item.flex_factor.has_value() && item.flex_factor.value() == 0) { - freeze_item_setting_target_main_size_to_hypothetical_main_size(item); - } else if (used_flex_factor == FlexFactor::FlexGrowFactor) { - // FIXME: Spec doesn't include the == case, but we take a too basic approach to calculating the values used so this is appropriate - if (item.flex_base_size > item.hypothetical_main_size) { - freeze_item_setting_target_main_size_to_hypothetical_main_size(item); + // 5. Loop + while (true) { + // a. Check for flexible items. + // If all the flex items on the line are frozen, free space has been distributed; exit this loop. + if (all_of(line.items, [](auto const& item) { return item.frozen; })) { + break; + } + + // b. Calculate the remaining free space as for initial free space, above. + line.remaining_free_space = calculate_remaining_free_space(); + + // If the sum of the unfrozen flex items’ flex factors is less than one, multiply the initial free space by this sum. + if (auto sum_of_flex_factor_of_unfrozen_items = line.sum_of_flex_factor_of_unfrozen_items(); sum_of_flex_factor_of_unfrozen_items < 1) { + auto value = initial_free_space * sum_of_flex_factor_of_unfrozen_items; + // If the magnitude of this value is less than the magnitude of the remaining free space, use this as the remaining free space. + if (abs(value) < abs(line.remaining_free_space)) + line.remaining_free_space = value; + } + + // c. If the remaining free space is non-zero, distribute it proportional to the flex factors: + if (line.remaining_free_space != 0) { + // If using the flex grow factor + if (used_flex_factor == FlexFactor::FlexGrowFactor) { + // For every unfrozen item on the line, + // find the ratio of the item’s flex grow factor to the sum of the flex grow factors of all unfrozen items on the line. + auto sum_of_flex_factor_of_unfrozen_items = line.sum_of_flex_factor_of_unfrozen_items(); + for (auto& item : line.items) { + if (item.frozen) + continue; + float ratio = item.flex_factor.value() / sum_of_flex_factor_of_unfrozen_items; + // Set the item’s target main size to its flex base size plus a fraction of the remaining free space proportional to the ratio. + item.target_main_size = item.flex_base_size + (line.remaining_free_space * ratio); + } + } + // If using the flex shrink factor + else if (used_flex_factor == FlexFactor::FlexShrinkFactor) { + // For every unfrozen item on the line, multiply its flex shrink factor by its inner flex base size, and note this as its scaled flex shrink factor. + for (auto& item : line.items) { + item.scaled_flex_shrink_factor = item.flex_factor.value() * item.flex_base_size.value(); } - } else if (used_flex_factor == FlexFactor::FlexShrinkFactor) { - if (item.flex_base_size < item.hypothetical_main_size) { - freeze_item_setting_target_main_size_to_hypothetical_main_size(item); + auto sum_of_scaled_flex_shrink_factors_of_all_unfrozen_items_on_line = line.sum_of_scaled_flex_shrink_factor_of_unfrozen_items(); + for (auto& item : line.items) { + // Find the ratio of the item’s scaled flex shrink factor to the sum of the scaled flex shrink factors of all unfrozen items on the line. + float ratio = 1.0f; + if (sum_of_scaled_flex_shrink_factors_of_all_unfrozen_items_on_line != 0) + ratio = item.scaled_flex_shrink_factor / sum_of_scaled_flex_shrink_factors_of_all_unfrozen_items_on_line; + + // Set the item’s target main size to its flex base size minus a fraction of the absolute value of the remaining free space proportional to the ratio. + // (Note this may result in a negative inner main size; it will be corrected in the next step.) + item.target_main_size = item.flex_base_size - (abs(line.remaining_free_space) * ratio); } } } - // 6.3. Calculate initial free space - auto calculate_free_space = [&]() { - CSSPixels sum_of_items_on_line = 0; - for (auto& item : flex_line.items) { - if (item.frozen) - sum_of_items_on_line += item.target_main_size + item.margins.main_before + item.margins.main_after + item.borders.main_before + item.borders.main_after + item.padding.main_before + item.padding.main_after; - else - sum_of_items_on_line += item.flex_base_size + item.margins.main_before + item.margins.main_after + item.borders.main_before + item.borders.main_after + item.padding.main_before + item.padding.main_after; - } - return m_available_space_for_items->main.to_px_or_zero() - sum_of_items_on_line; - }; + // d. Fix min/max violations. + CSSPixels total_violation = 0; - CSSPixels initial_free_space = calculate_free_space(); - flex_line.remaining_free_space = initial_free_space; + // Clamp each non-frozen item’s target main size by its used min and max main sizes and floor its content-box size at zero. + for (auto& item : line.items) { + if (item.frozen) + continue; + auto used_min_main_size = has_main_min_size(item.box) + ? specified_main_min_size(item.box) + : automatic_minimum_size(item); - // 6.4 Loop - auto for_each_unfrozen_item = [&flex_line](auto callback) { - for (auto& item : flex_line.items) { - if (!item.frozen) - callback(item); - } - }; + auto used_max_main_size = has_main_max_size(item.box) + ? specified_main_max_size(item.box) + : NumericLimits<float>::max(); - while (number_of_unfrozen_items_on_line > 0) { - // b Calculate the remaining free space - flex_line.remaining_free_space = calculate_free_space(); - float sum_of_unfrozen_flex_items_flex_factors = 0; - for_each_unfrozen_item([&](FlexItem const& item) { - sum_of_unfrozen_flex_items_flex_factors += item.flex_factor.value_or(1); - }); - - if (sum_of_unfrozen_flex_items_flex_factors < 1) { - auto intermediate_free_space = initial_free_space * sum_of_unfrozen_flex_items_flex_factors; - if (abs(intermediate_free_space) < abs(flex_line.remaining_free_space)) - flex_line.remaining_free_space = intermediate_free_space; - } + auto original_target_main_size = item.target_main_size; + item.target_main_size = css_clamp(item.target_main_size, used_min_main_size, used_max_main_size); + item.target_main_size = max(item.target_main_size, CSSPixels(0)); - // c Distribute free space proportional to the flex factors - if (flex_line.remaining_free_space != 0) { - if (used_flex_factor == FlexFactor::FlexGrowFactor) { - float sum_of_flex_grow_factor_of_unfrozen_items = sum_of_unfrozen_flex_items_flex_factors; - for_each_unfrozen_item([&](FlexItem& item) { - float ratio = item.flex_factor.value_or(1) / sum_of_flex_grow_factor_of_unfrozen_items; - item.target_main_size = item.flex_base_size + (flex_line.remaining_free_space * ratio); - }); - } else if (used_flex_factor == FlexFactor::FlexShrinkFactor) { - float sum_of_scaled_flex_shrink_factor_of_unfrozen_items = 0; - for_each_unfrozen_item([&](FlexItem& item) { - item.scaled_flex_shrink_factor = item.flex_factor.value_or(1) * item.flex_base_size.value(); - sum_of_scaled_flex_shrink_factor_of_unfrozen_items += item.scaled_flex_shrink_factor; - }); - - for_each_unfrozen_item([&](FlexItem& item) { - float ratio = 1.0f; - if (sum_of_scaled_flex_shrink_factor_of_unfrozen_items != 0.0f) - ratio = item.scaled_flex_shrink_factor / sum_of_scaled_flex_shrink_factor_of_unfrozen_items; - item.target_main_size = item.flex_base_size - (abs(flex_line.remaining_free_space) * ratio); - }); - } - } else { - // This isn't spec but makes sense. - for_each_unfrozen_item([&](FlexItem& item) { - item.target_main_size = item.flex_base_size; - }); - } - // d Fix min/max violations. - CSSPixels adjustments = 0.0f; - for_each_unfrozen_item([&](FlexItem& item) { - auto min_main = has_main_min_size(item.box) - ? specified_main_min_size(item.box) - : automatic_minimum_size(item); - auto max_main = has_main_max_size(item.box) - ? specified_main_max_size(item.box) - : NumericLimits<float>::max(); - - CSSPixels original_target_size = item.target_main_size; - - if (item.target_main_size < min_main) { - item.target_main_size = min_main; - item.is_min_violation = true; - } + // If the item’s target main size was made smaller by this, it’s a max violation. + if (item.target_main_size < original_target_main_size) + item.is_max_violation = true; - if (item.target_main_size > max_main) { - item.target_main_size = max_main; - item.is_max_violation = true; - } - CSSPixels delta = item.target_main_size - original_target_size; - adjustments += delta; - }); - // e Freeze over-flexed items - CSSPixels total_violation = adjustments; - if (total_violation == 0) { - for_each_unfrozen_item([&](FlexItem& item) { - --number_of_unfrozen_items_on_line; + // If the item’s target main size was made larger by this, it’s a min violation. + if (item.target_main_size > original_target_main_size) + item.is_min_violation = true; + + total_violation += item.target_main_size - original_target_main_size; + } + + // e. Freeze over-flexed items. + // The total violation is the sum of the adjustments from the previous step ∑(clamped size - unclamped size). + + // If the total violation is: + // Zero + // Freeze all items. + if (total_violation == 0) { + for (auto& item : line.items) { + if (!item.frozen) item.frozen = true; - }); - } else if (total_violation > 0) { - for_each_unfrozen_item([&](FlexItem& item) { - if (item.is_min_violation) { - --number_of_unfrozen_items_on_line; - item.frozen = true; - } - }); - } else if (total_violation < 0) { - for_each_unfrozen_item([&](FlexItem& item) { - if (item.is_max_violation) { - --number_of_unfrozen_items_on_line; - item.frozen = true; - } - }); } } - - // 6.5. - for (auto& item : flex_line.items) { - item.main_size = item.target_main_size; - set_main_size(item.box, item.main_size.value()); + // Positive + // Freeze all the items with min violations. + else if (total_violation > 0) { + for (auto& item : line.items) { + if (!item.frozen && item.is_min_violation) + item.frozen = true; + } + } + // Negative + // Freeze all the items with max violations. + else { + for (auto& item : line.items) { + if (!item.frozen && item.is_max_violation) + item.frozen = true; + } } + // NOTE: This freezes at least one item, ensuring that the loop makes progress and eventually terminates. + + // f. Return to the start of this loop. + } - flex_line.remaining_free_space = calculate_free_space(); + // NOTE: Calculate the remaining free space once again here, since it's needed later when aligning items. + line.remaining_free_space = calculate_remaining_free_space(); + + // 6. Set each item’s used main size to its target main size. + for (auto& item : line.items) { + item.main_size = item.target_main_size; + set_main_size(item.box, item.target_main_size); + } +} + +// https://drafts.csswg.org/css-flexbox-1/#resolve-flexible-lengths +void FlexFormattingContext::resolve_flexible_lengths() +{ + for (auto& line : m_flex_lines) { + resolve_flexible_lengths_for_line(line); } } @@ -2089,4 +2116,24 @@ CSSPixelPoint FlexFormattingContext::calculate_static_position(Box const& box) c return static_position_offset + diff; } +float FlexFormattingContext::FlexLine::sum_of_flex_factor_of_unfrozen_items() const +{ + float sum = 0; + for (auto const& item : items) { + if (!item.frozen) + sum += item.flex_factor.value(); + } + return sum; +} + +float FlexFormattingContext::FlexLine::sum_of_scaled_flex_shrink_factor_of_unfrozen_items() const +{ + float sum = 0; + for (auto const& item : items) { + if (!item.frozen) + sum += item.scaled_flex_shrink_factor; + } + return sum; +} + } diff --git a/Userland/Libraries/LibWeb/Layout/FlexFormattingContext.h b/Userland/Libraries/LibWeb/Layout/FlexFormattingContext.h index 020d8f9253..d04f150648 100644 --- a/Userland/Libraries/LibWeb/Layout/FlexFormattingContext.h +++ b/Userland/Libraries/LibWeb/Layout/FlexFormattingContext.h @@ -61,6 +61,21 @@ private: float scaled_flex_shrink_factor { 0 }; float desired_flex_fraction { 0 }; + CSSPixels outer_hypothetical_main_size() const + { + return hypothetical_main_size + margins.main_before + margins.main_after + borders.main_before + borders.main_after + padding.main_before + padding.main_after; + } + + CSSPixels outer_target_main_size() const + { + return target_main_size + margins.main_before + margins.main_after + borders.main_before + borders.main_after + padding.main_before + padding.main_after; + } + + CSSPixels outer_flex_base_size() const + { + return flex_base_size + margins.main_before + margins.main_after + borders.main_before + borders.main_after + padding.main_before + padding.main_after; + } + // The used main size of this flex item. Empty until determined. Optional<CSSPixels> main_size {}; @@ -91,6 +106,9 @@ private: CSSPixels cross_size { 0 }; CSSPixels remaining_free_space { 0 }; float chosen_flex_fraction { 0 }; + + float sum_of_flex_factor_of_unfrozen_items() const; + float sum_of_scaled_flex_shrink_factor_of_unfrozen_items() const; }; bool has_definite_main_size(Box const&) const; |