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