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