Skip to main content

hpx_browser/layout/
engine.rs

1use std::collections::{HashMap, HashSet};
2
3use tracing::error;
4
5use crate::{
6    css_cascade::ComputedStyle,
7    css_parser::{self, ComponentValue, TokenKind},
8    css_values::{
9        property::{CssValue, PropertyId},
10        types::{display as css_display, font as css_font, length as css_length},
11    },
12    dom::{Dom, NodeData, NodeId},
13    layout::{
14        query::DOMRect, resolve::ResolveContext, style_map::computed_to_taffy, viewport::Viewport,
15    },
16};
17
18const LAYOUT_BUILD_LIMIT: usize = 100_000;
19
20pub struct LayoutEngine {
21    tree: taffy::TaffyTree,
22    dom_to_taffy: HashMap<u32, taffy::NodeId>,
23    viewport: Viewport,
24    dirty: bool,
25    root_taffy: Option<taffy::NodeId>,
26}
27
28impl LayoutEngine {
29    pub fn new(viewport: Viewport) -> Self {
30        Self {
31            tree: taffy::TaffyTree::new(),
32            dom_to_taffy: HashMap::new(),
33            viewport,
34            dirty: true,
35            root_taffy: None,
36        }
37    }
38
39    pub fn mark_dirty(&mut self) {
40        self.dirty = true;
41    }
42
43    pub fn compute(&mut self, dom: &Dom) {
44        self.tree = taffy::TaffyTree::new();
45        self.dom_to_taffy.clear();
46
47        let ctx = ResolveContext {
48            font_size: 16.0,
49            root_font_size: 16.0,
50            viewport_w: self.viewport.width,
51            viewport_h: self.viewport.height,
52        };
53
54        let root = self.build_node(dom, NodeId::DOCUMENT, &ctx);
55        self.root_taffy = root;
56
57        if let Some(root_id) = self.root_taffy {
58            let avail = taffy::Size {
59                width: taffy::AvailableSpace::Definite(self.viewport.width),
60                height: taffy::AvailableSpace::Definite(self.viewport.height),
61            };
62            self.tree.compute_layout(root_id, avail).ok();
63        }
64
65        self.dirty = false;
66    }
67
68    pub fn ensure_computed(&mut self, dom: &Dom) {
69        if self.dirty {
70            self.compute(dom);
71        }
72    }
73
74    pub fn get_bounding_rect(&mut self, dom: &Dom, node_id: NodeId) -> DOMRect {
75        self.ensure_computed(dom);
76
77        let taffy_id = match self.dom_to_taffy.get(&node_id.to_raw()) {
78            Some(id) => *id,
79            None => return DOMRect::default(),
80        };
81
82        let layout = match self.tree.layout(taffy_id) {
83            Ok(l) => *l,
84            Err(_) => return DOMRect::default(),
85        };
86
87        let (abs_x, abs_y) = self.absolute_position(taffy_id);
88
89        DOMRect::new(
90            abs_x as f64,
91            abs_y as f64,
92            layout.size.width as f64,
93            layout.size.height as f64,
94        )
95    }
96
97    pub fn get_computed_style(&mut self, dom: &Dom, node_id: NodeId) -> ComputedStyle {
98        let node = match dom.get(node_id) {
99            Some(n) => n,
100            None => return ComputedStyle::default(),
101        };
102        match &node.data {
103            NodeData::Element(elem) => {
104                let inline = self.parse_inline_style(elem);
105                ComputedStyle::resolve(&inline, None)
106            }
107            _ => ComputedStyle::default(),
108        }
109    }
110
111    pub fn get_offset_width(&mut self, dom: &Dom, node_id: NodeId) -> f64 {
112        self.ensure_computed(dom);
113        self.taffy_size(node_id).0
114    }
115
116    pub fn get_offset_height(&mut self, dom: &Dom, node_id: NodeId) -> f64 {
117        self.ensure_computed(dom);
118        self.taffy_size(node_id).1
119    }
120
121    pub fn get_offset_top(&mut self, dom: &Dom, node_id: NodeId) -> f64 {
122        self.ensure_computed(dom);
123        self.taffy_position(node_id).1
124    }
125
126    pub fn get_offset_left(&mut self, dom: &Dom, node_id: NodeId) -> f64 {
127        self.ensure_computed(dom);
128        self.taffy_position(node_id).0
129    }
130
131    // --- Internal ---
132
133    fn build_node(
134        &mut self,
135        dom: &Dom,
136        root: NodeId,
137        ctx: &ResolveContext,
138    ) -> Option<taffy::NodeId> {
139        enum Work {
140            Visit(NodeId),
141            Finish(NodeId),
142        }
143        let mut stack: Vec<Work> = vec![Work::Visit(root)];
144        let mut visited: HashSet<NodeId> = HashSet::with_capacity(64);
145        let mut steps: usize = 0;
146        while let Some(work) = stack.pop() {
147            match work {
148                Work::Visit(node_id) => {
149                    if !visited.insert(node_id) {
150                        continue;
151                    }
152                    steps += 1;
153                    if steps > LAYOUT_BUILD_LIMIT {
154                        error!(
155                            "Layout build cycle from {:?} — visited {} unique nodes",
156                            root,
157                            visited.len()
158                        );
159                        return None;
160                    }
161                    stack.push(Work::Finish(node_id));
162                    let kids = dom.children(node_id);
163                    for c in kids.into_iter().rev() {
164                        stack.push(Work::Visit(c));
165                    }
166                }
167                Work::Finish(node_id) => {
168                    self.finish_node(dom, node_id, ctx);
169                }
170            }
171        }
172        self.dom_to_taffy.get(&root.to_raw()).copied()
173    }
174
175    fn finish_node(&mut self, dom: &Dom, node_id: NodeId, ctx: &ResolveContext) {
176        let node = match dom.get(node_id) {
177            Some(n) => n,
178            None => return,
179        };
180
181        let children: Vec<taffy::NodeId> = dom
182            .children(node_id)
183            .into_iter()
184            .filter_map(|cid| self.dom_to_taffy.get(&cid.to_raw()).copied())
185            .collect();
186
187        let taffy_id = match &node.data {
188            NodeData::Document | NodeData::DocumentFragment => {
189                let style = taffy::Style {
190                    display: taffy::Display::Block,
191                    size: taffy::Size {
192                        width: taffy::Dimension::length(ctx.viewport_w),
193                        height: taffy::Dimension::auto(),
194                    },
195                    ..Default::default()
196                };
197                match self.tree.new_with_children(style, &children) {
198                    Ok(id) => id,
199                    Err(_) => return,
200                }
201            }
202            NodeData::Element(elem) => {
203                let inline_style = self.parse_inline_style(elem);
204                let computed = ComputedStyle::resolve(&inline_style, None);
205                if let Some(CssValue::Display(css_display::Display::None)) =
206                    computed.get(&PropertyId::Display)
207                {
208                    return;
209                }
210                let taffy_style = computed_to_taffy(&computed, ctx);
211                match self.tree.new_with_children(taffy_style, &children) {
212                    Ok(id) => id,
213                    Err(_) => return,
214                }
215            }
216            NodeData::Text(text) => {
217                let char_count = text.chars().count() as f32;
218                let width = char_count * ctx.font_size * 0.6;
219                let height = ctx.font_size * 1.2;
220                let style = taffy::Style {
221                    size: taffy::Size {
222                        width: taffy::Dimension::length(width),
223                        height: taffy::Dimension::length(height),
224                    },
225                    ..Default::default()
226                };
227                match self.tree.new_leaf(style) {
228                    Ok(id) => id,
229                    Err(_) => return,
230                }
231            }
232            _ => return,
233        };
234        self.dom_to_taffy.insert(node_id.to_raw(), taffy_id);
235    }
236
237    fn parse_inline_style(&self, elem: &crate::dom::ElementData) -> HashMap<PropertyId, CssValue> {
238        let mut map = HashMap::new();
239        let style_attr = elem.attrs.iter().find(|a| a.name.local == "style");
240        let Some(attr) = style_attr else {
241            return map;
242        };
243        let (decls, _errors) = css_parser::parse_declaration_list(&attr.value);
244        for decl in &decls {
245            let prop_id = PropertyId::from_name(decl.name);
246            if let Some(value) = component_values_to_css(&prop_id, &decl.value) {
247                map.insert(prop_id, value);
248            }
249        }
250        map
251    }
252
253    fn absolute_position(&self, taffy_id: taffy::NodeId) -> (f32, f32) {
254        let mut x = 0.0f32;
255        let mut y = 0.0f32;
256        let mut current = taffy_id;
257        loop {
258            if let Ok(layout) = self.tree.layout(current) {
259                x += layout.location.x;
260                y += layout.location.y;
261            }
262            match self.tree.parent(current) {
263                Some(parent) => current = parent,
264                None => break,
265            }
266        }
267        (x, y)
268    }
269
270    fn taffy_size(&self, node_id: NodeId) -> (f64, f64) {
271        match self.dom_to_taffy.get(&node_id.to_raw()) {
272            Some(taffy_id) => match self.tree.layout(*taffy_id) {
273                Ok(layout) => (
274                    crate::layout::layout_unit::LayoutUnit::from_taffy_f32(layout.size.width)
275                        .to_f64_px(),
276                    crate::layout::layout_unit::LayoutUnit::from_taffy_f32(layout.size.height)
277                        .to_f64_px(),
278                ),
279                Err(_) => (0.0, 0.0),
280            },
281            None => (0.0, 0.0),
282        }
283    }
284
285    fn taffy_position(&self, node_id: NodeId) -> (f64, f64) {
286        match self.dom_to_taffy.get(&node_id.to_raw()) {
287            Some(taffy_id) => match self.tree.layout(*taffy_id) {
288                Ok(layout) => (
289                    crate::layout::layout_unit::LayoutUnit::from_taffy_f32(layout.location.x)
290                        .to_f64_px(),
291                    crate::layout::layout_unit::LayoutUnit::from_taffy_f32(layout.location.y)
292                        .to_f64_px(),
293                ),
294                Err(_) => (0.0, 0.0),
295            },
296            None => (0.0, 0.0),
297        }
298    }
299}
300
301// --- Inline style token → CssValue conversion ---
302
303fn first_token<'a, 'b>(values: &'b [ComponentValue<'a>]) -> Option<&'b ComponentValue<'a>> {
304    values
305        .iter()
306        .find(|v| !matches!(v, ComponentValue::Token(t) if t.kind.is_whitespace()))
307}
308
309fn component_values_to_css(prop: &PropertyId, values: &[ComponentValue<'_>]) -> Option<CssValue> {
310    let tok = first_token(values)?;
311    match tok {
312        ComponentValue::Token(t) => match &t.kind {
313            TokenKind::Ident(ident) => parse_keyword(prop, ident),
314            TokenKind::Dimension { value, unit, .. } => parse_dimension(prop, *value, unit),
315            TokenKind::Number { value, .. } => parse_number(prop, *value),
316            TokenKind::Percentage { value, .. } => parse_percentage(prop, *value),
317            _ => None,
318        },
319        _ => None,
320    }
321}
322
323fn parse_keyword(prop: &PropertyId, ident: &str) -> Option<CssValue> {
324    let lower = ident.to_ascii_lowercase();
325    match prop {
326        PropertyId::Display => match lower.as_str() {
327            "none" => Some(CssValue::Display(css_display::Display::None)),
328            "block" => Some(CssValue::Display(css_display::Display::Block)),
329            "inline" => Some(CssValue::Display(css_display::Display::Inline)),
330            "inline-block" => Some(CssValue::Display(css_display::Display::InlineBlock)),
331            "flex" => Some(CssValue::Display(css_display::Display::Flex)),
332            "inline-flex" => Some(CssValue::Display(css_display::Display::InlineFlex)),
333            "grid" => Some(CssValue::Display(css_display::Display::Grid)),
334            "inline-grid" => Some(CssValue::Display(css_display::Display::InlineGrid)),
335            "table" => Some(CssValue::Display(css_display::Display::Table)),
336            "contents" => Some(CssValue::Display(css_display::Display::Contents)),
337            "flow-root" => Some(CssValue::Display(css_display::Display::FlowRoot)),
338            "list-item" => Some(CssValue::Display(css_display::Display::ListItem)),
339            _ => None,
340        },
341        PropertyId::Position => match lower.as_str() {
342            "static" => Some(CssValue::Position(css_display::Position::Static)),
343            "relative" => Some(CssValue::Position(css_display::Position::Relative)),
344            "absolute" => Some(CssValue::Position(css_display::Position::Absolute)),
345            "fixed" => Some(CssValue::Position(css_display::Position::Fixed)),
346            "sticky" => Some(CssValue::Position(css_display::Position::Sticky)),
347            _ => None,
348        },
349        PropertyId::FlexDirection => match lower.as_str() {
350            "row" => Some(CssValue::FlexDirection(css_display::FlexDirection::Row)),
351            "row-reverse" => Some(CssValue::FlexDirection(
352                css_display::FlexDirection::RowReverse,
353            )),
354            "column" => Some(CssValue::FlexDirection(css_display::FlexDirection::Column)),
355            "column-reverse" => Some(CssValue::FlexDirection(
356                css_display::FlexDirection::ColumnReverse,
357            )),
358            _ => None,
359        },
360        PropertyId::FlexWrap => match lower.as_str() {
361            "nowrap" => Some(CssValue::FlexWrap(css_display::FlexWrap::Nowrap)),
362            "wrap" => Some(CssValue::FlexWrap(css_display::FlexWrap::Wrap)),
363            "wrap-reverse" => Some(CssValue::FlexWrap(css_display::FlexWrap::WrapReverse)),
364            _ => None,
365        },
366        PropertyId::BoxSizing => match lower.as_str() {
367            "content-box" => Some(CssValue::BoxSizing(css_display::BoxSizing::ContentBox)),
368            "border-box" => Some(CssValue::BoxSizing(css_display::BoxSizing::BorderBox)),
369            _ => None,
370        },
371        PropertyId::OverflowX | PropertyId::OverflowY => match lower.as_str() {
372            "visible" => Some(CssValue::Overflow(css_display::Overflow::Visible)),
373            "hidden" => Some(CssValue::Overflow(css_display::Overflow::Hidden)),
374            "scroll" => Some(CssValue::Overflow(css_display::Overflow::Scroll)),
375            "auto" => Some(CssValue::Overflow(css_display::Overflow::Auto)),
376            "clip" => Some(CssValue::Overflow(css_display::Overflow::Clip)),
377            _ => None,
378        },
379        PropertyId::Visibility => match lower.as_str() {
380            "visible" => Some(CssValue::Visibility(css_display::Visibility::Visible)),
381            "hidden" => Some(CssValue::Visibility(css_display::Visibility::Hidden)),
382            "collapse" => Some(CssValue::Visibility(css_display::Visibility::Collapse)),
383            _ => None,
384        },
385        PropertyId::AlignItems
386        | PropertyId::AlignSelf
387        | PropertyId::AlignContent
388        | PropertyId::JustifyContent
389        | PropertyId::JustifyItems
390        | PropertyId::JustifySelf => match lower.as_str() {
391            "normal" => Some(CssValue::Alignment(css_display::AlignmentValue::Normal)),
392            "stretch" => Some(CssValue::Alignment(css_display::AlignmentValue::Stretch)),
393            "center" => Some(CssValue::Alignment(css_display::AlignmentValue::Center)),
394            "start" => Some(CssValue::Alignment(css_display::AlignmentValue::Start)),
395            "end" => Some(CssValue::Alignment(css_display::AlignmentValue::End)),
396            "flex-start" => Some(CssValue::Alignment(css_display::AlignmentValue::FlexStart)),
397            "flex-end" => Some(CssValue::Alignment(css_display::AlignmentValue::FlexEnd)),
398            "baseline" => Some(CssValue::Alignment(css_display::AlignmentValue::Baseline)),
399            "space-between" => Some(CssValue::Alignment(
400                css_display::AlignmentValue::SpaceBetween,
401            )),
402            "space-around" => Some(CssValue::Alignment(
403                css_display::AlignmentValue::SpaceAround,
404            )),
405            "space-evenly" => Some(CssValue::Alignment(
406                css_display::AlignmentValue::SpaceEvenly,
407            )),
408            _ => None,
409        },
410        PropertyId::Float => match lower.as_str() {
411            "none" => Some(CssValue::Float(css_display::Float::None)),
412            "left" => Some(CssValue::Float(css_display::Float::Left)),
413            "right" => Some(CssValue::Float(css_display::Float::Right)),
414            _ => None,
415        },
416        PropertyId::Clear => match lower.as_str() {
417            "none" => Some(CssValue::Clear(css_display::Clear::None)),
418            "left" => Some(CssValue::Clear(css_display::Clear::Left)),
419            "right" => Some(CssValue::Clear(css_display::Clear::Right)),
420            "both" => Some(CssValue::Clear(css_display::Clear::Both)),
421            _ => None,
422        },
423        _ => None,
424    }
425}
426
427fn length_from_unit(value: f64, unit: &str) -> Option<css_length::Length> {
428    match unit.to_ascii_lowercase().as_str() {
429        "px" => Some(css_length::Length::Px(value)),
430        "em" => Some(css_length::Length::Em(value)),
431        "rem" => Some(css_length::Length::Rem(value)),
432        "vw" => Some(css_length::Length::Vw(value)),
433        "vh" => Some(css_length::Length::Vh(value)),
434        "vmin" => Some(css_length::Length::Vmin(value)),
435        "vmax" => Some(css_length::Length::Vmax(value)),
436        "cm" => Some(css_length::Length::Cm(value)),
437        "mm" => Some(css_length::Length::Mm(value)),
438        "in" => Some(css_length::Length::In(value)),
439        "pt" => Some(css_length::Length::Pt(value)),
440        "pc" => Some(css_length::Length::Pc(value)),
441        "ch" => Some(css_length::Length::Ch(value)),
442        "ex" => Some(css_length::Length::Ex(value)),
443        _ => None,
444    }
445}
446
447fn parse_dimension(prop: &PropertyId, value: f64, unit: &str) -> Option<CssValue> {
448    let length = length_from_unit(value, unit)?;
449    match prop {
450        PropertyId::Width
451        | PropertyId::Height
452        | PropertyId::MinWidth
453        | PropertyId::MinHeight
454        | PropertyId::MaxWidth
455        | PropertyId::MaxHeight => Some(CssValue::LengthPercentageAuto(
456            css_length::LengthPercentageAuto::Length(length),
457        )),
458        PropertyId::MarginTop
459        | PropertyId::MarginRight
460        | PropertyId::MarginBottom
461        | PropertyId::MarginLeft => Some(CssValue::LengthPercentageAuto(
462            css_length::LengthPercentageAuto::Length(length),
463        )),
464        PropertyId::PaddingTop
465        | PropertyId::PaddingRight
466        | PropertyId::PaddingBottom
467        | PropertyId::PaddingLeft => Some(CssValue::LengthPercentage(
468            css_length::LengthPercentage::Length(length),
469        )),
470        PropertyId::BorderTopWidth
471        | PropertyId::BorderRightWidth
472        | PropertyId::BorderBottomWidth
473        | PropertyId::BorderLeftWidth => Some(CssValue::Length(length)),
474        PropertyId::FlexBasis => Some(CssValue::LengthPercentageAuto(
475            css_length::LengthPercentageAuto::Length(length),
476        )),
477        PropertyId::RowGap | PropertyId::ColumnGap | PropertyId::Gap => Some(
478            CssValue::LengthPercentage(css_length::LengthPercentage::Length(length)),
479        ),
480        PropertyId::FontSize => Some(CssValue::Length(length)),
481        _ => None,
482    }
483}
484
485fn parse_number(prop: &PropertyId, value: f64) -> Option<CssValue> {
486    match prop {
487        PropertyId::FlexGrow => Some(CssValue::Number(value)),
488        PropertyId::FlexShrink => Some(CssValue::Number(value)),
489        PropertyId::FlexBasis => (value == 0.0).then_some(CssValue::LengthPercentageAuto(
490            css_length::LengthPercentageAuto::Length(css_length::Length::Zero),
491        )),
492        PropertyId::ZIndex => Some(CssValue::Integer(value as i32)),
493        PropertyId::Opacity => Some(CssValue::Number(value)),
494        PropertyId::FontWeight => {
495            let weight = match value as u32 {
496                700 => css_font::FontWeight::Bold,
497                _ => css_font::FontWeight::Numeric(value),
498            };
499            Some(CssValue::FontWeight(weight))
500        }
501        _ => None,
502    }
503}
504
505fn parse_percentage(prop: &PropertyId, value: f64) -> Option<CssValue> {
506    match prop {
507        PropertyId::Width
508        | PropertyId::Height
509        | PropertyId::MinWidth
510        | PropertyId::MinHeight
511        | PropertyId::MaxWidth
512        | PropertyId::MaxHeight => Some(CssValue::LengthPercentageAuto(
513            css_length::LengthPercentageAuto::Percentage(value),
514        )),
515        PropertyId::MarginTop
516        | PropertyId::MarginRight
517        | PropertyId::MarginBottom
518        | PropertyId::MarginLeft => Some(CssValue::LengthPercentageAuto(
519            css_length::LengthPercentageAuto::Percentage(value),
520        )),
521        PropertyId::PaddingTop
522        | PropertyId::PaddingRight
523        | PropertyId::PaddingBottom
524        | PropertyId::PaddingLeft => Some(CssValue::LengthPercentage(
525            css_length::LengthPercentage::Percentage(value),
526        )),
527        PropertyId::RowGap | PropertyId::ColumnGap | PropertyId::Gap => Some(
528            CssValue::LengthPercentage(css_length::LengthPercentage::Percentage(value)),
529        ),
530        _ => None,
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537    use crate::dom::{Attribute, QualName};
538
539    fn make_dom_with_styled_div(style: &str) -> Dom {
540        let mut dom = Dom::new();
541        let html = dom.create_element(QualName::new("html"), vec![]);
542        dom.append_child(NodeId::DOCUMENT, html);
543        let body = dom.create_element(QualName::new("body"), vec![]);
544        dom.append_child(html, body);
545        let div = dom.create_element(
546            QualName::new("div"),
547            vec![Attribute {
548                name: QualName::new("style"),
549                value: style.to_string(),
550            }],
551        );
552        dom.append_child(body, div);
553        dom
554    }
555
556    #[test]
557    fn layout_basic_div() {
558        let dom = make_dom_with_styled_div("width: 200px; height: 100px");
559        let viewport = Viewport::new(1920.0, 1080.0);
560        let mut engine = LayoutEngine::new(viewport);
561        engine.compute(&dom);
562
563        let html = dom.child_elements(NodeId::DOCUMENT)[0];
564        let body = dom.child_elements(html)[0];
565        let div = dom.child_elements(body)[0];
566
567        let rect = engine.get_bounding_rect(&dom, div);
568        assert!(
569            rect.width >= 200.0,
570            "width should be >= 200, got {}",
571            rect.width
572        );
573        assert!(
574            rect.height >= 100.0,
575            "height should be >= 100, got {}",
576            rect.height
577        );
578    }
579
580    #[test]
581    fn layout_text_node_has_size() {
582        let mut dom = Dom::new();
583        let html = dom.create_element(QualName::new("html"), vec![]);
584        dom.append_child(NodeId::DOCUMENT, html);
585        let body = dom.create_element(QualName::new("body"), vec![]);
586        dom.append_child(html, body);
587        let text = dom.create_text("Hello world".to_string());
588        dom.append_child(body, text);
589
590        let viewport = Viewport::new(1920.0, 1080.0);
591        let mut engine = LayoutEngine::new(viewport);
592        engine.compute(&dom);
593
594        let (w, h) = engine.taffy_size(text);
595        assert!(w > 0.0, "text width should be > 0, got {}", w);
596        assert!(h > 0.0, "text height should be > 0, got {}", h);
597    }
598
599    #[test]
600    fn layout_offset_width() {
601        let dom = make_dom_with_styled_div("width: 300px; height: 150px");
602        let viewport = Viewport::new(1920.0, 1080.0);
603        let mut engine = LayoutEngine::new(viewport);
604
605        let html = dom.child_elements(NodeId::DOCUMENT)[0];
606        let body = dom.child_elements(html)[0];
607        let div = dom.child_elements(body)[0];
608
609        let w = engine.get_offset_width(&dom, div);
610        assert!(w >= 300.0, "offsetWidth should be >= 300, got {}", w);
611        let h = engine.get_offset_height(&dom, div);
612        assert!(h >= 150.0, "offsetHeight should be >= 150, got {}", h);
613    }
614
615    #[test]
616    fn dirty_tracking() {
617        let dom = make_dom_with_styled_div("width: 100px");
618        let viewport = Viewport::new(1920.0, 1080.0);
619        let mut engine = LayoutEngine::new(viewport);
620
621        assert!(engine.dirty);
622        engine.compute(&dom);
623        assert!(!engine.dirty);
624        engine.mark_dirty();
625        assert!(engine.dirty);
626    }
627
628    #[test]
629    fn display_none_hides_node() {
630        let dom = make_dom_with_styled_div("display: none");
631        let viewport = Viewport::new(1920.0, 1080.0);
632        let mut engine = LayoutEngine::new(viewport);
633        engine.compute(&dom);
634
635        let html = dom.child_elements(NodeId::DOCUMENT)[0];
636        let body = dom.child_elements(html)[0];
637        let div = dom.child_elements(body)[0];
638
639        assert!(!engine.dom_to_taffy.contains_key(&div.to_raw()));
640    }
641
642    #[test]
643    fn flex_layout_sizes() {
644        let mut dom = Dom::new();
645        let html = dom.create_element(QualName::new("html"), vec![]);
646        dom.append_child(NodeId::DOCUMENT, html);
647        let body = dom.create_element(QualName::new("body"), vec![]);
648        dom.append_child(html, body);
649        let flex_container = dom.create_element(
650            QualName::new("div"),
651            vec![Attribute {
652                name: QualName::new("style"),
653                value: "display: flex; width: 500px; height: 200px".to_string(),
654            }],
655        );
656        dom.append_child(body, flex_container);
657        let child1 = dom.create_element(
658            QualName::new("div"),
659            vec![Attribute {
660                name: QualName::new("style"),
661                value: "width: 100px; height: 50px".to_string(),
662            }],
663        );
664        dom.append_child(flex_container, child1);
665        let child2 = dom.create_element(
666            QualName::new("div"),
667            vec![Attribute {
668                name: QualName::new("style"),
669                value: "width: 150px; height: 60px".to_string(),
670            }],
671        );
672        dom.append_child(flex_container, child2);
673
674        let viewport = Viewport::new(1920.0, 1080.0);
675        let mut engine = LayoutEngine::new(viewport);
676        engine.compute(&dom);
677
678        let w1 = engine.get_offset_width(&dom, child1);
679        assert!(w1 >= 100.0, "child1 width should be >= 100, got {}", w1);
680
681        let w2 = engine.get_offset_width(&dom, child2);
682        assert!(w2 >= 150.0, "child2 width should be >= 150, got {}", w2);
683
684        let x1 = engine.get_offset_left(&dom, child1);
685        let x2 = engine.get_offset_left(&dom, child2);
686        assert!(x2 > x1, "child2 should be to the right of child1");
687    }
688
689    #[test]
690    fn get_computed_style_returns_resolved() {
691        let dom = make_dom_with_styled_div("display: flex; width: 200px");
692        let viewport = Viewport::new(1920.0, 1080.0);
693        let mut engine = LayoutEngine::new(viewport);
694
695        let html = dom.child_elements(NodeId::DOCUMENT)[0];
696        let body = dom.child_elements(html)[0];
697        let div = dom.child_elements(body)[0];
698
699        let style = engine.get_computed_style(&dom, div);
700        assert_eq!(
701            style.get(&PropertyId::Display),
702            Some(&CssValue::Display(css_display::Display::Flex))
703        );
704    }
705
706    #[test]
707    fn dom_rect_from_layout() {
708        let layout = taffy::Layout::new();
709        let rect = DOMRect::from_taffy_layout(&layout);
710        assert_eq!(rect.width, 0.0);
711    }
712
713    #[test]
714    fn margin_offset() {
715        let mut dom = Dom::new();
716        let html = dom.create_element(QualName::new("html"), vec![]);
717        dom.append_child(NodeId::DOCUMENT, html);
718        let body = dom.create_element(QualName::new("body"), vec![]);
719        dom.append_child(html, body);
720        let div = dom.create_element(
721            QualName::new("div"),
722            vec![Attribute {
723                name: QualName::new("style"),
724                value: "width: 100px; height: 50px; margin-left: 30px; margin-top: 20px"
725                    .to_string(),
726            }],
727        );
728        dom.append_child(body, div);
729
730        let viewport = Viewport::new(1920.0, 1080.0);
731        let mut engine = LayoutEngine::new(viewport);
732        engine.compute(&dom);
733
734        let x = engine.get_offset_left(&dom, div);
735        let y = engine.get_offset_top(&dom, div);
736        assert!(x >= 30.0, "offsetLeft should be >= 30, got {}", x);
737        assert!(y >= 20.0, "offsetTop should be >= 20, got {}", y);
738    }
739}