1pub mod image;
2pub mod layout;
3pub mod painter;
4pub mod parser;
5pub mod styler;
6pub mod table;
7
8use layout::LayoutEngine;
9use painter::paint;
10use styler::StyleEngine;
11pub type NodeId = usize;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct Rgba {
16 pub r: u8,
17 pub g: u8,
18 pub b: u8,
19 pub a: u8,
20}
21
22impl Rgba {
23 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
24 Self { r, g, b, a: 255 }
25 }
26}
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Edges<T> {
29 pub top: T,
30 pub right: T,
31 pub bottom: T,
32 pub left: T,
33}
34
35impl<T: Copy> Edges<T> {
36 pub const fn all(value: T) -> Self {
37 Self {
38 top: value,
39 right: value,
40 bottom: value,
41 left: value,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum FontWeight {
48 Normal,
49 Bold,
50 Weight(u16),
51}
52
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum FontStyle {
55 Normal,
56 Italic,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub enum TextAlign {
61 Left,
62 Center,
63 Right,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq)]
67pub enum Display {
68 Block,
69 Inline,
70 InlineBlock,
71 None,
72 Table,
73 TableRow,
74 TableCell,
75 ListItem,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq)]
79pub enum VerticalAlign {
80 Top,
81 Middle,
82 Bottom,
83 Baseline,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq)]
87pub enum TextDecoration {
88 None,
89 Underline,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq)]
93pub enum SizeValue {
94 Auto,
95 Px(f32),
96 Percent(f32),
97}
98
99#[derive(Debug, Clone, Copy, PartialEq)]
100pub struct BorderSpec {
101 pub width: f32,
102 pub color: Rgba,
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub struct ComputedStyle {
108 pub color: Rgba,
110 pub background_color: Option<Rgba>,
112 pub font_size: f32,
114 pub font_weight: FontWeight,
116 pub font_style: FontStyle,
118 pub font_family: Vec<String>,
120 pub text_align: TextAlign,
122 pub line_height: f32,
124 pub padding: Edges<f32>,
126 pub margin: Edges<f32>,
128 pub width: SizeValue,
130 pub height: SizeValue,
132 pub display: Display,
134 pub vertical_align: VerticalAlign,
136 pub border: Edges<BorderSpec>,
138 pub text_decoration: TextDecoration,
140 pub href: Option<String>,
142}
143
144impl Default for ComputedStyle {
145 fn default() -> Self {
147 Self {
148 color: Rgba::rgb(0, 0, 0),
149 background_color: None,
150 font_size: 16.0,
151 font_weight: FontWeight::Normal,
152 font_style: FontStyle::Normal,
153 font_family: vec!["sans-serif".to_string()],
154 text_align: TextAlign::Left,
155 line_height: 19.2,
156 padding: Edges::all(0.0),
157 margin: Edges::all(0.0),
158 width: SizeValue::Auto,
159 height: SizeValue::Auto,
160 display: Display::Block,
161 vertical_align: VerticalAlign::Baseline,
162 border: Edges::all(BorderSpec {
163 width: 0.0,
164 color: Rgba::rgb(0, 0, 0),
165 }),
166 text_decoration: TextDecoration::None,
167 href: None,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq)]
174pub struct Rect {
175 pub x: f32,
177 pub y: f32,
179 pub width: f32,
181 pub height: f32,
183}
184
185impl Rect {
186 pub fn right(self) -> f32 {
191 self.x + self.width
192 }
193
194 pub fn bottom(self) -> f32 {
199 self.y + self.height
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq)]
205pub struct Point {
206 pub x: f32,
207 pub y: f32,
208}
209
210#[derive(Debug, Clone, PartialEq)]
211pub struct TextLayout {
212 pub lines: Vec<String>,
213 pub line_height: f32,
214 pub font_size: f32,
215}
216
217#[derive(Debug, Clone, PartialEq)]
218pub enum NodeContent {
219 Text(TextLayout),
220 Image {
221 source: image::ImageSource,
222 width: f32,
223 height: f32,
224 },
225 Box,
226 Hr,
227}
228
229#[derive(Debug, Clone, PartialEq)]
232pub struct StyledNode {
233 pub node_id: NodeId,
235 pub tag: Option<String>,
237 pub attrs: std::collections::HashMap<String, String>,
239 pub text: Option<String>,
241 pub style: ComputedStyle,
243 pub children: Vec<StyledNode>,
244}
245
246#[derive(Debug, Clone, PartialEq)]
247pub struct LayoutNode {
248 pub node_id: NodeId,
249 pub rect: Rect,
250 pub style: ComputedStyle,
251 pub content: NodeContent,
252 pub bullet_origin: Option<Point>,
253 pub children: Vec<LayoutNode>,
254 pub tag: Option<String>,
255}
256
257pub type LayoutTree = LayoutNode;
258
259#[derive(Debug, Clone, PartialEq)]
261pub enum DrawCommand {
262 FillRect {
263 rect: Rect,
264 color: Rgba,
265 },
266 StrokeRect {
267 rect: Rect,
268 color: Rgba,
269 width: f32,
270 },
271 DrawText {
272 text: String,
273 origin: Point,
274 color: Rgba,
275 font_size: f32,
276 },
277 DrawImagePlaceholder {
278 rect: Rect,
279 },
280 DrawImage {
281 rect: Rect,
282 source: image::ImageSource,
283 },
284 DrawLine {
285 start: Point,
286 end: Point,
287 color: Rgba,
288 width: f32,
289 },
290 Link {
291 rect: Rect,
292 href: String,
293 },
294}
295
296#[derive(Debug)]
297pub struct HtmlRenderer {
298 styler: StyleEngine,
299 layout: LayoutEngine,
300 last_width: f32,
301 style_cache: Option<StyledNode>,
302 layout_cache: Option<LayoutTree>,
303 cached_html: String,
304}
305
306impl Default for HtmlRenderer {
307 fn default() -> Self {
308 Self {
309 styler: StyleEngine::default(),
310 layout: LayoutEngine::default(),
311 last_width: -1.0,
312 style_cache: None,
313 layout_cache: None,
314 cached_html: String::new(),
315 }
316 }
317}
318
319impl HtmlRenderer {
320 pub fn render(&mut self, html: &str, available_width: f32, debug: bool) -> Vec<DrawCommand> {
322 self.render_html(html, available_width, debug)
323 }
324
325 pub fn render_html(&mut self, html: &str, width: f32, debug: bool ) -> Vec<DrawCommand> {
326 let html_changed = self.cached_html != html;
328
329 if self.style_cache.is_none() || html_changed {
331 let dom = parser::parse(html);
332 self.style_cache = Some(self.styler.compute(&dom, debug));
333 self.cached_html = html.to_string();
334 self.layout_cache = None;
335 self.last_width = -1.0;
336 }
337
338 let width_changed = (width - self.last_width).abs() > f32::EPSILON;
340 if self.layout_cache.is_none() || width_changed {
341 if let Some(base_style_tree) = &self.style_cache {
342 let mut style_tree = base_style_tree.clone();
343 table::normalize_tables(&mut style_tree, width);
344 let layout_tree = self.layout.compute(&style_tree, width, debug);
345 self.layout_cache = Some(layout_tree);
346 self.last_width = width;
347 }
348 }
349
350 let mut commands = Vec::new();
352 if let Some(layout_tree) = &self.layout_cache {
353 paint(layout_tree, &mut commands);
354 }
355 commands
356 }
357
358 pub fn style_tree(&mut self, html: &str) -> StyledNode {
361 let dom = parser::parse(html);
362 self.styler.compute(&dom,false )
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::{Display, HtmlRenderer, SizeValue, StyledNode, TextAlign};
369
370 fn find_first_tag<'a>(node: &'a StyledNode, tag: &str) -> Option<&'a StyledNode> {
371 if node.tag.as_deref() == Some(tag) {
372 return Some(node);
373 }
374 for child in &node.children {
375 if let Some(found) = find_first_tag(child, tag) {
376 return Some(found);
377 }
378 }
379 None
380 }
381
382 #[test]
383 fn font_tag_fallbacks_map_to_style() {
384 let html = r##"<font color="#ff0000" size="5">hello</font>"##;
385 let mut renderer = HtmlRenderer::default();
386 let tree = renderer.style_tree(html);
387 let font_node = find_first_tag(&tree, "font").expect("font node");
388 assert_eq!(font_node.style.color.r, 255);
389 assert!(matches!(font_node.style.display, Display::Inline));
390 assert_eq!(font_node.style.font_size, 24.0);
391 }
392
393 #[test]
394 fn td_attribute_width_and_alignment_are_resolved() {
395 let html = r#"<table width="600"><tr><td width="200" align="center">X</td><td>Y</td></tr></table>"#;
396 let mut renderer = HtmlRenderer::default();
397 let tree = renderer.style_tree(html);
398 let cell = find_first_tag(&tree, "td").expect("cell");
399 assert!(matches!(cell.style.width, SizeValue::Px(200.0)));
400 assert!(matches!(cell.style.text_align, TextAlign::Center));
401 }
402}