headless_engine/render/
layout.rs1use 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 let mut root_box = Self::build_tree(&root_element, interactive);
47
48 let mut y_cursor = 70.0; 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 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 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 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}