More correct caching logic (#911)
1//! Computes the [flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) layout algorithm on [`TaffyTree`](crate::TaffyTree) according to the [spec](https://www.w3.org/TR/css-flexbox-1/)
2use crate::compute::common::alignment::{compute_alignment_offset, resolve_self_alignment_safety};
3use crate::geometry::{Line, Point, Rect, Size};
4use crate::style::{
5 AlignContent, AlignContentKeyword, AlignItems, AlignItemsKeyword, AlignSelf, AvailableSpace, FlexWrap,
6 JustifyContent, LengthPercentageAuto, Overflow, Position,
7};
8use crate::style::{CoreStyle, FlexDirection, FlexboxContainerStyle, FlexboxItemStyle};
9use crate::style_helpers::{TaffyMaxContent, TaffyMinContent};
10use crate::tree::{Layout, LayoutInput, LayoutOutput, RunMode, SizingMode};
11use crate::tree::{LayoutFlexboxContainer, LayoutPartialTreeExt, NodeId};
12use crate::util::debug::debug_log;
13use crate::util::sys::{f32_max, new_vec_with_capacity, Vec};
14use crate::util::MaybeMath;
15use crate::util::{MaybeResolve, ResolveOrZero};
16use crate::{BoxGenerationMode, BoxSizing, Direction, RequestedAxis};
17
18use super::common::alignment::apply_alignment_fallback;
19#[cfg(feature = "content_size")]
20use super::common::content_size::compute_content_size_contribution;
21
22/// The intermediate results of a flexbox calculation for a single item
23struct FlexItem {
24 /// The identifier for the associated node
25 node: NodeId,
26
27 /// The order of the node relative to it's siblings
28 order: u32,
29
30 /// The base size of this item
31 size: Size<Option<f32>>,
32 /// The minimum allowable size of this item
33 min_size: Size<Option<f32>>,
34 /// The maximum allowable size of this item
35 max_size: Size<Option<f32>>,
36 /// The cross-alignment of this item
37 align_self: AlignSelf,
38
39 /// The overflow style of the item
40 overflow: Point<Overflow>,
41 /// The width of the scrollbars (if it has any)
42 scrollbar_width: f32,
43 /// The flex shrink style of the item
44 flex_shrink: f32,
45 /// The flex grow style of the item
46 flex_grow: f32,
47
48 /// The minimum size of the item. This differs from min_size above because it also
49 /// takes into account content based automatic minimum sizes
50 resolved_minimum_main_size: f32,
51
52 /// The final offset of this item
53 inset: Rect<Option<f32>>,
54 /// The margin of this item
55 margin: Rect<f32>,
56 /// Whether each margin is an auto margin or not
57 margin_is_auto: Rect<bool>,
58 /// The padding of this item
59 padding: Rect<f32>,
60 /// The border of this item
61 border: Rect<f32>,
62
63 /// The default size of this item
64 flex_basis: f32,
65 /// The default size of this item, minus padding and border
66 inner_flex_basis: f32,
67 /// The amount by which this item has deviated from its target size
68 violation: f32,
69 /// Is the size of this item locked
70 frozen: bool,
71
72 /// Either the max- or min- content flex fraction
73 /// See https://www.w3.org/TR/css-flexbox-1/#intrinsic-main-sizes
74 content_flex_fraction: f32,
75
76 /// The proposed inner size of this item
77 hypothetical_inner_size: Size<f32>,
78 /// The proposed outer size of this item
79 hypothetical_outer_size: Size<f32>,
80 /// The size that this item wants to be
81 target_size: Size<f32>,
82 /// The size that this item wants to be, plus any padding and border
83 outer_target_size: Size<f32>,
84
85 /// The position of the bottom edge of this item
86 baseline: f32,
87
88 /// A temporary value for the main offset
89 ///
90 /// Offset is the relative position from the item's natural flow position based on
91 /// relative position values, alignment, and justification. Does not include margin/padding/border.
92 offset_main: f32,
93 /// A temporary value for the cross offset
94 ///
95 /// Offset is the relative position from the item's natural flow position based on
96 /// relative position values, alignment, and justification. Does not include margin/padding/border.
97 offset_cross: f32,
98}
99
100impl FlexItem {
101 /// Returns true if the item is a <https://www.w3.org/TR/css-overflow-3/#scroll-container>
102 fn is_scroll_container(&self) -> bool {
103 self.overflow.x.is_scroll_container() | self.overflow.y.is_scroll_container()
104 }
105}
106
107/// A line of [`FlexItem`] used for intermediate computation
108struct FlexLine<'a> {
109 /// The slice of items to iterate over during computation of this line
110 items: &'a mut [FlexItem],
111 /// The dimensions of the cross-axis
112 cross_size: f32,
113 /// The relative offset of the cross-axis
114 offset_cross: f32,
115}
116
117/// Values that can be cached during the flexbox algorithm
118struct AlgoConstants {
119 /// The direction of the current segment being laid out
120 dir: FlexDirection,
121 /// The layout direction of the current segment being laid out
122 layout_direction: Direction,
123 /// Is this segment a row
124 is_row: bool,
125 /// Is this segment a column
126 is_column: bool,
127 /// Is wrapping enabled (in either direction)
128 is_wrap: bool,
129 /// Is the wrap direction inverted
130 is_wrap_reverse: bool,
131
132 /// The item's min_size style
133 min_size: Size<Option<f32>>,
134 /// The item's max_size style
135 max_size: Size<Option<f32>>,
136 /// The margin of this section
137 margin: Rect<f32>,
138 /// The border of this section
139 border: Rect<f32>,
140 /// The space between the content box and the border box.
141 /// This consists of padding + border + scrollbar_gutter.
142 content_box_inset: Rect<f32>,
143 /// The size reserved for scrollbar gutters in each axis
144 scrollbar_gutter: Point<f32>,
145 /// The gap of this section
146 gap: Size<f32>,
147 /// The align_items property of this node
148 align_items: AlignItems,
149 /// The align_content property of this node
150 align_content: AlignContent,
151 /// The justify_content property of this node
152 justify_content: Option<JustifyContent>,
153
154 /// The border-box size of the node being laid out (if known)
155 node_outer_size: Size<Option<f32>>,
156 /// The content-box size of the node being laid out (if known)
157 node_inner_size: Size<Option<f32>>,
158
159 /// The size of the virtual container containing the flex items.
160 container_size: Size<f32>,
161 /// The size of the internal container
162 inner_container_size: Size<f32>,
163}
164
165/// Computes the layout of a box according to the flexbox algorithm
166pub fn compute_flexbox_layout(
167 tree: &mut impl LayoutFlexboxContainer,
168 node: NodeId,
169 inputs: LayoutInput,
170) -> LayoutOutput {
171 let LayoutInput { known_dimensions, parent_size, run_mode, .. } = inputs;
172 let style = tree.get_flexbox_container_style(node);
173
174 // Pull these out earlier to avoid borrowing issues
175 let aspect_ratio = style.aspect_ratio();
176 let padding = style.padding().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
177 let border = style.border().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
178 let padding_border_sum = padding.sum_axes() + border.sum_axes();
179 let box_sizing_adjustment =
180 if style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO };
181
182 let min_size = style
183 .min_size()
184 .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
185 .maybe_apply_aspect_ratio(aspect_ratio)
186 .maybe_add(box_sizing_adjustment);
187 let max_size = style
188 .max_size()
189 .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
190 .maybe_apply_aspect_ratio(aspect_ratio)
191 .maybe_add(box_sizing_adjustment);
192 let clamped_style_size = if inputs.sizing_mode == SizingMode::InherentSize {
193 style
194 .size()
195 .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
196 .maybe_apply_aspect_ratio(aspect_ratio)
197 .maybe_add(box_sizing_adjustment)
198 .maybe_clamp(min_size, max_size)
199 } else {
200 Size::NONE
201 };
202
203 // If both min and max in a given axis are set and max <= min then this determines the size in that axis
204 let min_max_definite_size = min_size.zip_map(max_size, |min, max| match (min, max) {
205 (Some(min), Some(max)) if max <= min => Some(min),
206 _ => None,
207 });
208
209 // The size of the container should be floored by the padding and border
210 let styled_based_known_dimensions =
211 known_dimensions.or(min_max_definite_size.or(clamped_style_size).maybe_max(padding_border_sum));
212
213 // Short-circuit layout if the container's size is fully determined by the container's size and the run mode
214 // is ComputeSize (and thus the container's size is all that we're interested in)
215 if run_mode == RunMode::ComputeSize {
216 if let Size { width: Some(width), height: Some(height) } = styled_based_known_dimensions {
217 return LayoutOutput::from_outer_size(Size { width, height });
218 }
219
220 // We can also short-circuit if the width is known and only the width has been requested.
221 if inputs.axis == RequestedAxis::Horizontal {
222 if let Some(width) = styled_based_known_dimensions.width {
223 return LayoutOutput::from_outer_size(Size { width, height: 0.0 });
224 }
225 }
226 }
227
228 // Short-circuit layout if the container's size is fully determined by the container's size and the run mode
229 // is ComputeSize (and thus the container's size is all that we're interested in)
230 if run_mode == RunMode::ComputeSize {
231 if let Size { width: Some(width), height: Some(height) } = styled_based_known_dimensions {
232 return LayoutOutput::from_outer_size(Size { width, height });
233 }
234 }
235
236 debug_log!("FLEX:", dbg:style.flex_direction());
237 drop(style);
238
239 compute_preliminary(tree, node, LayoutInput { known_dimensions: styled_based_known_dimensions, ..inputs })
240}
241
242/// Compute a preliminary size for an item
243fn compute_preliminary(tree: &mut impl LayoutFlexboxContainer, node: NodeId, inputs: LayoutInput) -> LayoutOutput {
244 let LayoutInput { known_dimensions, parent_size, available_space, run_mode, .. } = inputs;
245
246 // Define some general constants we will need for the remainder of the algorithm.
247 let mut constants = compute_constants(tree, tree.get_flexbox_container_style(node), known_dimensions, parent_size);
248
249 // 9. Flex Layout Algorithm
250
251 // 9.1. Initial Setup
252
253 // 1. Generate anonymous flex items as described in §4 Flex Items.
254 debug_log!("generate_anonymous_flex_items");
255 let mut flex_items = generate_anonymous_flex_items(tree, node, &constants);
256
257 // 9.2. Line Length Determination
258
259 // 2. Determine the available main and cross space for the flex items
260 debug_log!("determine_available_space");
261 let available_space = determine_available_space(known_dimensions, available_space, &constants);
262
263 // 3. Determine the flex base size and hypothetical main size of each item.
264 debug_log!("determine_flex_base_size");
265 determine_flex_base_size(tree, &constants, available_space, &mut flex_items);
266
267 #[cfg(feature = "debug")]
268 for item in flex_items.iter() {
269 debug_log!("item.flex_basis", item.flex_basis);
270 debug_log!("item.inner_flex_basis", item.inner_flex_basis);
271 debug_log!("item.hypothetical_outer_size", dbg:item.hypothetical_outer_size);
272 debug_log!("item.hypothetical_inner_size", dbg:item.hypothetical_inner_size);
273 debug_log!("item.resolved_minimum_main_size", dbg:item.resolved_minimum_main_size);
274 }
275
276 // 4. Determine the main size of the flex container
277 // This has already been done as part of compute_constants. The inner size is exposed as constants.node_inner_size.
278
279 // 9.3. Main Size Determination
280
281 // 5. Collect flex items into flex lines.
282 debug_log!("collect_flex_lines");
283 let mut flex_lines = collect_flex_lines(&constants, available_space, &mut flex_items);
284
285 // If container size is undefined, determine the container's main size
286 // and then re-resolve gaps based on newly determined size
287 debug_log!("determine_container_main_size");
288 if let Some(inner_main_size) = constants.node_inner_size.main(constants.dir) {
289 let outer_main_size = inner_main_size + constants.content_box_inset.main_axis_sum(constants.dir);
290 constants.inner_container_size.set_main(constants.dir, inner_main_size);
291 constants.container_size.set_main(constants.dir, outer_main_size);
292 } else {
293 // Sets constants.container_size and constants.outer_container_size
294 determine_container_main_size(tree, available_space, &mut flex_lines, &mut constants);
295 constants.node_inner_size.set_main(constants.dir, Some(constants.inner_container_size.main(constants.dir)));
296 constants.node_outer_size.set_main(constants.dir, Some(constants.container_size.main(constants.dir)));
297
298 debug_log!("constants.node_outer_size", dbg:constants.node_outer_size);
299 debug_log!("constants.node_inner_size", dbg:constants.node_inner_size);
300
301 // Re-resolve percentage gaps
302 let style = tree.get_flexbox_container_style(node);
303 let inner_container_size = constants.inner_container_size.main(constants.dir);
304 let new_gap = style
305 .gap()
306 .main(constants.dir)
307 .maybe_resolve(inner_container_size, |val, basis| tree.calc(val, basis))
308 .unwrap_or(0.0);
309 constants.gap.set_main(constants.dir, new_gap);
310 }
311
312 // 6. Resolve the flexible lengths of all the flex items to find their used main size.
313 debug_log!("resolve_flexible_lengths");
314 for line in &mut flex_lines {
315 resolve_flexible_lengths(line, &constants);
316 }
317
318 // 9.4. Cross Size Determination
319
320 // 7. Determine the hypothetical cross size of each item.
321 debug_log!("determine_hypothetical_cross_size");
322 for line in &mut flex_lines {
323 determine_hypothetical_cross_size(tree, line, &constants, available_space);
324 }
325
326 // Calculate child baselines. This function is internally smart and only computes child baselines
327 // if they are necessary.
328 debug_log!("calculate_children_base_lines");
329 calculate_children_base_lines(tree, known_dimensions, available_space, &mut flex_lines, &constants);
330
331 // 8. Calculate the cross size of each flex line.
332 debug_log!("calculate_cross_size");
333 calculate_cross_size(&mut flex_lines, known_dimensions, &constants);
334
335 // 9. Handle 'align-content: stretch'.
336 debug_log!("handle_align_content_stretch");
337 handle_align_content_stretch(&mut flex_lines, known_dimensions, &constants);
338
339 // 10. Collapse visibility:collapse items. If any flex items have visibility: collapse,
340 // note the cross size of the line they’re in as the item’s strut size, and restart
341 // layout from the beginning.
342 //
343 // In this second layout round, when collecting items into lines, treat the collapsed
344 // items as having zero main size. For the rest of the algorithm following that step,
345 // ignore the collapsed items entirely (as if they were display:none) except that after
346 // calculating the cross size of the lines, if any line’s cross size is less than the
347 // largest strut size among all the collapsed items in the line, set its cross size to
348 // that strut size.
349 //
350 // Skip this step in the second layout round.
351
352 // TODO implement once (if ever) we support visibility:collapse
353
354 // 11. Determine the used cross size of each flex item.
355 debug_log!("determine_used_cross_size");
356 determine_used_cross_size(tree, &mut flex_lines, &constants);
357
358 // 9.5. Main-Axis Alignment
359
360 // 12. Distribute any remaining free space.
361 debug_log!("distribute_remaining_free_space");
362 distribute_remaining_free_space(&mut flex_lines, &constants);
363
364 // 9.6. Cross-Axis Alignment
365
366 // 13. Resolve cross-axis auto margins (also includes 14).
367 debug_log!("resolve_cross_axis_auto_margins");
368 resolve_cross_axis_auto_margins(&mut flex_lines, &constants);
369
370 // 15. Determine the flex container’s used cross size.
371 debug_log!("determine_container_cross_size");
372 let total_line_cross_size = determine_container_cross_size(&flex_lines, known_dimensions, &mut constants);
373
374 // We have the container size.
375 // If our caller does not care about performing layout we are done now.
376 if run_mode == RunMode::ComputeSize {
377 return LayoutOutput::from_outer_size(constants.container_size);
378 }
379
380 // 16. Align all flex lines per align-content.
381 debug_log!("align_flex_lines_per_align_content");
382 align_flex_lines_per_align_content(&mut flex_lines, &constants, total_line_cross_size);
383
384 // Do a final layout pass and gather the resulting layouts
385 debug_log!("final_layout_pass");
386 let inflow_content_size = final_layout_pass(tree, &mut flex_lines, &constants);
387
388 // Before returning we perform absolute layout on all absolutely positioned children
389 debug_log!("perform_absolute_layout_on_absolute_children");
390 let absolute_content_size = perform_absolute_layout_on_absolute_children(tree, node, &constants);
391
392 debug_log!("hidden_layout");
393 let len = tree.child_count(node);
394 for order in 0..len {
395 let child = tree.get_child_id(node, order);
396 if tree.get_flexbox_child_style(child).box_generation_mode() == BoxGenerationMode::None {
397 tree.set_unrounded_layout(child, &Layout::with_order(order as u32));
398 tree.perform_child_layout(
399 child,
400 Size::NONE,
401 Size::NONE,
402 Size::MAX_CONTENT,
403 SizingMode::InherentSize,
404 Line::FALSE,
405 );
406 }
407 }
408
409 // 8.5. Flex Container Baselines: calculate the flex container's first baseline
410 // See https://www.w3.org/TR/css-flexbox-1/#flex-baselines
411 let first_vertical_baseline = if flex_lines.is_empty() {
412 None
413 } else {
414 flex_lines[0]
415 .items
416 .iter()
417 .find(|item| constants.is_column || item.align_self == AlignSelf::BASELINE)
418 .or_else(|| flex_lines[0].items.iter().next())
419 .map(|child| {
420 let offset_vertical = if constants.is_row { child.offset_cross } else { child.offset_main };
421 offset_vertical + child.baseline
422 })
423 };
424
425 LayoutOutput::from_sizes_and_baselines(
426 constants.container_size,
427 inflow_content_size.f32_max(absolute_content_size),
428 Point { x: None, y: first_vertical_baseline },
429 )
430}
431
432/// Compute constants that can be reused during the flexbox algorithm.
433#[inline]
434fn compute_constants(
435 tree: &impl LayoutFlexboxContainer,
436 style: impl FlexboxContainerStyle,
437 known_dimensions: Size<Option<f32>>,
438 parent_size: Size<Option<f32>>,
439) -> AlgoConstants {
440 let dir = style.flex_direction();
441 let is_row = dir.is_row();
442 let is_column = dir.is_column();
443 let is_wrap = matches!(style.flex_wrap(), FlexWrap::Wrap | FlexWrap::WrapReverse);
444 let is_wrap_reverse = style.flex_wrap() == FlexWrap::WrapReverse;
445
446 let aspect_ratio = style.aspect_ratio();
447 let margin = style.margin().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
448 let padding = style.padding().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
449 let border = style.border().resolve_or_zero(parent_size.width, |val, basis| tree.calc(val, basis));
450 let padding_border_sum = padding.sum_axes() + border.sum_axes();
451 let box_sizing_adjustment =
452 if style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO };
453
454 let align_items = style.align_items().unwrap_or(AlignItems::STRETCH);
455 let align_content = style.align_content().unwrap_or(AlignContent::STRETCH);
456 let justify_content = style.justify_content();
457 let layout_direction = style.direction();
458
459 // Scrollbar gutters are reserved when the `overflow` property is set to `Overflow::Scroll`.
460 // However, the axis are switched (transposed) because a node that scrolls vertically needs
461 // *horizontal* space to be reserved for a scrollbar
462 let scrollbar_gutter = style.overflow().transpose().map(|overflow| match overflow {
463 Overflow::Scroll => style.scrollbar_width(),
464 _ => 0.0,
465 });
466 let mut content_box_inset = padding + border;
467 content_box_inset.bottom += scrollbar_gutter.y;
468
469 match layout_direction {
470 Direction::Ltr => content_box_inset.right += scrollbar_gutter.x,
471 Direction::Rtl => content_box_inset.left += scrollbar_gutter.x,
472 };
473
474 let node_outer_size = known_dimensions;
475 let node_inner_size = node_outer_size.maybe_sub(content_box_inset.sum_axes());
476 let gap = style.gap().resolve_or_zero(node_inner_size.or(Size::zero()), |val, basis| tree.calc(val, basis));
477
478 let container_size = Size::zero();
479 let inner_container_size = Size::zero();
480
481 AlgoConstants {
482 dir,
483 layout_direction,
484 is_row,
485 is_column,
486 is_wrap,
487 is_wrap_reverse,
488 min_size: style
489 .min_size()
490 .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
491 .maybe_apply_aspect_ratio(aspect_ratio)
492 .maybe_add(box_sizing_adjustment),
493 max_size: style
494 .max_size()
495 .maybe_resolve(parent_size, |val, basis| tree.calc(val, basis))
496 .maybe_apply_aspect_ratio(aspect_ratio)
497 .maybe_add(box_sizing_adjustment),
498 margin,
499 border,
500 gap,
501 content_box_inset,
502 scrollbar_gutter,
503 align_items,
504 align_content,
505 justify_content,
506 node_outer_size,
507 node_inner_size,
508 container_size,
509 inner_container_size,
510 }
511}
512
513/// Generate anonymous flex items.
514///
515/// # [9.1. Initial Setup](https://www.w3.org/TR/css-flexbox-1/#box-manip)
516///
517/// - [**Generate anonymous flex items**](https://www.w3.org/TR/css-flexbox-1/#algo-anon-box) as described in [§4 Flex Items](https://www.w3.org/TR/css-flexbox-1/#flex-items).
518#[inline]
519fn generate_anonymous_flex_items(
520 tree: &impl LayoutFlexboxContainer,
521 node: NodeId,
522 constants: &AlgoConstants,
523) -> Vec<FlexItem> {
524 tree.child_ids(node)
525 .enumerate()
526 .map(|(index, child)| (index, child, tree.get_flexbox_child_style(child)))
527 .filter(|(_, _, style)| style.position() != Position::Absolute)
528 .filter(|(_, _, style)| style.box_generation_mode() != BoxGenerationMode::None)
529 .map(|(index, child, child_style)| {
530 let aspect_ratio = child_style.aspect_ratio();
531 let padding = child_style
532 .padding()
533 .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis));
534 let border = child_style
535 .border()
536 .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis));
537 let pb_sum = (padding + border).sum_axes();
538 let box_sizing_adjustment =
539 if child_style.box_sizing() == BoxSizing::ContentBox { pb_sum } else { Size::ZERO };
540 FlexItem {
541 node: child,
542 order: index as u32,
543 size: child_style
544 .size()
545 .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis))
546 .maybe_apply_aspect_ratio(aspect_ratio)
547 .maybe_add(box_sizing_adjustment),
548 min_size: child_style
549 .min_size()
550 .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis))
551 .maybe_apply_aspect_ratio(aspect_ratio)
552 .maybe_add(box_sizing_adjustment),
553 max_size: child_style
554 .max_size()
555 .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis))
556 .maybe_apply_aspect_ratio(aspect_ratio)
557 .maybe_add(box_sizing_adjustment),
558
559 inset: child_style
560 .inset()
561 .zip_size(constants.node_inner_size, |p, s| p.maybe_resolve(s, |val, basis| tree.calc(val, basis))),
562 margin: child_style
563 .margin()
564 .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis)),
565 margin_is_auto: child_style.margin().map(LengthPercentageAuto::is_auto),
566 padding: child_style
567 .padding()
568 .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis)),
569 border: child_style
570 .border()
571 .resolve_or_zero(constants.node_inner_size.width, |val, basis| tree.calc(val, basis)),
572 align_self: child_style.align_self().unwrap_or(constants.align_items),
573 overflow: child_style.overflow(),
574 scrollbar_width: child_style.scrollbar_width(),
575 flex_grow: child_style.flex_grow(),
576 flex_shrink: child_style.flex_shrink(),
577 flex_basis: 0.0,
578 inner_flex_basis: 0.0,
579 violation: 0.0,
580 frozen: false,
581
582 resolved_minimum_main_size: 0.0,
583 hypothetical_inner_size: Size::zero(),
584 hypothetical_outer_size: Size::zero(),
585 target_size: Size::zero(),
586 outer_target_size: Size::zero(),
587 content_flex_fraction: 0.0,
588
589 baseline: 0.0,
590
591 offset_main: 0.0,
592 offset_cross: 0.0,
593 }
594 })
595 .collect()
596}
597
598/// Determine the available main and cross space for the flex items.
599///
600/// # [9.2. Line Length Determination](https://www.w3.org/TR/css-flexbox-1/#line-sizing)
601///
602/// - [**Determine the available main and cross space for the flex items**](https://www.w3.org/TR/css-flexbox-1/#algo-available).
603///
604/// For each dimension, if that dimension of the flex container’s content box is a definite size, use that;
605/// if that dimension of the flex container is being sized under a min or max-content constraint, the available space in that dimension is that constraint;
606/// otherwise, subtract the flex container’s margin, border, and padding from the space available to the flex container in that dimension and use that value.
607/// **This might result in an infinite value**.
608#[inline]
609#[must_use]
610fn determine_available_space(
611 known_dimensions: Size<Option<f32>>,
612 outer_available_space: Size<AvailableSpace>,
613 constants: &AlgoConstants,
614) -> Size<AvailableSpace> {
615 // Note: min/max/preferred size styles have already been applied to known_dimensions in the `compute` function above
616 let width = match known_dimensions.width {
617 Some(node_width) => AvailableSpace::Definite(node_width - constants.content_box_inset.horizontal_axis_sum()),
618 None => outer_available_space
619 .width
620 .maybe_sub(constants.margin.horizontal_axis_sum())
621 .maybe_sub(constants.content_box_inset.horizontal_axis_sum()),
622 };
623
624 let height = match known_dimensions.height {
625 Some(node_height) => AvailableSpace::Definite(node_height - constants.content_box_inset.vertical_axis_sum()),
626 None => outer_available_space
627 .height
628 .maybe_sub(constants.margin.vertical_axis_sum())
629 .maybe_sub(constants.content_box_inset.vertical_axis_sum()),
630 };
631
632 Size { width, height }
633}
634
635/// Determine the flex base size and hypothetical main size of each item.
636///
637/// # [9.2. Line Length Determination](https://www.w3.org/TR/css-flexbox-1/#line-sizing)
638///
639/// - [**Determine the flex base size and hypothetical main size of each item:**](https://www.w3.org/TR/css-flexbox-1/#algo-main-item)
640///
641/// - A. If the item has a definite used flex basis, that’s the flex base size.
642///
643/// - B. If the flex item has ...
644///
645/// - an intrinsic aspect ratio,
646/// - a used flex basis of content, and
647/// - a definite cross size,
648///
649/// then the flex base size is calculated from its inner cross size and the flex item’s intrinsic aspect ratio.
650///
651/// - C. If the used flex basis is content or depends on its available space, and the flex container is being sized under a min-content
652/// or max-content constraint (e.g. when performing automatic table layout \[CSS21\]), size the item under that constraint.
653/// The flex base size is the item’s resulting main size.
654///
655/// - E. Otherwise, size the item into the available space using its used flex basis in place of its main size, treating a value of content as max-content.
656/// If a cross size is needed to determine the main size (e.g. when the flex item’s main size is in its block axis) and the flex item’s cross size is auto and not definite,
657/// in this calculation use fit-content as the flex item’s cross size. The flex base size is the item’s resulting main size.
658///
659/// When determining the flex base size, the item’s min and max main sizes are ignored (no clamping occurs).
660/// Furthermore, the sizing calculations that floor the content box size at zero when applying box-sizing are also ignored.
661/// (For example, an item with a specified size of zero, positive padding, and box-sizing: border-box will have an outer flex base size of zero—and hence a negative inner flex base size.)
662#[inline]
663fn determine_flex_base_size(
664 tree: &mut impl LayoutFlexboxContainer,
665 constants: &AlgoConstants,
666 available_space: Size<AvailableSpace>,
667 flex_items: &mut [FlexItem],
668) {
669 let dir = constants.dir;
670
671 for child in flex_items.iter_mut() {
672 let child_style = tree.get_flexbox_child_style(child.node);
673
674 // Parent size for child sizing
675 let cross_axis_parent_size = constants.node_inner_size.cross(dir);
676 let child_parent_size = Size::from_cross(dir, cross_axis_parent_size);
677
678 // Available space for child sizing
679 let cross_axis_margin_sum = constants.margin.cross_axis_sum(dir);
680 let child_min_cross = child.min_size.cross(dir).maybe_add(cross_axis_margin_sum);
681 let child_max_cross = child.max_size.cross(dir).maybe_add(cross_axis_margin_sum);
682
683 // Clamp available space by min- and max- size
684 let cross_axis_available_space: AvailableSpace = match available_space.cross(dir) {
685 AvailableSpace::Definite(val) => AvailableSpace::Definite(
686 cross_axis_parent_size.unwrap_or(val).maybe_clamp(child_min_cross, child_max_cross),
687 ),
688 AvailableSpace::MinContent => match child_min_cross {
689 Some(min) => AvailableSpace::Definite(min),
690 None => AvailableSpace::MinContent,
691 },
692 AvailableSpace::MaxContent => match child_max_cross {
693 Some(max) => AvailableSpace::Definite(max),
694 None => AvailableSpace::MaxContent,
695 },
696 };
697
698 // Known dimensions for child sizing
699 let child_known_dimensions = {
700 let mut ckd = child.size.with_main(dir, None);
701 if child.align_self == AlignSelf::STRETCH
702 && !child.margin_is_auto.cross_start(constants.dir)
703 && !child.margin_is_auto.cross_end(constants.dir)
704 && ckd.cross(dir).is_none()
705 {
706 ckd.set_cross(
707 dir,
708 cross_axis_available_space.into_option().maybe_sub(child.margin.cross_axis_sum(dir)),
709 );
710 }
711 ckd
712 };
713
714 let container_width = constants.node_inner_size.main(dir);
715 let box_sizing_adjustment = if child_style.box_sizing() == BoxSizing::ContentBox {
716 let padding = child_style.padding().resolve_or_zero(container_width, |val, basis| tree.calc(val, basis));
717 let border = child_style.border().resolve_or_zero(container_width, |val, basis| tree.calc(val, basis));
718 (padding + border).sum_axes()
719 } else {
720 Size::ZERO
721 }
722 .main(dir);
723 let flex_basis = child_style
724 .flex_basis()
725 .maybe_resolve(container_width, |val, basis| tree.calc(val, basis))
726 .maybe_add(box_sizing_adjustment);
727
728 drop(child_style);
729
730 child.flex_basis = 'flex_basis: {
731 // A. If the item has a definite used flex basis, that’s the flex base size.
732
733 // B. If the flex item has an intrinsic aspect ratio,
734 // a used flex basis of content, and a definite cross size,
735 // then the flex base size is calculated from its inner
736 // cross size and the flex item’s intrinsic aspect ratio.
737
738 // Note: `child.size` has already been resolved against aspect_ratio in generate_anonymous_flex_items
739 // So B will just work here by using main_size without special handling for aspect_ratio
740 let main_size = child.size.main(dir);
741 if let Some(flex_basis) = flex_basis.or(main_size) {
742 break 'flex_basis flex_basis;
743 };
744
745 // C. If the used flex basis is content or depends on its available space,
746 // and the flex container is being sized under a min-content or max-content
747 // constraint (e.g. when performing automatic table layout [CSS21]),
748 // size the item under that constraint. The flex base size is the item’s
749 // resulting main size.
750
751 // This is covered by the implementation of E below, which passes the available_space constraint
752 // through to the child size computation. It may need a separate implementation if/when D is implemented.
753
754 // D. Otherwise, if the used flex basis is content or depends on its
755 // available space, the available main size is infinite, and the flex item’s
756 // inline axis is parallel to the main axis, lay the item out using the rules
757 // for a box in an orthogonal flow [CSS3-WRITING-MODES]. The flex base size
758 // is the item’s max-content main size.
759
760 // TODO if/when vertical writing modes are supported
761
762 // E. Otherwise, size the item into the available space using its used flex basis
763 // in place of its main size, treating a value of content as max-content.
764 // If a cross size is needed to determine the main size (e.g. when the
765 // flex item’s main size is in its block axis) and the flex item’s cross size
766 // is auto and not definite, in this calculation use fit-content as the
767 // flex item’s cross size. The flex base size is the item’s resulting main size.
768
769 let child_available_space = Size::MAX_CONTENT
770 .with_main(
771 dir,
772 // Map AvailableSpace::Definite to AvailableSpace::MaxContent
773 if available_space.main(dir) == AvailableSpace::MinContent {
774 AvailableSpace::MinContent
775 } else {
776 AvailableSpace::MaxContent
777 },
778 )
779 .with_cross(dir, cross_axis_available_space);
780
781 debug_log!("COMPUTE CHILD BASE SIZE:");
782 break 'flex_basis tree.measure_child_size(
783 child.node,
784 child_known_dimensions,
785 child_parent_size,
786 child_available_space,
787 SizingMode::ContentSize,
788 dir.main_axis(),
789 Line::FALSE,
790 );
791 };
792
793 // Floor flex-basis by the padding_border_sum (floors inner_flex_basis at zero)
794 // This seems to be in violation of the spec which explicitly states that the content box should not be floored at zero
795 // (like it usually is) when calculating the flex-basis. But including this matches both Chrome and Firefox's behaviour.
796 //
797 // TODO: resolve spec violation
798 // Spec: https://www.w3.org/TR/css-flexbox-1/#intrinsic-item-contributions
799 // Spec: https://www.w3.org/TR/css-flexbox-1/#change-2016-max-contribution
800 let padding_border_sum = child.padding.main_axis_sum(constants.dir) + child.border.main_axis_sum(constants.dir);
801 child.flex_basis = child.flex_basis.max(padding_border_sum);
802
803 // The hypothetical main size is the item’s flex base size clamped according to its
804 // used min and max main sizes (and flooring the content box size at zero).
805
806 child.inner_flex_basis =
807 child.flex_basis - child.padding.main_axis_sum(constants.dir) - child.border.main_axis_sum(constants.dir);
808
809 let padding_border_axes_sums = (child.padding + child.border).sum_axes().map(Some);
810
811 // Note that it is important that the `parent_size` parameter in the main axis is not set for this
812 // function call as it used for resolving percentages, and percentage size in an axis should not contribute
813 // to a min-content contribution in that same axis. However the `parent_size` and `available_space` *should*
814 // be set to their usual values in the cross axis so that wrapping content can wrap correctly.
815 //
816 // See https://drafts.csswg.org/css-sizing-3/#min-percentage-contribution
817 let style_min_main_size =
818 child.min_size.or(child.overflow.map(Overflow::maybe_into_automatic_min_size).into()).main(dir);
819
820 child.resolved_minimum_main_size = style_min_main_size.unwrap_or({
821 let min_content_main_size = {
822 let child_available_space = Size::MIN_CONTENT.with_cross(dir, cross_axis_available_space);
823
824 debug_log!("COMPUTE CHILD MIN SIZE:");
825 tree.measure_child_size(
826 child.node,
827 child_known_dimensions,
828 child_parent_size,
829 child_available_space,
830 SizingMode::ContentSize,
831 dir.main_axis(),
832 Line::FALSE,
833 )
834 };
835
836 // 4.5. Automatic Minimum Size of Flex Items
837 // https://www.w3.org/TR/css-flexbox-1/#min-size-auto
838 let clamped_min_content_size =
839 min_content_main_size.maybe_min(child.size.main(dir)).maybe_min(child.max_size.main(dir));
840 clamped_min_content_size.maybe_max(padding_border_axes_sums.main(dir))
841 });
842
843 let hypothetical_inner_min_main =
844 child.resolved_minimum_main_size.maybe_max(padding_border_axes_sums.main(constants.dir));
845 let hypothetical_inner_size =
846 child.flex_basis.maybe_clamp(Some(hypothetical_inner_min_main), child.max_size.main(constants.dir));
847 let hypothetical_outer_size = hypothetical_inner_size + child.margin.main_axis_sum(constants.dir);
848
849 child.hypothetical_inner_size.set_main(constants.dir, hypothetical_inner_size);
850 child.hypothetical_outer_size.set_main(constants.dir, hypothetical_outer_size);
851 }
852}
853
854/// Collect flex items into flex lines.
855///
856/// # [9.3. Main Size Determination](https://www.w3.org/TR/css-flexbox-1/#main-sizing)
857///
858/// - [**Collect flex items into flex lines**](https://www.w3.org/TR/css-flexbox-1/#algo-line-break):
859///
860/// - If the flex container is single-line, collect all the flex items into a single flex line.
861///
862/// - Otherwise, starting from the first uncollected item, collect consecutive items one by one until the first time that the next collected item would not fit into the flex container’s inner main size
863/// (or until a forced break is encountered, see [§10 Fragmenting Flex Layout](https://www.w3.org/TR/css-flexbox-1/#pagination)).
864/// If the very first uncollected item wouldn't fit, collect just it into the line.
865///
866/// For this step, the size of a flex item is its outer hypothetical main size. (**Note: This can be negative**.)
867///
868/// Repeat until all flex items have been collected into flex lines.
869///
870/// **Note that the "collect as many" line will collect zero-sized flex items onto the end of the previous line even if the last non-zero item exactly "filled up" the line**.
871#[inline]
872fn collect_flex_lines<'a>(
873 constants: &AlgoConstants,
874 available_space: Size<AvailableSpace>,
875 flex_items: &'a mut Vec<FlexItem>,
876) -> Vec<FlexLine<'a>> {
877 if !constants.is_wrap {
878 let mut lines = new_vec_with_capacity(1);
879 lines.push(FlexLine { items: flex_items.as_mut_slice(), cross_size: 0.0, offset_cross: 0.0 });
880 lines
881 } else {
882 let main_axis_available_space = match constants.max_size.main(constants.dir) {
883 Some(max_size) => AvailableSpace::Definite(
884 available_space
885 .main(constants.dir)
886 .into_option()
887 .unwrap_or(max_size)
888 .maybe_max(constants.min_size.main(constants.dir)),
889 ),
890 None => available_space.main(constants.dir),
891 };
892
893 match main_axis_available_space {
894 // If we're sizing under a max-content constraint then the flex items will never wrap
895 // (at least for now - future extensions to the CSS spec may add provisions for forced wrap points)
896 AvailableSpace::MaxContent => {
897 let mut lines = new_vec_with_capacity(1);
898 lines.push(FlexLine { items: flex_items.as_mut_slice(), cross_size: 0.0, offset_cross: 0.0 });
899 lines
900 }
901 // If flex-wrap is Wrap and we're sizing under a min-content constraint, then we take every possible wrapping opportunity
902 // and place each item in it's own line
903 AvailableSpace::MinContent => {
904 let mut lines = new_vec_with_capacity(flex_items.len());
905 let mut items = &mut flex_items[..];
906 while !items.is_empty() {
907 let (line_items, rest) = items.split_at_mut(1);
908 lines.push(FlexLine { items: line_items, cross_size: 0.0, offset_cross: 0.0 });
909 items = rest;
910 }
911 lines
912 }
913 AvailableSpace::Definite(main_axis_available_space) => {
914 let mut lines = new_vec_with_capacity(1);
915 let mut flex_items = &mut flex_items[..];
916 let main_axis_gap = constants.gap.main(constants.dir);
917
918 while !flex_items.is_empty() {
919 // Find index of the first item in the next line
920 // (or the last item if all remaining items are in the current line)
921 let mut line_length = 0.0;
922 let index = flex_items
923 .iter()
924 .enumerate()
925 .find(|&(idx, child)| {
926 // Gaps only occur between items (not before the first one or after the last one)
927 // So first item in the line does not contribute a gap to the line length
928 let gap_contribution = if idx == 0 { 0.0 } else { main_axis_gap };
929 line_length += child.hypothetical_outer_size.main(constants.dir) + gap_contribution;
930 line_length > main_axis_available_space && idx != 0
931 })
932 .map(|(idx, _)| idx)
933 .unwrap_or(flex_items.len());
934
935 let (items, rest) = flex_items.split_at_mut(index);
936 lines.push(FlexLine { items, cross_size: 0.0, offset_cross: 0.0 });
937 flex_items = rest;
938 }
939 lines
940 }
941 }
942 }
943}
944
945/// Determine the container's main size (if not already known)
946fn determine_container_main_size(
947 tree: &mut impl LayoutFlexboxContainer,
948 available_space: Size<AvailableSpace>,
949 lines: &mut [FlexLine<'_>],
950 constants: &mut AlgoConstants,
951) {
952 let dir = constants.dir;
953 let main_content_box_inset = constants.content_box_inset.main_axis_sum(constants.dir);
954
955 let outer_main_size: f32 = constants.node_outer_size.main(constants.dir).unwrap_or_else(|| {
956 match available_space.main(dir) {
957 AvailableSpace::Definite(main_axis_available_space) => {
958 let longest_line_length: f32 = lines
959 .iter()
960 .map(|line| {
961 let line_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
962 let total_target_size = line
963 .items
964 .iter()
965 .map(|child| {
966 let padding_border_sum = (child.padding + child.border).main_axis_sum(constants.dir);
967 (child.flex_basis.maybe_max(child.min_size.main(constants.dir))
968 + child.margin.main_axis_sum(constants.dir))
969 .max(padding_border_sum)
970 })
971 .sum::<f32>();
972 total_target_size + line_main_axis_gap
973 })
974 .max_by(|a, b| a.total_cmp(b))
975 .unwrap_or(0.0);
976 let size = longest_line_length + main_content_box_inset;
977 if lines.len() > 1 {
978 f32_max(size, main_axis_available_space)
979 } else {
980 size
981 }
982 }
983 AvailableSpace::MinContent if constants.is_wrap => {
984 let longest_line_length: f32 = lines
985 .iter()
986 .map(|line| {
987 let line_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
988 let total_target_size = line
989 .items
990 .iter()
991 .map(|child| {
992 let padding_border_sum = (child.padding + child.border).main_axis_sum(constants.dir);
993 (child.flex_basis.maybe_max(child.min_size.main(constants.dir))
994 + child.margin.main_axis_sum(constants.dir))
995 .max(padding_border_sum)
996 })
997 .sum::<f32>();
998 total_target_size + line_main_axis_gap
999 })
1000 .max_by(|a, b| a.total_cmp(b))
1001 .unwrap_or(0.0);
1002 longest_line_length + main_content_box_inset
1003 }
1004 AvailableSpace::MinContent | AvailableSpace::MaxContent => {
1005 // Define a base main_size variable. This is mutated once for iteration over the outer
1006 // loop over the flex lines as:
1007 // "The flex container’s max-content size is the largest sum of the afore-calculated sizes of all items within a single line."
1008 let mut main_size = 0.0;
1009
1010 for line in lines.iter_mut() {
1011 for item in line.items.iter_mut() {
1012 let style_min = item.min_size.main(constants.dir);
1013 let style_preferred = item.size.main(constants.dir);
1014 let style_max = item.max_size.main(constants.dir);
1015
1016 // The spec seems a bit unclear on this point (my initial reading was that the `.maybe_max(style_preferred)` should
1017 // not be included here), however this matches both Chrome and Firefox as of 9th March 2023.
1018 //
1019 // Spec: https://www.w3.org/TR/css-flexbox-1/#intrinsic-item-contributions
1020 // Spec modification: https://www.w3.org/TR/css-flexbox-1/#change-2016-max-contribution
1021 // Issue: https://github.com/w3c/csswg-drafts/issues/1435
1022 // Gentest: padding_border_overrides_size_flex_basis_0.html
1023 let clamping_basis = Some(item.flex_basis).maybe_max(style_preferred);
1024 let flex_basis_min = clamping_basis.filter(|_| item.flex_shrink == 0.0);
1025 let flex_basis_max = clamping_basis.filter(|_| item.flex_grow == 0.0);
1026
1027 let min_main_size = style_min
1028 .maybe_max(flex_basis_min)
1029 .or(flex_basis_min)
1030 .unwrap_or(item.resolved_minimum_main_size)
1031 .max(item.resolved_minimum_main_size);
1032 let max_main_size =
1033 style_max.maybe_min(flex_basis_max).or(flex_basis_max).unwrap_or(f32::INFINITY);
1034
1035 let content_contribution = match (min_main_size, style_preferred, max_main_size) {
1036 // If the clamping values are such that max <= min, then we can avoid the expensive step of computing the content size
1037 // as we know that the clamping values will override it anyway
1038 (min, Some(pref), max) if max <= min || max <= pref => {
1039 pref.min(max).max(min) + item.margin.main_axis_sum(constants.dir)
1040 }
1041 (min, _, max) if max <= min => min + item.margin.main_axis_sum(constants.dir),
1042
1043 // Else compute the min- or -max content size and apply the full formula for computing the
1044 // min- or max- content contribution
1045 _ if item.is_scroll_container() => {
1046 item.flex_basis + item.margin.main_axis_sum(constants.dir)
1047 }
1048 _ => {
1049 // Parent size for child sizing
1050 let cross_axis_parent_size = constants.node_inner_size.cross(dir);
1051
1052 // Available space for child sizing
1053 let cross_axis_margin_sum = constants.margin.cross_axis_sum(dir);
1054 let child_min_cross = item.min_size.cross(dir).maybe_add(cross_axis_margin_sum);
1055 let child_max_cross = item.max_size.cross(dir).maybe_add(cross_axis_margin_sum);
1056 let cross_axis_available_space: AvailableSpace = available_space
1057 .cross(dir)
1058 .map_definite_value(|val| cross_axis_parent_size.unwrap_or(val))
1059 .maybe_clamp(child_min_cross, child_max_cross);
1060
1061 let child_available_space = available_space.with_cross(dir, cross_axis_available_space);
1062
1063 // Known dimensions for child sizing
1064 let child_known_dimensions = {
1065 let mut ckd = item.size.with_main(dir, None);
1066 if item.align_self == AlignSelf::STRETCH && ckd.cross(dir).is_none() {
1067 ckd.set_cross(
1068 dir,
1069 cross_axis_available_space
1070 .into_option()
1071 .maybe_sub(item.margin.cross_axis_sum(dir)),
1072 );
1073 }
1074 ckd
1075 };
1076
1077 // Either the min- or max- content size depending on which constraint we are sizing under.
1078 // TODO: Optimise by using already computed values where available
1079 debug_log!("COMPUTE CHILD BASE SIZE (for intrinsic main size):");
1080 let content_main_size = tree.measure_child_size(
1081 item.node,
1082 child_known_dimensions,
1083 constants.node_inner_size,
1084 child_available_space,
1085 SizingMode::InherentSize,
1086 dir.main_axis(),
1087 Line::FALSE,
1088 ) + item.margin.main_axis_sum(constants.dir);
1089
1090 // This is somewhat bizarre in that it's asymmetrical depending whether the flex container is a column or a row.
1091 //
1092 // I *think* this might relate to https://drafts.csswg.org/css-flexbox-1/#algo-main-container:
1093 //
1094 // "The automatic block size of a block-level flex container is its max-content size."
1095 //
1096 // Which could suggest that flex-basis defining a vertical size does not shrink because it is in the block axis, and the automatic size
1097 // in the block axis is a MAX content size. Whereas a flex-basis defining a horizontal size does shrink because the automatic size in
1098 // inline axis is MIN content size (although I don't have a reference for that).
1099 //
1100 // Ultimately, this was not found by reading the spec, but by trial and error fixing tests to align with Webkit/Firefox output.
1101 // (see the `flex_basis_unconstraint_row` and `flex_basis_uncontraint_column` generated tests which demonstrate this)
1102 if constants.is_row {
1103 content_main_size.maybe_clamp(style_min, style_max).max(main_content_box_inset)
1104 } else {
1105 content_main_size
1106 .max(item.flex_basis)
1107 .maybe_clamp(style_min, style_max)
1108 .max(main_content_box_inset)
1109 }
1110 }
1111 };
1112 item.content_flex_fraction = {
1113 let diff = content_contribution - item.flex_basis;
1114 if diff > 0.0 {
1115 diff / f32_max(1.0, item.flex_grow)
1116 } else if diff < 0.0 {
1117 let scaled_shrink_factor = f32_max(1.0, item.flex_shrink * item.inner_flex_basis);
1118 diff / scaled_shrink_factor
1119 } else {
1120 // We are assuming that diff is 0.0 here and that we haven't accidentally introduced a NaN
1121 0.0
1122 }
1123 };
1124 }
1125
1126 // TODO Spec says to scale everything by the line's max flex fraction. But neither Chrome nor firefox implement this
1127 // so we don't either. But if we did want to, we'd need this computation here (and to use it below):
1128 //
1129 // Within each line, find the largest max-content flex fraction among all the flex items.
1130 // let line_flex_fraction = line
1131 // .items
1132 // .iter()
1133 // .map(|item| item.content_flex_fraction)
1134 // .max_by(|a, b| a.total_cmp(b))
1135 // .unwrap_or(0.0); // Unwrap case never gets hit because there is always at least one item a line
1136
1137 // Add each item’s flex base size to the product of:
1138 // - its flex grow factor (or scaled flex shrink factor,if the chosen max-content flex fraction was negative)
1139 // - the chosen max-content flex fraction
1140 // then clamp that result by the max main size floored by the min main size.
1141 //
1142 // The flex container’s max-content size is the largest sum of the afore-calculated sizes of all items within a single line.
1143 let item_main_size_sum = line
1144 .items
1145 .iter_mut()
1146 .map(|item| {
1147 let flex_fraction = item.content_flex_fraction;
1148 // let flex_fraction = line_flex_fraction;
1149
1150 let flex_contribution = if item.content_flex_fraction > 0.0 {
1151 f32_max(1.0, item.flex_grow) * flex_fraction
1152 } else if item.content_flex_fraction < 0.0 {
1153 let scaled_shrink_factor = f32_max(1.0, item.flex_shrink) * item.inner_flex_basis;
1154 scaled_shrink_factor * flex_fraction
1155 } else {
1156 0.0
1157 };
1158 let size = item.flex_basis + flex_contribution;
1159 item.outer_target_size.set_main(constants.dir, size);
1160 item.target_size.set_main(constants.dir, size);
1161 size
1162 })
1163 .sum::<f32>();
1164
1165 let gap_sum = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
1166 main_size = f32_max(main_size, item_main_size_sum + gap_sum)
1167 }
1168
1169 main_size + main_content_box_inset
1170 }
1171 }
1172 });
1173
1174 let outer_main_size = outer_main_size
1175 .maybe_clamp(constants.min_size.main(constants.dir), constants.max_size.main(constants.dir))
1176 .max(main_content_box_inset - constants.scrollbar_gutter.main(constants.dir));
1177
1178 // let outer_main_size = inner_main_size + constants.padding_border.main_axis_sum(constants.dir);
1179 let inner_main_size = f32_max(outer_main_size - main_content_box_inset, 0.0);
1180 constants.container_size.set_main(constants.dir, outer_main_size);
1181 constants.inner_container_size.set_main(constants.dir, inner_main_size);
1182 constants.node_inner_size.set_main(constants.dir, Some(inner_main_size));
1183}
1184
1185/// Resolve the flexible lengths of the items within a flex line.
1186/// Sets the `main` component of each item's `target_size` and `outer_target_size`
1187///
1188/// # [9.7. Resolving Flexible Lengths](https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths)
1189#[inline]
1190fn resolve_flexible_lengths(line: &mut FlexLine, constants: &AlgoConstants) {
1191 let total_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
1192
1193 // 1. Determine the used flex factor. Sum the outer hypothetical main sizes of all
1194 // items on the line. If the sum is less than the flex container’s inner main size,
1195 // use the flex grow factor for the rest of this algorithm; otherwise, use the
1196 // flex shrink factor.
1197
1198 let total_hypothetical_outer_main_size =
1199 line.items.iter().map(|child| child.hypothetical_outer_size.main(constants.dir)).sum::<f32>();
1200 let used_flex_factor: f32 = total_main_axis_gap + total_hypothetical_outer_main_size;
1201 let growing = used_flex_factor < constants.node_inner_size.main(constants.dir).unwrap_or(0.0);
1202 let shrinking = used_flex_factor > constants.node_inner_size.main(constants.dir).unwrap_or(0.0);
1203 let exactly_sized = !growing & !shrinking;
1204
1205 // 2. Size inflexible items. Freeze, setting its target main size to its hypothetical main size
1206 // - Any item that has a flex factor of zero
1207 // - If using the flex grow factor: any item that has a flex base size
1208 // greater than its hypothetical main size
1209 // - If using the flex shrink factor: any item that has a flex base size
1210 // smaller than its hypothetical main size
1211
1212 for child in line.items.iter_mut() {
1213 let inner_target_size = child.hypothetical_inner_size.main(constants.dir);
1214 child.target_size.set_main(constants.dir, inner_target_size);
1215
1216 if exactly_sized
1217 || (child.flex_grow == 0.0 && child.flex_shrink == 0.0)
1218 || (growing && child.flex_basis > child.hypothetical_inner_size.main(constants.dir))
1219 || (shrinking && child.flex_basis < child.hypothetical_inner_size.main(constants.dir))
1220 {
1221 child.frozen = true;
1222 let outer_target_size = inner_target_size + child.margin.main_axis_sum(constants.dir);
1223 child.outer_target_size.set_main(constants.dir, outer_target_size);
1224 }
1225 }
1226
1227 if exactly_sized {
1228 return;
1229 }
1230
1231 // 3. Calculate initial free space. Sum the outer sizes of all items on the line,
1232 // and subtract this from the flex container’s inner main size. For frozen items,
1233 // use their outer target main size; for other items, use their outer flex base size.
1234
1235 let used_space: f32 = total_main_axis_gap
1236 + line
1237 .items
1238 .iter()
1239 .map(|child| {
1240 if child.frozen {
1241 child.outer_target_size.main(constants.dir)
1242 } else {
1243 child.flex_basis + child.margin.main_axis_sum(constants.dir)
1244 }
1245 })
1246 .sum::<f32>();
1247
1248 let initial_free_space = constants.node_inner_size.main(constants.dir).maybe_sub(used_space).unwrap_or(0.0);
1249
1250 // 4. Loop
1251
1252 loop {
1253 // a. Check for flexible items. If all the flex items on the line are frozen,
1254 // free space has been distributed; exit this loop.
1255
1256 if line.items.iter().all(|child| child.frozen) {
1257 break;
1258 }
1259
1260 // b. Calculate the remaining free space as for initial free space, above.
1261 // If the sum of the unfrozen flex items’ flex factors is less than one,
1262 // multiply the initial free space by this sum. If the magnitude of this
1263 // value is less than the magnitude of the remaining free space, use this
1264 // as the remaining free space.
1265
1266 let used_space: f32 = total_main_axis_gap
1267 + line
1268 .items
1269 .iter()
1270 .map(|child| {
1271 if child.frozen {
1272 child.outer_target_size.main(constants.dir)
1273 } else {
1274 child.flex_basis + child.margin.main_axis_sum(constants.dir)
1275 }
1276 })
1277 .sum::<f32>();
1278
1279 let mut unfrozen: Vec<&mut FlexItem> = line.items.iter_mut().filter(|child| !child.frozen).collect();
1280
1281 let (sum_flex_grow, sum_flex_shrink): (f32, f32) =
1282 unfrozen.iter().fold((0.0, 0.0), |(flex_grow, flex_shrink), item| {
1283 (flex_grow + item.flex_grow, flex_shrink + item.flex_shrink)
1284 });
1285
1286 let free_space = if growing && sum_flex_grow < 1.0 {
1287 (initial_free_space * sum_flex_grow - total_main_axis_gap)
1288 .maybe_min(constants.node_inner_size.main(constants.dir).maybe_sub(used_space))
1289 } else if shrinking && sum_flex_shrink < 1.0 {
1290 (initial_free_space * sum_flex_shrink - total_main_axis_gap)
1291 .maybe_max(constants.node_inner_size.main(constants.dir).maybe_sub(used_space))
1292 } else {
1293 (constants.node_inner_size.main(constants.dir).maybe_sub(used_space))
1294 .unwrap_or(used_flex_factor - used_space)
1295 };
1296
1297 // c. Distribute free space proportional to the flex factors.
1298 // - If the remaining free space is zero
1299 // Do Nothing
1300 // - If using the flex grow factor
1301 // Find the ratio of the item’s flex grow factor to the sum of the
1302 // flex grow factors of all unfrozen items on the line. Set the item’s
1303 // target main size to its flex base size plus a fraction of the remaining
1304 // free space proportional to the ratio.
1305 // - If using the flex shrink factor
1306 // For every unfrozen item on the line, multiply its flex shrink factor by
1307 // its inner flex base size, and note this as its scaled flex shrink factor.
1308 // Find the ratio of the item’s scaled flex shrink factor to the sum of the
1309 // scaled flex shrink factors of all unfrozen items on the line. Set the item’s
1310 // target main size to its flex base size minus a fraction of the absolute value
1311 // of the remaining free space proportional to the ratio. Note this may result
1312 // in a negative inner main size; it will be corrected in the next step.
1313 // - Otherwise
1314 // Do Nothing
1315
1316 if free_space.is_normal() {
1317 if growing && sum_flex_grow > 0.0 {
1318 for child in &mut unfrozen {
1319 child
1320 .target_size
1321 .set_main(constants.dir, child.flex_basis + free_space * (child.flex_grow / sum_flex_grow));
1322 }
1323 } else if shrinking && sum_flex_shrink > 0.0 {
1324 let sum_scaled_shrink_factor: f32 =
1325 unfrozen.iter().map(|child| child.inner_flex_basis * child.flex_shrink).sum();
1326
1327 if sum_scaled_shrink_factor > 0.0 {
1328 for child in &mut unfrozen {
1329 let scaled_shrink_factor = child.inner_flex_basis * child.flex_shrink;
1330 child.target_size.set_main(
1331 constants.dir,
1332 child.flex_basis + free_space * (scaled_shrink_factor / sum_scaled_shrink_factor),
1333 )
1334 }
1335 }
1336 }
1337 }
1338
1339 // d. Fix min/max violations. Clamp each non-frozen item’s target main size by its
1340 // used min and max main sizes and floor its content-box size at zero. If the
1341 // item’s target main size was made smaller by this, it’s a max violation.
1342 // If the item’s target main size was made larger by this, it’s a min violation.
1343
1344 let total_violation = unfrozen.iter_mut().fold(0.0, |acc, child| -> f32 {
1345 let resolved_min_main: Option<f32> = child.resolved_minimum_main_size.into();
1346 let max_main = child.max_size.main(constants.dir);
1347 let clamped = child.target_size.main(constants.dir).maybe_clamp(resolved_min_main, max_main).max(0.0);
1348 child.violation = clamped - child.target_size.main(constants.dir);
1349 child.target_size.set_main(constants.dir, clamped);
1350 child.outer_target_size.set_main(
1351 constants.dir,
1352 child.target_size.main(constants.dir) + child.margin.main_axis_sum(constants.dir),
1353 );
1354
1355 acc + child.violation
1356 });
1357
1358 // e. Freeze over-flexed items. The total violation is the sum of the adjustments
1359 // from the previous step ∑(clamped size - unclamped size). If the total violation is:
1360 // - Zero
1361 // Freeze all items.
1362 // - Positive
1363 // Freeze all the items with min violations.
1364 // - Negative
1365 // Freeze all the items with max violations.
1366
1367 for child in &mut unfrozen {
1368 match total_violation {
1369 v if v > 0.0 => child.frozen = child.violation > 0.0,
1370 v if v < 0.0 => child.frozen = child.violation < 0.0,
1371 _ => child.frozen = true,
1372 }
1373 }
1374
1375 // f. Return to the start of this loop.
1376 }
1377}
1378
1379/// Determine the hypothetical cross size of each item.
1380///
1381/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1382///
1383/// - [**Determine the hypothetical cross size of each item**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-item)
1384/// by performing layout with the used main size and the available space, treating auto as fit-content.
1385#[inline]
1386fn determine_hypothetical_cross_size(
1387 tree: &mut impl LayoutFlexboxContainer,
1388 line: &mut FlexLine,
1389 constants: &AlgoConstants,
1390 available_space: Size<AvailableSpace>,
1391) {
1392 for child in line.items.iter_mut() {
1393 let padding_border_sum = (child.padding + child.border).cross_axis_sum(constants.dir);
1394
1395 let child_known_main = constants.container_size.main(constants.dir).into();
1396
1397 let child_cross = child
1398 .size
1399 .cross(constants.dir)
1400 .maybe_clamp(child.min_size.cross(constants.dir), child.max_size.cross(constants.dir))
1401 .maybe_max(padding_border_sum);
1402
1403 let child_available_cross = available_space
1404 .cross(constants.dir)
1405 .maybe_clamp(child.min_size.cross(constants.dir), child.max_size.cross(constants.dir))
1406 .maybe_max(padding_border_sum);
1407
1408 let child_inner_cross = child_cross.unwrap_or_else(|| {
1409 tree.measure_child_size(
1410 child.node,
1411 Size {
1412 width: if constants.is_row { child.target_size.width.into() } else { child_cross },
1413 height: if constants.is_row { child_cross } else { child.target_size.height.into() },
1414 },
1415 constants.node_inner_size,
1416 Size {
1417 width: if constants.is_row { child_known_main } else { child_available_cross },
1418 height: if constants.is_row { child_available_cross } else { child_known_main },
1419 },
1420 SizingMode::ContentSize,
1421 constants.dir.cross_axis(),
1422 Line::FALSE,
1423 )
1424 .maybe_clamp(child.min_size.cross(constants.dir), child.max_size.cross(constants.dir))
1425 .max(padding_border_sum)
1426 });
1427 let child_outer_cross = child_inner_cross + child.margin.cross_axis_sum(constants.dir);
1428
1429 child.hypothetical_inner_size.set_cross(constants.dir, child_inner_cross);
1430 child.hypothetical_outer_size.set_cross(constants.dir, child_outer_cross);
1431 }
1432}
1433
1434/// Calculate the base lines of the children.
1435#[inline]
1436fn calculate_children_base_lines(
1437 tree: &mut impl LayoutFlexboxContainer,
1438 node_size: Size<Option<f32>>,
1439 available_space: Size<AvailableSpace>,
1440 flex_lines: &mut [FlexLine],
1441 constants: &AlgoConstants,
1442) {
1443 // Only compute baselines for flex rows because we only support baseline alignment in the cross axis
1444 // where that axis is also the inline axis
1445 // TODO: this may need revisiting if/when we support vertical writing modes
1446 if !constants.is_row {
1447 return;
1448 }
1449
1450 for line in flex_lines {
1451 // If a flex line has one or zero items participating in baseline alignment then baseline alignment is a no-op so we skip
1452 let line_baseline_child_count =
1453 line.items.iter().filter(|child| child.align_self == AlignSelf::BASELINE).count();
1454 if line_baseline_child_count <= 1 {
1455 continue;
1456 }
1457
1458 for child in line.items.iter_mut() {
1459 // Only calculate baselines for children participating in baseline alignment
1460 if child.align_self != AlignSelf::BASELINE {
1461 continue;
1462 }
1463
1464 let measured_size_and_baselines = tree.perform_child_layout(
1465 child.node,
1466 Size {
1467 width: if constants.is_row {
1468 child.target_size.width.into()
1469 } else {
1470 child.hypothetical_inner_size.width.into()
1471 },
1472 height: if constants.is_row {
1473 child.hypothetical_inner_size.height.into()
1474 } else {
1475 child.target_size.height.into()
1476 },
1477 },
1478 constants.node_inner_size,
1479 Size {
1480 width: if constants.is_row {
1481 constants.container_size.width.into()
1482 } else {
1483 available_space.width.maybe_set(node_size.width)
1484 },
1485 height: if constants.is_row {
1486 available_space.height.maybe_set(node_size.height)
1487 } else {
1488 constants.container_size.height.into()
1489 },
1490 },
1491 SizingMode::ContentSize,
1492 Line::FALSE,
1493 );
1494
1495 let baseline = measured_size_and_baselines.first_baselines.y;
1496 let height = measured_size_and_baselines.size.height;
1497
1498 child.baseline = baseline.unwrap_or(height) + child.margin.top;
1499 }
1500 }
1501}
1502
1503/// Calculate the cross size of each flex line.
1504///
1505/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1506///
1507/// - [**Calculate the cross size of each flex line**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-line).
1508#[inline]
1509fn calculate_cross_size(flex_lines: &mut [FlexLine], node_size: Size<Option<f32>>, constants: &AlgoConstants) {
1510 // If the flex container is single-line and has a definite cross size,
1511 // the cross size of the flex line is the flex container’s inner cross size.
1512 if !constants.is_wrap && node_size.cross(constants.dir).is_some() {
1513 let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1514 let cross_min_size = constants.min_size.cross(constants.dir);
1515 let cross_max_size = constants.max_size.cross(constants.dir);
1516 flex_lines[0].cross_size = node_size
1517 .cross(constants.dir)
1518 .maybe_clamp(cross_min_size, cross_max_size)
1519 .maybe_sub(cross_axis_padding_border)
1520 .maybe_max(0.0)
1521 .unwrap_or(0.0);
1522 } else {
1523 // Otherwise, for each flex line:
1524 //
1525 // 1. Collect all the flex items whose inline-axis is parallel to the main-axis, whose
1526 // align-self is baseline, and whose cross-axis margins are both non-auto. Find the
1527 // largest of the distances between each item’s baseline and its hypothetical outer
1528 // cross-start edge, and the largest of the distances between each item’s baseline
1529 // and its hypothetical outer cross-end edge, and sum these two values.
1530
1531 // 2. Among all the items not collected by the previous step, find the largest
1532 // outer hypothetical cross size.
1533
1534 // 3. The used cross-size of the flex line is the largest of the numbers found in the
1535 // previous two steps and zero.
1536 for line in flex_lines.iter_mut() {
1537 let max_baseline: f32 = line.items.iter().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x));
1538 line.cross_size = line
1539 .items
1540 .iter()
1541 .map(|child| {
1542 if child.align_self == AlignSelf::BASELINE
1543 && !child.margin_is_auto.cross_start(constants.dir)
1544 && !child.margin_is_auto.cross_end(constants.dir)
1545 {
1546 max_baseline - child.baseline + child.hypothetical_outer_size.cross(constants.dir)
1547 } else {
1548 child.hypothetical_outer_size.cross(constants.dir)
1549 }
1550 })
1551 .fold(0.0, |acc, x| acc.max(x));
1552 }
1553
1554 // If the flex container is single-line, then clamp the line’s cross-size to be within the container’s computed min and max cross sizes.
1555 // Note that if CSS 2.1’s definition of min/max-width/height applied more generally, this behavior would fall out automatically.
1556 if !constants.is_wrap {
1557 let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1558 let cross_min_size = constants.min_size.cross(constants.dir);
1559 let cross_max_size = constants.max_size.cross(constants.dir);
1560 flex_lines[0].cross_size = flex_lines[0].cross_size.maybe_clamp(
1561 cross_min_size.maybe_sub(cross_axis_padding_border),
1562 cross_max_size.maybe_sub(cross_axis_padding_border),
1563 );
1564 }
1565 }
1566}
1567
1568/// Handle 'align-content: stretch'.
1569///
1570/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1571///
1572/// - [**Handle 'align-content: stretch'**](https://www.w3.org/TR/css-flexbox-1/#algo-line-stretch). If the flex container has a definite cross size, align-content is stretch,
1573/// and the sum of the flex lines' cross sizes is less than the flex container’s inner cross size,
1574/// increase the cross size of each flex line by equal amounts such that the sum of their cross sizes exactly equals the flex container’s inner cross size.
1575#[inline]
1576fn handle_align_content_stretch(flex_lines: &mut [FlexLine], node_size: Size<Option<f32>>, constants: &AlgoConstants) {
1577 if constants.align_content == AlignContent::STRETCH {
1578 let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1579 let cross_min_size = constants.min_size.cross(constants.dir);
1580 let cross_max_size = constants.max_size.cross(constants.dir);
1581 let container_min_inner_cross = node_size
1582 .cross(constants.dir)
1583 .or(cross_min_size)
1584 .maybe_clamp(cross_min_size, cross_max_size)
1585 .maybe_sub(cross_axis_padding_border)
1586 .maybe_max(0.0)
1587 .unwrap_or(0.0);
1588
1589 let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len());
1590 let lines_total_cross: f32 = flex_lines.iter().map(|line| line.cross_size).sum::<f32>() + total_cross_axis_gap;
1591
1592 if lines_total_cross < container_min_inner_cross {
1593 let remaining = container_min_inner_cross - lines_total_cross;
1594 let addition = remaining / flex_lines.len() as f32;
1595 flex_lines.iter_mut().for_each(|line| line.cross_size += addition);
1596 }
1597 }
1598}
1599
1600/// Determine the used cross size of each flex item.
1601///
1602/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1603///
1604/// - [**Determine the used cross size of each flex item**](https://www.w3.org/TR/css-flexbox-1/#algo-stretch). If a flex item has align-self: stretch, its computed cross size property is auto,
1605/// and neither of its cross-axis margins are auto, the used outer cross size is the used cross size of its flex line, clamped according to the item’s used min and max cross sizes.
1606/// Otherwise, the used cross size is the item’s hypothetical cross size.
1607///
1608/// If the flex item has align-self: stretch, redo layout for its contents, treating this used size as its definite cross size so that percentage-sized children can be resolved.
1609///
1610/// **Note that this step does not affect the main size of the flex item, even if it has an intrinsic aspect ratio**.
1611#[inline]
1612fn determine_used_cross_size(
1613 tree: &impl LayoutFlexboxContainer,
1614 flex_lines: &mut [FlexLine],
1615 constants: &AlgoConstants,
1616) {
1617 for line in flex_lines {
1618 let line_cross_size = line.cross_size;
1619
1620 for child in line.items.iter_mut() {
1621 let child_style = tree.get_flexbox_child_style(child.node);
1622 child.target_size.set_cross(
1623 constants.dir,
1624 if child.align_self == AlignSelf::STRETCH
1625 && !child.margin_is_auto.cross_start(constants.dir)
1626 && !child.margin_is_auto.cross_end(constants.dir)
1627 && child_style.size().cross(constants.dir).is_auto()
1628 {
1629 // For some reason this particular usage of max_width is an exception to the rule that max_width's transfer
1630 // using the aspect_ratio (if set). Both Chrome and Firefox agree on this. And reading the spec, it seems like
1631 // a reasonable interpretation. Although it seems to me that the spec *should* apply aspect_ratio here.
1632 let padding = child_style
1633 .padding()
1634 .resolve_or_zero(constants.node_inner_size, |val, basis| tree.calc(val, basis));
1635 let border = child_style
1636 .border()
1637 .resolve_or_zero(constants.node_inner_size, |val, basis| tree.calc(val, basis));
1638 let pb_sum = (padding + border).sum_axes();
1639 let box_sizing_adjustment =
1640 if child_style.box_sizing() == BoxSizing::ContentBox { pb_sum } else { Size::ZERO };
1641
1642 let max_size_ignoring_aspect_ratio = child_style
1643 .max_size()
1644 .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis))
1645 .maybe_add(box_sizing_adjustment);
1646
1647 (line_cross_size - child.margin.cross_axis_sum(constants.dir)).maybe_clamp(
1648 child.min_size.cross(constants.dir),
1649 max_size_ignoring_aspect_ratio.cross(constants.dir),
1650 )
1651 } else {
1652 child.hypothetical_inner_size.cross(constants.dir)
1653 },
1654 );
1655
1656 child.outer_target_size.set_cross(
1657 constants.dir,
1658 child.target_size.cross(constants.dir) + child.margin.cross_axis_sum(constants.dir),
1659 );
1660 }
1661 }
1662}
1663
1664/// Distribute any remaining free space.
1665///
1666/// # [9.5. Main-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#main-alignment)
1667///
1668/// - [**Distribute any remaining free space**](https://www.w3.org/TR/css-flexbox-1/#algo-main-align). For each flex line:
1669///
1670/// 1. If the remaining free space is positive and at least one main-axis margin on this line is `auto`, distribute the free space equally among these margins.
1671/// Otherwise, set all `auto` margins to zero.
1672///
1673/// 2. Align the items along the main-axis per `justify-content`.
1674#[inline]
1675fn distribute_remaining_free_space(flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1676 for line in flex_lines {
1677 let total_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
1678 let used_space: f32 = total_main_axis_gap
1679 + line.items.iter().map(|child| child.outer_target_size.main(constants.dir)).sum::<f32>();
1680 let free_space = constants.inner_container_size.main(constants.dir) - used_space;
1681 let mut num_auto_margins = 0;
1682
1683 for child in line.items.iter_mut() {
1684 if child.margin_is_auto.main_start(constants.dir) {
1685 num_auto_margins += 1;
1686 }
1687 if child.margin_is_auto.main_end(constants.dir) {
1688 num_auto_margins += 1;
1689 }
1690 }
1691
1692 if free_space > 0.0 && num_auto_margins > 0 {
1693 let margin = free_space / num_auto_margins as f32;
1694
1695 for child in line.items.iter_mut() {
1696 if child.margin_is_auto.main_start(constants.dir) {
1697 if constants.is_row {
1698 child.margin.left = margin;
1699 } else {
1700 child.margin.top = margin;
1701 }
1702 }
1703 if child.margin_is_auto.main_end(constants.dir) {
1704 if constants.is_row {
1705 child.margin.right = margin;
1706 } else {
1707 child.margin.bottom = margin;
1708 }
1709 }
1710 }
1711 }
1712
1713 let num_items = line.items.len();
1714 let layout_reverse = constants.dir.is_reverse();
1715 let gap = constants.gap.main(constants.dir);
1716 let raw_justify_content_mode = constants.justify_content.unwrap_or(JustifyContent::FLEX_START);
1717 let justify_content_mode = apply_alignment_fallback(free_space, num_items, raw_justify_content_mode);
1718
1719 let justify_item = |(i, child): (usize, &mut FlexItem)| {
1720 child.offset_main =
1721 compute_alignment_offset(free_space, num_items, gap, justify_content_mode, layout_reverse, i == 0);
1722 };
1723
1724 if layout_reverse {
1725 line.items.iter_mut().rev().enumerate().for_each(justify_item);
1726 } else {
1727 line.items.iter_mut().enumerate().for_each(justify_item);
1728 }
1729 }
1730}
1731
1732/// Resolve cross-axis `auto` margins.
1733///
1734/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1735///
1736/// - [**Resolve cross-axis `auto` margins**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-margins).
1737/// If a flex item has auto cross-axis margins:
1738///
1739/// - If its outer cross size (treating those auto margins as zero) is less than the cross size of its flex line,
1740/// distribute the difference in those sizes equally to the auto margins.
1741///
1742/// - Otherwise, if the block-start or inline-start margin (whichever is in the cross axis) is auto, set it to zero.
1743/// Set the opposite margin so that the outer cross size of the item equals the cross size of its flex line.
1744#[inline]
1745fn resolve_cross_axis_auto_margins(flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1746 for line in flex_lines {
1747 let line_cross_size = line.cross_size;
1748 let max_baseline: f32 = line.items.iter_mut().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x));
1749
1750 for child in line.items.iter_mut() {
1751 let free_space = line_cross_size - child.outer_target_size.cross(constants.dir);
1752
1753 if child.margin_is_auto.cross_start(constants.dir) && child.margin_is_auto.cross_end(constants.dir) {
1754 if constants.is_row {
1755 child.margin.top = free_space / 2.0;
1756 child.margin.bottom = free_space / 2.0;
1757 } else {
1758 child.margin.left = free_space / 2.0;
1759 child.margin.right = free_space / 2.0;
1760 }
1761 } else if child.margin_is_auto.cross_start(constants.dir) {
1762 if constants.is_row {
1763 child.margin.top = free_space;
1764 } else {
1765 child.margin.left = free_space;
1766 }
1767 } else if child.margin_is_auto.cross_end(constants.dir) {
1768 if constants.is_row {
1769 child.margin.bottom = free_space;
1770 } else {
1771 child.margin.right = free_space;
1772 }
1773 } else {
1774 // 14. Align all flex items along the cross-axis.
1775 child.offset_cross = align_flex_items_along_cross_axis(child, free_space, max_baseline, constants);
1776 }
1777 }
1778 }
1779}
1780
1781/// Align all flex items along the cross-axis.
1782///
1783/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1784///
1785/// - [**Align all flex items along the cross-axis**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-align) per `align-self`,
1786/// if neither of the item's cross-axis margins are `auto`.
1787#[inline]
1788fn align_flex_items_along_cross_axis(
1789 child: &FlexItem,
1790 free_space: f32,
1791 max_baseline: f32,
1792 constants: &AlgoConstants,
1793) -> f32 {
1794 let cross_axis_should_reverse = constants.is_column && matches!(constants.layout_direction, Direction::Rtl);
1795
1796 // If align-self uses a "safe" overflow-position keyword and the item would overflow its
1797 // line cross size, fall back to logical Start to avoid data loss. See CSS Box Alignment 3
1798 // §4.3 <https://www.w3.org/TR/css-align-3/#overflow-values>. Otherwise, drop the safety
1799 // field so the match below operates on a bare keyword and stays exhaustive.
1800 let align_keyword = if child.align_self.is_safe() && free_space < 0.0 {
1801 AlignItemsKeyword::Start
1802 } else {
1803 child.align_self.keyword
1804 };
1805
1806 match align_keyword {
1807 AlignItemsKeyword::Start => {
1808 if cross_axis_should_reverse {
1809 free_space
1810 } else {
1811 0.0
1812 }
1813 }
1814 AlignItemsKeyword::FlexStart => {
1815 if constants.is_wrap_reverse ^ cross_axis_should_reverse {
1816 free_space
1817 } else {
1818 0.0
1819 }
1820 }
1821 AlignItemsKeyword::End => {
1822 if cross_axis_should_reverse {
1823 0.0
1824 } else {
1825 free_space
1826 }
1827 }
1828 AlignItemsKeyword::FlexEnd => {
1829 if constants.is_wrap_reverse ^ cross_axis_should_reverse {
1830 0.0
1831 } else {
1832 free_space
1833 }
1834 }
1835 AlignItemsKeyword::Center => free_space / 2.0,
1836 AlignItemsKeyword::Baseline => {
1837 if constants.is_row {
1838 max_baseline - child.baseline
1839 } else {
1840 // Until we support vertical writing modes, baseline alignment only makes sense if
1841 // the constants.direction is row, so we treat it as flex-start alignment in columns.
1842 let baseline_column_should_reverse = cross_axis_should_reverse && !constants.is_wrap;
1843 if constants.is_wrap_reverse ^ baseline_column_should_reverse {
1844 free_space
1845 } else {
1846 0.0
1847 }
1848 }
1849 }
1850 AlignItemsKeyword::Stretch => {
1851 if constants.is_wrap_reverse ^ cross_axis_should_reverse {
1852 free_space
1853 } else {
1854 0.0
1855 }
1856 }
1857 }
1858}
1859
1860/// Determine the flex container’s used cross size.
1861///
1862/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1863///
1864/// - [**Determine the flex container’s used cross size**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-container):
1865///
1866/// - If the cross size property is a definite size, use that, clamped by the used min and max cross sizes of the flex container.
1867///
1868/// - Otherwise, use the sum of the flex lines' cross sizes, clamped by the used min and max cross sizes of the flex container.
1869#[inline]
1870#[must_use]
1871fn determine_container_cross_size(
1872 flex_lines: &[FlexLine],
1873 node_size: Size<Option<f32>>,
1874 constants: &mut AlgoConstants,
1875) -> f32 {
1876 let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len());
1877 let total_line_cross_size: f32 = flex_lines.iter().map(|line| line.cross_size).sum::<f32>();
1878
1879 let padding_border_sum = constants.content_box_inset.cross_axis_sum(constants.dir);
1880 let cross_scrollbar_gutter = constants.scrollbar_gutter.cross(constants.dir);
1881 let min_cross_size = constants.min_size.cross(constants.dir);
1882 let max_cross_size = constants.max_size.cross(constants.dir);
1883 let outer_container_size = node_size
1884 .cross(constants.dir)
1885 .unwrap_or(total_line_cross_size + total_cross_axis_gap + padding_border_sum)
1886 .maybe_clamp(min_cross_size, max_cross_size)
1887 .max(padding_border_sum - cross_scrollbar_gutter);
1888 let inner_container_size = f32_max(outer_container_size - padding_border_sum, 0.0);
1889
1890 constants.container_size.set_cross(constants.dir, outer_container_size);
1891 constants.inner_container_size.set_cross(constants.dir, inner_container_size);
1892
1893 total_line_cross_size
1894}
1895
1896/// Align all flex lines per `align-content`.
1897///
1898/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1899///
1900/// - [**Align all flex lines**](https://www.w3.org/TR/css-flexbox-1/#algo-line-align) per `align-content`.
1901#[inline]
1902fn align_flex_lines_per_align_content(flex_lines: &mut [FlexLine], constants: &AlgoConstants, total_cross_size: f32) {
1903 let num_lines = flex_lines.len();
1904 let gap = constants.gap.cross(constants.dir);
1905 let total_cross_axis_gap = sum_axis_gaps(gap, num_lines);
1906 let free_space = constants.inner_container_size.cross(constants.dir) - total_cross_size - total_cross_axis_gap;
1907
1908 let align_content_mode = apply_alignment_fallback(free_space, num_lines, constants.align_content);
1909
1910 let align_line = |(i, line): (usize, &mut FlexLine)| {
1911 line.offset_cross =
1912 compute_alignment_offset(free_space, num_lines, gap, align_content_mode, constants.is_wrap_reverse, i == 0);
1913 };
1914
1915 if constants.is_wrap_reverse {
1916 flex_lines.iter_mut().rev().enumerate().for_each(align_line);
1917 } else {
1918 flex_lines.iter_mut().enumerate().for_each(align_line);
1919 }
1920}
1921
1922/// Calculates the layout for a flex-item
1923#[allow(clippy::too_many_arguments)]
1924fn calculate_flex_item(
1925 tree: &mut impl LayoutFlexboxContainer,
1926 item: &mut FlexItem,
1927 total_offset_main: &mut f32,
1928 total_offset_cross: f32,
1929 line_offset_cross: f32,
1930 #[cfg(feature = "content_size")] total_content_size: &mut Size<f32>,
1931 container_size: Size<f32>,
1932 node_inner_size: Size<Option<f32>>,
1933 direction: FlexDirection,
1934 layout_direction: Direction,
1935) {
1936 let layout_output = tree.perform_child_layout(
1937 item.node,
1938 item.target_size.map(|s| s.into()),
1939 node_inner_size,
1940 container_size.map(|s| s.into()),
1941 SizingMode::ContentSize,
1942 Line::FALSE,
1943 );
1944 let LayoutOutput {
1945 size,
1946 #[cfg(feature = "content_size")]
1947 content_size,
1948 ..
1949 } = layout_output;
1950
1951 let is_rtl_row = direction.is_row() && layout_direction.is_rtl();
1952 let is_rtl_column = direction.is_column() && layout_direction.is_rtl();
1953 let main_relative_inset = if is_rtl_row {
1954 item.inset.main_end(direction).or(item.inset.main_start(direction).map(|pos| -pos)).unwrap_or(0.0)
1955 } else {
1956 item.inset.main_start(direction).or(item.inset.main_end(direction).map(|pos| -pos)).unwrap_or(0.0)
1957 };
1958 let cross_relative_inset = if is_rtl_column {
1959 item.inset.cross_end(direction).map(|pos| -pos).or(item.inset.cross_start(direction)).unwrap_or(0.0)
1960 } else {
1961 item.inset.cross_start(direction).or(item.inset.cross_end(direction).map(|pos| -pos)).unwrap_or(0.0)
1962 };
1963 let effective_line_offset_cross = if is_rtl_column { 0.0 } else { line_offset_cross };
1964
1965 let offset_main = if is_rtl_row {
1966 *total_offset_main - item.offset_main - item.margin.main_end(direction) - main_relative_inset - size.width
1967 } else {
1968 *total_offset_main + item.offset_main + item.margin.main_start(direction) + main_relative_inset
1969 };
1970
1971 let offset_cross = total_offset_cross
1972 + item.offset_cross
1973 + effective_line_offset_cross
1974 + item.margin.cross_start(direction)
1975 + cross_relative_inset;
1976
1977 if direction.is_row() {
1978 let baseline_offset_cross =
1979 total_offset_cross + item.offset_cross + effective_line_offset_cross + item.margin.cross_start(direction);
1980 let inner_baseline = layout_output.first_baselines.y.unwrap_or(size.height);
1981 item.baseline = baseline_offset_cross + inner_baseline;
1982 } else {
1983 let baseline_offset_main = *total_offset_main + item.offset_main + item.margin.main_start(direction);
1984 let inner_baseline = layout_output.first_baselines.y.unwrap_or(size.height);
1985 item.baseline = baseline_offset_main + inner_baseline;
1986 }
1987
1988 let location = if direction.is_row() {
1989 Point { x: offset_main, y: offset_cross }
1990 } else {
1991 Point { x: offset_cross, y: offset_main }
1992 };
1993 let scrollbar_size = Size {
1994 width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
1995 height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
1996 };
1997
1998 tree.set_unrounded_layout(
1999 item.node,
2000 &Layout {
2001 order: item.order,
2002 size,
2003 #[cfg(feature = "content_size")]
2004 content_size,
2005 scrollbar_size,
2006 location,
2007 padding: item.padding,
2008 border: item.border,
2009 margin: item.margin,
2010 },
2011 );
2012
2013 if is_rtl_row {
2014 *total_offset_main -= item.offset_main + item.margin.main_axis_sum(direction) + size.main(direction);
2015 } else {
2016 *total_offset_main += item.offset_main + item.margin.main_axis_sum(direction) + size.main(direction);
2017 }
2018
2019 #[cfg(feature = "content_size")]
2020 {
2021 let contribution_location = if layout_direction.is_rtl() {
2022 Point { x: container_size.width - (location.x + size.width), y: location.y }
2023 } else {
2024 location
2025 };
2026 *total_content_size = total_content_size.f32_max(compute_content_size_contribution(
2027 contribution_location,
2028 size,
2029 content_size,
2030 item.overflow,
2031 ));
2032 }
2033}
2034
2035/// Calculates the layout line
2036#[allow(clippy::too_many_arguments)]
2037fn calculate_layout_line(
2038 tree: &mut impl LayoutFlexboxContainer,
2039 line: &mut FlexLine,
2040 total_offset_cross: &mut f32,
2041 #[cfg(feature = "content_size")] content_size: &mut Size<f32>,
2042 container_size: Size<f32>,
2043 node_inner_size: Size<Option<f32>>,
2044 padding_border: Rect<f32>,
2045 direction: FlexDirection,
2046 layout_direction: Direction,
2047) {
2048 let mut total_offset_main = if layout_direction.is_rtl() && direction.is_row() {
2049 container_size.width - padding_border.main_end(direction)
2050 } else {
2051 padding_border.main_start(direction)
2052 };
2053 let line_offset_cross = line.offset_cross;
2054
2055 let is_rtl_column = layout_direction.is_rtl() && direction.is_column();
2056 if is_rtl_column {
2057 *total_offset_cross -= line_offset_cross + line.cross_size;
2058 }
2059
2060 if direction.is_reverse() {
2061 for item in line.items.iter_mut().rev() {
2062 calculate_flex_item(
2063 tree,
2064 item,
2065 &mut total_offset_main,
2066 *total_offset_cross,
2067 line_offset_cross,
2068 #[cfg(feature = "content_size")]
2069 content_size,
2070 container_size,
2071 node_inner_size,
2072 direction,
2073 layout_direction,
2074 );
2075 }
2076 } else {
2077 for item in line.items.iter_mut() {
2078 calculate_flex_item(
2079 tree,
2080 item,
2081 &mut total_offset_main,
2082 *total_offset_cross,
2083 line_offset_cross,
2084 #[cfg(feature = "content_size")]
2085 content_size,
2086 container_size,
2087 node_inner_size,
2088 direction,
2089 layout_direction,
2090 );
2091 }
2092 }
2093
2094 if !is_rtl_column {
2095 *total_offset_cross += line_offset_cross + line.cross_size;
2096 }
2097}
2098
2099/// Do a final layout pass and collect the resulting layouts.
2100#[inline]
2101fn final_layout_pass(
2102 tree: &mut impl LayoutFlexboxContainer,
2103 flex_lines: &mut [FlexLine],
2104 constants: &AlgoConstants,
2105) -> Size<f32> {
2106 let mut total_offset_cross = if constants.is_column && constants.layout_direction.is_rtl() {
2107 constants.container_size.width - constants.content_box_inset.cross_end(constants.dir)
2108 } else {
2109 constants.content_box_inset.cross_start(constants.dir)
2110 };
2111
2112 #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
2113 let mut content_size = Size::ZERO;
2114
2115 if constants.is_wrap_reverse {
2116 for line in flex_lines.iter_mut().rev() {
2117 calculate_layout_line(
2118 tree,
2119 line,
2120 &mut total_offset_cross,
2121 #[cfg(feature = "content_size")]
2122 &mut content_size,
2123 constants.container_size,
2124 constants.node_inner_size,
2125 constants.content_box_inset,
2126 constants.dir,
2127 constants.layout_direction,
2128 );
2129 }
2130 } else {
2131 for line in flex_lines.iter_mut() {
2132 calculate_layout_line(
2133 tree,
2134 line,
2135 &mut total_offset_cross,
2136 #[cfg(feature = "content_size")]
2137 &mut content_size,
2138 constants.container_size,
2139 constants.node_inner_size,
2140 constants.content_box_inset,
2141 constants.dir,
2142 constants.layout_direction,
2143 );
2144 }
2145 }
2146
2147 content_size.width += if constants.layout_direction.is_rtl() {
2148 constants.content_box_inset.left - constants.border.left - constants.scrollbar_gutter.x
2149 } else {
2150 constants.content_box_inset.right - constants.border.right - constants.scrollbar_gutter.x
2151 };
2152 content_size.height += constants.content_box_inset.bottom - constants.border.bottom - constants.scrollbar_gutter.y;
2153
2154 content_size
2155}
2156
2157/// Perform absolute layout on all absolutely positioned children.
2158#[inline]
2159fn perform_absolute_layout_on_absolute_children(
2160 tree: &mut impl LayoutFlexboxContainer,
2161 node: NodeId,
2162 constants: &AlgoConstants,
2163) -> Size<f32> {
2164 let container_width = constants.container_size.width;
2165 let container_height = constants.container_size.height;
2166 let inset_relative_size =
2167 constants.container_size - constants.border.sum_axes() - constants.scrollbar_gutter.into();
2168
2169 #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
2170 let mut content_size = Size::ZERO;
2171
2172 for order in 0..tree.child_count(node) {
2173 let child = tree.get_child_id(node, order);
2174 let child_style = tree.get_flexbox_child_style(child);
2175
2176 // Skip items that are display:none or are not position:absolute
2177 if child_style.box_generation_mode() == BoxGenerationMode::None || child_style.position() != Position::Absolute
2178 {
2179 continue;
2180 }
2181
2182 let overflow = child_style.overflow();
2183 let scrollbar_width = child_style.scrollbar_width();
2184 let aspect_ratio = child_style.aspect_ratio();
2185 let align_self = child_style.align_self().unwrap_or(constants.align_items);
2186 let margin = child_style
2187 .margin()
2188 .map(|margin| margin.resolve_to_option(inset_relative_size.width, |val, basis| tree.calc(val, basis)));
2189 let padding =
2190 child_style.padding().resolve_or_zero(Some(inset_relative_size.width), |val, basis| tree.calc(val, basis));
2191 let border =
2192 child_style.border().resolve_or_zero(Some(inset_relative_size.width), |val, basis| tree.calc(val, basis));
2193 let padding_border_sum = (padding + border).sum_axes();
2194 let box_sizing_adjustment =
2195 if child_style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO };
2196
2197 // Resolve inset
2198 // Insets are resolved against the container size minus border
2199 let left =
2200 child_style.inset().left.maybe_resolve(inset_relative_size.width, |val, basis| tree.calc(val, basis));
2201 let right =
2202 child_style.inset().right.maybe_resolve(inset_relative_size.width, |val, basis| tree.calc(val, basis));
2203 let top = child_style.inset().top.maybe_resolve(inset_relative_size.height, |val, basis| tree.calc(val, basis));
2204 let bottom =
2205 child_style.inset().bottom.maybe_resolve(inset_relative_size.height, |val, basis| tree.calc(val, basis));
2206
2207 // Compute known dimensions from min/max/inherent size styles
2208 let style_size = child_style
2209 .size()
2210 .maybe_resolve(inset_relative_size, |val, basis| tree.calc(val, basis))
2211 .maybe_apply_aspect_ratio(aspect_ratio)
2212 .maybe_add(box_sizing_adjustment);
2213 let min_size = child_style
2214 .min_size()
2215 .maybe_resolve(inset_relative_size, |val, basis| tree.calc(val, basis))
2216 .maybe_apply_aspect_ratio(aspect_ratio)
2217 .maybe_add(box_sizing_adjustment)
2218 .or(padding_border_sum.map(Some))
2219 .maybe_max(padding_border_sum);
2220 let max_size = child_style
2221 .max_size()
2222 .maybe_resolve(inset_relative_size, |val, basis| tree.calc(val, basis))
2223 .maybe_apply_aspect_ratio(aspect_ratio)
2224 .maybe_add(box_sizing_adjustment);
2225 let mut known_dimensions = style_size.maybe_clamp(min_size, max_size);
2226
2227 drop(child_style);
2228
2229 // Fill in width from left/right and reapply aspect ratio if:
2230 // - Width is not already known
2231 // - Item has both left and right inset properties set
2232 if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) {
2233 let new_width_raw = inset_relative_size.width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right;
2234 known_dimensions.width = Some(f32_max(new_width_raw, 0.0));
2235 known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
2236 }
2237
2238 // Fill in height from top/bottom and reapply aspect ratio if:
2239 // - Height is not already known
2240 // - Item has both top and bottom inset properties set
2241 if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) {
2242 let new_height_raw =
2243 inset_relative_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom;
2244 known_dimensions.height = Some(f32_max(new_height_raw, 0.0));
2245 known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
2246 }
2247 let measured_size = tree.measure_child_size_both(
2248 child,
2249 known_dimensions,
2250 constants.node_inner_size,
2251 Size {
2252 width: AvailableSpace::Definite(container_width.maybe_clamp(min_size.width, max_size.width)),
2253 height: AvailableSpace::Definite(container_height.maybe_clamp(min_size.height, max_size.height)),
2254 },
2255 SizingMode::InherentSize,
2256 Line::FALSE,
2257 );
2258 let final_size = known_dimensions.unwrap_or(measured_size).maybe_clamp(min_size, max_size);
2259
2260 let layout_output = tree.perform_child_layout(
2261 child,
2262 final_size.map(Some),
2263 constants.node_inner_size,
2264 Size {
2265 width: AvailableSpace::Definite(container_width.maybe_clamp(min_size.width, max_size.width)),
2266 height: AvailableSpace::Definite(container_height.maybe_clamp(min_size.height, max_size.height)),
2267 },
2268 SizingMode::InherentSize,
2269 Line::FALSE,
2270 );
2271
2272 let non_auto_margin = margin.map(|m| m.unwrap_or(0.0));
2273
2274 let free_space = Size {
2275 width: constants.container_size.width - final_size.width - non_auto_margin.horizontal_axis_sum(),
2276 height: constants.container_size.height - final_size.height - non_auto_margin.vertical_axis_sum(),
2277 }
2278 .f32_max(Size::ZERO);
2279
2280 // Expand auto margins to fill available space
2281 let resolved_margin = {
2282 let auto_margin_size = Size {
2283 width: {
2284 let auto_margin_count = margin.left.is_none() as u8 + margin.right.is_none() as u8;
2285 if auto_margin_count > 0 {
2286 free_space.width / auto_margin_count as f32
2287 } else {
2288 0.0
2289 }
2290 },
2291 height: {
2292 let auto_margin_count = margin.top.is_none() as u8 + margin.bottom.is_none() as u8;
2293 if auto_margin_count > 0 {
2294 free_space.height / auto_margin_count as f32
2295 } else {
2296 0.0
2297 }
2298 },
2299 };
2300
2301 Rect {
2302 left: margin.left.unwrap_or(auto_margin_size.width),
2303 right: margin.right.unwrap_or(auto_margin_size.width),
2304 top: margin.top.unwrap_or(auto_margin_size.height),
2305 bottom: margin.bottom.unwrap_or(auto_margin_size.height),
2306 }
2307 };
2308
2309 // Determine flex-relative insets
2310 let (start_main, end_main) = if constants.is_row { (left, right) } else { (top, bottom) };
2311 let (start_cross, end_cross) = if constants.is_row { (top, bottom) } else { (left, right) };
2312 let main_axis_is_horizontal = constants.is_row;
2313 let cross_axis_is_horizontal = !constants.is_row;
2314 let main_is_rtl = main_axis_is_horizontal && constants.layout_direction.is_rtl();
2315 let cross_is_rtl = cross_axis_is_horizontal && constants.layout_direction.is_rtl();
2316 let main_axis_flex_start_reversed = constants.dir.is_reverse() ^ main_is_rtl;
2317 let cross_axis_flex_start_reversed = constants.is_wrap_reverse ^ cross_is_rtl;
2318 let main_start_scrollbar_offset =
2319 if main_is_rtl { constants.scrollbar_gutter.main(constants.dir) } else { 0.0 };
2320 let cross_start_scrollbar_offset =
2321 if cross_is_rtl { constants.scrollbar_gutter.cross(constants.dir) } else { 0.0 };
2322 let main_end_scrollbar_offset = if main_is_rtl { 0.0 } else { constants.scrollbar_gutter.main(constants.dir) };
2323 let cross_end_scrollbar_offset =
2324 if cross_is_rtl { 0.0 } else { constants.scrollbar_gutter.cross(constants.dir) };
2325
2326 // Apply main-axis alignment
2327 // let free_main_space = free_space.main(constants.dir) - resolved_margin.main_axis_sum(constants.dir);
2328 let offset_main = if start_main.is_some() || end_main.is_some() {
2329 if main_is_rtl && end_main.is_some() {
2330 constants.container_size.main(constants.dir)
2331 - constants.border.main_end(constants.dir)
2332 - main_end_scrollbar_offset
2333 - final_size.main(constants.dir)
2334 - end_main.unwrap_or(0.0)
2335 - resolved_margin.main_end(constants.dir)
2336 } else if let Some(start) = start_main {
2337 start
2338 + constants.border.main_start(constants.dir)
2339 + main_start_scrollbar_offset
2340 + resolved_margin.main_start(constants.dir)
2341 } else {
2342 constants.container_size.main(constants.dir)
2343 - constants.border.main_end(constants.dir)
2344 - main_end_scrollbar_offset
2345 - final_size.main(constants.dir)
2346 - end_main.unwrap_or(0.0)
2347 - resolved_margin.main_end(constants.dir)
2348 }
2349 } else {
2350 // Stretch is an invalid value for justify_content in the flexbox algorithm, so we
2351 // treat it as if it wasn't set (and thus we default to FlexStart behaviour).
2352 //
2353 // The `safe` overflow-position keyword is intentionally NOT applied here, even when
2354 // the abs-positioned item would overflow the main axis: Chrome does not apply safe
2355 // fallback to `justify-content` on absolutely-positioned flex items (only the
2356 // cross-axis `align-self` does so). Matching the layout authority over a strict
2357 // spec read keeps gentest fixtures green; reconsider if Chromium changes behavior.
2358 match (constants.justify_content.unwrap_or(JustifyContent::START).keyword(), main_axis_flex_start_reversed)
2359 {
2360 (AlignContentKeyword::SpaceBetween, _)
2361 | (AlignContentKeyword::Stretch, false)
2362 | (AlignContentKeyword::FlexStart, false)
2363 | (AlignContentKeyword::FlexEnd, true) => {
2364 constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2365 }
2366 (AlignContentKeyword::Start, false) => {
2367 constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2368 }
2369 (AlignContentKeyword::Start, true) => {
2370 constants.container_size.main(constants.dir)
2371 - constants.content_box_inset.main_end(constants.dir)
2372 - final_size.main(constants.dir)
2373 - resolved_margin.main_end(constants.dir)
2374 }
2375 (AlignContentKeyword::End, false) => {
2376 constants.container_size.main(constants.dir)
2377 - constants.content_box_inset.main_end(constants.dir)
2378 - final_size.main(constants.dir)
2379 - resolved_margin.main_end(constants.dir)
2380 }
2381 (AlignContentKeyword::End, true) => {
2382 constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2383 }
2384 (AlignContentKeyword::FlexEnd, false)
2385 | (AlignContentKeyword::FlexStart, true)
2386 | (AlignContentKeyword::Stretch, true) => {
2387 constants.container_size.main(constants.dir)
2388 - constants.content_box_inset.main_end(constants.dir)
2389 - final_size.main(constants.dir)
2390 - resolved_margin.main_end(constants.dir)
2391 }
2392 (AlignContentKeyword::SpaceEvenly, _)
2393 | (AlignContentKeyword::SpaceAround, _)
2394 | (AlignContentKeyword::Center, _) => {
2395 (constants.container_size.main(constants.dir)
2396 + constants.content_box_inset.main_start(constants.dir)
2397 - constants.content_box_inset.main_end(constants.dir)
2398 - final_size.main(constants.dir)
2399 + resolved_margin.main_start(constants.dir)
2400 - resolved_margin.main_end(constants.dir))
2401 / 2.0
2402 }
2403 }
2404 };
2405
2406 // Apply cross-axis alignment
2407 // let free_cross_space = free_space.cross(constants.dir) - resolved_margin.cross_axis_sum(constants.dir);
2408 let offset_cross = if start_cross.is_some() || end_cross.is_some() {
2409 if cross_is_rtl && end_cross.is_some() {
2410 constants.container_size.cross(constants.dir)
2411 - constants.border.cross_end(constants.dir)
2412 - cross_end_scrollbar_offset
2413 - final_size.cross(constants.dir)
2414 - end_cross.unwrap_or(0.0)
2415 - resolved_margin.cross_end(constants.dir)
2416 } else if let Some(start) = start_cross {
2417 start
2418 + constants.border.cross_start(constants.dir)
2419 + cross_start_scrollbar_offset
2420 + resolved_margin.cross_start(constants.dir)
2421 } else {
2422 constants.container_size.cross(constants.dir)
2423 - constants.border.cross_end(constants.dir)
2424 - cross_end_scrollbar_offset
2425 - final_size.cross(constants.dir)
2426 - end_cross.unwrap_or(0.0)
2427 - resolved_margin.cross_end(constants.dir)
2428 }
2429 } else {
2430 let cross_overflows = final_size.cross(constants.dir) + resolved_margin.cross_axis_sum(constants.dir)
2431 > constants.container_size.cross(constants.dir)
2432 - constants.content_box_inset.cross_axis_sum(constants.dir);
2433 let cross_keyword = resolve_self_alignment_safety(align_self, cross_overflows);
2434 match (cross_keyword, cross_axis_flex_start_reversed) {
2435 // Stretch alignment does not apply to absolutely positioned items
2436 // See "Example 3" at https://www.w3.org/TR/css-flexbox-1/#abspos-items
2437 // Note: Stretch should be FlexStart not Start when we support both
2438 (AlignItemsKeyword::Start, false) => {
2439 constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2440 }
2441 (AlignItemsKeyword::Start, true) => {
2442 constants.container_size.cross(constants.dir)
2443 - constants.content_box_inset.cross_end(constants.dir)
2444 - final_size.cross(constants.dir)
2445 - resolved_margin.cross_end(constants.dir)
2446 }
2447 (AlignItemsKeyword::End, false) => {
2448 constants.container_size.cross(constants.dir)
2449 - constants.content_box_inset.cross_end(constants.dir)
2450 - final_size.cross(constants.dir)
2451 - resolved_margin.cross_end(constants.dir)
2452 }
2453 (AlignItemsKeyword::End, true) => {
2454 constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2455 }
2456 (AlignItemsKeyword::Baseline | AlignItemsKeyword::Stretch | AlignItemsKeyword::FlexStart, false)
2457 | (AlignItemsKeyword::FlexEnd, true) => {
2458 constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2459 }
2460 (AlignItemsKeyword::Baseline | AlignItemsKeyword::Stretch | AlignItemsKeyword::FlexStart, true)
2461 | (AlignItemsKeyword::FlexEnd, false) => {
2462 constants.container_size.cross(constants.dir)
2463 - constants.content_box_inset.cross_end(constants.dir)
2464 - final_size.cross(constants.dir)
2465 - resolved_margin.cross_end(constants.dir)
2466 }
2467 (AlignItemsKeyword::Center, _) => {
2468 (constants.container_size.cross(constants.dir)
2469 + constants.content_box_inset.cross_start(constants.dir)
2470 - constants.content_box_inset.cross_end(constants.dir)
2471 - final_size.cross(constants.dir)
2472 + resolved_margin.cross_start(constants.dir)
2473 - resolved_margin.cross_end(constants.dir))
2474 / 2.0
2475 }
2476 }
2477 };
2478
2479 let location = match constants.is_row {
2480 true => Point { x: offset_main, y: offset_cross },
2481 false => Point { x: offset_cross, y: offset_main },
2482 };
2483 let scrollbar_size = Size {
2484 width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
2485 height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
2486 };
2487 tree.set_unrounded_layout(
2488 child,
2489 &Layout {
2490 order: order as u32,
2491 size: final_size,
2492 #[cfg(feature = "content_size")]
2493 content_size: layout_output.content_size,
2494 scrollbar_size,
2495 location,
2496 padding,
2497 border,
2498 margin: resolved_margin,
2499 },
2500 );
2501
2502 #[cfg(feature = "content_size")]
2503 {
2504 let size_content_size_contribution = Size {
2505 width: match overflow.x {
2506 Overflow::Visible => f32_max(final_size.width, layout_output.content_size.width),
2507 _ => final_size.width,
2508 },
2509 height: match overflow.y {
2510 Overflow::Visible => f32_max(final_size.height, layout_output.content_size.height),
2511 _ => final_size.height,
2512 },
2513 };
2514 if size_content_size_contribution.has_non_zero_area() {
2515 let absolute_area_offset = Point {
2516 x: constants.border.left
2517 + if constants.layout_direction.is_rtl() { constants.scrollbar_gutter.x } else { 0.0 },
2518 y: constants.border.top,
2519 };
2520 let relative_location =
2521 Point { x: location.x - absolute_area_offset.x, y: location.y - absolute_area_offset.y };
2522 let content_size_contribution = Size {
2523 width: if constants.layout_direction.is_rtl() {
2524 let overflow_extra_width =
2525 f32_max(size_content_size_contribution.width - final_size.width, 0.0);
2526 f32_max(inset_relative_size.width - relative_location.x, 0.0) + overflow_extra_width
2527 } else {
2528 relative_location.x + size_content_size_contribution.width
2529 },
2530 height: relative_location.y + size_content_size_contribution.height,
2531 };
2532 content_size = content_size.f32_max(content_size_contribution);
2533 }
2534 }
2535 }
2536
2537 content_size
2538}
2539
2540/// Computes the total space taken up by gaps in an axis given:
2541/// - The size of each gap
2542/// - The number of items (children or flex-lines) between which there are gaps
2543#[inline(always)]
2544fn sum_axis_gaps(gap: f32, num_items: usize) -> f32 {
2545 // Gaps only exist between items, so...
2546 if num_items <= 1 {
2547 // ...if there are less than 2 items then there are no gaps
2548 0.0
2549 } else {
2550 // ...otherwise there are (num_items - 1) gaps
2551 gap * (num_items - 1) as f32
2552 }
2553}