From 1cceb5f6a09fedec81f925ba189fae102166cdd9 Mon Sep 17 00:00:00 2001 From: Oriol Brufau Date: Wed, 14 May 2025 13:35:49 -0700 Subject: [PATCH] layout: Inform child layout about final block size (#36980) Before this patch it wasn't possibly to simultaneously support intrinsic min/max sizes and content alignment in the block axis. For example, block containers only support the former, and flex containers only the latter. The reason is that the final block size was decided by the parent formatting context *after* performing layout, while content alignment is performed *during* layout. To address the problem, this introduces the struct `LazySize`, which contains the data to resolve the final size, except for the intrinsic size. Thus the parent formatting context can first create a `LazySize`, then pass it to the child layout so that (if necessary) it can compute the final size once the intrinsic one is known, and after layout the parent formatting context uses it to actually size the child. This PR just provides the functionality that will be used by follow-ups, but at this point no layout is using the `LazySize` provided by the parent, so there shouldn't be any behavior change yet. Testing: Unnecessary (no behavior change) This is part of #36981 and #36982 Signed-off-by: Oriol Brufau --- components/layout/flexbox/layout.rs | 42 +++++++----- components/layout/flow/mod.rs | 66 +++++++++--------- components/layout/formatting_contexts.rs | 5 ++ components/layout/geom.rs | 86 +++++++++++++++++++++++- components/layout/positioned.rs | 61 ++++++++--------- components/layout/taffy/layout.rs | 15 ++++- 6 files changed, 194 insertions(+), 81 deletions(-) diff --git a/components/layout/flexbox/layout.rs b/components/layout/flexbox/layout.rs index 3ddbb71ba89..c83ce4b3067 100644 --- a/components/layout/flexbox/layout.rs +++ b/components/layout/flexbox/layout.rs @@ -32,7 +32,7 @@ use crate::formatting_contexts::{Baselines, IndependentFormattingContextContents use crate::fragment_tree::{ BoxFragment, CollapsedBlockMargins, Fragment, FragmentFlags, SpecificLayoutInfo, }; -use crate::geom::{AuOrAuto, LogicalRect, LogicalSides, LogicalVec2, Size, Sizes}; +use crate::geom::{AuOrAuto, LazySize, LogicalRect, LogicalSides, LogicalVec2, Size, Sizes}; use crate::layout_box_base::CacheableLayoutResult; use crate::positioned::{ AbsolutelyPositionedBox, PositioningContext, PositioningContextLength, relative_adjustement, @@ -1936,6 +1936,27 @@ impl FlexItem<'_> { } } + let lazy_block_size = if cross_axis_is_item_block_axis { + // 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)); + LazySize::new( + &self.content_cross_sizes, + Direction::Block, + Size::FitContent, + Au::zero, + stretch_size, + self.is_table(), + ) + } else { + used_main_size.into() + }; + let layout = non_replaced.layout( flex_context.layout_context, &mut positioning_context, @@ -1945,6 +1966,7 @@ impl FlexItem<'_> { flex_axis == FlexAxis::Column || self.cross_size_stretches_to_line || self.depends_on_block_constraints, + &lazy_block_size, ); let CacheableLayoutResult { fragments, @@ -1962,22 +1984,7 @@ impl FlexItem<'_> { }); let hypothetical_cross_size = if cross_axis_is_item_block_axis { - // 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)); - self.content_cross_sizes.resolve( - Direction::Block, - Size::FitContent, - Au::zero, - stretch_size, - || content_block_size.into(), - self.is_table(), - ) + lazy_block_size.resolve(|| content_block_size) } else { inline_size }; @@ -2687,6 +2694,7 @@ impl FlexItemBox { flex_context.containing_block, &self.independent_formatting_context.base, false, /* depends_on_block_constraints */ + &LazySize::intrinsic(), ) .content_block_size }; diff --git a/components/layout/flow/mod.rs b/components/layout/flow/mod.rs index 6adb63153d6..99b84d088e5 100644 --- a/components/layout/flow/mod.rs +++ b/components/layout/flow/mod.rs @@ -36,8 +36,8 @@ use crate::fragment_tree::{ BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, FragmentFlags, }; use crate::geom::{ - AuOrAuto, LogicalRect, LogicalSides, LogicalSides1D, LogicalVec2, PhysicalPoint, PhysicalRect, - PhysicalSides, Size, Sizes, ToLogical, ToLogicalWithContainingBlock, + AuOrAuto, LazySize, LogicalRect, LogicalSides, LogicalSides1D, LogicalVec2, PhysicalPoint, + PhysicalRect, PhysicalSides, Size, Sizes, ToLogical, ToLogicalWithContainingBlock, }; use crate::layout_box_base::{CacheableLayoutResult, LayoutBoxBase}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext, PositioningContextLength}; @@ -1217,6 +1217,15 @@ impl IndependentNonReplacedContents { ignore_block_margins_for_stretch, ); + let lazy_block_size = LazySize::new( + &block_sizes, + Direction::Block, + Size::FitContent, + Au::zero, + available_block_size, + layout_style.is_table(), + ); + let layout = self.layout( layout_context, positioning_context, @@ -1224,19 +1233,13 @@ impl IndependentNonReplacedContents { containing_block, base, false, /* depends_on_block_constraints */ + &lazy_block_size, ); let inline_size = layout .content_inline_size_for_table .unwrap_or(containing_block_for_children.size.inline); - let block_size = block_sizes.resolve( - Direction::Block, - Size::FitContent, - Au::zero, - available_block_size, - || layout.content_block_size.into(), - layout_style.is_table(), - ); + let block_size = lazy_block_size.resolve(|| layout.content_block_size); let ResolvedMargins { margin, @@ -1369,16 +1372,14 @@ impl IndependentNonReplacedContents { ) }; - let compute_block_size = |layout: &CacheableLayoutResult| { - content_box_sizes.block.resolve( - Direction::Block, - Size::FitContent, - Au::zero, - available_block_size, - || layout.content_block_size.into(), - is_table, - ) - }; + let lazy_block_size = LazySize::new( + &content_box_sizes.block, + Direction::Block, + Size::FitContent, + Au::zero, + available_block_size, + is_table, + ); // The final inline size can depend on the available space, which depends on where // we are placing the box, since floats reduce the available space. @@ -1407,10 +1408,11 @@ impl IndependentNonReplacedContents { containing_block, base, false, /* depends_on_block_constraints */ + &lazy_block_size, ); content_size = LogicalVec2 { - block: compute_block_size(&layout), + block: lazy_block_size.resolve(|| layout.content_block_size), inline: layout.content_inline_size_for_table.unwrap_or(inline_size), }; @@ -1472,6 +1474,7 @@ impl IndependentNonReplacedContents { containing_block, base, false, /* depends_on_block_constraints */ + &lazy_block_size, ); let inline_size = if let Some(inline_size) = layout.content_inline_size_for_table { @@ -1485,7 +1488,7 @@ impl IndependentNonReplacedContents { proposed_inline_size }; content_size = LogicalVec2 { - block: compute_block_size(&layout), + block: lazy_block_size.resolve(|| layout.content_block_size), inline: inline_size, }; @@ -2419,6 +2422,15 @@ impl IndependentFormattingContext { "Mixed horizontal and vertical writing modes are not supported yet" ); + let lazy_block_size = LazySize::new( + &content_box_sizes_and_pbm.content_box_sizes.block, + Direction::Block, + Size::FitContent, + Au::zero, + available_block_size, + is_table, + ); + let independent_layout = non_replaced.layout( layout_context, child_positioning_context, @@ -2426,18 +2438,12 @@ impl IndependentFormattingContext { containing_block, &self.base, false, /* depends_on_block_constraints */ + &lazy_block_size, ); let inline_size = independent_layout .content_inline_size_for_table .unwrap_or(inline_size); - let block_size = content_box_sizes_and_pbm.content_box_sizes.block.resolve( - Direction::Block, - Size::FitContent, - Au::zero, - available_block_size, - || independent_layout.content_block_size.into(), - is_table, - ); + let block_size = lazy_block_size.resolve(|| independent_layout.content_block_size); let content_size = LogicalVec2 { block: block_size, diff --git a/components/layout/formatting_contexts.rs b/components/layout/formatting_contexts.rs index d704011d0e7..3942af63312 100644 --- a/components/layout/formatting_contexts.rs +++ b/components/layout/formatting_contexts.rs @@ -15,6 +15,7 @@ use crate::dom_traversal::{Contents, NodeAndStyleInfo}; use crate::flexbox::FlexContainer; use crate::flow::BlockFormattingContext; use crate::fragment_tree::{BaseFragmentInfo, FragmentFlags}; +use crate::geom::LazySize; use crate::layout_box_base::{ CacheableLayoutResult, CacheableLayoutResultAndInputs, LayoutBoxBase, }; @@ -242,6 +243,7 @@ impl IndependentNonReplacedContents { containing_block_for_children: &ContainingBlock, containing_block: &ContainingBlock, depends_on_block_constraints: bool, + _lazy_block_size: &LazySize, ) -> CacheableLayoutResult { match self { IndependentNonReplacedContents::Flow(bfc) => bfc.layout( @@ -281,6 +283,7 @@ impl IndependentNonReplacedContents { level = "trace", ) )] + #[allow(clippy::too_many_arguments)] pub fn layout( &self, layout_context: &LayoutContext, @@ -289,6 +292,7 @@ impl IndependentNonReplacedContents { containing_block: &ContainingBlock, base: &LayoutBoxBase, depends_on_block_constraints: bool, + lazy_block_size: &LazySize, ) -> CacheableLayoutResult { if let Some(cache) = base.cached_layout_result.borrow().as_ref() { let cache = &**cache; @@ -317,6 +321,7 @@ impl IndependentNonReplacedContents { containing_block_for_children, containing_block, depends_on_block_constraints, + lazy_block_size, ); *base.cached_layout_result.borrow_mut() = Some(Box::new(CacheableLayoutResultAndInputs { diff --git a/components/layout/geom.rs b/components/layout/geom.rs index 6a09519b7ed..4065b785832 100644 --- a/components/layout/geom.rs +++ b/components/layout/geom.rs @@ -2,7 +2,7 @@ * 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::LazyCell; +use std::cell::{LazyCell, OnceCell}; use std::convert::From; use std::fmt; use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; @@ -1102,3 +1102,87 @@ impl Sizes { ) } } + +struct LazySizeData<'a> { + sizes: &'a Sizes, + axis: Direction, + automatic_size: Size, + get_automatic_minimum_size: fn() -> Au, + stretch_size: Option, + is_table: bool, +} + +/// Represents a size that can't be fully resolved until the intrinsic size +/// is known. This is useful in the block axis, since the intrinsic size +/// depends on layout, but the other inputs are known beforehand. +pub(crate) struct LazySize<'a> { + result: OnceCell, + data: Option>, +} + +impl<'a> LazySize<'a> { + pub(crate) fn new( + sizes: &'a Sizes, + axis: Direction, + automatic_size: Size, + get_automatic_minimum_size: fn() -> Au, + stretch_size: Option, + is_table: bool, + ) -> Self { + Self { + result: OnceCell::new(), + data: Some(LazySizeData { + sizes, + axis, + automatic_size, + get_automatic_minimum_size, + stretch_size, + is_table, + }), + } + } + + /// Creates a [`LazySize`] that will resolve to the intrinsic size. + /// Should be equivalent to [`LazySize::new()`] with default parameters, + /// but avoiding the trouble of getting a reference to a [`Sizes::default()`] + /// which lives long enough. + /// + /// TODO: It's not clear what this should do if/when [`LazySize::resolve()`] + /// is changed to accept a [`ContentSizes`] as the intrinsic size. + pub(crate) fn intrinsic() -> Self { + Self { + result: OnceCell::new(), + data: None, + } + } + + /// Resolves the [`LazySize`] into [`Au`], caching the result. + /// The argument is a callback that computes the intrinsic size lazily. + /// + /// TODO: The intrinsic size should probably be a [`ContentSizes`] instead of [`Au`]. + pub(crate) fn resolve(&self, get_content_size: impl FnOnce() -> Au) -> Au { + *self.result.get_or_init(|| { + let Some(ref data) = self.data else { + return get_content_size(); + }; + data.sizes.resolve( + data.axis, + data.automatic_size, + data.get_automatic_minimum_size, + data.stretch_size, + || get_content_size().into(), + data.is_table, + ) + }) + } +} + +impl From for LazySize<'_> { + /// Creates a [`LazySize`] that will resolve to the given [`Au`], + /// ignoring the intrinsic size. + fn from(value: Au) -> Self { + let result = OnceCell::new(); + result.set(value).unwrap(); + LazySize { result, data: None } + } +} diff --git a/components/layout/positioned.rs b/components/layout/positioned.rs index b607462d6eb..6280864d533 100644 --- a/components/layout/positioned.rs +++ b/components/layout/positioned.rs @@ -24,12 +24,11 @@ use crate::fragment_tree::{ BoxFragment, Fragment, FragmentFlags, HoistedSharedFragment, SpecificLayoutInfo, }; use crate::geom::{ - AuOrAuto, LengthPercentageOrAuto, LogicalRect, LogicalSides, LogicalSides1D, LogicalVec2, - PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, PhysicalVec, Size, Sizes, ToLogical, - ToLogicalWithContainingBlock, + AuOrAuto, LazySize, LengthPercentageOrAuto, LogicalRect, LogicalSides, LogicalSides1D, + LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, PhysicalVec, Size, + Sizes, ToLogical, ToLogicalWithContainingBlock, }; use crate::layout_box_base::LayoutBoxBase; -use crate::sizing::ContentSizes; use crate::style_ext::{Clamp, ComputedValuesExt, ContentBoxSizesAndPBM, DisplayInside}; use crate::{ ConstraintSpace, ContainingBlock, ContainingBlockSize, DefiniteContainingBlock, @@ -560,18 +559,32 @@ impl HoistedAbsolutelyPositionedBox { // The block size can depend on layout results, so we only solve it extrinsically, // we may have to resolve it properly later on. - let extrinsic_block_size = block_axis_solver.solve_size_extrinsically(); + let block_automatic_size = block_axis_solver.automatic_size(); + let block_stretch_size = Some(block_axis_solver.stretch_size()); + let extrinsic_block_size = block_axis_solver.computed_sizes.resolve_extrinsic( + block_automatic_size, + Au::zero(), + block_stretch_size, + ); // The inline axis can be fully resolved, computing intrinsic sizes using the // extrinsic block size. - let inline_size = inline_axis_solver.solve_size(|| { + let get_inline_content_size = || { let ratio = context.preferred_aspect_ratio(&pbm.padding_border_sums); let constraint_space = ConstraintSpace::new(extrinsic_block_size, style.writing_mode, ratio); context .inline_content_sizes(layout_context, &constraint_space) .sizes - }); + }; + let inline_size = inline_axis_solver.computed_sizes.resolve( + Direction::Inline, + inline_axis_solver.automatic_size(), + Au::zero, + Some(inline_axis_solver.stretch_size()), + get_inline_content_size, + is_table, + ); let containing_block_for_children = ContainingBlock { size: ContainingBlockSize { @@ -587,6 +600,14 @@ impl HoistedAbsolutelyPositionedBox { "Mixed horizontal and vertical writing modes are not supported yet" ); + let lazy_block_size = LazySize::new( + &block_axis_solver.computed_sizes, + Direction::Block, + block_automatic_size, + Au::zero, + block_stretch_size, + is_table, + ); let independent_layout = non_replaced.layout( layout_context, &mut positioning_context, @@ -594,6 +615,7 @@ impl HoistedAbsolutelyPositionedBox { containing_block, &context.base, false, /* depends_on_block_constraints */ + &lazy_block_size, ); // Tables can become narrower than predicted due to collapsed columns @@ -602,8 +624,8 @@ impl HoistedAbsolutelyPositionedBox { .unwrap_or(inline_size); // Now we can properly solve the block size. - let block_size = block_axis_solver - .solve_size(|| independent_layout.content_block_size.into()); + let block_size = + lazy_block_size.resolve(|| independent_layout.content_block_size); content_size = LogicalVec2 { inline: inline_size, @@ -765,27 +787,6 @@ impl AbsoluteAxisSolver<'_> { ) } - #[inline] - fn solve_size_extrinsically(&self) -> SizeConstraint { - self.computed_sizes.resolve_extrinsic( - self.automatic_size(), - Au::zero(), - Some(self.stretch_size()), - ) - } - - #[inline] - fn solve_size(&self, get_content_size: impl FnOnce() -> ContentSizes) -> Au { - self.computed_sizes.resolve( - self.axis, - self.automatic_size(), - Au::zero, - Some(self.stretch_size()), - get_content_size, - self.is_table, - ) - } - fn solve_margins(&self, size: Au) -> LogicalSides1D { if self.box_offsets.either_auto() { LogicalSides1D::new( diff --git a/components/layout/taffy/layout.rs b/components/layout/taffy/layout.rs index a5838c1bd65..61c4a0508e9 100644 --- a/components/layout/taffy/layout.rs +++ b/components/layout/taffy/layout.rs @@ -23,8 +23,8 @@ use crate::fragment_tree::{ BoxFragment, CollapsedBlockMargins, Fragment, FragmentFlags, SpecificLayoutInfo, }; use crate::geom::{ - LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, Size, SizeConstraint, - Sizes, + LazySize, LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, Size, + SizeConstraint, Sizes, }; use crate::layout_box_base::CacheableLayoutResult; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext, PositioningContextLength}; @@ -251,6 +251,12 @@ impl taffy::LayoutPartialTree for TaffyContainerContext<'_> { style, }; + let lazy_block_size = match content_box_known_dimensions.height { + // FIXME: use the correct min/max sizes. + None => LazySize::intrinsic(), + Some(height) => Au::from_f32_px(height).into(), + }; + child.positioning_context = PositioningContext::default(); let layout = non_replaced.layout_without_caching( self.layout_context, @@ -258,13 +264,16 @@ impl taffy::LayoutPartialTree for TaffyContainerContext<'_> { &content_box_size_override, containing_block, false, /* depends_on_block_constraints */ + &lazy_block_size, ); child.child_fragments = layout.fragments; self.child_specific_layout_infos[usize::from(node_id)] = layout.specific_layout_info; - let block_size = layout.content_block_size.to_f32_px(); + let block_size = lazy_block_size + .resolve(|| layout.content_block_size) + .to_f32_px(); let computed_size = taffy::Size { width: inline_size + pb_sum.inline,