Skip to main content

rich/
layout.rs

1//! Screen layout — split a region into ratioed rows and columns.
2//!
3//! Port of `rich/layout.py` (core). A [`Layout`] is a tree: a leaf holds a
4//! renderable, a branch splits its region among children either into columns
5//! (stacked vertically via [`Layout::split_column`]) or rows (side by side via
6//! [`Layout::split_row`]). Region sizes come from [`ratio_resolve`], and each
7//! leaf is rendered to an exact `(width, height)` block, then tiled.
8//!
9//! Scope: sizing (`size`/`ratio`/`minimum_size`), row/column splits, and leaf
10//! rendering are ported. The interactive placeholder shown for an *empty* leaf
11//! (upstream's fancy `_Placeholder`) is rendered as blank space (see
12//! docs/DIVERGENCES.md).
13
14use crate::console::{Console, ConsoleOptions, Justify};
15use crate::protocol::Renderable;
16use crate::ratio::{ratio_resolve, Edge};
17use crate::segment::Segment;
18
19/// The axis along which a branch splits its children.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21enum Direction {
22    /// Children are stacked vertically (a `split_column`).
23    Column,
24    /// Children are placed side by side (a `split_row`).
25    Row,
26}
27
28/// A node in a layout tree. Mirrors `rich.layout.Layout`.
29pub struct Layout {
30    renderable: Option<Box<dyn Renderable>>,
31    children: Vec<Layout>,
32    direction: Direction,
33    /// A fixed size along the parent's split axis, if pinned.
34    size: Option<usize>,
35    /// Flex weight when unsized (defaults to 1).
36    ratio: usize,
37    /// The smallest size this region may shrink to.
38    minimum_size: usize,
39}
40
41impl Default for Layout {
42    fn default() -> Self {
43        Layout::new()
44    }
45}
46
47impl Layout {
48    /// An empty layout (no renderable, no children).
49    pub fn new() -> Self {
50        Layout {
51            renderable: None,
52            children: Vec::new(),
53            direction: Direction::Column,
54            size: None,
55            ratio: 1,
56            minimum_size: 1,
57        }
58    }
59
60    /// A leaf layout wrapping `renderable`.
61    pub fn with_renderable(renderable: Box<dyn Renderable>) -> Self {
62        let mut layout = Layout::new();
63        layout.renderable = Some(renderable);
64        layout
65    }
66
67    /// Pin this region to a fixed size along its parent's split axis.
68    pub fn size(mut self, size: usize) -> Self {
69        self.size = Some(size);
70        self
71    }
72
73    /// Set the flex weight used when this region is unsized.
74    pub fn ratio(mut self, ratio: usize) -> Self {
75        self.ratio = ratio;
76        self
77    }
78
79    /// Set the minimum size this region may shrink to.
80    pub fn minimum_size(mut self, minimum_size: usize) -> Self {
81        self.minimum_size = minimum_size;
82        self
83    }
84
85    /// Split into children stacked vertically. Port of `Layout.split_column`.
86    pub fn split_column(&mut self, children: Vec<Layout>) {
87        self.direction = Direction::Column;
88        self.children = children;
89    }
90
91    /// Split into children placed side by side. Port of `Layout.split_row`.
92    pub fn split_row(&mut self, children: Vec<Layout>) {
93        self.direction = Direction::Row;
94        self.children = children;
95    }
96
97    fn edge(&self) -> Edge {
98        Edge::new(self.size, self.ratio, self.minimum_size)
99    }
100
101    /// Render this node into exactly `height` lines, each `width` cells wide.
102    fn render_region(&self, console: &Console, width: usize, height: usize) -> Vec<Vec<Segment>> {
103        if self.children.is_empty() {
104            return self.render_leaf(console, width, height);
105        }
106        let edges: Vec<Edge> = self.children.iter().map(Layout::edge).collect();
107        match self.direction {
108            Direction::Column => {
109                // Divide the height; stack the children's line blocks.
110                let heights = ratio_resolve(height, &edges);
111                let mut lines = Vec::with_capacity(height);
112                for (child, child_height) in self.children.iter().zip(heights) {
113                    lines.extend(child.render_region(console, width, child_height));
114                }
115                lines
116            }
117            Direction::Row => {
118                // Divide the width; place the children's blocks side by side.
119                let widths = ratio_resolve(width, &edges);
120                let blocks: Vec<Vec<Vec<Segment>>> = self
121                    .children
122                    .iter()
123                    .zip(widths)
124                    .map(|(child, child_width)| child.render_region(console, child_width, height))
125                    .collect();
126                (0..height)
127                    .map(|y| {
128                        let mut row = Vec::new();
129                        for block in &blocks {
130                            row.extend(block[y].iter().cloned());
131                        }
132                        row
133                    })
134                    .collect()
135            }
136        }
137    }
138
139    /// Render a leaf renderable to a `(width, height)` block (blank if empty).
140    fn render_leaf(&self, console: &Console, width: usize, height: usize) -> Vec<Vec<Segment>> {
141        let lines = match &self.renderable {
142            Some(renderable) => {
143                let mut options = console.options().update_dimensions(width, height);
144                options.justify = Justify::Default;
145                console.render_lines(renderable.as_ref(), &options, true)
146            }
147            None => Vec::new(),
148        };
149        Segment::set_shape(lines, width, height)
150    }
151}
152
153impl Renderable for Layout {
154    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
155        let width = options.max_width;
156        let height = options.height.unwrap_or_else(|| console.height());
157        let lines = self.render_region(console, width, height);
158
159        let mut segments = Vec::new();
160        let last = lines.len().saturating_sub(1);
161        for (index, line) in lines.into_iter().enumerate() {
162            segments.extend(line);
163            if index != last {
164                segments.push(Segment::line());
165            }
166        }
167        segments
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::color::ColorSystem;
175    use crate::text::Text;
176
177    fn console(width: usize, height: usize) -> Console {
178        Console::builder()
179            .force_terminal(true)
180            .color_system(Some(ColorSystem::Truecolor))
181            .width(width)
182            .height(height)
183            .build()
184    }
185
186    fn leaf(s: &str) -> Layout {
187        Layout::with_renderable(Box::new(Text::new(s)))
188    }
189
190    /// A line of `text` left-justified into `width` cells.
191    fn cell(text: &str, width: usize) -> String {
192        format!("{text}{}", " ".repeat(width - text.chars().count()))
193    }
194
195    #[test]
196    fn column_split_stacks() {
197        let c = console(24, 4);
198        let mut lay = Layout::new();
199        lay.split_column(vec![leaf("top"), leaf("bottom")]);
200        // Captured from real rich 15.0.0: two ratio-1 rows over height 4.
201        let blank = " ".repeat(24);
202        let expected = format!(
203            "{}\n{blank}\n{}\n{blank}\n",
204            cell("top", 24),
205            cell("bottom", 24)
206        );
207        assert_eq!(c.capture(|con| con.print(&lay)), expected);
208    }
209
210    #[test]
211    fn row_split_side_by_side() {
212        let c = console(24, 4);
213        let mut lay = Layout::new();
214        lay.split_row(vec![leaf("L"), leaf("R")]);
215        // Two ratio-1 columns of width 12; only row 0 has content.
216        let blank = " ".repeat(24);
217        let row0 = format!("{}{}", cell("L", 12), cell("R", 12));
218        let expected = format!("{row0}\n{blank}\n{blank}\n{blank}\n");
219        assert_eq!(c.capture(|con| con.print(&lay)), expected);
220    }
221}