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