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}