layout: Implement the fit-content() sizing function (#36056)

Spec: https://drafts.csswg.org/css-sizing-3/#funcdef-width-fit-content

It's similar to the `fit-content` keyword but, instead of clamping the
stretch size between `min-content` and `max-content`, it clamps the
provided argument.

So now that we support `fit-content`, it's quite straightforward to add.
It's just not completely clear what should happen when the argument has
a cyclic percentage, so this may need some further adjustments depending
on the outcome of https://github.com/w3c/csswg-drafts/issues/11805

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Oriol Brufau 2025-03-23 15:26:44 +01:00 committed by GitHub
parent 3a356ffb74
commit 8c2ac88ad0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 100 additions and 192 deletions

24
Cargo.lock generated
View file

@ -6513,7 +6513,7 @@ dependencies = [
[[package]]
name = "selectors"
version = "0.26.0"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"bitflags 2.9.0",
"cssparser",
@ -6798,7 +6798,7 @@ dependencies = [
[[package]]
name = "servo_arc"
version = "0.4.0"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"serde",
"stable_deref_trait",
@ -7248,7 +7248,7 @@ dependencies = [
[[package]]
name = "stylo"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"app_units",
"arrayvec",
@ -7306,7 +7306,7 @@ dependencies = [
[[package]]
name = "stylo_atoms"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"string_cache",
"string_cache_codegen",
@ -7315,12 +7315,12 @@ dependencies = [
[[package]]
name = "stylo_config"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
[[package]]
name = "stylo_derive"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"darling",
"proc-macro2",
@ -7332,7 +7332,7 @@ dependencies = [
[[package]]
name = "stylo_dom"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"bitflags 2.9.0",
"stylo_malloc_size_of",
@ -7341,7 +7341,7 @@ dependencies = [
[[package]]
name = "stylo_malloc_size_of"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"app_units",
"cssparser",
@ -7358,12 +7358,12 @@ dependencies = [
[[package]]
name = "stylo_static_prefs"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
[[package]]
name = "stylo_traits"
version = "0.0.1"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"app_units",
"bitflags 2.9.0",
@ -7746,7 +7746,7 @@ dependencies = [
[[package]]
name = "to_shmem"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"cssparser",
"servo_arc",
@ -7759,7 +7759,7 @@ dependencies = [
[[package]]
name = "to_shmem_derive"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#4b44fbdb7f93c3f57eb99ad5f14cda5e82af4467"
source = "git+https://github.com/servo/stylo?branch=2025-03-15#bd0a7031b68ed87f9dbba0714e735fa5cf9df676"
dependencies = [
"darling",
"proc-macro2",

View file

@ -2599,11 +2599,8 @@ impl FlexItemBox {
BoxSizing::BorderBox => length - main_padding_border_sum,
}
};
size.maybe_map(|v| {
v.maybe_to_used_value(container_definite_main_size)
size.resolve_percentages_for_preferred(container_definite_main_size)
.map(apply_box_sizing)
})
.unwrap_or_default()
},
},
}

View file

@ -2144,9 +2144,11 @@ impl<'container> PlacementState<'container> {
fn block_size_is_zero_or_intrinsic(size: &StyleSize, containing_block: &ContainingBlock) -> bool {
match size {
StyleSize::Auto | StyleSize::MinContent | StyleSize::MaxContent | StyleSize::FitContent => {
true
},
StyleSize::Auto |
StyleSize::MinContent |
StyleSize::MaxContent |
StyleSize::FitContent |
StyleSize::FitContentFunction(_) => true,
StyleSize::Stretch => {
// TODO: Should this return true when the containing block has a definite size of 0px?
!containing_block.size.block.is_definite()

View file

@ -133,6 +133,17 @@ impl<T: Clone> LogicalVec2<T> {
block: f(&self.block),
}
}
pub(crate) fn map_with<U, V>(
&self,
other: &LogicalVec2<U>,
f: impl Fn(&T, &U) -> V,
) -> LogicalVec2<V> {
LogicalVec2 {
inline: f(&self.inline, &other.inline),
block: f(&self.block, &other.block),
}
}
}
impl<T: Add<Output = T> + Copy> Add<LogicalVec2<T>> for LogicalVec2<T> {
@ -697,6 +708,8 @@ pub(crate) enum Size<T> {
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`.
@ -730,34 +743,28 @@ impl<T: Clone> Size<T> {
}
#[inline]
pub fn map<U>(&self, f: impl FnOnce(T) -> U) -> Size<U> {
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())),
}
}
#[inline]
pub fn maybe_map<U>(&self, f: impl FnOnce(T) -> Option<U>) -> Option<Size<U>> {
Some(match self {
Size::Numeric(numeric) => Size::Numeric(f(numeric.clone())?),
_ => self.map(|_| unreachable!("This shouldn't be called for keywords")),
})
}
}
impl From<StyleSize> for Size<LengthPercentage> {
fn from(size: StyleSize) -> Self {
match size {
StyleSize::LengthPercentage(length) => Size::Numeric(length.0),
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 => Size::Stretch,
StyleSize::AnchorSizeFunction(_) => unreachable!("anchor-size() should be disabled"),
}
@ -767,11 +774,12 @@ impl From<StyleSize> for Size<LengthPercentage> {
impl From<StyleMaxSize> for Size<LengthPercentage> {
fn from(max_size: StyleMaxSize) -> Self {
match max_size {
StyleMaxSize::LengthPercentage(length) => Size::Numeric(length.0),
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 => Size::Stretch,
StyleMaxSize::AnchorSizeFunction(_) => unreachable!("anchor-size() should be disabled"),
}
@ -784,25 +792,49 @@ impl Size<LengthPercentage> {
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 maybe_percentages_relative_to_basis(
&self,
basis: &LogicalVec2<Option<Au>>,
) -> LogicalVec2<Size<Au>> {
LogicalVec2 {
inline: self
.inline
.maybe_map(|v| v.maybe_to_used_value(basis.inline))
.unwrap_or_default(),
block: self
.block
.maybe_map(|v| v.maybe_to_used_value(basis.block))
.unwrap_or_default(),
}
}
pub(crate) fn percentages_relative_to_basis(
&self,
basis: &LogicalVec2<Au>,
@ -831,6 +863,7 @@ impl Size<Au> {
},
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))
},
@ -852,6 +885,7 @@ impl Size<Au> {
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,
@ -870,6 +904,7 @@ impl Size<Au> {
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,
@ -888,7 +923,11 @@ impl Size<Au> {
#[inline]
pub(crate) fn maybe_resolve_extrinsic(&self, stretch_size: Option<Au>) -> Option<Au> {
match self {
Self::Initial | Self::MinContent | Self::MaxContent | Self::FitContent => None,
Self::Initial |
Self::MinContent |
Self::MaxContent |
Self::FitContent |
Self::FitContentFunction(_) => None,
Self::Stretch => stretch_size,
Self::Numeric(numeric) => Some(*numeric),
}

View file

@ -191,6 +191,10 @@ pub(crate) fn outer_inline(
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,
})
};

View file

@ -1014,18 +1014,16 @@ impl LayoutStyle<'_> {
depends_on_block_constraints(&max_size.block) ||
style.depends_on_block_constraints_due_to_relative_positioning(writing_mode);
let box_size = box_size.maybe_percentages_relative_to_basis(&containing_block.size);
let content_box_size = style
.content_box_size_for_box_size(box_size, &pbm)
.map(|v| v.map(Au::from));
let box_size = box_size.map_with(&containing_block.size, |size, basis| {
size.resolve_percentages_for_preferred(*basis)
});
let content_box_size = style.content_box_size_for_box_size(box_size, &pbm);
let min_size = min_size.percentages_relative_to_basis(&containing_block_size_or_zero);
let content_min_box_size = style
.content_min_box_size_for_min_size(min_size, &pbm)
.map(|v| v.map(Au::from));
let max_size = max_size.maybe_percentages_relative_to_basis(&containing_block.size);
let content_max_box_size = style
.content_max_box_size_for_max_size(max_size, &pbm)
.map(|v| v.map(Au::from));
let content_min_box_size = style.content_min_box_size_for_min_size(min_size, &pbm);
let max_size = max_size.map_with(&containing_block.size, |size, basis| {
size.resolve_percentages_for_max(*basis)
});
let content_max_box_size = style.content_max_box_size_for_max_size(max_size, &pbm);
ContentBoxSizesAndPBM {
content_box_sizes: LogicalVec2 {
block: Sizes::new(

View file

@ -54,6 +54,7 @@ pub fn dimension(val: &stylo::Size) -> taffy::Dimension {
stylo::Size::MaxContent => taffy::Dimension::Auto,
stylo::Size::MinContent => taffy::Dimension::Auto,
stylo::Size::FitContent => taffy::Dimension::Auto,
stylo::Size::FitContentFunction(_) => taffy::Dimension::Auto,
stylo::Size::Stretch => taffy::Dimension::Auto,
// Anchor positioning will be flagged off for time being
@ -71,6 +72,7 @@ pub fn max_size_dimension(val: &stylo::MaxSize) -> taffy::Dimension {
stylo::MaxSize::MaxContent => taffy::Dimension::Auto,
stylo::MaxSize::MinContent => taffy::Dimension::Auto,
stylo::MaxSize::FitContent => taffy::Dimension::Auto,
stylo::MaxSize::FitContentFunction(_) => taffy::Dimension::Auto,
stylo::MaxSize::Stretch => taffy::Dimension::Auto,
// Anchor positioning will be flagged off for time being

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-001.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-002.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-003.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-004.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-005.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-006.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-007.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-008.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-009.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-010.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-011.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-012.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-013.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-014.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-015.html]
expected: FAIL

View file

@ -1,2 +0,0 @@
[fit-content-length-percentage-016.html]
expected: FAIL

View file

@ -1,9 +0,0 @@
[height-valid.html]
[e.style['height'\] = "fit-content(10%)" should set the property value]
expected: FAIL
[e.style['height'\] = "fit-content(0.5em)" should set the property value]
expected: FAIL
[e.style['height'\] = "fit-content(calc(10% - 0.5em))" should set the property value]
expected: FAIL

View file

@ -1,12 +0,0 @@
[max-height-computed.html]
[Property max-height value 'fit-content(10px)']
expected: FAIL
[Property max-height value 'fit-content(20%)']
expected: FAIL
[Property max-height value 'fit-content(calc(10% + 40px))']
expected: FAIL
[Property max-height value 'fit-content(calc(10px + 0.5em))']
expected: FAIL

View file

@ -1,9 +0,0 @@
[max-height-valid.html]
[e.style['max-height'\] = "fit-content(10%)" should set the property value]
expected: FAIL
[e.style['max-height'\] = "fit-content(0.5em)" should set the property value]
expected: FAIL
[e.style['max-height'\] = "fit-content(calc(10% - 0.5em))" should set the property value]
expected: FAIL

View file

@ -1,12 +0,0 @@
[max-width-computed.html]
[Property max-width value 'fit-content(10px)']
expected: FAIL
[Property max-width value 'fit-content(20%)']
expected: FAIL
[Property max-width value 'fit-content(calc(10% + 40px))']
expected: FAIL
[Property max-width value 'fit-content(calc(10px + 0.5em))']
expected: FAIL

View file

@ -1,9 +0,0 @@
[max-width-valid.html]
[e.style['max-width'\] = "fit-content(10%)" should set the property value]
expected: FAIL
[e.style['max-width'\] = "fit-content(0.5em)" should set the property value]
expected: FAIL
[e.style['max-width'\] = "fit-content(calc(10% - 0.5em))" should set the property value]
expected: FAIL

View file

@ -1,12 +0,0 @@
[min-height-computed.html]
[Property min-height value 'fit-content(10px)']
expected: FAIL
[Property min-height value 'fit-content(20%)']
expected: FAIL
[Property min-height value 'fit-content(calc(10% + 40px))']
expected: FAIL
[Property min-height value 'fit-content(calc(10px + 0.5em))']
expected: FAIL

View file

@ -1,9 +0,0 @@
[min-height-valid.html]
[e.style['min-height'\] = "fit-content(10%)" should set the property value]
expected: FAIL
[e.style['min-height'\] = "fit-content(0.5em)" should set the property value]
expected: FAIL
[e.style['min-height'\] = "fit-content(calc(10% - 0.5em))" should set the property value]
expected: FAIL

View file

@ -1,12 +0,0 @@
[min-width-computed.html]
[Property min-width value 'fit-content(10px)']
expected: FAIL
[Property min-width value 'fit-content(20%)']
expected: FAIL
[Property min-width value 'fit-content(calc(10% + 40px))']
expected: FAIL
[Property min-width value 'fit-content(calc(10px + 0.5em))']
expected: FAIL

View file

@ -1,9 +0,0 @@
[min-width-valid.html]
[e.style['min-width'\] = "fit-content(10%)" should set the property value]
expected: FAIL
[e.style['min-width'\] = "fit-content(0.5em)" should set the property value]
expected: FAIL
[e.style['min-width'\] = "fit-content(calc(10% - 0.5em))" should set the property value]
expected: FAIL

View file

@ -1,9 +0,0 @@
[width-valid.html]
[e.style['width'\] = "fit-content(10%)" should set the property value]
expected: FAIL
[e.style['width'\] = "fit-content(0.5em)" should set the property value]
expected: FAIL
[e.style['width'\] = "fit-content(calc(10% - 0.5em))" should set the property value]
expected: FAIL