Skip to main content

rusty_rich/
layout.rs

1//! Layout — split-pane layout system. Equivalent to Rich's `layout.py`.
2
3use std::collections::HashMap;
4
5use crate::console::{Console, ConsoleOptions, DynRenderable, Renderable};
6
7// ---------------------------------------------------------------------------
8// Region
9// ---------------------------------------------------------------------------
10
11/// A region on screen.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Region {
14    pub x: usize,
15    pub y: usize,
16    pub width: usize,
17    pub height: usize,
18}
19
20// ---------------------------------------------------------------------------
21// Direction
22// ---------------------------------------------------------------------------
23
24/// Direction of a split.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Direction {
27    /// Split content side by side (left to right).
28    Horizontal,
29    /// Split content stacked (top to bottom).
30    Vertical,
31}
32
33// ---------------------------------------------------------------------------
34// LayoutNode
35// ---------------------------------------------------------------------------
36
37/// A layout node — can be a leaf (containing a renderable) or a split.
38#[derive(Debug, Clone)]
39pub enum LayoutNode {
40    /// A split container with children and a direction.
41    Split {
42        /// Direction of the split (horizontal or vertical).
43        direction: Direction,
44        /// Relative size ratios for children.
45        sizes: Vec<usize>,
46        /// Child layout nodes.
47        children: Vec<LayoutNode>,
48    },
49    /// A leaf with a renderable name (placeholder) and optional fixed size.
50    Leaf {
51        /// Name identifier for this leaf.
52        name: String,
53        /// Optional label for the renderable.
54        renderable: Option<String>,
55        /// Optional fixed size constraint.
56        size: Option<usize>,
57    },
58}
59
60impl LayoutNode {
61    /// Create a new split node with equal-size children.
62    ///
63    /// Each child is assigned an initial ratio of 1. Use
64    /// [`sizes`](LayoutNode::sizes) to customize the ratios.
65    pub fn split(direction: Direction, children: Vec<LayoutNode>) -> Self {
66        let sizes = vec![1; children.len()];
67        Self::Split { direction, sizes, children }
68    }
69
70    /// Builder: set the size ratios for the children of this split node.
71    pub fn sizes(mut self, sizes: Vec<usize>) -> Self {
72        if let Self::Split { sizes: ref mut s, .. } = self {
73            *s = sizes;
74        }
75        self
76    }
77}
78
79// ---------------------------------------------------------------------------
80// Layout
81// ---------------------------------------------------------------------------
82
83/// The Layout compute engine. Assigns screen regions to a tree of layout
84/// nodes by recursively splitting available space.
85#[derive(Debug)]
86pub struct Layout {
87    /// The root [`LayoutNode`] defining the split hierarchy.
88    pub root: LayoutNode,
89    /// Whether the layout is visible.
90    pub visible: bool,
91    /// Minimum size for any region.
92    pub minimum_size: usize,
93    /// Named renderables for leaf nodes.
94    pub renderables: HashMap<String, DynRenderable>,
95    /// Active splitters for dividing regions among children.
96    pub splitters: Vec<Box<dyn Splitter>>,
97    /// Auto-incrementing pane ID counter.
98    next_pane_id: usize,
99}
100
101impl Layout {
102    /// Create a new layout with the given root node.
103    pub fn new(root: LayoutNode) -> Self {
104        Self {
105            root,
106            visible: true,
107            minimum_size: 1,
108            renderables: HashMap::new(),
109            splitters: Vec::new(),
110            next_pane_id: 0,
111        }
112    }
113
114    /// Create a new layout from a named renderable (single-pane leaf).
115    ///
116    /// This is a convenience constructor that wraps the renderable in a leaf
117    /// node with the given name.
118    pub fn from_renderable(
119        name: impl Into<String>,
120        renderable: impl Renderable + Send + Sync + 'static,
121    ) -> Self {
122        let name = name.into();
123        let node = LayoutNode::Leaf {
124            name: name.clone(),
125            renderable: None,
126            size: None,
127        };
128        let mut renderables = HashMap::new();
129        renderables.insert(name, DynRenderable::new(renderable));
130        Self {
131            root: node,
132            visible: true,
133            minimum_size: 1,
134            renderables,
135            splitters: Vec::new(),
136            next_pane_id: 1,
137        }
138    }
139
140    /// Split the current root node into the given direction.
141    ///
142    /// The existing root becomes the first child of the new split, and a new
143    /// empty leaf is added as the second child.
144    /// Returns a mutable reference to the root node.
145    pub fn split(&mut self, direction: Direction) -> &mut LayoutNode {
146        let name_a = format!("_split_a_{}", self.next_pane_id);
147        let name_b = format!("_split_b_{}", self.next_pane_id + 1);
148        self.next_pane_id += 2;
149
150        let old_root = std::mem::replace(
151            &mut self.root,
152            LayoutNode::Split {
153                direction,
154                sizes: vec![1, 1],
155                children: vec![
156                    LayoutNode::Leaf { name: name_a, renderable: None, size: None },
157                    LayoutNode::Leaf { name: name_b, renderable: None, size: None },
158                ],
159            },
160        );
161
162        // Put the old root back as the first child
163        if let LayoutNode::Split { ref mut children, .. } = self.root {
164            children[0] = old_root;
165        }
166
167        &mut self.root
168    }
169
170    /// Remove the split at the root.  If the root is a split, it is replaced
171    /// with its first child.  If the root is already a leaf, this is a no-op.
172    pub fn unsplit(&mut self) {
173        let replacement = std::mem::replace(
174            &mut self.root,
175            LayoutNode::Leaf { name: String::new(), renderable: None, size: None },
176        );
177        match replacement {
178            LayoutNode::Split { mut children, .. } if !children.is_empty() => {
179                self.root = children.remove(0);
180            }
181            other => {
182                self.root = other;
183            }
184        }
185    }
186
187    /// Convenience: split the root into a column layout (horizontal split).
188    pub fn split_column(&mut self) -> &mut Self {
189        self.split(Direction::Horizontal);
190        self
191    }
192
193    /// Convenience: split the root into a row layout (vertical split).
194    pub fn split_row(&mut self) -> &mut Self {
195        self.split(Direction::Vertical);
196        self
197    }
198
199    /// Add a child pane with a renderable and a ratio weight.
200    ///
201    /// If the root is already a `Split` node, the new child is appended.
202    /// If the root is a `Leaf`, it is first converted to a `Split` containing
203    /// the old leaf and the new child.
204    ///
205    /// Returns the pane ID (index of the new child in the children list).
206    pub fn add_split(
207        &mut self,
208        renderable: impl Renderable + Send + Sync + 'static,
209        ratio: usize,
210    ) -> usize {
211        let id = self.next_pane_id;
212        self.next_pane_id += 1;
213        let name = format!("_pane_{}", id);
214        self.renderables.insert(name.clone(), DynRenderable::new(renderable));
215
216        match &mut self.root {
217            LayoutNode::Split { children, sizes, .. } => {
218                children.push(LayoutNode::Leaf {
219                    name: name.clone(),
220                    renderable: None,
221                    size: None,
222                });
223                sizes.push(ratio);
224                children.len() - 1
225            }
226            LayoutNode::Leaf { .. } => {
227                // Convert leaf root to a Split containing old + new children
228                let old_root = std::mem::replace(
229                    &mut self.root,
230                    LayoutNode::Split {
231                        direction: Direction::Vertical,
232                        sizes: vec![1, ratio],
233                        children: vec![
234                            LayoutNode::Leaf {
235                                name: String::new(),
236                                renderable: None,
237                                size: None,
238                            },
239                            LayoutNode::Leaf {
240                                name: name.clone(),
241                                renderable: None,
242                                size: None,
243                            },
244                        ],
245                    },
246                );
247                if let LayoutNode::Split { ref mut children, .. } = self.root {
248                    children[0] = old_root;
249                }
250                1
251            }
252        }
253    }
254
255    /// Get the root renderable (if the root is a leaf and has a renderable).
256    pub fn renderable(&self) -> Option<&dyn Renderable> {
257        match &self.root {
258            LayoutNode::Leaf { name, .. } => {
259                self.renderables.get(name).map(|dr| dr as &dyn Renderable)
260            }
261            _ => None,
262        }
263    }
264
265    /// Get child layout nodes (if the root is a split).
266    pub fn children(&self) -> &[LayoutNode] {
267        match &self.root {
268            LayoutNode::Split { children, .. } => children,
269            _ => &[],
270        }
271    }
272
273    /// Get the active splitters.
274    pub fn splitters(&self) -> Vec<&dyn Splitter> {
275        self.splitters.iter().map(|s| s.as_ref()).collect()
276    }
277
278    /// Get the layout tree root.
279    pub fn tree(&self) -> &LayoutNode {
280        &self.root
281    }
282
283    /// Apply a function to all leaf renderables, replacing each with the
284    /// result.
285    pub fn map(&mut self, f: impl Fn(&dyn Renderable) -> DynRenderable) {
286        let mut new_renderables = HashMap::new();
287        for (name, dr) in &self.renderables {
288            let new_dr = f(dr as &dyn Renderable);
289            new_renderables.insert(name.clone(), new_dr);
290        }
291        self.renderables = new_renderables;
292    }
293
294    /// Get a named renderable from the tree.
295    pub fn get(&self, name: &str) -> Option<&dyn Renderable> {
296        self.renderables.get(name).map(|dr| dr as &dyn Renderable)
297    }
298
299    /// Update a named renderable, returning `true` if it existed.
300    pub fn update(
301        &mut self,
302        name: &str,
303        renderable: impl Renderable + Send + Sync + 'static,
304    ) -> bool {
305        if self.renderables.contains_key(name) {
306            self.renderables.insert(name.to_string(), DynRenderable::new(renderable));
307            true
308        } else {
309            false
310        }
311    }
312
313    /// Refresh the layout on screen by re-rendering all visible regions.
314    ///
315    /// Computes the layout for the current terminal size and renders each
316    /// leaf's renderable into the console.
317    pub fn refresh_screen(&mut self, console: &mut Console) {
318        if !self.visible {
319            return;
320        }
321        let dims = crate::console::ConsoleDimensions::detect();
322        let regions = self.compute(dims.width, dims.height);
323        // Sort regions top-to-bottom so they render in order
324        for (name, _region) in &regions {
325            if let Some(renderable) = self.renderables.get(name) {
326                // Render each pane — in a full implementation we'd clip to
327                // the region; here we just print sequentially.
328                let rendered = renderable.render(&ConsoleOptions::default());
329                let text = rendered.to_ansi();
330                if !text.is_empty() {
331                    let _ = console.print_str(&text);
332                }
333            }
334        }
335    }
336
337    /// Compute region assignments by recursively splitting the given area.
338    ///
339    /// Returns a list of `(name, region)` pairs for each leaf node in the
340    /// layout tree.
341    pub fn compute(&self, total_width: usize, total_height: usize) -> Vec<(String, Region)> {
342        let mut regions = Vec::new();
343        let region = Region { x: 0, y: 0, width: total_width, height: total_height };
344        Self::layout_node(&self.root, region, &mut regions);
345        regions
346    }
347
348    fn layout_node(node: &LayoutNode, region: Region, out: &mut Vec<(String, Region)>) {
349        match node {
350            LayoutNode::Leaf { name, size, .. } => {
351                let mut r = region;
352                if let Some(s) = size {
353                    r.width = r.width.min(*s);
354                    r.height = r.height.min(*s);
355                } else {
356                    r.width = r.width.max(2);
357                    r.height = r.height.max(1);
358                }
359                out.push((name.clone(), r));
360            }
361            LayoutNode::Split { direction, sizes, children } => {
362                let total_size: usize = sizes.iter().sum();
363                if total_size == 0 || children.is_empty() {
364                    return;
365                }
366                let count = children.len();
367
368                match direction {
369                    Direction::Horizontal => {
370                        let mut x = region.x;
371                        let total_spacing = count.saturating_sub(1);
372                        let avail = region.width.saturating_sub(total_spacing);
373                        for (i, child) in children.iter().enumerate() {
374                            let ratio = sizes.get(i).copied().unwrap_or(1);
375                            let child_w = (avail * ratio) / total_size;
376                            let child_r = Region {
377                                x,
378                                y: region.y,
379                                width: child_w.max(1),
380                                height: region.height,
381                            };
382                            Self::layout_node(child, child_r, out);
383                            x += child_w + 1; // 1 char gutter
384                        }
385                    }
386                    Direction::Vertical => {
387                        let mut y = region.y;
388                        for (i, child) in children.iter().enumerate() {
389                            let ratio = sizes.get(i).copied().unwrap_or(1);
390                            let child_h = (region.height * ratio) / total_size;
391                            let child_r = Region {
392                                x: region.x,
393                                y,
394                                width: region.width,
395                                height: child_h.max(1),
396                            };
397                            Self::layout_node(child, child_r, out);
398                            y += child_h;
399                        }
400                    }
401                }
402            }
403        }
404    }
405}
406
407// ---------------------------------------------------------------------------
408// Splitter trait and implementations
409// ---------------------------------------------------------------------------
410
411/// Trait for layout splitters (interface).
412///
413/// A `Splitter` defines how to divide a [`Region`] among a list of child
414/// [`LayoutNode`]s given a [`Direction`].
415pub trait Splitter: std::fmt::Debug {
416    /// Split `region` among `children` according to `direction`.
417    ///
418    /// Returns one [`Region`] per child.
419    fn split(&self, region: &Region, children: &[LayoutNode], direction: &Direction) -> Vec<Region>;
420}
421
422/// Default splitter that divides space equally among all children.
423#[derive(Debug)]
424pub struct NoSplitter;
425
426impl Splitter for NoSplitter {
427    fn split(&self, region: &Region, children: &[LayoutNode], direction: &Direction) -> Vec<Region> {
428        let count = children.len().max(1);
429        match direction {
430            Direction::Horizontal => {
431                let col_width = region.width / count;
432                children
433                    .iter()
434                    .enumerate()
435                    .map(|(i, _)| Region {
436                        x: region.x + i * col_width,
437                        y: region.y,
438                        width: col_width,
439                        height: region.height,
440                    })
441                    .collect()
442            }
443            Direction::Vertical => {
444                let row_height = region.height / count;
445                children
446                    .iter()
447                    .enumerate()
448                    .map(|(i, _)| Region {
449                        x: region.x,
450                        y: region.y + i * row_height,
451                        width: region.width,
452                        height: row_height,
453                    })
454                    .collect()
455            }
456        }
457    }
458}
459
460/// Splits a region into equal-width columns (ignores the direction).
461#[derive(Debug)]
462pub struct ColumnSplitter;
463
464impl Splitter for ColumnSplitter {
465    fn split(&self, region: &Region, children: &[LayoutNode], _direction: &Direction) -> Vec<Region> {
466        let count = children.len().max(1);
467        let col_width = region.width / count;
468        children
469            .iter()
470            .enumerate()
471            .map(|(i, _)| Region {
472                x: region.x + i * col_width,
473                y: region.y,
474                width: col_width,
475                height: region.height,
476            })
477            .collect()
478    }
479}
480
481/// Splits a region into equal-height rows (ignores the direction).
482#[derive(Debug)]
483pub struct RowSplitter;
484
485impl Splitter for RowSplitter {
486    fn split(&self, region: &Region, children: &[LayoutNode], _direction: &Direction) -> Vec<Region> {
487        let count = children.len().max(1);
488        let row_height = region.height / count;
489        children
490            .iter()
491            .enumerate()
492            .map(|(i, _)| Region {
493                x: region.x,
494                y: region.y + i * row_height,
495                width: region.width,
496                height: row_height,
497            })
498            .collect()
499    }
500}
501
502// ---------------------------------------------------------------------------
503// Tests
504// ---------------------------------------------------------------------------
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn test_region_defaults() {
512        let r = Region { x: 0, y: 0, width: 80, height: 24 };
513        assert_eq!(r.width, 80);
514        assert_eq!(r.height, 24);
515    }
516
517    #[test]
518    fn test_layout_single_leaf() {
519        let node = LayoutNode::Leaf {
520            name: "root".into(),
521            renderable: None,
522            size: None,
523        };
524        let layout = Layout::new(node);
525        let regions = layout.compute(80, 24);
526        assert_eq!(regions.len(), 1);
527        assert_eq!(regions[0].0, "root");
528    }
529
530    #[test]
531    fn test_layout_horizontal_split() {
532        let children = vec![
533            LayoutNode::Leaf { name: "left".into(), renderable: None, size: None },
534            LayoutNode::Leaf { name: "right".into(), renderable: None, size: None },
535        ];
536        let node = LayoutNode::split(Direction::Horizontal, children);
537        let layout = Layout::new(node);
538        let regions = layout.compute(80, 24);
539        assert_eq!(regions.len(), 2);
540        assert!(regions[0].1.x < regions[1].1.x);
541    }
542
543    #[test]
544    fn test_layout_vertical_split() {
545        let children = vec![
546            LayoutNode::Leaf { name: "top".into(), renderable: None, size: None },
547            LayoutNode::Leaf { name: "bottom".into(), renderable: None, size: None },
548        ];
549        let node = LayoutNode::split(Direction::Vertical, children);
550        let layout = Layout::new(node);
551        let regions = layout.compute(80, 24);
552        assert_eq!(regions.len(), 2);
553        assert!(regions[0].1.y < regions[1].1.y);
554    }
555
556    #[test]
557    fn test_split_method() {
558        let mut layout = Layout::new(LayoutNode::Leaf {
559            name: "root".into(),
560            renderable: None,
561            size: None,
562        });
563        layout.split(Direction::Horizontal);
564        // Root should now be a Split
565        match &layout.root {
566            LayoutNode::Split { children, .. } => {
567                assert_eq!(children.len(), 2);
568            }
569            _ => panic!("expected Split after split()"),
570        }
571    }
572
573    #[test]
574    fn test_unsplit_method() {
575        let mut layout = Layout::new(LayoutNode::Leaf {
576            name: "root".into(),
577            renderable: None,
578            size: None,
579        });
580        layout.split(Direction::Horizontal);
581        layout.unsplit();
582        // Root should be back to a Leaf (the original)
583        match &layout.root {
584            LayoutNode::Leaf { .. } => {} // ok
585            _ => panic!("expected Leaf after unsplit()"),
586        }
587    }
588
589    #[test]
590    fn test_split_column() {
591        let mut layout = Layout::new(LayoutNode::Leaf {
592            name: "root".into(),
593            renderable: None,
594            size: None,
595        });
596        layout.split_column();
597        match &layout.root {
598            LayoutNode::Split { direction, .. } => {
599                assert_eq!(*direction, Direction::Horizontal);
600            }
601            _ => panic!("expected Horizontal split"),
602        }
603    }
604
605    #[test]
606    fn test_split_row() {
607        let mut layout = Layout::new(LayoutNode::Leaf {
608            name: "root".into(),
609            renderable: None,
610            size: None,
611        });
612        layout.split_row();
613        match &layout.root {
614            LayoutNode::Split { direction, .. } => {
615                assert_eq!(*direction, Direction::Vertical);
616            }
617            _ => panic!("expected Vertical split"),
618        }
619    }
620
621    #[test]
622    fn test_children_method() {
623        let mut layout = Layout::new(LayoutNode::Leaf {
624            name: "root".into(),
625            renderable: None,
626            size: None,
627        });
628        // Before split, children is empty
629        assert!(layout.children().is_empty());
630        layout.split(Direction::Horizontal);
631        assert_eq!(layout.children().len(), 2);
632    }
633
634    #[test]
635    fn test_tree_method() {
636        let layout = Layout::new(LayoutNode::Leaf {
637            name: "root".into(),
638            renderable: None,
639            size: None,
640        });
641        match layout.tree() {
642            LayoutNode::Leaf { name, .. } => assert_eq!(name, "root"),
643            _ => panic!("expected Leaf"),
644        }
645    }
646
647    #[test]
648    fn test_renderable_none_for_empty_layout() {
649        let layout = Layout::new(LayoutNode::Leaf {
650            name: "root".into(),
651            renderable: None,
652            size: None,
653        });
654        // No renderable registered
655        assert!(layout.renderable().is_none());
656    }
657
658    #[test]
659    fn test_from_renderable() {
660        let layout = Layout::from_renderable("main", "hello world");
661        assert!(layout.get("main").is_some());
662    }
663
664    #[test]
665    fn test_get_and_update() {
666        let mut layout = Layout::from_renderable("main", "initial");
667        assert!(layout.get("main").is_some());
668
669        let updated = layout.update("main", "updated");
670        assert!(updated);
671
672        // Non-existent key
673        let not_found = layout.update("nonexistent", "nope");
674        assert!(!not_found);
675    }
676
677    #[test]
678    fn test_map() {
679        let mut layout = Layout::from_renderable("main", "hello");
680        layout.map(|_r| DynRenderable::new("mapped"));
681        assert!(layout.get("main").is_some());
682    }
683
684    #[test]
685    fn test_add_split_to_leaf() {
686        let mut layout = Layout::from_renderable("main", "content");
687        let id = layout.add_split("second", 2);
688        // Root should now be a Split
689        match &layout.root {
690            LayoutNode::Split { children, sizes, .. } => {
691                assert_eq!(children.len(), 2);
692                assert_eq!(*sizes, vec![1, 2]);
693                assert_eq!(id, 1);
694            }
695            _ => panic!("expected Split after add_split"),
696        }
697    }
698
699    #[test]
700    fn test_no_splitter() {
701        let splitter = NoSplitter;
702        let children = vec![
703            LayoutNode::Leaf { name: "a".into(), renderable: None, size: None },
704            LayoutNode::Leaf { name: "b".into(), renderable: None, size: None },
705        ];
706        let region = Region { x: 0, y: 0, width: 80, height: 24 };
707        let regions = splitter.split(&region, &children, &Direction::Horizontal);
708        assert_eq!(regions.len(), 2);
709        assert_eq!(regions[0].width, 40);
710        assert_eq!(regions[1].width, 40);
711    }
712
713    #[test]
714    fn test_column_splitter() {
715        let splitter = ColumnSplitter;
716        let children = vec![
717            LayoutNode::Leaf { name: "a".into(), renderable: None, size: None },
718            LayoutNode::Leaf { name: "b".into(), renderable: None, size: None },
719            LayoutNode::Leaf { name: "c".into(), renderable: None, size: None },
720        ];
721        let region = Region { x: 0, y: 0, width: 90, height: 24 };
722        let regions = splitter.split(&region, &children, &Direction::Vertical);
723        assert_eq!(regions.len(), 3);
724        assert_eq!(regions[0].width, 30);
725        assert_eq!(regions[1].x, 30);
726        assert_eq!(regions[2].x, 60);
727    }
728
729    #[test]
730    fn test_row_splitter() {
731        let splitter = RowSplitter;
732        let children = vec![
733            LayoutNode::Leaf { name: "a".into(), renderable: None, size: None },
734            LayoutNode::Leaf { name: "b".into(), renderable: None, size: None },
735        ];
736        let region = Region { x: 0, y: 0, width: 80, height: 24 };
737        let regions = splitter.split(&region, &children, &Direction::Horizontal);
738        assert_eq!(regions.len(), 2);
739        assert_eq!(regions[0].height, 12);
740        assert_eq!(regions[1].y, 12);
741    }
742
743    #[test]
744    fn test_splitters_method() {
745        let layout = Layout::new(LayoutNode::Leaf {
746            name: "root".into(),
747            renderable: None,
748            size: None,
749        });
750        assert!(layout.splitters().is_empty());
751    }
752
753    #[test]
754    fn test_compute_with_fixed_size() {
755        let node = LayoutNode::Leaf {
756            name: "fixed".into(),
757            renderable: None,
758            size: Some(10),
759        };
760        let layout = Layout::new(node);
761        let regions = layout.compute(80, 24);
762        assert_eq!(regions[0].1.width, 10);
763        assert_eq!(regions[0].1.height, 10);
764    }
765}