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 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
301fn 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}