Skip to main content

appscale_core/
layout.rs

1//! Layout Engine — Taffy integration for Flexbox + CSS Grid computation.
2//!
3//! The layout engine owns the Taffy tree and computes absolute positions
4//! for every node. It runs on a background thread (layout can be expensive)
5//! and produces a LayoutResult that the mount phase consumes.
6
7use crate::tree::{NodeId, ShadowTree};
8use crate::platform::PlatformBridge;
9use rustc_hash::FxHashMap;
10use serde::{Serialize, Deserialize};
11use taffy::prelude::*;
12
13// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14// Public types
15// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16
17/// Framework-level layout style (what the developer writes in JSX).
18/// This is a subset of CSS that maps cleanly to Taffy.
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20pub struct LayoutStyle {
21    #[serde(default)]
22    pub display: Display,
23    #[serde(default)]
24    pub position: Position,
25    #[serde(default)]
26    pub flex_direction: FlexDirection,
27    #[serde(default)]
28    pub flex_wrap: FlexWrap,
29    #[serde(default)]
30    pub flex_grow: f32,
31    #[serde(default = "default_flex_shrink")]
32    pub flex_shrink: f32,
33    #[serde(default)]
34    pub justify_content: Option<JustifyContent>,
35    #[serde(default)]
36    pub align_items: Option<AlignItems>,
37    #[serde(default)]
38    pub width: Dimension,
39    #[serde(default)]
40    pub height: Dimension,
41    #[serde(default)]
42    pub min_width: Dimension,
43    #[serde(default)]
44    pub min_height: Dimension,
45    #[serde(default)]
46    pub max_width: Dimension,
47    #[serde(default)]
48    pub max_height: Dimension,
49    #[serde(default)]
50    pub aspect_ratio: Option<f32>,
51    #[serde(default)]
52    pub margin: Edges,
53    #[serde(default)]
54    pub padding: Edges,
55    #[serde(default)]
56    pub gap: f32,
57    #[serde(default)]
58    pub overflow: Overflow,
59}
60
61fn default_flex_shrink() -> f32 { 1.0 }
62
63#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
64pub enum Display { #[default] Flex, Grid, None }
65
66#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
67pub enum Position { #[default] Relative, Absolute }
68
69#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
70pub enum FlexDirection { #[default] Column, Row, ColumnReverse, RowReverse }
71
72#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
73pub enum FlexWrap { #[default] NoWrap, Wrap, WrapReverse }
74
75#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
76pub enum JustifyContent { FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround, SpaceEvenly }
77
78#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
79pub enum AlignItems { FlexStart, FlexEnd, Center, Stretch, Baseline }
80
81#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
82pub enum Dimension { #[default] Auto, Points(f32), Percent(f32) }
83
84#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
85pub enum Overflow { #[default] Visible, Hidden, Scroll }
86
87#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
88pub struct Edges {
89    #[serde(default)]
90    pub top: f32,
91    #[serde(default)]
92    pub right: f32,
93    #[serde(default)]
94    pub bottom: f32,
95    #[serde(default)]
96    pub left: f32,
97}
98
99/// Computed layout for a single node — absolute screen coordinates.
100#[derive(Debug, Clone, Copy, Default)]
101pub struct ComputedLayout {
102    pub x: f32,
103    pub y: f32,
104    pub width: f32,
105    pub height: f32,
106}
107
108// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
109// Layout engine
110// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
111
112pub struct LayoutEngine {
113    taffy: TaffyTree<NodeId>,
114    node_map: FxHashMap<NodeId, taffy::NodeId>,
115    computed: FxHashMap<NodeId, ComputedLayout>,
116    root: Option<NodeId>,
117}
118
119impl LayoutEngine {
120    pub fn new() -> Self {
121        Self {
122            taffy: TaffyTree::new(),
123            node_map: FxHashMap::default(),
124            computed: FxHashMap::default(),
125            root: None,
126        }
127    }
128
129    pub fn set_root(&mut self, id: NodeId) {
130        self.root = Some(id);
131    }
132
133    pub fn create_node(&mut self, id: NodeId, style: &LayoutStyle) -> Result<(), LayoutError> {
134        let taffy_style = convert_style(style);
135        let taffy_node = self.taffy
136            .new_leaf_with_context(taffy_style, id)
137            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
138        self.node_map.insert(id, taffy_node);
139        Ok(())
140    }
141
142    pub fn update_style(&mut self, id: NodeId, style: &LayoutStyle) -> Result<(), LayoutError> {
143        let taffy_node = self.node_map.get(&id)
144            .ok_or(LayoutError::NodeNotFound(id))?;
145        let taffy_style = convert_style(style);
146        self.taffy.set_style(*taffy_node, taffy_style)
147            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
148        Ok(())
149    }
150
151    pub fn remove_node(&mut self, id: NodeId) {
152        if let Some(taffy_node) = self.node_map.remove(&id) {
153            let _ = self.taffy.remove(taffy_node);
154        }
155        self.computed.remove(&id);
156    }
157
158    /// Sync children from the shadow tree to the Taffy tree.
159    pub fn set_children_from_tree(
160        &mut self,
161        parent_id: NodeId,
162        tree: &ShadowTree,
163    ) -> Result<(), LayoutError> {
164        let parent_taffy = *self.node_map.get(&parent_id)
165            .ok_or(LayoutError::NodeNotFound(parent_id))?;
166
167        let children: Vec<taffy::NodeId> = tree.children_of(parent_id)
168            .iter()
169            .filter_map(|id| self.node_map.get(id).copied())
170            .collect();
171
172        self.taffy.set_children(parent_taffy, &children)
173            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
174        Ok(())
175    }
176
177    /// Compute layout for the entire tree.
178    pub fn compute(
179        &mut self,
180        tree: &ShadowTree,
181        screen_width: f32,
182        screen_height: f32,
183        platform: &dyn PlatformBridge,
184    ) -> Result<(), LayoutError> {
185        let root_id = self.root.ok_or(LayoutError::NoRoot)?;
186        let root_taffy = *self.node_map.get(&root_id)
187            .ok_or(LayoutError::NodeNotFound(root_id))?;
188
189        let available = taffy::Size {
190            width: AvailableSpace::Definite(screen_width),
191            height: AvailableSpace::Definite(screen_height),
192        };
193
194        // Compute with text measurement callback
195        self.taffy.compute_layout_with_measure(
196            root_taffy,
197            available,
198            |known_dims, available_space, _node_id, node_context, _style| {
199                if let Some(framework_id) = node_context {
200                    // Check if this is a Text node that needs measurement
201                    if let Some(node) = tree.get(*framework_id) {
202                        if node.view_type == crate::platform::ViewType::Text {
203                            if let Some(crate::platform::PropValue::String(text)) = node.props.get("text") {
204                                let max_w = match available_space.width {
205                                    AvailableSpace::Definite(w) => w,
206                                    _ => f32::INFINITY,
207                                };
208                                let metrics = platform.measure_text(
209                                    text,
210                                    &crate::platform::TextStyle::default(),
211                                    max_w,
212                                );
213                                return taffy::Size {
214                                    width: metrics.width,
215                                    height: metrics.height,
216                                };
217                            }
218                        }
219                    }
220                }
221                taffy::Size::ZERO
222            },
223        ).map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
224
225        // Collect computed layouts with absolute positions
226        self.computed.clear();
227        self.collect_layouts(root_taffy, 0.0, 0.0);
228
229        Ok(())
230    }
231
232    fn collect_layouts(&mut self, node: taffy::NodeId, parent_x: f32, parent_y: f32) {
233        let layout = self.taffy.layout(node).unwrap();
234        let abs_x = parent_x + layout.location.x;
235        let abs_y = parent_y + layout.location.y;
236
237        let framework_id = self.taffy.get_node_context(node).copied();
238        if let Some(fid) = framework_id {
239            self.computed.insert(fid, ComputedLayout {
240                x: abs_x,
241                y: abs_y,
242                width: layout.size.width,
243                height: layout.size.height,
244            });
245        }
246
247        let children = self.taffy.children(node).unwrap();
248        for &child in &children {
249            self.collect_layouts(child, abs_x, abs_y);
250        }
251    }
252
253    /// Get computed layout for a node.
254    pub fn get_computed(&self, id: NodeId) -> Option<&ComputedLayout> {
255        self.computed.get(&id)
256    }
257
258    /// Hit test: find nodes at a given screen coordinate.
259    /// Returns nodes sorted by specificity (smallest area first).
260    pub fn hit_test(&self, x: f32, y: f32) -> Vec<NodeId> {
261        let mut hits: Vec<(NodeId, f32)> = self.computed.iter()
262            .filter(|(_, layout)| {
263                x >= layout.x && x <= layout.x + layout.width &&
264                y >= layout.y && y <= layout.y + layout.height
265            })
266            .map(|(&id, layout)| (id, layout.width * layout.height))
267            .collect();
268
269        hits.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
270        hits.into_iter().map(|(id, _)| id).collect()
271    }
272}
273
274// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
275// Style conversion
276// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
277
278fn convert_style(style: &LayoutStyle) -> taffy::Style {
279    taffy::Style {
280        display: match style.display {
281            Display::Flex => taffy::Display::Flex,
282            Display::Grid => taffy::Display::Grid,
283            Display::None => taffy::Display::None,
284        },
285        position: match style.position {
286            Position::Relative => taffy::Position::Relative,
287            Position::Absolute => taffy::Position::Absolute,
288        },
289        flex_direction: match style.flex_direction {
290            FlexDirection::Row => taffy::FlexDirection::Row,
291            FlexDirection::Column => taffy::FlexDirection::Column,
292            FlexDirection::RowReverse => taffy::FlexDirection::RowReverse,
293            FlexDirection::ColumnReverse => taffy::FlexDirection::ColumnReverse,
294        },
295        flex_wrap: match style.flex_wrap {
296            FlexWrap::NoWrap => taffy::FlexWrap::NoWrap,
297            FlexWrap::Wrap => taffy::FlexWrap::Wrap,
298            FlexWrap::WrapReverse => taffy::FlexWrap::WrapReverse,
299        },
300        flex_grow: style.flex_grow,
301        flex_shrink: style.flex_shrink,
302        justify_content: style.justify_content.map(|j| match j {
303            JustifyContent::FlexStart => taffy::JustifyContent::FlexStart,
304            JustifyContent::FlexEnd => taffy::JustifyContent::FlexEnd,
305            JustifyContent::Center => taffy::JustifyContent::Center,
306            JustifyContent::SpaceBetween => taffy::JustifyContent::SpaceBetween,
307            JustifyContent::SpaceAround => taffy::JustifyContent::SpaceAround,
308            JustifyContent::SpaceEvenly => taffy::JustifyContent::SpaceEvenly,
309        }),
310        align_items: style.align_items.map(|a| match a {
311            AlignItems::FlexStart => taffy::AlignItems::FlexStart,
312            AlignItems::FlexEnd => taffy::AlignItems::FlexEnd,
313            AlignItems::Center => taffy::AlignItems::Center,
314            AlignItems::Stretch => taffy::AlignItems::Stretch,
315            AlignItems::Baseline => taffy::AlignItems::Baseline,
316        }),
317        size: taffy::Size {
318            width: convert_dimension(style.width),
319            height: convert_dimension(style.height),
320        },
321        min_size: taffy::Size {
322            width: convert_dimension(style.min_width),
323            height: convert_dimension(style.min_height),
324        },
325        max_size: taffy::Size {
326            width: convert_dimension(style.max_width),
327            height: convert_dimension(style.max_height),
328        },
329        aspect_ratio: style.aspect_ratio,
330        margin: taffy::Rect {
331            top: length(style.margin.top),
332            right: length(style.margin.right),
333            bottom: length(style.margin.bottom),
334            left: length(style.margin.left),
335        },
336        padding: taffy::Rect {
337            top: length_padding(style.padding.top),
338            right: length_padding(style.padding.right),
339            bottom: length_padding(style.padding.bottom),
340            left: length_padding(style.padding.left),
341        },
342        gap: taffy::Size {
343            width: taffy::LengthPercentage::Length(style.gap),
344            height: taffy::LengthPercentage::Length(style.gap),
345        },
346        overflow: taffy::Point {
347            x: convert_overflow(style.overflow),
348            y: convert_overflow(style.overflow),
349        },
350        ..Default::default()
351    }
352}
353
354fn convert_dimension(dim: Dimension) -> taffy::Dimension {
355    match dim {
356        Dimension::Auto => taffy::Dimension::Auto,
357        Dimension::Points(v) => taffy::Dimension::Length(v),
358        Dimension::Percent(v) => taffy::Dimension::Percent(v / 100.0),
359    }
360}
361
362fn length(v: f32) -> taffy::LengthPercentageAuto {
363    taffy::LengthPercentageAuto::Length(v)
364}
365
366fn length_padding(v: f32) -> taffy::LengthPercentage {
367    taffy::LengthPercentage::Length(v)
368}
369
370fn convert_overflow(o: Overflow) -> taffy::Overflow {
371    match o {
372        Overflow::Visible => taffy::Overflow::Visible,
373        Overflow::Hidden => taffy::Overflow::Hidden,
374        Overflow::Scroll => taffy::Overflow::Scroll,
375    }
376}
377
378// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
379// Errors
380// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
381
382#[derive(Debug, thiserror::Error)]
383pub enum LayoutError {
384    #[error("Taffy error: {0}")]
385    TaffyError(String),
386
387    #[error("Node not found: {0}")]
388    NodeNotFound(NodeId),
389
390    #[error("No root node set")]
391    NoRoot,
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use crate::platform::mock::MockPlatform;
398    use crate::platform::ViewType;
399    use std::collections::HashMap;
400    use std::sync::Arc;
401
402    #[test]
403    fn test_basic_layout() {
404        let platform = Arc::new(MockPlatform::new());
405        let mut tree = ShadowTree::new();
406        let mut layout = LayoutEngine::new();
407
408        // Create: root (flex column) → child (100x50)
409        tree.create_node(NodeId(1), ViewType::Container, HashMap::new());
410        tree.create_node(NodeId(2), ViewType::Container, HashMap::new());
411        tree.set_root(NodeId(1));
412        tree.append_child(NodeId(1), NodeId(2));
413
414        layout.create_node(NodeId(1), &LayoutStyle {
415            width: Dimension::Points(390.0),
416            height: Dimension::Points(844.0),
417            ..Default::default()
418        }).unwrap();
419
420        layout.create_node(NodeId(2), &LayoutStyle {
421            width: Dimension::Points(100.0),
422            height: Dimension::Points(50.0),
423            ..Default::default()
424        }).unwrap();
425
426        layout.set_root(NodeId(1));
427        layout.set_children_from_tree(NodeId(1), &tree).unwrap();
428        layout.compute(&tree, 390.0, 844.0, &*platform).unwrap();
429
430        let root_layout = layout.get_computed(NodeId(1)).unwrap();
431        assert_eq!(root_layout.width, 390.0);
432        assert_eq!(root_layout.height, 844.0);
433
434        let child_layout = layout.get_computed(NodeId(2)).unwrap();
435        assert_eq!(child_layout.width, 100.0);
436        assert_eq!(child_layout.height, 50.0);
437        assert_eq!(child_layout.x, 0.0);
438        assert_eq!(child_layout.y, 0.0);
439    }
440
441    #[test]
442    fn stress_deep_nesting() {
443        // 50-level deep nested tree — exercises Taffy's recursive layout
444        let platform = Arc::new(MockPlatform::new());
445        let mut tree = ShadowTree::new();
446        let mut layout = LayoutEngine::new();
447        let depth = 50;
448
449        for i in 1..=(depth as u64) {
450            tree.create_node(NodeId(i), ViewType::Container, HashMap::new());
451            let style = if i == 1 {
452                LayoutStyle {
453                    width: Dimension::Points(400.0),
454                    height: Dimension::Points(800.0),
455                    ..Default::default()
456                }
457            } else {
458                LayoutStyle {
459                    flex_grow: 1.0,
460                    ..Default::default()
461                }
462            };
463            layout.create_node(NodeId(i), &style).unwrap();
464        }
465
466        tree.set_root(NodeId(1));
467        layout.set_root(NodeId(1));
468
469        for i in 1..(depth as u64) {
470            tree.append_child(NodeId(i), NodeId(i + 1));
471            layout.set_children_from_tree(NodeId(i), &tree).unwrap();
472        }
473
474        layout.compute(&tree, 400.0, 800.0, &*platform).unwrap();
475
476        // Root should be full size
477        let root = layout.get_computed(NodeId(1)).unwrap();
478        assert_eq!(root.width, 400.0);
479        assert_eq!(root.height, 800.0);
480
481        // Deepest node should still have computed layout
482        let deepest = layout.get_computed(NodeId(depth as u64)).unwrap();
483        assert!(deepest.width > 0.0, "Deepest node should have non-zero width");
484        assert!(deepest.height > 0.0, "Deepest node should have non-zero height");
485    }
486
487    #[test]
488    fn stress_wide_tree() {
489        // 200 children in a single flex row container
490        let platform = Arc::new(MockPlatform::new());
491        let mut tree = ShadowTree::new();
492        let mut layout = LayoutEngine::new();
493        let child_count = 200u64;
494
495        tree.create_node(NodeId(1), ViewType::Container, HashMap::new());
496        layout.create_node(NodeId(1), &LayoutStyle {
497            width: Dimension::Points(1000.0),
498            height: Dimension::Points(100.0),
499            flex_direction: FlexDirection::Row,
500            ..Default::default()
501        }).unwrap();
502
503        tree.set_root(NodeId(1));
504        layout.set_root(NodeId(1));
505
506        for i in 2..=(child_count + 1) {
507            tree.create_node(NodeId(i), ViewType::Container, HashMap::new());
508            layout.create_node(NodeId(i), &LayoutStyle {
509                width: Dimension::Points(5.0),
510                height: Dimension::Points(20.0),
511                ..Default::default()
512            }).unwrap();
513            tree.append_child(NodeId(1), NodeId(i));
514        }
515
516        layout.set_children_from_tree(NodeId(1), &tree).unwrap();
517        layout.compute(&tree, 1000.0, 100.0, &*platform).unwrap();
518
519        // All children should be laid out
520        for i in 2..=(child_count + 1) {
521            let cl = layout.get_computed(NodeId(i));
522            assert!(cl.is_some(), "Child {} should have computed layout", i);
523        }
524
525        // Children should be positioned left-to-right
526        let first = layout.get_computed(NodeId(2)).unwrap();
527        let second = layout.get_computed(NodeId(3)).unwrap();
528        assert!(second.x > first.x, "Second child should be to the right of first");
529    }
530
531    #[test]
532    fn stress_rapid_mutations() {
533        // Create nodes, update styles, remove and recreate — simulates heavy reconciliation
534        let platform = Arc::new(MockPlatform::new());
535        let mut tree = ShadowTree::new();
536        let mut layout = LayoutEngine::new();
537
538        // Initial tree: root with 10 children
539        tree.create_node(NodeId(1), ViewType::Container, HashMap::new());
540        layout.create_node(NodeId(1), &LayoutStyle {
541            width: Dimension::Points(400.0),
542            height: Dimension::Points(800.0),
543            ..Default::default()
544        }).unwrap();
545        tree.set_root(NodeId(1));
546        layout.set_root(NodeId(1));
547
548        for i in 2..=11u64 {
549            tree.create_node(NodeId(i), ViewType::Container, HashMap::new());
550            layout.create_node(NodeId(i), &LayoutStyle {
551                height: Dimension::Points(50.0),
552                ..Default::default()
553            }).unwrap();
554            tree.append_child(NodeId(1), NodeId(i));
555        }
556        layout.set_children_from_tree(NodeId(1), &tree).unwrap();
557        layout.compute(&tree, 400.0, 800.0, &*platform).unwrap();
558        assert_eq!(tree.len(), 11);
559
560        // Remove half the children
561        for i in (7..=11u64).rev() {
562            tree.remove_child(NodeId(1), NodeId(i));
563            layout.remove_node(NodeId(i));
564        }
565        layout.set_children_from_tree(NodeId(1), &tree).unwrap();
566        layout.compute(&tree, 400.0, 800.0, &*platform).unwrap();
567        assert_eq!(tree.len(), 6);
568
569        // Add new children with different styles
570        for i in 100..=104u64 {
571            tree.create_node(NodeId(i), ViewType::Container, HashMap::new());
572            layout.create_node(NodeId(i), &LayoutStyle {
573                height: Dimension::Points(30.0),
574                ..Default::default()
575            }).unwrap();
576            tree.append_child(NodeId(1), NodeId(i));
577        }
578        layout.set_children_from_tree(NodeId(1), &tree).unwrap();
579        layout.compute(&tree, 400.0, 800.0, &*platform).unwrap();
580        assert_eq!(tree.len(), 11);
581
582        // All current children should have layouts
583        for &id in tree.children_of(NodeId(1)) {
584            assert!(layout.get_computed(id).is_some());
585        }
586    }
587
588    #[test]
589    fn stress_mixed_dimensions() {
590        // Mix of fixed, percentage, and auto dimensions
591        let platform = Arc::new(MockPlatform::new());
592        let mut tree = ShadowTree::new();
593        let mut layout = LayoutEngine::new();
594
595        tree.create_node(NodeId(1), ViewType::Container, HashMap::new());
596        layout.create_node(NodeId(1), &LayoutStyle {
597            width: Dimension::Points(400.0),
598            height: Dimension::Points(600.0),
599            ..Default::default()
600        }).unwrap();
601        tree.set_root(NodeId(1));
602        layout.set_root(NodeId(1));
603
604        // Child 1: fixed 100x100
605        tree.create_node(NodeId(2), ViewType::Container, HashMap::new());
606        layout.create_node(NodeId(2), &LayoutStyle {
607            width: Dimension::Points(100.0),
608            height: Dimension::Points(100.0),
609            ..Default::default()
610        }).unwrap();
611        tree.append_child(NodeId(1), NodeId(2));
612
613        // Child 2: 50% width, fixed height
614        tree.create_node(NodeId(3), ViewType::Container, HashMap::new());
615        layout.create_node(NodeId(3), &LayoutStyle {
616            width: Dimension::Percent(50.0),
617            height: Dimension::Points(80.0),
618            ..Default::default()
619        }).unwrap();
620        tree.append_child(NodeId(1), NodeId(3));
621
622        // Child 3: flex-grow
623        tree.create_node(NodeId(4), ViewType::Container, HashMap::new());
624        layout.create_node(NodeId(4), &LayoutStyle {
625            flex_grow: 1.0,
626            ..Default::default()
627        }).unwrap();
628        tree.append_child(NodeId(1), NodeId(4));
629
630        layout.set_children_from_tree(NodeId(1), &tree).unwrap();
631        layout.compute(&tree, 400.0, 600.0, &*platform).unwrap();
632
633        let c1 = layout.get_computed(NodeId(2)).unwrap();
634        assert_eq!(c1.width, 100.0);
635        assert_eq!(c1.height, 100.0);
636
637        let c2 = layout.get_computed(NodeId(3)).unwrap();
638        assert_eq!(c2.width, 200.0); // 50% of 400
639        assert_eq!(c2.height, 80.0);
640
641        let c3 = layout.get_computed(NodeId(4)).unwrap();
642        assert!(c3.height > 0.0, "Flex-grow child should expand");
643    }
644}