Skip to main content

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