From 1d464a576a6506196ff10e2c5bbee1969272fc54 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 6 Aug 2024 12:33:37 +0200 Subject: [PATCH] layout: Add support for `align-content: stretch` (#32906) This adds support for `align-content: stretch` by splitting flex line layout into two phases. The first phase takes place before determing how much extra space to allocate for stretching items. Then line layout finishes, which might cause two layouts for items with `align-self: stretch`. Signed-off-by: Martin Robinson Co-authored-by: Oriol Brufau --- components/layout_2020/flexbox/layout.rs | 976 ++++++++++-------- .../css/css-flexbox/align-content-006.htm.ini | 2 - .../align-content-wrap-003.html.ini | 6 - .../align-content_stretch.html.ini | 2 - .../flexbox_align-content-stretch-2.html.ini | 2 - .../flexbox_align-content-stretch.html.ini | 2 - 6 files changed, 521 insertions(+), 469 deletions(-) delete mode 100644 tests/wpt/meta/css/css-flexbox/align-content-006.htm.ini delete mode 100644 tests/wpt/meta/css/css-flexbox/align-content_stretch.html.ini delete mode 100644 tests/wpt/meta/css/css-flexbox/flexbox_align-content-stretch-2.html.ini delete mode 100644 tests/wpt/meta/css/css-flexbox/flexbox_align-content-stretch.html.ini diff --git a/components/layout_2020/flexbox/layout.rs b/components/layout_2020/flexbox/layout.rs index c14c8959ab7..77bbfa1120e 100644 --- a/components/layout_2020/flexbox/layout.rs +++ b/components/layout_2020/flexbox/layout.rs @@ -92,12 +92,6 @@ enum FlexContent { FlexItemPlaceholder, } -/// A flex line with some intermediate results -struct FlexLine<'a> { - items: &'a mut [FlexItem<'a>], - outer_hypothetical_main_sizes_sum: Au, -} - /// Return type of `FlexItem::layout` struct FlexItemLayoutResult { hypothetical_cross_size: Au, @@ -115,8 +109,27 @@ impl FlexItemLayoutResult { } } +struct InitialFlexLineLayout<'a> { + /// The items that are placed in this line. + items: &'a mut [FlexItem<'a>], + + /// The initial size of this flex line, not taking into account `align-content: stretch`. + line_size: FlexRelativeVec2, + + /// The layout results of the initial layout pass of this flex line. These may be replaced + /// if necessary due to the use of `align-content: stretch` or `align-self: stretch`. + item_layout_results: Vec, + + /// The used main size of each item in this line. + item_used_main_sizes: Vec, + + /// The free space available to this line after the initial layout. + free_space_in_main_axis: Au, +} + /// Return type of `FlexLine::layout` -struct FlexLineLayoutResult { +struct FinalFlexLineLayout { + /// The final cross size of this flex line. cross_size: Au, /// The [`BoxFragment`]s and [`PositioningContext`]s of all flex items, /// one per flex item in "order-modified document order." @@ -469,15 +482,18 @@ impl FlexContainer { // “Resolve the flexible lengths of all the flex items to find their *used main size*.” // https://drafts.csswg.org/css-flexbox/#algo-flex - let flex_lines = collect_flex_lines( + let initial_line_layouts = do_initial_flex_line_layout( &mut flex_context, container_main_size, &mut flex_items, main_gap, ); - let line_count = flex_lines.len(); - let content_cross_size = flex_lines.iter().map(|line| line.cross_size).sum::() + + let line_count = initial_line_layouts.len(); + let content_cross_size = initial_line_layouts + .iter() + .map(|layout| layout.line_size.cross) + .sum::() + cross_gap * (line_count as i32 - 1); // https://drafts.csswg.org/css-flexbox/#algo-cross-container @@ -495,8 +511,9 @@ impl FlexContainer { let mut cross_start_position_cursor = Au::zero(); let mut line_interval = cross_gap; + let mut extra_space_for_align_stretch = Au::zero(); if let Some(cross_size) = flex_context.container_definite_inner_size.cross { - let free_space = cross_size - content_cross_size; + let mut free_space = cross_size - content_cross_size; let layout_is_flex_reversed = flex_context.flex_wrap_reverse; // Implement fallback alignment. @@ -510,11 +527,23 @@ impl FlexContainer { let mut resolved_align_content = align_content_style.value(); let mut is_safe = align_content_style.flags() == AlignFlags::SAFE; - // Fallback occurs in two cases: + // From https://drafts.csswg.org/css-flexbox/#algo-line-align: + // > Some alignments can only be fulfilled in certain situations or are + // > limited in how much space they can consume; for example, space-between + // > can only operate when there is more than one alignment subject, and + // > baseline alignment, once fulfilled, might not be enough to absorb all + // > the excess space. In these cases a fallback alignment takes effect (as + // > defined below) to fully consume the excess space. + let fallback_is_needed = match resolved_align_content { + _ if free_space <= Au::zero() => true, + AlignFlags::STRETCH => line_count < 1, + AlignFlags::SPACE_BETWEEN | + AlignFlags::SPACE_AROUND | + AlignFlags::SPACE_EVENLY => line_count < 2, + _ => false, + }; - // 1. If there is only a single item being aligned and alignment is a distributed alignment keyword - // https://www.w3.org/TR/css-align-3/#distribution-values - if line_count <= 1 || free_space <= Au::zero() { + if fallback_is_needed { (resolved_align_content, is_safe) = match resolved_align_content { AlignFlags::STRETCH => (AlignFlags::FLEX_START, true), AlignFlags::SPACE_BETWEEN => (AlignFlags::FLEX_START, true), @@ -532,6 +561,15 @@ impl FlexContainer { resolved_align_content }; + // TODO: There are bad cases where we end up distributing much less free space than we have. + // For instance if we have 999 Au of free space and 1000 lines, we won't distribute any! + // We need to calculate how much free space to distribute to a line for every line, subtracting + // that value from the total. + if resolved_align_content == AlignFlags::STRETCH { + extra_space_for_align_stretch = free_space / initial_line_layouts.len() as i32; + free_space -= extra_space_for_align_stretch * initial_line_layouts.len() as i32; + } + // Implement "unsafe" alignment. "safe" alignment is handled by the fallback process above. cross_start_position_cursor = match resolved_align_content { AlignFlags::START => Au::zero(), @@ -576,7 +614,20 @@ impl FlexContainer { }; }; - let line_cross_start_positions = flex_lines + let final_line_layouts: Vec<_> = initial_line_layouts + .into_iter() + .map(|initial_layout| { + let final_line_cross_size = + initial_layout.line_size.cross + extra_space_for_align_stretch; + initial_layout.finish_with_final_cross_size( + &mut flex_context, + main_gap, + final_line_cross_size, + ) + }) + .collect(); + + let line_cross_start_positions = final_line_layouts .iter() .map(|line| { let cross_start = cross_start_position_cursor; @@ -607,50 +658,55 @@ impl FlexContainer { let mut baseline_alignment_participating_baselines = Baselines::default(); let mut all_baselines = Baselines::default(); - let num_lines = flex_lines.len(); - let mut flex_item_fragments = izip!(flex_lines.into_iter(), line_cross_start_positions) - .enumerate() - .flat_map(|(index, (mut line, line_cross_start_position))| { - let flow_relative_line_position = match (flex_axis, flex_wrap_reverse) { - (FlexAxis::Row, false) => LogicalVec2 { - block: line_cross_start_position, - inline: Au::zero(), - }, - (FlexAxis::Row, true) => LogicalVec2 { - block: container_cross_size - line_cross_start_position - line.cross_size, - inline: Au::zero(), - }, - (FlexAxis::Column, false) => LogicalVec2 { - block: Au::zero(), - inline: line_cross_start_position, - }, - (FlexAxis::Column, true) => LogicalVec2 { - block: Au::zero(), - inline: container_cross_size - line_cross_start_position - line.cross_size, - }, - }; + let num_lines = final_line_layouts.len(); + let mut flex_item_fragments = + izip!(final_line_layouts.into_iter(), line_cross_start_positions) + .enumerate() + .flat_map(|(index, (mut line, line_cross_start_position))| { + let flow_relative_line_position = match (flex_axis, flex_wrap_reverse) { + (FlexAxis::Row, false) => LogicalVec2 { + block: line_cross_start_position, + inline: Au::zero(), + }, + (FlexAxis::Row, true) => LogicalVec2 { + block: container_cross_size - + line_cross_start_position - + line.cross_size, + inline: Au::zero(), + }, + (FlexAxis::Column, false) => LogicalVec2 { + block: Au::zero(), + inline: line_cross_start_position, + }, + (FlexAxis::Column, true) => LogicalVec2 { + block: Au::zero(), + inline: container_cross_size - + line_cross_start_position - + line.cross_size, + }, + }; - let line_shared_alignment_baseline = line - .shared_alignment_baseline - .map(|baseline| baseline + flow_relative_line_position.block); - let line_all_baselines = - line.all_baselines.offset(flow_relative_line_position.block); - if index == 0 { - baseline_alignment_participating_baselines.first = - line_shared_alignment_baseline; - all_baselines.first = line_all_baselines.first; - } - if index == num_lines - 1 { - baseline_alignment_participating_baselines.last = - line_shared_alignment_baseline; - all_baselines.last = line_all_baselines.last; - } + let line_shared_alignment_baseline = line + .shared_alignment_baseline + .map(|baseline| baseline + flow_relative_line_position.block); + let line_all_baselines = + line.all_baselines.offset(flow_relative_line_position.block); + if index == 0 { + baseline_alignment_participating_baselines.first = + line_shared_alignment_baseline; + all_baselines.first = line_all_baselines.first; + } + if index == num_lines - 1 { + baseline_alignment_participating_baselines.last = + line_shared_alignment_baseline; + all_baselines.last = line_all_baselines.last; + } - for (fragment, _) in &mut line.item_fragments { - fragment.content_rect.start_corner += flow_relative_line_position - } - line.item_fragments - }); + for (fragment, _) in &mut line.item_fragments { + fragment.content_rect.start_corner += flow_relative_line_position + } + line.item_fragments + }); let fragments = absolutely_positioned_items_with_original_order .into_iter() @@ -919,21 +975,24 @@ fn used_flex_direction(container_style: &ComputedValues) -> FlexDirection { // “Collect flex items into flex lines” // https://drafts.csswg.org/css-flexbox/#algo-line-break -fn collect_flex_lines<'items>( +fn do_initial_flex_line_layout<'items>( flex_context: &mut FlexContext, container_main_size: Au, mut items: &'items mut [FlexItem<'items>], main_gap: Au, -) -> Vec { +) -> Vec> { if flex_context.container_is_single_line { - let mut line = FlexLine { - outer_hypothetical_main_sizes_sum: items - .iter() - .map(|item| item.hypothetical_main_size + item.pbm_auto_is_zero.main) - .sum(), + let outer_hypothetical_main_sizes_sum = items + .iter() + .map(|item| item.hypothetical_main_size + item.pbm_auto_is_zero.main) + .sum(); + vec![InitialFlexLineLayout::new( + flex_context, items, - }; - vec![line.layout(flex_context, container_main_size, main_gap)] + outer_hypothetical_main_sizes_sum, + container_main_size, + main_gap, + )] } else { let mut lines = Vec::new(); let mut line_size_so_far = Au::zero(); @@ -953,310 +1012,79 @@ fn collect_flex_lines<'items>( } else { // We found something that doesn’t fit. This line ends *before* this item. let (line_items, rest) = items.split_at_mut(index); - let mut line = FlexLine { - items: line_items, - outer_hypothetical_main_sizes_sum: line_size_so_far, - }; items = rest; - lines.push(line.layout(flex_context, container_main_size, main_gap)); + lines.push(InitialFlexLineLayout::new( + flex_context, + line_items, + line_size_so_far, + container_main_size, + main_gap, + )); + // The next line has this item. line_size_so_far = item_size; index = 1; } } // The last line is added even without finding an item that doesn’t fit - let mut line = FlexLine { + lines.push(InitialFlexLineLayout::new( + flex_context, items, - outer_hypothetical_main_sizes_sum: line_size_so_far, - }; - lines.push(line.layout(flex_context, container_main_size, main_gap)); + line_size_so_far, + container_main_size, + main_gap, + )); lines } } -impl FlexLine<'_> { - fn layout( - &mut self, +impl InitialFlexLineLayout<'_> { + fn new<'items>( flex_context: &mut FlexContext, + items: &'items mut [FlexItem<'items>], + outer_hypothetical_main_sizes_sum: Au, container_main_size: Au, main_gap: Au, - ) -> FlexLineLayoutResult { - let item_count = self.items.len(); - let (item_used_main_sizes, mut free_space) = - self.resolve_flexible_lengths(container_main_size - main_gap * (item_count as i32 - 1)); + ) -> InitialFlexLineLayout<'items> { + let item_count = items.len(); + let (item_used_main_sizes, free_space) = Self::resolve_flexible_lengths( + items, + outer_hypothetical_main_sizes_sum, + container_main_size - main_gap * (item_count as i32 - 1), + ); // https://drafts.csswg.org/css-flexbox/#algo-cross-item - let mut item_layout_results = self - .items + let item_layout_results = items .iter_mut() .zip(&item_used_main_sizes) .map(|(item, &used_main_size)| item.layout(used_main_size, flex_context, None)) .collect::>(); // https://drafts.csswg.org/css-flexbox/#algo-cross-line - let line_cross_size = self.cross_size(&item_layout_results, flex_context); + let line_cross_size = Self::cross_size(items, &item_layout_results, flex_context); let line_size = FlexRelativeVec2 { main: container_main_size, cross: line_cross_size, }; - // FIXME: Handle `align-content: stretch` - // https://drafts.csswg.org/css-flexbox/#algo-line-stretch - - // FIXME: Collapse `visibility: collapse` items - // This involves “restart layout from the beginning” with a modified second round, - // which will make structuring the code… interesting. - // https://drafts.csswg.org/css-flexbox/#algo-visibility - - // Determine the used cross size of each flex item - // https://drafts.csswg.org/css-flexbox/#algo-stretch - let mut shared_alignment_baseline = None; - let mut item_used_cross_sizes = Vec::with_capacity(item_count); - let mut item_cross_margins = Vec::with_capacity(item_count); - for (item, item_layout_result, used_main_size) in izip!( - self.items.iter_mut(), - item_layout_results.iter_mut(), - &item_used_main_sizes - ) { - let has_stretch = item.align_self.0.value() == AlignFlags::STRETCH; - let used_cross_size = if has_stretch && - item.content_box_size.cross.is_auto() && - !(item.margin.cross_start.is_auto() || item.margin.cross_end.is_auto()) - { - (line_cross_size - item.pbm_auto_is_zero.cross).clamp_between_extremums( - item.content_min_size.cross, - item.content_max_size.cross, - ) - } else { - item_layout_result.hypothetical_cross_size - }; - item_used_cross_sizes.push(used_cross_size); - - if has_stretch { - // “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.” - *item_layout_result = - item.layout(*used_main_size, flex_context, Some(used_cross_size)); - } - - // TODO: This also needs to check whether we have a compatible writing mode. - let baseline = item_layout_result - .get_or_synthesize_baseline_with_block_size(used_cross_size, item); - if matches!( - item.align_self.0.value(), - AlignFlags::BASELINE | AlignFlags::LAST_BASELINE - ) { - shared_alignment_baseline = - Some(shared_alignment_baseline.unwrap_or(baseline).max(baseline)); - } - item_layout_result.baseline_relative_to_margin_box = Some(baseline); - - // https://drafts.csswg.org/css-flexbox/#algo-cross-margins - item_cross_margins.push(item.resolve_auto_cross_margins( - flex_context, - line_cross_size, - used_cross_size, - )); - } - - // Layout of items is over. These should no longer be mutable. - let item_layout_results = item_layout_results; - - // Distribute any remaining free space - // https://drafts.csswg.org/css-flexbox/#algo-main-align - let (item_main_margins, free_space_distributed) = - self.resolve_auto_main_margins(free_space); - if free_space_distributed { - free_space = Au::zero(); - } - - // Align the items along the main-axis per justify-content. - let layout_is_flex_reversed = flex_context.flex_direction_is_reversed; - - // Implement fallback alignment. - // - // In addition to the spec at https://www.w3.org/TR/css-align-3/ this implementation follows - // the resolution of https://github.com/w3c/csswg-drafts/issues/10154 - let resolved_justify_content: AlignFlags = { - let justify_content_style = flex_context.justify_content.0.primary(); - - // Inital values from the style system - let mut resolved_justify_content = justify_content_style.value(); - let mut is_safe = justify_content_style.flags() == AlignFlags::SAFE; - - // Fallback occurs in two cases: - - // 1. If there is only a single item being aligned and alignment is a distributed alignment keyword - // https://www.w3.org/TR/css-align-3/#distribution-values - if item_count <= 1 || free_space <= Au::zero() { - (resolved_justify_content, is_safe) = match resolved_justify_content { - AlignFlags::STRETCH => (AlignFlags::FLEX_START, true), - AlignFlags::SPACE_BETWEEN => (AlignFlags::FLEX_START, true), - AlignFlags::SPACE_AROUND => (AlignFlags::CENTER, true), - AlignFlags::SPACE_EVENLY => (AlignFlags::CENTER, true), - _ => (resolved_justify_content, is_safe), - } - }; - - // 2. If free space is negative the "safe" alignment variants all fallback to Start alignment - if free_space <= Au::zero() && is_safe { - resolved_justify_content = AlignFlags::START; - } - - resolved_justify_content - }; - - // Implement "unsafe" alignment. "safe" alignment is handled by the fallback process above. - let main_start_position = match resolved_justify_content { - AlignFlags::START => Au::zero(), - AlignFlags::FLEX_START => { - if layout_is_flex_reversed { - free_space - } else { - Au::zero() - } - }, - AlignFlags::END => free_space, - AlignFlags::FLEX_END => { - if layout_is_flex_reversed { - Au::zero() - } else { - free_space - } - }, - AlignFlags::CENTER => free_space / 2, - AlignFlags::STRETCH => Au::zero(), - AlignFlags::SPACE_BETWEEN => Au::zero(), - AlignFlags::SPACE_AROUND => (free_space / item_count as i32) / 2, - AlignFlags::SPACE_EVENLY => free_space / (item_count + 1) as i32, - - // TODO: Implement all alignments. Note: not all alignment values are valid for content distribution - _ => Au::zero(), - }; - - let item_main_interval = match resolved_justify_content { - AlignFlags::START => Au::zero(), - AlignFlags::FLEX_START => Au::zero(), - AlignFlags::END => Au::zero(), - AlignFlags::FLEX_END => Au::zero(), - AlignFlags::CENTER => Au::zero(), - AlignFlags::STRETCH => Au::zero(), - AlignFlags::SPACE_BETWEEN => free_space / (item_count - 1) as i32, - AlignFlags::SPACE_AROUND => free_space / item_count as i32, - AlignFlags::SPACE_EVENLY => free_space / (item_count + 1) as i32, - - // TODO: Implement all alignments. Note: not all alignment values are valid for content distribution - _ => Au::zero(), - }; - let item_main_interval = item_main_interval + main_gap; - - let mut all_baselines = Baselines::default(); - let mut main_position_cursor = main_start_position; - let item_fragments = izip!( - self.items.iter(), - item_main_margins, - item_cross_margins, - &item_used_main_sizes, - &item_used_cross_sizes, - item_layout_results.into_iter() - ) - .map( - |( - item, - item_main_margins, - item_cross_margins, - item_used_main_size, - item_used_cross_size, - item_layout_result, - )| { - let item_margin = FlexRelativeSides { - main_start: item_main_margins.0, - main_end: item_main_margins.1, - cross_start: item_cross_margins.0, - cross_end: item_cross_margins.1, - }; - - // https://drafts.csswg.org/css-flexbox/#algo-main-align - // “Align the items along the main-axis” - main_position_cursor += - item_margin.main_start + item.border.main_start + item.padding.main_start; - let item_content_main_start_position = main_position_cursor; - main_position_cursor += *item_used_main_size + - item.padding.main_end + - item.border.main_end + - item_margin.main_end + - item_main_interval; - - // https://drafts.csswg.org/css-flexbox/#algo-cross-align - let item_content_cross_start_position = item.align_along_cross_axis( - &item_margin, - item_used_cross_size, - line_cross_size, - item_layout_result - .baseline_relative_to_margin_box - .unwrap_or_default(), - shared_alignment_baseline.unwrap_or_default(), - ); - - let start_corner = FlexRelativeVec2 { - main: item_content_main_start_position, - cross: item_content_cross_start_position, - }; - let size = FlexRelativeVec2 { - main: *item_used_main_size, - cross: *item_used_cross_size, - }; - - // Need to collect both baselines from baseline participation and other baselines. - let content_rect = flex_context - .rect_to_flow_relative(line_size, FlexRelativeRect { start_corner, size }); - let margin = flex_context.sides_to_flow_relative(item_margin); - let collapsed_margin = CollapsedBlockMargins::from_margin(&margin); - - if let Some(item_baseline) = - item_layout_result.baseline_relative_to_margin_box.as_ref() - { - let item_baseline = *item_baseline + item_content_cross_start_position - - item.border.cross_start - - item.padding.cross_start - - item_margin.cross_start; - all_baselines.first.get_or_insert(item_baseline); - all_baselines.last = Some(item_baseline); - } - - ( - BoxFragment::new( - item.box_.base_fragment_info(), - item.box_.style().clone(), - item_layout_result.fragments, - content_rect, - flex_context.sides_to_flow_relative(item.padding), - flex_context.sides_to_flow_relative(item.border), - margin, - None, /* clearance */ - collapsed_margin, - ), - item_layout_result.positioning_context, - ) - }, - ) - .collect(); - - FlexLineLayoutResult { - cross_size: line_cross_size, - item_fragments, - all_baselines, - shared_alignment_baseline, + InitialFlexLineLayout { + items, + line_size, + item_layout_results, + item_used_main_sizes, + free_space_in_main_axis: free_space, } } /// Return the *main size* of each item, and the line’s remainaing free space /// - fn resolve_flexible_lengths(&self, container_main_size: Au) -> (Vec, Au) { - let mut frozen = vec![false; self.items.len()]; - let mut target_main_sizes_vec = self - .items + fn resolve_flexible_lengths<'items>( + items: &'items [FlexItem<'items>], + outer_hypothetical_main_sizes_sum: Au, + container_main_size: Au, + ) -> (Vec, Au) { + let mut frozen = vec![false; items.len()]; + let mut target_main_sizes_vec = items .iter() .map(|item| item.flex_base_size) .collect::>(); @@ -1266,7 +1094,7 @@ impl FlexLine<'_> { let frozen = Cell::from_mut(&mut *frozen).as_slice_of_cells(); let frozen_count = Cell::new(0); - let grow = self.outer_hypothetical_main_sizes_sum < container_main_size; + let grow = outer_hypothetical_main_sizes_sum < container_main_size; let flex_factor = |item: &FlexItem| { let position_style = item.box_.style().get_position(); if grow { @@ -1275,10 +1103,10 @@ impl FlexLine<'_> { position_style.flex_shrink.0 } }; - let items = || self.items.iter().zip(target_main_sizes).zip(frozen); + let items_and_main_sizes = || items.iter().zip(target_main_sizes).zip(frozen); // “Size inflexible items” - for ((item, target_main_size), frozen) in items() { + for ((item, target_main_size), frozen) in items_and_main_sizes() { let is_inflexible = flex_factor(item) == 0. || if grow { item.flex_base_size > item.hypothetical_main_size @@ -1292,10 +1120,10 @@ impl FlexLine<'_> { } } - let check_for_flexible_items = || frozen_count.get() < self.items.len(); + let check_for_flexible_items = || frozen_count.get() < items.len(); let free_space = || { container_main_size - - items() + items_and_main_sizes() .map(|((item, target_main_size), frozen)| { item.pbm_auto_is_zero.main + if frozen.get() { @@ -1309,7 +1137,7 @@ impl FlexLine<'_> { // https://drafts.csswg.org/css-flexbox/#initial-free-space let initial_free_space = free_space(); let unfrozen_items = || { - items().filter_map(|(item_and_target_main_size, frozen)| { + items_and_main_sizes().filter_map(|(item_and_target_main_size, frozen)| { if !frozen.get() { Some(item_and_target_main_size) } else { @@ -1389,7 +1217,7 @@ impl FlexLine<'_> { // “Freeze all the items with min violations.” // “If the item’s target main size was made larger by [clamping], // it’s a min violation.” - for (item_and_target_main_size, frozen) in items() { + for (item_and_target_main_size, frozen) in items_and_main_sizes() { if violation(item_and_target_main_size) > Au::zero() { let (item, target_main_size) = item_and_target_main_size; target_main_size.set(item.content_min_size.main); @@ -1403,7 +1231,7 @@ impl FlexLine<'_> { // “Freeze all the items with max violations.” // “If the item’s target main size was made smaller by [clamping], // it’s a max violation.” - for (item_and_target_main_size, frozen) in items() { + for (item_and_target_main_size, frozen) in items_and_main_sizes() { if violation(item_and_target_main_size) < Au::zero() { let (item, target_main_size) = item_and_target_main_size; let Some(max_size) = item.content_max_size.main else { @@ -1418,13 +1246,314 @@ impl FlexLine<'_> { } } } + + /// + fn cross_size<'items>( + items: &'items [FlexItem<'items>], + item_layout_results: &[FlexItemLayoutResult], + flex_context: &FlexContext, + ) -> Au { + if flex_context.container_is_single_line { + if let Some(size) = flex_context.container_definite_inner_size.cross { + return size; + } + } + + let mut max_ascent = Au::zero(); + let mut max_descent = Au::zero(); + let mut max_outer_hypothetical_cross_size = Au::zero(); + for (item_result, item) in item_layout_results.iter().zip(&*items) { + // TODO: check inline-axis is parallel to main axis, check no auto cross margins + if matches!( + item.align_self.0.value(), + AlignFlags::BASELINE | AlignFlags::LAST_BASELINE + ) { + let baseline = item_result.get_or_synthesize_baseline_with_block_size( + item_result.hypothetical_cross_size, + item, + ); + let hypothetical_margin_box_cross_size = + item_result.hypothetical_cross_size + item.pbm_auto_is_zero.cross; + max_ascent = max_ascent.max(baseline); + max_descent = max_descent.max(hypothetical_margin_box_cross_size - baseline); + } else { + max_outer_hypothetical_cross_size = max_outer_hypothetical_cross_size + .max(item_result.hypothetical_cross_size + item.pbm_auto_is_zero.cross); + } + } + + // https://drafts.csswg.org/css-flexbox/#baseline-participation + let largest = max_outer_hypothetical_cross_size.max(max_ascent + max_descent); + if flex_context.container_is_single_line { + largest.clamp_between_extremums( + flex_context.container_min_cross_size, + flex_context.container_max_cross_size, + ) + } else { + largest + } + } + + fn finish_with_final_cross_size( + mut self, + flex_context: &mut FlexContext, + main_gap: Au, + final_line_cross_size: Au, + ) -> FinalFlexLineLayout { + // FIXME: Collapse `visibility: collapse` items + // This involves “restart layout from the beginning” with a modified second round, + // which will make structuring the code… interesting. + // https://drafts.csswg.org/css-flexbox/#algo-visibility + + // Distribute any remaining main free space to auto margins according to + // https://drafts.csswg.org/css-flexbox/#algo-main-align. + let auto_margins_count = self + .items + .iter() + .map(|item| { + item.margin.main_start.is_auto() as u32 + item.margin.main_end.is_auto() as u32 + }) + .sum::(); + let (space_distributed_to_auto_main_margins, free_space_in_main_axis) = + if self.free_space_in_main_axis > Au::zero() && auto_margins_count > 0 { + ( + self.free_space_in_main_axis / auto_margins_count as i32, + Au::zero(), + ) + } else { + (Au::zero(), self.free_space_in_main_axis) + }; + + // Determine the used cross size of each flex item + // https://drafts.csswg.org/css-flexbox/#algo-stretch + let item_count = self.items.len(); + let mut shared_alignment_baseline = None; + let mut item_used_cross_sizes = Vec::with_capacity(item_count); + let mut item_margins = Vec::with_capacity(item_count); + for (item, item_layout_result, used_main_size) in izip!( + self.items.iter_mut(), + self.item_layout_results.iter_mut(), + self.item_used_main_sizes.iter(), + ) { + let has_stretch = item.align_self.0.value() == AlignFlags::STRETCH; + let used_cross_size = if has_stretch && + item.content_box_size.cross.is_auto() && + !(item.margin.cross_start.is_auto() || item.margin.cross_end.is_auto()) + { + (final_line_cross_size - item.pbm_auto_is_zero.cross).clamp_between_extremums( + item.content_min_size.cross, + item.content_max_size.cross, + ) + } else { + item_layout_result.hypothetical_cross_size + }; + item_used_cross_sizes.push(used_cross_size); + + if has_stretch { + // “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.” + *item_layout_result = + item.layout(*used_main_size, flex_context, Some(used_cross_size)); + } + + // TODO: This also needs to check whether we have a compatible writing mode. + let baseline = item_layout_result + .get_or_synthesize_baseline_with_block_size(used_cross_size, item); + if matches!( + item.align_self.0.value(), + AlignFlags::BASELINE | AlignFlags::LAST_BASELINE + ) { + shared_alignment_baseline = + Some(shared_alignment_baseline.unwrap_or(baseline).max(baseline)); + } + item_layout_result.baseline_relative_to_margin_box = Some(baseline); + + item_margins.push(item.resolve_auto_margins( + flex_context, + final_line_cross_size, + used_cross_size, + space_distributed_to_auto_main_margins, + )); + } + + // Align the items along the main-axis per justify-content. + let layout_is_flex_reversed = flex_context.flex_direction_is_reversed; + + // Implement fallback alignment. + // + // In addition to the spec at https://www.w3.org/TR/css-align-3/ this implementation follows + // the resolution of https://github.com/w3c/csswg-drafts/issues/10154 + let resolved_justify_content: AlignFlags = { + let justify_content_style = flex_context.justify_content.0.primary(); + + // Inital values from the style system + let mut resolved_justify_content = justify_content_style.value(); + let mut is_safe = justify_content_style.flags() == AlignFlags::SAFE; + + // Fallback occurs in two cases: + + // 1. If there is only a single item being aligned and alignment is a distributed alignment keyword + // https://www.w3.org/TR/css-align-3/#distribution-values + if item_count <= 1 || free_space_in_main_axis <= Au::zero() { + (resolved_justify_content, is_safe) = match resolved_justify_content { + AlignFlags::STRETCH => (AlignFlags::FLEX_START, true), + AlignFlags::SPACE_BETWEEN => (AlignFlags::FLEX_START, true), + AlignFlags::SPACE_AROUND => (AlignFlags::CENTER, true), + AlignFlags::SPACE_EVENLY => (AlignFlags::CENTER, true), + _ => (resolved_justify_content, is_safe), + } + }; + + // 2. If free space is negative the "safe" alignment variants all fallback to Start alignment + if free_space_in_main_axis <= Au::zero() && is_safe { + resolved_justify_content = AlignFlags::START; + } + + resolved_justify_content + }; + + // Implement "unsafe" alignment. "safe" alignment is handled by the fallback process above. + let main_start_position = match resolved_justify_content { + AlignFlags::START => Au::zero(), + AlignFlags::FLEX_START => { + if layout_is_flex_reversed { + free_space_in_main_axis + } else { + Au::zero() + } + }, + AlignFlags::END => free_space_in_main_axis, + AlignFlags::FLEX_END => { + if layout_is_flex_reversed { + Au::zero() + } else { + free_space_in_main_axis + } + }, + AlignFlags::CENTER => free_space_in_main_axis / 2, + AlignFlags::STRETCH => Au::zero(), + AlignFlags::SPACE_BETWEEN => Au::zero(), + AlignFlags::SPACE_AROUND => (free_space_in_main_axis / item_count as i32) / 2, + AlignFlags::SPACE_EVENLY => free_space_in_main_axis / (item_count + 1) as i32, + + // TODO: Implement all alignments. Note: not all alignment values are valid for content distribution + _ => Au::zero(), + }; + + let item_main_interval = match resolved_justify_content { + AlignFlags::START => Au::zero(), + AlignFlags::FLEX_START => Au::zero(), + AlignFlags::END => Au::zero(), + AlignFlags::FLEX_END => Au::zero(), + AlignFlags::CENTER => Au::zero(), + AlignFlags::STRETCH => Au::zero(), + AlignFlags::SPACE_BETWEEN => free_space_in_main_axis / (item_count - 1) as i32, + AlignFlags::SPACE_AROUND => free_space_in_main_axis / item_count as i32, + AlignFlags::SPACE_EVENLY => free_space_in_main_axis / (item_count + 1) as i32, + + // TODO: Implement all alignments. Note: not all alignment values are valid for content distribution + _ => Au::zero(), + }; + let item_main_interval = item_main_interval + main_gap; + + let mut all_baselines = Baselines::default(); + let mut main_position_cursor = main_start_position; + let item_fragments = izip!( + self.items.iter(), + item_margins, + self.item_used_main_sizes.iter(), + item_used_cross_sizes.iter(), + self.item_layout_results.into_iter() + ) + .map( + |(item, item_margin, item_used_main_size, item_used_cross_size, item_layout_result)| { + // https://drafts.csswg.org/css-flexbox/#algo-main-align + // “Align the items along the main-axis” + main_position_cursor += + item_margin.main_start + item.border.main_start + item.padding.main_start; + let item_content_main_start_position = main_position_cursor; + main_position_cursor += *item_used_main_size + + item.padding.main_end + + item.border.main_end + + item_margin.main_end + + item_main_interval; + + // https://drafts.csswg.org/css-flexbox/#algo-cross-align + let item_content_cross_start_position = item.align_along_cross_axis( + &item_margin, + item_used_cross_size, + final_line_cross_size, + item_layout_result + .baseline_relative_to_margin_box + .unwrap_or_default(), + shared_alignment_baseline.unwrap_or_default(), + ); + + let start_corner = FlexRelativeVec2 { + main: item_content_main_start_position, + cross: item_content_cross_start_position, + }; + let size = FlexRelativeVec2 { + main: *item_used_main_size, + cross: *item_used_cross_size, + }; + + // Need to collect both baselines from baseline participation and other baselines. + let final_line_size = FlexRelativeVec2 { + main: self.line_size.main, + cross: final_line_cross_size, + }; + let content_rect = flex_context.rect_to_flow_relative( + final_line_size, + FlexRelativeRect { start_corner, size }, + ); + let margin = flex_context.sides_to_flow_relative(item_margin); + let collapsed_margin = CollapsedBlockMargins::from_margin(&margin); + + if let Some(item_baseline) = + item_layout_result.baseline_relative_to_margin_box.as_ref() + { + let item_baseline = *item_baseline + item_content_cross_start_position - + item.border.cross_start - + item.padding.cross_start - + item_margin.cross_start; + all_baselines.first.get_or_insert(item_baseline); + all_baselines.last = Some(item_baseline); + } + + ( + BoxFragment::new( + item.box_.base_fragment_info(), + item.box_.style().clone(), + item_layout_result.fragments, + content_rect, + flex_context.sides_to_flow_relative(item.padding), + flex_context.sides_to_flow_relative(item.border), + margin, + None, /* clearance */ + collapsed_margin, + ), + item_layout_result.positioning_context, + ) + }, + ) + .collect(); + + FinalFlexLineLayout { + cross_size: final_line_cross_size, + item_fragments, + all_baselines, + shared_alignment_baseline, + } + } } -impl<'a> FlexItem<'a> { - // Return the hypothetical cross size together with laid out contents of the fragment. - // https://drafts.csswg.org/css-flexbox/#algo-cross-item - // “performing layout as if it were an in-flow block-level box - // with the used main size and the given available space, treating `auto` as `fit-content`.” +impl FlexItem<'_> { + /// Return the hypothetical cross size together with laid out contents of the fragment. + /// From : + /// > performing layout as if it were an in-flow block-level box with the used main + /// > size and the given available space, treating `auto` as `fit-content`. fn layout( &mut self, used_main_size: Au, @@ -1562,117 +1691,48 @@ impl<'a> FlexItem<'a> { self.border.cross_end + self.padding.cross_end } -} -impl<'items> FlexLine<'items> { - /// - fn cross_size( - &self, - item_layout_results: &[FlexItemLayoutResult], - flex_context: &FlexContext, - ) -> Au { - if flex_context.container_is_single_line { - if let Some(size) = flex_context.container_definite_inner_size.cross { - return size; - } - } - - let mut max_ascent = Au::zero(); - let mut max_descent = Au::zero(); - let mut max_outer_hypothetical_cross_size = Au::zero(); - for (item_result, item) in item_layout_results.iter().zip(&*self.items) { - // TODO: check inline-axis is parallel to main axis, check no auto cross margins - if matches!( - item.align_self.0.value(), - AlignFlags::BASELINE | AlignFlags::LAST_BASELINE - ) { - let baseline = item_result.get_or_synthesize_baseline_with_block_size( - item_result.hypothetical_cross_size, - item, - ); - let hypothetical_margin_box_cross_size = - item_result.hypothetical_cross_size + item.pbm_auto_is_zero.cross; - max_ascent = max_ascent.max(baseline); - max_descent = max_descent.max(hypothetical_margin_box_cross_size - baseline); - } else { - max_outer_hypothetical_cross_size = max_outer_hypothetical_cross_size - .max(item_result.hypothetical_cross_size + item.pbm_auto_is_zero.cross); - } - } - - // FIXME: add support for `align-self: baseline` - // and computing the baseline of flex items. - // https://drafts.csswg.org/css-flexbox/#baseline-participation - let largest = max_outer_hypothetical_cross_size.max(max_ascent + max_descent); - if flex_context.container_is_single_line { - largest.clamp_between_extremums( - flex_context.container_min_cross_size, - flex_context.container_max_cross_size, - ) - } else { - largest - } - } - - // Return the main-start and main-end margin of each item in the line, - // with `auto` values resolved, - // and return whether free space has been distributed. - fn resolve_auto_main_margins( - &self, - remaining_free_space: Au, - ) -> (impl Iterator + '_, bool) { - let each_auto_margin = if remaining_free_space > Au::zero() { - let auto_margins_count = self - .items - .iter() - .map(|item| { - item.margin.main_start.is_auto() as u32 + item.margin.main_end.is_auto() as u32 - }) - .sum::(); - if auto_margins_count > 0 { - remaining_free_space / auto_margins_count as i32 - } else { - Au::zero() - } - } else { - Au::zero() - }; - ( - self.items.iter().map(move |item| { - ( - item.margin.main_start.auto_is(|| each_auto_margin), - item.margin.main_end.auto_is(|| each_auto_margin), - ) - }), - each_auto_margin > Au::zero(), - ) - } -} - -impl FlexItem<'_> { - /// Return the cross-start and cross-end margin, with `auto` values resolved. - /// - fn resolve_auto_cross_margins( + /// Return the cross-start, cross-end, main-start, and main-end margins, with `auto` values resolved. + /// See: + /// + /// - + /// - + fn resolve_auto_margins( &self, flex_context: &FlexContext, line_cross_size: Au, item_cross_content_size: Au, - ) -> (Au, Au) { + space_distributed_to_auto_main_margins: Au, + ) -> FlexRelativeSides { + let main_start = self + .margin + .main_start + .auto_is(|| space_distributed_to_auto_main_margins); + let main_end = self + .margin + .main_end + .auto_is(|| space_distributed_to_auto_main_margins); + let auto_count = match (self.margin.cross_start, self.margin.cross_end) { - (AuOrAuto::LengthPercentage(start), AuOrAuto::LengthPercentage(end)) => { - return (start, end); + (AuOrAuto::LengthPercentage(cross_start), AuOrAuto::LengthPercentage(cross_end)) => { + return FlexRelativeSides { + cross_start, + cross_end, + main_start, + main_end, + } }, (AuOrAuto::Auto, AuOrAuto::Auto) => 2, _ => 1, }; - let outer_size = self.pbm_auto_is_zero.cross + item_cross_content_size; - let available = line_cross_size - outer_size; - let start; - let end; + let outer_cross_size = self.pbm_auto_is_zero.cross + item_cross_content_size; + let available = line_cross_size - outer_cross_size; + let cross_start; + let cross_end; if available > Au::zero() { let each_auto_margin = available / auto_count; - start = self.margin.cross_start.auto_is(|| each_auto_margin); - end = self.margin.cross_end.auto_is(|| each_auto_margin); + cross_start = self.margin.cross_start.auto_is(|| each_auto_margin); + cross_end = self.margin.cross_end.auto_is(|| each_auto_margin); } else { // “the block-start or inline-start margin (whichever is in the cross axis)” // This margin is the cross-end on iff `flex-wrap` is `wrap-reverse`, @@ -1695,14 +1755,20 @@ impl FlexItem<'_> { // set it to zero. Set the opposite margin so that the outer cross size of the item // equals the cross size of its flex line.” if flex_wrap_reverse { - start = self.margin.cross_start.auto_is(|| available); - end = self.margin.cross_end.auto_is(Au::zero); + cross_start = self.margin.cross_start.auto_is(|| available); + cross_end = self.margin.cross_end.auto_is(Au::zero); } else { - start = self.margin.cross_start.auto_is(Au::zero); - end = self.margin.cross_end.auto_is(|| available); + cross_start = self.margin.cross_start.auto_is(Au::zero); + cross_end = self.margin.cross_end.auto_is(|| available); } } - (start, end) + + FlexRelativeSides { + cross_start, + cross_end, + main_start, + main_end, + } } /// Return the coordinate of the cross-start side of the content area diff --git a/tests/wpt/meta/css/css-flexbox/align-content-006.htm.ini b/tests/wpt/meta/css/css-flexbox/align-content-006.htm.ini deleted file mode 100644 index 79e75f956b2..00000000000 --- a/tests/wpt/meta/css/css-flexbox/align-content-006.htm.ini +++ /dev/null @@ -1,2 +0,0 @@ -[align-content-006.htm] - expected: FAIL diff --git a/tests/wpt/meta/css/css-flexbox/align-content-wrap-003.html.ini b/tests/wpt/meta/css/css-flexbox/align-content-wrap-003.html.ini index 75af4a4f3e2..0a06bb106d9 100644 --- a/tests/wpt/meta/css/css-flexbox/align-content-wrap-003.html.ini +++ b/tests/wpt/meta/css/css-flexbox/align-content-wrap-003.html.ini @@ -5,9 +5,6 @@ [.flexbox 33] expected: FAIL - [.flexbox 8] - expected: FAIL - [.flexbox 34] expected: FAIL @@ -47,9 +44,6 @@ [.flexbox 23] expected: FAIL - [.flexbox 22] - expected: FAIL - [.flexbox 44] expected: FAIL diff --git a/tests/wpt/meta/css/css-flexbox/align-content_stretch.html.ini b/tests/wpt/meta/css/css-flexbox/align-content_stretch.html.ini deleted file mode 100644 index 144d0ff6fe1..00000000000 --- a/tests/wpt/meta/css/css-flexbox/align-content_stretch.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[align-content_stretch.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-flexbox/flexbox_align-content-stretch-2.html.ini b/tests/wpt/meta/css/css-flexbox/flexbox_align-content-stretch-2.html.ini deleted file mode 100644 index 424957d4961..00000000000 --- a/tests/wpt/meta/css/css-flexbox/flexbox_align-content-stretch-2.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[flexbox_align-content-stretch-2.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-flexbox/flexbox_align-content-stretch.html.ini b/tests/wpt/meta/css/css-flexbox/flexbox_align-content-stretch.html.ini deleted file mode 100644 index 989e514562a..00000000000 --- a/tests/wpt/meta/css/css-flexbox/flexbox_align-content-stretch.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[flexbox_align-content-stretch.html] - expected: FAIL