Skip to main content

foundry_runtime/
layout.rs

1use std::collections::HashMap;
2use taffy::prelude::*;
3
4use crate::scene::{
5    AlignItems, Display, ElementKind, FlexDirection, FlexWrap, JustifyContent, LayoutRect, NodeId,
6    Overflow, Position, ResolvedStyle, SceneGraph, SceneNode, SizeValue,
7};
8
9pub struct LayoutEngine {
10    tree: TaffyTree,
11    node_map: HashMap<NodeId, taffy::NodeId>,
12    reverse_map: HashMap<taffy::NodeId, NodeId>,
13}
14
15impl Default for LayoutEngine {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl LayoutEngine {
22    pub fn new() -> Self {
23        Self {
24            tree: TaffyTree::new(),
25            node_map: HashMap::new(),
26            reverse_map: HashMap::new(),
27        }
28    }
29
30    pub fn compute(&mut self, scene: &mut SceneGraph, viewport_width: f32, viewport_height: f32) {
31        self.tree = TaffyTree::new();
32        self.node_map.clear();
33        self.reverse_map.clear();
34
35        if let Some(root_id) = scene.root {
36            // Force html and body nodes to fill viewport (like browsers do)
37            for i in 0..scene.nodes.len() {
38                let tag = scene.nodes[i].tag.clone();
39                if tag == "html" || tag == "body" {
40                    if scene.nodes[i].style.width == SizeValue::Auto {
41                        scene.nodes[i].style.width = SizeValue::Px(viewport_width);
42                    }
43                    if scene.nodes[i].style.height == SizeValue::Auto {
44                        scene.nodes[i].style.min_height = SizeValue::Px(viewport_height);
45                    }
46                    // Body scrolls by default when content overflows
47                    if tag == "body" && scene.nodes[i].style.overflow == Overflow::Visible {
48                        scene.nodes[i].style.overflow = Overflow::Scroll;
49                    }
50                }
51            }
52
53            let taffy_root = self.build_taffy_tree(scene, root_id, viewport_width, viewport_height);
54
55            self.tree
56                .compute_layout(
57                    taffy_root,
58                    Size {
59                        width: AvailableSpace::Definite(viewport_width),
60                        height: AvailableSpace::Definite(viewport_height),
61                    },
62                )
63                .ok();
64
65            self.apply_layout(scene, root_id, 0.0, 0.0);
66        }
67    }
68
69    fn build_taffy_tree(
70        &mut self,
71        scene: &SceneGraph,
72        node_id: NodeId,
73        vw: f32,
74        vh: f32,
75    ) -> taffy::NodeId {
76        let node = scene.get(node_id);
77        let style = &node.style;
78
79        let taffy_style = self.convert_style(style, node, vw, vh, scene);
80
81        let children: Vec<taffy::NodeId> = node
82            .children
83            .iter()
84            .map(|&child_id| self.build_taffy_tree(scene, child_id, vw, vh))
85            .collect();
86
87        let taffy_node = self.tree.new_with_children(taffy_style, &children).unwrap();
88        self.node_map.insert(node_id, taffy_node);
89        self.reverse_map.insert(taffy_node, node_id);
90        taffy_node
91    }
92
93    fn convert_style(
94        &self,
95        style: &ResolvedStyle,
96        node: &SceneNode,
97        vw: f32,
98        vh: f32,
99        scene_graph: &SceneGraph,
100    ) -> Style {
101        self.build_taffy_style(style, node, vw, vh, scene_graph)
102    }
103
104    #[allow(clippy::field_reassign_with_default)]
105    fn build_taffy_style(
106        &self,
107        style: &ResolvedStyle,
108        node: &SceneNode,
109        vw: f32,
110        vh: f32,
111        scene_graph: &SceneGraph,
112    ) -> Style {
113        let mut ts = Style::default();
114
115        // Display
116        ts.display = match style.display {
117            Display::Flex => taffy::Display::Flex,
118            Display::Block => taffy::Display::Block,
119            Display::None => taffy::Display::None,
120            Display::Inline => taffy::Display::Flex, // approximate inline as flex
121        };
122
123        // Flex properties
124        ts.flex_direction = match style.flex_direction {
125            FlexDirection::Row => taffy::FlexDirection::Row,
126            FlexDirection::Column => taffy::FlexDirection::Column,
127            FlexDirection::RowReverse => taffy::FlexDirection::RowReverse,
128            FlexDirection::ColumnReverse => taffy::FlexDirection::ColumnReverse,
129        };
130
131        ts.justify_content = Some(match style.justify_content {
132            JustifyContent::Start => taffy::JustifyContent::FlexStart,
133            JustifyContent::Center => taffy::JustifyContent::Center,
134            JustifyContent::End => taffy::JustifyContent::FlexEnd,
135            JustifyContent::SpaceBetween => taffy::JustifyContent::SpaceBetween,
136            JustifyContent::SpaceAround => taffy::JustifyContent::SpaceAround,
137            JustifyContent::SpaceEvenly => taffy::JustifyContent::SpaceEvenly,
138        });
139
140        ts.align_items = Some(match style.align_items {
141            AlignItems::Stretch => taffy::AlignItems::Stretch,
142            AlignItems::Start => taffy::AlignItems::FlexStart,
143            AlignItems::Center => taffy::AlignItems::Center,
144            AlignItems::End => taffy::AlignItems::FlexEnd,
145        });
146
147        ts.flex_wrap = match style.flex_wrap {
148            FlexWrap::NoWrap => taffy::FlexWrap::NoWrap,
149            FlexWrap::Wrap => taffy::FlexWrap::Wrap,
150        };
151
152        ts.flex_grow = style.flex_grow;
153        ts.flex_shrink = style.flex_shrink;
154        ts.gap = Size {
155            width: LengthPercentage::Length(style.gap),
156            height: LengthPercentage::Length(style.gap),
157        };
158
159        // Position
160        ts.position = match style.position {
161            Position::Relative => taffy::Position::Relative,
162            Position::Absolute | Position::Fixed => taffy::Position::Absolute,
163        };
164
165        ts.inset = Rect {
166            top: self.convert_length_auto(style.top, vw, vh),
167            right: self.convert_length_auto(style.right, vw, vh),
168            bottom: self.convert_length_auto(style.bottom, vw, vh),
169            left: self.convert_length_auto(style.left, vw, vh),
170        };
171
172        // Size
173        ts.size = Size {
174            width: self.convert_dimension(style.font_size, style.width, vw, vh),
175            height: self.convert_dimension(style.font_size, style.height, vw, vh),
176        };
177        ts.min_size = Size {
178            width: self.convert_dimension(style.font_size, style.min_width, vw, vh),
179            height: self.convert_dimension(style.font_size, style.min_height, vw, vh),
180        };
181        ts.max_size = Size {
182            width: self.convert_dimension(style.font_size, style.max_width, vw, vh),
183            height: self.convert_dimension(style.font_size, style.max_height, vw, vh),
184        };
185
186        // Margin
187        ts.margin = Rect {
188            top: LengthPercentageAuto::Length(style.margin[0]),
189            right: LengthPercentageAuto::Length(style.margin[1]),
190            bottom: LengthPercentageAuto::Length(style.margin[2]),
191            left: LengthPercentageAuto::Length(style.margin[3]),
192        };
193
194        // Padding
195        ts.padding = Rect {
196            top: LengthPercentage::Length(style.padding[0]),
197            right: LengthPercentage::Length(style.padding[1]),
198            bottom: LengthPercentage::Length(style.padding[2]),
199            left: LengthPercentage::Length(style.padding[3]),
200        };
201
202        // Border
203        ts.border = Rect {
204            top: LengthPercentage::Length(style.border_width[0]),
205            right: LengthPercentage::Length(style.border_width[1]),
206            bottom: LengthPercentage::Length(style.border_width[2]),
207            left: LengthPercentage::Length(style.border_width[3]),
208        };
209
210        // Text nodes: inherit font properties from parent for sizing
211        if node.kind == ElementKind::Text {
212            if let Some(text) = &node.text_content {
213                // Use parent's font size since CSS is applied to the container, not the text node
214                let font_size = if let Some(parent_id) = node.parent {
215                    scene_graph.get(parent_id).style.font_size
216                } else {
217                    style.font_size
218                };
219                let font_weight = if let Some(parent_id) = node.parent {
220                    scene_graph.get(parent_id).style.font_weight
221                } else {
222                    style.font_weight
223                };
224                let line_height = if let Some(parent_id) = node.parent {
225                    scene_graph.get(parent_id).style.line_height
226                } else {
227                    style.line_height
228                };
229
230                let char_count = text.chars().count() as f32;
231                let ratio = if font_weight >= 700 { 0.72 } else { 0.62 };
232                let text_width = char_count * font_size * ratio;
233                let text_height = font_size * line_height;
234                ts.size = Size {
235                    width: Dimension::Length(text_width),
236                    height: Dimension::Length(text_height),
237                };
238            }
239        }
240
241        // Block display: default to column layout for children
242        if style.display == Display::Block {
243            ts.display = taffy::Display::Flex;
244            ts.flex_direction = taffy::FlexDirection::Column;
245        }
246
247        ts
248    }
249
250    fn convert_dimension(&self, font_size: f32, val: SizeValue, vw: f32, vh: f32) -> Dimension {
251        match val {
252            SizeValue::Px(v) => Dimension::Length(v),
253            SizeValue::Percent(v) => Dimension::Percent(v / 100.0),
254            SizeValue::Em(v) => Dimension::Length(v * font_size),
255            SizeValue::Rem(v) => Dimension::Length(v * 16.0),
256            SizeValue::Vh(v) => Dimension::Length(v / 100.0 * vh),
257            SizeValue::Vw(v) => Dimension::Length(v / 100.0 * vw),
258            SizeValue::Auto => Dimension::Auto,
259        }
260    }
261
262    fn convert_length_auto(&self, val: SizeValue, vw: f32, vh: f32) -> LengthPercentageAuto {
263        match val {
264            SizeValue::Px(v) => LengthPercentageAuto::Length(v),
265            SizeValue::Percent(v) => LengthPercentageAuto::Percent(v / 100.0),
266            SizeValue::Vh(v) => LengthPercentageAuto::Length(v / 100.0 * vh),
267            SizeValue::Vw(v) => LengthPercentageAuto::Length(v / 100.0 * vw),
268            _ => LengthPercentageAuto::Auto,
269        }
270    }
271
272    fn apply_layout(&self, scene: &mut SceneGraph, node_id: NodeId, parent_x: f32, parent_y: f32) {
273        if let Some(&taffy_node) = self.node_map.get(&node_id) {
274            if let Ok(layout) = self.tree.layout(taffy_node) {
275                let x = parent_x + layout.location.x;
276                let y = parent_y + layout.location.y;
277
278                let scene_node = scene.get_mut(node_id);
279                scene_node.layout = LayoutRect {
280                    x,
281                    y,
282                    width: layout.size.width,
283                    height: layout.size.height,
284                };
285                scene_node.dirty = false;
286
287                let children: Vec<NodeId> = scene_node.children.clone();
288                for child_id in children {
289                    self.apply_layout(scene, child_id, x, y);
290                }
291
292                // Compute content_height from children (for scroll)
293                let mut max_bottom: f32 = 0.0;
294                for &child_id in &scene.get(node_id).children {
295                    let child = scene.get(child_id);
296                    let child_bottom = child.layout.y + child.layout.height - y;
297                    if child_bottom > max_bottom {
298                        max_bottom = child_bottom;
299                    }
300                }
301                scene.get_mut(node_id).content_height = max_bottom;
302            }
303        }
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use crate::scene::Color;
311
312    /// Build a scene with a single root element of the given tag.
313    fn scene_with_root(tag: &str) -> (SceneGraph, NodeId) {
314        let mut scene = SceneGraph::new();
315        let root = scene.add_node(ElementKind::from_tag(tag), tag.to_string());
316        (scene, root)
317    }
318
319    fn child(scene: &mut SceneGraph, parent: NodeId, tag: &str) -> NodeId {
320        let id = scene.add_node(ElementKind::from_tag(tag), tag.to_string());
321        scene.add_child(parent, id);
322        id
323    }
324
325    fn text_child(scene: &mut SceneGraph, parent: NodeId, text: &str) -> NodeId {
326        let id = scene.add_node(ElementKind::Text, "#text".to_string());
327        scene.get_mut(id).text_content = Some(text.to_string());
328        scene.add_child(parent, id);
329        id
330    }
331
332    #[test]
333    fn flex_row_children_are_laid_out_side_by_side() {
334        let (mut scene, root) = scene_with_root("div");
335        scene.get_mut(root).style.display = Display::Flex;
336        scene.get_mut(root).style.flex_direction = FlexDirection::Row;
337        scene.get_mut(root).style.width = SizeValue::Px(300.0);
338        scene.get_mut(root).style.height = SizeValue::Px(100.0);
339
340        let a = child(&mut scene, root, "div");
341        let b = child(&mut scene, root, "div");
342        for id in [a, b] {
343            scene.get_mut(id).style.width = SizeValue::Px(50.0);
344            scene.get_mut(id).style.height = SizeValue::Px(20.0);
345        }
346
347        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
348
349        assert_eq!(scene.get(root).layout.width, 300.0);
350        assert_eq!(scene.get(a).layout.x, 0.0);
351        assert_eq!(scene.get(b).layout.x, 50.0);
352        assert_eq!(scene.get(a).layout.y, scene.get(b).layout.y);
353        assert_eq!(scene.get(a).layout.width, 50.0);
354    }
355
356    #[test]
357    fn flex_gap_separates_children() {
358        let (mut scene, root) = scene_with_root("div");
359        scene.get_mut(root).style.display = Display::Flex;
360        scene.get_mut(root).style.width = SizeValue::Px(300.0);
361        scene.get_mut(root).style.gap = 12.0;
362
363        let a = child(&mut scene, root, "div");
364        let b = child(&mut scene, root, "div");
365        for id in [a, b] {
366            scene.get_mut(id).style.width = SizeValue::Px(50.0);
367            scene.get_mut(id).style.height = SizeValue::Px(20.0);
368        }
369
370        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
371
372        assert_eq!(scene.get(b).layout.x - scene.get(a).layout.x, 62.0);
373    }
374
375    #[test]
376    fn nested_flex_positions_are_absolute_not_relative() {
377        // outer (padding 10) > middle (column, margin-top 5) > inner
378        let (mut scene, outer) = scene_with_root("div");
379        scene.get_mut(outer).style.display = Display::Flex;
380        scene.get_mut(outer).style.width = SizeValue::Px(400.0);
381        scene.get_mut(outer).style.height = SizeValue::Px(300.0);
382        scene.get_mut(outer).style.padding = [10.0, 10.0, 10.0, 10.0];
383
384        let middle = child(&mut scene, outer, "div");
385        scene.get_mut(middle).style.display = Display::Flex;
386        scene.get_mut(middle).style.flex_direction = FlexDirection::Column;
387        scene.get_mut(middle).style.width = SizeValue::Px(200.0);
388        scene.get_mut(middle).style.height = SizeValue::Px(200.0);
389        scene.get_mut(middle).style.margin = [5.0, 0.0, 0.0, 0.0];
390
391        let inner = child(&mut scene, middle, "div");
392        scene.get_mut(inner).style.width = SizeValue::Px(30.0);
393        scene.get_mut(inner).style.height = SizeValue::Px(30.0);
394
395        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
396
397        // middle sits inside the padding box of outer, offset by its own top margin
398        assert_eq!(scene.get(middle).layout.x, 10.0);
399        assert_eq!(scene.get(middle).layout.y, 15.0);
400        // inner coordinates are accumulated through both ancestors
401        assert_eq!(scene.get(inner).layout.x, 10.0);
402        assert_eq!(scene.get(inner).layout.y, 15.0);
403        assert_eq!(scene.get(inner).layout.width, 30.0);
404    }
405
406    #[test]
407    fn padding_shrinks_the_content_box_of_a_grown_child() {
408        let (mut scene, root) = scene_with_root("div");
409        scene.get_mut(root).style.display = Display::Flex;
410        scene.get_mut(root).style.width = SizeValue::Px(200.0);
411        scene.get_mut(root).style.height = SizeValue::Px(100.0);
412        scene.get_mut(root).style.padding = [10.0, 20.0, 10.0, 20.0];
413
414        let inner = child(&mut scene, root, "div");
415        scene.get_mut(inner).style.flex_grow = 1.0;
416
417        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
418
419        let l = &scene.get(inner).layout;
420        assert_eq!(l.x, 20.0);
421        assert_eq!(l.y, 10.0);
422        assert_eq!(l.width, 160.0); // 200 - 20 - 20
423        assert_eq!(l.height, 80.0); // 100 - 10 - 10 (align-items: stretch)
424    }
425
426    #[test]
427    fn margins_offset_a_child_on_every_side() {
428        let (mut scene, root) = scene_with_root("div");
429        scene.get_mut(root).style.display = Display::Flex;
430        scene.get_mut(root).style.width = SizeValue::Px(200.0);
431        scene.get_mut(root).style.height = SizeValue::Px(200.0);
432
433        let inner = child(&mut scene, root, "div");
434        // top, right, bottom, left
435        scene.get_mut(inner).style.margin = [7.0, 11.0, 13.0, 17.0];
436        scene.get_mut(inner).style.flex_grow = 1.0;
437
438        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
439
440        let l = &scene.get(inner).layout;
441        assert_eq!(l.x, 17.0);
442        assert_eq!(l.y, 7.0);
443        assert_eq!(l.width, 200.0 - 17.0 - 11.0);
444        assert_eq!(l.height, 200.0 - 7.0 - 13.0);
445    }
446
447    #[test]
448    fn border_width_is_part_of_the_box() {
449        let (mut scene, root) = scene_with_root("div");
450        scene.get_mut(root).style.display = Display::Flex;
451        scene.get_mut(root).style.width = SizeValue::Px(100.0);
452        scene.get_mut(root).style.height = SizeValue::Px(100.0);
453        scene.get_mut(root).style.border_width = [4.0; 4];
454        scene.get_mut(root).style.border_color = Color::BLACK;
455
456        let inner = child(&mut scene, root, "div");
457        scene.get_mut(inner).style.flex_grow = 1.0;
458
459        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
460
461        // border-box: the outer node keeps its declared size, the child is inset
462        assert_eq!(scene.get(root).layout.width, 100.0);
463        assert_eq!(scene.get(inner).layout.x, 4.0);
464        assert_eq!(scene.get(inner).layout.width, 92.0);
465    }
466
467    #[test]
468    fn percent_em_rem_and_viewport_units_resolve() {
469        let (mut scene, root) = scene_with_root("div");
470        scene.get_mut(root).style.display = Display::Flex;
471        scene.get_mut(root).style.width = SizeValue::Px(400.0);
472        scene.get_mut(root).style.height = SizeValue::Px(400.0);
473
474        let pct = child(&mut scene, root, "div");
475        scene.get_mut(pct).style.width = SizeValue::Percent(25.0);
476        scene.get_mut(pct).style.height = SizeValue::Px(10.0);
477
478        let em = child(&mut scene, root, "div");
479        scene.get_mut(em).style.font_size = 20.0;
480        scene.get_mut(em).style.width = SizeValue::Em(2.0);
481        scene.get_mut(em).style.height = SizeValue::Px(10.0);
482
483        let rem = child(&mut scene, root, "div");
484        scene.get_mut(rem).style.font_size = 20.0; // rem must ignore local font-size
485        scene.get_mut(rem).style.width = SizeValue::Rem(2.0);
486        scene.get_mut(rem).style.height = SizeValue::Px(10.0);
487
488        let vw = child(&mut scene, root, "div");
489        scene.get_mut(vw).style.width = SizeValue::Vw(50.0);
490        scene.get_mut(vw).style.height = SizeValue::Vh(10.0);
491
492        // no shrinking: assert the resolved sizes, not the flex fallout
493        for id in [pct, em, rem, vw] {
494            scene.get_mut(id).style.flex_shrink = 0.0;
495        }
496
497        LayoutEngine::new().compute(&mut scene, 1000.0, 600.0);
498
499        assert_eq!(scene.get(pct).layout.width, 100.0); // 25% of 400
500        assert_eq!(scene.get(em).layout.width, 40.0); // 2em at 20px
501        assert_eq!(scene.get(rem).layout.width, 32.0); // 2rem at the 16px root size
502        assert_eq!(scene.get(vw).layout.width, 500.0); // 50vw of 1000
503        assert_eq!(scene.get(vw).layout.height, 60.0); // 10vh of 600
504    }
505
506    #[test]
507    fn absolute_positioning_uses_inset() {
508        let (mut scene, root) = scene_with_root("div");
509        scene.get_mut(root).style.display = Display::Flex;
510        scene.get_mut(root).style.width = SizeValue::Px(300.0);
511        scene.get_mut(root).style.height = SizeValue::Px(300.0);
512
513        let abs = child(&mut scene, root, "div");
514        scene.get_mut(abs).style.position = Position::Absolute;
515        scene.get_mut(abs).style.top = SizeValue::Px(25.0);
516        scene.get_mut(abs).style.left = SizeValue::Px(40.0);
517        scene.get_mut(abs).style.width = SizeValue::Px(50.0);
518        scene.get_mut(abs).style.height = SizeValue::Px(50.0);
519
520        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
521
522        assert_eq!(scene.get(abs).layout.x, 40.0);
523        assert_eq!(scene.get(abs).layout.y, 25.0);
524    }
525
526    #[test]
527    fn display_none_collapses_the_box() {
528        let (mut scene, root) = scene_with_root("div");
529        scene.get_mut(root).style.display = Display::Flex;
530        scene.get_mut(root).style.width = SizeValue::Px(200.0);
531        scene.get_mut(root).style.height = SizeValue::Px(200.0);
532
533        let hidden = child(&mut scene, root, "div");
534        scene.get_mut(hidden).style.display = Display::None;
535        scene.get_mut(hidden).style.width = SizeValue::Px(80.0);
536        scene.get_mut(hidden).style.height = SizeValue::Px(80.0);
537
538        let shown = child(&mut scene, root, "div");
539        scene.get_mut(shown).style.width = SizeValue::Px(80.0);
540        scene.get_mut(shown).style.height = SizeValue::Px(80.0);
541
542        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
543
544        assert_eq!(scene.get(hidden).layout.width, 0.0);
545        assert_eq!(scene.get(hidden).layout.height, 0.0);
546        // the hidden box takes no space in the main axis
547        assert_eq!(scene.get(shown).layout.x, 0.0);
548    }
549
550    #[test]
551    fn block_display_stacks_children_vertically() {
552        let (mut scene, root) = scene_with_root("div");
553        scene.get_mut(root).style.display = Display::Block;
554        scene.get_mut(root).style.width = SizeValue::Px(200.0);
555        scene.get_mut(root).style.height = SizeValue::Px(200.0);
556
557        let a = child(&mut scene, root, "div");
558        let b = child(&mut scene, root, "div");
559        for id in [a, b] {
560            scene.get_mut(id).style.width = SizeValue::Px(50.0);
561            scene.get_mut(id).style.height = SizeValue::Px(30.0);
562        }
563
564        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
565
566        assert_eq!(scene.get(a).layout.y, 0.0);
567        assert_eq!(scene.get(b).layout.y, 30.0);
568        assert_eq!(scene.get(a).layout.x, scene.get(b).layout.x);
569    }
570
571    #[test]
572    fn text_nodes_are_measured_from_the_parent_font() {
573        let (mut scene, root) = scene_with_root("div");
574        scene.get_mut(root).style.display = Display::Flex;
575        scene.get_mut(root).style.width = SizeValue::Px(1000.0);
576        scene.get_mut(root).style.height = SizeValue::Px(200.0);
577
578        let p = child(&mut scene, root, "p");
579        scene.get_mut(p).style.font_size = 20.0;
580        scene.get_mut(p).style.line_height = 1.5;
581        let t = text_child(&mut scene, p, "hello"); // 5 chars
582
583        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
584
585        let l = &scene.get(t).layout;
586        // the regular-weight advance estimate is 0.62em per character
587        assert!(
588            (l.width - 5.0 * 20.0 * 0.62).abs() < 0.01,
589            "width {}",
590            l.width
591        );
592        assert!((l.height - 20.0 * 1.5).abs() < 0.01, "height {}", l.height);
593    }
594
595    #[test]
596    fn bold_text_is_measured_wider_than_regular_text() {
597        let mut widths = Vec::new();
598        for weight in [400u16, 700u16] {
599            let (mut scene, root) = scene_with_root("div");
600            scene.get_mut(root).style.display = Display::Flex;
601            scene.get_mut(root).style.width = SizeValue::Px(1000.0);
602            let p = child(&mut scene, root, "p");
603            scene.get_mut(p).style.font_weight = weight;
604            let t = text_child(&mut scene, p, "hello world");
605            LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
606            widths.push(scene.get(t).layout.width);
607        }
608        assert!(widths[1] > widths[0], "{:?}", widths);
609    }
610
611    #[test]
612    fn html_and_body_are_stretched_to_the_viewport() {
613        let mut scene = SceneGraph::new();
614        let html = scene.add_node(ElementKind::from_tag("html"), "html".to_string());
615        let body = child(&mut scene, html, "body");
616
617        LayoutEngine::new().compute(&mut scene, 1024.0, 768.0);
618
619        assert_eq!(scene.get(html).layout.width, 1024.0);
620        assert_eq!(scene.get(body).layout.width, 1024.0);
621        assert!(scene.get(html).layout.height >= 768.0);
622        // body becomes scrollable so overflowing content can be reached
623        assert_eq!(scene.get(body).style.overflow, Overflow::Scroll);
624    }
625
626    #[test]
627    fn explicit_html_size_is_not_overridden() {
628        let mut scene = SceneGraph::new();
629        let html = scene.add_node(ElementKind::from_tag("html"), "html".to_string());
630        scene.get_mut(html).style.width = SizeValue::Px(300.0);
631
632        LayoutEngine::new().compute(&mut scene, 1024.0, 768.0);
633
634        assert_eq!(scene.get(html).layout.width, 300.0);
635    }
636
637    #[test]
638    fn content_height_tracks_overflowing_children() {
639        let (mut scene, root) = scene_with_root("div");
640        scene.get_mut(root).style.display = Display::Flex;
641        scene.get_mut(root).style.flex_direction = FlexDirection::Column;
642        scene.get_mut(root).style.width = SizeValue::Px(200.0);
643        scene.get_mut(root).style.height = SizeValue::Px(100.0);
644        scene.get_mut(root).style.overflow = Overflow::Scroll;
645
646        for _ in 0..4 {
647            let c = child(&mut scene, root, "div");
648            scene.get_mut(c).style.height = SizeValue::Px(60.0);
649            scene.get_mut(c).style.flex_shrink = 0.0;
650        }
651
652        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
653
654        assert_eq!(scene.get(root).layout.height, 100.0);
655        assert_eq!(scene.get(root).content_height, 240.0);
656    }
657
658    #[test]
659    fn min_and_max_size_constraints_are_applied() {
660        let (mut scene, root) = scene_with_root("div");
661        scene.get_mut(root).style.display = Display::Flex;
662        scene.get_mut(root).style.width = SizeValue::Px(400.0);
663        scene.get_mut(root).style.height = SizeValue::Px(400.0);
664
665        let clamped = child(&mut scene, root, "div");
666        scene.get_mut(clamped).style.width = SizeValue::Px(300.0);
667        scene.get_mut(clamped).style.max_width = SizeValue::Px(120.0);
668        scene.get_mut(clamped).style.height = SizeValue::Px(10.0);
669        scene.get_mut(clamped).style.min_height = SizeValue::Px(50.0);
670
671        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
672
673        assert_eq!(scene.get(clamped).layout.width, 120.0);
674        assert_eq!(scene.get(clamped).layout.height, 50.0);
675    }
676
677    #[test]
678    fn justify_content_center_centres_the_child() {
679        let (mut scene, root) = scene_with_root("div");
680        scene.get_mut(root).style.display = Display::Flex;
681        scene.get_mut(root).style.justify_content = JustifyContent::Center;
682        scene.get_mut(root).style.align_items = AlignItems::Center;
683        scene.get_mut(root).style.width = SizeValue::Px(200.0);
684        scene.get_mut(root).style.height = SizeValue::Px(200.0);
685
686        let inner = child(&mut scene, root, "div");
687        scene.get_mut(inner).style.width = SizeValue::Px(40.0);
688        scene.get_mut(inner).style.height = SizeValue::Px(40.0);
689
690        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
691
692        assert_eq!(scene.get(inner).layout.x, 80.0);
693        assert_eq!(scene.get(inner).layout.y, 80.0);
694    }
695
696    #[test]
697    fn layout_clears_the_dirty_flag() {
698        let (mut scene, root) = scene_with_root("div");
699        scene.get_mut(root).style.width = SizeValue::Px(10.0);
700        scene.get_mut(root).style.height = SizeValue::Px(10.0);
701        scene.get_mut(root).dirty = true;
702
703        LayoutEngine::new().compute(&mut scene, 800.0, 600.0);
704
705        assert!(!scene.get(root).dirty);
706    }
707}