Skip to main content

ferroui_controls/
grid_length.rs

1use ferroui_base::utilities::span_helpers::{parse_double, InvariantF64};
2use ferroui_base::utilities::{FormatError, SpanStringTokenizer};
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::str::FromStr;
6
7/// Defines the valid units for a [`GridLength`].
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
9#[repr(i32)]
10pub enum GridUnitType {
11    /// The row or column is auto-sized to fit its content.
12    #[default]
13    Auto = 0,
14
15    /// The row or column is sized in device independent pixels.
16    Pixel = 1,
17
18    /// The row or column is sized as a weighted proportion of available space.
19    Star = 2,
20}
21
22/// Holds the width or height of a `Grid`'s column and row definitions.
23///
24/// The default value is an auto-sized length with a value of zero.
25#[derive(Clone, Copy, Debug, Default)]
26pub struct GridLength {
27    type_: GridUnitType,
28    value: f64,
29}
30
31impl GridLength {
32    /// A length that indicates that a row or column should auto-size to fit
33    /// its content.
34    pub const AUTO: GridLength = GridLength {
35        type_: GridUnitType::Auto,
36        value: 0.0,
37    };
38
39    /// A length that indicates that a row or column should fill its content.
40    pub const STAR: GridLength = GridLength {
41        type_: GridUnitType::Star,
42        value: 1.0,
43    };
44
45    /// Creates a length in device independent pixels.
46    ///
47    /// Panics if the value is negative, NaN or infinite.
48    pub fn from_pixels(value: f64) -> Self {
49        Self::new(value, GridUnitType::Pixel)
50    }
51
52    /// Creates a length with the given unit.
53    ///
54    /// Panics if the value is negative, NaN or infinite.
55    pub fn new(value: f64, type_: GridUnitType) -> Self {
56        if !Self::is_valid_value(value) {
57            panic!("Invalid value");
58        }
59
60        Self { type_, value }
61    }
62
63    fn is_valid_value(value: f64) -> bool {
64        !(value < 0.0 || value.is_nan() || value.is_infinite())
65    }
66
67    /// A length that indicates that a row or column should auto-size to fit
68    /// its content.
69    #[inline]
70    pub const fn auto() -> Self {
71        Self::AUTO
72    }
73
74    /// A length that indicates that a row or column should fill its content.
75    #[inline]
76    pub const fn star() -> Self {
77        Self::STAR
78    }
79
80    /// The unit of the length.
81    #[inline]
82    pub const fn grid_unit_type(&self) -> GridUnitType {
83        self.type_
84    }
85
86    /// Whether the length has a [`GridUnitType`] of `Pixel`.
87    #[inline]
88    pub fn is_absolute(&self) -> bool {
89        self.type_ == GridUnitType::Pixel
90    }
91
92    /// Whether the length has a [`GridUnitType`] of `Auto`.
93    #[inline]
94    pub fn is_auto(&self) -> bool {
95        self.type_ == GridUnitType::Auto
96    }
97
98    /// Whether the length has a [`GridUnitType`] of `Star`.
99    #[inline]
100    pub fn is_star(&self) -> bool {
101        self.type_ == GridUnitType::Star
102    }
103
104    /// The length.
105    #[inline]
106    pub const fn value(&self) -> f64 {
107        self.value
108    }
109
110    /// Parses a string to return a [`GridLength`].
111    pub fn parse(s: &str) -> Result<GridLength, FormatError> {
112        let s = s.to_uppercase();
113
114        if s == "AUTO" {
115            Ok(Self::AUTO)
116        } else if let Some(value_string) = s.strip_suffix('*') {
117            let value_string = value_string.trim();
118            let value = if !value_string.is_empty() {
119                Self::parse_value(value_string)?
120            } else {
121                1.0
122            };
123            Ok(GridLength {
124                type_: GridUnitType::Star,
125                value,
126            })
127        } else {
128            let value = Self::parse_value(&s)?;
129            Ok(GridLength {
130                type_: GridUnitType::Pixel,
131                value,
132            })
133        }
134    }
135
136    fn parse_value(s: &str) -> Result<f64, FormatError> {
137        match parse_double(s) {
138            Some(value) if Self::is_valid_value(value) => Ok(value),
139            _ => Err(FormatError::invalid_input(s)),
140        }
141    }
142
143    /// Parses a string to return a collection of [`GridLength`]s.
144    pub fn parse_lengths(s: &str) -> Result<Vec<GridLength>, FormatError> {
145        let mut result = Vec::new();
146
147        SpanStringTokenizer::new(s).scope(|tokenizer| {
148            while let Some(item) = tokenizer.try_read_string()? {
149                result.push(Self::parse(item)?);
150            }
151            Ok(())
152        })?;
153
154        Ok(result)
155    }
156}
157
158impl PartialEq for GridLength {
159    fn eq(&self, other: &Self) -> bool {
160        (self.is_auto() && other.is_auto()) || (self.value == other.value && self.type_ == other.type_)
161    }
162}
163
164impl Hash for GridLength {
165    fn hash<H: Hasher>(&self, state: &mut H) {
166        // Auto lengths compare equal whatever their value is.
167        if !self.is_auto() {
168            // Positive and negative zero compare equal.
169            (self.value + 0.0).to_bits().hash(state);
170        }
171        self.type_.hash(state);
172    }
173}
174
175impl fmt::Display for GridLength {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        if self.is_auto() {
178            return f.write_str("Auto");
179        }
180
181        InvariantF64(self.value).fmt(f)?;
182        if self.is_star() {
183            f.write_str("*")?;
184        }
185        Ok(())
186    }
187}
188
189impl FromStr for GridLength {
190    type Err = FormatError;
191
192    fn from_str(s: &str) -> Result<Self, Self::Err> {
193        Self::parse(s)
194    }
195}