Skip to main content

azul_css/props/layout/
dimensions.rs

1//! CSS properties related to dimensions and sizing.
2
3use alloc::string::{String, ToString};
4
5use crate::props::{
6    basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
7    formatter::PrintAsCssValue,
8    macros::PixelValueTaker,
9};
10
11// -- Type Definitions --
12
13macro_rules! define_dimension_property {
14    ($struct_name:ident, $default_fn:expr) => {
15        #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16        #[repr(C)]
17        pub struct $struct_name {
18            pub inner: PixelValue,
19        }
20
21        impl Default for $struct_name {
22            fn default() -> Self {
23                $default_fn()
24            }
25        }
26
27        impl PixelValueTaker for $struct_name {
28            fn from_pixel_value(inner: PixelValue) -> Self {
29                Self { inner }
30            }
31        }
32
33        impl_pixel_value!($struct_name);
34
35        impl PrintAsCssValue for $struct_name {
36            fn print_as_css_value(&self) -> String {
37                self.inner.to_string()
38            }
39        }
40    };
41}
42
43// Custom implementation for LayoutWidth to support min-content and max-content
44#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
45#[repr(C, u8)]
46pub enum LayoutWidth {
47    Auto, // NEW: Represents CSS 'auto' or unset value
48    Px(PixelValue),
49    MinContent,
50    MaxContent,
51}
52
53impl Default for LayoutWidth {
54    fn default() -> Self {
55        LayoutWidth::Auto // FIXED: Auto is now the default, not Px(0)
56    }
57}
58
59impl PixelValueTaker for LayoutWidth {
60    fn from_pixel_value(inner: PixelValue) -> Self {
61        LayoutWidth::Px(inner)
62    }
63}
64
65impl PrintAsCssValue for LayoutWidth {
66    fn print_as_css_value(&self) -> String {
67        match self {
68            LayoutWidth::Auto => "auto".to_string(),
69            LayoutWidth::Px(v) => v.to_string(),
70            LayoutWidth::MinContent => "min-content".to_string(),
71            LayoutWidth::MaxContent => "max-content".to_string(),
72        }
73    }
74}
75
76impl LayoutWidth {
77    pub fn px(value: f32) -> Self {
78        LayoutWidth::Px(PixelValue::px(value))
79    }
80
81    pub const fn const_px(value: isize) -> Self {
82        LayoutWidth::Px(PixelValue::const_px(value))
83    }
84
85    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
86        match (self, other) {
87            (LayoutWidth::Px(a), LayoutWidth::Px(b)) => LayoutWidth::Px(a.interpolate(b, t)),
88            // Can't interpolate between keywords, so just return start value at t < 0.5, end value
89            // otherwise
90            (_, LayoutWidth::Px(b)) if t >= 0.5 => LayoutWidth::Px(*b),
91            (LayoutWidth::Px(a), _) if t < 0.5 => LayoutWidth::Px(*a),
92            // Handle Auto variant
93            (LayoutWidth::Auto, LayoutWidth::Auto) => LayoutWidth::Auto,
94            (a, _) if t < 0.5 => *a,
95            (_, b) => *b,
96        }
97    }
98}
99
100// Custom implementation for LayoutHeight to support min-content and max-content
101#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
102#[repr(C, u8)]
103pub enum LayoutHeight {
104    Auto, // NEW: Represents CSS 'auto' or unset value
105    Px(PixelValue),
106    MinContent,
107    MaxContent,
108}
109
110impl Default for LayoutHeight {
111    fn default() -> Self {
112        LayoutHeight::Auto // FIXED: Auto is now the default, not Px(0)
113    }
114}
115
116impl PixelValueTaker for LayoutHeight {
117    fn from_pixel_value(inner: PixelValue) -> Self {
118        LayoutHeight::Px(inner)
119    }
120}
121
122impl PrintAsCssValue for LayoutHeight {
123    fn print_as_css_value(&self) -> String {
124        match self {
125            LayoutHeight::Auto => "auto".to_string(),
126            LayoutHeight::Px(v) => v.to_string(),
127            LayoutHeight::MinContent => "min-content".to_string(),
128            LayoutHeight::MaxContent => "max-content".to_string(),
129        }
130    }
131}
132
133impl LayoutHeight {
134    pub fn px(value: f32) -> Self {
135        LayoutHeight::Px(PixelValue::px(value))
136    }
137
138    pub const fn const_px(value: isize) -> Self {
139        LayoutHeight::Px(PixelValue::const_px(value))
140    }
141
142    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
143        match (self, other) {
144            (LayoutHeight::Px(a), LayoutHeight::Px(b)) => LayoutHeight::Px(a.interpolate(b, t)),
145            // Can't interpolate between keywords, so just return start value at t < 0.5, end value
146            // otherwise
147            (_, LayoutHeight::Px(b)) if t >= 0.5 => LayoutHeight::Px(*b),
148            (LayoutHeight::Px(a), _) if t < 0.5 => LayoutHeight::Px(*a),
149            // Handle Auto variant
150            (LayoutHeight::Auto, LayoutHeight::Auto) => LayoutHeight::Auto,
151            (a, _) if t < 0.5 => *a,
152            (_, b) => *b,
153        }
154    }
155}
156
157define_dimension_property!(LayoutMinWidth, || Self {
158    inner: PixelValue::zero()
159});
160define_dimension_property!(LayoutMinHeight, || Self {
161    inner: PixelValue::zero()
162});
163define_dimension_property!(LayoutMaxWidth, || Self {
164    inner: PixelValue::px(core::f32::MAX)
165});
166define_dimension_property!(LayoutMaxHeight, || Self {
167    inner: PixelValue::px(core::f32::MAX)
168});
169
170/// Represents a `box-sizing` attribute
171#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
172#[repr(C)]
173pub enum LayoutBoxSizing {
174    ContentBox,
175    BorderBox,
176}
177
178impl Default for LayoutBoxSizing {
179    fn default() -> Self {
180        LayoutBoxSizing::ContentBox
181    }
182}
183
184impl PrintAsCssValue for LayoutBoxSizing {
185    fn print_as_css_value(&self) -> String {
186        String::from(match self {
187            LayoutBoxSizing::ContentBox => "content-box",
188            LayoutBoxSizing::BorderBox => "border-box",
189        })
190    }
191}
192
193// -- Parser --
194
195#[cfg(feature = "parser")]
196mod parser {
197
198    use alloc::string::ToString;
199
200    use super::*;
201    use crate::props::basic::pixel::parse_pixel_value;
202
203    macro_rules! define_pixel_dimension_parser {
204        ($fn_name:ident, $struct_name:ident, $error_name:ident, $error_owned_name:ident) => {
205            #[derive(Clone, PartialEq)]
206            pub enum $error_name<'a> {
207                PixelValue(CssPixelValueParseError<'a>),
208            }
209
210            impl_debug_as_display!($error_name<'a>);
211            impl_display! { $error_name<'a>, {
212                PixelValue(e) => format!("{}", e),
213            }}
214
215            impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
216
217            #[derive(Debug, Clone, PartialEq)]
218            pub enum $error_owned_name {
219                PixelValue(CssPixelValueParseErrorOwned),
220            }
221
222            impl<'a> $error_name<'a> {
223                pub fn to_contained(&self) -> $error_owned_name {
224                    match self {
225                        $error_name::PixelValue(e) => {
226                            $error_owned_name::PixelValue(e.to_contained())
227                        }
228                    }
229                }
230            }
231
232            impl $error_owned_name {
233                pub fn to_shared<'a>(&'a self) -> $error_name<'a> {
234                    match self {
235                        $error_owned_name::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
236                    }
237                }
238            }
239
240            pub fn $fn_name<'a>(input: &'a str) -> Result<$struct_name, $error_name<'a>> {
241                parse_pixel_value(input)
242                    .map(|v| $struct_name { inner: v })
243                    .map_err($error_name::PixelValue)
244            }
245        };
246    }
247
248    // Custom parsers for LayoutWidth and LayoutHeight with min-content/max-content support
249
250    #[derive(Clone, PartialEq)]
251    pub enum LayoutWidthParseError<'a> {
252        PixelValue(CssPixelValueParseError<'a>),
253        InvalidKeyword(&'a str),
254    }
255
256    impl_debug_as_display!(LayoutWidthParseError<'a>);
257    impl_display! { LayoutWidthParseError<'a>, {
258        PixelValue(e) => format!("{}", e),
259        InvalidKeyword(k) => format!("Invalid width keyword: \"{}\"", k),
260    }}
261
262    impl_from! { CssPixelValueParseError<'a>, LayoutWidthParseError::PixelValue }
263
264    #[derive(Debug, Clone, PartialEq)]
265    pub enum LayoutWidthParseErrorOwned {
266        PixelValue(CssPixelValueParseErrorOwned),
267        InvalidKeyword(String),
268    }
269
270    impl<'a> LayoutWidthParseError<'a> {
271        pub fn to_contained(&self) -> LayoutWidthParseErrorOwned {
272            match self {
273                LayoutWidthParseError::PixelValue(e) => {
274                    LayoutWidthParseErrorOwned::PixelValue(e.to_contained())
275                }
276                LayoutWidthParseError::InvalidKeyword(k) => {
277                    LayoutWidthParseErrorOwned::InvalidKeyword(k.to_string())
278                }
279            }
280        }
281    }
282
283    impl LayoutWidthParseErrorOwned {
284        pub fn to_shared<'a>(&'a self) -> LayoutWidthParseError<'a> {
285            match self {
286                LayoutWidthParseErrorOwned::PixelValue(e) => {
287                    LayoutWidthParseError::PixelValue(e.to_shared())
288                }
289                LayoutWidthParseErrorOwned::InvalidKeyword(k) => {
290                    LayoutWidthParseError::InvalidKeyword(k)
291                }
292            }
293        }
294    }
295
296    pub fn parse_layout_width<'a>(
297        input: &'a str,
298    ) -> Result<LayoutWidth, LayoutWidthParseError<'a>> {
299        let trimmed = input.trim();
300        match trimmed {
301            "auto" => Ok(LayoutWidth::Auto),
302            "min-content" => Ok(LayoutWidth::MinContent),
303            "max-content" => Ok(LayoutWidth::MaxContent),
304            _ => parse_pixel_value(trimmed)
305                .map(LayoutWidth::Px)
306                .map_err(LayoutWidthParseError::PixelValue),
307        }
308    }
309
310    #[derive(Clone, PartialEq)]
311    pub enum LayoutHeightParseError<'a> {
312        PixelValue(CssPixelValueParseError<'a>),
313        InvalidKeyword(&'a str),
314    }
315
316    impl_debug_as_display!(LayoutHeightParseError<'a>);
317    impl_display! { LayoutHeightParseError<'a>, {
318        PixelValue(e) => format!("{}", e),
319        InvalidKeyword(k) => format!("Invalid height keyword: \"{}\"", k),
320    }}
321
322    impl_from! { CssPixelValueParseError<'a>, LayoutHeightParseError::PixelValue }
323
324    #[derive(Debug, Clone, PartialEq)]
325    pub enum LayoutHeightParseErrorOwned {
326        PixelValue(CssPixelValueParseErrorOwned),
327        InvalidKeyword(String),
328    }
329
330    impl<'a> LayoutHeightParseError<'a> {
331        pub fn to_contained(&self) -> LayoutHeightParseErrorOwned {
332            match self {
333                LayoutHeightParseError::PixelValue(e) => {
334                    LayoutHeightParseErrorOwned::PixelValue(e.to_contained())
335                }
336                LayoutHeightParseError::InvalidKeyword(k) => {
337                    LayoutHeightParseErrorOwned::InvalidKeyword(k.to_string())
338                }
339            }
340        }
341    }
342
343    impl LayoutHeightParseErrorOwned {
344        pub fn to_shared<'a>(&'a self) -> LayoutHeightParseError<'a> {
345            match self {
346                LayoutHeightParseErrorOwned::PixelValue(e) => {
347                    LayoutHeightParseError::PixelValue(e.to_shared())
348                }
349                LayoutHeightParseErrorOwned::InvalidKeyword(k) => {
350                    LayoutHeightParseError::InvalidKeyword(k)
351                }
352            }
353        }
354    }
355
356    pub fn parse_layout_height<'a>(
357        input: &'a str,
358    ) -> Result<LayoutHeight, LayoutHeightParseError<'a>> {
359        let trimmed = input.trim();
360        match trimmed {
361            "auto" => Ok(LayoutHeight::Auto),
362            "min-content" => Ok(LayoutHeight::MinContent),
363            "max-content" => Ok(LayoutHeight::MaxContent),
364            _ => parse_pixel_value(trimmed)
365                .map(LayoutHeight::Px)
366                .map_err(LayoutHeightParseError::PixelValue),
367        }
368    }
369    define_pixel_dimension_parser!(
370        parse_layout_min_width,
371        LayoutMinWidth,
372        LayoutMinWidthParseError,
373        LayoutMinWidthParseErrorOwned
374    );
375    define_pixel_dimension_parser!(
376        parse_layout_min_height,
377        LayoutMinHeight,
378        LayoutMinHeightParseError,
379        LayoutMinHeightParseErrorOwned
380    );
381    define_pixel_dimension_parser!(
382        parse_layout_max_width,
383        LayoutMaxWidth,
384        LayoutMaxWidthParseError,
385        LayoutMaxWidthParseErrorOwned
386    );
387    define_pixel_dimension_parser!(
388        parse_layout_max_height,
389        LayoutMaxHeight,
390        LayoutMaxHeightParseError,
391        LayoutMaxHeightParseErrorOwned
392    );
393
394    // -- Box Sizing Parser --
395
396    #[derive(Clone, PartialEq)]
397    pub enum LayoutBoxSizingParseError<'a> {
398        InvalidValue(&'a str),
399    }
400
401    impl_debug_as_display!(LayoutBoxSizingParseError<'a>);
402    impl_display! { LayoutBoxSizingParseError<'a>, {
403        InvalidValue(v) => format!("Invalid box-sizing value: \"{}\"", v),
404    }}
405
406    #[derive(Debug, Clone, PartialEq)]
407    pub enum LayoutBoxSizingParseErrorOwned {
408        InvalidValue(String),
409    }
410
411    impl<'a> LayoutBoxSizingParseError<'a> {
412        pub fn to_contained(&self) -> LayoutBoxSizingParseErrorOwned {
413            match self {
414                LayoutBoxSizingParseError::InvalidValue(s) => {
415                    LayoutBoxSizingParseErrorOwned::InvalidValue(s.to_string())
416                }
417            }
418        }
419    }
420
421    impl LayoutBoxSizingParseErrorOwned {
422        pub fn to_shared<'a>(&'a self) -> LayoutBoxSizingParseError<'a> {
423            match self {
424                LayoutBoxSizingParseErrorOwned::InvalidValue(s) => {
425                    LayoutBoxSizingParseError::InvalidValue(s)
426                }
427            }
428        }
429    }
430
431    pub fn parse_layout_box_sizing<'a>(
432        input: &'a str,
433    ) -> Result<LayoutBoxSizing, LayoutBoxSizingParseError<'a>> {
434        match input.trim() {
435            "content-box" => Ok(LayoutBoxSizing::ContentBox),
436            "border-box" => Ok(LayoutBoxSizing::BorderBox),
437            other => Err(LayoutBoxSizingParseError::InvalidValue(other)),
438        }
439    }
440}
441
442#[cfg(feature = "parser")]
443pub use self::parser::*;
444
445#[cfg(all(test, feature = "parser"))]
446mod tests {
447    use super::*;
448    use crate::props::basic::pixel::PixelValue;
449
450    #[test]
451    fn test_parse_layout_width() {
452        assert_eq!(
453            parse_layout_width("150px").unwrap(),
454            LayoutWidth::Px(PixelValue::px(150.0))
455        );
456        assert_eq!(
457            parse_layout_width("2.5em").unwrap(),
458            LayoutWidth::Px(PixelValue::em(2.5))
459        );
460        assert_eq!(
461            parse_layout_width("75%").unwrap(),
462            LayoutWidth::Px(PixelValue::percent(75.0))
463        );
464        assert_eq!(
465            parse_layout_width("0").unwrap(),
466            LayoutWidth::Px(PixelValue::px(0.0))
467        );
468        assert_eq!(
469            parse_layout_width("  100pt  ").unwrap(),
470            LayoutWidth::Px(PixelValue::pt(100.0))
471        );
472        assert_eq!(
473            parse_layout_width("min-content").unwrap(),
474            LayoutWidth::MinContent
475        );
476        assert_eq!(
477            parse_layout_width("max-content").unwrap(),
478            LayoutWidth::MaxContent
479        );
480    }
481
482    #[test]
483    fn test_parse_layout_height_invalid() {
484        // "auto" is now a valid value for height (CSS spec)
485        assert!(parse_layout_height("auto").is_ok());
486        // Liberal parsing accepts whitespace between number and unit
487        assert!(parse_layout_height("150 px").is_ok());
488        assert!(parse_layout_height("px").is_err());
489        assert!(parse_layout_height("invalid").is_err());
490    }
491
492    #[test]
493    fn test_parse_layout_box_sizing() {
494        assert_eq!(
495            parse_layout_box_sizing("content-box").unwrap(),
496            LayoutBoxSizing::ContentBox
497        );
498        assert_eq!(
499            parse_layout_box_sizing("border-box").unwrap(),
500            LayoutBoxSizing::BorderBox
501        );
502        assert_eq!(
503            parse_layout_box_sizing("  border-box  ").unwrap(),
504            LayoutBoxSizing::BorderBox
505        );
506    }
507
508    #[test]
509    fn test_parse_layout_box_sizing_invalid() {
510        assert!(parse_layout_box_sizing("padding-box").is_err());
511        assert!(parse_layout_box_sizing("borderbox").is_err());
512        assert!(parse_layout_box_sizing("").is_err());
513    }
514}