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
42#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))]
43std::compile_error!("Taffy only supports targets with a pointer width of 32 or 64 bits");
44
45/// CompactLengthInner implementation for 64 bit platforms
46#[cfg(target_pointer_width = "64")]
47mod inner {
48    use super::compat::{f32_from_bits, f32_to_bits, tag_ptr};
49
50    /// The low byte (8 bits)
51    const TAG_MASK: usize = 0b11111111;
52    /// The low 3 bits
53    const CALC_TAG_MASK: usize = 0b111;
54    // The high 63 bits
55    // const CALC_PTR_MASK: usize = usize::MAX ^ 0b111;
56
57    /// On 64 bit platforms the tag, value and pointer are packed into a single 64 bit pointer
58    ///
59    /// The tagged pointer always has a tag and may contain an f32 value or a pointer
60    /// (or neither) depending on the variant indicated by the tag.
61    #[derive(Copy, Clone, Debug, PartialEq)]
62    pub(super) struct CompactLengthInner {
63        /// The tagged pointer
64        tagged_ptr: *const (),
65    }
66    impl CompactLengthInner {
67        /// Construct a `CompactLengthInner` from a tag and pointer
68        #[inline(always)]
69        pub(super) fn from_ptr(ptr: *const (), tag: usize) -> Self {
70            let tagged_ptr = tag_ptr(ptr, tag);
71            Self { tagged_ptr }
72        }
73
74        /// Construct a `CompactLengthInner` from a tag and numeric value
75        #[inline(always)]
76        pub(super) const fn from_val(val: f32, tag: usize) -> Self {
77            let tagged_ptr = (((f32_to_bits(val) as usize) << 32) | tag) as *const ();
78            Self { tagged_ptr }
79        }
80
81        /// Construct a `CompactLengthInner` from only a tag
82        #[inline(always)]
83        pub(super) const fn from_tag(tag: usize) -> Self {
84            let tagged_ptr = tag as *const ();
85            Self { tagged_ptr }
86        }
87
88        /// Get the calc tag (low 3 bits)
89        #[inline(always)]
90        #[cfg(feature = "calc")]
91        pub(super) fn calc_tag(self) -> usize {
92            (self.tagged_ptr as usize) & CALC_TAG_MASK
93        }
94
95        /// Get the general tag (low 8 bits)
96        #[inline(always)]
97        pub(super) fn tag(self) -> usize {
98            (self.tagged_ptr as usize) & TAG_MASK
99        }
100
101        /// Get the pointer value
102        #[inline(always)]
103        pub(super) fn ptr(self) -> *const () {
104            self.tagged_ptr
105        }
106
107        /// Get the numeric value
108        #[inline(always)]
109        pub(super) fn value(self) -> f32 {
110            f32_from_bits((self.tagged_ptr as usize >> 32) as u32)
111        }
112
113        /// Get the serialized value.
114        #[inline(always)]
115        #[cfg(feature = "serde")]
116        pub(super) fn serialized(self) -> u64 {
117            (self.tagged_ptr as usize as u64).rotate_left(32)
118        }
119
120        /// Derialized from a value.
121        #[inline(always)]
122        #[cfg(feature = "serde")]
123        pub(super) fn from_serialized(value: u64) -> Self {
124            Self { tagged_ptr: value.rotate_right(32) as usize as *const () }
125        }
126    }
127}
128
129/// CompactLengthInner implementation for 32 bit platforms
130#[cfg(target_pointer_width = "32")]
131mod inner {
132    use super::compat::{f32_from_bits, f32_to_bits};
133
134    /// On 32 bit platforms the tag is stored separately.
135    /// Either an f32 value or a pointer (or neither) are packed into the ptr field
136    /// depending on the variant indicated by the tag
137    #[derive(Copy, Clone, Debug, PartialEq)]
138    pub(super) struct CompactLengthInner {
139        /// The tag indicating what kind of value we are storing
140        tag: usize,
141        /// The pointer of numeric value
142        ptr: *const (),
143    }
144
145    impl CompactLengthInner {
146        /// Construct a `CompactLengthInner` from a tag and pointer
147        #[inline(always)]
148        pub(super) fn from_ptr(ptr: *const (), tag: usize) -> Self {
149            Self { ptr, tag }
150        }
151
152        /// Construct a `CompactLengthInner` from a tag and numeric value
153        #[inline(always)]
154        pub(super) const fn from_val(val: f32, tag: usize) -> Self {
155            Self { ptr: f32_to_bits(val) as usize as *const (), tag }
156        }
157
158        /// Construct a `CompactLengthInner` from only a tag
159        #[inline(always)]
160        pub(super) const fn from_tag(tag: usize) -> Self {
161            Self { ptr: 0 as *const (), tag }
162        }
163
164        /// Get the calc tag (low 3 bits)
165        #[inline(always)]
166        #[cfg(feature = "calc")]
167        pub(super) fn calc_tag(self) -> usize {
168            self.tag
169        }
170
171        /// Get the general tag (low 8 bits)
172        #[inline(always)]
173        pub(super) fn tag(self) -> usize {
174            self.tag
175        }
176
177        /// Get the pointer value
178        #[inline(always)]
179        pub(super) fn ptr(self) -> *const () {
180            self.ptr
181        }
182
183        /// Get the numeric value
184        #[inline(always)]
185        pub(super) fn value(self) -> f32 {
186            f32_from_bits(self.ptr as u32)
187        }
188
189        /// Get the serialized value.
190        #[inline(always)]
191        #[cfg(feature = "serde")]
192        pub(super) fn serialized(self) -> u64 {
193            (self.tag as u64) << 32 | (self.ptr as u64)
194        }
195
196        /// Derialized from a value.
197        #[inline(always)]
198        #[cfg(feature = "serde")]
199        pub(super) fn from_serialized(value: u64) -> Self {
200            Self { tag: (value >> 32) as usize, ptr: (value & 0xFFFFFFFF) as usize as *const () }
201        }
202    }
203}
204
205use inner::CompactLengthInner;
206
207/// A representation of a length as a compact 64-bit tagged pointer
208#[derive(Copy, Clone, PartialEq, Debug)]
209#[repr(transparent)]
210pub struct CompactLength(CompactLengthInner);
211
212impl CompactLength {
213    /// The tag indicating a calc() value
214    #[cfg(feature = "calc")]
215    pub const CALC_TAG: usize = 0b000;
216    /// The tag indicating a length value
217    pub const LENGTH_TAG: usize = 0b0000_0001;
218    /// The tag indicating a percentage value
219    pub const PERCENT_TAG: usize = 0b0000_0010;
220    /// The tag indicating an auto value
221    pub const AUTO_TAG: usize = 0b0000_0011;
222    /// The tag indicating an fr value
223    pub const FR_TAG: usize = 0b0000_0100;
224    /// The tag indicating a min-content value
225    pub const MIN_CONTENT_TAG: usize = 0b00000111;
226    /// The tag indicating a max-content value
227    pub const MAX_CONTENT_TAG: usize = 0b00001111;
228    /// The tag indicating a fit-content value with px limit
229    pub const FIT_CONTENT_PX_TAG: usize = 0b00010111;
230    /// The tag indicating a fit-content value with percent limit
231    pub const FIT_CONTENT_PERCENT_TAG: usize = 0b00011111;
232    /// The tag indicating a plain fit-content keyword value (no limit)
233    pub const FIT_CONTENT_KEYWORD_TAG: usize = 0b00100111;
234    /// The tag indicating a stretch keyword value
235    pub const STRETCH_TAG: usize = 0b00101111;
236    /// The tag indicating a content keyword value
237    pub const CONTENT_TAG: usize = 0b00110111;
238}
239
240impl CompactLength {
241    /// An absolute length in some abstract units. Users of Taffy may define what they correspond
242    /// to in their application (pixels, logical pixels, mm, etc) as they see fit.
243    #[inline(always)]
244    pub const fn length(val: f32) -> Self {
245        Self(CompactLengthInner::from_val(val, Self::LENGTH_TAG))
246    }
247
248    /// A percentage length relative to the size of the containing block.
249    ///
250    /// **NOTE: percentages are represented as a f32 value in the range [0.0, 1.0] NOT the range [0.0, 100.0]**
251    #[inline(always)]
252    pub const fn percent(val: f32) -> Self {
253        Self(CompactLengthInner::from_val(val, Self::PERCENT_TAG))
254    }
255
256    /// A `calc()` value. The value passed here is treated as an opaque handle to
257    /// the actual calc representation and may be a pointer, index, etc.
258    ///
259    /// The low 3 bits are used as a tag value and will be returned as 0.
260    #[inline]
261    #[cfg(feature = "calc")]
262    pub fn calc(ptr: *const ()) -> Self {
263        assert_ne!(ptr as u64, 0);
264        assert_eq!(ptr as u64 & 0b111, 0);
265        Self(CompactLengthInner::from_ptr(ptr, Self::CALC_TAG))
266    }
267
268    /// The dimension should be automatically computed according to algorithm-specific rules
269    /// regarding the default size of boxes.
270    #[inline(always)]
271    pub const fn auto() -> Self {
272        Self(CompactLengthInner::from_tag(Self::AUTO_TAG))
273    }
274
275    /// The dimension as a fraction of the total available grid space (`fr` units in CSS)
276    /// Specified value is the numerator of the fraction. Denominator is the sum of all fraction specified in that grid dimension
277    /// Spec: <https://www.w3.org/TR/css3-grid-layout/#fr-unit>
278    #[inline(always)]
279    pub const fn fr(val: f32) -> Self {
280        Self(CompactLengthInner::from_val(val, Self::FR_TAG))
281    }
282
283    /// The size should be the "min-content" size.
284    /// This is the smallest size that can fit the item's contents with ALL soft line-wrapping opportunities taken
285    #[inline(always)]
286    pub const fn min_content() -> Self {
287        Self(CompactLengthInner::from_tag(Self::MIN_CONTENT_TAG))
288    }
289
290    /// The size should be the "max-content" size.
291    /// This is the smallest size that can fit the item's contents with NO soft line-wrapping opportunities taken
292    #[inline(always)]
293    pub const fn max_content() -> Self {
294        Self(CompactLengthInner::from_tag(Self::MAX_CONTENT_TAG))
295    }
296
297    /// The size should be computed according to the "fit content" formula:
298    ///    `max(min_content, min(max_content, limit))`
299    /// where:
300    ///    - `min_content` is the [min-content](Self::min_content) size
301    ///    - `max_content` is the [max-content](Self::max_content) size
302    ///    - `limit` is a LENGTH value passed to this function
303    ///
304    /// The effect of this is that the item takes the size of `limit` clamped
305    /// by the min-content and max-content sizes.
306    #[inline(always)]
307    pub const fn fit_content_px(limit: f32) -> Self {
308        Self(CompactLengthInner::from_val(limit, Self::FIT_CONTENT_PX_TAG))
309    }
310
311    /// The size should be computed according to the "fit content" formula:
312    ///    `max(min_content, min(max_content, limit))`
313    /// where:
314    ///    - `min_content` is the [min-content](Self::min_content) size
315    ///    - `max_content` is the [max-content](Self::max_content) size
316    ///    - `limit` is a PERCENTAGE value passed to this function
317    ///
318    /// The effect of this is that the item takes the size of `limit` clamped
319    /// by the min-content and max-content sizes.
320    #[inline(always)]
321    pub const fn fit_content_percent(limit: f32) -> Self {
322        Self(CompactLengthInner::from_val(limit, Self::FIT_CONTENT_PERCENT_TAG))
323    }
324
325    /// The size should be computed according to the "fit content" formula:
326    ///    `max(min_content, min(max_content, stretch))`
327    /// where:
328    ///    - `min_content` is the [min-content](Self::min_content) size
329    ///    - `max_content` is the [max-content](Self::max_content) size
330    ///    - `stretch` is the "stretch-fit" size (the size the box would take if it filled the available space)
331    #[inline(always)]
332    pub const fn fit_content_keyword() -> Self {
333        Self(CompactLengthInner::from_tag(Self::FIT_CONTENT_KEYWORD_TAG))
334    }
335
336    /// The size should be the "stretch-fit" size: the size the box would take
337    /// if it filled the available space
338    /// (<https://www.w3.org/TR/css-sizing-4/#stretch-fit-sizing>)
339    #[inline(always)]
340    pub const fn stretch() -> Self {
341        Self(CompactLengthInner::from_tag(Self::STRETCH_TAG))
342    }
343
344    /// The size should be an automatic size based on the box's content
345    /// (<https://www.w3.org/TR/css-flexbox-1/#valdef-flex-basis-content>)
346    ///
347    /// This keyword is only valid for `flex-basis`. In any other context it behaves as [`auto`](Self::auto).
348    #[inline(always)]
349    pub const fn content() -> Self {
350        Self(CompactLengthInner::from_tag(Self::CONTENT_TAG))
351    }
352
353    /// Get the primary tag
354    #[inline(always)]
355    pub fn tag(self) -> usize {
356        self.0.tag()
357    }
358
359    /// Get the numeric value associated with the `CompactLength`
360    /// (e.g. the pixel value for a LENGTH variant)
361    #[inline(always)]
362    pub fn value(self) -> f32 {
363        self.0.value()
364    }
365
366    /// Get the calc pointer of the `CompactLength`
367    #[inline(always)]
368    #[cfg(feature = "calc")]
369    pub fn calc_value(self) -> *const () {
370        self.0.ptr()
371    }
372
373    /// Returns true if the value is 0 px
374    #[inline(always)]
375    #[cfg(feature = "calc")]
376    pub fn is_calc(self) -> bool {
377        self.0.calc_tag() == 0
378    }
379
380    /// Returns true if the value is 0 px
381    #[inline(always)]
382    pub fn is_zero(self) -> bool {
383        self.0 == Self::ZERO.0
384    }
385
386    /// Returns true if the value is a length or percentage value
387    #[inline(always)]
388    pub fn is_length_or_percentage(self) -> bool {
389        matches!(self.tag(), Self::LENGTH_TAG | Self::PERCENT_TAG)
390    }
391
392    /// Returns true if the value is auto
393    #[inline(always)]
394    pub fn is_auto(self) -> bool {
395        self.tag() == Self::AUTO_TAG
396    }
397
398    /// Returns true if the value is the content keyword
399    #[inline(always)]
400    pub fn is_content(self) -> bool {
401        self.tag() == Self::CONTENT_TAG
402    }
403
404    /// Returns true if the value is min-content
405    #[inline(always)]
406    pub fn is_min_content(self) -> bool {
407        matches!(self.tag(), Self::MIN_CONTENT_TAG)
408    }
409
410    /// Returns true if the value is max-content
411    #[inline(always)]
412    pub fn is_max_content(self) -> bool {
413        matches!(self.tag(), Self::MAX_CONTENT_TAG)
414    }
415
416    /// Returns true if the value is a fit-content(...) value
417    #[inline(always)]
418    pub fn is_fit_content(self) -> bool {
419        matches!(self.tag(), Self::FIT_CONTENT_PX_TAG | Self::FIT_CONTENT_PERCENT_TAG)
420    }
421
422    /// Returns true if the value is min-content, max-content, fit-content, fit-content(...), or stretch
423    #[inline(always)]
424    pub fn is_sizing_keyword(self) -> bool {
425        matches!(
426            self.tag(),
427            Self::MIN_CONTENT_TAG
428                | Self::MAX_CONTENT_TAG
429                | Self::FIT_CONTENT_KEYWORD_TAG
430                | Self::FIT_CONTENT_PX_TAG
431                | Self::FIT_CONTENT_PERCENT_TAG
432                | Self::STRETCH_TAG
433        )
434    }
435
436    /// Returns true if the value is max-content or a fit-content(...) value
437    #[inline(always)]
438    pub fn is_max_or_fit_content(self) -> bool {
439        matches!(self.tag(), Self::MAX_CONTENT_TAG | Self::FIT_CONTENT_PX_TAG | Self::FIT_CONTENT_PERCENT_TAG)
440    }
441
442    /// Returns true if the max track sizing function is `MaxContent`, `FitContent` or `Auto` else false.
443    /// "In all cases, treat auto and fit-content() as max-content, except where specified otherwise for fit-content()."
444    /// See: <https://www.w3.org/TR/css-grid-1/#algo-terms>
445    #[inline(always)]
446    pub fn is_max_content_alike(&self) -> bool {
447        matches!(
448            self.tag(),
449            CompactLength::AUTO_TAG
450                | CompactLength::MAX_CONTENT_TAG
451                | CompactLength::FIT_CONTENT_PX_TAG
452                | CompactLength::FIT_CONTENT_PERCENT_TAG
453        )
454    }
455
456    /// Returns true if the min track sizing function is `MinContent` or `MaxContent`, else false.
457    #[inline(always)]
458    pub fn is_min_or_max_content(&self) -> bool {
459        matches!(self.tag(), Self::MIN_CONTENT_TAG | Self::MAX_CONTENT_TAG)
460    }
461
462    /// Returns true if the value is auto, min-content, max-content, or fit-content(...)
463    #[inline(always)]
464    pub fn is_intrinsic(self) -> bool {
465        matches!(
466            self.tag(),
467            Self::AUTO_TAG
468                | Self::MIN_CONTENT_TAG
469                | Self::MAX_CONTENT_TAG
470                | Self::FIT_CONTENT_PX_TAG
471                | Self::FIT_CONTENT_PERCENT_TAG
472        )
473    }
474
475    /// Returns true if the value is and fr value
476    #[inline(always)]
477    pub fn is_fr(self) -> bool {
478        self.tag() == Self::FR_TAG
479    }
480
481    /// Whether the track sizing functions depends on the size of the parent node
482    #[inline(always)]
483    pub fn uses_percentage(self) -> bool {
484        #[cfg(feature = "calc")]
485        {
486            matches!(self.tag(), CompactLength::PERCENT_TAG | CompactLength::FIT_CONTENT_PERCENT_TAG) || self.is_calc()
487        }
488        #[cfg(not(feature = "calc"))]
489        {
490            matches!(self.tag(), CompactLength::PERCENT_TAG | CompactLength::FIT_CONTENT_PERCENT_TAG)
491        }
492    }
493
494    /// Resolve percentage values against the passed parent_size, returning Some(value)
495    /// Non-percentage values always return None.
496    #[inline(always)]
497    pub fn resolved_percentage_size(
498        self,
499        parent_size: f32,
500        calc_resolver: impl Fn(*const (), f32) -> f32,
501    ) -> Option<f32> {
502        match self.tag() {
503            CompactLength::PERCENT_TAG => Some(self.value() * parent_size),
504            #[cfg(feature = "calc")]
505            _ if self.is_calc() => Some(calc_resolver(self.0.ptr(), parent_size)),
506            _ => None,
507        }
508    }
509}
510
511impl TaffyZero for CompactLength {
512    const ZERO: Self = Self::length(0.0);
513}
514impl TaffyAuto for CompactLength {
515    const AUTO: Self = Self::auto();
516}
517impl TaffyMinContent for CompactLength {
518    const MIN_CONTENT: Self = Self::min_content();
519}
520impl TaffyMaxContent for CompactLength {
521    const MAX_CONTENT: Self = Self::max_content();
522}
523impl FromLength for CompactLength {
524    fn from_length<Input: Into<f64> + Copy>(value: Input) -> Self {
525        Self::length(value.into() as f32)
526    }
527}
528impl FromPercent for CompactLength {
529    fn from_percent<Input: Into<f64> + Copy>(value: Input) -> Self {
530        Self::percent(value.into() as f32)
531    }
532}
533impl FromFr for CompactLength {
534    fn from_fr<Input: Into<f64> + Copy>(value: Input) -> Self {
535        Self::fr(value.into() as f32)
536    }
537}
538impl TaffyFitContent for CompactLength {
539    fn fit_content(lp: LengthPercentage) -> Self {
540        let value = lp.0.value();
541        match lp.0.tag() {
542            Self::LENGTH_TAG => Self::fit_content_px(value),
543            Self::PERCENT_TAG => Self::fit_content_percent(value),
544            _ => unreachable!(),
545        }
546    }
547}
548
549#[cfg(feature = "serde")]
550impl serde::Serialize for CompactLength {
551    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
552    where
553        S: serde::Serializer,
554    {
555        #[cfg(feature = "calc")]
556        {
557            if self.tag() == Self::CALC_TAG {
558                Err(serde::ser::Error::custom("Cannot serialize Calc value"))
559            } else {
560                serializer.serialize_u64(self.0.serialized())
561            }
562        }
563
564        #[cfg(not(feature = "calc"))]
565        serializer.serialize_u64(self.0.serialized())
566    }
567}
568
569#[cfg(feature = "serde")]
570impl<'de> serde::Deserialize<'de> for CompactLength {
571    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
572    where
573        D: serde::Deserializer<'de>,
574    {
575        let bits: u64 = u64::deserialize(deserializer)?;
576        let value = Self(CompactLengthInner::from_serialized(bits));
577        // Note: validation intentionally excludes the CALC_TAG as deserializing calc() values is not supported
578        if matches!(
579            value.tag(),
580            CompactLength::LENGTH_TAG
581                | CompactLength::PERCENT_TAG
582                | CompactLength::AUTO_TAG
583                | CompactLength::MIN_CONTENT_TAG
584                | CompactLength::MAX_CONTENT_TAG
585                | CompactLength::FIT_CONTENT_KEYWORD_TAG
586                | CompactLength::FIT_CONTENT_PX_TAG
587                | CompactLength::FIT_CONTENT_PERCENT_TAG
588                | CompactLength::STRETCH_TAG
589                | CompactLength::CONTENT_TAG
590                | CompactLength::FR_TAG
591        ) {
592            Ok(value)
593        } else {
594            Err(serde::de::Error::custom("Cannot deserialize Calc value"))
595        }
596    }
597}