1use std::{fmt, sync::Arc};
4
5use crate::text::wrapped_line_count;
6use crate::view::{CanvasMeasure, CanvasRequirements, Claim, Kind, distribute, fit_text_lines};
7use crate::{
8 Align, BlockStyle, Canvas, CanvasContext, CanvasItem, CanvasSizing, Composition, Grapheme,
9 Length, LineGlyphs, LineNetwork, Overflow, Position, PrintableLines, PrintableText, Sides,
10 TableRole, TextStyle, VerticalAlign, View,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum TableRowKind {
16 Header,
17 Body(usize),
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct TableCell<'a> {
27 text: &'a str,
28 row: usize,
29 column: usize,
30}
31
32impl<'a> TableCell<'a> {
33 pub const fn row(&self) -> usize {
35 self.row
36 }
37
38 pub const fn column(&self) -> usize {
40 self.column
41 }
42
43 pub const fn text(&self) -> &'a str {
45 self.text
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct TableRowPosition {
52 index: usize,
53 len: usize,
54}
55
56impl TableRowPosition {
57 pub const fn index(self) -> usize {
59 self.index
60 }
61
62 pub const fn len(self) -> usize {
64 self.len
65 }
66
67 pub const fn is_empty(self) -> bool {
72 self.len == 0
73 }
74}
75
76#[derive(Debug, Default)]
78pub struct TableRowCells {
79 values: Vec<String>,
80}
81
82impl TableRowCells {
83 pub fn display(&mut self, value: impl fmt::Display) {
85 self.values.push(value.to_string());
86 }
87
88 pub fn text(&mut self, text: impl Into<String>) {
90 self.values.push(text.into());
91 }
92}
93
94pub trait TableRow {
101 fn write_cells(&self, cells: &mut TableRowCells);
103}
104
105#[derive(Debug, Clone, Default, PartialEq, Eq)]
107pub struct TextTableRow(Vec<String>);
108
109impl<I, S> From<I> for TextTableRow
110where
111 I: IntoIterator<Item = S>,
112 S: Into<String>,
113{
114 fn from(values: I) -> Self {
115 Self(values.into_iter().map(Into::into).collect())
116 }
117}
118
119impl TableRow for TextTableRow {
120 fn write_cells(&self, cells: &mut TableRowCells) {
121 cells.values.extend(self.0.iter().cloned());
122 }
123}
124
125type RowFormatter<'a, T> = dyn Fn(&T, TableRowPosition, &mut TableRowCells) + 'a;
126type RowCellStyler<'a, T> = dyn Fn(&T, TableCell<'_>, &BlockStyle) -> Option<BlockStyle> + 'a;
127
128#[derive(Clone)]
130pub struct TableRowPresentation<'a, T> {
131 format: Arc<RowFormatter<'a, T>>,
132 cell_style: Option<Arc<RowCellStyler<'a, T>>>,
133}
134
135impl<T> fmt::Debug for TableRowPresentation<'_, T> {
136 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 formatter.write_str("TableRowPresentation { .. }")
138 }
139}
140
141impl<'a, T: 'a> TableRowPresentation<'a, T> {
142 pub fn new<F>(format: F) -> Self
144 where
145 F: Fn(&T, TableRowPosition, &mut TableRowCells) + 'a,
146 {
147 Self {
148 format: Arc::new(format),
149 cell_style: None,
150 }
151 }
152
153 #[must_use]
158 pub fn cell_style<S>(mut self, cell_style: S) -> Self
159 where
160 S: Fn(&T, TableCell<'_>, &BlockStyle) -> Option<BlockStyle> + 'a,
161 {
162 self.cell_style = Some(Arc::new(cell_style));
163 self
164 }
165}
166
167impl<'a, T> TableRowPresentation<'a, T>
168where
169 T: TableRow + 'a,
170{
171 pub fn display() -> Self {
173 Self::new(|row: &T, _, cells| row.write_cells(cells))
174 }
175}
176
177#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
178struct RowOffset {
179 start: usize,
180 end: usize,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct Table<Row = TextTableRow> {
207 headers: Vec<String>,
208 rows: Vec<Row>,
209 hidden: bool,
210 offset: RowOffset,
211}
212
213impl<Row> Default for Table<Row> {
214 fn default() -> Self {
215 Self {
216 headers: Vec::new(),
217 rows: Vec::new(),
218 hidden: false,
219 offset: RowOffset::default(),
220 }
221 }
222}
223
224impl<Row> Table<Row> {
225 pub fn new() -> Self {
227 Self::default()
228 }
229
230 #[must_use]
237 pub fn headers<I, S>(mut self, headers: I) -> Self
238 where
239 I: IntoIterator<Item = S>,
240 S: Into<String>,
241 {
242 self.headers = headers.into_iter().map(Into::into).collect();
243 self
244 }
245
246 #[must_use]
248 pub fn row(mut self, row: Row) -> Self {
249 self.rows.push(row);
250 self
251 }
252
253 #[must_use]
255 pub fn rows<I>(mut self, rows: I) -> Self
256 where
257 I: IntoIterator<Item = Row>,
258 {
259 self.rows.extend(rows);
260 self
261 }
262
263 #[must_use]
265 pub const fn hidden(mut self, hidden: bool) -> Self {
266 self.hidden = hidden;
267 self
268 }
269
270 #[must_use]
274 pub const fn offset(mut self, start: usize, end: usize) -> Self {
275 self.offset = RowOffset { start, end };
276 self
277 }
278
279 pub const fn is_hidden(&self) -> bool {
281 self.hidden
282 }
283
284 fn visible_rows(&self) -> &[Row] {
285 let end = self.rows.len().saturating_sub(self.offset.end);
286 if self.offset.start >= end {
287 return &[];
288 }
289 &self.rows[self.offset.start..end]
290 }
291}
292
293impl<Row> Table<Row>
294where
295 Row: TableRow,
296{
297 pub fn column_count(&self) -> usize {
303 self.rows
304 .iter()
305 .map(|row| {
306 let mut cells = TableRowCells::default();
307 row.write_cells(&mut cells);
308 cells.values.len()
309 })
310 .chain(std::iter::once(self.headers.len()))
311 .max()
312 .unwrap_or_default()
313 }
314}
315
316impl Table<TextTableRow> {
317 pub fn text() -> TextTable {
319 TextTable { table: Self::new() }
320 }
321}
322
323#[derive(Debug, Clone, Default, PartialEq, Eq)]
325pub struct TextTable {
326 table: Table<TextTableRow>,
327}
328
329impl TextTable {
330 #[must_use]
332 pub fn headers<I, S>(mut self, headers: I) -> Self
333 where
334 I: IntoIterator<Item = S>,
335 S: Into<String>,
336 {
337 self.table = self.table.headers(headers);
338 self
339 }
340
341 #[must_use]
343 pub fn row<I, S>(mut self, row: I) -> Self
344 where
345 I: IntoIterator<Item = S>,
346 S: Into<String>,
347 {
348 self.table.rows.push(TextTableRow::from(row));
349 self
350 }
351
352 #[must_use]
354 pub fn rows<I, R, S>(mut self, rows: I) -> Self
355 where
356 I: IntoIterator<Item = R>,
357 R: IntoIterator<Item = S>,
358 S: Into<String>,
359 {
360 self.table
361 .rows
362 .extend(rows.into_iter().map(TextTableRow::from));
363 self
364 }
365
366 #[must_use]
368 pub fn hidden(mut self, hidden: bool) -> Self {
369 self.table = self.table.hidden(hidden);
370 self
371 }
372
373 #[must_use]
375 pub fn offset(mut self, start: usize, end: usize) -> Self {
376 self.table = self.table.offset(start, end);
377 self
378 }
379
380 pub fn column_count(&self) -> usize {
382 self.table.column_count()
383 }
384}
385
386impl std::ops::Deref for TextTable {
387 type Target = Table<TextTableRow>;
388
389 fn deref(&self) -> &Self::Target {
390 &self.table
391 }
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub struct TableBorder(TableBorderKind);
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419enum TableBorderKind {
420 Network(LineGlyphs),
421 Markdown,
422 Booktabs,
423}
424
425impl TableBorder {
426 pub const NORMAL: Self = Self::network(LineGlyphs::NORMAL);
428 pub const ROUNDED: Self = Self::network(LineGlyphs::ROUNDED);
430 pub const THICK: Self = Self::network(LineGlyphs::THICK);
432 pub const DOUBLE: Self = Self::network(LineGlyphs::DOUBLE);
434 pub const ASCII: Self = Self::network(LineGlyphs::ASCII);
436 pub const MARKDOWN: Self = Self(TableBorderKind::Markdown);
438 pub const BOOKTABS: Self = Self(TableBorderKind::Booktabs);
440 pub const HIDDEN: Self = Self::network(LineGlyphs::HIDDEN);
442
443 pub const fn network(glyphs: LineGlyphs) -> Self {
445 Self(TableBorderKind::Network(glyphs))
446 }
447}
448
449#[derive(Debug, Clone)]
455pub struct TablePresentation {
456 header_style: BlockStyle,
457 cell_style: BlockStyle,
458 header_styles: Vec<Option<BlockStyle>>,
459 column_styles: Vec<Option<BlockStyle>>,
460 border_style: TextStyle,
461 border: TableBorder,
462 border_top: bool,
463 border_bottom: bool,
464 border_left: bool,
465 border_right: bool,
466 border_header: bool,
467 border_column: bool,
468 border_row: bool,
469 padding: u16,
470 width: Option<u16>,
471}
472
473impl PartialEq for TablePresentation {
474 fn eq(&self, other: &Self) -> bool {
475 self.header_style == other.header_style
476 && self.cell_style == other.cell_style
477 && self.header_styles == other.header_styles
478 && self.column_styles == other.column_styles
479 && self.border_style == other.border_style
480 && self.border == other.border
481 && self.border_top == other.border_top
482 && self.border_bottom == other.border_bottom
483 && self.border_left == other.border_left
484 && self.border_right == other.border_right
485 && self.border_header == other.border_header
486 && self.border_column == other.border_column
487 && self.border_row == other.border_row
488 && self.padding == other.padding
489 && self.width == other.width
490 }
491}
492
493impl TablePresentation {
494 pub fn new(header_style: BlockStyle, cell_style: BlockStyle, border_style: TextStyle) -> Self {
496 Self {
497 header_style,
498 cell_style,
499 header_styles: Vec::new(),
500 column_styles: Vec::new(),
501 border_style,
502 border: TableBorder::NORMAL,
503 border_top: true,
504 border_bottom: true,
505 border_left: true,
506 border_right: true,
507 border_header: true,
508 border_column: true,
509 border_row: false,
510 padding: 1,
511 width: None,
512 }
513 }
514
515 pub const fn get_style(&self, role: TableRole) -> &BlockStyle {
517 match role {
518 TableRole::Header => &self.header_style,
519 TableRole::Cell => &self.cell_style,
520 }
521 }
522
523 pub const fn get_border_style(&self) -> &TextStyle {
525 &self.border_style
526 }
527
528 #[must_use]
530 pub fn style(mut self, role: TableRole, style: BlockStyle) -> Self {
531 match role {
532 TableRole::Header => self.header_style = style,
533 TableRole::Cell => self.cell_style = style,
534 }
535 self
536 }
537
538 #[must_use]
540 pub fn header_style(self, style: BlockStyle) -> Self {
541 self.style(TableRole::Header, style)
542 }
543
544 #[must_use]
546 pub fn cell_style(self, style: BlockStyle) -> Self {
547 self.style(TableRole::Cell, style)
548 }
549
550 #[must_use]
555 pub fn header_styles<I>(mut self, styles: I) -> Self
556 where
557 I: IntoIterator<Item = Option<BlockStyle>>,
558 {
559 self.header_styles = styles.into_iter().collect();
560 self
561 }
562
563 #[must_use]
568 pub fn column_styles<I>(mut self, styles: I) -> Self
569 where
570 I: IntoIterator<Item = Option<BlockStyle>>,
571 {
572 self.column_styles = styles.into_iter().collect();
573 self
574 }
575
576 #[must_use]
578 pub fn border_style(mut self, style: TextStyle) -> Self {
579 self.border_style = style;
580 self
581 }
582
583 #[must_use]
585 pub const fn border(mut self, border: TableBorder) -> Self {
586 self.border = border;
587 self
588 }
589
590 #[must_use]
592 pub const fn border_top(mut self, enabled: bool) -> Self {
593 self.border_top = enabled;
594 self
595 }
596
597 #[must_use]
599 pub const fn border_bottom(mut self, enabled: bool) -> Self {
600 self.border_bottom = enabled;
601 self
602 }
603
604 #[must_use]
606 pub const fn border_left(mut self, enabled: bool) -> Self {
607 self.border_left = enabled;
608 self
609 }
610
611 #[must_use]
613 pub const fn border_right(mut self, enabled: bool) -> Self {
614 self.border_right = enabled;
615 self
616 }
617
618 #[must_use]
620 pub const fn border_header(mut self, enabled: bool) -> Self {
621 self.border_header = enabled;
622 self
623 }
624
625 #[must_use]
631 pub const fn border_column(mut self, enabled: bool) -> Self {
632 self.border_column = enabled;
633 self
634 }
635
636 #[must_use]
638 pub const fn border_row(mut self, enabled: bool) -> Self {
639 self.border_row = enabled;
640 self
641 }
642
643 #[must_use]
645 pub const fn padding(mut self, padding: u16) -> Self {
646 self.padding = padding;
647 self
648 }
649
650 #[must_use]
673 pub const fn width(mut self, width: u16) -> Self {
674 self.width = Some(width);
675 self
676 }
677
678 pub fn compose<T>(&self, table: &Table<T>) -> View
685 where
686 T: TableRow,
687 {
688 self.compose_with(table, &TableRowPresentation::display())
689 }
690
691 pub fn compose_with<T>(&self, table: &Table<T>, rows: &TableRowPresentation<'_, T>) -> View {
693 if table.is_hidden() {
694 return View::empty();
695 }
696
697 let visible_rows = table.visible_rows();
698 let mut formatted = Vec::with_capacity(visible_rows.len());
699 for (index, row) in visible_rows.iter().enumerate() {
700 let mut cells = TableRowCells::default();
701 (rows.format)(
702 row,
703 TableRowPosition {
704 index,
705 len: visible_rows.len(),
706 },
707 &mut cells,
708 );
709 formatted.push(cells.values);
710 }
711 let columns = column_count(&formatted, table.headers.len());
714 if columns == 0 {
715 return View::empty();
716 }
717
718 let item = TableItem(Arc::new(TableFrame::new(
719 table,
720 self,
721 visible_rows,
722 &formatted,
723 rows.cell_style.as_deref(),
724 columns,
725 )));
726 let canvas = Canvas::new().sizing(item.sizing()).item(item);
727 let frame = self.width.map_or_else(BlockStyle::new, |width| {
728 BlockStyle::new().width(Length::Cells(width))
729 });
730 View::block(frame, View::canvas(canvas))
731 }
732
733 fn fallback_style(&self, kind: TableRowKind, column: usize) -> &BlockStyle {
734 let styles = match kind {
735 TableRowKind::Header => &self.header_styles,
736 TableRowKind::Body(_) => &self.column_styles,
737 };
738 styles
739 .get(column)
740 .and_then(Option::as_ref)
741 .unwrap_or(match kind {
742 TableRowKind::Header => &self.header_style,
743 TableRowKind::Body(_) => &self.cell_style,
744 })
745 }
746}
747
748#[derive(Debug, Clone, PartialEq)]
749struct TableFrame {
750 presentation: TablePresentation,
751 rows: Vec<TableFrameRow>,
752 columns: usize,
753}
754
755impl TableFrame {
756 fn new<T>(
757 table: &Table<T>,
758 presentation: &TablePresentation,
759 visible_rows: &[T],
760 formatted_rows: &[Vec<String>],
761 cell_style: Option<&RowCellStyler<'_, T>>,
762 columns: usize,
763 ) -> Self {
764 let mut rows =
765 Vec::with_capacity(visible_rows.len() + usize::from(!table.headers.is_empty()));
766 if !table.headers.is_empty() {
767 rows.push(TableFrameRow::new(
768 &table.headers,
769 columns,
770 TableRowKind::Header,
771 presentation,
772 |_, _| None,
773 ));
774 }
775 for (index, (row, formatted)) in visible_rows.iter().zip(formatted_rows).enumerate() {
776 rows.push(TableFrameRow::new(
777 formatted,
778 columns,
779 TableRowKind::Body(index),
780 presentation,
781 |cell, fallback| cell_style.and_then(|style| style(row, cell, fallback)),
782 ));
783 }
784 let mut presentation = presentation.clone();
785 presentation.header_style =
786 frame_cell_style(presentation.header_style, presentation.padding);
787 presentation.cell_style = frame_cell_style(presentation.cell_style, presentation.padding);
788 for style in presentation
789 .header_styles
790 .iter_mut()
791 .chain(&mut presentation.column_styles)
792 .flatten()
793 {
794 *style = frame_cell_style(style.clone(), presentation.padding);
795 }
796 Self {
797 presentation,
798 rows,
799 columns,
800 }
801 }
802
803 fn line_width(&self) -> usize {
804 usize::from(self.presentation.border_left)
805 + usize::from(self.presentation.border_right)
806 + usize::from(self.presentation.border_column) * self.columns.saturating_sub(1)
807 }
808
809 fn line_height(&self) -> usize {
810 usize::from(self.presentation.border_top)
811 + usize::from(self.presentation.border_bottom)
812 + (0..self.rows.len().saturating_sub(1))
813 .filter(|gap| self.draws_row_rule(*gap))
814 .count()
815 }
816
817 fn draws_row_rule(&self, gap: usize) -> bool {
818 match self.rows.get(gap).map(|row| row.kind) {
819 Some(TableRowKind::Header) => self.presentation.border_header,
820 _ => self.presentation.border_row,
821 }
822 }
823
824 fn column_requirements(&self) -> Vec<(usize, usize)> {
825 (0..self.columns)
826 .map(|column| {
827 self.rows
828 .iter()
829 .map(|row| {
830 let cell = &row.cells[column];
831 cell.width_requirements(cell.style(&self.presentation, row.kind, column))
832 })
833 .fold((0, 0), |(demand, floor), next| {
834 (demand.max(next.0), floor.max(next.1))
835 })
836 })
837 .collect()
838 }
839
840 fn column_widths(&self, width: usize) -> Vec<usize> {
841 let requirements = self.column_requirements();
842 let kind = if self.presentation.width.is_some() {
843 Kind::fill(1)
844 } else {
845 Kind::Auto
846 };
847 let claims: Vec<Claim> = requirements
848 .iter()
849 .map(|(demand, floor)| Claim {
850 kind,
851 demand: *demand,
852 floor: *floor,
853 })
854 .collect();
855 distribute(width.saturating_sub(self.line_width()), &claims)
856 }
857
858 fn row_heights(&self, columns: &[usize]) -> Vec<usize> {
859 self.rows
860 .iter()
861 .map(|row| {
862 row.cells
863 .iter()
864 .enumerate()
865 .zip(columns)
866 .map(|((column, cell), width)| {
867 cell.height_at(*width, cell.style(&self.presentation, row.kind, column))
868 })
869 .max()
870 .unwrap_or(0)
871 })
872 .collect()
873 }
874
875 fn height_at(&self, width: usize) -> usize {
876 self.row_heights(&self.column_widths(width))
877 .iter()
878 .sum::<usize>()
879 .saturating_add(self.line_height())
880 }
881
882 fn draw(&self, context: &mut CanvasContext) {
883 let columns = self.column_widths(context.size().width());
884 let heights = self.row_heights(&columns);
885
886 self.record_cell_rows(context, &columns, &heights);
887 match self.presentation.border.0 {
888 TableBorderKind::Network(glyphs) => {
889 self.record_line_network(context, &columns, &heights, glyphs);
890 }
891 TableBorderKind::Markdown => {
892 self.record_straight_rules(context, &columns, &heights, '|', '-', '-', '-', true);
893 }
894 TableBorderKind::Booktabs => {
895 self.record_straight_rules(context, &columns, &heights, ' ', '━', '─', '━', false);
896 }
897 }
898 }
899
900 fn record_cell_rows(&self, context: &mut CanvasContext, columns: &[usize], heights: &[usize]) {
901 let mut y = usize::from(self.presentation.border_top);
902 for (row_index, (row, height)) in self.rows.iter().zip(heights).enumerate() {
903 let rendered = row
904 .cells
905 .iter()
906 .zip(columns)
907 .enumerate()
908 .map(|(column, (cell, width))| {
909 cell.rendered_rows(
910 *width,
911 *height,
912 cell.style(&self.presentation, row.kind, column),
913 )
914 })
915 .collect::<Vec<_>>();
916 for line in 0..*height {
917 let mut runs = Vec::<TextRun>::new();
918 let mut x = 0;
919 if self.presentation.border_left {
920 push_run(&mut runs, x, " ", TextStyle::new());
921 x += 1;
922 }
923 for (column, ((cell, rows), width)) in
924 row.cells.iter().zip(&rendered).zip(columns).enumerate()
925 {
926 let style = cell.style(&self.presentation, row.kind, column);
927 push_run(&mut runs, x, &rows[line], style.text_style().clone());
928 x = x.saturating_add(*width);
929 if column + 1 < columns.len() && self.presentation.border_column {
930 push_run(&mut runs, x, " ", TextStyle::new());
931 x += 1;
932 }
933 }
934 if self.presentation.border_right {
935 push_run(&mut runs, x, " ", TextStyle::new());
936 }
937 for run in runs {
938 context.text_with(
939 Position::new(position(run.x), position(y + line)),
940 run.text,
941 run.style,
942 Composition::Replace,
943 );
944 }
945 }
946 y = y.saturating_add(*height);
947 if row_index + 1 < self.rows.len() && self.draws_row_rule(row_index) {
948 y += 1;
949 }
950 }
951 }
952
953 fn rule_columns(&self, columns: &[usize]) -> Vec<usize> {
954 let mut rules = Vec::with_capacity(columns.len() + 1);
955 let mut x = 0usize;
956 if self.presentation.border_left {
957 rules.push(x);
958 x += 1;
959 }
960 for (index, width) in columns.iter().enumerate() {
961 if index > 0 && self.presentation.border_column {
962 rules.push(x);
963 x += 1;
964 }
965 x = x.saturating_add(*width);
966 }
967 if self.presentation.border_right {
968 rules.push(x);
969 }
970 rules
971 }
972
973 fn rule_rows(&self, heights: &[usize]) -> Vec<usize> {
974 let mut rules = Vec::with_capacity(self.rows.len() + 1);
975 let mut y = 0usize;
976 if self.presentation.border_top {
977 rules.push(y);
978 y += 1;
979 }
980 for (row_index, height) in heights.iter().enumerate() {
981 y = y.saturating_add(*height);
982 if row_index + 1 < heights.len() && self.draws_row_rule(row_index) {
983 rules.push(y);
984 y += 1;
985 }
986 }
987 if self.presentation.border_bottom {
988 rules.push(y);
989 }
990 rules
991 }
992
993 fn record_line_network(
994 &self,
995 context: &mut CanvasContext,
996 columns: &[usize],
997 heights: &[usize],
998 glyphs: LineGlyphs,
999 ) {
1000 let width = columns
1001 .iter()
1002 .sum::<usize>()
1003 .saturating_add(self.line_width());
1004 let height = heights
1005 .iter()
1006 .sum::<usize>()
1007 .saturating_add(self.line_height());
1008 if width == 0 || height == 0 {
1009 return;
1010 }
1011
1012 let rows = self.rule_rows(heights);
1013 let columns = self.rule_columns(columns);
1014 if rows.is_empty() && columns.is_empty() {
1015 return;
1016 }
1017
1018 let mut network = LineNetwork::new(glyphs, self.rule_style());
1019 let first_x = if self.presentation.border_left { 0 } else { -1 };
1024 let last_x = if self.presentation.border_right {
1025 position(width - 1)
1026 } else {
1027 position(width)
1028 };
1029 let first_y = if self.presentation.border_top { 0 } else { -1 };
1030 let last_y = if self.presentation.border_bottom {
1031 position(height - 1)
1032 } else {
1033 position(height)
1034 };
1035 for y in rows {
1036 network.horizontal(position(y), first_x..=last_x);
1037 }
1038 for x in columns {
1039 network.vertical(position(x), first_y..=last_y);
1040 }
1041 context.line_network(network);
1042 }
1043
1044 #[allow(clippy::too_many_arguments)]
1045 fn record_straight_rules(
1046 &self,
1047 context: &mut CanvasContext,
1048 columns: &[usize],
1049 heights: &[usize],
1050 vertical: char,
1051 top: char,
1052 middle: char,
1053 bottom: char,
1054 vertical_last: bool,
1055 ) {
1056 let width = columns
1057 .iter()
1058 .sum::<usize>()
1059 .saturating_add(self.line_width());
1060 let height = heights
1061 .iter()
1062 .sum::<usize>()
1063 .saturating_add(self.line_height());
1064 if width == 0 || height == 0 {
1065 return;
1066 }
1067
1068 let record_verticals = |context: &mut CanvasContext| {
1069 for x in self.rule_columns(columns) {
1070 self.record_cell_line(context, x, 0, x, height - 1, vertical);
1071 }
1072 };
1073 let record_horizontals = |context: &mut CanvasContext| {
1074 let rows = self.rule_rows(heights);
1075 for (index, y) in rows.iter().copied().enumerate() {
1076 let glyph = if self.presentation.border_top && index == 0 {
1077 top
1078 } else if self.presentation.border_bottom && index + 1 == rows.len() {
1079 bottom
1080 } else {
1081 middle
1082 };
1083 self.record_cell_line(context, 0, y, width - 1, y, glyph);
1084 }
1085 };
1086
1087 if vertical_last {
1088 record_horizontals(context);
1089 record_verticals(context);
1090 } else {
1091 record_verticals(context);
1092 record_horizontals(context);
1093 }
1094 }
1095
1096 fn record_cell_line(
1097 &self,
1098 context: &mut CanvasContext,
1099 from_x: usize,
1100 from_y: usize,
1101 to_x: usize,
1102 to_y: usize,
1103 glyph: char,
1104 ) {
1105 let glyph = glyph.to_string();
1106 context.line(
1107 Position::new(position(from_x), position(from_y)),
1108 Position::new(position(to_x), position(to_y)),
1109 Grapheme::new(&glyph),
1110 self.rule_style(),
1111 );
1112 }
1113
1114 fn rule_style(&self) -> TextStyle {
1115 let mut style = TextStyle::new();
1116 if let Some(color) = self.presentation.border_style.get_foreground() {
1117 style = style.foreground(color);
1118 }
1119 if let Some(color) = self.presentation.border_style.get_background() {
1120 style = style.background(color);
1121 }
1122 style
1123 }
1124}
1125
1126#[derive(Debug, Clone, PartialEq)]
1127struct TableFrameRow {
1128 kind: TableRowKind,
1129 cells: Vec<TableFrameCell>,
1130}
1131
1132impl TableFrameRow {
1133 fn new<F>(
1134 values: &[String],
1135 columns: usize,
1136 kind: TableRowKind,
1137 presentation: &TablePresentation,
1138 mut override_style: F,
1139 ) -> Self
1140 where
1141 F: FnMut(TableCell<'_>, &BlockStyle) -> Option<BlockStyle>,
1142 {
1143 let cells = (0..columns)
1144 .map(|column| {
1145 let text = values.get(column).map_or("", String::as_str);
1146 let style_override = match kind {
1147 TableRowKind::Header => None,
1148 TableRowKind::Body(row) => {
1149 let cell = TableCell { text, row, column };
1150 override_style(cell, presentation.fallback_style(kind, column))
1151 }
1152 };
1153 TableFrameCell {
1154 text: text.to_owned(),
1155 style_override: style_override
1156 .map(|style| frame_cell_style(style, presentation.padding)),
1157 }
1158 })
1159 .collect();
1160 Self { kind, cells }
1161 }
1162}
1163
1164#[derive(Debug, Clone, PartialEq)]
1165struct TableFrameCell {
1166 text: String,
1167 style_override: Option<BlockStyle>,
1168}
1169
1170impl TableFrameCell {
1171 fn style<'a>(
1172 &'a self,
1173 presentation: &'a TablePresentation,
1174 kind: TableRowKind,
1175 column: usize,
1176 ) -> &'a BlockStyle {
1177 self.style_override
1178 .as_ref()
1179 .unwrap_or_else(|| presentation.fallback_style(kind, column))
1180 }
1181
1182 fn width_requirements(&self, style: &BlockStyle) -> (usize, usize) {
1183 let padding = style.get_padding();
1184 let frame = usize::from(padding.left) + usize::from(padding.right);
1185 let lines = PrintableLines::new(&self.text).lines();
1186 let demand = lines.iter().map(|line| line.width()).max().unwrap_or(0);
1187 let floor = lines
1188 .iter()
1189 .flat_map(|line| line.graphemes().map(Grapheme::width))
1190 .max()
1191 .unwrap_or(0);
1192 (demand.saturating_add(frame), floor.saturating_add(frame))
1193 }
1194
1195 fn height_at(&self, width: usize, style: &BlockStyle) -> usize {
1196 let padding = style.get_padding();
1197 let content_width = width
1198 .saturating_sub(usize::from(padding.left).saturating_add(usize::from(padding.right)));
1199 let content_height = match style.get_overflow() {
1200 Overflow::Wrap => wrapped_line_count(PrintableLines::new(&self.text), content_width),
1201 Overflow::Clip(_) => PrintableLines::new(&self.text).lines().len(),
1202 };
1203 content_height
1204 .saturating_add(usize::from(padding.top))
1205 .saturating_add(usize::from(padding.bottom))
1206 }
1207
1208 fn rendered_rows(&self, width: usize, height: usize, style: &BlockStyle) -> Vec<String> {
1209 let padding = style.get_padding();
1210 let content_width = width
1211 .saturating_sub(usize::from(padding.left).saturating_add(usize::from(padding.right)));
1212 let content_height = height
1213 .saturating_sub(usize::from(padding.top).saturating_add(usize::from(padding.bottom)));
1214 let mut lines = fit_text_lines(&self.text, Some(content_width), style.get_overflow());
1215 lines.truncate(content_height);
1216 let vertical_gap = content_height.saturating_sub(lines.len());
1217 let above = match style.get_vertical_align() {
1218 VerticalAlign::Top => 0,
1219 VerticalAlign::Center => vertical_gap / 2,
1220 VerticalAlign::Bottom => vertical_gap,
1221 };
1222 let blank = " ".repeat(width);
1223 let mut rows = Vec::with_capacity(height);
1224 rows.resize(
1225 usize::from(padding.top).saturating_add(above),
1226 blank.clone(),
1227 );
1228 for line in lines {
1229 let line_width = PrintableText::new(&line).width();
1230 let gap = content_width.saturating_sub(line_width);
1231 let before = match style.get_align() {
1232 Align::Left => 0,
1233 Align::Center => gap / 2,
1234 Align::Right => gap,
1235 };
1236 let after = gap.saturating_sub(before);
1237 rows.push(format!(
1238 "{}{}{}",
1239 " ".repeat(usize::from(padding.left).saturating_add(before)),
1240 line,
1241 " ".repeat(after.saturating_add(usize::from(padding.right)))
1242 ));
1243 }
1244 rows.resize(height, blank);
1245 rows
1246 }
1247
1248 #[cfg(test)]
1249 fn view(&self, style: &BlockStyle) -> View {
1250 let style = style.clone().width(Length::fill(1)).height(Length::fill(1));
1251 let text_style = style.text_style().clone();
1252 View::block(style, View::text(self.text.clone(), text_style))
1253 }
1254}
1255
1256fn frame_cell_style(mut style: BlockStyle, padding: u16) -> BlockStyle {
1257 style = style
1258 .reset_border()
1259 .margin(Sides::default())
1260 .reset_width()
1261 .reset_height()
1262 .reset_min_width()
1263 .reset_min_height()
1264 .reset_max_width()
1265 .reset_max_height();
1266 if style.get_padding() == Default::default() {
1267 style = style.padding((0, padding));
1268 }
1269 style
1270}
1271
1272struct TextRun {
1273 x: usize,
1274 text: String,
1275 style: TextStyle,
1276}
1277
1278fn push_run(runs: &mut Vec<TextRun>, x: usize, text: &str, style: TextStyle) {
1279 if let Some(run) = runs.last_mut().filter(|run| run.style == style) {
1280 run.text.push_str(text);
1281 } else {
1282 runs.push(TextRun {
1283 x,
1284 text: text.to_owned(),
1285 style,
1286 });
1287 }
1288}
1289
1290#[derive(Debug, Clone, PartialEq)]
1291struct TableItem(Arc<TableFrame>);
1292
1293impl TableItem {
1294 fn sizing(&self) -> CanvasSizing {
1295 CanvasSizing::intrinsic(self.clone())
1296 }
1297}
1298
1299impl CanvasMeasure for TableItem {
1300 fn width_requirements(&self) -> CanvasRequirements {
1301 let requirements = self.0.column_requirements();
1302 CanvasRequirements::new(
1303 requirements
1304 .iter()
1305 .map(|requirement| requirement.0)
1306 .sum::<usize>()
1307 .saturating_add(self.0.line_width()),
1308 requirements
1309 .iter()
1310 .map(|requirement| requirement.1)
1311 .sum::<usize>()
1312 .saturating_add(self.0.line_width()),
1313 )
1314 }
1315
1316 fn height_requirements(&self, width: usize) -> CanvasRequirements {
1317 CanvasRequirements::new(self.0.height_at(width), 0)
1318 }
1319}
1320
1321impl CanvasItem for TableItem {
1322 fn draw(&self, context: &mut CanvasContext) {
1323 self.0.draw(context);
1324 }
1325}
1326
1327fn position(value: usize) -> i64 {
1328 i64::try_from(value).unwrap_or(i64::MAX)
1329}
1330
1331fn column_count(rows: &[Vec<String>], headers: usize) -> usize {
1333 rows.iter()
1334 .map(Vec::len)
1335 .chain(std::iter::once(headers))
1336 .max()
1337 .unwrap_or_default()
1338}
1339
1340#[cfg(test)]
1341mod tests {
1342 use super::*;
1343 use crate::test_support::{plain_exact as plain, plain_rows, style_at};
1344 use crate::{
1345 Align, Available, Border, Color, ComponentTheme, Overflow, PrintableText, SemanticTokens,
1346 StyledGrapheme, VerticalAlign, measure, resolve,
1347 };
1348
1349 fn styles() -> ComponentTheme {
1350 ComponentTheme::from_tokens(&SemanticTokens {
1351 text: Color::WHITE,
1352 text_muted: Color::BRIGHT_BLACK,
1353 background: Color::BLACK,
1354 surface: Color::BLACK,
1355 accent: Color::CYAN,
1356 accent_text: Color::BLACK,
1357 success: Color::GREEN,
1358 warning: Color::YELLOW,
1359 error: Color::RED,
1360 border: Color::BRIGHT_BLACK,
1361 })
1362 }
1363
1364 fn sample() -> TextTable {
1365 Table::text()
1366 .headers(["Name", "Location"])
1367 .row(["Kini", "New York"])
1368 .row(["Iris", "Paris"])
1369 }
1370
1371 #[test]
1372 fn composes_headers_rows_and_borders() {
1373 assert_eq!(
1374 plain(&styles().table().compose(&sample())),
1375 "\
1376┌──────┬──────────┐
1377│ Name │ Location │
1378├──────┼──────────┤
1379│ Kini │ New York │
1380│ Iris │ Paris │
1381└──────┴──────────┘"
1382 );
1383 }
1384
1385 #[test]
1386 fn empty_tables_compose_nothing() {
1387 let table_style = styles().table().clone();
1388 assert!(measure(&table_style.compose(&Table::text())).is_empty());
1389 assert!(measure(&table_style.compose(&sample().hidden(true))).is_empty());
1390
1391 let headers_only = Table::text().headers(["Name", "Location"]);
1392 assert_eq!(
1393 plain(&table_style.compose(&headers_only)),
1394 "\
1395┌──────┬──────────┐
1396│ Name │ Location │
1397└──────┴──────────┘"
1398 );
1399
1400 let rows_only = Table::text().row(["Kini", "New York"]);
1401 assert_eq!(
1402 plain(&table_style.compose(&rows_only)),
1403 "\
1404┌──────┬──────────┐
1405│ Kini │ New York │
1406└──────┴──────────┘"
1407 );
1408 }
1409
1410 #[test]
1411 fn ragged_rows_pad_missing_cells() {
1412 let table = Table::text()
1413 .headers(["A", "B", "C"])
1414 .row(["1"])
1415 .row(["1", "2", "3"]);
1416
1417 assert_eq!(
1418 plain(&styles().table().compose(&table)),
1419 "\
1420┌───┬───┬───┐
1421│ A │ B │ C │
1422├───┼───┼───┤
1423│ 1 │ │ │
1424│ 1 │ 2 │ 3 │
1425└───┴───┴───┘"
1426 );
1427 }
1428
1429 #[test]
1430 fn offsets_select_body_rows_and_keep_headers() {
1431 let table = sample().row(["Eli", "London"]).offset(1, 1);
1432
1433 assert_eq!(
1434 plain(&styles().table().compose(&table)),
1435 "\
1436┌──────┬──────────┐
1437│ Name │ Location │
1438├──────┼──────────┤
1439│ Iris │ Paris │
1440└──────┴──────────┘",
1441 "an offset selects body rows; headers still set the column width"
1442 );
1443 }
1444
1445 #[test]
1446 fn aligns_cjk_content_by_terminal_cell_width() {
1447 let table = Table::text()
1448 .headers(["名前", "所在地"])
1449 .row(["日本語", "東京"])
1450 .row(["ab", "Paris"]);
1451 let view = styles().table().compose(&table);
1452
1453 assert_eq!(
1454 plain(&view),
1455 "\
1456┌────────┬────────┐
1457│ 名前 │ 所在地 │
1458├────────┼────────┤
1459│ 日本語 │ 東京 │
1460│ ab │ Paris │
1461└────────┴────────┘"
1462 );
1463 let widths: Vec<usize> = plain_rows(&view)
1464 .iter()
1465 .map(|row| PrintableText::new(row).width())
1466 .collect();
1467 assert!(widths.iter().all(|width| *width == widths[0]));
1468 }
1469
1470 #[test]
1471 fn multiline_cells_extend_the_row() {
1472 let table = Table::text()
1473 .headers(["Key", "Value"])
1474 .row(["a", "one\ntwo"]);
1475
1476 assert_eq!(
1477 plain(&styles().table().compose(&table)),
1478 "\
1479┌─────┬───────┐
1480│ Key │ Value │
1481├─────┼───────┤
1482│ a │ one │
1483│ │ two │
1484└─────┴───────┘"
1485 );
1486 }
1487
1488 #[test]
1489 fn width_expands_and_shrinks_columns() {
1490 let table_style = styles().table().clone();
1491 let wide = table_style.clone().width(28);
1492 let view = wide.compose(&sample());
1493 for row in plain_rows(&view) {
1494 assert_eq!(PrintableText::new(&row).width(), 28);
1495 }
1496
1497 let narrow = table_style.width(16);
1498 assert_eq!(
1499 plain(&narrow.compose(&sample())),
1500 "\
1501┌──────┬───────┐
1502│ Name │ Locat │
1503│ │ ion │
1504├──────┼───────┤
1505│ Kini │ New │
1506│ │ York │
1507│ Iris │ Paris │
1508└──────┴───────┘"
1509 );
1510 }
1511
1512 #[test]
1513 fn a_width_constraint_narrower_than_a_grapheme_keeps_rows_rectangular() {
1514 let table = Table::text().headers(["A", "B"]).row(["日本語", "x"]);
1520 let view = styles().table().clone().width(9).compose(&table);
1521
1522 assert_eq!(
1523 plain(&view),
1524 "\
1525┌────┬───┐
1526│ A │ B │
1527├────┼───┤
1528│ 日 │ x │
1529│ 本 │ │
1530│ 語 │ │
1531└────┴───┘",
1532 "the column floors at one wide grapheme and wraps the rest"
1533 );
1534
1535 let padded = styles()
1536 .table()
1537 .clone()
1538 .padding(0)
1539 .width(5)
1540 .compose(&Table::text().row(["日本", "ab"]));
1541 let widths: Vec<usize> = plain_rows(&padded)
1542 .iter()
1543 .map(|row| PrintableText::new(row).width())
1544 .collect();
1545 assert!(
1546 widths.iter().all(|width| *width == widths[0]),
1547 "rectangular at its floor: {widths:?}"
1548 );
1549 }
1550
1551 #[test]
1552 fn width_below_the_column_minimum_overflows() {
1553 let narrow = styles().table().clone().width(4);
1554 let view = narrow.compose(&sample());
1555 let width = PrintableText::new(&plain_rows(&view)[0]).width();
1556
1557 assert!(
1558 width > 4,
1559 "a table cannot shrink below its minimum: {width}"
1560 );
1561 assert_eq!(width, 9);
1562 }
1563
1564 #[test]
1565 fn border_presets_change_every_glyph() {
1566 let table_style = styles().table().clone();
1567 let cases = [
1568 (
1569 TableBorder::MARKDOWN,
1570 [
1571 "|------|----------|",
1572 "| Name | Location |",
1573 "|------|----------|",
1574 "| Kini | New York |",
1575 "| Iris | Paris |",
1576 "|------|----------|",
1577 ]
1578 .join("\n"),
1579 ),
1580 (
1581 TableBorder::BOOKTABS,
1585 [
1586 "━━━━━━━━━━━━━━━━━━━",
1587 " Name Location ",
1588 "───────────────────",
1589 " Kini New York ",
1590 " Iris Paris ",
1591 "━━━━━━━━━━━━━━━━━━━",
1592 ]
1593 .join("\n"),
1594 ),
1595 (
1596 TableBorder::HIDDEN,
1597 [
1598 " ",
1599 " Name Location ",
1600 " ",
1601 " Kini New York ",
1602 " Iris Paris ",
1603 " ",
1604 ]
1605 .join("\n"),
1606 ),
1607 ];
1608
1609 for (border, expected) in cases {
1610 assert_eq!(
1611 plain(&table_style.clone().border(border).compose(&sample())),
1612 expected
1613 );
1614 }
1615 }
1616
1617 #[test]
1618 fn edge_toggles_omit_their_glyphs() {
1619 let table_style = styles()
1620 .table()
1621 .clone()
1622 .border_top(false)
1623 .border_bottom(false)
1624 .border_left(false)
1625 .border_right(false)
1626 .border_column(false);
1627
1628 assert_eq!(
1629 plain(&table_style.compose(&sample())),
1630 [
1631 " Name Location ",
1632 "────────────────",
1633 " Kini New York ",
1634 " Iris Paris ",
1635 ]
1636 .join("\n")
1637 );
1638 }
1639
1640 #[test]
1641 fn row_rules_separate_every_body_row() {
1642 let table_style = styles().table().clone().border_row(true);
1643
1644 assert_eq!(
1645 plain(&table_style.compose(&sample())),
1646 "\
1647┌──────┬──────────┐
1648│ Name │ Location │
1649├──────┼──────────┤
1650│ Kini │ New York │
1651├──────┼──────────┤
1652│ Iris │ Paris │
1653└──────┴──────────┘"
1654 );
1655 }
1656
1657 #[test]
1658 fn a_disabled_header_rule_leaves_the_first_gap_to_the_header_policy() {
1659 let table_style = styles()
1660 .table()
1661 .clone()
1662 .border_header(false)
1663 .border_row(true);
1664
1665 assert_eq!(
1666 plain(&table_style.compose(&sample())),
1667 "\
1668┌──────┬──────────┐
1669│ Name │ Location │
1670│ Kini │ New York │
1671├──────┼──────────┤
1672│ Iris │ Paris │
1673└──────┴──────────┘",
1674 "the header owns its gap even when body rows carry rules"
1675 );
1676 }
1677
1678 #[test]
1679 fn a_disabled_column_rule_occupies_no_cell() {
1680 let table_style = styles().table().clone().border_column(false);
1681
1682 assert_eq!(
1683 plain(&table_style.compose(&sample())),
1684 "\
1685┌────────────────┐
1686│ Name Location │
1687├────────────────┤
1688│ Kini New York │
1689│ Iris Paris │
1690└────────────────┘",
1691 "the columns touch while the outer edges and row rules remain"
1692 );
1693 }
1694
1695 #[test]
1696 fn each_disabled_outer_edge_removes_its_own_cells() {
1697 let table = Table::text().headers(["A", "B"]).row(["1", "2"]);
1698 let table_style = styles().table().clone();
1699 let cases = [
1700 (
1701 table_style.clone().border_top(false),
1702 ["│ A │ B │", "├───┼───┤", "│ 1 │ 2 │", "└───┴───┘"].join("\n"),
1703 ),
1704 (
1705 table_style.clone().border_bottom(false),
1706 ["┌───┬───┐", "│ A │ B │", "├───┼───┤", "│ 1 │ 2 │"].join("\n"),
1707 ),
1708 (
1709 table_style.clone().border_left(false),
1710 ["───┬───┐", " A │ B │", "───┼───┤", " 1 │ 2 │", "───┴───┘"].join("\n"),
1711 ),
1712 (
1713 table_style.border_right(false),
1714 ["┌───┬───", "│ A │ B ", "├───┼───", "│ 1 │ 2 ", "└───┴───"].join("\n"),
1715 ),
1716 ];
1717
1718 for (presentation, expected) in cases {
1719 assert_eq!(plain(&presentation.compose(&table)), expected);
1720 }
1721 }
1722
1723 #[test]
1724 fn a_single_cell_has_no_internal_separator() {
1725 let table = Table::text().row(["only"]);
1726
1727 assert_eq!(
1728 plain(&styles().table().compose(&table)),
1729 "\
1730┌──────┐
1731│ only │
1732└──────┘"
1733 );
1734 }
1735
1736 #[test]
1737 fn a_standalone_network_rule_uses_its_straight_glyph() {
1738 let glyphs = LineGlyphs {
1739 isolated: 'o',
1740 end_up: 'u',
1741 end_right: 'r',
1742 end_down: 'd',
1743 end_left: 'l',
1744 vertical: 'v',
1745 horizontal: 'h',
1746 ..LineGlyphs::NORMAL
1747 };
1748 let one_column = Table::text().row([""]);
1749 let vertical = styles()
1750 .table()
1751 .clone()
1752 .border(TableBorder::network(glyphs))
1753 .padding(0)
1754 .border_top(false)
1755 .border_bottom(false)
1756 .border_right(false)
1757 .border_column(false)
1758 .compose(&one_column);
1759 assert_eq!(plain(&vertical), "v");
1760
1761 let one_cell = Table::text().row(["x"]);
1762 let horizontal = styles()
1763 .table()
1764 .clone()
1765 .border(TableBorder::network(glyphs))
1766 .padding(0)
1767 .border_bottom(false)
1768 .border_left(false)
1769 .border_right(false)
1770 .border_column(false)
1771 .compose(&one_cell);
1772 assert_eq!(plain(&horizontal), "h\nx");
1773 }
1774
1775 #[test]
1776 fn zero_width_cells_preserve_corners_next_to_disabled_edges() {
1777 let table = Table::text().row([""]);
1778 let left = styles()
1779 .table()
1780 .clone()
1781 .padding(0)
1782 .border_bottom(false)
1783 .border_right(false)
1784 .border_column(false)
1785 .compose(&table);
1786 assert_eq!(plain(&left), "┌\n│");
1787
1788 let right = styles()
1789 .table()
1790 .clone()
1791 .padding(0)
1792 .border_bottom(false)
1793 .border_left(false)
1794 .border_column(false)
1795 .compose(&table);
1796 assert_eq!(plain(&right), "┐\n│");
1797 }
1798
1799 fn accent_rows() -> TableRowPresentation<'static, TextTableRow> {
1800 TableRowPresentation::display().cell_style(|_, cell, _| match cell.row() {
1801 1 => Some(BlockStyle::new().foreground(Color::GREEN)),
1802 _ => None,
1803 })
1804 }
1805
1806 #[test]
1807 fn cell_replacements_override_column_and_table_styles() {
1808 let component_styles = styles();
1809 let table_style = component_styles
1810 .table()
1811 .clone()
1812 .header_styles([
1813 Some(BlockStyle::new().foreground(Color::CYAN).bold()),
1814 Some(BlockStyle::new().foreground(Color::CYAN).bold()),
1815 ])
1816 .column_styles([None, Some(BlockStyle::new().align(Align::Right))]);
1817 let view = table_style.compose_with(&sample(), &accent_rows());
1818
1819 assert_eq!(
1821 style_at(&view, 1, 2),
1822 TextStyle::new().foreground(Color::CYAN).bold()
1823 );
1824 for column in [2, 9] {
1826 assert_eq!(
1827 style_at(&view, 4, column),
1828 TextStyle::new().foreground(Color::GREEN)
1829 );
1830 }
1831 assert_eq!(
1833 style_at(&view, 3, 2),
1834 component_styles
1835 .table()
1836 .get_style(TableRole::Cell)
1837 .text_style()
1838 .clone()
1839 );
1840 assert_eq!(style_at(&view, 3, 9), TextStyle::new());
1844 assert_eq!(
1845 plain_rows(&view)[3],
1846 "│ Kini │ New York │",
1847 "the widest cell of a column has no slack to realign"
1848 );
1849 }
1850
1851 #[test]
1852 fn a_styler_realigns_a_column_that_has_slack() {
1853 let table = sample().row(["Eli", "London"]);
1856 let table_style = styles()
1857 .table()
1858 .clone()
1859 .column_styles([None, Some(BlockStyle::new().align(Align::Right))]);
1860 let view = table_style.compose_with(&table, &accent_rows());
1861
1862 assert_eq!(
1863 plain_rows(&view)[5],
1864 "│ Eli │ London │",
1865 "the (_, 1) arm right-aligns column 1 where the cell has slack"
1866 );
1867 }
1868
1869 #[test]
1870 fn a_typed_cell_override_keeps_padding_and_loses_the_rest_of_the_box() {
1871 let rows = TableRowPresentation::<TextTableRow>::display().cell_style(|_, _, _| {
1872 Some(
1873 BlockStyle::new()
1874 .padding((0, 3))
1875 .margin(2)
1876 .width(30)
1877 .height(4)
1878 .min_width(24)
1879 .min_height(3)
1880 .max_width(28)
1881 .max_height(3)
1882 .border(Border::DOUBLE),
1883 )
1884 });
1885
1886 let table_style = styles().table().clone();
1887
1888 assert_eq!(
1893 plain(&table_style.compose_with(&sample(), &rows)),
1894 "\
1895┌──────────┬──────────────┐
1896│ Name │ Location │
1897├──────────┼──────────────┤
1898│ Kini │ New York │
1899│ Iris │ Paris │
1900└──────────┴──────────────┘"
1901 );
1902 }
1903
1904 #[test]
1905 fn an_offset_excluded_row_does_not_widen_the_table() {
1906 let table = Table::text()
1907 .headers(["A", "B"])
1908 .row(["1", "2"])
1909 .row(["w", "x", "y", "z"])
1910 .offset(0, 1);
1911
1912 assert_eq!(
1913 table.column_count(),
1914 4,
1915 "the data-level count still spans every owned row"
1916 );
1917 assert_eq!(
1918 plain(&styles().table().compose(&table)),
1919 "\
1920┌───┬───┐
1921│ A │ B │
1922├───┼───┤
1923│ 1 │ 2 │
1924└───┴───┘",
1925 "composition spans only the visible rows"
1926 );
1927 }
1928
1929 #[test]
1930 fn cell_alignment_comes_from_the_cell_style() {
1931 let table_style = styles().table().clone().column_styles([
1932 Some(BlockStyle::new().align(Align::Right)),
1933 Some(BlockStyle::new().align(Align::Right)),
1934 ]);
1935
1936 assert_eq!(
1937 plain(&table_style.compose(&sample())),
1938 "\
1939┌──────┬──────────┐
1940│ Name │ Location │
1941├──────┼──────────┤
1942│ Kini │ New York │
1943│ Iris │ Paris │
1944└──────┴──────────┘"
1945 );
1946 }
1947
1948 #[test]
1949 fn vertical_alignment_places_short_cells_within_a_tall_row() {
1950 let table = Table::text().row(["a", "one\ntwo"]);
1951 let table_style = styles().table().clone().column_styles([
1952 Some(BlockStyle::new().vertical_align(VerticalAlign::Bottom)),
1953 Some(BlockStyle::new().vertical_align(VerticalAlign::Bottom)),
1954 ]);
1955
1956 assert_eq!(
1957 plain(&table_style.compose(&table)),
1958 "\
1959┌───┬─────┐
1960│ │ one │
1961│ a │ two │
1962└───┴─────┘"
1963 );
1964 }
1965
1966 #[test]
1967 fn padding_widens_every_cell() {
1968 let table_style = styles().table().clone().padding(0);
1969
1970 assert_eq!(
1971 plain(&table_style.compose(&sample())),
1972 "\
1973┌────┬────────┐
1974│Name│Location│
1975├────┼────────┤
1976│Kini│New York│
1977│Iris│Paris │
1978└────┴────────┘"
1979 );
1980 }
1981
1982 #[test]
1983 fn table_data_is_inspectable_without_a_style() {
1984 let table = sample().hidden(true).offset(1, 0);
1985
1986 assert_eq!(table.column_count(), 2);
1987 assert!(table.is_hidden());
1988
1989 let ragged = Table::text().headers(["A"]).row(["1", "2", "3"]);
1990 assert_eq!(ragged.column_count(), 3);
1991 }
1992
1993 #[test]
1994 fn the_typed_styler_sees_each_body_cell_its_row_and_its_text() {
1995 let rows = TableRowPresentation::<TextTableRow>::display().cell_style(|_, cell, _| {
1996 let expected = match cell.column() {
1997 0 => "Kini",
1998 _ => "New York",
1999 };
2000 assert_eq!(cell.text(), expected, "column {}", cell.column());
2001 assert_eq!(cell.row(), 0);
2002 None
2003 });
2004
2005 let table = Table::text()
2006 .headers(["Name", "Location"])
2007 .row(["Kini", "New York"]);
2008 let _ = styles().table().compose_with(&table, &rows);
2009 }
2010
2011 #[test]
2012 fn role_setters_replace_the_styles_they_name() {
2013 let underline = BlockStyle::new().underlined();
2014 let table_style = styles()
2015 .table()
2016 .clone()
2017 .header_style(underline.clone())
2018 .cell_style(underline.clone())
2019 .border_style(TextStyle::new().underlined());
2020
2021 for role in [TableRole::Header, TableRole::Cell] {
2022 assert_eq!(table_style.get_style(role), &underline);
2023 }
2024 assert_eq!(
2025 table_style.get_border_style(),
2026 &TextStyle::new().underlined()
2027 );
2028 }
2029
2030 #[test]
2031 fn border_drawing_preserves_only_the_legacy_color_properties() {
2032 let presentation = styles().table().clone().border_style(
2033 TextStyle::new()
2034 .foreground(Color::RED)
2035 .background(Color::BLUE)
2036 .bold()
2037 .underlined(),
2038 );
2039 let view = presentation.compose(&Table::text().row(["cell"]));
2040
2041 assert_eq!(
2042 style_at(&view, 0, 0),
2043 TextStyle::new()
2044 .foreground(Color::RED)
2045 .background(Color::BLUE)
2046 );
2047 }
2048
2049 fn wide() -> TextTable {
2051 Table::text()
2052 .headers(["N", "Description"])
2053 .row(["a", "the quick brown fox jumps again!!"])
2054 .row(["b", "over the lazy dog"])
2055 }
2056
2057 #[test]
2058 fn an_area_narrower_than_the_table_shrinks_its_columns_and_its_lines() {
2059 let view = styles().table().compose(&wide());
2060 assert_eq!(measure(&view).width(), 41, "the natural width");
2061
2062 let rows: Vec<String> = resolve(&view, Available::columns(18))
2063 .unwrap()
2064 .rows()
2065 .iter()
2066 .map(|row| row.iter().map(StyledGrapheme::symbol).collect())
2067 .collect();
2068
2069 for row in &rows {
2070 assert_eq!(
2071 PrintableText::new(row).width(),
2072 18,
2073 "every row fits the area: {row}"
2074 );
2075 }
2076 let boundaries = |row: &str| -> Vec<usize> {
2079 PrintableText::new(row)
2080 .as_str()
2081 .chars()
2082 .enumerate()
2083 .filter(|(_, glyph)| "│┼┬┴├┤┌┐└┘".contains(*glyph))
2084 .map(|(index, _)| index)
2085 .collect()
2086 };
2087 let expected = boundaries(&rows[0]);
2088 for row in &rows {
2089 assert_eq!(boundaries(row), expected, "columns line up: {row}");
2090 }
2091 }
2092
2093 #[test]
2094 fn an_area_wider_than_the_table_does_not_widen_it() {
2095 let view = styles().table().compose(&wide());
2096
2097 let rows: Vec<String> = resolve(&view, Available::columns(200))
2098 .unwrap()
2099 .rows()
2100 .iter()
2101 .map(|row| row.iter().map(StyledGrapheme::symbol).collect())
2102 .collect();
2103
2104 for row in &rows {
2105 assert_eq!(
2106 PrintableText::new(row).width(),
2107 41,
2108 "an area only caps a table; it never pads one out: {row}"
2109 );
2110 }
2111 }
2112
2113 #[test]
2114 fn table_presentations_compare_by_complete_style_lists() {
2115 let table_style = styles().table().clone();
2116 assert_eq!(table_style, styles().table().clone());
2117 assert_ne!(
2118 table_style.clone(),
2119 table_style
2120 .clone()
2121 .column_styles([None, Some(BlockStyle::new().bold())])
2122 );
2123 assert_ne!(table_style.clone(), table_style.border(TableBorder::ASCII));
2124 }
2125
2126 #[test]
2127 fn canonical_composition_binds_an_intrinsic_canvas_item() {
2128 let view = styles().table().compose(&sample());
2129 let View::Block(style, _, child) = view else {
2130 panic!("a Table is boxed only to state its outer sizing")
2131 };
2132 assert_eq!(style.get_border(), None);
2133 assert!(matches!(*child, View::Canvas(_)));
2134 }
2135
2136 #[test]
2137 fn narrow_height_measurement_matches_drawing_and_shorter_height_crops() {
2138 let view = styles().table().compose(&wide());
2139 let complete = resolve(&view, Available::columns(18)).unwrap();
2140 let exact = resolve(&view, Available::size(18, complete.size().height()));
2141 assert_eq!(
2142 exact.unwrap(),
2143 complete,
2144 "measurement and drawing use one bound frame"
2145 );
2146
2147 let cropped = resolve(&view, Available::size(18, 3)).unwrap();
2148 assert_eq!(cropped.size().height(), 3);
2149 assert_eq!(cropped.rows(), &complete.rows()[..3]);
2150 }
2151
2152 #[test]
2153 fn table_cell_height_matches_the_shared_block_text_model() {
2154 let cases = [
2155 (
2156 TableFrameCell {
2157 text: "words that wrap\n日本語".to_owned(),
2158 style_override: None,
2159 },
2160 BlockStyle::new().padding((2, 1)),
2161 ),
2162 (
2163 TableFrameCell {
2164 text: "words that clip\n日本語".to_owned(),
2165 style_override: None,
2166 },
2167 BlockStyle::new()
2168 .padding((1, 2))
2169 .overflow(Overflow::ellipsis()),
2170 ),
2171 ];
2172
2173 for (cell, style) in cases {
2174 let floor = cell.width_requirements(&style).1;
2175 for width in [floor, floor.saturating_add(3), floor.saturating_add(12)] {
2176 assert_eq!(
2177 cell.height_at(width, &style),
2178 resolve(&cell.view(&style), Available::columns(width))
2179 .unwrap()
2180 .size()
2181 .height(),
2182 "{cell:?} at width {width}"
2183 );
2184 }
2185 }
2186 }
2187
2188 #[test]
2189 fn an_outer_borderless_block_can_state_fixed_or_fill_table_size() {
2190 let table = styles().table().compose(&sample());
2191 let fixed = View::block(BlockStyle::new().width(30), table.clone());
2192 assert_eq!(resolve(&fixed, Available::NONE).unwrap().size().width(), 30);
2193
2194 let fill = View::block(BlockStyle::new().width(Length::fill(1)), table);
2195 assert_eq!(
2196 resolve(&fill, Available::columns(32))
2197 .unwrap()
2198 .size()
2199 .width(),
2200 32
2201 );
2202 }
2203
2204 #[test]
2205 fn every_border_preset_keeps_its_observable_glyphs() {
2206 let table = Table::text().headers(["A", "B"]).row(["1", "2"]);
2207 let cases = [
2208 (TableBorder::NORMAL, '┌', '┬', '┼', '└'),
2209 (TableBorder::ROUNDED, '╭', '┬', '┼', '╰'),
2210 (TableBorder::THICK, '┏', '┳', '╋', '┗'),
2211 (TableBorder::DOUBLE, '╔', '╦', '╬', '╚'),
2212 (TableBorder::ASCII, '+', '+', '+', '+'),
2213 (TableBorder::MARKDOWN, '|', '|', '|', '|'),
2214 (TableBorder::BOOKTABS, '━', '━', '─', '━'),
2215 (TableBorder::HIDDEN, ' ', ' ', ' ', ' '),
2216 ];
2217
2218 for (border, top_left, top_middle, middle, bottom_left) in cases {
2219 let rows = plain_rows(&styles().table().clone().border(border).compose(&table));
2220 assert_eq!(rows[0].chars().next(), Some(top_left));
2221 assert!(rows[0].contains(top_middle));
2222 assert!(rows[2].contains(middle));
2223 assert_eq!(rows[4].chars().next(), Some(bottom_left));
2224 }
2225 }
2226
2227 #[test]
2228 fn caller_owned_line_glyphs_derive_every_table_junction() {
2229 let glyphs = LineGlyphs {
2230 isolated: 'o',
2231 end_up: 'u',
2232 end_right: 'r',
2233 end_down: 'd',
2234 end_left: 'l',
2235 vertical: 'v',
2236 horizontal: 'h',
2237 corner_down_right: 'A',
2238 corner_down_left: 'B',
2239 corner_up_right: 'C',
2240 corner_up_left: 'D',
2241 tee_right: 'E',
2242 tee_down: 'H',
2243 tee_left: 'F',
2244 tee_up: 'I',
2245 cross: 'G',
2246 };
2247 let table = Table::text()
2248 .headers(["a", "b"])
2249 .row(["1", "2"])
2250 .row(["3", "4"]);
2251 let view = styles()
2252 .table()
2253 .clone()
2254 .border(TableBorder::network(glyphs))
2255 .border_row(true)
2256 .compose(&table);
2257
2258 assert_eq!(
2259 plain(&view),
2260 [
2261 "AhhhHhhhB",
2262 "v a v b v",
2263 "EhhhGhhhF",
2264 "v 1 v 2 v",
2265 "EhhhGhhhF",
2266 "v 3 v 4 v",
2267 "ChhhIhhhD",
2268 ]
2269 .join("\n")
2270 );
2271 }
2272
2273 #[test]
2274 fn theme_table_is_the_canonical_presentation_shortcut() {
2275 let tokens = SemanticTokens {
2276 text: Color::WHITE,
2277 text_muted: Color::BRIGHT_BLACK,
2278 background: Color::BLACK,
2279 surface: Color::BLACK,
2280 accent: Color::CYAN,
2281 accent_text: Color::BLACK,
2282 success: Color::GREEN,
2283 warning: Color::YELLOW,
2284 error: Color::RED,
2285 border: Color::BRIGHT_BLACK,
2286 };
2287 let theme = crate::Theme::from_tokens(tokens);
2288 assert_eq!(
2289 theme.table(&sample()),
2290 theme.components().table().compose(&sample())
2291 );
2292 }
2293}