1use blitz_traits::shell::Viewport;
2
3use crate::{
4 dom::{Dom, NodeId},
5 layout::query::DOMRect,
6};
7
8pub struct LayoutEngine {
9 dirty: bool,
10 viewport: Viewport,
11}
12
13impl LayoutEngine {
14 pub fn new(viewport: crate::layout::Viewport) -> Self {
15 let blitz_viewport = Viewport {
16 window_size: (viewport.width as u32, viewport.height as u32),
17 ..Default::default()
18 };
19 Self {
20 dirty: true,
21 viewport: blitz_viewport,
22 }
23 }
24
25 pub fn mark_dirty(&mut self) {
26 self.dirty = true;
27 }
28
29 pub fn compute(&mut self, dom: &mut Dom) {
30 let inner = dom.inner_mut();
31 inner.set_viewport(self.viewport.clone());
32 inner.resolve(0.0);
33 self.dirty = false;
34 }
35
36 pub fn ensure_computed(&mut self, dom: &mut Dom) {
37 if self.dirty {
38 self.compute(dom);
39 }
40 }
41
42 pub fn get_bounding_rect(&mut self, dom: &mut Dom, node_id: NodeId) -> DOMRect {
43 self.ensure_computed(dom);
44 let inner = dom.inner();
45 let Some(node) = inner.get_node(node_id.0) else {
46 return DOMRect::default();
47 };
48 let layout = &node.final_layout;
49 let (abs_x, abs_y) = self.absolute_position(inner, node_id.0);
50 DOMRect::new(
51 abs_x as f64,
52 abs_y as f64,
53 layout.size.width as f64,
54 layout.size.height as f64,
55 )
56 }
57
58 pub fn get_computed_style(
59 &mut self,
60 _dom: &mut Dom,
61 _node_id: NodeId,
62 ) -> crate::dom::ElementData {
63 crate::dom::ElementData {
64 name: crate::dom::QualName::new(""),
65 attrs: vec![],
66 shadow_root: None,
67 }
68 }
69
70 pub fn get_offset_width(&mut self, dom: &mut Dom, node_id: NodeId) -> f64 {
71 self.ensure_computed(dom);
72 self.node_size(dom, node_id).0
73 }
74
75 pub fn get_offset_height(&mut self, dom: &mut Dom, node_id: NodeId) -> f64 {
76 self.ensure_computed(dom);
77 self.node_size(dom, node_id).1
78 }
79
80 pub fn get_offset_top(&mut self, dom: &mut Dom, node_id: NodeId) -> f64 {
81 self.ensure_computed(dom);
82 self.node_position(dom, node_id).1
83 }
84
85 pub fn get_offset_left(&mut self, dom: &mut Dom, node_id: NodeId) -> f64 {
86 self.ensure_computed(dom);
87 self.node_position(dom, node_id).0
88 }
89
90 fn absolute_position(&self, inner: &blitz_dom::BaseDocument, node_id: usize) -> (f32, f32) {
91 let mut x = 0.0f32;
92 let mut y = 0.0f32;
93 let mut current_id = node_id;
94 while let Some(node) = inner.get_node(current_id) {
95 x += node.final_layout.location.x;
96 y += node.final_layout.location.y;
97 match node.parent {
98 Some(pid) => current_id = pid,
99 None => break,
100 }
101 }
102 (x, y)
103 }
104
105 fn node_size(&self, dom: &Dom, node_id: NodeId) -> (f64, f64) {
106 let inner = dom.inner();
107 let Some(node) = inner.get_node(node_id.0) else {
108 return (0.0, 0.0);
109 };
110 (
111 node.final_layout.size.width as f64,
112 node.final_layout.size.height as f64,
113 )
114 }
115
116 fn node_position(&self, dom: &Dom, node_id: NodeId) -> (f64, f64) {
117 let inner = dom.inner();
118 let Some(node) = inner.get_node(node_id.0) else {
119 return (0.0, 0.0);
120 };
121 (
122 node.final_layout.location.x as f64,
123 node.final_layout.location.y as f64,
124 )
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::dom::{Attribute, Dom, QualName};
132
133 fn make_dom_with_styled_div(style: &str) -> Dom {
134 let mut dom = Dom::new();
135 let html = dom.create_element(QualName::new("html"), vec![]);
136 dom.append_child(NodeId::DOCUMENT, html);
137 let body = dom.create_element(QualName::new("body"), vec![]);
138 dom.append_child(html, body);
139 let div = dom.create_element(
140 QualName::new("div"),
141 vec![Attribute {
142 name: QualName::new("style"),
143 value: style.to_string(),
144 }],
145 );
146 dom.append_child(body, div);
147 dom
148 }
149
150 #[test]
151 fn layout_basic_div() {
152 let mut dom = make_dom_with_styled_div("width: 200px; height: 100px");
153 let viewport = crate::layout::Viewport::new(1920.0, 1080.0);
154 let mut engine = LayoutEngine::new(viewport);
155 engine.compute(&mut dom);
156
157 let html = dom.child_elements(NodeId::DOCUMENT)[0];
158 let body = dom.child_elements(html)[0];
159 let div = dom.child_elements(body)[0];
160
161 let rect = engine.get_bounding_rect(&mut dom, div);
162 assert!(
163 rect.width >= 200.0,
164 "width should be >= 200, got {}",
165 rect.width
166 );
167 }
168
169 #[test]
170 fn dirty_tracking() {
171 let mut dom = make_dom_with_styled_div("width: 100px");
172 let viewport = crate::layout::Viewport::new(1920.0, 1080.0);
173 let mut engine = LayoutEngine::new(viewport);
174
175 assert!(engine.dirty);
176 engine.compute(&mut dom);
177 assert!(!engine.dirty);
178 engine.mark_dirty();
179 assert!(engine.dirty);
180 }
181
182 fn find_child_by_tag(dom: &Dom, parent: NodeId, tag: &str) -> Option<NodeId> {
183 dom.child_elements(parent).into_iter().find(|&id| {
184 dom.get(id)
185 .and_then(|n| n.as_element().cloned())
186 .is_some_and(|e| e.name.local == tag)
187 })
188 }
189
190 fn find_child_by_class(dom: &Dom, parent: NodeId, class_part: &str) -> Option<NodeId> {
191 dom.child_elements(parent).into_iter().find(|&id| {
192 dom.get(id)
193 .and_then(|n| n.as_element().cloned())
194 .is_some_and(|e| {
195 e.attrs
196 .iter()
197 .any(|a| a.name.local == "class" && a.value.contains(class_part))
198 })
199 })
200 }
201
202 #[test]
203 fn full_rendering_pipeline_html_parse_resolve_layout() {
204 let html = r#"<!DOCTYPE html>
205<html>
206<body style="margin: 0; padding: 0;">
207 <div style="display: flex; width: 800px; height: 600px;">
208 <div style="width: 200px; height: 100%;">
209 <p>Nav 1</p>
210 <p>Nav 2</p>
211 </div>
212 <div style="flex-grow: 1; height: 100%;">
213 <div style="width: 100%; height: 80px;">
214 <h1>Page Title</h1>
215 </div>
216 <div style="padding: 20px;">
217 <p>Hello World</p>
218 <p>Second paragraph</p>
219 </div>
220 </div>
221 </div>
222</body>
223</html>"#;
224
225 let mut dom = crate::html_parser::parse_html(html);
226 let viewport = crate::layout::Viewport::new(1920.0, 1080.0);
227 let mut engine = LayoutEngine::new(viewport);
228 engine.compute(&mut dom);
229
230 let body = find_body(&dom);
231
232 let container_id = dom.child_elements(body)[0];
233
234 let container_rect = engine.get_bounding_rect(&mut dom, container_id);
235 assert!(
236 container_rect.width >= 790.0,
237 "container width should be ~800px, got {}",
238 container_rect.width
239 );
240 assert!(
241 container_rect.height >= 590.0,
242 "container height should be ~600px, got {}",
243 container_rect.height
244 );
245
246 let children = dom.child_elements(container_id);
247 assert!(
248 children.len() >= 2,
249 "container should have 2 flex children, got {}",
250 children.len()
251 );
252
253 let sidebar_rect = engine.get_bounding_rect(&mut dom, children[0]);
254 let main_rect = engine.get_bounding_rect(&mut dom, children[1]);
255
256 assert!(
257 sidebar_rect.width >= 190.0,
258 "sidebar width should be ~200px, got {}",
259 sidebar_rect.width
260 );
261 assert!(
262 main_rect.width >= 500.0,
263 "main width should fill remaining ~600px, got {}",
264 main_rect.width
265 );
266 assert!(
267 sidebar_rect.height >= 500.0,
268 "sidebar height should be substantial, got {}",
269 sidebar_rect.height
270 );
271
272 let main_children = dom.child_elements(children[1]);
273 assert!(
274 main_children.len() >= 2,
275 "main should have header + content, got {}",
276 main_children.len()
277 );
278
279 let header_rect = engine.get_bounding_rect(&mut dom, main_children[0]);
280 assert!(
281 header_rect.height >= 70.0,
282 "header height should be ~80px, got {}",
283 header_rect.height
284 );
285
286 let h1_id = dom.child_elements(main_children[0])[0];
287 let h1_text = dom.text_content(h1_id);
288 assert_eq!(h1_text, "Page Title");
289 let h1_rect = engine.get_bounding_rect(&mut dom, h1_id);
290 assert!(
291 h1_rect.height >= 20.0,
292 "h1 should have height, got {}",
293 h1_rect.height
294 );
295
296 let content_id = main_children[1];
297 let paragraphs = dom.get_elements_by_tag_name(content_id, "p");
298 assert_eq!(paragraphs.len(), 2, "should have 2 paragraphs in content");
299 let p1_text = dom.text_content(paragraphs[0]);
300 let p2_text = dom.text_content(paragraphs[1]);
301 assert_eq!(p1_text, "Hello World");
302 assert_eq!(p2_text, "Second paragraph");
303 }
304
305 #[test]
306 fn layout_with_flexbox_grow() {
307 let html = r#"<!DOCTYPE html>
308<html>
309<body>
310 <div style="display: flex; width: 600px; height: 200px;">
311 <div style="width: 100px; height: 100px;">A</div>
312 <div style="flex-grow: 1; height: 100px;">B</div>
313 <div style="width: 150px; height: 100px;">C</div>
314 </div>
315</body>
316</html>"#;
317
318 let mut dom = crate::html_parser::parse_html(html);
319 let viewport = crate::layout::Viewport::new(1920.0, 1080.0);
320 let mut engine = LayoutEngine::new(viewport);
321 engine.compute(&mut dom);
322
323 let body = find_body(&dom);
324 let body_children = dom.child_elements(body);
325
326 let flex_div = body_children
327 .iter()
328 .find(|&&id| {
329 dom.get(id)
330 .and_then(|n| n.as_element().cloned())
331 .is_some_and(|e| {
332 e.attrs
333 .iter()
334 .any(|a| a.name.local == "style" && a.value.contains("flex"))
335 })
336 })
337 .copied()
338 .unwrap_or(body_children[0]);
339
340 let flex_rect = engine.get_bounding_rect(&mut dom, flex_div);
341 assert!(
342 flex_rect.width >= 590.0,
343 "flex container width should be ~600px, got {}",
344 flex_rect.width
345 );
346 assert!(
347 flex_rect.height >= 190.0,
348 "flex container height should be ~200px, got {}",
349 flex_rect.height
350 );
351
352 let children = dom.child_elements(flex_div);
353 assert!(
354 children.len() >= 3,
355 "should have at least 3 flex children, got {}",
356 children.len()
357 );
358
359 let a_rect = engine.get_bounding_rect(&mut dom, children[0]);
360 let b_rect = engine.get_bounding_rect(&mut dom, children[1]);
361 let c_rect = engine.get_bounding_rect(&mut dom, children[2]);
362
363 assert!(
364 a_rect.width >= 90.0,
365 "A width should be ~100px, got {}",
366 a_rect.width
367 );
368 assert!(
369 c_rect.width >= 140.0,
370 "C width should be ~150px, got {}",
371 c_rect.width
372 );
373 assert!(
374 b_rect.width >= 300.0,
375 "B should fill remaining space ~350px, got {}",
376 b_rect.width
377 );
378 assert!(
379 b_rect.x > a_rect.x + a_rect.width - 1.0,
380 "B should be to the right of A"
381 );
382 assert!(
383 c_rect.x > b_rect.x + b_rect.width - 1.0,
384 "C should be to the right of B"
385 );
386 }
387
388 #[test]
389 fn layout_style_block_resolves() {
390 let html = r#"<!DOCTYPE html>
391<html>
392<head>
393 <style>
394 .flex-box { display: flex; width: 400px; height: 200px; }
395 .child-a { width: 100px; }
396 .child-b { flex-grow: 1; }
397 </style>
398</head>
399<body>
400 <div class="flex-box">
401 <div class="child-a">A</div>
402 <div class="child-b">B</div>
403 </div>
404</body>
405</html>"#;
406
407 let mut dom = crate::html_parser::parse_html(html);
408 let viewport = crate::layout::Viewport::new(1920.0, 1080.0);
409 let mut engine = LayoutEngine::new(viewport);
410 engine.compute(&mut dom);
411
412 let body = find_body(&dom);
413 let body_children = dom.child_elements(body);
414 let flex_div = body_children
415 .iter()
416 .find(|&&id| {
417 dom.get(id)
418 .and_then(|n| n.as_element().cloned())
419 .is_some_and(|e| {
420 e.attrs
421 .iter()
422 .any(|a| a.name.local == "class" && a.value.contains("flex-box"))
423 })
424 })
425 .copied();
426
427 if let Some(flex_id) = flex_div {
428 let rect = engine.get_bounding_rect(&mut dom, flex_id);
429 assert!(
430 rect.width >= 390.0,
431 "style block flex container width should be ~400px, got {}",
432 rect.width
433 );
434 } else {
435 panic!("should have flex-box element");
436 }
437 }
438
439 #[test]
440 fn debug_tree_structure() {
441 let html = r#"<!DOCTYPE html>
442<html>
443<body>
444 <div class="container">
445 <div class="sidebar"><p>Nav</p></div>
446 <div class="main"><h1>Title</h1></div>
447 </div>
448</body>
449</html>"#;
450 let mut dom = crate::html_parser::parse_html(html);
451 let viewport = crate::layout::Viewport::new(1920.0, 1080.0);
452 let mut engine = LayoutEngine::new(viewport);
453 engine.compute(&mut dom);
454
455 fn print_tree(dom: &Dom, id: NodeId, depth: usize) {
456 let node = match dom.get(id) {
457 Some(n) => n,
458 None => return,
459 };
460 let indent = " ".repeat(depth);
461 let tag = node
462 .as_element()
463 .map(|e| {
464 format!(
465 "{} [class={:?}]",
466 e.name.local,
467 e.attrs
468 .iter()
469 .find(|a| a.name.local == "class")
470 .map(|a| &a.value)
471 )
472 })
473 .unwrap_or_else(|| format!("{:?}", node.data));
474 let rect = dom
475 .inner()
476 .get_node(id.0)
477 .map(|n| {
478 let l = &n.final_layout;
479 format!(
480 "({:.0},{:.0} {:.0}x{:.0})",
481 l.location.x, l.location.y, l.size.width, l.size.height
482 )
483 })
484 .unwrap_or_default();
485 println!("{}{} {}", indent, tag, rect);
486 for child_id in dom.children(id) {
487 print_tree(dom, child_id, depth + 1);
488 }
489 }
490
491 print_tree(&dom, NodeId::DOCUMENT, 0);
492 }
493
494 fn find_body(dom: &Dom) -> NodeId {
495 let html = dom.child_elements(NodeId::DOCUMENT)[0];
496 dom.child_elements(html)
497 .iter()
498 .find(|&&id| {
499 dom.get(id)
500 .and_then(|n| n.as_element().cloned())
501 .is_some_and(|e| e.name.local == "body")
502 })
503 .copied()
504 .unwrap_or(html)
505 }
506
507 #[test]
508 fn layout_with_padding_and_margin() {
509 let html = r#"<!DOCTYPE html>
510<html>
511<body>
512 <div style="width: 300px; height: 200px; padding: 10px; margin: 20px;">
513 <div style="width: 100%; height: 100%;"></div>
514 </div>
515</body>
516</html>"#;
517
518 let mut dom = crate::html_parser::parse_html(html);
519 let viewport = crate::layout::Viewport::new(1920.0, 1080.0);
520 let mut engine = LayoutEngine::new(viewport);
521 engine.compute(&mut dom);
522
523 let body = find_body(&dom);
524 let body_children = dom.child_elements(body);
525 let outer_div = body_children
526 .iter()
527 .find(|&&id| {
528 dom.get(id)
529 .and_then(|n| n.as_element().cloned())
530 .is_some_and(|e| {
531 e.attrs
532 .iter()
533 .any(|a| a.name.local == "style" && a.value.contains("300px"))
534 })
535 })
536 .copied()
537 .expect("should find div with 300px style");
538
539 let outer_rect = engine.get_bounding_rect(&mut dom, outer_div);
540 assert!(
541 outer_rect.width > 0.0,
542 "outer div should have non-zero width, got {}",
543 outer_rect.width
544 );
545 assert!(
546 outer_rect.height > 0.0,
547 "outer div should have non-zero height, got {}",
548 outer_rect.height
549 );
550 }
551}