layout: Clean up the flexible length resolution algorithm (#34153)

Instead of doing so much zipping, which is confusing, create a temporary
data structure for each item that holds all relevant information. In
addition, add detailed specification text so it is easier to understand
what is going on.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2024-11-06 10:54:20 +01:00 committed by GitHub
parent 756c249145
commit a61522a1e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1378,77 +1378,106 @@ impl InitialFlexLineLayout<'_> {
outer_hypothetical_main_sizes_sum: Au, outer_hypothetical_main_sizes_sum: Au,
container_main_size: Au, container_main_size: Au,
) -> (Vec<Au>, Au) { ) -> (Vec<Au>, Au) {
let mut frozen = vec![false; items.len()]; struct FlexibleLengthResolutionItem<'items> {
let mut target_main_sizes_vec = items item: &'items FlexItem<'items>,
.iter() frozen: Cell<bool>,
.map(|item| item.flex_base_size) target_main_size: Cell<Au>,
.collect::<Vec<_>>(); flex_factor: f32,
// Using `Cell`s reconciles mutability with multiple borrows in closures
let target_main_sizes = Cell::from_mut(&mut *target_main_sizes_vec).as_slice_of_cells();
let frozen = Cell::from_mut(&mut *frozen).as_slice_of_cells();
let frozen_count = Cell::new(0);
let grow = outer_hypothetical_main_sizes_sum < container_main_size;
let flex_factor = |item: &FlexItem| {
let position_style = item.box_.style().get_position();
if grow {
position_style.flex_grow.0
} else {
position_style.flex_shrink.0
}
};
let items_and_main_sizes = || items.iter().zip(target_main_sizes).zip(frozen);
// “Size inflexible items”
for ((item, target_main_size), frozen) in items_and_main_sizes() {
let is_inflexible = flex_factor(item) == 0. ||
if grow {
item.flex_base_size > item.hypothetical_main_size
} else {
item.flex_base_size < item.hypothetical_main_size
};
if is_inflexible {
frozen_count.set(frozen_count.get() + 1);
frozen.set(true);
target_main_size.set(item.hypothetical_main_size);
}
} }
let check_for_flexible_items = || frozen_count.get() < items.len(); // > 1. Determine the used flex factor. Sum the outer hypothetical main sizes of all
let free_space = |all_items_frozen: bool| { // > items on the line. If the sum is less than the flex containers inner main
container_main_size - // > size, use the flex grow factor for the rest of this algorithm; otherwise, use
items_and_main_sizes() // > the flex shrink factor.
.map(|((item, target_main_size), frozen)| { let grow = outer_hypothetical_main_sizes_sum < container_main_size;
item.pbm_auto_is_zero.main +
if all_items_frozen || frozen.get() { let mut frozen_count = 0;
target_main_size.get() let items: Vec<_> = items
} else { .iter()
item.flex_base_size .map(|item| {
} // > 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.
.sum() let target_main_size = Cell::new(item.flex_base_size);
};
// https://drafts.csswg.org/css-flexbox/#initial-free-space // > 3. Size inflexible items. Freeze, setting its target main size to its hypothetical main size…
let initial_free_space = free_space(false); // > - any item that has a flex factor of zero
let unfrozen_items = || { // > - if using the flex grow factor: any item that has a flex base size
items_and_main_sizes().filter_map(|(item_and_target_main_size, frozen)| { // > greater than its hypothetical main size
if !frozen.get() { // > - if using the flex shrink factor: any item that has a flex base size
Some(item_and_target_main_size) // > smaller than its hypothetical main size
let flex_factor = if grow {
item.box_.style().get_position().flex_grow.0
} else { } else {
None item.box_.style().get_position().flex_shrink.0
};
let is_inflexible = flex_factor == 0. ||
if grow {
item.flex_base_size > item.hypothetical_main_size
} else {
item.flex_base_size < item.hypothetical_main_size
};
let frozen = Cell::new(false);
if is_inflexible {
frozen_count += 1;
frozen.set(true);
target_main_size.set(item.hypothetical_main_size);
}
FlexibleLengthResolutionItem {
item,
frozen,
target_main_size,
flex_factor,
} }
}) })
.collect();
let unfrozen_items = || items.iter().filter(|item| !item.frozen.get());
let main_sizes = |items: Vec<FlexibleLengthResolutionItem>| {
items
.into_iter()
.map(|item| item.target_main_size.get())
.collect()
}; };
// https://drafts.csswg.org/css-flexbox/#initial-free-space
// > 4. Calculate initial free space. Sum the outer sizes of all items on the line, and
// > subtract this from the flex containers inner main size. For frozen items, use
// > their outer target main size; for other items, use their outer flex base size.
let free_space = |all_items_frozen| {
let items_size = items
.iter()
.map(|item| {
item.item.pbm_auto_is_zero.main +
if all_items_frozen || item.frozen.get() {
item.target_main_size.get()
} else {
item.item.flex_base_size
}
})
.sum();
container_main_size - items_size
};
let initial_free_space = free_space(false);
loop { loop {
// https://drafts.csswg.org/css-flexbox/#remaining-free-space // https://drafts.csswg.org/css-flexbox/#remaining-free-space
let mut remaining_free_space = free_space(false); let mut remaining_free_space = free_space(false);
if !check_for_flexible_items() { // > 5. a. Check for flexible items. If all the flex items on the line are
return (target_main_sizes_vec, remaining_free_space); // > frozen, free space has been distributed; exit this loop.
if frozen_count >= items.len() {
return (main_sizes(items), remaining_free_space);
} }
let unfrozen_items_flex_factor_sum: f32 =
unfrozen_items().map(|(item, _)| flex_factor(item)).sum(); // > 5. b. Calculate the remaining free space as for initial free space, above. If the
// FIXME: I (Simon) transcribed the spec but I dont yet understand why this algorithm // > sum of the unfrozen flex items flex factors is less than one, multiply the
// > initial free space by this sum. If the magnitude of this value is less than
// > the magnitude of the remaining free space, use this as the remaining free
// > space.
let unfrozen_items_flex_factor_sum =
unfrozen_items().map(|item| item.flex_factor).sum();
if unfrozen_items_flex_factor_sum < 1. { if unfrozen_items_flex_factor_sum < 1. {
let multiplied = initial_free_space.scale_by(unfrozen_items_flex_factor_sum); let multiplied = initial_free_space.scale_by(unfrozen_items_flex_factor_sum);
if multiplied.abs() < remaining_free_space.abs() { if multiplied.abs() < remaining_free_space.abs() {
@ -1456,67 +1485,88 @@ impl InitialFlexLineLayout<'_> {
} }
} }
// “Distribute free space proportional to the flex factors.” // > 5. c. If the remaining free space is non-zero, distribute it proportional
// to the flex factors:
//
// FIXME: is it a problem if floating point precision errors accumulate // FIXME: is it a problem if floating point precision errors accumulate
// and we get not-quite-zero remaining free space when we should get zero here? // and we get not-quite-zero remaining free space when we should get zero here?
if remaining_free_space != Au::zero() { if remaining_free_space != Au::zero() {
// > If using the flex grow factor:
// > For every unfrozen item on the line, find the ratio of the items flex grow factor to
// > the sum of the flex grow factors of all unfrozen items on the line. Set the items
// > target main size to its flex base size plus a fraction of the remaining free space
// > proportional to the ratio.
if grow { if grow {
for (item, target_main_size) in unfrozen_items() { for item in unfrozen_items() {
let grow_factor = item.box_.style().get_position().flex_grow.0; let ratio = item.flex_factor / unfrozen_items_flex_factor_sum;
let ratio = grow_factor / unfrozen_items_flex_factor_sum; item.target_main_size
target_main_size .set(item.item.flex_base_size + remaining_free_space.scale_by(ratio));
.set(item.flex_base_size + remaining_free_space.scale_by(ratio));
} }
// > If using the flex shrink factor
// > 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. Find the ratio of the
// > items scaled flex shrink factor to the sum of the scaled flex shrink factors of all
// > unfrozen items on the line. Set the items 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.
} else { } else {
// https://drafts.csswg.org/css-flexbox/#scaled-flex-shrink-factor // https://drafts.csswg.org/css-flexbox/#scaled-flex-shrink-factor
let scaled_shrink_factor = |item: &FlexItem| { let scaled_shrink_factor = |item: &FlexibleLengthResolutionItem| {
let shrink_factor = item.box_.style().get_position().flex_shrink.0; item.item.flex_base_size.scale_by(item.flex_factor)
item.flex_base_size.scale_by(shrink_factor)
}; };
let scaled_shrink_factors_sum: Au = unfrozen_items() let scaled_shrink_factors_sum: Au =
.map(|(item, _)| scaled_shrink_factor(item)) unfrozen_items().map(scaled_shrink_factor).sum();
.sum();
if scaled_shrink_factors_sum > Au::zero() { if scaled_shrink_factors_sum > Au::zero() {
for (item, target_main_size) in unfrozen_items() { for item in unfrozen_items() {
let ratio = scaled_shrink_factor(item).0 as f32 / let ratio = scaled_shrink_factor(item).0 as f32 /
scaled_shrink_factors_sum.0 as f32; scaled_shrink_factors_sum.0 as f32;
target_main_size.set( item.target_main_size.set(
item.flex_base_size - remaining_free_space.abs().scale_by(ratio), item.item.flex_base_size -
remaining_free_space.abs().scale_by(ratio),
); );
} }
} }
} }
} }
// “Fix min/max violations.” // > 5. d. Fix min/max violations. Clamp each non-frozen items target main size
let violation = |(item, target_main_size): (&FlexItem, &Cell<Au>)| { // > by its used min and max main sizes and floor its content-box size at zero.
let size = target_main_size.get(); // > If the items target main size was made smaller by this, its a max
// > violation. If the items target main size was made larger by this, its a
// > min violation.
let violation = |item: &FlexibleLengthResolutionItem| {
let size = item.target_main_size.get();
let clamped = size.clamp_between_extremums( let clamped = size.clamp_between_extremums(
item.content_min_size.main, item.item.content_min_size.main,
item.content_max_size.main, item.item.content_max_size.main,
); );
clamped - size clamped - size
}; };
// “Freeze over-flexed items.” // > 5. 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.
// > - Positive: Freeze all the items with min violations.
// > - Negative: Freeze all the items with max violations.
let total_violation: Au = unfrozen_items().map(violation).sum(); let total_violation: Au = unfrozen_items().map(violation).sum();
match total_violation.cmp(&Au::zero()) { match total_violation.cmp(&Au::zero()) {
Ordering::Equal => { Ordering::Equal => {
// “Freeze all items.” // “Freeze all items.”
// Return instead, as thats what the next loop iteration would do. // Return instead, as thats what the next loop iteration would do.
let remaining_free_space = free_space(true); let remaining_free_space = free_space(true);
return (target_main_sizes_vec, remaining_free_space); return (main_sizes(items), remaining_free_space);
}, },
Ordering::Greater => { Ordering::Greater => {
// “Freeze all the items with min violations.” // “Freeze all the items with min violations.”
// “If the items target main size was made larger by [clamping], // “If the items target main size was made larger by [clamping],
// its a min violation.” // its a min violation.”
for (item_and_target_main_size, frozen) in items_and_main_sizes() { for item in items.iter() {
if violation(item_and_target_main_size) > Au::zero() { if violation(item) > Au::zero() {
let (item, target_main_size) = item_and_target_main_size; item.target_main_size.set(item.item.content_min_size.main);
target_main_size.set(item.content_min_size.main); item.frozen.set(true);
frozen_count.set(frozen_count.get() + 1); frozen_count += 1;
frozen.set(true);
} }
} }
}, },
@ -1525,15 +1575,14 @@ impl InitialFlexLineLayout<'_> {
// “Freeze all the items with max violations.” // “Freeze all the items with max violations.”
// “If the items target main size was made smaller by [clamping], // “If the items target main size was made smaller by [clamping],
// its a max violation.” // its a max violation.”
for (item_and_target_main_size, frozen) in items_and_main_sizes() { for item in items.iter() {
if violation(item_and_target_main_size) < Au::zero() { if violation(item) < Au::zero() {
let (item, target_main_size) = item_and_target_main_size; let Some(max_size) = item.item.content_max_size.main else {
let Some(max_size) = item.content_max_size.main else {
unreachable!() unreachable!()
}; };
target_main_size.set(max_size); item.target_main_size.set(max_size);
frozen_count.set(frozen_count.get() + 1); item.frozen.set(true);
frozen.set(true); frozen_count += 1;
} }
} }
}, },