1use crate::cells::{cell_len, set_cell_size};
17use crate::console::{Console, ConsoleOptions, Justify};
18use crate::protocol::Renderable;
19use crate::r#box::{Box as BoxSet, RowLevel, HEAVY_HEAD};
20use crate::segment::Segment;
21use crate::style::Style;
22use crate::text::Text;
23use crate::theme::Theme;
24
25struct Column {
27 header: String,
28 justify: Justify,
29 width: Option<usize>,
31 style: Style,
33 header_content_style: Option<Style>,
37 header_fill: Option<Style>,
41 ratio: Option<usize>,
44 min_width: Option<usize>,
46 max_width: Option<usize>,
49 no_wrap: bool,
51}
52
53pub struct Table {
55 columns: Vec<Column>,
56 rows: Vec<Vec<String>>,
57 box_set: BoxSet,
58 show_header: bool,
59 show_lines: bool,
60 show_edge: bool,
61 pad_edge: bool,
62 collapse_padding: bool,
63 expand: bool,
64 title: Option<String>,
65 caption: Option<String>,
66 padding: (usize, usize, usize, usize),
67 header_style: Style,
68 border_style: Style,
69 style: Style,
70}
71
72impl Default for Table {
73 fn default() -> Self {
74 Table {
75 columns: Vec::new(),
76 rows: Vec::new(),
77 box_set: HEAVY_HEAD,
78 show_header: true,
79 show_lines: false,
80 show_edge: true,
81 pad_edge: true,
82 collapse_padding: false,
83 expand: false,
84 title: None,
85 caption: None,
86 padding: (0, 1, 0, 1),
87 header_style: Style::parse("bold").expect("valid built-in style"),
88 border_style: Style::new(),
89 style: Style::new(),
90 }
91 }
92}
93
94impl Table {
95 pub fn new() -> Self {
96 Table::default()
97 }
98
99 pub fn box_set(mut self, box_set: BoxSet) -> Self {
101 self.box_set = box_set;
102 self
103 }
104
105 pub fn border_style(mut self, style: Style) -> Self {
108 self.border_style = style;
109 self
110 }
111
112 pub fn show_header(mut self, show: bool) -> Self {
114 self.show_header = show;
115 self
116 }
117
118 pub fn expand(mut self, expand: bool) -> Self {
120 self.expand = expand;
121 self
122 }
123
124 pub fn show_lines(mut self, show: bool) -> Self {
126 self.show_lines = show;
127 self
128 }
129
130 pub fn show_edge(mut self, show: bool) -> Self {
133 self.show_edge = show;
134 self
135 }
136
137 pub fn pad_edge(mut self, pad: bool) -> Self {
140 self.pad_edge = pad;
141 self
142 }
143
144 pub fn collapse_padding(mut self, collapse: bool) -> Self {
147 self.collapse_padding = collapse;
148 self
149 }
150
151 pub fn style(mut self, style: Style) -> Self {
155 self.style = style;
156 self
157 }
158
159 fn cell_padding(&self, index: usize, ncols: usize) -> (usize, usize) {
162 let (_, pr, _, pl) = self.padding;
163 let mut left = if self.collapse_padding && index > 0 {
166 pl.saturating_sub(pr)
167 } else {
168 pl
169 };
170 let mut right = pr;
171 if !self.pad_edge && index == 0 {
173 left = 0;
174 }
175 if !self.pad_edge && index + 1 == ncols {
176 right = 0;
177 }
178 (left, right)
179 }
180
181 pub fn title(mut self, title: impl Into<String>) -> Self {
183 self.title = Some(title.into());
184 self
185 }
186
187 pub fn caption(mut self, caption: impl Into<String>) -> Self {
189 self.caption = Some(caption.into());
190 self
191 }
192
193 pub fn add_column(&mut self, header: impl Into<String>) -> &mut Self {
195 self.add_column_justify(header, Justify::Left)
196 }
197
198 pub fn add_column_justify(&mut self, header: impl Into<String>, justify: Justify) -> &mut Self {
200 self.columns.push(Column {
201 header: header.into(),
202 justify,
203 width: None,
204 style: Style::new(),
205 header_content_style: None,
206 header_fill: None,
207 ratio: None,
208 min_width: None,
209 max_width: None,
210 no_wrap: false,
211 });
212 self
213 }
214
215 pub fn column_width(&mut self, width: usize) -> &mut Self {
219 if let Some(column) = self.columns.last_mut() {
220 column.width = Some(width);
221 }
222 self
223 }
224
225 pub fn column_ratio(&mut self, ratio: usize) -> &mut Self {
229 if let Some(column) = self.columns.last_mut() {
230 column.ratio = Some(ratio);
231 }
232 self
233 }
234
235 pub fn column_min_width(&mut self, min_width: usize) -> &mut Self {
238 if let Some(column) = self.columns.last_mut() {
239 column.min_width = Some(min_width);
240 }
241 self
242 }
243
244 pub fn column_max_width(&mut self, max_width: usize) -> &mut Self {
247 if let Some(column) = self.columns.last_mut() {
248 column.max_width = Some(max_width);
249 }
250 self
251 }
252
253 pub fn column_style(&mut self, style: Style) -> &mut Self {
256 if let Some(column) = self.columns.last_mut() {
257 column.style = style;
258 }
259 self
260 }
261
262 pub fn column_header_style(&mut self, style: Style) -> &mut Self {
266 if let Some(column) = self.columns.last_mut() {
267 column.header_content_style = Some(style);
268 }
269 self
270 }
271
272 pub fn column_header_fill(&mut self, style: Style) -> &mut Self {
276 if let Some(column) = self.columns.last_mut() {
277 column.header_fill = Some(style);
278 }
279 self
280 }
281
282 pub fn column_no_wrap(&mut self) -> &mut Self {
285 if let Some(column) = self.columns.last_mut() {
286 column.no_wrap = true;
287 }
288 self
289 }
290
291 pub fn add_row(&mut self, cells: &[&str]) -> &mut Self {
293 self.rows
294 .push(cells.iter().map(|s| s.to_string()).collect());
295 self
296 }
297
298 fn max_content_widths(&self) -> Vec<usize> {
300 let mut widths = vec![0usize; self.columns.len()];
301 for (index, column) in self.columns.iter().enumerate() {
302 if self.show_header {
303 widths[index] = cell_len(&column.header);
304 }
305 }
306 for row in &self.rows {
307 for (index, cell) in row.iter().enumerate() {
308 if index < widths.len() {
309 widths[index] = widths[index].max(cell_len(cell));
310 }
311 }
312 }
313 widths
314 }
315
316 fn column_widths(&self, available: usize) -> Vec<usize> {
320 let ncols = self.columns.len();
321 let content = self.max_content_widths();
324 let mut widths: Vec<i64> = self
325 .columns
326 .iter()
327 .zip(&content)
328 .enumerate()
329 .map(|(index, (column, &measured))| {
330 let (pl, pr) = self.cell_padding(index, ncols);
331 let content_width = match column.width {
332 Some(w) => w,
333 None => {
334 let mut w = measured;
335 if let Some(min) = column.min_width {
336 w = w.max(min);
337 }
338 if let Some(max) = column.max_width {
339 w = w.min(max);
340 }
341 w
342 }
343 };
344 (content_width + pl + pr) as i64
345 })
346 .collect();
347
348 if self.expand {
352 let ratios: Vec<i64> = self
353 .columns
354 .iter()
355 .filter(|c| c.ratio.is_some())
356 .map(|c| c.ratio.unwrap() as i64)
357 .collect();
358 if ratios.iter().any(|&r| r > 0) {
359 let fixed_widths: Vec<i64> = widths
360 .iter()
361 .zip(&self.columns)
362 .map(|(&w, c)| if c.ratio.is_some() { 0 } else { w })
363 .collect();
364 let flex_minimum: Vec<i64> = self
365 .columns
366 .iter()
367 .enumerate()
368 .filter(|(_, c)| c.ratio.is_some())
369 .map(|(index, c)| {
370 let (pl, pr) = self.cell_padding(index, ncols);
371 (c.width.unwrap_or(1) + pl + pr) as i64
372 })
373 .collect();
374 let flexible_width = available as i64 - fixed_widths.iter().sum::<i64>();
375 let flex_widths = ratio_distribute(flexible_width, &ratios, Some(&flex_minimum));
376 let mut iter_flex = flex_widths.into_iter();
377 for (index, column) in self.columns.iter().enumerate() {
378 if column.ratio.is_some() {
379 widths[index] = fixed_widths[index] + iter_flex.next().unwrap_or(0);
380 }
381 }
382 }
383 }
384
385 let table_width: i64 = widths.iter().sum();
386 if table_width > available as i64 {
387 let wrapable: Vec<bool> = self
390 .columns
391 .iter()
392 .map(|c| c.width.is_none() && !c.no_wrap)
393 .collect();
394 widths = collapse_widths(widths, &wrapable, available as i64);
395 let table_width: i64 = widths.iter().sum();
398 if table_width > available as i64 {
399 let excess = table_width - available as i64;
400 let ratios = vec![1i64; widths.len()];
401 widths = ratio_reduce(excess, &ratios, &widths, &widths);
402 }
403 }
404
405 let table_width: i64 = widths.iter().sum();
408 if self.expand && table_width < available as i64 && table_width > 0 {
409 let pad = ratio_distribute(available as i64 - table_width, &widths, None);
410 for (width, extra) in widths.iter_mut().zip(pad) {
411 *width += extra;
412 }
413 }
414 widths.into_iter().map(|w| w.max(0) as usize).collect()
415 }
416
417 fn cell_style(&self, index: usize, is_header: bool) -> Style {
420 if is_header {
421 match self.columns.get(index).and_then(|c| c.header_fill.as_ref()) {
423 Some(fill) => self.header_style.combine(fill),
424 None => self.header_style.clone(),
425 }
426 } else {
427 self.columns
428 .get(index)
429 .map(|c| c.style.clone())
430 .unwrap_or_default()
431 }
432 }
433
434 fn render_row(
436 &self,
437 theme: &Theme,
438 cells: &[String],
439 content_widths: &[usize],
440 is_header: bool,
441 edges: (char, char, char),
442 ) -> Vec<Vec<Segment>> {
443 let (pt, _, pb, _) = self.padding;
446 let (edge_left, edge_vertical, edge_right) = edges;
447 let border = Some(self.style.combine(&self.border_style));
448 let ncols = self.columns.len();
449
450 let mut cell_lines: Vec<Vec<Vec<Segment>>> = Vec::with_capacity(ncols);
452 let mut height = 1;
453 for (index, width) in content_widths.iter().enumerate() {
454 let style = self.cell_style(index, is_header);
455 let cell_fill = Some(style.clone());
456 let content = cells.get(index).map(String::as_str).unwrap_or("");
457 let column = self.columns.get(index);
458 let justify = column.map(|c| c.justify).unwrap_or(Justify::Left);
459 let no_wrap = column.map(|c| c.no_wrap).unwrap_or(false);
460 let wrapped = if no_wrap {
463 ellipsis_crop(content, *width)
464 } else {
465 wrap_cell(content, *width).join("\n")
466 };
467 let mut text = Text::new(wrapped).justify(justify);
468 if is_header {
471 if let Some(span) = column.and_then(|c| c.header_content_style.clone()) {
472 let len = text.plain().len();
473 text.stylize(span, 0, len);
474 }
475 }
476 let mut lines = text.render_lines(theme, &style, Some(*width));
477 if lines.is_empty() {
478 lines.push(Vec::new());
479 }
480 let blank = || Segment::new(" ".repeat(*width), cell_fill.clone());
482 let mut padded_lines: Vec<Vec<Segment>> = Vec::new();
483 for _ in 0..pt {
484 padded_lines.push(vec![blank()]);
485 }
486 for line in &lines {
487 let padded = Segment::adjust_line_length(line, *width, cell_fill.clone());
488 padded_lines.push(Segment::simplify(&padded));
489 }
490 for _ in 0..pb {
491 padded_lines.push(vec![blank()]);
492 }
493 height = height.max(padded_lines.len());
494 cell_lines.push(padded_lines);
495 }
496
497 for (index, lines) in cell_lines.iter_mut().enumerate() {
499 let fill = Some(self.cell_style(index, is_header));
500 while lines.len() < height {
501 lines.push(vec![Segment::new(
502 " ".repeat(content_widths[index]),
503 fill.clone(),
504 )]);
505 }
506 }
507
508 let last = ncols.saturating_sub(1);
509 let mut rows_out: Vec<Vec<Segment>> = Vec::with_capacity(height);
510 #[allow(clippy::needless_range_loop)]
513 for r in 0..height {
514 let mut row = Vec::new();
515 if self.show_edge {
516 row.push(Segment::new(edge_left.to_string(), border.clone()));
517 }
518 for (c, column_lines) in cell_lines.iter().enumerate() {
519 let fill = Some(self.cell_style(c, is_header));
520 let (cpl, cpr) = self.cell_padding(c, ncols);
521 if cpl > 0 {
522 row.push(Segment::new(" ".repeat(cpl), fill.clone()));
523 }
524 row.extend(column_lines[r].clone());
525 if cpr > 0 {
526 row.push(Segment::new(" ".repeat(cpr), fill.clone()));
527 }
528 if c != last {
529 row.push(Segment::new(edge_vertical.to_string(), border.clone()));
530 } else if self.show_edge {
531 row.push(Segment::new(edge_right.to_string(), border.clone()));
532 }
533 }
534 rows_out.push(row);
535 }
536 rows_out
537 }
538}
539
540impl Renderable for Table {
541 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
542 if self.columns.is_empty() {
543 return Vec::new();
544 }
545 let box_set = self.box_set.substitute(
547 console.legacy_windows(),
548 console.safe_box(),
549 console.ascii_only(),
550 );
551 let ncols = self.columns.len();
552 let extra_width = (if self.show_edge { 2 } else { 0 }) + ncols.saturating_sub(1);
555 let available = options.max_width.saturating_sub(extra_width);
556
557 let rendered_widths = self.column_widths(available);
558 let content_widths: Vec<usize> = rendered_widths
559 .iter()
560 .enumerate()
561 .map(|(index, w)| {
562 let (pl, pr) = self.cell_padding(index, ncols);
563 w.saturating_sub(pl + pr)
564 })
565 .collect();
566 let border = Some(self.style.combine(&self.border_style));
567
568 let table_width: usize = rendered_widths.iter().sum::<usize>() + extra_width;
570
571 let mut lines: Vec<Vec<Segment>> = Vec::new();
572
573 if let Some(title) = &self.title {
575 let style = Style::parse("italic").expect("valid built-in style");
576 lines.push(vec![Segment::new(center(title, table_width), Some(style))]);
577 }
578
579 let edge = self.show_edge;
580 if edge {
581 lines.push(vec![Segment::new(
582 box_set.get_top(&rendered_widths, edge),
583 border.clone(),
584 )]);
585 }
586
587 let head_edges = (box_set.head_left, box_set.head_vertical, box_set.head_right);
588 let body_edges = (box_set.mid_left, box_set.mid_vertical, box_set.mid_right);
589
590 if self.show_header {
591 let headers: Vec<String> = self.columns.iter().map(|c| c.header.clone()).collect();
592 lines.extend(self.render_row(
593 console.theme(),
594 &headers,
595 &content_widths,
596 true,
597 head_edges,
598 ));
599 lines.push(vec![Segment::new(
600 box_set.get_row(&rendered_widths, RowLevel::Head, edge),
601 border.clone(),
602 )]);
603 }
604
605 let row_last = self.rows.len().saturating_sub(1);
606 for (index, row) in self.rows.iter().enumerate() {
607 lines.extend(self.render_row(console.theme(), row, &content_widths, false, body_edges));
608 if self.show_lines && index != row_last {
609 lines.push(vec![Segment::new(
610 box_set.get_row(&rendered_widths, RowLevel::Row, edge),
611 border.clone(),
612 )]);
613 }
614 }
615
616 if edge {
617 lines.push(vec![Segment::new(
618 box_set.get_bottom(&rendered_widths, edge),
619 border.clone(),
620 )]);
621 }
622
623 if let Some(caption) = &self.caption {
625 let style = Style::parse("dim italic").expect("valid built-in style");
626 lines.push(vec![Segment::new(
627 center(caption, table_width),
628 Some(style),
629 )]);
630 }
631
632 let mut segments = Vec::new();
634 let last = lines.len().saturating_sub(1);
635 for (index, line) in lines.into_iter().enumerate() {
636 segments.extend(line);
637 if index != last {
638 segments.push(Segment::line());
639 }
640 }
641 segments
642 }
643}
644
645fn wrap_cell(content: &str, width: usize) -> Vec<String> {
649 if width == 0 {
650 return vec![String::new()];
651 }
652 let breaks = crate::wrap::divide_line(content, width, false);
655 let chars: Vec<char> = content.chars().collect();
656 let mut lines: Vec<String> = Vec::new();
657 let mut start = 0;
658 for stop in breaks {
659 lines.push(chars[start..stop].iter().collect());
660 start = stop;
661 }
662 lines.push(chars[start..].iter().collect());
663 lines
666 .iter()
667 .map(|line| ellipsis_crop(line.trim_end(), width))
668 .collect()
669}
670
671fn ellipsis_crop(text: &str, width: usize) -> String {
674 if cell_len(text) <= width {
675 return text.to_string();
676 }
677 if width == 0 {
678 return String::new();
679 }
680 format!("{}\u{2026}", set_cell_size(text, width - 1))
681}
682
683fn center(text: &str, width: usize) -> String {
685 let excess = width.saturating_sub(cell_len(text));
686 let left = excess / 2;
687 let right = excess - left;
688 format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
689}
690
691fn round_half_even(value: f64) -> i64 {
693 let floor = value.floor();
694 let diff = value - floor;
695 if (diff - 0.5).abs() < 1e-9 {
696 let f = floor as i64;
697 if f % 2 == 0 {
698 f
699 } else {
700 f + 1
701 }
702 } else {
703 value.round() as i64
704 }
705}
706
707fn ratio_reduce(total: i64, ratios: &[i64], maximums: &[i64], values: &[i64]) -> Vec<i64> {
710 let ratios: Vec<i64> = ratios
711 .iter()
712 .zip(maximums)
713 .map(|(&r, &m)| if m != 0 { r } else { 0 })
714 .collect();
715 let mut total_ratio: i64 = ratios.iter().sum();
716 if total_ratio == 0 {
717 return values.to_vec();
718 }
719 let mut total_remaining = total;
720 let mut result = Vec::with_capacity(values.len());
721 for ((&ratio, &maximum), &value) in ratios.iter().zip(maximums).zip(values) {
722 if ratio != 0 && total_ratio > 0 {
723 let distributed = maximum.min(round_half_even(
724 ratio as f64 * total_remaining as f64 / total_ratio as f64,
725 ));
726 result.push(value - distributed);
727 total_remaining -= distributed;
728 total_ratio -= ratio;
729 } else {
730 result.push(value);
731 }
732 }
733 result
734}
735
736fn ratio_distribute(total: i64, ratios: &[i64], minimums: Option<&[i64]>) -> Vec<i64> {
740 let ratios: Vec<i64> = match minimums {
742 Some(mins) => ratios
743 .iter()
744 .zip(mins)
745 .map(|(&r, &m)| if m != 0 { r } else { 0 })
746 .collect(),
747 None => ratios.to_vec(),
748 };
749 let mut total_ratio: i64 = ratios.iter().sum();
750 let mut total_remaining = total;
751 let mut result = Vec::with_capacity(ratios.len());
752 for (index, &ratio) in ratios.iter().enumerate() {
753 let minimum = minimums.map_or(0, |m| m[index]);
754 let distributed = if total_ratio > 0 {
755 let numerator = ratio * total_remaining;
758 let ceil_div = (numerator + total_ratio - 1) / total_ratio;
759 minimum.max(ceil_div)
760 } else {
761 total_remaining
762 };
763 result.push(distributed);
764 total_ratio -= ratio;
765 total_remaining -= distributed;
766 }
767 result
768}
769
770fn collapse_widths(mut widths: Vec<i64>, wrapable: &[bool], max_width: i64) -> Vec<i64> {
773 let mut total_width: i64 = widths.iter().sum();
774 let mut excess_width = total_width - max_width;
775 if wrapable.iter().any(|&w| w) {
776 while total_width != 0 && excess_width > 0 {
777 let max_column = widths
778 .iter()
779 .zip(wrapable)
780 .filter(|(_, &w)| w)
781 .map(|(&x, _)| x)
782 .max()
783 .unwrap_or(0);
784 let second_max_column = widths
785 .iter()
786 .zip(wrapable)
787 .map(|(&x, &w)| if w && x != max_column { x } else { 0 })
788 .max()
789 .unwrap_or(0);
790 let column_difference = max_column - second_max_column;
791 let ratios: Vec<i64> = widths
792 .iter()
793 .zip(wrapable)
794 .map(|(&x, &w)| i64::from(x == max_column && w))
795 .collect();
796 if !ratios.iter().any(|&r| r != 0) || column_difference == 0 {
797 break;
798 }
799 let max_reduce = vec![excess_width.min(column_difference); widths.len()];
800 widths = ratio_reduce(excess_width, &ratios, &max_reduce, &widths);
801 total_width = widths.iter().sum();
802 excess_width = total_width - max_width;
803 }
804 }
805 widths
806}
807
808#[cfg(test)]
809mod tests {
810 use super::*;
811 use crate::color::ColorSystem;
812 use crate::r#box::SQUARE;
813
814 fn console() -> Console {
815 Console::builder()
816 .force_terminal(true)
817 .color_system(Some(ColorSystem::Truecolor))
818 .width(40)
819 .no_color(false)
820 .build()
821 }
822
823 #[test]
824 fn simple_square_table() {
825 let mut table = Table::new().box_set(SQUARE);
826 table.add_column("Name");
827 table.add_column("Age");
828 table.add_row(&["Alice", "30"]);
829 table.add_row(&["Bob", "7"]);
830 let out = console().render_export(&table);
831 let expected = concat!(
832 "┌───────┬─────┐\n",
833 "│\x1b[1m \x1b[0m\x1b[1mName \x1b[0m\x1b[1m \x1b[0m│\x1b[1m \x1b[0m\x1b[1mAge\x1b[0m\x1b[1m \x1b[0m│\n",
834 "├───────┼─────┤\n",
835 "│ Alice │ 30 │\n",
836 "│ Bob │ 7 │\n",
837 "└───────┴─────┘\n",
838 );
839 assert_eq!(out, expected);
840 }
841}