1use crate::console::RenderContext;
6use crate::renderable::{Renderable, Segment};
7use crate::style::Style;
8use crate::text::{Span, Text};
9use unicode_width::UnicodeWidthStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum ColumnMode {
14 #[default]
16 Equal,
17 Fit,
19}
20
21#[derive(Debug, Clone)]
23pub struct Columns {
24 items: Vec<Text>,
26 num_columns: usize,
28 mode: ColumnMode,
30 gap: usize,
32 expand: bool,
34 style: Style,
36}
37
38impl Columns {
39 pub fn new<I, T>(items: I) -> Self
41 where
42 I: IntoIterator<Item = T>,
43 T: Into<Text>,
44 {
45 Columns {
46 items: items.into_iter().map(Into::into).collect(),
47 num_columns: 0,
48 mode: ColumnMode::Equal,
49 gap: 2,
50 expand: true,
51 style: Style::new(),
52 }
53 }
54
55 pub fn num_columns(mut self, n: usize) -> Self {
57 self.num_columns = n;
58 self
59 }
60
61 pub fn mode(mut self, mode: ColumnMode) -> Self {
63 self.mode = mode;
64 self
65 }
66
67 pub fn gap(mut self, gap: usize) -> Self {
69 self.gap = gap;
70 self
71 }
72
73 pub fn expand(mut self, expand: bool) -> Self {
75 self.expand = expand;
76 self
77 }
78
79 pub fn style(mut self, style: Style) -> Self {
81 self.style = style;
82 self
83 }
84
85 fn calculate_columns(&self, width: usize) -> usize {
87 if self.num_columns > 0 {
88 return self.num_columns;
89 }
90
91 if self.items.is_empty() {
92 return 1;
93 }
94
95 let max_item_width = self
97 .items
98 .iter()
99 .map(|item| item.width())
100 .max()
101 .unwrap_or(1);
102
103 let min_col_width = max_item_width + self.gap;
105 let cols = (width + self.gap) / min_col_width.max(1);
106
107 cols.max(1).min(self.items.len())
108 }
109}
110
111impl Renderable for Columns {
112 fn render(&self, context: &RenderContext) -> Vec<Segment> {
113 if self.items.is_empty() {
114 return vec![];
115 }
116
117 let num_cols = self.calculate_columns(context.width);
118 let num_rows = self.items.len().div_ceil(num_cols);
119
120 let total_gap = self.gap * (num_cols.saturating_sub(1));
122 let available = context.width.saturating_sub(total_gap);
123 let col_width = available / num_cols.max(1);
124
125 let mut segments = Vec::new();
126
127 for row_idx in 0..num_rows {
128 let mut row_spans = Vec::new();
129
130 for col_idx in 0..num_cols {
131 let item_idx = row_idx * num_cols + col_idx;
132
133 if col_idx > 0 {
134 row_spans.push(Span::raw(" ".repeat(self.gap)));
136 }
137
138 if item_idx < self.items.len() {
139 let item = &self.items[item_idx];
140 let content = item.plain_text();
141 let content_width = UnicodeWidthStr::width(content.as_str());
142
143 let displayed = if content_width > col_width {
145 truncate_to_width(&content, col_width)
146 } else {
147 let padding = col_width - content_width;
148 format!("{}{}", content, " ".repeat(padding))
149 };
150
151 row_spans.push(Span::styled(displayed, self.style));
152 } else {
153 row_spans.push(Span::raw(" ".repeat(col_width)));
155 }
156 }
157
158 segments.push(Segment::line(row_spans));
159 }
160
161 segments
162 }
163}
164
165fn truncate_to_width(s: &str, width: usize) -> String {
166 use unicode_segmentation::UnicodeSegmentation;
167
168 let mut result = String::new();
169 let mut current_width = 0;
170
171 for grapheme in s.graphemes(true) {
172 let grapheme_width = UnicodeWidthStr::width(grapheme);
173 if current_width + grapheme_width > width {
174 break;
175 }
176 result.push_str(grapheme);
177 current_width += grapheme_width;
178 }
179
180 while current_width < width {
182 result.push(' ');
183 current_width += 1;
184 }
185
186 result
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_columns_basic() {
195 let items = vec!["a", "b", "c", "d", "e", "f"];
196 let columns = Columns::new(items).num_columns(3);
197
198 let context = RenderContext {
199 width: 30,
200 height: None,
201 };
202 let segments = columns.render(&context);
203
204 assert_eq!(segments.len(), 2);
206 }
207
208 #[test]
209 fn test_columns_auto() {
210 let items = vec!["short", "items", "here"];
211 let columns = Columns::new(items);
212
213 let context = RenderContext {
214 width: 40,
215 height: None,
216 };
217 let segments = columns.render(&context);
218
219 assert!(!segments.is_empty());
221 }
222
223 #[test]
224 fn test_columns_empty() {
225 let columns = Columns::new(Vec::<&str>::new());
226 let context = RenderContext {
227 width: 40,
228 height: None,
229 };
230 let segments = columns.render(&context);
231 assert!(segments.is_empty());
232 }
233}