Skip to main content

saorsa_tui/layout/
engine.rs

1//! Taffy-based layout engine.
2//!
3//! Wraps a [`taffy::TaffyTree`] to compute CSS Flexbox and Grid layouts,
4//! mapping [`WidgetId`] to Taffy nodes and producing integer-cell
5//! [`LayoutRect`] results for terminal rendering.
6
7use std::collections::HashMap;
8
9use taffy::prelude::*;
10
11use crate::focus::WidgetId;
12use crate::geometry::Rect;
13
14/// A layout rectangle in terminal cell coordinates.
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub struct LayoutRect {
17    /// X position (column).
18    pub x: u16,
19    /// Y position (row).
20    pub y: u16,
21    /// Width in columns.
22    pub width: u16,
23    /// Height in rows.
24    pub height: u16,
25}
26
27impl LayoutRect {
28    /// Create a new layout rectangle.
29    pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
30        Self {
31            x,
32            y,
33            width,
34            height,
35        }
36    }
37
38    /// Convert to a [`Rect`].
39    pub const fn to_rect(self) -> Rect {
40        Rect::new(self.x, self.y, self.width, self.height)
41    }
42}
43
44/// Errors from layout operations.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum LayoutError {
47    /// The widget was not found in the layout tree.
48    WidgetNotFound(WidgetId),
49    /// An error occurred in Taffy.
50    TaffyError(String),
51    /// No root node has been set.
52    NoRoot,
53}
54
55impl std::fmt::Display for LayoutError {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Self::WidgetNotFound(id) => write!(f, "widget not found: {id}"),
59            Self::TaffyError(e) => write!(f, "taffy error: {e}"),
60            Self::NoRoot => write!(f, "no root node set"),
61        }
62    }
63}
64
65impl std::error::Error for LayoutError {}
66
67/// Layout engine backed by Taffy.
68///
69/// Manages a tree of layout nodes associated with widget IDs, computes
70/// CSS Flexbox and Grid layout, and returns integer-cell results.
71pub struct LayoutEngine {
72    taffy: TaffyTree<()>,
73    widget_to_node: HashMap<WidgetId, NodeId>,
74    node_to_widget: HashMap<NodeId, WidgetId>,
75    root: Option<NodeId>,
76}
77
78impl LayoutEngine {
79    /// Create a new empty layout engine.
80    pub fn new() -> Self {
81        Self {
82            taffy: TaffyTree::new(),
83            widget_to_node: HashMap::new(),
84            node_to_widget: HashMap::new(),
85            root: None,
86        }
87    }
88
89    /// Add a leaf node with the given style.
90    pub fn add_node(&mut self, widget_id: WidgetId, style: Style) -> Result<(), LayoutError> {
91        let node = self
92            .taffy
93            .new_leaf(style)
94            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
95        self.widget_to_node.insert(widget_id, node);
96        self.node_to_widget.insert(node, widget_id);
97        Ok(())
98    }
99
100    /// Add a node with children.
101    pub fn add_node_with_children(
102        &mut self,
103        widget_id: WidgetId,
104        style: Style,
105        children: &[WidgetId],
106    ) -> Result<(), LayoutError> {
107        let child_nodes: Vec<NodeId> = children
108            .iter()
109            .map(|id| {
110                self.widget_to_node
111                    .get(id)
112                    .copied()
113                    .ok_or(LayoutError::WidgetNotFound(*id))
114            })
115            .collect::<Result<Vec<_>, _>>()?;
116
117        let node = self
118            .taffy
119            .new_with_children(style, &child_nodes)
120            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
121        self.widget_to_node.insert(widget_id, node);
122        self.node_to_widget.insert(node, widget_id);
123        Ok(())
124    }
125
126    /// Set the root node for layout computation.
127    pub fn set_root(&mut self, widget_id: WidgetId) -> Result<(), LayoutError> {
128        let node = self
129            .widget_to_node
130            .get(&widget_id)
131            .copied()
132            .ok_or(LayoutError::WidgetNotFound(widget_id))?;
133        self.root = Some(node);
134        Ok(())
135    }
136
137    /// Update the style of an existing node.
138    pub fn update_style(&mut self, widget_id: WidgetId, style: Style) -> Result<(), LayoutError> {
139        let node = self
140            .widget_to_node
141            .get(&widget_id)
142            .copied()
143            .ok_or(LayoutError::WidgetNotFound(widget_id))?;
144        self.taffy
145            .set_style(node, style)
146            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
147        Ok(())
148    }
149
150    /// Replace the children list for an existing node.
151    pub fn set_children(
152        &mut self,
153        widget_id: WidgetId,
154        children: &[WidgetId],
155    ) -> Result<(), LayoutError> {
156        let node = self
157            .widget_to_node
158            .get(&widget_id)
159            .copied()
160            .ok_or(LayoutError::WidgetNotFound(widget_id))?;
161        let child_nodes: Vec<NodeId> = children
162            .iter()
163            .map(|id| {
164                self.widget_to_node
165                    .get(id)
166                    .copied()
167                    .ok_or(LayoutError::WidgetNotFound(*id))
168            })
169            .collect::<Result<Vec<_>, _>>()?;
170        self.taffy
171            .set_children(node, &child_nodes)
172            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
173        Ok(())
174    }
175
176    /// Remove a node from the layout tree.
177    pub fn remove_node(&mut self, widget_id: WidgetId) -> Result<(), LayoutError> {
178        let node = self
179            .widget_to_node
180            .remove(&widget_id)
181            .ok_or(LayoutError::WidgetNotFound(widget_id))?;
182        self.node_to_widget.remove(&node);
183        self.taffy
184            .remove(node)
185            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
186        if self.root == Some(node) {
187            self.root = None;
188        }
189        Ok(())
190    }
191
192    /// Compute layout using the given available space.
193    pub fn compute(
194        &mut self,
195        available_width: u16,
196        available_height: u16,
197    ) -> Result<(), LayoutError> {
198        let root = self.root.ok_or(LayoutError::NoRoot)?;
199        let available = taffy::Size {
200            width: AvailableSpace::Definite(f32::from(available_width)),
201            height: AvailableSpace::Definite(f32::from(available_height)),
202        };
203        self.taffy
204            .compute_layout(root, available)
205            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
206        Ok(())
207    }
208
209    /// Get the computed layout for a widget as a [`LayoutRect`].
210    pub fn layout(&self, widget_id: WidgetId) -> Result<LayoutRect, LayoutError> {
211        let node = self
212            .widget_to_node
213            .get(&widget_id)
214            .copied()
215            .ok_or(LayoutError::WidgetNotFound(widget_id))?;
216        let layout = self
217            .taffy
218            .layout(node)
219            .map_err(|e| LayoutError::TaffyError(format!("{e}")))?;
220
221        Ok(LayoutRect {
222            x: round_position(layout.location.x),
223            y: round_position(layout.location.y),
224            width: round_size(layout.size.width),
225            height: round_size(layout.size.height),
226        })
227    }
228
229    /// Get the computed layout for a widget as a [`Rect`].
230    pub fn layout_rect(&self, widget_id: WidgetId) -> Result<Rect, LayoutError> {
231        self.layout(widget_id).map(|lr| lr.to_rect())
232    }
233
234    /// Check if a widget has a layout node.
235    pub fn has_node(&self, widget_id: WidgetId) -> bool {
236        self.widget_to_node.contains_key(&widget_id)
237    }
238
239    /// Return the number of nodes in the tree.
240    pub fn node_count(&self) -> usize {
241        self.widget_to_node.len()
242    }
243}
244
245impl Default for LayoutEngine {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251/// Round a position value: floor to integer cells.
252pub fn round_position(value: f32) -> u16 {
253    if value < 0.0 {
254        0
255    } else if value > f32::from(u16::MAX) {
256        u16::MAX
257    } else {
258        value.floor() as u16
259    }
260}
261
262/// Round a size value: round to nearest integer cells.
263pub fn round_size(value: f32) -> u16 {
264    if value < 0.0 {
265        0
266    } else if value > f32::from(u16::MAX) {
267        u16::MAX
268    } else {
269        value.round() as u16
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::geometry::Rect;
277    use taffy::prelude::{
278        AlignItems, Dimension, Display, FlexDirection, GridPlacement, JustifyContent,
279        LengthPercentage, LengthPercentageAuto, Line, Style, auto, fr, length,
280    };
281
282    fn wid(n: u64) -> WidgetId {
283        n
284    }
285
286    #[test]
287    fn empty_engine() {
288        let engine = LayoutEngine::new();
289        assert_eq!(engine.node_count(), 0);
290        assert!(!engine.has_node(wid(1)));
291    }
292
293    #[test]
294    fn add_leaf_node() {
295        let mut engine = LayoutEngine::new();
296        let result = engine.add_node(wid(1), Style::default());
297        assert!(result.is_ok());
298        assert!(engine.has_node(wid(1)));
299        assert_eq!(engine.node_count(), 1);
300    }
301
302    #[test]
303    fn add_with_children() {
304        let mut engine = LayoutEngine::new();
305        engine.add_node(wid(1), Style::default()).ok();
306        engine.add_node(wid(2), Style::default()).ok();
307        let result = engine.add_node_with_children(wid(3), Style::default(), &[wid(1), wid(2)]);
308        assert!(result.is_ok());
309        assert_eq!(engine.node_count(), 3);
310    }
311
312    #[test]
313    fn set_root() {
314        let mut engine = LayoutEngine::new();
315        engine.add_node(wid(1), Style::default()).ok();
316        let result = engine.set_root(wid(1));
317        assert!(result.is_ok());
318    }
319
320    #[test]
321    fn remove_node() {
322        let mut engine = LayoutEngine::new();
323        engine.add_node(wid(1), Style::default()).ok();
324        assert!(engine.has_node(wid(1)));
325        let result = engine.remove_node(wid(1));
326        assert!(result.is_ok());
327        assert!(!engine.has_node(wid(1)));
328        assert_eq!(engine.node_count(), 0);
329    }
330
331    #[test]
332    fn update_style() {
333        let mut engine = LayoutEngine::new();
334        engine.add_node(wid(1), Style::default()).ok();
335        let new_style = Style {
336            size: taffy::Size {
337                width: Dimension::Length(50.0),
338                height: Dimension::Length(25.0),
339            },
340            ..Default::default()
341        };
342        let result = engine.update_style(wid(1), new_style);
343        assert!(result.is_ok());
344    }
345
346    #[test]
347    fn compute_single_node() {
348        let mut engine = LayoutEngine::new();
349        engine
350            .add_node(
351                wid(1),
352                Style {
353                    size: taffy::Size {
354                        width: Dimension::Length(80.0),
355                        height: Dimension::Length(24.0),
356                    },
357                    ..Default::default()
358                },
359            )
360            .ok();
361        engine.set_root(wid(1)).ok();
362        let result = engine.compute(80, 24);
363        assert!(result.is_ok());
364
365        let layout = engine.layout(wid(1));
366        assert!(layout.is_ok());
367        let rect = layout.ok();
368        assert!(rect.is_some());
369        let rect = rect.unwrap_or_default();
370        assert_eq!(rect.width, 80);
371        assert_eq!(rect.height, 24);
372    }
373
374    #[test]
375    fn compute_two_children_row() {
376        let mut engine = LayoutEngine::new();
377        engine
378            .add_node(
379                wid(1),
380                Style {
381                    flex_grow: 1.0,
382                    ..Default::default()
383                },
384            )
385            .ok();
386        engine
387            .add_node(
388                wid(2),
389                Style {
390                    flex_grow: 1.0,
391                    ..Default::default()
392                },
393            )
394            .ok();
395        engine
396            .add_node_with_children(
397                wid(3),
398                Style {
399                    size: taffy::Size {
400                        width: Dimension::Length(80.0),
401                        height: Dimension::Length(24.0),
402                    },
403                    ..Default::default()
404                },
405                &[wid(1), wid(2)],
406            )
407            .ok();
408        engine.set_root(wid(3)).ok();
409        engine.compute(80, 24).ok();
410
411        let l1 = engine.layout(wid(1)).unwrap_or_default();
412        let l2 = engine.layout(wid(2)).unwrap_or_default();
413        assert_eq!(l1.width, 40);
414        assert_eq!(l2.width, 40);
415        assert_eq!(l1.height, 24);
416    }
417
418    #[test]
419    fn compute_two_children_column() {
420        let mut engine = LayoutEngine::new();
421        engine
422            .add_node(
423                wid(1),
424                Style {
425                    flex_grow: 1.0,
426                    ..Default::default()
427                },
428            )
429            .ok();
430        engine
431            .add_node(
432                wid(2),
433                Style {
434                    flex_grow: 1.0,
435                    ..Default::default()
436                },
437            )
438            .ok();
439        engine
440            .add_node_with_children(
441                wid(3),
442                Style {
443                    flex_direction: FlexDirection::Column,
444                    size: taffy::Size {
445                        width: Dimension::Length(80.0),
446                        height: Dimension::Length(24.0),
447                    },
448                    ..Default::default()
449                },
450                &[wid(1), wid(2)],
451            )
452            .ok();
453        engine.set_root(wid(3)).ok();
454        engine.compute(80, 24).ok();
455
456        let l1 = engine.layout(wid(1)).unwrap_or_default();
457        let l2 = engine.layout(wid(2)).unwrap_or_default();
458        assert_eq!(l1.height, 12);
459        assert_eq!(l2.height, 12);
460        assert_eq!(l1.width, 80);
461    }
462
463    #[test]
464    fn layout_rect_conversion() {
465        let lr = LayoutRect::new(5, 10, 40, 20);
466        let rect = lr.to_rect();
467        assert_eq!(rect, Rect::new(5, 10, 40, 20));
468    }
469
470    #[test]
471    fn widget_not_found_error() {
472        let engine = LayoutEngine::new();
473        let result = engine.layout(wid(999));
474        assert!(result.is_err());
475        match result {
476            Err(LayoutError::WidgetNotFound(id)) => assert_eq!(id, wid(999)),
477            _ => unreachable!(),
478        }
479    }
480
481    #[test]
482    fn no_root_error() {
483        let mut engine = LayoutEngine::new();
484        let result = engine.compute(80, 24);
485        assert!(result.is_err());
486        match result {
487            Err(LayoutError::NoRoot) => {}
488            _ => unreachable!(),
489        }
490    }
491
492    #[test]
493    fn round_position_values() {
494        assert_eq!(round_position(0.0), 0);
495        assert_eq!(round_position(5.7), 5); // floor
496        assert_eq!(round_position(10.99), 10); // floor
497        assert_eq!(round_position(-1.0), 0); // negative clamped
498    }
499
500    #[test]
501    fn round_size_values() {
502        assert_eq!(round_size(0.0), 0);
503        assert_eq!(round_size(5.4), 5); // round
504        assert_eq!(round_size(5.5), 6); // round up
505        assert_eq!(round_size(-1.0), 0); // negative clamped
506    }
507
508    #[test]
509    fn children_not_found_error() {
510        let mut engine = LayoutEngine::new();
511        let result = engine.add_node_with_children(wid(1), Style::default(), &[wid(999)]);
512        assert!(result.is_err());
513        match result {
514            Err(LayoutError::WidgetNotFound(id)) => assert_eq!(id, wid(999)),
515            _ => unreachable!(),
516        }
517    }
518
519    #[test]
520    fn remove_root_clears_root() {
521        let mut engine = LayoutEngine::new();
522        engine.add_node(wid(1), Style::default()).ok();
523        engine.set_root(wid(1)).ok();
524        engine.remove_node(wid(1)).ok();
525        let result = engine.compute(80, 24);
526        assert!(matches!(result, Err(LayoutError::NoRoot)));
527    }
528
529    // --- Flexbox integration tests (Task 5) ---
530
531    #[test]
532    fn flex_row_equal_grow() {
533        let mut engine = LayoutEngine::new();
534        for i in 1..=3 {
535            engine
536                .add_node(
537                    wid(i),
538                    Style {
539                        flex_grow: 1.0,
540                        ..Default::default()
541                    },
542                )
543                .ok();
544        }
545        engine
546            .add_node_with_children(
547                wid(10),
548                Style {
549                    size: taffy::Size {
550                        width: Dimension::Length(90.0),
551                        height: Dimension::Length(30.0),
552                    },
553                    ..Default::default()
554                },
555                &[wid(1), wid(2), wid(3)],
556            )
557            .ok();
558        engine.set_root(wid(10)).ok();
559        engine.compute(90, 30).ok();
560
561        for i in 1..=3 {
562            let l = engine.layout(wid(i)).unwrap_or_default();
563            assert_eq!(l.width, 30, "child {i} width should be 30");
564        }
565    }
566
567    #[test]
568    fn flex_column_equal_grow() {
569        let mut engine = LayoutEngine::new();
570        for i in 1..=3 {
571            engine
572                .add_node(
573                    wid(i),
574                    Style {
575                        flex_grow: 1.0,
576                        ..Default::default()
577                    },
578                )
579                .ok();
580        }
581        engine
582            .add_node_with_children(
583                wid(10),
584                Style {
585                    flex_direction: FlexDirection::Column,
586                    size: taffy::Size {
587                        width: Dimension::Length(60.0),
588                        height: Dimension::Length(30.0),
589                    },
590                    ..Default::default()
591                },
592                &[wid(1), wid(2), wid(3)],
593            )
594            .ok();
595        engine.set_root(wid(10)).ok();
596        engine.compute(60, 30).ok();
597
598        for i in 1..=3 {
599            let l = engine.layout(wid(i)).unwrap_or_default();
600            assert_eq!(l.height, 10, "child {i} height should be 10");
601        }
602    }
603
604    #[test]
605    fn flex_row_unequal_grow() {
606        let mut engine = LayoutEngine::new();
607        engine
608            .add_node(
609                wid(1),
610                Style {
611                    flex_grow: 1.0,
612                    ..Default::default()
613                },
614            )
615            .ok();
616        engine
617            .add_node(
618                wid(2),
619                Style {
620                    flex_grow: 2.0,
621                    ..Default::default()
622                },
623            )
624            .ok();
625        engine
626            .add_node(
627                wid(3),
628                Style {
629                    flex_grow: 1.0,
630                    ..Default::default()
631                },
632            )
633            .ok();
634        engine
635            .add_node_with_children(
636                wid(10),
637                Style {
638                    size: taffy::Size {
639                        width: Dimension::Length(80.0),
640                        height: Dimension::Length(20.0),
641                    },
642                    ..Default::default()
643                },
644                &[wid(1), wid(2), wid(3)],
645            )
646            .ok();
647        engine.set_root(wid(10)).ok();
648        engine.compute(80, 20).ok();
649
650        let l1 = engine.layout(wid(1)).unwrap_or_default();
651        let l2 = engine.layout(wid(2)).unwrap_or_default();
652        let l3 = engine.layout(wid(3)).unwrap_or_default();
653        assert_eq!(l1.width, 20);
654        assert_eq!(l2.width, 40);
655        assert_eq!(l3.width, 20);
656    }
657
658    #[test]
659    fn flex_column_fixed_and_grow() {
660        let mut engine = LayoutEngine::new();
661        engine
662            .add_node(
663                wid(1),
664                Style {
665                    size: taffy::Size {
666                        width: auto(),
667                        height: Dimension::Length(5.0),
668                    },
669                    ..Default::default()
670                },
671            )
672            .ok();
673        engine
674            .add_node(
675                wid(2),
676                Style {
677                    flex_grow: 1.0,
678                    ..Default::default()
679                },
680            )
681            .ok();
682        engine
683            .add_node_with_children(
684                wid(10),
685                Style {
686                    flex_direction: FlexDirection::Column,
687                    size: taffy::Size {
688                        width: Dimension::Length(80.0),
689                        height: Dimension::Length(25.0),
690                    },
691                    ..Default::default()
692                },
693                &[wid(1), wid(2)],
694            )
695            .ok();
696        engine.set_root(wid(10)).ok();
697        engine.compute(80, 25).ok();
698
699        let l1 = engine.layout(wid(1)).unwrap_or_default();
700        let l2 = engine.layout(wid(2)).unwrap_or_default();
701        assert_eq!(l1.height, 5);
702        assert_eq!(l2.height, 20);
703    }
704
705    #[test]
706    fn flex_justify_center() {
707        let mut engine = LayoutEngine::new();
708        engine
709            .add_node(
710                wid(1),
711                Style {
712                    size: taffy::Size {
713                        width: Dimension::Length(20.0),
714                        height: Dimension::Length(10.0),
715                    },
716                    ..Default::default()
717                },
718            )
719            .ok();
720        engine
721            .add_node_with_children(
722                wid(10),
723                Style {
724                    justify_content: Some(JustifyContent::Center),
725                    size: taffy::Size {
726                        width: Dimension::Length(80.0),
727                        height: Dimension::Length(10.0),
728                    },
729                    ..Default::default()
730                },
731                &[wid(1)],
732            )
733            .ok();
734        engine.set_root(wid(10)).ok();
735        engine.compute(80, 10).ok();
736
737        let l = engine.layout(wid(1)).unwrap_or_default();
738        assert_eq!(l.x, 30); // (80-20)/2 = 30
739    }
740
741    #[test]
742    fn flex_justify_space_between() {
743        let mut engine = LayoutEngine::new();
744        engine
745            .add_node(
746                wid(1),
747                Style {
748                    size: taffy::Size {
749                        width: Dimension::Length(10.0),
750                        height: Dimension::Length(10.0),
751                    },
752                    ..Default::default()
753                },
754            )
755            .ok();
756        engine
757            .add_node(
758                wid(2),
759                Style {
760                    size: taffy::Size {
761                        width: Dimension::Length(10.0),
762                        height: Dimension::Length(10.0),
763                    },
764                    ..Default::default()
765                },
766            )
767            .ok();
768        engine
769            .add_node_with_children(
770                wid(10),
771                Style {
772                    justify_content: Some(JustifyContent::SpaceBetween),
773                    size: taffy::Size {
774                        width: Dimension::Length(80.0),
775                        height: Dimension::Length(10.0),
776                    },
777                    ..Default::default()
778                },
779                &[wid(1), wid(2)],
780            )
781            .ok();
782        engine.set_root(wid(10)).ok();
783        engine.compute(80, 10).ok();
784
785        let l1 = engine.layout(wid(1)).unwrap_or_default();
786        let l2 = engine.layout(wid(2)).unwrap_or_default();
787        assert_eq!(l1.x, 0);
788        assert_eq!(l2.x, 70); // 80 - 10 = 70
789    }
790
791    #[test]
792    fn flex_align_items_center() {
793        let mut engine = LayoutEngine::new();
794        engine
795            .add_node(
796                wid(1),
797                Style {
798                    size: taffy::Size {
799                        width: Dimension::Length(20.0),
800                        height: Dimension::Length(10.0),
801                    },
802                    ..Default::default()
803                },
804            )
805            .ok();
806        engine
807            .add_node_with_children(
808                wid(10),
809                Style {
810                    align_items: Some(AlignItems::Center),
811                    size: taffy::Size {
812                        width: Dimension::Length(80.0),
813                        height: Dimension::Length(30.0),
814                    },
815                    ..Default::default()
816                },
817                &[wid(1)],
818            )
819            .ok();
820        engine.set_root(wid(10)).ok();
821        engine.compute(80, 30).ok();
822
823        let l = engine.layout(wid(1)).unwrap_or_default();
824        assert_eq!(l.y, 10); // (30-10)/2 = 10
825    }
826
827    #[test]
828    fn flex_nested() {
829        let mut engine = LayoutEngine::new();
830        // Inner children
831        engine
832            .add_node(
833                wid(1),
834                Style {
835                    flex_grow: 1.0,
836                    ..Default::default()
837                },
838            )
839            .ok();
840        engine
841            .add_node(
842                wid(2),
843                Style {
844                    flex_grow: 1.0,
845                    ..Default::default()
846                },
847            )
848            .ok();
849        // Inner container (column)
850        engine
851            .add_node_with_children(
852                wid(3),
853                Style {
854                    flex_direction: FlexDirection::Column,
855                    flex_grow: 1.0,
856                    ..Default::default()
857                },
858                &[wid(1), wid(2)],
859            )
860            .ok();
861        // Sibling
862        engine
863            .add_node(
864                wid(4),
865                Style {
866                    flex_grow: 1.0,
867                    ..Default::default()
868                },
869            )
870            .ok();
871        // Root (row)
872        engine
873            .add_node_with_children(
874                wid(10),
875                Style {
876                    size: taffy::Size {
877                        width: Dimension::Length(80.0),
878                        height: Dimension::Length(20.0),
879                    },
880                    ..Default::default()
881                },
882                &[wid(3), wid(4)],
883            )
884            .ok();
885        engine.set_root(wid(10)).ok();
886        engine.compute(80, 20).ok();
887
888        let l3 = engine.layout(wid(3)).unwrap_or_default();
889        let l4 = engine.layout(wid(4)).unwrap_or_default();
890        assert_eq!(l3.width, 40);
891        assert_eq!(l4.width, 40);
892        let l1 = engine.layout(wid(1)).unwrap_or_default();
893        let l2 = engine.layout(wid(2)).unwrap_or_default();
894        assert_eq!(l1.height, 10);
895        assert_eq!(l2.height, 10);
896    }
897
898    #[test]
899    fn flex_with_gap() {
900        let mut engine = LayoutEngine::new();
901        engine
902            .add_node(
903                wid(1),
904                Style {
905                    size: taffy::Size {
906                        width: Dimension::Length(20.0),
907                        height: Dimension::Length(10.0),
908                    },
909                    ..Default::default()
910                },
911            )
912            .ok();
913        engine
914            .add_node(
915                wid(2),
916                Style {
917                    size: taffy::Size {
918                        width: Dimension::Length(20.0),
919                        height: Dimension::Length(10.0),
920                    },
921                    ..Default::default()
922                },
923            )
924            .ok();
925        engine
926            .add_node_with_children(
927                wid(10),
928                Style {
929                    gap: taffy::Size {
930                        width: LengthPercentage::Length(10.0),
931                        height: LengthPercentage::Length(0.0),
932                    },
933                    size: taffy::Size {
934                        width: Dimension::Length(80.0),
935                        height: Dimension::Length(10.0),
936                    },
937                    ..Default::default()
938                },
939                &[wid(1), wid(2)],
940            )
941            .ok();
942        engine.set_root(wid(10)).ok();
943        engine.compute(80, 10).ok();
944
945        let l1 = engine.layout(wid(1)).unwrap_or_default();
946        let l2 = engine.layout(wid(2)).unwrap_or_default();
947        assert_eq!(l1.x, 0);
948        assert_eq!(l2.x, 30); // 20 + 10 gap
949    }
950
951    // --- Grid layout tests (Task 6) ---
952
953    #[test]
954    fn grid_two_columns_equal() {
955        let mut engine = LayoutEngine::new();
956        engine.add_node(wid(1), Style::default()).ok();
957        engine.add_node(wid(2), Style::default()).ok();
958        engine
959            .add_node_with_children(
960                wid(10),
961                Style {
962                    display: Display::Grid,
963                    grid_template_columns: vec![fr(1.0), fr(1.0)],
964                    size: taffy::Size {
965                        width: Dimension::Length(80.0),
966                        height: Dimension::Length(20.0),
967                    },
968                    ..Default::default()
969                },
970                &[wid(1), wid(2)],
971            )
972            .ok();
973        engine.set_root(wid(10)).ok();
974        engine.compute(80, 20).ok();
975
976        let l1 = engine.layout(wid(1)).unwrap_or_default();
977        let l2 = engine.layout(wid(2)).unwrap_or_default();
978        assert_eq!(l1.width, 40);
979        assert_eq!(l2.width, 40);
980    }
981
982    #[test]
983    fn grid_three_columns_fr() {
984        let mut engine = LayoutEngine::new();
985        for i in 1..=3 {
986            engine.add_node(wid(i), Style::default()).ok();
987        }
988        engine
989            .add_node_with_children(
990                wid(10),
991                Style {
992                    display: Display::Grid,
993                    grid_template_columns: vec![fr(1.0), fr(2.0), fr(1.0)],
994                    size: taffy::Size {
995                        width: Dimension::Length(80.0),
996                        height: Dimension::Length(20.0),
997                    },
998                    ..Default::default()
999                },
1000                &[wid(1), wid(2), wid(3)],
1001            )
1002            .ok();
1003        engine.set_root(wid(10)).ok();
1004        engine.compute(80, 20).ok();
1005
1006        let l1 = engine.layout(wid(1)).unwrap_or_default();
1007        let l2 = engine.layout(wid(2)).unwrap_or_default();
1008        let l3 = engine.layout(wid(3)).unwrap_or_default();
1009        assert_eq!(l1.width, 20);
1010        assert_eq!(l2.width, 40);
1011        assert_eq!(l3.width, 20);
1012    }
1013
1014    #[test]
1015    fn grid_columns_mixed_units() {
1016        let mut engine = LayoutEngine::new();
1017        engine.add_node(wid(1), Style::default()).ok();
1018        engine.add_node(wid(2), Style::default()).ok();
1019        engine
1020            .add_node_with_children(
1021                wid(10),
1022                Style {
1023                    display: Display::Grid,
1024                    grid_template_columns: vec![length(20.0), fr(1.0)],
1025                    size: taffy::Size {
1026                        width: Dimension::Length(80.0),
1027                        height: Dimension::Length(20.0),
1028                    },
1029                    ..Default::default()
1030                },
1031                &[wid(1), wid(2)],
1032            )
1033            .ok();
1034        engine.set_root(wid(10)).ok();
1035        engine.compute(80, 20).ok();
1036
1037        let l1 = engine.layout(wid(1)).unwrap_or_default();
1038        let l2 = engine.layout(wid(2)).unwrap_or_default();
1039        assert_eq!(l1.width, 20);
1040        assert_eq!(l2.width, 60);
1041    }
1042
1043    #[test]
1044    fn grid_rows_and_columns() {
1045        let mut engine = LayoutEngine::new();
1046        for i in 1..=4 {
1047            engine.add_node(wid(i), Style::default()).ok();
1048        }
1049        engine
1050            .add_node_with_children(
1051                wid(10),
1052                Style {
1053                    display: Display::Grid,
1054                    grid_template_columns: vec![fr(1.0), fr(1.0)],
1055                    grid_template_rows: vec![fr(1.0), fr(1.0)],
1056                    size: taffy::Size {
1057                        width: Dimension::Length(80.0),
1058                        height: Dimension::Length(20.0),
1059                    },
1060                    ..Default::default()
1061                },
1062                &[wid(1), wid(2), wid(3), wid(4)],
1063            )
1064            .ok();
1065        engine.set_root(wid(10)).ok();
1066        engine.compute(80, 20).ok();
1067
1068        let l1 = engine.layout(wid(1)).unwrap_or_default();
1069        let l4 = engine.layout(wid(4)).unwrap_or_default();
1070        assert_eq!(l1.width, 40);
1071        assert_eq!(l1.height, 10);
1072        assert_eq!(l4.x, 40);
1073        assert_eq!(l4.y, 10);
1074    }
1075
1076    #[test]
1077    fn grid_placement_span() {
1078        let mut engine = LayoutEngine::new();
1079        // First child spans 2 columns
1080        engine
1081            .add_node(
1082                wid(1),
1083                Style {
1084                    grid_column: Line {
1085                        start: GridPlacement::from_span(2),
1086                        end: GridPlacement::Auto,
1087                    },
1088                    ..Default::default()
1089                },
1090            )
1091            .ok();
1092        engine.add_node(wid(2), Style::default()).ok();
1093        engine.add_node(wid(3), Style::default()).ok();
1094        engine
1095            .add_node_with_children(
1096                wid(10),
1097                Style {
1098                    display: Display::Grid,
1099                    grid_template_columns: vec![fr(1.0), fr(1.0)],
1100                    size: taffy::Size {
1101                        width: Dimension::Length(80.0),
1102                        height: Dimension::Length(30.0),
1103                    },
1104                    ..Default::default()
1105                },
1106                &[wid(1), wid(2), wid(3)],
1107            )
1108            .ok();
1109        engine.set_root(wid(10)).ok();
1110        engine.compute(80, 30).ok();
1111
1112        let l1 = engine.layout(wid(1)).unwrap_or_default();
1113        assert_eq!(l1.width, 80); // spans full width
1114    }
1115
1116    #[test]
1117    fn box_model_padding_shrinks_content() {
1118        let mut engine = LayoutEngine::new();
1119        engine
1120            .add_node(
1121                wid(1),
1122                Style {
1123                    flex_grow: 1.0,
1124                    ..Default::default()
1125                },
1126            )
1127            .ok();
1128        engine
1129            .add_node_with_children(
1130                wid(10),
1131                Style {
1132                    padding: taffy::Rect {
1133                        left: LengthPercentage::Length(5.0),
1134                        right: LengthPercentage::Length(5.0),
1135                        top: LengthPercentage::Length(2.0),
1136                        bottom: LengthPercentage::Length(2.0),
1137                    },
1138                    size: taffy::Size {
1139                        width: Dimension::Length(80.0),
1140                        height: Dimension::Length(24.0),
1141                    },
1142                    ..Default::default()
1143                },
1144                &[wid(1)],
1145            )
1146            .ok();
1147        engine.set_root(wid(10)).ok();
1148        engine.compute(80, 24).ok();
1149
1150        let l = engine.layout(wid(1)).unwrap_or_default();
1151        assert_eq!(l.x, 5);
1152        assert_eq!(l.y, 2);
1153        assert_eq!(l.width, 70); // 80 - 5 - 5
1154        assert_eq!(l.height, 20); // 24 - 2 - 2
1155    }
1156
1157    #[test]
1158    fn box_model_margin_creates_space() {
1159        let mut engine = LayoutEngine::new();
1160        engine
1161            .add_node(
1162                wid(1),
1163                Style {
1164                    size: taffy::Size {
1165                        width: Dimension::Length(30.0),
1166                        height: Dimension::Length(10.0),
1167                    },
1168                    margin: taffy::Rect {
1169                        left: LengthPercentageAuto::Length(0.0),
1170                        right: LengthPercentageAuto::Length(5.0),
1171                        top: LengthPercentageAuto::Length(0.0),
1172                        bottom: LengthPercentageAuto::Length(0.0),
1173                    },
1174                    ..Default::default()
1175                },
1176            )
1177            .ok();
1178        engine
1179            .add_node(
1180                wid(2),
1181                Style {
1182                    size: taffy::Size {
1183                        width: Dimension::Length(30.0),
1184                        height: Dimension::Length(10.0),
1185                    },
1186                    ..Default::default()
1187                },
1188            )
1189            .ok();
1190        engine
1191            .add_node_with_children(
1192                wid(10),
1193                Style {
1194                    size: taffy::Size {
1195                        width: Dimension::Length(80.0),
1196                        height: Dimension::Length(10.0),
1197                    },
1198                    ..Default::default()
1199                },
1200                &[wid(1), wid(2)],
1201            )
1202            .ok();
1203        engine.set_root(wid(10)).ok();
1204        engine.compute(80, 10).ok();
1205
1206        let l2 = engine.layout(wid(2)).unwrap_or_default();
1207        assert_eq!(l2.x, 35); // 30 + 5 margin
1208    }
1209
1210    #[test]
1211    fn box_model_border_width() {
1212        let mut engine = LayoutEngine::new();
1213        engine
1214            .add_node(
1215                wid(1),
1216                Style {
1217                    flex_grow: 1.0,
1218                    ..Default::default()
1219                },
1220            )
1221            .ok();
1222        engine
1223            .add_node_with_children(
1224                wid(10),
1225                Style {
1226                    border: taffy::Rect {
1227                        left: LengthPercentage::Length(1.0),
1228                        right: LengthPercentage::Length(1.0),
1229                        top: LengthPercentage::Length(1.0),
1230                        bottom: LengthPercentage::Length(1.0),
1231                    },
1232                    size: taffy::Size {
1233                        width: Dimension::Length(80.0),
1234                        height: Dimension::Length(24.0),
1235                    },
1236                    ..Default::default()
1237                },
1238                &[wid(1)],
1239            )
1240            .ok();
1241        engine.set_root(wid(10)).ok();
1242        engine.compute(80, 24).ok();
1243
1244        let l = engine.layout(wid(1)).unwrap_or_default();
1245        assert_eq!(l.x, 1);
1246        assert_eq!(l.y, 1);
1247        assert_eq!(l.width, 78); // 80 - 1 - 1
1248        assert_eq!(l.height, 22); // 24 - 1 - 1
1249    }
1250
1251    #[test]
1252    fn box_model_combined() {
1253        let mut engine = LayoutEngine::new();
1254        engine
1255            .add_node(
1256                wid(1),
1257                Style {
1258                    flex_grow: 1.0,
1259                    ..Default::default()
1260                },
1261            )
1262            .ok();
1263        engine
1264            .add_node_with_children(
1265                wid(10),
1266                Style {
1267                    padding: taffy::Rect {
1268                        left: LengthPercentage::Length(2.0),
1269                        right: LengthPercentage::Length(2.0),
1270                        top: LengthPercentage::Length(1.0),
1271                        bottom: LengthPercentage::Length(1.0),
1272                    },
1273                    border: taffy::Rect {
1274                        left: LengthPercentage::Length(1.0),
1275                        right: LengthPercentage::Length(1.0),
1276                        top: LengthPercentage::Length(1.0),
1277                        bottom: LengthPercentage::Length(1.0),
1278                    },
1279                    size: taffy::Size {
1280                        width: Dimension::Length(80.0),
1281                        height: Dimension::Length(24.0),
1282                    },
1283                    ..Default::default()
1284                },
1285                &[wid(1)],
1286            )
1287            .ok();
1288        engine.set_root(wid(10)).ok();
1289        engine.compute(80, 24).ok();
1290
1291        let l = engine.layout(wid(1)).unwrap_or_default();
1292        // border(1) + padding(2) = 3 each side
1293        assert_eq!(l.x, 3);
1294        assert_eq!(l.y, 2); // border(1) + padding(1)
1295        assert_eq!(l.width, 74); // 80 - 3 - 3
1296        assert_eq!(l.height, 20); // 24 - 2 - 2
1297    }
1298
1299    #[test]
1300    fn layout_error_display() {
1301        let e1 = LayoutError::WidgetNotFound(wid(42));
1302        assert!(format!("{e1}").contains("42"));
1303
1304        let e2 = LayoutError::TaffyError("boom".into());
1305        assert!(format!("{e2}").contains("boom"));
1306
1307        let e3 = LayoutError::NoRoot;
1308        assert!(format!("{e3}").contains("no root"));
1309    }
1310}