Skip to main content

whisker_css/shorthand/
padding_margin.rs

1//! `padding` / `margin` shorthand support.
2//!
3//! Both shorthands take 1–4 length-percentages and expand to the
4//! four per-side longhands. `margin` additionally accepts `auto` for
5//! centering.
6
7use crate::css::Css;
8use crate::data_type::LengthPercentage;
9
10/// Argument to [`Css::padding`]. Built via `impl From` for common
11/// shapes:
12///
13/// | Source                                  | CSS shorthand |
14/// |-----------------------------------------|---------------|
15/// | `Length` / `Percentage` / `LengthPercentage` | `<v>`         |
16/// | `(t_b, l_r)`                            | `<t_b> <l_r>` |
17/// | `(t, r_l, b)`                           | `<t> <r_l> <b>` |
18/// | `(t, r, b, l)`                          | `<t> <r> <b> <l>` |
19#[derive(Clone, Debug, PartialEq)]
20pub struct Padding {
21    /// Top, right, bottom, left — already expanded to all four sides.
22    pub trbl: [LengthPercentage; 4],
23}
24
25impl<T: Into<LengthPercentage>> From<T> for Padding {
26    fn from(v: T) -> Self {
27        let v = v.into();
28        Self {
29            trbl: [v.clone(), v.clone(), v.clone(), v],
30        }
31    }
32}
33
34// Two-value form: `vertical, horizontal`.
35impl<A, B> From<(A, B)> for Padding
36where
37    A: Into<LengthPercentage>,
38    B: Into<LengthPercentage>,
39{
40    fn from((y, x): (A, B)) -> Self {
41        let y = y.into();
42        let x = x.into();
43        Self {
44            trbl: [y.clone(), x.clone(), y, x],
45        }
46    }
47}
48
49// Three-value form: `top, horizontal, bottom`.
50impl<A, B, C> From<(A, B, C)> for Padding
51where
52    A: Into<LengthPercentage>,
53    B: Into<LengthPercentage>,
54    C: Into<LengthPercentage>,
55{
56    fn from((t, x, b): (A, B, C)) -> Self {
57        let x = x.into();
58        Self {
59            trbl: [t.into(), x.clone(), b.into(), x],
60        }
61    }
62}
63
64// Four-value form: `top, right, bottom, left`.
65impl<A, B, C, D> From<(A, B, C, D)> for Padding
66where
67    A: Into<LengthPercentage>,
68    B: Into<LengthPercentage>,
69    C: Into<LengthPercentage>,
70    D: Into<LengthPercentage>,
71{
72    fn from((t, r, b, l): (A, B, C, D)) -> Self {
73        Self {
74            trbl: [t.into(), r.into(), b.into(), l.into()],
75        }
76    }
77}
78
79/// Per-side value for `margin`. Negative lengths and `auto` are
80/// allowed (unlike padding).
81#[derive(Clone, Debug, PartialEq)]
82pub enum MarginValue {
83    /// An explicit length or percentage. Negative values are valid.
84    LengthPercentage(LengthPercentage),
85    /// `auto` — distribute remaining space (used for centering).
86    Auto,
87}
88
89impl crate::to_css::ToCss for MarginValue {
90    fn to_css(&self, dest: &mut dyn core::fmt::Write) -> core::fmt::Result {
91        match self {
92            MarginValue::LengthPercentage(lp) => lp.to_css(dest),
93            MarginValue::Auto => dest.write_str("auto"),
94        }
95    }
96}
97
98impl From<LengthPercentage> for MarginValue {
99    fn from(v: LengthPercentage) -> Self {
100        Self::LengthPercentage(v)
101    }
102}
103
104impl From<crate::data_type::Length> for MarginValue {
105    fn from(v: crate::data_type::Length) -> Self {
106        Self::LengthPercentage(v.into())
107    }
108}
109
110impl From<crate::data_type::Percentage> for MarginValue {
111    fn from(v: crate::data_type::Percentage) -> Self {
112        Self::LengthPercentage(v.into())
113    }
114}
115
116/// Argument to [`Css::margin`]. Same shape as [`Padding`] but
117/// values may be `MarginValue::Auto`.
118#[derive(Clone, Debug, PartialEq)]
119pub struct Margin {
120    /// Top, right, bottom, left — already expanded to all four sides.
121    pub trbl: [MarginValue; 4],
122}
123
124impl<T: Into<MarginValue>> From<T> for Margin {
125    fn from(v: T) -> Self {
126        let v = v.into();
127        Self {
128            trbl: [v.clone(), v.clone(), v.clone(), v],
129        }
130    }
131}
132
133impl<A, B> From<(A, B)> for Margin
134where
135    A: Into<MarginValue>,
136    B: Into<MarginValue>,
137{
138    fn from((y, x): (A, B)) -> Self {
139        let y = y.into();
140        let x = x.into();
141        Self {
142            trbl: [y.clone(), x.clone(), y, x],
143        }
144    }
145}
146
147impl<A, B, C> From<(A, B, C)> for Margin
148where
149    A: Into<MarginValue>,
150    B: Into<MarginValue>,
151    C: Into<MarginValue>,
152{
153    fn from((t, x, b): (A, B, C)) -> Self {
154        let x = x.into();
155        Self {
156            trbl: [t.into(), x.clone(), b.into(), x],
157        }
158    }
159}
160
161impl<A, B, C, D> From<(A, B, C, D)> for Margin
162where
163    A: Into<MarginValue>,
164    B: Into<MarginValue>,
165    C: Into<MarginValue>,
166    D: Into<MarginValue>,
167{
168    fn from((t, r, b, l): (A, B, C, D)) -> Self {
169        Self {
170            trbl: [t.into(), r.into(), b.into(), l.into()],
171        }
172    }
173}
174
175impl Css {
176    /// Sets `padding` shorthand. Expands to the four per-side
177    /// longhands so later overrides win cleanly. The argument
178    /// accepts a single length, a `(y, x)` tuple, a `(t, x, b)`
179    /// tuple, or a `(t, r, b, l)` tuple.
180    /// <https://lynxjs.org/api/css/properties/padding>
181    pub fn padding(self, v: impl Into<Padding>) -> Self {
182        let Padding { trbl: [t, r, b, l] } = v.into();
183        self.padding_top(t)
184            .padding_right(r)
185            .padding_bottom(b)
186            .padding_left(l)
187    }
188
189    /// Sets `margin` shorthand. Same shape rules as
190    /// [`Css::padding`], with `auto` allowed per side.
191    /// <https://lynxjs.org/api/css/properties/margin>
192    pub fn margin(self, v: impl Into<Margin>) -> Self {
193        let Margin { trbl: [t, r, b, l] } = v.into();
194        self.push("margin-top", t)
195            .push("margin-right", r)
196            .push("margin-bottom", b)
197            .push("margin-left", l)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use crate::ext::*;
204    use crate::shorthand::padding_margin::MarginValue;
205    use crate::Css;
206
207    #[test]
208    fn padding_single_value_expands_to_four() {
209        let s = Css::new().padding(px(8));
210        assert_eq!(
211            s.to_string(),
212            "padding-top: 8px; padding-right: 8px; padding-bottom: 8px; padding-left: 8px;"
213        );
214    }
215
216    #[test]
217    fn padding_two_value_y_x() {
218        let s = Css::new().padding((px(8), px(16)));
219        assert_eq!(
220            s.to_string(),
221            "padding-top: 8px; padding-right: 16px; padding-bottom: 8px; padding-left: 16px;"
222        );
223    }
224
225    #[test]
226    fn padding_three_value_t_x_b() {
227        let s = Css::new().padding((px(8), px(16), px(4)));
228        assert_eq!(
229            s.to_string(),
230            "padding-top: 8px; padding-right: 16px; padding-bottom: 4px; padding-left: 16px;"
231        );
232    }
233
234    #[test]
235    fn padding_four_value_trbl() {
236        let s = Css::new().padding((px(2), px(4), px(6), px(8)));
237        assert_eq!(
238            s.to_string(),
239            "padding-top: 2px; padding-right: 4px; padding-bottom: 6px; padding-left: 8px;"
240        );
241    }
242
243    #[test]
244    fn padding_then_single_side_override() {
245        let s = Css::new().padding(px(8)).padding_top(px(0));
246        assert_eq!(
247            s.to_string(),
248            "padding-right: 8px; padding-bottom: 8px; padding-left: 8px; padding-top: 0px;"
249        );
250    }
251
252    #[test]
253    fn padding_percentages() {
254        let s = Css::new().padding(50.percent());
255        assert_eq!(
256            s.to_string(),
257            "padding-top: 50%; padding-right: 50%; padding-bottom: 50%; padding-left: 50%;"
258        );
259    }
260
261    #[test]
262    fn margin_single_value() {
263        let s = Css::new().margin(px(8));
264        assert_eq!(
265            s.to_string(),
266            "margin-top: 8px; margin-right: 8px; margin-bottom: 8px; margin-left: 8px;"
267        );
268    }
269
270    #[test]
271    fn margin_auto_centers() {
272        let s = Css::new().margin((px(0), MarginValue::Auto));
273        assert_eq!(
274            s.to_string(),
275            "margin-top: 0px; margin-right: auto; margin-bottom: 0px; margin-left: auto;"
276        );
277    }
278
279    #[test]
280    fn margin_four_value_with_negative() {
281        let s = Css::new().margin((px(-4), px(0), px(4), MarginValue::Auto));
282        assert_eq!(
283            s.to_string(),
284            "margin-top: -4px; margin-right: 0px; margin-bottom: 4px; margin-left: auto;"
285        );
286    }
287
288    #[test]
289    fn margin_three_value_t_x_b() {
290        let s = Css::new().margin((px(2), px(4), px(6)));
291        assert_eq!(
292            s.to_string(),
293            "margin-top: 2px; margin-right: 4px; margin-bottom: 6px; margin-left: 4px;"
294        );
295    }
296
297    #[test]
298    fn margin_value_from_length_percentage() {
299        let v: MarginValue = px(4).into();
300        let v2: MarginValue = 25.percent().into();
301        let v3: MarginValue =
302            crate::data_type::LengthPercentage::Length(crate::data_type::Length::Px(8.0)).into();
303        assert!(matches!(v, MarginValue::LengthPercentage(_)));
304        assert!(matches!(v2, MarginValue::LengthPercentage(_)));
305        assert!(matches!(v3, MarginValue::LengthPercentage(_)));
306    }
307}