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