Skip to main content

rich/
columns.rs

1//! Columns — arrange renderables in a grid.
2//!
3//! Port of upstream `rich/columns.py`. [`Columns`] packs items into as many
4//! equal-gap columns as fit the available width, filling row by row.
5//!
6//! Slice scope: string items with the default padding `(0, 1)` (which upstream
7//! renders as a box-less grid with collapsed single-space gaps and no edge
8//! padding). `equal`/`expand`/`column_first`/`right_to_left`/`align` and
9//! non-string renderables are deferred with the rest of `columns.py`.
10
11use crate::cells::cell_len;
12use crate::console::{Console, ConsoleOptions};
13use crate::protocol::Renderable;
14use crate::segment::Segment;
15use crate::style::Style;
16use crate::text::Text;
17
18/// Arranges items into a grid of columns. Mirrors `rich.columns.Columns`.
19pub struct Columns {
20    items: Vec<String>,
21    /// `(top, right, bottom, left)`; only left/right are used for gap sizing.
22    padding: (usize, usize, usize, usize),
23}
24
25impl Columns {
26    /// Columns of string items with the default padding `(0, 1)`.
27    pub fn new(items: Vec<String>) -> Self {
28        Columns {
29            items,
30            padding: (0, 1, 0, 1),
31        }
32    }
33}
34
35/// The width sequence for `column_count`: item widths, then zero-padded so the
36/// final row is complete. Port of the non-`column_first` branch of
37/// `iter_renderables`.
38fn iter_widths(widths: &[usize], column_count: usize) -> Vec<usize> {
39    let mut sequence = widths.to_vec();
40    let remainder = widths.len() % column_count;
41    if remainder != 0 {
42        sequence.resize(widths.len() + (column_count - remainder), 0);
43    }
44    sequence
45}
46
47/// Choose the largest column count whose total width fits `max_width`.
48/// Direct port of the width-fitting `while` loop in `Columns.__rich_console__`.
49fn compute_column_count(widths: &[usize], max_width: usize, width_padding: usize) -> usize {
50    let mut column_count = widths.len();
51    while column_count > 1 {
52        let sequence = iter_widths(widths, column_count);
53        let mut columns: Vec<usize> = Vec::new();
54        let mut column_no = 0usize;
55        let mut broke = false;
56        for width in sequence {
57            if column_no == columns.len() {
58                columns.push(width);
59            } else {
60                columns[column_no] = columns[column_no].max(width);
61            }
62            let total: usize =
63                columns.iter().sum::<usize>() + width_padding * columns.len().saturating_sub(1);
64            if total > max_width {
65                column_count = columns.len().saturating_sub(1);
66                broke = true;
67                break;
68            }
69            column_no = (column_no + 1) % column_count;
70        }
71        if !broke {
72            break;
73        }
74    }
75    column_count.max(1)
76}
77
78/// Per-column widths for a fixed `column_count` (max over the round-robin items).
79fn column_widths(widths: &[usize], column_count: usize) -> Vec<usize> {
80    let mut result = vec![0usize; column_count];
81    for (index, &width) in widths.iter().enumerate() {
82        let column = index % column_count;
83        result[column] = result[column].max(width);
84    }
85    result
86}
87
88impl Renderable for Columns {
89    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
90        if self.items.is_empty() {
91            return Vec::new();
92        }
93        let (_, right, _, left) = self.padding;
94        let width_padding = left.max(right);
95        let widths: Vec<usize> = self.items.iter().map(|item| cell_len(item)).collect();
96
97        let column_count = compute_column_count(&widths, options.max_width, width_padding);
98        let col_widths = column_widths(&widths, column_count);
99
100        let style = Some(Style::new());
101        let gap = " ".repeat(width_padding);
102
103        let mut lines: Vec<Vec<Segment>> = Vec::new();
104        let mut start = 0;
105        while start < self.items.len() {
106            let mut row: Vec<Segment> = Vec::new();
107            for (column, &col_width) in col_widths.iter().enumerate() {
108                if column > 0 {
109                    row.push(Segment::new(gap.clone(), style.clone()));
110                }
111                let index = start + column;
112                let content = self.items.get(index).map(String::as_str).unwrap_or("");
113                let rendered = Text::new(content).render_lines(
114                    console.theme(),
115                    &Style::new(),
116                    Some(col_width),
117                );
118                let cell = rendered.into_iter().next().unwrap_or_default();
119                let padded = Segment::adjust_line_length(&cell, col_width, style.clone());
120                row.extend(Segment::simplify(&padded));
121            }
122            lines.push(row);
123            start += column_count;
124        }
125
126        let mut segments = Vec::new();
127        let last = lines.len().saturating_sub(1);
128        for (index, line) in lines.into_iter().enumerate() {
129            segments.extend(line);
130            if index != last {
131                segments.push(Segment::line());
132            }
133        }
134        segments
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::color::ColorSystem;
142
143    fn console(width: usize) -> Console {
144        Console::builder()
145            .force_terminal(true)
146            .color_system(Some(ColorSystem::Truecolor))
147            .width(width)
148            .build()
149    }
150
151    fn columns(items: &[&str]) -> Columns {
152        Columns::new(items.iter().map(|s| s.to_string()).collect())
153    }
154
155    #[test]
156    fn packs_into_two_rows() {
157        let out =
158            console(20).render_export(&columns(&["one", "two", "three", "four", "five", "six"]));
159        assert_eq!(out, "one  two three four\nfive six           \n");
160    }
161
162    #[test]
163    fn single_row_when_it_fits() {
164        let out = console(30).render_export(&columns(&["alpha", "beta", "gamma", "delta"]));
165        assert_eq!(out, "alpha beta gamma delta\n");
166    }
167}