⎇ 7eee673c5ea65f64f35c53f5ee2fe5faf62b9c1e
src/compute/flexbox.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
Improve main module (lib.rs) docs + re-export more types at the crate root (#444)
1//! Computes the [flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) layout algorithm on [`Taffy`](crate::Taffy) according to the [spec](https://www.w3.org/TR/css-flexbox-1/)
2use crate::compute::common::alignment::compute_alignment_offset;
3use crate::geometry::{Line, Point, Rect, Size};
4use crate::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::{NodeId, PartialLayoutTree, PartialLayoutTreeExt};
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 [`PartialLayoutTree`] according to the flexbox algorithm
155pub fn compute_flexbox_layout(tree: &mut impl PartialLayoutTree, 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 PartialLayoutTree, 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.get_unrounded_layout_mut(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 PartialLayoutTree,
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 PartialLayoutTree,
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::NONE.with_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 PartialLayoutTree,
806 available_space: Size<AvailableSpace>,
807 lines: &mut Vec<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 modifcation: 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 asymetrical 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 PartialLayoutTree,
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 PartialLayoutTree,
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 }
1400}
1401
1402/// Handle 'align-content: stretch'.
1403///
1404/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1405///
1406/// - [**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,
1407/// and the sum of the flex lines' cross sizes is less than the flex container’s inner cross size,
1408/// 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.
1409#[inline]
1410fn handle_align_content_stretch(flex_lines: &mut [FlexLine], node_size: Size<Option<f32>>, constants: &AlgoConstants) {
1411 if constants.align_content == AlignContent::Stretch {
1412 let cross_axis_padding_border = constants.content_box_inset.cross_axis_sum(constants.dir);
1413 let cross_min_size = constants.min_size.cross(constants.dir);
1414 let cross_max_size = constants.max_size.cross(constants.dir);
1415 let container_min_inner_cross = node_size
1416 .cross(constants.dir)
1417 .or(cross_min_size)
1418 .maybe_clamp(cross_min_size, cross_max_size)
1419 .maybe_sub(cross_axis_padding_border)
1420 .maybe_max(0.0)
1421 .unwrap_or(0.0);
1422
1423 let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len());
1424 let lines_total_cross: f32 = flex_lines.iter().map(|line| line.cross_size).sum::<f32>() + total_cross_axis_gap;
1425
1426 if lines_total_cross < container_min_inner_cross {
1427 let remaining = container_min_inner_cross - lines_total_cross;
1428 let addition = remaining / flex_lines.len() as f32;
1429 flex_lines.iter_mut().for_each(|line| line.cross_size += addition);
1430 }
1431 }
1432}
1433
1434/// Determine the used cross size of each flex item.
1435///
1436/// # [9.4. Cross Size Determination](https://www.w3.org/TR/css-flexbox-1/#cross-sizing)
1437///
1438/// - [**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,
1439/// 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.
1440/// Otherwise, the used cross size is the item’s hypothetical cross size.
1441///
1442/// 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.
1443///
1444/// **Note that this step does not affect the main size of the flex item, even if it has an intrinsic aspect ratio**.
1445#[inline]
1446fn determine_used_cross_size(tree: &impl PartialLayoutTree, flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1447 for line in flex_lines {
1448 let line_cross_size = line.cross_size;
1449
1450 for child in line.items.iter_mut() {
1451 let child_style = tree.get_style(child.node);
1452 child.target_size.set_cross(
1453 constants.dir,
1454 if child.align_self == AlignSelf::Stretch
1455 && !child.margin_is_auto.cross_start(constants.dir)
1456 && !child.margin_is_auto.cross_end(constants.dir)
1457 && child_style.size.cross(constants.dir) == Dimension::Auto
1458 {
1459 // For some reason this particular usage of max_width is an exception to the rule that max_width's transfer
1460 // using the aspect_ratio (if set). Both Chrome and Firefox agree on this. And reading the spec, it seems like
1461 // a reasonable interpretation. Although it seems to me that the spec *should* apply aspect_ratio here.
1462 let max_size_ignoring_aspect_ratio = child_style.max_size.maybe_resolve(constants.node_inner_size);
1463
1464 (line_cross_size - child.margin.cross_axis_sum(constants.dir)).maybe_clamp(
1465 child.min_size.cross(constants.dir),
1466 max_size_ignoring_aspect_ratio.cross(constants.dir),
1467 )
1468 } else {
1469 child.hypothetical_inner_size.cross(constants.dir)
1470 },
1471 );
1472
1473 child.outer_target_size.set_cross(
1474 constants.dir,
1475 child.target_size.cross(constants.dir) + child.margin.cross_axis_sum(constants.dir),
1476 );
1477 }
1478 }
1479}
1480
1481/// Distribute any remaining free space.
1482///
1483/// # [9.5. Main-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#main-alignment)
1484///
1485/// - [**Distribute any remaining free space**](https://www.w3.org/TR/css-flexbox-1/#algo-main-align). For each flex line:
1486///
1487/// 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.
1488/// Otherwise, set all `auto` margins to zero.
1489///
1490/// 2. Align the items along the main-axis per `justify-content`.
1491#[inline]
1492fn distribute_remaining_free_space(flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1493 for line in flex_lines {
1494 let total_main_axis_gap = sum_axis_gaps(constants.gap.main(constants.dir), line.items.len());
1495 let used_space: f32 = total_main_axis_gap
1496 + line.items.iter().map(|child| child.outer_target_size.main(constants.dir)).sum::<f32>();
1497 let free_space = constants.inner_container_size.main(constants.dir) - used_space;
1498 let mut num_auto_margins = 0;
1499
1500 for child in line.items.iter_mut() {
1501 if child.margin_is_auto.main_start(constants.dir) {
1502 num_auto_margins += 1;
1503 }
1504 if child.margin_is_auto.main_end(constants.dir) {
1505 num_auto_margins += 1;
1506 }
1507 }
1508
1509 if free_space > 0.0 && num_auto_margins > 0 {
1510 let margin = free_space / num_auto_margins as f32;
1511
1512 for child in line.items.iter_mut() {
1513 if child.margin_is_auto.main_start(constants.dir) {
1514 if constants.is_row {
1515 child.margin.left = margin;
1516 } else {
1517 child.margin.top = margin;
1518 }
1519 }
1520 if child.margin_is_auto.main_end(constants.dir) {
1521 if constants.is_row {
1522 child.margin.right = margin;
1523 } else {
1524 child.margin.bottom = margin;
1525 }
1526 }
1527 }
1528 } else {
1529 let num_items = line.items.len();
1530 let layout_reverse = constants.dir.is_reverse();
1531 let gap = constants.gap.main(constants.dir);
1532 let justify_content_mode = constants.justify_content.unwrap_or(JustifyContent::FlexStart);
1533
1534 let justify_item = |(i, child): (usize, &mut FlexItem)| {
1535 child.offset_main =
1536 compute_alignment_offset(free_space, num_items, gap, justify_content_mode, layout_reverse, i == 0);
1537 };
1538
1539 if layout_reverse {
1540 line.items.iter_mut().rev().enumerate().for_each(justify_item);
1541 } else {
1542 line.items.iter_mut().enumerate().for_each(justify_item);
1543 }
1544 }
1545 }
1546}
1547
1548/// Resolve cross-axis `auto` margins.
1549///
1550/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1551///
1552/// - [**Resolve cross-axis `auto` margins**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-margins).
1553/// If a flex item has auto cross-axis margins:
1554///
1555/// - If its outer cross size (treating those auto margins as zero) is less than the cross size of its flex line,
1556/// distribute the difference in those sizes equally to the auto margins.
1557///
1558/// - Otherwise, if the block-start or inline-start margin (whichever is in the cross axis) is auto, set it to zero.
1559/// Set the opposite margin so that the outer cross size of the item equals the cross size of its flex line.
1560#[inline]
1561fn resolve_cross_axis_auto_margins(flex_lines: &mut [FlexLine], constants: &AlgoConstants) {
1562 for line in flex_lines {
1563 let line_cross_size = line.cross_size;
1564 let max_baseline: f32 = line.items.iter_mut().map(|child| child.baseline).fold(0.0, |acc, x| acc.max(x));
1565
1566 for child in line.items.iter_mut() {
1567 let free_space = line_cross_size - child.outer_target_size.cross(constants.dir);
1568
1569 if child.margin_is_auto.cross_start(constants.dir) && child.margin_is_auto.cross_end(constants.dir) {
1570 if constants.is_row {
1571 child.margin.top = free_space / 2.0;
1572 child.margin.bottom = free_space / 2.0;
1573 } else {
1574 child.margin.left = free_space / 2.0;
1575 child.margin.right = free_space / 2.0;
1576 }
1577 } else if child.margin_is_auto.cross_start(constants.dir) {
1578 if constants.is_row {
1579 child.margin.top = free_space;
1580 } else {
1581 child.margin.left = free_space;
1582 }
1583 } else if child.margin_is_auto.cross_end(constants.dir) {
1584 if constants.is_row {
1585 child.margin.bottom = free_space;
1586 } else {
1587 child.margin.right = free_space;
1588 }
1589 } else {
1590 // 14. Align all flex items along the cross-axis.
1591 child.offset_cross = align_flex_items_along_cross_axis(child, free_space, max_baseline, constants);
1592 }
1593 }
1594 }
1595}
1596
1597/// Align all flex items along the cross-axis.
1598///
1599/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1600///
1601/// - [**Align all flex items along the cross-axis**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-align) per `align-self`,
1602/// if neither of the item's cross-axis margins are `auto`.
1603#[inline]
1604fn align_flex_items_along_cross_axis(
1605 child: &FlexItem,
1606 free_space: f32,
1607 max_baseline: f32,
1608 constants: &AlgoConstants,
1609) -> f32 {
1610 match child.align_self {
1611 AlignSelf::Start => 0.0,
1612 AlignSelf::FlexStart => {
1613 if constants.is_wrap_reverse {
1614 free_space
1615 } else {
1616 0.0
1617 }
1618 }
1619 AlignSelf::End => free_space,
1620 AlignSelf::FlexEnd => {
1621 if constants.is_wrap_reverse {
1622 0.0
1623 } else {
1624 free_space
1625 }
1626 }
1627 AlignSelf::Center => free_space / 2.0,
1628 AlignSelf::Baseline => {
1629 if constants.is_row {
1630 max_baseline - child.baseline
1631 } else {
1632 // Until we support vertical writing modes, baseline alignment only makes sense if
1633 // the constants.direction is row, so we treat it as flex-start alignment in columns.
1634 if constants.is_wrap_reverse {
1635 free_space
1636 } else {
1637 0.0
1638 }
1639 }
1640 }
1641 AlignSelf::Stretch => {
1642 if constants.is_wrap_reverse {
1643 free_space
1644 } else {
1645 0.0
1646 }
1647 }
1648 }
1649}
1650
1651/// Determine the flex container’s used cross size.
1652///
1653/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1654///
1655/// - [**Determine the flex container’s used cross size**](https://www.w3.org/TR/css-flexbox-1/#algo-cross-container):
1656///
1657/// - If the cross size property is a definite size, use that, clamped by the used min and max cross sizes of the flex container.
1658///
1659/// - Otherwise, use the sum of the flex lines' cross sizes, clamped by the used min and max cross sizes of the flex container.
1660#[inline]
1661#[must_use]
1662fn determine_container_cross_size(
1663 flex_lines: &[FlexLine],
1664 node_size: Size<Option<f32>>,
1665 constants: &mut AlgoConstants,
1666) -> f32 {
1667 let total_cross_axis_gap = sum_axis_gaps(constants.gap.cross(constants.dir), flex_lines.len());
1668 let total_line_cross_size: f32 = flex_lines.iter().map(|line| line.cross_size).sum::<f32>();
1669
1670 let padding_border_sum = constants.content_box_inset.cross_axis_sum(constants.dir);
1671 let cross_scrollbar_gutter = constants.scrollbar_gutter.cross(constants.dir);
1672 let min_cross_size = constants.min_size.cross(constants.dir);
1673 let max_cross_size = constants.max_size.cross(constants.dir);
1674 let outer_container_size = node_size
1675 .cross(constants.dir)
1676 .unwrap_or(total_line_cross_size + total_cross_axis_gap + padding_border_sum)
1677 .maybe_clamp(min_cross_size, max_cross_size)
1678 .max(padding_border_sum - cross_scrollbar_gutter);
1679 let inner_container_size = f32_max(outer_container_size - padding_border_sum, 0.0);
1680
1681 constants.container_size.set_cross(constants.dir, outer_container_size);
1682 constants.inner_container_size.set_cross(constants.dir, inner_container_size);
1683
1684 total_line_cross_size
1685}
1686
1687/// Align all flex lines per `align-content`.
1688///
1689/// # [9.6. Cross-Axis Alignment](https://www.w3.org/TR/css-flexbox-1/#cross-alignment)
1690///
1691/// - [**Align all flex lines**](https://www.w3.org/TR/css-flexbox-1/#algo-line-align) per `align-content`.
1692#[inline]
1693fn align_flex_lines_per_align_content(flex_lines: &mut [FlexLine], constants: &AlgoConstants, total_cross_size: f32) {
1694 let num_lines = flex_lines.len();
1695 let gap = constants.gap.cross(constants.dir);
1696 let align_content_mode = constants.align_content;
1697 let total_cross_axis_gap = sum_axis_gaps(gap, num_lines);
1698 let free_space = constants.inner_container_size.cross(constants.dir) - total_cross_size - total_cross_axis_gap;
1699
1700 let align_line = |(i, line): (usize, &mut FlexLine)| {
1701 line.offset_cross =
1702 compute_alignment_offset(free_space, num_lines, gap, align_content_mode, constants.is_wrap_reverse, i == 0);
1703 };
1704
1705 if constants.is_wrap_reverse {
1706 flex_lines.iter_mut().rev().enumerate().for_each(align_line);
1707 } else {
1708 flex_lines.iter_mut().enumerate().for_each(align_line);
1709 }
1710}
1711
1712/// Calculates the layout for a flex-item
1713#[allow(clippy::too_many_arguments)]
1714fn calculate_flex_item(
1715 tree: &mut impl PartialLayoutTree,
1716 item: &mut FlexItem,
1717 total_offset_main: &mut f32,
1718 total_offset_cross: f32,
1719 line_offset_cross: f32,
1720 #[cfg(feature = "content_size")] total_content_size: &mut Size<f32>,
1721 container_size: Size<f32>,
1722 node_inner_size: Size<Option<f32>>,
1723 direction: FlexDirection,
1724) {
1725 let layout_output = tree.perform_child_layout(
1726 item.node,
1727 item.target_size.map(|s| s.into()),
1728 node_inner_size,
1729 container_size.map(|s| s.into()),
1730 SizingMode::ContentSize,
1731 Line::FALSE,
1732 );
1733 let LayoutOutput {
1734 size,
1735 #[cfg(feature = "content_size")]
1736 content_size,
1737 ..
1738 } = layout_output;
1739
1740 let offset_main = *total_offset_main
1741 + item.offset_main
1742 + item.margin.main_start(direction)
1743 + (item.inset.main_start(direction).or(item.inset.main_end(direction).map(|pos| -pos)).unwrap_or(0.0));
1744
1745 let offset_cross = total_offset_cross
1746 + item.offset_cross
1747 + line_offset_cross
1748 + item.margin.cross_start(direction)
1749 + (item.inset.cross_start(direction).or(item.inset.cross_end(direction).map(|pos| -pos)).unwrap_or(0.0));
1750
1751 if direction.is_row() {
1752 let baseline_offset_cross = total_offset_cross + item.offset_cross + item.margin.cross_start(direction);
1753 let inner_baseline = layout_output.first_baselines.y.unwrap_or(size.height);
1754 item.baseline = baseline_offset_cross + inner_baseline;
1755 } else {
1756 let baseline_offset_main = *total_offset_main + item.offset_main + item.margin.main_start(direction);
1757 let inner_baseline = layout_output.first_baselines.y.unwrap_or(size.height);
1758 item.baseline = baseline_offset_main + inner_baseline;
1759 }
1760
1761 let location = match direction.is_row() {
1762 true => Point { x: offset_main, y: offset_cross },
1763 false => Point { x: offset_cross, y: offset_main },
1764 };
1765 let scrollbar_size = Size {
1766 width: if item.overflow.y == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
1767 height: if item.overflow.x == Overflow::Scroll { item.scrollbar_width } else { 0.0 },
1768 };
1769
1770 *tree.get_unrounded_layout_mut(item.node) = Layout {
1771 order: item.order,
1772 size,
1773 #[cfg(feature = "content_size")]
1774 content_size,
1775 scrollbar_size,
1776 location,
1777 padding: item.padding,
1778 border: item.border,
1779 };
1780
1781 *total_offset_main += item.offset_main + item.margin.main_axis_sum(direction) + size.main(direction);
1782
1783 #[cfg(feature = "content_size")]
1784 {
1785 *total_content_size =
1786 total_content_size.f32_max(compute_content_size_contribution(location, size, content_size, item.overflow));
1787 }
1788}
1789
1790/// Calculates the layout line
1791#[allow(clippy::too_many_arguments)]
1792fn calculate_layout_line(
1793 tree: &mut impl PartialLayoutTree,
1794 line: &mut FlexLine,
1795 total_offset_cross: &mut f32,
1796 #[cfg(feature = "content_size")] content_size: &mut Size<f32>,
1797 container_size: Size<f32>,
1798 node_inner_size: Size<Option<f32>>,
1799 padding_border: Rect<f32>,
1800 direction: FlexDirection,
1801) {
1802 let mut total_offset_main = padding_border.main_start(direction);
1803 let line_offset_cross = line.offset_cross;
1804
1805 if direction.is_reverse() {
1806 for item in line.items.iter_mut().rev() {
1807 calculate_flex_item(
1808 tree,
1809 item,
1810 &mut total_offset_main,
1811 *total_offset_cross,
1812 line_offset_cross,
1813 #[cfg(feature = "content_size")]
1814 content_size,
1815 container_size,
1816 node_inner_size,
1817 direction,
1818 );
1819 }
1820 } else {
1821 for item in line.items.iter_mut() {
1822 calculate_flex_item(
1823 tree,
1824 item,
1825 &mut total_offset_main,
1826 *total_offset_cross,
1827 line_offset_cross,
1828 #[cfg(feature = "content_size")]
1829 content_size,
1830 container_size,
1831 node_inner_size,
1832 direction,
1833 );
1834 }
1835 }
1836
1837 *total_offset_cross += line_offset_cross + line.cross_size;
1838}
1839
1840/// Do a final layout pass and collect the resulting layouts.
1841#[inline]
1842fn final_layout_pass(
1843 tree: &mut impl PartialLayoutTree,
1844 flex_lines: &mut [FlexLine],
1845 constants: &AlgoConstants,
1846) -> Size<f32> {
1847 let mut total_offset_cross = constants.content_box_inset.cross_start(constants.dir);
1848
1849 #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
1850 let mut content_size = Size::ZERO;
1851
1852 if constants.is_wrap_reverse {
1853 for line in flex_lines.iter_mut().rev() {
1854 calculate_layout_line(
1855 tree,
1856 line,
1857 &mut total_offset_cross,
1858 #[cfg(feature = "content_size")]
1859 &mut content_size,
1860 constants.container_size,
1861 constants.node_inner_size,
1862 constants.content_box_inset,
1863 constants.dir,
1864 );
1865 }
1866 } else {
1867 for line in flex_lines.iter_mut() {
1868 calculate_layout_line(
1869 tree,
1870 line,
1871 &mut total_offset_cross,
1872 #[cfg(feature = "content_size")]
1873 &mut content_size,
1874 constants.container_size,
1875 constants.node_inner_size,
1876 constants.content_box_inset,
1877 constants.dir,
1878 );
1879 }
1880 }
1881
1882 content_size
1883}
1884
1885/// Perform absolute layout on all absolutely positioned children.
1886#[inline]
1887fn perform_absolute_layout_on_absolute_children(
1888 tree: &mut impl PartialLayoutTree,
1889 node: NodeId,
1890 constants: &AlgoConstants,
1891) -> Size<f32> {
1892 let container_width = constants.container_size.width;
1893 let container_height = constants.container_size.height;
1894
1895 #[cfg_attr(not(feature = "content_size"), allow(unused_mut))]
1896 let mut content_size = Size::ZERO;
1897
1898 for order in 0..tree.child_count(node) {
1899 let child = tree.get_child_id(node, order);
1900 let child_style = tree.get_style(child);
1901
1902 // Skip items that are display:none or are not position:absolute
1903 if child_style.display == Display::None || child_style.position != Position::Absolute {
1904 continue;
1905 }
1906
1907 let overflow = child_style.overflow;
1908 let scrollbar_width = child_style.scrollbar_width;
1909 let aspect_ratio = child_style.aspect_ratio;
1910 let align_self = child_style.align_self.unwrap_or(constants.align_items);
1911 let margin = child_style.margin.map(|margin| margin.resolve_to_option(container_width));
1912 let padding = child_style.padding.resolve_or_zero(Some(container_width));
1913 let border = child_style.border.resolve_or_zero(Some(container_width));
1914 let padding_border_sum = (padding + border).sum_axes();
1915
1916 // Resolve inset
1917 let left = child_style.inset.left.maybe_resolve(container_width);
1918 let right = child_style.inset.right.maybe_resolve(container_width);
1919 let top = child_style.inset.top.maybe_resolve(container_height);
1920 let bottom = child_style.inset.bottom.maybe_resolve(container_height);
1921
1922 // Compute known dimensions from min/max/inherent size styles
1923 let style_size =
1924 child_style.size.maybe_resolve(constants.container_size).maybe_apply_aspect_ratio(aspect_ratio);
1925 let min_size = child_style
1926 .min_size
1927 .maybe_resolve(constants.container_size)
1928 .maybe_apply_aspect_ratio(aspect_ratio)
1929 .or(padding_border_sum.map(Some))
1930 .maybe_max(padding_border_sum);
1931 let max_size =
1932 child_style.max_size.maybe_resolve(constants.container_size).maybe_apply_aspect_ratio(aspect_ratio);
1933 let mut known_dimensions = style_size.maybe_clamp(min_size, max_size);
1934
1935 // Fill in width from left/right and reapply aspect ratio if:
1936 // - Width is not already known
1937 // - Item has both left and right inset properties set
1938 if let (None, Some(left), Some(right)) = (known_dimensions.width, left, right) {
1939 let new_width_raw = container_width.maybe_sub(margin.left).maybe_sub(margin.right) - left - right;
1940 known_dimensions.width = Some(f32_max(new_width_raw, 0.0));
1941 known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
1942 }
1943
1944 // Fill in height from top/bottom and reapply aspect ratio if:
1945 // - Height is not already known
1946 // - Item has both top and bottom inset properties set
1947 if let (None, Some(top), Some(bottom)) = (known_dimensions.height, top, bottom) {
1948 let new_height_raw = container_height.maybe_sub(margin.top).maybe_sub(margin.bottom) - top - bottom;
1949 known_dimensions.height = Some(f32_max(new_height_raw, 0.0));
1950 known_dimensions = known_dimensions.maybe_apply_aspect_ratio(aspect_ratio).maybe_clamp(min_size, max_size);
1951 }
1952
1953 let layout_output = tree.perform_child_layout(
1954 child,
1955 known_dimensions,
1956 constants.node_inner_size,
1957 Size {
1958 width: AvailableSpace::Definite(container_width.maybe_clamp(min_size.width, max_size.width)),
1959 height: AvailableSpace::Definite(container_height.maybe_clamp(min_size.height, max_size.height)),
1960 },
1961 SizingMode::ContentSize,
1962 Line::FALSE,
1963 );
1964 let measured_size = layout_output.size;
1965 let final_size = known_dimensions.unwrap_or(measured_size).maybe_clamp(min_size, max_size);
1966
1967 let non_auto_margin = margin.map(|m| m.unwrap_or(0.0));
1968
1969 let free_space = Size {
1970 width: constants.container_size.width - final_size.width - non_auto_margin.horizontal_axis_sum(),
1971 height: constants.container_size.height - final_size.height - non_auto_margin.vertical_axis_sum(),
1972 }
1973 .f32_max(Size::ZERO);
1974
1975 // Expand auto margins to fill available space
1976 let resolved_margin = {
1977 let auto_margin_size = Size {
1978 width: {
1979 let auto_margin_count = margin.left.is_none() as u8 + margin.right.is_none() as u8;
1980 if auto_margin_count > 0 {
1981 free_space.width / auto_margin_count as f32
1982 } else {
1983 0.0
1984 }
1985 },
1986 height: {
1987 let auto_margin_count = margin.top.is_none() as u8 + margin.bottom.is_none() as u8;
1988 if auto_margin_count > 0 {
1989 free_space.height / auto_margin_count as f32
1990 } else {
1991 0.0
1992 }
1993 },
1994 };
1995
1996 Rect {
1997 left: margin.left.unwrap_or(auto_margin_size.width),
1998 right: margin.right.unwrap_or(auto_margin_size.width),
1999 top: margin.top.unwrap_or(auto_margin_size.height),
2000 bottom: margin.bottom.unwrap_or(auto_margin_size.height),
2001 }
2002 };
2003
2004 // Determine flex-relative insets
2005 let (start_main, end_main) = if constants.is_row { (left, right) } else { (top, bottom) };
2006 let (start_cross, end_cross) = if constants.is_row { (top, bottom) } else { (left, right) };
2007
2008 // Apply main-axis alignment
2009 // let free_main_space = free_space.main(constants.dir) - resolved_margin.main_axis_sum(constants.dir);
2010 let offset_main = if let Some(start) = start_main {
2011 start + constants.border.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2012 } else if let Some(end) = end_main {
2013 constants.container_size.main(constants.dir)
2014 - constants.border.main_end(constants.dir)
2015 - final_size.main(constants.dir)
2016 - end
2017 - resolved_margin.main_end(constants.dir)
2018 } else {
2019 // Stretch is an invalid value for justify_content in the flexbox algorithm, so we
2020 // treat it as if it wasn't set (and thus we default to FlexStart behaviour)
2021 match (constants.justify_content.unwrap_or(JustifyContent::Start), constants.is_wrap_reverse) {
2022 (JustifyContent::SpaceBetween, _)
2023 | (JustifyContent::Start, _)
2024 | (JustifyContent::Stretch, false)
2025 | (JustifyContent::FlexStart, false)
2026 | (JustifyContent::FlexEnd, true) => {
2027 constants.content_box_inset.main_start(constants.dir) + resolved_margin.main_start(constants.dir)
2028 }
2029 (JustifyContent::End, _)
2030 | (JustifyContent::FlexEnd, false)
2031 | (JustifyContent::FlexStart, true)
2032 | (JustifyContent::Stretch, true) => {
2033 constants.container_size.main(constants.dir)
2034 - constants.content_box_inset.main_end(constants.dir)
2035 - final_size.main(constants.dir)
2036 - resolved_margin.main_end(constants.dir)
2037 }
2038 (JustifyContent::SpaceEvenly, _) | (JustifyContent::SpaceAround, _) | (JustifyContent::Center, _) => {
2039 (constants.container_size.main(constants.dir)
2040 + constants.content_box_inset.main_start(constants.dir)
2041 - constants.content_box_inset.main_end(constants.dir)
2042 - final_size.main(constants.dir)
2043 + resolved_margin.main_start(constants.dir)
2044 - resolved_margin.main_end(constants.dir))
2045 / 2.0
2046 }
2047 }
2048 };
2049
2050 // Apply cross-axis alignment
2051 // let free_cross_space = free_space.cross(constants.dir) - resolved_margin.cross_axis_sum(constants.dir);
2052 let offset_cross = if let Some(start) = start_cross {
2053 start + constants.border.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2054 } else if let Some(end) = end_cross {
2055 constants.container_size.cross(constants.dir)
2056 - constants.border.cross_end(constants.dir)
2057 - final_size.cross(constants.dir)
2058 - end
2059 - resolved_margin.cross_end(constants.dir)
2060 } else {
2061 match (align_self, constants.is_wrap_reverse) {
2062 // Stretch alignment does not apply to absolutely positioned items
2063 // See "Example 3" at https://www.w3.org/TR/css-flexbox-1/#abspos-items
2064 // Note: Stretch should be FlexStart not Start when we support both
2065 (AlignSelf::Start, _)
2066 | (AlignSelf::Baseline | AlignSelf::Stretch | AlignSelf::FlexStart, false)
2067 | (AlignSelf::FlexEnd, true) => {
2068 constants.content_box_inset.cross_start(constants.dir) + resolved_margin.cross_start(constants.dir)
2069 }
2070 (AlignSelf::End, _)
2071 | (AlignSelf::Baseline | AlignSelf::Stretch | AlignSelf::FlexStart, true)
2072 | (AlignSelf::FlexEnd, false) => {
2073 constants.container_size.cross(constants.dir)
2074 - constants.content_box_inset.cross_end(constants.dir)
2075 - final_size.cross(constants.dir)
2076 - resolved_margin.cross_end(constants.dir)
2077 }
2078 (AlignSelf::Center, _) => {
2079 (constants.container_size.cross(constants.dir)
2080 + constants.content_box_inset.cross_start(constants.dir)
2081 - constants.content_box_inset.cross_end(constants.dir)
2082 - final_size.cross(constants.dir)
2083 + resolved_margin.cross_start(constants.dir)
2084 - resolved_margin.cross_end(constants.dir))
2085 / 2.0
2086 }
2087 }
2088 };
2089
2090 let location = match constants.is_row {
2091 true => Point { x: offset_main, y: offset_cross },
2092 false => Point { x: offset_cross, y: offset_main },
2093 };
2094 let scrollbar_size = Size {
2095 width: if overflow.y == Overflow::Scroll { scrollbar_width } else { 0.0 },
2096 height: if overflow.x == Overflow::Scroll { scrollbar_width } else { 0.0 },
2097 };
2098 *tree.get_unrounded_layout_mut(child) = Layout {
2099 order: order as u32,
2100 size: final_size,
2101 #[cfg(feature = "content_size")]
2102 content_size: layout_output.content_size,
2103 scrollbar_size,
2104 location,
2105 padding,
2106 border,
2107 };
2108
2109 #[cfg(feature = "content_size")]
2110 {
2111 let size_content_size_contribution = Size {
2112 width: match overflow.x {
2113 Overflow::Visible => f32_max(final_size.width, layout_output.content_size.width),
2114 _ => final_size.width,
2115 },
2116 height: match overflow.y {
2117 Overflow::Visible => f32_max(final_size.height, layout_output.content_size.height),
2118 _ => final_size.height,
2119 },
2120 };
2121 if size_content_size_contribution.has_non_zero_area() {
2122 let content_size_contribution = Size {
2123 width: location.x + size_content_size_contribution.width,
2124 height: location.y + size_content_size_contribution.height,
2125 };
2126 content_size = content_size.f32_max(content_size_contribution);
2127 }
2128 }
2129 }
2130
2131 content_size
2132}
2133
2134/// Computes the total space taken up by gaps in an axis given:
2135/// - The size of each gap
2136/// - The number of items (children or flex-lines) between which there are gaps
2137#[inline(always)]
2138fn sum_axis_gaps(gap: f32, num_items: usize) -> f32 {
2139 // Gaps only exist between items, so...
2140 if num_items <= 1 {
2141 // ...if there are less than 2 items then there are no gaps
2142 0.0
2143 } else {
2144 // ...otherwise there are (num_items - 1) gaps
2145 gap * (num_items - 1) as f32
2146 }
2147}
2148
2149#[cfg(test)]
2150mod tests {
2151 #![allow(clippy::redundant_clone)]
2152
2153 use crate::{
2154 geometry::Size,
2155 style::{FlexWrap, Style},
2156 util::{MaybeMath, ResolveOrZero},
2157 Taffy,
2158 };
2159
2160 // Make sure we get correct constants
2161 #[test]
2162 fn correct_constants() {
2163 let mut tree: Taffy<()> = Taffy::with_capacity(16);
2164
2165 let style = Style::default();
2166 let node_id = tree.new_leaf(style.clone()).unwrap();
2167
2168 let node_size = Size::NONE;
2169 let parent_size = Size::NONE;
2170
2171 let constants = super::compute_constants(tree.style(node_id).unwrap(), node_size, parent_size);
2172
2173 assert!(constants.dir == style.flex_direction);
2174 assert!(constants.is_row == style.flex_direction.is_row());
2175 assert!(constants.is_column == style.flex_direction.is_column());
2176 assert!(constants.is_wrap_reverse == (style.flex_wrap == FlexWrap::WrapReverse));
2177
2178 let margin = style.margin.resolve_or_zero(parent_size);
2179 assert_eq!(constants.margin, margin);
2180
2181 let border = style.border.resolve_or_zero(parent_size);
2182 let padding = style.padding.resolve_or_zero(parent_size);
2183 let padding_border = padding + border;
2184 assert_eq!(constants.border, border);
2185 assert_eq!(constants.content_box_inset, padding_border);
2186
2187 let inner_size = Size {
2188 width: node_size.width.maybe_sub(padding_border.horizontal_axis_sum()),
2189 height: node_size.height.maybe_sub(padding_border.vertical_axis_sum()),
2190 };
2191 assert_eq!(constants.node_inner_size, inner_size);
2192
2193 assert_eq!(constants.container_size, Size::zero());
2194 assert_eq!(constants.inner_container_size, Size::zero());
2195 }
2196}