Skip to main content

taffy/style/
compact_length.rs

1//! A tagged-pointer abstraction that allows size styles in Taffy to be represented
2//! in just 64 bits. Wrapped by types in the `super::dimension` and `super::grid` modules.
3use super::LengthPercentage;
4use crate::style_helpers::{
5    FromFr, FromLength, FromPercent, TaffyAuto, TaffyFitContent, TaffyMaxContent, TaffyMinContent, TaffyZero,
6};
7
8/// Note: these two functions are copied directly from the std (core) library. But by duplicating them
9/// here we can reduce MSRV from 1.84 all the way down to 1.65 while retaining const constructors and
10/// strict pointer provenance
11mod compat {
12    #![allow(unsafe_code)]
13    #![allow(unknown_lints)]
14    #![allow(unnecessary_transmutes)]
15
16    /// Raw transmutation from `f32` to `u32`.
17    pub const fn f32_to_bits(val: f32) -> u32 {
18        // SAFETY: `u32` is a plain old datatype so we can always transmute to it.
19        unsafe { core::mem::transmute(val) }
20    }
21    /// Raw transmutation from `u32` to `f32`.
22    pub const fn f32_from_bits(v: u32) -> f32 {
23        // SAFETY: `u32` is a plain old datatype so we can always transmute from it.
24        unsafe { core::mem::transmute(v) }
25    }
26
27    /// Tag a pointer preserving provenance (requires Rust 1.84)
28    #[inline(always)]
29    #[cfg(all(target_pointer_width = "64", feature = "strict_provenance"))]
30    pub fn tag_ptr(ptr: *const (), tag: usize) -> *const () {
31        ptr.map_addr(|a| a | tag)
32    }
33
34    /// Tag a pointer exposing provenance (works back to Rust 1.0)
35    #[inline(always)]
36    #[cfg(all(target_pointer_width = "64", not(feature = "strict_provenance")))]
37    pub fn tag_ptr(ptr: *const (), tag: usize) -> *const () {
38        (ptr as usize | tag) as *const ()
39    }
40
41    /// Clear the tag bits of a pointer preserving provenance (requires Rust 1.84)
42    #[inline(always)]
43    #[cfg(all(target_pointer_width = "64", feature = "strict_provenance"))]
44    pub fn untag_ptr(ptr: *const (), mask: usize) -> *const () {
45        ptr.map_addr(|a| a & !mask)
46    }
47
48    /// Clear the tag bits of a pointer exposing provenance (works back to Rust 1.0)
49    #[inline(always)]
50    #[cfg(all(target_pointer_width = "64", not(feature = "strict_provenance")))]
51    pub fn untag_ptr(ptr: *const (), mask: usize) -> *const () {
52        (ptr as usize & !mask) as *const ()
53    }
54}
55
56#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))]
57std::compile_error!("Taffy only supports targets with a pointer width of 32 or 64 bits");
58
59/// CompactLengthInner implementation for 64 bit platforms
60#[cfg(target_pointer_width = "64")]
61mod inner {
62    use super::compat::{f32_from_bits, f32_to_bits, tag_ptr, untag_ptr};
63
64    /// The low byte (8 bits)
65    const TAG_MASK: usize = 0b11111111;
66    /// The low 3 bits
67    const CALC_TAG_MASK: usize = 0b111;
68
69    /// On 64 bit platforms the tag, value and pointer are packed into a single 64 bit pointer
70    ///
71    /// The tagged pointer always has a tag and may contain an f32 value or a pointer
72    /// (or neither) depending on the variant indicated by the tag.
73    #[derive(Copy, Clone, Debug, PartialEq)]
74    pub(super) struct CompactLengthInner {
75        /// The tagged pointer
76        tagged_ptr: *const (),
77    }
78    impl CompactLengthInner {
79        /// Construct a `CompactLengthInner` from a tag and pointer
80        #[inline(always)]
81        pub(super) fn from_ptr(ptr: *const (), tag: usize) -> Self {
82            let tagged_ptr = tag_ptr(ptr, tag);
83            Self { tagged_ptr }
84        }
85
86        /// Construct a `CompactLengthInner` from a tag and numeric value
87        #[inline(always)]
88        pub(super) const fn from_val(val: f32, tag: usize) -> Self {
89            let tagged_ptr = (((f32_to_bits(val) as usize) << 32) | tag) as *const ();
90            Self { tagged_ptr }
91        }
92
93        /// Construct a `CompactLengthInner` from only a tag
94        #[inline(always)]
95        pub(super) const fn from_tag(tag: usize) -> Self {
96            let tagged_ptr = tag as *const ();
97            Self { tagged_ptr }
98        }
99
100        /// Get the calc tag (low 3 bits)
101        #[inline(always)]
102        #[cfg(feature = "calc")]
103        pub(super) fn calc_tag(self) -> usize {
104            (self.tagged_ptr as usize) & CALC_TAG_MASK
105        }
106
107        /// Get the general tag (low 8 bits)
108        #[inline(always)]
109        pub(super) fn tag(self) -> usize {
110            (self.tagged_ptr as usize) & TAG_MASK
111        }
112
113        /// Get the pointer value (with the tag bits cleared)
114        #[inline(always)]
115        pub(super) fn ptr(self) -> *const () {
116            untag_ptr(self.tagged_ptr, CALC_TAG_MASK)
117        }
118
119        /// Get the numeric value
120        #[inline(always)]
121        pub(super) fn value(self) -> f32 {
122            f32_from_bits((self.tagged_ptr as usize >> 32) as u32)
123        }
124
125        /// Get the serialized value.
126        #[inline(always)]
127        #[cfg(feature = "serde")]
128        pub(super) fn serialized(self) -> u64 {
129            (self.tagged_ptr as usize as u64).rotate_left(32)
130        }
131
132        /// Derialized from a value.
133        #[inline(always)]
134        #[cfg(feature = "serde")]
135        pub(super) fn from_serialized(value: u64) -> Self {
136            Self { tagged_ptr: value.rotate_right(32) as usize as *const () }
137        }
138    }
139}
140
141/// CompactLengthInner implementation for 32 bit platforms
142#[cfg(target_pointer_width = "32")]
143mod inner {
144    use super::compat::{f32_from_bits, f32_to_bits};
145
146    /// On 32 bit platforms the tag is stored separately.
147    /// Either an f32 value or a pointer (or neither) are packed into the ptr field
148    /// depending on the variant indicated by the tag
149    #[derive(Copy, Clone, Debug, PartialEq)]
150    pub(super) struct CompactLengthInner {
151        /// The tag indicating what kind of value we are storing
152        tag: usize,
153        /// The pointer of numeric value
154        ptr: *const (),
155    }
156
157    impl CompactLengthInner {
158        /// Construct a `CompactLengthInner` from a tag and pointer
159        #[inline(always)]
160        pub(super) fn from_ptr(ptr: *const (), tag: usize) -> Self {
161            Self { ptr, tag }
162        }
163
164        /// Construct a `CompactLengthInner` from a tag and numeric value
165        #[inline(always)]
166        pub(super) const fn from_val(val: f32, tag: usize) -> Self {
167            Self { ptr: f32_to_bits(val) as usize as *const (), tag }
168        }
169
170        /// Construct a `CompactLengthInner` from only a tag
171        #[inline(always)]
172        pub(super) const fn from_tag(tag: usize) -> Self {
173            Self { ptr: 0 as *const (), tag }
174        }
175
176        /// Get the calc tag (low 3 bits)
177        #[inline(always)]
178        #[cfg(feature = "calc")]
179        pub(super) fn calc_tag(self) -> usize {
180            self.tag
181        }
182
183        /// Get the general tag (low 8 bits)
184        #[inline(always)]
185        pub(super) fn tag(self) -> usize {
186            self.tag
187        }
188
189        /// Get the pointer value
190        #[inline(always)]
191        pub(super) fn ptr(self) -> *const () {
192            self.ptr
193        }
194
195        /// Get the numeric value
196        #[inline(always)]
197        pub(super) fn value(self) -> f32 {
198            f32_from_bits(self.ptr as u32)
199        }
200
201        /// Get the serialized value.
202        #[inline(always)]
203        #[cfg(feature = "serde")]
204        pub(super) fn serialized(self) -> u64 {
205            (self.tag as u64) << 32 | (self.ptr as u64)
206        }
207
208        /// Derialized from a value.
209        #[inline(always)]
210        #[cfg(feature = "serde")]
211        pub(super) fn from_serialized(value: u64) -> Self {
212            Self { tag: (value >> 32) as usize, ptr: (value & 0xFFFFFFFF) as usize as *const () }
213        }
214    }
215}
216
217use inner::CompactLengthInner;
218
219/// A representation of a length as a compact 64-bit tagged pointer
220#[derive(Copy, Clone, PartialEq, Debug)]
221#[repr(transparent)]
222pub struct CompactLength(CompactLengthInner);
223
224/// Tag layout
225///
226/// Tags are 8 bits. The low 3 bits are shared with pointer variants (such as `calc()`), which require
227/// an 8-byte aligned pointer and so always have those bits clear. They are assigned as follows:
228///
229///   - Bit 0 (`NON_POINTER_BIT`): set for every variant that is *not* a pointer. Pointer variants
230///     (`calc()`) have this bit clear, so `is_plain_calc()` is a single bit test.
231///   - Bit 1 (`KEYWORD_BIT`): set for every variant that is *not* a plain length, percentage or calc()
232///     value (i.e. everything that cannot be resolved to a length given only a percentage basis).
233///   - Bit 2 (`PERCENTAGE_BIT`): set for every variant whose resolved value depends on a percentage basis,
234///     including all `calc()` variants. `uses_percentage()` is a single bit test.
235///
236/// Pointer variants are distinguished by bit 1: `calc()` is `0b100` and `fit-content(calc())` is `0b110`.
237/// Any pointer variant's `tag()` includes address bits 3..7, so pointer variants must be tested with low-bit
238/// masks (`is_plain_calc()`, `is_fit_content_calc()`, `has_calc()`) rather than as `match` arms.
239///
240/// Within the keyword class, bit 3 (`SIZING_KEYWORD_BIT`) is set for the CSS sizing keywords
241/// (`min-content`, `max-content`, `fit-content`, `fit-content()`, `stretch`) and clear for `auto`,
242/// `fr` and `content`. The remaining high bits distinguish variants within a class.
243impl CompactLength {
244    /// Bit set for all non-pointer (i.e. not `calc()`) tags
245    pub const NON_POINTER_BIT: usize = 0b0001;
246    /// Bit set for all tags that are not a plain length, percentage or calc() value
247    pub const KEYWORD_BIT: usize = 0b0010;
248    /// Bit set for all tags whose value depends on a percentage basis (including `calc()`)
249    pub const PERCENTAGE_BIT: usize = 0b0100;
250    /// Bit set (in addition to `KEYWORD_BIT`) for all CSS sizing keyword tags
251    pub const SIZING_KEYWORD_BIT: usize = 0b1000;
252
253    /// The tag indicating a calc() value
254    #[cfg(feature = "calc")]
255    pub const CALC_TAG: usize = 0b0000_0100;
256    /// The tag indicating a fit-content() value with a calc() limit
257    #[cfg(feature = "calc")]
258    pub const FIT_CONTENT_CALC_TAG: usize = 0b0000_0110;
259    /// The tag indicating a length value
260    pub const LENGTH_TAG: usize = 0b0000_0001;
261    /// The tag indicating a percentage value
262    pub const PERCENT_TAG: usize = 0b0000_0101;
263    /// The tag indicating an auto value
264    pub const AUTO_TAG: usize = 0b0000_0011;
265    /// The tag indicating an fr value
266    pub const FR_TAG: usize = 0b0001_0011;
267    /// The tag indicating a content keyword value
268    pub const CONTENT_TAG: usize = 0b0010_0011;
269    /// The tag indicating a min-content value
270    pub const MIN_CONTENT_TAG: usize = 0b0000_1011;
271    /// The tag indicating a max-content value
272    pub const MAX_CONTENT_TAG: usize = 0b0001_1011;
273    /// The tag indicating a fit-content value with px limit
274    pub const FIT_CONTENT_PX_TAG: usize = 0b0010_1011;
275    /// The tag indicating a plain fit-content keyword value (no limit)
276    pub const FIT_CONTENT_KEYWORD_TAG: usize = 0b0011_1011;
277    /// The tag indicating a stretch keyword value
278    pub const STRETCH_TAG: usize = 0b0100_1011;
279    /// The tag indicating a fit-content value with percent limit
280    pub const FIT_CONTENT_PERCENT_TAG: usize = 0b0000_1111;
281}
282
283impl CompactLength {
284    /// An absolute length in some abstract units. Users of Taffy may define what they correspond
285    /// to in their application (pixels, logical pixels, mm, etc) as they see fit.
286    #[inline(always)]
287    pub const fn length(val: f32) -> Self {
288        Self(CompactLengthInner::from_val(val, Self::LENGTH_TAG))
289    }
290
291    /// A percentage length relative to the size of the containing block.
292    ///
293    /// **NOTE: percentages are represented as a f32 value in the range [0.0, 1.0] NOT the range [0.0, 100.0]**
294    #[inline(always)]
295    pub const fn percent(val: f32) -> Self {
296        Self(CompactLengthInner::from_val(val, Self::PERCENT_TAG))
297    }
298
299    /// A `calc()` value. The value passed here is treated as an opaque handle to
300    /// the actual calc representation and may be a pointer, index, etc.
301    ///
302    /// The low 3 bits are used as a tag value and will be returned as 0.
303    #[inline]
304    #[cfg(feature = "calc")]
305    pub fn calc(ptr: *const ()) -> Self {
306        assert_ne!(ptr as u64, 0);
307        assert_eq!(ptr as u64 & 0b111, 0);
308        Self(CompactLengthInner::from_ptr(ptr, Self::CALC_TAG))
309    }
310
311    /// A `fit-content(calc())` value. The value passed here is treated as an opaque handle to
312    /// the actual calc representation of the limit and may be a pointer, index, etc.
313    ///
314    /// The low 3 bits are used as a tag value and will be returned as 0.
315    #[inline]
316    #[cfg(feature = "calc")]
317    pub fn fit_content_calc(ptr: *const ()) -> Self {
318        assert_ne!(ptr as u64, 0);
319        assert_eq!(ptr as u64 & 0b111, 0);
320        Self(CompactLengthInner::from_ptr(ptr, Self::FIT_CONTENT_CALC_TAG))
321    }
322
323    /// The dimension should be automatically computed according to algorithm-specific rules
324    /// regarding the default size of boxes.
325    #[inline(always)]
326    pub const fn auto() -> Self {
327        Self(CompactLengthInner::from_tag(Self::AUTO_TAG))
328    }
329
330    /// The dimension as a fraction of the total available grid space (`fr` units in CSS)
331    /// Specified value is the numerator of the fraction. Denominator is the sum of all fraction specified in that grid dimension
332    /// Spec: <https://www.w3.org/TR/css3-grid-layout/#fr-unit>
333    #[inline(always)]
334    pub const fn fr(val: f32) -> Self {
335        Self(CompactLengthInner::from_val(val, Self::FR_TAG))
336    }
337
338    /// The size should be the "min-content" size.
339    /// This is the smallest size that can fit the item's contents with ALL soft line-wrapping opportunities taken
340    #[inline(always)]
341    pub const fn min_content() -> Self {
342        Self(CompactLengthInner::from_tag(Self::MIN_CONTENT_TAG))
343    }
344
345    /// The size should be the "max-content" size.
346    /// This is the smallest size that can fit the item's contents with NO soft line-wrapping opportunities taken
347    #[inline(always)]
348    pub const fn max_content() -> Self {
349        Self(CompactLengthInner::from_tag(Self::MAX_CONTENT_TAG))
350    }
351
352    /// The size should be computed according to the "fit content" formula:
353    ///    `max(min_content, min(max_content, limit))`
354    /// where:
355    ///    - `min_content` is the [min-content](Self::min_content) size
356    ///    - `max_content` is the [max-content](Self::max_content) size
357    ///    - `limit` is a LENGTH value passed to this function
358    ///
359    /// The effect of this is that the item takes the size of `limit` clamped
360    /// by the min-content and max-content sizes.
361    #[inline(always)]
362    pub const fn fit_content_px(limit: f32) -> Self {
363        Self(CompactLengthInner::from_val(limit, Self::FIT_CONTENT_PX_TAG))
364    }
365
366    /// The size should be computed according to the "fit content" formula:
367    ///    `max(min_content, min(max_content, limit))`
368    /// where:
369    ///    - `min_content` is the [min-content](Self::min_content) size
370    ///    - `max_content` is the [max-content](Self::max_content) size
371    ///    - `limit` is a PERCENTAGE value passed to this function
372    ///
373    /// The effect of this is that the item takes the size of `limit` clamped
374    /// by the min-content and max-content sizes.
375    #[inline(always)]
376    pub const fn fit_content_percent(limit: f32) -> Self {
377        Self(CompactLengthInner::from_val(limit, Self::FIT_CONTENT_PERCENT_TAG))
378    }
379
380    /// The size should be computed according to the "fit content" formula:
381    ///    `max(min_content, min(max_content, stretch))`
382    /// where:
383    ///    - `min_content` is the [min-content](Self::min_content) size
384    ///    - `max_content` is the [max-content](Self::max_content) size
385    ///    - `stretch` is the "stretch-fit" size (the size the box would take if it filled the available space)
386    #[inline(always)]
387    pub const fn fit_content_keyword() -> Self {
388        Self(CompactLengthInner::from_tag(Self::FIT_CONTENT_KEYWORD_TAG))
389    }
390
391    /// The size should be the "stretch-fit" size: the size the box would take
392    /// if it filled the available space
393    /// (<https://www.w3.org/TR/css-sizing-4/#stretch-fit-sizing>)
394    #[inline(always)]
395    pub const fn stretch() -> Self {
396        Self(CompactLengthInner::from_tag(Self::STRETCH_TAG))
397    }
398
399    /// The size should be an automatic size based on the box's content
400    /// (<https://www.w3.org/TR/css-flexbox-1/#valdef-flex-basis-content>)
401    ///
402    /// This keyword is only valid for `flex-basis`. In any other context it behaves as [`auto`](Self::auto).
403    #[inline(always)]
404    pub const fn content() -> Self {
405        Self(CompactLengthInner::from_tag(Self::CONTENT_TAG))
406    }
407
408    /// Get the primary tag
409    #[inline(always)]
410    pub fn tag(self) -> usize {
411        self.0.tag()
412    }
413
414    /// Get the numeric value associated with the `CompactLength`
415    /// (e.g. the pixel value for a LENGTH variant)
416    #[inline(always)]
417    pub fn value(self) -> f32 {
418        self.0.value()
419    }
420
421    /// Get the calc pointer of the `CompactLength`
422    #[inline(always)]
423    #[cfg(feature = "calc")]
424    pub fn calc_value(self) -> *const () {
425        self.0.ptr()
426    }
427
428    /// Returns true if the value is a plain calc() value (not a fit-content(calc()) value)
429    #[inline(always)]
430    #[cfg(feature = "calc")]
431    pub fn is_plain_calc(self) -> bool {
432        self.0.calc_tag() & (Self::NON_POINTER_BIT | Self::KEYWORD_BIT) == 0
433    }
434
435    /// Returns true if the value is a fit-content() value with a calc() limit
436    #[inline(always)]
437    pub fn is_fit_content_calc(self) -> bool {
438        self.tag() & (Self::NON_POINTER_BIT | Self::KEYWORD_BIT) == Self::KEYWORD_BIT
439    }
440
441    /// Returns true if the value holds a calc() pointer: either a plain calc() value or a
442    /// fit-content() value with a calc() limit. `calc_value()` is valid for exactly these values.
443    #[inline(always)]
444    pub fn has_calc(self) -> bool {
445        self.tag() & Self::NON_POINTER_BIT == 0
446    }
447
448    /// Returns true if the value is 0 px
449    #[inline(always)]
450    pub fn is_zero(self) -> bool {
451        self.0 == Self::ZERO.0
452    }
453
454    /// Returns true if the value is a length or percentage value
455    #[inline(always)]
456    pub fn is_length_or_percentage(self) -> bool {
457        self.tag() & (Self::NON_POINTER_BIT | Self::KEYWORD_BIT) == Self::NON_POINTER_BIT
458    }
459
460    /// Returns true if the value is a length, a percentage or a plain `calc()` value (not a
461    /// `fit-content(calc())` value): anything that resolves to a length given a percentage basis
462    #[inline(always)]
463    pub fn is_length_percentage_or_calc(self) -> bool {
464        self.tag() & Self::KEYWORD_BIT == 0
465    }
466
467    /// Returns true if the value is auto
468    #[inline(always)]
469    pub fn is_auto(self) -> bool {
470        self.tag() == Self::AUTO_TAG
471    }
472
473    /// Returns true if the value is the content keyword
474    #[inline(always)]
475    pub fn is_content(self) -> bool {
476        self.tag() == Self::CONTENT_TAG
477    }
478
479    /// Returns true if the value is min-content
480    #[inline(always)]
481    pub fn is_min_content(self) -> bool {
482        matches!(self.tag(), Self::MIN_CONTENT_TAG)
483    }
484
485    /// Returns true if the value is max-content
486    #[inline(always)]
487    pub fn is_max_content(self) -> bool {
488        matches!(self.tag(), Self::MAX_CONTENT_TAG)
489    }
490
491    /// Returns true if the value is a fit-content(...) value
492    #[inline(always)]
493    pub fn is_fit_content(self) -> bool {
494        matches!(self.tag(), Self::FIT_CONTENT_PX_TAG | Self::FIT_CONTENT_PERCENT_TAG) || self.is_fit_content_calc()
495    }
496
497    /// Returns true if the value is min-content, max-content, fit-content, fit-content(...), or stretch
498    #[inline(always)]
499    pub fn is_sizing_keyword(self) -> bool {
500        const MASK: usize =
501            CompactLength::NON_POINTER_BIT | CompactLength::KEYWORD_BIT | CompactLength::SIZING_KEYWORD_BIT;
502        self.tag() & MASK == MASK || self.is_fit_content_calc()
503    }
504
505    /// Returns true if the value is max-content or a fit-content(...) value
506    #[inline(always)]
507    pub fn is_max_or_fit_content(self) -> bool {
508        matches!(self.tag(), Self::MAX_CONTENT_TAG | Self::FIT_CONTENT_PX_TAG | Self::FIT_CONTENT_PERCENT_TAG)
509            || self.is_fit_content_calc()
510    }
511
512    /// Returns true if the max track sizing function is `MaxContent`, `FitContent` or `Auto` else false.
513    /// "In all cases, treat auto and fit-content() as max-content, except where specified otherwise for fit-content()."
514    /// See: <https://www.w3.org/TR/css-grid-1/#algo-terms>
515    #[inline(always)]
516    pub fn is_max_content_alike(&self) -> bool {
517        matches!(
518            self.tag(),
519            CompactLength::AUTO_TAG
520                | CompactLength::MAX_CONTENT_TAG
521                | CompactLength::FIT_CONTENT_PX_TAG
522                | CompactLength::FIT_CONTENT_PERCENT_TAG
523        ) || self.is_fit_content_calc()
524    }
525
526    /// Returns true if the min track sizing function is `MinContent` or `MaxContent`, else false.
527    #[inline(always)]
528    pub fn is_min_or_max_content(&self) -> bool {
529        matches!(self.tag(), Self::MIN_CONTENT_TAG | Self::MAX_CONTENT_TAG)
530    }
531
532    /// Returns true if the value is auto, min-content, max-content, or fit-content(...)
533    #[inline(always)]
534    pub fn is_intrinsic(self) -> bool {
535        matches!(
536            self.tag(),
537            Self::AUTO_TAG
538                | Self::MIN_CONTENT_TAG
539                | Self::MAX_CONTENT_TAG
540                | Self::FIT_CONTENT_PX_TAG
541                | Self::FIT_CONTENT_PERCENT_TAG
542        ) || self.is_fit_content_calc()
543    }
544
545    /// Returns true if the value is and fr value
546    #[inline(always)]
547    pub fn is_fr(self) -> bool {
548        self.tag() == Self::FR_TAG
549    }
550
551    /// Whether the track sizing functions depends on the size of the parent node
552    #[inline(always)]
553    pub fn uses_percentage(self) -> bool {
554        self.tag() & Self::PERCENTAGE_BIT != 0
555    }
556
557    /// Resolve percentage values against the passed parent_size, returning Some(value)
558    /// Non-percentage values always return None.
559    #[inline(always)]
560    pub fn resolved_percentage_size(
561        self,
562        parent_size: f32,
563        calc_resolver: impl Fn(*const (), f32) -> f32,
564    ) -> Option<f32> {
565        match self.tag() {
566            CompactLength::PERCENT_TAG => Some(self.value() * parent_size),
567            #[cfg(feature = "calc")]
568            _ if self.is_plain_calc() => Some(calc_resolver(self.0.ptr(), parent_size)),
569            _ => None,
570        }
571    }
572}
573
574impl TaffyZero for CompactLength {
575    const ZERO: Self = Self::length(0.0);
576}
577impl TaffyAuto for CompactLength {
578    const AUTO: Self = Self::auto();
579}
580impl TaffyMinContent for CompactLength {
581    const MIN_CONTENT: Self = Self::min_content();
582}
583impl TaffyMaxContent for CompactLength {
584    const MAX_CONTENT: Self = Self::max_content();
585}
586impl FromLength for CompactLength {
587    fn from_length<Input: Into<f64> + Copy>(value: Input) -> Self {
588        Self::length(value.into() as f32)
589    }
590}
591impl FromPercent for CompactLength {
592    fn from_percent<Input: Into<f64> + Copy>(value: Input) -> Self {
593        Self::percent(value.into() as f32)
594    }
595}
596impl FromFr for CompactLength {
597    fn from_fr<Input: Into<f64> + Copy>(value: Input) -> Self {
598        Self::fr(value.into() as f32)
599    }
600}
601impl TaffyFitContent for CompactLength {
602    fn fit_content(lp: LengthPercentage) -> Self {
603        let value = lp.0.value();
604        match lp.0.tag() {
605            Self::LENGTH_TAG => Self::fit_content_px(value),
606            Self::PERCENT_TAG => Self::fit_content_percent(value),
607            #[cfg(feature = "calc")]
608            _ if lp.0.is_plain_calc() => Self::fit_content_calc(lp.0.calc_value()),
609            _ => unreachable!(),
610        }
611    }
612}
613
614#[cfg(feature = "serde")]
615impl serde::Serialize for CompactLength {
616    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
617    where
618        S: serde::Serializer,
619    {
620        #[cfg(feature = "calc")]
621        {
622            if self.has_calc() {
623                Err(serde::ser::Error::custom("Cannot serialize Calc value"))
624            } else {
625                serializer.serialize_u64(self.0.serialized())
626            }
627        }
628
629        #[cfg(not(feature = "calc"))]
630        serializer.serialize_u64(self.0.serialized())
631    }
632}
633
634#[cfg(feature = "serde")]
635impl<'de> serde::Deserialize<'de> for CompactLength {
636    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
637    where
638        D: serde::Deserializer<'de>,
639    {
640        let bits: u64 = u64::deserialize(deserializer)?;
641        let value = Self(CompactLengthInner::from_serialized(bits));
642        // Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported.
643        // All of the accepted tags have `NON_POINTER_BIT` set, so a calc() tag can never pass validation.
644        if matches!(
645            value.tag(),
646            CompactLength::LENGTH_TAG
647                | CompactLength::PERCENT_TAG
648                | CompactLength::AUTO_TAG
649                | CompactLength::MIN_CONTENT_TAG
650                | CompactLength::MAX_CONTENT_TAG
651                | CompactLength::FIT_CONTENT_KEYWORD_TAG
652                | CompactLength::FIT_CONTENT_PX_TAG
653                | CompactLength::FIT_CONTENT_PERCENT_TAG
654                | CompactLength::STRETCH_TAG
655                | CompactLength::CONTENT_TAG
656                | CompactLength::FR_TAG
657        ) {
658            Ok(value)
659        } else {
660            Err(serde::de::Error::custom("Cannot deserialize Calc value"))
661        }
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use super::{CompactLength, CompactLengthInner};
668
669    impl CompactLength {
670        fn from_raw_tag(tag: usize) -> Self {
671            Self(CompactLengthInner::from_tag(tag))
672        }
673    }
674
675    const NON_PERCENTAGE_TAGS: &[usize] = &[
676        CompactLength::LENGTH_TAG,
677        CompactLength::AUTO_TAG,
678        CompactLength::FR_TAG,
679        CompactLength::MIN_CONTENT_TAG,
680        CompactLength::MAX_CONTENT_TAG,
681        CompactLength::FIT_CONTENT_PX_TAG,
682        CompactLength::FIT_CONTENT_KEYWORD_TAG,
683        CompactLength::STRETCH_TAG,
684        CompactLength::CONTENT_TAG,
685    ];
686    const PERCENTAGE_TAGS: &[usize] = &[CompactLength::PERCENT_TAG, CompactLength::FIT_CONTENT_PERCENT_TAG];
687
688    #[test]
689    fn tags_are_unique_and_fit_in_a_byte() {
690        let mut all: Vec<usize> = NON_PERCENTAGE_TAGS.iter().chain(PERCENTAGE_TAGS).copied().collect();
691        #[cfg(feature = "calc")]
692        all.push(CompactLength::CALC_TAG);
693        #[cfg(feature = "calc")]
694        all.push(CompactLength::FIT_CONTENT_CALC_TAG);
695        assert!(all.iter().all(|tag| *tag <= 0xFF));
696        all.sort_unstable();
697        all.dedup();
698        assert_eq!(all.len(), NON_PERCENTAGE_TAGS.len() + PERCENTAGE_TAGS.len() + 2 * cfg!(feature = "calc") as usize);
699    }
700
701    #[test]
702    fn non_pointer_tags_have_non_pointer_bit_set() {
703        for tag in NON_PERCENTAGE_TAGS.iter().chain(PERCENTAGE_TAGS) {
704            assert_ne!(tag & CompactLength::NON_POINTER_BIT, 0, "tag {tag:#010b}");
705        }
706        #[cfg(feature = "calc")]
707        assert_eq!(CompactLength::CALC_TAG & CompactLength::NON_POINTER_BIT, 0);
708    }
709
710    #[test]
711    fn keyword_bits_match_predicates() {
712        let all = NON_PERCENTAGE_TAGS.iter().chain(PERCENTAGE_TAGS).map(|tag| CompactLength::from_raw_tag(*tag));
713        for value in all {
714            let tag = value.tag();
715            let is_plain = matches!(tag, CompactLength::LENGTH_TAG | CompactLength::PERCENT_TAG);
716            assert_eq!(tag & CompactLength::KEYWORD_BIT == 0, is_plain, "tag {tag:#010b}");
717            assert_eq!(value.is_length_or_percentage(), is_plain, "tag {tag:#010b}");
718            let is_sizing_keyword = matches!(
719                tag,
720                CompactLength::MIN_CONTENT_TAG
721                    | CompactLength::MAX_CONTENT_TAG
722                    | CompactLength::FIT_CONTENT_KEYWORD_TAG
723                    | CompactLength::FIT_CONTENT_PX_TAG
724                    | CompactLength::FIT_CONTENT_PERCENT_TAG
725                    | CompactLength::STRETCH_TAG
726            );
727            assert_eq!(value.is_sizing_keyword(), is_sizing_keyword, "tag {tag:#010b}");
728        }
729        #[cfg(feature = "calc")]
730        {
731            assert_eq!(CompactLength::CALC_TAG & CompactLength::KEYWORD_BIT, 0);
732            assert_ne!(CompactLength::FIT_CONTENT_CALC_TAG & CompactLength::KEYWORD_BIT, 0);
733            assert_eq!(CompactLength::FIT_CONTENT_CALC_TAG & CompactLength::NON_POINTER_BIT, 0);
734            assert_ne!(CompactLength::FIT_CONTENT_CALC_TAG & CompactLength::PERCENTAGE_BIT, 0);
735        }
736    }
737
738    #[test]
739    fn percentage_bit_matches_uses_percentage() {
740        for tag in NON_PERCENTAGE_TAGS {
741            assert_eq!(tag & CompactLength::PERCENTAGE_BIT, 0, "tag {tag:#010b}");
742        }
743        for tag in PERCENTAGE_TAGS {
744            assert_ne!(tag & CompactLength::PERCENTAGE_BIT, 0, "tag {tag:#010b}");
745        }
746        #[cfg(feature = "calc")]
747        assert_ne!(CompactLength::CALC_TAG & CompactLength::PERCENTAGE_BIT, 0);
748
749        assert!(!CompactLength::length(10.0).uses_percentage());
750        assert!(!CompactLength::auto().uses_percentage());
751        assert!(!CompactLength::fr(1.0).uses_percentage());
752        assert!(!CompactLength::min_content().uses_percentage());
753        assert!(!CompactLength::max_content().uses_percentage());
754        assert!(!CompactLength::fit_content_px(10.0).uses_percentage());
755        assert!(!CompactLength::fit_content_keyword().uses_percentage());
756        assert!(!CompactLength::stretch().uses_percentage());
757        assert!(!CompactLength::content().uses_percentage());
758        assert!(CompactLength::percent(0.5).uses_percentage());
759        assert!(CompactLength::fit_content_percent(0.5).uses_percentage());
760    }
761
762    #[test]
763    #[cfg(feature = "calc")]
764    fn fit_content_calc_round_trips_pointer() {
765        let boxed = Box::new(0u64);
766        let ptr = &*boxed as *const u64 as *const ();
767        let value = CompactLength::fit_content_calc(ptr);
768        assert!(value.is_fit_content_calc());
769        assert!(value.has_calc());
770        assert!(!value.is_plain_calc());
771        assert!(value.is_fit_content());
772        assert!(value.is_sizing_keyword());
773        assert!(value.is_intrinsic());
774        assert!(value.is_max_or_fit_content());
775        assert!(value.is_max_content_alike());
776        assert!(value.uses_percentage());
777        assert!(!value.is_length_or_percentage());
778        assert_eq!(value.calc_value(), ptr);
779
780        let calc = CompactLength::calc(ptr);
781        assert!(calc.is_plain_calc());
782        assert!(calc.has_calc());
783        assert!(!calc.is_fit_content_calc());
784        assert!(!calc.is_fit_content());
785        assert!(!calc.is_sizing_keyword());
786    }
787
788    #[test]
789    fn calc_round_trips_pointer() {
790        let boxed = Box::new(42u64);
791        let ptr = &*boxed as *const u64 as *const ();
792        let value = CompactLength::calc(ptr);
793        assert!(value.is_plain_calc());
794        assert!(value.uses_percentage());
795        assert!(!value.is_length_or_percentage());
796        assert!(!value.is_auto());
797        assert!(!value.is_intrinsic());
798        assert_eq!(value.calc_value(), ptr);
799    }
800}