Skip to main content

azul_css/props/layout/
dimensions.rs

1//! CSS properties related to dimensions and sizing.
2
3use alloc::{
4    string::{String, ToString},
5    vec::Vec,
6};
7
8use crate::{
9    impl_option, impl_option_inner, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_eq,
10    impl_vec_hash, impl_vec_mut, impl_vec_ord, impl_vec_partialeq, impl_vec_partialord,
11    props::{
12        basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
13        formatter::PrintAsCssValue,
14        macros::PixelValueTaker,
15    },
16};
17
18// -- Calc AST --
19
20/// A single item in a `calc()` expression, stored as a flat stack-machine representation.
21///
22/// The expression `calc(33.333% - 10px)` is stored as:
23/// ```text
24/// [Value(33.333%), Sub, Value(10px)]
25/// ```
26///
27/// For nested expressions like `calc(100% - (20px + 5%))`:
28/// ```text
29/// [Value(100%), Sub, BraceOpen, Value(20px), Add, Value(5%), BraceClose]
30/// ```
31///
32/// **Resolution**: Walk left to right. When `BraceClose` is hit, resolve everything
33/// back to the matching `BraceOpen`, replace that span with a single `Value`, and continue.
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35#[repr(C, u8)]
36pub enum CalcAstItem {
37    /// A literal value (e.g. `10px`, `33.333%`, `2em`)
38    Value(PixelValue),
39    /// `+` operator
40    Add,
41    /// `-` operator
42    Sub,
43    /// `*` operator
44    Mul,
45    /// `/` operator
46    Div,
47    /// `(` — opens a sub-expression
48    BraceOpen,
49    /// `)` — closes a sub-expression; triggers resolution of the inner span
50    BraceClose,
51}
52
53// C-compatible Vec for CalcAstItem
54impl_vec!(
55    CalcAstItem,
56    CalcAstItemVec,
57    CalcAstItemVecDestructor,
58    CalcAstItemVecDestructorType,
59    CalcAstItemVecSlice,
60    OptionCalcAstItem
61);
62impl_vec_clone!(CalcAstItem, CalcAstItemVec, CalcAstItemVecDestructor);
63impl_vec_debug!(CalcAstItem, CalcAstItemVec);
64impl_vec_partialeq!(CalcAstItem, CalcAstItemVec);
65impl_vec_eq!(CalcAstItem, CalcAstItemVec);
66impl_vec_partialord!(CalcAstItem, CalcAstItemVec);
67impl_vec_ord!(CalcAstItem, CalcAstItemVec);
68impl_vec_hash!(CalcAstItem, CalcAstItemVec);
69impl_vec_mut!(CalcAstItem, CalcAstItemVec);
70
71impl_option!(
72    CalcAstItem,
73    OptionCalcAstItem,
74    copy = false,
75    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
76);
77
78/// Parse a calc() inner expression (the part between the parentheses) into
79/// a flat `CalcAstItemVec` suitable for stack-machine evaluation.
80///
81/// Examples:
82/// - `"100% - 20px"` → `[Value(100%), Sub, Value(20px)]`
83/// - `"(100% - 20px) / 3"` → `[BraceOpen, Value(100%), Sub, Value(20px), BraceClose, Div, Value(3)]`
84///
85/// **Tokenisation rules**:
86///  - Whitespace is skipped between tokens.
87///  - `+`, `-`, `*`, `/` are operators (but `-` at the start of a number is
88///    part of the number literal, e.g. `-10px`).
89///  - `(` / `)` produce `BraceOpen` / `BraceClose`.
90///  - Anything else is parsed as a `PixelValue` via `parse_pixel_value`.
91#[cfg(feature = "parser")]
92fn parse_calc_expression(input: &str) -> Result<CalcAstItemVec, ()> {
93    use crate::props::basic::pixel::parse_pixel_value;
94
95    let mut items: Vec<CalcAstItem> = Vec::new();
96    let input = input.trim();
97    let bytes = input.as_bytes();
98    let mut i = 0;
99
100    while i < bytes.len() {
101        // Skip whitespace
102        if bytes[i].is_ascii_whitespace() {
103            i += 1;
104            continue;
105        }
106
107        match bytes[i] {
108            b'+' => { items.push(CalcAstItem::Add); i += 1; }
109            b'*' => { items.push(CalcAstItem::Mul); i += 1; }
110            b'/' => { items.push(CalcAstItem::Div); i += 1; }
111            b'(' => { items.push(CalcAstItem::BraceOpen); i += 1; }
112            b')' => { items.push(CalcAstItem::BraceClose); i += 1; }
113            b'-' => {
114                // Decide: is this a subtraction operator or a negative number?
115                // It's a negative number if:
116                //   - it's the first token, OR
117                //   - the previous token is an operator or BraceOpen
118                let is_negative_number = items.is_empty()
119                    || matches!(
120                        items.last(),
121                        Some(CalcAstItem::Add)
122                            | Some(CalcAstItem::Sub)
123                            | Some(CalcAstItem::Mul)
124                            | Some(CalcAstItem::Div)
125                            | Some(CalcAstItem::BraceOpen)
126                    );
127
128                if is_negative_number {
129                    // Parse as negative number value
130                    let rest = &input[i..];
131                    let end = find_value_end(rest);
132                    if end == 0 { return Err(()); }
133                    let val_str = &rest[..end];
134                    let pv = parse_pixel_value(val_str).map_err(|_| ())?;
135                    items.push(CalcAstItem::Value(pv));
136                    i += end;
137                } else {
138                    items.push(CalcAstItem::Sub);
139                    i += 1;
140                }
141            }
142            _ => {
143                // Must be a numeric value (e.g. 100%, 20px, 3, 1.5em)
144                let rest = &input[i..];
145                let end = find_value_end(rest);
146                if end == 0 { return Err(()); }
147                let val_str = &rest[..end];
148                let pv = parse_pixel_value(val_str).map_err(|_| ())?;
149                items.push(CalcAstItem::Value(pv));
150                i += end;
151            }
152        }
153    }
154
155    if items.is_empty() {
156        return Err(());
157    }
158
159    Ok(CalcAstItemVec::from(items))
160}
161
162/// Find the end of a numeric value token in a calc() expression.
163/// Returns the byte offset where the value ends.
164#[cfg(feature = "parser")]
165fn find_value_end(s: &str) -> usize {
166    let bytes = s.as_bytes();
167    let mut i = 0;
168
169    // Optional leading sign
170    if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
171        i += 1;
172    }
173
174    // Digits and decimal point
175    while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
176        i += 1;
177    }
178
179    // Unit suffix (alphabetic characters like px, %, em, rem, vw, vh, etc.)
180    while i < bytes.len() && (bytes[i].is_ascii_alphabetic() || bytes[i] == b'%') {
181        i += 1;
182    }
183
184    i
185}
186
187// -- Type Definitions --
188
189macro_rules! define_dimension_property {
190    ($struct_name:ident, $default_fn:expr) => {
191        #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
192        #[repr(C)]
193        pub struct $struct_name {
194            pub inner: PixelValue,
195        }
196
197        impl Default for $struct_name {
198            fn default() -> Self {
199                $default_fn()
200            }
201        }
202
203        impl PixelValueTaker for $struct_name {
204            fn from_pixel_value(inner: PixelValue) -> Self {
205                Self { inner }
206            }
207        }
208
209        impl_pixel_value!($struct_name);
210
211        impl PrintAsCssValue for $struct_name {
212            fn print_as_css_value(&self) -> String {
213                self.inner.to_string()
214            }
215        }
216    };
217}
218
219// Custom implementation for LayoutWidth to support min-content, max-content, and calc()
220#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
221#[repr(C, u8)]
222pub enum LayoutWidth {
223    Auto,
224    Px(PixelValue),
225    MinContent,
226    MaxContent,
227    /// `calc()` expression stored as a flat stack-machine AST
228    Calc(CalcAstItemVec),
229}
230
231impl Default for LayoutWidth {
232    fn default() -> Self {
233        LayoutWidth::Auto
234    }
235}
236
237impl PixelValueTaker for LayoutWidth {
238    fn from_pixel_value(inner: PixelValue) -> Self {
239        LayoutWidth::Px(inner)
240    }
241}
242
243impl PrintAsCssValue for LayoutWidth {
244    fn print_as_css_value(&self) -> String {
245        match self {
246            LayoutWidth::Auto => "auto".to_string(),
247            LayoutWidth::Px(v) => v.to_string(),
248            LayoutWidth::MinContent => "min-content".to_string(),
249            LayoutWidth::MaxContent => "max-content".to_string(),
250            LayoutWidth::Calc(items) => {
251                let inner: Vec<String> = items.iter().map(|i| match i {
252                    CalcAstItem::Value(v) => v.to_string(),
253                    CalcAstItem::Add => "+".to_string(),
254                    CalcAstItem::Sub => "-".to_string(),
255                    CalcAstItem::Mul => "*".to_string(),
256                    CalcAstItem::Div => "/".to_string(),
257                    CalcAstItem::BraceOpen => "(".to_string(),
258                    CalcAstItem::BraceClose => ")".to_string(),
259                }).collect();
260                alloc::format!("calc({})", inner.join(" "))
261            }
262        }
263    }
264}
265
266impl LayoutWidth {
267    pub fn px(value: f32) -> Self {
268        LayoutWidth::Px(PixelValue::px(value))
269    }
270
271    pub const fn const_px(value: isize) -> Self {
272        LayoutWidth::Px(PixelValue::const_px(value))
273    }
274
275    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
276        match (self, other) {
277            (LayoutWidth::Px(a), LayoutWidth::Px(b)) => LayoutWidth::Px(a.interpolate(b, t)),
278            (_, LayoutWidth::Px(b)) if t >= 0.5 => LayoutWidth::Px(*b),
279            (LayoutWidth::Px(a), _) if t < 0.5 => LayoutWidth::Px(*a),
280            (LayoutWidth::Auto, LayoutWidth::Auto) => LayoutWidth::Auto,
281            (a, _) if t < 0.5 => a.clone(),
282            (_, b) => b.clone(),
283        }
284    }
285}
286
287// Custom implementation for LayoutHeight to support min-content, max-content, and calc()
288#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
289#[repr(C, u8)]
290pub enum LayoutHeight {
291    Auto,
292    Px(PixelValue),
293    MinContent,
294    MaxContent,
295    /// `calc()` expression stored as a flat stack-machine AST
296    Calc(CalcAstItemVec),
297}
298
299impl Default for LayoutHeight {
300    fn default() -> Self {
301        LayoutHeight::Auto
302    }
303}
304
305impl PixelValueTaker for LayoutHeight {
306    fn from_pixel_value(inner: PixelValue) -> Self {
307        LayoutHeight::Px(inner)
308    }
309}
310
311impl PrintAsCssValue for LayoutHeight {
312    fn print_as_css_value(&self) -> String {
313        match self {
314            LayoutHeight::Auto => "auto".to_string(),
315            LayoutHeight::Px(v) => v.to_string(),
316            LayoutHeight::MinContent => "min-content".to_string(),
317            LayoutHeight::MaxContent => "max-content".to_string(),
318            LayoutHeight::Calc(items) => {
319                let inner: Vec<String> = items.iter().map(|i| match i {
320                    CalcAstItem::Value(v) => v.to_string(),
321                    CalcAstItem::Add => "+".to_string(),
322                    CalcAstItem::Sub => "-".to_string(),
323                    CalcAstItem::Mul => "*".to_string(),
324                    CalcAstItem::Div => "/".to_string(),
325                    CalcAstItem::BraceOpen => "(".to_string(),
326                    CalcAstItem::BraceClose => ")".to_string(),
327                }).collect();
328                alloc::format!("calc({})", inner.join(" "))
329            }
330        }
331    }
332}
333
334impl LayoutHeight {
335    pub fn px(value: f32) -> Self {
336        LayoutHeight::Px(PixelValue::px(value))
337    }
338
339    pub const fn const_px(value: isize) -> Self {
340        LayoutHeight::Px(PixelValue::const_px(value))
341    }
342
343    pub fn interpolate(&self, other: &Self, t: f32) -> Self {
344        match (self, other) {
345            (LayoutHeight::Px(a), LayoutHeight::Px(b)) => LayoutHeight::Px(a.interpolate(b, t)),
346            (_, LayoutHeight::Px(b)) if t >= 0.5 => LayoutHeight::Px(*b),
347            (LayoutHeight::Px(a), _) if t < 0.5 => LayoutHeight::Px(*a),
348            (LayoutHeight::Auto, LayoutHeight::Auto) => LayoutHeight::Auto,
349            (a, _) if t < 0.5 => a.clone(),
350            (_, b) => b.clone(),
351        }
352    }
353}
354
355define_dimension_property!(LayoutMinWidth, || Self {
356    inner: PixelValue::zero()
357});
358define_dimension_property!(LayoutMinHeight, || Self {
359    inner: PixelValue::zero()
360});
361define_dimension_property!(LayoutMaxWidth, || Self {
362    inner: PixelValue::px(core::f32::MAX)
363});
364define_dimension_property!(LayoutMaxHeight, || Self {
365    inner: PixelValue::px(core::f32::MAX)
366});
367
368/// Represents a `box-sizing` attribute
369#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
370#[repr(C)]
371pub enum LayoutBoxSizing {
372    ContentBox,
373    BorderBox,
374}
375
376impl Default for LayoutBoxSizing {
377    fn default() -> Self {
378        LayoutBoxSizing::ContentBox
379    }
380}
381
382impl PrintAsCssValue for LayoutBoxSizing {
383    fn print_as_css_value(&self) -> String {
384        String::from(match self {
385            LayoutBoxSizing::ContentBox => "content-box",
386            LayoutBoxSizing::BorderBox => "border-box",
387        })
388    }
389}
390
391// -- Parser --
392
393#[cfg(feature = "parser")]
394mod parser {
395
396    use alloc::string::ToString;
397
398    use super::*;
399    use crate::props::basic::pixel::parse_pixel_value;
400
401    macro_rules! define_pixel_dimension_parser {
402        ($fn_name:ident, $struct_name:ident, $error_name:ident, $error_owned_name:ident) => {
403            #[derive(Clone, PartialEq)]
404            pub enum $error_name<'a> {
405                PixelValue(CssPixelValueParseError<'a>),
406            }
407
408            impl_debug_as_display!($error_name<'a>);
409            impl_display! { $error_name<'a>, {
410                PixelValue(e) => format!("{}", e),
411            }}
412
413            impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
414
415            #[derive(Debug, Clone, PartialEq)]
416            pub enum $error_owned_name {
417                PixelValue(CssPixelValueParseErrorOwned),
418            }
419
420            impl<'a> $error_name<'a> {
421                pub fn to_contained(&self) -> $error_owned_name {
422                    match self {
423                        $error_name::PixelValue(e) => {
424                            $error_owned_name::PixelValue(e.to_contained())
425                        }
426                    }
427                }
428            }
429
430            impl $error_owned_name {
431                pub fn to_shared<'a>(&'a self) -> $error_name<'a> {
432                    match self {
433                        $error_owned_name::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
434                    }
435                }
436            }
437
438            pub fn $fn_name<'a>(input: &'a str) -> Result<$struct_name, $error_name<'a>> {
439                parse_pixel_value(input)
440                    .map(|v| $struct_name { inner: v })
441                    .map_err($error_name::PixelValue)
442            }
443        };
444    }
445
446    // Custom parsers for LayoutWidth and LayoutHeight with min-content/max-content support
447
448    #[derive(Clone, PartialEq)]
449    pub enum LayoutWidthParseError<'a> {
450        PixelValue(CssPixelValueParseError<'a>),
451        InvalidKeyword(&'a str),
452    }
453
454    impl_debug_as_display!(LayoutWidthParseError<'a>);
455    impl_display! { LayoutWidthParseError<'a>, {
456        PixelValue(e) => format!("{}", e),
457        InvalidKeyword(k) => format!("Invalid width keyword: \"{}\"", k),
458    }}
459
460    impl_from! { CssPixelValueParseError<'a>, LayoutWidthParseError::PixelValue }
461
462    #[derive(Debug, Clone, PartialEq)]
463    pub enum LayoutWidthParseErrorOwned {
464        PixelValue(CssPixelValueParseErrorOwned),
465        InvalidKeyword(String),
466    }
467
468    impl<'a> LayoutWidthParseError<'a> {
469        pub fn to_contained(&self) -> LayoutWidthParseErrorOwned {
470            match self {
471                LayoutWidthParseError::PixelValue(e) => {
472                    LayoutWidthParseErrorOwned::PixelValue(e.to_contained())
473                }
474                LayoutWidthParseError::InvalidKeyword(k) => {
475                    LayoutWidthParseErrorOwned::InvalidKeyword(k.to_string())
476                }
477            }
478        }
479    }
480
481    impl LayoutWidthParseErrorOwned {
482        pub fn to_shared<'a>(&'a self) -> LayoutWidthParseError<'a> {
483            match self {
484                LayoutWidthParseErrorOwned::PixelValue(e) => {
485                    LayoutWidthParseError::PixelValue(e.to_shared())
486                }
487                LayoutWidthParseErrorOwned::InvalidKeyword(k) => {
488                    LayoutWidthParseError::InvalidKeyword(k)
489                }
490            }
491        }
492    }
493
494    pub fn parse_layout_width<'a>(
495        input: &'a str,
496    ) -> Result<LayoutWidth, LayoutWidthParseError<'a>> {
497        let trimmed = input.trim();
498        match trimmed {
499            "auto" => Ok(LayoutWidth::Auto),
500            "min-content" => Ok(LayoutWidth::MinContent),
501            "max-content" => Ok(LayoutWidth::MaxContent),
502            s if s.starts_with("calc(") && s.ends_with(')') => {
503                let inner = &s[5..s.len() - 1];
504                parse_calc_expression(inner)
505                    .map(LayoutWidth::Calc)
506                    .map_err(|_| LayoutWidthParseError::InvalidKeyword(input))
507            }
508            _ => parse_pixel_value(trimmed)
509                .map(LayoutWidth::Px)
510                .map_err(LayoutWidthParseError::PixelValue),
511        }
512    }
513
514    #[derive(Clone, PartialEq)]
515    pub enum LayoutHeightParseError<'a> {
516        PixelValue(CssPixelValueParseError<'a>),
517        InvalidKeyword(&'a str),
518    }
519
520    impl_debug_as_display!(LayoutHeightParseError<'a>);
521    impl_display! { LayoutHeightParseError<'a>, {
522        PixelValue(e) => format!("{}", e),
523        InvalidKeyword(k) => format!("Invalid height keyword: \"{}\"", k),
524    }}
525
526    impl_from! { CssPixelValueParseError<'a>, LayoutHeightParseError::PixelValue }
527
528    #[derive(Debug, Clone, PartialEq)]
529    pub enum LayoutHeightParseErrorOwned {
530        PixelValue(CssPixelValueParseErrorOwned),
531        InvalidKeyword(String),
532    }
533
534    impl<'a> LayoutHeightParseError<'a> {
535        pub fn to_contained(&self) -> LayoutHeightParseErrorOwned {
536            match self {
537                LayoutHeightParseError::PixelValue(e) => {
538                    LayoutHeightParseErrorOwned::PixelValue(e.to_contained())
539                }
540                LayoutHeightParseError::InvalidKeyword(k) => {
541                    LayoutHeightParseErrorOwned::InvalidKeyword(k.to_string())
542                }
543            }
544        }
545    }
546
547    impl LayoutHeightParseErrorOwned {
548        pub fn to_shared<'a>(&'a self) -> LayoutHeightParseError<'a> {
549            match self {
550                LayoutHeightParseErrorOwned::PixelValue(e) => {
551                    LayoutHeightParseError::PixelValue(e.to_shared())
552                }
553                LayoutHeightParseErrorOwned::InvalidKeyword(k) => {
554                    LayoutHeightParseError::InvalidKeyword(k)
555                }
556            }
557        }
558    }
559
560    pub fn parse_layout_height<'a>(
561        input: &'a str,
562    ) -> Result<LayoutHeight, LayoutHeightParseError<'a>> {
563        let trimmed = input.trim();
564        match trimmed {
565            "auto" => Ok(LayoutHeight::Auto),
566            "min-content" => Ok(LayoutHeight::MinContent),
567            "max-content" => Ok(LayoutHeight::MaxContent),
568            s if s.starts_with("calc(") && s.ends_with(')') => {
569                let inner = &s[5..s.len() - 1];
570                parse_calc_expression(inner)
571                    .map(LayoutHeight::Calc)
572                    .map_err(|_| LayoutHeightParseError::InvalidKeyword(input))
573            }
574            _ => parse_pixel_value(trimmed)
575                .map(LayoutHeight::Px)
576                .map_err(LayoutHeightParseError::PixelValue),
577        }
578    }
579    define_pixel_dimension_parser!(
580        parse_layout_min_width,
581        LayoutMinWidth,
582        LayoutMinWidthParseError,
583        LayoutMinWidthParseErrorOwned
584    );
585    define_pixel_dimension_parser!(
586        parse_layout_min_height,
587        LayoutMinHeight,
588        LayoutMinHeightParseError,
589        LayoutMinHeightParseErrorOwned
590    );
591    define_pixel_dimension_parser!(
592        parse_layout_max_width,
593        LayoutMaxWidth,
594        LayoutMaxWidthParseError,
595        LayoutMaxWidthParseErrorOwned
596    );
597    define_pixel_dimension_parser!(
598        parse_layout_max_height,
599        LayoutMaxHeight,
600        LayoutMaxHeightParseError,
601        LayoutMaxHeightParseErrorOwned
602    );
603
604    // -- Box Sizing Parser --
605
606    #[derive(Clone, PartialEq)]
607    pub enum LayoutBoxSizingParseError<'a> {
608        InvalidValue(&'a str),
609    }
610
611    impl_debug_as_display!(LayoutBoxSizingParseError<'a>);
612    impl_display! { LayoutBoxSizingParseError<'a>, {
613        InvalidValue(v) => format!("Invalid box-sizing value: \"{}\"", v),
614    }}
615
616    #[derive(Debug, Clone, PartialEq)]
617    pub enum LayoutBoxSizingParseErrorOwned {
618        InvalidValue(String),
619    }
620
621    impl<'a> LayoutBoxSizingParseError<'a> {
622        pub fn to_contained(&self) -> LayoutBoxSizingParseErrorOwned {
623            match self {
624                LayoutBoxSizingParseError::InvalidValue(s) => {
625                    LayoutBoxSizingParseErrorOwned::InvalidValue(s.to_string())
626                }
627            }
628        }
629    }
630
631    impl LayoutBoxSizingParseErrorOwned {
632        pub fn to_shared<'a>(&'a self) -> LayoutBoxSizingParseError<'a> {
633            match self {
634                LayoutBoxSizingParseErrorOwned::InvalidValue(s) => {
635                    LayoutBoxSizingParseError::InvalidValue(s)
636                }
637            }
638        }
639    }
640
641    pub fn parse_layout_box_sizing<'a>(
642        input: &'a str,
643    ) -> Result<LayoutBoxSizing, LayoutBoxSizingParseError<'a>> {
644        match input.trim() {
645            "content-box" => Ok(LayoutBoxSizing::ContentBox),
646            "border-box" => Ok(LayoutBoxSizing::BorderBox),
647            other => Err(LayoutBoxSizingParseError::InvalidValue(other)),
648        }
649    }
650}
651
652#[cfg(feature = "parser")]
653pub use self::parser::*;
654
655#[cfg(all(test, feature = "parser"))]
656mod tests {
657    use super::*;
658    use crate::props::basic::pixel::PixelValue;
659
660    #[test]
661    fn test_parse_layout_width() {
662        assert_eq!(
663            parse_layout_width("150px").unwrap(),
664            LayoutWidth::Px(PixelValue::px(150.0))
665        );
666        assert_eq!(
667            parse_layout_width("2.5em").unwrap(),
668            LayoutWidth::Px(PixelValue::em(2.5))
669        );
670        assert_eq!(
671            parse_layout_width("75%").unwrap(),
672            LayoutWidth::Px(PixelValue::percent(75.0))
673        );
674        assert_eq!(
675            parse_layout_width("0").unwrap(),
676            LayoutWidth::Px(PixelValue::px(0.0))
677        );
678        assert_eq!(
679            parse_layout_width("  100pt  ").unwrap(),
680            LayoutWidth::Px(PixelValue::pt(100.0))
681        );
682        assert_eq!(
683            parse_layout_width("min-content").unwrap(),
684            LayoutWidth::MinContent
685        );
686        assert_eq!(
687            parse_layout_width("max-content").unwrap(),
688            LayoutWidth::MaxContent
689        );
690    }
691
692    #[test]
693    fn test_parse_layout_height_invalid() {
694        // "auto" is now a valid value for height (CSS spec)
695        assert!(parse_layout_height("auto").is_ok());
696        // Liberal parsing accepts whitespace between number and unit
697        assert!(parse_layout_height("150 px").is_ok());
698        assert!(parse_layout_height("px").is_err());
699        assert!(parse_layout_height("invalid").is_err());
700    }
701
702    #[test]
703    fn test_parse_layout_box_sizing() {
704        assert_eq!(
705            parse_layout_box_sizing("content-box").unwrap(),
706            LayoutBoxSizing::ContentBox
707        );
708        assert_eq!(
709            parse_layout_box_sizing("border-box").unwrap(),
710            LayoutBoxSizing::BorderBox
711        );
712        assert_eq!(
713            parse_layout_box_sizing("  border-box  ").unwrap(),
714            LayoutBoxSizing::BorderBox
715        );
716    }
717
718    #[test]
719    fn test_parse_layout_box_sizing_invalid() {
720        assert!(parse_layout_box_sizing("padding-box").is_err());
721        assert!(parse_layout_box_sizing("borderbox").is_err());
722        assert!(parse_layout_box_sizing("").is_err());
723    }
724}