Clamp scroll-container flex baselines (#995)
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 // Scroll containers' baselines are determined from their content as if scrolled to the
1499 // initial position, but are additionally clamped to their border box.
1500 // See https://github.com/w3c/csswg-drafts/issues/7660
1501 let baseline = if child.overflow.y.is_scroll_container() {
1502 baseline.unwrap_or(height).min(height).max(0.0)
1503 } else {
1504 baseline.unwrap_or(height)
1505 };
1506
1507 child.baseline = baseline + child.margin.top;
1508 }
1509 }
1510}
1511
1512/// Calculate the cross size of each flex line.
1513///
1514/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1515///
1516/// - [**Calculate the cross size of each flex line**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-line).
1517#[inline]
1518fn calculate_cross_size(flex_lines: &mut [FlexLine], node_size: Size<Option<f32>>, constants: &AlgoConstants) {
1519 // If the flex container is single-line and has a definite cross size,
1520 // the cross size of the flex line is the flex container’s inner cross size.
1521 if !constants.is_wrap && node_size.cross(constants.dir).is_some() {
1522 let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1523 let cross_min_size = constants.min_size.cross(constants.dir);
1524 let cross_max_size = constants.max_size.cross(constants.dir);
1525 flex_lines[0].cross_size = node_size
1526 .cross(constants.dir)
1527 .maybe_clamp(cross_min_size, cross_max_size)
1528 .maybe_sub(cross_axis_padding_border)
1529 .maybe_max(0.0)
1530 .unwrap_or(0.0);
1531 } else {
1532 // Otherwise, for each flex line:
1533 //
1534 // 1. Collect all the flex items whose inline-axis is parallel to the main-axis, whose
1535 // align-self is baseline, and whose cross-axis margins are both non-auto. Find the
1536 // largest of the distances between each item’s baseline and its hypothetical outer
1537 // cross-start edge, and the largest of the distances between each item’s baseline
1538 // and its hypothetical outer cross-end edge, and sum these two values.
1539
1540 // 2. Among all the items not collected by the previous step, find the largest
1541 // outer hypothetical cross size.
1542
1543 // 3. The used cross-size of the flex line is the largest of the numbers found in the
1544 // previous two steps and zero.
1545 for line in flex_lines.iter_mut() {
1546 let max_baseline: f32 = line.items.iter().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x));
1547 line.cross_size = line
1548 .items
1549 .iter()
1550 .map(|child| {
1551 if child.align_self == AlignSelf::BASELINE
1552 && !child.margin_is_auto.cross_start(constants.dir)
1553 && !child.margin_is_auto.cross_end(constants.dir)
1554 {
1555 max_baseline - child.baseline + child.hypothetical_outer_size.cross(constants.dir)
1556 } else {
1557 child.hypothetical_outer_size.cross(constants.dir)
1558 }
1559 })
1560 .fold(0.0, |acc, x| acc.max(x));
1561 }
1562
1563 // 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.
1564 // Note that if CSS 2.1’s definition of min/max-width/height applied more generally, this behavior would fall out automatically.
1565 if !constants.is_wrap {
1566 let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1567 let cross_min_size = constants.min_size.cross(constants.dir);
1568 let cross_max_size = constants.max_size.cross(constants.dir);
1569 flex_lines[0].cross_size = flex_lines[0].cross_size.maybe_clamp(
1570 cross_min_size.maybe_sub(cross_axis_padding_border),
1571 cross_max_size.maybe_sub(cross_axis_padding_border),
1572 );
1573 }
1574 }
1575}
1576
1577/// Handle 'align-content: stretch'.
1578///
1579/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1580///
1581/// - [**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,
1582/// and the sum of the flex lines' cross sizes is less than the flex container’s inner cross size,
1583/// 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.
1584#[inline]
1585fn handle_align_content_stretch(flex_lines: &mut [FlexLine], node_size: Size<Option<f32>>, constants: &AlgoConstants) {
1586 if constants.align_content == AlignContent::STRETCH {
1587 let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1588 let cross_min_size = constants.min_size.cross(constants.dir);
1589 let cross_max_size = constants.max_size.cross(constants.dir);
1590 let container_min_inner_cross = node_size
1591 .cross(constants.dir)
1592 .or(cross_min_size)
1593 .maybe_clamp(cross_min_size, cross_max_size)
1594 .maybe_sub(cross_axis_padding_border)
1595 .maybe_max(0.0)
1596 .unwrap_or(0.0);
1597
1598 let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len());
1599 let lines_total_cross: f32 = flex_lines.iter().map(|line| line.cross_size).sum::<f32>() + total_cross_axis_gap;
1600
1601 if lines_total_cross < container_min_inner_cross {
1602 let remaining = container_min_inner_cross - lines_total_cross;
1603 let addition = remaining / flex_lines.len() as f32;
1604 flex_lines.iter_mut().for_each(|line| line.cross_size += addition);
1605 }
1606 }
1607}
1608
1609/// Determine the used cross size of each flex item.
1610///
1611/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1612///
1613/// - [**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,
1614/// 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.
1615/// Otherwise, the used cross size is the item’s hypothetical cross size.
1616///
1617/// 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.
1618///
1619/// **Note that this step does not affect the main size of the flex item, even if it has an intrinsic aspect ratio**.
1620#[inline]
1621fn determine_used_cross_size(
1622 tree: &impl LayoutFlexboxContainer,
1623 flex_lines: &mut [FlexLine],
1624 constants: &AlgoConstants,
1625) {
1626 for line in flex_lines {
1627 let line_cross_size = line.cross_size;
1628
1629 for child in line.items.iter_mut() {
1630 let child_style = tree.get_flexbox_child_style(child.node);
1631 child.target_size.set_cross(
1632 constants.dir,
1633 if child.align_self == AlignSelf::STRETCH
1634 && !child.margin_is_auto.cross_start(constants.dir)
1635 && !child.margin_is_auto.cross_end(constants.dir)
1636 && child_style.size().cross(constants.dir).is_auto()
1637 {
1638 // For some reason this particular usage of max_width is an exception to the rule that max_width's transfer
1639 // using the aspect_ratio (if set). Both Chrome and Firefox agree on this. And reading the spec, it seems like
1640 // a reasonable interpretation. Although it seems to me that the spec *should* apply aspect_ratio here.
1641 let padding = child_style
1642 .padding()
1643 .resolve_or_zero(constants.node_inner_size, |val, basis| tree.calc(val, basis));
1644 let border = child_style
1645 .border()
1646 .resolve_or_zero(constants.node_inner_size, |val, basis| tree.calc(val, basis));
1647 let pb_sum = (padding + border).sum_axes();
1648 let box_sizing_adjustment =
1649 if child_style.box_sizing() == BoxSizing::ContentBox { pb_sum } else { Size::ZERO };
1650
1651 let max_size_ignoring_aspect_ratio = child_style
1652 .max_size()
1653 .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis))
1654 .maybe_add(box_sizing_adjustment);
1655
1656 (line_cross_size - child.margin.cross_axis_sum(constants.dir)).maybe_clamp(
1657 child.min_size.cross(constants.dir),
1658 max_size_ignoring_aspect_ratio.cross(constants.dir),
1659 )
1660 } else {
1661 child.hypothetical_inner_size.cross(constants.dir)
1662 },
1663 );
1664
1665 child.outer_target_size.set_cross(
1666 constants.dir,
1667 child.target_size.cross(constants.dir) + child.margin.cross_axis_sum(constants.dir),
1668 );
1669 }
1670 }
1671}
1672
1673/// Distribute any remaining free space.
1674///
1675/// # [9.5. Main-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#main-alignment)
1676///
1677/// - [**Distribute any remaining free space**](https://www.w3.org/TR/css-flexbox-1/#algo-main-align). For each flex line:
1678///
1679/// 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.
1680/// Otherwise, set all `auto` margins to zero.
1681///
1682/// 2. Align the items along the main-axis per `justify-content`.
1683#[inline]
1684fn distribute_remaining_free_space(flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1685 for line in flex_lines {
1686 let total_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
1687 let used_space: f32 = total_main_axis_gap
1688 + line.items.iter().map(|child| child.outer_target_size.main(constants.dir)).sum::<f32>();
1689 let free_space = constants.inner_container_size.main(constants.dir) - used_space;
1690 let mut num_auto_margins = 0;
1691
1692 for child in line.items.iter_mut() {
1693 if child.margin_is_auto.main_start(constants.dir) {
1694 num_auto_margins += 1;
1695 }
1696 if child.margin_is_auto.main_end(constants.dir) {
1697 num_auto_margins += 1;
1698 }
1699 }
1700
1701 if free_space > 0.0 && num_auto_margins > 0 {
1702 let margin = free_space / num_auto_margins as f32;
1703
1704 for child in line.items.iter_mut() {
1705 if child.margin_is_auto.main_start(constants.dir) {
1706 if constants.is_row {
1707 child.margin.left = margin;
1708 } else {
1709 child.margin.top = margin;
1710 }
1711 }
1712 if child.margin_is_auto.main_end(constants.dir) {
1713 if constants.is_row {
1714 child.margin.right = margin;
1715 } else {
1716 child.margin.bottom = margin;
1717 }
1718 }
1719 }
1720 }
1721
1722 let num_items = line.items.len();
1723 let layout_reverse = constants.dir.is_reverse();
1724 let gap = constants.gap.main(constants.dir);
1725 let raw_justify_content_mode = constants.justify_content.unwrap_or(JustifyContent::FLEX_START);
1726 let justify_content_mode = apply_alignment_fallback(free_space, num_items, raw_justify_content_mode);
1727
1728 let justify_item = |(i, child): (usize, &mut FlexItem)| {
1729 child.offset_main =
1730 compute_alignment_offset(free_space, num_items, gap, justify_content_mode, layout_reverse, i == 0);
1731 };
1732
1733 if layout_reverse {
1734 line.items.iter_mut().rev().enumerate().for_each(justify_item);
1735 } else {
1736 line.items.iter_mut().enumerate().for_each(justify_item);
1737 }
1738 }
1739}
1740
1741/// Resolve cross-axis `auto` margins.
1742///
1743/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1744///
1745/// - [**Resolve cross-axis `auto` margins**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-margins).
1746/// If a flex item has auto cross-axis margins:
1747///
1748/// - If its outer cross size (treating those auto margins as zero) is less than the cross size of its flex line,
1749/// distribute the difference in those sizes equally to the auto margins.
1750///
1751/// - Otherwise, if the block-start or inline-start margin (whichever is in the cross axis) is auto, set it to zero.
1752/// Set the opposite margin so that the outer cross size of the item equals the cross size of its flex line.
1753#[inline]
1754fn resolve_cross_axis_auto_margins(flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1755 for line in flex_lines {
1756 let line_cross_size = line.cross_size;
1757 let max_baseline: f32 = line.items.iter_mut().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x));
1758
1759 for child in line.items.iter_mut() {
1760 let free_space = line_cross_size - child.outer_target_size.cross(constants.dir);
1761
1762 if child.margin_is_auto.cross_start(constants.dir) && child.margin_is_auto.cross_end(constants.dir) {
1763 if constants.is_row {
1764 child.margin.top = free_space / 2.0;
1765 child.margin.bottom = free_space / 2.0;
1766 } else {
1767 child.margin.left = free_space / 2.0;
1768 child.margin.right = free_space / 2.0;
1769 }
1770 } else if child.margin_is_auto.cross_start(constants.dir) {
1771 if constants.is_row {
1772 child.margin.top = free_space;
1773 } else {
1774 child.margin.left = free_space;
1775 }
1776 } else if child.margin_is_auto.cross_end(constants.dir) {
1777 if constants.is_row {
1778 child.margin.bottom = free_space;
1779 } else {
1780 child.margin.right = free_space;
1781 }
1782 } else {
1783 // 14. Align all flex items along the cross-axis.
1784 child.offset_cross = align_flex_items_along_cross_axis(child, free_space, max_baseline, constants);
1785 }
1786 }
1787 }
1788}
1789
1790/// Align all flex items along the cross-axis.
1791///
1792/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1793///
1794/// - [**Align all flex items along the cross-axis**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-align) per `align-self`,
1795/// if neither of the item's cross-axis margins are `auto`.
1796#[inline]
1797fn align_flex_items_along_cross_axis(
1798 child: &FlexItem,
1799 free_space: f32,
1800 max_baseline: f32,
1801 constants: &AlgoConstants,
1802) -> f32 {
1803 let cross_axis_should_reverse = constants.is_column && matches!(constants.layout_direction, Direction::Rtl);
1804
1805 // If align-self uses a "safe" overflow-position keyword and the item would overflow its
1806 // line cross size, fall back to logical Start to avoid data loss. See CSS Box Alignment 3
1807 // §4.3 <https://www.w3.org/TR/css-align-3/#overflow-values>. Otherwise, drop the safety
1808 // field so the match below operates on a bare keyword and stays exhaustive.
1809 let align_keyword = if child.align_self.is_safe() && free_space < 0.0 {
1810 AlignItemsKeyword::Start
1811 } else {
1812 child.align_self.keyword
1813 };
1814
1815 match align_keyword {
1816 AlignItemsKeyword::Start => {
1817 if cross_axis_should_reverse {
1818 free_space
1819 } else {
1820 0.0
1821 }
1822 }
1823 AlignItemsKeyword::FlexStart => {
1824 if constants.is_wrap_reverse ^ cross_axis_should_reverse {
1825 free_space
1826 } else {
1827 0.0
1828 }
1829 }
1830 AlignItemsKeyword::End => {
1831 if cross_axis_should_reverse {
1832 0.0
1833 } else {
1834 free_space
1835 }
1836 }
1837 AlignItemsKeyword::FlexEnd => {
1838 if constants.is_wrap_reverse ^ cross_axis_should_reverse {
1839 0.0
1840 } else {
1841 free_space
1842 }
1843 }
1844 AlignItemsKeyword::Center => free_space / 2.0,
1845 AlignItemsKeyword::Baseline => {
1846 if constants.is_row {
1847 max_baseline - child.baseline
1848 } else {
1849 // Until we support vertical writing modes, baseline alignment only makes sense if
1850 // the constants.direction is row, so we treat it as flex-start alignment in columns.
1851 let baseline_column_should_reverse = cross_axis_should_reverse && !constants.is_wrap;
1852 if constants.is_wrap_reverse ^ baseline_column_should_reverse {
1853 free_space
1854 } else {
1855 0.0
1856 }
1857 }
1858 }
1859 AlignItemsKeyword::Stretch => {
1860 if constants.is_wrap_reverse ^ cross_axis_should_reverse {
1861 free_space
1862 } else {
1863 0.0
1864 }
1865 }
1866 }
1867}
1868
1869/// Determine the flex container’s used cross size.
1870///
1871/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1872///
1873/// - [**Determine the flex container’s used cross size**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-container):
1874///
1875/// - If the cross size property is a definite size, use that, clamped by the used min and max cross sizes of the flex container.
1876///
1877/// - Otherwise, use the sum of the flex lines' cross sizes, clamped by the used min and max cross sizes of the flex container.
1878#[inline]
1879#[must_use]
1880fn determine_container_cross_size(
1881 flex_lines: &[FlexLine],
1882 node_size: Size<Option<f32>>,
1883 constants: &mut AlgoConstants,
1884) -> f32 {
1885 let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len());
1886 let total_line_cross_size: f32 = flex_lines.iter().map(|line| line.cross_size).sum::<f32>();
1887
1888 let padding_border_sum = constants.content_box_inset.cross_axis_sum(constants.dir);
1889 let cross_scrollbar_gutter = constants.scrollbar_gutter.cross(constants.dir);
1890 let min_cross_size = constants.min_size.cross(constants.dir);
1891 let max_cross_size = constants.max_size.cross(constants.dir);
1892 let outer_container_size = node_size
1893 .cross(constants.dir)
1894 .unwrap_or(total_line_cross_size + total_cross_axis_gap + padding_border_sum)
1895 .maybe_clamp(min_cross_size, max_cross_size)
1896 .max(padding_border_sum - cross_scrollbar_gutter);
1897 let inner_container_size = f32_max(outer_container_size - padding_border_sum, 0.0);
1898
1899 constants.container_size.set_cross(constants.dir, outer_container_size);
1900 constants.inner_container_size.set_cross(constants.dir, inner_container_size);
1901
1902 total_line_cross_size
1903}
1904
1905/// Align all flex lines per `align-content`.
1906///
1907/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1908///
1909/// - [**Align all flex lines**](https://www.w3.org/TR/css-flexbox-1/#algo-line-align) per `align-content`.
1910#[inline]
1911fn align_flex_lines_per_align_content(flex_lines: &mut [FlexLine], constants: &AlgoConstants, total_cross_size: f32) {
1912 let num_lines = flex_lines.len();
1913 let gap = constants.gap.cross(constants.dir);
1914 let total_cross_axis_gap = sum_axis_gaps(gap, num_lines);
1915 let free_space = constants.inner_container_size.cross(constants.dir) - total_cross_size - total_cross_axis_gap;
1916
1917 let align_content_mode = apply_alignment_fallback(free_space, num_lines, constants.align_content);
1918
1919 let align_line = |(i, line): (usize, &mut FlexLine)| {
1920 line.offset_cross =
1921 compute_alignment_offset(free_space, num_lines, gap, align_content_mode, constants.is_wrap_reverse, i == 0);
1922 };
1923
1924 if constants.is_wrap_reverse {
1925 flex_lines.iter_mut().rev().enumerate().for_each(align_line);
1926 } else {
1927 flex_lines.iter_mut().enumerate().for_each(align_line);
1928 }
1929}
1930
1931/// Calculates the layout for a flex-item
1932#[allow(clippy::too_many_arguments)]
1933fn calculate_flex_item(
1934 tree: &mut impl LayoutFlexboxContainer,
1935 item: &mut FlexItem,
1936 total_offset_main: &mut f32,
1937 total_offset_cross: f32,
1938 line_offset_cross: f32,
1939 #[cfg(feature = "content_size")] total_content_size: &mut Size<f32>,
1940 container_size: Size<f32>,
1941 node_inner_size: Size<Option<f32>>,
1942 direction: FlexDirection,
1943 layout_direction: Direction,
1944) {
1945 let layout_output = tree.perform_child_layout(
1946 item.node,
1947 item.target_size.map(|s| s.into()),
1948 node_inner_size,
1949 container_size.map(|s| s.into()),
1950 SizingMode::ContentSize,
1951 Line::FALSE,
1952 );
1953 let LayoutOutput {
1954 size,
1955 #[cfg(feature = "content_size")]
1956 content_size,
1957 ..
1958 } = layout_output;
1959
1960 let is_rtl_row = direction.is_row() && layout_direction.is_rtl();
1961 let is_rtl_column = direction.is_column() && layout_direction.is_rtl();
1962 let main_relative_inset = if is_rtl_row {
1963 item.inset.main_end(direction).or(item.inset.main_start(direction).map(|pos| -pos)).unwrap_or(0.0)
1964 } else {
1965 item.inset.main_start(direction).or(item.inset.main_end(direction).map(|pos| -pos)).unwrap_or(0.0)
1966 };
1967 let cross_relative_inset = if is_rtl_column {
1968 item.inset.cross_end(direction).map(|pos| -pos).or(item.inset.cross_start(direction)).unwrap_or(0.0)
1969 } else {
1970 item.inset.cross_start(direction).or(item.inset.cross_end(direction).map(|pos| -pos)).unwrap_or(0.0)
1971 };
1972 let effective_line_offset_cross = if is_rtl_column { 0.0 } else { line_offset_cross };
1973
1974 let offset_main = if is_rtl_row {
1975 *total_offset_main - item.offset_main - item.margin.main_end(direction) - main_relative_inset - size.width
1976 } else {
1977 *total_offset_main + item.offset_main + item.margin.main_start(direction) + main_relative_inset
1978 };
1979
1980 let offset_cross = total_offset_cross
1981 + item.offset_cross
1982 + effective_line_offset_cross
1983 + item.margin.cross_start(direction)
1984 + cross_relative_inset;
1985
1986 if direction.is_row() {
1987 let baseline_offset_cross =
1988 total_offset_cross + item.offset_cross + effective_line_offset_cross + item.margin.cross_start(direction);
1989 // Scroll containers' baselines are determined from their content as if scrolled to the initial
1990 // position, but are additionally clamped to their border box.
1991 // See https://github.com/w3c/csswg-drafts/issues/7660
1992 let inner_baseline = {
1993 let baseline = layout_output.first_baselines.y.unwrap_or(size.height);
1994 if item.overflow.y.is_scroll_container() {
1995 baseline.min(size.height).max(0.0)
1996 } else {
1997 baseline
1998 }
1999 };
2000 item.baseline = baseline_offset_cross + inner_baseline;
2001 } else {
2002 let baseline_offset_main = *total_offset_main + item.offset_main + item.margin.main_start(direction);
2003 let inner_baseline = layout_output.first_baselines.y.unwrap_or(size.height);
2004 item.baseline = baseline_offset_main + inner_baseline;
2005 }
2006
2007 let location = if direction.is_row() {
2008 Point { x: offset_main, y: offset_cross }
2009 } else {
2010 Point { x: offset_cross, y: offset_main }
2011 };
2012 let scrollbar_size = Size {
2013 width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
2014 height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
2015 };
2016
2017 tree.set_unrounded_layout(
2018 item.node,
2019 &Layout {
2020 order: item.order,
2021 size,
2022 #[cfg(feature = "content_size")]
2023 content_size,
2024 scrollbar_size,
2025 location,
2026 padding: item.padding,
2027 border: item.border,
2028 margin: item.margin,
2029 },
2030 );
2031
2032 if is_rtl_row {
2033 *total_offset_main -= item.offset_main + item.margin.main_axis_sum(direction) + size.main(direction);
2034 } else {
2035 *total_offset_main += item.offset_main + item.margin.main_axis_sum(direction) + size.main(direction);
2036 }
2037
2038 #[cfg(feature = "content_size")]
2039 {
2040 let contribution_location = if layout_direction.is_rtl() {
2041 Point { x: container_size.width - (location.x + size.width), y: location.y }
2042 } else {
2043 location
2044 };
2045 *total_content_size = total_content_size.f32_max(compute_content_size_contribution(
2046 contribution_location,
2047 size,
2048 content_size,
2049 item.overflow,
2050 ));
2051 }
2052}
2053
2054/// Calculates the layout line
2055#[allow(clippy::too_many_arguments)]
2056fn calculate_layout_line(
2057 tree: &mut impl LayoutFlexboxContainer,
2058 line: &mut FlexLine,
2059 total_offset_cross: &mut f32,
2060 #[cfg(feature = "content_size")] content_size: &mut Size<f32>,
2061 container_size: Size<f32>,
2062 node_inner_size: Size<Option<f32>>,
2063 padding_border: Rect<f32>,
2064 direction: FlexDirection,
2065 layout_direction: Direction,
2066) {
2067 let mut total_offset_main = if layout_direction.is_rtl() && direction.is_row() {
2068 container_size.width - padding_border.main_end(direction)
2069 } else {
2070 padding_border.main_start(direction)
2071 };
2072 let line_offset_cross = line.offset_cross;
2073
2074 let is_rtl_column = layout_direction.is_rtl() && direction.is_column();
2075 if is_rtl_column {
2076 *total_offset_cross -= line_offset_cross + line.cross_size;
2077 }
2078
2079 if direction.is_reverse() {
2080 for item in line.items.iter_mut().rev() {
2081 calculate_flex_item(
2082 tree,
2083 item,
2084 &mut total_offset_main,
2085 *total_offset_cross,
2086 line_offset_cross,
2087 #[cfg(feature = "content_size")]
2088 content_size,
2089 container_size,
2090 node_inner_size,
2091 direction,
2092 layout_direction,
2093 );
2094 }
2095 } else {
2096 for item in line.items.iter_mut() {
2097 calculate_flex_item(
2098 tree,
2099 item,
2100 &mut total_offset_main,
2101 *total_offset_cross,
2102 line_offset_cross,
2103 #[cfg(feature = "content_size")]
2104 content_size,
2105 container_size,
2106 node_inner_size,
2107 direction,
2108 layout_direction,
2109 );
2110 }
2111 }
2112
2113 if !is_rtl_column {
2114 *total_offset_cross += line_offset_cross + line.cross_size;
2115 }
2116}
2117
2118/// Do a final layout pass and collect the resulting layouts.
2119#[inline]
2120fn final_layout_pass(
2121 tree: &mut impl LayoutFlexboxContainer,
2122 flex_lines: &mut [FlexLine],
2123 constants: &AlgoConstants,
2124) -> Size<f32> {
2125 let mut total_offset_cross = if constants.is_column && constants.layout_direction.is_rtl() {
2126 constants.container_size.width - constants.content_box_inset.cross_end(constants.dir)
2127 } else {
2128 constants.content_box_inset.cross_start(constants.dir)
2129 };
2130
2131 #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
2132 let mut content_size = Size::ZERO;
2133
2134 if constants.is_wrap_reverse {
2135 for line in flex_lines.iter_mut().rev() {
2136 calculate_layout_line(
2137 tree,
2138 line,
2139 &mut total_offset_cross,
2140 #[cfg(feature = "content_size")]
2141 &mut content_size,
2142 constants.container_size,
2143 constants.node_inner_size,
2144 constants.content_box_inset,
2145 constants.dir,
2146 constants.layout_direction,
2147 );
2148 }
2149 } else {
2150 for line in flex_lines.iter_mut() {
2151 calculate_layout_line(
2152 tree,
2153 line,
2154 &mut total_offset_cross,
2155 #[cfg(feature = "content_size")]
2156 &mut content_size,
2157 constants.container_size,
2158 constants.node_inner_size,
2159 constants.content_box_inset,
2160 constants.dir,
2161 constants.layout_direction,
2162 );
2163 }
2164 }
2165
2166 content_size.width += if constants.layout_direction.is_rtl() {
2167 constants.content_box_inset.left - constants.border.left - constants.scrollbar_gutter.x
2168 } else {
2169 constants.content_box_inset.right - constants.border.right - constants.scrollbar_gutter.x
2170 };
2171 content_size.height += constants.content_box_inset.bottom - constants.border.bottom - constants.scrollbar_gutter.y;
2172
2173 content_size
2174}
2175
2176/// Perform absolute layout on all absolutely positioned children.
2177#[inline]
2178fn perform_absolute_layout_on_absolute_children(
2179 tree: &mut impl LayoutFlexboxContainer,
2180 node: NodeId,
2181 constants: &AlgoConstants,
2182) -> Size<f32> {
2183 let container_width = constants.container_size.width;
2184 let container_height = constants.container_size.height;
2185 let inset_relative_size =
2186 constants.container_size - constants.border.sum_axes() - constants.scrollbar_gutter.into();
2187
2188 #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
2189 let mut content_size = Size::ZERO;
2190
2191 for order in 0..tree.child_count(node) {
2192 let child = tree.get_child_id(node, order);
2193 let child_style = tree.get_flexbox_child_style(child);
2194
2195 // Skip items that are display:none or are not position:absolute
2196 if child_style.box_generation_mode() == BoxGenerationMode::None || child_style.position() != Position::Absolute
2197 {
2198 continue;
2199 }
2200
2201 let overflow = child_style.overflow();
2202 let scrollbar_width = child_style.scrollbar_width();
2203 let aspect_ratio = child_style.aspect_ratio();
2204 let align_self = child_style.align_self().unwrap_or(constants.align_items);
2205 let margin = child_style
2206 .margin()
2207 .map(|margin| margin.resolve_to_option(inset_relative_size.width, |val, basis| tree.calc(val, basis)));
2208 let padding =
2209 child_style.padding().resolve_or_zero(Some(inset_relative_size.width), |val, basis| tree.calc(val, basis));
2210 let border =
2211 child_style.border().resolve_or_zero(Some(inset_relative_size.width), |val, basis| tree.calc(val, basis));
2212 let padding_border_sum = (padding + border).sum_axes();
2213 let box_sizing_adjustment =
2214 if child_style.box_sizing() == BoxSizing::ContentBox { padding_border_sum } else { Size::ZERO };
2215
2216 // Resolve inset
2217 // Insets are resolved against the container size minus border
2218 let left =
2219 child_style.inset().left.maybe_resolve(inset_relative_size.width, |val, basis| tree.calc(val, basis));
2220 let right =
2221 child_style.inset().right.maybe_resolve(inset_relative_size.width, |val, basis| tree.calc(val, basis));
2222 let top = child_style.inset().top.maybe_resolve(inset_relative_size.height, |val, basis| tree.calc(val, basis));
2223 let bottom =
2224 child_style.inset().bottom.maybe_resolve(inset_relative_size.height, |val, basis| tree.calc(val, basis));
2225
2226 // Compute known dimensions from min/max/inherent size styles
2227 let style_size = child_style
2228 .size()
2229 .maybe_resolve(inset_relative_size, |val, basis| tree.calc(val, basis))
2230 .maybe_apply_aspect_ratio(aspect_ratio)
2231 .maybe_add(box_sizing_adjustment);
2232 let min_size = child_style
2233 .min_size()
2234 .maybe_resolve(inset_relative_size, |val, basis| tree.calc(val, basis))
2235 .maybe_apply_aspect_ratio(aspect_ratio)
2236 .maybe_add(box_sizing_adjustment)
2237 .or(padding_border_sum.map(Some))
2238 .maybe_max(padding_border_sum);
2239 let max_size = child_style
2240 .max_size()
2241 .maybe_resolve(inset_relative_size, |val, basis| tree.calc(val, basis))
2242 .maybe_apply_aspect_ratio(aspect_ratio)
2243 .maybe_add(box_sizing_adjustment);
2244 let mut known_dimensions = style_size.maybe_clamp(min_size, max_size);
2245
2246 drop(child_style);
2247
2248 // Fill in width from left/right and reapply aspect ratio if:
2249 // - Width is not already known
2250 // - Item has both left and right inset properties set
2251 if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) {
2252 let new_width_raw = inset_relative_size.width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right;
2253 known_dimensions.width = Some(f32_max(new_width_raw, 0.0));
2254 known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
2255 }
2256
2257 // Fill in height from top/bottom and reapply aspect ratio if:
2258 // - Height is not already known
2259 // - Item has both top and bottom inset properties set
2260 if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) {
2261 let new_height_raw =
2262 inset_relative_size.height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom;
2263 known_dimensions.height = Some(f32_max(new_height_raw, 0.0));
2264 known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
2265 }
2266 let measured_size = tree.measure_child_size_both(
2267 child,
2268 known_dimensions,
2269 constants.node_inner_size,
2270 Size {
2271 width: AvailableSpace::Definite(container_width.maybe_clamp(min_size.width, max_size.width)),
2272 height: AvailableSpace::Definite(container_height.maybe_clamp(min_size.height, max_size.height)),
2273 },
2274 SizingMode::InherentSize,
2275 Line::FALSE,
2276 );
2277 let final_size = known_dimensions.unwrap_or(measured_size).maybe_clamp(min_size, max_size);
2278
2279 let layout_output = tree.perform_child_layout(
2280 child,
2281 final_size.map(Some),
2282 constants.node_inner_size,
2283 Size {
2284 width: AvailableSpace::Definite(container_width.maybe_clamp(min_size.width, max_size.width)),
2285 height: AvailableSpace::Definite(container_height.maybe_clamp(min_size.height, max_size.height)),
2286 },
2287 SizingMode::InherentSize,
2288 Line::FALSE,
2289 );
2290
2291 let non_auto_margin = margin.map(|m| m.unwrap_or(0.0));
2292
2293 let free_space = Size {
2294 width: constants.container_size.width - final_size.width - non_auto_margin.horizontal_axis_sum(),
2295 height: constants.container_size.height - final_size.height - non_auto_margin.vertical_axis_sum(),
2296 }
2297 .f32_max(Size::ZERO);
2298
2299 // Expand auto margins to fill available space
2300 let resolved_margin = {
2301 let auto_margin_size = Size {
2302 width: {
2303 let auto_margin_count = margin.left.is_none() as u8 + margin.right.is_none() as u8;
2304 if auto_margin_count > 0 {
2305 free_space.width / auto_margin_count as f32
2306 } else {
2307 0.0
2308 }
2309 },
2310 height: {
2311 let auto_margin_count = margin.top.is_none() as u8 + margin.bottom.is_none() as u8;
2312 if auto_margin_count > 0 {
2313 free_space.height / auto_margin_count as f32
2314 } else {
2315 0.0
2316 }
2317 },
2318 };
2319
2320 Rect {
2321 left: margin.left.unwrap_or(auto_margin_size.width),
2322 right: margin.right.unwrap_or(auto_margin_size.width),
2323 top: margin.top.unwrap_or(auto_margin_size.height),
2324 bottom: margin.bottom.unwrap_or(auto_margin_size.height),
2325 }
2326 };
2327
2328 // Determine flex-relative insets
2329 let (start_main, end_main) = if constants.is_row { (left, right) } else { (top, bottom) };
2330 let (start_cross, end_cross) = if constants.is_row { (top, bottom) } else { (left, right) };
2331 let main_axis_is_horizontal = constants.is_row;
2332 let cross_axis_is_horizontal = !constants.is_row;
2333 let main_is_rtl = main_axis_is_horizontal && constants.layout_direction.is_rtl();
2334 let cross_is_rtl = cross_axis_is_horizontal && constants.layout_direction.is_rtl();
2335 let main_axis_flex_start_reversed = constants.dir.is_reverse() ^ main_is_rtl;
2336 let cross_axis_flex_start_reversed = constants.is_wrap_reverse ^ cross_is_rtl;
2337 let main_start_scrollbar_offset =
2338 if main_is_rtl { constants.scrollbar_gutter.main(constants.dir) } else { 0.0 };
2339 let cross_start_scrollbar_offset =
2340 if cross_is_rtl { constants.scrollbar_gutter.cross(constants.dir) } else { 0.0 };
2341 let main_end_scrollbar_offset = if main_is_rtl { 0.0 } else { constants.scrollbar_gutter.main(constants.dir) };
2342 let cross_end_scrollbar_offset =
2343 if cross_is_rtl { 0.0 } else { constants.scrollbar_gutter.cross(constants.dir) };
2344
2345 // Apply main-axis alignment
2346 // let free_main_space = free_space.main(constants.dir) - resolved_margin.main_axis_sum(constants.dir);
2347 let offset_main = if start_main.is_some() || end_main.is_some() {
2348 if main_is_rtl && end_main.is_some() {
2349 constants.container_size.main(constants.dir)
2350 - constants.border.main_end(constants.dir)
2351 - main_end_scrollbar_offset
2352 - final_size.main(constants.dir)
2353 - end_main.unwrap_or(0.0)
2354 - resolved_margin.main_end(constants.dir)
2355 } else if let Some(start) = start_main {
2356 start
2357 + constants.border.main_start(constants.dir)
2358 + main_start_scrollbar_offset
2359 + resolved_margin.main_start(constants.dir)
2360 } else {
2361 constants.container_size.main(constants.dir)
2362 - constants.border.main_end(constants.dir)
2363 - main_end_scrollbar_offset
2364 - final_size.main(constants.dir)
2365 - end_main.unwrap_or(0.0)
2366 - resolved_margin.main_end(constants.dir)
2367 }
2368 } else {
2369 // Stretch is an invalid value for justify_content in the flexbox algorithm, so we
2370 // treat it as if it wasn't set (and thus we default to FlexStart behaviour).
2371 //
2372 // The `safe` overflow-position keyword is intentionally NOT applied here, even when
2373 // the abs-positioned item would overflow the main axis: Chrome does not apply safe
2374 // fallback to `justify-content` on absolutely-positioned flex items (only the
2375 // cross-axis `align-self` does so). Matching the layout authority over a strict
2376 // spec read keeps gentest fixtures green; reconsider if Chromium changes behavior.
2377 match (constants.justify_content.unwrap_or(JustifyContent::START).keyword(), main_axis_flex_start_reversed)
2378 {
2379 (AlignContentKeyword::SpaceBetween, _)
2380 | (AlignContentKeyword::Stretch, false)
2381 | (AlignContentKeyword::FlexStart, false)
2382 | (AlignContentKeyword::FlexEnd, true) => {
2383 constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2384 }
2385 (AlignContentKeyword::Start, false) => {
2386 constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2387 }
2388 (AlignContentKeyword::Start, true) => {
2389 constants.container_size.main(constants.dir)
2390 - constants.content_box_inset.main_end(constants.dir)
2391 - final_size.main(constants.dir)
2392 - resolved_margin.main_end(constants.dir)
2393 }
2394 (AlignContentKeyword::End, false) => {
2395 constants.container_size.main(constants.dir)
2396 - constants.content_box_inset.main_end(constants.dir)
2397 - final_size.main(constants.dir)
2398 - resolved_margin.main_end(constants.dir)
2399 }
2400 (AlignContentKeyword::End, true) => {
2401 constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2402 }
2403 (AlignContentKeyword::FlexEnd, false)
2404 | (AlignContentKeyword::FlexStart, true)
2405 | (AlignContentKeyword::Stretch, true) => {
2406 constants.container_size.main(constants.dir)
2407 - constants.content_box_inset.main_end(constants.dir)
2408 - final_size.main(constants.dir)
2409 - resolved_margin.main_end(constants.dir)
2410 }
2411 (AlignContentKeyword::SpaceEvenly, _)
2412 | (AlignContentKeyword::SpaceAround, _)
2413 | (AlignContentKeyword::Center, _) => {
2414 (constants.container_size.main(constants.dir)
2415 + constants.content_box_inset.main_start(constants.dir)
2416 - constants.content_box_inset.main_end(constants.dir)
2417 - final_size.main(constants.dir)
2418 + resolved_margin.main_start(constants.dir)
2419 - resolved_margin.main_end(constants.dir))
2420 / 2.0
2421 }
2422 }
2423 };
2424
2425 // Apply cross-axis alignment
2426 // let free_cross_space = free_space.cross(constants.dir) - resolved_margin.cross_axis_sum(constants.dir);
2427 let offset_cross = if start_cross.is_some() || end_cross.is_some() {
2428 if cross_is_rtl && end_cross.is_some() {
2429 constants.container_size.cross(constants.dir)
2430 - constants.border.cross_end(constants.dir)
2431 - cross_end_scrollbar_offset
2432 - final_size.cross(constants.dir)
2433 - end_cross.unwrap_or(0.0)
2434 - resolved_margin.cross_end(constants.dir)
2435 } else if let Some(start) = start_cross {
2436 start
2437 + constants.border.cross_start(constants.dir)
2438 + cross_start_scrollbar_offset
2439 + resolved_margin.cross_start(constants.dir)
2440 } else {
2441 constants.container_size.cross(constants.dir)
2442 - constants.border.cross_end(constants.dir)
2443 - cross_end_scrollbar_offset
2444 - final_size.cross(constants.dir)
2445 - end_cross.unwrap_or(0.0)
2446 - resolved_margin.cross_end(constants.dir)
2447 }
2448 } else {
2449 let cross_overflows = final_size.cross(constants.dir) + resolved_margin.cross_axis_sum(constants.dir)
2450 > constants.container_size.cross(constants.dir)
2451 - constants.content_box_inset.cross_axis_sum(constants.dir);
2452 let cross_keyword = resolve_self_alignment_safety(align_self, cross_overflows);
2453 match (cross_keyword, cross_axis_flex_start_reversed) {
2454 // Stretch alignment does not apply to absolutely positioned items
2455 // See "Example 3" at https://www.w3.org/TR/css-flexbox-1/#abspos-items
2456 // Note: Stretch should be FlexStart not Start when we support both
2457 (AlignItemsKeyword::Start, false) => {
2458 constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2459 }
2460 (AlignItemsKeyword::Start, true) => {
2461 constants.container_size.cross(constants.dir)
2462 - constants.content_box_inset.cross_end(constants.dir)
2463 - final_size.cross(constants.dir)
2464 - resolved_margin.cross_end(constants.dir)
2465 }
2466 (AlignItemsKeyword::End, false) => {
2467 constants.container_size.cross(constants.dir)
2468 - constants.content_box_inset.cross_end(constants.dir)
2469 - final_size.cross(constants.dir)
2470 - resolved_margin.cross_end(constants.dir)
2471 }
2472 (AlignItemsKeyword::End, true) => {
2473 constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2474 }
2475 (AlignItemsKeyword::Baseline | AlignItemsKeyword::Stretch | AlignItemsKeyword::FlexStart, false)
2476 | (AlignItemsKeyword::FlexEnd, true) => {
2477 constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2478 }
2479 (AlignItemsKeyword::Baseline | AlignItemsKeyword::Stretch | AlignItemsKeyword::FlexStart, true)
2480 | (AlignItemsKeyword::FlexEnd, false) => {
2481 constants.container_size.cross(constants.dir)
2482 - constants.content_box_inset.cross_end(constants.dir)
2483 - final_size.cross(constants.dir)
2484 - resolved_margin.cross_end(constants.dir)
2485 }
2486 (AlignItemsKeyword::Center, _) => {
2487 (constants.container_size.cross(constants.dir)
2488 + constants.content_box_inset.cross_start(constants.dir)
2489 - constants.content_box_inset.cross_end(constants.dir)
2490 - final_size.cross(constants.dir)
2491 + resolved_margin.cross_start(constants.dir)
2492 - resolved_margin.cross_end(constants.dir))
2493 / 2.0
2494 }
2495 }
2496 };
2497
2498 let location = match constants.is_row {
2499 true => Point { x: offset_main, y: offset_cross },
2500 false => Point { x: offset_cross, y: offset_main },
2501 };
2502 let scrollbar_size = Size {
2503 width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
2504 height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
2505 };
2506 tree.set_unrounded_layout(
2507 child,
2508 &Layout {
2509 order: order as u32,
2510 size: final_size,
2511 #[cfg(feature = "content_size")]
2512 content_size: layout_output.content_size,
2513 scrollbar_size,
2514 location,
2515 padding,
2516 border,
2517 margin: resolved_margin,
2518 },
2519 );
2520
2521 #[cfg(feature = "content_size")]
2522 {
2523 let size_content_size_contribution = Size {
2524 width: match overflow.x {
2525 Overflow::Visible => f32_max(final_size.width, layout_output.content_size.width),
2526 _ => final_size.width,
2527 },
2528 height: match overflow.y {
2529 Overflow::Visible => f32_max(final_size.height, layout_output.content_size.height),
2530 _ => final_size.height,
2531 },
2532 };
2533 if size_content_size_contribution.has_non_zero_area() {
2534 let absolute_area_offset = Point {
2535 x: constants.border.left
2536 + if constants.layout_direction.is_rtl() { constants.scrollbar_gutter.x } else { 0.0 },
2537 y: constants.border.top,
2538 };
2539 let relative_location =
2540 Point { x: location.x - absolute_area_offset.x, y: location.y - absolute_area_offset.y };
2541 let content_size_contribution = Size {
2542 width: if constants.layout_direction.is_rtl() {
2543 let overflow_extra_width =
2544 f32_max(size_content_size_contribution.width - final_size.width, 0.0);
2545 f32_max(inset_relative_size.width - relative_location.x, 0.0) + overflow_extra_width
2546 } else {
2547 relative_location.x + size_content_size_contribution.width
2548 },
2549 height: relative_location.y + size_content_size_contribution.height,
2550 };
2551 content_size = content_size.f32_max(content_size_contribution);
2552 }
2553 }
2554 }
2555
2556 content_size
2557}
2558
2559/// Computes the total space taken up by gaps in an axis given:
2560/// - The size of each gap
2561/// - The number of items (children or flex-lines) between which there are gaps
2562#[inline(always)]
2563fn sum_axis_gaps(gap: f32, num_items: usize) -> f32 {
2564 // Gaps only exist between items, so...
2565 if num_items <= 1 {
2566 // ...if there are less than 2 items then there are no gaps
2567 0.0
2568 } else {
2569 // ...otherwise there are (num_items - 1) gaps
2570 gap * (num_items - 1) as f32
2571 }
2572}