tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 1061HTML 997Markdown 12TOML 83120 Definitions

fit content

303768a02ae67d8e068c0ff853da0dc08e6f8618 · by Jonathan Kelley · (5 months ago) · parent b116057a7 · browse the tree at this commit

Semantic delta · 31

src/compute/flexbox.rs

src/style/compact_length.rs

src/style/dimension.rs

4 files · +261 −12

diff --git a/src/compute/flexbox.rs b/src/compute/flexbox.rs
index edeafe252..f5d61da83 100644
--- a/src/compute/flexbox.rs
+++ b/src/compute/flexbox.rs
@@ -2,8 +2,8 @@
 use crate::compute::common::alignment::compute_alignment_offset;
 use crate::geometry::{Line, Point, Rect, Size};
 use crate::style::{
-    AlignContent, AlignItems, AlignSelf, AvailableSpace, FlexWrap, JustifyContent, LengthPercentageAuto, Overflow,
-    Position,
+    AlignContent, AlignItems, AlignSelf, AvailableSpace, Dimension, FlexWrap, JustifyContent, LengthPercentageAuto,
+    Overflow, Position,
 };
 use crate::style::{CoreStyle, FlexDirection, FlexboxContainerStyle, FlexboxItemStyle};
 use crate::style_helpers::{TaffyMaxContent, TaffyMinContent};
@@ -29,6 +29,8 @@ struct FlexItem {
 
     /// The base size of this item
     size: Size<Option<f32>>,
+    /// The raw specified size of this item
+    raw_size: Size<Dimension>,
     /// The minimum allowable size of this item
     min_size: Size<Option<f32>>,
     /// The maximum allowable size of this item
@@ -60,6 +62,8 @@ struct FlexItem {
     /// The border of this item
     border: Rect<f32>,
 
+    /// The raw flex-basis style of this item
+    raw_flex_basis: Dimension,
     /// The default size of this item
     flex_basis: f32,
     /// The default size of this item, minus padding and border
@@ -530,6 +534,7 @@ fn generate_anonymous_flex_items(
                     .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis))
                     .maybe_apply_aspect_ratio(aspect_ratio)
                     .maybe_add(box_sizing_adjustment),
+                raw_size: child_style.size(),
                 min_size: child_style
                     .min_size()
                     .maybe_resolve(constants.node_inner_size, |val, basis| tree.calc(val, basis))
@@ -559,6 +564,7 @@ fn generate_anonymous_flex_items(
                 scrollbar_width: child_style.scrollbar_width(),
                 flex_grow: child_style.flex_grow(),
                 flex_shrink: child_style.flex_shrink(),
+                raw_flex_basis: child_style.flex_basis(),
                 flex_basis: 0.0,
                 inner_flex_basis: 0.0,
                 violation: 0.0,
@@ -645,6 +651,34 @@ fn determine_available_space(
 ///   Furthermore, the sizing calculations that floor the content box size at zero when applying box-sizing are also ignored.
 ///   (For example, an item with a specified size of zero, positive padding, and box-sizing: border-box will have an outer flex base size of zero—and hence a negative inner flex base size.)
 #[inline]
+fn flex_intrinsic_available_space(
+    dimension: Dimension,
+    parent_size: Option<f32>,
+    available_space: AvailableSpace,
+    calc_resolver: impl Fn(*const (), f32) -> f32,
+) -> Option<AvailableSpace> {
+    if dimension.is_min_content() {
+        Some(AvailableSpace::MinContent)
+    } else if dimension.is_max_content() {
+        Some(AvailableSpace::MaxContent)
+    } else if dimension.is_fit_content_keyword() {
+        Some(match available_space {
+            AvailableSpace::Definite(limit) => AvailableSpace::Definite(limit),
+            AvailableSpace::MinContent => AvailableSpace::MinContent,
+            AvailableSpace::MaxContent => AvailableSpace::MaxContent,
+        })
+    } else if dimension.is_fit_content() {
+        Some(
+            dimension
+                .definite_limit(parent_size, calc_resolver)
+                .map(AvailableSpace::Definite)
+                .unwrap_or(AvailableSpace::MaxContent),
+        )
+    } else {
+        None
+    }
+}
+
 fn determine_flex_base_size(
     tree: &mut impl LayoutFlexboxContainer,
     constants: &AlgoConstants,
@@ -709,9 +743,31 @@ fn determine_flex_base_size(
             .flex_basis()
             .maybe_resolve(container_width, |val, basis| tree.calc(val, basis))
             .maybe_add(box_sizing_adjustment);
+        let raw_flex_basis = child.raw_flex_basis;
 
         drop(child_style);
 
+        let intrinsic_main_size = flex_intrinsic_available_space(
+            child.raw_size.main(dir),
+            constants.node_inner_size.main(dir),
+            available_space.main(dir),
+            |val, basis| tree.calc(val, basis),
+        )
+        .map(|main_available_space| {
+            let child_available_space =
+                Size::MAX_CONTENT.with_main(dir, main_available_space).with_cross(dir, cross_axis_available_space);
+
+            tree.measure_child_size(
+                child.node,
+                child_known_dimensions.with_main(dir, None),
+                child_parent_size,
+                child_available_space,
+                SizingMode::InherentSize,
+                dir.main_axis(),
+                Line::FALSE,
+            )
+        });
+
         child.flex_basis = 'flex_basis: {
             // A. If the item has a definite used flex basis, that’s the flex base size.
 
@@ -722,11 +778,31 @@ fn determine_flex_base_size(
 
             // Note: `child.size` has already been resolved against aspect_ratio in generate_anonymous_flex_items
             // So B will just work here by using main_size without special handling for aspect_ratio
-            let main_size = child.size.main(dir);
+            let main_size = intrinsic_main_size.or(child.size.main(dir));
             if let Some(flex_basis) = flex_basis.or(main_size) {
                 break 'flex_basis flex_basis;
             };
 
+            if let Some(main_available_space) = flex_intrinsic_available_space(
+                raw_flex_basis,
+                container_width,
+                available_space.main(dir),
+                |val, basis| tree.calc(val, basis),
+            ) {
+                let child_available_space =
+                    Size::MAX_CONTENT.with_main(dir, main_available_space).with_cross(dir, cross_axis_available_space);
+
+                break 'flex_basis tree.measure_child_size(
+                    child.node,
+                    child_known_dimensions.with_main(dir, None),
+                    child_parent_size,
+                    child_available_space,
+                    SizingMode::ContentSize,
+                    dir.main_axis(),
+                    Line::FALSE,
+                );
+            }
+
             // C. If the used flex basis is content or depends on its available space,
             //    and the flex container is being sized under a min-content or max-content
             //    constraint (e.g. when performing automatic table layout [CSS21]),
diff --git a/src/style/compact_length.rs b/src/style/compact_length.rs
index 93c85c981..173ccf090 100644
--- a/src/style/compact_length.rs
+++ b/src/style/compact_length.rs
@@ -227,6 +227,8 @@ impl CompactLength {
     pub const MAX_CONTENT_TAG: usize = 0b00001111;
     /// The tag indicating a fit-content value with px limit
     pub const FIT_CONTENT_PX_TAG: usize = 0b00010111;
+    /// The tag indicating the `fit-content` keyword
+    pub const FIT_CONTENT_KEYWORD_TAG: usize = 0b00100111;
     /// The tag indicating a fit-content value with percent limit
     pub const FIT_CONTENT_PERCENT_TAG: usize = 0b00011111;
 }
@@ -302,6 +304,13 @@ impl CompactLength {
         Self(CompactLengthInner::from_val(limit, Self::FIT_CONTENT_PX_TAG))
     }
 
+    /// The size should be computed according to the `fit-content` keyword formula:
+    /// stretch-fit clamped between the min-content and max-content sizes.
+    #[inline(always)]
+    pub const fn fit_content_keyword() -> Self {
+        Self(CompactLengthInner::from_tag(Self::FIT_CONTENT_KEYWORD_TAG))
+    }
+
     /// The size should be computed according to the "fit content" formula:
     ///    `max(min_content, min(max_content, limit))`
     /// where:
@@ -376,13 +385,25 @@ impl CompactLength {
     /// Returns true if the value is a fit-content(...) value
     #[inline(always)]
     pub fn is_fit_content(self) -> bool {
-        matches!(self.tag(), Self::FIT_CONTENT_PX_TAG | Self::FIT_CONTENT_PERCENT_TAG)
+        matches!(self.tag(), Self::FIT_CONTENT_PX_TAG | Self::FIT_CONTENT_PERCENT_TAG | Self::FIT_CONTENT_KEYWORD_TAG)
+    }
+
+    /// Returns true if the value is the `fit-content` keyword
+    #[inline(always)]
+    pub fn is_fit_content_keyword(self) -> bool {
+        self.tag() == Self::FIT_CONTENT_KEYWORD_TAG
     }
 
     /// Returns true if the value is max-content or a fit-content(...) value
     #[inline(always)]
     pub fn is_max_or_fit_content(self) -> bool {
-        matches!(self.tag(), Self::MAX_CONTENT_TAG | Self::FIT_CONTENT_PX_TAG | Self::FIT_CONTENT_PERCENT_TAG)
+        matches!(
+            self.tag(),
+            Self::MAX_CONTENT_TAG
+                | Self::FIT_CONTENT_PX_TAG
+                | Self::FIT_CONTENT_PERCENT_TAG
+                | Self::FIT_CONTENT_KEYWORD_TAG
+        )
     }
 
     /// Returns true if the max track sizing function is `MaxContent`, `FitContent` or `Auto` else false.
@@ -394,6 +415,7 @@ impl CompactLength {
             self.tag(),
             CompactLength::AUTO_TAG
                 | CompactLength::MAX_CONTENT_TAG
+                | CompactLength::FIT_CONTENT_KEYWORD_TAG
                 | CompactLength::FIT_CONTENT_PX_TAG
                 | CompactLength::FIT_CONTENT_PERCENT_TAG
         )
@@ -412,6 +434,7 @@ impl CompactLength {
             self.tag(),
             Self::AUTO_TAG
                 | Self::MIN_CONTENT_TAG
+                | Self::FIT_CONTENT_KEYWORD_TAG
                 | Self::MAX_CONTENT_TAG
                 | Self::FIT_CONTENT_PX_TAG
                 | Self::FIT_CONTENT_PERCENT_TAG
@@ -528,6 +551,7 @@ impl<'de> serde::Deserialize<'de> for CompactLength {
                 | CompactLength::AUTO_TAG
                 | CompactLength::MIN_CONTENT_TAG
                 | CompactLength::MAX_CONTENT_TAG
+                | CompactLength::FIT_CONTENT_KEYWORD_TAG
                 | CompactLength::FIT_CONTENT_PX_TAG
                 | CompactLength::FIT_CONTENT_PERCENT_TAG
                 | CompactLength::FR_TAG
diff --git a/src/style/dimension.rs b/src/style/dimension.rs
index 1307b194f..23781ff90 100644
--- a/src/style/dimension.rs
+++ b/src/style/dimension.rs
@@ -1,7 +1,9 @@
 //! Style types for representing lengths / sizes
 use super::CompactLength;
 use crate::geometry::Rect;
-use crate::style_helpers::{FromLength, FromPercent, TaffyAuto, TaffyZero};
+use crate::style_helpers::{
+    FromLength, FromPercent, TaffyAuto, TaffyFitContent, TaffyMaxContent, TaffyMinContent, TaffyZero,
+};
 #[cfg(feature = "parse")]
 use crate::util::parse::{from_str_from_css, parse_css_str_entirely, CssParseResult, FromCss, Parser, Token};
 
@@ -159,6 +161,12 @@ impl LengthPercentageAuto {
         Self(CompactLength::auto())
     }
 
+    /// The `fit-content` keyword.
+    #[inline(always)]
+    pub const fn fit_content_keyword() -> Self {
+        Self(CompactLength::fit_content_keyword())
+    }
+
     /// A `calc()` value. The value passed here is treated as an opaque handle to
     /// the actual calc representation and may be a pointer, index, etc.
     ///
@@ -213,7 +221,17 @@ impl<'de> serde::Deserialize<'de> for LengthPercentageAuto {
     {
         let inner = CompactLength::deserialize(deserializer)?;
         // Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
-        if matches!(inner.tag(), CompactLength::LENGTH_TAG | CompactLength::PERCENT_TAG | CompactLength::AUTO_TAG) {
+        if matches!(
+            inner.tag(),
+            CompactLength::LENGTH_TAG
+                | CompactLength::PERCENT_TAG
+                | CompactLength::AUTO_TAG
+                | CompactLength::MIN_CONTENT_TAG
+                | CompactLength::MAX_CONTENT_TAG
+                | CompactLength::FIT_CONTENT_PX_TAG
+                | CompactLength::FIT_CONTENT_PERCENT_TAG
+                | CompactLength::FIT_CONTENT_KEYWORD_TAG
+        ) {
             Ok(Self(inner))
         } else {
             Err(serde::de::Error::custom("Invalid tag"))
@@ -233,6 +251,17 @@ impl TaffyZero for Dimension {
 impl TaffyAuto for Dimension {
     const AUTO: Self = Self(CompactLength::AUTO);
 }
+impl TaffyMinContent for Dimension {
+    const MIN_CONTENT: Self = Self(CompactLength::MIN_CONTENT);
+}
+impl TaffyMaxContent for Dimension {
+    const MAX_CONTENT: Self = Self(CompactLength::MAX_CONTENT);
+}
+impl TaffyFitContent for Dimension {
+    fn fit_content(argument: LengthPercentage) -> Self {
+        Self(CompactLength::fit_content(argument))
+    }
+}
 impl FromLength for Dimension {
     fn from_length<Input: Into<f32> + Copy>(value: Input) -> Self {
         Self::length(value.into())
@@ -291,6 +320,12 @@ impl Dimension {
         Self(CompactLength::auto())
     }
 
+    /// The `fit-content` keyword.
+    #[inline(always)]
+    pub const fn fit_content_keyword() -> Self {
+        Self(CompactLength::fit_content_keyword())
+    }
+
     /// A `calc()` value. The value passed here is treated as an opaque handle to
     /// the actual calc representation and may be a pointer, index, etc.
     ///
@@ -328,6 +363,78 @@ impl Dimension {
         self.0.is_auto()
     }
 
+    /// Returns true if value is any intrinsic size keyword/function.
+    #[inline(always)]
+    pub fn is_intrinsic(self) -> bool {
+        self.0.is_intrinsic() && !self.is_auto()
+    }
+
+    /// Returns true if value is min-content.
+    #[inline(always)]
+    pub fn is_min_content(self) -> bool {
+        self.0.is_min_content()
+    }
+
+    /// Returns true if value is max-content.
+    #[inline(always)]
+    pub fn is_max_content(self) -> bool {
+        self.0.is_max_content()
+    }
+
+    /// Returns true if value is any fit-content keyword/function.
+    #[inline(always)]
+    pub fn is_fit_content(self) -> bool {
+        self.0.is_fit_content()
+    }
+
+    /// Returns true if value is the `fit-content` keyword.
+    #[inline(always)]
+    pub fn is_fit_content_keyword(self) -> bool {
+        self.0.is_fit_content_keyword()
+    }
+
+    /// Returns whether the value can be resolved using `Self::definite_value`.
+    #[inline(always)]
+    pub fn has_definite_value(self, parent_size: Option<f32>) -> bool {
+        match self.0.tag() {
+            CompactLength::LENGTH_TAG => true,
+            CompactLength::PERCENT_TAG => parent_size.is_some(),
+            #[cfg(feature = "calc")]
+            _ if self.0.is_calc() => parent_size.is_some(),
+            _ => false,
+        }
+    }
+
+    /// Resolve fixed point values directly and percentages against the parent size.
+    #[inline(always)]
+    pub fn definite_value(
+        self,
+        parent_size: Option<f32>,
+        calc_resolver: impl Fn(*const (), f32) -> f32,
+    ) -> Option<f32> {
+        match self.0.tag() {
+            CompactLength::LENGTH_TAG => Some(self.0.value()),
+            CompactLength::PERCENT_TAG => parent_size.map(|size| self.0.value() * size),
+            #[cfg(feature = "calc")]
+            _ if self.0.is_calc() => parent_size.map(|size| calc_resolver(self.0.calc_value(), size)),
+            _ => None,
+        }
+    }
+
+    /// Resolve the explicit limit used by `fit-content(<length-percentage>)`.
+    #[inline(always)]
+    pub fn definite_limit(
+        self,
+        parent_size: Option<f32>,
+        calc_resolver: impl Fn(*const (), f32) -> f32,
+    ) -> Option<f32> {
+        match self.0.tag() {
+            CompactLength::FIT_CONTENT_PX_TAG => Some(self.0.value()),
+            CompactLength::FIT_CONTENT_PERCENT_TAG => parent_size.map(|size| self.0.value() * size),
+            _ => self.definite_value(parent_size, calc_resolver),
+        }
+    }
+
     /// Get the raw `CompactLength` tag
     pub fn tag(self) -> usize {
         self.0.tag()
@@ -347,7 +454,17 @@ impl<'de> serde::Deserialize<'de> for Dimension {
     {
         let inner = CompactLength::deserialize(deserializer)?;
         // Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
-        if matches!(inner.tag(), CompactLength::LENGTH_TAG | CompactLength::PERCENT_TAG | CompactLength::AUTO_TAG) {
+        if matches!(
+            inner.tag(),
+            CompactLength::LENGTH_TAG
+                | CompactLength::PERCENT_TAG
+                | CompactLength::AUTO_TAG
+                | CompactLength::MIN_CONTENT_TAG
+                | CompactLength::MAX_CONTENT_TAG
+                | CompactLength::FIT_CONTENT_PX_TAG
+                | CompactLength::FIT_CONTENT_PERCENT_TAG
+                | CompactLength::FIT_CONTENT_KEYWORD_TAG
+        ) {
             Ok(Self(inner))
         } else {
             Err(serde::de::Error::custom("Invalid tag"))
diff --git a/src/util/resolve.rs b/src/util/resolve.rs
index c40fec300..9f9906a7f 100644
--- a/src/util/resolve.rs
+++ b/src/util/resolve.rs
@@ -46,10 +46,16 @@ impl MaybeResolve<Option<f32>, Option<f32>> for LengthPercentageAuto {
         match self.0.tag() {
             CompactLength::AUTO_TAG => None,
             CompactLength::LENGTH_TAG => Some(self.0.value()),
-            CompactLength::PERCENT_TAG => context.map(|dim| dim * self.0.value()),
+            CompactLength::PERCENT_TAG | CompactLength::FIT_CONTENT_PERCENT_TAG => {
+                context.map(|dim| dim * self.0.value())
+            }
+            CompactLength::MIN_CONTENT_TAG
+            | CompactLength::MAX_CONTENT_TAG
+            | CompactLength::FIT_CONTENT_PX_TAG
+            | CompactLength::FIT_CONTENT_KEYWORD_TAG => None,
             #[cfg(feature = "calc")]
             _ if self.0.is_calc() => context.map(|dim| calc(self.0.calc_value(), dim)),
-            _ => unreachable!(),
+            _ => None,
         }
     }
 }
@@ -62,10 +68,16 @@ impl MaybeResolve<Option<f32>, Option<f32>> for Dimension {
         match self.0.tag() {
             CompactLength::AUTO_TAG => None,
             CompactLength::LENGTH_TAG => Some(self.0.value()),
-            CompactLength::PERCENT_TAG => context.map(|dim| dim * self.0.value()),
+            CompactLength::PERCENT_TAG | CompactLength::FIT_CONTENT_PERCENT_TAG => {
+                context.map(|dim| dim * self.0.value())
+            }
+            CompactLength::MIN_CONTENT_TAG
+            | CompactLength::MAX_CONTENT_TAG
+            | CompactLength::FIT_CONTENT_PX_TAG
+            | CompactLength::FIT_CONTENT_KEYWORD_TAG => None,
             #[cfg(feature = "calc")]
             _ if self.0.is_calc() => context.map(|dim| calc(self.0.calc_value(), dim)),
-            _ => unreachable!(),
+            _ => None,
         }
     }
 }
@@ -213,6 +225,26 @@ mod tests {
             mr_case(Dimension::from_percent(1.0), Some(-5.0), Some(-5.0));
             mr_case(Dimension::from_percent(1.0), Some(50.0), Some(50.0));
         }
+
+        /// Intrinsic size keywords/functions should remain unresolved in the generic numeric resolver.
+        #[test]
+        fn intrinsic_sizes_do_not_resolve_to_numeric_values() {
+            mr_case(Dimension::MIN_CONTENT, None, None);
+            mr_case(Dimension::MIN_CONTENT, Some(50.0), None);
+            mr_case(Dimension::MAX_CONTENT, None, None);
+            mr_case(Dimension::MAX_CONTENT, Some(50.0), None);
+            mr_case(Dimension::fit_content_keyword(), None, None);
+            mr_case(Dimension::fit_content_keyword(), Some(50.0), None);
+            mr_case(fit_content::<Dimension>(length(12.0)), None, None);
+            mr_case(fit_content::<Dimension>(length(12.0)), Some(50.0), None);
+        }
+
+        /// `fit-content(percentage)` carries the percentage limit until the algorithm consumes it.
+        #[test]
+        fn fit_content_percent_resolves_its_limit() {
+            mr_case(fit_content::<Dimension>(percent(0.5)), None, None);
+            mr_case(fit_content::<Dimension>(percent(0.5)), Some(80.0), Some(40.0));
+        }
     }
 
     mod maybe_resolve_size_dimension {