Skip to main content

oxidize_html/
lib.rs

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;
11// triggering workflow
12pub 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/// The computed style of a DOM node, including all inherited styles.
106#[derive(Debug, Clone, PartialEq)]
107pub struct ComputedStyle {
108    /// The computed color of the text.
109    pub color: Rgba,
110    /// The computed background color of the node.
111    pub background_color: Option<Rgba>,
112    /// The computed font size of the text.
113    pub font_size: f32,
114    /// The computed font weight of the text.
115    pub font_weight: FontWeight,
116    /// The computed font style of the text.
117    pub font_style: FontStyle,
118    /// The computed font family of the text.
119    pub font_family: Vec<String>,
120    /// The computed text alignment of the node.
121    pub text_align: TextAlign,
122    /// The computed line height of the text.
123    pub line_height: f32,
124    /// The computed padding of the node.
125    pub padding: Edges<f32>,
126    /// The computed margin of the node.
127    pub margin: Edges<f32>,
128    /// The computed width of the node.
129    pub width: SizeValue,
130    /// The computed height of the node.
131    pub height: SizeValue,
132    /// The computed display property of the node.
133    pub display: Display,
134    /// The computed vertical alignment of the node.
135    pub vertical_align: VerticalAlign,
136    /// The computed border of the node.
137    pub border: Edges<BorderSpec>,
138    /// The computed text decoration of the node.
139    pub text_decoration: TextDecoration,
140    /// The computed href of the link, if the node is a link.
141    pub href: Option<String>,
142}
143
144impl Default for ComputedStyle {
145    /// Creates a default computed style with default values.
146    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/// A rectangle in 2D space.
173#[derive(Debug, Clone, Copy, PartialEq)]
174pub struct Rect {
175    /// The x-coordinate of the top-left corner of the rectangle.
176    pub x: f32,
177    /// The y-coordinate of the top-left corner of the rectangle.
178    pub y: f32,
179    /// The width of the rectangle.
180    pub width: f32,
181    /// The height of the rectangle.
182    pub height: f32,
183}
184
185impl Rect {
186    /// Calculates the right edge of the rectangle.
187    ///
188    /// # Return
189    /// f32 calculated by self.x + self.width.
190    pub fn right(self) -> f32 {
191        self.x + self.width
192    }
193
194    /// Calculates the bottom edge of the rectangle.
195    ///
196    ///# Return
197    /// f32 calculated by self.y + self.height.
198    pub fn bottom(self) -> f32 {
199        self.y + self.height
200    }
201}
202
203/// A point in 2D space.
204#[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/// A node in the DOM tree, with computed style and children.
230
231#[derive(Debug, Clone, PartialEq)]
232pub struct StyledNode {
233    /// Unique identifier for the node, used for caching and referencing in layout and paint stages.
234    pub node_id: NodeId,
235    /// The tag name of the node, e.g. "div", "p", "span", etc.
236    pub tag: Option<String>,
237    /// The attributes of the node, e.g. "id" and "class" attributes.
238    pub attrs: std::collections::HashMap<String, String>,
239    /// The text content of the node, if any.
240    pub text: Option<String>,
241    /// The
242    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/// A command to draw a shape or text on the screen
260#[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    /// Renders the given HTML string into a list of draw commands to be used with GPUI to render HTML.
321    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        // Check if the HTML has changed since last render.
327        let html_changed = self.cached_html != html;
328
329        // If the HTML has changed or if the style cache is empty, recompute the dom and style tree.
330        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        // If the width has changed, recompute the layout tree.
339        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        // Take the layout tree and convert it to a list of draw commands.
351        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    /// Parses the given HTML into a tree and returns the root node of a style tree.
359    /// Only used in test functions
360    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}