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    /// Create an empty [`Columns`] container.
27    ///
28    /// # Examples
29    ///
30    /// ```rust
31    /// use rusty_rich::Columns;
32    ///
33    /// let mut cols = Columns::new();
34    /// cols.add("item 1");
35    /// cols.add("item 2");
36    /// ```
37    pub fn new() -> Self {
38        Self {
39            renderables: Vec::new(),
40            equal: false,
41            expand: false,
42            padding: 1,
43            width: None,
44        }
45    }
46
47    /// Add a renderable to the column layout.
48    pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) {
49        self.renderables.push(DynRenderable::new(renderable));
50    }
51
52    /// Builder: set padding between columns (in characters).
53    pub fn padding(mut self, padding: usize) -> Self {
54        self.padding = padding;
55        self
56    }
57    /// Builder: force all columns to have equal width.
58    pub fn equal(mut self) -> Self {
59        self.equal = true;
60        self
61    }
62    /// Builder: expand columns to fill the available width.
63    pub fn expand(mut self) -> Self {
64        self.expand = true;
65        self
66    }
67}
68
69impl Renderable for Columns {
70    fn render(&self, options: &ConsoleOptions) -> RenderResult {
71        let count = self.renderables.len();
72        if count == 0 {
73            return RenderResult::new();
74        }
75
76        let available = self.width.unwrap_or(options.max_width);
77        let total_padding = (count.saturating_sub(1)) * self.padding;
78        let col_width = available.saturating_sub(total_padding) / count;
79
80        // Render each column
81        let rendered: Vec<RenderResult> = self
82            .renderables
83            .iter()
84            .map(|r| r.render(&options.update_width(col_width.max(1))))
85            .collect();
86
87        // Find max lines
88        let max_lines = rendered.iter().map(|r| r.lines.len()).max().unwrap_or(0);
89
90        let mut lines: Vec<Vec<Segment>> = Vec::new();
91
92        for line_idx in 0..max_lines {
93            let mut line_segments: Vec<Segment> = Vec::new();
94
95            for (col_idx, col_result) in rendered.iter().enumerate() {
96                if col_idx > 0 {
97                    line_segments.push(Segment::new(" ".repeat(self.padding)));
98                }
99
100                if let Some(col_line) = col_result.lines.get(line_idx) {
101                    line_segments.extend(col_line.iter().cloned());
102                } else {
103                    // Fill with spaces
104                    line_segments.push(Segment::new(" ".repeat(col_width)));
105                }
106            }
107
108            if line_idx < max_lines - 1 {
109                line_segments.push(Segment::line());
110            }
111            lines.push(line_segments);
112        }
113
114        RenderResult {
115            lines,
116            items: Vec::new(),
117        }
118    }
119}
120
121impl Default for Columns {
122    fn default() -> Self {
123        Self::new()
124    }
125}