tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 1077HTML 1008Markdown 12TOML 83209 Definitions

⎇ 32973e7ca8635aa870a77f52d0d0070381ae5f8c src/algo.rs Rust · 1262 lines · 55.0 KB History Who touched what raw

Factor out some logic into geometry module 32973e7ca · Emil Sjölander ·

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