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