mirror of
https://github.com/servo/servo.git
synced 2025-09-27 15:20:09 +01:00
This keyword is de-facto required for web compatibility, Firefox is also enabling it (https://bugzil.la/1988938). This patch just aliases it to `stretch`. Stylo PR: https://github.com/servo/stylo/pull/243 Testing: Various tests pass Signed-off-by: Oriol Brufau <obrufau@igalia.com>
826 lines
32 KiB
Rust
826 lines
32 KiB
Rust
/* 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/. */
|
||
|
||
//! <https://drafts.csswg.org/css-sizing/>
|
||
|
||
use std::cell::{LazyCell, OnceCell};
|
||
use std::ops::{Add, AddAssign};
|
||
|
||
use app_units::{Au, MAX_AU};
|
||
use malloc_size_of_derive::MallocSizeOf;
|
||
use style::Zero;
|
||
use style::logical_geometry::Direction;
|
||
use style::values::computed::{
|
||
LengthPercentage, MaxSize as StyleMaxSize, Percentage, Size as StyleSize,
|
||
};
|
||
|
||
use crate::context::LayoutContext;
|
||
use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, ContentBoxSizesAndPBM, LayoutStyle};
|
||
use crate::{ConstraintSpace, IndefiniteContainingBlock, LogicalVec2};
|
||
|
||
#[derive(PartialEq)]
|
||
pub(crate) enum IntrinsicSizingMode {
|
||
/// Used to refer to a min-content contribution or max-content contribution.
|
||
/// This is the size that a box contributes to its containing block’s min-content
|
||
/// or max-content size. Note this is based on the outer size of the box,
|
||
/// and takes into account the relevant sizing properties of the element.
|
||
/// <https://drafts.csswg.org/css-sizing-3/#contributions>
|
||
Contribution,
|
||
/// Used to refer to a min-content size or max-content size.
|
||
/// This is the size based on the contents of an element, without regard for its context.
|
||
/// Note this is usually based on the inner (content-box) size of the box,
|
||
/// and ignores the relevant sizing properties of the element.
|
||
/// <https://drafts.csswg.org/css-sizing-3/#intrinsic>
|
||
Size,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Default, MallocSizeOf)]
|
||
pub(crate) struct ContentSizes {
|
||
pub min_content: Au,
|
||
pub max_content: Au,
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/css-sizing/#intrinsic-sizes>
|
||
impl ContentSizes {
|
||
pub fn max(&self, other: Self) -> Self {
|
||
Self {
|
||
min_content: self.min_content.max(other.min_content),
|
||
max_content: self.max_content.max(other.max_content),
|
||
}
|
||
}
|
||
|
||
pub fn max_assign(&mut self, other: Self) {
|
||
*self = self.max(other);
|
||
}
|
||
|
||
pub fn union(&self, other: &Self) -> Self {
|
||
Self {
|
||
min_content: self.min_content.max(other.min_content),
|
||
max_content: self.max_content + other.max_content,
|
||
}
|
||
}
|
||
|
||
pub fn map(&self, f: impl Fn(Au) -> Au) -> Self {
|
||
Self {
|
||
min_content: f(self.min_content),
|
||
max_content: f(self.max_content),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Zero for ContentSizes {
|
||
fn zero() -> Self {
|
||
Au::zero().into()
|
||
}
|
||
|
||
fn is_zero(&self) -> bool {
|
||
self.min_content.is_zero() && self.max_content.is_zero()
|
||
}
|
||
}
|
||
|
||
impl Add for ContentSizes {
|
||
type Output = Self;
|
||
|
||
fn add(self, rhs: Self) -> Self {
|
||
Self {
|
||
min_content: self.min_content + rhs.min_content,
|
||
max_content: self.max_content + rhs.max_content,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl AddAssign for ContentSizes {
|
||
fn add_assign(&mut self, rhs: Self) {
|
||
*self = self.add(rhs)
|
||
}
|
||
}
|
||
|
||
impl ContentSizes {
|
||
/// Clamps the provided amount to be between the min-content and the max-content.
|
||
/// This is called "shrink-to-fit" in CSS2, and "fit-content" in CSS Sizing.
|
||
/// <https://drafts.csswg.org/css2/visudet.html#shrink-to-fit-float>
|
||
/// <https://drafts.csswg.org/css-sizing/#funcdef-width-fit-content>
|
||
pub fn shrink_to_fit(&self, available_size: Au) -> Au {
|
||
// This formula is slightly different than what the spec says,
|
||
// to ensure that the minimum wins for a malformed ContentSize
|
||
// whose min_content is larger than its max_content.
|
||
available_size.min(self.max_content).max(self.min_content)
|
||
}
|
||
}
|
||
|
||
impl From<Au> for ContentSizes {
|
||
fn from(size: Au) -> Self {
|
||
Self {
|
||
min_content: size,
|
||
max_content: size,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(crate) fn outer_inline(
|
||
layout_style: &LayoutStyle,
|
||
containing_block: &IndefiniteContainingBlock,
|
||
auto_minimum: &LogicalVec2<Au>,
|
||
auto_block_size_stretches_to_containing_block: bool,
|
||
is_replaced: bool,
|
||
establishes_containing_block: bool,
|
||
get_preferred_aspect_ratio: impl FnOnce(&LogicalVec2<Au>) -> Option<AspectRatio>,
|
||
get_inline_content_size: impl FnOnce(&ConstraintSpace) -> InlineContentSizesResult,
|
||
get_tentative_block_content_size: impl FnOnce(Option<AspectRatio>) -> Option<ContentSizes>,
|
||
) -> InlineContentSizesResult {
|
||
let ContentBoxSizesAndPBM {
|
||
content_box_sizes,
|
||
pbm,
|
||
mut depends_on_block_constraints,
|
||
preferred_size_computes_to_auto,
|
||
} = layout_style.content_box_sizes_and_padding_border_margin(containing_block);
|
||
let margin = pbm.margin.map(|v| v.auto_is(Au::zero));
|
||
let pbm_sums = LogicalVec2 {
|
||
block: pbm.padding_border_sums.block + margin.block_sum(),
|
||
inline: pbm.padding_border_sums.inline + margin.inline_sum(),
|
||
};
|
||
let style = layout_style.style();
|
||
let is_table = layout_style.is_table();
|
||
let content_size = LazyCell::new(|| {
|
||
let constraint_space = if establishes_containing_block {
|
||
let available_block_size = containing_block
|
||
.size
|
||
.block
|
||
.map(|v| Au::zero().max(v - pbm_sums.block));
|
||
let automatic_size = if preferred_size_computes_to_auto.block &&
|
||
auto_block_size_stretches_to_containing_block
|
||
{
|
||
depends_on_block_constraints = true;
|
||
Size::Stretch
|
||
} else {
|
||
Size::FitContent
|
||
};
|
||
let aspect_ratio = get_preferred_aspect_ratio(&pbm.padding_border_sums);
|
||
let block_size =
|
||
if let Some(block_content_size) = get_tentative_block_content_size(aspect_ratio) {
|
||
SizeConstraint::Definite(content_box_sizes.block.resolve(
|
||
Direction::Block,
|
||
automatic_size,
|
||
|| auto_minimum.block,
|
||
available_block_size,
|
||
|| block_content_size,
|
||
is_table,
|
||
))
|
||
} else {
|
||
content_box_sizes.block.resolve_extrinsic(
|
||
automatic_size,
|
||
auto_minimum.block,
|
||
available_block_size,
|
||
)
|
||
};
|
||
ConstraintSpace::new(block_size, style, aspect_ratio)
|
||
} else {
|
||
// This assumes that there is no preferred aspect ratio, or that there is no
|
||
// block size constraint to be transferred so the ratio is irrelevant.
|
||
// We only get into here for anonymous blocks, for which the assumption holds.
|
||
ConstraintSpace::new(
|
||
containing_block.size.block.into(),
|
||
containing_block.style,
|
||
None,
|
||
)
|
||
};
|
||
get_inline_content_size(&constraint_space)
|
||
});
|
||
let resolve_non_initial = |inline_size, stretch_values| {
|
||
Some(match inline_size {
|
||
Size::Initial => return None,
|
||
Size::Numeric(numeric) => (numeric, numeric, false),
|
||
Size::MinContent => (
|
||
content_size.sizes.min_content,
|
||
content_size.sizes.min_content,
|
||
content_size.depends_on_block_constraints,
|
||
),
|
||
Size::MaxContent => (
|
||
content_size.sizes.max_content,
|
||
content_size.sizes.max_content,
|
||
content_size.depends_on_block_constraints,
|
||
),
|
||
Size::FitContent => (
|
||
content_size.sizes.min_content,
|
||
content_size.sizes.max_content,
|
||
content_size.depends_on_block_constraints,
|
||
),
|
||
Size::FitContentFunction(size) => {
|
||
let size = content_size.sizes.shrink_to_fit(size);
|
||
(size, size, content_size.depends_on_block_constraints)
|
||
},
|
||
Size::Stretch => return stretch_values,
|
||
})
|
||
};
|
||
let (mut preferred_min_content, preferred_max_content, preferred_depends_on_block_constraints) =
|
||
resolve_non_initial(content_box_sizes.inline.preferred, None)
|
||
.unwrap_or_else(|| resolve_non_initial(Size::FitContent, None).unwrap());
|
||
let (mut min_min_content, mut min_max_content, mut min_depends_on_block_constraints) =
|
||
resolve_non_initial(
|
||
content_box_sizes.inline.min,
|
||
Some((Au::zero(), Au::zero(), false)),
|
||
)
|
||
.unwrap_or((auto_minimum.inline, auto_minimum.inline, false));
|
||
let (mut max_min_content, max_max_content, max_depends_on_block_constraints) =
|
||
resolve_non_initial(content_box_sizes.inline.max, None)
|
||
.map(|(min_content, max_content, depends_on_block_constraints)| {
|
||
(
|
||
Some(min_content),
|
||
Some(max_content),
|
||
depends_on_block_constraints,
|
||
)
|
||
})
|
||
.unwrap_or_default();
|
||
|
||
// https://drafts.csswg.org/css-sizing-3/#replaced-percentage-min-contribution
|
||
// > If the box is replaced, a cyclic percentage in the value of any max size property
|
||
// > or preferred size property (width/max-width/height/max-height), is resolved against
|
||
// > zero when calculating the min-content contribution in the corresponding axis.
|
||
//
|
||
// This means that e.g. the min-content contribution of `width: calc(100% + 100px)`
|
||
// should be 100px, but it's just zero on other browsers, so we do the same.
|
||
if is_replaced {
|
||
let has_percentage = |size: Size<LengthPercentage>| {
|
||
// We need a comment here to avoid breaking `./mach test-tidy`.
|
||
matches!(size, Size::Numeric(numeric) if numeric.has_percentage())
|
||
};
|
||
let writing_mode = containing_block.style.writing_mode;
|
||
if content_box_sizes.inline.preferred.is_initial() &&
|
||
has_percentage(style.box_size(writing_mode).inline)
|
||
{
|
||
preferred_min_content = Au::zero();
|
||
}
|
||
if content_box_sizes.inline.max.is_initial() &&
|
||
has_percentage(style.max_box_size(writing_mode).inline)
|
||
{
|
||
max_min_content = Some(Au::zero());
|
||
}
|
||
}
|
||
|
||
// Regardless of their sizing properties, tables are always forced to be at least
|
||
// as big as their min-content size, so floor the minimums.
|
||
if is_table {
|
||
min_min_content.max_assign(content_size.sizes.min_content);
|
||
min_max_content.max_assign(content_size.sizes.min_content);
|
||
min_depends_on_block_constraints |= content_size.depends_on_block_constraints;
|
||
}
|
||
|
||
InlineContentSizesResult {
|
||
sizes: ContentSizes {
|
||
min_content: preferred_min_content
|
||
.clamp_between_extremums(min_min_content, max_min_content) +
|
||
pbm_sums.inline,
|
||
max_content: preferred_max_content
|
||
.clamp_between_extremums(min_max_content, max_max_content) +
|
||
pbm_sums.inline,
|
||
},
|
||
depends_on_block_constraints: depends_on_block_constraints &&
|
||
(preferred_depends_on_block_constraints ||
|
||
min_depends_on_block_constraints ||
|
||
max_depends_on_block_constraints),
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, MallocSizeOf)]
|
||
pub(crate) struct InlineContentSizesResult {
|
||
pub sizes: ContentSizes,
|
||
pub depends_on_block_constraints: bool,
|
||
}
|
||
|
||
pub(crate) trait ComputeInlineContentSizes {
|
||
fn compute_inline_content_sizes(
|
||
&self,
|
||
layout_context: &LayoutContext,
|
||
constraint_space: &ConstraintSpace,
|
||
) -> InlineContentSizesResult;
|
||
|
||
/// Returns the same result as [`Self::compute_inline_content_sizes()`], but adjusted
|
||
/// to floor the max-content size by the min-content size.
|
||
/// This is being discussed in <https://github.com/w3c/csswg-drafts/issues/12076>.
|
||
fn compute_inline_content_sizes_with_fixup(
|
||
&self,
|
||
layout_context: &LayoutContext,
|
||
constraint_space: &ConstraintSpace,
|
||
) -> InlineContentSizesResult {
|
||
let mut result = self.compute_inline_content_sizes(layout_context, constraint_space);
|
||
let sizes = &mut result.sizes;
|
||
sizes.max_content.max_assign(sizes.min_content);
|
||
result
|
||
}
|
||
}
|
||
|
||
/// The possible values accepted by the sizing properties.
|
||
/// <https://drafts.csswg.org/css-sizing/#sizing-properties>
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub(crate) enum Size<T> {
|
||
/// Represents an `auto` value for the preferred and minimum size properties,
|
||
/// or `none` for the maximum size properties.
|
||
/// <https://drafts.csswg.org/css-sizing/#valdef-width-auto>
|
||
/// <https://drafts.csswg.org/css-sizing/#valdef-max-width-none>
|
||
Initial,
|
||
/// <https://drafts.csswg.org/css-sizing/#valdef-width-min-content>
|
||
MinContent,
|
||
/// <https://drafts.csswg.org/css-sizing/#valdef-width-max-content>
|
||
MaxContent,
|
||
/// <https://drafts.csswg.org/css-sizing-4/#valdef-width-fit-content>
|
||
FitContent,
|
||
/// <https://drafts.csswg.org/css-sizing-3/#funcdef-width-fit-content>
|
||
FitContentFunction(T),
|
||
/// <https://drafts.csswg.org/css-sizing-4/#valdef-width-stretch>
|
||
Stretch,
|
||
/// Represents a numeric `<length-percentage>`, but resolved as a `T`.
|
||
/// <https://drafts.csswg.org/css-sizing/#valdef-width-length-percentage-0>
|
||
Numeric(T),
|
||
}
|
||
|
||
impl<T: Copy> Copy for Size<T> {}
|
||
|
||
impl<T> Default for Size<T> {
|
||
#[inline]
|
||
fn default() -> Self {
|
||
Self::Initial
|
||
}
|
||
}
|
||
|
||
impl<T> Size<T> {
|
||
#[inline]
|
||
pub(crate) fn is_initial(&self) -> bool {
|
||
matches!(self, Self::Initial)
|
||
}
|
||
}
|
||
|
||
impl<T: Clone> Size<T> {
|
||
#[inline]
|
||
pub(crate) fn to_numeric(&self) -> Option<T> {
|
||
match self {
|
||
Self::Numeric(numeric) => Some(numeric).cloned(),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
pub(crate) fn map<U>(&self, f: impl FnOnce(T) -> U) -> Size<U> {
|
||
match self {
|
||
Size::Initial => Size::Initial,
|
||
Size::MinContent => Size::MinContent,
|
||
Size::MaxContent => Size::MaxContent,
|
||
Size::FitContent => Size::FitContent,
|
||
Size::FitContentFunction(size) => Size::FitContentFunction(f(size.clone())),
|
||
Size::Stretch => Size::Stretch,
|
||
Size::Numeric(numeric) => Size::Numeric(f(numeric.clone())),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<StyleSize> for Size<LengthPercentage> {
|
||
fn from(size: StyleSize) -> Self {
|
||
match size {
|
||
StyleSize::LengthPercentage(lp) => Size::Numeric(lp.0),
|
||
StyleSize::Auto => Size::Initial,
|
||
StyleSize::MinContent => Size::MinContent,
|
||
StyleSize::MaxContent => Size::MaxContent,
|
||
StyleSize::FitContent => Size::FitContent,
|
||
StyleSize::FitContentFunction(lp) => Size::FitContentFunction(lp.0),
|
||
StyleSize::Stretch | StyleSize::WebkitFillAvailable => Size::Stretch,
|
||
StyleSize::AnchorSizeFunction(_) | StyleSize::AnchorContainingCalcFunction(_) => {
|
||
unreachable!("anchor-size() should be disabled")
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<StyleMaxSize> for Size<LengthPercentage> {
|
||
fn from(max_size: StyleMaxSize) -> Self {
|
||
match max_size {
|
||
StyleMaxSize::LengthPercentage(lp) => Size::Numeric(lp.0),
|
||
StyleMaxSize::None => Size::Initial,
|
||
StyleMaxSize::MinContent => Size::MinContent,
|
||
StyleMaxSize::MaxContent => Size::MaxContent,
|
||
StyleMaxSize::FitContent => Size::FitContent,
|
||
StyleMaxSize::FitContentFunction(lp) => Size::FitContentFunction(lp.0),
|
||
StyleMaxSize::Stretch | StyleMaxSize::WebkitFillAvailable => Size::Stretch,
|
||
StyleMaxSize::AnchorSizeFunction(_) | StyleMaxSize::AnchorContainingCalcFunction(_) => {
|
||
unreachable!("anchor-size() should be disabled")
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Size<LengthPercentage> {
|
||
#[inline]
|
||
pub(crate) fn to_percentage(&self) -> Option<Percentage> {
|
||
self.to_numeric()
|
||
.and_then(|length_percentage| length_percentage.to_percentage())
|
||
}
|
||
|
||
/// Resolves percentages in a preferred size, against the provided basis.
|
||
/// If the basis is missing, percentages are considered cyclic.
|
||
/// <https://www.w3.org/TR/css-sizing-3/#preferred-size-properties>
|
||
/// <https://www.w3.org/TR/css-sizing-3/#cyclic-percentage-size>
|
||
#[inline]
|
||
pub(crate) fn resolve_percentages_for_preferred(&self, basis: Option<Au>) -> Size<Au> {
|
||
match self {
|
||
Size::Numeric(numeric) => numeric
|
||
.maybe_to_used_value(basis)
|
||
.map_or(Size::Initial, Size::Numeric),
|
||
Size::FitContentFunction(numeric) => {
|
||
// Under discussion in https://github.com/w3c/csswg-drafts/issues/11805
|
||
numeric
|
||
.maybe_to_used_value(basis)
|
||
.map_or(Size::FitContent, Size::FitContentFunction)
|
||
},
|
||
_ => self.map(|_| unreachable!("This shouldn't be called for keywords")),
|
||
}
|
||
}
|
||
|
||
/// Resolves percentages in a maximum size, against the provided basis.
|
||
/// If the basis is missing, percentages are considered cyclic.
|
||
/// <https://www.w3.org/TR/css-sizing-3/#preferred-size-properties>
|
||
/// <https://www.w3.org/TR/css-sizing-3/#cyclic-percentage-size>
|
||
#[inline]
|
||
pub(crate) fn resolve_percentages_for_max(&self, basis: Option<Au>) -> Size<Au> {
|
||
match self {
|
||
Size::Numeric(numeric) => numeric
|
||
.maybe_to_used_value(basis)
|
||
.map_or(Size::Initial, Size::Numeric),
|
||
Size::FitContentFunction(numeric) => {
|
||
// Under discussion in https://github.com/w3c/csswg-drafts/issues/11805
|
||
numeric
|
||
.maybe_to_used_value(basis)
|
||
.map_or(Size::MaxContent, Size::FitContentFunction)
|
||
},
|
||
_ => self.map(|_| unreachable!("This shouldn't be called for keywords")),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl LogicalVec2<Size<LengthPercentage>> {
|
||
pub(crate) fn percentages_relative_to_basis(
|
||
&self,
|
||
basis: &LogicalVec2<Au>,
|
||
) -> LogicalVec2<Size<Au>> {
|
||
LogicalVec2 {
|
||
inline: self.inline.map(|value| value.to_used_value(basis.inline)),
|
||
block: self.block.map(|value| value.to_used_value(basis.block)),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Size<Au> {
|
||
/// Resolves a preferred size into a numerical value.
|
||
/// <https://www.w3.org/TR/css-sizing-3/#preferred-size-properties>
|
||
#[inline]
|
||
pub(crate) fn resolve_for_preferred<F: FnOnce() -> ContentSizes>(
|
||
&self,
|
||
automatic_size: Size<Au>,
|
||
stretch_size: Option<Au>,
|
||
content_size: &LazyCell<ContentSizes, F>,
|
||
) -> Au {
|
||
match self {
|
||
Self::Initial => {
|
||
assert!(!automatic_size.is_initial());
|
||
automatic_size.resolve_for_preferred(automatic_size, stretch_size, content_size)
|
||
},
|
||
Self::MinContent => content_size.min_content,
|
||
Self::MaxContent => content_size.max_content,
|
||
Self::FitContentFunction(size) => content_size.shrink_to_fit(*size),
|
||
Self::FitContent => {
|
||
content_size.shrink_to_fit(stretch_size.unwrap_or_else(|| content_size.max_content))
|
||
},
|
||
Self::Stretch => stretch_size.unwrap_or_else(|| content_size.max_content),
|
||
Self::Numeric(numeric) => *numeric,
|
||
}
|
||
}
|
||
|
||
/// Resolves a minimum size into a numerical value.
|
||
/// <https://www.w3.org/TR/css-sizing-3/#min-size-properties>
|
||
#[inline]
|
||
pub(crate) fn resolve_for_min<F: FnOnce() -> ContentSizes>(
|
||
&self,
|
||
get_automatic_minimum_size: impl FnOnce() -> Au,
|
||
stretch_size: Option<Au>,
|
||
content_size: &LazyCell<ContentSizes, F>,
|
||
is_table: bool,
|
||
) -> Au {
|
||
let result = match self {
|
||
Self::Initial => get_automatic_minimum_size(),
|
||
Self::MinContent => content_size.min_content,
|
||
Self::MaxContent => content_size.max_content,
|
||
Self::FitContentFunction(size) => content_size.shrink_to_fit(*size),
|
||
Self::FitContent => content_size.shrink_to_fit(stretch_size.unwrap_or_default()),
|
||
Self::Stretch => stretch_size.unwrap_or_default(),
|
||
Self::Numeric(numeric) => *numeric,
|
||
};
|
||
if is_table {
|
||
// In addition to the specified minimum, the inline size of a table is forced to be
|
||
// at least as big as its min-content size.
|
||
//
|
||
// Note that if there are collapsed columns, only the inline size of the table grid will
|
||
// shrink, while the size of the table wrapper (being computed here) won't be affected.
|
||
// However, collapsed rows should typically affect the block size of the table wrapper,
|
||
// so it might be wrong to use this function for that case.
|
||
// This is being discussed in https://github.com/w3c/csswg-drafts/issues/11408
|
||
result.max(content_size.min_content)
|
||
} else {
|
||
result
|
||
}
|
||
}
|
||
|
||
/// Resolves a maximum size into a numerical value.
|
||
/// <https://www.w3.org/TR/css-sizing-3/#max-size-properties>
|
||
#[inline]
|
||
pub(crate) fn resolve_for_max<F: FnOnce() -> ContentSizes>(
|
||
&self,
|
||
stretch_size: Option<Au>,
|
||
content_size: &LazyCell<ContentSizes, F>,
|
||
) -> Option<Au> {
|
||
Some(match self {
|
||
Self::Initial => return None,
|
||
Self::MinContent => content_size.min_content,
|
||
Self::MaxContent => content_size.max_content,
|
||
Self::FitContentFunction(size) => content_size.shrink_to_fit(*size),
|
||
Self::FitContent => content_size.shrink_to_fit(stretch_size.unwrap_or(MAX_AU)),
|
||
Self::Stretch => return stretch_size,
|
||
Self::Numeric(numeric) => *numeric,
|
||
})
|
||
}
|
||
|
||
/// Tries to resolve an extrinsic size into a numerical value.
|
||
/// Extrinsic sizes are those based on the context of an element, without regard for its contents.
|
||
/// <https://drafts.csswg.org/css-sizing-3/#extrinsic>
|
||
///
|
||
/// Returns `None` if either:
|
||
/// - The size is intrinsic.
|
||
/// - The size is the initial one.
|
||
/// TODO: should we allow it to behave as `stretch` instead of assuming it's intrinsic?
|
||
/// - The provided `stretch_size` is `None` but we need its value.
|
||
#[inline]
|
||
pub(crate) fn maybe_resolve_extrinsic(&self, stretch_size: Option<Au>) -> Option<Au> {
|
||
match self {
|
||
Self::Initial |
|
||
Self::MinContent |
|
||
Self::MaxContent |
|
||
Self::FitContent |
|
||
Self::FitContentFunction(_) => None,
|
||
Self::Stretch => stretch_size,
|
||
Self::Numeric(numeric) => Some(*numeric),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Represents the sizing constraint that the preferred, min and max sizing properties
|
||
/// impose on one axis.
|
||
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)]
|
||
pub(crate) enum SizeConstraint {
|
||
/// Represents a definite preferred size, clamped by minimum and maximum sizes (if any).
|
||
Definite(Au),
|
||
/// Represents an indefinite preferred size that allows a range of values between
|
||
/// the first argument (minimum size) and the second one (maximum size).
|
||
MinMax(Au, Option<Au>),
|
||
}
|
||
|
||
impl Default for SizeConstraint {
|
||
#[inline]
|
||
fn default() -> Self {
|
||
Self::MinMax(Au::default(), None)
|
||
}
|
||
}
|
||
|
||
impl SizeConstraint {
|
||
#[inline]
|
||
pub(crate) fn new(preferred_size: Option<Au>, min_size: Au, max_size: Option<Au>) -> Self {
|
||
preferred_size.map_or_else(
|
||
|| Self::MinMax(min_size, max_size),
|
||
|size| Self::Definite(size.clamp_between_extremums(min_size, max_size)),
|
||
)
|
||
}
|
||
|
||
#[inline]
|
||
pub(crate) fn is_definite(self) -> bool {
|
||
matches!(self, Self::Definite(_))
|
||
}
|
||
|
||
#[inline]
|
||
pub(crate) fn to_definite(self) -> Option<Au> {
|
||
match self {
|
||
Self::Definite(size) => Some(size),
|
||
_ => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<Option<Au>> for SizeConstraint {
|
||
fn from(size: Option<Au>) -> Self {
|
||
size.map(SizeConstraint::Definite).unwrap_or_default()
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Default)]
|
||
pub(crate) struct Sizes {
|
||
/// <https://drafts.csswg.org/css-sizing-3/#preferred-size-properties>
|
||
pub preferred: Size<Au>,
|
||
/// <https://drafts.csswg.org/css-sizing-3/#min-size-properties>
|
||
pub min: Size<Au>,
|
||
/// <https://drafts.csswg.org/css-sizing-3/#max-size-properties>
|
||
pub max: Size<Au>,
|
||
}
|
||
|
||
impl Sizes {
|
||
#[inline]
|
||
pub(crate) fn new(preferred: Size<Au>, min: Size<Au>, max: Size<Au>) -> Self {
|
||
Self {
|
||
preferred,
|
||
min,
|
||
max,
|
||
}
|
||
}
|
||
|
||
/// Resolves the three sizes into a single numerical value.
|
||
#[inline]
|
||
pub(crate) fn resolve(
|
||
&self,
|
||
axis: Direction,
|
||
automatic_size: Size<Au>,
|
||
get_automatic_minimum_size: impl FnOnce() -> Au,
|
||
stretch_size: Option<Au>,
|
||
get_content_size: impl FnOnce() -> ContentSizes,
|
||
is_table: bool,
|
||
) -> Au {
|
||
if is_table && axis == Direction::Block {
|
||
// The intrinsic block size of a table already takes sizing properties into account,
|
||
// but it can be a smaller amount if there are collapsed rows.
|
||
// Therefore, disregard sizing properties and just defer to the intrinsic size.
|
||
// This is being discussed in https://github.com/w3c/csswg-drafts/issues/11408
|
||
return get_content_size().max_content;
|
||
}
|
||
let (preferred, min, max) = self.resolve_each(
|
||
automatic_size,
|
||
get_automatic_minimum_size,
|
||
stretch_size,
|
||
get_content_size,
|
||
is_table,
|
||
);
|
||
preferred.clamp_between_extremums(min, max)
|
||
}
|
||
|
||
/// Resolves each of the three sizes into a numerical value, separately.
|
||
/// - The 1st returned value is the resolved preferred size.
|
||
/// - The 2nd returned value is the resolved minimum size.
|
||
/// - The 3rd returned value is the resolved maximum size. `None` means no maximum.
|
||
#[inline]
|
||
pub(crate) fn resolve_each(
|
||
&self,
|
||
automatic_size: Size<Au>,
|
||
get_automatic_minimum_size: impl FnOnce() -> Au,
|
||
stretch_size: Option<Au>,
|
||
get_content_size: impl FnOnce() -> ContentSizes,
|
||
is_table: bool,
|
||
) -> (Au, Au, Option<Au>) {
|
||
// The provided `get_content_size` is a FnOnce but we may need its result multiple times.
|
||
// A LazyCell will only invoke it once if needed, and then reuse the result.
|
||
let content_size = LazyCell::new(get_content_size);
|
||
(
|
||
self.preferred
|
||
.resolve_for_preferred(automatic_size, stretch_size, &content_size),
|
||
self.min.resolve_for_min(
|
||
get_automatic_minimum_size,
|
||
stretch_size,
|
||
&content_size,
|
||
is_table,
|
||
),
|
||
self.max.resolve_for_max(stretch_size, &content_size),
|
||
)
|
||
}
|
||
|
||
/// Tries to extrinsically resolve the three sizes into a single [`SizeConstraint`].
|
||
/// Values that are intrinsic or need `stretch_size` when it's `None` are handled as such:
|
||
/// - On the preferred size, they make the returned value be an indefinite [`SizeConstraint::MinMax`].
|
||
/// - On the min size, they are treated as `auto`, enforcing the automatic minimum size.
|
||
/// - On the max size, they are treated as `none`, enforcing no maximum.
|
||
#[inline]
|
||
pub(crate) fn resolve_extrinsic(
|
||
&self,
|
||
automatic_size: Size<Au>,
|
||
automatic_minimum_size: Au,
|
||
stretch_size: Option<Au>,
|
||
) -> SizeConstraint {
|
||
let (preferred, min, max) =
|
||
self.resolve_each_extrinsic(automatic_size, automatic_minimum_size, stretch_size);
|
||
SizeConstraint::new(preferred, min, max)
|
||
}
|
||
|
||
/// Tries to extrinsically resolve each of the three sizes into a numerical value, separately.
|
||
/// This can't resolve values that are intrinsic or need `stretch_size` but it's `None`.
|
||
/// - The 1st returned value is the resolved preferred size. If it can't be resolved then
|
||
/// the returned value is `None`. Note that this is different than treating it as `auto`.
|
||
/// TODO: This needs to be discussed in <https://github.com/w3c/csswg-drafts/issues/11387>.
|
||
/// - The 2nd returned value is the resolved minimum size. If it can't be resolved then we
|
||
/// treat it as the initial `auto`, returning the automatic minimum size.
|
||
/// - The 3rd returned value is the resolved maximum size. If it can't be resolved then we
|
||
/// treat it as the initial `none`, returning `None`.
|
||
#[inline]
|
||
pub(crate) fn resolve_each_extrinsic(
|
||
&self,
|
||
automatic_size: Size<Au>,
|
||
automatic_minimum_size: Au,
|
||
stretch_size: Option<Au>,
|
||
) -> (Option<Au>, Au, Option<Au>) {
|
||
(
|
||
if self.preferred.is_initial() {
|
||
automatic_size.maybe_resolve_extrinsic(stretch_size)
|
||
} else {
|
||
self.preferred.maybe_resolve_extrinsic(stretch_size)
|
||
},
|
||
self.min
|
||
.maybe_resolve_extrinsic(stretch_size)
|
||
.unwrap_or(automatic_minimum_size),
|
||
self.max.maybe_resolve_extrinsic(stretch_size),
|
||
)
|
||
}
|
||
}
|
||
|
||
struct LazySizeData<'a> {
|
||
sizes: &'a Sizes,
|
||
axis: Direction,
|
||
automatic_size: Size<Au>,
|
||
get_automatic_minimum_size: fn() -> Au,
|
||
stretch_size: Option<Au>,
|
||
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<Au>,
|
||
data: Option<LazySizeData<'a>>,
|
||
}
|
||
|
||
impl<'a> LazySize<'a> {
|
||
pub(crate) fn new(
|
||
sizes: &'a Sizes,
|
||
axis: Direction,
|
||
automatic_size: Size<Au>,
|
||
get_automatic_minimum_size: fn() -> Au,
|
||
stretch_size: Option<Au>,
|
||
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<Au> 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 }
|
||
}
|
||
}
|