Skip to main content

cssbox_core/
layout.rs

1//! Layout entry point and dispatch.
2
3use crate::block;
4use crate::flex;
5use crate::fragment::{Fragment, FragmentKind, LayoutResult};
6use crate::geometry::Size;
7use crate::grid;
8use crate::position;
9use crate::table;
10use crate::tree::{BoxTree, FormattingContextType, NodeId};
11
12/// Text measurement callback.
13pub trait TextMeasure {
14    /// Measure the width and height of a text string within a maximum width.
15    fn measure(&self, text: &str, font_size: f32, max_width: f32) -> Size;
16}
17
18/// A fixed-width text measurer for testing (every character = 8px wide).
19pub struct FixedWidthTextMeasure;
20
21impl TextMeasure for FixedWidthTextMeasure {
22    fn measure(&self, text: &str, font_size: f32, max_width: f32) -> Size {
23        let char_width = 8.0;
24        let line_height = font_size * 1.2;
25
26        if text.is_empty() {
27            return Size::new(0.0, line_height);
28        }
29
30        let chars_per_line = (max_width / char_width).floor().max(1.0) as usize;
31        let words: Vec<&str> = text.split_whitespace().collect();
32
33        if words.is_empty() {
34            return Size::new(0.0, line_height);
35        }
36
37        let mut lines = 1usize;
38        let mut current_line_chars = 0usize;
39        let mut max_line_width = 0.0f32;
40
41        for word in words.iter() {
42            let word_chars = word.len();
43            let needed = if current_line_chars > 0 {
44                word_chars + 1 // space before word
45            } else {
46                word_chars
47            };
48
49            if current_line_chars > 0 && current_line_chars + needed > chars_per_line {
50                // Wrap to new line
51                let line_width = current_line_chars as f32 * char_width;
52                max_line_width = max_line_width.max(line_width);
53                lines += 1;
54                current_line_chars = word_chars;
55            } else {
56                if current_line_chars > 0 {
57                    current_line_chars += 1; // space
58                }
59                current_line_chars += word_chars;
60            }
61        }
62
63        // Final line
64        let line_width = current_line_chars as f32 * char_width;
65        max_line_width = max_line_width.max(line_width);
66
67        Size::new(max_line_width, lines as f32 * line_height)
68    }
69}
70
71/// Layout context passed through the layout tree.
72pub struct LayoutContext<'a> {
73    pub tree: &'a BoxTree,
74    pub text_measure: &'a dyn TextMeasure,
75    pub viewport: Size,
76}
77
78/// Compute layout for an entire box tree.
79pub fn compute_layout(
80    tree: &BoxTree,
81    text_measure: &dyn TextMeasure,
82    viewport: Size,
83) -> LayoutResult {
84    let ctx = LayoutContext {
85        tree,
86        text_measure,
87        viewport,
88    };
89
90    let root = tree.root();
91    let root_fragment = layout_node(&ctx, root, viewport.width, viewport.height);
92
93    // After main layout, resolve absolute/fixed positioned elements
94    let root_fragment = position::resolve_positioned(tree, root_fragment, viewport);
95
96    LayoutResult {
97        root: root_fragment,
98    }
99}
100
101/// Layout a single node and its subtree.
102pub fn layout_node(
103    ctx: &LayoutContext,
104    node: NodeId,
105    containing_block_width: f32,
106    containing_block_height: f32,
107) -> Fragment {
108    let style = ctx.tree.style(node);
109
110    // Skip display: none
111    if style.display.is_none() {
112        let mut f = Fragment::new(node, FragmentKind::Box);
113        f.size = Size::ZERO;
114        return f;
115    }
116
117    // Text nodes
118    if let Some(text) = ctx.tree.node(node).text_content() {
119        return layout_text(ctx, node, text, containing_block_width);
120    }
121
122    // Determine formatting context for children
123    let fc = ctx.tree.formatting_context(node);
124
125    match fc {
126        FormattingContextType::Block => {
127            block::layout_block(ctx, node, containing_block_width, containing_block_height)
128        }
129        FormattingContextType::Inline => {
130            block::layout_block(ctx, node, containing_block_width, containing_block_height)
131        }
132        FormattingContextType::Flex => {
133            flex::layout_flex(ctx, node, containing_block_width, containing_block_height)
134        }
135        FormattingContextType::Grid => {
136            grid::layout_grid(ctx, node, containing_block_width, containing_block_height)
137        }
138        FormattingContextType::Table => {
139            table::layout_table(ctx, node, containing_block_width, containing_block_height)
140        }
141    }
142}
143
144/// Layout a text node.
145fn layout_text(ctx: &LayoutContext, node: NodeId, text: &str, max_width: f32) -> Fragment {
146    let style = ctx.tree.style(node);
147    let font_size = style.line_height / 1.2; // approximate
148    let size = ctx.text_measure.measure(text, font_size, max_width);
149
150    let mut fragment = Fragment::new(node, FragmentKind::TextRun);
151    fragment.size = size;
152    fragment
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::style::ComputedStyle;
159    use crate::tree::BoxTreeBuilder;
160    use crate::values::LengthPercentageAuto;
161
162    #[test]
163    fn test_simple_block_layout() {
164        let mut builder = BoxTreeBuilder::new();
165        let root = builder.root(ComputedStyle::block());
166        let mut child_style = ComputedStyle::block();
167        child_style.height = LengthPercentageAuto::px(100.0);
168        builder.element(root, child_style);
169
170        let tree = builder.build();
171        let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
172
173        let root_rect = result.bounding_rect(tree.root()).unwrap();
174        assert_eq!(root_rect.width, 800.0);
175        assert_eq!(root_rect.height, 100.0);
176    }
177
178    #[test]
179    fn test_two_blocks_stack_vertically() {
180        let mut builder = BoxTreeBuilder::new();
181        let root = builder.root(ComputedStyle::block());
182
183        let mut child1_style = ComputedStyle::block();
184        child1_style.height = LengthPercentageAuto::px(50.0);
185        let c1 = builder.element(root, child1_style);
186
187        let mut child2_style = ComputedStyle::block();
188        child2_style.height = LengthPercentageAuto::px(75.0);
189        let c2 = builder.element(root, child2_style);
190
191        let tree = builder.build();
192        let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
193
194        let root_rect = result.bounding_rect(tree.root()).unwrap();
195        assert_eq!(root_rect.height, 125.0); // 50 + 75
196
197        let c1_rect = result.bounding_rect(c1).unwrap();
198        assert_eq!(c1_rect.y, 0.0);
199        assert_eq!(c1_rect.height, 50.0);
200
201        let c2_rect = result.bounding_rect(c2).unwrap();
202        assert_eq!(c2_rect.y, 50.0);
203        assert_eq!(c2_rect.height, 75.0);
204    }
205
206    #[test]
207    fn test_fixed_width_text_measure() {
208        let m = FixedWidthTextMeasure;
209        let size = m.measure("Hello World", 16.0, 800.0);
210        assert_eq!(size.width, 88.0); // 11 chars * 8px
211        assert!((size.height - 19.2).abs() < 0.01); // 16 * 1.2
212    }
213}