Skip to main content

jag_ui/
layout.rs

1//! Thin wrapper around [Taffy](https://docs.rs/taffy) for flexbox / grid
2//! layout.
3//!
4//! Each layout node can optionally carry a [`FocusId`] so the layout tree
5//! can be correlated with focusable UI elements.
6
7use taffy::prelude::*;
8
9use crate::focus::FocusId;
10
11/// Layout tree backed by Taffy.
12pub struct Layout {
13    tree: TaffyTree<Option<FocusId>>,
14}
15
16impl std::fmt::Debug for Layout {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.debug_struct("Layout").finish_non_exhaustive()
19    }
20}
21
22impl Default for Layout {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl Layout {
29    /// Create an empty layout tree.
30    pub fn new() -> Self {
31        Self {
32            tree: TaffyTree::new(),
33        }
34    }
35
36    /// Add a leaf node with the given style and optional [`FocusId`].
37    pub fn add_node(&mut self, style: Style, id: Option<FocusId>) -> NodeId {
38        self.tree
39            .new_leaf_with_context(style, id)
40            .expect("taffy: failed to create leaf node")
41    }
42
43    /// Attach `child` as a child of `parent`.
44    pub fn add_child(&mut self, parent: NodeId, child: NodeId) {
45        self.tree
46            .add_child(parent, child)
47            .expect("taffy: failed to add child");
48    }
49
50    /// Compute layout for the subtree rooted at `root`.
51    pub fn compute(&mut self, root: NodeId, available: Size<AvailableSpace>) {
52        self.tree
53            .compute_layout(root, available)
54            .expect("taffy: layout computation failed");
55    }
56
57    /// Retrieve the computed layout for a node.
58    pub fn get_layout(&self, node: NodeId) -> &taffy::tree::Layout {
59        self.tree
60            .layout(node)
61            .expect("taffy: layout not computed for node")
62    }
63
64    /// Remove all nodes from the layout tree.
65    pub fn clear(&mut self) {
66        self.tree.clear();
67    }
68}
69
70// ---------------------------------------------------------------------------
71// Tests
72// ---------------------------------------------------------------------------
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn add_node_and_get_layout() {
80        let mut layout = Layout::new();
81        let root = layout.add_node(
82            Style {
83                size: Size {
84                    width: length(100.0),
85                    height: length(50.0),
86                },
87                ..Default::default()
88            },
89            None,
90        );
91        layout.compute(root, Size::MAX_CONTENT);
92
93        let result = layout.get_layout(root);
94        assert!((result.size.width - 100.0).abs() < f32::EPSILON);
95        assert!((result.size.height - 50.0).abs() < f32::EPSILON);
96    }
97
98    #[test]
99    fn parent_child_layout() {
100        let mut layout = Layout::new();
101
102        let child = layout.add_node(
103            Style {
104                size: Size {
105                    width: length(40.0),
106                    height: length(20.0),
107                },
108                ..Default::default()
109            },
110            Some(FocusId(1)),
111        );
112
113        let parent = layout.add_node(
114            Style {
115                size: Size {
116                    width: length(200.0),
117                    height: length(100.0),
118                },
119                ..Default::default()
120            },
121            None,
122        );
123
124        layout.add_child(parent, child);
125        layout.compute(parent, Size::MAX_CONTENT);
126
127        let child_layout = layout.get_layout(child);
128        assert!((child_layout.size.width - 40.0).abs() < f32::EPSILON);
129        assert!((child_layout.size.height - 20.0).abs() < f32::EPSILON);
130
131        // Child should be positioned at (0, 0) within parent by default.
132        assert!((child_layout.location.x).abs() < f32::EPSILON);
133        assert!((child_layout.location.y).abs() < f32::EPSILON);
134    }
135
136    #[test]
137    fn clear_allows_reuse() {
138        let mut layout = Layout::new();
139        let _node = layout.add_node(Style::default(), None);
140        layout.clear();
141
142        // Should be able to add nodes again after clear.
143        let root = layout.add_node(
144            Style {
145                size: Size {
146                    width: length(10.0),
147                    height: length(10.0),
148                },
149                ..Default::default()
150            },
151            None,
152        );
153        layout.compute(root, Size::MAX_CONTENT);
154        let result = layout.get_layout(root);
155        assert!((result.size.width - 10.0).abs() < f32::EPSILON);
156    }
157}