blob: 3f94f01528f0cf67dee9a1359d5c9d63c9dfd0d5 [file] [log] [blame] [edit]
/* 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_wrap::computed_value::T as FlexWrap;
use style::values::computed::LengthPercentage;
use style::values::generics::flex::GenericFlexBasis as FlexBasis;
use style::values::generics::length::LengthPercentageOrNormal;
use style::values::specified::align::AlignFlags;
use super::geom::{FlexAxis, FlexRelativeRect, FlexRelativeSides, FlexRelativeVec2};
use super::{FlexContainer, FlexContainerConfig, FlexItemBox, FlexLevelBox};
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::formatting_contexts::Baselines;
use crate::fragment_tree::{
BoxFragment, CollapsedBlockMargins, Fragment, FragmentFlags, SpecificLayoutInfo,
};
use crate::geom::{AuOrAuto, LogicalRect, LogicalSides, LogicalVec2};
use crate::layout_box_base::CacheableLayoutResult;
use crate::positioned::{
AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, relative_adjustement,
};
use crate::sizing::{
ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult, IntrinsicSizingMode,
LazySize, Size, SizeConstraint, Sizes,
};
use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, ContentBoxSizesAndPBM, LayoutStyle};
use crate::{ConstraintSpace, ContainingBlock, ContainingBlockSize, IndefiniteContainingBlock};
/// 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>,
containing_block: &'a ContainingBlock<'a>, // For items
container_inner_size_constraint: FlexRelativeVec2<SizeConstraint>,
}
/// A flex item with some intermediate results
struct FlexItem<'a> {
box_: &'a FlexItemBox,
/// The preferred, min and max inner cross sizes.
content_cross_sizes: Sizes,
padding: FlexRelativeSides<Au>,
border: FlexRelativeSides<Au>,
margin: FlexRelativeSides<AuOrAuto>,
/// 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<Au>,
/// <https://drafts.csswg.org/css-flexbox/#algo-main-item>
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,
/// <https://drafts.csswg.org/css-flexbox/#algo-main-item>
hypothetical_main_size: Au,
/// The used min main size of the flex item.
/// <https://drafts.csswg.org/css-flexbox/#min-main-size-property>
content_min_main_size: Au,
/// The used max main size of the flex item.
/// <https://drafts.csswg.org/css-flexbox/#max-main-size-property>
content_max_main_size: Option<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,
/// <https://drafts.csswg.org/css-sizing-4/#preferred-aspect-ratio>
preferred_aspect_ratio: Option<AspectRatio>,
/// The automatic size in the cross axis.
/// <https://drafts.csswg.org/css-sizing-3/#automatic-size>
automatic_cross_size: Size<Au>,
automatic_cross_size_for_intrinsic_sizing: Size<Au>,
}
/// 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<AbsolutelyPositionedBox>),
FlexItemPlaceholder,
}
/// Return type of `FlexItem::layout`
struct FlexItemLayoutResult {
hypothetical_cross_size: Au,
fragments: Vec<Fragment>,
positioning_context: PositioningContext,
// Either the first or the last baseline, depending on ‘align-self’.
baseline_relative_to_margin_box: Option<Au>,
// The content size of this layout in the block axis. This is known before layout
// for replaced elements, but for non-replaced it's only known after layout.
content_block_size: Au,
// The containing block size used to generate this layout.
containing_block_size: ContainingBlockSize,
// Whether or not this layout depended on block constraints.
depends_on_block_constraints: bool,
// The specific layout info that this flex item had.
specific_layout_info: Option<SpecificLayoutInfo>,
}
/// 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<Au>,
item_margin: FlexRelativeSides<Au>,
item_main_interval: Au,
final_line_cross_size: Au,
shared_alignment_baseline: &Option<Au>,
flex_context: &mut FlexContext,
all_baselines: &mut Baselines,
main_position_cursor: &mut Au,
) -> (ArcRefCell<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,
},
);
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_OR_GRID_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),
flex_context
.sides_to_flow_relative(item_margin)
.to_physical(container_writing_mode),
self.layout_result.specific_layout_info,
);
// 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)
}
let fragment = ArcRefCell::new(fragment);
self.item
.box_
.independent_formatting_context
.base
.set_fragment(Fragment::Box(fragment.clone()));
(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<(ArcRefCell<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<Au>,
/// 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 {
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,
)
}
fn sides_to_flex_relative<T>(&self, sides: LogicalSides<T>) -> FlexRelativeSides<T> {
self.main_start_cross_start_sides_are
.sides_to_flex_relative(sides)
}
fn sides_to_flow_relative<T>(&self, sides: FlexRelativeSides<T>) -> LogicalSides<T> {
self.main_start_cross_start_sides_are
.sides_to_flow_relative(sides)
}
}
impl FlexContext<'_> {
#[inline]
fn sides_to_flow_relative<T>(&self, x: FlexRelativeSides<T>) -> LogicalSides<T> {
self.config.sides_to_flow_relative(x)
}
#[inline]
fn rect_to_flow_relative(
&self,
base_rect_size: FlexRelativeVec2<Au>,
rect: FlexRelativeRect<Au>,
) -> LogicalRect<Au> {
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,
outer_min_main_size: Au,
outer_max_main_size: Option<Au>,
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 {
#[servo_tracing::instrument(name = "FlexContainer::compute_inline_content_sizes", skip_all)]
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 {
// <https://drafts.csswg.org/css-flexbox/#intrinsic-cross-sizes>
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.
// <https://drafts.csswg.org/css-flexbox-1/#intrinsic-main-sizes>
// > 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![];
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,
&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,
outer_min_main_size,
outer_max_main_size,
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.
// > 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,
}
}
/// <https://drafts.csswg.org/css-flexbox/#layout-algorithm>
#[servo_tracing::instrument(
name = "FlexContainer::layout",
skip_all,
fields(self_address = self as *const _ as usize)
)]
pub(crate) fn layout(
&self,
layout_context: &LayoutContext,
positioning_context: &mut PositioningContext,
containing_block: &ContainingBlock,
lazy_block_size: &LazySize,
) -> CacheableLayoutResult {
let mut flex_context = FlexContext {
config: self.config.clone(),
layout_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 => lazy_block_size.resolve(|| {
let mut containing_block = IndefiniteContainingBlock::from(containing_block);
containing_block.size.block = None;
self.main_content_sizes(layout_context, &containing_block, || &flex_context)
.sizes
.max_content
}),
};
// 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::<Vec<_>>();
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::<Vec<_>>();
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::<Au>() +
cross_gap * (line_count as i32 - 1);
let content_block_size = match self.config.flex_axis {
FlexAxis::Row => content_cross_size,
FlexAxis::Column => container_main_size,
};
// https://drafts.csswg.org/css-flexbox/#algo-cross-container
let container_cross_size = match self.config.flex_axis {
FlexAxis::Row => lazy_block_size.resolve(|| content_cross_size),
FlexAxis::Column => containing_block.size.inline,
};
let container_size = FlexRelativeVec2 {
main: container_main_size,
cross: container_cross_size,
};
let mut remaining_free_cross_space = container_cross_size - content_cross_size;
// 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, false),
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.borrow_mut().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(fragment);
child_positioning_context.adjust_static_position_of_hoisted_fragments(
&fragment,
PositioningContextLength::zero(),
);
positioning_context.append(child_positioning_context);
fragment
},
})
.collect::<Vec<_>>();
// 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),
};
// TODO: `depends_on_block_constraints` could be false in some corner cases
// in order to improve performance.
// - In a single-line column container where all items have the grow and shrink
// factors set to zero and the flex basis doesn't depend on block constraints,
// and `justify-content` is `start` or equivalent.
// This is unlikely because the flex shrink factor defaults to 1.
// - In a single-line row container where all items have `align-self: start` or
// equivalent, and the cross size doesn't depend on block constraints.
// This is unlikely because `align-self` stretches by default.
// - In a multi-line row container where `align-content` is `start` or equivalent,
// and no item cross size depends on block constraints.
// This is unlikely because `align-content` defaults to `stretch`.
let depends_on_block_constraints = true;
CacheableLayoutResult {
fragments,
content_block_size,
content_inline_size_for_table: None,
baselines,
depends_on_block_constraints,
specific_layout_info: None,
collapsible_margins_in_children: CollapsedBlockMargins::zero(),
}
}
/// 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 <https://drafts.csswg.org/css-flexbox/#abspos-items>:
/// > 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<AbsolutelyPositionedBox>,
containing_block: &ContainingBlock,
container_size: FlexRelativeVec2<Au>,
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
/// <https://drafts.csswg.org/css-flexbox/#algo-line-align>. 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.is_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 containing_block = IndefiniteContainingBlock::from(flex_context.containing_block);
let content_box_sizes_and_pbm = box_
.independent_formatting_context
.layout_style()
.content_box_sizes_and_padding_border_margin(&containing_block);
box_.to_flex_item(
flex_context.layout_context,
&containing_block,
&content_box_sizes_and_pbm,
&flex_context.config,
&|| flex_context,
)
}
}
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 a computed preferred cross size of `auto` will stretch
/// to fill the cross size of its flex line.
/// <https://drafts.csswg.org/css-flexbox/#stretched>
fn item_with_auto_cross_size_stretches_to_line_size(
align_self: AlignItems,
margin: &FlexRelativeSides<AuOrAuto>,
) -> bool {
align_self.0.value() == AlignFlags::STRETCH &&
!margin.cross_start.is_auto() &&
!margin.cross_end.is_auto()
}
/// “Collect flex items into flex lines”
/// <https://drafts.csswg.org/css-flexbox/#algo-line-break>
fn do_initial_flex_line_layout<'items>(
flex_context: &mut FlexContext,
container_main_size: Au,
mut items: Vec<FlexItem<'items>>,
main_gap: Au,
) -> Vec<InitialFlexLineLayout<'items>> {
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));
if flex_context.layout_context.use_rayon {
lines.par_drain(..).map(construct_line).collect()
} else {
lines.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<FlexLineItem<'a>>,
/// The initial size of this flex line, not taking into account `align-content: stretch`.
line_size: FlexRelativeVec2<Au>,
/// 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<FlexItem<'items>>,
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: Vec<_> = if flex_context.layout_context.use_rayon {
items
.par_iter()
.zip(&item_used_main_sizes)
.map(|(item, used_main_size)| item.layout(*used_main_size, flex_context, None))
.collect()
} else {
items
.iter()
.zip(&item_used_main_sizes)
.map(|(item, used_main_size)| item.layout(*used_main_size, flex_context, None))
.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
/// <https://drafts.csswg.org/css-flexbox/#resolve-flexible-lengths>
fn resolve_flexible_lengths<'items>(
items: &'items [FlexItem<'items>],
outer_hypothetical_main_sizes_sum: Au,
container_main_size: Au,
) -> (Vec<Au>, Au) {
struct FlexibleLengthResolutionItem<'items> {
item: &'items FlexItem<'items>,
frozen: Cell<bool>,
target_main_size: Cell<Au>,
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<FlexibleLengthResolutionItem>| {
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.is_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_main_size,
item.item.content_max_main_size,
);
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_main_size);
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_main_size else {
unreachable!()
};
item.target_main_size.set(max_size);
item.frozen.set(true);
frozen_count += 1;
}
}
},
}
}
}
/// <https://drafts.csswg.org/css-flexbox/#algo-cross-line>
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::<u32>();
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 cross_axis = match flex_context.config.flex_axis {
FlexAxis::Row => Direction::Block,
FlexAxis::Column => Direction::Inline,
};
let layout = &mut item.layout_result;
let get_content_size = || match cross_axis {
Direction::Block => layout.content_block_size.into(),
Direction::Inline => item
.item
.inline_content_sizes(flex_context, item.used_main_size),
};
let used_cross_size = item.item.content_cross_sizes.resolve(
cross_axis,
item.item.automatic_cross_size,
Au::zero,
Some(Au::zero().max(final_line_cross_size - item.item.pbm_auto_is_zero.cross)),
get_content_size,
// Tables have a special sizing in the block axis in that handles collapsed rows,
// but it would prevent stretching. So we only recognize tables in the inline axis.
// The interaction of collapsed table tracks and the flexbox algorithms is unclear,
// see https://github.com/w3c/csswg-drafts/issues/11408.
item.item.box_.independent_formatting_context.is_table() &&
cross_axis == Direction::Inline,
);
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.”
// However, as resolved in https://github.com/w3c/csswg-drafts/issues/11784,
// we do that when the cross size is `stretch`. We also need to do it if the
// inline size changes, which may happen with a `fit-content` cross size.
let needs_new_layout = match cross_axis {
Direction::Block => {
(match item.item.content_cross_sizes.preferred {
Size::Initial => item.item.automatic_cross_size == Size::Stretch,
Size::Stretch => true,
_ => false,
}) && SizeConstraint::Definite(used_cross_size) !=
layout.containing_block_size.block &&
layout.depends_on_block_constraints
},
Direction::Inline => used_cross_size != layout.containing_block_size.inline,
};
if needs_new_layout {
#[cfg(feature = "tracing")]
tracing::warn!(
name: "Flex item stretch cache miss",
cached_inline = ?layout.containing_block_size.inline,
cached_block = ?layout.containing_block_size.block,
required_cross_size = ?used_cross_size,
cross_axis = ?cross_axis,
depends_on_block_constraints = layout.depends_on_block_constraints,
);
*layout =
item.item
.layout(item.used_main_size, flex_context, Some(used_cross_size));
}
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 <https://drafts.csswg.org/css-flexbox/#algo-cross-item>:
/// > performing layout as if it were an in-flow block-level box with the used main
/// > size and the given available space, treating `auto` as `fit-content`.
#[servo_tracing::instrument(
name = "FlexItem::layout",
skip_all,
fields(
self_address = self as *const _ as usize,
box_address = self.box_ as *const _ as usize,
)
)]
#[allow(clippy::too_many_arguments)]
fn layout(
&self,
used_main_size: Au,
flex_context: &FlexContext,
used_cross_size_override: Option<Au>,
) -> FlexItemLayoutResult {
let containing_block = flex_context.containing_block;
let independent_formatting_context = &self.box_.independent_formatting_context;
let is_table = independent_formatting_context.is_table();
let mut positioning_context = PositioningContext::default();
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 => {
let stretch_size = containing_block
.size
.block
.to_definite()
.map(|size| Au::zero().max(size - self.pbm_auto_is_zero.cross));
let tentative_block_content_size = independent_formatting_context
.tentative_block_content_size(self.preferred_aspect_ratio);
if let Some(block_content_size) = tentative_block_content_size {
SizeConstraint::Definite(self.content_cross_sizes.resolve(
Direction::Block,
Size::FitContent,
Au::zero,
stretch_size,
|| block_content_size,
is_table,
))
} else {
self.content_cross_sizes.resolve_extrinsic(
Size::FitContent,
Au::zero(),
stretch_size,
)
}
},
};
(used_main_size, cross_size)
} else {
let cross_size = used_cross_size_override.unwrap_or_else(|| {
let stretch_size =
Au::zero().max(containing_block.size.inline - self.pbm_auto_is_zero.cross);
self.content_cross_sizes.resolve(
Direction::Inline,
Size::FitContent,
Au::zero,
Some(stretch_size),
|| self.inline_content_sizes(flex_context, used_main_size),
is_table,
)
});
// 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.
// <https://drafts.csswg.org/css-flexbox-1/#definite-sizes>
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 item_style = independent_formatting_context.style();
let item_as_containing_block = ContainingBlock {
size: ContainingBlockSize {
inline: inline_size,
block: block_size,
},
style: item_style,
};
let lazy_block_size = if !cross_axis_is_item_block_axis {
used_main_size.into()
} else if let Some(cross_size) = used_cross_size_override {
cross_size.into()
} else {
let stretch_size = containing_block
.size
.block
.to_definite()
.map(|size| Au::zero().max(size - self.pbm_auto_is_zero.cross));
LazySize::new(
&self.content_cross_sizes,
Direction::Block,
Size::FitContent,
Au::zero,
stretch_size,
is_table,
)
};
let layout = independent_formatting_context.layout(
flex_context.layout_context,
&mut positioning_context,
&item_as_containing_block,
containing_block,
self.preferred_aspect_ratio,
&lazy_block_size,
);
let CacheableLayoutResult {
fragments,
content_block_size,
baselines: content_box_baselines,
depends_on_block_constraints,
specific_layout_info,
..
} = layout;
let hypothetical_cross_size = if cross_axis_is_item_block_axis {
lazy_block_size.resolve(|| content_block_size)
} else {
inline_size
};
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,
};
FlexItemLayoutResult {
hypothetical_cross_size,
fragments,
positioning_context,
baseline_relative_to_margin_box,
content_block_size,
containing_block_size: item_as_containing_block.size,
depends_on_block_constraints,
specific_layout_info,
}
}
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:
///
/// - <https://drafts.csswg.org/css-flexbox/#algo-cross-margins>
/// - <https://drafts.csswg.org/css-flexbox/#algo-main-align>
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<Au> {
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<Au>,
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
}
#[inline]
fn inline_content_sizes(&self, flex_context: &FlexContext, block_size: Au) -> ContentSizes {
self.box_.inline_content_sizes(
flex_context,
SizeConstraint::Definite(block_size),
self.preferred_aspect_ratio,
)
}
}
impl FlexItemBox {
fn to_flex_item<'a>(
&self,
layout_context: &LayoutContext,
containing_block: &IndefiniteContainingBlock,
content_box_sizes_and_pbm: &ContentBoxSizesAndPBM,
config: &FlexContainerConfig,
flex_context_getter: &impl Fn() -> &'a FlexContext<'a>,
) -> FlexItem<'_> {
let flex_axis = config.flex_axis;
let style = self.style();
let cross_axis_is_item_block_axis = cross_axis_is_item_block_axis(
containing_block.writing_mode.is_horizontal(),
style.writing_mode.is_horizontal(),
flex_axis,
);
let main_axis = if cross_axis_is_item_block_axis {
Direction::Inline
} else {
Direction::Block
};
let align_self = AlignItems(config.resolve_align_self_for_child(style));
let ContentBoxSizesAndPBM {
content_box_sizes,
pbm,
depends_on_block_constraints,
preferred_size_computes_to_auto,
} = content_box_sizes_and_pbm;
let preferred_aspect_ratio = self
.independent_formatting_context
.preferred_aspect_ratio(&pbm.padding_border_sums);
let padding = config.sides_to_flex_relative(pbm.padding);
let border = config.sides_to_flex_relative(pbm.border);
let margin = config.sides_to_flex_relative(pbm.margin);
let padding_border = padding.sum_by_axis() + border.sum_by_axis();
let margin_auto_is_zero = config.sides_to_flex_relative(pbm.margin.auto_is(Au::zero));
let pbm_auto_is_zero = FlexRelativeVec2 {
main: padding_border.main,
cross: padding_border.cross,
} + margin_auto_is_zero.sum_by_axis();
let (content_main_sizes, content_cross_sizes, cross_size_computes_to_auto) = match flex_axis
{
FlexAxis::Row => (
&content_box_sizes.inline,
&content_box_sizes.block,
preferred_size_computes_to_auto.block,
),
FlexAxis::Column => (
&content_box_sizes.block,
&content_box_sizes.inline,
preferred_size_computes_to_auto.inline,
),
};
let automatic_cross_size = if cross_size_computes_to_auto &&
item_with_auto_cross_size_stretches_to_line_size(align_self, &margin)
{
Size::Stretch
} else {
Size::FitContent
};
let automatic_cross_size_for_intrinsic_sizing = if config.container_is_single_line {
automatic_cross_size
} else {
Size::FitContent
};
let containing_block_size = flex_axis.vec2_to_flex_relative(containing_block.size);
let stretch_size = FlexRelativeVec2 {
main: containing_block_size
.main
.map(|v| Au::zero().max(v - pbm_auto_is_zero.main)),
cross: containing_block_size
.cross
.map(|v| Au::zero().max(v - pbm_auto_is_zero.cross)),
};
let is_table = self.independent_formatting_context.is_table();
let tentative_cross_content_size = if cross_axis_is_item_block_axis {
self.independent_formatting_context
.tentative_block_content_size(preferred_aspect_ratio)
} else {
None
};
let (preferred_cross_size, min_cross_size, max_cross_size) =
if let Some(cross_content_size) = tentative_cross_content_size {
let (preferred, min, max) = content_cross_sizes.resolve_each(
automatic_cross_size_for_intrinsic_sizing,
Au::zero,
stretch_size.cross,
|| cross_content_size,
is_table,
);
(Some(preferred), min, max)
} else {
content_cross_sizes.resolve_each_extrinsic(
automatic_cross_size_for_intrinsic_sizing,
Au::zero(),
stretch_size.cross,
)
};
let cross_size = SizeConstraint::new(preferred_cross_size, min_cross_size, max_cross_size);
// <https://drafts.csswg.org/css-flexbox/#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 =
LazyCell::new(|| match (preferred_aspect_ratio, cross_size) {
(Some(ratio), SizeConstraint::Definite(cross_size)) => {
Some(ratio.compute_dependent_size(main_axis, cross_size))
},
_ => None,
});
// <https://drafts.csswg.org/css-flexbox/#algo-main-item>
let flex_base_size_is_definite = Cell::new(true);
let main_content_sizes = LazyCell::new(|| {
let flex_item = &self.independent_formatting_context;
// > 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(transferred_size_suggestion) = *transferred_size_suggestion {
return transferred_size_suggestion.into();
}
flex_base_size_is_definite.set(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, 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, min_cross_size);
let transferred_max =
max_cross_size.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 {
self.layout_for_block_content_size(
flex_context_getter(),
&pbm_auto_is_zero,
content_box_sizes,
preferred_aspect_ratio,
automatic_cross_size_for_intrinsic_sizing,
IntrinsicSizingMode::Size,
)
.into()
}
});
let flex_base_size = self
.flex_basis(
containing_block_size.main,
content_main_sizes.preferred,
padding_border.main,
)
.resolve_for_preferred(Size::MaxContent, stretch_size.main, &main_content_sizes);
let flex_base_size_is_definite = flex_base_size_is_definite.take();
let content_max_main_size = content_main_sizes
.max
.resolve_for_max(stretch_size.main, &main_content_sizes);
let get_automatic_minimum_size = || {
// This is an implementation of <https://drafts.csswg.org/css-flexbox/#min-size-auto>.
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_main_sizes
.preferred
.maybe_resolve_extrinsic(stretch_size.main);
let is_replaced = self.independent_formatting_context.is_replaced();
// > **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 content_size_suggestion = match preferred_aspect_ratio {
Some(ratio) => main_content_sizes.min_content.clamp_between_extremums(
ratio.compute_dependent_size(main_axis, min_cross_size),
max_cross_size.map(|l| ratio.compute_dependent_size(main_axis, l)),
),
None => main_content_sizes.min_content,
};
// > 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(content_max_main_size)
};
let content_min_main_size = content_main_sizes.min.resolve_for_min(
get_automatic_minimum_size,
stretch_size.main,
&main_content_sizes,
is_table,
);
FlexItem {
box_: self,
content_cross_sizes: content_cross_sizes.clone(),
padding,
border,
margin: config.sides_to_flex_relative(pbm.margin),
pbm_auto_is_zero,
flex_base_size,
flex_base_size_is_definite,
hypothetical_main_size: flex_base_size
.clamp_between_extremums(content_min_main_size, content_max_main_size),
content_min_main_size,
content_max_main_size,
align_self,
depends_on_block_constraints: *depends_on_block_constraints,
preferred_aspect_ratio,
automatic_cross_size,
automatic_cross_size_for_intrinsic_sizing,
}
}
fn main_content_size_info<'a>(
&self,
layout_context: &LayoutContext,
containing_block: &IndefiniteContainingBlock,
config: &FlexContainerConfig,
flex_context_getter: &impl Fn() -> &'a FlexContext<'a>,
) -> FlexItemBoxInlineContentSizesInfo {
let content_box_sizes_and_pbm = self
.independent_formatting_context
.layout_style()
.content_box_sizes_and_padding_border_margin(containing_block);
// 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 FlexItem {
flex_base_size,
content_min_main_size,
content_max_main_size,
pbm_auto_is_zero,
preferred_aspect_ratio,
automatic_cross_size_for_intrinsic_sizing,
..
} = self.to_flex_item(
layout_context,
containing_block,
&content_box_sizes_and_pbm,
config,
flex_context_getter,
);
// Compute the min-content and max-content contributions of the item.
// <https://drafts.csswg.org/css-flexbox/#intrinsic-item-contributions>
let (content_contribution_sizes, depends_on_block_constraints) = match config.flex_axis {
FlexAxis::Row => {
let auto_minimum = LogicalVec2 {
inline: content_min_main_size,
block: Au::zero(),
};
let InlineContentSizesResult {
sizes,
depends_on_block_constraints,
} = self
.independent_formatting_context
.outer_inline_content_sizes(
layout_context,
containing_block,
&auto_minimum,
automatic_cross_size_for_intrinsic_sizing == Size::Stretch,
);
(sizes, depends_on_block_constraints)
},
FlexAxis::Column => {
let size = self.layout_for_block_content_size(
flex_context_getter(),
&pbm_auto_is_zero,
&content_box_sizes_and_pbm.content_box_sizes,
preferred_aspect_ratio,
automatic_cross_size_for_intrinsic_sizing,
IntrinsicSizingMode::Contribution,
);
(size.into(), true)
},
};
let outer_flex_base_size = flex_base_size + pbm_auto_is_zero.main;
let outer_min_main_size = content_min_main_size + pbm_auto_is_zero.main;
let outer_max_main_size = content_max_main_size.map(|v| v + 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;
let style_position = &self.style().get_position();
if style_position.flex_grow.is_zero() {
min_content_main_size_for_multiline_container.min_assign(outer_flex_base_size);
}
if style_position.flex_shrink.is_zero() {
min_content_main_size_for_multiline_container.max_assign(outer_flex_base_size);
}
min_content_main_size_for_multiline_container =
min_content_main_size_for_multiline_container
.clamp_between_extremums(outer_min_main_size, outer_max_main_size);
FlexItemBoxInlineContentSizesInfo {
outer_flex_base_size,
outer_min_main_size,
outer_max_main_size,
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,
}
}
/// <https://drafts.csswg.org/css-flexbox-1/#flex-basis-property>
/// 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<Au>,
main_preferred_size: Size<Au>,
main_padding_border_sum: Au,
) -> Size<Au> {
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::<LengthPercentage>::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.resolve_percentages_for_preferred(container_definite_main_size)
.map(apply_box_sizing)
},
},
}
}
#[allow(clippy::too_many_arguments)]
#[servo_tracing::instrument(name = "FlexContainer::layout_for_block_content_size", skip_all)]
fn layout_for_block_content_size(
&self,
flex_context: &FlexContext,
pbm_auto_is_zero: &FlexRelativeVec2<Au>,
content_box_sizes: &LogicalVec2<Sizes>,
preferred_aspect_ratio: Option<AspectRatio>,
automatic_inline_size: Size<Au>,
intrinsic_sizing_mode: IntrinsicSizingMode,
) -> Au {
let content_block_size = || {
let mut positioning_context = PositioningContext::default();
let style = self.independent_formatting_context.style();
// We are computing the intrinsic block size, so the tentative block size that we use
// as an input to the intrinsic inline sizes needs to ignore the values of the sizing
// properties in the block axis.
let tentative_block_size = SizeConstraint::default();
// TODO: This is wrong if the item writing mode is different from the flex
// container's writing mode.
let inline_size = {
let stretch_size =
flex_context.containing_block.size.inline - pbm_auto_is_zero.cross;
let get_content_size = || {
self.inline_content_sizes(
flex_context,
tentative_block_size,
preferred_aspect_ratio,
)
};
content_box_sizes.inline.resolve(
Direction::Inline,
automatic_inline_size,
Au::zero,
Some(stretch_size),
get_content_size,
false,
)
};
let item_as_containing_block = ContainingBlock {
size: ContainingBlockSize {
inline: inline_size,
block: tentative_block_size,
},
style,
};
self.independent_formatting_context
.layout(
flex_context.layout_context,
&mut positioning_context,
&item_as_containing_block,
flex_context.containing_block,
preferred_aspect_ratio,
&LazySize::intrinsic(),
)
.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 - pbm_auto_is_zero.main);
let inner_block_size = content_box_sizes.block.resolve(
Direction::Block,
Size::FitContent,
Au::zero,
stretch_size,
|| ContentSizes::from(content_block_size()),
// Tables have a special sizing in the block axis that handles collapsed rows
// by ignoring the sizing properties and instead relying on the content block size,
// which should indirectly take sizing properties into account.
// However, above we laid out the table with a SizeConstraint::default() block size,
// so the content block size doesn't take sizing properties into account.
// Therefore, pretending that it's never a table tends to provide a better result.
false, /* is_table */
);
inner_block_size + pbm_auto_is_zero.main
},
IntrinsicSizingMode::Size => content_block_size(),
}
}
fn inline_content_sizes(
&self,
flex_context: &FlexContext,
block_size: SizeConstraint,
preferred_aspect_ratio: Option<AspectRatio>,
) -> ContentSizes {
let writing_mode = self.independent_formatting_context.style().writing_mode;
let constraint_space =
ConstraintSpace::new(block_size, writing_mode, preferred_aspect_ratio);
self.independent_formatting_context
.inline_content_sizes(flex_context.layout_context, &constraint_space)
.sizes
}
}