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