Skip to main content

taffy/style/
dimension.rs

1//! Style types for representing lengths / sizes
2use super::CompactLength;
3use crate::geometry::Rect;
4use crate::style_helpers::{FromLength, FromPercent, TaffyAuto, TaffyZero};
5#[cfg(feature = "parse")]
6use crate::util::parse::{from_str_from_css, CssParseResult, FromCss, Parser, Token};
7
8/// A unit of linear measurement
9///
10/// This is commonly combined with [`Rect`], [`Point`](crate::geometry::Point) and [`Size<T>`](crate::geometry::Size).
11#[derive(Copy, Clone, PartialEq, Debug)]
12#[cfg_attr(feature = "serde", derive(Serialize))]
13pub struct LengthPercentage(pub(crate) CompactLength);
14impl TaffyZero for LengthPercentage {
15    const ZERO: Self = Self(CompactLength::ZERO);
16}
17impl FromLength for LengthPercentage {
18    fn from_length<Input: Into<f64> + Copy>(value: Input) -> Self {
19        Self::length(value.into() as f32)
20    }
21}
22impl FromPercent for LengthPercentage {
23    fn from_percent<Input: Into<f64> + Copy>(value: Input) -> Self {
24        Self::percent(value.into() as f32)
25    }
26}
27
28#[cfg(feature = "parse")]
29impl FromCss for LengthPercentage {
30    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
31        match parser.next()?.clone() {
32            Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
33            Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
34            token => Err(parser.new_unexpected_token_error(token))?,
35        }
36    }
37}
38#[cfg(feature = "parse")]
39from_str_from_css!(LengthPercentage);
40
41impl LengthPercentage {
42    /// An absolute length in some abstract units. Users of Taffy may define what they correspond
43    /// to in their application (pixels, logical pixels, mm, etc) as they see fit.
44    #[inline(always)]
45    pub const fn length(val: f32) -> Self {
46        Self(CompactLength::length(val))
47    }
48
49    /// A percentage length relative to the size of the containing block.
50    ///
51    /// **NOTE: percentages are represented as a f32 value in the range [0.0, 1.0] NOT the range [0.0, 100.0]**
52    #[inline(always)]
53    pub const fn percent(val: f32) -> Self {
54        Self(CompactLength::percent(val))
55    }
56
57    /// A `calc()` value. The value passed here is treated as an opaque handle to
58    /// the actual calc representation and may be a pointer, index, etc.
59    ///
60    /// The low 3 bits are used as a tag value and will be returned as 0.
61    #[inline(always)]
62    #[cfg(feature = "calc")]
63    pub fn calc(ptr: *const ()) -> Self {
64        Self(CompactLength::calc(ptr))
65    }
66
67    /// Create a LengthPercentage from a raw `CompactLength`.
68    /// # Safety
69    /// CompactLength must represent a valid variant for LengthPercentage
70    #[allow(unsafe_code)]
71    pub const unsafe fn from_raw(val: CompactLength) -> Self {
72        Self(val)
73    }
74
75    /// Get the underlying `CompactLength` representation of the value
76    pub const fn into_raw(self) -> CompactLength {
77        self.0
78    }
79
80    /// Expand the compact representation into an [`ExpandedLengthPercentage`] enum.
81    ///
82    /// This is useful when integrating with other libraries (e.g. for style inspection or
83    /// serialization) as it allows the value to be pattern-matched without having to work
84    /// with the raw [`CompactLength`] tagged-pointer representation directly.
85    pub fn expand(self) -> ExpandedLengthPercentage {
86        match self.0.tag() {
87            CompactLength::LENGTH_TAG => ExpandedLengthPercentage::Length(self.0.value()),
88            CompactLength::PERCENT_TAG => ExpandedLengthPercentage::Percent(self.0.value()),
89            #[cfg(feature = "calc")]
90            _ if self.0.is_plain_calc() => ExpandedLengthPercentage::Calc(self.0.calc_value()),
91            _ => unreachable!("LengthPercentage contains a value with an invalid tag"),
92        }
93    }
94}
95
96/// The expanded, non-compact representation of a [`LengthPercentage`].
97///
98/// Obtained via [`LengthPercentage::expand`]. Can be converted back into a [`LengthPercentage`]
99/// using the [`From`] implementation.
100#[derive(Copy, Clone, PartialEq, Debug)]
101pub enum ExpandedLengthPercentage {
102    /// An absolute length (see [`LengthPercentage::length`])
103    Length(f32),
104    /// A percentage length (see [`LengthPercentage::percent`])
105    Percent(f32),
106    /// A `calc()` value (see [`LengthPercentage::calc`]). The pointer is an opaque handle to the
107    /// calc representation, exactly as passed to the constructor.
108    #[cfg(feature = "calc")]
109    Calc(*const ()),
110}
111
112impl From<LengthPercentage> for ExpandedLengthPercentage {
113    fn from(value: LengthPercentage) -> Self {
114        value.expand()
115    }
116}
117
118impl From<ExpandedLengthPercentage> for LengthPercentage {
119    fn from(value: ExpandedLengthPercentage) -> Self {
120        match value {
121            ExpandedLengthPercentage::Length(val) => Self::length(val),
122            ExpandedLengthPercentage::Percent(val) => Self::percent(val),
123            #[cfg(feature = "calc")]
124            ExpandedLengthPercentage::Calc(ptr) => Self::calc(ptr),
125        }
126    }
127}
128
129#[cfg(feature = "serde")]
130impl<'de> serde::Deserialize<'de> for LengthPercentage {
131    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132    where
133        D: serde::Deserializer<'de>,
134    {
135        let inner = CompactLength::deserialize(deserializer)?;
136        // Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
137        if matches!(inner.tag(), CompactLength::LENGTH_TAG | CompactLength::PERCENT_TAG) {
138            Ok(Self(inner))
139        } else {
140            Err(serde::de::Error::custom("Invalid tag"))
141        }
142    }
143}
144
145/// A unit of linear measurement
146///
147/// This is commonly combined with [`Rect`], [`Point`](crate::geometry::Point) and [`Size<T>`](crate::geometry::Size).
148#[derive(Copy, Clone, PartialEq, Debug)]
149#[cfg_attr(feature = "serde", derive(Serialize))]
150pub struct LengthPercentageAuto(pub(crate) CompactLength);
151impl TaffyZero for LengthPercentageAuto {
152    const ZERO: Self = Self(CompactLength::ZERO);
153}
154impl TaffyAuto for LengthPercentageAuto {
155    const AUTO: Self = Self(CompactLength::AUTO);
156}
157impl FromLength for LengthPercentageAuto {
158    fn from_length<Input: Into<f64> + Copy>(value: Input) -> Self {
159        Self::length(value.into() as f32)
160    }
161}
162impl FromPercent for LengthPercentageAuto {
163    fn from_percent<Input: Into<f64> + Copy>(value: Input) -> Self {
164        Self::percent(value.into() as f32)
165    }
166}
167impl From<LengthPercentage> for LengthPercentageAuto {
168    fn from(input: LengthPercentage) -> Self {
169        Self(input.0)
170    }
171}
172
173#[cfg(feature = "parse")]
174impl FromCss for LengthPercentageAuto {
175    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
176        match parser.next()?.clone() {
177            Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
178            Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
179            Token::Ident(ident) if ident == "auto" => Ok(Self::auto()),
180            token => Err(parser.new_unexpected_token_error(token))?,
181        }
182    }
183}
184#[cfg(feature = "parse")]
185from_str_from_css!(LengthPercentageAuto);
186
187impl LengthPercentageAuto {
188    /// An absolute length in some abstract units. Users of Taffy may define what they correspond
189    /// to in their application (pixels, logical pixels, mm, etc) as they see fit.
190    #[inline(always)]
191    pub const fn length(val: f32) -> Self {
192        Self(CompactLength::length(val))
193    }
194
195    /// A percentage length relative to the size of the containing block.
196    ///
197    /// **NOTE: percentages are represented as a f32 value in the range [0.0, 1.0] NOT the range [0.0, 100.0]**
198    #[inline(always)]
199    pub const fn percent(val: f32) -> Self {
200        Self(CompactLength::percent(val))
201    }
202
203    /// The dimension should be automatically computed according to algorithm-specific rules
204    /// regarding the default size of boxes.
205    #[inline(always)]
206    pub const fn auto() -> Self {
207        Self(CompactLength::auto())
208    }
209
210    /// A `calc()` value. The value passed here is treated as an opaque handle to
211    /// the actual calc representation and may be a pointer, index, etc.
212    ///
213    /// The low 3 bits are used as a tag value and will be returned as 0.
214    #[inline]
215    #[cfg(feature = "calc")]
216    pub fn calc(ptr: *const ()) -> Self {
217        Self(CompactLength::calc(ptr))
218    }
219
220    /// Create a LengthPercentageAuto from a raw `CompactLength`.
221    /// # Safety
222    /// CompactLength must represent a valid variant for LengthPercentageAuto
223    #[allow(unsafe_code)]
224    pub const unsafe fn from_raw(val: CompactLength) -> Self {
225        Self(val)
226    }
227
228    /// Get the underlying `CompactLength` representation of the value
229    pub const fn into_raw(self) -> CompactLength {
230        self.0
231    }
232
233    /// Returns:
234    ///   - Some(length) for Length variants
235    ///   - Some(resolved) using the provided context for Percent variants
236    ///   - None for Auto variants
237    #[inline(always)]
238    pub fn resolve_to_option(self, context: f32, calc_resolver: impl Fn(*const (), f32) -> f32) -> Option<f32> {
239        match self.0.tag() {
240            CompactLength::LENGTH_TAG => Some(self.0.value()),
241            CompactLength::PERCENT_TAG => Some(context * self.0.value()),
242            CompactLength::AUTO_TAG => None,
243            #[cfg(feature = "calc")]
244            _ if self.0.is_plain_calc() => Some(calc_resolver(self.0.calc_value(), context)),
245            _ => unreachable!("LengthPercentageAuto values cannot be constructed with other tags"),
246        }
247    }
248
249    /// Returns true if value is LengthPercentageAuto::Auto
250    #[inline(always)]
251    pub fn is_auto(self) -> bool {
252        self.0.is_auto()
253    }
254
255    /// Expand the compact representation into an [`ExpandedLengthPercentageAuto`] enum.
256    ///
257    /// This is useful when integrating with other libraries (e.g. for style inspection or
258    /// serialization) as it allows the value to be pattern-matched without having to work
259    /// with the raw [`CompactLength`] tagged-pointer representation directly.
260    pub fn expand(self) -> ExpandedLengthPercentageAuto {
261        match self.0.tag() {
262            CompactLength::LENGTH_TAG => ExpandedLengthPercentageAuto::Length(self.0.value()),
263            CompactLength::PERCENT_TAG => ExpandedLengthPercentageAuto::Percent(self.0.value()),
264            CompactLength::AUTO_TAG => ExpandedLengthPercentageAuto::Auto,
265            #[cfg(feature = "calc")]
266            _ if self.0.is_plain_calc() => ExpandedLengthPercentageAuto::Calc(self.0.calc_value()),
267            _ => unreachable!("LengthPercentageAuto contains a value with an invalid tag"),
268        }
269    }
270}
271
272/// The expanded, non-compact representation of a [`LengthPercentageAuto`].
273///
274/// Obtained via [`LengthPercentageAuto::expand`]. Can be converted back into a
275/// [`LengthPercentageAuto`] using the [`From`] implementation.
276#[derive(Copy, Clone, PartialEq, Debug)]
277pub enum ExpandedLengthPercentageAuto {
278    /// An absolute length (see [`LengthPercentageAuto::length`])
279    Length(f32),
280    /// A percentage length (see [`LengthPercentageAuto::percent`])
281    Percent(f32),
282    /// The automatic keyword (see [`LengthPercentageAuto::auto`])
283    Auto,
284    /// A `calc()` value (see [`LengthPercentageAuto::calc`]). The pointer is an opaque handle to
285    /// the calc representation, exactly as passed to the constructor.
286    #[cfg(feature = "calc")]
287    Calc(*const ()),
288}
289
290impl From<LengthPercentageAuto> for ExpandedLengthPercentageAuto {
291    fn from(value: LengthPercentageAuto) -> Self {
292        value.expand()
293    }
294}
295
296impl From<ExpandedLengthPercentageAuto> for LengthPercentageAuto {
297    fn from(value: ExpandedLengthPercentageAuto) -> Self {
298        match value {
299            ExpandedLengthPercentageAuto::Length(val) => Self::length(val),
300            ExpandedLengthPercentageAuto::Percent(val) => Self::percent(val),
301            ExpandedLengthPercentageAuto::Auto => Self::auto(),
302            #[cfg(feature = "calc")]
303            ExpandedLengthPercentageAuto::Calc(ptr) => Self::calc(ptr),
304        }
305    }
306}
307
308#[cfg(feature = "serde")]
309impl<'de> serde::Deserialize<'de> for LengthPercentageAuto {
310    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
311    where
312        D: serde::Deserializer<'de>,
313    {
314        let inner = CompactLength::deserialize(deserializer)?;
315        // Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
316        if matches!(inner.tag(), CompactLength::LENGTH_TAG | CompactLength::PERCENT_TAG | CompactLength::AUTO_TAG) {
317            Ok(Self(inner))
318        } else {
319            Err(serde::de::Error::custom("Invalid tag"))
320        }
321    }
322}
323
324/// A unit of linear measurement
325///
326/// This is commonly combined with [`Rect`], [`Point`](crate::geometry::Point) and [`Size<T>`](crate::geometry::Size).
327#[derive(Copy, Clone, PartialEq, Debug)]
328#[cfg_attr(feature = "serde", derive(Serialize))]
329pub struct Dimension(pub(crate) CompactLength);
330impl TaffyZero for Dimension {
331    const ZERO: Self = Self(CompactLength::ZERO);
332}
333impl TaffyAuto for Dimension {
334    const AUTO: Self = Self(CompactLength::AUTO);
335}
336impl FromLength for Dimension {
337    fn from_length<Input: Into<f64> + Copy>(value: Input) -> Self {
338        Self::length(value.into() as f32)
339    }
340}
341impl FromPercent for Dimension {
342    fn from_percent<Input: Into<f64> + Copy>(value: Input) -> Self {
343        Self::percent(value.into() as f32)
344    }
345}
346impl From<LengthPercentage> for Dimension {
347    fn from(input: LengthPercentage) -> Self {
348        Self(input.0)
349    }
350}
351impl From<LengthPercentageAuto> for Dimension {
352    fn from(input: LengthPercentageAuto) -> Self {
353        Self(input.0)
354    }
355}
356
357#[cfg(feature = "parse")]
358impl FromCss for Dimension {
359    fn from_css<'i>(parser: &mut Parser<'i, '_>) -> CssParseResult<'i, Self> {
360        let token = parser.next()?.clone();
361        match token {
362            Token::Percentage { unit_value, .. } => Ok(Self::percent(unit_value)),
363            Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::length(value)),
364            Token::Ident(ref ident) => match ident.as_ref() {
365                "auto" => Ok(Self::auto()),
366                "min-content" => Ok(Self::min_content()),
367                "max-content" => Ok(Self::max_content()),
368                "fit-content" => Ok(Self::fit_content()),
369                "stretch" => Ok(Self::stretch()),
370                "content" => Ok(Self::content()),
371                _ => Err(parser.new_unexpected_token_error(token))?,
372            },
373            Token::Function(ref name) if name.as_ref() == "fit-content" => parser.parse_nested_block(|parser| {
374                let token = parser.next()?.clone();
375                match token {
376                    Token::Percentage { unit_value, .. } => Ok(Self::fit_content_percent(unit_value)),
377                    Token::Dimension { unit, value, .. } if unit == "px" => Ok(Self::fit_content_px(value)),
378                    token => Err(parser.new_unexpected_token_error(token))?,
379                }
380            }),
381            token => Err(parser.new_unexpected_token_error(token))?,
382        }
383    }
384}
385#[cfg(feature = "parse")]
386from_str_from_css!(Dimension);
387
388impl Dimension {
389    /// An absolute length in some abstract units. Users of Taffy may define what they correspond
390    /// to in their application (pixels, logical pixels, mm, etc) as they see fit.
391    #[inline(always)]
392    pub const fn length(val: f32) -> Self {
393        Self(CompactLength::length(val))
394    }
395
396    /// A percentage length relative to the size of the containing block.
397    ///
398    /// **NOTE: percentages are represented as a f32 value in the range [0.0, 1.0] NOT the range [0.0, 100.0]**
399    #[inline(always)]
400    pub const fn percent(val: f32) -> Self {
401        Self(CompactLength::percent(val))
402    }
403
404    /// The dimension should be automatically computed according to algorithm-specific rules
405    /// regarding the default size of boxes.
406    #[inline(always)]
407    pub const fn auto() -> Self {
408        Self(CompactLength::auto())
409    }
410
411    /// The size should be the "min-content" size.
412    /// This is the smallest size that can fit the item's contents with ALL soft line-wrapping opportunities taken
413    #[inline(always)]
414    pub const fn min_content() -> Self {
415        Self(CompactLength::min_content())
416    }
417
418    /// The size should be the "max-content" size.
419    /// This is the smallest size that can fit the item's contents with NO soft line-wrapping opportunities taken
420    #[inline(always)]
421    pub const fn max_content() -> Self {
422        Self(CompactLength::max_content())
423    }
424
425    /// The size should be computed according to the "fit content" formula:
426    ///    `max(min_content, min(max_content, stretch))`
427    /// where `stretch` is the size the box would take if it filled the available space
428    #[inline(always)]
429    pub const fn fit_content() -> Self {
430        Self(CompactLength::fit_content_keyword())
431    }
432
433    /// The size should be computed according to the "fit content" formula:
434    ///    `max(min_content, min(max_content, limit))`
435    /// where `limit` is a LENGTH value
436    #[inline(always)]
437    pub const fn fit_content_px(limit: f32) -> Self {
438        Self(CompactLength::fit_content_px(limit))
439    }
440
441    /// The size should be the "stretch-fit" size: the size the box would take
442    /// if it filled the available space
443    /// (<https://www.w3.org/TR/css-sizing-4/#stretch-fit-sizing>)
444    #[inline(always)]
445    pub const fn stretch() -> Self {
446        Self(CompactLength::stretch())
447    }
448
449    /// The size should be an automatic size based on the box's content
450    /// (<https://www.w3.org/TR/css-flexbox-1/#valdef-flex-basis-content>)
451    ///
452    /// This keyword is only valid for `flex-basis`. In any other context it behaves as [`auto`](Self::auto).
453    #[inline(always)]
454    pub const fn content() -> Self {
455        Self(CompactLength::content())
456    }
457
458    /// The size should be computed according to the "fit content" formula:
459    ///    `max(min_content, min(max_content, limit))`
460    /// where `limit` is a PERCENTAGE value
461    #[inline(always)]
462    pub const fn fit_content_percent(limit: f32) -> Self {
463        Self(CompactLength::fit_content_percent(limit))
464    }
465
466    /// A `calc()` value. The value passed here is treated as an opaque handle to
467    /// the actual calc representation and may be a pointer, index, etc.
468    ///
469    /// The low 3 bits are used as a tag value and will be returned as 0.
470    #[inline]
471    #[cfg(feature = "calc")]
472    pub fn calc(ptr: *const ()) -> Self {
473        Self(CompactLength::calc(ptr))
474    }
475
476    /// A `fit-content()` value whose limit is a `calc()` value. The value passed here is treated as an opaque
477    /// handle to the actual calc representation and may be a pointer, index, etc.
478    ///
479    /// The low 3 bits are used as a tag value and will be returned as 0.
480    #[inline]
481    #[cfg(feature = "calc")]
482    pub fn fit_content_calc(ptr: *const ()) -> Self {
483        Self(CompactLength::fit_content_calc(ptr))
484    }
485
486    /// Returns true if the value is a `fit-content()` value with a `calc()` limit
487    #[inline(always)]
488    pub fn is_fit_content_calc(self) -> bool {
489        self.0.is_fit_content_calc()
490    }
491
492    /// Get the calc() pointer (valid for `calc()` and `fit-content(calc())` values)
493    #[inline(always)]
494    #[cfg(feature = "calc")]
495    pub fn calc_value(self) -> *const () {
496        self.0.calc_value()
497    }
498
499    /// Create a LengthPercentageAuto from a raw `CompactLength`.
500    /// # Safety
501    /// CompactLength must represent a valid variant for LengthPercentageAuto
502    #[allow(unsafe_code)]
503    pub const unsafe fn from_raw(val: CompactLength) -> Self {
504        Self(val)
505    }
506
507    /// Get the underlying `CompactLength` representation of the value
508    pub const fn into_raw(self) -> CompactLength {
509        self.0
510    }
511
512    /// Get Length value if value is Length variant
513    #[cfg(feature = "grid")]
514    pub fn into_option(self) -> Option<f32> {
515        match self.0.tag() {
516            CompactLength::LENGTH_TAG => Some(self.0.value()),
517            _ => None,
518        }
519    }
520    /// Returns true if value is Auto
521    #[inline(always)]
522    pub fn is_auto(self) -> bool {
523        self.0.is_auto()
524    }
525
526    /// Returns true if value is min-content, max-content, fit-content, fit-content(...), or stretch
527    #[inline(always)]
528    pub fn is_sizing_keyword(self) -> bool {
529        self.0.is_sizing_keyword()
530    }
531
532    /// Returns true if value is the stretch keyword
533    #[inline(always)]
534    pub fn is_stretch(self) -> bool {
535        self.0.tag() == CompactLength::STRETCH_TAG
536    }
537
538    /// Returns true if value is the content keyword
539    #[inline(always)]
540    pub fn is_content(self) -> bool {
541        self.0.is_content()
542    }
543
544    /// Get the raw `CompactLength` tag
545    pub fn tag(self) -> usize {
546        self.0.tag()
547    }
548
549    /// Get the raw `CompactLength` value for non-calc variants that have a numeric parameter
550    pub fn value(self) -> f32 {
551        self.0.value()
552    }
553
554    /// Expand the compact representation into an [`ExpandedDimension`] enum.
555    ///
556    /// This is useful when integrating with other libraries (e.g. for style inspection or
557    /// serialization) as it allows the value to be pattern-matched without having to work
558    /// with the raw [`CompactLength`] tagged-pointer representation directly.
559    pub fn expand(self) -> ExpandedDimension {
560        match self.0.tag() {
561            CompactLength::LENGTH_TAG => ExpandedDimension::Length(self.0.value()),
562            CompactLength::PERCENT_TAG => ExpandedDimension::Percent(self.0.value()),
563            CompactLength::AUTO_TAG => ExpandedDimension::Auto,
564            CompactLength::MIN_CONTENT_TAG => ExpandedDimension::MinContent,
565            CompactLength::MAX_CONTENT_TAG => ExpandedDimension::MaxContent,
566            CompactLength::FIT_CONTENT_PX_TAG => ExpandedDimension::FitContentPx(self.0.value()),
567            CompactLength::FIT_CONTENT_PERCENT_TAG => ExpandedDimension::FitContentPercent(self.0.value()),
568            CompactLength::FIT_CONTENT_KEYWORD_TAG => ExpandedDimension::FitContent,
569            #[cfg(feature = "calc")]
570            _ if self.0.is_fit_content_calc() => ExpandedDimension::FitContentCalc(self.0.calc_value()),
571            CompactLength::STRETCH_TAG => ExpandedDimension::Stretch,
572            CompactLength::CONTENT_TAG => ExpandedDimension::Content,
573            #[cfg(feature = "calc")]
574            _ if self.0.is_plain_calc() => ExpandedDimension::Calc(self.0.calc_value()),
575            _ => unreachable!("Dimension contains a value with an invalid tag"),
576        }
577    }
578}
579
580/// The expanded, non-compact representation of a [`Dimension`].
581///
582/// Obtained via [`Dimension::expand`]. Can be converted back into a [`Dimension`] using the
583/// [`From`] implementation.
584#[derive(Copy, Clone, PartialEq, Debug)]
585pub enum ExpandedDimension {
586    /// An absolute length (see [`Dimension::length`])
587    Length(f32),
588    /// A percentage length (see [`Dimension::percent`])
589    Percent(f32),
590    /// The automatic keyword (see [`Dimension::auto`])
591    Auto,
592    /// The `min-content` keyword (see [`Dimension::min_content`])
593    MinContent,
594    /// The `max-content` keyword (see [`Dimension::max_content`])
595    MaxContent,
596    /// A `fit-content(...)` value with a length limit (see [`Dimension::fit_content_px`])
597    FitContentPx(f32),
598    /// A `fit-content(...)` value with a percentage limit (see [`Dimension::fit_content_percent`])
599    FitContentPercent(f32),
600    /// The size is the fit-content size with a `calc()` limit (an opaque handle to the calc representation)
601    #[cfg(feature = "calc")]
602    FitContentCalc(*const ()),
603    /// The `fit-content` keyword with no limit (see [`Dimension::fit_content`])
604    FitContent,
605    /// The `stretch` keyword (see [`Dimension::stretch`])
606    Stretch,
607    /// The `content` keyword (see [`Dimension::content`])
608    Content,
609    /// A `calc()` value (see [`Dimension::calc`]). The pointer is an opaque handle to the calc
610    /// representation, exactly as passed to the constructor.
611    #[cfg(feature = "calc")]
612    Calc(*const ()),
613}
614
615impl From<Dimension> for ExpandedDimension {
616    fn from(value: Dimension) -> Self {
617        value.expand()
618    }
619}
620
621impl From<ExpandedDimension> for Dimension {
622    fn from(value: ExpandedDimension) -> Self {
623        match value {
624            ExpandedDimension::Length(val) => Self::length(val),
625            ExpandedDimension::Percent(val) => Self::percent(val),
626            ExpandedDimension::Auto => Self::auto(),
627            ExpandedDimension::MinContent => Self::min_content(),
628            ExpandedDimension::MaxContent => Self::max_content(),
629            ExpandedDimension::FitContentPx(val) => Self::fit_content_px(val),
630            ExpandedDimension::FitContentPercent(val) => Self::fit_content_percent(val),
631            ExpandedDimension::FitContent => Self::fit_content(),
632            #[cfg(feature = "calc")]
633            ExpandedDimension::FitContentCalc(ptr) => Self::fit_content_calc(ptr),
634            ExpandedDimension::Stretch => Self::stretch(),
635            ExpandedDimension::Content => Self::content(),
636            #[cfg(feature = "calc")]
637            ExpandedDimension::Calc(ptr) => Self::calc(ptr),
638        }
639    }
640}
641
642#[cfg(feature = "serde")]
643impl<'de> serde::Deserialize<'de> for Dimension {
644    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
645    where
646        D: serde::Deserializer<'de>,
647    {
648        let inner = CompactLength::deserialize(deserializer)?;
649        // Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
650        if matches!(
651            inner.tag(),
652            CompactLength::LENGTH_TAG
653                | CompactLength::PERCENT_TAG
654                | CompactLength::AUTO_TAG
655                | CompactLength::MIN_CONTENT_TAG
656                | CompactLength::MAX_CONTENT_TAG
657                | CompactLength::FIT_CONTENT_KEYWORD_TAG
658                | CompactLength::FIT_CONTENT_PX_TAG
659                | CompactLength::FIT_CONTENT_PERCENT_TAG
660                | CompactLength::STRETCH_TAG
661                | CompactLength::CONTENT_TAG
662        ) {
663            Ok(Self(inner))
664        } else {
665            Err(serde::de::Error::custom("Invalid tag"))
666        }
667    }
668}
669
670impl Rect<Dimension> {
671    /// Create a new Rect with length values
672    #[must_use]
673    pub const fn from_length(start: f32, end: f32, top: f32, bottom: f32) -> Self {
674        Rect {
675            left: Dimension(CompactLength::length(start)),
676            right: Dimension(CompactLength::length(end)),
677            top: Dimension(CompactLength::length(top)),
678            bottom: Dimension(CompactLength::length(bottom)),
679        }
680    }
681
682    /// Create a new Rect with percentage values
683    #[must_use]
684    pub const fn from_percent(start: f32, end: f32, top: f32, bottom: f32) -> Self {
685        Rect {
686            left: Dimension(CompactLength::percent(start)),
687            right: Dimension(CompactLength::percent(end)),
688            top: Dimension(CompactLength::percent(top)),
689            bottom: Dimension(CompactLength::percent(bottom)),
690        }
691    }
692}
693
694#[cfg(test)]
695mod expand_tests {
696    use super::*;
697
698    /// A helper that produces a valid `calc()` handle for tests: a non-null pointer whose low
699    /// three bits are zero (as required by `CompactLength::calc`).
700    #[cfg(feature = "calc")]
701    fn calc_handle() -> *const () {
702        #[allow(dead_code)]
703        #[repr(align(8))]
704        struct Aligned(u64);
705        static HANDLE: Aligned = Aligned(0);
706        &HANDLE as *const Aligned as *const ()
707    }
708
709    #[test]
710    fn length_percentage_round_trips() {
711        let cases = [LengthPercentage::length(12.0), LengthPercentage::percent(0.5), LengthPercentage::ZERO];
712        for value in cases {
713            assert_eq!(LengthPercentage::from(value.expand()), value);
714            assert_eq!(ExpandedLengthPercentage::from(value), value.expand());
715        }
716        assert_eq!(LengthPercentage::length(3.0).expand(), ExpandedLengthPercentage::Length(3.0));
717        assert_eq!(LengthPercentage::percent(0.25).expand(), ExpandedLengthPercentage::Percent(0.25));
718    }
719
720    #[test]
721    fn length_percentage_auto_round_trips() {
722        let cases =
723            [LengthPercentageAuto::length(12.0), LengthPercentageAuto::percent(0.5), LengthPercentageAuto::auto()];
724        for value in cases {
725            assert_eq!(LengthPercentageAuto::from(value.expand()), value);
726        }
727        assert_eq!(LengthPercentageAuto::auto().expand(), ExpandedLengthPercentageAuto::Auto);
728    }
729
730    #[test]
731    fn dimension_round_trips_all_keywords() {
732        let cases = [
733            Dimension::length(12.0),
734            Dimension::percent(0.5),
735            Dimension::auto(),
736            Dimension::min_content(),
737            Dimension::max_content(),
738            Dimension::fit_content_px(30.0),
739            Dimension::fit_content_percent(0.75),
740            Dimension::fit_content(),
741            Dimension::stretch(),
742            Dimension::content(),
743        ];
744        for value in cases {
745            assert_eq!(Dimension::from(value.expand()), value);
746        }
747        assert_eq!(Dimension::fit_content_px(30.0).expand(), ExpandedDimension::FitContentPx(30.0));
748        assert_eq!(Dimension::content().expand(), ExpandedDimension::Content);
749    }
750
751    #[cfg(feature = "calc")]
752    #[test]
753    fn calc_round_trips() {
754        let handle = calc_handle();
755        assert_eq!(LengthPercentage::calc(handle).expand(), ExpandedLengthPercentage::Calc(handle));
756        assert_eq!(LengthPercentage::from(ExpandedLengthPercentage::Calc(handle)), LengthPercentage::calc(handle));
757        assert_eq!(Dimension::calc(handle).expand(), ExpandedDimension::Calc(handle));
758        assert_eq!(Dimension::fit_content_calc(handle).expand(), ExpandedDimension::FitContentCalc(handle));
759        assert_eq!(Dimension::from(ExpandedDimension::FitContentCalc(handle)), Dimension::fit_content_calc(handle));
760        assert_eq!(LengthPercentageAuto::calc(handle).expand(), ExpandedLengthPercentageAuto::Calc(handle));
761    }
762}