Skip to main content

azul_css/props/layout/
spacing.rs

1//! CSS properties for `margin`, `padding`, and `gap` (column-gap / row-gap).
2//!
3//! Shorthand parsers (`parse_layout_padding`, `parse_layout_margin`) and
4//! longhand per-side parsers are gated behind `#[cfg(feature = "parser")]`.
5
6use alloc::{
7    string::{String, ToString},
8    vec::Vec,
9};
10
11#[cfg(feature = "parser")]
12use crate::props::basic::pixel::{parse_pixel_value_with_auto, PixelValueWithAuto};
13use crate::{
14    css::PrintAsCssValue,
15    props::{
16        basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
17        macros::PixelValueTaker,
18    },
19};
20
21// --- TYPE DEFINITIONS ---
22
23// Spacing properties - wrapper structs around PixelValue for type safety
24
25macro_rules! impl_spacing_type_impls {
26    ($name:ident) => {
27        impl ::core::fmt::Debug for $name {
28            fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
29                write!(f, "{}", self.inner)
30            }
31        }
32
33        impl PixelValueTaker for $name {
34            fn from_pixel_value(inner: PixelValue) -> Self {
35                Self { inner }
36            }
37        }
38
39        impl_pixel_value!($name);
40    };
41}
42
43/// Layout padding top value
44#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
45#[repr(C)]
46pub struct LayoutPaddingTop {
47    pub inner: PixelValue,
48}
49impl_spacing_type_impls!(LayoutPaddingTop);
50
51/// Layout padding right value
52#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[repr(C)]
54pub struct LayoutPaddingRight {
55    pub inner: PixelValue,
56}
57impl_spacing_type_impls!(LayoutPaddingRight);
58
59/// Layout padding bottom value
60#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
61#[repr(C)]
62pub struct LayoutPaddingBottom {
63    pub inner: PixelValue,
64}
65impl_spacing_type_impls!(LayoutPaddingBottom);
66
67/// Layout padding left value
68#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69#[repr(C)]
70pub struct LayoutPaddingLeft {
71    pub inner: PixelValue,
72}
73impl_spacing_type_impls!(LayoutPaddingLeft);
74
75/// Layout padding inline start value (for RTL/LTR support)
76#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
77#[repr(C)]
78pub struct LayoutPaddingInlineStart {
79    pub inner: PixelValue,
80}
81impl_spacing_type_impls!(LayoutPaddingInlineStart);
82
83/// Layout padding inline end value (for RTL/LTR support)
84#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
85#[repr(C)]
86pub struct LayoutPaddingInlineEnd {
87    pub inner: PixelValue,
88}
89impl_spacing_type_impls!(LayoutPaddingInlineEnd);
90
91/// Layout margin top value
92#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
93#[repr(C)]
94pub struct LayoutMarginTop {
95    pub inner: PixelValue,
96}
97impl_spacing_type_impls!(LayoutMarginTop);
98
99/// Layout margin right value
100#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
101#[repr(C)]
102pub struct LayoutMarginRight {
103    pub inner: PixelValue,
104}
105impl_spacing_type_impls!(LayoutMarginRight);
106
107/// Layout margin bottom value
108#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
109#[repr(C)]
110pub struct LayoutMarginBottom {
111    pub inner: PixelValue,
112}
113impl_spacing_type_impls!(LayoutMarginBottom);
114
115/// Layout margin left value
116#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
117#[repr(C)]
118pub struct LayoutMarginLeft {
119    pub inner: PixelValue,
120}
121impl_spacing_type_impls!(LayoutMarginLeft);
122
123/// Layout column gap value (for flexbox/grid)
124#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[repr(C)]
126pub struct LayoutColumnGap {
127    pub inner: PixelValue,
128}
129impl_spacing_type_impls!(LayoutColumnGap);
130
131/// Layout row gap value (for flexbox/grid)
132#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133#[repr(C)]
134pub struct LayoutRowGap {
135    pub inner: PixelValue,
136}
137impl_spacing_type_impls!(LayoutRowGap);
138
139// --- PARSERS ---
140
141#[cfg(feature = "parser")]
142macro_rules! impl_spacing_parse_error {
143    ($borrowed:ident, $owned:ident, $property_name:expr) => {
144        #[cfg(feature = "parser")]
145        impl_debug_as_display!($borrowed<'a>);
146
147        #[cfg(feature = "parser")]
148        impl_display! { $borrowed<'a>, {
149            PixelValueParseError(e) => format!("Could not parse pixel value: {}", e),
150            TooManyValues => concat!("Too many values: ", $property_name, " property accepts at most 4 values."),
151            TooFewValues => concat!("Too few values: ", $property_name, " property requires at least 1 value."),
152        }}
153
154        #[cfg(feature = "parser")]
155        impl_from!(
156            CssPixelValueParseError<'a>,
157            $borrowed::PixelValueParseError
158        );
159
160        #[cfg(feature = "parser")]
161        impl<'a> $borrowed<'a> {
162            pub fn to_contained(&self) -> $owned {
163                match self {
164                    $borrowed::PixelValueParseError(e) => {
165                        $owned::PixelValueParseError(e.to_contained())
166                    }
167                    $borrowed::TooManyValues => $owned::TooManyValues,
168                    $borrowed::TooFewValues => $owned::TooFewValues,
169                }
170            }
171        }
172
173        #[cfg(feature = "parser")]
174        impl $owned {
175            pub fn to_shared<'a>(&'a self) -> $borrowed<'a> {
176                match self {
177                    $owned::PixelValueParseError(e) => {
178                        $borrowed::PixelValueParseError(e.to_shared())
179                    }
180                    $owned::TooManyValues => $borrowed::TooManyValues,
181                    $owned::TooFewValues => $borrowed::TooFewValues,
182                }
183            }
184        }
185    };
186}
187
188// -- Padding Shorthand Parser --
189
190/// Error from parsing a CSS `padding` shorthand value.
191#[cfg(feature = "parser")]
192#[derive(Clone, PartialEq)]
193pub enum LayoutPaddingParseError<'a> {
194    PixelValueParseError(CssPixelValueParseError<'a>),
195    TooManyValues,
196    TooFewValues,
197}
198
199/// Owned variant of [`LayoutPaddingParseError`].
200#[cfg(feature = "parser")]
201#[derive(Debug, Clone, PartialEq)]
202#[repr(C, u8)]
203pub enum LayoutPaddingParseErrorOwned {
204    PixelValueParseError(CssPixelValueParseErrorOwned),
205    TooManyValues,
206    TooFewValues,
207}
208
209#[cfg(feature = "parser")]
210impl_spacing_parse_error!(LayoutPaddingParseError, LayoutPaddingParseErrorOwned, "padding");
211
212/// Result of parsing the CSS `padding` shorthand property (1–4 values).
213#[cfg(feature = "parser")]
214#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
215pub struct LayoutPadding {
216    pub top: PixelValueWithAuto,
217    pub bottom: PixelValueWithAuto,
218    pub left: PixelValueWithAuto,
219    pub right: PixelValueWithAuto,
220}
221
222#[cfg(feature = "parser")]
223pub fn parse_layout_padding<'a>(
224    input: &'a str,
225) -> Result<LayoutPadding, LayoutPaddingParseError<'a>> {
226    let values: Vec<_> = input.split_whitespace().collect();
227
228    let parsed_values: Vec<PixelValueWithAuto> = values
229        .iter()
230        .map(|s| parse_pixel_value_with_auto(s))
231        .collect::<Result<_, _>>()?;
232
233    match parsed_values.len() {
234        1 => {
235            // top, right, bottom, left
236            let all = parsed_values[0];
237            Ok(LayoutPadding {
238                top: all,
239                right: all,
240                bottom: all,
241                left: all,
242            })
243        }
244        2 => {
245            // top/bottom, left/right
246            let vertical = parsed_values[0];
247            let horizontal = parsed_values[1];
248            Ok(LayoutPadding {
249                top: vertical,
250                right: horizontal,
251                bottom: vertical,
252                left: horizontal,
253            })
254        }
255        3 => {
256            // top, left/right, bottom
257            let top = parsed_values[0];
258            let horizontal = parsed_values[1];
259            let bottom = parsed_values[2];
260            Ok(LayoutPadding {
261                top,
262                right: horizontal,
263                bottom,
264                left: horizontal,
265            })
266        }
267        4 => {
268            // top, right, bottom, left
269            Ok(LayoutPadding {
270                top: parsed_values[0],
271                right: parsed_values[1],
272                bottom: parsed_values[2],
273                left: parsed_values[3],
274            })
275        }
276        0 => Err(LayoutPaddingParseError::TooFewValues),
277        _ => Err(LayoutPaddingParseError::TooManyValues),
278    }
279}
280
281// -- Margin Shorthand Parser --
282
283/// Error from parsing a CSS `margin` shorthand value.
284#[cfg(feature = "parser")]
285#[derive(Clone, PartialEq)]
286pub enum LayoutMarginParseError<'a> {
287    PixelValueParseError(CssPixelValueParseError<'a>),
288    TooManyValues,
289    TooFewValues,
290}
291
292/// Owned variant of [`LayoutMarginParseError`].
293#[cfg(feature = "parser")]
294#[derive(Debug, Clone, PartialEq)]
295#[repr(C, u8)]
296pub enum LayoutMarginParseErrorOwned {
297    PixelValueParseError(CssPixelValueParseErrorOwned),
298    TooManyValues,
299    TooFewValues,
300}
301
302#[cfg(feature = "parser")]
303impl_spacing_parse_error!(LayoutMarginParseError, LayoutMarginParseErrorOwned, "margin");
304
305/// Result of parsing the CSS `margin` shorthand property (1–4 values).
306#[cfg(feature = "parser")]
307#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
308pub struct LayoutMargin {
309    pub top: PixelValueWithAuto,
310    pub bottom: PixelValueWithAuto,
311    pub left: PixelValueWithAuto,
312    pub right: PixelValueWithAuto,
313}
314
315#[cfg(feature = "parser")]
316pub fn parse_layout_margin<'a>(input: &'a str) -> Result<LayoutMargin, LayoutMarginParseError<'a>> {
317    // Margin parsing logic is identical to padding, so we can reuse the padding parser
318    // and just map the Ok and Err variants to the margin-specific types.
319    match parse_layout_padding(input) {
320        Ok(padding) => Ok(LayoutMargin {
321            top: padding.top,
322            left: padding.left,
323            right: padding.right,
324            bottom: padding.bottom,
325        }),
326        Err(e) => match e {
327            LayoutPaddingParseError::PixelValueParseError(err) => {
328                Err(LayoutMarginParseError::PixelValueParseError(err))
329            }
330            LayoutPaddingParseError::TooManyValues => Err(LayoutMarginParseError::TooManyValues),
331            LayoutPaddingParseError::TooFewValues => Err(LayoutMarginParseError::TooFewValues),
332        },
333    }
334}
335
336// -- Longhand Property Parsers --
337
338macro_rules! typed_pixel_value_parser {
339    (
340        $fn:ident, $fn_str:expr, $return:ident, $return_str:expr, $import_str:expr, $test_str:expr
341    ) => {
342        ///Parses a `
343        #[doc = $return_str]
344        ///` attribute from a `&str`
345        ///
346        ///# Example
347        ///
348        ///```rust
349        #[doc = $import_str]
350        #[doc = $test_str]
351        ///```
352        pub fn $fn<'a>(input: &'a str) -> Result<$return, CssPixelValueParseError<'a>> {
353            crate::props::basic::parse_pixel_value(input).map(|e| $return { inner: e })
354        }
355
356        impl crate::props::formatter::FormatAsCssValue for $return {
357            fn format_as_css_value(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
358                self.inner.format_as_css_value(f)
359            }
360        }
361    };
362    ($fn:ident, $return:ident) => {
363        typed_pixel_value_parser!(
364            $fn,
365            stringify!($fn),
366            $return,
367            stringify!($return),
368            concat!(
369                "# extern crate azul_css;",
370                "\r\n",
371                "# use azul_css::props::layout::spacing::",
372                stringify!($fn),
373                ";",
374                "\r\n",
375                "# use azul_css::props::basic::pixel::PixelValue;\r\n",
376                "# use azul_css::props::layout::spacing::",
377                stringify!($return),
378                ";\r\n"
379            ),
380            concat!(
381                "assert_eq!(",
382                stringify!($fn),
383                "(\"5px\"), Ok(",
384                stringify!($return),
385                " { inner: PixelValue::px(5.0) }));"
386            )
387        );
388    };
389}
390
391#[cfg(feature = "parser")]
392typed_pixel_value_parser!(parse_layout_padding_top, LayoutPaddingTop);
393#[cfg(feature = "parser")]
394typed_pixel_value_parser!(parse_layout_padding_right, LayoutPaddingRight);
395#[cfg(feature = "parser")]
396typed_pixel_value_parser!(parse_layout_padding_bottom, LayoutPaddingBottom);
397#[cfg(feature = "parser")]
398typed_pixel_value_parser!(parse_layout_padding_left, LayoutPaddingLeft);
399#[cfg(feature = "parser")]
400typed_pixel_value_parser!(parse_layout_padding_inline_start, LayoutPaddingInlineStart);
401#[cfg(feature = "parser")]
402typed_pixel_value_parser!(parse_layout_padding_inline_end, LayoutPaddingInlineEnd);
403
404#[cfg(feature = "parser")]
405typed_pixel_value_parser!(parse_layout_margin_top, LayoutMarginTop);
406#[cfg(feature = "parser")]
407typed_pixel_value_parser!(parse_layout_margin_right, LayoutMarginRight);
408#[cfg(feature = "parser")]
409typed_pixel_value_parser!(parse_layout_margin_bottom, LayoutMarginBottom);
410#[cfg(feature = "parser")]
411typed_pixel_value_parser!(parse_layout_margin_left, LayoutMarginLeft);
412
413#[cfg(feature = "parser")]
414typed_pixel_value_parser!(parse_layout_column_gap, LayoutColumnGap);
415#[cfg(feature = "parser")]
416typed_pixel_value_parser!(parse_layout_row_gap, LayoutRowGap);
417
418#[cfg(all(test, feature = "parser"))]
419mod tests {
420    use super::*;
421    use crate::props::basic::pixel::{PixelValue, PixelValueWithAuto};
422
423    #[test]
424    fn test_parse_layout_padding_shorthand() {
425        // 1 value
426        let result = parse_layout_padding("10px").unwrap();
427        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
428        assert_eq!(
429            result.right,
430            PixelValueWithAuto::Exact(PixelValue::px(10.0))
431        );
432        assert_eq!(
433            result.bottom,
434            PixelValueWithAuto::Exact(PixelValue::px(10.0))
435        );
436        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
437
438        // 2 values
439        let result = parse_layout_padding("5% 2em").unwrap();
440        assert_eq!(
441            result.top,
442            PixelValueWithAuto::Exact(PixelValue::percent(5.0))
443        );
444        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
445        assert_eq!(
446            result.bottom,
447            PixelValueWithAuto::Exact(PixelValue::percent(5.0))
448        );
449        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
450
451        // 3 values
452        let result = parse_layout_padding("1px 2px 3px").unwrap();
453        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
454        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
455        assert_eq!(
456            result.bottom,
457            PixelValueWithAuto::Exact(PixelValue::px(3.0))
458        );
459        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
460
461        // 4 values
462        let result = parse_layout_padding("1px 2px 3px 4px").unwrap();
463        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
464        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
465        assert_eq!(
466            result.bottom,
467            PixelValueWithAuto::Exact(PixelValue::px(3.0))
468        );
469        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(4.0)));
470
471        // Whitespace
472        let result = parse_layout_padding("  1px   2px  ").unwrap();
473        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
474        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
475    }
476
477    #[test]
478    fn test_parse_layout_padding_errors() {
479        assert!(matches!(
480            parse_layout_padding("").err().unwrap(),
481            LayoutPaddingParseError::TooFewValues
482        ));
483        assert!(matches!(
484            parse_layout_padding("1px 2px 3px 4px 5px").err().unwrap(),
485            LayoutPaddingParseError::TooManyValues
486        ));
487        assert!(matches!(
488            parse_layout_padding("1px oops 3px").err().unwrap(),
489            LayoutPaddingParseError::PixelValueParseError(_)
490        ));
491    }
492
493    #[test]
494    fn test_parse_layout_margin_shorthand() {
495        // 1 value with auto
496        let result = parse_layout_margin("auto").unwrap();
497        assert_eq!(result.top, PixelValueWithAuto::Auto);
498        assert_eq!(result.right, PixelValueWithAuto::Auto);
499        assert_eq!(result.bottom, PixelValueWithAuto::Auto);
500        assert_eq!(result.left, PixelValueWithAuto::Auto);
501
502        // 2 values
503        let result = parse_layout_margin("10px auto").unwrap();
504        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
505        assert_eq!(result.right, PixelValueWithAuto::Auto);
506        assert_eq!(
507            result.bottom,
508            PixelValueWithAuto::Exact(PixelValue::px(10.0))
509        );
510        assert_eq!(result.left, PixelValueWithAuto::Auto);
511    }
512
513    #[test]
514    fn test_parse_layout_margin_errors() {
515        assert!(matches!(
516            parse_layout_margin("").err().unwrap(),
517            LayoutMarginParseError::TooFewValues
518        ));
519        assert!(matches!(
520            parse_layout_margin("1px 2px 3px 4px 5px").err().unwrap(),
521            LayoutMarginParseError::TooManyValues
522        ));
523        assert!(matches!(
524            parse_layout_margin("1px invalid").err().unwrap(),
525            LayoutMarginParseError::PixelValueParseError(_)
526        ));
527    }
528
529    #[test]
530    fn test_parse_longhand_spacing() {
531        assert_eq!(
532            parse_layout_padding_left("2em").unwrap(),
533            LayoutPaddingLeft {
534                inner: PixelValue::em(2.0)
535            }
536        );
537        assert!(parse_layout_margin_top("auto").is_err()); // Longhands don't parse "auto"
538        assert_eq!(
539            parse_layout_column_gap("20px").unwrap(),
540            LayoutColumnGap {
541                inner: PixelValue::px(20.0)
542            }
543        );
544    }
545}