expanse/
algo.rs

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