//! Implements the track sizing algorithm //! use super::types::{GridItem, GridTrack, TrackCounts}; use crate::geometry::{AbstractAxis, Line, Size}; use crate::style::{ AlignContent, AlignSelf, AvailableSpace, LengthPercentage, MaxTrackSizingFunction, MinTrackSizingFunction, }; use crate::style_helpers::TaffyMinContent; use crate::tree::{LayoutPartialTree, LayoutPartialTreeExt, SizingMode}; use crate::util::sys::{f32_max, f32_min, Vec}; use crate::util::{MaybeMath, ResolveOrZero}; use core::cmp::Ordering; /// Takes an axis, and a list of grid items sorted firstly by whether they cross a flex track /// in the specified axis (items that don't cross a flex track first) and then by the number /// of tracks they cross in specified axis (ascending order). struct ItemBatcher { /// The axis in which the ItemBatcher is operating. Used when querying properties from items. axis: AbstractAxis, /// The starting index of the current batch index_offset: usize, /// The span of the items in the current batch current_span: u16, /// Whether the current batch of items cross a flexible track current_is_flex: bool, } impl ItemBatcher { /// Create a new ItemBatcher for the specified axis #[inline(always)] fn new(axis: AbstractAxis) -> Self { ItemBatcher { index_offset: 0, axis, current_span: 1, current_is_flex: false } } /// This is basically a manual version of Iterator::next which passes `items` /// in as a parameter on each iteration to work around borrow checker rules #[inline] fn next<'items>(&mut self, items: &'items mut [GridItem]) -> Option<(&'items mut [GridItem], bool)> { if self.current_is_flex || self.index_offset >= items.len() { return None; } let item = &items[self.index_offset]; self.current_span = item.span(self.axis); self.current_is_flex = item.crosses_flexible_track(self.axis); let next_index_offset = if self.current_is_flex { items.len() } else { items .iter() .position(|item: &GridItem| { item.crosses_flexible_track(self.axis) || item.span(self.axis) > self.current_span }) .unwrap_or(items.len()) }; let batch_range = self.index_offset..next_index_offset; self.index_offset = next_index_offset; let batch = &mut items[batch_range]; Some((batch, self.current_is_flex)) } } /// This struct captures a bunch of variables which are used to compute the intrinsic sizes of children so that those variables /// don't have to be passed around all over the place below. It then has methods that implement the intrinsic sizing computations struct IntrisicSizeMeasurer<'tree, 'oat, Tree, EstimateFunction> where Tree: LayoutPartialTree, EstimateFunction: Fn(&GridTrack, Option) -> Option, { /// The layout tree tree: &'tree mut Tree, /// The tracks in the opposite axis to the one we are currently sizing other_axis_tracks: &'oat [GridTrack], /// A function that computes an estimate of an other-axis track's size which is passed to /// the child size measurement functions get_track_size_estimate: EstimateFunction, /// The axis we are currently sizing axis: AbstractAxis, /// The available grid space inner_node_size: Size>, } impl IntrisicSizeMeasurer<'_, '_, Tree, EstimateFunction> where Tree: LayoutPartialTree, EstimateFunction: Fn(&GridTrack, Option) -> Option, { /// Compute the available_space to be passed to the child sizing functions /// These are estimates based on either the max track sizing function or the provisional base size in the opposite /// axis to the one currently being sized. /// https://www.w3.org/TR/css-grid-1/#algo-overview #[inline(always)] fn available_space(&self, item: &mut GridItem) -> Size> { item.available_space_cached( self.axis, self.other_axis_tracks, self.inner_node_size.get(self.axis.other()), &self.get_track_size_estimate, ) } /// Compute the item's resolved margins for size contributions. Horizontal percentage margins always resolve /// to zero if the container size is indefinite as otherwise this would introduce a cyclic dependency. #[inline(always)] fn margins_axis_sums_with_baseline_shims(&self, item: &GridItem) -> Size { item.margins_axis_sums_with_baseline_shims(self.inner_node_size.width) } /// Retrieve the item's min content contribution from the cache or compute it using the provided parameters #[inline(always)] fn min_content_contribution(&mut self, item: &mut GridItem) -> f32 { let available_space = self.available_space(item); let margin_axis_sums = self.margins_axis_sums_with_baseline_shims(item); let contribution = item.min_content_contribution_cached(self.axis, self.tree, available_space, self.inner_node_size); contribution + margin_axis_sums.get(self.axis) } /// Retrieve the item's max content contribution from the cache or compute it using the provided parameters #[inline(always)] fn max_content_contribution(&mut self, item: &mut GridItem) -> f32 { let available_space = self.available_space(item); let margin_axis_sums = self.margins_axis_sums_with_baseline_shims(item); let contribution = item.max_content_contribution_cached(self.axis, self.tree, available_space, self.inner_node_size); contribution + margin_axis_sums.get(self.axis) } /// The minimum contribution of an item is the smallest outer size it can have. /// Specifically: /// - If the item’s computed preferred size behaves as auto or depends on the size of its containing block in the relevant axis: /// Its minimum contribution is the outer size that would result from assuming the item’s used minimum size as its preferred size; /// - Else the item’s minimum contribution is its min-content contribution. /// /// Because the minimum contribution often depends on the size of the item’s content, it is considered a type of intrinsic size contribution. #[inline(always)] fn minimum_contribution(&mut self, item: &mut GridItem, axis_tracks: &[GridTrack]) -> f32 { let available_space = self.available_space(item); let margin_axis_sums = self.margins_axis_sums_with_baseline_shims(item); let contribution = item.minimum_contribution_cached(self.tree, self.axis, axis_tracks, available_space, self.inner_node_size); contribution + margin_axis_sums.get(self.axis) } } /// To make track sizing efficient we want to order tracks /// Here a placement is either a Line representing a row-start/row-end or a column-start/column-end #[inline(always)] pub(super) fn cmp_by_cross_flex_then_span_then_start( axis: AbstractAxis, ) -> impl FnMut(&GridItem, &GridItem) -> Ordering { move |item_a: &GridItem, item_b: &GridItem| -> Ordering { match (item_a.crosses_flexible_track(axis), item_b.crosses_flexible_track(axis)) { (false, true) => Ordering::Less, (true, false) => Ordering::Greater, _ => { let placement_a = item_a.placement(axis); let placement_b = item_b.placement(axis); match placement_a.span().cmp(&placement_b.span()) { Ordering::Less => Ordering::Less, Ordering::Greater => Ordering::Greater, Ordering::Equal => placement_a.start.cmp(&placement_b.start), } } } } } /// When applying the track sizing algorithm and estimating the size in the other axis for content sizing items /// we should take into account align-content/justify-content if both the grid container and all items in the /// other axis have definite sizes. This function computes such a per-gutter additional size adjustment. #[inline(always)] pub(super) fn compute_alignment_gutter_adjustment( alignment: AlignContent, axis_inner_node_size: Option, get_track_size_estimate: impl Fn(&GridTrack, Option) -> Option, tracks: &[GridTrack], ) -> f32 { if tracks.len() <= 1 { return 0.0; } // As items never cross the outermost gutters in a grid, we can simplify our calculations by treating // AlignContent::Start and AlignContent::End the same let outer_gutter_weight = match alignment { AlignContent::Start => 1, AlignContent::FlexStart => 1, AlignContent::End => 1, AlignContent::FlexEnd => 1, AlignContent::Center => 1, AlignContent::Stretch => 0, AlignContent::SpaceBetween => 0, AlignContent::SpaceAround => 1, AlignContent::SpaceEvenly => 1, }; let inner_gutter_weight = match alignment { AlignContent::FlexStart => 0, AlignContent::Start => 0, AlignContent::FlexEnd => 0, AlignContent::End => 0, AlignContent::Center => 0, AlignContent::Stretch => 0, AlignContent::SpaceBetween => 1, AlignContent::SpaceAround => 2, AlignContent::SpaceEvenly => 1, }; if inner_gutter_weight == 0 { return 0.0; } if let Some(axis_inner_node_size) = axis_inner_node_size { let free_space = tracks .iter() .map(|track| get_track_size_estimate(track, Some(axis_inner_node_size))) .sum::>() .map(|track_size_sum| f32_max(0.0, axis_inner_node_size - track_size_sum)) .unwrap_or(0.0); let weighted_track_count = (((tracks.len() - 3) / 2) * inner_gutter_weight as usize) + (2 * outer_gutter_weight as usize); return (free_space / weighted_track_count as f32) * inner_gutter_weight as f32; } 0.0 } /// Convert origin-zero coordinates track placement in grid track vector indexes #[inline(always)] pub(super) fn resolve_item_track_indexes(items: &mut [GridItem], column_counts: TrackCounts, row_counts: TrackCounts) { for item in items { item.column_indexes = item.column.map(|line| line.into_track_vec_index(column_counts) as u16); item.row_indexes = item.row.map(|line| line.into_track_vec_index(row_counts) as u16); } } /// Determine (in each axis) whether the item crosses any flexible tracks #[inline(always)] pub(super) fn determine_if_item_crosses_flexible_or_intrinsic_tracks( items: &mut Vec, columns: &[GridTrack], rows: &[GridTrack], ) { for item in items { item.crosses_flexible_column = item.track_range_excluding_lines(AbstractAxis::Inline).any(|i| columns[i].is_flexible()); item.crosses_intrinsic_column = item.track_range_excluding_lines(AbstractAxis::Inline).any(|i| columns[i].has_intrinsic_sizing_function()); item.crosses_flexible_row = item.track_range_excluding_lines(AbstractAxis::Block).any(|i| rows[i].is_flexible()); item.crosses_intrinsic_row = item.track_range_excluding_lines(AbstractAxis::Block).any(|i| rows[i].has_intrinsic_sizing_function()); } } /// Track sizing algorithm /// Note: Gutters are treated as empty fixed-size tracks for the purpose of the track sizing algorithm. #[allow(clippy::too_many_arguments)] pub(super) fn track_sizing_algorithm( tree: &mut Tree, axis: AbstractAxis, axis_min_size: Option, axis_max_size: Option, axis_alignment: AlignContent, other_axis_alignment: AlignContent, available_grid_space: Size, inner_node_size: Size>, axis_tracks: &mut [GridTrack], other_axis_tracks: &mut [GridTrack], items: &mut [GridItem], get_track_size_estimate: fn(&GridTrack, Option) -> Option, has_baseline_aligned_item: bool, ) { // 11.4 Initialise Track sizes // Initialize each track’s base size and growth limit. initialize_track_sizes(axis_tracks, inner_node_size.get(axis)); // 11.5.1 Shim item baselines if has_baseline_aligned_item { resolve_item_baselines(tree, axis, items, inner_node_size); } // If all tracks have base_size = growth_limit, then skip the rest of this function. // Note: this can only happen both track sizing function have the same fixed track sizing function if axis_tracks.iter().all(|track| track.base_size == track.growth_limit) { return; } // Pre-computations for 11.5 Resolve Intrinsic Track Sizes // Compute an additional amount to add to each spanned gutter when computing item's estimated size in the // in the opposite axis based on the alignment, container size, and estimated track sizes in that axis let gutter_alignment_adjustment = compute_alignment_gutter_adjustment( other_axis_alignment, inner_node_size.get(axis.other()), get_track_size_estimate, other_axis_tracks, ); if other_axis_tracks.len() > 3 { let len = other_axis_tracks.len(); let inner_gutter_tracks = other_axis_tracks[2..len].iter_mut().step_by(2); for track in inner_gutter_tracks { track.content_alignment_adjustment = gutter_alignment_adjustment; } } // 11.5 Resolve Intrinsic Track Sizes resolve_intrinsic_track_sizes( tree, axis, axis_tracks, other_axis_tracks, items, available_grid_space.get(axis), inner_node_size, get_track_size_estimate, ); // 11.6. Maximise Tracks // Distributes free space (if any) to tracks with FINITE growth limits, up to their limits. maximise_tracks(axis_tracks, inner_node_size.get(axis), available_grid_space.get(axis)); // For the purpose of the final two expansion steps ("Expand Flexible Tracks" and "Stretch auto Tracks"), we only want to expand // 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 // something like stretch alignment), not just any available space. To do this we map definite available space to AvailableSpace::MaxContent // in the case that inner_node_size is None let axis_available_space_for_expansion = if let Some(available_space) = inner_node_size.get(axis) { AvailableSpace::Definite(available_space) } else { match available_grid_space.get(axis) { AvailableSpace::MinContent => AvailableSpace::MinContent, AvailableSpace::MaxContent | AvailableSpace::Definite(_) => AvailableSpace::MaxContent, } }; // 11.7. Expand Flexible Tracks // This step sizes flexible tracks using the largest value it can assign to an fr without exceeding the available space. expand_flexible_tracks( tree, axis, axis_tracks, items, axis_min_size, axis_max_size, axis_available_space_for_expansion, inner_node_size, ); // 11.8. Stretch auto Tracks // This step expands tracks that have an auto max track sizing function by dividing any remaining positive, definite free space equally amongst them. if axis_alignment == AlignContent::Stretch { stretch_auto_tracks(axis_tracks, axis_min_size, axis_available_space_for_expansion); } } /// Whether it is a minimum or maximum size's space being distributed /// This controls behaviour of the space distribution algorithm when distributing beyond limits /// See "distributing space beyond limits" at https://www.w3.org/TR/css-grid-1/#extra-space #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum IntrinsicContributionType { /// It's a minimum size's space being distributed Minimum, /// It's a maximum size's space being distributed Maximum, } /// Add any planned base size increases to the base size after a round of distributing space to base sizes /// Reset the planed base size increase to zero ready for the next round. #[inline(always)] fn flush_planned_base_size_increases(tracks: &mut [GridTrack]) { for track in tracks { track.base_size += track.base_size_planned_increase; track.base_size_planned_increase = 0.0; } } /// Add any planned growth limit increases to the growth limit after a round of distributing space to growth limits /// Reset the planed growth limit increase to zero ready for the next round. #[inline(always)] fn flush_planned_growth_limit_increases(tracks: &mut [GridTrack], set_infinitely_growable: bool) { for track in tracks { if track.growth_limit_planned_increase > 0.0 { track.growth_limit = if track.growth_limit == f32::INFINITY { track.base_size + track.growth_limit_planned_increase } else { track.growth_limit + track.growth_limit_planned_increase }; track.infinitely_growable = set_infinitely_growable; } else { track.infinitely_growable = false; } track.growth_limit_planned_increase = 0.0 } } /// 11.4 Initialise Track sizes /// Initialize each track’s base size and growth limit. #[inline(always)] fn initialize_track_sizes(axis_tracks: &mut [GridTrack], axis_inner_node_size: Option) { for track in axis_tracks.iter_mut() { // For each track, if the track’s min track sizing function is: // - A fixed sizing function // Resolve to an absolute length and use that size as the track’s initial base size. // Note: Indefinite lengths cannot occur, as they’re treated as auto. // - An intrinsic sizing function // Use an initial base size of zero. track.base_size = track.min_track_sizing_function.definite_value(axis_inner_node_size).unwrap_or(0.0); // For each track, if the track’s max track sizing function is: // - A fixed sizing function // Resolve to an absolute length and use that size as the track’s initial growth limit. // - An intrinsic sizing function // Use an initial growth limit of infinity. // - A flexible sizing function // Use an initial growth limit of infinity. track.growth_limit = track.max_track_sizing_function.definite_value(axis_inner_node_size).unwrap_or(f32::INFINITY); // In all cases, if the growth limit is less than the base size, increase the growth limit to match the base size. if track.growth_limit < track.base_size { track.growth_limit = track.base_size; } } } /// 11.5.1 Shim baseline-aligned items so their intrinsic size contributions reflect their baseline alignment. fn resolve_item_baselines( tree: &mut impl LayoutPartialTree, axis: AbstractAxis, items: &mut [GridItem], inner_node_size: Size>, ) { // Sort items by track in the other axis (row) start position so that we can iterate items in groups which // are in the same track in the other axis (row) let other_axis = axis.other(); items.sort_by_key(|item| item.placement(other_axis).start); // Iterate over grid rows let mut remaining_items = &mut items[0..]; while !remaining_items.is_empty() { // Get the row index of the current row let current_row = remaining_items[0].placement(other_axis).start; // 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) let next_row_first_item = remaining_items.iter().position(|item| item.placement(other_axis).start != current_row); // Use this index to split the `remaining_items` slice in two slices: // - A `row_items` slice containing the items (that start) in the current row // - A new `remaining_items` consisting of the remainder of the `remaining_items` slice // that hasn't been split off into `row_items let row_items = if let Some(index) = next_row_first_item { let (row_items, tail) = remaining_items.split_at_mut(index); remaining_items = tail; row_items } else { let row_items = remaining_items; remaining_items = &mut []; row_items }; // Count how many items in *this row* are baseline aligned // If a row has one or zero items participating in baseline alignment then baseline alignment is a no-op // for those items and we skip further computations for that row let row_baseline_item_count = row_items.iter().filter(|item| item.align_self == AlignSelf::Baseline).count(); if row_baseline_item_count <= 1 { continue; } // Compute the baselines of all items in the row for item in row_items.iter_mut() { let measured_size_and_baselines = tree.perform_child_layout( item.node, Size::NONE, inner_node_size, Size::MIN_CONTENT, SizingMode::InherentSize, Line::FALSE, ); let baseline = measured_size_and_baselines.first_baselines.y; let height = measured_size_and_baselines.size.height; item.baseline = Some(baseline.unwrap_or(height) + item.margin.top.resolve_or_zero(inner_node_size.width)); } // Compute the max baseline of all items in the row let row_max_baseline = row_items.iter().map(|item| item.baseline.unwrap_or(0.0)).max_by(|a, b| a.total_cmp(b)).unwrap(); // Compute the baseline shim for each item in the row for item in row_items.iter_mut() { item.baseline_shim = row_max_baseline - item.baseline.unwrap_or(0.0); } } } /// 11.5 Resolve Intrinsic Track Sizes #[allow(clippy::too_many_arguments)] fn resolve_intrinsic_track_sizes( tree: &mut impl LayoutPartialTree, axis: AbstractAxis, axis_tracks: &mut [GridTrack], other_axis_tracks: &[GridTrack], items: &mut [GridItem], axis_available_grid_space: AvailableSpace, inner_node_size: Size>, get_track_size_estimate: fn(&GridTrack, Option) -> Option, ) { // Step 1. Shim baseline-aligned items so their intrinsic size contributions reflect their baseline alignment. // Already done at this point. See resolve_item_baselines function. // Step 2. // The track sizing algorithm requires us to iterate through the items in ascendeding order of the number of // tracks they span (first items that span 1 track, then items that span 2 tracks, etc). // To avoid having to do multiple iterations of the items, we pre-sort them into this order. items.sort_by(cmp_by_cross_flex_then_span_then_start(axis)); // Step 2, Step 3 and Step 4 // 2 & 3. Iterate over items that don't cross a flex track. Items should have already been sorted in ascending order // of the number of tracks they span. Step 2 is the 1 track case and has an optimised implementation // 4. Next, repeat the previous step instead considering (together, rather than grouped by span size) all items // that do span a track with a flexible sizing function while // Compute item's intrinsic (content-based) sizes // Note: For items with a specified minimum size of auto (the initial value), the minimum contribution is usually equivalent // to the min-content contribution—but can differ in some cases, see §6.6 Automatic Minimum Size of Grid Items. // Also, minimum contribution <= min-content contribution <= max-content contribution. let axis_inner_node_size = inner_node_size.get(axis); let flex_factor_sum = axis_tracks.iter().map(|track| track.flex_factor()).sum::(); let mut item_sizer = IntrisicSizeMeasurer { tree, other_axis_tracks, axis, inner_node_size, get_track_size_estimate }; let mut batched_item_iterator = ItemBatcher::new(axis); while let Some((batch, is_flex)) = batched_item_iterator.next(items) { // 2. Size tracks to fit non-spanning items: For each track with an intrinsic track sizing function and not a flexible sizing function, // consider the items in it with a span of 1: let batch_span = batch[0].placement(axis).span(); if !is_flex && batch_span == 1 { for item in batch.iter_mut() { let track_index = item.placement_indexes(axis).start + 1; let track = &axis_tracks[track_index as usize]; // Handle base sizes let new_base_size = match track.min_track_sizing_function { MinTrackSizingFunction::MinContent => { f32_max(track.base_size, item_sizer.min_content_contribution(item)) } // If the container size is indefinite and has not yet been resolved then percentage sized // tracks should be treated as min-content (this matches Chrome's behaviour and seems sensible) MinTrackSizingFunction::Fixed(LengthPercentage::Percent(_)) => { if axis_inner_node_size.is_none() { f32_max(track.base_size, item_sizer.min_content_contribution(item)) } else { track.base_size } } MinTrackSizingFunction::MaxContent => { f32_max(track.base_size, item_sizer.max_content_contribution(item)) } MinTrackSizingFunction::Auto => { let space = match axis_available_grid_space { // QUIRK: The spec says that: // // If the grid container is being sized under a min- or max-content constraint, use the items’ limited // min-content contributions in place of their minimum contributions here. // // However, in practice browsers only seem to apply this rule if the item is not a scroll container // (note that overflow:hidden counts as a scroll container), giving the automatic minimum size of scroll // containers (zero) precedence over the min-content contributions. AvailableSpace::MinContent | AvailableSpace::MaxContent if !item.overflow.get(axis).is_scroll_container() => { let axis_minimum_size = item_sizer.minimum_contribution(item, axis_tracks); let axis_min_content_size = item_sizer.min_content_contribution(item); let limit = track.max_track_sizing_function.definite_limit(axis_inner_node_size); axis_min_content_size.maybe_min(limit).max(axis_minimum_size) } _ => item_sizer.minimum_contribution(item, axis_tracks), }; f32_max(track.base_size, space) } MinTrackSizingFunction::Fixed(_) => { // Do nothing as it's not an intrinsic track sizing function track.base_size } }; let track = &mut axis_tracks[track_index as usize]; track.base_size = new_base_size; // Handle growth limits if let MaxTrackSizingFunction::FitContent(_) = track.max_track_sizing_function { // If item is not a scroll container, then increase the growth limit to at least the // size of the min-content contribution if !item.overflow.get(axis).is_scroll_container() { let min_content_contribution = item_sizer.min_content_contribution(item); track.growth_limit_planned_increase = f32_max(track.growth_limit_planned_increase, min_content_contribution); } // Always increase the growth limit to at least the size of the *fit-content limited* // max-cotent contribution let fit_content_limit = track.fit_content_limit(axis_inner_node_size); let max_content_contribution = f32_min(item_sizer.max_content_contribution(item), fit_content_limit); track.growth_limit_planned_increase = f32_max(track.growth_limit_planned_increase, max_content_contribution); } else if track.max_track_sizing_function.is_max_content_alike() || track.max_track_sizing_function.uses_percentage() && axis_inner_node_size.is_none() { // If the container size is indefinite and has not yet been resolved then percentage sized // tracks should be treated as auto (this matches Chrome's behaviour and seems sensible) track.growth_limit_planned_increase = f32_max(track.growth_limit_planned_increase, item_sizer.max_content_contribution(item)); } else if track.max_track_sizing_function.is_intrinsic() { track.growth_limit_planned_increase = f32_max(track.growth_limit_planned_increase, item_sizer.min_content_contribution(item)); } } for track in axis_tracks.iter_mut() { if track.growth_limit_planned_increase > 0.0 { track.growth_limit = if track.growth_limit == f32::INFINITY { track.growth_limit_planned_increase } else { f32_max(track.growth_limit, track.growth_limit_planned_increase) }; } track.infinitely_growable = false; track.growth_limit_planned_increase = 0.0; if track.growth_limit < track.base_size { track.growth_limit = track.base_size; } } continue; } let use_flex_factor_for_distribution = is_flex && flex_factor_sum != 0.0; // 1. For intrinsic minimums: // First increase the base size of tracks with an intrinsic min track sizing function let has_intrinsic_min_track_sizing_function = move |track: &GridTrack| track.min_track_sizing_function.definite_value(axis_inner_node_size).is_none(); for item in batch.iter_mut().filter(|item| item.crosses_intrinsic_track(axis)) { // ...by distributing extra space as needed to accommodate these items’ minimum contributions. // // QUIRK: The spec says that: // // If the grid container is being sized under a min- or max-content constraint, use the items’ limited min-content contributions // in place of their minimum contributions here. // // However, in practice browsers only seem to apply this rule if the item is not a scroll container (note that overflow:hidden counts as // a scroll container), giving the automatic minimum size of scroll containers (zero) precedence over the min-content contributions. let space = match axis_available_grid_space { AvailableSpace::MinContent | AvailableSpace::MaxContent if !item.overflow.get(axis).is_scroll_container() => { let axis_minimum_size = item_sizer.minimum_contribution(item, axis_tracks); let axis_min_content_size = item_sizer.min_content_contribution(item); let limit = item.spanned_track_limit(axis, axis_tracks, axis_inner_node_size); axis_min_content_size.maybe_min(limit).max(axis_minimum_size) } _ => item_sizer.minimum_contribution(item, axis_tracks), }; let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; if space > 0.0 { if item.overflow.get(axis).is_scroll_container() { let fit_content_limit = move |track: &GridTrack| track.fit_content_limited_growth_limit(axis_inner_node_size); distribute_item_space_to_base_size( is_flex, use_flex_factor_for_distribution, space, tracks, has_intrinsic_min_track_sizing_function, fit_content_limit, IntrinsicContributionType::Minimum, ); } else { distribute_item_space_to_base_size( is_flex, use_flex_factor_for_distribution, space, tracks, has_intrinsic_min_track_sizing_function, |track| track.growth_limit, IntrinsicContributionType::Minimum, ); } } } flush_planned_base_size_increases(axis_tracks); // 2. For content-based minimums: // Next continue to increase the base size of tracks with a min track sizing function of min-content or max-content // by distributing extra space as needed to account for these items' min-content contributions. let has_min_or_max_content_min_track_sizing_function = move |track: &GridTrack| { use MinTrackSizingFunction::{MaxContent, MinContent}; matches!(track.min_track_sizing_function, MinContent | MaxContent) }; for item in batch.iter_mut() { let space = item_sizer.min_content_contribution(item); let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; if space > 0.0 { if item.overflow.get(axis).is_scroll_container() { let fit_content_limit = move |track: &GridTrack| track.fit_content_limited_growth_limit(axis_inner_node_size); distribute_item_space_to_base_size( is_flex, use_flex_factor_for_distribution, space, tracks, has_min_or_max_content_min_track_sizing_function, fit_content_limit, IntrinsicContributionType::Minimum, ); } else { distribute_item_space_to_base_size( is_flex, use_flex_factor_for_distribution, space, tracks, has_min_or_max_content_min_track_sizing_function, |track| track.growth_limit, IntrinsicContributionType::Minimum, ); } } } flush_planned_base_size_increases(axis_tracks); // 3. For max-content minimums: // If the grid container is being sized under a max-content constraint, continue to increase the base size of tracks with // a min track sizing function of auto or max-content by distributing extra space as needed to account for these items' // limited max-content contributions. // Define fit_content_limited_growth_limit function. This is passed to the distribute_space_up_to_limits // helper function, and is used to compute the limit to distribute up to for each track. // Wrapping the method on GridTrack is necessary in order to resolve percentage fit-content arguments. if axis_available_grid_space == AvailableSpace::MaxContent { /// Whether a track: /// - has an Auto MIN track sizing function /// - Does not have a MinContent MAX track sizing function /// /// The latter condition was added in order to match Chrome. But I believe it is due to the provision /// under minmax here https://www.w3.org/TR/css-grid-1/#track-sizes which states that: /// /// "If the max is less than the min, then the max will be floored by the min (essentially yielding minmax(min, min))" #[inline(always)] fn has_auto_min_track_sizing_function(track: &GridTrack) -> bool { track.min_track_sizing_function == MinTrackSizingFunction::Auto && track.max_track_sizing_function != MaxTrackSizingFunction::MinContent } /// Whether a track has a MaxContent min track sizing function #[inline(always)] fn has_max_content_min_track_sizing_function(track: &GridTrack) -> bool { track.min_track_sizing_function == MinTrackSizingFunction::MaxContent } for item in batch.iter_mut() { let axis_max_content_size = item_sizer.max_content_contribution(item); let limit = item.spanned_track_limit(axis, axis_tracks, axis_inner_node_size); let space = axis_max_content_size.maybe_min(limit); let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; if space > 0.0 { // If any of the tracks spanned by the item have a MaxContent min track sizing function then // distribute space only to those tracks. Otherwise distribute space to tracks with an Auto min // track sizing function. // // Note: this prioritisation of MaxContent over Auto is not mentioned in the spec (which suggests that // we ought to distribute space evenly between MaxContent and Auto tracks). But it is implemented like // this in both Chrome and Firefox (and it does have a certain logic to it), so we implement it too for // compatibility. // // See: https://www.w3.org/TR/css-grid-1/#track-size-max-content-min if tracks.iter().any(has_max_content_min_track_sizing_function) { distribute_item_space_to_base_size( is_flex, use_flex_factor_for_distribution, space, tracks, has_max_content_min_track_sizing_function, |_| f32::INFINITY, IntrinsicContributionType::Maximum, ); } else { let fit_content_limited_growth_limit = move |track: &GridTrack| track.fit_content_limited_growth_limit(axis_inner_node_size); distribute_item_space_to_base_size( is_flex, use_flex_factor_for_distribution, space, tracks, has_auto_min_track_sizing_function, fit_content_limited_growth_limit, IntrinsicContributionType::Maximum, ); } } } flush_planned_base_size_increases(axis_tracks); } // In all cases, continue to increase the base size of tracks with a min track sizing function of max-content by distributing // extra space as needed to account for these items' max-content contributions. let has_max_content_min_track_sizing_function = move |track: &GridTrack| matches!(track.min_track_sizing_function, MinTrackSizingFunction::MaxContent); for item in batch.iter_mut() { let axis_max_content_size = item_sizer.max_content_contribution(item); let space = axis_max_content_size; let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; if space > 0.0 { distribute_item_space_to_base_size( is_flex, use_flex_factor_for_distribution, space, tracks, has_max_content_min_track_sizing_function, |track| track.growth_limit, IntrinsicContributionType::Maximum, ); } } flush_planned_base_size_increases(axis_tracks); // 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. for track in axis_tracks.iter_mut() { if track.growth_limit < track.base_size { track.growth_limit = track.base_size; } } // If a track is a flexible track, then it has flexible max track sizing function // It cannot also have an intrinsic max track sizing function, so these steps do not apply. if !is_flex { // 5. For intrinsic maximums: Next increase the growth limit of tracks with an intrinsic max track sizing function by // distributing extra space as needed to account for these items' min-content contributions. let has_intrinsic_max_track_sizing_function = move |track: &GridTrack| track.max_track_sizing_function.definite_value(axis_inner_node_size).is_none(); for item in batch.iter_mut() { let axis_min_content_size = item_sizer.min_content_contribution(item); let space = axis_min_content_size; let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; if space > 0.0 { distribute_item_space_to_growth_limit( space, tracks, has_intrinsic_max_track_sizing_function, inner_node_size.get(axis), ); } } // Mark any tracks whose growth limit changed from infinite to finite in this step as infinitely growable for the next step. flush_planned_growth_limit_increases(axis_tracks, true); // 6. For max-content maximums: Lastly continue to increase the growth limit of tracks with a max track sizing function of max-content // by distributing extra space as needed to account for these items' max-content contributions. However, limit the growth of any // fit-content() tracks by their fit-content() argument. let has_max_content_max_track_sizing_function = |track: &GridTrack| { track.max_track_sizing_function.is_max_content_alike() || (track.max_track_sizing_function.uses_percentage() && axis_inner_node_size.is_none()) }; for item in batch.iter_mut() { let axis_max_content_size = item_sizer.max_content_contribution(item); let space = axis_max_content_size; let tracks = &mut axis_tracks[item.track_range_excluding_lines(axis)]; if space > 0.0 { distribute_item_space_to_growth_limit( space, tracks, has_max_content_max_track_sizing_function, inner_node_size.get(axis), ); } } // Mark any tracks whose growth limit changed from infinite to finite in this step as infinitely growable for the next step. flush_planned_growth_limit_increases(axis_tracks, false); } } // Step 5. If any track still has an infinite growth limit (because, for example, it had no items placed // in it or it is a flexible track), set its growth limit to its base size. // NOTE: this step is super-important to ensure that the "Maximise Tracks" step doesn't affect flexible tracks axis_tracks .iter_mut() .filter(|track| track.growth_limit == f32::INFINITY) .for_each(|track| track.growth_limit = track.base_size); } /// 11.5.1. Distributing Extra Space Across Spanned Tracks /// https://www.w3.org/TR/css-grid-1/#extra-space #[inline(always)] fn distribute_item_space_to_base_size( is_flex: bool, use_flex_factor_for_distribution: bool, space: f32, tracks: &mut [GridTrack], track_is_affected: impl Fn(&GridTrack) -> bool, track_limit: impl Fn(&GridTrack) -> f32, intrinsic_contribution_type: IntrinsicContributionType, ) { if is_flex { let filter = |track: &GridTrack| track.is_flexible() && track_is_affected(track); if use_flex_factor_for_distribution { distribute_item_space_to_base_size_inner( space, tracks, filter, |track| track.flex_factor(), track_limit, intrinsic_contribution_type, ) } else { distribute_item_space_to_base_size_inner( space, tracks, filter, |_| 1.0, track_limit, intrinsic_contribution_type, ) } } else { distribute_item_space_to_base_size_inner( space, tracks, track_is_affected, |_| 1.0, track_limit, intrinsic_contribution_type, ) } /// Inner function that doesn't account for differences due to distributing to flex items /// This difference is handled by the closure passed in above fn distribute_item_space_to_base_size_inner( space: f32, tracks: &mut [GridTrack], track_is_affected: impl Fn(&GridTrack) -> bool, track_distribution_proportion: impl Fn(&GridTrack) -> f32, track_limit: impl Fn(&GridTrack) -> f32, intrinsic_contribution_type: IntrinsicContributionType, ) { // Skip this distribution if there is either // - no space to distribute // - no affected tracks to distribute space to if space == 0.0 || !tracks.iter().any(&track_is_affected) { return; } // Define get_base_size function. This is passed to the distribute_space_up_to_limits helper function // to indicate that it is the base size that is being distributed to. let get_base_size = |track: &GridTrack| track.base_size; // 1. Find the space to distribute let track_sizes: f32 = tracks.iter().map(|track| track.base_size).sum(); let extra_space: f32 = f32_max(0.0, space - track_sizes); // 2. Distribute space up to limits: // Note: there are two exit conditions to this loop: // - We run out of space to distribute (extra_space falls below THRESHOLD) // - We run out of growable tracks to distribute to /// Define a small constant to avoid infinite loops due to rounding errors. Rather than stopping distributing /// extra space when it gets to exactly zero, we will stop when it falls below this amount const THRESHOLD: f32 = 0.000001; let extra_space = distribute_space_up_to_limits( extra_space, tracks, &track_is_affected, &track_distribution_proportion, get_base_size, &track_limit, ); // 3. Distribute remaining span beyond limits (if any) if extra_space > THRESHOLD { // When accommodating minimum contributions or accommodating min-content contributions: // - any affected track that happens to also have an intrinsic max track sizing function; // When accommodating max-content contributions: // - any affected track that happens to also have a max-content max track sizing function let mut filter = match intrinsic_contribution_type { IntrinsicContributionType::Minimum => { (|track: &GridTrack| track.max_track_sizing_function.is_intrinsic()) as fn(&GridTrack) -> bool } IntrinsicContributionType::Maximum => { use MaxTrackSizingFunction::{FitContent, MaxContent}; (|track: &GridTrack| { matches!(track.max_track_sizing_function, MaxContent | FitContent(_)) || track.min_track_sizing_function == MinTrackSizingFunction::MaxContent }) as fn(&GridTrack) -> bool } }; // If there are no such tracks (matching filter above), then use all affected tracks. let number_of_tracks = tracks.iter().filter(|track| track_is_affected(track)).filter(|track| filter(track)).count(); if number_of_tracks == 0 { filter = (|_| true) as fn(&GridTrack) -> bool; } distribute_space_up_to_limits( extra_space, tracks, filter, &track_distribution_proportion, get_base_size, &track_limit, // Should apply only fit-content limit here? ); } // 4. For each affected track, if the track’s item-incurred increase is larger than the track’s planned increase // set the track’s planned increase to that value. for track in tracks.iter_mut() { if track.item_incurred_increase > track.base_size_planned_increase { track.base_size_planned_increase = track.item_incurred_increase; } // Reset the item_incurresed increase ready for the next space distribution track.item_incurred_increase = 0.0; } } } /// 11.5.1. Distributing Extra Space Across Spanned Tracks /// This is simplified (and faster) version of the algorithm for growth limits /// https://www.w3.org/TR/css-grid-1/#extra-space fn distribute_item_space_to_growth_limit( space: f32, tracks: &mut [GridTrack], track_is_affected: impl Fn(&GridTrack) -> bool, axis_inner_node_size: Option, ) { // Skip this distribution if there is either // - no space to distribute // - no affected tracks to distribute space to if space == 0.0 || tracks.iter().filter(|track| track_is_affected(track)).count() == 0 { return; } // 1. Find the space to distribute let track_sizes: f32 = tracks .iter() .map(|track| if track.growth_limit == f32::INFINITY { track.base_size } else { track.growth_limit }) .sum(); let extra_space: f32 = f32_max(0.0, space - track_sizes); // 2. Distribute space up to limits: // For growth limits the limit is either Infinity, or the growth limit itself. Which means that: // - If there are any tracks with infinite limits then all space will be distributed to those track(s). // - Otherwise no space will be distributed as part of this step let number_of_growable_tracks = tracks .iter() .filter(|track| track_is_affected(track)) .filter(|track| { track.infinitely_growable || track.fit_content_limited_growth_limit(axis_inner_node_size) == f32::INFINITY }) .count(); if number_of_growable_tracks > 0 { let item_incurred_increase = extra_space / number_of_growable_tracks as f32; for track in tracks.iter_mut().filter(|track| track_is_affected(track)).filter(|track| { track.infinitely_growable || track.fit_content_limited_growth_limit(axis_inner_node_size) == f32::INFINITY }) { track.item_incurred_increase = item_incurred_increase; } } else { // 3. Distribute space beyond limits // If space remains after all tracks are frozen, unfreeze and continue to distribute space to the item-incurred increase // ...when handling any intrinsic growth limit: all affected tracks. distribute_space_up_to_limits( extra_space, tracks, track_is_affected, |_| 1.0, |track| if track.growth_limit == f32::INFINITY { track.base_size } else { track.growth_limit }, move |track| track.fit_content_limit(axis_inner_node_size), ); }; // 4. For each affected track, if the track’s item-incurred increase is larger than the track’s planned increase // set the track’s planned increase to that value. for track in tracks.iter_mut() { if track.item_incurred_increase > track.growth_limit_planned_increase { track.growth_limit_planned_increase = track.item_incurred_increase; } // Reset the item_incurresed increase ready for the next space distribution track.item_incurred_increase = 0.0; } } /// 11.6 Maximise Tracks /// Distributes free space (if any) to tracks with FINITE growth limits, up to their limits. #[inline(always)] fn maximise_tracks( axis_tracks: &mut [GridTrack], axis_inner_node_size: Option, axis_available_grid_space: AvailableSpace, ) { let used_space: f32 = axis_tracks.iter().map(|track| track.base_size).sum(); let free_space = axis_available_grid_space.compute_free_space(used_space); if free_space == f32::INFINITY { axis_tracks.iter_mut().for_each(|track| track.base_size = track.growth_limit); } else if free_space > 0.0 { distribute_space_up_to_limits( free_space, axis_tracks, |_| true, |_| 1.0, |track| track.base_size, move |track: &GridTrack| track.fit_content_limited_growth_limit(axis_inner_node_size), ); for track in axis_tracks.iter_mut() { track.base_size += track.item_incurred_increase; track.item_incurred_increase = 0.0; } } } /// 11.7. Expand Flexible Tracks /// This step sizes flexible tracks using the largest value it can assign to an fr without exceeding the available space. #[allow(clippy::too_many_arguments)] #[inline(always)] fn expand_flexible_tracks( tree: &mut impl LayoutPartialTree, axis: AbstractAxis, axis_tracks: &mut [GridTrack], items: &mut [GridItem], axis_min_size: Option, axis_max_size: Option, axis_available_space_for_expansion: AvailableSpace, inner_node_size: Size>, ) { // First, find the grid’s used flex fraction: let flex_fraction = match axis_available_space_for_expansion { // If the free space is zero: // The used flex fraction is zero. // Otherwise, if the free space is a definite length: // The used flex fraction is the result of finding the size of an fr using all of the grid tracks and // a space to fill of the available grid space. AvailableSpace::Definite(available_space) => { let used_space: f32 = axis_tracks.iter().map(|track| track.base_size).sum(); let free_space = available_space - used_space; if free_space <= 0.0 { 0.0 } else { find_size_of_fr(axis_tracks, available_space) } } // If ... sizing the grid container under a min-content constraint the used flex fraction is zero. AvailableSpace::MinContent => 0.0, // Otherwise, if the free space is an indefinite length: AvailableSpace::MaxContent => { // The used flex fraction is the maximum of: let flex_fraction = f32_max( // For each flexible track, if the flexible track’s flex factor is greater than one, // the result of dividing the track’s base size by its flex factor; otherwise, the track’s base size. axis_tracks .iter() .filter(|track| track.max_track_sizing_function.is_flexible()) .map(|track| { let flex_factor = track.flex_factor(); if flex_factor > 1.0 { track.base_size / flex_factor } else { track.base_size } }) .max_by(|a, b| a.total_cmp(b)) .unwrap_or(0.0), // For each grid item that crosses a flexible track, the result of finding the size of an fr using all the grid tracks // that the item crosses and a space to fill of the item’s max-content contribution. items .iter_mut() .filter(|item| item.crosses_flexible_track(axis)) .map(|item| { let tracks = &axis_tracks[item.track_range_excluding_lines(axis)]; // TODO: plumb estimate of other axis size (known_dimensions) in here rather than just passing Size::NONE? let max_content_contribution = item.max_content_contribution_cached(axis, tree, Size::NONE, inner_node_size); find_size_of_fr(tracks, max_content_contribution) }) .max_by(|a, b| a.total_cmp(b)) .unwrap_or(0.0), ); // If using this flex fraction would cause the grid to be smaller than the grid container’s min-width/height (or larger than the // grid container’s max-width/height), then redo this step, treating the free space as definite and the available grid space as equal // to the grid container’s inner size when it’s sized to its min-width/height (max-width/height). // (Note: min_size takes precedence over max_size) let hypothetical_grid_size: f32 = axis_tracks .iter() .map(|track| match track.max_track_sizing_function { MaxTrackSizingFunction::Fraction(track_flex_factor) => { f32_max(track.base_size, track_flex_factor * flex_fraction) } _ => track.base_size, }) .sum(); let axis_min_size = axis_min_size.unwrap_or(0.0); let axis_max_size = axis_max_size.unwrap_or(f32::INFINITY); if hypothetical_grid_size < axis_min_size { find_size_of_fr(axis_tracks, axis_min_size) } else if hypothetical_grid_size > axis_max_size { find_size_of_fr(axis_tracks, axis_max_size) } else { flex_fraction } } }; // For each flexible track, if the product of the used flex fraction and the track’s flex factor is greater // than the track’s base size, set its base size to that product. for track in axis_tracks.iter_mut() { if let MaxTrackSizingFunction::Fraction(track_flex_factor) = track.max_track_sizing_function { track.base_size = f32_max(track.base_size, track_flex_factor * flex_fraction); } } } /// 11.7.1. Find the Size of an fr /// This algorithm finds the largest size that an fr unit can be without exceeding the target size. /// It must be called with a set of grid tracks and some quantity of space to fill. #[inline(always)] fn find_size_of_fr(tracks: &[GridTrack], space_to_fill: f32) -> f32 { // Handle the trivial case where there is no space to fill // Do not remove as otherwise the loop below will loop infinitely if space_to_fill == 0.0 { return 0.0; } // If the product of the hypothetical fr size (computed below) and any flexible track’s flex factor // is less than the track’s base size, then we must restart this algorithm treating all such tracks as inflexible. // We therefore wrap the entire algorithm in a loop, with an hypothetical_fr_size of INFINITY such that the above // condition can never be true for the first iteration. let mut hypothetical_fr_size = f32::INFINITY; let mut previous_iter_hypothetical_fr_size; loop { // Let leftover space be the space to fill minus the base sizes of the non-flexible grid tracks. // 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. // We compute both of these in a single loop to avoid iterating over the data twice let mut used_space = 0.0; let mut naive_flex_factor_sum = 0.0; for track in tracks.iter() { match track.max_track_sizing_function { // Tracks for which flex_factor * hypothetical_fr_size < track.base_size are treated as inflexible MaxTrackSizingFunction::Fraction(flex_factor) if flex_factor * hypothetical_fr_size >= track.base_size => { naive_flex_factor_sum += flex_factor; } _ => used_space += track.base_size, }; } let leftover_space = space_to_fill - used_space; let flex_factor = f32_max(naive_flex_factor_sum, 1.0); // Let the hypothetical fr size be the leftover space divided by the flex factor sum. previous_iter_hypothetical_fr_size = hypothetical_fr_size; hypothetical_fr_size = leftover_space / flex_factor; // If the product of the hypothetical fr size and a flexible track’s flex factor is less than the track’s base size, // restart this algorithm treating all such tracks as inflexible. // We keep track of the hypothetical_fr_size let hypothetical_fr_size_is_valid = tracks.iter().all(|track| match track.max_track_sizing_function { MaxTrackSizingFunction::Fraction(flex_factor) => { flex_factor * hypothetical_fr_size >= track.base_size || flex_factor * previous_iter_hypothetical_fr_size < track.base_size } _ => true, }); if hypothetical_fr_size_is_valid { break; } } // Return the hypothetical fr size. hypothetical_fr_size } /// 11.8. Stretch auto Tracks /// This step expands tracks that have an auto max track sizing function by dividing any remaining positive, definite free space equally amongst them. #[inline(always)] fn stretch_auto_tracks( axis_tracks: &mut [GridTrack], axis_min_size: Option, axis_available_space_for_expansion: AvailableSpace, ) { let num_auto_tracks = axis_tracks.iter().filter(|track| track.max_track_sizing_function == MaxTrackSizingFunction::Auto).count(); if num_auto_tracks > 0 { let used_space: f32 = axis_tracks.iter().map(|track| track.base_size).sum(); // If the free space is indefinite, but the grid container has a definite min-width/height // use that size to calculate the free space for this step instead. let free_space = if axis_available_space_for_expansion.is_definite() { axis_available_space_for_expansion.compute_free_space(used_space) } else { match axis_min_size { Some(size) => size - used_space, None => 0.0, } }; if free_space > 0.0 { let extra_space_per_auto_track = free_space / num_auto_tracks as f32; axis_tracks .iter_mut() .filter(|track| track.max_track_sizing_function == MaxTrackSizingFunction::Auto) .for_each(|track| track.base_size += extra_space_per_auto_track); } } } /// Helper function for distributing space to tracks evenly /// Used by both distribute_item_space_to_base_size and maximise_tracks steps #[inline(always)] fn distribute_space_up_to_limits( space_to_distribute: f32, tracks: &mut [GridTrack], track_is_affected: impl Fn(&GridTrack) -> bool, track_distribution_proportion: impl Fn(&GridTrack) -> f32, track_affected_property: impl Fn(&GridTrack) -> f32, track_limit: impl Fn(&GridTrack) -> f32, ) -> f32 { /// Define a small constant to avoid infinite loops due to rounding errors. Rather than stopping distributing /// extra space when it gets to exactly zero, we will stop when it falls below this amount const THRESHOLD: f32 = 0.01; let mut space_to_distribute = space_to_distribute; while space_to_distribute > THRESHOLD { let track_distribution_proportion_sum: f32 = tracks .iter() .filter(|track| track_affected_property(track) + track.item_incurred_increase < track_limit(track)) .filter(|track| track_is_affected(track)) .map(&track_distribution_proportion) .sum(); if track_distribution_proportion_sum == 0.0 { break; } // Compute item-incurred increase for this iteration let min_increase_limit = tracks .iter() .filter(|track| track_affected_property(track) + track.item_incurred_increase < track_limit(track)) .filter(|track| track_is_affected(track)) .map(|track| (track_limit(track) - track_affected_property(track)) / track_distribution_proportion(track)) .min_by(|a, b| a.total_cmp(b)) .unwrap(); // We will never pass an empty track list to this function let iteration_item_incurred_increase = f32_min(min_increase_limit, space_to_distribute / track_distribution_proportion_sum); for track in tracks.iter_mut().filter(|track| track_is_affected(track)) { let increase = iteration_item_incurred_increase * track_distribution_proportion(track); if increase > 0.0 && track_affected_property(track) + increase <= track_limit(track) + THRESHOLD { track.item_incurred_increase += increase; space_to_distribute -= increase; } } } space_to_distribute }