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}