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