gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 1118HTML 1044Markdown 12TOML 94787 Sutichaykuna

⎇ badc72208966183d5d0d8d9ab8fc5763feeb8670 src/algo.rs Rust · 1285 siq'ikuna · 55.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

use geometry in flexitem props badc72208 · Emil Sjölander ·

1use std::f32;
2
3use crate::layout;
4
5use crate::style;
6use crate::style::{AlignContent, AlignSelf, Dimension, FlexWrap, JustifyContent, PositionType};
7
8use crate::number::Number::*;
9use crate::number::*;
10
11use crate::geometry::{Point, Rect, Size};
12
13#[derive(Debug)]
14struct ComputeResult {
15    size: Size<f32>,
16    children: Vec<layout::Node>,
17}
18
19struct FlexItem<'a> {
20    node: &'a style::Node,
21
22    size: Size<Number>,
23    min_size: Size<Number>,
24    max_size: Size<Number>,
25
26    position: Rect<Number>,
27    margin: Rect<f32>,
28    padding: Rect<f32>,
29    border: Rect<f32>,
30
31    flex_basis: f32,
32    inner_flex_basis: f32,
33    violation: f32,
34    frozen: bool,
35
36    hypothetical_inner_size: Size<f32>,
37    hypothetical_outer_size: Size<f32>,
38    target_size: Size<f32>,
39    outer_target_size: Size<f32>,
40
41    baseline: f32,
42
43    // temporary values for holding offset in the main / cross direction.
44    // offset is the relative position from the item's natural flow position based on
45    // relative position values, alignment, and justification. Does not unclude margin/padding/border.
46    offset_main: f32,
47    offset_cross: f32,
48}
49
50struct FlexLine<'a> {
51    items: Vec<FlexItem<'a>>,
52    cross_size: f32,
53    offset_cross: f32,
54}
55
56pub fn compute(root: &style::Node) -> layout::Node {
57    // TODO - Don't do two passes here just to handle min/max.
58    // Probably want to pass min/max down as top level paramerer instead.
59    let first_pass = compute_internal(
60        root,
61        Size { width: root.size.width.resolve(Undefined), height: root.size.height.resolve(Undefined) },
62        Size { width: Undefined, height: Undefined },
63        Undefined,
64    );
65
66    let result = compute_internal(
67        root,
68        Size {
69            width: first_pass
70                .size
71                .width
72                .maybe_max(root.min_size.width.resolve(Undefined))
73                .maybe_min(root.max_size.width.resolve(Undefined))
74                .to_number(),
75            height: first_pass
76                .size
77                .height
78                .maybe_max(root.min_size.height.resolve(Undefined))
79                .maybe_min(root.max_size.height.resolve(Undefined))
80                .to_number(),
81        },
82        Size { width: Undefined, height: Undefined },
83        Undefined,
84    );
85
86    let mut layout = layout::Node {
87        size: Size { width: result.size.width, height: result.size.height },
88        location: Point { x: 0.0, y: 0.0 },
89        children: result.children,
90    };
91
92    round_layout(&mut layout, 0.0, 0.0);
93    layout
94}
95
96fn round_layout(layout: &mut layout::Node, abs_x: f32, abs_y: f32) {
97    let abs_x = abs_x + layout.location.x;
98    let abs_y = abs_y + layout.location.y;
99
100    layout.location.x = layout.location.x.round();
101    layout.location.y = layout.location.y.round();
102    layout.size.width = (abs_x + layout.size.width).round() - abs_x.round();
103    layout.size.height = (abs_y + layout.size.height).round() - abs_y.round();
104    layout.children.iter_mut().for_each(|child| round_layout(child, abs_x, abs_y));
105}
106
107fn compute_internal(
108    node: &style::Node,
109    node_size: Size<Number>,
110    parent_size: Size<Number>,
111    percent_calc_base: Number,
112) -> ComputeResult {
113    // Define some general constants we will need for the remainder
114    // of the algorithm.
115
116    let is_row = node.flex_direction.is_row();
117    let is_column = node.flex_direction.is_column();
118    let is_wrap_reverse = node.flex_wrap == FlexWrap::WrapReverse;
119
120    let margin = Rect {
121        start: node.margin.start.resolve(percent_calc_base).or_else(0.0),
122        end: node.margin.end.resolve(percent_calc_base).or_else(0.0),
123        top: node.margin.top.resolve(percent_calc_base).or_else(0.0),
124        bottom: node.margin.bottom.resolve(percent_calc_base).or_else(0.0),
125    };
126
127    let padding = Rect {
128        start: node.padding.start.resolve(percent_calc_base).or_else(0.0),
129        end: node.padding.end.resolve(percent_calc_base).or_else(0.0),
130        top: node.padding.top.resolve(percent_calc_base).or_else(0.0),
131        bottom: node.padding.bottom.resolve(percent_calc_base).or_else(0.0),
132    };
133
134    let border = Rect {
135        start: node.border.start.resolve(percent_calc_base).or_else(0.0),
136        end: node.border.end.resolve(percent_calc_base).or_else(0.0),
137        top: node.border.top.resolve(percent_calc_base).or_else(0.0),
138        bottom: node.border.bottom.resolve(percent_calc_base).or_else(0.0),
139    };
140
141    let padding_border = Rect {
142        start: padding.start + border.start,
143        end: padding.end + border.end,
144        top: padding.top + border.top,
145        bottom: padding.bottom + border.bottom,
146    };
147
148    let node_inner_size = Size {
149        width: node_size.width - padding_border.horizontal(),
150        height: node_size.height - padding_border.vertical(),
151    };
152
153    let percent_calc_base_child = node_inner_size.width;
154
155    // 9.2. Line Length Determination
156
157    // 1. Generate anonymous flex items as described in §4 Flex Items.
158
159    // 2. Determine the available main and cross space for the flex items.
160    //    For each dimension, if that dimension of the flex container’s content box
161    //    is a definite size, use that; if that dimension of the flex container is
162    //    being sized under a min or max-content constraint, the available space in
163    //    that dimension is that constraint; otherwise, subtract the flex container’s
164    //    margin, border, and padding from the space available to the flex container
165    //    in that dimension and use that value. This might result in an infinite value.
166
167    let available_space = Size {
168        width: node_size.width.or_else(parent_size.width - margin.horizontal()) - padding_border.horizontal(),
169        height: node_size.height.or_else(parent_size.height - margin.vertical()) - padding_border.vertical(),
170    };
171
172    let mut flex_items: Vec<FlexItem> = node
173        .children
174        .iter()
175        .filter(|child| child.position_type != PositionType::Absolute)
176        .map(|child| FlexItem {
177            node: child,
178
179            size: Size {
180                width: child.size.width.resolve(percent_calc_base_child),
181                height: child.size.height.resolve(percent_calc_base_child),
182            },
183
184            min_size: Size {
185                width: child.min_size.width.resolve(percent_calc_base_child),
186                height: child.min_size.height.resolve(percent_calc_base_child),
187            },
188
189            max_size: Size {
190                width: child.max_size.width.resolve(percent_calc_base_child),
191                height: child.max_size.height.resolve(percent_calc_base_child),
192            },
193
194            position: Rect {
195                start: child.position.start.resolve(percent_calc_base_child),
196                end: child.position.end.resolve(percent_calc_base_child),
197                top: child.position.top.resolve(percent_calc_base_child),
198                bottom: child.position.bottom.resolve(percent_calc_base_child),
199            },
200
201            margin: Rect {
202                start: child.margin.start.resolve(percent_calc_base_child).or_else(0.0),
203                end: child.margin.end.resolve(percent_calc_base_child).or_else(0.0),
204                top: child.margin.top.resolve(percent_calc_base_child).or_else(0.0),
205                bottom: child.margin.bottom.resolve(percent_calc_base_child).or_else(0.0),
206            },
207
208            padding: Rect {
209                start: child.padding.start.resolve(percent_calc_base_child).or_else(0.0),
210                end: child.padding.end.resolve(percent_calc_base_child).or_else(0.0),
211                top: child.padding.top.resolve(percent_calc_base_child).or_else(0.0),
212                bottom: child.padding.bottom.resolve(percent_calc_base_child).or_else(0.0),
213            },
214
215            border: Rect {
216                start: child.border.start.resolve(percent_calc_base_child).or_else(0.0),
217                end: child.border.end.resolve(percent_calc_base_child).or_else(0.0),
218                top: child.border.top.resolve(percent_calc_base_child).or_else(0.0),
219                bottom: child.border.bottom.resolve(percent_calc_base_child).or_else(0.0),
220            },
221
222            flex_basis: 0.0,
223            inner_flex_basis: 0.0,
224            violation: 0.0,
225            frozen: false,
226
227            hypothetical_inner_size: Size { width: 0.0, height: 0.0 },
228            hypothetical_outer_size: Size { width: 0.0, height: 0.0 },
229            target_size: Size { width: 0.0, height: 0.0 },
230            outer_target_size: Size { width: 0.0, height: 0.0 },
231
232            baseline: 0.0,
233
234            offset_main: 0.0,
235            offset_cross: 0.0,
236        })
237        .collect();
238
239    // TODO - this does not follow spec. See commented out code below
240    // 3. Determine the flex base size and hypothetical main size of each item:
241    flex_items.iter_mut().for_each(|child| {
242        // A. If the item has a definite used flex basis, that’s the flex base size.
243
244        let flex_basis = child.node.flex_basis.resolve(percent_calc_base_child);
245        if flex_basis.is_defined() {
246            child.flex_basis = flex_basis.or_else(0.0);
247            return;
248        };
249
250        // B. If the flex item has an intrinsic aspect ratio,
251        //    a used flex basis of content, and a definite cross size,
252        //    then the flex base size is calculated from its inner
253        //    cross size and the flex item’s intrinsic aspect ratio.
254
255        if let Defined(ratio) = child.node.aspect_ratio {
256            if let Defined(cross) = node_size.cross(node.flex_direction) {
257                if child.node.flex_basis == Dimension::Auto {
258                    child.flex_basis = cross * ratio;
259                    return;
260                }
261            }
262        }
263
264        // C. If the used flex basis is content or depends on its available space,
265        //    and the flex container is being sized under a min-content or max-content
266        //    constraint (e.g. when performing automatic table layout [CSS21]),
267        //    size the item under that constraint. The flex base size is the item’s
268        //    resulting main size.
269
270        // TODO - Probably need to cover this case in future
271
272        // D. Otherwise, if the used flex basis is content or depends on its
273        //    available space, the available main size is infinite, and the flex item’s
274        //    inline axis is parallel to the main axis, lay the item out using the rules
275        //    for a box in an orthogonal flow [CSS3-WRITING-MODES]. The flex base size
276        //    is the item’s max-content main size.
277
278        // TODO - Probably need to cover this case in future
279
280        // E. Otherwise, size the item into the available space using its used flex basis
281        //    in place of its main size, treating a value of content as max-content.
282        //    If a cross size is needed to determine the main size (e.g. when the
283        //    flex item’s main size is in its block axis) and the flex item’s cross size
284        //    is auto and not definite, in this calculation use fit-content as the
285        //    flex item’s cross size. The flex base size is the item’s resulting main size.
286
287        child.flex_basis = compute_internal(
288            child.node,
289            Size {
290                width: child.size.width.maybe_max(child.min_size.width).maybe_min(child.max_size.width),
291                height: child.size.height.maybe_max(child.min_size.height).maybe_min(child.max_size.height),
292            },
293            available_space,
294            percent_calc_base_child,
295        )
296        .size
297        .main(node.flex_direction)
298        .maybe_max(child.min_size.main(node.flex_direction))
299        .maybe_min(child.max_size.main(node.flex_direction));
300    });
301
302    // The hypothetical main size is the item’s flex base size clamped according to its
303    // used min and max main sizes (and flooring the content box size at zero).
304
305    flex_items.iter_mut().for_each(|child| {
306        child.inner_flex_basis =
307            child.flex_basis - child.padding.main(node.flex_direction) - child.border.main(node.flex_direction);
308
309        // TODO - not really spec abiding but needs to be done somewhere. probably somewhere else though.
310        // The following logic was developed not from the spec but by trail and error looking into how
311        // webkit handled various scenarios. Can probably be solved better by passing in
312        // min-content max-content constraints fromt the top
313        let min_main = if is_row {
314            compute_internal(
315                child.node,
316                Size { width: Undefined, height: Undefined },
317                available_space,
318                percent_calc_base_child,
319            )
320            .size
321            .width
322            .maybe_max(child.min_size.width)
323            .maybe_min(child.size.width)
324            .to_number()
325        } else {
326            child.min_size.main(node.flex_direction)
327        };
328
329        child.hypothetical_inner_size.set_main(
330            node.flex_direction,
331            child.flex_basis.maybe_max(min_main).maybe_min(child.max_size.main(node.flex_direction)),
332        );
333
334        child.hypothetical_outer_size.set_main(
335            node.flex_direction,
336            child.hypothetical_inner_size.main(node.flex_direction) + child.margin.main(node.flex_direction),
337        );
338    });
339
340    // 9.3. Main Size Determination
341
342    // 5. Collect flex items into flex lines:
343    //    - If the flex container is single-line, collect all the flex items into
344    //      a single flex line.
345    //    - Otherwise, starting from the first uncollected item, collect consecutive
346    //      items one by one until the first time that the next collected item would
347    //      not fit into the flex container’s inner main size (or until a forced break
348    //      is encountered, see §10 Fragmenting Flex Layout). If the very first
349    //      uncollected item wouldn’t fit, collect just it into the line.
350    //
351    //      For this step, the size of a flex item is its outer hypothetical main size. (Note: This can be negative.)
352    //      Repeat until all flex items have been collected into flex lines
353    //
354    //      Note that the "collect as many" line will collect zero-sized flex items onto
355    //      the end of the previous line even if the last non-zero item exactly "filled up" the line.
356
357    let mut flex_lines = {
358        let mut lines: Vec<FlexLine> = vec![];
359        let mut line_length = 0.0;
360
361        if node.flex_wrap == FlexWrap::NoWrap {
362            lines.push(FlexLine { items: flex_items, cross_size: 0.0, offset_cross: 0.0 });
363        } else {
364            let mut line = FlexLine { items: vec![], cross_size: 0.0, offset_cross: 0.0 };
365
366            for child in flex_items {
367                line_length += child.hypothetical_outer_size.main(node.flex_direction);
368
369                if let Defined(main) = available_space.main(node.flex_direction) {
370                    if line_length > main && !line.items.is_empty() {
371                        line_length = child.hypothetical_outer_size.main(node.flex_direction);
372                        lines.push(line);
373                        line = FlexLine { items: vec![], cross_size: 0.0, offset_cross: 0.0 };
374                    }
375                }
376
377                line.items.push(child);
378            }
379
380            lines.push(line);
381        }
382
383        lines
384    };
385
386    // 6. Resolve the flexible lengths of all the flex items to find their used main size.
387    //    See §9.7 Resolving Flexible Lengths.
388    //
389    // 9.7. Resolving Flexible Lengths
390
391    flex_lines.iter_mut().for_each(|line| {
392        // 1. Determine the used flex factor. Sum the outer hypothetical main sizes of all
393        //    items on the line. If the sum is less than the flex container’s inner main size,
394        //    use the flex grow factor for the rest of this algorithm; otherwise, use the
395        //    flex shrink factor.
396
397        let used_flex_factor: f32 =
398            line.items.iter().map(|child| child.hypothetical_outer_size.main(node.flex_direction)).sum();
399        let growing = used_flex_factor < node_inner_size.main(node.flex_direction).or_else(0.0);
400        let shrinking = !growing;
401
402        // 2. Size inflexible items. Freeze, setting its target main size to its hypothetical main size
403        //    - Any item that has a flex factor of zero
404        //    - If using the flex grow factor: any item that has a flex base size
405        //      greater than its hypothetical main size
406        //    - If using the flex shrink factor: any item that has a flex base size
407        //      smaller than its hypothetical main size
408
409        line.items.iter_mut().for_each(|child| {
410            // TODO - This is not found by reading the spec. Maybe this can be done in some other place
411            // instead. This was found by trail and error fixing tests to align with webkit output.
412            if node_inner_size.main(node.flex_direction).is_undefined() && is_row {
413                child.target_size.set_main(
414                    node.flex_direction,
415                    compute_internal(
416                        child.node,
417                        Size {
418                            width: child.size.width.maybe_max(child.min_size.width).maybe_min(child.max_size.width),
419                            height: child.size.height.maybe_max(child.min_size.height).maybe_min(child.max_size.height),
420                        },
421                        available_space,
422                        percent_calc_base_child,
423                    )
424                    .size
425                    .main(node.flex_direction)
426                    .maybe_max(child.min_size.main(node.flex_direction))
427                    .maybe_min(child.max_size.main(node.flex_direction)),
428                );
429            } else {
430                child
431                    .target_size
432                    .set_main(node.flex_direction, child.hypothetical_inner_size.main(node.flex_direction));
433            }
434
435            // TODO this should really only be set inside the if-statement below but
436            // that casues the target_main_size to never be set for some items
437
438            child.outer_target_size.set_main(
439                node.flex_direction,
440                child.target_size.main(node.flex_direction) + child.margin.main(node.flex_direction),
441            );
442
443            if (child.node.flex_grow == 0.0 && child.node.flex_shrink == 0.0)
444                || (growing && child.flex_basis > child.hypothetical_inner_size.main(node.flex_direction))
445                || (shrinking && child.flex_basis < child.hypothetical_inner_size.main(node.flex_direction))
446            {
447                child.frozen = true;
448            }
449        });
450
451        // 3. Calculate initial free space. Sum the outer sizes of all items on the line,
452        //    and subtract this from the flex container’s inner main size. For frozen items,
453        //    use their outer target main size; for other items, use their outer flex base size.
454
455        let used_space: f32 = line
456            .items
457            .iter()
458            .map(|child| {
459                child.margin.main(node.flex_direction)
460                    + if child.frozen { child.target_size.main(node.flex_direction) } else { child.flex_basis }
461            })
462            .sum();
463
464        let initial_free_space = (node_inner_size.main(node.flex_direction) - used_space).or_else(0.0);
465
466        // 4. Loop
467
468        loop {
469            // a. Check for flexible items. If all the flex items on the line are frozen,
470            //    free space has been distributed; exit this loop.
471
472            let mut frozen: Vec<&mut FlexItem> = vec![];
473            let mut unfrozen: Vec<&mut FlexItem> = vec![];
474
475            line.items.iter_mut().for_each(|child| {
476                if child.frozen {
477                    frozen.push(child);
478                } else {
479                    unfrozen.push(child);
480                }
481            });
482
483            if unfrozen.is_empty() {
484                break;
485            }
486
487            // b. Calculate the remaining free space as for initial free space, above.
488            //    If the sum of the unfrozen flex items’ flex factors is less than one,
489            //    multiply the initial free space by this sum. If the magnitude of this
490            //    value is less than the magnitude of the remaining free space, use this
491            //    as the remaining free space.
492
493            let used_space: f32 = Iterator::chain(frozen.iter(), unfrozen.iter())
494                .map(|child| {
495                    child.margin.main(node.flex_direction)
496                        + if child.frozen { child.target_size.main(node.flex_direction) } else { child.flex_basis }
497                })
498                .sum();
499
500            let sum_flex_grow: f32 = unfrozen.iter().map(|item| item.node.flex_grow).sum();
501            let sum_flex_shrink: f32 = unfrozen.iter().map(|item| item.node.flex_shrink).sum();
502
503            let free_space = if growing && sum_flex_grow < 1.0 {
504                (initial_free_space * sum_flex_grow).maybe_min(node_inner_size.main(node.flex_direction) - used_space)
505            } else if shrinking && sum_flex_shrink < 1.0 {
506                (initial_free_space * sum_flex_shrink).maybe_max(node_inner_size.main(node.flex_direction) - used_space)
507            } else {
508                (node_inner_size.main(node.flex_direction) - used_space).or_else(0.0)
509            };
510
511            // c. Distribute free space proportional to the flex factors.
512            //    - If the remaining free space is zero
513            //        Do Nothing
514            //    - If using the flex grow factor
515            //        Find the ratio of the item’s flex grow factor to the sum of the
516            //        flex grow factors of all unfrozen items on the line. Set the item’s
517            //        target main size to its flex base size plus a fraction of the remaining
518            //        free space proportional to the ratio.
519            //    - If using the flex shrink factor
520            //        For every unfrozen item on the line, multiply its flex shrink factor by
521            //        its inner flex base size, and note this as its scaled flex shrink factor.
522            //        Find the ratio of the item’s scaled flex shrink factor to the sum of the
523            //        scaled flex shrink factors of all unfrozen items on the line. Set the item’s
524            //        target main size to its flex base size minus a fraction of the absolute value
525            //        of the remaining free space proportional to the ratio. Note this may result
526            //        in a negative inner main size; it will be corrected in the next step.
527            //    - Otherwise
528            //        Do Nothing
529
530            if free_space.is_normal() {
531                if growing && sum_flex_grow > 0.0 {
532                    unfrozen.iter_mut().for_each(|child| {
533                        child.target_size.set_main(
534                            node.flex_direction,
535                            child.flex_basis + free_space * (child.node.flex_grow / sum_flex_grow),
536                        );
537                    });
538                } else if shrinking && sum_flex_shrink > 0.0 {
539                    let sum_scaled_shrink_factor: f32 =
540                        unfrozen.iter().map(|child| child.inner_flex_basis * child.node.flex_shrink).sum();
541
542                    if sum_scaled_shrink_factor > 0.0 {
543                        unfrozen.iter_mut().for_each(|child| {
544                            let scaled_shrink_factor = child.inner_flex_basis * child.node.flex_shrink;
545                            child.target_size.set_main(
546                                node.flex_direction,
547                                child.flex_basis + free_space * (scaled_shrink_factor / sum_scaled_shrink_factor),
548                            )
549                        });
550                    }
551                }
552            }
553
554            // d. Fix min/max violations. Clamp each non-frozen item’s target main size by its
555            //    used min and max main sizes and floor its content-box size at zero. If the
556            //    item’s target main size was made smaller by this, it’s a max violation.
557            //    If the item’s target main size was made larger by this, it’s a min violation.
558
559            let total_violation = unfrozen.iter_mut().fold(0.0, |acc, child| {
560                // TODO - not really spec abiding but needs to be done somewhere. probably somewhere else though.
561                // The following logic was developed not from the spec but by trail and error looking into how
562                // webkit handled various scenarios. Can probably be solved better by passing in
563                // min-content max-content constraints fromt the top
564                let min_main = if is_row {
565                    compute_internal(
566                        child.node,
567                        Size { width: Undefined, height: Undefined },
568                        available_space,
569                        percent_calc_base_child,
570                    )
571                    .size
572                    .width
573                    .maybe_min(child.size.width)
574                    .maybe_max(child.min_size.width)
575                    .to_number()
576                } else {
577                    child.min_size.main(node.flex_direction)
578                };
579
580                let max_main = child.max_size.main(node.flex_direction);
581                let clamped =
582                    child.target_size.main(node.flex_direction).maybe_min(max_main).maybe_max(min_main).max(0.0);
583                child.violation = clamped - child.target_size.main(node.flex_direction);
584                child.target_size.set_main(node.flex_direction, clamped);
585                child.outer_target_size.set_main(
586                    node.flex_direction,
587                    child.target_size.main(node.flex_direction) + child.margin.main(node.flex_direction),
588                );
589
590                acc + child.violation
591            });
592
593            // e. Freeze over-flexed items. The total violation is the sum of the adjustments
594            //    from the previous step ∑(clamped size - unclamped size). If the total violation is:
595            //    - Zero
596            //        Freeze all items.
597            //    - Positive
598            //        Freeze all the items with min violations.
599            //    - Negative
600            //        Freeze all the items with max violations.
601
602            unfrozen.iter_mut().for_each(|child| match total_violation {
603                v if v > 0.0 => child.frozen = child.violation > 0.0,
604                v if v < 0.0 => child.frozen = child.violation < 0.0,
605                _ => child.frozen = true,
606            })
607
608            // f. Return to the start of this loop.
609        }
610    });
611
612    // Not part of the spec from what i can see but seems correct
613    let container_main_size = node_size.main(node.flex_direction).or_else({
614        let longest_line = flex_lines.iter().fold(f32::MIN, |acc, line| {
615            let length: f32 = line.items.iter().map(|item| item.outer_target_size.main(node.flex_direction)).sum();
616            acc.max(length)
617        });
618
619        let size = longest_line + padding_border.main(node.flex_direction);
620        match available_space.main(node.flex_direction) {
621            Defined(val) if flex_lines.len() > 1 && size < val => val,
622            _ => size,
623        }
624    });
625
626    let inner_container_main_size = container_main_size - padding_border.main(node.flex_direction);
627
628    // 9.4. Cross Size Determination
629
630    // 7. Determine the hypothetical cross size of each item by performing layout with the
631    //    used main size and the available space, treating auto as fit-content.
632
633    flex_lines.iter_mut().for_each(|line| {
634        line.items.iter_mut().for_each(|child| {
635            let child_cross = child
636                .size
637                .cross(node.flex_direction)
638                .maybe_max(child.min_size.cross(node.flex_direction))
639                .maybe_min(child.max_size.cross(node.flex_direction));
640
641            child.hypothetical_inner_size.set_cross(
642                node.flex_direction,
643                compute_internal(
644                    child.node,
645                    Size {
646                        width: if is_row {
647                            child.target_size.main(node.flex_direction).to_number()
648                        } else {
649                            child_cross
650                        },
651                        height: if is_row {
652                            child_cross
653                        } else {
654                            child.target_size.main(node.flex_direction).to_number()
655                        },
656                    },
657                    Size {
658                        width: if is_row { container_main_size.to_number() } else { available_space.width },
659                        height: if is_row { available_space.height } else { container_main_size.to_number() },
660                    },
661                    percent_calc_base_child,
662                )
663                .size
664                .cross(node.flex_direction)
665                .maybe_max(child.min_size.cross(node.flex_direction))
666                .maybe_min(child.max_size.cross(node.flex_direction)),
667            );
668
669            child.hypothetical_outer_size.set_cross(
670                node.flex_direction,
671                child.hypothetical_inner_size.cross(node.flex_direction) + child.margin.cross(node.flex_direction),
672            );
673        });
674    });
675
676    // TODO - probably should move this somewhere else as it doesn't make a ton of sense here but we need it below
677    // TODO - This is expensive and should only be done if we really require a baseline. aka, make it lazy
678
679    fn calc_baseline(layout: &layout::Node) -> f32 {
680        if layout.children.is_empty() {
681            layout.size.height
682        } else {
683            calc_baseline(&layout.children[0])
684        }
685    };
686
687    flex_lines.iter_mut().for_each(|line| {
688        line.items.iter_mut().for_each(|child| {
689            let result = compute_internal(
690                child.node,
691                Size {
692                    width: if is_row {
693                        child.target_size.main(node.flex_direction).to_number()
694                    } else {
695                        child.hypothetical_inner_size.cross(node.flex_direction).to_number()
696                    },
697                    height: if is_row {
698                        child.hypothetical_inner_size.cross(node.flex_direction).to_number()
699                    } else {
700                        child.target_size.main(node.flex_direction).to_number()
701                    },
702                },
703                Size {
704                    width: if is_row { container_main_size.to_number() } else { node_size.cross(node.flex_direction) },
705                    height: if is_row { node_size.cross(node.flex_direction) } else { container_main_size.to_number() },
706                },
707                percent_calc_base_child,
708            );
709            child.baseline = calc_baseline(&layout::Node {
710                size: result.size,
711                location: Point { x: 0.0, y: 0.0 },
712                children: result.children,
713            });
714        });
715    });
716
717    // 8. Calculate the cross size of each flex line.
718    //    If the flex container is single-line and has a definite cross size, the cross size
719    //    of the flex line is the flex container’s inner cross size. Otherwise, for each flex line:
720    //
721    //    If the flex container is single-line, then clamp the line’s cross-size to be within
722    //    the container’s computed min and max cross sizes. Note that if CSS 2.1’s definition
723    //    of min/max-width/height applied more generally, this behavior would fall out automatically.
724
725    if flex_lines.len() == 1 && node_size.cross(node.flex_direction).is_defined() {
726        flex_lines[0].cross_size =
727            (node_size.cross(node.flex_direction) - padding_border.cross(node.flex_direction)).or_else(0.0);
728    } else {
729        flex_lines.iter_mut().for_each(|line| {
730            //    1. Collect all the flex items whose inline-axis is parallel to the main-axis, whose
731            //       align-self is baseline, and whose cross-axis margins are both non-auto. Find the
732            //       largest of the distances between each item’s baseline and its hypothetical outer
733            //       cross-start edge, and the largest of the distances between each item’s baseline
734            //       and its hypothetical outer cross-end edge, and sum these two values.
735
736            //    2. Among all the items not collected by the previous step, find the largest
737            //       outer hypothetical cross size.
738
739            //    3. The used cross-size of the flex line is the largest of the numbers found in the
740            //       previous two steps and zero.
741
742            let max_baseline: f32 = line.items.iter().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x));
743            line.cross_size = line
744                .items
745                .iter()
746                .map(|child| {
747                    if child.node.align_self(node) == AlignSelf::Baseline
748                        && child.node.cross_margin_start(node.flex_direction) != Dimension::Auto
749                        && child.node.cross_margin_end(node.flex_direction) != Dimension::Auto
750                        && child.node.cross_size(node.flex_direction) == Dimension::Auto
751                    {
752                        max_baseline - child.baseline + child.hypothetical_outer_size.cross(node.flex_direction)
753                    } else {
754                        child.hypothetical_outer_size.cross(node.flex_direction)
755                    }
756                })
757                .fold(0.0, |acc, x| acc.max(x));
758        });
759    }
760
761    // 9. Handle 'align-content: stretch'. If the flex container has a definite cross size,
762    //    align-content is stretch, and the sum of the flex lines' cross sizes is less than
763    //    the flex container’s inner cross size, increase the cross size of each flex line
764    //    by equal amounts such that the sum of their cross sizes exactly equals the
765    //    flex container’s inner cross size.
766
767    if node.align_content == AlignContent::Stretch && node_size.cross(node.flex_direction).is_defined() {
768        let total_cross: f32 = flex_lines.iter().map(|line| line.cross_size).sum();
769        let inner_cross =
770            (node_size.cross(node.flex_direction) - padding_border.cross(node.flex_direction)).or_else(0.0);
771
772        if total_cross < inner_cross {
773            let remaining = inner_cross - total_cross;
774            let addition = remaining / flex_lines.len() as f32;
775            flex_lines.iter_mut().for_each(|line| line.cross_size += addition);
776        }
777    }
778
779    // 10. Collapse visibility:collapse items. If any flex items have visibility: collapse,
780    //     note the cross size of the line they’re in as the item’s strut size, and restart
781    //     layout from the beginning.
782    //
783    //     In this second layout round, when collecting items into lines, treat the collapsed
784    //     items as having zero main size. For the rest of the algorithm following that step,
785    //     ignore the collapsed items entirely (as if they were display:none) except that after
786    //     calculating the cross size of the lines, if any line’s cross size is less than the
787    //     largest strut size among all the collapsed items in the line, set its cross size to
788    //     that strut size.
789    //
790    //     Skip this step in the second layout round.
791
792    // TODO implement once (if ever) we support visibility:collapse
793
794    // 11. Determine the used cross size of each flex item. If a flex item has align-self: stretch,
795    //     its computed cross size property is auto, and neither of its cross-axis margins are auto,
796    //     the used outer cross size is the used cross size of its flex line, clamped according to
797    //     the item’s used min and max cross sizes. Otherwise, the used cross size is the item’s
798    //     hypothetical cross size.
799    //
800    //     If the flex item has align-self: stretch, redo layout for its contents, treating this
801    //     used size as its definite cross size so that percentage-sized children can be resolved.
802    //
803    //     Note that this step does not affect the main size of the flex item, even if it has an
804    //     intrinsic aspect ratio.
805
806    flex_lines.iter_mut().for_each(|line| {
807        let line_cross_size = line.cross_size;
808
809        line.items.iter_mut().for_each(|child| {
810            child.target_size.set_cross(
811                node.flex_direction,
812                if child.node.align_self(node) == AlignSelf::Stretch
813                    && child.node.cross_margin_start(node.flex_direction) != Dimension::Auto
814                    && child.node.cross_margin_end(node.flex_direction) != Dimension::Auto
815                    && child.node.cross_size(node.flex_direction) == Dimension::Auto
816                {
817                    (line_cross_size - child.margin.cross(node.flex_direction))
818                        .maybe_max(child.min_size.cross(node.flex_direction))
819                        .maybe_min(child.max_size.cross(node.flex_direction))
820                } else {
821                    child.hypothetical_inner_size.cross(node.flex_direction)
822                },
823            );
824
825            child.outer_target_size.set_cross(
826                node.flex_direction,
827                child.target_size.cross(node.flex_direction) + child.margin.cross(node.flex_direction),
828            );
829        });
830    });
831
832    // 9.5. Main-Axis Alignment
833
834    // 12. Distribute any remaining free space. For each flex line:
835    //     1. If the remaining free space is positive and at least one main-axis margin on this
836    //        line is auto, distribute the free space equally among these margins. Otherwise,
837    //        set all auto margins to zero.
838    //     2. Align the items along the main-axis per justify-content.
839
840    flex_lines.iter_mut().for_each(|line| {
841        let used_space: f32 = line.items.iter().map(|child| child.outer_target_size.main(node.flex_direction)).sum();
842        let free_space = inner_container_main_size - used_space;
843        let mut num_auto_margins = 0;
844
845        line.items.iter_mut().for_each(|child| {
846            if child.node.main_margin_start(node.flex_direction) == Dimension::Auto {
847                num_auto_margins += 1;
848            }
849            if child.node.main_margin_end(node.flex_direction) == Dimension::Auto {
850                num_auto_margins += 1;
851            }
852        });
853
854        if free_space > 0.0 && num_auto_margins > 0 {
855            let margin = free_space / num_auto_margins as f32;
856
857            line.items.iter_mut().for_each(|child| {
858                if child.node.main_margin_start(node.flex_direction) == Dimension::Auto {
859                    if is_row {
860                        child.margin.start = margin;
861                    } else {
862                        child.margin.top = margin;
863                    }
864                }
865                if child.node.main_margin_end(node.flex_direction) == Dimension::Auto {
866                    if is_row {
867                        child.margin.end = margin;
868                    } else {
869                        child.margin.bottom = margin;
870                    }
871                }
872            });
873        } else {
874            let num_items = line.items.len();
875            let layout_reverse = node.flex_direction.is_reverse();
876
877            let justify_item = |(i, child): (usize, &mut FlexItem)| {
878                let is_first = i == 0;
879
880                child.offset_main = match node.justify_content {
881                    JustifyContent::FlexStart => {
882                        if layout_reverse && is_first {
883                            free_space
884                        } else {
885                            0.0
886                        }
887                    }
888                    JustifyContent::Center => {
889                        if is_first {
890                            free_space / 2.0
891                        } else {
892                            0.0
893                        }
894                    }
895                    JustifyContent::FlexEnd => {
896                        if is_first && !layout_reverse {
897                            free_space
898                        } else {
899                            0.0
900                        }
901                    }
902                    JustifyContent::SpaceBetween => {
903                        if is_first {
904                            0.0
905                        } else {
906                            free_space / (num_items - 1) as f32
907                        }
908                    }
909                    JustifyContent::SpaceAround => {
910                        if is_first {
911                            (free_space / num_items as f32) / 2.0
912                        } else {
913                            free_space / num_items as f32
914                        }
915                    }
916                    JustifyContent::SpaceEvenly => free_space / (num_items + 1) as f32,
917                };
918            };
919
920            if layout_reverse {
921                line.items.iter_mut().rev().enumerate().for_each(justify_item);
922            } else {
923                line.items.iter_mut().enumerate().for_each(justify_item);
924            }
925        }
926    });
927
928    // 9.6. Cross-Axis Alignment
929
930    // 13. Resolve cross-axis auto margins. If a flex item has auto cross-axis margins:
931    //     - If its outer cross size (treating those auto margins as zero) is less than the
932    //       cross size of its flex line, distribute the difference in those sizes equally
933    //       to the auto margins.
934    //     - Otherwise, if the block-start or inline-start margin (whichever is in the cross axis)
935    //       is auto, set it to zero. Set the opposite margin so that the outer cross size of the
936    //       item equals the cross size of its flex line.
937
938    flex_lines.iter_mut().for_each(|line| {
939        let line_cross_size = line.cross_size;
940        let max_baseline: f32 = line.items.iter_mut().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x));
941
942        line.items.iter_mut().for_each(|child| {
943            let free_space = line_cross_size - child.outer_target_size.cross(node.flex_direction);
944
945            if child.node.cross_margin_start(node.flex_direction) == Dimension::Auto
946                && child.node.cross_margin_end(node.flex_direction) == Dimension::Auto
947            {
948                if is_row {
949                    child.margin.top = free_space / 2.0;
950                    child.margin.bottom = free_space / 2.0;
951                } else {
952                    child.margin.start = free_space / 2.0;
953                    child.margin.end = free_space / 2.0;
954                }
955            } else if child.node.cross_margin_start(node.flex_direction) == Dimension::Auto {
956                if is_row {
957                    child.margin.top = free_space;
958                } else {
959                    child.margin.start = free_space;
960                }
961            } else if child.node.cross_margin_end(node.flex_direction) == Dimension::Auto {
962                if is_row {
963                    child.margin.bottom = free_space;
964                } else {
965                    child.margin.end = free_space;
966                }
967            } else {
968                // 14. Align all flex items along the cross-axis per align-self, if neither of the item’s
969                //     cross-axis margins are auto.
970
971                child.offset_cross = match child.node.align_self(node) {
972                    AlignSelf::Auto => 0.0, // Should never happen
973                    AlignSelf::FlexStart => {
974                        if is_wrap_reverse {
975                            free_space
976                        } else {
977                            0.0
978                        }
979                    }
980                    AlignSelf::FlexEnd => {
981                        if is_wrap_reverse {
982                            0.0
983                        } else {
984                            free_space
985                        }
986                    }
987                    AlignSelf::Center => free_space / 2.0,
988                    AlignSelf::Baseline => {
989                        if is_row {
990                            max_baseline - child.baseline
991                        } else {
992                            // basline alignment only makes sense if the direction is row
993                            // we treat it as flex-start alignment in columns.
994                            if is_wrap_reverse {
995                                free_space
996                            } else {
997                                0.0
998                            }
999                        }
1000                    }
1001                    AlignSelf::Stretch => {
1002                        if is_wrap_reverse {
1003                            free_space
1004                        } else {
1005                            0.0
1006                        }
1007                    }
1008                };
1009            }
1010        });
1011    });
1012
1013    // 15. Determine the flex container’s used cross size:
1014    //     - If the cross size property is a definite size, use that, clamped by the used
1015    //       min and max cross sizes of the flex container.
1016    //     - Otherwise, use the sum of the flex lines' cross sizes, clamped by the used
1017    //       min and max cross sizes of the flex container.
1018
1019    let total_cross_size: f32 = flex_lines.iter().map(|line| line.cross_size).sum();
1020    let container_cross_size =
1021        node_size.cross(node.flex_direction).or_else(total_cross_size + padding_border.cross(node.flex_direction));
1022    let inner_container_cross_size = container_cross_size - padding_border.cross(node.flex_direction);
1023
1024    // 16. Align all flex lines per align-content.
1025
1026    let free_space = inner_container_cross_size - total_cross_size;
1027    let num_lines = flex_lines.len();
1028
1029    let align_line = |(i, line): (usize, &mut FlexLine)| {
1030        let is_first = i == 0;
1031
1032        line.offset_cross = match node.align_content {
1033            AlignContent::FlexStart => {
1034                if is_first && is_wrap_reverse {
1035                    free_space
1036                } else {
1037                    0.0
1038                }
1039            }
1040            AlignContent::FlexEnd => {
1041                if is_first && !is_wrap_reverse {
1042                    free_space
1043                } else {
1044                    0.0
1045                }
1046            }
1047            AlignContent::Center => {
1048                if is_first {
1049                    free_space / 2.0
1050                } else {
1051                    0.0
1052                }
1053            }
1054            AlignContent::Stretch => 0.0,
1055            AlignContent::SpaceBetween => {
1056                if is_first {
1057                    0.0
1058                } else {
1059                    free_space / (num_lines - 1) as f32
1060                }
1061            }
1062            AlignContent::SpaceAround => {
1063                if is_first {
1064                    (free_space / num_lines as f32) / 2.0
1065                } else {
1066                    free_space / num_lines as f32
1067                }
1068            }
1069        };
1070    };
1071
1072    if is_wrap_reverse {
1073        flex_lines.iter_mut().rev().enumerate().for_each(align_line);
1074    } else {
1075        flex_lines.iter_mut().enumerate().for_each(align_line);
1076    }
1077
1078    let container_width = if is_row { container_main_size } else { container_cross_size };
1079    let container_height = if is_column { container_main_size } else { container_cross_size };
1080
1081    // Do a final layout pass and gather the resulting layouts
1082    let mut children: Vec<layout::Node> = {
1083        let mut lines: Vec<Vec<layout::Node>> = vec![];
1084        let mut total_offset_cross = padding_border.cross_start(node.flex_direction);
1085
1086        let layout_line = |line: &mut FlexLine| {
1087            let mut children: Vec<layout::Node> = vec![];
1088            let mut total_offset_main = padding_border.main_start(node.flex_direction);
1089            let line_offset_cross = line.offset_cross;
1090
1091            let layout_item = |child: &mut FlexItem| {
1092                let result = compute_internal(
1093                    child.node,
1094                    Size {
1095                        width: if is_row {
1096                            child.target_size.main(node.flex_direction).to_number()
1097                        } else {
1098                            child.target_size.cross(node.flex_direction).to_number()
1099                        },
1100                        height: if is_row {
1101                            child.target_size.cross(node.flex_direction).to_number()
1102                        } else {
1103                            child.target_size.main(node.flex_direction).to_number()
1104                        },
1105                    },
1106                    Size { width: container_width.to_number(), height: container_height.to_number() },
1107                    percent_calc_base_child,
1108                );
1109
1110                let offset_main = total_offset_main
1111                    + child.offset_main
1112                    + child.margin.main_start(node.flex_direction)
1113                    + (child.position.main_start(node.flex_direction).or_else(0.0)
1114                        - child.position.main_end(node.flex_direction).or_else(0.0));
1115
1116                let offset_cross = total_offset_cross
1117                    + child.offset_cross
1118                    + line_offset_cross
1119                    + child.margin.cross_start(node.flex_direction)
1120                    + (child.position.cross_start(node.flex_direction).or_else(0.0)
1121                        - child.position.cross_end(node.flex_direction).or_else(0.0));
1122
1123                children.push(layout::Node {
1124                    size: Size { width: result.size.width, height: result.size.height },
1125                    location: Point {
1126                        x: if is_row { offset_main } else { offset_cross },
1127                        y: if is_column { offset_main } else { offset_cross },
1128                    },
1129                    children: result.children,
1130                });
1131
1132                total_offset_main +=
1133                    child.offset_main + child.margin.main(node.flex_direction) + result.size.main(node.flex_direction);
1134            };
1135
1136            if node.flex_direction.is_reverse() {
1137                line.items.iter_mut().rev().for_each(layout_item);
1138            } else {
1139                line.items.iter_mut().for_each(layout_item);
1140            }
1141
1142            total_offset_cross += line_offset_cross + line.cross_size;
1143
1144            if node.flex_direction.is_reverse() {
1145                children.reverse();
1146            }
1147
1148            lines.push(children);
1149        };
1150
1151        if is_wrap_reverse {
1152            flex_lines.iter_mut().rev().for_each(layout_line);
1153        } else {
1154            flex_lines.iter_mut().for_each(layout_line);
1155        }
1156
1157        if is_wrap_reverse {
1158            lines.into_iter().rev().flat_map(|x| x).collect()
1159        } else {
1160            lines.into_iter().flat_map(|x| x).collect()
1161        }
1162    };
1163
1164    // Before returning we perform absolute layout on all absolutely positioned children
1165    let mut absolute_children: Vec<layout::Node> = node
1166        .children
1167        .iter()
1168        .filter(|child| child.position_type == PositionType::Absolute)
1169        .map(|child| {
1170            let container_width = container_width.to_number();
1171            let container_height = container_height.to_number();
1172
1173            let start = child.position.start.resolve(container_width) + child.margin.start.resolve(container_width);
1174            let end = child.position.end.resolve(container_width) + child.margin.end.resolve(container_width);
1175            let top = child.position.top.resolve(container_height) + child.margin.top.resolve(container_height);
1176            let bottom =
1177                child.position.bottom.resolve(container_height) + child.margin.bottom.resolve(container_height);
1178
1179            let (start_main, end_main) = if is_row { (start, end) } else { (top, bottom) };
1180            let (start_cross, end_cross) = if is_row { (top, bottom) } else { (start, end) };
1181
1182            let child_width = child
1183                .size
1184                .width
1185                .resolve(container_width)
1186                .maybe_max(child.min_size.width.resolve(container_width))
1187                .maybe_min(child.max_size.width.resolve(container_width));
1188
1189            let child_height = child
1190                .size
1191                .height
1192                .resolve(container_height)
1193                .maybe_max(child.min_size.height.resolve(container_height))
1194                .maybe_min(child.max_size.height.resolve(container_height));
1195
1196            let width = child_width.or_else(container_width - start - end);
1197            let height = child_height.or_else(container_height - top - bottom);
1198
1199            let result = compute_internal(
1200                child,
1201                Size { width, height },
1202                Size { width: container_width, height: container_height },
1203                container_width,
1204            );
1205
1206            let free_main_space = container_main_size
1207                - result
1208                    .size
1209                    .main(node.flex_direction)
1210                    .maybe_max(child.min_main_size(node.flex_direction).resolve(percent_calc_base_child))
1211                    .maybe_min(child.max_main_size(node.flex_direction).resolve(percent_calc_base_child));
1212
1213            let free_cross_space = container_cross_size
1214                - result
1215                    .size
1216                    .cross(node.flex_direction)
1217                    .maybe_max(child.min_cross_size(node.flex_direction).resolve(percent_calc_base_child))
1218                    .maybe_min(child.max_cross_size(node.flex_direction).resolve(percent_calc_base_child));
1219
1220            let offset_main = if start_main.is_defined() {
1221                start_main.or_else(0.0) + border.main_start(node.flex_direction)
1222            } else if end_main.is_defined() {
1223                free_main_space - end_main.or_else(0.0) - border.main_end(node.flex_direction)
1224            } else {
1225                match node.justify_content {
1226                    JustifyContent::SpaceBetween | JustifyContent::FlexStart => {
1227                        padding_border.main_start(node.flex_direction)
1228                    }
1229                    JustifyContent::FlexEnd => free_main_space - padding_border.main_end(node.flex_direction),
1230                    JustifyContent::SpaceEvenly | JustifyContent::SpaceAround | JustifyContent::Center => {
1231                        free_main_space / 2.0
1232                    }
1233                }
1234            };
1235
1236            let offset_cross = if start_cross.is_defined() {
1237                start_cross.or_else(0.0) + border.cross_start(node.flex_direction)
1238            } else if end_cross.is_defined() {
1239                free_cross_space - end_cross.or_else(0.0) - border.cross_end(node.flex_direction)
1240            } else {
1241                match child.align_self(node) {
1242                    AlignSelf::Auto => 0.0, // Should never happen
1243                    AlignSelf::FlexStart => {
1244                        if is_wrap_reverse {
1245                            free_cross_space - padding_border.cross_end(node.flex_direction)
1246                        } else {
1247                            padding_border.cross_start(node.flex_direction)
1248                        }
1249                    }
1250                    AlignSelf::FlexEnd => {
1251                        if is_wrap_reverse {
1252                            padding_border.cross_start(node.flex_direction)
1253                        } else {
1254                            free_cross_space - padding_border.cross_end(node.flex_direction)
1255                        }
1256                    }
1257                    AlignSelf::Center => free_cross_space / 2.0,
1258                    AlignSelf::Baseline => free_cross_space / 2.0, // Treat as center for now until we have baseline support
1259                    AlignSelf::Stretch => {
1260                        if is_wrap_reverse {
1261                            free_cross_space - padding_border.cross_end(node.flex_direction)
1262                        } else {
1263                            padding_border.cross_start(node.flex_direction)
1264                        }
1265                    }
1266                }
1267            };
1268
1269            layout::Node {
1270                size: Size { width: result.size.width, height: result.size.height },
1271                location: Point {
1272                    x: if is_row { offset_main } else { offset_cross },
1273                    y: if is_column { offset_main } else { offset_cross },
1274                },
1275                children: result.children,
1276            }
1277        })
1278        .collect();
1279
1280    // TODO - This adds all the absolute children to the end of the list instead of adding them in
1281    // the child order they were defined. This has to be fixed.
1282    children.append(&mut absolute_children);
1283
1284    ComputeResult { size: Size { width: container_width, height: container_height }, children }
1285}