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