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 $borrowed<'_> {
162            #[must_use] 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            #[must_use] pub fn to_shared(&self) -> $borrowed<'_> {
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, Eq)]
193pub enum LayoutPaddingParseError<'a> {
194    PixelValueParseError(CssPixelValueParseError<'a>),
195    TooManyValues,
196    TooFewValues,
197}
198#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
199/// Owned variant of [`LayoutPaddingParseError`].
200#[cfg(feature = "parser")]
201#[derive(Debug, Clone, PartialEq, Eq)]
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")]
223/// # Errors
224///
225/// Returns an error if `input` is not a valid CSS `padding` value.
226pub fn parse_layout_padding(
227    input: &str,
228) -> Result<LayoutPadding, LayoutPaddingParseError<'_>> {
229    let values: Vec<_> = input.split_whitespace().collect();
230
231    let parsed_values: Vec<PixelValueWithAuto> = values
232        .iter()
233        .map(|s| parse_pixel_value_with_auto(s))
234        .collect::<Result<_, _>>()?;
235
236    match parsed_values.len() {
237        1 => {
238            // top, right, bottom, left
239            let all = parsed_values[0];
240            Ok(LayoutPadding {
241                top: all,
242                right: all,
243                bottom: all,
244                left: all,
245            })
246        }
247        2 => {
248            // top/bottom, left/right
249            let vertical = parsed_values[0];
250            let horizontal = parsed_values[1];
251            Ok(LayoutPadding {
252                top: vertical,
253                right: horizontal,
254                bottom: vertical,
255                left: horizontal,
256            })
257        }
258        3 => {
259            // top, left/right, bottom
260            let top = parsed_values[0];
261            let horizontal = parsed_values[1];
262            let bottom = parsed_values[2];
263            Ok(LayoutPadding {
264                top,
265                right: horizontal,
266                bottom,
267                left: horizontal,
268            })
269        }
270        4 => {
271            // top, right, bottom, left
272            Ok(LayoutPadding {
273                top: parsed_values[0],
274                right: parsed_values[1],
275                bottom: parsed_values[2],
276                left: parsed_values[3],
277            })
278        }
279        0 => Err(LayoutPaddingParseError::TooFewValues),
280        _ => Err(LayoutPaddingParseError::TooManyValues),
281    }
282}
283
284// -- Margin Shorthand Parser --
285
286/// Error from parsing a CSS `margin` shorthand value.
287#[cfg(feature = "parser")]
288#[derive(Clone, PartialEq, Eq)]
289pub enum LayoutMarginParseError<'a> {
290    PixelValueParseError(CssPixelValueParseError<'a>),
291    TooManyValues,
292    TooFewValues,
293}
294#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
295/// Owned variant of [`LayoutMarginParseError`].
296#[cfg(feature = "parser")]
297#[derive(Debug, Clone, PartialEq, Eq)]
298#[repr(C, u8)]
299pub enum LayoutMarginParseErrorOwned {
300    PixelValueParseError(CssPixelValueParseErrorOwned),
301    TooManyValues,
302    TooFewValues,
303}
304
305#[cfg(feature = "parser")]
306impl_spacing_parse_error!(LayoutMarginParseError, LayoutMarginParseErrorOwned, "margin");
307
308/// Result of parsing the CSS `margin` shorthand property (1–4 values).
309#[cfg(feature = "parser")]
310#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
311pub struct LayoutMargin {
312    pub top: PixelValueWithAuto,
313    pub bottom: PixelValueWithAuto,
314    pub left: PixelValueWithAuto,
315    pub right: PixelValueWithAuto,
316}
317
318#[cfg(feature = "parser")]
319/// # Errors
320///
321/// Returns an error if `input` is not a valid CSS `margin` value.
322pub fn parse_layout_margin(input: &str) -> Result<LayoutMargin, LayoutMarginParseError<'_>> {
323    // Margin parsing logic is identical to padding, so we can reuse the padding parser
324    // and just map the Ok and Err variants to the margin-specific types.
325    match parse_layout_padding(input) {
326        Ok(padding) => Ok(LayoutMargin {
327            top: padding.top,
328            left: padding.left,
329            right: padding.right,
330            bottom: padding.bottom,
331        }),
332        Err(e) => match e {
333            LayoutPaddingParseError::PixelValueParseError(err) => {
334                Err(LayoutMarginParseError::PixelValueParseError(err))
335            }
336            LayoutPaddingParseError::TooManyValues => Err(LayoutMarginParseError::TooManyValues),
337            LayoutPaddingParseError::TooFewValues => Err(LayoutMarginParseError::TooFewValues),
338        },
339    }
340}
341
342// -- Longhand Property Parsers --
343
344macro_rules! typed_pixel_value_parser {
345    (
346        $fn:ident, $fn_str:expr, $return:ident, $return_str:expr, $import_str:expr, $test_str:expr
347    ) => {
348        ///Parses a `
349        #[doc = $return_str]
350        ///` attribute from a `&str`
351        ///
352        ///# Example
353        ///
354        ///```rust
355        #[doc = $import_str]
356        #[doc = $test_str]
357        ///```
358        /// # Errors
359        ///
360        /// Returns an error if `input` is not a valid CSS value for this property.
361        pub fn $fn(input: &str) -> Result<$return, CssPixelValueParseError<'_>> {
362            crate::props::basic::parse_pixel_value(input).map(|e| $return { inner: e })
363        }
364
365        impl crate::props::formatter::FormatAsCssValue for $return {
366            fn format_as_css_value(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
367                self.inner.format_as_css_value(f)
368            }
369        }
370    };
371    ($fn:ident, $return:ident) => {
372        typed_pixel_value_parser!(
373            $fn,
374            stringify!($fn),
375            $return,
376            stringify!($return),
377            concat!(
378                "# extern crate azul_css;",
379                "\r\n",
380                "# use azul_css::props::layout::spacing::",
381                stringify!($fn),
382                ";",
383                "\r\n",
384                "# use azul_css::props::basic::pixel::PixelValue;\r\n",
385                "# use azul_css::props::layout::spacing::",
386                stringify!($return),
387                ";\r\n"
388            ),
389            concat!(
390                "assert_eq!(",
391                stringify!($fn),
392                "(\"5px\"), Ok(",
393                stringify!($return),
394                " { inner: PixelValue::px(5.0) }));"
395            )
396        );
397    };
398}
399
400#[cfg(feature = "parser")]
401typed_pixel_value_parser!(parse_layout_padding_top, LayoutPaddingTop);
402#[cfg(feature = "parser")]
403typed_pixel_value_parser!(parse_layout_padding_right, LayoutPaddingRight);
404#[cfg(feature = "parser")]
405typed_pixel_value_parser!(parse_layout_padding_bottom, LayoutPaddingBottom);
406#[cfg(feature = "parser")]
407typed_pixel_value_parser!(parse_layout_padding_left, LayoutPaddingLeft);
408#[cfg(feature = "parser")]
409typed_pixel_value_parser!(parse_layout_padding_inline_start, LayoutPaddingInlineStart);
410#[cfg(feature = "parser")]
411typed_pixel_value_parser!(parse_layout_padding_inline_end, LayoutPaddingInlineEnd);
412
413#[cfg(feature = "parser")]
414typed_pixel_value_parser!(parse_layout_margin_top, LayoutMarginTop);
415#[cfg(feature = "parser")]
416typed_pixel_value_parser!(parse_layout_margin_right, LayoutMarginRight);
417#[cfg(feature = "parser")]
418typed_pixel_value_parser!(parse_layout_margin_bottom, LayoutMarginBottom);
419#[cfg(feature = "parser")]
420typed_pixel_value_parser!(parse_layout_margin_left, LayoutMarginLeft);
421
422#[cfg(feature = "parser")]
423typed_pixel_value_parser!(parse_layout_column_gap, LayoutColumnGap);
424#[cfg(feature = "parser")]
425typed_pixel_value_parser!(parse_layout_row_gap, LayoutRowGap);
426
427#[cfg(all(test, feature = "parser"))]
428mod tests {
429    use super::*;
430    use crate::props::basic::pixel::{PixelValue, PixelValueWithAuto};
431
432    #[test]
433    fn test_parse_layout_padding_shorthand() {
434        // 1 value
435        let result = parse_layout_padding("10px").unwrap();
436        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
437        assert_eq!(
438            result.right,
439            PixelValueWithAuto::Exact(PixelValue::px(10.0))
440        );
441        assert_eq!(
442            result.bottom,
443            PixelValueWithAuto::Exact(PixelValue::px(10.0))
444        );
445        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
446
447        // 2 values
448        let result = parse_layout_padding("5% 2em").unwrap();
449        assert_eq!(
450            result.top,
451            PixelValueWithAuto::Exact(PixelValue::percent(5.0))
452        );
453        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
454        assert_eq!(
455            result.bottom,
456            PixelValueWithAuto::Exact(PixelValue::percent(5.0))
457        );
458        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::em(2.0)));
459
460        // 3 values
461        let result = parse_layout_padding("1px 2px 3px").unwrap();
462        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
463        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
464        assert_eq!(
465            result.bottom,
466            PixelValueWithAuto::Exact(PixelValue::px(3.0))
467        );
468        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
469
470        // 4 values
471        let result = parse_layout_padding("1px 2px 3px 4px").unwrap();
472        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
473        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
474        assert_eq!(
475            result.bottom,
476            PixelValueWithAuto::Exact(PixelValue::px(3.0))
477        );
478        assert_eq!(result.left, PixelValueWithAuto::Exact(PixelValue::px(4.0)));
479
480        // Whitespace
481        let result = parse_layout_padding("  1px   2px  ").unwrap();
482        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(1.0)));
483        assert_eq!(result.right, PixelValueWithAuto::Exact(PixelValue::px(2.0)));
484    }
485
486    #[test]
487    fn test_parse_layout_padding_errors() {
488        assert!(matches!(
489            parse_layout_padding("").err().unwrap(),
490            LayoutPaddingParseError::TooFewValues
491        ));
492        assert!(matches!(
493            parse_layout_padding("1px 2px 3px 4px 5px").err().unwrap(),
494            LayoutPaddingParseError::TooManyValues
495        ));
496        assert!(matches!(
497            parse_layout_padding("1px oops 3px").err().unwrap(),
498            LayoutPaddingParseError::PixelValueParseError(_)
499        ));
500    }
501
502    #[test]
503    fn test_parse_layout_margin_shorthand() {
504        // 1 value with auto
505        let result = parse_layout_margin("auto").unwrap();
506        assert_eq!(result.top, PixelValueWithAuto::Auto);
507        assert_eq!(result.right, PixelValueWithAuto::Auto);
508        assert_eq!(result.bottom, PixelValueWithAuto::Auto);
509        assert_eq!(result.left, PixelValueWithAuto::Auto);
510
511        // 2 values
512        let result = parse_layout_margin("10px auto").unwrap();
513        assert_eq!(result.top, PixelValueWithAuto::Exact(PixelValue::px(10.0)));
514        assert_eq!(result.right, PixelValueWithAuto::Auto);
515        assert_eq!(
516            result.bottom,
517            PixelValueWithAuto::Exact(PixelValue::px(10.0))
518        );
519        assert_eq!(result.left, PixelValueWithAuto::Auto);
520    }
521
522    #[test]
523    fn test_parse_layout_margin_errors() {
524        assert!(matches!(
525            parse_layout_margin("").err().unwrap(),
526            LayoutMarginParseError::TooFewValues
527        ));
528        assert!(matches!(
529            parse_layout_margin("1px 2px 3px 4px 5px").err().unwrap(),
530            LayoutMarginParseError::TooManyValues
531        ));
532        assert!(matches!(
533            parse_layout_margin("1px invalid").err().unwrap(),
534            LayoutMarginParseError::PixelValueParseError(_)
535        ));
536    }
537
538    #[test]
539    fn test_parse_longhand_spacing() {
540        assert_eq!(
541            parse_layout_padding_left("2em").unwrap(),
542            LayoutPaddingLeft {
543                inner: PixelValue::em(2.0)
544            }
545        );
546        assert!(parse_layout_margin_top("auto").is_err()); // Longhands don't parse "auto"
547        assert_eq!(
548            parse_layout_column_gap("20px").unwrap(),
549            LayoutColumnGap {
550                inner: PixelValue::px(20.0)
551            }
552        );
553    }
554}