Skip to main content

headless_engine/render/
layout.rs

1use crate::dom::interactive::InteractiveElement;
2use crate::render::css::{ComputedStyle, CssParser, Display, FlexDirection};
3use scraper::{ElementRef, Html, Node};
4
5#[derive(Debug, Clone)]
6pub struct Rect {
7    pub x: f32,
8    pub y: f32,
9    pub width: f32,
10    pub height: f32,
11}
12
13#[derive(Debug, Clone)]
14pub enum LayoutContent {
15    Element {
16        tag: String,
17        children: Vec<LayoutBox>,
18    },
19    Text(String),
20    Image {
21        src: String,
22        alt: String,
23    },
24}
25
26#[derive(Debug, Clone)]
27pub struct LayoutBox {
28    pub rect: Rect,
29    pub style: ComputedStyle,
30    pub content: LayoutContent,
31    pub interactive_index: Option<usize>,
32}
33
34pub struct LayoutEngine;
35
36impl LayoutEngine {
37    pub fn build_and_layout(
38        html_str: &str,
39        interactive: &[InteractiveElement],
40        viewport_width: f32,
41    ) -> LayoutBox {
42        let document = Html::parse_document(html_str);
43        let root_element = document.root_element();
44
45        // 1. Build initial layout box tree
46        let mut root_box = Self::build_tree(&root_element, interactive);
47
48        // 2. Perform layout calculation pass
49        let mut y_cursor = 70.0; // below top browser header bar
50        let x_margin = 40.0;
51        let content_width = (viewport_width - (x_margin * 2.0)).max(300.0);
52
53        root_box.rect = Rect {
54            x: 0.0,
55            y: 0.0,
56            width: viewport_width,
57            height: 1200.0,
58        };
59
60        Self::layout_box(&mut root_box, x_margin, &mut y_cursor, content_width);
61
62        root_box
63    }
64
65    fn build_tree(element: &ElementRef, interactive: &[InteractiveElement]) -> LayoutBox {
66        let tag = element.value().name().to_string();
67        let mut style = CssParser::default_style_for_tag(&tag);
68
69        if let Some(inline_style) = element.value().attr("style") {
70            CssParser::parse_inline_style(inline_style, &mut style);
71        }
72
73        // Check if matching interactive element
74        let text_content = element
75            .text()
76            .collect::<Vec<_>>()
77            .join(" ")
78            .trim()
79            .to_string();
80        let interactive_index = interactive
81            .iter()
82            .find(|i| {
83                (!i.text.is_empty() && i.text == text_content)
84                    || (element
85                        .value()
86                        .attr("id")
87                        .is_some_and(|id| id == i.name || i.selector.contains(id)))
88                    || (element.value().attr("name").is_some_and(|n| n == i.name))
89            })
90            .map(|i| i.index);
91
92        let mut children = Vec::new();
93
94        if tag == "img" {
95            let src = element.value().attr("src").unwrap_or("").to_string();
96            let alt = element.value().attr("alt").unwrap_or("").to_string();
97            return LayoutBox {
98                rect: Rect {
99                    x: 0.0,
100                    y: 0.0,
101                    width: 320.0,
102                    height: 180.0,
103                },
104                style,
105                content: LayoutContent::Image { src, alt },
106                interactive_index,
107            };
108        }
109
110        for child in element.children() {
111            match child.value() {
112                Node::Element(_) => {
113                    if let Some(child_el) = ElementRef::wrap(child) {
114                        let child_box = Self::build_tree(&child_el, interactive);
115                        if !child_box.style.is_hidden {
116                            children.push(child_box);
117                        }
118                    }
119                }
120                Node::Text(txt) => {
121                    let text = txt.text.trim().to_string();
122                    if !text.is_empty() {
123                        children.push(LayoutBox {
124                            rect: Rect {
125                                x: 0.0,
126                                y: 0.0,
127                                width: 0.0,
128                                height: 0.0,
129                            },
130                            style: style.clone(),
131                            content: LayoutContent::Text(text),
132                            interactive_index: None,
133                        });
134                    }
135                }
136                _ => {}
137            }
138        }
139
140        LayoutBox {
141            rect: Rect {
142                x: 0.0,
143                y: 0.0,
144                width: 0.0,
145                height: 0.0,
146            },
147            style,
148            content: LayoutContent::Element { tag, children },
149            interactive_index,
150        }
151    }
152
153    fn layout_box(box_node: &mut LayoutBox, x: f32, y_cursor: &mut f32, available_width: f32) {
154        if box_node.style.is_hidden {
155            return;
156        }
157
158        let start_x = x + box_node.style.margin_left + box_node.style.padding_left;
159        let start_y = *y_cursor + box_node.style.margin_top;
160
161        match &mut box_node.content {
162            LayoutContent::Text(txt) => {
163                let char_count = txt.chars().count();
164                let approx_line_width = available_width.max(100.0);
165                let chars_per_line =
166                    (approx_line_width / (box_node.style.font_size * 0.55)).max(10.0) as usize;
167                let lines_count = (char_count / chars_per_line).max(1);
168                let height = lines_count as f32 * (box_node.style.font_size * 1.4);
169
170                box_node.rect = Rect {
171                    x: start_x,
172                    y: start_y,
173                    width: available_width,
174                    height,
175                };
176                *y_cursor = start_y + height + box_node.style.margin_bottom;
177            }
178            LayoutContent::Image { .. } => {
179                let width = box_node.style.width.unwrap_or(320.0).min(available_width);
180                let height = box_node.style.height.unwrap_or(180.0);
181
182                box_node.rect = Rect {
183                    x: start_x,
184                    y: start_y,
185                    width,
186                    height,
187                };
188                *y_cursor = start_y + height + box_node.style.margin_bottom;
189            }
190            LayoutContent::Element { children, .. } => {
191                let element_width = box_node.style.width.unwrap_or(available_width);
192                let mut current_child_y = start_y + box_node.style.padding_top;
193
194                if box_node.style.display == Display::Flex
195                    && box_node.style.flex_direction == FlexDirection::Row
196                {
197                    // Flex row layout
198                    let mut child_x = start_x;
199                    let mut max_row_height: f32 = 0.0;
200                    let child_width = if !children.is_empty() {
201                        (element_width - (box_node.style.gap * (children.len() as f32 - 1.0)))
202                            / children.len() as f32
203                    } else {
204                        element_width
205                    };
206
207                    for child in children.iter_mut() {
208                        let mut temp_y = current_child_y;
209                        Self::layout_box(child, child_x, &mut temp_y, child_width);
210                        let child_h = child.rect.height;
211                        if child_h > max_row_height {
212                            max_row_height = child_h;
213                        }
214                        child_x += child_width + box_node.style.gap;
215                    }
216                    current_child_y += max_row_height;
217                } else {
218                    // Block / normal vertical flow layout
219                    for child in children.iter_mut() {
220                        Self::layout_box(child, start_x, &mut current_child_y, element_width);
221                    }
222                }
223
224                let total_height = (current_child_y - start_y) + box_node.style.padding_bottom;
225                box_node.rect = Rect {
226                    x: start_x,
227                    y: start_y,
228                    width: element_width,
229                    height: total_height,
230                };
231
232                *y_cursor = start_y + total_height + box_node.style.margin_bottom;
233            }
234        }
235    }
236}