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