Skip to main content

whisker_css/prop/
box_model.rs

1//! Box model properties: sizing, padding, margin, gap, aspect-ratio.
2//!
3//! Lynx property index:
4//! <https://lynxjs.org/api/css/properties>
5
6use crate::css::Css;
7use crate::data_type::LengthPercentage;
8use crate::keyword::BoxSizing;
9use crate::shorthand::padding_margin::MarginValue;
10use crate::value::Size;
11
12impl Css {
13    // ---------- Width / Height ----------
14
15    /// Sets `width`. Lynx default: `auto`.
16    /// <https://lynxjs.org/api/css/properties/width>
17    pub fn width(self, v: impl Into<Size>) -> Self {
18        self.push("width", v.into())
19    }
20
21    /// Sets `height`. Lynx default: `auto`.
22    /// <https://lynxjs.org/api/css/properties/height>
23    pub fn height(self, v: impl Into<Size>) -> Self {
24        self.push("height", v.into())
25    }
26
27    /// Sets `min-width`. Lynx default: `0`.
28    /// <https://lynxjs.org/api/css/properties/min-width>
29    pub fn min_width(self, v: impl Into<Size>) -> Self {
30        self.push("min-width", v.into())
31    }
32
33    /// Sets `min-height`. Lynx default: `0`.
34    /// <https://lynxjs.org/api/css/properties/min-height>
35    pub fn min_height(self, v: impl Into<Size>) -> Self {
36        self.push("min-height", v.into())
37    }
38
39    /// Sets `max-width`. Lynx default: `none`.
40    /// <https://lynxjs.org/api/css/properties/max-width>
41    pub fn max_width(self, v: impl Into<Size>) -> Self {
42        self.push("max-width", v.into())
43    }
44
45    /// Sets `max-height`. Lynx default: `none`.
46    /// <https://lynxjs.org/api/css/properties/max-height>
47    pub fn max_height(self, v: impl Into<Size>) -> Self {
48        self.push("max-height", v.into())
49    }
50
51    // ---------- box-sizing / aspect-ratio ----------
52
53    /// Sets `box-sizing`. Lynx default: `border-box`.
54    /// <https://lynxjs.org/api/css/properties/box-sizing>
55    pub fn box_sizing(self, v: BoxSizing) -> Self {
56        self.push("box-sizing", v)
57    }
58
59    /// Sets `aspect-ratio` to `<width> / <height>`.
60    /// <https://lynxjs.org/api/css/properties/aspect-ratio>
61    pub fn aspect_ratio(self, width: f32, height: f32) -> Self {
62        self.push_raw("aspect-ratio", format!("{width} / {height}"))
63    }
64
65    // ---------- Padding longhand ----------
66
67    /// Sets `padding-top`. Negative values are clamped to zero by Lynx.
68    /// <https://lynxjs.org/api/css/properties/padding-top>
69    pub fn padding_top(self, v: impl Into<LengthPercentage>) -> Self {
70        self.push("padding-top", v.into())
71    }
72
73    /// Sets `padding-right`. Negative values are clamped to zero by Lynx.
74    /// <https://lynxjs.org/api/css/properties/padding-right>
75    pub fn padding_right(self, v: impl Into<LengthPercentage>) -> Self {
76        self.push("padding-right", v.into())
77    }
78
79    /// Sets `padding-bottom`. Negative values are clamped to zero by Lynx.
80    /// <https://lynxjs.org/api/css/properties/padding-bottom>
81    pub fn padding_bottom(self, v: impl Into<LengthPercentage>) -> Self {
82        self.push("padding-bottom", v.into())
83    }
84
85    /// Sets `padding-left`. Negative values are clamped to zero by Lynx.
86    /// <https://lynxjs.org/api/css/properties/padding-left>
87    pub fn padding_left(self, v: impl Into<LengthPercentage>) -> Self {
88        self.push("padding-left", v.into())
89    }
90
91    // ---------- Margin longhand ----------
92
93    /// Sets `margin-top`. Lynx allows negative values and `auto`.
94    /// <https://lynxjs.org/api/css/properties/margin-top>
95    pub fn margin_top(self, v: impl Into<MarginValue>) -> Self {
96        self.push("margin-top", v.into())
97    }
98
99    /// Sets `margin-right`. Lynx allows negative values and `auto`.
100    /// <https://lynxjs.org/api/css/properties/margin-right>
101    pub fn margin_right(self, v: impl Into<MarginValue>) -> Self {
102        self.push("margin-right", v.into())
103    }
104
105    /// Sets `margin-bottom`. Lynx allows negative values and `auto`.
106    /// <https://lynxjs.org/api/css/properties/margin-bottom>
107    pub fn margin_bottom(self, v: impl Into<MarginValue>) -> Self {
108        self.push("margin-bottom", v.into())
109    }
110
111    /// Sets `margin-left`. Lynx allows negative values and `auto`.
112    /// <https://lynxjs.org/api/css/properties/margin-left>
113    pub fn margin_left(self, v: impl Into<MarginValue>) -> Self {
114        self.push("margin-left", v.into())
115    }
116
117    // ---------- Gap ----------
118
119    /// Sets `gap` — shorthand for `row-gap` and `column-gap`.
120    /// <https://lynxjs.org/api/css/properties/gap>
121    pub fn gap(self, v: impl Into<LengthPercentage>) -> Self {
122        let v = v.into();
123        self.push("row-gap", v.clone()).push("column-gap", v)
124    }
125
126    /// Sets `row-gap` — inline gap between rows in flex/grid layouts.
127    /// <https://lynxjs.org/api/css/properties/row-gap>
128    pub fn row_gap(self, v: impl Into<LengthPercentage>) -> Self {
129        self.push("row-gap", v.into())
130    }
131
132    /// Sets `column-gap` — gap between columns in flex/grid layouts.
133    /// <https://lynxjs.org/api/css/properties/column-gap>
134    pub fn column_gap(self, v: impl Into<LengthPercentage>) -> Self {
135        self.push("column-gap", v.into())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use crate::data_type::{FitContent, MaxContent};
142    use crate::ext::*;
143    use crate::keyword::BoxSizing;
144    use crate::value::Size;
145    use crate::Css;
146
147    #[test]
148    fn width_height_basic() {
149        let s = Css::new().width(px(100)).height(50.percent());
150        assert_eq!(s.to_string(), "width: 100px; height: 50%;");
151    }
152
153    #[test]
154    fn min_max_dimensions() {
155        let s = Css::new()
156            .min_width(px(50))
157            .min_height(px(50))
158            .max_width(percent(80))
159            .max_height(Size::None);
160        assert_eq!(
161            s.to_string(),
162            "min-width: 50px; min-height: 50px; max-width: 80%; max-height: none;"
163        );
164    }
165
166    #[test]
167    fn intrinsic_sizing_keywords() {
168        let s = Css::new()
169            .width(Size::Auto)
170            .height(MaxContent)
171            .min_width(Size::MinContent)
172            .max_width(FitContent::keyword());
173        assert_eq!(
174            s.to_string(),
175            "width: auto; height: max-content; min-width: min-content; max-width: fit-content;"
176        );
177    }
178
179    #[test]
180    fn box_sizing_keyword() {
181        let s = Css::new().box_sizing(BoxSizing::BorderBox);
182        assert_eq!(s.to_string(), "box-sizing: border-box;");
183    }
184
185    #[test]
186    fn aspect_ratio_pair() {
187        let s = Css::new().aspect_ratio(16.0, 9.0);
188        assert_eq!(s.to_string(), "aspect-ratio: 16 / 9;");
189    }
190
191    #[test]
192    fn padding_longhands() {
193        let s = Css::new()
194            .padding_top(px(2))
195            .padding_right(px(4))
196            .padding_bottom(px(6))
197            .padding_left(px(8));
198        assert_eq!(
199            s.to_string(),
200            "padding-top: 2px; padding-right: 4px; padding-bottom: 6px; padding-left: 8px;"
201        );
202    }
203
204    #[test]
205    fn margin_longhands_allow_negatives() {
206        let s = Css::new()
207            .margin_top(px(-4))
208            .margin_right(0.percent())
209            .margin_bottom(px(8))
210            .margin_left(percent(-50.0));
211        assert_eq!(
212            s.to_string(),
213            "margin-top: -4px; margin-right: 0%; margin-bottom: 8px; margin-left: -50%;"
214        );
215    }
216
217    #[test]
218    fn gap_expands_to_row_and_column() {
219        let s = Css::new().gap(px(8));
220        assert_eq!(s.to_string(), "row-gap: 8px; column-gap: 8px;");
221    }
222
223    #[test]
224    fn row_and_column_gap_individual() {
225        let s = Css::new().row_gap(px(4)).column_gap(px(12));
226        assert_eq!(s.to_string(), "row-gap: 4px; column-gap: 12px;");
227    }
228
229    #[test]
230    fn padding_top_override_via_last_write_wins() {
231        let s = Css::new().padding_top(px(8)).padding_top(px(0));
232        assert_eq!(s.to_string(), "padding-top: 0px;");
233    }
234}