tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 520HTML 244Markdown 7TOML 41339 Definitions

⎇ aad4f024eaed6abb769426720eac588c9e7bd301 src/compute/flexbox.rs Rust · 2211 lines · 102.6 KB History Who touched what raw

fix: single-line flex-container should clamp the line's cross-size (#638) aad4f024e · RomChung ·

1//! Computes the [flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) layout algorithm on [`TaffyTree`](crate::TaffyTree) according to the [spec](https://www.w3.org/TR/css-flexbox-1/)
2use crate::compute::common::alignment::compute_alignment_offset;
3use crate::geometry::{Line, Point, Rect, Size};
4use crate::style::{
5    AlignContent, AlignItems, AlignSelf, AvailableSpace, Dimension, Display, FlexWrap, JustifyContent,
6    LengthPercentageAuto, Overflow, Position,
7};
8use crate::style::{FlexDirection, Style};
9use crate::style_helpers::{TaffyMaxContent, TaffyMinContent};
10use crate::tree::{Layout, LayoutInput, LayoutOutput, RunMode, SizingMode};
11use crate::tree::{LayoutPartialTree, LayoutPartialTreeExt, NodeId};
12use crate::util::debug::debug_log;
13use crate::util::sys::{f32_max, new_vec_with_capacity, Vec};
14use crate::util::MaybeMath;
15use crate::util::{MaybeResolve, ResolveOrZero};
16
17#[cfg(feature = "content_size")]
18use super::common::content_size::compute_content_size_contribution;
19
20/// The intermediate results of a flexbox calculation for a single item
21struct FlexItem {
22    /// The identifier for the associated node
23    node: NodeId,
24
25    /// The order of the node relative to it's siblings
26    order: u32,
27
28    /// The base size of this item
29    size: Size<Option<f32>>,
30    /// The minimum allowable size of this item
31    min_size: Size<Option<f32>>,
32    /// The maximum allowable size of this item
33    max_size: Size<Option<f32>>,
34    /// The cross-alignment of this item
35    align_self: AlignSelf,
36
37    /// The overflow style of the item
38    overflow: Point<Overflow>,
39    /// The width of the scrollbars (if it has any)
40    scrollbar_width: f32,
41    /// The flex shrink style of the item
42    flex_shrink: f32,
43    /// The flex grow style of the item
44    flex_grow: f32,
45
46    /// The minimum size of the item. This differs from min_size above because it also
47    /// takes into account content based automatic minimum sizes
48    resolved_minimum_main_size: f32,
49
50    /// The final offset of this item
51    inset: Rect<Option<f32>>,
52    /// The margin of this item
53    margin: Rect<f32>,
54    /// Whether each margin is an auto margin or not
55    margin_is_auto: Rect<bool>,
56    /// The padding of this item
57    padding: Rect<f32>,
58    /// The border of this item
59    border: Rect<f32>,
60
61    /// The default size of this item
62    flex_basis: f32,
63    /// The default size of this item, minus padding and border
64    inner_flex_basis: f32,
65    /// The amount by which this item has deviated from its target size
66    violation: f32,
67    /// Is the size of this item locked
68    frozen: bool,
69
70    /// Either the max- or min- content flex fraction
71    /// See https://www.w3.org/TR/css-flexbox-1/#intrinsic-main-sizes
72    content_flex_fraction: f32,
73
74    /// The proposed inner size of this item
75    hypothetical_inner_size: Size<f32>,
76    /// The proposed outer size of this item
77    hypothetical_outer_size: Size<f32>,
78    /// The size that this item wants to be
79    target_size: Size<f32>,
80    /// The size that this item wants to be, plus any padding and border
81    outer_target_size: Size<f32>,
82
83    /// The position of the bottom edge of this item
84    baseline: f32,
85
86    /// A temporary value for the main offset
87    ///
88    /// Offset is the relative position from the item's natural flow position based on
89    /// relative position values, alignment, and justification. Does not include margin/padding/border.
90    offset_main: f32,
91    /// A temporary value for the cross offset
92    ///
93    /// Offset is the relative position from the item's natural flow position based on
94    /// relative position values, alignment, and justification. Does not include margin/padding/border.
95    offset_cross: f32,
96}
97
98/// A line of [`FlexItem`] used for intermediate computation
99struct FlexLine<'a> {
100    /// The slice of items to iterate over during computation of this line
101    items: &'a mut [FlexItem],
102    /// The dimensions of the cross-axis
103    cross_size: f32,
104    /// The relative offset of the cross-axis
105    offset_cross: f32,
106}
107
108/// Values that can be cached during the flexbox algorithm
109struct AlgoConstants {
110    /// The direction of the current segment being laid out
111    dir: FlexDirection,
112    /// Is this segment a row
113    is_row: bool,
114    /// Is this segment a column
115    is_column: bool,
116    /// Is wrapping enabled (in either direction)
117    is_wrap: bool,
118    /// Is the wrap direction inverted
119    is_wrap_reverse: bool,
120
121    /// The item's min_size style
122    min_size: Size<Option<f32>>,
123    /// The item's max_size style
124    max_size: Size<Option<f32>>,
125    /// The margin of this section
126    margin: Rect<f32>,
127    /// The border of this section
128    border: Rect<f32>,
129    /// The space between the content box and the border box.
130    /// This consists of padding + border + scrollbar_gutter.
131    content_box_inset: Rect<f32>,
132    /// The size reserved for scrollbar gutters in each axis
133    scrollbar_gutter: Point<f32>,
134    /// The gap of this section
135    gap: Size<f32>,
136    /// The align_items property of this node
137    align_items: AlignItems,
138    /// The align_content property of this node
139    align_content: AlignContent,
140    /// The justify_content property of this node
141    justify_content: Option<JustifyContent>,
142
143    /// The border-box size of the node being laid out (if known)
144    node_outer_size: Size<Option<f32>>,
145    /// The content-box size of the node being laid out (if known)
146    node_inner_size: Size<Option<f32>>,
147
148    /// The size of the virtual container containing the flex items.
149    container_size: Size<f32>,
150    /// The size of the internal container
151    inner_container_size: Size<f32>,
152}
153
154/// Computes the layout of [`LayoutPartialTree`] according to the flexbox algorithm
155pub fn compute_flexbox_layout(tree: &mut impl LayoutPartialTree, node: NodeId, inputs: LayoutInput) -> LayoutOutput {
156    let LayoutInput { known_dimensions, parent_size, run_mode, .. } = inputs;
157    let style = tree.get_style(node);
158
159    // Pull these out earlier to avoid borrowing issues
160    let aspect_ratio = style.aspect_ratio;
161    let min_size = style.min_size.maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio);
162    let max_size = style.max_size.maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio);
163    let clamped_style_size = if inputs.sizing_mode == SizingMode::InherentSize {
164        style.size.maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size)
165    } else {
166        Size::NONE
167    };
168
169    // If both min and max in a given axis are set and max <= min then this determines the size in that axis
170    let min_max_definite_size = min_size.zip_map(max_size, |min, max| match (min, max) {
171        (Some(min), Some(max)) if max <= min => Some(min),
172        _ => None,
173    });
174    let styled_based_known_dimensions = known_dimensions.or(min_max_definite_size).or(clamped_style_size);
175
176    // Short-circuit layout if the container's size is fully determined by the container's size and the run mode
177    // is ComputeSize (and thus the container's size is all that we're interested in)
178    if run_mode == RunMode::ComputeSize {
179        if let Size { width: Some(width), height: Some(height) } = styled_based_known_dimensions {
180            return LayoutOutput::from_outer_size(Size { width, height });
181        }
182    }
183
184    debug_log!("FLEX: single-pass");
185    compute_preliminary(tree, node, LayoutInput { known_dimensions: styled_based_known_dimensions, ..inputs })
186}
187
188/// Compute a preliminary size for an item
189fn compute_preliminary(tree: &mut impl LayoutPartialTree, node: NodeId, inputs: LayoutInput) -> LayoutOutput {
190    let LayoutInput { known_dimensions, parent_size, available_space, run_mode, .. } = inputs;
191
192    // Define some general constants we will need for the remainder of the algorithm.
193    let mut constants = compute_constants(tree.get_style(node), known_dimensions, parent_size);
194
195    // 9. Flex Layout Algorithm
196
197    // 9.1. Initial Setup
198
199    // 1. Generate anonymous flex items as described in §4 Flex Items.
200    debug_log!("generate_anonymous_flex_items");
201    let mut flex_items = generate_anonymous_flex_items(tree, node, &constants);
202
203    // 9.2. Line Length Determination
204
205    // 2. Determine the available main and cross space for the flex items
206    debug_log!("determine_available_space");
207    let available_space = determine_available_space(known_dimensions, available_space, &constants);
208
209    // 3. Determine the flex base size and hypothetical main size of each item.
210    debug_log!("determine_flex_base_size");
211    determine_flex_base_size(tree, &constants, available_space, &mut flex_items);
212
213    #[cfg(feature = "debug")]
214    for item in flex_items.iter() {
215        debug_log!("item.flex_basis", item.flex_basis);
216        debug_log!("item.inner_flex_basis", item.inner_flex_basis);
217        debug_log!("item.hypothetical_outer_size", dbg:item.hypothetical_outer_size);
218        debug_log!("item.hypothetical_inner_size", dbg:item.hypothetical_inner_size);
219        debug_log!("item.resolved_minimum_main_size", dbg:item.resolved_minimum_main_size);
220    }
221
222    // 4. Determine the main size of the flex container
223    // This has already been done as part of compute_constants. The inner size is exposed as constants.node_inner_size.
224
225    // 9.3. Main Size Determination
226
227    // 5. Collect flex items into flex lines.
228    debug_log!("collect_flex_lines");
229    let mut flex_lines = collect_flex_lines(&constants, available_space, &mut flex_items);
230
231    // If container size is undefined, determine the container's main size
232    // and then re-resolve gaps based on newly determined size
233    debug_log!("determine_container_main_size");
234    let original_gap = constants.gap;
235    if let Some(inner_main_size) = constants.node_inner_size.main(constants.dir) {
236        let outer_main_size = inner_main_size + constants.content_box_inset.main_axis_sum(constants.dir);
237        constants.inner_container_size.set_main(constants.dir, inner_main_size);
238        constants.container_size.set_main(constants.dir, outer_main_size);
239    } else {
240        // Sets constants.container_size and constants.outer_container_size
241        determine_container_main_size(tree, available_space, &mut flex_lines, &mut constants);
242        constants.node_inner_size.set_main(constants.dir, Some(constants.inner_container_size.main(constants.dir)));
243        constants.node_outer_size.set_main(constants.dir, Some(constants.container_size.main(constants.dir)));
244
245        debug_log!("constants.node_outer_size", dbg:constants.node_outer_size);
246        debug_log!("constants.node_inner_size", dbg:constants.node_inner_size);
247
248        // Re-resolve percentage gaps
249        let style = tree.get_style(node);
250        let inner_container_size = constants.inner_container_size.main(constants.dir);
251        let new_gap = style.gap.main(constants.dir).maybe_resolve(inner_container_size).unwrap_or(0.0);
252        constants.gap.set_main(constants.dir, new_gap);
253    }
254
255    // 6. Resolve the flexible lengths of all the flex items to find their used main size.
256    debug_log!("resolve_flexible_lengths");
257    for line in &mut flex_lines {
258        resolve_flexible_lengths(line, &constants, original_gap);
259    }
260
261    // 9.4. Cross Size Determination
262
263    // 7. Determine the hypothetical cross size of each item.
264    debug_log!("determine_hypothetical_cross_size");
265    for line in &mut flex_lines {
266        determine_hypothetical_cross_size(tree, line, &constants, available_space);
267    }
268
269    // Calculate child baselines. This function is internally smart and only computes child baselines
270    // if they are necessary.
271    debug_log!("calculate_children_base_lines");
272    calculate_children_base_lines(tree, known_dimensions, available_space, &mut flex_lines, &constants);
273
274    // 8. Calculate the cross size of each flex line.
275    debug_log!("calculate_cross_size");
276    calculate_cross_size(&mut flex_lines, known_dimensions, &constants);
277
278    // 9. Handle 'align-content: stretch'.
279    debug_log!("handle_align_content_stretch");
280    handle_align_content_stretch(&mut flex_lines, known_dimensions, &constants);
281
282    // 10. Collapse visibility:collapse items. If any flex items have visibility: collapse,
283    //     note the cross size of the line they’re in as the item’s strut size, and restart
284    //     layout from the beginning.
285    //
286    //     In this second layout round, when collecting items into lines, treat the collapsed
287    //     items as having zero main size. For the rest of the algorithm following that step,
288    //     ignore the collapsed items entirely (as if they were display:none) except that after
289    //     calculating the cross size of the lines, if any line’s cross size is less than the
290    //     largest strut size among all the collapsed items in the line, set its cross size to
291    //     that strut size.
292    //
293    //     Skip this step in the second layout round.
294
295    // TODO implement once (if ever) we support visibility:collapse
296
297    // 11. Determine the used cross size of each flex item.
298    debug_log!("determine_used_cross_size");
299    determine_used_cross_size(tree, &mut flex_lines, &constants);
300
301    // 9.5. Main-Axis Alignment
302
303    // 12. Distribute any remaining free space.
304    debug_log!("distribute_remaining_free_space");
305    distribute_remaining_free_space(&mut flex_lines, &constants);
306
307    // 9.6. Cross-Axis Alignment
308
309    // 13. Resolve cross-axis auto margins (also includes 14).
310    debug_log!("resolve_cross_axis_auto_margins");
311    resolve_cross_axis_auto_margins(&mut flex_lines, &constants);
312
313    // 15. Determine the flex container’s used cross size.
314    debug_log!("determine_container_cross_size");
315    let total_line_cross_size = determine_container_cross_size(&flex_lines, known_dimensions, &mut constants);
316
317    // We have the container size.
318    // If our caller does not care about performing layout we are done now.
319    if run_mode == RunMode::ComputeSize {
320        return LayoutOutput::from_outer_size(constants.container_size);
321    }
322
323    // 16. Align all flex lines per align-content.
324    debug_log!("align_flex_lines_per_align_content");
325    align_flex_lines_per_align_content(&mut flex_lines, &constants, total_line_cross_size);
326
327    // Do a final layout pass and gather the resulting layouts
328    debug_log!("final_layout_pass");
329    let inflow_content_size = final_layout_pass(tree, &mut flex_lines, &constants);
330
331    // Before returning we perform absolute layout on all absolutely positioned children
332    debug_log!("perform_absolute_layout_on_absolute_children");
333    let absolute_content_size = perform_absolute_layout_on_absolute_children(tree, node, &constants);
334
335    debug_log!("hidden_layout");
336    let len = tree.child_count(node);
337    for order in 0..len {
338        let child = tree.get_child_id(node, order);
339        if tree.get_style(child).display == Display::None {
340            tree.set_unrounded_layout(child, &Layout::with_order(order as u32));
341            tree.perform_child_layout(
342                child,
343                Size::NONE,
344                Size::NONE,
345                Size::MAX_CONTENT,
346                SizingMode::InherentSize,
347                Line::FALSE,
348            );
349        }
350    }
351
352    // 8.5. Flex Container Baselines: calculate the flex container's first baseline
353    // See https://www.w3.org/TR/css-flexbox-1/#flex-baselines
354    let first_vertical_baseline = if flex_lines.is_empty() {
355        None
356    } else {
357        flex_lines[0]
358            .items
359            .iter()
360            .find(|item| constants.is_column || item.align_self == AlignSelf::Baseline)
361            .or_else(|| flex_lines[0].items.iter().next())
362            .map(|child| {
363                let offset_vertical = if constants.is_row { child.offset_cross } else { child.offset_main };
364                offset_vertical + child.baseline
365            })
366    };
367
368    LayoutOutput::from_sizes_and_baselines(
369        constants.container_size,
370        inflow_content_size.f32_max(absolute_content_size),
371        Point { x: None, y: first_vertical_baseline },
372    )
373}
374
375/// Compute constants that can be reused during the flexbox algorithm.
376#[inline]
377fn compute_constants(
378    style: &Style,
379    known_dimensions: Size<Option<f32>>,
380    parent_size: Size<Option<f32>>,
381) -> AlgoConstants {
382    let dir = style.flex_direction;
383    let is_row = dir.is_row();
384    let is_column = dir.is_column();
385    let is_wrap = matches!(style.flex_wrap, FlexWrap::Wrap | FlexWrap::WrapReverse);
386    let is_wrap_reverse = style.flex_wrap == FlexWrap::WrapReverse;
387
388    let aspect_ratio = style.aspect_ratio;
389    let margin = style.margin.resolve_or_zero(parent_size.width);
390    let padding = style.padding.resolve_or_zero(parent_size.width);
391    let border = style.border.resolve_or_zero(parent_size.width);
392    let align_items = style.align_items.unwrap_or(AlignItems::Stretch);
393    let align_content = style.align_content.unwrap_or(AlignContent::Stretch);
394    let justify_content = style.justify_content;
395
396    // Scrollbar gutters are reserved when the `overflow` property is set to `Overflow::Scroll`.
397    // However, the axis are switched (transposed) because a node that scrolls vertically needs
398    // *horizontal* space to be reserved for a scrollbar
399    let scrollbar_gutter = style.overflow.transpose().map(|overflow| match overflow {
400        Overflow::Scroll => style.scrollbar_width,
401        _ => 0.0,
402    });
403    // TODO: make side configurable based on the `direction` property
404    let mut content_box_inset = padding + border;
405    content_box_inset.right += scrollbar_gutter.x;
406    content_box_inset.bottom += scrollbar_gutter.y;
407
408    let node_outer_size = known_dimensions;
409    let node_inner_size = node_outer_size.maybe_sub(content_box_inset.sum_axes());
410    let gap = style.gap.resolve_or_zero(node_inner_size.or(Size::zero()));
411
412    let container_size = Size::zero();
413    let inner_container_size = Size::zero();
414
415    AlgoConstants {
416        dir,
417        is_row,
418        is_column,
419        is_wrap,
420        is_wrap_reverse,
421        min_size: style.min_size.maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio),
422        max_size: style.max_size.maybe_resolve(parent_size).maybe_apply_aspect_ratio(aspect_ratio),
423        margin,
424        border,
425        gap,
426        content_box_inset,
427        scrollbar_gutter,
428        align_items,
429        align_content,
430        justify_content,
431        node_outer_size,
432        node_inner_size,
433        container_size,
434        inner_container_size,
435    }
436}
437
438/// Generate anonymous flex items.
439///
440/// # [9.1. Initial Setup](https://www.w3.org/TR/css-flexbox-1/#box-manip)
441///
442/// - [**Generate anonymous flex items**](https://www.w3.org/TR/css-flexbox-1/#algo-anon-box) as described in [§4 Flex Items](https://www.w3.org/TR/css-flexbox-1/#flex-items).
443#[inline]
444fn generate_anonymous_flex_items(
445    tree: &impl LayoutPartialTree,
446    node: NodeId,
447    constants: &AlgoConstants,
448) -> Vec<FlexItem> {
449    tree.child_ids(node)
450        .enumerate()
451        .map(|(index, child)| (index, child, tree.get_style(child)))
452        .filter(|(_, _, style)| style.position != Position::Absolute)
453        .filter(|(_, _, style)| style.display != Display::None)
454        .map(|(index, child, child_style)| {
455            let aspect_ratio = child_style.aspect_ratio;
456            FlexItem {
457                node: child,
458                order: index as u32,
459                size: child_style.size.maybe_resolve(constants.node_inner_size).maybe_apply_aspect_ratio(aspect_ratio),
460                min_size: child_style
461                    .min_size
462                    .maybe_resolve(constants.node_inner_size)
463                    .maybe_apply_aspect_ratio(aspect_ratio),
464                max_size: child_style
465                    .max_size
466                    .maybe_resolve(constants.node_inner_size)
467                    .maybe_apply_aspect_ratio(aspect_ratio),
468
469                inset: child_style.inset.zip_size(constants.node_inner_size, |p, s| p.maybe_resolve(s)),
470                margin: child_style.margin.resolve_or_zero(constants.node_inner_size.width),
471                margin_is_auto: child_style.margin.map(|m| m == LengthPercentageAuto::Auto),
472                padding: child_style.padding.resolve_or_zero(constants.node_inner_size.width),
473                border: child_style.border.resolve_or_zero(constants.node_inner_size.width),
474                align_self: child_style.align_self.unwrap_or(constants.align_items),
475                overflow: child_style.overflow,
476                scrollbar_width: child_style.scrollbar_width,
477                flex_grow: child_style.flex_grow,
478                flex_shrink: child_style.flex_shrink,
479                flex_basis: 0.0,
480                inner_flex_basis: 0.0,
481                violation: 0.0,
482                frozen: false,
483
484                resolved_minimum_main_size: 0.0,
485                hypothetical_inner_size: Size::zero(),
486                hypothetical_outer_size: Size::zero(),
487                target_size: Size::zero(),
488                outer_target_size: Size::zero(),
489                content_flex_fraction: 0.0,
490
491                baseline: 0.0,
492
493                offset_main: 0.0,
494                offset_cross: 0.0,
495            }
496        })
497        .collect()
498}
499
500/// Determine the available main and cross space for the flex items.
501///
502/// # [9.2. Line Length Determination](https://www.w3.org/TR/css-flexbox-1/#line-sizing)
503///
504/// - [**Determine the available main and cross space for the flex items**](https://www.w3.org/TR/css-flexbox-1/#algo-available).
505/// For each dimension, if that dimension of the flex container’s content box is a definite size, use that;
506/// if that dimension of the flex container is being sized under a min or max-content constraint, the available space in that dimension is that constraint;
507/// otherwise, subtract the flex container’s margin, border, and padding from the space available to the flex container in that dimension and use that value.
508/// **This might result in an infinite value**.
509#[inline]
510#[must_use]
511fn determine_available_space(
512    known_dimensions: Size<Option<f32>>,
513    outer_available_space: Size<AvailableSpace>,
514    constants: &AlgoConstants,
515) -> Size<AvailableSpace> {
516    // Note: min/max/preferred size styles have already been applied to known_dimensions in the `compute` function above
517    let width = match known_dimensions.width {
518        Some(node_width) => AvailableSpace::Definite(node_width - constants.content_box_inset.horizontal_axis_sum()),
519        None => outer_available_space
520            .width
521            .maybe_sub(constants.margin.horizontal_axis_sum())
522            .maybe_sub(constants.content_box_inset.horizontal_axis_sum()),
523    };
524
525    let height = match known_dimensions.height {
526        Some(node_height) => AvailableSpace::Definite(node_height - constants.content_box_inset.vertical_axis_sum()),
527        None => outer_available_space
528            .height
529            .maybe_sub(constants.margin.vertical_axis_sum())
530            .maybe_sub(constants.content_box_inset.vertical_axis_sum()),
531    };
532
533    Size { width, height }
534}
535
536/// Determine the flex base size and hypothetical main size of each item.
537///
538/// # [9.2. Line Length Determination](https://www.w3.org/TR/css-flexbox-1/#line-sizing)
539///
540/// - [**Determine the flex base size and hypothetical main size of each item:**](https://www.w3.org/TR/css-flexbox-1/#algo-main-item)
541///
542///     - A. If the item has a definite used flex basis, that’s the flex base size.
543///
544///     - B. If the flex item has ...
545///
546///         - an intrinsic aspect ratio,
547///         - a used flex basis of content, and
548///         - a definite cross size,
549///
550///     then the flex base size is calculated from its inner cross size and the flex item’s intrinsic aspect ratio.
551///
552///     - C. If the used flex basis is content or depends on its available space, and the flex container is being sized under a min-content
553///         or max-content constraint (e.g. when performing automatic table layout \[CSS21\]), size the item under that constraint.
554///         The flex base size is the item’s resulting main size.
555///
556///     - E. Otherwise, size the item into the available space using its used flex basis in place of its main size, treating a value of content as max-content.
557///         If a cross size is needed to determine the main size (e.g. when the flex item’s main size is in its block axis) and the flex item’s cross size is auto and not definite,
558///         in this calculation use fit-content as the flex item’s cross size. The flex base size is the item’s resulting main size.
559///
560///     When determining the flex base size, the item’s min and max main sizes are ignored (no clamping occurs).
561///     Furthermore, the sizing calculations that floor the content box size at zero when applying box-sizing are also ignored.
562///     (For example, an item with a specified size of zero, positive padding, and box-sizing: border-box will have an outer flex base size of zero—and hence a negative inner flex base size.)
563#[inline]
564fn determine_flex_base_size(
565    tree: &mut impl LayoutPartialTree,
566    constants: &AlgoConstants,
567    available_space: Size<AvailableSpace>,
568    flex_items: &mut [FlexItem],
569) {
570    let dir = constants.dir;
571
572    for child in flex_items.iter_mut() {
573        let child_style = tree.get_style(child.node);
574
575        // Parent size for child sizing
576        let cross_axis_parent_size = constants.node_inner_size.cross(dir);
577        let child_parent_size = Size::from_cross(dir, cross_axis_parent_size);
578
579        // Available space for child sizing
580        let cross_axis_margin_sum = constants.margin.cross_axis_sum(dir);
581        let child_min_cross = child.min_size.cross(dir).maybe_add(cross_axis_margin_sum);
582        let child_max_cross = child.max_size.cross(dir).maybe_add(cross_axis_margin_sum);
583        let cross_axis_available_space: AvailableSpace = available_space
584            .cross(dir)
585            .map_definite_value(|val| cross_axis_parent_size.unwrap_or(val))
586            .maybe_clamp(child_min_cross, child_max_cross);
587
588        // Known dimensions for child sizing
589        let child_known_dimensions = {
590            let mut ckd = child.size.with_main(dir, None);
591            if child.align_self == AlignSelf::Stretch && ckd.cross(dir).is_none() {
592                ckd.set_cross(
593                    dir,
594                    cross_axis_available_space.into_option().maybe_sub(child.margin.cross_axis_sum(dir)),
595                );
596            }
597            ckd
598        };
599
600        child.flex_basis = 'flex_basis: {
601            // A. If the item has a definite used flex basis, that’s the flex base size.
602
603            // B. If the flex item has an intrinsic aspect ratio,
604            //    a used flex basis of content, and a definite cross size,
605            //    then the flex base size is calculated from its inner
606            //    cross size and the flex item’s intrinsic aspect ratio.
607
608            // Note: `child.size` has already been resolved against aspect_ratio in generate_anonymous_flex_items
609            // So B will just work here by using main_size without special handling for aspect_ratio
610
611            let flex_basis = child_style.flex_basis.maybe_resolve(constants.node_inner_size.main(dir));
612            let main_size = child.size.main(dir);
613            if let Some(flex_basis) = flex_basis.or(main_size) {
614                break 'flex_basis flex_basis;
615            };
616
617            // C. If the used flex basis is content or depends on its available space,
618            //    and the flex container is being sized under a min-content or max-content
619            //    constraint (e.g. when performing automatic table layout [CSS21]),
620            //    size the item under that constraint. The flex base size is the item’s
621            //    resulting main size.
622
623            // This is covered by the implementation of E below, which passes the available_space constraint
624            // through to the child size computation. It may need a separate implementation if/when D is implemented.
625
626            // D. Otherwise, if the used flex basis is content or depends on its
627            //    available space, the available main size is infinite, and the flex item’s
628            //    inline axis is parallel to the main axis, lay the item out using the rules
629            //    for a box in an orthogonal flow [CSS3-WRITING-MODES]. The flex base size
630            //    is the item’s max-content main size.
631
632            // TODO if/when vertical writing modes are supported
633
634            // E. Otherwise, size the item into the available space using its used flex basis
635            //    in place of its main size, treating a value of content as max-content.
636            //    If a cross size is needed to determine the main size (e.g. when the
637            //    flex item’s main size is in its block axis) and the flex item’s cross size
638            //    is auto and not definite, in this calculation use fit-content as the
639            //    flex item’s cross size. The flex base size is the item’s resulting main size.
640
641            let child_available_space = Size::MAX_CONTENT
642                .with_main(
643                    dir,
644                    // Map AvailableSpace::Definite to AvailableSpace::MaxContent
645                    if available_space.main(dir) == AvailableSpace::MinContent {
646                        AvailableSpace::MinContent
647                    } else {
648                        AvailableSpace::MaxContent
649                    },
650                )
651                .with_cross(dir, cross_axis_available_space);
652
653            break 'flex_basis tree.measure_child_size(
654                child.node,
655                child_known_dimensions,
656                child_parent_size,
657                child_available_space,
658                SizingMode::ContentSize,
659                dir.main_axis(),
660                Line::FALSE,
661            );
662        };
663
664        // Floor flex-basis by the padding_border_sum (floors inner_flex_basis at zero)
665        // This seems to be in violation of the spec which explicitly states that the content box should not be floored at zero
666        // (like it usually is) when calculating the flex-basis. But including this matches both Chrome and Firefox's behaviour.
667        //
668        // TODO: resolve spec violation
669        // Spec: https://www.w3.org/TR/css-flexbox-1/#intrinsic-item-contributions
670        // Spec: https://www.w3.org/TR/css-flexbox-1/#change-2016-max-contribution
671        let padding_border_sum = child.padding.main_axis_sum(constants.dir) + child.border.main_axis_sum(constants.dir);
672        child.flex_basis = child.flex_basis.max(padding_border_sum);
673
674        // The hypothetical main size is the item’s flex base size clamped according to its
675        // used min and max main sizes (and flooring the content box size at zero).
676
677        child.inner_flex_basis =
678            child.flex_basis - child.padding.main_axis_sum(constants.dir) - child.border.main_axis_sum(constants.dir);
679
680        let padding_border_axes_sums = (child.padding + child.border).sum_axes().map(Some);
681        let hypothetical_inner_min_main =
682            child.min_size.main(constants.dir).maybe_max(padding_border_axes_sums.main(constants.dir));
683        let hypothetical_inner_size =
684            child.flex_basis.maybe_clamp(hypothetical_inner_min_main, child.max_size.main(constants.dir));
685        let hypothetical_outer_size = hypothetical_inner_size + child.margin.main_axis_sum(constants.dir);
686
687        child.hypothetical_inner_size.set_main(constants.dir, hypothetical_inner_size);
688        child.hypothetical_outer_size.set_main(constants.dir, hypothetical_outer_size);
689
690        // Note that it is important that the `parent_size` parameter in the main axis is not set for this
691        // function call as it used for resolving percentages, and percentage size in an axis should not contribute
692        // to a min-content contribution in that same axis. However the `parent_size` and `available_space` *should*
693        // be set to their usual values in the cross axis so that wrapping content can wrap correctly.
694        //
695        // See https://drafts.csswg.org/css-sizing-3/#min-percentage-contribution
696        let style_min_main_size =
697            child.min_size.or(child.overflow.map(Overflow::maybe_into_automatic_min_size).into()).main(dir);
698
699        child.resolved_minimum_main_size = style_min_main_size.unwrap_or({
700            let min_content_main_size = {
701                let child_available_space = Size::MIN_CONTENT.with_cross(dir, cross_axis_available_space);
702
703                tree.measure_child_size(
704                    child.node,
705                    child_known_dimensions,
706                    child_parent_size,
707                    child_available_space,
708                    SizingMode::ContentSize,
709                    dir.main_axis(),
710                    Line::FALSE,
711                )
712            };
713
714            // 4.5. Automatic Minimum Size of Flex Items
715            // https://www.w3.org/TR/css-flexbox-1/#min-size-auto
716            let clamped_min_content_size =
717                min_content_main_size.maybe_min(child.size.main(dir)).maybe_min(child.max_size.main(dir));
718            clamped_min_content_size.maybe_max(padding_border_axes_sums.main(dir))
719        });
720    }
721}
722
723/// Collect flex items into flex lines.
724///
725/// # [9.3. Main Size Determination](https://www.w3.org/TR/css-flexbox-1/#main-sizing)
726///
727/// - [**Collect flex items into flex lines**](https://www.w3.org/TR/css-flexbox-1/#algo-line-break):
728///
729///     - If the flex container is single-line, collect all the flex items into a single flex line.
730///
731///     - Otherwise, starting from the first uncollected item, collect consecutive items one by one until the first time that the next collected item would not fit into the flex container’s inner main size
732///         (or until a forced break is encountered, see [§10 Fragmenting Flex Layout](https://www.w3.org/TR/css-flexbox-1/#pagination)).
733///         If the very first uncollected item wouldn't fit, collect just it into the line.
734///
735///         For this step, the size of a flex item is its outer hypothetical main size. (**Note: This can be negative**.)
736///
737///         Repeat until all flex items have been collected into flex lines.
738///
739///         **Note that the "collect as many" line will collect zero-sized flex items onto the end of the previous line even if the last non-zero item exactly "filled up" the line**.
740#[inline]
741fn collect_flex_lines<'a>(
742    constants: &AlgoConstants,
743    available_space: Size<AvailableSpace>,
744    flex_items: &'a mut Vec<FlexItem>,
745) -> Vec<FlexLine<'a>> {
746    if !constants.is_wrap {
747        let mut lines = new_vec_with_capacity(1);
748        lines.push(FlexLine { items: flex_items.as_mut_slice(), cross_size: 0.0, offset_cross: 0.0 });
749        lines
750    } else {
751        match available_space.main(constants.dir) {
752            // If we're sizing under a max-content constraint then the flex items will never wrap
753            // (at least for now - future extensions to the CSS spec may add provisions for forced wrap points)
754            AvailableSpace::MaxContent => {
755                let mut lines = new_vec_with_capacity(1);
756                lines.push(FlexLine { items: flex_items.as_mut_slice(), cross_size: 0.0, offset_cross: 0.0 });
757                lines
758            }
759            // If flex-wrap is Wrap and we're sizing under a min-content constraint, then we take every possible wrapping opportunity
760            // and place each item in it's own line
761            AvailableSpace::MinContent => {
762                let mut lines = new_vec_with_capacity(flex_items.len());
763                let mut items = &mut flex_items[..];
764                while !items.is_empty() {
765                    let (line_items, rest) = items.split_at_mut(1);
766                    lines.push(FlexLine { items: line_items, cross_size: 0.0, offset_cross: 0.0 });
767                    items = rest;
768                }
769                lines
770            }
771            AvailableSpace::Definite(main_axis_available_space) => {
772                let mut lines = new_vec_with_capacity(1);
773                let mut flex_items = &mut flex_items[..];
774                let main_axis_gap = constants.gap.main(constants.dir);
775
776                while !flex_items.is_empty() {
777                    // Find index of the first item in the next line
778                    // (or the last item if all remaining items are in the current line)
779                    let mut line_length = 0.0;
780                    let index = flex_items
781                        .iter()
782                        .enumerate()
783                        .find(|&(idx, child)| {
784                            // Gaps only occur between items (not before the first one or after the last one)
785                            // So first item in the line does not contribute a gap to the line length
786                            let gap_contribution = if idx == 0 { 0.0 } else { main_axis_gap };
787                            line_length += child.hypothetical_outer_size.main(constants.dir) + gap_contribution;
788                            line_length > main_axis_available_space && idx != 0
789                        })
790                        .map(|(idx, _)| idx)
791                        .unwrap_or(flex_items.len());
792
793                    let (items, rest) = flex_items.split_at_mut(index);
794                    lines.push(FlexLine { items, cross_size: 0.0, offset_cross: 0.0 });
795                    flex_items = rest;
796                }
797                lines
798            }
799        }
800    }
801}
802
803/// Determine the container's main size (if not already known)
804fn determine_container_main_size(
805    tree: &mut impl LayoutPartialTree,
806    available_space: Size<AvailableSpace>,
807    lines: &mut [FlexLine<'_>],
808    constants: &mut AlgoConstants,
809) {
810    let dir = constants.dir;
811    let main_content_box_inset = constants.content_box_inset.main_axis_sum(constants.dir);
812
813    let outer_main_size: f32 = constants.node_outer_size.main(constants.dir).unwrap_or_else(|| {
814        match available_space.main(dir) {
815            AvailableSpace::Definite(main_axis_available_space) => {
816                let longest_line_length: f32 = lines
817                    .iter()
818                    .map(|line| {
819                        let line_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
820                        let total_target_size = line
821                            .items
822                            .iter()
823                            .map(|child| {
824                                let padding_border_sum = (child.padding + child.border).main_axis_sum(constants.dir);
825                                (child.flex_basis + child.margin.main_axis_sum(constants.dir)).max(padding_border_sum)
826                            })
827                            .sum::<f32>();
828                        total_target_size + line_main_axis_gap
829                    })
830                    .max_by(|a, b| a.total_cmp(b))
831                    .unwrap_or(0.0);
832                let size = longest_line_length + main_content_box_inset;
833                if lines.len() > 1 {
834                    f32_max(size, main_axis_available_space)
835                } else {
836                    size
837                }
838            }
839            AvailableSpace::MinContent if constants.is_wrap => {
840                let longest_line_length: f32 = lines
841                    .iter()
842                    .map(|line| {
843                        let line_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
844                        let total_target_size = line
845                            .items
846                            .iter()
847                            .map(|child| {
848                                let padding_border_sum = (child.padding + child.border).main_axis_sum(constants.dir);
849                                (child.flex_basis + child.margin.main_axis_sum(constants.dir)).max(padding_border_sum)
850                            })
851                            .sum::<f32>();
852                        total_target_size + line_main_axis_gap
853                    })
854                    .max_by(|a, b| a.total_cmp(b))
855                    .unwrap_or(0.0);
856                longest_line_length + main_content_box_inset
857            }
858            AvailableSpace::MinContent | AvailableSpace::MaxContent => {
859                // Define a base main_size variable. This is mutated once for iteration over the outer
860                // loop over the flex lines as:
861                //   "The flex container’s max-content size is the largest sum of the afore-calculated sizes of all items within a single line."
862                let mut main_size = 0.0;
863
864                for line in lines.iter_mut() {
865                    for item in line.items.iter_mut() {
866                        let style_min = item.min_size.main(constants.dir);
867                        let style_preferred = item.size.main(constants.dir);
868                        let style_max = item.max_size.main(constants.dir);
869
870                        // The spec seems a bit unclear on this point (my initial reading was that the `.maybe_max(style_preferred)` should
871                        // not be included here), however this matches both Chrome and Firefox as of 9th March 2023.
872                        //
873                        // Spec: https://www.w3.org/TR/css-flexbox-1/#intrinsic-item-contributions
874                        // Spec modification: https://www.w3.org/TR/css-flexbox-1/#change-2016-max-contribution
875                        // Issue: https://github.com/w3c/csswg-drafts/issues/1435
876                        // Gentest: padding_border_overrides_size_flex_basis_0.html
877                        let clamping_basis = Some(item.flex_basis).maybe_max(style_preferred);
878                        let flex_basis_min = clamping_basis.filter(|_| item.flex_shrink == 0.0);
879                        let flex_basis_max = clamping_basis.filter(|_| item.flex_grow == 0.0);
880
881                        let min_main_size = style_min
882                            .maybe_max(flex_basis_min)
883                            .or(flex_basis_min)
884                            .unwrap_or(item.resolved_minimum_main_size)
885                            .max(item.resolved_minimum_main_size);
886                        let max_main_size =
887                            style_max.maybe_min(flex_basis_max).or(flex_basis_max).unwrap_or(f32::INFINITY);
888
889                        let content_contribution = match (min_main_size, style_preferred, max_main_size) {
890                            // If the clamping values are such that max <= min, then we can avoid the expensive step of computing the content size
891                            // as we know that the clamping values will override it anyway
892                            (min, Some(pref), max) if max <= min || max <= pref => {
893                                pref.min(max).max(min) + item.margin.main_axis_sum(constants.dir)
894                            }
895                            (min, _, max) if max <= min => min + item.margin.main_axis_sum(constants.dir),
896                            // Else compute the min- or -max content size and apply the full formula for computing the
897                            // min- or max- content contributuon
898                            _ => {
899                                // Parent size for child sizing
900                                let cross_axis_parent_size = constants.node_inner_size.cross(dir);
901
902                                // Available space for child sizing
903                                let cross_axis_margin_sum = constants.margin.cross_axis_sum(dir);
904                                let child_min_cross = item.min_size.cross(dir).maybe_add(cross_axis_margin_sum);
905                                let child_max_cross = item.max_size.cross(dir).maybe_add(cross_axis_margin_sum);
906                                let cross_axis_available_space: AvailableSpace = available_space
907                                    .cross(dir)
908                                    .map_definite_value(|val| cross_axis_parent_size.unwrap_or(val))
909                                    .maybe_clamp(child_min_cross, child_max_cross);
910
911                                let child_available_space = available_space.with_cross(dir, cross_axis_available_space);
912
913                                // Either the min- or max- content size depending on which constraint we are sizing under.
914                                // TODO: Optimise by using already computed values where available
915                                let content_main_size = tree.measure_child_size(
916                                    item.node,
917                                    Size::NONE,
918                                    constants.node_inner_size,
919                                    child_available_space,
920                                    SizingMode::InherentSize,
921                                    dir.main_axis(),
922                                    Line::FALSE,
923                                ) + item.margin.main_axis_sum(constants.dir);
924
925                                // This is somewhat bizarre in that it's asymmetrical depending whether the flex container is a column or a row.
926                                //
927                                // I *think* this might relate to https://drafts.csswg.org/css-flexbox-1/#algo-main-container:
928                                //
929                                //    "The automatic block size of a block-level flex container is its max-content size."
930                                //
931                                // Which could suggest that flex-basis defining a vertical size does not shrink because it is in the block axis, and the automatic size
932                                // in the block axis is a MAX content size. Whereas a flex-basis defining a horizontal size does shrink because the automatic size in
933                                // inline axis is MIN content size (although I don't have a reference for that).
934                                //
935                                // Ultimately, this was not found by reading the spec, but by trial and error fixing tests to align with Webkit/Firefox output.
936                                // (see the `flex_basis_unconstraint_row` and `flex_basis_uncontraint_column` generated tests which demonstrate this)
937                                if constants.is_row {
938                                    content_main_size.maybe_clamp(style_min, style_max).max(main_content_box_inset)
939                                } else {
940                                    content_main_size
941                                        .max(item.flex_basis)
942                                        .maybe_clamp(style_min, style_max)
943                                        .max(main_content_box_inset)
944                                }
945                            }
946                        };
947                        item.content_flex_fraction = {
948                            let diff = content_contribution - item.flex_basis;
949                            if diff > 0.0 {
950                                diff / f32_max(1.0, item.flex_grow)
951                            } else if diff < 0.0 {
952                                let scaled_shrink_factor = f32_max(1.0, item.flex_shrink * item.inner_flex_basis);
953                                diff / scaled_shrink_factor
954                            } else {
955                                // We are assuming that diff is 0.0 here and that we haven't accidentally introduced a NaN
956                                0.0
957                            }
958                        };
959                    }
960
961                    // TODO Spec says to scale everything by the line's max flex fraction. But neither Chrome nor firefox implement this
962                    // so we don't either. But if we did want to, we'd need this computation here (and to use it below):
963                    //
964                    // Within each line, find the largest max-content flex fraction among all the flex items.
965                    // let line_flex_fraction = line
966                    //     .items
967                    //     .iter()
968                    //     .map(|item| item.content_flex_fraction)
969                    //     .max_by(|a, b| a.total_cmp(b))
970                    //     .unwrap_or(0.0); // Unwrap case never gets hit because there is always at least one item a line
971
972                    // Add each item’s flex base size to the product of:
973                    //   - its flex grow factor (or scaled flex shrink factor,if the chosen max-content flex fraction was negative)
974                    //   - the chosen max-content flex fraction
975                    // then clamp that result by the max main size floored by the min main size.
976                    //
977                    // The flex container’s max-content size is the largest sum of the afore-calculated sizes of all items within a single line.
978                    let item_main_size_sum = line
979                        .items
980                        .iter_mut()
981                        .map(|item| {
982                            let flex_fraction = item.content_flex_fraction;
983                            // let flex_fraction = line_flex_fraction;
984
985                            let flex_contribution = if item.content_flex_fraction > 0.0 {
986                                f32_max(1.0, item.flex_grow) * flex_fraction
987                            } else if item.content_flex_fraction < 0.0 {
988                                let scaled_shrink_factor = f32_max(1.0, item.flex_shrink) * item.inner_flex_basis;
989                                scaled_shrink_factor * flex_fraction
990                            } else {
991                                0.0
992                            };
993                            let size = item.flex_basis + flex_contribution;
994                            item.outer_target_size.set_main(constants.dir, size);
995                            item.target_size.set_main(constants.dir, size);
996                            size
997                        })
998                        .sum::<f32>();
999
1000                    let gap_sum = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
1001                    main_size = f32_max(main_size, item_main_size_sum + gap_sum)
1002                }
1003
1004                main_size + main_content_box_inset
1005            }
1006        }
1007    });
1008
1009    let outer_main_size = outer_main_size
1010        .maybe_clamp(constants.min_size.main(constants.dir), constants.max_size.main(constants.dir))
1011        .max(main_content_box_inset - constants.scrollbar_gutter.main(constants.dir));
1012
1013    // let outer_main_size = inner_main_size + constants.padding_border.main_axis_sum(constants.dir);
1014    let inner_main_size = f32_max(outer_main_size - main_content_box_inset, 0.0);
1015    constants.container_size.set_main(constants.dir, outer_main_size);
1016    constants.inner_container_size.set_main(constants.dir, inner_main_size);
1017    constants.node_inner_size.set_main(constants.dir, Some(inner_main_size));
1018}
1019
1020/// Resolve the flexible lengths of the items within a flex line.
1021/// Sets the `main` component of each item's `target_size` and `outer_target_size`
1022///
1023/// # [9.7. Resolving Flexible Lengths](https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths)
1024#[inline]
1025fn resolve_flexible_lengths(line: &mut FlexLine, constants: &AlgoConstants, original_gap: Size<f32>) {
1026    let total_original_main_axis_gap = sum_axis_gaps(original_gap.main(constants.dir), line.items.len());
1027    let total_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
1028
1029    // 1. Determine the used flex factor. Sum the outer hypothetical main sizes of all
1030    //    items on the line. If the sum is less than the flex container’s inner main size,
1031    //    use the flex grow factor for the rest of this algorithm; otherwise, use the
1032    //    flex shrink factor.
1033
1034    let total_hypothetical_outer_main_size =
1035        line.items.iter().map(|child| child.hypothetical_outer_size.main(constants.dir)).sum::<f32>();
1036    let used_flex_factor: f32 = total_original_main_axis_gap + total_hypothetical_outer_main_size;
1037    let growing = used_flex_factor < constants.node_inner_size.main(constants.dir).unwrap_or(0.0);
1038    let shrinking = !growing;
1039
1040    // 2. Size inflexible items. Freeze, setting its target main size to its hypothetical main size
1041    //    - Any item that has a flex factor of zero
1042    //    - If using the flex grow factor: any item that has a flex base size
1043    //      greater than its hypothetical main size
1044    //    - If using the flex shrink factor: any item that has a flex base size
1045    //      smaller than its hypothetical main size
1046
1047    for child in line.items.iter_mut() {
1048        let inner_target_size = child.hypothetical_inner_size.main(constants.dir);
1049        child.target_size.set_main(constants.dir, inner_target_size);
1050
1051        if (child.flex_grow == 0.0 && child.flex_shrink == 0.0)
1052            || (growing && child.flex_basis > child.hypothetical_inner_size.main(constants.dir))
1053            || (shrinking && child.flex_basis < child.hypothetical_inner_size.main(constants.dir))
1054        {
1055            child.frozen = true;
1056            let outer_target_size = inner_target_size + child.margin.main_axis_sum(constants.dir);
1057            child.outer_target_size.set_main(constants.dir, outer_target_size);
1058        }
1059    }
1060
1061    // 3. Calculate initial free space. Sum the outer sizes of all items on the line,
1062    //    and subtract this from the flex container’s inner main size. For frozen items,
1063    //    use their outer target main size; for other items, use their outer flex base size.
1064
1065    let used_space: f32 = total_main_axis_gap
1066        + line
1067            .items
1068            .iter()
1069            .map(|child| {
1070                child.margin.main_axis_sum(constants.dir)
1071                    + if child.frozen { child.outer_target_size.main(constants.dir) } else { child.flex_basis }
1072            })
1073            .sum::<f32>();
1074
1075    let initial_free_space = constants.node_inner_size.main(constants.dir).maybe_sub(used_space).unwrap_or(0.0);
1076
1077    // 4. Loop
1078
1079    loop {
1080        // a. Check for flexible items. If all the flex items on the line are frozen,
1081        //    free space has been distributed; exit this loop.
1082
1083        if line.items.iter().all(|child| child.frozen) {
1084            break;
1085        }
1086
1087        // b. Calculate the remaining free space as for initial free space, above.
1088        //    If the sum of the unfrozen flex items’ flex factors is less than one,
1089        //    multiply the initial free space by this sum. If the magnitude of this
1090        //    value is less than the magnitude of the remaining free space, use this
1091        //    as the remaining free space.
1092
1093        let used_space: f32 = total_main_axis_gap
1094            + line
1095                .items
1096                .iter()
1097                .map(|child| {
1098                    child.margin.main_axis_sum(constants.dir)
1099                        + if child.frozen { child.outer_target_size.main(constants.dir) } else { child.flex_basis }
1100                })
1101                .sum::<f32>();
1102
1103        let mut unfrozen: Vec<&mut FlexItem> = line.items.iter_mut().filter(|child| !child.frozen).collect();
1104
1105        let (sum_flex_grow, sum_flex_shrink): (f32, f32) =
1106            unfrozen.iter().fold((0.0, 0.0), |(flex_grow, flex_shrink), item| {
1107                (flex_grow + item.flex_grow, flex_shrink + item.flex_shrink)
1108            });
1109
1110        let free_space = if growing && sum_flex_grow < 1.0 {
1111            (initial_free_space * sum_flex_grow - total_main_axis_gap)
1112                .maybe_min(constants.node_inner_size.main(constants.dir).maybe_sub(used_space))
1113        } else if shrinking && sum_flex_shrink < 1.0 {
1114            (initial_free_space * sum_flex_shrink - total_main_axis_gap)
1115                .maybe_max(constants.node_inner_size.main(constants.dir).maybe_sub(used_space))
1116        } else {
1117            (constants.node_inner_size.main(constants.dir).maybe_sub(used_space))
1118                .unwrap_or(used_flex_factor - used_space)
1119        };
1120
1121        // c. Distribute free space proportional to the flex factors.
1122        //    - If the remaining free space is zero
1123        //        Do Nothing
1124        //    - If using the flex grow factor
1125        //        Find the ratio of the item’s flex grow factor to the sum of the
1126        //        flex grow factors of all unfrozen items on the line. Set the item’s
1127        //        target main size to its flex base size plus a fraction of the remaining
1128        //        free space proportional to the ratio.
1129        //    - If using the flex shrink factor
1130        //        For every unfrozen item on the line, multiply its flex shrink factor by
1131        //        its inner flex base size, and note this as its scaled flex shrink factor.
1132        //        Find the ratio of the item’s scaled flex shrink factor to the sum of the
1133        //        scaled flex shrink factors of all unfrozen items on the line. Set the item’s
1134        //        target main size to its flex base size minus a fraction of the absolute value
1135        //        of the remaining free space proportional to the ratio. Note this may result
1136        //        in a negative inner main size; it will be corrected in the next step.
1137        //    - Otherwise
1138        //        Do Nothing
1139
1140        if free_space.is_normal() {
1141            if growing && sum_flex_grow > 0.0 {
1142                for child in &mut unfrozen {
1143                    child
1144                        .target_size
1145                        .set_main(constants.dir, child.flex_basis + free_space * (child.flex_grow / sum_flex_grow));
1146                }
1147            } else if shrinking && sum_flex_shrink > 0.0 {
1148                let sum_scaled_shrink_factor: f32 =
1149                    unfrozen.iter().map(|child| child.inner_flex_basis * child.flex_shrink).sum();
1150
1151                if sum_scaled_shrink_factor > 0.0 {
1152                    for child in &mut unfrozen {
1153                        let scaled_shrink_factor = child.inner_flex_basis * child.flex_shrink;
1154                        child.target_size.set_main(
1155                            constants.dir,
1156                            child.flex_basis + free_space * (scaled_shrink_factor / sum_scaled_shrink_factor),
1157                        )
1158                    }
1159                }
1160            }
1161        }
1162
1163        // d. Fix min/max violations. Clamp each non-frozen item’s target main size by its
1164        //    used min and max main sizes and floor its content-box size at zero. If the
1165        //    item’s target main size was made smaller by this, it’s a max violation.
1166        //    If the item’s target main size was made larger by this, it’s a min violation.
1167
1168        let total_violation = unfrozen.iter_mut().fold(0.0, |acc, child| -> f32 {
1169            let resolved_min_main: Option<f32> = child.resolved_minimum_main_size.into();
1170            let max_main = child.max_size.main(constants.dir);
1171            let clamped = child.target_size.main(constants.dir).maybe_clamp(resolved_min_main, max_main).max(0.0);
1172            child.violation = clamped - child.target_size.main(constants.dir);
1173            child.target_size.set_main(constants.dir, clamped);
1174            child.outer_target_size.set_main(
1175                constants.dir,
1176                child.target_size.main(constants.dir) + child.margin.main_axis_sum(constants.dir),
1177            );
1178
1179            acc + child.violation
1180        });
1181
1182        // e. Freeze over-flexed items. The total violation is the sum of the adjustments
1183        //    from the previous step ∑(clamped size - unclamped size). If the total violation is:
1184        //    - Zero
1185        //        Freeze all items.
1186        //    - Positive
1187        //        Freeze all the items with min violations.
1188        //    - Negative
1189        //        Freeze all the items with max violations.
1190
1191        for child in &mut unfrozen {
1192            match total_violation {
1193                v if v > 0.0 => child.frozen = child.violation > 0.0,
1194                v if v < 0.0 => child.frozen = child.violation < 0.0,
1195                _ => child.frozen = true,
1196            }
1197        }
1198
1199        // f. Return to the start of this loop.
1200    }
1201}
1202
1203/// Determine the hypothetical cross size of each item.
1204///
1205/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1206///
1207/// - [**Determine the hypothetical cross size of each item**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-item)
1208///     by performing layout with the used main size and the available space, treating auto as fit-content.
1209#[inline]
1210fn determine_hypothetical_cross_size(
1211    tree: &mut impl LayoutPartialTree,
1212    line: &mut FlexLine,
1213    constants: &AlgoConstants,
1214    available_space: Size<AvailableSpace>,
1215) {
1216    for child in line.items.iter_mut() {
1217        let padding_border_sum = (child.padding + child.border).cross_axis_sum(constants.dir);
1218
1219        let child_known_main = constants.container_size.main(constants.dir).into();
1220
1221        let child_cross = child
1222            .size
1223            .cross(constants.dir)
1224            .maybe_clamp(child.min_size.cross(constants.dir), child.max_size.cross(constants.dir))
1225            .maybe_max(padding_border_sum);
1226
1227        let child_available_cross = available_space
1228            .cross(constants.dir)
1229            .maybe_clamp(child.min_size.cross(constants.dir), child.max_size.cross(constants.dir))
1230            .maybe_max(padding_border_sum);
1231
1232        let child_inner_cross = child_cross.unwrap_or_else(|| {
1233            tree.measure_child_size(
1234                child.node,
1235                Size {
1236                    width: if constants.is_row { child.target_size.width.into() } else { child_cross },
1237                    height: if constants.is_row { child_cross } else { child.target_size.height.into() },
1238                },
1239                constants.node_inner_size,
1240                Size {
1241                    width: if constants.is_row { child_known_main } else { child_available_cross },
1242                    height: if constants.is_row { child_available_cross } else { child_known_main },
1243                },
1244                SizingMode::ContentSize,
1245                constants.dir.cross_axis(),
1246                Line::FALSE,
1247            )
1248            .maybe_clamp(child.min_size.cross(constants.dir), child.max_size.cross(constants.dir))
1249            .max(padding_border_sum)
1250        });
1251        let child_outer_cross = child_inner_cross + child.margin.cross_axis_sum(constants.dir);
1252
1253        child.hypothetical_inner_size.set_cross(constants.dir, child_inner_cross);
1254        child.hypothetical_outer_size.set_cross(constants.dir, child_outer_cross);
1255    }
1256}
1257
1258/// Calculate the base lines of the children.
1259#[inline]
1260fn calculate_children_base_lines(
1261    tree: &mut impl LayoutPartialTree,
1262    node_size: Size<Option<f32>>,
1263    available_space: Size<AvailableSpace>,
1264    flex_lines: &mut [FlexLine],
1265    constants: &AlgoConstants,
1266) {
1267    // Only compute baselines for flex rows because we only support baseline alignment in the cross axis
1268    // where that axis is also the inline axis
1269    // TODO: this may need revisiting if/when we support vertical writing modes
1270    if !constants.is_row {
1271        return;
1272    }
1273
1274    for line in flex_lines {
1275        // If a flex line has one or zero items participating in baseline alignment then baseline alignment is a no-op so we skip
1276        let line_baseline_child_count =
1277            line.items.iter().filter(|child| child.align_self == AlignSelf::Baseline).count();
1278        if line_baseline_child_count <= 1 {
1279            continue;
1280        }
1281
1282        for child in line.items.iter_mut() {
1283            // Only calculate baselines for children participating in baseline alignment
1284            if child.align_self != AlignSelf::Baseline {
1285                continue;
1286            }
1287
1288            let measured_size_and_baselines = tree.perform_child_layout(
1289                child.node,
1290                Size {
1291                    width: if constants.is_row {
1292                        child.target_size.width.into()
1293                    } else {
1294                        child.hypothetical_inner_size.width.into()
1295                    },
1296                    height: if constants.is_row {
1297                        child.hypothetical_inner_size.height.into()
1298                    } else {
1299                        child.target_size.height.into()
1300                    },
1301                },
1302                constants.node_inner_size,
1303                Size {
1304                    width: if constants.is_row {
1305                        constants.container_size.width.into()
1306                    } else {
1307                        available_space.width.maybe_set(node_size.width)
1308                    },
1309                    height: if constants.is_row {
1310                        available_space.height.maybe_set(node_size.height)
1311                    } else {
1312                        constants.container_size.height.into()
1313                    },
1314                },
1315                SizingMode::ContentSize,
1316                Line::FALSE,
1317            );
1318
1319            let baseline = measured_size_and_baselines.first_baselines.y;
1320            let height = measured_size_and_baselines.size.height;
1321
1322            child.baseline = baseline.unwrap_or(height) + child.margin.top;
1323        }
1324    }
1325}
1326
1327/// Calculate the cross size of each flex line.
1328///
1329/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1330///
1331/// - [**Calculate the cross size of each flex line**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-line).
1332///
1333///     If the flex container is single-line and has a definite cross size, the cross size of the flex line is the flex container’s inner cross size.
1334///
1335///     Otherwise, for each flex line:
1336///
1337///     1. Collect all the flex items whose inline-axis is parallel to the main-axis, whose align-self is baseline, and whose cross-axis margins are both non-auto.
1338///         Find the largest of the distances between each item’s baseline and its hypothetical outer cross-start edge,
1339///         and the largest of the distances between each item’s baseline and its hypothetical outer cross-end edge, and sum these two values.
1340///
1341///     2. Among all the items not collected by the previous step, find the largest outer hypothetical cross size.
1342///
1343///     3. The used cross-size of the flex line is the largest of the numbers found in the previous two steps and zero.
1344///
1345///         If the flex container is single-line, then clamp the line’s cross-size to be within the container’s computed min and max cross sizes.
1346///         **Note that if CSS 2.1’s definition of min/max-width/height applied more generally, this behavior would fall out automatically**.
1347#[inline]
1348fn calculate_cross_size(flex_lines: &mut [FlexLine], node_size: Size<Option<f32>>, constants: &AlgoConstants) {
1349    // Note: AlignContent::SpaceEvenly and AlignContent::SpaceAround behave like AlignContent::Stretch when there is only
1350    // a single flex line in the container. See: https://www.w3.org/TR/css-flexbox-1/#align-content-property
1351    // Also: align_content is ignored entirely (and thus behaves like Stretch) when `flex_wrap` is set to `nowrap`.
1352    if flex_lines.len() == 1
1353        && node_size.cross(constants.dir).is_some()
1354        && (!constants.is_wrap
1355            || matches!(
1356                constants.align_content,
1357                AlignContent::Stretch | AlignContent::SpaceEvenly | AlignContent::SpaceAround
1358            ))
1359    {
1360        let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1361        let cross_min_size = constants.min_size.cross(constants.dir);
1362        let cross_max_size = constants.max_size.cross(constants.dir);
1363        flex_lines[0].cross_size = node_size
1364            .cross(constants.dir)
1365            .maybe_clamp(cross_min_size, cross_max_size)
1366            .maybe_sub(cross_axis_padding_border)
1367            .maybe_max(0.0)
1368            .unwrap_or(0.0);
1369    } else {
1370        for line in flex_lines.iter_mut() {
1371            //    1. Collect all the flex items whose inline-axis is parallel to the main-axis, whose
1372            //       align-self is baseline, and whose cross-axis margins are both non-auto. Find the
1373            //       largest of the distances between each item’s baseline and its hypothetical outer
1374            //       cross-start edge, and the largest of the distances between each item’s baseline
1375            //       and its hypothetical outer cross-end edge, and sum these two values.
1376
1377            //    2. Among all the items not collected by the previous step, find the largest
1378            //       outer hypothetical cross size.
1379
1380            //    3. The used cross-size of the flex line is the largest of the numbers found in the
1381            //       previous two steps and zero.
1382
1383            let max_baseline: f32 = line.items.iter().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x));
1384            line.cross_size = line
1385                .items
1386                .iter()
1387                .map(|child| {
1388                    if child.align_self == AlignSelf::Baseline
1389                        && !child.margin_is_auto.cross_start(constants.dir)
1390                        && !child.margin_is_auto.cross_end(constants.dir)
1391                    {
1392                        max_baseline - child.baseline + child.hypothetical_outer_size.cross(constants.dir)
1393                    } else {
1394                        child.hypothetical_outer_size.cross(constants.dir)
1395                    }
1396                })
1397                .fold(0.0, |acc, x| acc.max(x));
1398        }
1399        //  If the flex container is single-line, then clamp the line’s cross-size to be within the container’s computed min and max cross sizes.
1400        if !constants.is_wrap {
1401            let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1402            let cross_min_size = constants.min_size.cross(constants.dir);
1403            let cross_max_size = constants.max_size.cross(constants.dir);
1404            flex_lines[0].cross_size = flex_lines[0].cross_size.maybe_clamp(
1405                cross_min_size.maybe_sub(cross_axis_padding_border),
1406                cross_max_size.maybe_sub(cross_axis_padding_border),
1407            );
1408        }
1409    }
1410}
1411
1412/// Handle 'align-content: stretch'.
1413///
1414/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1415///
1416/// - [**Handle 'align-content: stretch'**](https://www.w3.org/TR/css-flexbox-1/#algo-line-stretch). If the flex container has a definite cross size, align-content is stretch,
1417///     and the sum of the flex lines' cross sizes is less than the flex container’s inner cross size,
1418///     increase the cross size of each flex line by equal amounts such that the sum of their cross sizes exactly equals the flex container’s inner cross size.
1419#[inline]
1420fn handle_align_content_stretch(flex_lines: &mut [FlexLine], node_size: Size<Option<f32>>, constants: &AlgoConstants) {
1421    if constants.align_content == AlignContent::Stretch {
1422        let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1423        let cross_min_size = constants.min_size.cross(constants.dir);
1424        let cross_max_size = constants.max_size.cross(constants.dir);
1425        let container_min_inner_cross = node_size
1426            .cross(constants.dir)
1427            .or(cross_min_size)
1428            .maybe_clamp(cross_min_size, cross_max_size)
1429            .maybe_sub(cross_axis_padding_border)
1430            .maybe_max(0.0)
1431            .unwrap_or(0.0);
1432
1433        let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len());
1434        let lines_total_cross: f32 = flex_lines.iter().map(|line| line.cross_size).sum::<f32>() + total_cross_axis_gap;
1435
1436        if lines_total_cross < container_min_inner_cross {
1437            let remaining = container_min_inner_cross - lines_total_cross;
1438            let addition = remaining / flex_lines.len() as f32;
1439            flex_lines.iter_mut().for_each(|line| line.cross_size += addition);
1440        }
1441    }
1442}
1443
1444/// Determine the used cross size of each flex item.
1445///
1446/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1447///
1448/// - [**Determine the used cross size of each flex item**](https://www.w3.org/TR/css-flexbox-1/#algo-stretch). If a flex item has align-self: stretch, its computed cross size property is auto,
1449///     and neither of its cross-axis margins are auto, the used outer cross size is the used cross size of its flex line, clamped according to the item’s used min and max cross sizes.
1450///     Otherwise, the used cross size is the item’s hypothetical cross size.
1451///
1452///     If the flex item has align-self: stretch, redo layout for its contents, treating this used size as its definite cross size so that percentage-sized children can be resolved.
1453///
1454///     **Note that this step does not affect the main size of the flex item, even if it has an intrinsic aspect ratio**.
1455#[inline]
1456fn determine_used_cross_size(tree: &impl LayoutPartialTree, flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1457    for line in flex_lines {
1458        let line_cross_size = line.cross_size;
1459
1460        for child in line.items.iter_mut() {
1461            let child_style = tree.get_style(child.node);
1462            child.target_size.set_cross(
1463                constants.dir,
1464                if child.align_self == AlignSelf::Stretch
1465                    && !child.margin_is_auto.cross_start(constants.dir)
1466                    && !child.margin_is_auto.cross_end(constants.dir)
1467                    && child_style.size.cross(constants.dir) == Dimension::Auto
1468                {
1469                    // For some reason this particular usage of max_width is an exception to the rule that max_width's transfer
1470                    // using the aspect_ratio (if set). Both Chrome and Firefox agree on this. And reading the spec, it seems like
1471                    // a reasonable interpretation. Although it seems to me that the spec *should* apply aspect_ratio here.
1472                    let max_size_ignoring_aspect_ratio = child_style.max_size.maybe_resolve(constants.node_inner_size);
1473
1474                    (line_cross_size - child.margin.cross_axis_sum(constants.dir)).maybe_clamp(
1475                        child.min_size.cross(constants.dir),
1476                        max_size_ignoring_aspect_ratio.cross(constants.dir),
1477                    )
1478                } else {
1479                    child.hypothetical_inner_size.cross(constants.dir)
1480                },
1481            );
1482
1483            child.outer_target_size.set_cross(
1484                constants.dir,
1485                child.target_size.cross(constants.dir) + child.margin.cross_axis_sum(constants.dir),
1486            );
1487        }
1488    }
1489}
1490
1491/// Distribute any remaining free space.
1492///
1493/// # [9.5. Main-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#main-alignment)
1494///
1495/// - [**Distribute any remaining free space**](https://www.w3.org/TR/css-flexbox-1/#algo-main-align). For each flex line:
1496///
1497///     1. If the remaining free space is positive and at least one main-axis margin on this line is `auto`, distribute the free space equally among these margins.
1498///         Otherwise, set all `auto` margins to zero.
1499///
1500///     2. Align the items along the main-axis per `justify-content`.
1501#[inline]
1502fn distribute_remaining_free_space(flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1503    for line in flex_lines {
1504        let total_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
1505        let used_space: f32 = total_main_axis_gap
1506            + line.items.iter().map(|child| child.outer_target_size.main(constants.dir)).sum::<f32>();
1507        let free_space = constants.inner_container_size.main(constants.dir) - used_space;
1508        let mut num_auto_margins = 0;
1509
1510        for child in line.items.iter_mut() {
1511            if child.margin_is_auto.main_start(constants.dir) {
1512                num_auto_margins += 1;
1513            }
1514            if child.margin_is_auto.main_end(constants.dir) {
1515                num_auto_margins += 1;
1516            }
1517        }
1518
1519        if free_space > 0.0 && num_auto_margins > 0 {
1520            let margin = free_space / num_auto_margins as f32;
1521
1522            for child in line.items.iter_mut() {
1523                if child.margin_is_auto.main_start(constants.dir) {
1524                    if constants.is_row {
1525                        child.margin.left = margin;
1526                    } else {
1527                        child.margin.top = margin;
1528                    }
1529                }
1530                if child.margin_is_auto.main_end(constants.dir) {
1531                    if constants.is_row {
1532                        child.margin.right = margin;
1533                    } else {
1534                        child.margin.bottom = margin;
1535                    }
1536                }
1537            }
1538        } else {
1539            let num_items = line.items.len();
1540            let layout_reverse = constants.dir.is_reverse();
1541            let gap = constants.gap.main(constants.dir);
1542            let justify_content_mode = constants.justify_content.unwrap_or(JustifyContent::FlexStart);
1543
1544            let justify_item = |(i, child): (usize, &mut FlexItem)| {
1545                child.offset_main =
1546                    compute_alignment_offset(free_space, num_items, gap, justify_content_mode, layout_reverse, i == 0);
1547            };
1548
1549            if layout_reverse {
1550                line.items.iter_mut().rev().enumerate().for_each(justify_item);
1551            } else {
1552                line.items.iter_mut().enumerate().for_each(justify_item);
1553            }
1554        }
1555    }
1556}
1557
1558/// Resolve cross-axis `auto` margins.
1559///
1560/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1561///
1562/// - [**Resolve cross-axis `auto` margins**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-margins).
1563///     If a flex item has auto cross-axis margins:
1564///
1565///     - If its outer cross size (treating those auto margins as zero) is less than the cross size of its flex line,
1566///         distribute the difference in those sizes equally to the auto margins.
1567///
1568///     - Otherwise, if the block-start or inline-start margin (whichever is in the cross axis) is auto, set it to zero.
1569///         Set the opposite margin so that the outer cross size of the item equals the cross size of its flex line.
1570#[inline]
1571fn resolve_cross_axis_auto_margins(flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1572    for line in flex_lines {
1573        let line_cross_size = line.cross_size;
1574        let max_baseline: f32 = line.items.iter_mut().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x));
1575
1576        for child in line.items.iter_mut() {
1577            let free_space = line_cross_size - child.outer_target_size.cross(constants.dir);
1578
1579            if child.margin_is_auto.cross_start(constants.dir) && child.margin_is_auto.cross_end(constants.dir) {
1580                if constants.is_row {
1581                    child.margin.top = free_space / 2.0;
1582                    child.margin.bottom = free_space / 2.0;
1583                } else {
1584                    child.margin.left = free_space / 2.0;
1585                    child.margin.right = free_space / 2.0;
1586                }
1587            } else if child.margin_is_auto.cross_start(constants.dir) {
1588                if constants.is_row {
1589                    child.margin.top = free_space;
1590                } else {
1591                    child.margin.left = free_space;
1592                }
1593            } else if child.margin_is_auto.cross_end(constants.dir) {
1594                if constants.is_row {
1595                    child.margin.bottom = free_space;
1596                } else {
1597                    child.margin.right = free_space;
1598                }
1599            } else {
1600                // 14. Align all flex items along the cross-axis.
1601                child.offset_cross = align_flex_items_along_cross_axis(child, free_space, max_baseline, constants);
1602            }
1603        }
1604    }
1605}
1606
1607/// Align all flex items along the cross-axis.
1608///
1609/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1610///
1611/// - [**Align all flex items along the cross-axis**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-align) per `align-self`,
1612///     if neither of the item's cross-axis margins are `auto`.
1613#[inline]
1614fn align_flex_items_along_cross_axis(
1615    child: &FlexItem,
1616    free_space: f32,
1617    max_baseline: f32,
1618    constants: &AlgoConstants,
1619) -> f32 {
1620    match child.align_self {
1621        AlignSelf::Start => 0.0,
1622        AlignSelf::FlexStart => {
1623            if constants.is_wrap_reverse {
1624                free_space
1625            } else {
1626                0.0
1627            }
1628        }
1629        AlignSelf::End => free_space,
1630        AlignSelf::FlexEnd => {
1631            if constants.is_wrap_reverse {
1632                0.0
1633            } else {
1634                free_space
1635            }
1636        }
1637        AlignSelf::Center => free_space / 2.0,
1638        AlignSelf::Baseline => {
1639            if constants.is_row {
1640                max_baseline - child.baseline
1641            } else {
1642                // Until we support vertical writing modes, baseline alignment only makes sense if
1643                // the constants.direction is row, so we treat it as flex-start alignment in columns.
1644                if constants.is_wrap_reverse {
1645                    free_space
1646                } else {
1647                    0.0
1648                }
1649            }
1650        }
1651        AlignSelf::Stretch => {
1652            if constants.is_wrap_reverse {
1653                free_space
1654            } else {
1655                0.0
1656            }
1657        }
1658    }
1659}
1660
1661/// Determine the flex container’s used cross size.
1662///
1663/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1664///
1665/// - [**Determine the flex container’s used cross size**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-container):
1666///
1667///     - If the cross size property is a definite size, use that, clamped by the used min and max cross sizes of the flex container.
1668///
1669///     - Otherwise, use the sum of the flex lines' cross sizes, clamped by the used min and max cross sizes of the flex container.
1670#[inline]
1671#[must_use]
1672fn determine_container_cross_size(
1673    flex_lines: &[FlexLine],
1674    node_size: Size<Option<f32>>,
1675    constants: &mut AlgoConstants,
1676) -> f32 {
1677    let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len());
1678    let total_line_cross_size: f32 = flex_lines.iter().map(|line| line.cross_size).sum::<f32>();
1679
1680    let padding_border_sum = constants.content_box_inset.cross_axis_sum(constants.dir);
1681    let cross_scrollbar_gutter = constants.scrollbar_gutter.cross(constants.dir);
1682    let min_cross_size = constants.min_size.cross(constants.dir);
1683    let max_cross_size = constants.max_size.cross(constants.dir);
1684    let outer_container_size = node_size
1685        .cross(constants.dir)
1686        .unwrap_or(total_line_cross_size + total_cross_axis_gap + padding_border_sum)
1687        .maybe_clamp(min_cross_size, max_cross_size)
1688        .max(padding_border_sum - cross_scrollbar_gutter);
1689    let inner_container_size = f32_max(outer_container_size - padding_border_sum, 0.0);
1690
1691    constants.container_size.set_cross(constants.dir, outer_container_size);
1692    constants.inner_container_size.set_cross(constants.dir, inner_container_size);
1693
1694    total_line_cross_size
1695}
1696
1697/// Align all flex lines per `align-content`.
1698///
1699/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1700///
1701/// - [**Align all flex lines**](https://www.w3.org/TR/css-flexbox-1/#algo-line-align) per `align-content`.
1702#[inline]
1703fn align_flex_lines_per_align_content(flex_lines: &mut [FlexLine], constants: &AlgoConstants, total_cross_size: f32) {
1704    let num_lines = flex_lines.len();
1705    let gap = constants.gap.cross(constants.dir);
1706    let align_content_mode = constants.align_content;
1707    let total_cross_axis_gap = sum_axis_gaps(gap, num_lines);
1708    let free_space = constants.inner_container_size.cross(constants.dir) - total_cross_size - total_cross_axis_gap;
1709
1710    let align_line = |(i, line): (usize, &mut FlexLine)| {
1711        line.offset_cross =
1712            compute_alignment_offset(free_space, num_lines, gap, align_content_mode, constants.is_wrap_reverse, i == 0);
1713    };
1714
1715    if constants.is_wrap_reverse {
1716        flex_lines.iter_mut().rev().enumerate().for_each(align_line);
1717    } else {
1718        flex_lines.iter_mut().enumerate().for_each(align_line);
1719    }
1720}
1721
1722/// Calculates the layout for a flex-item
1723#[allow(clippy::too_many_arguments)]
1724fn calculate_flex_item(
1725    tree: &mut impl LayoutPartialTree,
1726    item: &mut FlexItem,
1727    total_offset_main: &mut f32,
1728    total_offset_cross: f32,
1729    line_offset_cross: f32,
1730    #[cfg(feature = "content_size")] total_content_size: &mut Size<f32>,
1731    container_size: Size<f32>,
1732    node_inner_size: Size<Option<f32>>,
1733    direction: FlexDirection,
1734) {
1735    let layout_output = tree.perform_child_layout(
1736        item.node,
1737        item.target_size.map(|s| s.into()),
1738        node_inner_size,
1739        container_size.map(|s| s.into()),
1740        SizingMode::ContentSize,
1741        Line::FALSE,
1742    );
1743    let LayoutOutput {
1744        size,
1745        #[cfg(feature = "content_size")]
1746        content_size,
1747        ..
1748    } = layout_output;
1749
1750    let offset_main = *total_offset_main
1751        + item.offset_main
1752        + item.margin.main_start(direction)
1753        + (item.inset.main_start(direction).or(item.inset.main_end(direction).map(|pos| -pos)).unwrap_or(0.0));
1754
1755    let offset_cross = total_offset_cross
1756        + item.offset_cross
1757        + line_offset_cross
1758        + item.margin.cross_start(direction)
1759        + (item.inset.cross_start(direction).or(item.inset.cross_end(direction).map(|pos| -pos)).unwrap_or(0.0));
1760
1761    if direction.is_row() {
1762        let baseline_offset_cross = total_offset_cross + item.offset_cross + item.margin.cross_start(direction);
1763        let inner_baseline = layout_output.first_baselines.y.unwrap_or(size.height);
1764        item.baseline = baseline_offset_cross + inner_baseline;
1765    } else {
1766        let baseline_offset_main = *total_offset_main + item.offset_main + item.margin.main_start(direction);
1767        let inner_baseline = layout_output.first_baselines.y.unwrap_or(size.height);
1768        item.baseline = baseline_offset_main + inner_baseline;
1769    }
1770
1771    let location = match direction.is_row() {
1772        true => Point { x: offset_main, y: offset_cross },
1773        false => Point { x: offset_cross, y: offset_main },
1774    };
1775    let scrollbar_size = Size {
1776        width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
1777        height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
1778    };
1779
1780    tree.set_unrounded_layout(
1781        item.node,
1782        &Layout {
1783            order: item.order,
1784            size,
1785            #[cfg(feature = "content_size")]
1786            content_size,
1787            scrollbar_size,
1788            location,
1789            padding: item.padding,
1790            border: item.border,
1791        },
1792    );
1793
1794    *total_offset_main += item.offset_main + item.margin.main_axis_sum(direction) + size.main(direction);
1795
1796    #[cfg(feature = "content_size")]
1797    {
1798        *total_content_size =
1799            total_content_size.f32_max(compute_content_size_contribution(location, size, content_size, item.overflow));
1800    }
1801}
1802
1803/// Calculates the layout line
1804#[allow(clippy::too_many_arguments)]
1805fn calculate_layout_line(
1806    tree: &mut impl LayoutPartialTree,
1807    line: &mut FlexLine,
1808    total_offset_cross: &mut f32,
1809    #[cfg(feature = "content_size")] content_size: &mut Size<f32>,
1810    container_size: Size<f32>,
1811    node_inner_size: Size<Option<f32>>,
1812    padding_border: Rect<f32>,
1813    direction: FlexDirection,
1814) {
1815    let mut total_offset_main = padding_border.main_start(direction);
1816    let line_offset_cross = line.offset_cross;
1817
1818    if direction.is_reverse() {
1819        for item in line.items.iter_mut().rev() {
1820            calculate_flex_item(
1821                tree,
1822                item,
1823                &mut total_offset_main,
1824                *total_offset_cross,
1825                line_offset_cross,
1826                #[cfg(feature = "content_size")]
1827                content_size,
1828                container_size,
1829                node_inner_size,
1830                direction,
1831            );
1832        }
1833    } else {
1834        for item in line.items.iter_mut() {
1835            calculate_flex_item(
1836                tree,
1837                item,
1838                &mut total_offset_main,
1839                *total_offset_cross,
1840                line_offset_cross,
1841                #[cfg(feature = "content_size")]
1842                content_size,
1843                container_size,
1844                node_inner_size,
1845                direction,
1846            );
1847        }
1848    }
1849
1850    *total_offset_cross += line_offset_cross + line.cross_size;
1851}
1852
1853/// Do a final layout pass and collect the resulting layouts.
1854#[inline]
1855fn final_layout_pass(
1856    tree: &mut impl LayoutPartialTree,
1857    flex_lines: &mut [FlexLine],
1858    constants: &AlgoConstants,
1859) -> Size<f32> {
1860    let mut total_offset_cross = constants.content_box_inset.cross_start(constants.dir);
1861
1862    #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
1863    let mut content_size = Size::ZERO;
1864
1865    if constants.is_wrap_reverse {
1866        for line in flex_lines.iter_mut().rev() {
1867            calculate_layout_line(
1868                tree,
1869                line,
1870                &mut total_offset_cross,
1871                #[cfg(feature = "content_size")]
1872                &mut content_size,
1873                constants.container_size,
1874                constants.node_inner_size,
1875                constants.content_box_inset,
1876                constants.dir,
1877            );
1878        }
1879    } else {
1880        for line in flex_lines.iter_mut() {
1881            calculate_layout_line(
1882                tree,
1883                line,
1884                &mut total_offset_cross,
1885                #[cfg(feature = "content_size")]
1886                &mut content_size,
1887                constants.container_size,
1888                constants.node_inner_size,
1889                constants.content_box_inset,
1890                constants.dir,
1891            );
1892        }
1893    }
1894
1895    content_size
1896}
1897
1898/// Perform absolute layout on all absolutely positioned children.
1899#[inline]
1900fn perform_absolute_layout_on_absolute_children(
1901    tree: &mut impl LayoutPartialTree,
1902    node: NodeId,
1903    constants: &AlgoConstants,
1904) -> Size<f32> {
1905    let container_width = constants.container_size.width;
1906    let container_height = constants.container_size.height;
1907
1908    #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
1909    let mut content_size = Size::ZERO;
1910
1911    for order in 0..tree.child_count(node) {
1912        let child = tree.get_child_id(node, order);
1913        let child_style = tree.get_style(child);
1914
1915        // Skip items that are display:none or are not position:absolute
1916        if child_style.display == Display::None || child_style.position != Position::Absolute {
1917            continue;
1918        }
1919
1920        let overflow = child_style.overflow;
1921        let scrollbar_width = child_style.scrollbar_width;
1922        let aspect_ratio = child_style.aspect_ratio;
1923        let align_self = child_style.align_self.unwrap_or(constants.align_items);
1924        let margin = child_style.margin.map(|margin| margin.resolve_to_option(container_width));
1925        let padding = child_style.padding.resolve_or_zero(Some(container_width));
1926        let border = child_style.border.resolve_or_zero(Some(container_width));
1927        let padding_border_sum = (padding + border).sum_axes();
1928
1929        // Resolve inset
1930        let left = child_style.inset.left.maybe_resolve(container_width);
1931        let right = child_style.inset.right.maybe_resolve(container_width);
1932        let top = child_style.inset.top.maybe_resolve(container_height);
1933        let bottom = child_style.inset.bottom.maybe_resolve(container_height);
1934
1935        // Compute known dimensions from min/max/inherent size styles
1936        let style_size =
1937            child_style.size.maybe_resolve(constants.container_size).maybe_apply_aspect_ratio(aspect_ratio);
1938        let min_size = child_style
1939            .min_size
1940            .maybe_resolve(constants.container_size)
1941            .maybe_apply_aspect_ratio(aspect_ratio)
1942            .or(padding_border_sum.map(Some))
1943            .maybe_max(padding_border_sum);
1944        let max_size =
1945            child_style.max_size.maybe_resolve(constants.container_size).maybe_apply_aspect_ratio(aspect_ratio);
1946        let mut known_dimensions = style_size.maybe_clamp(min_size, max_size);
1947
1948        // Fill in width from left/right and reapply aspect ratio if:
1949        //   - Width is not already known
1950        //   - Item has both left and right inset properties set
1951        if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) {
1952            let new_width_raw = container_width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right;
1953            known_dimensions.width = Some(f32_max(new_width_raw, 0.0));
1954            known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
1955        }
1956
1957        // Fill in height from top/bottom and reapply aspect ratio if:
1958        //   - Height is not already known
1959        //   - Item has both top and bottom inset properties set
1960        if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) {
1961            let new_height_raw = container_height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom;
1962            known_dimensions.height = Some(f32_max(new_height_raw, 0.0));
1963            known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
1964        }
1965
1966        let layout_output = tree.perform_child_layout(
1967            child,
1968            known_dimensions,
1969            constants.node_inner_size,
1970            Size {
1971                width: AvailableSpace::Definite(container_width.maybe_clamp(min_size.width, max_size.width)),
1972                height: AvailableSpace::Definite(container_height.maybe_clamp(min_size.height, max_size.height)),
1973            },
1974            SizingMode::ContentSize,
1975            Line::FALSE,
1976        );
1977        let measured_size = layout_output.size;
1978        let final_size = known_dimensions.unwrap_or(measured_size).maybe_clamp(min_size, max_size);
1979
1980        let non_auto_margin = margin.map(|m| m.unwrap_or(0.0));
1981
1982        let free_space = Size {
1983            width: constants.container_size.width - final_size.width - non_auto_margin.horizontal_axis_sum(),
1984            height: constants.container_size.height - final_size.height - non_auto_margin.vertical_axis_sum(),
1985        }
1986        .f32_max(Size::ZERO);
1987
1988        // Expand auto margins to fill available space
1989        let resolved_margin = {
1990            let auto_margin_size = Size {
1991                width: {
1992                    let auto_margin_count = margin.left.is_none() as u8 + margin.right.is_none() as u8;
1993                    if auto_margin_count > 0 {
1994                        free_space.width / auto_margin_count as f32
1995                    } else {
1996                        0.0
1997                    }
1998                },
1999                height: {
2000                    let auto_margin_count = margin.top.is_none() as u8 + margin.bottom.is_none() as u8;
2001                    if auto_margin_count > 0 {
2002                        free_space.height / auto_margin_count as f32
2003                    } else {
2004                        0.0
2005                    }
2006                },
2007            };
2008
2009            Rect {
2010                left: margin.left.unwrap_or(auto_margin_size.width),
2011                right: margin.right.unwrap_or(auto_margin_size.width),
2012                top: margin.top.unwrap_or(auto_margin_size.height),
2013                bottom: margin.bottom.unwrap_or(auto_margin_size.height),
2014            }
2015        };
2016
2017        // Determine flex-relative insets
2018        let (start_main, end_main) = if constants.is_row { (left, right) } else { (top, bottom) };
2019        let (start_cross, end_cross) = if constants.is_row { (top, bottom) } else { (left, right) };
2020
2021        // Apply main-axis alignment
2022        // let free_main_space = free_space.main(constants.dir) - resolved_margin.main_axis_sum(constants.dir);
2023        let offset_main = if let Some(start) = start_main {
2024            start + constants.border.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2025        } else if let Some(end) = end_main {
2026            constants.container_size.main(constants.dir)
2027                - constants.border.main_end(constants.dir)
2028                - final_size.main(constants.dir)
2029                - end
2030                - resolved_margin.main_end(constants.dir)
2031        } else {
2032            // Stretch is an invalid value for justify_content in the flexbox algorithm, so we
2033            // treat it as if it wasn't set (and thus we default to FlexStart behaviour)
2034            match (constants.justify_content.unwrap_or(JustifyContent::Start), constants.is_wrap_reverse) {
2035                (JustifyContent::SpaceBetween, _)
2036                | (JustifyContent::Start, _)
2037                | (JustifyContent::Stretch, false)
2038                | (JustifyContent::FlexStart, false)
2039                | (JustifyContent::FlexEnd, true) => {
2040                    constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2041                }
2042                (JustifyContent::End, _)
2043                | (JustifyContent::FlexEnd, false)
2044                | (JustifyContent::FlexStart, true)
2045                | (JustifyContent::Stretch, true) => {
2046                    constants.container_size.main(constants.dir)
2047                        - constants.content_box_inset.main_end(constants.dir)
2048                        - final_size.main(constants.dir)
2049                        - resolved_margin.main_end(constants.dir)
2050                }
2051                (JustifyContent::SpaceEvenly, _) | (JustifyContent::SpaceAround, _) | (JustifyContent::Center, _) => {
2052                    (constants.container_size.main(constants.dir)
2053                        + constants.content_box_inset.main_start(constants.dir)
2054                        - constants.content_box_inset.main_end(constants.dir)
2055                        - final_size.main(constants.dir)
2056                        + resolved_margin.main_start(constants.dir)
2057                        - resolved_margin.main_end(constants.dir))
2058                        / 2.0
2059                }
2060            }
2061        };
2062
2063        // Apply cross-axis alignment
2064        // let free_cross_space = free_space.cross(constants.dir) - resolved_margin.cross_axis_sum(constants.dir);
2065        let offset_cross = if let Some(start) = start_cross {
2066            start + constants.border.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2067        } else if let Some(end) = end_cross {
2068            constants.container_size.cross(constants.dir)
2069                - constants.border.cross_end(constants.dir)
2070                - final_size.cross(constants.dir)
2071                - end
2072                - resolved_margin.cross_end(constants.dir)
2073        } else {
2074            match (align_self, constants.is_wrap_reverse) {
2075                // Stretch alignment does not apply to absolutely positioned items
2076                // See "Example 3" at https://www.w3.org/TR/css-flexbox-1/#abspos-items
2077                // Note: Stretch should be FlexStart not Start when we support both
2078                (AlignSelf::Start, _)
2079                | (AlignSelf::Baseline | AlignSelf::Stretch | AlignSelf::FlexStart, false)
2080                | (AlignSelf::FlexEnd, true) => {
2081                    constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2082                }
2083                (AlignSelf::End, _)
2084                | (AlignSelf::Baseline | AlignSelf::Stretch | AlignSelf::FlexStart, true)
2085                | (AlignSelf::FlexEnd, false) => {
2086                    constants.container_size.cross(constants.dir)
2087                        - constants.content_box_inset.cross_end(constants.dir)
2088                        - final_size.cross(constants.dir)
2089                        - resolved_margin.cross_end(constants.dir)
2090                }
2091                (AlignSelf::Center, _) => {
2092                    (constants.container_size.cross(constants.dir)
2093                        + constants.content_box_inset.cross_start(constants.dir)
2094                        - constants.content_box_inset.cross_end(constants.dir)
2095                        - final_size.cross(constants.dir)
2096                        + resolved_margin.cross_start(constants.dir)
2097                        - resolved_margin.cross_end(constants.dir))
2098                        / 2.0
2099                }
2100            }
2101        };
2102
2103        let location = match constants.is_row {
2104            true => Point { x: offset_main, y: offset_cross },
2105            false => Point { x: offset_cross, y: offset_main },
2106        };
2107        let scrollbar_size = Size {
2108            width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
2109            height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
2110        };
2111        tree.set_unrounded_layout(
2112            child,
2113            &Layout {
2114                order: order as u32,
2115                size: final_size,
2116                #[cfg(feature = "content_size")]
2117                content_size: layout_output.content_size,
2118                scrollbar_size,
2119                location,
2120                padding,
2121                border,
2122            },
2123        );
2124
2125        #[cfg(feature = "content_size")]
2126        {
2127            let size_content_size_contribution = Size {
2128                width: match overflow.x {
2129                    Overflow::Visible => f32_max(final_size.width, layout_output.content_size.width),
2130                    _ => final_size.width,
2131                },
2132                height: match overflow.y {
2133                    Overflow::Visible => f32_max(final_size.height, layout_output.content_size.height),
2134                    _ => final_size.height,
2135                },
2136            };
2137            if size_content_size_contribution.has_non_zero_area() {
2138                let content_size_contribution = Size {
2139                    width: location.x + size_content_size_contribution.width,
2140                    height: location.y + size_content_size_contribution.height,
2141                };
2142                content_size = content_size.f32_max(content_size_contribution);
2143            }
2144        }
2145    }
2146
2147    content_size
2148}
2149
2150/// Computes the total space taken up by gaps in an axis given:
2151///   - The size of each gap
2152///   - The number of items (children or flex-lines) between which there are gaps
2153#[inline(always)]
2154fn sum_axis_gaps(gap: f32, num_items: usize) -> f32 {
2155    // Gaps only exist between items, so...
2156    if num_items <= 1 {
2157        // ...if there are less than 2 items then there are no gaps
2158        0.0
2159    } else {
2160        // ...otherwise there are (num_items - 1) gaps
2161        gap * (num_items - 1) as f32
2162    }
2163}
2164
2165#[cfg(test)]
2166mod tests {
2167
2168    use crate::{
2169        geometry::Size,
2170        style::{FlexWrap, Style},
2171        util::{MaybeMath, ResolveOrZero},
2172        TaffyTree,
2173    };
2174
2175    // Make sure we get correct constants
2176    #[test]
2177    fn correct_constants() {
2178        let mut tree: TaffyTree<()> = TaffyTree::with_capacity(16);
2179
2180        let style = Style::default();
2181        let node_id = tree.new_leaf(style.clone()).unwrap();
2182
2183        let node_size = Size::NONE;
2184        let parent_size = Size::NONE;
2185
2186        let constants = super::compute_constants(tree.style(node_id).unwrap(), node_size, parent_size);
2187
2188        assert!(constants.dir == style.flex_direction);
2189        assert!(constants.is_row == style.flex_direction.is_row());
2190        assert!(constants.is_column == style.flex_direction.is_column());
2191        assert!(constants.is_wrap_reverse == (style.flex_wrap == FlexWrap::WrapReverse));
2192
2193        let margin = style.margin.resolve_or_zero(parent_size);
2194        assert_eq!(constants.margin, margin);
2195
2196        let border = style.border.resolve_or_zero(parent_size);
2197        let padding = style.padding.resolve_or_zero(parent_size);
2198        let padding_border = padding + border;
2199        assert_eq!(constants.border, border);
2200        assert_eq!(constants.content_box_inset, padding_border);
2201
2202        let inner_size = Size {
2203            width: node_size.width.maybe_sub(padding_border.horizontal_axis_sum()),
2204            height: node_size.height.maybe_sub(padding_border.vertical_axis_sum()),
2205        };
2206        assert_eq!(constants.node_inner_size, inner_size);
2207
2208        assert_eq!(constants.container_size, Size::zero());
2209        assert_eq!(constants.inner_container_size, Size::zero());
2210    }
2211}