Skip to main content

cssbox_core/
block.rs

1//! Block formatting context layout.
2//!
3//! Implements CSS 2.1 §9.4.1 — Block Formatting Contexts and
4//! CSS 2.1 §10.3.3 — Block-level non-replaced elements in normal flow.
5
6use crate::box_model::{resolve_block_width, BoxModel};
7use crate::float::FloatContext;
8use crate::fragment::{Fragment, FragmentKind};
9use crate::geometry::{Point, Size};
10use crate::inline;
11use crate::layout::{self, LayoutContext};
12use crate::style::{BoxSizing, Float};
13use crate::tree::{FormattingContextType, NodeId};
14
15/// Layout a block-level element and its children.
16pub fn layout_block(
17    ctx: &LayoutContext,
18    node: NodeId,
19    containing_block_width: f32,
20    containing_block_height: f32,
21) -> Fragment {
22    let style = ctx.tree.style(node);
23    let mut fragment = Fragment::new(node, FragmentKind::Box);
24
25    // 1. Resolve width and margins
26    let (content_width, margin) = resolve_block_width(style, containing_block_width);
27
28    // 2. Resolve border and padding
29    let border = BoxModel::resolve_border(style);
30    let padding = BoxModel::resolve_padding(style, containing_block_width);
31
32    fragment.margin = margin;
33    fragment.border = border;
34    fragment.padding = padding;
35
36    // 3. Layout children
37    let fc_type = ctx.tree.formatting_context(node);
38
39    let mut float_ctx = FloatContext::new(content_width);
40
41    let content_height = match fc_type {
42        FormattingContextType::Inline => layout_inline_children(
43            ctx,
44            node,
45            content_width,
46            &mut fragment.children,
47            &mut float_ctx,
48        ),
49        _ => layout_block_children(
50            ctx,
51            node,
52            content_width,
53            containing_block_height,
54            &mut fragment.children,
55            &mut float_ctx,
56        ),
57    };
58
59    // 4. Resolve height
60    let specified_height = style.height.resolve(containing_block_height);
61    let mut final_height = match specified_height {
62        Some(mut h) => {
63            if style.box_sizing == BoxSizing::BorderBox {
64                h = (h - border.vertical() - padding.vertical()).max(0.0);
65            }
66            h
67        }
68        None => content_height,
69    };
70
71    // Apply min/max height
72    let min_h = style.min_height.resolve(containing_block_height);
73    let max_h = style
74        .max_height
75        .resolve(containing_block_height)
76        .unwrap_or(f32::INFINITY);
77    final_height = final_height.max(min_h).min(max_h);
78
79    // If this block establishes a BFC, it must contain floats
80    if style.establishes_bfc() {
81        final_height = final_height.max(float_ctx.clear_all());
82    }
83
84    fragment.size = Size::new(content_width, final_height);
85
86    fragment
87}
88
89/// Layout block-level children within a block formatting context.
90fn layout_block_children(
91    ctx: &LayoutContext,
92    parent: NodeId,
93    containing_width: f32,
94    containing_height: f32,
95    fragments: &mut Vec<Fragment>,
96    float_ctx: &mut FloatContext,
97) -> f32 {
98    let children = ctx.tree.children(parent);
99    let mut cursor_y: f32 = 0.0;
100    let mut prev_margin_bottom: f32 = 0.0;
101    let mut is_first_child = true;
102
103    for &child_id in children {
104        let child_style = ctx.tree.style(child_id);
105
106        // Skip display:none
107        if child_style.display.is_none() {
108            continue;
109        }
110
111        // Handle out-of-flow elements
112        if child_style.position.is_absolutely_positioned() {
113            // Absolutely positioned elements are laid out later in position::resolve_positioned
114            let mut child_fragment =
115                layout::layout_node(ctx, child_id, containing_width, containing_height);
116            child_fragment.position = Point::ZERO; // placeholder, resolved later
117            fragments.push(child_fragment);
118            continue;
119        }
120
121        // Handle floats
122        if child_style.float != Float::None {
123            let child_fragment =
124                layout::layout_node(ctx, child_id, containing_width, containing_height);
125            let floated = float_ctx.place_float(child_fragment, child_style.float, cursor_y);
126            fragments.push(floated);
127            continue;
128        }
129
130        // Handle clear
131        if child_style.clear != crate::style::Clear::None {
132            let clear_y = float_ctx.clear(child_style.clear);
133            cursor_y = cursor_y.max(clear_y);
134        }
135
136        // Layout the child
137        let mut child_fragment =
138            layout::layout_node(ctx, child_id, containing_width, containing_height);
139
140        // Margin collapsing (CSS 2.1 §8.3.1)
141        let child_margin_top = child_fragment.margin.top;
142        let collapsed_margin = collapse_margins(prev_margin_bottom, child_margin_top);
143
144        if is_first_child {
145            // First child: potentially collapse with parent's top margin
146            // (simplified — full collapsing is more complex)
147            cursor_y += child_margin_top;
148        } else {
149            // Collapse adjacent sibling margins
150            cursor_y -= prev_margin_bottom;
151            cursor_y += collapsed_margin;
152        }
153
154        // Position the child
155        child_fragment.position = Point::new(
156            child_fragment.margin.left + child_fragment.border.left + child_fragment.padding.left
157                - child_fragment.border.left
158                - child_fragment.padding.left,
159            cursor_y,
160        );
161        // Actually, position is the top-left of the border box, including margin offset
162        child_fragment.position = Point::new(child_fragment.margin.left, cursor_y);
163
164        // Advance cursor
165        cursor_y += child_fragment.border_box().height;
166        prev_margin_bottom = child_fragment.margin.bottom;
167        is_first_child = false;
168
169        fragments.push(child_fragment);
170    }
171
172    // Account for last child's bottom margin (may collapse with parent)
173    cursor_y
174}
175
176/// Layout inline-level children within a block container.
177fn layout_inline_children(
178    ctx: &LayoutContext,
179    parent: NodeId,
180    containing_width: f32,
181    fragments: &mut Vec<Fragment>,
182    float_ctx: &mut FloatContext,
183) -> f32 {
184    inline::layout_inline_formatting_context(ctx, parent, containing_width, fragments, float_ctx)
185}
186
187/// Collapse two adjacent margins per CSS 2.1 §8.3.1.
188///
189/// - Both positive: use the larger.
190/// - Both negative: use the more negative (larger absolute value).
191/// - One of each: sum them (positive + negative).
192fn collapse_margins(margin_a: f32, margin_b: f32) -> f32 {
193    if margin_a >= 0.0 && margin_b >= 0.0 {
194        margin_a.max(margin_b)
195    } else if margin_a < 0.0 && margin_b < 0.0 {
196        margin_a.min(margin_b)
197    } else {
198        margin_a + margin_b
199    }
200}
201
202/// Compute shrink-to-fit width for a block (CSS 2.1 §10.3.5).
203/// Used for floats, inline-blocks, absolutely positioned elements, etc.
204pub fn shrink_to_fit_width(ctx: &LayoutContext, node: NodeId, available_width: f32) -> f32 {
205    // Preferred width: layout with no constraint (max-content)
206    let preferred = compute_intrinsic_width(ctx, node, f32::INFINITY);
207    // Preferred minimum width: layout with zero width (min-content)
208    let preferred_min = compute_intrinsic_width(ctx, node, 0.0);
209
210    // shrink-to-fit = min(max(preferred minimum, available), preferred)
211    preferred_min.max(0.0).max(preferred.min(available_width))
212}
213
214/// Compute intrinsic width of a node given a constraint.
215fn compute_intrinsic_width(ctx: &LayoutContext, node: NodeId, available: f32) -> f32 {
216    let children = ctx.tree.children(node);
217    let style = ctx.tree.style(node);
218
219    let _border = BoxModel::resolve_border(style);
220    let _padding = BoxModel::resolve_padding(style, available);
221
222    let mut max_child_width: f32 = 0.0;
223
224    for &child_id in children {
225        let child_style = ctx.tree.style(child_id);
226        if child_style.display.is_none() || child_style.is_out_of_flow() {
227            continue;
228        }
229
230        if let Some(text) = ctx.tree.node(child_id).text_content() {
231            let size = ctx.text_measure.measure(text, 16.0, available);
232            max_child_width = max_child_width.max(size.width);
233        } else if child_style.display.is_block_level() {
234            let child_width = if let Some(w) = child_style.width.resolve(available) {
235                w
236            } else {
237                compute_intrinsic_width(ctx, child_id, available)
238            };
239            let child_box = BoxModel::resolve(child_style, available);
240            max_child_width =
241                max_child_width.max(child_width + child_box.horizontal_border_padding());
242        }
243    }
244
245    max_child_width
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::layout::{compute_layout, FixedWidthTextMeasure};
252    use crate::style::ComputedStyle;
253    use crate::tree::BoxTreeBuilder;
254    use crate::values::{LengthPercentage, LengthPercentageAuto};
255
256    #[test]
257    fn test_margin_collapsing_positive() {
258        assert_eq!(collapse_margins(10.0, 20.0), 20.0);
259    }
260
261    #[test]
262    fn test_margin_collapsing_negative() {
263        assert_eq!(collapse_margins(-10.0, -20.0), -20.0);
264    }
265
266    #[test]
267    fn test_margin_collapsing_mixed() {
268        assert_eq!(collapse_margins(10.0, -5.0), 5.0);
269    }
270
271    #[test]
272    fn test_block_with_padding_and_children() {
273        let mut builder = BoxTreeBuilder::new();
274        let mut root_style = ComputedStyle::block();
275        root_style.padding_top = LengthPercentage::px(10.0);
276        root_style.padding_bottom = LengthPercentage::px(10.0);
277        let root = builder.root(root_style);
278
279        let mut child_style = ComputedStyle::block();
280        child_style.height = LengthPercentageAuto::px(100.0);
281        builder.element(root, child_style);
282
283        let tree = builder.build();
284        let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
285
286        let root_rect = result.bounding_rect(tree.root()).unwrap();
287        // Root: 10px top padding + 100px child + 10px bottom padding = 120px border box height
288        assert_eq!(root_rect.height, 120.0);
289    }
290
291    #[test]
292    fn test_nested_blocks() {
293        let mut builder = BoxTreeBuilder::new();
294        let root = builder.root(ComputedStyle::block());
295
296        let mut outer_style = ComputedStyle::block();
297        outer_style.padding_left = LengthPercentage::px(20.0);
298        outer_style.padding_right = LengthPercentage::px(20.0);
299        let outer = builder.element(root, outer_style);
300
301        let mut inner_style = ComputedStyle::block();
302        inner_style.height = LengthPercentageAuto::px(50.0);
303        let inner = builder.element(outer, inner_style);
304
305        let tree = builder.build();
306        let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
307
308        let inner_rect = result.bounding_rect(inner).unwrap();
309        // Inner width: 800 - 20 - 20 = 760
310        assert_eq!(inner_rect.width, 760.0);
311        // Inner x: outer x (0) + outer border-left (0) + outer padding-left (20)
312        assert_eq!(inner_rect.x, 20.0);
313    }
314
315    #[test]
316    fn test_percentage_width() {
317        let mut builder = BoxTreeBuilder::new();
318        let root = builder.root(ComputedStyle::block());
319
320        let mut child_style = ComputedStyle::block();
321        child_style.width = LengthPercentageAuto::percent(50.0);
322        child_style.height = LengthPercentageAuto::px(100.0);
323        let child = builder.element(root, child_style);
324
325        let tree = builder.build();
326        let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
327
328        let child_rect = result.bounding_rect(child).unwrap();
329        assert_eq!(child_rect.width, 400.0); // 50% of 800
330    }
331
332    #[test]
333    fn test_min_max_width() {
334        let mut builder = BoxTreeBuilder::new();
335        let root = builder.root(ComputedStyle::block());
336
337        let mut child_style = ComputedStyle::block();
338        child_style.width = LengthPercentageAuto::px(1000.0);
339        child_style.max_width = crate::values::LengthPercentageNone::px(500.0);
340        child_style.height = LengthPercentageAuto::px(50.0);
341        let child = builder.element(root, child_style);
342
343        let tree = builder.build();
344        let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
345
346        let child_rect = result.bounding_rect(child).unwrap();
347        assert_eq!(child_rect.width, 500.0); // clamped by max-width
348    }
349}