1use 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
12pub trait TextMeasure {
14 fn measure(&self, text: &str, font_size: f32, max_width: f32) -> Size;
16}
17
18pub 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 } else {
46 word_chars
47 };
48
49 if current_line_chars > 0 && current_line_chars + needed > chars_per_line {
50 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; }
59 current_line_chars += word_chars;
60 }
61 }
62
63 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
71pub struct LayoutContext<'a> {
73 pub tree: &'a BoxTree,
74 pub text_measure: &'a dyn TextMeasure,
75 pub viewport: Size,
76}
77
78pub 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 let root_fragment = position::resolve_positioned(tree, root_fragment, viewport);
95
96 LayoutResult {
97 root: root_fragment,
98 }
99}
100
101pub 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 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 if let Some(text) = ctx.tree.node(node).text_content() {
119 return layout_text(ctx, node, text, containing_block_width);
120 }
121
122 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
144fn 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; 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); 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); assert!((size.height - 19.2).abs() < 0.01); }
213}