1use std::fmt;
20
21use crate::cell::Cell;
22use crate::{self as term, Style};
23use crate::{Color, Constraint, Line, Paint, Size};
24
25pub use crate::Element;
26
27#[derive(Debug)]
28pub struct TableOptions {
29 pub overflow: bool,
31 pub spacing: usize,
33 pub border: Option<Color>,
35}
36
37impl Default for TableOptions {
38 fn default() -> Self {
39 Self {
40 overflow: false,
41 spacing: 1,
42 border: None,
43 }
44 }
45}
46
47impl TableOptions {
48 pub fn bordered() -> Self {
49 Self {
50 border: Some(term::colors::FAINT),
51 spacing: 3,
52 ..Self::default()
53 }
54 }
55}
56
57#[derive(Debug)]
58enum Row<const W: usize, T> {
59 Header([T; W]),
60 Data([T; W]),
61 Divider,
62}
63
64#[derive(Debug)]
65pub struct Table<const W: usize, T> {
66 rows: Vec<Row<W, T>>,
67 widths: [usize; W],
68 opts: TableOptions,
69}
70
71impl<const W: usize, T> Default for Table<W, T> {
72 fn default() -> Self {
73 Self {
74 rows: Vec::new(),
75 widths: [0; W],
76 opts: TableOptions::default(),
77 }
78 }
79}
80
81impl<const W: usize, T: Cell + fmt::Debug + Send + Sync> Element for Table<W, T>
82where
83 T::Padded: Into<Line>,
84{
85 fn size(&self, parent: Constraint) -> Size {
86 Table::size(self, parent)
87 }
88
89 fn render(&self, parent: Constraint) -> Vec<Line> {
90 let mut lines = Vec::new();
91 let border = self.opts.border;
92 let inner = self.inner(parent);
93 let cols = inner.cols;
94
95 if self.is_empty() {
97 return lines;
98 }
99
100 if let Some(color) = border {
101 lines.push(
102 Line::default()
103 .item(Paint::new("╭").fg(color))
104 .item(Paint::new("─".repeat(cols)).fg(color))
105 .item(Paint::new("╮").fg(color)),
106 );
107 }
108
109 for row in &self.rows {
110 let mut line = Line::default();
111
112 match row {
113 Row::Header(cells) | Row::Data(cells) => {
114 if let Some(color) = border {
115 line.push(Paint::new("│ ").fg(color));
116 }
117 for (i, cell) in cells.iter().enumerate() {
118 let pad = if i == cells.len() - 1 {
119 0
120 } else {
121 self.widths[i] + self.opts.spacing
122 };
123 line = line.extend(
124 cell.pad(pad)
125 .into()
126 .style(Style::default().bg(cell.background())),
127 );
128 }
129 Line::pad(&mut line, cols);
130 Line::truncate(&mut line, cols, "…");
131
132 if let Some(color) = border {
133 line.push(Paint::new(" │").fg(color));
134 }
135 lines.push(line);
136 }
137 Row::Divider => {
138 if let Some(color) = border {
139 lines.push(
140 Line::default()
141 .item(Paint::new("├").fg(color))
142 .item(Paint::new("─".repeat(cols)).fg(color))
143 .item(Paint::new("┤").fg(color)),
144 );
145 } else {
146 lines.push(Line::default());
147 }
148 }
149 }
150 }
151 if let Some(color) = border {
152 lines.push(
153 Line::default()
154 .item(Paint::new("╰").fg(color))
155 .item(Paint::new("─".repeat(cols)).fg(color))
156 .item(Paint::new("╯").fg(color)),
157 );
158 }
159 lines
160 }
161}
162
163impl<const W: usize, T: Cell> Table<W, T> {
164 pub fn new(opts: TableOptions) -> Self {
165 Self {
166 rows: Vec::new(),
167 widths: [0; W],
168 opts,
169 }
170 }
171
172 pub fn size(&self, parent: Constraint) -> Size {
173 self.outer(parent)
174 }
175
176 pub fn divider(&mut self) {
177 self.rows.push(Row::Divider);
178 }
179
180 pub fn push(&mut self, row: [T; W]) {
181 for (i, cell) in row.iter().enumerate() {
182 self.widths[i] = self.widths[i].max(cell.width());
183 }
184 self.rows.push(Row::Data(row));
185 }
186
187 pub fn header(&mut self, row: [T; W]) {
188 for (i, cell) in row.iter().enumerate() {
189 self.widths[i] = self.widths[i].max(cell.width());
190 }
191 self.rows.push(Row::Header(row));
192 }
193
194 pub fn extend(&mut self, rows: impl IntoIterator<Item = [T; W]>) {
195 for row in rows.into_iter() {
196 self.push(row);
197 }
198 }
199
200 pub fn is_empty(&self) -> bool {
201 !self.rows.iter().any(|r| matches!(r, Row::Data { .. }))
202 }
203
204 fn inner(&self, c: Constraint) -> Size {
205 let mut outer = self.outer(c);
206
207 if self.opts.border.is_some() {
208 outer.cols -= 2;
209 outer.rows -= 2;
210 }
211 outer
212 }
213
214 fn outer(&self, c: Constraint) -> Size {
215 let mut cols = self.widths.iter().sum::<usize>() + (W - 1) * self.opts.spacing;
216 let mut rows = self.rows.len();
217 let padding = 2;
218
219 if self.opts.border.is_some() {
221 cols += 2 + padding;
222 rows += 2;
223 }
224 Size::new(cols, rows).constrain(c)
225 }
226}
227
228#[cfg(test)]
229mod test {
230 use crate::Element;
231
232 use super::*;
233 use pretty_assertions::assert_eq;
234
235 #[test]
236 fn test_truncate() {
237 assert_eq!("🍍".truncate(1, "…"), String::from("…"));
238 assert_eq!("🍍".truncate(1, ""), String::from(""));
239 assert_eq!("🍍🍍".truncate(2, "…"), String::from("…"));
240 assert_eq!("🍍🍍".truncate(3, "…"), String::from("🍍…"));
241 assert_eq!("🍍".truncate(1, "🍎"), String::from(""));
242 assert_eq!("🍍".truncate(2, "🍎"), String::from("🍍"));
243 assert_eq!("🍍🍍".truncate(3, "🍎"), String::from("🍎"));
244 assert_eq!("🍍🍍🍍".truncate(4, "🍎"), String::from("🍍🍎"));
245 assert_eq!("hello".truncate(3, "…"), String::from("he…"));
246 }
247
248 #[test]
249 fn test_table() {
250 let mut t = Table::new(TableOptions::default());
251
252 t.push(["pineapple", "rosemary"]);
253 t.push(["apples", "pears"]);
254
255 #[rustfmt::skip]
256 assert_eq!(
257 t.display(Constraint::UNBOUNDED),
258 [
259 "pineapple rosemary\n",
260 "apples pears \n"
261 ].join("")
262 );
263 }
264
265 #[test]
266 fn test_table_border() {
267 let mut t = Table::new(TableOptions {
268 border: Some(Color::Unset),
269 spacing: 3,
270 ..TableOptions::default()
271 });
272
273 t.push(["Country", "Population", "Code"]);
274 t.divider();
275 t.push(["France", "60M", "FR"]);
276 t.push(["Switzerland", "7M", "CH"]);
277 t.push(["Germany", "80M", "DE"]);
278
279 let inner = t.inner(Constraint::UNBOUNDED);
280 assert_eq!(inner.cols, 33);
281 assert_eq!(inner.rows, 5);
282
283 let outer = t.outer(Constraint::UNBOUNDED);
284 assert_eq!(outer.cols, 35);
285 assert_eq!(outer.rows, 7);
286
287 assert_eq!(
288 t.display(Constraint::UNBOUNDED),
289 r#"
290╭─────────────────────────────────╮
291│ Country Population Code │
292├─────────────────────────────────┤
293│ France 60M FR │
294│ Switzerland 7M CH │
295│ Germany 80M DE │
296╰─────────────────────────────────╯
297"#
298 .trim_start()
299 );
300 }
301
302 #[test]
303 fn test_table_border_truncated() {
304 let mut t = Table::new(TableOptions {
305 border: Some(Color::Unset),
306 spacing: 3,
307 ..TableOptions::default()
308 });
309
310 t.push(["Code", "Name"]);
311 t.divider();
312 t.push(["FR", "France"]);
313 t.push(["CH", "Switzerland"]);
314 t.push(["DE", "Germany"]);
315
316 let constrain = Constraint::max(Size {
317 cols: 19,
318 rows: usize::MAX,
319 });
320 let outer = t.outer(constrain);
321 assert_eq!(outer.cols, 19);
322 assert_eq!(outer.rows, 7);
323
324 let inner = t.inner(constrain);
325 assert_eq!(inner.cols, 17);
326 assert_eq!(inner.rows, 5);
327
328 assert_eq!(
329 t.display(constrain),
330 r#"
331╭─────────────────╮
332│ Code Name │
333├─────────────────┤
334│ FR France │
335│ CH Switzer… │
336│ DE Germany │
337╰─────────────────╯
338"#
339 .trim_start()
340 );
341 }
342
343 #[test]
344 fn test_table_border_maximized() {
345 let mut t = Table::new(TableOptions {
346 border: Some(Color::Unset),
347 spacing: 3,
348 ..TableOptions::default()
349 });
350
351 t.push(["Code", "Name"]);
352 t.divider();
353 t.push(["FR", "France"]);
354 t.push(["CH", "Switzerland"]);
355 t.push(["DE", "Germany"]);
356
357 let constrain = Constraint::new(
358 Size { cols: 26, rows: 0 },
359 Size {
360 cols: 26,
361 rows: usize::MAX,
362 },
363 );
364 let outer = t.outer(constrain);
365 assert_eq!(outer.cols, 26);
366 assert_eq!(outer.rows, 7);
367
368 let inner = t.inner(constrain);
369 assert_eq!(inner.cols, 24);
370 assert_eq!(inner.rows, 5);
371
372 assert_eq!(
373 t.display(constrain),
374 r#"
375╭────────────────────────╮
376│ Code Name │
377├────────────────────────┤
378│ FR France │
379│ CH Switzerland │
380│ DE Germany │
381╰────────────────────────╯
382"#
383 .trim_start()
384 );
385 }
386
387 #[test]
388 fn test_table_truncate() {
389 let mut t = Table::default();
390 let constrain = Constraint::new(
391 Size::MIN,
392 Size {
393 cols: 16,
394 rows: usize::MAX,
395 },
396 );
397
398 t.push(["pineapple", "rosemary"]);
399 t.push(["apples", "pears"]);
400
401 #[rustfmt::skip]
402 assert_eq!(
403 t.display(constrain),
404 [
405 "pineapple rosem…\n",
406 "apples pears \n"
407 ].join("")
408 );
409 }
410
411 #[test]
412 fn test_table_unicode() {
413 let mut t = Table::new(TableOptions::default());
414
415 t.push(["🍍pineapple", "__rosemary", "__sage"]);
416 t.push(["__pears", "🍎apples", "🍌bananas"]);
417
418 #[rustfmt::skip]
419 assert_eq!(
420 t.display(Constraint::UNBOUNDED),
421 [
422 "🍍pineapple __rosemary __sage \n",
423 "__pears 🍎apples 🍌bananas\n"
424 ].join("")
425 );
426 }
427
428 #[test]
429 fn test_table_unicode_truncate() {
430 let mut t = Table::new(TableOptions {
431 ..TableOptions::default()
432 });
433 let constrain = Constraint::max(Size {
434 cols: 16,
435 rows: usize::MAX,
436 });
437 t.push(["🍍pineapple", "__rosemary"]);
438 t.push(["__pears", "🍎apples"]);
439
440 #[rustfmt::skip]
441 assert_eq!(
442 t.display(constrain),
443 [
444 "🍍pineapple __r…\n",
445 "__pears 🍎a…\n"
446 ].join("")
447 );
448 }
449}