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