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