Skip to main content

kozan_primitives/
units.rs

1/// A length value that may be absolute, relative, or intrinsic.
2///
3/// This is the building block of the style system — widths, heights,
4/// margins, paddings, font sizes, and many other properties are all
5/// expressed as `Dimension` values that get resolved to concrete pixels
6/// during style resolution and layout.
7///
8/// Absolute values ([`Px`](Dimension::Px)) are ready to use immediately.
9/// Relative values ([`Percent`](Dimension::Percent), [`Em`](Dimension::Em),
10/// [`Rem`](Dimension::Rem)) need a reference value from the parent or root.
11/// Viewport values ([`Vw`](Dimension::Vw), [`Vh`](Dimension::Vh)) need
12/// the viewport size. Intrinsic values ([`Auto`](Dimension::Auto),
13/// [`MinContent`](Dimension::MinContent), etc.) are resolved by the
14/// layout algorithm itself.
15#[derive(Clone, Copy, Debug, PartialEq, Default)]
16pub enum Dimension {
17    /// Absolute pixels (after DPI scaling).
18    Px(f32),
19    /// Percentage of the parent's corresponding dimension.
20    Percent(f32),
21    /// Relative to the element's computed `font-size`.
22    Em(f32),
23    /// Relative to the root element's computed `font-size`.
24    Rem(f32),
25    /// Percentage of the viewport width.
26    Vw(f32),
27    /// Percentage of the viewport height.
28    Vh(f32),
29    /// The smaller of `vw` and `vh`.
30    Vmin(f32),
31    /// The larger of `vw` and `vh`.
32    Vmax(f32),
33    /// Size determined by the layout algorithm.
34    #[default]
35    Auto,
36    /// The smallest size that fits the content without overflow.
37    MinContent,
38    /// The largest size the content can fill without wrapping.
39    MaxContent,
40    /// Clamp between min-content and max-content, or the available
41    /// space if it's between those bounds.
42    FitContent,
43}
44
45impl Dimension {
46    #[must_use]
47    pub fn is_auto(self) -> bool {
48        matches!(self, Dimension::Auto)
49    }
50
51    /// True for any value that resolves to a concrete number (not
52    /// auto/min-content/max-content/fit-content).
53    #[must_use]
54    pub fn is_definite(self) -> bool {
55        matches!(
56            self,
57            Dimension::Px(_)
58                | Dimension::Percent(_)
59                | Dimension::Em(_)
60                | Dimension::Rem(_)
61                | Dimension::Vw(_)
62                | Dimension::Vh(_)
63                | Dimension::Vmin(_)
64                | Dimension::Vmax(_)
65        )
66    }
67
68    /// True for values that the layout algorithm determines.
69    #[must_use]
70    pub fn is_intrinsic(self) -> bool {
71        matches!(
72            self,
73            Dimension::Auto | Dimension::MinContent | Dimension::MaxContent | Dimension::FitContent
74        )
75    }
76
77    /// Resolve an absolute or parent-relative value to pixels.
78    ///
79    /// Only resolves [`Px`](Dimension::Px) and [`Percent`](Dimension::Percent).
80    /// For font-relative and viewport-relative values, use
81    /// [`resolve_full`](Self::resolve_full).
82    #[must_use]
83    pub fn resolve(self, parent: f32) -> Option<f32> {
84        match self {
85            Dimension::Px(v) => Some(v),
86            Dimension::Percent(pct) => Some(parent * pct / 100.0),
87            _ => None,
88        }
89    }
90
91    /// Resolve with all context values available.
92    #[must_use]
93    pub fn resolve_full(self, ctx: &ResolveContext) -> Option<f32> {
94        match self {
95            Dimension::Px(v) => Some(v),
96            Dimension::Percent(pct) => Some(ctx.parent * pct / 100.0),
97            Dimension::Em(v) => Some(v * ctx.font_size),
98            Dimension::Rem(v) => Some(v * ctx.root_font_size),
99            Dimension::Vw(v) => Some(v * ctx.viewport_width / 100.0),
100            Dimension::Vh(v) => Some(v * ctx.viewport_height / 100.0),
101            Dimension::Vmin(v) => Some(v * ctx.viewport_width.min(ctx.viewport_height) / 100.0),
102            Dimension::Vmax(v) => Some(v * ctx.viewport_width.max(ctx.viewport_height) / 100.0),
103            Dimension::Auto
104            | Dimension::MinContent
105            | Dimension::MaxContent
106            | Dimension::FitContent => None,
107        }
108    }
109
110    /// Resolve, falling back to a default for unresolvable values.
111    #[must_use]
112    pub fn resolve_or(self, parent: f32, fallback: f32) -> f32 {
113        self.resolve(parent).unwrap_or(fallback)
114    }
115}
116
117/// All the context needed to resolve any [`Dimension`] variant to pixels.
118#[derive(Clone, Copy, Debug)]
119pub struct ResolveContext {
120    /// The parent element's resolved value for the same property.
121    pub parent: f32,
122    /// The element's computed font-size (for `em` units).
123    pub font_size: f32,
124    /// The root element's computed font-size (for `rem` units).
125    pub root_font_size: f32,
126    /// Viewport width in pixels.
127    pub viewport_width: f32,
128    /// Viewport height in pixels.
129    pub viewport_height: f32,
130}
131
132/// Shorthand: absolute pixels.
133#[must_use]
134pub fn px(value: f32) -> Dimension {
135    Dimension::Px(value)
136}
137
138/// Shorthand: percentage of parent.
139#[must_use]
140pub fn pct(value: f32) -> Dimension {
141    Dimension::Percent(value)
142}
143
144/// Shorthand: relative to element's font-size.
145#[must_use]
146pub fn em(value: f32) -> Dimension {
147    Dimension::Em(value)
148}
149
150/// Shorthand: relative to root font-size.
151#[must_use]
152pub fn rem(value: f32) -> Dimension {
153    Dimension::Rem(value)
154}
155
156/// Shorthand: percentage of viewport width.
157#[must_use]
158pub fn vw(value: f32) -> Dimension {
159    Dimension::Vw(value)
160}
161
162/// Shorthand: percentage of viewport height.
163#[must_use]
164pub fn vh(value: f32) -> Dimension {
165    Dimension::Vh(value)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn px_resolves_to_itself() {
174        assert_eq!(px(100.0).resolve(500.0), Some(100.0));
175    }
176
177    #[test]
178    fn percent_resolves_against_parent() {
179        assert_eq!(pct(50.0).resolve(200.0), Some(100.0));
180    }
181
182    #[test]
183    fn auto_resolves_to_none() {
184        assert_eq!(Dimension::Auto.resolve(500.0), None);
185    }
186
187    #[test]
188    fn resolve_or_with_fallback() {
189        assert_eq!(Dimension::Auto.resolve_or(500.0, 0.0), 0.0);
190        assert_eq!(px(42.0).resolve_or(500.0, 0.0), 42.0);
191    }
192
193    #[test]
194    fn default_is_auto() {
195        assert_eq!(Dimension::default(), Dimension::Auto);
196    }
197
198    #[test]
199    fn is_definite() {
200        assert!(px(10.0).is_definite());
201        assert!(pct(50.0).is_definite());
202        assert!(em(1.5).is_definite());
203        assert!(vw(100.0).is_definite());
204        assert!(!Dimension::Auto.is_definite());
205        assert!(!Dimension::MinContent.is_definite());
206    }
207
208    #[test]
209    fn is_intrinsic() {
210        assert!(Dimension::Auto.is_intrinsic());
211        assert!(Dimension::MinContent.is_intrinsic());
212        assert!(Dimension::MaxContent.is_intrinsic());
213        assert!(Dimension::FitContent.is_intrinsic());
214        assert!(!px(10.0).is_intrinsic());
215    }
216
217    #[test]
218    fn resolve_full_em_and_rem() {
219        let ctx = ResolveContext {
220            parent: 500.0,
221            font_size: 16.0,
222            root_font_size: 18.0,
223            viewport_width: 1920.0,
224            viewport_height: 1080.0,
225        };
226
227        assert_eq!(em(2.0).resolve_full(&ctx), Some(32.0));
228        assert_eq!(rem(2.0).resolve_full(&ctx), Some(36.0));
229    }
230
231    #[test]
232    fn resolve_full_viewport_units() {
233        let ctx = ResolveContext {
234            parent: 0.0,
235            font_size: 16.0,
236            root_font_size: 16.0,
237            viewport_width: 1920.0,
238            viewport_height: 1080.0,
239        };
240
241        assert_eq!(vw(50.0).resolve_full(&ctx), Some(960.0));
242        assert_eq!(vh(100.0).resolve_full(&ctx), Some(1080.0));
243        assert_eq!(Dimension::Vmin(50.0).resolve_full(&ctx), Some(540.0));
244        assert_eq!(Dimension::Vmax(50.0).resolve_full(&ctx), Some(960.0));
245    }
246
247    #[test]
248    fn intrinsic_values_dont_resolve() {
249        let ctx = ResolveContext {
250            parent: 500.0,
251            font_size: 16.0,
252            root_font_size: 16.0,
253            viewport_width: 1920.0,
254            viewport_height: 1080.0,
255        };
256
257        assert!(Dimension::Auto.resolve_full(&ctx).is_none());
258        assert!(Dimension::MinContent.resolve_full(&ctx).is_none());
259    }
260}