Skip to main content

uzor_core/layout/
tree.rs

1use std::collections::HashMap;
2use crate::types::rect::Rect;
3use crate::types::state::WidgetId;
4use super::types::*;
5
6#[derive(Clone, Debug)]
7pub struct LayoutTree {
8    pub root: LayoutNode,
9    pub computed: HashMap<WidgetId, LayoutComputed>,
10}
11
12impl LayoutTree {
13    pub fn new(root: LayoutNode) -> Self {
14        Self {
15            root,
16            computed: HashMap::new(),
17        }
18    }
19
20    /// Compute layout for the entire tree given a viewport size
21    pub fn compute(&mut self, viewport: Rect) {
22        self.computed.clear();
23        
24        let mut ctx = LayoutContext {
25            computed: &mut self.computed,
26        };
27
28        // Initial pass: layout the root
29        layout_node_at(&self.root, viewport, &mut ctx, 0, None);
30    }
31
32    pub fn get_rect(&self, id: &WidgetId) -> Option<Rect> {
33        self.computed.get(id).map(|c| c.rect)
34    }
35
36    pub fn get_computed(&self, id: &WidgetId) -> Option<&LayoutComputed> {
37        self.computed.get(id)
38    }
39}
40
41struct LayoutContext<'a> {
42    computed: &'a mut HashMap<WidgetId, LayoutComputed>,
43}
44
45fn layout_node_at(
46    node: &LayoutNode,
47    target_rect: Rect,
48    ctx: &mut LayoutContext,
49    parent_z: i32,
50    parent_clip: Option<Rect>,
51) {
52    // 1. Apply margins to get the border box
53    let margin = &node.style.margin;
54    let x = target_rect.min_x() + margin.left + node.style.offset_x;
55    let y = target_rect.min_y() + margin.top + node.style.offset_y;
56    
57    // 2. Determine actual size (if specific size requested vs target_rect provided by parent)
58    // If parent passed a specific rect, we generally respect it (it already calculated flex).
59    // If we have Fix size, we enforce it? 
60    // For this "minimal" engine, we assume the parent did the flex math and gave us our box.
61    
62    let width = target_rect.width() - margin.width();
63    let height = target_rect.height() - margin.height();
64    
65    let final_rect = Rect::new(x, y, width.max(0.0), height.max(0.0));
66    
67    // 3. Compute Content Rect (minus padding)
68    let padding = &node.style.padding;
69    let content_x = x + padding.left;
70    let content_y = y + padding.top;
71    let content_w = (width - padding.width()).max(0.0);
72    let content_h = (height - padding.height()).max(0.0);
73    let content_rect = Rect::new(content_x, content_y, content_w, content_h);
74    
75    // 4. Handle Clipping
76    let mut current_clip = parent_clip;
77    if node.flags.contains(LayoutFlags::CLIP_CONTENT) {
78        current_clip = match current_clip {
79            Some(parent) => Some(parent.intersect(content_rect)), // Intersection logic needed in WidgetRect
80            None => Some(content_rect),
81        };
82    }
83    
84    // 5. Z-Index
85    let z_order = parent_z + node.style.z_index;
86    
87    // 6. Store computed
88    ctx.computed.insert(node.id.clone(), LayoutComputed {
89        rect: final_rect,
90        content_rect,
91        clip_rect: current_clip,
92        z_order,
93    });
94    
95    // 7. Layout Children
96    match node.style.display {
97        Display::Flex => layout_flex(node, content_rect, ctx, z_order, current_clip),
98        Display::Stack => layout_stack(node, content_rect, ctx, z_order, current_clip),
99        Display::None => {}, // Skip children
100        Display::Grid => layout_flex(node, content_rect, ctx, z_order, current_clip), // Fallback
101    }
102}
103
104fn layout_flex(
105    node: &LayoutNode,
106    content_rect: Rect,
107    ctx: &mut LayoutContext,
108    z: i32,
109    clip: Option<Rect>,
110) {
111    let dir = node.style.direction;
112    let gap = node.style.gap;
113    
114    // Simplified Flex:
115    // 1. Count fixed vs fill children
116    // 2. Allocate fixed
117    // 3. Distribute remaining to fill
118    
119    let mut total_fixed = 0.0;
120    let mut fill_count = 0;
121    
122    for child in &node.children {
123        if child.style.display == Display::None || child.style.position == Position::Absolute { continue; }
124        
125        let size_spec = match dir {
126            FlexDirection::Row => child.style.width,
127            FlexDirection::Column => child.style.height,
128        };
129        
130        match size_spec {
131            SizeSpec::Fix(v) => total_fixed += v,
132            SizeSpec::Pct(p) => {
133                let basis = match dir {
134                    FlexDirection::Row => content_rect.width(),
135                    FlexDirection::Column => content_rect.height(),
136                };
137                total_fixed += basis * p;
138            },
139            SizeSpec::Fill => fill_count += 1,
140            SizeSpec::Content => {
141                // To support Content properly we need a measure pass. 
142                // For now treat as Fill or Min? Let's treat as "Fit content" -> requires measure.
143                // Minimal hack: treat as Fix(0) but let it expand?
144                // Let's assume Fix(0) for V1
145            }
146        }
147    }
148    
149    // Add gaps
150    let visible_children = node.children.iter().filter(|c| c.style.display != Display::None && c.style.position != Position::Absolute).count();
151    let total_gap = if visible_children > 1 { (visible_children - 1) as f64 * gap } else { 0.0 };
152    total_fixed += total_gap;
153    
154    // Calculate flexible unit
155    let available_space = match dir {
156        FlexDirection::Row => content_rect.width(),
157        FlexDirection::Column => content_rect.height(),
158    };
159    
160    let remaining = (available_space - total_fixed).max(0.0);
161    let flex_unit = if fill_count > 0 { remaining / fill_count as f64 } else { 0.0 };
162    
163    // Layout pass
164    let mut cursor = match dir {
165        FlexDirection::Row => content_rect.min_x(),
166        FlexDirection::Column => content_rect.min_y(),
167    };
168    
169    for child in &node.children {
170        if child.style.display == Display::None { continue; }
171        
172        // Handle Absolute Children separately
173        if child.style.position == Position::Absolute {
174            // Absolute is relative to parent content rect
175             // TODO: Support top/left/right/bottom constraints
176             // For now just give it parent content rect? Or 0 size?
177             // Let's assume Absolute takes 0 size flow and is placed at top-left of content
178             layout_node_at(child, content_rect, ctx, z + 100, clip);
179             continue;
180        }
181
182        let (child_w, child_h) = match dir {
183            FlexDirection::Row => {
184                let w = match child.style.width {
185                    SizeSpec::Fix(v) => v,
186                    SizeSpec::Pct(p) => content_rect.width() * p,
187                    SizeSpec::Fill => flex_unit,
188                    SizeSpec::Content => 100.0, // Placeholder
189                };
190                let h = match child.style.height {
191                    SizeSpec::Fix(v) => v,
192                    SizeSpec::Pct(p) => content_rect.height() * p,
193                    SizeSpec::Fill => content_rect.height(),
194                    SizeSpec::Content => content_rect.height(), // Stretch cross axis by default
195                };
196                (w, h)
197            },
198            FlexDirection::Column => {
199                let h = match child.style.height {
200                    SizeSpec::Fix(v) => v,
201                    SizeSpec::Pct(p) => content_rect.height() * p,
202                    SizeSpec::Fill => flex_unit,
203                    SizeSpec::Content => 30.0, // Placeholder
204                };
205                let w = match child.style.width {
206                    SizeSpec::Fix(v) => v,
207                    SizeSpec::Pct(p) => content_rect.width() * p,
208                    SizeSpec::Fill => content_rect.width(),
209                    SizeSpec::Content => content_rect.width(), // Stretch cross axis by default
210                };
211                (w, h)
212            }
213        };
214        
215        let child_x = match dir {
216            FlexDirection::Row => cursor,
217            FlexDirection::Column => content_rect.min_x(),
218        };
219        let child_y = match dir {
220            FlexDirection::Row => content_rect.min_y(),
221            FlexDirection::Column => cursor,
222        };
223        
224        let child_rect = Rect::new(child_x, child_y, child_w, child_h);
225        layout_node_at(child, child_rect, ctx, z, clip);
226        
227        // Advance cursor
228        match dir {
229            FlexDirection::Row => cursor += child_w + gap,
230            FlexDirection::Column => cursor += child_h + gap,
231        }
232    }
233}
234
235fn layout_stack(
236    node: &LayoutNode,
237    content_rect: Rect,
238    ctx: &mut LayoutContext,
239    z: i32,
240    clip: Option<Rect>,
241) {
242    // Stack: all children get the full content rect
243    for (i, child) in node.children.iter().enumerate() {
244        if child.style.display == Display::None { continue; }
245        
246        // Z-index increases for each child in stack unless specified
247        let child_z = z + child.style.z_index + (i as i32);
248        
249        layout_node_at(child, content_rect, ctx, child_z, clip);
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_flex_column_layout() {
259        // Root: VBox
260        // - Header: Fix(50)
261        // - Content: Fill
262        
263        let header = LayoutNode::new("header")
264            .with_style(LayoutStyle {
265                height: SizeSpec::Fix(50.0),
266                width: SizeSpec::Fill,
267                ..Default::default()
268            });
269
270        let content = LayoutNode::new("content")
271            .with_style(LayoutStyle {
272                height: SizeSpec::Fill,
273                width: SizeSpec::Fill,
274                ..Default::default()
275            });
276
277        let root = LayoutNode::new("root")
278            .with_style(LayoutStyle {
279                display: Display::Flex,
280                direction: FlexDirection::Column,
281                width: SizeSpec::Fix(200.0),
282                height: SizeSpec::Fix(200.0),
283                ..Default::default()
284            })
285            .with_child(header)
286            .with_child(content);
287
288        let mut tree = LayoutTree::new(root);
289        let viewport = Rect::new(0.0, 0.0, 200.0, 200.0);
290        
291        tree.compute(viewport);
292
293        // Check Root
294        let root_rect = tree.get_rect(&WidgetId::new("root")).unwrap();
295        assert_eq!(root_rect.width, 200.0);
296        assert_eq!(root_rect.height, 200.0);
297
298        // Check Header
299        let header_rect = tree.get_rect(&WidgetId::new("header")).unwrap();
300        assert_eq!(header_rect.y, 0.0);
301        assert_eq!(header_rect.height, 50.0);
302        assert_eq!(header_rect.width, 200.0);
303
304        // Check Content
305        let content_rect = tree.get_rect(&WidgetId::new("content")).unwrap();
306        assert_eq!(content_rect.y, 50.0);
307        assert_eq!(content_rect.height, 150.0); // 200 - 50
308        assert_eq!(content_rect.width, 200.0);
309    }
310
311    #[test]
312    fn test_flex_row_layout_with_gap() {
313        // Root: HBox, gap=10
314        // - Left: Fix(50)
315        // - Right: Fill
316
317        let left = LayoutNode::new("left")
318            .with_style(LayoutStyle {
319                width: SizeSpec::Fix(50.0),
320                height: SizeSpec::Fill,
321                ..Default::default()
322            });
323
324        let right = LayoutNode::new("right")
325            .with_style(LayoutStyle {
326                width: SizeSpec::Fill,
327                height: SizeSpec::Fill,
328                ..Default::default()
329            });
330
331        let root = LayoutNode::new("root")
332            .with_style(LayoutStyle {
333                display: Display::Flex,
334                direction: FlexDirection::Row,
335                gap: 10.0,
336                width: SizeSpec::Fix(200.0),
337                height: SizeSpec::Fix(100.0),
338                ..Default::default()
339            })
340            .with_child(left)
341            .with_child(right);
342
343        let mut tree = LayoutTree::new(root);
344        let viewport = Rect::new(0.0, 0.0, 200.0, 100.0);
345        
346        tree.compute(viewport);
347
348        let left_rect = tree.get_rect(&WidgetId::new("left")).unwrap();
349        assert_eq!(left_rect.x, 0.0);
350        assert_eq!(left_rect.width, 50.0);
351
352        let right_rect = tree.get_rect(&WidgetId::new("right")).unwrap();
353        assert_eq!(right_rect.x, 60.0); // 50 + 10 gap
354        assert_eq!(right_rect.width, 140.0); // 200 - 50 - 10
355    }
356}