fast_rich/
layout.rs

1use crate::console::RenderContext;
2use crate::renderable::{Renderable, Segment};
3use std::sync::Arc;
4
5/// A node in the layout tree for creating splits and grids.
6#[derive(Clone)]
7pub struct Layout {
8    /// Renderable content (optional, for leaf nodes).
9    renderable: Option<Arc<dyn Renderable + Send + Sync>>,
10    /// Child layouts.
11    children: Vec<Layout>,
12    /// Split direction.
13    direction: Direction,
14    /// Fixed size (width or height depending on parent direction).
15    size: Option<u16>,
16    /// Ratio for flexible sizing.
17    ratio: u32,
18    /// Name for debugging.
19    name: Option<String>,
20    /// Minimum size.
21    minimum_size: u16,
22    /// Is this layout visible?
23    visible: bool,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum Direction {
28    Horizontal,
29    Vertical,
30}
31
32impl Layout {
33    /// Create a new empty layout.
34    pub fn new() -> Self {
35        Self {
36            renderable: None,
37            children: Vec::new(),
38            direction: Direction::Vertical,
39            size: None,
40            ratio: 1,
41            name: None,
42            minimum_size: 0,
43            visible: true,
44        }
45    }
46
47    /// Set the name of the layout (useful for debugging).
48    pub fn with_name(mut self, name: &str) -> Self {
49        self.name = Some(name.to_string());
50        self
51    }
52
53    /// Set a fixed size for this layout.
54    pub fn with_size(mut self, size: u16) -> Self {
55        self.size = Some(size);
56        self
57    }
58
59    /// Set a ratio for this layout (default is 1).
60    pub fn with_ratio(mut self, ratio: u32) -> Self {
61        self.ratio = ratio;
62        self
63    }
64
65    /// Set a minimum size for this layout.
66    pub fn with_minimum_size(mut self, size: u16) -> Self {
67        self.minimum_size = size;
68        self
69    }
70
71    /// Set the renderable content.
72    pub fn update<R: Renderable + Send + Sync + 'static>(&mut self, renderable: R) {
73        self.renderable = Some(Arc::new(renderable));
74    }
75
76    /// Get mutable access to children.
77    pub fn children_mut(&mut self) -> &mut Vec<Layout> {
78        &mut self.children
79    }
80
81    /// Split the layout horizontally (into columns).
82    pub fn split_row(&mut self, layouts: Vec<Layout>) {
83        self.direction = Direction::Horizontal;
84        self.children = layouts;
85    }
86
87    /// Split the layout vertically (into rows).
88    pub fn split_column(&mut self, layouts: Vec<Layout>) {
89        self.direction = Direction::Vertical;
90        self.children = layouts;
91    }
92
93    /// Calculate split sizes for a given total space.
94    fn calculate_splits(&self, total_size: u16) -> Vec<u16> {
95        let count = self.children.len();
96        if count == 0 {
97            return Vec::new();
98        }
99
100        let mut sizes = vec![0; count];
101        let mut remaining = total_size;
102        let mut flexible_indices = Vec::new();
103
104        // 1. Assign fixed sizes
105        for (i, child) in self.children.iter().enumerate() {
106            if let Some(fixed) = child.size {
107                let s = std::cmp::min(fixed, remaining);
108                sizes[i] = s;
109                remaining -= s;
110            } else {
111                flexible_indices.push(i);
112            }
113        }
114
115        // 2. Resolve flexible sizes
116        let mut candidates = flexible_indices;
117
118        while !candidates.is_empty() {
119            let total_ratio: u32 = candidates.iter().map(|&i| self.children[i].ratio).sum();
120
121            // If remaining is 0 or no ratio, fill rest with 0
122            if remaining == 0 || total_ratio == 0 {
123                for &i in &candidates {
124                    sizes[i] = 0;
125                }
126                break;
127            }
128
129            let unit = remaining as f64 / total_ratio as f64;
130
131            // Find if any candidate needs to be fixed to min_size
132            let mut violator = None;
133            for (idx_in_candidates, &i) in candidates.iter().enumerate() {
134                let child = &self.children[i];
135                let ideal = child.ratio as f64 * unit;
136                if ideal < child.minimum_size as f64 {
137                    violator = Some(idx_in_candidates);
138                    break; // Fix one at a time
139                }
140            }
141
142            if let Some(idx_c) = violator {
143                let i = candidates.remove(idx_c);
144                let child = &self.children[i];
145                let s = std::cmp::min(child.minimum_size, remaining);
146                sizes[i] = s;
147                remaining -= s;
148            } else {
149                // No violators, distribute rest
150                let mut distributed = 0;
151                for (idx, &i) in candidates.iter().enumerate() {
152                    let child = &self.children[i];
153                    let s = if idx == candidates.len() - 1 {
154                        remaining - distributed
155                    } else {
156                        (child.ratio as f64 * unit).round() as u16
157                    };
158                    sizes[i] = s;
159                    distributed += s;
160                }
161                break;
162            }
163        }
164
165        sizes
166    }
167}
168
169impl Default for Layout {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175impl Renderable for Layout {
176    fn render(&self, context: &RenderContext) -> Vec<Segment> {
177        if !self.visible {
178            return Vec::new();
179        }
180
181        // Leaf node: Render content
182        if self.children.is_empty() {
183            if let Some(r) = &self.renderable {
184                // TODO: Optimization - Pass specific constraint context based on parent split
185                // For now, we render with full context width, but we might want to clip/shape it using new context
186                return r.render(context);
187            }
188            // Empty placeholder
189            let blank_line = " ".repeat(context.width);
190            return vec![Segment::new(vec![crate::text::Span::raw(blank_line)])];
191        }
192
193
194        // Branch node: Calculate splits
195        let (width, _height) = match self.direction {
196            Direction::Horizontal => (context.width as u16, context.height.unwrap_or(0) as u16),
197            Direction::Vertical => (
198                context.width as u16,
199                context.height.unwrap_or(0) as u16,
200            ),
201        };
202
203        let mut segments = Vec::new();
204
205        if self.direction == Direction::Vertical {
206            if let Some(total_height) = context.height {
207                // Fixed height: Calculate splits based on height
208                let splits = self.calculate_splits(total_height as u16);
209                
210                for (i, child) in self.children.iter().enumerate() {
211                    let h = splits[i] as usize;
212                    if h == 0 {
213                        continue;
214                    }
215
216                    // Render child with constrained height
217                    let child_ctx = RenderContext {
218                        width: context.width,
219                        height: Some(h),
220                    };
221                    let child_segments = child.render(&child_ctx);
222
223                    // Ensure we output exactly `h` lines
224                    // 1. Take up to `h` lines from render output
225                    let mut count = 0;
226                    for segment in child_segments {
227                        if count < h {
228                            segments.push(segment);
229                            count += 1;
230                        } else {
231                            break;
232                        }
233                    }
234
235                    // 2. Pad with empty lines if short
236                    if count < h {
237                        let blank_line = " ".repeat(context.width);
238                        for _ in count..h {
239                            segments.push(Segment::new(vec![crate::text::Span::raw(blank_line.clone())]));
240                        }
241                    }
242                }
243            } else {
244                // Unconstrained height: Stack children vertically (Flow layout)
245                for child in &self.children {
246                    let child_segments = child.render(context);
247                    segments.extend(child_segments);
248                }
249            }
250        } else {
251            // Horizontal split (Columns)
252            let splits = self.calculate_splits(width);
253            let mut columns_output: Vec<Vec<Segment>> = Vec::new();
254            let mut max_lines = 0;
255
256            // If we have a fixed height, we expect all columns to be that height (or padded to it)
257            // If explicit height is None, we determine max height from content.
258            let target_height = context.height;
259
260            for (i, child) in self.children.iter().enumerate() {
261                let w = splits[i] as usize;
262                if w == 0 {
263                    columns_output.push(Vec::new());
264                    continue;
265                }
266
267                // Pass through the parent's height constraint to children
268                let child_ctx = RenderContext { 
269                    width: w, 
270                    height: target_height,
271                };
272                let child_segs = child.render(&child_ctx);
273                max_lines = std::cmp::max(max_lines, child_segs.len());
274                columns_output.push(child_segs);
275            }
276
277            // If we have a target height, use it as the number of lines to output
278            // (Assuming children respected it, max_lines should match target_height, 
279            // but we use max_lines if target_height is None, or target_height if Some)
280             let final_lines = target_height.unwrap_or(max_lines);
281
282            // Zip lines
283            for line_idx in 0..final_lines {
284                let mut line_spans = Vec::new();
285                for (col_idx, _child) in self.children.iter().enumerate() {
286                    let w = splits[col_idx] as usize;
287                    let segs = &columns_output[col_idx];
288
289                    if line_idx < segs.len() {
290                        line_spans.extend(segs[line_idx].spans.clone());
291                    } else {
292                        // Empty space for this column
293                        line_spans.push(crate::text::Span::raw(" ".repeat(w)));
294                    }
295                }
296                segments.push(Segment::line(line_spans));
297            }
298        }
299
300        segments
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_calculate_splits_ratios() {
310        // Equal split
311        let mut layout = Layout::new();
312        layout.split_row(vec![
313            Layout::new().with_ratio(1),
314            Layout::new().with_ratio(1),
315        ]);
316        let splits = layout.calculate_splits(100);
317        assert_eq!(splits, vec![50, 50]);
318
319        // 1:3 split
320        let mut layout = Layout::new();
321        layout.split_row(vec![
322            Layout::new().with_ratio(1),
323            Layout::new().with_ratio(3),
324        ]);
325        let splits = layout.calculate_splits(100);
326        assert_eq!(splits, vec![25, 75]);
327    }
328
329    #[test]
330    fn test_calculate_splits_fixed() {
331        let mut layout = Layout::new();
332        layout.split_row(vec![
333            Layout::new().with_size(10),
334            Layout::new().with_size(20),
335        ]);
336        let splits = layout.calculate_splits(100);
337        // If NO ratio items, implementation should just give fixed?
338        // Wait, if NO ratio items, `flexible_indices` is empty, so loop 2 doesn't run.
339        // So expected is [10, 20]. (Correct)
340        assert_eq!(splits[0], 10);
341        assert_eq!(splits[1], 20);
342    }
343
344    #[test]
345    fn test_calculate_splits_mixed() {
346        let mut layout = Layout::new();
347        layout.split_row(vec![
348            Layout::new().with_size(10), // Fixed 10
349            Layout::new().with_ratio(1), // Takes half of remaining (90/2 = 45)
350            Layout::new().with_ratio(1), // Takes other half (45)
351        ]);
352        let splits = layout.calculate_splits(100);
353        assert_eq!(splits, vec![10, 45, 45]);
354    }
355
356    #[test]
357    fn test_calculate_splits_rounding() {
358        // 100 / 3 = 33.333
359        // Should be 33, 33, 34
360        let mut layout = Layout::new();
361        layout.split_row(vec![
362            Layout::new().with_ratio(1),
363            Layout::new().with_ratio(1),
364            Layout::new().with_ratio(1),
365        ]);
366        let splits = layout.calculate_splits(100);
367        assert_eq!(splits, vec![33, 33, 34]);
368        assert_eq!(splits.iter().sum::<u16>(), 100);
369    }
370    #[test]
371    fn test_calculate_splits_min_size_simple() {
372        let mut layout = Layout::new();
373        layout.split_row(vec![
374            Layout::new().with_ratio(1).with_minimum_size(60),
375            Layout::new().with_ratio(1),
376        ]);
377        let splits = layout.calculate_splits(100);
378        assert_eq!(splits, vec![60, 40]);
379    }
380
381    #[test]
382    fn test_calculate_splits_min_size_priority() {
383        let mut layout = Layout::new();
384        layout.split_row(vec![
385            Layout::new().with_ratio(1).with_minimum_size(80),
386            Layout::new().with_ratio(1).with_minimum_size(10),
387        ]);
388        let splits = layout.calculate_splits(100);
389        assert_eq!(splits, vec![80, 20]);
390    }
391
392    #[test]
393    fn test_calculate_splits_complex_min() {
394        let mut layout = Layout::new();
395        layout.split_row(vec![
396            Layout::new().with_size(5),
397            Layout::new().with_ratio(1).with_minimum_size(10),
398            Layout::new().with_ratio(1),
399        ]);
400        let splits = layout.calculate_splits(20);
401        assert_eq!(splits, vec![5, 10, 5]);
402    }
403
404    #[test]
405    fn test_vertical_split_ratios() {
406        let mut layout = Layout::new();
407        layout.split_column(vec![
408            Layout::new().with_ratio(1).with_name("Top"),
409            Layout::new().with_ratio(1).with_name("Bottom"),
410        ]);
411
412        // Mock context with height
413        let context = RenderContext { width: 80, height: Some(10) };
414        let segments = layout.render(&context);
415
416        // Should have 10 lines total
417        assert_eq!(segments.len(), 10);
418        // 80 chars wide - check first line
419        if !segments.is_empty() {
420             assert_eq!(segments[0].plain_text().len(), 80);
421        }
422    }
423
424    #[test]
425    fn test_vertical_split_stacking() {
426        let mut layout = Layout::new();
427        layout.split_column(vec![
428            Layout::new().with_size(1).with_name("Top"),
429            Layout::new().with_name("Bottom"),
430        ]);
431
432        // Unconstrained height
433        let context = RenderContext { width: 80, height: None };
434        let segments = layout.render(&context);
435
436        // Each leaf layout renders 1 blank line by default if empty
437        assert_eq!(segments.len(), 2);
438    }
439
440    #[test]
441    fn test_horizontal_split_propagates_height() {
442        let mut layout = Layout::new();
443        layout.split_row(vec![
444            Layout::new().with_ratio(1),
445            Layout::new().with_ratio(1),
446        ]);
447
448        // If we pass a height, it should be enforced on children (columns)
449        let context = RenderContext { width: 80, height: Some(5) };
450        let segments = layout.render(&context);
451
452        // Should have 5 lines
453        assert_eq!(segments.len(), 5);
454    }
455}