Skip to main content

rusty_rich/
columns.rs

1//! Columns — render renderables side by side. Equivalent to Rich's `columns.py`.
2
3use crate::console::{ConsoleOptions, DynRenderable, RenderResult, Renderable};
4use crate::segment::Segment;
5
6/// Renders a set of renderables in side-by-side columns.
7#[derive(Clone)]
8pub struct Columns {
9    pub renderables: Vec<DynRenderable>,
10    pub equal: bool,
11    pub expand: bool,
12    pub padding: usize,
13    pub width: Option<usize>,
14}
15
16impl std::fmt::Debug for Columns {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.debug_struct("Columns")
19            .field("count", &self.renderables.len())
20            .field("equal", &self.equal)
21            .finish()
22    }
23}
24
25impl Columns {
26    pub fn new() -> Self {
27        Self {
28            renderables: Vec::new(),
29            equal: false,
30            expand: false,
31            padding: 1,
32            width: None,
33        }
34    }
35
36    pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) {
37        self.renderables.push(DynRenderable::new(renderable));
38    }
39
40    pub fn padding(mut self, padding: usize) -> Self { self.padding = padding; self }
41    pub fn equal(mut self) -> Self { self.equal = true; self }
42    pub fn expand(mut self) -> Self { self.expand = true; self }
43}
44
45impl Renderable for Columns {
46    fn render(&self, options: &ConsoleOptions) -> RenderResult {
47        let count = self.renderables.len();
48        if count == 0 {
49            return RenderResult::new();
50        }
51
52        let available = self.width.unwrap_or(options.max_width);
53        let total_padding = (count.saturating_sub(1)) * self.padding;
54        let col_width = if self.equal {
55            (available.saturating_sub(total_padding)) / count
56        } else {
57            available.saturating_sub(total_padding) / count
58        };
59
60        // Render each column
61        let rendered: Vec<RenderResult> = self
62            .renderables
63            .iter()
64            .map(|r| r.render(&options.update_width(col_width.max(1))))
65            .collect();
66
67        // Find max lines
68        let max_lines = rendered.iter().map(|r| r.lines.len()).max().unwrap_or(0);
69
70        let mut lines: Vec<Vec<Segment>> = Vec::new();
71
72        for line_idx in 0..max_lines {
73            let mut line_segments: Vec<Segment> = Vec::new();
74
75            for (col_idx, col_result) in rendered.iter().enumerate() {
76                if col_idx > 0 {
77                    line_segments.push(Segment::new(" ".repeat(self.padding)));
78                }
79
80                if let Some(col_line) = col_result.lines.get(line_idx) {
81                    line_segments.extend(col_line.iter().cloned());
82                } else {
83                    // Fill with spaces
84                    line_segments.push(Segment::new(" ".repeat(col_width)));
85                }
86            }
87
88            if line_idx < max_lines - 1 {
89                line_segments.push(Segment::line());
90            }
91            lines.push(line_segments);
92        }
93
94        RenderResult { lines, items: Vec::new() }
95    }
96}
97
98impl Default for Columns {
99    fn default() -> Self {
100        Self::new()
101    }
102}