Skip to main content

hpx_browser/layout/
engine.rs

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}