Skip to main content

astrelis_ui/
length.rs

1//! Type-safe length and dimension types for UI styling.
2//!
3//! Provides enum-based alternatives to raw floats for better type safety
4//! and clearer intent in style declarations.
5
6use astrelis_core::math::Vec2;
7use std::fmt;
8
9/// Length value for UI dimensions.
10///
11/// Represents different ways to specify sizes in the UI system.
12///
13/// # Examples
14/// ```
15/// use astrelis_ui::Length;
16///
17/// let fixed = Length::Px(100.0);
18/// let relative = Length::Percent(50.0);
19/// let auto_size = Length::Auto;
20/// let viewport_width = Length::Vw(80.0); // 80% of viewport width
21/// ```
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum Length {
24    /// Fixed pixel value
25    Px(f32),
26    /// Percentage of parent size (0.0 - 100.0)
27    Percent(f32),
28    /// Automatic sizing based on content
29    Auto,
30    /// Percentage of viewport width (0.0 - 100.0)
31    Vw(f32),
32    /// Percentage of viewport height (0.0 - 100.0)
33    Vh(f32),
34    /// Percentage of smaller viewport dimension (0.0 - 100.0)
35    Vmin(f32),
36    /// Percentage of larger viewport dimension (0.0 - 100.0)
37    Vmax(f32),
38}
39
40impl Length {
41    /// Create a pixel length.
42    pub fn px(value: f32) -> Self {
43        Self::Px(value)
44    }
45
46    /// Create a percentage length.
47    pub fn percent(value: f32) -> Self {
48        Self::Percent(value)
49    }
50
51    /// Create an auto length.
52    pub fn auto() -> Self {
53        Self::Auto
54    }
55
56    /// Create a viewport width length (1vw = 1% of viewport width).
57    pub fn vw(value: f32) -> Self {
58        Self::Vw(value)
59    }
60
61    /// Create a viewport height length (1vh = 1% of viewport height).
62    pub fn vh(value: f32) -> Self {
63        Self::Vh(value)
64    }
65
66    /// Create a viewport minimum length (1vmin = 1% of smaller viewport dimension).
67    pub fn vmin(value: f32) -> Self {
68        Self::Vmin(value)
69    }
70
71    /// Create a viewport maximum length (1vmax = 1% of larger viewport dimension).
72    pub fn vmax(value: f32) -> Self {
73        Self::Vmax(value)
74    }
75
76    /// Resolve viewport-relative units to pixels.
77    ///
78    /// Converts vw/vh/vmin/vmax units to absolute pixel values based on the viewport size.
79    /// Other unit types (Px, Percent, Auto) are returned unchanged.
80    ///
81    /// # Arguments
82    /// * `viewport_size` - The viewport dimensions (width, height) in pixels
83    ///
84    /// # Examples
85    /// ```
86    /// use astrelis_ui::Length;
87    /// use astrelis_core::math::Vec2;
88    ///
89    /// let viewport = Vec2::new(1280.0, 720.0);
90    ///
91    /// let width = Length::Vw(50.0).resolve(viewport);
92    /// assert_eq!(width, Length::Px(640.0)); // 50% of 1280px
93    ///
94    /// let height = Length::Vh(100.0).resolve(viewport);
95    /// assert_eq!(height, Length::Px(720.0)); // 100% of 720px
96    /// ```
97    pub fn resolve(self, viewport_size: Vec2) -> Self {
98        match self {
99            Length::Vw(v) => Length::Px(v * viewport_size.x / 100.0),
100            Length::Vh(v) => Length::Px(v * viewport_size.y / 100.0),
101            Length::Vmin(v) => {
102                let min = viewport_size.x.min(viewport_size.y);
103                Length::Px(v * min / 100.0)
104            }
105            Length::Vmax(v) => {
106                let max = viewport_size.x.max(viewport_size.y);
107                Length::Px(v * max / 100.0)
108            }
109            other => other, // Px/Percent/Auto unchanged
110        }
111    }
112
113    /// Convert to Taffy Dimension.
114    ///
115    /// # Panics
116    /// Panics if called on viewport-relative units (Vw/Vh/Vmin/Vmax).
117    /// These must be resolved to pixels first using [`Length::resolve`].
118    pub fn to_dimension(self) -> taffy::Dimension {
119        match self {
120            Length::Px(v) => taffy::Dimension::Length(v),
121            Length::Percent(v) => taffy::Dimension::Percent(v / 100.0),
122            Length::Auto => taffy::Dimension::Auto,
123            Length::Vw(_) | Length::Vh(_) | Length::Vmin(_) | Length::Vmax(_) => {
124                panic!(
125                    "Viewport-relative units must be resolved to pixels before converting to Taffy dimension. \
126                     Call .resolve(viewport_size) first."
127                );
128            }
129        }
130    }
131
132    /// Convert from Taffy Dimension.
133    pub fn from_dimension(dim: taffy::Dimension) -> Self {
134        match dim {
135            taffy::Dimension::Length(v) => Length::Px(v),
136            taffy::Dimension::Percent(v) => Length::Percent(v * 100.0),
137            taffy::Dimension::Auto => Length::Auto,
138        }
139    }
140
141    /// Check if this is a fixed pixel value.
142    pub fn is_px(&self) -> bool {
143        matches!(self, Length::Px(_))
144    }
145
146    /// Check if this is a percentage value.
147    pub fn is_percent(&self) -> bool {
148        matches!(self, Length::Percent(_))
149    }
150
151    /// Check if this is auto.
152    pub fn is_auto(&self) -> bool {
153        matches!(self, Length::Auto)
154    }
155
156    /// Check if this is a viewport-relative unit.
157    pub fn is_viewport(&self) -> bool {
158        matches!(
159            self,
160            Length::Vw(_) | Length::Vh(_) | Length::Vmin(_) | Length::Vmax(_)
161        )
162    }
163}
164
165impl From<f32> for Length {
166    fn from(value: f32) -> Self {
167        Length::Px(value)
168    }
169}
170
171impl From<Length> for taffy::Dimension {
172    fn from(length: Length) -> Self {
173        length.to_dimension()
174    }
175}
176
177impl fmt::Display for Length {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Length::Px(v) => write!(f, "{}px", v),
181            Length::Percent(v) => write!(f, "{}%", v),
182            Length::Auto => write!(f, "auto"),
183            Length::Vw(v) => write!(f, "{}vw", v),
184            Length::Vh(v) => write!(f, "{}vh", v),
185            Length::Vmin(v) => write!(f, "{}vmin", v),
186            Length::Vmax(v) => write!(f, "{}vmax", v),
187        }
188    }
189}
190
191/// Length value that can also be Auto (for margins/insets).
192///
193/// Similar to Length but allows automatic positioning.
194#[derive(Debug, Clone, Copy, PartialEq)]
195pub enum LengthAuto {
196    /// Fixed pixel value
197    Px(f32),
198    /// Percentage of parent size (0.0 - 100.0)
199    Percent(f32),
200    /// Automatic positioning
201    Auto,
202    /// Percentage of viewport width (0.0 - 100.0)
203    Vw(f32),
204    /// Percentage of viewport height (0.0 - 100.0)
205    Vh(f32),
206    /// Percentage of smaller viewport dimension (0.0 - 100.0)
207    Vmin(f32),
208    /// Percentage of larger viewport dimension (0.0 - 100.0)
209    Vmax(f32),
210}
211
212impl LengthAuto {
213    /// Create a pixel length.
214    pub fn px(value: f32) -> Self {
215        Self::Px(value)
216    }
217
218    /// Create a percentage length.
219    pub fn percent(value: f32) -> Self {
220        Self::Percent(value)
221    }
222
223    /// Create an auto length.
224    pub fn auto() -> Self {
225        Self::Auto
226    }
227
228    /// Create a viewport width length (1vw = 1% of viewport width).
229    pub fn vw(value: f32) -> Self {
230        Self::Vw(value)
231    }
232
233    /// Create a viewport height length (1vh = 1% of viewport height).
234    pub fn vh(value: f32) -> Self {
235        Self::Vh(value)
236    }
237
238    /// Create a viewport minimum length (1vmin = 1% of smaller viewport dimension).
239    pub fn vmin(value: f32) -> Self {
240        Self::Vmin(value)
241    }
242
243    /// Create a viewport maximum length (1vmax = 1% of larger viewport dimension).
244    pub fn vmax(value: f32) -> Self {
245        Self::Vmax(value)
246    }
247
248    /// Resolve viewport-relative units to pixels.
249    pub fn resolve(self, viewport_size: Vec2) -> Self {
250        match self {
251            LengthAuto::Vw(v) => LengthAuto::Px(v * viewport_size.x / 100.0),
252            LengthAuto::Vh(v) => LengthAuto::Px(v * viewport_size.y / 100.0),
253            LengthAuto::Vmin(v) => {
254                let min = viewport_size.x.min(viewport_size.y);
255                LengthAuto::Px(v * min / 100.0)
256            }
257            LengthAuto::Vmax(v) => {
258                let max = viewport_size.x.max(viewport_size.y);
259                LengthAuto::Px(v * max / 100.0)
260            }
261            other => other,
262        }
263    }
264
265    /// Convert to Taffy LengthPercentageAuto.
266    ///
267    /// # Panics
268    /// Panics if called on viewport-relative units (Vw/Vh/Vmin/Vmax).
269    /// These must be resolved to pixels first using [`LengthAuto::resolve`].
270    pub fn to_length_percentage_auto(self) -> taffy::LengthPercentageAuto {
271        match self {
272            LengthAuto::Px(v) => taffy::LengthPercentageAuto::Length(v),
273            LengthAuto::Percent(v) => taffy::LengthPercentageAuto::Percent(v / 100.0),
274            LengthAuto::Auto => taffy::LengthPercentageAuto::Auto,
275            LengthAuto::Vw(_) | LengthAuto::Vh(_) | LengthAuto::Vmin(_) | LengthAuto::Vmax(_) => {
276                panic!(
277                    "Viewport-relative units must be resolved to pixels before converting to Taffy dimension. \
278                     Call .resolve(viewport_size) first."
279                );
280            }
281        }
282    }
283
284    /// Convert from Taffy LengthPercentageAuto.
285    pub fn from_length_percentage_auto(lpa: taffy::LengthPercentageAuto) -> Self {
286        match lpa {
287            taffy::LengthPercentageAuto::Length(v) => LengthAuto::Px(v),
288            taffy::LengthPercentageAuto::Percent(v) => LengthAuto::Percent(v * 100.0),
289            taffy::LengthPercentageAuto::Auto => LengthAuto::Auto,
290        }
291    }
292}
293
294impl From<f32> for LengthAuto {
295    fn from(value: f32) -> Self {
296        LengthAuto::Px(value)
297    }
298}
299
300impl From<Length> for LengthAuto {
301    fn from(length: Length) -> Self {
302        match length {
303            Length::Px(v) => LengthAuto::Px(v),
304            Length::Percent(v) => LengthAuto::Percent(v),
305            Length::Auto => LengthAuto::Auto,
306            Length::Vw(v) => LengthAuto::Vw(v),
307            Length::Vh(v) => LengthAuto::Vh(v),
308            Length::Vmin(v) => LengthAuto::Vmin(v),
309            Length::Vmax(v) => LengthAuto::Vmax(v),
310        }
311    }
312}
313
314impl From<LengthAuto> for taffy::LengthPercentageAuto {
315    fn from(length: LengthAuto) -> Self {
316        length.to_length_percentage_auto()
317    }
318}
319
320impl fmt::Display for LengthAuto {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        match self {
323            LengthAuto::Px(v) => write!(f, "{}px", v),
324            LengthAuto::Percent(v) => write!(f, "{}%", v),
325            LengthAuto::Auto => write!(f, "auto"),
326            LengthAuto::Vw(v) => write!(f, "{}vw", v),
327            LengthAuto::Vh(v) => write!(f, "{}vh", v),
328            LengthAuto::Vmin(v) => write!(f, "{}vmin", v),
329            LengthAuto::Vmax(v) => write!(f, "{}vmax", v),
330        }
331    }
332}
333
334/// Length value without Auto (for padding/border).
335///
336/// Similar to Length but doesn't allow Auto sizing.
337#[derive(Debug, Clone, Copy, PartialEq)]
338pub enum LengthPercentage {
339    /// Fixed pixel value
340    Px(f32),
341    /// Percentage of parent size (0.0 - 100.0)
342    Percent(f32),
343    /// Percentage of viewport width (0.0 - 100.0)
344    Vw(f32),
345    /// Percentage of viewport height (0.0 - 100.0)
346    Vh(f32),
347    /// Percentage of smaller viewport dimension (0.0 - 100.0)
348    Vmin(f32),
349    /// Percentage of larger viewport dimension (0.0 - 100.0)
350    Vmax(f32),
351}
352
353impl LengthPercentage {
354    /// Create a pixel length.
355    pub fn px(value: f32) -> Self {
356        Self::Px(value)
357    }
358
359    /// Create a percentage length.
360    pub fn percent(value: f32) -> Self {
361        Self::Percent(value)
362    }
363
364    /// Create a viewport width length (1vw = 1% of viewport width).
365    pub fn vw(value: f32) -> Self {
366        Self::Vw(value)
367    }
368
369    /// Create a viewport height length (1vh = 1% of viewport height).
370    pub fn vh(value: f32) -> Self {
371        Self::Vh(value)
372    }
373
374    /// Create a viewport minimum length (1vmin = 1% of smaller viewport dimension).
375    pub fn vmin(value: f32) -> Self {
376        Self::Vmin(value)
377    }
378
379    /// Create a viewport maximum length (1vmax = 1% of larger viewport dimension).
380    pub fn vmax(value: f32) -> Self {
381        Self::Vmax(value)
382    }
383
384    /// Resolve viewport-relative units to pixels.
385    pub fn resolve(self, viewport_size: Vec2) -> Self {
386        match self {
387            LengthPercentage::Vw(v) => LengthPercentage::Px(v * viewport_size.x / 100.0),
388            LengthPercentage::Vh(v) => LengthPercentage::Px(v * viewport_size.y / 100.0),
389            LengthPercentage::Vmin(v) => {
390                let min = viewport_size.x.min(viewport_size.y);
391                LengthPercentage::Px(v * min / 100.0)
392            }
393            LengthPercentage::Vmax(v) => {
394                let max = viewport_size.x.max(viewport_size.y);
395                LengthPercentage::Px(v * max / 100.0)
396            }
397            other => other,
398        }
399    }
400
401    /// Convert to Taffy LengthPercentage.
402    ///
403    /// # Panics
404    /// Panics if called on viewport-relative units (Vw/Vh/Vmin/Vmax).
405    /// These must be resolved to pixels first using [`LengthPercentage::resolve`].
406    pub fn to_length_percentage(self) -> taffy::LengthPercentage {
407        match self {
408            LengthPercentage::Px(v) => taffy::LengthPercentage::Length(v),
409            LengthPercentage::Percent(v) => taffy::LengthPercentage::Percent(v / 100.0),
410            LengthPercentage::Vw(_)
411            | LengthPercentage::Vh(_)
412            | LengthPercentage::Vmin(_)
413            | LengthPercentage::Vmax(_) => {
414                panic!(
415                    "Viewport-relative units must be resolved to pixels before converting to Taffy dimension. \
416                     Call .resolve(viewport_size) first."
417                );
418            }
419        }
420    }
421
422    /// Convert from Taffy LengthPercentage.
423    pub fn from_length_percentage(lp: taffy::LengthPercentage) -> Self {
424        match lp {
425            taffy::LengthPercentage::Length(v) => LengthPercentage::Px(v),
426            taffy::LengthPercentage::Percent(v) => LengthPercentage::Percent(v * 100.0),
427        }
428    }
429}
430
431impl From<f32> for LengthPercentage {
432    fn from(value: f32) -> Self {
433        LengthPercentage::Px(value)
434    }
435}
436
437impl From<LengthPercentage> for taffy::LengthPercentage {
438    fn from(length: LengthPercentage) -> Self {
439        length.to_length_percentage()
440    }
441}
442
443impl fmt::Display for LengthPercentage {
444    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445        match self {
446            LengthPercentage::Px(v) => write!(f, "{}px", v),
447            LengthPercentage::Percent(v) => write!(f, "{}%", v),
448            LengthPercentage::Vw(v) => write!(f, "{}vw", v),
449            LengthPercentage::Vh(v) => write!(f, "{}vh", v),
450            LengthPercentage::Vmin(v) => write!(f, "{}vmin", v),
451            LengthPercentage::Vmax(v) => write!(f, "{}vmax", v),
452        }
453    }
454}
455
456/// Helper function to create a Taffy length dimension.
457pub fn length(value: f32) -> taffy::Dimension {
458    taffy::Dimension::Length(value)
459}
460
461/// Helper function to create a Taffy percent dimension.
462pub fn percent(value: f32) -> taffy::Dimension {
463    taffy::Dimension::Percent(value / 100.0)
464}
465
466/// Helper function to create auto dimension.
467pub fn auto() -> taffy::Dimension {
468    taffy::Dimension::Auto
469}
470
471/// Helper function to create a viewport width length.
472///
473/// # Examples
474/// ```
475/// use astrelis_ui::vw;
476///
477/// let width = vw(80.0); // 80% of viewport width
478/// ```
479pub fn vw(value: f32) -> Length {
480    Length::Vw(value)
481}
482
483/// Helper function to create a viewport height length.
484///
485/// # Examples
486/// ```
487/// use astrelis_ui::vh;
488///
489/// let height = vh(100.0); // 100% of viewport height
490/// ```
491pub fn vh(value: f32) -> Length {
492    Length::Vh(value)
493}
494
495/// Helper function to create a viewport minimum length.
496///
497/// # Examples
498/// ```
499/// use astrelis_ui::vmin;
500///
501/// let size = vmin(50.0); // 50% of smaller viewport dimension
502/// ```
503pub fn vmin(value: f32) -> Length {
504    Length::Vmin(value)
505}
506
507/// Helper function to create a viewport maximum length.
508///
509/// # Examples
510/// ```
511/// use astrelis_ui::vmax;
512///
513/// let size = vmax(50.0); // 50% of larger viewport dimension
514/// ```
515pub fn vmax(value: f32) -> Length {
516    Length::Vmax(value)
517}
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522
523    #[test]
524    fn test_length_px() {
525        let len = Length::Px(100.0);
526        assert!(len.is_px());
527        assert!(!len.is_percent());
528        assert!(!len.is_auto());
529        assert_eq!(len.to_string(), "100px");
530    }
531
532    #[test]
533    fn test_length_percent() {
534        let len = Length::Percent(50.0);
535        assert!(!len.is_px());
536        assert!(len.is_percent());
537        assert!(!len.is_auto());
538        assert_eq!(len.to_string(), "50%");
539    }
540
541    #[test]
542    fn test_length_auto() {
543        let len = Length::Auto;
544        assert!(!len.is_px());
545        assert!(!len.is_percent());
546        assert!(len.is_auto());
547        assert_eq!(len.to_string(), "auto");
548    }
549
550    #[test]
551    fn test_length_from_f32() {
552        let len: Length = 100.0.into();
553        assert_eq!(len, Length::Px(100.0));
554    }
555
556    #[test]
557    fn test_length_to_dimension() {
558        let len = Length::Px(100.0);
559        let dim = len.to_dimension();
560        assert!(matches!(dim, taffy::Dimension::Length(100.0)));
561
562        let len = Length::Percent(50.0);
563        let dim = len.to_dimension();
564        assert!(matches!(dim, taffy::Dimension::Percent(0.5)));
565
566        let len = Length::Auto;
567        let dim = len.to_dimension();
568        assert!(matches!(dim, taffy::Dimension::Auto));
569    }
570
571    #[test]
572    fn test_length_percentage() {
573        let lp = LengthPercentage::Px(100.0);
574        assert_eq!(lp.to_string(), "100px");
575
576        let lp = LengthPercentage::Percent(50.0);
577        assert_eq!(lp.to_string(), "50%");
578    }
579
580    #[test]
581    fn test_length_auto_conversion() {
582        let la = LengthAuto::Px(100.0);
583        assert_eq!(la.to_string(), "100px");
584
585        let len: LengthAuto = Length::Percent(50.0).into();
586        assert_eq!(len, LengthAuto::Percent(50.0));
587    }
588
589    #[test]
590    fn test_helper_functions() {
591        let dim = length(100.0);
592        assert!(matches!(dim, taffy::Dimension::Length(100.0)));
593
594        let dim = percent(50.0);
595        assert!(matches!(dim, taffy::Dimension::Percent(0.5)));
596
597        let dim = auto();
598        assert!(matches!(dim, taffy::Dimension::Auto));
599    }
600}