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