Skip to main content

saorsa_core/layout/
mod.rs

1//! Layout system for splitting terminal areas.
2//!
3//! Provides constraint-based splitting, dock positioning, Taffy-based
4//! CSS Flexbox/Grid layout, and scroll region management.
5
6pub mod engine;
7pub mod scroll;
8pub mod style_converter;
9
10pub use engine::{LayoutEngine, LayoutError, LayoutRect};
11pub use scroll::{OverflowBehavior, ScrollManager, ScrollState};
12pub use style_converter::computed_to_taffy;
13
14use crate::geometry::Rect;
15
16/// Direction of layout splitting.
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Direction {
19    /// Stack children top to bottom.
20    Vertical,
21    /// Stack children left to right.
22    Horizontal,
23}
24
25/// Constraint for a layout segment.
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum Constraint {
28    /// Fixed size in cells.
29    Fixed(u16),
30    /// Minimum size in cells.
31    Min(u16),
32    /// Maximum size in cells.
33    Max(u16),
34    /// Percentage of available space (0-100).
35    Percentage(u8),
36    /// Fill remaining space (distributed equally among all Fill constraints).
37    Fill,
38}
39
40/// Dock position for anchoring a widget to an edge.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum Dock {
43    /// Dock to the top edge.
44    Top,
45    /// Dock to the bottom edge.
46    Bottom,
47    /// Dock to the left edge.
48    Left,
49    /// Dock to the right edge.
50    Right,
51}
52
53/// Layout utilities for splitting terminal areas.
54pub struct Layout;
55
56impl Layout {
57    /// Split an area into segments along the given direction using constraints.
58    ///
59    /// Returns a `Vec<Rect>` with one rect per constraint.
60    pub fn split(area: Rect, direction: Direction, constraints: &[Constraint]) -> Vec<Rect> {
61        if constraints.is_empty() {
62            return Vec::new();
63        }
64
65        let total = match direction {
66            Direction::Vertical => area.size.height,
67            Direction::Horizontal => area.size.width,
68        };
69
70        let sizes = solve_constraints(total, constraints);
71
72        let mut results = Vec::with_capacity(constraints.len());
73        let mut offset: u16 = 0;
74
75        for &size in &sizes {
76            let rect = match direction {
77                Direction::Vertical => Rect::new(
78                    area.position.x,
79                    area.position.y + offset,
80                    area.size.width,
81                    size,
82                ),
83                Direction::Horizontal => Rect::new(
84                    area.position.x + offset,
85                    area.position.y,
86                    size,
87                    area.size.height,
88                ),
89            };
90            results.push(rect);
91            offset = offset.saturating_add(size);
92        }
93
94        results
95    }
96
97    /// Dock a region to one edge of the area.
98    ///
99    /// Returns `(docked_rect, remaining_rect)`.
100    pub fn dock(area: Rect, dock: Dock, size: u16) -> (Rect, Rect) {
101        match dock {
102            Dock::Top => {
103                let s = size.min(area.size.height);
104                (
105                    Rect::new(area.position.x, area.position.y, area.size.width, s),
106                    Rect::new(
107                        area.position.x,
108                        area.position.y + s,
109                        area.size.width,
110                        area.size.height.saturating_sub(s),
111                    ),
112                )
113            }
114            Dock::Bottom => {
115                let s = size.min(area.size.height);
116                (
117                    Rect::new(
118                        area.position.x,
119                        area.position.y + area.size.height.saturating_sub(s),
120                        area.size.width,
121                        s,
122                    ),
123                    Rect::new(
124                        area.position.x,
125                        area.position.y,
126                        area.size.width,
127                        area.size.height.saturating_sub(s),
128                    ),
129                )
130            }
131            Dock::Left => {
132                let s = size.min(area.size.width);
133                (
134                    Rect::new(area.position.x, area.position.y, s, area.size.height),
135                    Rect::new(
136                        area.position.x + s,
137                        area.position.y,
138                        area.size.width.saturating_sub(s),
139                        area.size.height,
140                    ),
141                )
142            }
143            Dock::Right => {
144                let s = size.min(area.size.width);
145                (
146                    Rect::new(
147                        area.position.x + area.size.width.saturating_sub(s),
148                        area.position.y,
149                        s,
150                        area.size.height,
151                    ),
152                    Rect::new(
153                        area.position.x,
154                        area.position.y,
155                        area.size.width.saturating_sub(s),
156                        area.size.height,
157                    ),
158                )
159            }
160        }
161    }
162}
163
164/// Solve constraints to produce sizes that fit within `total`.
165fn solve_constraints(total: u16, constraints: &[Constraint]) -> Vec<u16> {
166    let n = constraints.len();
167    let mut sizes = vec![0u16; n];
168    let mut remaining = total;
169
170    // Pass 1: allocate Fixed constraints
171    for (i, c) in constraints.iter().enumerate() {
172        if let Constraint::Fixed(s) = c {
173            let s = (*s).min(remaining);
174            sizes[i] = s;
175            remaining = remaining.saturating_sub(s);
176        }
177    }
178
179    // Pass 2: allocate Percentage constraints
180    for (i, c) in constraints.iter().enumerate() {
181        if let Constraint::Percentage(p) = c {
182            let s = ((u32::from(total) * u32::from(*p)) / 100) as u16;
183            let s = s.min(remaining);
184            sizes[i] = s;
185            remaining = remaining.saturating_sub(s);
186        }
187    }
188
189    // Pass 3: allocate Min constraints (give at least min, but not more than remaining for now)
190    for (i, c) in constraints.iter().enumerate() {
191        if let Constraint::Min(min) = c {
192            let s = (*min).min(remaining);
193            sizes[i] = s;
194            remaining = remaining.saturating_sub(s);
195        }
196    }
197
198    // Pass 4: allocate Max constraints
199    for (i, c) in constraints.iter().enumerate() {
200        if let Constraint::Max(max) = c {
201            let s = (*max).min(remaining);
202            sizes[i] = s;
203            remaining = remaining.saturating_sub(s);
204        }
205    }
206
207    // Pass 5: distribute remaining among Fill constraints
208    let fill_count = constraints
209        .iter()
210        .filter(|c| matches!(c, Constraint::Fill))
211        .count();
212    if fill_count > 0 {
213        let each = remaining / fill_count as u16;
214        let mut extra = remaining % fill_count as u16;
215        for (i, c) in constraints.iter().enumerate() {
216            if matches!(c, Constraint::Fill) {
217                let bonus = if extra > 0 {
218                    extra -= 1;
219                    1
220                } else {
221                    0
222                };
223                sizes[i] = each + bonus;
224            }
225        }
226    }
227
228    sizes
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::geometry::Rect;
235
236    #[test]
237    fn vertical_split_fixed() {
238        let area = Rect::new(0, 0, 80, 24);
239        let rects = Layout::split(
240            area,
241            Direction::Vertical,
242            &[Constraint::Fixed(3), Constraint::Fixed(5)],
243        );
244        assert_eq!(rects.len(), 2);
245        assert_eq!(rects[0], Rect::new(0, 0, 80, 3));
246        assert_eq!(rects[1], Rect::new(0, 3, 80, 5));
247    }
248
249    #[test]
250    fn horizontal_split_fixed() {
251        let area = Rect::new(0, 0, 80, 24);
252        let rects = Layout::split(
253            area,
254            Direction::Horizontal,
255            &[Constraint::Fixed(20), Constraint::Fixed(30)],
256        );
257        assert_eq!(rects.len(), 2);
258        assert_eq!(rects[0], Rect::new(0, 0, 20, 24));
259        assert_eq!(rects[1], Rect::new(20, 0, 30, 24));
260    }
261
262    #[test]
263    fn vertical_fixed_plus_fill() {
264        let area = Rect::new(0, 0, 80, 24);
265        let rects = Layout::split(
266            area,
267            Direction::Vertical,
268            &[Constraint::Fixed(3), Constraint::Fill],
269        );
270        assert_eq!(rects.len(), 2);
271        assert_eq!(rects[0], Rect::new(0, 0, 80, 3));
272        assert_eq!(rects[1], Rect::new(0, 3, 80, 21));
273    }
274
275    #[test]
276    fn multiple_fills_distribute_equally() {
277        let area = Rect::new(0, 0, 80, 24);
278        let rects = Layout::split(
279            area,
280            Direction::Vertical,
281            &[Constraint::Fill, Constraint::Fill],
282        );
283        assert_eq!(rects.len(), 2);
284        assert_eq!(rects[0].size.height, 12);
285        assert_eq!(rects[1].size.height, 12);
286    }
287
288    #[test]
289    fn percentage_split() {
290        let area = Rect::new(0, 0, 100, 10);
291        let rects = Layout::split(
292            area,
293            Direction::Horizontal,
294            &[Constraint::Percentage(30), Constraint::Percentage(70)],
295        );
296        assert_eq!(rects[0].size.width, 30);
297        assert_eq!(rects[1].size.width, 70);
298    }
299
300    #[test]
301    fn empty_constraints() {
302        let area = Rect::new(0, 0, 80, 24);
303        let rects = Layout::split(area, Direction::Vertical, &[]);
304        assert!(rects.is_empty());
305    }
306
307    #[test]
308    fn dock_top() {
309        let area = Rect::new(0, 0, 80, 24);
310        let (docked, remaining) = Layout::dock(area, Dock::Top, 3);
311        assert_eq!(docked, Rect::new(0, 0, 80, 3));
312        assert_eq!(remaining, Rect::new(0, 3, 80, 21));
313    }
314
315    #[test]
316    fn dock_bottom() {
317        let area = Rect::new(0, 0, 80, 24);
318        let (docked, remaining) = Layout::dock(area, Dock::Bottom, 3);
319        assert_eq!(docked, Rect::new(0, 21, 80, 3));
320        assert_eq!(remaining, Rect::new(0, 0, 80, 21));
321    }
322
323    #[test]
324    fn dock_left() {
325        let area = Rect::new(0, 0, 80, 24);
326        let (docked, remaining) = Layout::dock(area, Dock::Left, 20);
327        assert_eq!(docked, Rect::new(0, 0, 20, 24));
328        assert_eq!(remaining, Rect::new(20, 0, 60, 24));
329    }
330
331    #[test]
332    fn dock_right() {
333        let area = Rect::new(0, 0, 80, 24);
334        let (docked, remaining) = Layout::dock(area, Dock::Right, 20);
335        assert_eq!(docked, Rect::new(60, 0, 20, 24));
336        assert_eq!(remaining, Rect::new(0, 0, 60, 24));
337    }
338
339    #[test]
340    fn dock_larger_than_area() {
341        let area = Rect::new(0, 0, 80, 10);
342        let (docked, remaining) = Layout::dock(area, Dock::Top, 20);
343        assert_eq!(docked, Rect::new(0, 0, 80, 10));
344        assert_eq!(remaining, Rect::new(0, 10, 80, 0));
345    }
346
347    #[test]
348    fn offset_area_split() {
349        let area = Rect::new(5, 10, 40, 20);
350        let rects = Layout::split(
351            area,
352            Direction::Vertical,
353            &[Constraint::Fixed(5), Constraint::Fill],
354        );
355        assert_eq!(rects[0], Rect::new(5, 10, 40, 5));
356        assert_eq!(rects[1], Rect::new(5, 15, 40, 15));
357    }
358}
359
360#[cfg(test)]
361mod integration_tests {
362    use super::engine::LayoutEngine;
363    use super::scroll::ScrollManager;
364    use super::style_converter::computed_to_taffy;
365    use crate::tcss::cascade::CascadeResolver;
366    use crate::tcss::matcher::StyleMatcher;
367    use crate::tcss::parser::parse_stylesheet;
368    use crate::tcss::tree::{WidgetNode, WidgetTree};
369
370    /// Parse TCSS, build tree, match, cascade, convert, compute layout.
371    fn layout_from_css(
372        css: &str,
373        tree: &WidgetTree,
374        root_id: u64,
375        width: u16,
376        height: u16,
377    ) -> LayoutEngine {
378        let result = parse_stylesheet(css);
379        assert!(result.is_ok(), "parse failed: {result:?}");
380        let stylesheet = match result {
381            Ok(s) => s,
382            Err(_) => unreachable!(),
383        };
384        let matcher = StyleMatcher::new(&stylesheet);
385        let mut engine = LayoutEngine::new();
386
387        // Build engine nodes bottom-up: leaves first, then parents
388        build_engine_nodes(tree, root_id, &matcher, &mut engine);
389
390        engine.set_root(root_id).ok();
391        engine.compute(width, height).ok();
392        engine
393    }
394
395    /// Recursively build engine nodes from widget tree.
396    fn build_engine_nodes(
397        tree: &WidgetTree,
398        widget_id: u64,
399        matcher: &StyleMatcher,
400        engine: &mut LayoutEngine,
401    ) {
402        let node = tree.get(widget_id);
403        assert!(node.is_some(), "widget {widget_id} not found");
404        let node = match node {
405            Some(n) => n,
406            None => unreachable!(),
407        };
408        let children: Vec<u64> = node.children.clone();
409
410        // Recurse into children first
411        for &child_id in &children {
412            build_engine_nodes(tree, child_id, matcher, engine);
413        }
414
415        // Match and cascade
416        let matched = matcher.match_widget(tree, widget_id);
417        let computed = CascadeResolver::resolve(&matched);
418        let taffy_style = computed_to_taffy(&computed);
419
420        if children.is_empty() {
421            engine.add_node(widget_id, taffy_style).ok();
422        } else {
423            engine
424                .add_node_with_children(widget_id, taffy_style, &children)
425                .ok();
426        }
427    }
428
429    #[test]
430    fn integration_parse_to_layout() {
431        let css = r#"
432            #root {
433                display: flex;
434                flex-direction: column;
435                width: 80;
436                height: 24;
437            }
438            Label {
439                flex-grow: 1;
440            }
441        "#;
442        let mut tree = WidgetTree::new();
443        let mut root = WidgetNode::new(1, "Container");
444        root.css_id = Some("root".into());
445        tree.add_node(root);
446        let mut label = WidgetNode::new(2, "Label");
447        label.parent = Some(1);
448        tree.add_node(label);
449
450        let engine = layout_from_css(css, &tree, 1, 80, 24);
451
452        let root_layout = engine.layout(1).unwrap_or_default();
453        assert_eq!(root_layout.width, 80);
454        assert_eq!(root_layout.height, 24);
455
456        let label_layout = engine.layout(2).unwrap_or_default();
457        // In column layout, label fills full width and grows vertically
458        assert_eq!(label_layout.width, 80);
459        assert_eq!(label_layout.height, 24);
460    }
461
462    #[test]
463    fn integration_flex_sidebar_layout() {
464        let css = r#"
465            #root { display: flex; width: 80; height: 24; }
466            #sidebar { width: 20; }
467            #main { flex-grow: 1; }
468        "#;
469        let mut tree = WidgetTree::new();
470        let mut root = WidgetNode::new(1, "Container");
471        root.css_id = Some("root".into());
472        tree.add_node(root);
473
474        let mut sidebar = WidgetNode::new(2, "Container");
475        sidebar.css_id = Some("sidebar".into());
476        sidebar.parent = Some(1);
477        tree.add_node(sidebar);
478
479        let mut main = WidgetNode::new(3, "Container");
480        main.css_id = Some("main".into());
481        main.parent = Some(1);
482        tree.add_node(main);
483
484        tree.get_mut(1)
485            .iter_mut()
486            .for_each(|n| n.children = vec![2, 3]);
487
488        let engine = layout_from_css(css, &tree, 1, 80, 24);
489
490        let sidebar_layout = engine.layout(2).unwrap_or_default();
491        let main_layout = engine.layout(3).unwrap_or_default();
492        assert_eq!(sidebar_layout.width, 20);
493        assert_eq!(main_layout.width, 60);
494        assert_eq!(main_layout.x, 20);
495    }
496
497    #[test]
498    fn integration_grid_dashboard() {
499        let css = r#"
500            #root {
501                display: grid;
502                grid-template-columns: 1fr 1fr 1fr;
503                grid-template-rows: 1fr 1fr;
504                width: 90;
505                height: 20;
506            }
507        "#;
508        let mut tree = WidgetTree::new();
509        let mut root = WidgetNode::new(1, "Container");
510        root.css_id = Some("root".into());
511        tree.add_node(root);
512
513        let mut child_ids = Vec::new();
514        for i in 2..=7 {
515            let mut child = WidgetNode::new(i, "Panel");
516            child.parent = Some(1);
517            tree.add_node(child);
518            child_ids.push(i);
519        }
520        tree.get_mut(1)
521            .iter_mut()
522            .for_each(|n| n.children = child_ids.clone());
523
524        let engine = layout_from_css(css, &tree, 1, 90, 20);
525
526        let l1 = engine.layout(2).unwrap_or_default();
527        let l2 = engine.layout(3).unwrap_or_default();
528        let l4 = engine.layout(5).unwrap_or_default();
529        assert_eq!(l1.width, 30);
530        assert_eq!(l1.height, 10);
531        assert_eq!(l2.x, 30);
532        assert_eq!(l4.y, 10); // second row
533    }
534
535    #[test]
536    fn integration_nested_flex_grid() {
537        let css = r#"
538            #root { display: flex; width: 80; height: 20; }
539            #left { flex-grow: 1; display: grid; grid-template-columns: 1fr 1fr; }
540            #right { flex-grow: 1; }
541        "#;
542        let mut tree = WidgetTree::new();
543        let mut root = WidgetNode::new(1, "Container");
544        root.css_id = Some("root".into());
545        tree.add_node(root);
546
547        let mut left = WidgetNode::new(2, "Container");
548        left.css_id = Some("left".into());
549        left.parent = Some(1);
550        tree.add_node(left);
551
552        let mut right = WidgetNode::new(3, "Container");
553        right.css_id = Some("right".into());
554        right.parent = Some(1);
555        tree.add_node(right);
556
557        // Grid children of left
558        let mut g1 = WidgetNode::new(4, "Panel");
559        g1.parent = Some(2);
560        tree.add_node(g1);
561        let mut g2 = WidgetNode::new(5, "Panel");
562        g2.parent = Some(2);
563        tree.add_node(g2);
564
565        tree.get_mut(1)
566            .iter_mut()
567            .for_each(|n| n.children = vec![2, 3]);
568        tree.get_mut(2)
569            .iter_mut()
570            .for_each(|n| n.children = vec![4, 5]);
571
572        let engine = layout_from_css(css, &tree, 1, 80, 20);
573
574        let left_layout = engine.layout(2).unwrap_or_default();
575        let right_layout = engine.layout(3).unwrap_or_default();
576        assert_eq!(left_layout.width, 40);
577        assert_eq!(right_layout.width, 40);
578
579        let g1_layout = engine.layout(4).unwrap_or_default();
580        let g2_layout = engine.layout(5).unwrap_or_default();
581        assert_eq!(g1_layout.width, 20);
582        assert_eq!(g2_layout.width, 20);
583    }
584
585    #[test]
586    fn integration_box_model_spacing() {
587        let css = r#"
588            #root { display: flex; width: 80; height: 24; padding: 2; }
589            #child { flex-grow: 1; }
590        "#;
591        let mut tree = WidgetTree::new();
592        let mut root = WidgetNode::new(1, "Container");
593        root.css_id = Some("root".into());
594        tree.add_node(root);
595
596        let mut child = WidgetNode::new(2, "Container");
597        child.css_id = Some("child".into());
598        child.parent = Some(1);
599        tree.add_node(child);
600
601        tree.get_mut(1)
602            .iter_mut()
603            .for_each(|n| n.children = vec![2]);
604
605        let engine = layout_from_css(css, &tree, 1, 80, 24);
606
607        let child_layout = engine.layout(2).unwrap_or_default();
608        assert_eq!(child_layout.x, 2);
609        assert_eq!(child_layout.y, 2);
610        assert_eq!(child_layout.width, 76); // 80 - 2 - 2
611        assert_eq!(child_layout.height, 20); // 24 - 2 - 2
612    }
613
614    #[test]
615    fn integration_scroll_region_setup() {
616        let css = r#"
617            #root { overflow: scroll; width: 80; height: 24; }
618        "#;
619        let result = parse_stylesheet(css);
620        assert!(result.is_ok());
621        let stylesheet = match result {
622            Ok(s) => s,
623            Err(_) => unreachable!(),
624        };
625        let matcher = StyleMatcher::new(&stylesheet);
626
627        let mut tree = WidgetTree::new();
628        let mut root = WidgetNode::new(1, "Container");
629        root.css_id = Some("root".into());
630        tree.add_node(root);
631
632        let matched = matcher.match_widget(&tree, 1);
633        let computed = CascadeResolver::resolve(&matched);
634
635        let (ox, oy) = super::scroll::extract_overflow(&computed);
636        assert_eq!(ox, super::scroll::OverflowBehavior::Scroll);
637        assert_eq!(oy, super::scroll::OverflowBehavior::Scroll);
638
639        let mut scroll_mgr = ScrollManager::new();
640        scroll_mgr.register(1, 200, 100, 80, 24);
641        assert!(scroll_mgr.can_scroll_x(1));
642        assert!(scroll_mgr.can_scroll_y(1));
643    }
644
645    #[test]
646    fn integration_zero_size_area() {
647        let css = r#"
648            #root { display: flex; width: 0; height: 0; }
649            Label { flex-grow: 1; }
650        "#;
651        let mut tree = WidgetTree::new();
652        let mut root = WidgetNode::new(1, "Container");
653        root.css_id = Some("root".into());
654        tree.add_node(root);
655
656        let mut label = WidgetNode::new(2, "Label");
657        label.parent = Some(1);
658        tree.add_node(label);
659
660        let engine = layout_from_css(css, &tree, 1, 0, 0);
661
662        let root_layout = engine.layout(1).unwrap_or_default();
663        assert_eq!(root_layout.width, 0);
664        assert_eq!(root_layout.height, 0);
665    }
666
667    #[test]
668    fn integration_large_tree() {
669        let css = r#"
670            #root { display: flex; flex-direction: column; width: 100; height: 100; }
671            .item { flex-grow: 1; }
672        "#;
673        let mut tree = WidgetTree::new();
674        let mut root = WidgetNode::new(1, "Container");
675        root.css_id = Some("root".into());
676        tree.add_node(root);
677
678        let mut child_ids = Vec::new();
679        for i in 2..=101 {
680            let mut child = WidgetNode::new(i, "Container");
681            child.classes.push("item".into());
682            child.parent = Some(1);
683            tree.add_node(child);
684            child_ids.push(i);
685        }
686        tree.get_mut(1)
687            .iter_mut()
688            .for_each(|n| n.children = child_ids.clone());
689
690        let engine = layout_from_css(css, &tree, 1, 100, 100);
691
692        // 100 children in column layout across 100 height = 1 each
693        let l = engine.layout(2).unwrap_or_default();
694        assert_eq!(l.height, 1);
695        assert_eq!(l.width, 100);
696    }
697
698    #[test]
699    fn integration_theme_affects_layout() {
700        // Test that variable resolution can affect layout properties
701        let css = r#"
702            :root { $sidebar-width: 30; }
703            #root { display: flex; width: 80; height: 24; }
704            #sidebar { width: $sidebar-width; }
705            #main { flex-grow: 1; }
706        "#;
707        let result = parse_stylesheet(css);
708        assert!(result.is_ok());
709        let stylesheet = match result {
710            Ok(s) => s,
711            Err(_) => unreachable!(),
712        };
713
714        let globals = crate::tcss::parser::extract_root_variables(&stylesheet);
715        let env = crate::tcss::variable::VariableEnvironment::with_global(globals);
716        let matcher = StyleMatcher::new(&stylesheet);
717
718        let mut tree = WidgetTree::new();
719        let mut root = WidgetNode::new(1, "Container");
720        root.css_id = Some("root".into());
721        tree.add_node(root);
722
723        let mut sidebar = WidgetNode::new(2, "Container");
724        sidebar.css_id = Some("sidebar".into());
725        sidebar.parent = Some(1);
726        tree.add_node(sidebar);
727
728        let mut main = WidgetNode::new(3, "Container");
729        main.css_id = Some("main".into());
730        main.parent = Some(1);
731        tree.add_node(main);
732
733        tree.get_mut(1)
734            .iter_mut()
735            .for_each(|n| n.children = vec![2, 3]);
736
737        // Resolve styles with variables
738        let mut engine = LayoutEngine::new();
739
740        for &wid in &[2, 3] {
741            let matched = matcher.match_widget(&tree, wid);
742            let computed = CascadeResolver::resolve_with_variables(&matched, &env);
743            let style = computed_to_taffy(&computed);
744            engine.add_node(wid, style).ok();
745        }
746
747        let matched = matcher.match_widget(&tree, 1);
748        let computed = CascadeResolver::resolve_with_variables(&matched, &env);
749        let style = computed_to_taffy(&computed);
750        engine.add_node_with_children(1, style, &[2, 3]).ok();
751
752        engine.set_root(1).ok();
753        engine.compute(80, 24).ok();
754
755        let sidebar_layout = engine.layout(2).unwrap_or_default();
756        let main_layout = engine.layout(3).unwrap_or_default();
757        assert_eq!(sidebar_layout.width, 30);
758        assert_eq!(main_layout.width, 50);
759    }
760}