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