Skip to main content

cssbox_core/
box_model.rs

1//! Box model resolution: margin, border, padding computation.
2
3use crate::geometry::Edges;
4use crate::style::ComputedStyle;
5
6/// Resolved box model dimensions for a layout box.
7#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub struct BoxModel {
9    pub margin: Edges,
10    pub border: Edges,
11    pub padding: Edges,
12}
13
14impl BoxModel {
15    /// Compute the border edges from a computed style.
16    pub fn resolve_border(style: &ComputedStyle) -> Edges {
17        Edges::new(
18            style.border_top_width,
19            style.border_right_width,
20            style.border_bottom_width,
21            style.border_left_width,
22        )
23    }
24
25    /// Compute padding edges, resolving percentages against containing block width.
26    pub fn resolve_padding(style: &ComputedStyle, containing_block_width: f32) -> Edges {
27        // CSS 2.1 §8.4: padding percentages resolve against containing block width,
28        // even for vertical padding.
29        Edges::new(
30            style.padding_top.resolve(containing_block_width),
31            style.padding_right.resolve(containing_block_width),
32            style.padding_bottom.resolve(containing_block_width),
33            style.padding_left.resolve(containing_block_width),
34        )
35    }
36
37    /// Compute margin edges, resolving percentages against containing block width.
38    /// Auto margins return 0.0 here — auto margin resolution happens during layout.
39    pub fn resolve_margin(style: &ComputedStyle, containing_block_width: f32) -> Edges {
40        // CSS 2.1 §8.3: margin percentages resolve against containing block width,
41        // even for vertical margins.
42        Edges::new(
43            style
44                .margin_top
45                .resolve(containing_block_width)
46                .unwrap_or(0.0),
47            style
48                .margin_right
49                .resolve(containing_block_width)
50                .unwrap_or(0.0),
51            style
52                .margin_bottom
53                .resolve(containing_block_width)
54                .unwrap_or(0.0),
55            style
56                .margin_left
57                .resolve(containing_block_width)
58                .unwrap_or(0.0),
59        )
60    }
61
62    /// Resolve all box model dimensions.
63    pub fn resolve(style: &ComputedStyle, containing_block_width: f32) -> Self {
64        Self {
65            margin: Self::resolve_margin(style, containing_block_width),
66            border: Self::resolve_border(style),
67            padding: Self::resolve_padding(style, containing_block_width),
68        }
69    }
70
71    /// Total horizontal space consumed by margin + border + padding.
72    pub fn horizontal_total(&self) -> f32 {
73        self.margin.horizontal() + self.border.horizontal() + self.padding.horizontal()
74    }
75
76    /// Total vertical space consumed by margin + border + padding.
77    pub fn vertical_total(&self) -> f32 {
78        self.margin.vertical() + self.border.vertical() + self.padding.vertical()
79    }
80
81    /// Horizontal space consumed by border + padding (no margin).
82    pub fn horizontal_border_padding(&self) -> f32 {
83        self.border.horizontal() + self.padding.horizontal()
84    }
85
86    /// Vertical space consumed by border + padding (no margin).
87    pub fn vertical_border_padding(&self) -> f32 {
88        self.border.vertical() + self.padding.vertical()
89    }
90}
91
92/// Resolve the content width of a block-level box per CSS 2.1 §10.3.3.
93///
94/// The constraint equation for block-level non-replaced elements in normal flow:
95/// margin-left + border-left + padding-left + width + padding-right + border-right + margin-right
96///   = containing block width
97pub fn resolve_block_width(style: &ComputedStyle, containing_block_width: f32) -> (f32, Edges) {
98    let border = BoxModel::resolve_border(style);
99    let padding = BoxModel::resolve_padding(style, containing_block_width);
100
101    let border_padding_h = border.horizontal() + padding.horizontal();
102
103    // Resolve specified width
104    let specified_width = style.width.resolve(containing_block_width);
105
106    // Resolve specified margins
107    let margin_left_specified = style.margin_left.resolve(containing_block_width);
108    let margin_right_specified = style.margin_right.resolve(containing_block_width);
109
110    let (content_width, margin_left, margin_right) = match specified_width {
111        Some(mut w) => {
112            // If box-sizing: border-box, width includes border + padding
113            if style.box_sizing == crate::style::BoxSizing::BorderBox {
114                w = (w - border_padding_h).max(0.0);
115            }
116
117            // Apply min/max constraints
118            let min_w = style.min_width.resolve(containing_block_width);
119            let max_w = style
120                .max_width
121                .resolve(containing_block_width)
122                .unwrap_or(f32::INFINITY);
123            w = w.max(min_w).min(max_w);
124
125            let remaining = containing_block_width - w - border_padding_h;
126
127            match (margin_left_specified, margin_right_specified) {
128                (Some(ml), Some(mr)) => {
129                    // Over-constrained: adjust margin-right (LTR)
130                    let _total = ml + mr;
131                    let actual_mr = remaining - ml;
132                    (w, ml, actual_mr)
133                }
134                (None, Some(mr)) => {
135                    let ml = remaining - mr;
136                    (w, ml, mr)
137                }
138                (Some(ml), None) => {
139                    let mr = remaining - ml;
140                    (w, ml, mr)
141                }
142                (None, None) => {
143                    // Both auto: split remaining space equally
144                    let each = remaining / 2.0;
145                    (w, each, each)
146                }
147            }
148        }
149        None => {
150            // Width is auto: fill available space
151            let ml = margin_left_specified.unwrap_or(0.0);
152            let mr = margin_right_specified.unwrap_or(0.0);
153            let mut w = containing_block_width - border_padding_h - ml - mr;
154
155            // Apply min/max constraints
156            let min_w = style.min_width.resolve(containing_block_width);
157            let max_w = style
158                .max_width
159                .resolve(containing_block_width)
160                .unwrap_or(f32::INFINITY);
161            w = w.max(min_w).min(max_w);
162
163            (w.max(0.0), ml, mr)
164        }
165    };
166
167    let margin = Edges::new(
168        style
169            .margin_top
170            .resolve(containing_block_width)
171            .unwrap_or(0.0),
172        margin_right,
173        style
174            .margin_bottom
175            .resolve(containing_block_width)
176            .unwrap_or(0.0),
177        margin_left,
178    );
179
180    (content_width, margin)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::style::ComputedStyle;
187    use crate::values::LengthPercentageAuto;
188
189    #[test]
190    fn test_block_width_auto_fills_container() {
191        let style = ComputedStyle::block();
192        let (width, margin) = resolve_block_width(&style, 800.0);
193        assert_eq!(width, 800.0);
194        assert_eq!(margin.left, 0.0);
195        assert_eq!(margin.right, 0.0);
196    }
197
198    #[test]
199    fn test_block_width_fixed_centers_with_auto_margins() {
200        let mut style = ComputedStyle::block();
201        style.width = LengthPercentageAuto::px(400.0);
202        style.margin_left = LengthPercentageAuto::Auto;
203        style.margin_right = LengthPercentageAuto::Auto;
204        let (width, margin) = resolve_block_width(&style, 800.0);
205        assert_eq!(width, 400.0);
206        assert_eq!(margin.left, 200.0);
207        assert_eq!(margin.right, 200.0);
208    }
209
210    #[test]
211    fn test_block_width_with_padding() {
212        let mut style = ComputedStyle::block();
213        style.padding_left = crate::values::LengthPercentage::px(20.0);
214        style.padding_right = crate::values::LengthPercentage::px(20.0);
215        let (width, _margin) = resolve_block_width(&style, 800.0);
216        assert_eq!(width, 760.0); // 800 - 20 - 20
217    }
218
219    #[test]
220    fn test_block_width_border_box() {
221        let mut style = ComputedStyle::block();
222        style.width = LengthPercentageAuto::px(400.0);
223        style.box_sizing = crate::style::BoxSizing::BorderBox;
224        style.padding_left = crate::values::LengthPercentage::px(20.0);
225        style.padding_right = crate::values::LengthPercentage::px(20.0);
226        style.border_left_width = 5.0;
227        style.border_right_width = 5.0;
228        let (width, _margin) = resolve_block_width(&style, 800.0);
229        assert_eq!(width, 350.0); // 400 - 20 - 20 - 5 - 5
230    }
231}