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