Skip to main content

rusty_rich/
layout.rs

1//! Layout — split-pane layout system. Equivalent to Rich's `layout.py`.
2
3
4/// A region on screen.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct Region {
7    pub x: usize,
8    pub y: usize,
9    pub width: usize,
10    pub height: usize,
11}
12
13/// Direction of a split.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Direction {
16    /// Split content side by side (left to right).
17    Horizontal,
18    /// Split content stacked (top to bottom).
19    Vertical,
20}
21
22/// A layout node — can be a leaf (containing a renderable) or a split.
23#[derive(Debug, Clone)]
24pub enum LayoutNode {
25    /// A split container with children and a direction.
26    Split {
27        /// Direction of the split (horizontal or vertical).
28        direction: Direction,
29        /// Relative size ratios for children.
30        sizes: Vec<usize>,
31        /// Child layout nodes.
32        children: Vec<LayoutNode>,
33    },
34    /// A leaf with a renderable name (placeholder) and optional fixed size.
35    Leaf {
36        /// Name identifier for this leaf.
37        name: String,
38        /// Optional label for the renderable.
39        renderable: Option<String>,
40        /// Optional fixed size constraint.
41        size: Option<usize>,
42    },
43}
44
45impl LayoutNode {
46    /// Create a new split node with equal-size children.
47    ///
48    /// Each child is assigned an initial ratio of 1. Use
49    /// [`sizes`](LayoutNode::sizes) to customize the ratios.
50    pub fn split(direction: Direction, children: Vec<LayoutNode>) -> Self {
51        let sizes = vec![1; children.len()];
52        Self::Split { direction, sizes, children }
53    }
54
55    /// Builder: set the size ratios for the children of this split node.
56    pub fn sizes(mut self, sizes: Vec<usize>) -> Self {
57        if let Self::Split { sizes: ref mut s, .. } = self {
58            *s = sizes;
59        }
60        self
61    }
62}
63
64/// The Layout compute engine. Assigns screen regions to a tree of layout
65/// nodes by recursively splitting available space.
66#[derive(Debug, Clone)]
67pub struct Layout {
68    /// The root [`LayoutNode`] defining the split hierarchy.
69    pub root: LayoutNode,
70    /// Whether the layout is visible.
71    pub visible: bool,
72    /// Minimum size for any region.
73    pub minimum_size: usize,
74}
75
76impl Layout {
77    /// Create a new layout with the given root node.
78    pub fn new(root: LayoutNode) -> Self {
79        Self {
80            root,
81            visible: true,
82            minimum_size: 1,
83        }
84    }
85
86    /// Compute region assignments by recursively splitting the given area.
87    ///
88    /// Returns a list of `(name, region)` pairs for each leaf node in the
89    /// layout tree.
90    pub fn compute(&self, total_width: usize, total_height: usize) -> Vec<(String, Region)> {
91        let mut regions = Vec::new();
92        let region = Region { x: 0, y: 0, width: total_width, height: total_height };
93        Self::layout_node(&self.root, region, &mut regions);
94        regions
95    }
96
97    fn layout_node(node: &LayoutNode, region: Region, out: &mut Vec<(String, Region)>) {
98        match node {
99            LayoutNode::Leaf { name, size, .. } => {
100                let mut r = region;
101                if let Some(s) = size {
102                    r.width = r.width.min(*s);
103                    r.height = r.height.min(*s);
104                } else {
105                    r.width = r.width.max(2);
106                    r.height = r.height.max(1);
107                }
108                out.push((name.clone(), r));
109            }
110            LayoutNode::Split { direction, sizes, children } => {
111                let total_size: usize = sizes.iter().sum();
112                let count = children.len();
113
114                match direction {
115                    Direction::Horizontal => {
116                        let mut x = region.x;
117                        let total_spacing = count.saturating_sub(1);
118                        let avail = region.width.saturating_sub(total_spacing);
119                        for (i, child) in children.iter().enumerate() {
120                            let ratio = sizes.get(i).copied().unwrap_or(1);
121                            let child_w = (avail * ratio) / total_size;
122                            let child_r = Region {
123                                x,
124                                y: region.y,
125                                width: child_w.max(1),
126                                height: region.height,
127                            };
128                            Self::layout_node(child, child_r, out);
129                            x += child_w + 1; // 1 char gutter
130                        }
131                    }
132                    Direction::Vertical => {
133                        let mut y = region.y;
134                        for (i, child) in children.iter().enumerate() {
135                            let ratio = sizes.get(i).copied().unwrap_or(1);
136                            let child_h = (region.height * ratio) / total_size;
137                            let child_r = Region {
138                                x: region.x,
139                                y,
140                                width: region.width,
141                                height: child_h.max(1),
142                            };
143                            Self::layout_node(child, child_r, out);
144                            y += child_h;
145                        }
146                    }
147                }
148            }
149        }
150    }
151}