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                let count = children.len();
364
365                match direction {
366                    Direction::Horizontal => {
367                        let mut x = region.x;
368                        let total_spacing = count.saturating_sub(1);
369                        let avail = region.width.saturating_sub(total_spacing);
370                        for (i, child) in children.iter().enumerate() {
371                            let ratio = sizes.get(i).copied().unwrap_or(1);
372                            let child_w = (avail * ratio) / total_size;
373                            let child_r = Region {
374                                x,
375                                y: region.y,
376                                width: child_w.max(1),
377                                height: region.height,
378                            };
379                            Self::layout_node(child, child_r, out);
380                            x += child_w + 1; // 1 char gutter
381                        }
382                    }
383                    Direction::Vertical => {
384                        let mut y = region.y;
385                        for (i, child) in children.iter().enumerate() {
386                            let ratio = sizes.get(i).copied().unwrap_or(1);
387                            let child_h = (region.height * ratio) / total_size;
388                            let child_r = Region {
389                                x: region.x,
390                                y,
391                                width: region.width,
392                                height: child_h.max(1),
393                            };
394                            Self::layout_node(child, child_r, out);
395                            y += child_h;
396                        }
397                    }
398                }
399            }
400        }
401    }
402}
403
404// ---------------------------------------------------------------------------
405// Splitter trait and implementations
406// ---------------------------------------------------------------------------
407
408/// Trait for layout splitters (interface).
409///
410/// A `Splitter` defines how to divide a [`Region`] among a list of child
411/// [`LayoutNode`]s given a [`Direction`].
412pub trait Splitter: std::fmt::Debug {
413    /// Split `region` among `children` according to `direction`.
414    ///
415    /// Returns one [`Region`] per child.
416    fn split(&self, region: &Region, children: &[LayoutNode], direction: &Direction) -> Vec<Region>;
417}
418
419/// Default splitter that divides space equally among all children.
420#[derive(Debug)]
421pub struct NoSplitter;
422
423impl Splitter for NoSplitter {
424    fn split(&self, region: &Region, children: &[LayoutNode], direction: &Direction) -> Vec<Region> {
425        let count = children.len().max(1);
426        match direction {
427            Direction::Horizontal => {
428                let col_width = region.width / count;
429                children
430                    .iter()
431                    .enumerate()
432                    .map(|(i, _)| Region {
433                        x: region.x + i * col_width,
434                        y: region.y,
435                        width: col_width,
436                        height: region.height,
437                    })
438                    .collect()
439            }
440            Direction::Vertical => {
441                let row_height = region.height / count;
442                children
443                    .iter()
444                    .enumerate()
445                    .map(|(i, _)| Region {
446                        x: region.x,
447                        y: region.y + i * row_height,
448                        width: region.width,
449                        height: row_height,
450                    })
451                    .collect()
452            }
453        }
454    }
455}
456
457/// Splits a region into equal-width columns (ignores the direction).
458#[derive(Debug)]
459pub struct ColumnSplitter;
460
461impl Splitter for ColumnSplitter {
462    fn split(&self, region: &Region, children: &[LayoutNode], _direction: &Direction) -> Vec<Region> {
463        let count = children.len().max(1);
464        let col_width = region.width / count;
465        children
466            .iter()
467            .enumerate()
468            .map(|(i, _)| Region {
469                x: region.x + i * col_width,
470                y: region.y,
471                width: col_width,
472                height: region.height,
473            })
474            .collect()
475    }
476}
477
478/// Splits a region into equal-height rows (ignores the direction).
479#[derive(Debug)]
480pub struct RowSplitter;
481
482impl Splitter for RowSplitter {
483    fn split(&self, region: &Region, children: &[LayoutNode], _direction: &Direction) -> Vec<Region> {
484        let count = children.len().max(1);
485        let row_height = region.height / count;
486        children
487            .iter()
488            .enumerate()
489            .map(|(i, _)| Region {
490                x: region.x,
491                y: region.y + i * row_height,
492                width: region.width,
493                height: row_height,
494            })
495            .collect()
496    }
497}
498
499// ---------------------------------------------------------------------------
500// Tests
501// ---------------------------------------------------------------------------
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506
507    #[test]
508    fn test_region_defaults() {
509        let r = Region { x: 0, y: 0, width: 80, height: 24 };
510        assert_eq!(r.width, 80);
511        assert_eq!(r.height, 24);
512    }
513
514    #[test]
515    fn test_layout_single_leaf() {
516        let node = LayoutNode::Leaf {
517            name: "root".into(),
518            renderable: None,
519            size: None,
520        };
521        let layout = Layout::new(node);
522        let regions = layout.compute(80, 24);
523        assert_eq!(regions.len(), 1);
524        assert_eq!(regions[0].0, "root");
525    }
526
527    #[test]
528    fn test_layout_horizontal_split() {
529        let children = vec![
530            LayoutNode::Leaf { name: "left".into(), renderable: None, size: None },
531            LayoutNode::Leaf { name: "right".into(), renderable: None, size: None },
532        ];
533        let node = LayoutNode::split(Direction::Horizontal, children);
534        let layout = Layout::new(node);
535        let regions = layout.compute(80, 24);
536        assert_eq!(regions.len(), 2);
537        assert!(regions[0].1.x < regions[1].1.x);
538    }
539
540    #[test]
541    fn test_layout_vertical_split() {
542        let children = vec![
543            LayoutNode::Leaf { name: "top".into(), renderable: None, size: None },
544            LayoutNode::Leaf { name: "bottom".into(), renderable: None, size: None },
545        ];
546        let node = LayoutNode::split(Direction::Vertical, children);
547        let layout = Layout::new(node);
548        let regions = layout.compute(80, 24);
549        assert_eq!(regions.len(), 2);
550        assert!(regions[0].1.y < regions[1].1.y);
551    }
552
553    #[test]
554    fn test_split_method() {
555        let mut layout = Layout::new(LayoutNode::Leaf {
556            name: "root".into(),
557            renderable: None,
558            size: None,
559        });
560        layout.split(Direction::Horizontal);
561        // Root should now be a Split
562        match &layout.root {
563            LayoutNode::Split { children, .. } => {
564                assert_eq!(children.len(), 2);
565            }
566            _ => panic!("expected Split after split()"),
567        }
568    }
569
570    #[test]
571    fn test_unsplit_method() {
572        let mut layout = Layout::new(LayoutNode::Leaf {
573            name: "root".into(),
574            renderable: None,
575            size: None,
576        });
577        layout.split(Direction::Horizontal);
578        layout.unsplit();
579        // Root should be back to a Leaf (the original)
580        match &layout.root {
581            LayoutNode::Leaf { .. } => {} // ok
582            _ => panic!("expected Leaf after unsplit()"),
583        }
584    }
585
586    #[test]
587    fn test_split_column() {
588        let mut layout = Layout::new(LayoutNode::Leaf {
589            name: "root".into(),
590            renderable: None,
591            size: None,
592        });
593        layout.split_column();
594        match &layout.root {
595            LayoutNode::Split { direction, .. } => {
596                assert_eq!(*direction, Direction::Horizontal);
597            }
598            _ => panic!("expected Horizontal split"),
599        }
600    }
601
602    #[test]
603    fn test_split_row() {
604        let mut layout = Layout::new(LayoutNode::Leaf {
605            name: "root".into(),
606            renderable: None,
607            size: None,
608        });
609        layout.split_row();
610        match &layout.root {
611            LayoutNode::Split { direction, .. } => {
612                assert_eq!(*direction, Direction::Vertical);
613            }
614            _ => panic!("expected Vertical split"),
615        }
616    }
617
618    #[test]
619    fn test_children_method() {
620        let mut layout = Layout::new(LayoutNode::Leaf {
621            name: "root".into(),
622            renderable: None,
623            size: None,
624        });
625        // Before split, children is empty
626        assert!(layout.children().is_empty());
627        layout.split(Direction::Horizontal);
628        assert_eq!(layout.children().len(), 2);
629    }
630
631    #[test]
632    fn test_tree_method() {
633        let layout = Layout::new(LayoutNode::Leaf {
634            name: "root".into(),
635            renderable: None,
636            size: None,
637        });
638        match layout.tree() {
639            LayoutNode::Leaf { name, .. } => assert_eq!(name, "root"),
640            _ => panic!("expected Leaf"),
641        }
642    }
643
644    #[test]
645    fn test_renderable_none_for_empty_layout() {
646        let layout = Layout::new(LayoutNode::Leaf {
647            name: "root".into(),
648            renderable: None,
649            size: None,
650        });
651        // No renderable registered
652        assert!(layout.renderable().is_none());
653    }
654
655    #[test]
656    fn test_from_renderable() {
657        let layout = Layout::from_renderable("main", "hello world");
658        assert!(layout.get("main").is_some());
659    }
660
661    #[test]
662    fn test_get_and_update() {
663        let mut layout = Layout::from_renderable("main", "initial");
664        assert!(layout.get("main").is_some());
665
666        let updated = layout.update("main", "updated");
667        assert!(updated);
668
669        // Non-existent key
670        let not_found = layout.update("nonexistent", "nope");
671        assert!(!not_found);
672    }
673
674    #[test]
675    fn test_map() {
676        let mut layout = Layout::from_renderable("main", "hello");
677        layout.map(|_r| DynRenderable::new("mapped"));
678        assert!(layout.get("main").is_some());
679    }
680
681    #[test]
682    fn test_add_split_to_leaf() {
683        let mut layout = Layout::from_renderable("main", "content");
684        let id = layout.add_split("second", 2);
685        // Root should now be a Split
686        match &layout.root {
687            LayoutNode::Split { children, sizes, .. } => {
688                assert_eq!(children.len(), 2);
689                assert_eq!(*sizes, vec![1, 2]);
690                assert_eq!(id, 1);
691            }
692            _ => panic!("expected Split after add_split"),
693        }
694    }
695
696    #[test]
697    fn test_no_splitter() {
698        let splitter = NoSplitter;
699        let children = vec![
700            LayoutNode::Leaf { name: "a".into(), renderable: None, size: None },
701            LayoutNode::Leaf { name: "b".into(), renderable: None, size: None },
702        ];
703        let region = Region { x: 0, y: 0, width: 80, height: 24 };
704        let regions = splitter.split(&region, &children, &Direction::Horizontal);
705        assert_eq!(regions.len(), 2);
706        assert_eq!(regions[0].width, 40);
707        assert_eq!(regions[1].width, 40);
708    }
709
710    #[test]
711    fn test_column_splitter() {
712        let splitter = ColumnSplitter;
713        let children = vec![
714            LayoutNode::Leaf { name: "a".into(), renderable: None, size: None },
715            LayoutNode::Leaf { name: "b".into(), renderable: None, size: None },
716            LayoutNode::Leaf { name: "c".into(), renderable: None, size: None },
717        ];
718        let region = Region { x: 0, y: 0, width: 90, height: 24 };
719        let regions = splitter.split(&region, &children, &Direction::Vertical);
720        assert_eq!(regions.len(), 3);
721        assert_eq!(regions[0].width, 30);
722        assert_eq!(regions[1].x, 30);
723        assert_eq!(regions[2].x, 60);
724    }
725
726    #[test]
727    fn test_row_splitter() {
728        let splitter = RowSplitter;
729        let children = vec![
730            LayoutNode::Leaf { name: "a".into(), renderable: None, size: None },
731            LayoutNode::Leaf { name: "b".into(), renderable: None, size: None },
732        ];
733        let region = Region { x: 0, y: 0, width: 80, height: 24 };
734        let regions = splitter.split(&region, &children, &Direction::Horizontal);
735        assert_eq!(regions.len(), 2);
736        assert_eq!(regions[0].height, 12);
737        assert_eq!(regions[1].y, 12);
738    }
739
740    #[test]
741    fn test_splitters_method() {
742        let layout = Layout::new(LayoutNode::Leaf {
743            name: "root".into(),
744            renderable: None,
745            size: None,
746        });
747        assert!(layout.splitters().is_empty());
748    }
749
750    #[test]
751    fn test_compute_with_fixed_size() {
752        let node = LayoutNode::Leaf {
753            name: "fixed".into(),
754            renderable: None,
755            size: Some(10),
756        };
757        let layout = Layout::new(node);
758        let regions = layout.compute(80, 24);
759        assert_eq!(regions[0].1.width, 10);
760        assert_eq!(regions[0].1.height, 10);
761    }
762}