Skip to main content

taffy/compute/grid/
track_sizing.rs

1//! Implements the track sizing algorithm
2//! <https://www.w3.org/TR/css-grid-1/#layout-algorithm>
3use super::types::{GridItem, GridTrack, TrackCounts};
4use crate::compute::common::alignment::child_first_baseline;
5use crate::geometry::{AbstractAxis, Line, Size};
6use crate::style::{AlignContent, AlignContentKeyword, AvailableSpace};
7use crate::style_helpers::TaffyMinContent;
8use crate::tree::{LayoutPartialTree, LayoutPartialTreeExt, SizingMode};
9use crate::util::sys::{f32_max, f32_min, Vec};
10use crate::util::{FrontBackVecBuilder, MaybeMath, ResolveOrZero};
11use crate::CompactLength;
12
13/// Takes an axis, and a list of grid items sorted firstly by whether they cross a flex track
14/// in the specified axis (items that don't cross a flex track first) and then by the number
15/// of tracks they cross in specified axis (ascending order).
16///
17/// The list holds references so that the `GridItem`s themselves stay in document order and
18/// only the (pointer-sized) references are sorted.
19struct ItemBatcher {
20    /// The axis in which the ItemBatcher is operating. Used when querying properties from items.
21    axis: AbstractAxis,
22    /// The starting index of the current batch
23    index_offset: usize,
24    /// The span of the items in the current batch
25    current_span: u16,
26    /// Whether the current batch of items cross a flexible track
27    current_is_flex: bool,
28}
29
30impl ItemBatcher {
31    /// Create a new ItemBatcher for the specified axis
32    #[inline(always)]
33    fn new(axis: AbstractAxis) -> Self {
34        ItemBatcher { index_offset: 0, axis, current_span: 1, current_is_flex: false }
35    }
36
37    /// This is basically a manual version of Iterator::next which passes `items`
38    /// in as a parameter on each iteration to work around borrow checker rules
39    #[inline]
40    fn next<'items, 'a>(
41        &mut self,
42        items: &'items mut [&'a mut GridItem],
43    ) -> Option<(&'items mut [&'a mut GridItem], bool)> {
44        if self.current_is_flex || self.index_offset >= items.len() {
45            return None;
46        }
47
48        let item = &items[self.index_offset];
49        self.current_span = item.span(self.axis);
50        self.current_is_flex = item.crosses_flexible_track(self.axis);
51
52        let next_index_offset = if self.current_is_flex {
53            items.len()
54        } else {
55            items
56                .iter()
57                .position(|item: &&mut GridItem| {
58                    item.crosses_flexible_track(self.axis) || item.span(self.axis) > self.current_span
59                })
60                .unwrap_or(items.len())
61        };
62
63        let batch_range = self.index_offset..next_index_offset;
64        self.index_offset = next_index_offset;
65
66        let batch = &mut items[batch_range];
67        Some((batch, self.current_is_flex))
68    }
69}
70
71/// This struct captures a bunch of variables which are used to compute the intrinsic sizes of children so that those variables
72/// don't have to be passed around all over the place below. It then has methods that implement the intrinsic sizing computations
73struct IntrinsicSizeMeasurer<'tree, 'oat, Tree, EstimateFunction>
74where
75    Tree: LayoutPartialTree,
76    EstimateFunction: Fn(&GridTrack, Option<f32>, &Tree) -> Option<f32>,
77{
78    /// The layout tree
79    tree: &'tree mut Tree,
80    /// The tracks in the opposite axis to the one we are currently sizing
81    other_axis_tracks: &'oat [GridTrack],
82    /// A function that computes an estimate of an other-axis track's size which is passed to
83    /// the child size measurement functions
84    get_track_size_estimate: EstimateFunction,
85    /// The axis we are currently sizing
86    axis: AbstractAxis,
87    /// The available grid space
88    inner_node_size: Size<Option<f32>>,
89}
90
91impl<Tree, EstimateFunction> IntrinsicSizeMeasurer<'_, '_, Tree, EstimateFunction>
92where
93    Tree: LayoutPartialTree,
94    EstimateFunction: Fn(&GridTrack, Option<f32>, &Tree) -> Option<f32>,
95{
96    /// Compute the available_space to be passed to the child sizing functions
97    /// These are estimates based on either the max track sizing function or the provisional base size in the opposite
98    /// axis to the one currently being sized.
99    /// https://www.w3.org/TR/css-grid-1/#algo-overview
100    #[inline(always)]
101    fn grid_area_size(&self, item: &mut GridItem, axis_tracks: &[GridTrack]) -> Size<Option<f32>> {
102        item.grid_area_size_cached(
103            self.axis,
104            axis_tracks,
105            self.other_axis_tracks,
106            self.inner_node_size,
107            |track, basis| (self.get_track_size_estimate)(track, basis, self.tree),
108            &|val, basis| self.tree.calc(val, basis),
109        )
110    }
111
112    /// Compute the item's resolved margins for size contributions. Horizontal percentage margins always resolve
113    /// to zero if the container size is indefinite as otherwise this would introduce a cyclic dependency.
114    #[inline(always)]
115    fn margins_axis_sums_with_baseline_shims(&self, item: &GridItem, percentage_basis: Option<f32>) -> Size<f32> {
116        item.margins_axis_sums_with_baseline_shims(percentage_basis, self.tree)
117    }
118
119    /// Simple pass-through function to `LayoutPartialTreeExt::calc`
120    #[inline(always)]
121    fn calc(&self, val: *const (), basis: f32) -> f32 {
122        self.tree.calc(val, basis)
123    }
124
125    /// Retrieve the item's min content contribution from the cache or compute it using the provided parameters
126    #[inline(always)]
127    fn min_content_contribution(&mut self, item: &mut GridItem, axis_tracks: &[GridTrack]) -> f32 {
128        let grid_area_size = self.grid_area_size(item, axis_tracks);
129        let available_space = grid_area_size.with(self.axis, None);
130        let margin_axis_sums = self.margins_axis_sums_with_baseline_shims(item, available_space.width);
131        let contribution = item.min_content_contribution_cached(self.axis, self.tree, grid_area_size, available_space);
132        contribution + margin_axis_sums.get(self.axis)
133    }
134
135    /// Retrieve the item's max content contribution from the cache or compute it using the provided parameters
136    #[inline(always)]
137    fn max_content_contribution(&mut self, item: &mut GridItem, axis_tracks: &[GridTrack]) -> f32 {
138        let grid_area_size = self.grid_area_size(item, axis_tracks);
139        let available_space = grid_area_size.with(self.axis, None);
140        let margin_axis_sums = self.margins_axis_sums_with_baseline_shims(item, available_space.width);
141        let contribution = item.max_content_contribution_cached(self.axis, self.tree, grid_area_size, available_space);
142        contribution + margin_axis_sums.get(self.axis)
143    }
144
145    /// The minimum contribution of an item is the smallest outer size it can have.
146    /// Specifically:
147    ///   - If the item’s computed preferred size behaves as auto or depends on the size of its containing block in the relevant axis:
148    ///     Its minimum contribution is the outer size that would result from assuming the item’s used minimum size as its preferred size;
149    ///   - Else the item’s minimum contribution is its min-content contribution.
150    ///
151    /// Because the minimum contribution often depends on the size of the item’s content, it is considered a type of intrinsic size contribution.
152    #[inline(always)]
153    fn minimum_contribution(&mut self, item: &mut GridItem, axis_tracks: &[GridTrack]) -> f32 {
154        let grid_area_size = self.grid_area_size(item, axis_tracks);
155        let available_space = grid_area_size.with(self.axis, None);
156        let margin_axis_sums = self.margins_axis_sums_with_baseline_shims(item, available_space.width);
157        let contribution =
158            item.minimum_contribution_cached(self.tree, self.axis, axis_tracks, grid_area_size, self.inner_node_size);
159        contribution + margin_axis_sums.get(self.axis)
160    }
161}
162
163/// When applying the track sizing algorithm and estimating the size in the other axis for content sizing items
164/// we should take into account align-content/justify-content if both the grid container and all items in the
165/// other axis have definite sizes. This function computes such a per-gutter additional size adjustment.
166#[inline(always)]
167pub(super) fn compute_alignment_gutter_adjustment(
168    alignment: AlignContent,
169    axis_inner_node_size: Option<f32>,
170    get_track_size_estimate: impl Fn(&GridTrack, Option<f32>) -> Option<f32>,
171    tracks: &[GridTrack],
172) -> f32 {
173    if tracks.len() <= 1 {
174        return 0.0;
175    }
176
177    // As items never cross the outermost gutters in a grid, we can simplify our calculations by
178    // treating Start and End the same. The safety modifier doesn't influence gutter weight;
179    // overflow fallback is handled when offsets are computed.
180    let outer_gutter_weight = match alignment.keyword() {
181        AlignContentKeyword::Start
182        | AlignContentKeyword::FlexStart
183        | AlignContentKeyword::End
184        | AlignContentKeyword::FlexEnd
185        | AlignContentKeyword::Center => 1,
186        // Normal is resolved to Stretch in `compute_grid_layout`
187        AlignContentKeyword::Normal | AlignContentKeyword::Stretch => 0,
188        AlignContentKeyword::SpaceBetween => 0,
189        AlignContentKeyword::SpaceAround => 1,
190        AlignContentKeyword::SpaceEvenly => 1,
191    };
192
193    let inner_gutter_weight = match alignment.keyword() {
194        AlignContentKeyword::FlexStart
195        | AlignContentKeyword::Start
196        | AlignContentKeyword::FlexEnd
197        | AlignContentKeyword::End
198        | AlignContentKeyword::Center
199        | AlignContentKeyword::Normal
200        | AlignContentKeyword::Stretch => 0,
201        AlignContentKeyword::SpaceBetween => 1,
202        AlignContentKeyword::SpaceAround => 2,
203        AlignContentKeyword::SpaceEvenly => 1,
204    };
205
206    if inner_gutter_weight == 0 {
207        return 0.0;
208    }
209
210    if let Some(axis_inner_node_size) = axis_inner_node_size {
211        let free_space = tracks
212            .iter()
213            .map(|track| get_track_size_estimate(track, Some(axis_inner_node_size)))
214            .sum::<Option<f32>>()
215            .map(|track_size_sum| f32_max(0.0, axis_inner_node_size - track_size_sum))
216            .unwrap_or(0.0);
217
218        let weighted_track_count =
219            (((tracks.len() - 3) / 2) * inner_gutter_weight as usize) + (2 * outer_gutter_weight as usize);
220
221        return (free_space / weighted_track_count as f32) * inner_gutter_weight as f32;
222    }
223
224    0.0
225}
226
227/// Convert origin-zero coordinates track placement in grid track vector indexes
228#[inline(always)]
229pub(super) fn resolve_item_track_indexes(items: &mut [GridItem], column_counts: TrackCounts, row_counts: TrackCounts) {
230    for item in items {
231        item.column_indexes = item.column.map(|line| line.into_track_vec_index(column_counts) as u16);
232        item.row_indexes = item.row.map(|line| line.into_track_vec_index(row_counts) as u16);
233    }
234}
235
236/// Determine (in each axis) whether the item crosses any flexible tracks
237#[inline(always)]
238pub(super) fn determine_if_item_crosses_flexible_or_intrinsic_tracks(
239    items: &mut Vec<GridItem>,
240    columns: &[GridTrack],
241    rows: &[GridTrack],
242) {
243    for item in items {
244        item.crosses_flexible_column =
245            item.track_range_excluding_lines(AbstractAxis::Inline).any(|i| columns[i].is_flexible());
246        item.crosses_intrinsic_column =
247            item.track_range_excluding_lines(AbstractAxis::Inline).any(|i| columns[i].has_intrinsic_sizing_function());
248        item.crosses_flexible_row =
249            item.track_range_excluding_lines(AbstractAxis::Block).any(|i| rows[i].is_flexible());
250        item.crosses_intrinsic_row =
251            item.track_range_excluding_lines(AbstractAxis::Block).any(|i| rows[i].has_intrinsic_sizing_function());
252    }
253}
254
255/// Track sizing algorithm
256/// Note: Gutters are treated as empty fixed-size tracks for the purpose of the track sizing algorithm.
257#[allow(clippy::too_many_arguments)]
258pub(super) fn track_sizing_algorithm<Tree: LayoutPartialTree>(
259    tree: &mut Tree,
260    axis: AbstractAxis,
261    axis_min_size: Option<f32>,
262    axis_max_size: Option<f32>,
263    axis_alignment: AlignContent,
264    other_axis_alignment: AlignContent,
265    available_grid_space: Size<AvailableSpace>,
266    inner_node_size: Size<Option<f32>>,
267    axis_tracks: &mut [GridTrack],
268    other_axis_tracks: &mut [GridTrack],
269    items: &mut [GridItem],
270    get_track_size_estimate: fn(&GridTrack, Option<f32>, &Tree) -> Option<f32>,
271    has_baseline_aligned_item: bool,
272) {
273    // 11.4 Initialise Track sizes
274    // Initialize each track’s base size and growth limit.
275    let percentage_basis = inner_node_size.get(axis).or(axis_min_size);
276    initialize_track_sizes(tree, axis_tracks, percentage_basis);
277
278    // Resolve fit-content() limits against the container's inner size (these are fixed for the rest of this pass)
279    let axis_inner_node_size = inner_node_size.get(axis);
280    for track in axis_tracks.iter_mut() {
281        track.resolve_fit_content_limit(axis_inner_node_size, |val, basis| tree.calc(val, basis));
282    }
283
284    // 11.5.1 Shim item baselines
285    if has_baseline_aligned_item {
286        resolve_item_baselines(tree, axis, items, inner_node_size);
287    }
288
289    // If all tracks have a fixed min track sizing function and base_size = growth_limit,
290    // then the track sizes are already final and we can skip the rest of this function.
291    // Note that tracks with an intrinsic min track sizing function can still grow beyond
292    // a fixed growth limit (e.g. minmax(auto, 0px)), so they cannot be skipped.
293    if axis_tracks.iter().all(|track| {
294        track.base_size == track.growth_limit
295            && track
296                .min_track_sizing_function
297                .definite_value(percentage_basis, |val, basis| tree.calc(val, basis))
298                .is_some()
299    }) {
300        return;
301    }
302
303    // Pre-computations for 11.5 Resolve Intrinsic Track Sizes
304
305    // Compute an additional amount to add to each spanned gutter when computing item's estimated size in the
306    // in the opposite axis based on the alignment, container size, and estimated track sizes in that axis
307    let gutter_alignment_adjustment = compute_alignment_gutter_adjustment(
308        other_axis_alignment,
309        inner_node_size.get(axis.other()),
310        |track, basis| get_track_size_estimate(track, basis, tree),
311        other_axis_tracks,
312    );
313    if other_axis_tracks.len() > 3 {
314        let len = other_axis_tracks.len();
315        let inner_gutter_tracks = other_axis_tracks[2..len].iter_mut().step_by(2);
316        for track in inner_gutter_tracks {
317            track.content_alignment_adjustment = gutter_alignment_adjustment;
318        }
319    }
320
321    // 11.5 Resolve Intrinsic Track Sizes
322    resolve_intrinsic_track_sizes(
323        tree,
324        axis,
325        axis_tracks,
326        other_axis_tracks,
327        items,
328        available_grid_space.get(axis),
329        inner_node_size,
330        get_track_size_estimate,
331    );
332
333    // 11.6. Maximise Tracks
334    // Distributes free space (if any) to tracks with FINITE growth limits, up to their limits.
335    maximise_tracks(axis_tracks, available_grid_space.get(axis));
336
337    // For the purpose of the final two expansion steps ("Expand Flexible Tracks" and "Stretch auto Tracks"), we only want to expand
338    // into space generated by the grid container's size (as defined by either it's preferred size style or by it's parent node through
339    // something like stretch alignment), not just any available space. To do this we map definite available space to AvailableSpace::MaxContent
340    // in the case that inner_node_size is None. A min-content constraint in the block axis is also mapped to AvailableSpace::MaxContent,
341    // as the min-content block size of a grid container is its max-content block size.
342    let axis_available_space_for_expansion = if let Some(available_space) = inner_node_size.get(axis) {
343        AvailableSpace::Definite(available_space)
344    } else {
345        match available_grid_space.get(axis) {
346            AvailableSpace::MinContent if axis == AbstractAxis::Inline => AvailableSpace::MinContent,
347            _ => AvailableSpace::MaxContent,
348        }
349    };
350
351    // 11.7. Expand Flexible Tracks
352    // This step sizes flexible tracks using the largest value it can assign to an fr without exceeding the available space.
353    expand_flexible_tracks(
354        tree,
355        axis,
356        axis_tracks,
357        other_axis_tracks,
358        items,
359        axis_min_size,
360        axis_max_size,
361        axis_available_space_for_expansion,
362        inner_node_size,
363        get_track_size_estimate,
364    );
365
366    // 11.8. Stretch auto Tracks
367    // This step expands tracks that have an auto max track sizing function by dividing any remaining positive, definite free space equally amongst them.
368    if axis_alignment == AlignContent::STRETCH {
369        stretch_auto_tracks(axis_tracks, axis_min_size, axis_available_space_for_expansion);
370    }
371}
372
373/// Whether it is a minimum or maximum size's space being distributed
374/// This controls behaviour of the space distribution algorithm when distributing beyond limits
375/// See "distributing space beyond limits" at https://www.w3.org/TR/css-grid-1/#extra-space
376#[derive(Copy, Clone, Debug, PartialEq, Eq)]
377enum IntrinsicContributionType {
378    /// It's a minimum size's space being distributed
379    Minimum,
380    /// It's a maximum size's space being distributed
381    Maximum,
382}
383
384/// Add any planned base size increases to the base size after a round of distributing space to base sizes
385/// Reset the planed base size increase to zero ready for the next round.
386#[inline(always)]
387fn flush_planned_base_size_increases(tracks: &mut [GridTrack]) {
388    for track in tracks {
389        track.base_size += track.base_size_planned_increase;
390        track.base_size_planned_increase = 0.0;
391    }
392}
393
394/// Add any planned growth limit increases to the growth limit after a round of distributing space to growth limits
395/// Reset the planed growth limit increase to zero ready for the next round.
396#[inline(always)]
397fn flush_planned_growth_limit_increases(tracks: &mut [GridTrack], set_infinitely_growable: bool) {
398    for track in tracks {
399        if track.growth_limit_planned_increase > 0.0 {
400            track.growth_limit = if track.growth_limit == f32::INFINITY {
401                track.base_size + track.growth_limit_planned_increase
402            } else {
403                track.growth_limit + track.growth_limit_planned_increase
404            };
405            track.infinitely_growable = set_infinitely_growable;
406        } else {
407            track.infinitely_growable = false;
408        }
409        track.growth_limit_planned_increase = 0.0
410    }
411}
412
413/// 11.4 Initialise Track sizes
414/// Initialize each track’s base size and growth limit.
415#[inline(always)]
416fn initialize_track_sizes(
417    tree: &impl LayoutPartialTree,
418    axis_tracks: &mut [GridTrack],
419    axis_inner_node_size: Option<f32>,
420) {
421    for track in axis_tracks.iter_mut() {
422        // For each track, if the track’s min track sizing function is:
423        // - A fixed sizing function
424        //     Resolve to an absolute length and use that size as the track’s initial base size.
425        //     Note: Indefinite lengths cannot occur, as they’re treated as auto.
426        // - An intrinsic sizing function
427        //     Use an initial base size of zero.
428        track.base_size = track
429            .min_track_sizing_function
430            .definite_value(axis_inner_node_size, |val, basis| tree.calc(val, basis))
431            .unwrap_or(0.0);
432
433        // For each track, if the track’s max track sizing function is:
434        // - A fixed sizing function
435        //     Resolve to an absolute length and use that size as the track’s initial growth limit.
436        // - An intrinsic sizing function
437        //     Use an initial growth limit of infinity.
438        // - A flexible sizing function
439        //     Use an initial growth limit of infinity.
440        track.growth_limit = track
441            .max_track_sizing_function
442            .definite_value(axis_inner_node_size, |val, basis| tree.calc(val, basis))
443            .unwrap_or(f32::INFINITY);
444
445        // In all cases, if the growth limit is less than the base size, increase the growth limit to match the base size.
446        if track.growth_limit < track.base_size {
447            track.growth_limit = track.base_size;
448        }
449    }
450}
451
452/// 11.5.1 Shim baseline-aligned items so their intrinsic size contributions reflect their baseline alignment.
453fn resolve_item_baselines(
454    tree: &mut impl LayoutPartialTree,
455    axis: AbstractAxis,
456    items: &mut [GridItem],
457    inner_node_size: Size<Option<f32>>,
458) {
459    // Sort items by track in the other axis (row) start position so that we can iterate items in groups which
460    // are in the same track in the other axis (row)
461    let other_axis = axis.other();
462    let mut items: Vec<&mut GridItem> = items.iter_mut().collect();
463    items.sort_by_key(|item| item.placement(other_axis).start);
464
465    // Iterate over grid rows
466    let mut remaining_items = &mut items[0..];
467    while !remaining_items.is_empty() {
468        // Get the row index of the current row
469        let current_row = remaining_items[0].placement(other_axis).start;
470
471        // Find the item index of the first item that is in a different row (or None if we've reached the end of the list)
472        let next_row_first_item =
473            remaining_items.iter().position(|item| item.placement(other_axis).start != current_row);
474
475        // Use this index to split the `remaining_items` slice in two slices:
476        //    - A `row_items` slice containing the items (that start) in the current row
477        //    - A new `remaining_items` consisting of the remainder of the `remaining_items` slice
478        //      that hasn't been split off into `row_items
479        let row_items = if let Some(index) = next_row_first_item {
480            let (row_items, tail) = remaining_items.split_at_mut(index);
481            remaining_items = tail;
482            row_items
483        } else {
484            let row_items = remaining_items;
485            remaining_items = &mut [];
486            row_items
487        };
488
489        // Count how many items in *this row* are baseline aligned
490        // If a row has one or zero items participating in baseline alignment then baseline alignment is a no-op
491        // for those items and we skip further computations for that row
492        let row_baseline_item_count = row_items.iter().filter(|item| item.participates_in_baseline_alignment()).count();
493        if row_baseline_item_count <= 1 {
494            continue;
495        }
496
497        // Compute the baselines of all items in the row participating in baseline alignment
498        for item in row_items.iter_mut() {
499            if !item.participates_in_baseline_alignment() {
500                continue;
501            }
502
503            let measured_size_and_baselines = tree.perform_child_layout(
504                item.node,
505                Size::NONE,
506                inner_node_size,
507                Size::MIN_CONTENT,
508                SizingMode::InherentSize,
509                Line::FALSE,
510            );
511
512            let height = measured_size_and_baselines.size.height;
513            let baseline = child_first_baseline(measured_size_and_baselines.baselines.first, height, item.overflow.y)
514                .unwrap_or(height);
515
516            item.baseline = Some(baseline);
517        }
518
519        // Items are aligned by the distance from the top of their margin box to their baseline
520        let margin_box_baseline = |item: &GridItem| {
521            item.baseline.unwrap_or(0.0)
522                + item.margin.top.resolve_or_zero(inner_node_size.width, |val, basis| tree.calc(val, basis))
523        };
524
525        // Compute the max baseline of all items in the row participating in baseline alignment
526        let row_max_baseline = row_items
527            .iter()
528            .filter(|item| item.participates_in_baseline_alignment())
529            .map(|item| margin_box_baseline(item))
530            .max_by(|a, b| a.total_cmp(b))
531            .unwrap();
532
533        // Compute the baseline shim for each item in the row participating in baseline alignment
534        for item in row_items.iter_mut() {
535            if item.participates_in_baseline_alignment() {
536                item.baseline_shim = row_max_baseline - margin_box_baseline(item);
537                item.known_dimensions_cache = None;
538            }
539        }
540    }
541}
542
543/// 11.5 Resolve Intrinsic Track Sizes
544#[allow(clippy::too_many_arguments)]
545fn resolve_intrinsic_track_sizes<Tree: LayoutPartialTree>(
546    tree: &mut Tree,
547    axis: AbstractAxis,
548    axis_tracks: &mut [GridTrack],
549    other_axis_tracks: &[GridTrack],
550    items: &mut [GridItem],
551    axis_available_grid_space: AvailableSpace,
552    inner_node_size: Size<Option<f32>>,
553    get_track_size_estimate: impl Fn(&GridTrack, Option<f32>, &Tree) -> Option<f32>,
554) {
555    // Step 1. Shim baseline-aligned items so their intrinsic size contributions reflect their baseline alignment.
556
557    // Already done at this point. See resolve_item_baselines function.
558
559    // Step 2.
560
561    // The track sizing algorithm requires us to iterate through the items in ascending order of the number of
562    // tracks they span (first items that span 1 track, then items that span 2 tracks, etc).
563    // To avoid having to do multiple iterations of the items, we pre-sort them into this order.
564    //
565    // Items that cross a flexible track are all processed together in a single batch (regardless of their span), so
566    // they only need to be moved to the end of the list. And the order in which items within a batch are processed
567    // does not affect the result, so the remaining items only need to be sorted if any of them span more than one track.
568    let mut partitioned_items: FrontBackVecBuilder<&mut GridItem> = FrontBackVecBuilder::with_capacity(items.len());
569    let mut needs_sort = false;
570    for item in items.iter_mut() {
571        if item.crosses_flexible_track(axis) {
572            partitioned_items.push_back(item);
573        } else {
574            needs_sort |= item.span(axis) > 1;
575            partitioned_items.push_front(item);
576        }
577    }
578    let (mut items, non_flex_item_count) = partitioned_items.into_vec();
579    if needs_sort {
580        items[..non_flex_item_count].sort_by_key(|item| item.span(axis));
581    }
582    let items = items.as_mut_slice();
583
584    // Step 2, Step 3 and Step 4
585    // 2 & 3. Iterate over items that don't cross a flex track. Items should have already been sorted in ascending order
586    // of the number of tracks they span. Step 2 is the 1 track case and has an optimised implementation
587    // 4. Next, repeat the previous step instead considering (together, rather than grouped by span size) all items
588    // that do span a track with a flexible sizing function while
589
590    // Compute item's intrinsic (content-based) sizes
591    // Note: For items with a specified minimum size of auto (the initial value), the minimum contribution is usually equivalent
592    // to the min-content contribution—but can differ in some cases, see §6.6 Automatic Minimum Size of Grid Items.
593    // Also, minimum contribution <= min-content contribution <= max-content contribution.
594
595    let axis_inner_node_size = inner_node_size.get(axis);
596    let mut item_sizer =
597        IntrinsicSizeMeasurer { tree, other_axis_tracks, axis, inner_node_size, get_track_size_estimate };
598
599    let mut batched_item_iterator = ItemBatcher::new(axis);
600    while let Some((batch, is_flex)) = batched_item_iterator.next(items) {
601        // 2. Size tracks to fit non-spanning items: For each track with an intrinsic track sizing function and not a flexible sizing function,
602        // consider the items in it with a span of 1:
603        let batch_span = batch[0].placement(axis).span();
604        if !is_flex && batch_span == 1 {
605            for item in batch.iter_mut() {
606                let track_index = item.placement_indexes(axis).start + 1;
607                let track = &axis_tracks[track_index as usize];
608
609                // Handle base sizes
610                let new_base_size = match track.min_track_sizing_function.0.tag() {
611                    CompactLength::MIN_CONTENT_TAG => {
612                        f32_max(track.base_size, item_sizer.min_content_contribution(item, axis_tracks))
613                    }
614                    CompactLength::MAX_CONTENT_TAG => {
615                        f32_max(track.base_size, item_sizer.max_content_contribution(item, axis_tracks))
616                    }
617                    // If the container size is indefinite and has not yet been resolved then percentage sized
618                    // tracks are treated as auto
619                    _ if track.min_track_sizing_function.behaves_as_auto(axis_inner_node_size) => {
620                        let space = match axis_available_grid_space {
621                            // QUIRK: The spec says that:
622                            //
623                            //   If the grid container is being sized under a min- or max-content constraint, use the items’ limited
624                            //   min-content contributions in place of their minimum contributions here.
625                            //
626                            // However, in practice browsers only seem to apply this rule if the item is not a scroll container
627                            // (note that overflow:hidden counts as a scroll container), giving the automatic minimum size of scroll
628                            // containers (zero) precedence over the min-content contributions.
629                            AvailableSpace::MinContent | AvailableSpace::MaxContent
630                                if !item.overflow.get(axis).is_scroll_container() =>
631                            {
632                                let axis_minimum_size = item_sizer.minimum_contribution(item, axis_tracks);
633                                let axis_min_content_size = item_sizer.min_content_contribution(item, axis_tracks);
634                                let limit = track
635                                    .max_track_sizing_function
636                                    .definite_limit(axis_inner_node_size, |val, basis| item_sizer.calc(val, basis));
637                                axis_min_content_size.maybe_min(limit).max(axis_minimum_size)
638                            }
639                            _ => item_sizer.minimum_contribution(item, axis_tracks),
640                        };
641                        f32_max(track.base_size, space)
642                    }
643                    // Do nothing as it's not an intrinsic track sizing function
644                    _ => track.base_size,
645                };
646                axis_tracks[track_index as usize].base_size = new_base_size;
647
648                // Handle growth limits
649                //
650                // Computing an item's contributions calls into the child, so they are only computed if they are used.
651                // The min-content contribution is computed for every item in an intrinsic track (even where only the
652                // max-content contribution determines the growth limit), as the check for whether track sizing needs
653                // to be re-run compares each such item's min-content contribution against the one cached here.
654                let track = &axis_tracks[track_index as usize];
655                let max_track_sizing_function = track.max_track_sizing_function;
656                let fit_content_limit = track.fit_content_limit();
657                let min_content_contribution = if track.has_intrinsic_sizing_function() {
658                    Some(item_sizer.min_content_contribution(item, axis_tracks))
659                } else {
660                    None
661                };
662                let planned_increase = if max_track_sizing_function.is_fit_content() {
663                    // Always increase the growth limit to at least the size of the *fit-content limited*
664                    // max-content contribution
665                    let max_content_contribution =
666                        f32_min(item_sizer.max_content_contribution(item, axis_tracks), fit_content_limit);
667
668                    // If item is not a scroll container, then increase the growth limit to at least the
669                    // size of the min-content contribution
670                    match min_content_contribution {
671                        Some(min_content_contribution) if !item.overflow.get(axis).is_scroll_container() => {
672                            Some(f32_max(min_content_contribution, max_content_contribution))
673                        }
674                        _ => Some(max_content_contribution),
675                    }
676                } else if max_track_sizing_function.is_max_content_alike()
677                    || max_track_sizing_function.uses_percentage() && axis_inner_node_size.is_none()
678                {
679                    // If the container size is indefinite and has not yet been resolved then percentage sized
680                    // tracks should be treated as auto (this matches Chrome's behaviour and seems sensible)
681                    Some(item_sizer.max_content_contribution(item, axis_tracks))
682                } else if max_track_sizing_function.is_intrinsic() {
683                    min_content_contribution
684                } else {
685                    None
686                };
687                if let Some(planned_increase) = planned_increase {
688                    let track = &mut axis_tracks[track_index as usize];
689                    track.growth_limit_planned_increase =
690                        f32_max(track.growth_limit_planned_increase, planned_increase);
691                }
692            }
693
694            for track in axis_tracks.iter_mut() {
695                if track.growth_limit_planned_increase > 0.0 {
696                    track.growth_limit = if track.growth_limit == f32::INFINITY {
697                        track.growth_limit_planned_increase
698                    } else {
699                        f32_max(track.growth_limit, track.growth_limit_planned_increase)
700                    };
701                }
702                track.infinitely_growable = false;
703                track.growth_limit_planned_increase = 0.0;
704                if track.growth_limit < track.base_size {
705                    track.growth_limit = track.base_size;
706                }
707            }
708
709            continue;
710        }
711
712        // 1. For intrinsic minimums:
713        // First increase the base size of tracks with an intrinsic min track sizing function
714        for item in batch.iter_mut().filter(|item| item.crosses_intrinsic_track(axis)) {
715            // ...by distributing extra space as needed to accommodate these items’ minimum contributions.
716            //
717            // QUIRK: The spec says that:
718            //
719            //   If the grid container is being sized under a min- or max-content constraint, use the items’ limited min-content contributions
720            //   in place of their minimum contributions here.
721            //
722            // However, in practice browsers only seem to apply this rule if the item is not a scroll container (note that overflow:hidden counts as
723            // a scroll container), giving the automatic minimum size of scroll containers (zero) precedence over the min-content contributions.
724            let space = match axis_available_grid_space {
725                AvailableSpace::MinContent | AvailableSpace::MaxContent
726                    if !item.overflow.get(axis).is_scroll_container() =>
727                {
728                    let axis_minimum_size = item_sizer.minimum_contribution(item, axis_tracks);
729                    let axis_min_content_size = item_sizer.min_content_contribution(item, axis_tracks);
730                    let limit = item.spanned_track_limit(axis, axis_tracks, axis_inner_node_size, &|val, basis| {
731                        item_sizer.calc(val, basis)
732                    });
733                    let limited_min_content = axis_min_content_size.maybe_min(limit).max(axis_minimum_size);
734
735                    // For items crossing flexible tracks, browsers hand the content-derived contribution to
736                    // the flexible tracks only in proportion to the crossed flex factor sum, clamped at one
737                    // (CSS Grid 2, 12.5: "if the sum is less than one, distribute that proportion of
738                    // space"). The proportion applies to the space left after covering the spanned
739                    // inflexible tracks, and the item's minimum contribution acts as a floor on the result:
740                    // a definite size still spreads over `0fr` tracks in full. This is what lets a `0fr`
741                    // track holding a `min-height: 0` item collapse to zero, which the
742                    // `grid-template-rows: 0fr` to `1fr` collapse animation pattern relies on.
743                    if is_flex {
744                        let spanned_tracks = &axis_tracks[item.track_range_excluding_lines(axis)];
745                        let inflexible_sizes: f32 = spanned_tracks
746                            .iter()
747                            .filter(|track| !track.is_flexible())
748                            .map(|track| track.base_size)
749                            .sum();
750                        let scale = f32_min(crossed_flex_factor_sum(spanned_tracks), 1.0);
751                        let excess = f32_max(limited_min_content - inflexible_sizes, 0.0);
752                        f32_max(axis_minimum_size, inflexible_sizes + excess * scale)
753                    } else {
754                        limited_min_content
755                    }
756                }
757                _ => item_sizer.minimum_contribution(item, axis_tracks),
758            };
759            let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)];
760            if space > 0.0 {
761                let has_intrinsic_min_track_sizing_function = |track: &GridTrack| {
762                    track
763                        .min_track_sizing_function
764                        .definite_value(axis_inner_node_size, |val, basis| item_sizer.calc(val, basis))
765                        .is_none()
766                };
767                if item.overflow.get(axis).is_scroll_container() {
768                    let fit_content_limit = move |track: &GridTrack| track.fit_content_limited_growth_limit();
769                    distribute_item_space_to_base_size(
770                        is_flex,
771                        space,
772                        tracks,
773                        has_intrinsic_min_track_sizing_function,
774                        fit_content_limit,
775                        IntrinsicContributionType::Minimum,
776                    );
777                } else {
778                    distribute_item_space_to_base_size(
779                        is_flex,
780                        space,
781                        tracks,
782                        has_intrinsic_min_track_sizing_function,
783                        |track| track.growth_limit,
784                        IntrinsicContributionType::Minimum,
785                    );
786                }
787            }
788        }
789        flush_planned_base_size_increases(axis_tracks);
790
791        // 2. For content-based minimums:
792        // Next continue to increase the base size of tracks with a min track sizing function of min-content or max-content
793        // by distributing extra space as needed to account for these items' min-content contributions.
794        let has_min_or_max_content_min_track_sizing_function =
795            move |track: &GridTrack| track.min_track_sizing_function.is_min_or_max_content();
796        for item in batch.iter_mut() {
797            if !item.spans_track_matching(axis, axis_tracks, has_min_or_max_content_min_track_sizing_function) {
798                continue;
799            }
800            let space = item_sizer.min_content_contribution(item, axis_tracks);
801            let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)];
802            if space > 0.0 {
803                if item.overflow.get(axis).is_scroll_container() {
804                    let fit_content_limit = move |track: &GridTrack| track.fit_content_limited_growth_limit();
805                    distribute_item_space_to_base_size(
806                        is_flex,
807                        space,
808                        tracks,
809                        has_min_or_max_content_min_track_sizing_function,
810                        fit_content_limit,
811                        IntrinsicContributionType::Minimum,
812                    );
813                } else {
814                    distribute_item_space_to_base_size(
815                        is_flex,
816                        space,
817                        tracks,
818                        has_min_or_max_content_min_track_sizing_function,
819                        |track| track.growth_limit,
820                        IntrinsicContributionType::Minimum,
821                    );
822                }
823            }
824        }
825        flush_planned_base_size_increases(axis_tracks);
826
827        // 3. For max-content minimums:
828
829        // If the grid container is being sized under a max-content constraint, continue to increase the base size of tracks with
830        // a min track sizing function of auto or max-content by distributing extra space as needed to account for these items'
831        // limited max-content contributions.
832
833        // Define fit_content_limited_growth_limit function. This is passed to the distribute_space_up_to_limits
834        // helper function, and is used to compute the limit to distribute up to for each track.
835        // Wrapping the method on GridTrack is necessary in order to resolve percentage fit-content arguments.
836        if axis_available_grid_space == AvailableSpace::MaxContent {
837            /// Whether a track:
838            ///   - has an Auto MIN track sizing function
839            ///   - Does not have a MinContent MAX track sizing function
840            ///
841            /// The latter condition was added in order to match Chrome. But I believe it is due to the provision
842            /// under minmax here https://www.w3.org/TR/css-grid-1/#track-sizes which states that:
843            ///
844            ///    "If the max is less than the min, then the max will be floored by the min (essentially yielding minmax(min, min))"
845            #[inline(always)]
846            fn has_auto_min_track_sizing_function(track: &GridTrack) -> bool {
847                track.min_track_sizing_function.is_auto() && !track.max_track_sizing_function.is_min_content()
848            }
849
850            /// Whether a track has a MaxContent min track sizing function
851            #[inline(always)]
852            fn has_max_content_min_track_sizing_function(track: &GridTrack) -> bool {
853                track.min_track_sizing_function.is_max_content()
854            }
855
856            for item in batch.iter_mut() {
857                if !item.spans_track_matching(axis, axis_tracks, |track| {
858                    has_auto_min_track_sizing_function(track) || has_max_content_min_track_sizing_function(track)
859                }) {
860                    continue;
861                }
862                let axis_max_content_size = item_sizer.max_content_contribution(item, axis_tracks);
863                let limit = item.spanned_track_limit(axis, axis_tracks, axis_inner_node_size, &|val, basis| {
864                    item_sizer.calc(val, basis)
865                });
866                let mut space = axis_max_content_size.maybe_min(limit);
867
868                // As for the intrinsic minimums above: scale the space beyond the spanned inflexible
869                // tracks by the crossed flex factor sum, clamped at one. Anchoring at the inflexible
870                // track sizes rather than the current base sizes keeps this pass from compounding with
871                // the scaling already applied to the min-content contribution.
872                if is_flex {
873                    let spanned_tracks = &axis_tracks[item.track_range_excluding_lines(axis)];
874                    let inflexible_sizes: f32 =
875                        spanned_tracks.iter().filter(|track| !track.is_flexible()).map(|track| track.base_size).sum();
876                    let scale = f32_min(crossed_flex_factor_sum(spanned_tracks), 1.0);
877                    space = inflexible_sizes + f32_max(space - inflexible_sizes, 0.0) * scale;
878                }
879                let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)];
880                if space > 0.0 {
881                    // If any of the tracks spanned by the item have a MaxContent min track sizing function then
882                    // distribute space only to those tracks. Otherwise distribute space to tracks with an Auto min
883                    // track sizing function.
884                    //
885                    // Note: this prioritisation of MaxContent over Auto is not mentioned in the spec (which suggests that
886                    // we ought to distribute space evenly between MaxContent and Auto tracks). But it is implemented like
887                    // this in both Chrome and Firefox (and it does have a certain logic to it), so we implement it too for
888                    // compatibility.
889                    //
890                    // See: https://www.w3.org/TR/css-grid-1/#track-size-max-content-min
891                    if tracks.iter().any(has_max_content_min_track_sizing_function) {
892                        distribute_item_space_to_base_size(
893                            is_flex,
894                            space,
895                            tracks,
896                            has_max_content_min_track_sizing_function,
897                            |_| f32::INFINITY,
898                            IntrinsicContributionType::Maximum,
899                        );
900                    } else {
901                        let fit_content_limited_growth_limit =
902                            move |track: &GridTrack| track.fit_content_limited_growth_limit();
903                        distribute_item_space_to_base_size(
904                            is_flex,
905                            space,
906                            tracks,
907                            has_auto_min_track_sizing_function,
908                            fit_content_limited_growth_limit,
909                            IntrinsicContributionType::Maximum,
910                        );
911                    }
912                }
913            }
914            flush_planned_base_size_increases(axis_tracks);
915        }
916
917        // In all cases, continue to increase the base size of tracks with a min track sizing function of max-content by distributing
918        // extra space as needed to account for these items' max-content contributions.
919        let has_max_content_min_track_sizing_function =
920            move |track: &GridTrack| track.min_track_sizing_function.is_max_content();
921        for item in batch.iter_mut() {
922            if !item.spans_track_matching(axis, axis_tracks, has_max_content_min_track_sizing_function) {
923                continue;
924            }
925            let axis_max_content_size = item_sizer.max_content_contribution(item, axis_tracks);
926            let space = axis_max_content_size;
927            let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)];
928            if space > 0.0 {
929                distribute_item_space_to_base_size(
930                    is_flex,
931                    space,
932                    tracks,
933                    has_max_content_min_track_sizing_function,
934                    |track| track.growth_limit,
935                    IntrinsicContributionType::Maximum,
936                );
937            }
938        }
939        flush_planned_base_size_increases(axis_tracks);
940
941        // 4. If at this point any track’s growth limit is now less than its base size, increase its growth limit to match its base size.
942        for track in axis_tracks.iter_mut() {
943            if track.growth_limit < track.base_size {
944                track.growth_limit = track.base_size;
945            }
946        }
947
948        // If a track is a flexible track, then it has flexible max track sizing function
949        // It cannot also have an intrinsic max track sizing function, so these steps do not apply.
950        if !is_flex {
951            // 5. For intrinsic maximums: Next increase the growth limit of tracks with an intrinsic max track sizing function by
952            // distributing extra space as needed to account for these items' min-content contributions.
953            let has_intrinsic_max_track_sizing_function =
954                move |track: &GridTrack| !track.max_track_sizing_function.has_definite_value(axis_inner_node_size);
955            for item in batch.iter_mut() {
956                if !item.spans_track_matching(axis, axis_tracks, has_intrinsic_max_track_sizing_function) {
957                    continue;
958                }
959                let axis_min_content_size = item_sizer.min_content_contribution(item, axis_tracks);
960                let space = axis_min_content_size;
961                let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)];
962                if space > 0.0 {
963                    distribute_item_space_to_growth_limit(space, tracks, has_intrinsic_max_track_sizing_function);
964                }
965            }
966            // Mark any tracks whose growth limit changed from infinite to finite in this step as infinitely growable for the next step.
967            flush_planned_growth_limit_increases(axis_tracks, true);
968
969            // 6. For max-content maximums: Lastly continue to increase the growth limit of tracks with a max track sizing function of max-content
970            // by distributing extra space as needed to account for these items' max-content contributions. However, limit the growth of any
971            // fit-content() tracks by their fit-content() argument.
972            let has_max_content_max_track_sizing_function = |track: &GridTrack| {
973                track.max_track_sizing_function.is_max_content_alike()
974                    || (track.max_track_sizing_function.uses_percentage() && axis_inner_node_size.is_none())
975            };
976            for item in batch.iter_mut() {
977                if !item.spans_track_matching(axis, axis_tracks, has_max_content_max_track_sizing_function) {
978                    continue;
979                }
980                let axis_max_content_size = item_sizer.max_content_contribution(item, axis_tracks);
981                let space = axis_max_content_size;
982                let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)];
983                if space > 0.0 {
984                    distribute_item_space_to_growth_limit(space, tracks, has_max_content_max_track_sizing_function);
985                }
986            }
987            // Mark any tracks whose growth limit changed from infinite to finite in this step as infinitely growable for the next step.
988            flush_planned_growth_limit_increases(axis_tracks, false);
989        }
990    }
991
992    // Step 5. If any track still has an infinite growth limit (because, for example, it had no items placed
993    // in it or it is a flexible track), set its growth limit to its base size.
994    // NOTE: this step is super-important to ensure that the "Maximise Tracks" step doesn't affect flexible tracks
995    axis_tracks
996        .iter_mut()
997        .filter(|track| track.growth_limit == f32::INFINITY)
998        .for_each(|track| track.growth_limit = track.base_size);
999}
1000
1001/// The sum of the flex factors of the flexible tracks in an item's spanned track range
1002#[inline(always)]
1003fn crossed_flex_factor_sum(tracks: &[GridTrack]) -> f32 {
1004    tracks.iter().filter(|track| track.is_flexible()).map(|track| track.flex_factor()).sum()
1005}
1006
1007/// 11.5.1. Distributing Extra Space Across Spanned Tracks
1008/// https://www.w3.org/TR/css-grid-1/#extra-space
1009#[inline(always)]
1010fn distribute_item_space_to_base_size(
1011    is_flex: bool,
1012    space: f32,
1013    tracks: &mut [GridTrack],
1014    track_is_affected: impl Fn(&GridTrack) -> bool,
1015    track_limit: impl Fn(&GridTrack) -> f32,
1016    intrinsic_contribution_type: IntrinsicContributionType,
1017) {
1018    if is_flex {
1019        let filter = |track: &GridTrack| track.is_flexible() && track_is_affected(track);
1020
1021        // If the sum of the flex factors of the affected tracks is greater than zero, distribute
1022        // space according to the ratios of the tracks' flex factors. Otherwise distribute space equally.
1023        //
1024        // Note: the spec says to compute the sum over all flexible tracks spanned by the item, and to
1025        // blend flex-factor-proportional and equal distribution when the sum is below one. But Chrome
1026        // computes the sum over only the affected tracks and uses pure flex-factor ratios whenever
1027        // that sum is non-zero, so we do the same for compatibility.
1028        let flex_factor_sum: f32 = tracks.iter().filter(|track| filter(track)).map(|track| track.flex_factor()).sum();
1029        if flex_factor_sum > 0.0 {
1030            distribute_item_space_to_base_size_inner(
1031                space,
1032                tracks,
1033                filter,
1034                |track| track.flex_factor(),
1035                track_limit,
1036                intrinsic_contribution_type,
1037            )
1038        } else {
1039            distribute_item_space_to_base_size_inner(
1040                space,
1041                tracks,
1042                filter,
1043                |_| 1.0,
1044                track_limit,
1045                intrinsic_contribution_type,
1046            )
1047        }
1048    } else {
1049        distribute_item_space_to_base_size_inner(
1050            space,
1051            tracks,
1052            track_is_affected,
1053            |_| 1.0,
1054            track_limit,
1055            intrinsic_contribution_type,
1056        )
1057    }
1058
1059    /// Inner function that doesn't account for differences due to distributing to flex items
1060    /// This difference is handled by the closure passed in above
1061    fn distribute_item_space_to_base_size_inner(
1062        space: f32,
1063        tracks: &mut [GridTrack],
1064        track_is_affected: impl Fn(&GridTrack) -> bool,
1065        track_distribution_proportion: impl Fn(&GridTrack) -> f32,
1066        track_limit: impl Fn(&GridTrack) -> f32,
1067        intrinsic_contribution_type: IntrinsicContributionType,
1068    ) {
1069        // Skip this distribution if there is either
1070        //   - no space to distribute
1071        //   - no affected tracks to distribute space to
1072        if space == 0.0 || !tracks.iter().any(&track_is_affected) {
1073            return;
1074        }
1075
1076        // Define get_base_size function. This is passed to the distribute_space_up_to_limits helper function
1077        // to indicate that it is the base size that is being distributed to.
1078        let get_base_size = |track: &GridTrack| track.base_size;
1079
1080        // 1. Find the space to distribute
1081        let track_sizes: f32 = tracks.iter().map(|track| track.base_size).sum();
1082        let extra_space: f32 = f32_max(0.0, space - track_sizes);
1083
1084        // 2. Distribute space up to limits:
1085        // Note: there are two exit conditions to this loop:
1086        //   - We run out of space to distribute (extra_space falls below THRESHOLD)
1087        //   - We run out of growable tracks to distribute to
1088
1089        /// Define a small constant to avoid infinite loops due to rounding errors. Rather than stopping distributing
1090        /// extra space when it gets to exactly zero, we will stop when it falls below this amount
1091        const THRESHOLD: f32 = 0.000001;
1092
1093        let extra_space = distribute_space_up_to_limits(
1094            extra_space,
1095            tracks,
1096            &track_is_affected,
1097            &track_distribution_proportion,
1098            get_base_size,
1099            &track_limit,
1100        );
1101
1102        // 3. Distribute remaining span beyond limits (if any)
1103        if extra_space > THRESHOLD {
1104            // When accommodating minimum contributions or accommodating min-content contributions:
1105            //   - any affected track that happens to also have an intrinsic max track sizing function;
1106            // When accommodating max-content contributions:
1107            //   - any affected track that happens to also have a max-content max track sizing function
1108            let mut filter = match intrinsic_contribution_type {
1109                IntrinsicContributionType::Minimum => {
1110                    (|track: &GridTrack| track.max_track_sizing_function.is_intrinsic()) as fn(&GridTrack) -> bool
1111                }
1112                IntrinsicContributionType::Maximum => {
1113                    (|track: &GridTrack| track.max_track_sizing_function.is_max_or_fit_content())
1114                        as fn(&GridTrack) -> bool
1115                }
1116            };
1117
1118            // If there are no such tracks (matching filter above), then use all affected tracks.
1119            let number_of_tracks =
1120                tracks.iter().filter(|track| track_is_affected(track)).filter(|track| filter(track)).count();
1121            if number_of_tracks == 0 {
1122                filter = (|_| true) as fn(&GridTrack) -> bool;
1123            }
1124
1125            // When distributing beyond limits, growth limits are ignored, but the argument
1126            // to any fit-content() max track sizing function still caps growth.
1127            distribute_space_up_to_limits(
1128                extra_space,
1129                tracks,
1130                |track| track_is_affected(track) && filter(track),
1131                &track_distribution_proportion,
1132                get_base_size,
1133                |track| track.fit_content_limit(),
1134            );
1135        }
1136
1137        // 4. For each affected track, if the track’s item-incurred increase is larger than the track’s planned increase
1138        // set the track’s planned increase to that value.
1139        for track in tracks.iter_mut() {
1140            if track.item_incurred_increase > track.base_size_planned_increase {
1141                track.base_size_planned_increase = track.item_incurred_increase;
1142            }
1143
1144            // Reset the item_incurresed increase ready for the next space distribution
1145            track.item_incurred_increase = 0.0;
1146        }
1147    }
1148}
1149
1150/// 11.5.1. Distributing Extra Space Across Spanned Tracks
1151/// This is simplified (and faster) version of the algorithm for growth limits
1152/// https://www.w3.org/TR/css-grid-1/#extra-space
1153fn distribute_item_space_to_growth_limit(
1154    space: f32,
1155    tracks: &mut [GridTrack],
1156    track_is_affected: impl Fn(&GridTrack) -> bool,
1157) {
1158    // Skip this distribution if there is either
1159    //   - no space to distribute
1160    //   - no affected tracks to distribute space to
1161    if space == 0.0 || tracks.iter().filter(|track| track_is_affected(track)).count() == 0 {
1162        return;
1163    }
1164
1165    // 1. Find the space to distribute
1166    let track_sizes: f32 = tracks
1167        .iter()
1168        .map(|track| if track.growth_limit == f32::INFINITY { track.base_size } else { track.growth_limit })
1169        .sum();
1170    let extra_space: f32 = f32_max(0.0, space - track_sizes);
1171
1172    // 2. Distribute space up to limits:
1173    // For growth limits the limit is the growth limit itself unless the track is marked infinitely growable (or its
1174    // growth limit is still infinite), in which case it is the track's fit-content() argument if it has one, or
1175    // infinity if it does not. As the affected size of a track that isn't growable is its growth limit, those
1176    // tracks won't receive any space in this step.
1177    let growth_limit_or_base_size =
1178        |track: &GridTrack| if track.growth_limit == f32::INFINITY { track.base_size } else { track.growth_limit };
1179    let extra_space = distribute_space_up_to_limits(
1180        extra_space,
1181        tracks,
1182        |track| track_is_affected(track) && (track.infinitely_growable || track.growth_limit == f32::INFINITY),
1183        |_| 1.0,
1184        growth_limit_or_base_size,
1185        move |track| track.fit_content_limit(),
1186    );
1187
1188    // 3. Distribute space beyond limits
1189    // If space remains after all tracks are frozen, unfreeze and continue to distribute space to the item-incurred increase
1190    // ...when handling any intrinsic growth limit: all affected tracks.
1191    if extra_space > 0.0 {
1192        distribute_space_up_to_limits(
1193            extra_space,
1194            tracks,
1195            track_is_affected,
1196            |_| 1.0,
1197            growth_limit_or_base_size,
1198            move |track| track.fit_content_limit(),
1199        );
1200    }
1201
1202    // 4. For each affected track, if the track’s item-incurred increase is larger than the track’s planned increase
1203    // set the track’s planned increase to that value.
1204    for track in tracks.iter_mut() {
1205        if track.item_incurred_increase > track.growth_limit_planned_increase {
1206            track.growth_limit_planned_increase = track.item_incurred_increase;
1207        }
1208
1209        // Reset the item_incurresed increase ready for the next space distribution
1210        track.item_incurred_increase = 0.0;
1211    }
1212}
1213
1214/// 11.6 Maximise Tracks
1215/// Distributes free space (if any) to tracks with FINITE growth limits, up to their limits.
1216#[inline(always)]
1217fn maximise_tracks(axis_tracks: &mut [GridTrack], axis_available_grid_space: AvailableSpace) {
1218    let used_space: f32 = axis_tracks.iter().map(|track| track.base_size).sum();
1219    let free_space = axis_available_grid_space.compute_free_space(used_space);
1220    if free_space == f32::INFINITY {
1221        axis_tracks.iter_mut().for_each(|track| track.base_size = track.growth_limit);
1222    } else if free_space > 0.0 {
1223        distribute_space_up_to_limits(
1224            free_space,
1225            axis_tracks,
1226            |_| true,
1227            |_| 1.0,
1228            |track| track.base_size,
1229            move |track: &GridTrack| track.fit_content_limited_growth_limit(),
1230        );
1231        for track in axis_tracks.iter_mut() {
1232            track.base_size += track.item_incurred_increase;
1233            track.item_incurred_increase = 0.0;
1234        }
1235    }
1236}
1237
1238/// 11.7. Expand Flexible Tracks
1239/// This step sizes flexible tracks using the largest value it can assign to an fr without exceeding the available space.
1240#[allow(clippy::too_many_arguments)]
1241#[inline(always)]
1242fn expand_flexible_tracks<Tree: LayoutPartialTree>(
1243    tree: &mut Tree,
1244    axis: AbstractAxis,
1245    axis_tracks: &mut [GridTrack],
1246    other_axis_tracks: &[GridTrack],
1247    items: &mut [GridItem],
1248    axis_min_size: Option<f32>,
1249    axis_max_size: Option<f32>,
1250    axis_available_space_for_expansion: AvailableSpace,
1251    inner_node_size: Size<Option<f32>>,
1252    get_track_size_estimate: impl Fn(&GridTrack, Option<f32>, &Tree) -> Option<f32>,
1253) {
1254    let mut item_sizer =
1255        IntrinsicSizeMeasurer { tree, other_axis_tracks, axis, inner_node_size, get_track_size_estimate };
1256
1257    // First, find the grid’s used flex fraction:
1258    let flex_fraction = match axis_available_space_for_expansion {
1259        // If the free space is zero:
1260        //    The used flex fraction is zero.
1261        // Otherwise, if the free space is a definite length:
1262        //   The used flex fraction is the result of finding the size of an fr using all of the grid tracks and
1263        //   a space to fill of the available grid space.
1264        AvailableSpace::Definite(available_space) => {
1265            let used_space: f32 = axis_tracks.iter().map(|track| track.base_size).sum();
1266            let free_space = available_space - used_space;
1267            if free_space <= 0.0 {
1268                0.0
1269            } else {
1270                find_size_of_fr(axis_tracks, available_space)
1271            }
1272        }
1273        // If ... sizing the grid container under a min-content constraint the used flex fraction is zero.
1274        AvailableSpace::MinContent => 0.0,
1275        // Otherwise, if the free space is an indefinite length:
1276        AvailableSpace::MaxContent => {
1277            // The used flex fraction is the maximum of:
1278            let flex_fraction = f32_max(
1279                // For each flexible track, if the flexible track’s flex factor is greater than one,
1280                // the result of dividing the track’s base size by its flex factor; otherwise, the track’s base size.
1281                axis_tracks
1282                    .iter()
1283                    .filter(|track| track.max_track_sizing_function.is_fr())
1284                    .map(|track| {
1285                        let flex_factor = track.flex_factor();
1286                        if flex_factor > 1.0 {
1287                            track.base_size / flex_factor
1288                        } else {
1289                            track.base_size
1290                        }
1291                    })
1292                    .max_by(|a, b| a.total_cmp(b))
1293                    .unwrap_or(0.0),
1294                // For each grid item that crosses a flexible track, the result of finding the size of an fr using all the grid tracks
1295                // that the item crosses and a space to fill of the item’s max-content contribution.
1296                items
1297                    .iter_mut()
1298                    .filter(|item| item.crosses_flexible_track(axis))
1299                    .map(|item| {
1300                        let max_content_contribution = item_sizer.max_content_contribution(item, axis_tracks);
1301                        let tracks = &axis_tracks[item.track_range_excluding_lines(axis)];
1302                        find_size_of_fr(tracks, max_content_contribution)
1303                    })
1304                    .max_by(|a, b| a.total_cmp(b))
1305                    .unwrap_or(0.0),
1306            );
1307
1308            // If using this flex fraction would cause the grid to be smaller than the grid container’s min-width/height (or larger than the
1309            // grid container’s max-width/height), then redo this step, treating the free space as definite and the available grid space as equal
1310            // to the grid container’s inner size when it’s sized to its min-width/height (max-width/height).
1311            // (Note: min_size takes precedence over max_size)
1312            let hypothetical_grid_size: f32 = axis_tracks
1313                .iter()
1314                .map(|track| {
1315                    if track.max_track_sizing_function.is_fr() {
1316                        let track_flex_factor = track.max_track_sizing_function.0.value();
1317                        f32_max(track.base_size, track_flex_factor * flex_fraction)
1318                    } else {
1319                        track.base_size
1320                    }
1321                })
1322                .sum();
1323            let axis_min_size = axis_min_size.unwrap_or(0.0);
1324            let axis_max_size = axis_max_size.unwrap_or(f32::INFINITY);
1325            if hypothetical_grid_size < axis_min_size {
1326                find_size_of_fr(axis_tracks, axis_min_size)
1327            } else if hypothetical_grid_size > axis_max_size {
1328                find_size_of_fr(axis_tracks, axis_max_size)
1329            } else {
1330                flex_fraction
1331            }
1332        }
1333    };
1334
1335    // For each flexible track, if the product of the used flex fraction and the track’s flex factor is greater
1336    // than the track’s base size, set its base size to that product.
1337    for track in axis_tracks.iter_mut().filter(|track| track.max_track_sizing_function.is_fr()) {
1338        let track_flex_factor = track.max_track_sizing_function.0.value();
1339        track.base_size = f32_max(track.base_size, track_flex_factor * flex_fraction);
1340    }
1341}
1342
1343/// 11.7.1. Find the Size of an fr
1344/// This algorithm finds the largest size that an fr unit can be without exceeding the target size.
1345/// It must be called with a set of grid tracks and some quantity of space to fill.
1346#[inline(always)]
1347fn find_size_of_fr(tracks: &[GridTrack], space_to_fill: f32) -> f32 {
1348    // Handle the trivial case where there is no space to fill
1349    // Do not remove as otherwise the loop below will loop infinitely
1350    if space_to_fill == 0.0 {
1351        return 0.0;
1352    }
1353
1354    // If the product of the hypothetical fr size (computed below) and any flexible track’s flex factor
1355    // is less than the track’s base size, then we must restart this algorithm treating all such tracks as inflexible.
1356    // We therefore wrap the entire algorithm in a loop, with an hypothetical_fr_size of INFINITY such that the above
1357    // condition can never be true for the first iteration.
1358    let mut hypothetical_fr_size = f32::INFINITY;
1359    let mut previous_iter_hypothetical_fr_size;
1360    // Every restart of the algorithm treats at least one more flexible track as inflexible, so a valid
1361    // hypothetical fr size is always found within `tracks.len() + 1` iterations. Non-finite values (which
1362    // can arise from non-finite style inputs) break that invariant as comparisons against `NaN` are always
1363    // false, so we bound the iteration count to guarantee termination.
1364    let max_iterations = tracks.len() + 1;
1365    for _ in 0..max_iterations {
1366        // Let leftover space be the space to fill minus the base sizes of the non-flexible grid tracks.
1367        // Let flex factor sum be the sum of the flex factors of the flexible tracks. If this value is less than 1, set it to 1 instead.
1368        // We compute both of these in a single loop to avoid iterating over the data twice
1369        let mut used_space = 0.0;
1370        let mut naive_flex_factor_sum = 0.0;
1371        for track in tracks.iter() {
1372            // Tracks for which flex_factor * hypothetical_fr_size < track.base_size are treated as inflexible
1373            if track.max_track_sizing_function.is_fr()
1374                && track.max_track_sizing_function.0.value() * hypothetical_fr_size >= track.base_size
1375            {
1376                naive_flex_factor_sum += track.max_track_sizing_function.0.value();
1377            } else {
1378                used_space += track.base_size;
1379            };
1380        }
1381        let leftover_space = space_to_fill - used_space;
1382        let flex_factor = f32_max(naive_flex_factor_sum, 1.0);
1383
1384        // Let the hypothetical fr size be the leftover space divided by the flex factor sum.
1385        previous_iter_hypothetical_fr_size = hypothetical_fr_size;
1386        hypothetical_fr_size = leftover_space / flex_factor;
1387
1388        // If the product of the hypothetical fr size and a flexible track’s flex factor is less than the track’s base size,
1389        // restart this algorithm treating all such tracks as inflexible.
1390        // We keep track of the hypothetical_fr_size
1391        let hypothetical_fr_size_is_valid = tracks.iter().all(|track| {
1392            if track.max_track_sizing_function.is_fr() {
1393                let flex_factor = track.max_track_sizing_function.0.value();
1394                flex_factor * hypothetical_fr_size >= track.base_size
1395                    || flex_factor * previous_iter_hypothetical_fr_size < track.base_size
1396            } else {
1397                true
1398            }
1399        });
1400        if hypothetical_fr_size_is_valid {
1401            break;
1402        }
1403    }
1404
1405    // Return the hypothetical fr size.
1406    hypothetical_fr_size
1407}
1408
1409/// 11.8. Stretch auto Tracks
1410/// This step expands tracks that have an auto max track sizing function by dividing any remaining positive, definite free space equally amongst them.
1411#[inline(always)]
1412fn stretch_auto_tracks(
1413    axis_tracks: &mut [GridTrack],
1414    axis_min_size: Option<f32>,
1415    axis_available_space_for_expansion: AvailableSpace,
1416) {
1417    let num_auto_tracks = axis_tracks.iter().filter(|track| track.max_track_sizing_function.is_auto()).count();
1418    if num_auto_tracks > 0 {
1419        let used_space: f32 = axis_tracks.iter().map(|track| track.base_size).sum();
1420
1421        // If the free space is indefinite, but the grid container has a definite min-width/height
1422        // use that size to calculate the free space for this step instead.
1423        let free_space = if axis_available_space_for_expansion.is_definite() {
1424            axis_available_space_for_expansion.compute_free_space(used_space)
1425        } else {
1426            match axis_min_size {
1427                Some(size) => size - used_space,
1428                None => 0.0,
1429            }
1430        };
1431        if free_space > 0.0 {
1432            let extra_space_per_auto_track = free_space / num_auto_tracks as f32;
1433            axis_tracks
1434                .iter_mut()
1435                .filter(|track| track.max_track_sizing_function.is_auto())
1436                .for_each(|track| track.base_size += extra_space_per_auto_track);
1437        }
1438    }
1439}
1440
1441/// Helper function for distributing space to tracks evenly
1442/// Used by both distribute_item_space_to_base_size and maximise_tracks steps
1443#[inline(always)]
1444fn distribute_space_up_to_limits(
1445    space_to_distribute: f32,
1446    tracks: &mut [GridTrack],
1447    track_is_affected: impl Fn(&GridTrack) -> bool,
1448    track_distribution_proportion: impl Fn(&GridTrack) -> f32,
1449    track_affected_property: impl Fn(&GridTrack) -> f32,
1450    track_limit: impl Fn(&GridTrack) -> f32,
1451) -> f32 {
1452    /// Define a small constant to avoid infinite loops due to rounding errors. Rather than stopping distributing
1453    /// extra space when it gets to exactly zero, we will stop when it falls below this amount
1454    const THRESHOLD: f32 = 0.01;
1455
1456    // Each iteration freezes at least one track at its limit, so the loop always completes within
1457    // `tracks.len() + 1` iterations. Non-finite values (which can arise from non-finite style inputs) can
1458    // prevent any space from being distributed, so we bound the iteration count to guarantee termination.
1459    let max_iterations = tracks.len() + 1;
1460
1461    let mut space_to_distribute = space_to_distribute;
1462    for _ in 0..max_iterations {
1463        if space_to_distribute <= THRESHOLD {
1464            break;
1465        }
1466        let track_distribution_proportion_sum: f32 = tracks
1467            .iter()
1468            .filter(|track| track_affected_property(track) + track.item_incurred_increase < track_limit(track))
1469            .filter(|track| track_is_affected(track))
1470            .map(&track_distribution_proportion)
1471            .sum();
1472
1473        if track_distribution_proportion_sum == 0.0 {
1474            break;
1475        }
1476
1477        // Compute item-incurred increase for this iteration
1478        let min_increase_limit = tracks
1479            .iter()
1480            .filter(|track| track_affected_property(track) + track.item_incurred_increase < track_limit(track))
1481            .filter(|track| track_is_affected(track))
1482            .map(|track| {
1483                (track_limit(track) - track_affected_property(track) - track.item_incurred_increase)
1484                    / track_distribution_proportion(track)
1485            })
1486            .min_by(|a, b| a.total_cmp(b))
1487            .unwrap(); // We will never pass an empty track list to this function
1488        let iteration_item_incurred_increase =
1489            f32_min(min_increase_limit, space_to_distribute / track_distribution_proportion_sum);
1490
1491        for track in tracks.iter_mut().filter(|track| track_is_affected(track)) {
1492            let increase = iteration_item_incurred_increase * track_distribution_proportion(track);
1493            if increase > 0.0
1494                && track_affected_property(track) + track.item_incurred_increase + increase
1495                    <= track_limit(track) + THRESHOLD
1496            {
1497                track.item_incurred_increase += increase;
1498                space_to_distribute -= increase;
1499            }
1500        }
1501    }
1502
1503    space_to_distribute
1504}