浏览代码

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. :^)
Andreas Kling 2 年之前
父节点
当前提交
109ed27423

+ 29 - 0
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>

+ 25 - 0
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>

+ 192 - 145
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;
+}
+
 }

+ 18 - 0
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;