/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::{Cell, LazyCell}; use std::cmp::Ordering; use app_units::Au; use atomic_refcell::AtomicRef; use itertools::izip; use rayon::iter::{ IndexedParallelIterator, IntoParallelRefIterator, ParallelDrainRange, ParallelIterator, }; use style::Zero; use style::computed_values::position::T as Position; use style::logical_geometry::Direction; use style::properties::ComputedValues; use style::properties::longhands::align_items::computed_value::T as AlignItems; use style::properties::longhands::box_sizing::computed_value::T as BoxSizing; use style::properties::longhands::flex_direction::computed_value::T as FlexDirection; use style::properties::longhands::flex_wrap::computed_value::T as FlexWrap; use style::values::computed::LengthPercentage; use style::values::generics::flex::GenericFlexBasis as FlexBasis; use style::values::generics::length::{GenericLengthPercentageOrAuto, LengthPercentageOrNormal}; use style::values::specified::align::AlignFlags; use super::geom::{FlexAxis, FlexRelativeRect, FlexRelativeSides, FlexRelativeVec2}; use super::{ CachedBlockSizeContribution, FlexContainer, FlexContainerConfig, FlexItemBox, FlexLevelBox, }; use crate::cell::ArcRefCell; use crate::context::LayoutContext; use crate::formatting_contexts::{ Baselines, IndependentFormattingContextContents, IndependentLayout, }; use crate::fragment_tree::{BoxFragment, CollapsedBlockMargins, Fragment, FragmentFlags}; use crate::geom::{AuOrAuto, LogicalRect, LogicalSides, LogicalVec2, Size, Sizes}; use crate::positioned::{ AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, relative_adjustement, }; use crate::sizing::{ ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult, IntrinsicSizingMode, }; use crate::style_ext::{ AspectRatio, Clamp, ComputedValuesExt, ContentBoxSizesAndPBM, LayoutStyle, PaddingBorderMargin, }; use crate::{ ConstraintSpace, ContainingBlock, ContainingBlockSize, IndefiniteContainingBlock, SizeConstraint, }; /// Layout parameters and intermediate results about a flex container, /// grouped to avoid passing around many parameters struct FlexContext<'a> { config: FlexContainerConfig, layout_context: &'a LayoutContext<'a>, positioning_context: &'a mut PositioningContext, containing_block: &'a ContainingBlock<'a>, // For items container_inner_size_constraint: FlexRelativeVec2, } /// A flex item with some intermediate results struct FlexItem<'a> { box_: &'a FlexItemBox, content_cross_size: Size, content_min_size: FlexRelativeVec2, content_max_size: FlexRelativeVec2>, padding: FlexRelativeSides, border: FlexRelativeSides, margin: FlexRelativeSides, /// Sum of padding, border, and margin (with `auto` assumed to be zero) in each axis. /// This is the difference between an outer and inner size. pbm_auto_is_zero: FlexRelativeVec2, /// flex_base_size: Au, /// Whether the [`Self::flex_base_size`] comes from a definite `flex-basis`. /// If false and the container main size is also indefinite, percentages in the item's /// content that resolve against its main size should be indefinite. flex_base_size_is_definite: bool, /// hypothetical_main_size: Au, /// This is `align-self`, defaulting to `align-items` if `auto` align_self: AlignItems, /// Whether or not the size of this [`FlexItem`] depends on its block constraints. depends_on_block_constraints: bool, /// preferred_aspect_ratio: Option, } /// Child of a FlexContainer. Can either be absolutely positioned, or not. If not, /// a placeholder is used and flex content is stored outside of this enum. enum FlexContent { AbsolutelyPositionedBox(ArcRefCell), FlexItemPlaceholder, } /// Return type of `FlexItem::layout` struct FlexItemLayoutResult { hypothetical_cross_size: Au, fragments: Vec, positioning_context: PositioningContext, // Either the first or the last baseline, depending on ‘align-self’. baseline_relative_to_margin_box: Option, // The content size of this layout. For replaced elements this is known before layout, // but for non-replaced it's only known after layout. content_size: LogicalVec2, // The containing block inline size used to generate this layout. containing_block_inline_size: Au, // The containing block block size used to generate this layout. containing_block_block_size: SizeConstraint, // Whether or not this layout depended on block constraints. depends_on_block_constraints: bool, // Whether or not this layout had a child that dependeded on block constraints. has_child_which_depends_on_block_constraints: bool, } impl FlexItemLayoutResult { fn compatible_with_containing_block_size(&self, containing_block: &ContainingBlock) -> bool { if containing_block.size.inline == self.containing_block_inline_size && (containing_block.size.block == self.containing_block_block_size || (!self.depends_on_block_constraints && !self.has_child_which_depends_on_block_constraints)) { return true; } #[cfg(feature = "tracing")] tracing::warn!( name: "NonReplaced stretch cache miss", cached_inline = ?self.containing_block_inline_size, cached_block = ?self.containing_block_block_size, required_inline = ?containing_block.size.inline, required_block = ?containing_block.size.block, depends_on_block_constraints = self.depends_on_block_constraints, has_child_which_depends_on_block_constraints = self.has_child_which_depends_on_block_constraints, ); false } fn compatible_with_containing_block_size_and_content_size( &self, containing_block: &ContainingBlock, size: LogicalVec2, ) -> bool { size == self.content_size && self.compatible_with_containing_block_size(containing_block) } } /// A data structure to hold all of the information about a flex item that has been placed /// into a flex line. This happens once the item is laid out and its line has been determined. struct FlexLineItem<'a> { /// The items that are placed in this line. item: FlexItem<'a>, /// The layout results of the initial layout pass for a flex line. These may be replaced /// if necessary due to the use of `align-content: stretch` or `align-self: stretch`. layout_result: FlexItemLayoutResult, /// The used main size of this item in its line. used_main_size: Au, } impl FlexLineItem<'_> { fn get_or_synthesize_baseline_with_cross_size(&self, cross_size: Au) -> Au { self.layout_result .baseline_relative_to_margin_box .unwrap_or_else(|| { self.item .synthesized_baseline_relative_to_margin_box(cross_size) }) } #[allow(clippy::too_many_arguments)] fn collect_fragment( mut self, initial_flex_layout: &InitialFlexLineLayout, item_used_size: FlexRelativeVec2, item_margin: FlexRelativeSides, item_main_interval: Au, final_line_cross_size: Au, shared_alignment_baseline: &Option, flex_context: &mut FlexContext, all_baselines: &mut Baselines, main_position_cursor: &mut Au, ) -> (BoxFragment, PositioningContext) { // https://drafts.csswg.org/css-flexbox/#algo-main-align // “Align the items along the main-axis” *main_position_cursor += item_margin.main_start + self.item.border.main_start + self.item.padding.main_start; let item_content_main_start_position = *main_position_cursor; *main_position_cursor += item_used_size.main + self.item.padding.main_end + self.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 = self.item.align_along_cross_axis( &item_margin, &item_used_size.cross, final_line_cross_size, self.layout_result .baseline_relative_to_margin_box .unwrap_or_default(), shared_alignment_baseline.unwrap_or_default(), flex_context.config.flex_wrap_is_reversed, ); let start_corner = FlexRelativeVec2 { main: item_content_main_start_position, cross: item_content_cross_start_position, }; // Need to collect both baselines from baseline participation and other baselines. let final_line_size = FlexRelativeVec2 { main: initial_flex_layout.line_size.main, cross: final_line_cross_size, }; let content_rect = flex_context.rect_to_flow_relative( final_line_size, FlexRelativeRect { start_corner, size: item_used_size, }, ); let margin = flex_context.sides_to_flow_relative(item_margin); let collapsed_margin = CollapsedBlockMargins::from_margin(&margin); if let Some(item_baseline) = self.layout_result.baseline_relative_to_margin_box.as_ref() { let item_baseline = *item_baseline + item_content_cross_start_position - self.item.border.cross_start - self.item.padding.cross_start - item_margin.cross_start; all_baselines.first.get_or_insert(item_baseline); all_baselines.last = Some(item_baseline); } let mut fragment_info = self.item.box_.base_fragment_info(); fragment_info.flags.insert(FragmentFlags::IS_FLEX_ITEM); if self.item.depends_on_block_constraints { fragment_info.flags.insert( FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM, ); } let flags = fragment_info.flags; let containing_block = flex_context.containing_block; let container_writing_mode = containing_block.style.writing_mode; let style = self.item.box_.style(); let mut fragment = BoxFragment::new( fragment_info, style.clone(), self.layout_result.fragments, content_rect.as_physical(Some(flex_context.containing_block)), flex_context .sides_to_flow_relative(self.item.padding) .to_physical(container_writing_mode), flex_context .sides_to_flow_relative(self.item.border) .to_physical(container_writing_mode), margin.to_physical(container_writing_mode), None, /* clearance */ collapsed_margin, ); // If this flex item establishes a containing block for absolutely-positioned // descendants, then lay out any relevant absolutely-positioned children. This // will remove those children from `self.positioning_context`. if style.establishes_containing_block_for_absolute_descendants(flags) { self.layout_result .positioning_context .layout_collected_children(flex_context.layout_context, &mut fragment); } if style.clone_position() == Position::Relative { fragment.content_rect.origin += relative_adjustement(style, containing_block) .to_physical_size(containing_block.style.writing_mode) } (fragment, self.layout_result.positioning_context) } } /// Once the final cross size of a line is known, the line can go through their final /// layout and this the return value. See [`InitialFlexLineLayout::finish_with_final_cross_size`]. 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." item_fragments: Vec<(BoxFragment, PositioningContext)>, /// The 'shared alignment baseline' of this flex line. This is the baseline used for /// baseline-aligned items if there are any, otherwise `None`. shared_alignment_baseline: Option, /// This is the baseline of the first and last items with compatible writing mode, regardless of /// whether they particpate in baseline alignement. This is used as a fallback baseline for the /// container, if there are no items participating in baseline alignment in the first or last /// flex lines. all_baselines: Baselines, } impl FlexContainerConfig { /// Whether an item with an `auto` preferred cross size needs to be stretched /// to fill the flex container. /// fn item_with_auto_cross_size_stretches_to_container_size( &self, item_style: &ComputedValues, item_margin: &FlexRelativeSides, ) -> bool { self.container_is_single_line && item_with_auto_cross_size_stretches_to_line_size( AlignItems(self.resolve_align_self_for_child(item_style)), item_margin, ) } fn resolve_reversable_flex_alignment( &self, align_flags: AlignFlags, reversed: bool, ) -> AlignFlags { match (align_flags.value(), reversed) { (AlignFlags::FLEX_START, false) => AlignFlags::START | align_flags.flags(), (AlignFlags::FLEX_START, true) => AlignFlags::END | align_flags.flags(), (AlignFlags::FLEX_END, false) => AlignFlags::END | align_flags.flags(), (AlignFlags::FLEX_END, true) => AlignFlags::START | align_flags.flags(), (_, _) => align_flags, } } fn resolve_align_self_for_child(&self, child_style: &ComputedValues) -> AlignFlags { self.resolve_reversable_flex_alignment( child_style .resolve_align_self(self.align_items, AlignItems(AlignFlags::STRETCH)) .0, self.flex_wrap_is_reversed, ) } fn resolve_justify_content_for_child(&self) -> AlignFlags { self.resolve_reversable_flex_alignment( self.justify_content.0.primary(), self.flex_direction_is_reversed, ) } } impl FlexContext<'_> { fn vec2_to_flex_relative(&self, x: LogicalVec2) -> FlexRelativeVec2 { self.config.flex_axis.vec2_to_flex_relative(x) } fn sides_to_flex_relative(&self, x: LogicalSides) -> FlexRelativeSides { self.config .main_start_cross_start_sides_are .sides_to_flex_relative(x) } fn sides_to_flow_relative(&self, x: FlexRelativeSides) -> LogicalSides { self.config .main_start_cross_start_sides_are .sides_to_flow_relative(x) } fn rect_to_flow_relative( &self, base_rect_size: FlexRelativeVec2, rect: FlexRelativeRect, ) -> LogicalRect { super::geom::rect_to_flow_relative( self.config.flex_axis, self.config.main_start_cross_start_sides_are, base_rect_size, rect, ) } } #[derive(Debug, Default)] struct DesiredFlexFractionAndGrowOrShrinkFactor { desired_flex_fraction: f32, flex_grow_or_shrink_factor: f32, } #[derive(Default)] struct FlexItemBoxInlineContentSizesInfo { outer_flex_base_size: Au, content_min_size_no_auto: FlexRelativeVec2, content_max_size: FlexRelativeVec2>, pbm_auto_is_zero: FlexRelativeVec2, min_flex_factors: DesiredFlexFractionAndGrowOrShrinkFactor, max_flex_factors: DesiredFlexFractionAndGrowOrShrinkFactor, min_content_main_size_for_multiline_container: Au, depends_on_block_constraints: bool, } impl ComputeInlineContentSizes for FlexContainer { #[cfg_attr( feature = "tracing", tracing::instrument( name = "FlexContainer::compute_inline_content_sizes", skip_all, fields(servo_profiling = true), level = "trace", ) )] fn compute_inline_content_sizes( &self, layout_context: &LayoutContext, constraint_space: &ConstraintSpace, ) -> InlineContentSizesResult { match self.config.flex_axis { FlexAxis::Row => { self.main_content_sizes(layout_context, &constraint_space.into(), || { unreachable!( "Unexpected FlexContext query during row flex intrinsic size calculation." ) }) }, FlexAxis::Column => self.cross_content_sizes(layout_context, &constraint_space.into()), } } } impl FlexContainer { fn cross_content_sizes( &self, layout_context: &LayoutContext, containing_block_for_children: &IndefiniteContainingBlock, ) -> InlineContentSizesResult { // assert_eq!( self.config.flex_axis, FlexAxis::Column, "The cross axis should be the inline one" ); let mut sizes = ContentSizes::zero(); let mut depends_on_block_constraints = false; for kid in self.children.iter() { let kid = &*kid.borrow(); match kid { FlexLevelBox::FlexItem(item) => { // TODO: For the max-content size we should distribute items into // columns, and sum the column sizes and gaps. // TODO: Use the proper automatic minimum size. let ifc = &item.independent_formatting_context; let result = ifc.outer_inline_content_sizes( layout_context, containing_block_for_children, &LogicalVec2::zero(), false, /* auto_block_size_stretches_to_containing_block */ ); sizes.max_assign(result.sizes); depends_on_block_constraints |= result.depends_on_block_constraints; }, FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => {}, } } InlineContentSizesResult { sizes, depends_on_block_constraints, } } fn main_content_sizes<'a>( &self, layout_context: &LayoutContext, containing_block_for_children: &IndefiniteContainingBlock, flex_context_getter: impl Fn() -> &'a FlexContext<'a>, ) -> InlineContentSizesResult { // - TODO: calculate intrinsic cross sizes when container is a column // (and check for ‘writing-mode’?) // - TODO: Collapsed flex items need to be skipped for intrinsic size calculation. // // > It is calculated, considering only non-collapsed flex items, by: // > 1. For each flex item, subtract its outer flex base size from its max-content // > contribution size. let mut chosen_max_flex_fraction = f32::NEG_INFINITY; let mut chosen_min_flex_fraction = f32::NEG_INFINITY; let mut sum_of_flex_grow_factors = 0.0; let mut sum_of_flex_shrink_factors = 0.0; let mut item_infos = vec![]; let container_is_horizontal = self.style.writing_mode.is_horizontal(); for kid in self.children.iter() { let kid = &*kid.borrow(); match kid { FlexLevelBox::FlexItem(item) => { sum_of_flex_grow_factors += item.style().get_position().flex_grow.0; sum_of_flex_shrink_factors += item.style().get_position().flex_shrink.0; let info = item.main_content_size_info( layout_context, containing_block_for_children, container_is_horizontal, &self.config, &flex_context_getter, ); // > 2. Place all flex items into lines of infinite length. Within // > each line, find the greatest (most positive) desired flex // > fraction among all the flex items. This is the line’s chosen flex // > fraction. chosen_max_flex_fraction = chosen_max_flex_fraction.max(info.max_flex_factors.desired_flex_fraction); chosen_min_flex_fraction = chosen_min_flex_fraction.max(info.min_flex_factors.desired_flex_fraction); item_infos.push(info) }, FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_) => {}, } } let normalize_flex_fraction = |chosen_flex_fraction| { if chosen_flex_fraction > 0.0 && sum_of_flex_grow_factors < 1.0 { // > 3. If the chosen flex fraction is positive, and the sum of the line’s // > flex grow factors is less than 1, > divide the chosen flex fraction by that // > sum. chosen_flex_fraction / sum_of_flex_grow_factors } else if chosen_flex_fraction < 0.0 && sum_of_flex_shrink_factors < 1.0 { // > If the chosen flex fraction is negative, and the sum of the line’s flex // > shrink factors is less than 1, > multiply the chosen flex fraction by that // > sum. chosen_flex_fraction * sum_of_flex_shrink_factors } else { chosen_flex_fraction } }; let chosen_min_flex_fraction = normalize_flex_fraction(chosen_min_flex_fraction); let chosen_max_flex_fraction = normalize_flex_fraction(chosen_max_flex_fraction); let main_gap = match self.config.flex_axis { FlexAxis::Row => self.style.clone_column_gap(), FlexAxis::Column => self.style.clone_row_gap(), }; let main_gap = match main_gap { LengthPercentageOrNormal::LengthPercentage(length_percentage) => { length_percentage.to_used_value(Au::zero()) }, LengthPercentageOrNormal::Normal => Au::zero(), }; let extra_space_from_main_gap = main_gap * (item_infos.len() as i32 - 1); let mut container_max_content_size = extra_space_from_main_gap; let mut container_min_content_size = if self.config.flex_wrap == FlexWrap::Nowrap { extra_space_from_main_gap } else { Au::zero() }; let mut container_depends_on_block_constraints = false; for FlexItemBoxInlineContentSizesInfo { outer_flex_base_size, content_min_size_no_auto, content_max_size, pbm_auto_is_zero, min_flex_factors, max_flex_factors, min_content_main_size_for_multiline_container, depends_on_block_constraints, } in item_infos.iter() { // > 4. Add each item’s flex base size to the product of its flex grow factor (scaled flex shrink // > factor, if shrinking) and the chosen flex fraction, then clamp that result by the max main size // > floored by the min main size. let outer_min_main_size = content_min_size_no_auto.main + pbm_auto_is_zero.main; let outer_max_main_size = content_max_size.main.map(|v| v + pbm_auto_is_zero.main); // > 5. The flex container’s max-content size is the largest sum (among all the lines) of the // > afore-calculated sizes of all items within a single line. container_max_content_size += (*outer_flex_base_size + Au::from_f32_px( max_flex_factors.flex_grow_or_shrink_factor * chosen_max_flex_fraction, )) .clamp_between_extremums(outer_min_main_size, outer_max_main_size); // > The min-content main size of a single-line flex container is calculated // > identically to the max-content main size, except that the flex items’ // > min-content contributions are used instead of their max-content contributions. // // > However, for a multi-line container, the min-content main size is simply the // > largest min-content contribution of all the non-collapsed flex items in the // > flex container. For this purpose, each item’s contribution is capped by the // > item’s flex base size if the item is not growable, floored by the item’s flex // > base size if the item is not shrinkable, and then further clamped by the item’s // > min and max main sizes. if self.config.flex_wrap == FlexWrap::Nowrap { container_min_content_size += (*outer_flex_base_size + Au::from_f32_px( min_flex_factors.flex_grow_or_shrink_factor * chosen_min_flex_fraction, )) .clamp_between_extremums(outer_min_main_size, outer_max_main_size); } else { container_min_content_size .max_assign(*min_content_main_size_for_multiline_container); } container_depends_on_block_constraints |= depends_on_block_constraints; } InlineContentSizesResult { sizes: ContentSizes { min_content: container_min_content_size, max_content: container_max_content_size, }, depends_on_block_constraints: container_depends_on_block_constraints, } } /// #[cfg_attr( feature = "tracing", tracing::instrument( name = "FlexContainer::layout", skip_all, fields(servo_profiling = true, self_address = self as *const _ as usize), level = "trace", ) )] pub(crate) fn layout( &self, layout_context: &LayoutContext, positioning_context: &mut PositioningContext, containing_block: &ContainingBlock, ) -> IndependentLayout { let depends_on_block_constraints = self.config.flex_direction == FlexDirection::Column; let mut flex_context = FlexContext { config: self.config.clone(), layout_context, positioning_context, containing_block, // https://drafts.csswg.org/css-flexbox/#definite-sizes container_inner_size_constraint: self.config.flex_axis.vec2_to_flex_relative( LogicalVec2 { inline: SizeConstraint::Definite(containing_block.size.inline), block: containing_block.size.block, }, ), }; // “Determine the main size of the flex container” // https://drafts.csswg.org/css-flexbox/#algo-main-container let container_main_size = match self.config.flex_axis { FlexAxis::Row => containing_block.size.inline, FlexAxis::Column => match containing_block.size.block { SizeConstraint::Definite(size) => size, SizeConstraint::MinMax(min, max) => self .main_content_sizes(layout_context, &containing_block.into(), || &flex_context) .sizes .max_content .clamp_between_extremums(min, max), }, }; // Actual length may be less, but we guess that usually not by a lot let mut flex_items = Vec::with_capacity(self.children.len()); // Absolutely-positioned children of the flex container may be interleaved // with flex items. We need to preserve their relative order for correct painting order, // which is the order of `Fragment`s in this function’s return value. // // Example: // absolutely_positioned_items_with_original_order = [Some(item), Some(item), None, Some(item), None] // flex_items = [item, item] let absolutely_positioned_items_with_original_order = self .children .iter() .map(|arcrefcell| { let borrowed = arcrefcell.borrow(); match &*borrowed { FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(absolutely_positioned) => { FlexContent::AbsolutelyPositionedBox(absolutely_positioned.clone()) }, FlexLevelBox::FlexItem(_) => { let item = AtomicRef::map(borrowed, |child| match child { FlexLevelBox::FlexItem(item) => item, _ => unreachable!(), }); flex_items.push(item); FlexContent::FlexItemPlaceholder }, } }) .collect::>(); let flex_item_boxes = flex_items.iter().map(|child| &**child); let flex_items = flex_item_boxes .map(|flex_item_box| FlexItem::new(&flex_context, flex_item_box)) .collect::>(); let row_gap = self.style.clone_row_gap(); let column_gap = self.style.clone_column_gap(); let (cross_gap, main_gap) = match flex_context.config.flex_axis { FlexAxis::Row => (row_gap, column_gap), FlexAxis::Column => (column_gap, row_gap), }; let cross_gap = match cross_gap { LengthPercentageOrNormal::LengthPercentage(length_percent) => length_percent .maybe_to_used_value( flex_context .container_inner_size_constraint .cross .to_definite(), ) .unwrap_or_default(), LengthPercentageOrNormal::Normal => Au::zero(), }; let main_gap = match main_gap { LengthPercentageOrNormal::LengthPercentage(length_percent) => length_percent .maybe_to_used_value( flex_context .container_inner_size_constraint .main .to_definite(), ) .unwrap_or_default(), LengthPercentageOrNormal::Normal => Au::zero(), }; // “Resolve the flexible lengths of all the flex items to find their *used main size*.” // https://drafts.csswg.org/css-flexbox/#algo-flex let initial_line_layouts = do_initial_flex_line_layout( &mut flex_context, container_main_size, flex_items, main_gap, ); 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 let container_cross_size = match flex_context.container_inner_size_constraint.cross { SizeConstraint::Definite(cross_size) => cross_size, SizeConstraint::MinMax(min, max) => { content_cross_size.clamp_between_extremums(min, max) }, }; let container_size = FlexRelativeVec2 { main: container_main_size, cross: container_cross_size, }; let content_block_size = flex_context .config .flex_axis .vec2_to_flow_relative(container_size) .block; let mut remaining_free_cross_space = match flex_context.container_inner_size_constraint.cross { SizeConstraint::Definite(cross_size) => cross_size - content_cross_size, _ => Au::zero(), }; // 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 num_lines = initial_line_layouts.len(); let resolved_align_content: AlignFlags = { // Computed value from the style system let align_content_style = flex_context.config.align_content.0.primary(); let mut is_safe = align_content_style.flags() == AlignFlags::SAFE; // From https://drafts.csswg.org/css-align/#distribution-flex // > `normal` behaves as `stretch`. let mut resolved_align_content = match align_content_style.value() { AlignFlags::NORMAL => AlignFlags::STRETCH, align_content => align_content, }; // 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 remaining_free_cross_space <= Au::zero() => true, AlignFlags::STRETCH => num_lines < 1, AlignFlags::SPACE_BETWEEN | AlignFlags::SPACE_AROUND | AlignFlags::SPACE_EVENLY => { num_lines < 2 }, _ => false, }; 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), AlignFlags::SPACE_AROUND => (AlignFlags::CENTER, true), AlignFlags::SPACE_EVENLY => (AlignFlags::CENTER, true), _ => (resolved_align_content, is_safe), } }; // 2. If free space is negative the "safe" alignment variants all fallback to Start alignment if remaining_free_cross_space <= Au::zero() && is_safe { resolved_align_content = AlignFlags::START; } resolved_align_content }; // Implement "unsafe" alignment. "safe" alignment is handled by the fallback process above. let flex_wrap_is_reversed = flex_context.config.flex_wrap_is_reversed; let resolved_align_content = self .config .resolve_reversable_flex_alignment(resolved_align_content, flex_wrap_is_reversed); let mut cross_start_position_cursor = match resolved_align_content { AlignFlags::START if flex_wrap_is_reversed => remaining_free_cross_space, AlignFlags::START => Au::zero(), AlignFlags::END if flex_wrap_is_reversed => Au::zero(), AlignFlags::END => remaining_free_cross_space, AlignFlags::CENTER => remaining_free_cross_space / 2, AlignFlags::STRETCH => Au::zero(), AlignFlags::SPACE_BETWEEN => Au::zero(), AlignFlags::SPACE_AROUND => remaining_free_cross_space / num_lines as i32 / 2, AlignFlags::SPACE_EVENLY => remaining_free_cross_space / (num_lines as i32 + 1), // TODO: Implement all alignments. Note: not all alignment values are valid for content distribution _ => Au::zero(), }; let inline_axis_is_main_axis = self.config.flex_axis == FlexAxis::Row; let mut baseline_alignment_participating_baselines = Baselines::default(); let mut all_baselines = Baselines::default(); let flex_item_fragments: Vec<_> = initial_line_layouts .into_iter() .enumerate() .flat_map(|(index, initial_line_layout)| { // We call `allocate_free_cross_space_for_flex_line` for each line to avoid having // leftover space when the number of lines doesn't evenly divide the total free space, // considering the precision of app units. let (space_to_add_to_line, space_to_add_after_line) = allocate_free_cross_space_for_flex_line( resolved_align_content, remaining_free_cross_space, (num_lines - index) as i32, ); remaining_free_cross_space -= space_to_add_to_line + space_to_add_after_line; let final_line_cross_size = initial_line_layout.line_size.cross + space_to_add_to_line; let mut final_line_layout = initial_line_layout.finish_with_final_cross_size( &mut flex_context, main_gap, final_line_cross_size, ); let line_cross_start_position = cross_start_position_cursor; cross_start_position_cursor = line_cross_start_position + final_line_cross_size + space_to_add_after_line + cross_gap; let flow_relative_line_position = match (self.config.flex_axis, flex_wrap_is_reversed) { (FlexAxis::Row, false) => LogicalVec2 { block: line_cross_start_position, inline: Au::zero(), }, (FlexAxis::Row, true) => LogicalVec2 { block: container_cross_size - line_cross_start_position - final_line_layout.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 - final_line_cross_size, }, }; if inline_axis_is_main_axis { let line_shared_alignment_baseline = final_line_layout .shared_alignment_baseline .map(|baseline| baseline + flow_relative_line_position.block); if index == 0 { baseline_alignment_participating_baselines.first = line_shared_alignment_baseline; } if index == num_lines - 1 { baseline_alignment_participating_baselines.last = line_shared_alignment_baseline; } } let line_all_baselines = final_line_layout .all_baselines .offset(flow_relative_line_position.block); if index == 0 { all_baselines.first = line_all_baselines.first; } if index == num_lines - 1 { all_baselines.last = line_all_baselines.last; } let physical_line_position = flow_relative_line_position.to_physical_size(self.style.writing_mode); for (fragment, _) in &mut final_line_layout.item_fragments { fragment.content_rect.origin += physical_line_position; } final_line_layout.item_fragments }) .collect(); let mut flex_item_fragments = flex_item_fragments.into_iter(); let fragments = absolutely_positioned_items_with_original_order .into_iter() .map(|child_as_abspos| match child_as_abspos { FlexContent::AbsolutelyPositionedBox(absolutely_positioned_box) => self .create_absolutely_positioned_flex_child_fragment( absolutely_positioned_box, containing_block, container_size, positioning_context, ), FlexContent::FlexItemPlaceholder => { // The `flex_item_fragments` iterator yields one fragment // per flex item, in the original order. let (fragment, mut child_positioning_context) = flex_item_fragments.next().unwrap(); let fragment = Fragment::Box(ArcRefCell::new(fragment)); child_positioning_context.adjust_static_position_of_hoisted_fragments( &fragment, PositioningContextLength::zero(), ); positioning_context.append(child_positioning_context); fragment }, }) .collect::>(); // There should be no more flex items assert!(flex_item_fragments.next().is_none()); let baselines = Baselines { first: baseline_alignment_participating_baselines .first .or(all_baselines.first), last: baseline_alignment_participating_baselines .last .or(all_baselines.last), }; IndependentLayout { fragments, content_block_size, content_inline_size_for_table: None, baselines, depends_on_block_constraints, specific_layout_info: None, } } /// Create a absolutely positioned flex child fragment, using the rules the /// specification dictates. This should take into account the alignment and /// justification values of the container and the child to position it within a /// "inset-modified containing block," which may be either the "static-position /// rectangle" that's calculated below or a modified version of the absolute's /// containing block adjusted by the insets specified in the item's style. /// /// From : /// > The cross-axis edges of the static-position rectangle of an /// > absolutely-positioned child of a flex container are the content edges of the /// > flex container The main-axis edges of the static-position rectangle are where /// > the margin edges of the child would be positioned if it were the sole flex item /// > in the flex container, assuming both the child and the flex container were /// > fixed-size boxes of their used size. (For this purpose, auto margins are /// > treated as zero.) fn create_absolutely_positioned_flex_child_fragment( &self, absolutely_positioned_box: ArcRefCell, containing_block: &ContainingBlock, container_size: FlexRelativeVec2, positioning_context: &mut PositioningContext, ) -> Fragment { let alignment = { let fragment = absolutely_positioned_box.borrow(); let make_flex_only_values_directional_for_absolutes = |value: AlignFlags, reversed: bool| match (value.value(), reversed) { (AlignFlags::NORMAL | AlignFlags::AUTO | AlignFlags::STRETCH, true) => { AlignFlags::END | AlignFlags::SAFE }, (AlignFlags::STRETCH, false) => AlignFlags::START | AlignFlags::SAFE, (AlignFlags::SPACE_BETWEEN, false) => AlignFlags::START | AlignFlags::SAFE, (AlignFlags::SPACE_BETWEEN, true) => AlignFlags::END | AlignFlags::SAFE, _ => value, }; let cross = make_flex_only_values_directional_for_absolutes( self.config .resolve_align_self_for_child(fragment.context.style()), self.config.flex_wrap_is_reversed, ); let main = make_flex_only_values_directional_for_absolutes( self.config.resolve_justify_content_for_child(), self.config.flex_direction_is_reversed, ); FlexRelativeVec2 { cross, main } }; let logical_alignment = self.config.flex_axis.vec2_to_flow_relative(alignment); let static_position_rect = LogicalRect { start_corner: LogicalVec2::zero(), size: self.config.flex_axis.vec2_to_flow_relative(container_size), } .as_physical(Some(containing_block)); let hoisted_box = AbsolutelyPositionedBox::to_hoisted( absolutely_positioned_box, static_position_rect, logical_alignment, self.config.writing_mode, ); let hoisted_fragment = hoisted_box.fragment.clone(); positioning_context.push(hoisted_box); Fragment::AbsoluteOrFixedPositioned(hoisted_fragment) } #[inline] pub(crate) fn layout_style(&self) -> LayoutStyle { LayoutStyle::Default(&self.style) } } /// Align all flex lines per `align-content` according to /// . Returns the space to add to /// each line or the space to add after each line. fn allocate_free_cross_space_for_flex_line( resolved_align_content: AlignFlags, remaining_free_cross_space: Au, remaining_line_count: i32, ) -> (Au, Au) { if remaining_free_cross_space == Au::zero() { return (Au::zero(), Au::zero()); } match resolved_align_content { AlignFlags::START => (Au::zero(), Au::zero()), AlignFlags::FLEX_START => (Au::zero(), Au::zero()), AlignFlags::END => (Au::zero(), Au::zero()), AlignFlags::FLEX_END => (Au::zero(), Au::zero()), AlignFlags::CENTER => (Au::zero(), Au::zero()), AlignFlags::STRETCH => ( remaining_free_cross_space / remaining_line_count, Au::zero(), ), AlignFlags::SPACE_BETWEEN => { if remaining_line_count > 1 { ( Au::zero(), remaining_free_cross_space / (remaining_line_count - 1), ) } else { (Au::zero(), Au::zero()) } }, AlignFlags::SPACE_AROUND => ( Au::zero(), remaining_free_cross_space / remaining_line_count, ), AlignFlags::SPACE_EVENLY => ( Au::zero(), remaining_free_cross_space / (remaining_line_count + 1), ), // TODO: Implement all alignments. Note: not all alignment values are valid for content distribution _ => (Au::zero(), Au::zero()), } } impl<'a> FlexItem<'a> { fn new(flex_context: &FlexContext, box_: &'a FlexItemBox) -> Self { let config = &flex_context.config; let containing_block = IndefiniteContainingBlock::from(flex_context.containing_block); let parent_writing_mode = containing_block.writing_mode; let item_writing_mode = box_.style().writing_mode; let container_is_horizontal = parent_writing_mode.is_horizontal(); let item_is_horizontal = item_writing_mode.is_horizontal(); let cross_axis_is_item_block_axis = cross_axis_is_item_block_axis( container_is_horizontal, item_is_horizontal, config.flex_axis, ); let ContentBoxSizesAndPBM { content_box_sizes, pbm, depends_on_block_constraints, } = box_ .independent_formatting_context .layout_style() .content_box_sizes_and_padding_border_margin(&containing_block); let content_box_size = content_box_sizes.map(|size| size.preferred); // TODO(#32853): handle size keywords. let content_min_box_size = content_box_sizes.map(|size| size.min.to_auto_or()); let content_max_box_size = content_box_sizes.map(|size| size.max.to_numeric()); let preferred_aspect_ratio = box_ .independent_formatting_context .preferred_aspect_ratio(&pbm.padding_border_sums); let margin_auto_is_zero = flex_context.sides_to_flex_relative(pbm.margin.auto_is(Au::zero)); let padding = flex_context.sides_to_flex_relative(pbm.padding); let border = flex_context.sides_to_flex_relative(pbm.border); let margin = flex_context.sides_to_flex_relative(pbm.margin); let padding_border = padding.sum_by_axis() + border.sum_by_axis(); let pbm_auto_is_zero = FlexRelativeVec2 { main: padding_border.main, cross: padding_border.cross, } + margin_auto_is_zero.sum_by_axis(); let item_with_auto_cross_size_stretches_to_container_size = config.item_with_auto_cross_size_stretches_to_container_size(box_.style(), &margin); let flex_relative_content_box_size = flex_context.vec2_to_flex_relative(content_box_size); let flex_relative_content_max_size = flex_context.vec2_to_flex_relative(content_max_box_size); let flex_relative_content_min_size = flex_context.vec2_to_flex_relative(content_min_box_size); let flex_relative_content_min_size = FlexRelativeVec2 { main: flex_relative_content_min_size.main.auto_is(|| { box_.automatic_min_size( flex_context.layout_context, flex_context.vec2_to_flex_relative(containing_block.size), cross_axis_is_item_block_axis, flex_relative_content_box_size, flex_relative_content_min_size, flex_relative_content_max_size, preferred_aspect_ratio, &pbm_auto_is_zero, item_with_auto_cross_size_stretches_to_container_size, |item| { let min_size_auto_is_zero = content_min_box_size.auto_is(Au::zero); item.layout_for_block_content_size( flex_context, &pbm, content_box_size, min_size_auto_is_zero, content_max_box_size, preferred_aspect_ratio, item_with_auto_cross_size_stretches_to_container_size, IntrinsicSizingMode::Size, ) }, ) }), cross: flex_relative_content_min_size.cross.auto_is(Au::zero), }; let align_self = AlignItems(config.resolve_align_self_for_child(box_.style())); let (flex_base_size, flex_base_size_is_definite) = box_.flex_base_size( flex_context.layout_context, flex_context .container_inner_size_constraint .map(|size| size.to_definite()), cross_axis_is_item_block_axis, flex_relative_content_box_size, flex_relative_content_min_size, flex_relative_content_max_size, preferred_aspect_ratio, padding_border, &pbm_auto_is_zero, item_with_auto_cross_size_stretches_to_container_size, |item| { let min_size = config .flex_axis .vec2_to_flow_relative(flex_relative_content_min_size); item.layout_for_block_content_size( flex_context, &pbm, content_box_size, min_size, content_max_box_size, preferred_aspect_ratio, item_with_auto_cross_size_stretches_to_container_size, IntrinsicSizingMode::Size, ) }, ); let hypothetical_main_size = flex_base_size.clamp_between_extremums( flex_relative_content_min_size.main, flex_relative_content_max_size.main, ); let margin: FlexRelativeSides = flex_context.sides_to_flex_relative(pbm.margin); Self { box_, content_cross_size: flex_relative_content_box_size.cross, content_min_size: flex_relative_content_min_size, content_max_size: flex_relative_content_max_size, padding, border, margin, pbm_auto_is_zero, flex_base_size, flex_base_size_is_definite, hypothetical_main_size, align_self, depends_on_block_constraints, preferred_aspect_ratio, } } fn stretches_to_line(&self) -> bool { // Note this returns false for a `stretch` size, because that stretches to the // containing block, not to the flex line. // To be discussed in https://github.com/w3c/csswg-drafts/issues/11784. self.content_cross_size.is_initial() && item_with_auto_cross_size_stretches_to_line_size(self.align_self, &self.margin) } } fn cross_axis_is_item_block_axis( container_is_horizontal: bool, item_is_horizontal: bool, flex_axis: FlexAxis, ) -> bool { let item_is_orthogonal = item_is_horizontal != container_is_horizontal; let container_is_row = flex_axis == FlexAxis::Row; container_is_row ^ item_is_orthogonal } /// Whether an item with an `auto` preferred cross size will stretched to fill the cross size of its flex line. /// fn item_with_auto_cross_size_stretches_to_line_size( align_self: AlignItems, margin: &FlexRelativeSides, ) -> bool { align_self.0.value() == AlignFlags::STRETCH && !margin.cross_start.is_auto() && !margin.cross_end.is_auto() } /// “Collect flex items into flex lines” /// fn do_initial_flex_line_layout<'items>( flex_context: &mut FlexContext, container_main_size: Au, mut items: Vec>, main_gap: Au, ) -> Vec> { let construct_line = |(items, outer_hypothetical_main_size)| { InitialFlexLineLayout::new( flex_context, items, outer_hypothetical_main_size, container_main_size, main_gap, ) }; if flex_context.config.container_is_single_line { let outer_hypothetical_main_sizes_sum = items .iter() .map(|item| item.hypothetical_main_size + item.pbm_auto_is_zero.main) .sum(); return vec![construct_line((items, outer_hypothetical_main_sizes_sum))]; } let mut lines = Vec::new(); let mut line_size_so_far = Au::zero(); let mut line_so_far_is_empty = true; let mut index = 0; while let Some(item) = items.get(index) { let item_size = item.hypothetical_main_size + item.pbm_auto_is_zero.main; let mut line_size_would_be = line_size_so_far + item_size; if !line_so_far_is_empty { line_size_would_be += main_gap; } let item_fits = line_size_would_be <= container_main_size; if item_fits || line_so_far_is_empty { line_size_so_far = line_size_would_be; line_so_far_is_empty = false; index += 1; continue; } // We found something that doesn’t fit. This line ends *before* this item. let remaining = items.split_off(index); lines.push((items, line_size_so_far)); items = remaining; // The next line has this item. line_size_so_far = item_size; index = 1; } // We didn't reach the end of the last line, so add all remaining items there. lines.push((items, line_size_so_far)); lines.par_drain(..).map(construct_line).collect() } /// The result of splitting the flex items into lines using their intrinsic sizes and doing an /// initial layout of each item. A final layout still needs to happen after this is produced to /// handle stretching. struct InitialFlexLineLayout<'a> { /// The items that are placed in this line. items: Vec>, /// The initial size of this flex line, not taking into account `align-content: stretch`. line_size: FlexRelativeVec2, /// The free space available to this line after the initial layout. free_space_in_main_axis: Au, } impl InitialFlexLineLayout<'_> { fn new<'items>( flex_context: &FlexContext, items: Vec>, outer_hypothetical_main_sizes_sum: Au, container_main_size: Au, main_gap: Au, ) -> InitialFlexLineLayout<'items> { let item_count = items.len(); let (item_used_main_sizes, free_space_in_main_axis) = 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 layout_results = items .par_iter() .zip(&item_used_main_sizes) .map(|(item, used_main_size)| { item.layout(*used_main_size, flex_context, None, None) .unwrap() }) .collect::>(); let items: Vec<_> = izip!( items.into_iter(), layout_results.into_iter(), item_used_main_sizes.into_iter() ) .map(|(item, layout_result, used_main_size)| FlexLineItem { item, layout_result, used_main_size, }) .collect(); // https://drafts.csswg.org/css-flexbox/#algo-cross-line let line_cross_size = Self::cross_size(&items, flex_context); let line_size = FlexRelativeVec2 { main: container_main_size, cross: line_cross_size, }; InitialFlexLineLayout { items, line_size, free_space_in_main_axis, } } /// Return the *main size* of each item, and the line’s remainaing free space /// fn resolve_flexible_lengths<'items>( items: &'items [FlexItem<'items>], outer_hypothetical_main_sizes_sum: Au, container_main_size: Au, ) -> (Vec, Au) { struct FlexibleLengthResolutionItem<'items> { item: &'items FlexItem<'items>, frozen: Cell, target_main_size: Cell, flex_factor: f32, } // > 1. Determine the used flex factor. Sum the outer hypothetical main sizes of all // > items on the line. If the sum is less than the flex container’s inner main // > size, use the flex grow factor for the rest of this algorithm; otherwise, use // > the flex shrink factor. let grow = outer_hypothetical_main_sizes_sum < container_main_size; let mut frozen_count = 0; let items: Vec<_> = items .iter() .map(|item| { // > 2. Each item in the flex line has a target main size, initially set to its // > flex base size. Each item is initially unfrozen and may become frozen. let target_main_size = Cell::new(item.flex_base_size); // > 3. Size inflexible items. Freeze, setting its target main size to its hypothetical main size… // > - any item that has a flex factor of zero // > - if using the flex grow factor: any item that has a flex base size // > greater than its hypothetical main size // > - if using the flex shrink factor: any item that has a flex base size // > smaller than its hypothetical main size let flex_factor = if grow { item.box_.style().get_position().flex_grow.0 } else { item.box_.style().get_position().flex_shrink.0 }; let is_inflexible = flex_factor == 0. || if grow { item.flex_base_size > item.hypothetical_main_size } else { item.flex_base_size < item.hypothetical_main_size }; let frozen = Cell::new(false); if is_inflexible { frozen_count += 1; frozen.set(true); target_main_size.set(item.hypothetical_main_size); } FlexibleLengthResolutionItem { item, frozen, target_main_size, flex_factor, } }) .collect(); let unfrozen_items = || items.iter().filter(|item| !item.frozen.get()); let main_sizes = |items: Vec| { items .into_iter() .map(|item| item.target_main_size.get()) .collect() }; // https://drafts.csswg.org/css-flexbox/#initial-free-space // > 4. Calculate initial free space. Sum the outer sizes of all items on the line, and // > subtract this from the flex container’s inner main size. For frozen items, use // > their outer target main size; for other items, use their outer flex base size. let free_space = |all_items_frozen| { let items_size = items .iter() .map(|item| { item.item.pbm_auto_is_zero.main + if all_items_frozen || item.frozen.get() { item.target_main_size.get() } else { item.item.flex_base_size } }) .sum(); container_main_size - items_size }; let initial_free_space = free_space(false); loop { // https://drafts.csswg.org/css-flexbox/#remaining-free-space let mut remaining_free_space = free_space(false); // > 5. a. Check for flexible items. If all the flex items on the line are // > frozen, free space has been distributed; exit this loop. if frozen_count >= items.len() { return (main_sizes(items), remaining_free_space); } // > 5. b. Calculate the remaining free space as for initial free space, above. If the // > sum of the unfrozen flex items’ flex factors is less than one, multiply the // > initial free space by this sum. If the magnitude of this value is less than // > the magnitude of the remaining free space, use this as the remaining free // > space. let unfrozen_items_flex_factor_sum = unfrozen_items().map(|item| item.flex_factor).sum(); if unfrozen_items_flex_factor_sum < 1. { let multiplied = initial_free_space.scale_by(unfrozen_items_flex_factor_sum); if multiplied.abs() < remaining_free_space.abs() { remaining_free_space = multiplied } } // > 5. c. If the remaining free space is non-zero, distribute it proportional // to the flex factors: // // FIXME: is it a problem if floating point precision errors accumulate // and we get not-quite-zero remaining free space when we should get zero here? if remaining_free_space != Au::zero() { // > If using the flex grow factor: // > For every unfrozen item on the line, find the ratio of the item’s flex grow factor to // > the sum of the flex grow factors of all unfrozen items on the line. Set the item’s // > target main size to its flex base size plus a fraction of the remaining free space // > proportional to the ratio. if grow { for item in unfrozen_items() { let ratio = item.flex_factor / unfrozen_items_flex_factor_sum; item.target_main_size .set(item.item.flex_base_size + remaining_free_space.scale_by(ratio)); } // > If using the flex shrink factor // > For every unfrozen item on the line, multiply its flex shrink factor by its inner flex // > base size, and note this as its scaled flex shrink factor. Find the ratio of the // > item’s scaled flex shrink factor to the sum of the scaled flex shrink factors of all // > unfrozen items on the line. Set the item’s target main size to its flex base size // > minus a fraction of the absolute value of the remaining free space proportional to the // > ratio. Note this may result in a negative inner main size; it will be corrected in the // > next step. } else { // https://drafts.csswg.org/css-flexbox/#scaled-flex-shrink-factor let scaled_shrink_factor = |item: &FlexibleLengthResolutionItem| { item.item.flex_base_size.scale_by(item.flex_factor) }; let scaled_shrink_factors_sum: Au = unfrozen_items().map(scaled_shrink_factor).sum(); if scaled_shrink_factors_sum > Au::zero() { for item in unfrozen_items() { let ratio = scaled_shrink_factor(item).0 as f32 / scaled_shrink_factors_sum.0 as f32; item.target_main_size.set( item.item.flex_base_size - remaining_free_space.abs().scale_by(ratio), ); } } } } // > 5. d. Fix min/max violations. Clamp each non-frozen item’s target main size // > by its used min and max main sizes and floor its content-box size at zero. // > If the item’s target main size was made smaller by this, it’s a max // > violation. If the item’s target main size was made larger by this, it’s a // > min violation. let violation = |item: &FlexibleLengthResolutionItem| { let size = item.target_main_size.get(); let clamped = size.clamp_between_extremums( item.item.content_min_size.main, item.item.content_max_size.main, ); clamped - size }; // > 5. e. Freeze over-flexed items. The total violation is the sum of the // > adjustments from the previous step ∑(clamped size - unclamped size). If the // > total violation is: // > - Zero: Freeze all items. // > - Positive: Freeze all the items with min violations. // > - Negative: Freeze all the items with max violations. let total_violation: Au = unfrozen_items().map(violation).sum(); match total_violation.cmp(&Au::zero()) { Ordering::Equal => { // “Freeze all items.” // Return instead, as that’s what the next loop iteration would do. let remaining_free_space = free_space(true); return (main_sizes(items), remaining_free_space); }, Ordering::Greater => { // “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 in items.iter() { if violation(item) > Au::zero() { item.target_main_size.set(item.item.content_min_size.main); item.frozen.set(true); frozen_count += 1; } } }, Ordering::Less => { // Negative total violation // “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 in items.iter() { if violation(item) < Au::zero() { let Some(max_size) = item.item.content_max_size.main else { unreachable!() }; item.target_main_size.set(max_size); item.frozen.set(true); frozen_count += 1; } } }, } } } /// fn cross_size<'items>(items: &'items [FlexLineItem<'items>], flex_context: &FlexContext) -> Au { if flex_context.config.container_is_single_line { if let SizeConstraint::Definite(size) = flex_context.container_inner_size_constraint.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 in items.iter() { // TODO: check inline-axis is parallel to main axis, check no auto cross margins if matches!( item.item.align_self.0.value(), AlignFlags::BASELINE | AlignFlags::LAST_BASELINE ) { let baseline = item.get_or_synthesize_baseline_with_cross_size( item.layout_result.hypothetical_cross_size, ); let hypothetical_margin_box_cross_size = item.layout_result.hypothetical_cross_size + item.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.layout_result.hypothetical_cross_size + item.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); match flex_context.container_inner_size_constraint.cross { SizeConstraint::MinMax(min, max) if flex_context.config.container_is_single_line => { largest.clamp_between_extremums(min, max) }, _ => 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.item.margin.main_start.is_auto() as u32 + item.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 in self.items.iter_mut() { let stretches = item.item.stretches_to_line(); let used_cross_size = if stretches { (final_line_cross_size - item.item.pbm_auto_is_zero.cross).clamp_between_extremums( item.item.content_min_size.cross, item.item.content_max_size.cross, ) } else { item.layout_result.hypothetical_cross_size }; item_used_cross_sizes.push(used_cross_size); // “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.” if stretches { let new_layout = item.item.layout( item.used_main_size, flex_context, Some(used_cross_size), Some(&mut item.layout_result), ); if let Some(layout) = new_layout { item.layout_result = layout; } } let baseline = item.get_or_synthesize_baseline_with_cross_size(used_cross_size); if matches!( item.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.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. // 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.config.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 flex_context.config.flex_direction_is_reversed { free_space_in_main_axis } else { Au::zero() } }, AlignFlags::END => free_space_in_main_axis, AlignFlags::FLEX_END => { if flex_context.config.flex_direction_is_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 items = std::mem::take(&mut self.items); let item_fragments = izip!(items, item_margins, item_used_cross_sizes.iter()) .map(|(item, item_margin, item_used_cross_size)| { let item_used_size = FlexRelativeVec2 { main: item.used_main_size, cross: *item_used_cross_size, }; item.collect_fragment( &self, item_used_size, item_margin, item_main_interval, final_line_cross_size, &shared_alignment_baseline, flex_context, &mut all_baselines, &mut main_position_cursor, ) }) .collect(); FinalFlexLineLayout { cross_size: final_line_cross_size, item_fragments, all_baselines, shared_alignment_baseline, } } } 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`. #[cfg_attr( feature = "tracing", tracing::instrument( name = "FlexItem::layout", skip_all, fields( servo_profiling = true, self_address = self as *const _ as usize, box_address = self.box_ as *const _ as usize, for_stretch = non_stretch_layout_result.is_some(), ), level = "trace", ) )] #[allow(clippy::too_many_arguments)] fn layout( &self, used_main_size: Au, flex_context: &FlexContext, used_cross_size_override: Option, non_stretch_layout_result: Option<&mut FlexItemLayoutResult>, ) -> Option { let containing_block = flex_context.containing_block; let mut positioning_context = PositioningContext::new_for_style(self.box_.style()) .unwrap_or_else(|| { PositioningContext::new_for_subtree( flex_context .positioning_context .collects_for_nearest_positioned_ancestor(), ) }); let independent_formatting_context = &self.box_.independent_formatting_context; let item_writing_mode = independent_formatting_context.style().writing_mode; let item_is_horizontal = item_writing_mode.is_horizontal(); let flex_axis = flex_context.config.flex_axis; let cross_axis_is_item_block_axis = cross_axis_is_item_block_axis( containing_block.style.writing_mode.is_horizontal(), item_is_horizontal, flex_axis, ); let (inline_size, block_size) = if cross_axis_is_item_block_axis { let cross_size = match used_cross_size_override { Some(s) => SizeConstraint::Definite(s), None => { // This means that an auto size with stretch alignment will behave different than // a stretch size. That's not what the spec says, but matches other browsers. // To be discussed in https://github.com/w3c/csswg-drafts/issues/11784. let stretch_size = containing_block .size .block .to_definite() .map(|size| Au::zero().max(size - self.pbm_auto_is_zero.cross)); SizeConstraint::new( self.content_cross_size .maybe_resolve_extrinsic(stretch_size), self.content_min_size.cross, self.content_max_size.cross, ) }, }; (used_main_size, cross_size) } else { let cross_size = used_cross_size_override.unwrap_or_else(|| { let style = self.box_.style(); let automatic_size = if flex_context .config .item_with_auto_cross_size_stretches_to_container_size(style, &self.margin) { Size::Stretch } else { Size::FitContent }; let stretch_size = Au::zero().max(containing_block.size.inline - self.pbm_auto_is_zero.cross); let content_size = LazyCell::new(|| { let constraint_space = ConstraintSpace::new( SizeConstraint::Definite(used_main_size), item_writing_mode, self.preferred_aspect_ratio, ); independent_formatting_context .inline_content_sizes(flex_context.layout_context, &constraint_space) .sizes }); self.content_cross_size .resolve_for_preferred(automatic_size, Some(stretch_size), &content_size) .clamp_between_extremums( self.content_min_size.cross, self.content_max_size.cross, ) }); // The main size of a flex item is considered to be definite if its flex basis is definite // or the flex container has a definite main size. // let main_size = if self.flex_base_size_is_definite || flex_context .container_inner_size_constraint .main .is_definite() { SizeConstraint::Definite(used_main_size) } else { SizeConstraint::default() }; (cross_size, main_size) }; let container_writing_mode = containing_block.style.writing_mode; let item_style = independent_formatting_context.style(); match &independent_formatting_context.contents { IndependentFormattingContextContents::Replaced(replaced) => { let min_size = flex_axis.vec2_to_flow_relative(self.content_min_size); let max_size = flex_axis.vec2_to_flow_relative(self.content_max_size); let size = replaced.used_size_as_if_inline_element_from_content_box_sizes( containing_block, item_style, self.preferred_aspect_ratio, LogicalVec2 { block: &Sizes::new( block_size .to_definite() .map_or(Size::Initial, Size::Numeric), Size::Numeric(min_size.block), max_size.block.map_or(Size::Initial, Size::Numeric), ), inline: &Sizes::new( Size::Numeric(inline_size), Size::Numeric(min_size.inline), max_size.inline.map_or(Size::Initial, Size::Numeric), ), }, Size::FitContent.into(), flex_axis.vec2_to_flow_relative(self.pbm_auto_is_zero), ); let hypothetical_cross_size = flex_axis.vec2_to_flex_relative(size).cross; if let Some(non_stretch_layout_result) = non_stretch_layout_result { if non_stretch_layout_result .compatible_with_containing_block_size_and_content_size( containing_block, size, ) { non_stretch_layout_result.hypothetical_cross_size = hypothetical_cross_size; return None; } } let fragments = replaced.make_fragments( flex_context.layout_context, item_style, size.to_physical_size(container_writing_mode), ); Some(FlexItemLayoutResult { hypothetical_cross_size, fragments, positioning_context, content_size: size, containing_block_inline_size: containing_block.size.inline, containing_block_block_size: containing_block.size.block, depends_on_block_constraints: false, has_child_which_depends_on_block_constraints: false, // We will need to synthesize the baseline, but since the used cross // size can differ from the hypothetical cross size, we should defer // synthesizing until needed. baseline_relative_to_margin_box: None, }) }, IndependentFormattingContextContents::NonReplaced(non_replaced) => { let calculate_hypothetical_cross_size = |content_block_size| { // TODO(#32853): handle size keywords. self.content_cross_size .to_numeric() .unwrap_or(if cross_axis_is_item_block_axis { content_block_size } else { inline_size }) .clamp_between_extremums( self.content_min_size.cross, self.content_max_size.cross, ) }; let item_as_containing_block = ContainingBlock { size: ContainingBlockSize { inline: inline_size, block: block_size, }, style: item_style, }; if let Some(non_stretch_layout_result) = non_stretch_layout_result { if non_stretch_layout_result .compatible_with_containing_block_size(&item_as_containing_block) { non_stretch_layout_result.hypothetical_cross_size = calculate_hypothetical_cross_size( non_stretch_layout_result.content_size.inline, ); return None; } } let cache = self.box_.block_content_size_cache.borrow_mut().take(); let layout = if let Some(cache) = cache.filter(|cache| { cache.compatible_with_item_as_containing_block(&item_as_containing_block) }) { positioning_context = cache.positioning_context; cache.layout } else { non_replaced.layout( flex_context.layout_context, &mut positioning_context, &item_as_containing_block, containing_block, ) }; let IndependentLayout { fragments, content_block_size, baselines: content_box_baselines, depends_on_block_constraints, .. } = layout; let depends_on_block_constraints = depends_on_block_constraints || (flex_axis == FlexAxis::Row && self.stretches_to_line()); let has_child_which_depends_on_block_constraints = fragments.iter().any(|fragment| { fragment.base().is_some_and(|base| base.flags.contains( FragmentFlags::SIZE_DEPENDS_ON_BLOCK_CONSTRAINTS_AND_CAN_BE_CHILD_OF_FLEX_ITEM)) }); let item_writing_mode_is_orthogonal_to_container_writing_mode = flex_context.config.writing_mode.is_horizontal() != item_style.writing_mode.is_horizontal(); let has_compatible_baseline = match flex_axis { FlexAxis::Row => !item_writing_mode_is_orthogonal_to_container_writing_mode, FlexAxis::Column => item_writing_mode_is_orthogonal_to_container_writing_mode, }; let baselines_relative_to_margin_box = if has_compatible_baseline { content_box_baselines.offset( self.margin.cross_start.auto_is(Au::zero) + self.padding.cross_start + self.border.cross_start, ) } else { Baselines::default() }; let baseline_relative_to_margin_box = match self.align_self.0.value() { // ‘baseline’ computes to ‘first baseline’. AlignFlags::BASELINE => baselines_relative_to_margin_box.first, AlignFlags::LAST_BASELINE => baselines_relative_to_margin_box.last, _ => None, }; Some(FlexItemLayoutResult { hypothetical_cross_size: calculate_hypothetical_cross_size(content_block_size), fragments, positioning_context, baseline_relative_to_margin_box, content_size: LogicalVec2 { inline: item_as_containing_block.size.inline, block: content_block_size, }, containing_block_inline_size: item_as_containing_block.size.inline, containing_block_block_size: item_as_containing_block.size.block, depends_on_block_constraints, has_child_which_depends_on_block_constraints, }) }, } } fn synthesized_baseline_relative_to_margin_box(&self, content_size: Au) -> Au { // If the item does not have a baseline in the necessary axis, // then one is synthesized from the flex item’s border box. // https://drafts.csswg.org/css-flexbox/#valdef-align-items-baseline content_size + self.margin.cross_start.auto_is(Au::zero) + self.padding.cross_start + self.border.cross_start + self.border.cross_end + self.padding.cross_end } /// 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, 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(cross_start), AuOrAuto::LengthPercentage(cross_end)) => { return FlexRelativeSides { cross_start, cross_end, main_start, main_end, }; }, (AuOrAuto::Auto, AuOrAuto::Auto) => 2, _ => 1, }; 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; 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`, // cross-start otherwise. // We know this because: // https://drafts.csswg.org/css-flexbox/#flex-wrap-property // “For the values that are not wrap-reverse, // the cross-start direction is equivalent to // either the inline-start or block-start direction of the current writing mode // (whichever is in the cross axis) // and the cross-end direction is the opposite direction of cross-start. // When flex-wrap is wrap-reverse, // the cross-start and cross-end directions are swapped.” let flex_wrap = flex_context.containing_block.style.get_position().flex_wrap; let flex_wrap_reverse = match flex_wrap { FlexWrap::Nowrap | FlexWrap::Wrap => false, FlexWrap::WrapReverse => true, }; // “if the block-start or inline-start margin (whichever is in the cross axis) is auto, // 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 { cross_start = self.margin.cross_start.auto_is(|| available); cross_end = self.margin.cross_end.auto_is(Au::zero); } else { cross_start = self.margin.cross_start.auto_is(Au::zero); cross_end = self.margin.cross_end.auto_is(|| available); } } FlexRelativeSides { cross_start, cross_end, main_start, main_end, } } /// Return the coordinate of the cross-start side of the content area fn align_along_cross_axis( &self, margin: &FlexRelativeSides, used_cross_size: &Au, line_cross_size: Au, propagated_baseline: Au, max_propagated_baseline: Au, wrap_reverse: bool, ) -> Au { let ending_alignment = line_cross_size - *used_cross_size - self.pbm_auto_is_zero.cross; let outer_cross_start = if self.margin.cross_start.is_auto() || self.margin.cross_end.is_auto() { Au::zero() } else { match self.align_self.0.value() { AlignFlags::STRETCH => Au::zero(), AlignFlags::CENTER => ending_alignment / 2, AlignFlags::BASELINE | AlignFlags::LAST_BASELINE => { max_propagated_baseline - propagated_baseline }, AlignFlags::START => { if !wrap_reverse { Au::zero() } else { ending_alignment } }, AlignFlags::END => { if !wrap_reverse { ending_alignment } else { Au::zero() } }, _ => Au::zero(), } }; outer_cross_start + margin.cross_start + self.border.cross_start + self.padding.cross_start } } impl FlexItemBox { fn main_content_size_info<'a>( &self, layout_context: &LayoutContext, containing_block: &IndefiniteContainingBlock, container_is_horizontal: bool, config: &FlexContainerConfig, flex_context_getter: &impl Fn() -> &'a FlexContext<'a>, ) -> FlexItemBoxInlineContentSizesInfo { let flex_axis = config.flex_axis; let main_start_cross_start = config.main_start_cross_start_sides_are; let style = &self.style(); let item_writing_mode = style.writing_mode; let item_is_horizontal = item_writing_mode.is_horizontal(); let cross_axis_is_item_block_axis = cross_axis_is_item_block_axis(container_is_horizontal, item_is_horizontal, flex_axis); let ContentBoxSizesAndPBM { content_box_sizes, pbm, .. } = self .independent_formatting_context .layout_style() .content_box_sizes_and_padding_border_margin(containing_block); let content_box_size = content_box_sizes.map(|size| size.preferred); // TODO(#32853): handle size keywords. let content_min_box_size = content_box_sizes.map(|size| size.min.to_auto_or()); let content_max_box_size = content_box_sizes.map(|size| size.max.to_numeric()); let preferred_aspect_ratio = self .independent_formatting_context .preferred_aspect_ratio(&pbm.padding_border_sums); let padding = main_start_cross_start.sides_to_flex_relative(pbm.padding); let border = main_start_cross_start.sides_to_flex_relative(pbm.border); let margin = main_start_cross_start.sides_to_flex_relative(pbm.margin); let padding_border = padding.sum_by_axis() + border.sum_by_axis(); let margin_auto_is_zero = pbm.margin.auto_is(Au::zero); let margin_auto_is_zero = main_start_cross_start.sides_to_flex_relative(margin_auto_is_zero); let pbm_auto_is_zero = FlexRelativeVec2 { main: padding_border.main, cross: padding_border.cross, } + margin_auto_is_zero.sum_by_axis(); let item_with_auto_cross_size_stretches_to_container_size = config.item_with_auto_cross_size_stretches_to_container_size(style, &margin); let automatic_min_size = self.automatic_min_size( layout_context, flex_axis.vec2_to_flex_relative(containing_block.size), cross_axis_is_item_block_axis, flex_axis.vec2_to_flex_relative(content_box_size), flex_axis.vec2_to_flex_relative(content_min_box_size), flex_axis.vec2_to_flex_relative(content_max_box_size), preferred_aspect_ratio, &pbm_auto_is_zero, item_with_auto_cross_size_stretches_to_container_size, |item| { item.layout_for_block_content_size( flex_context_getter(), &pbm, content_box_size, content_min_box_size.map(|v| v.auto_is(Au::zero)), content_max_box_size, preferred_aspect_ratio, item_with_auto_cross_size_stretches_to_container_size, IntrinsicSizingMode::Size, ) }, ); let content_min_size_no_auto = if cross_axis_is_item_block_axis { LogicalVec2 { inline: content_min_box_size.inline.auto_is(|| automatic_min_size), block: content_min_box_size.block.auto_is(Au::zero), } } else { LogicalVec2 { inline: content_min_box_size.inline.auto_is(Au::zero), block: content_min_box_size.block.auto_is(|| automatic_min_size), } }; let block_content_size_callback = |item: &FlexItemBox| { item.layout_for_block_content_size( flex_context_getter(), &pbm, content_box_size, content_min_size_no_auto, content_max_box_size, preferred_aspect_ratio, item_with_auto_cross_size_stretches_to_container_size, IntrinsicSizingMode::Size, ) }; // Compute the min-content and max-content contributions of the item. // let (content_contribution_sizes, depends_on_block_constraints) = match flex_axis { FlexAxis::Row => { let InlineContentSizesResult { sizes, depends_on_block_constraints, } = self .independent_formatting_context .outer_inline_content_sizes( layout_context, containing_block, &content_min_size_no_auto, item_with_auto_cross_size_stretches_to_container_size, ); (sizes, depends_on_block_constraints) }, FlexAxis::Column => { let size = self.layout_for_block_content_size( flex_context_getter(), &pbm, content_box_size, content_min_size_no_auto, content_max_box_size, preferred_aspect_ratio, item_with_auto_cross_size_stretches_to_container_size, IntrinsicSizingMode::Contribution, ); (size.into(), true) }, }; let content_box_size = flex_axis.vec2_to_flex_relative(content_box_size); let content_min_size_no_auto = flex_axis.vec2_to_flex_relative(content_min_size_no_auto); let content_max_size = flex_axis.vec2_to_flex_relative(content_max_box_size); // TODO: when laying out a column container with an indefinite main size, // we compute the base sizes of the items twice. We should consider caching. let (flex_base_size, _) = self.flex_base_size( layout_context, config .flex_axis .vec2_to_flex_relative(containing_block.size), cross_axis_is_item_block_axis, content_box_size, content_min_size_no_auto, content_max_size, preferred_aspect_ratio, padding_border, &pbm_auto_is_zero, item_with_auto_cross_size_stretches_to_container_size, block_content_size_callback, ); let outer_flex_base_size = flex_base_size + pbm_auto_is_zero.main; let max_flex_factors = self.desired_flex_factors_for_preferred_width( content_contribution_sizes.max_content, flex_base_size, outer_flex_base_size, ); // > The min-content main size of a single-line flex container is calculated // > identically to the max-content main size, except that the flex items’ // > min-content contributions are used instead of their max-content contributions. let min_flex_factors = self.desired_flex_factors_for_preferred_width( content_contribution_sizes.min_content, flex_base_size, outer_flex_base_size, ); // > However, for a multi-line container, the min-content main size is simply the // > largest min-content contribution of all the non-collapsed flex items in the // > flex container. For this purpose, each item’s contribution is capped by the // > item’s flex base size if the item is not growable, floored by the item’s flex // > base size if the item is not shrinkable, and then further clamped by the item’s // > min and max main sizes. let mut min_content_main_size_for_multiline_container = content_contribution_sizes.min_content; if style.get_position().flex_grow.is_zero() { min_content_main_size_for_multiline_container.min_assign(flex_base_size); } if style.get_position().flex_shrink.is_zero() { min_content_main_size_for_multiline_container.max_assign(flex_base_size); } min_content_main_size_for_multiline_container = min_content_main_size_for_multiline_container .clamp_between_extremums(content_min_size_no_auto.main, content_max_size.main); FlexItemBoxInlineContentSizesInfo { outer_flex_base_size, content_min_size_no_auto, content_max_size, pbm_auto_is_zero, min_flex_factors, max_flex_factors, min_content_main_size_for_multiline_container, depends_on_block_constraints, } } fn desired_flex_factors_for_preferred_width( &self, preferred_width: Au, flex_base_size: Au, outer_flex_base_size: Au, ) -> DesiredFlexFractionAndGrowOrShrinkFactor { let difference = (preferred_width - outer_flex_base_size).to_f32_px(); let (flex_grow_or_scaled_flex_shrink_factor, desired_flex_fraction) = if difference > 0.0 { // > If that result is positive, divide it by the item’s flex // > grow factor if the flex grow > factor is ≥ 1, or multiply // > it by the flex grow factor if the flex grow factor is < 1; let flex_grow_factor = self.style().get_position().flex_grow.0; ( flex_grow_factor, if flex_grow_factor >= 1.0 { difference / flex_grow_factor } else { difference * flex_grow_factor }, ) } else if difference < 0.0 { // > if the result is negative, divide it by the item’s scaled // > flex shrink factor (if dividing > by zero, treat the result // > as negative infinity). let flex_shrink_factor = self.style().get_position().flex_shrink.0; let scaled_flex_shrink_factor = flex_shrink_factor * flex_base_size.to_f32_px(); ( scaled_flex_shrink_factor, if scaled_flex_shrink_factor != 0.0 { difference / scaled_flex_shrink_factor } else { f32::NEG_INFINITY }, ) } else { (0.0, 0.0) }; DesiredFlexFractionAndGrowOrShrinkFactor { desired_flex_fraction, flex_grow_or_shrink_factor: flex_grow_or_scaled_flex_shrink_factor, } } /// This is an implementation of . #[allow(clippy::too_many_arguments)] fn automatic_min_size( &self, layout_context: &LayoutContext, containing_block_size: FlexRelativeVec2>, cross_axis_is_item_block_axis: bool, content_box_size: FlexRelativeVec2>, min_size: FlexRelativeVec2>, max_size: FlexRelativeVec2>, preferred_aspect_ratio: Option, pbm_auto_is_zero: &FlexRelativeVec2, auto_cross_size_stretches_to_container_size: bool, block_content_size_callback: impl FnOnce(&FlexItemBox) -> Au, ) -> Au { // FIXME(stshine): Consider more situations when auto min size is not needed. let style = &self.independent_formatting_context.style(); if style.establishes_scroll_container(self.base_fragment_info().flags) { return Au::zero(); } // > **specified size suggestion** // > If the item’s preferred main size is definite and not automatic, then the specified // > size suggestion is that size. It is otherwise undefined. let specified_size_suggestion = content_box_size.main.maybe_resolve_extrinsic( containing_block_size .main .map(|v| v - pbm_auto_is_zero.main), ); let is_replaced = self.independent_formatting_context.is_replaced(); let main_axis = if cross_axis_is_item_block_axis { Direction::Inline } else { Direction::Block }; let cross_stretch_size = containing_block_size .cross .map(|v| v - pbm_auto_is_zero.cross); let cross_size = SizeConstraint::new( if content_box_size.cross.is_initial() && auto_cross_size_stretches_to_container_size { cross_stretch_size } else { content_box_size .cross .maybe_resolve_extrinsic(cross_stretch_size) }, min_size.cross.auto_is(Au::zero), max_size.cross, ); // > **transferred size suggestion** // > If the item has a preferred aspect ratio and its preferred cross size is definite, then the // > transferred size suggestion is that size (clamped by its minimum and maximum cross sizes if they // > are definite), converted through the aspect ratio. It is otherwise undefined. let transferred_size_suggestion = match (preferred_aspect_ratio, cross_size) { (Some(ratio), SizeConstraint::Definite(cross_size)) => { Some(ratio.compute_dependent_size(main_axis, cross_size)) }, _ => None, }; // > **content size suggestion** // > The content size suggestion is the min-content size in the main axis, clamped, if it has a // > preferred aspect ratio, by any definite minimum and maximum cross sizes converted through the // > aspect ratio. let main_content_size = if cross_axis_is_item_block_axis { let writing_mode = style.writing_mode; let constraint_space = ConstraintSpace::new(cross_size, writing_mode, preferred_aspect_ratio); self.independent_formatting_context .inline_content_sizes(layout_context, &constraint_space) .sizes .min_content } else { block_content_size_callback(self) }; let content_size_suggestion = preferred_aspect_ratio .map(|ratio| { main_content_size.clamp_between_extremums( ratio.compute_dependent_size(main_axis, min_size.cross.auto_is(Au::zero)), max_size .cross .map(|l| ratio.compute_dependent_size(main_axis, l)), ) }) .unwrap_or(main_content_size); // > The content-based minimum size of a flex item is the smaller of its specified size // > suggestion and its content size suggestion if its specified size suggestion exists; // > otherwise, the smaller of its transferred size suggestion and its content size // > suggestion if the element is replaced and its transferred size suggestion exists; // > otherwise its content size suggestion. In all cases, the size is clamped by the maximum // > main size if it’s definite. match (specified_size_suggestion, transferred_size_suggestion) { (Some(specified), _) => specified.min(content_size_suggestion), (_, Some(transferred)) if is_replaced => transferred.min(content_size_suggestion), _ => content_size_suggestion, } .clamp_below_max(max_size.main) } /// /// Returns the used value of the `flex-basis` property, after resolving percentages, /// resolving `auto`, and taking `box-sizing` into account. /// Note that a return value of `Size::Initial` represents `flex-basis: content`, /// not `flex-basis: auto`, since the latter always resolves to something else. fn flex_basis( &self, container_definite_main_size: Option, main_preferred_size: Size, main_padding_border_sum: Au, ) -> Size { let style_position = &self.independent_formatting_context.style().get_position(); match &style_position.flex_basis { // https://drafts.csswg.org/css-flexbox-1/#valdef-flex-basis-content // > Indicates an automatic size based on the flex item’s content. FlexBasis::Content => Size::Initial, FlexBasis::Size(size) => match Size::::from(size.clone()) { // https://drafts.csswg.org/css-flexbox-1/#valdef-flex-basis-auto // > When specified on a flex item, the `auto` keyword retrieves // > the value of the main size property as the used `flex-basis`. Size::Initial => main_preferred_size, // https://drafts.csswg.org/css-flexbox-1/#flex-basis-property // > For all values other than `auto` and `content` (defined above), // > `flex-basis` is resolved the same way as `width` in horizontal // > writing modes, except that if a value would resolve to `auto` // > for `width`, it instead resolves to `content` for `flex-basis`. // > For example, percentage values of `flex-basis` are resolved // > against the flex item’s containing block (i.e. its flex container); // > and if that containing block’s size is indefinite, // > the used value for `flex-basis` is `content`. size => { let apply_box_sizing = |length: Au| { match style_position.box_sizing { BoxSizing::ContentBox => length, // This may make `length` negative, // but it will be clamped in the hypothetical main size BoxSizing::BorderBox => length - main_padding_border_sum, } }; size.maybe_map(|v| { v.maybe_to_used_value(container_definite_main_size) .map(apply_box_sizing) }) .unwrap_or_default() }, }, } } /// #[allow(clippy::too_many_arguments)] fn flex_base_size( &self, layout_context: &LayoutContext, container_definite_inner_size: FlexRelativeVec2>, cross_axis_is_item_block_axis: bool, content_box_size: FlexRelativeVec2>, content_min_box_size: FlexRelativeVec2, content_max_box_size: FlexRelativeVec2>, preferred_aspect_ratio: Option, padding_border_sums: FlexRelativeVec2, pbm_auto_is_zero: &FlexRelativeVec2, item_with_auto_cross_size_stretches_to_container_size: bool, block_content_size_callback: impl FnOnce(&FlexItemBox) -> Au, ) -> (Au, bool) { let used_flex_basis = self.flex_basis( container_definite_inner_size.main, content_box_size.main, padding_border_sums.main, ); let mut flex_base_size_is_definite = true; let content_size = LazyCell::new(|| { let flex_item = &self.independent_formatting_context; let main_axis = if cross_axis_is_item_block_axis { Direction::Inline } else { Direction::Block }; // > If a single-line flex container has a definite cross size, the automatic preferred // > outer cross size of any stretched flex items is the flex container’s inner cross size // > (clamped to the flex item’s min and max cross size) and is considered definite. let cross_stretch_size = container_definite_inner_size .cross .map(|v| v - pbm_auto_is_zero.cross); let cross_size = SizeConstraint::new( if content_box_size.cross.is_initial() && item_with_auto_cross_size_stretches_to_container_size { cross_stretch_size } else { content_box_size .cross .maybe_resolve_extrinsic(cross_stretch_size) }, content_min_box_size.cross, content_max_box_size.cross, ); // > B: If the flex item has ... // > - a preferred aspect ratio, // > - a used flex basis of content, and // > - a definite cross size, // > then the flex base size is calculated from its used cross size and the flex item’s aspect ratio. if let (Some(ratio), SizeConstraint::Definite(cross_size)) = (preferred_aspect_ratio, cross_size) { return ratio.compute_dependent_size(main_axis, cross_size).into(); } flex_base_size_is_definite = false; // FIXME: implement cases C, D. // > 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. 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, or when // > it has a preferred aspect ratio) and the flex item’s cross size is auto and not definite, // > in this calculation use fit-content as the flex item’s cross size. The flex base size is // > the item’s resulting main size. if cross_axis_is_item_block_axis { // The main axis is the inline axis, so we can get the content size from the normal // preferred widths calculation. let constraint_space = ConstraintSpace::new( cross_size, flex_item.style().writing_mode, preferred_aspect_ratio, ); let content_sizes = flex_item .inline_content_sizes(layout_context, &constraint_space) .sizes; if let Some(ratio) = preferred_aspect_ratio { let transferred_min = ratio.compute_dependent_size(main_axis, content_min_box_size.cross); let transferred_max = content_max_box_size .cross .map(|v| ratio.compute_dependent_size(main_axis, v)); content_sizes .map(|size| size.clamp_between_extremums(transferred_min, transferred_max)) } else { content_sizes } } else { block_content_size_callback(self).into() } }); let stretch_size = container_definite_inner_size .main .map(|container_size| Au::zero().max(container_size - pbm_auto_is_zero.main)); let flex_base_size = used_flex_basis.resolve_for_preferred(Size::MaxContent, stretch_size, &content_size); (flex_base_size, flex_base_size_is_definite) } #[allow(clippy::too_many_arguments)] #[cfg_attr( feature = "tracing", tracing::instrument( name = "FlexContainer::layout_for_block_content_size", skip_all, fields(servo_profiling = true), level = "trace", ) )] fn layout_for_block_content_size( &self, flex_context: &FlexContext, padding_border_margin: &PaddingBorderMargin, mut content_box_size: LogicalVec2>, mut min_size: LogicalVec2, mut max_size: LogicalVec2>, preferred_aspect_ratio: Option, item_with_auto_cross_size_stretches_to_container_size: bool, intrinsic_sizing_mode: IntrinsicSizingMode, ) -> Au { let mut positioning_context = PositioningContext::new_for_subtree( flex_context .positioning_context .collects_for_nearest_positioned_ancestor(), ); let style = self.independent_formatting_context.style(); match &self.independent_formatting_context.contents { IndependentFormattingContextContents::Replaced(replaced) => { content_box_size.inline = content_box_size.inline.map(|v| v.max(Au::zero())); if intrinsic_sizing_mode == IntrinsicSizingMode::Size { content_box_size.block = Size::Initial; min_size.block = Au::zero(); max_size.block = None; } replaced .used_size_as_if_inline_element_from_content_box_sizes( flex_context.containing_block, style, preferred_aspect_ratio, LogicalVec2 { block: &Sizes::new( content_box_size.block, Size::Numeric(min_size.block), max_size.block.map_or(Size::Initial, Size::Numeric), ), inline: &Sizes::new( content_box_size.inline, Size::Numeric(min_size.inline), max_size.inline.map_or(Size::Initial, Size::Numeric), ), }, Size::FitContent.into(), padding_border_margin.padding_border_sums + padding_border_margin.margin.auto_is(Au::zero).sum(), ) .block }, IndependentFormattingContextContents::NonReplaced(non_replaced) => { // TODO: This is wrong if the item writing mode is different from the flex // container's writing mode. let inline_size = { let initial_behavior = if item_with_auto_cross_size_stretches_to_container_size { Size::Stretch } else { Size::FitContent }; let stretch_size = flex_context.containing_block.size.inline - padding_border_margin.padding_border_sums.inline - padding_border_margin.margin.inline_start.auto_is(Au::zero) - padding_border_margin.margin.inline_end.auto_is(Au::zero); let content_size = LazyCell::new(|| { let constraint_space = ConstraintSpace::new( SizeConstraint::default(), style.writing_mode, non_replaced.preferred_aspect_ratio(), ); self.independent_formatting_context .inline_content_sizes(flex_context.layout_context, &constraint_space) .sizes }); content_box_size .inline .resolve_for_preferred(initial_behavior, Some(stretch_size), &content_size) .clamp_between_extremums(min_size.inline, max_size.inline) }; let item_as_containing_block = ContainingBlock { size: ContainingBlockSize { inline: inline_size, block: SizeConstraint::default(), }, style, }; let content_block_size = || { if let Some(cache) = &*self.block_content_size_cache.borrow() { if inline_size == cache.containing_block_inline_size { return cache.layout.content_block_size; } else { #[cfg(feature = "tracing")] tracing::warn!( name: "NonReplaced cache miss", cached = ?cache.containing_block_inline_size, required = ?inline_size, ); } } else { #[cfg(feature = "tracing")] tracing::warn!(name: "NonReplaced no cache", required = ?inline_size); } let layout = non_replaced.layout( flex_context.layout_context, &mut positioning_context, &item_as_containing_block, flex_context.containing_block, ); let content_block_size = layout.content_block_size; *self.block_content_size_cache.borrow_mut() = Some(CachedBlockSizeContribution { containing_block_inline_size: item_as_containing_block.size.inline, layout, positioning_context, }); content_block_size }; match intrinsic_sizing_mode { IntrinsicSizingMode::Contribution => { let stretch_size = flex_context.containing_block.size.block.to_definite().map( |block_size| { block_size - padding_border_margin.padding_border_sums.block - padding_border_margin.margin.block_start.auto_is(Au::zero) - padding_border_margin.margin.block_end.auto_is(Au::zero) }, ); let inner_block_size = content_box_size .block .resolve_for_preferred( Size::FitContent, stretch_size, &LazyCell::new(|| ContentSizes::from(content_block_size())), ) .clamp_between_extremums(min_size.block, max_size.block); inner_block_size + padding_border_margin.padding_border_sums.block + padding_border_margin.margin.block_start.auto_is(Au::zero) + padding_border_margin.margin.block_end.auto_is(Au::zero) }, IntrinsicSizingMode::Size => content_block_size(), } }, } } }