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