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        // Branch node: Calculate splits
194        let (width, _height) = match self.direction {
195            Direction::Horizontal => (context.width as u16, context.height.unwrap_or(0) as u16),
196            Direction::Vertical => (context.width as u16, context.height.unwrap_or(0) as u16),
197        };
198
199        let mut segments = Vec::new();
200
201        if self.direction == Direction::Vertical {
202            if let Some(total_height) = context.height {
203                // Fixed height: Calculate splits based on height
204                let splits = self.calculate_splits(total_height as u16);
205
206                for (i, child) in self.children.iter().enumerate() {
207                    let h = splits[i] as usize;
208                    if h == 0 {
209                        continue;
210                    }
211
212                    // Render child with constrained height
213                    let child_ctx = RenderContext {
214                        width: context.width,
215                        height: Some(h),
216                    };
217                    let child_segments = child.render(&child_ctx);
218
219                    // Ensure we output exactly `h` lines
220                    // 1. Take up to `h` lines from render output
221                    let mut count = 0;
222                    for segment in child_segments {
223                        if count < h {
224                            segments.push(segment);
225                            count += 1;
226                        } else {
227                            break;
228                        }
229                    }
230
231                    // 2. Pad with empty lines if short
232                    if count < h {
233                        let blank_line = " ".repeat(context.width);
234                        for _ in count..h {
235                            segments.push(Segment::new(vec![crate::text::Span::raw(
236                                blank_line.clone(),
237                            )]));
238                        }
239                    }
240                }
241            } else {
242                // Unconstrained height: Stack children vertically (Flow layout)
243                for child in &self.children {
244                    let child_segments = child.render(context);
245                    segments.extend(child_segments);
246                }
247            }
248        } else {
249            // Horizontal split (Columns)
250            let splits = self.calculate_splits(width);
251            let mut columns_output: Vec<Vec<Segment>> = Vec::new();
252            let mut max_lines = 0;
253
254            // If we have a fixed height, we expect all columns to be that height (or padded to it)
255            // If explicit height is None, we determine max height from content.
256            let target_height = context.height;
257
258            for (i, child) in self.children.iter().enumerate() {
259                let w = splits[i] as usize;
260                if w == 0 {
261                    columns_output.push(Vec::new());
262                    continue;
263                }
264
265                // Pass through the parent's height constraint to children
266                let child_ctx = RenderContext {
267                    width: w,
268                    height: target_height,
269                };
270                let child_segs = child.render(&child_ctx);
271                max_lines = std::cmp::max(max_lines, child_segs.len());
272                columns_output.push(child_segs);
273            }
274
275            // If we have a target height, use it as the number of lines to output
276            // (Assuming children respected it, max_lines should match target_height,
277            // but we use max_lines if target_height is None, or target_height if Some)
278            let final_lines = target_height.unwrap_or(max_lines);
279
280            // Zip lines
281            for line_idx in 0..final_lines {
282                let mut line_spans = Vec::new();
283                for (col_idx, _child) in self.children.iter().enumerate() {
284                    let w = splits[col_idx] as usize;
285                    let segs = &columns_output[col_idx];
286
287                    if line_idx < segs.len() {
288                        line_spans.extend(segs[line_idx].spans.clone());
289                    } else {
290                        // Empty space for this column
291                        line_spans.push(crate::text::Span::raw(" ".repeat(w)));
292                    }
293                }
294                segments.push(Segment::line(line_spans));
295            }
296        }
297
298        segments
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_calculate_splits_ratios() {
308        // Equal split
309        let mut layout = Layout::new();
310        layout.split_row(vec![
311            Layout::new().with_ratio(1),
312            Layout::new().with_ratio(1),
313        ]);
314        let splits = layout.calculate_splits(100);
315        assert_eq!(splits, vec![50, 50]);
316
317        // 1:3 split
318        let mut layout = Layout::new();
319        layout.split_row(vec![
320            Layout::new().with_ratio(1),
321            Layout::new().with_ratio(3),
322        ]);
323        let splits = layout.calculate_splits(100);
324        assert_eq!(splits, vec![25, 75]);
325    }
326
327    #[test]
328    fn test_calculate_splits_fixed() {
329        let mut layout = Layout::new();
330        layout.split_row(vec![
331            Layout::new().with_size(10),
332            Layout::new().with_size(20),
333        ]);
334        let splits = layout.calculate_splits(100);
335        // If NO ratio items, implementation should just give fixed?
336        // Wait, if NO ratio items, `flexible_indices` is empty, so loop 2 doesn't run.
337        // So expected is [10, 20]. (Correct)
338        assert_eq!(splits[0], 10);
339        assert_eq!(splits[1], 20);
340    }
341
342    #[test]
343    fn test_calculate_splits_mixed() {
344        let mut layout = Layout::new();
345        layout.split_row(vec![
346            Layout::new().with_size(10), // Fixed 10
347            Layout::new().with_ratio(1), // Takes half of remaining (90/2 = 45)
348            Layout::new().with_ratio(1), // Takes other half (45)
349        ]);
350        let splits = layout.calculate_splits(100);
351        assert_eq!(splits, vec![10, 45, 45]);
352    }
353
354    #[test]
355    fn test_calculate_splits_rounding() {
356        // 100 / 3 = 33.333
357        // Should be 33, 33, 34
358        let mut layout = Layout::new();
359        layout.split_row(vec![
360            Layout::new().with_ratio(1),
361            Layout::new().with_ratio(1),
362            Layout::new().with_ratio(1),
363        ]);
364        let splits = layout.calculate_splits(100);
365        assert_eq!(splits, vec![33, 33, 34]);
366        assert_eq!(splits.iter().sum::<u16>(), 100);
367    }
368    #[test]
369    fn test_calculate_splits_min_size_simple() {
370        let mut layout = Layout::new();
371        layout.split_row(vec![
372            Layout::new().with_ratio(1).with_minimum_size(60),
373            Layout::new().with_ratio(1),
374        ]);
375        let splits = layout.calculate_splits(100);
376        assert_eq!(splits, vec![60, 40]);
377    }
378
379    #[test]
380    fn test_calculate_splits_min_size_priority() {
381        let mut layout = Layout::new();
382        layout.split_row(vec![
383            Layout::new().with_ratio(1).with_minimum_size(80),
384            Layout::new().with_ratio(1).with_minimum_size(10),
385        ]);
386        let splits = layout.calculate_splits(100);
387        assert_eq!(splits, vec![80, 20]);
388    }
389
390    #[test]
391    fn test_calculate_splits_complex_min() {
392        let mut layout = Layout::new();
393        layout.split_row(vec![
394            Layout::new().with_size(5),
395            Layout::new().with_ratio(1).with_minimum_size(10),
396            Layout::new().with_ratio(1),
397        ]);
398        let splits = layout.calculate_splits(20);
399        assert_eq!(splits, vec![5, 10, 5]);
400    }
401
402    #[test]
403    fn test_vertical_split_ratios() {
404        let mut layout = Layout::new();
405        layout.split_column(vec![
406            Layout::new().with_ratio(1).with_name("Top"),
407            Layout::new().with_ratio(1).with_name("Bottom"),
408        ]);
409
410        // Mock context with height
411        let context = RenderContext {
412            width: 80,
413            height: Some(10),
414        };
415        let segments = layout.render(&context);
416
417        // Should have 10 lines total
418        assert_eq!(segments.len(), 10);
419        // 80 chars wide - check first line
420        if !segments.is_empty() {
421            assert_eq!(segments[0].plain_text().len(), 80);
422        }
423    }
424
425    #[test]
426    fn test_vertical_split_stacking() {
427        let mut layout = Layout::new();
428        layout.split_column(vec![
429            Layout::new().with_size(1).with_name("Top"),
430            Layout::new().with_name("Bottom"),
431        ]);
432
433        // Unconstrained height
434        let context = RenderContext {
435            width: 80,
436            height: None,
437        };
438        let segments = layout.render(&context);
439
440        // Each leaf layout renders 1 blank line by default if empty
441        assert_eq!(segments.len(), 2);
442    }
443
444    #[test]
445    fn test_horizontal_split_propagates_height() {
446        let mut layout = Layout::new();
447        layout.split_row(vec![
448            Layout::new().with_ratio(1),
449            Layout::new().with_ratio(1),
450        ]);
451
452        // If we pass a height, it should be enforced on children (columns)
453        let context = RenderContext {
454            width: 80,
455            height: Some(5),
456        };
457        let segments = layout.render(&context);
458
459        // Should have 5 lines
460        assert_eq!(segments.len(), 5);
461    }
462}