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