1use alloc::vec;
5use alloc::vec::Vec;
6
7use itertools::Itertools;
8use ratatui_core::buffer::Buffer;
9use ratatui_core::layout::{Constraint, Flex, Layout, Rect};
10use ratatui_core::style::{Style, Styled};
11use ratatui_core::text::Text;
12use ratatui_core::widgets::{StatefulWidget, Widget};
13
14pub use self::cell::Cell;
15pub use self::highlight_spacing::HighlightSpacing;
16pub use self::row::Row;
17pub use self::state::TableState;
18use crate::block::{Block, BlockExt};
19
20mod cell;
21mod highlight_spacing;
22mod row;
23mod state;
24
25#[derive(Debug, Clone, Eq, PartialEq, Hash)]
233pub struct Table<'a> {
234 rows: Vec<Row<'a>>,
236
237 header: Option<Row<'a>>,
239
240 footer: Option<Row<'a>>,
242
243 widths: Vec<Constraint>,
245
246 column_spacing: u16,
248
249 block: Option<Block<'a>>,
251
252 style: Style,
254
255 row_highlight_style: Style,
257
258 column_highlight_style: Style,
260
261 cell_highlight_style: Style,
263
264 highlight_symbol: Text<'a>,
266
267 highlight_spacing: HighlightSpacing,
269
270 flex: Flex,
272}
273
274impl Default for Table<'_> {
275 fn default() -> Self {
276 Self {
277 rows: Vec::new(),
278 header: None,
279 footer: None,
280 widths: Vec::new(),
281 column_spacing: 1,
282 block: None,
283 style: Style::new(),
284 row_highlight_style: Style::new(),
285 column_highlight_style: Style::new(),
286 cell_highlight_style: Style::new(),
287 highlight_symbol: Text::default(),
288 highlight_spacing: HighlightSpacing::default(),
289 flex: Flex::Start,
290 }
291 }
292}
293
294impl<'a> Table<'a> {
295 pub fn new<R, C>(rows: R, widths: C) -> Self
319 where
320 R: IntoIterator,
321 R::Item: Into<Row<'a>>,
322 C: IntoIterator,
323 C::Item: Into<Constraint>,
324 {
325 let widths = widths.into_iter().map(Into::into).collect_vec();
326 ensure_percentages_less_than_100(&widths);
327
328 let rows = rows.into_iter().map(Into::into).collect();
329 Self {
330 rows,
331 widths,
332 ..Default::default()
333 }
334 }
335
336 #[must_use = "method moves the value of self and returns the modified value"]
360 pub fn rows<T>(mut self, rows: T) -> Self
361 where
362 T: IntoIterator<Item = Row<'a>>,
363 {
364 self.rows = rows.into_iter().collect();
365 self
366 }
367
368 #[must_use = "method moves the value of self and returns the modified value"]
386 pub fn header(mut self, header: Row<'a>) -> Self {
387 self.header = Some(header);
388 self
389 }
390
391 #[must_use = "method moves the value of self and returns the modified value"]
409 pub fn footer(mut self, footer: Row<'a>) -> Self {
410 self.footer = Some(footer);
411 self
412 }
413
414 #[must_use = "method moves the value of self and returns the modified value"]
439 pub fn widths<I>(mut self, widths: I) -> Self
440 where
441 I: IntoIterator,
442 I::Item: Into<Constraint>,
443 {
444 let widths = widths.into_iter().map(Into::into).collect_vec();
445 ensure_percentages_less_than_100(&widths);
446 self.widths = widths;
447 self
448 }
449
450 #[must_use = "method moves the value of self and returns the modified value"]
465 pub const fn column_spacing(mut self, spacing: u16) -> Self {
466 self.column_spacing = spacing;
467 self
468 }
469
470 #[must_use = "method moves the value of self and returns the modified value"]
489 pub fn block(mut self, block: Block<'a>) -> Self {
490 self.block = Some(block);
491 self
492 }
493
494 #[must_use = "method moves the value of self and returns the modified value"]
532 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
533 self.style = style.into();
534 self
535 }
536
537 #[must_use = "method moves the value of self and returns the modified value"]
561 #[deprecated(note = "use `row_highlight_style()` instead")]
562 pub fn highlight_style<S: Into<Style>>(self, highlight_style: S) -> Self {
563 self.row_highlight_style(highlight_style)
564 }
565
566 #[must_use = "method moves the value of self and returns the modified value"]
586 pub fn row_highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
587 self.row_highlight_style = highlight_style.into();
588 self
589 }
590
591 #[must_use = "method moves the value of self and returns the modified value"]
611 pub fn column_highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
612 self.column_highlight_style = highlight_style.into();
613 self
614 }
615
616 #[must_use = "method moves the value of self and returns the modified value"]
636 pub fn cell_highlight_style<S: Into<Style>>(mut self, highlight_style: S) -> Self {
637 self.cell_highlight_style = highlight_style.into();
638 self
639 }
640
641 #[must_use = "method moves the value of self and returns the modified value"]
656 pub fn highlight_symbol<T: Into<Text<'a>>>(mut self, highlight_symbol: T) -> Self {
657 self.highlight_symbol = highlight_symbol.into();
658 self
659 }
660
661 #[must_use = "method moves the value of self and returns the modified value"]
690 pub const fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
691 self.highlight_spacing = value;
692 self
693 }
694
695 #[must_use = "method moves the value of self and returns the modified value"]
719 pub const fn flex(mut self, flex: Flex) -> Self {
720 self.flex = flex;
721 self
722 }
723}
724
725impl Widget for Table<'_> {
726 fn render(self, area: Rect, buf: &mut Buffer) {
727 Widget::render(&self, area, buf);
728 }
729}
730
731impl Widget for &Table<'_> {
732 fn render(self, area: Rect, buf: &mut Buffer) {
733 let mut state = TableState::default();
734 StatefulWidget::render(self, area, buf, &mut state);
735 }
736}
737
738impl StatefulWidget for Table<'_> {
739 type State = TableState;
740
741 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
742 StatefulWidget::render(&self, area, buf, state);
743 }
744}
745
746impl StatefulWidget for &Table<'_> {
747 type State = TableState;
748
749 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
750 buf.set_style(area, self.style);
751 self.block.as_ref().render(area, buf);
752 let table_area = self.block.inner_if_some(area);
753 if table_area.is_empty() {
754 return;
755 }
756
757 if state.selected.is_some_and(|s| s >= self.rows.len()) {
758 state.select(Some(self.rows.len().saturating_sub(1)));
759 }
760
761 if self.rows.is_empty() {
762 state.select(None);
763 }
764
765 let column_count = self.column_count();
766 if state.selected_column.is_some_and(|s| s >= column_count) {
767 state.select_column(Some(column_count.saturating_sub(1)));
768 }
769 if column_count == 0 {
770 state.select_column(None);
771 }
772
773 let selection_width = self.selection_width(state);
774 let column_widths = self.get_column_widths(table_area.width, selection_width, column_count);
775 let (header_area, rows_area, footer_area) = self.layout(table_area);
776
777 self.render_header(header_area, buf, &column_widths);
778
779 self.render_rows(rows_area, buf, state, selection_width, &column_widths);
780
781 self.render_footer(footer_area, buf, &column_widths);
782 }
783}
784
785impl Table<'_> {
787 fn layout(&self, area: Rect) -> (Rect, Rect, Rect) {
789 let header_top_margin = self.header.as_ref().map_or(0, |h| h.top_margin);
790 let header_height = self.header.as_ref().map_or(0, |h| h.height);
791 let header_bottom_margin = self.header.as_ref().map_or(0, |h| h.bottom_margin);
792 let footer_top_margin = self.footer.as_ref().map_or(0, |h| h.top_margin);
793 let footer_height = self.footer.as_ref().map_or(0, |f| f.height);
794 let footer_bottom_margin = self.footer.as_ref().map_or(0, |h| h.bottom_margin);
795 let layout = Layout::vertical([
796 Constraint::Length(header_top_margin),
797 Constraint::Length(header_height),
798 Constraint::Length(header_bottom_margin),
799 Constraint::Min(0),
800 Constraint::Length(footer_top_margin),
801 Constraint::Length(footer_height),
802 Constraint::Length(footer_bottom_margin),
803 ])
804 .split(area);
805 let (header_area, rows_area, footer_area) = (layout[1], layout[3], layout[5]);
806 (header_area, rows_area, footer_area)
807 }
808
809 fn render_header(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) {
810 if let Some(ref header) = self.header {
811 buf.set_style(area, header.style);
812 for ((x, width), cell) in column_widths.iter().zip(header.cells.iter()) {
813 cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf);
814 }
815 }
816 }
817
818 fn render_footer(&self, area: Rect, buf: &mut Buffer, column_widths: &[(u16, u16)]) {
819 if let Some(ref footer) = self.footer {
820 buf.set_style(area, footer.style);
821 for ((x, width), cell) in column_widths.iter().zip(footer.cells.iter()) {
822 cell.render(Rect::new(area.x + x, area.y, *width, area.height), buf);
823 }
824 }
825 }
826
827 fn render_rows(
828 &self,
829 area: Rect,
830 buf: &mut Buffer,
831 state: &mut TableState,
832 selection_width: u16,
833 columns_widths: &[(u16, u16)],
834 ) {
835 if self.rows.is_empty() {
836 return;
837 }
838
839 let (start_index, end_index) = self.visible_rows(state, area);
840 state.offset = start_index;
841
842 let mut y_offset = 0;
843
844 let mut selected_row_area = None;
845 for (i, row) in self
846 .rows
847 .iter()
848 .enumerate()
849 .skip(start_index)
850 .take(end_index - start_index)
851 {
852 let y = area.y + y_offset + row.top_margin;
853 let height = (y + row.height).min(area.bottom()).saturating_sub(y);
854 let row_area = Rect { y, height, ..area };
855 buf.set_style(row_area, row.style);
856
857 let is_selected = state.selected.is_some_and(|index| index == i);
858 if selection_width > 0 && is_selected {
859 let selection_area = Rect {
860 width: selection_width,
861 ..row_area
862 };
863 buf.set_style(selection_area, row.style);
864 (&self.highlight_symbol).render(selection_area, buf);
865 }
866 for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
867 cell.render(
868 Rect::new(row_area.x + x, row_area.y, *width, row_area.height),
869 buf,
870 );
871 }
872 if is_selected {
873 selected_row_area = Some(row_area);
874 }
875 y_offset += row.height_with_margin();
876 }
877
878 let selected_column_area = state.selected_column.and_then(|s| {
879 columns_widths.get(s).map(|(x, width)| Rect {
882 x: x + area.x,
883 width: *width,
884 ..area
885 })
886 });
887
888 match (selected_row_area, selected_column_area) {
889 (Some(row_area), Some(col_area)) => {
890 buf.set_style(row_area, self.row_highlight_style);
891 buf.set_style(col_area, self.column_highlight_style);
892 let cell_area = row_area.intersection(col_area);
893 buf.set_style(cell_area, self.cell_highlight_style);
894 }
895 (Some(row_area), None) => {
896 buf.set_style(row_area, self.row_highlight_style);
897 }
898 (None, Some(col_area)) => {
899 buf.set_style(col_area, self.column_highlight_style);
900 }
901 (None, None) => (),
902 }
903 }
904
905 fn visible_rows(&self, state: &TableState, area: Rect) -> (usize, usize) {
914 let last_row = self.rows.len().saturating_sub(1);
915 let mut start = state.offset.min(last_row);
916
917 if let Some(selected) = state.selected {
918 start = start.min(selected);
919 }
920
921 let mut end = start;
922 let mut height = 0;
923
924 for item in self.rows.iter().skip(start) {
925 if height + item.height > area.height {
926 break;
927 }
928 height += item.height_with_margin();
929 end += 1;
930 }
931
932 if let Some(selected) = state.selected {
933 let selected = selected.min(last_row);
934
935 while selected >= end {
937 height = height.saturating_add(self.rows[end].height_with_margin());
938 end += 1;
939 while height > area.height {
940 height = height.saturating_sub(self.rows[start].height_with_margin());
941 start += 1;
942 }
943 }
944 }
945
946 if height < area.height && end < self.rows.len() {
948 end += 1;
949 }
950
951 (start, end)
952 }
953
954 fn get_column_widths(
959 &self,
960 max_width: u16,
961 selection_width: u16,
962 col_count: usize,
963 ) -> Vec<(u16, u16)> {
964 let widths = if self.widths.is_empty() {
965 vec![Constraint::Length(max_width / col_count.max(1) as u16); col_count]
967 } else {
968 self.widths.clone()
969 };
970 let [_selection_area, columns_area] =
972 Layout::horizontal([Constraint::Length(selection_width), Constraint::Fill(0)])
973 .areas(Rect::new(0, 0, max_width, 1));
974 let rects = Layout::horizontal(widths)
975 .flex(self.flex)
976 .spacing(self.column_spacing)
977 .split(columns_area);
978 rects.iter().map(|c| (c.x, c.width)).collect()
979 }
980
981 fn column_count(&self) -> usize {
982 self.rows
983 .iter()
984 .chain(self.footer.iter())
985 .chain(self.header.iter())
986 .map(|r| r.cells.len())
987 .max()
988 .unwrap_or_default()
989 }
990
991 fn selection_width(&self, state: &TableState) -> u16 {
994 let has_selection = state.selected.is_some();
995 if self.highlight_spacing.should_add(has_selection) {
996 self.highlight_symbol.width() as u16
997 } else {
998 0
999 }
1000 }
1001}
1002
1003fn ensure_percentages_less_than_100(widths: &[Constraint]) {
1004 for w in widths {
1005 if let Constraint::Percentage(p) = w {
1006 assert!(
1007 *p <= 100,
1008 "Percentages should be between 0 and 100 inclusively."
1009 );
1010 }
1011 }
1012}
1013
1014impl Styled for Table<'_> {
1015 type Item = Self;
1016
1017 fn style(&self) -> Style {
1018 self.style
1019 }
1020
1021 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
1022 self.style(style)
1023 }
1024}
1025
1026impl<'a, Item> FromIterator<Item> for Table<'a>
1027where
1028 Item: Into<Row<'a>>,
1029{
1030 fn from_iter<Iter: IntoIterator<Item = Item>>(rows: Iter) -> Self {
1035 let widths: [Constraint; 0] = [];
1036 Self::new(rows, widths)
1037 }
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use alloc::string::ToString;
1043 use alloc::{format, vec};
1044
1045 use ratatui_core::layout::Constraint::*;
1046 use ratatui_core::style::{Color, Modifier, Style, Stylize};
1047 use ratatui_core::text::Line;
1048 use rstest::{fixture, rstest};
1049
1050 use super::*;
1051 use crate::table::Cell;
1052
1053 #[test]
1054 fn new() {
1055 let rows = [Row::new(vec![Cell::from("")])];
1056 let widths = [Constraint::Percentage(100)];
1057 let table = Table::new(rows.clone(), widths);
1058 assert_eq!(table.rows, rows);
1059 assert_eq!(table.header, None);
1060 assert_eq!(table.footer, None);
1061 assert_eq!(table.widths, widths);
1062 assert_eq!(table.column_spacing, 1);
1063 assert_eq!(table.block, None);
1064 assert_eq!(table.style, Style::default());
1065 assert_eq!(table.row_highlight_style, Style::default());
1066 assert_eq!(table.highlight_symbol, Text::default());
1067 assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
1068 assert_eq!(table.flex, Flex::Start);
1069 }
1070
1071 #[test]
1072 fn default() {
1073 let table = Table::default();
1074 assert_eq!(table.rows, []);
1075 assert_eq!(table.header, None);
1076 assert_eq!(table.footer, None);
1077 assert_eq!(table.widths, []);
1078 assert_eq!(table.column_spacing, 1);
1079 assert_eq!(table.block, None);
1080 assert_eq!(table.style, Style::default());
1081 assert_eq!(table.row_highlight_style, Style::default());
1082 assert_eq!(table.highlight_symbol, Text::default());
1083 assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
1084 assert_eq!(table.flex, Flex::Start);
1085 }
1086
1087 #[test]
1088 fn collect() {
1089 let table = (0..4)
1090 .map(|i| -> Row { (0..4).map(|j| format!("{i}*{j} = {}", i * j)).collect() })
1091 .collect::<Table>()
1092 .widths([Constraint::Percentage(25); 4]);
1093
1094 let expected_rows: Vec<Row> = vec![
1095 Row::new(["0*0 = 0", "0*1 = 0", "0*2 = 0", "0*3 = 0"]),
1096 Row::new(["1*0 = 0", "1*1 = 1", "1*2 = 2", "1*3 = 3"]),
1097 Row::new(["2*0 = 0", "2*1 = 2", "2*2 = 4", "2*3 = 6"]),
1098 Row::new(["3*0 = 0", "3*1 = 3", "3*2 = 6", "3*3 = 9"]),
1099 ];
1100
1101 assert_eq!(table.rows, expected_rows);
1102 assert_eq!(table.widths, [Constraint::Percentage(25); 4]);
1103 }
1104
1105 #[test]
1106 fn widths() {
1107 let table = Table::default().widths([Constraint::Length(100)]);
1108 assert_eq!(table.widths, [Constraint::Length(100)]);
1109
1110 #[expect(clippy::needless_borrows_for_generic_args)]
1113 let table = Table::default().widths(&[Constraint::Length(100)]);
1114 assert_eq!(table.widths, [Constraint::Length(100)]);
1115
1116 let table = Table::default().widths(vec![Constraint::Length(100)]);
1117 assert_eq!(table.widths, [Constraint::Length(100)]);
1118
1119 #[expect(clippy::needless_borrows_for_generic_args)]
1122 let table = Table::default().widths(&vec![Constraint::Length(100)]);
1123 assert_eq!(table.widths, [Constraint::Length(100)]);
1124
1125 let table = Table::default().widths([100].into_iter().map(Constraint::Length));
1126 assert_eq!(table.widths, [Constraint::Length(100)]);
1127 }
1128
1129 #[test]
1130 fn rows() {
1131 let rows = [Row::new(vec![Cell::from("")])];
1132 let table = Table::default().rows(rows.clone());
1133 assert_eq!(table.rows, rows);
1134 }
1135
1136 #[test]
1137 fn column_spacing() {
1138 let table = Table::default().column_spacing(2);
1139 assert_eq!(table.column_spacing, 2);
1140 }
1141
1142 #[test]
1143 fn block() {
1144 let block = Block::bordered().title("Table");
1145 let table = Table::default().block(block.clone());
1146 assert_eq!(table.block, Some(block));
1147 }
1148
1149 #[test]
1150 fn header() {
1151 let header = Row::new(vec![Cell::from("")]);
1152 let table = Table::default().header(header.clone());
1153 assert_eq!(table.header, Some(header));
1154 }
1155
1156 #[test]
1157 fn footer() {
1158 let footer = Row::new(vec![Cell::from("")]);
1159 let table = Table::default().footer(footer.clone());
1160 assert_eq!(table.footer, Some(footer));
1161 }
1162
1163 #[test]
1164 #[expect(deprecated)]
1165 fn highlight_style() {
1166 let style = Style::default().red().italic();
1167 let table = Table::default().highlight_style(style);
1168 assert_eq!(table.row_highlight_style, style);
1169 }
1170
1171 #[test]
1172 fn row_highlight_style() {
1173 let style = Style::default().red().italic();
1174 let table = Table::default().row_highlight_style(style);
1175 assert_eq!(table.row_highlight_style, style);
1176 }
1177
1178 #[test]
1179 fn column_highlight_style() {
1180 let style = Style::default().red().italic();
1181 let table = Table::default().column_highlight_style(style);
1182 assert_eq!(table.column_highlight_style, style);
1183 }
1184
1185 #[test]
1186 fn cell_highlight_style() {
1187 let style = Style::default().red().italic();
1188 let table = Table::default().cell_highlight_style(style);
1189 assert_eq!(table.cell_highlight_style, style);
1190 }
1191
1192 #[test]
1193 fn highlight_symbol() {
1194 let table = Table::default().highlight_symbol(">>");
1195 assert_eq!(table.highlight_symbol, Text::from(">>"));
1196 }
1197
1198 #[test]
1199 fn highlight_spacing() {
1200 let table = Table::default().highlight_spacing(HighlightSpacing::Always);
1201 assert_eq!(table.highlight_spacing, HighlightSpacing::Always);
1202 }
1203
1204 #[test]
1205 #[should_panic = "Percentages should be between 0 and 100 inclusively"]
1206 fn table_invalid_percentages() {
1207 let _ = Table::default().widths([Constraint::Percentage(110)]);
1208 }
1209
1210 #[test]
1211 fn widths_conversions() {
1212 let array = [Constraint::Percentage(100)];
1213 let table = Table::new(Vec::<Row>::new(), array);
1214 assert_eq!(table.widths, [Constraint::Percentage(100)], "array");
1215
1216 let array_ref = &[Constraint::Percentage(100)];
1217 let table = Table::new(Vec::<Row>::new(), array_ref);
1218 assert_eq!(table.widths, [Constraint::Percentage(100)], "array ref");
1219
1220 let vec = vec![Constraint::Percentage(100)];
1221 let slice = vec.as_slice();
1222 let table = Table::new(Vec::<Row>::new(), slice);
1223 assert_eq!(table.widths, [Constraint::Percentage(100)], "slice");
1224
1225 let vec = vec![Constraint::Percentage(100)];
1226 let table = Table::new(Vec::<Row>::new(), vec);
1227 assert_eq!(table.widths, [Constraint::Percentage(100)], "vec");
1228
1229 let vec_ref = &vec![Constraint::Percentage(100)];
1230 let table = Table::new(Vec::<Row>::new(), vec_ref);
1231 assert_eq!(table.widths, [Constraint::Percentage(100)], "vec ref");
1232 }
1233
1234 #[cfg(test)]
1235 mod state {
1236 use ratatui_core::buffer::Buffer;
1237 use ratatui_core::layout::{Constraint, Rect};
1238 use ratatui_core::widgets::StatefulWidget;
1239
1240 use super::*;
1241 use crate::table::{Row, Table, TableState};
1242
1243 #[fixture]
1244 fn table_buf() -> Buffer {
1245 Buffer::empty(Rect::new(0, 0, 10, 10))
1246 }
1247
1248 #[rstest]
1249 fn test_list_state_empty_list(mut table_buf: Buffer) {
1250 let mut state = TableState::default();
1251
1252 let rows: Vec<Row> = Vec::new();
1253 let widths = vec![Constraint::Percentage(100)];
1254 let table = Table::new(rows, widths);
1255 state.select_first();
1256 StatefulWidget::render(table, table_buf.area, &mut table_buf, &mut state);
1257 assert_eq!(state.selected, None);
1258 assert_eq!(state.selected_column, None);
1259 }
1260
1261 #[rstest]
1262 fn test_list_state_single_item(mut table_buf: Buffer) {
1263 let mut state = TableState::default();
1264
1265 let widths = vec![Constraint::Percentage(100)];
1266
1267 let items = vec![Row::new(vec!["Item 1"])];
1268 let table = Table::new(items, widths);
1269 state.select_first();
1270 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1271 assert_eq!(state.selected, Some(0));
1272 assert_eq!(state.selected_column, None);
1273
1274 state.select_last();
1275 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1276 assert_eq!(state.selected, Some(0));
1277 assert_eq!(state.selected_column, None);
1278
1279 state.select_previous();
1280 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1281 assert_eq!(state.selected, Some(0));
1282 assert_eq!(state.selected_column, None);
1283
1284 state.select_next();
1285 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1286 assert_eq!(state.selected, Some(0));
1287 assert_eq!(state.selected_column, None);
1288
1289 let mut state = TableState::default();
1290
1291 state.select_first_column();
1292 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1293 assert_eq!(state.selected_column, Some(0));
1294 assert_eq!(state.selected, None);
1295
1296 state.select_last_column();
1297 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1298 assert_eq!(state.selected_column, Some(0));
1299 assert_eq!(state.selected, None);
1300
1301 state.select_previous_column();
1302 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1303 assert_eq!(state.selected_column, Some(0));
1304 assert_eq!(state.selected, None);
1305
1306 state.select_next_column();
1307 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1308 assert_eq!(state.selected_column, Some(0));
1309 assert_eq!(state.selected, None);
1310 }
1311 }
1312
1313 #[cfg(test)]
1314 mod render {
1315 use ratatui_core::layout::Alignment;
1316
1317 use super::*;
1318
1319 #[test]
1320 fn render_empty_area() {
1321 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1322 let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
1323 let table = Table::new(rows, vec![Constraint::Length(5); 2]);
1324 Widget::render(table, Rect::new(0, 0, 0, 0), &mut buf);
1325 assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 15, 3)));
1326 }
1327
1328 #[test]
1329 fn render_default() {
1330 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1331 let table = Table::default();
1332 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1333 assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 15, 3)));
1334 }
1335
1336 #[test]
1337 fn render_with_block() {
1338 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1339 let rows = vec![
1340 Row::new(vec!["Cell1", "Cell2"]),
1341 Row::new(vec!["Cell3", "Cell4"]),
1342 ];
1343 let block = Block::bordered().title("Block");
1344 let table = Table::new(rows, vec![Constraint::Length(5); 2]).block(block);
1345 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1346 #[rustfmt::skip]
1347 let expected = Buffer::with_lines([
1348 "┌Block────────┐",
1349 "│Cell1 Cell2 │",
1350 "└─────────────┘",
1351 ]);
1352 assert_eq!(buf, expected);
1353 }
1354
1355 #[test]
1356 fn render_with_header() {
1357 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1358 let header = Row::new(vec!["Head1", "Head2"]);
1359 let rows = vec![
1360 Row::new(vec!["Cell1", "Cell2"]),
1361 Row::new(vec!["Cell3", "Cell4"]),
1362 ];
1363 let table = Table::new(rows, [Constraint::Length(5); 2]).header(header);
1364 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1365 #[rustfmt::skip]
1366 let expected = Buffer::with_lines([
1367 "Head1 Head2 ",
1368 "Cell1 Cell2 ",
1369 "Cell3 Cell4 ",
1370 ]);
1371 assert_eq!(buf, expected);
1372 }
1373
1374 #[test]
1375 fn render_with_footer() {
1376 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1377 let footer = Row::new(vec!["Foot1", "Foot2"]);
1378 let rows = vec![
1379 Row::new(vec!["Cell1", "Cell2"]),
1380 Row::new(vec!["Cell3", "Cell4"]),
1381 ];
1382 let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
1383 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1384 #[rustfmt::skip]
1385 let expected = Buffer::with_lines([
1386 "Cell1 Cell2 ",
1387 "Cell3 Cell4 ",
1388 "Foot1 Foot2 ",
1389 ]);
1390 assert_eq!(buf, expected);
1391 }
1392
1393 #[test]
1394 fn render_with_header_and_footer() {
1395 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1396 let header = Row::new(vec!["Head1", "Head2"]);
1397 let footer = Row::new(vec!["Foot1", "Foot2"]);
1398 let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
1399 let table = Table::new(rows, [Constraint::Length(5); 2])
1400 .header(header)
1401 .footer(footer);
1402 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1403 #[rustfmt::skip]
1404 let expected = Buffer::with_lines([
1405 "Head1 Head2 ",
1406 "Cell1 Cell2 ",
1407 "Foot1 Foot2 ",
1408 ]);
1409 assert_eq!(buf, expected);
1410 }
1411
1412 #[test]
1413 fn render_with_header_margin() {
1414 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1415 let header = Row::new(vec!["Head1", "Head2"]).bottom_margin(1);
1416 let rows = vec![
1417 Row::new(vec!["Cell1", "Cell2"]),
1418 Row::new(vec!["Cell3", "Cell4"]),
1419 ];
1420 let table = Table::new(rows, [Constraint::Length(5); 2]).header(header);
1421 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1422 #[rustfmt::skip]
1423 let expected = Buffer::with_lines([
1424 "Head1 Head2 ",
1425 " ",
1426 "Cell1 Cell2 ",
1427 ]);
1428 assert_eq!(buf, expected);
1429 }
1430
1431 #[test]
1432 fn render_with_footer_margin() {
1433 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1434 let footer = Row::new(vec!["Foot1", "Foot2"]).top_margin(1);
1435 let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
1436 let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
1437 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1438 #[rustfmt::skip]
1439 let expected = Buffer::with_lines([
1440 "Cell1 Cell2 ",
1441 " ",
1442 "Foot1 Foot2 ",
1443 ]);
1444 assert_eq!(buf, expected);
1445 }
1446
1447 #[test]
1448 fn render_with_row_margin() {
1449 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1450 let rows = vec![
1451 Row::new(vec!["Cell1", "Cell2"]).bottom_margin(1),
1452 Row::new(vec!["Cell3", "Cell4"]),
1453 ];
1454 let table = Table::new(rows, [Constraint::Length(5); 2]);
1455 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1456 #[rustfmt::skip]
1457 let expected = Buffer::with_lines([
1458 "Cell1 Cell2 ",
1459 " ",
1460 "Cell3 Cell4 ",
1461 ]);
1462 assert_eq!(buf, expected);
1463 }
1464
1465 #[test]
1466 fn render_with_tall_row() {
1467 let mut buf = Buffer::empty(Rect::new(0, 0, 23, 3));
1468 let rows = vec![
1469 Row::new(vec!["Cell1", "Cell2"]),
1470 Row::new(vec![
1471 Text::raw("Cell3-Line1\nCell3-Line2\nCell3-Line3"),
1472 Text::raw("Cell4-Line1\nCell4-Line2\nCell4-Line3"),
1473 ])
1474 .height(3),
1475 ];
1476 let table = Table::new(rows, [Constraint::Length(11); 2]);
1477 Widget::render(table, Rect::new(0, 0, 23, 3), &mut buf);
1478 #[rustfmt::skip]
1479 let expected = Buffer::with_lines([
1480 "Cell1 Cell2 ",
1481 "Cell3-Line1 Cell4-Line1",
1482 "Cell3-Line2 Cell4-Line2",
1483 ]);
1484 assert_eq!(buf, expected);
1485 }
1486
1487 #[test]
1488 fn render_with_alignment() {
1489 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 3));
1490 let rows = vec![
1491 Row::new(vec![Line::from("Left").alignment(Alignment::Left)]),
1492 Row::new(vec![Line::from("Center").alignment(Alignment::Center)]),
1493 Row::new(vec![Line::from("Right").alignment(Alignment::Right)]),
1494 ];
1495 let table = Table::new(rows, [Percentage(100)]);
1496 Widget::render(table, Rect::new(0, 0, 10, 3), &mut buf);
1497 let expected = Buffer::with_lines(["Left ", " Center ", " Right"]);
1498 assert_eq!(buf, expected);
1499 }
1500
1501 #[test]
1502 fn render_with_overflow_does_not_panic() {
1503 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
1504 let table = Table::new(Vec::<Row>::new(), [Constraint::Min(20); 1])
1505 .header(Row::new([Line::from("").alignment(Alignment::Right)]))
1506 .footer(Row::new([Line::from("").alignment(Alignment::Right)]));
1507 Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf);
1508 }
1509
1510 #[test]
1511 fn render_with_selected_column_and_incorrect_width_count_does_not_panic() {
1512 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
1513 let table = Table::new(
1514 vec![Row::new(vec!["Row1", "Row2", "Row3"])],
1515 [Constraint::Length(10); 1],
1516 );
1517 let mut state = TableState::new().with_selected_column(2);
1518 StatefulWidget::render(table, Rect::new(0, 0, 20, 3), &mut buf, &mut state);
1519 }
1520
1521 #[test]
1522 fn render_with_selected() {
1523 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1524 let rows = vec![
1525 Row::new(vec!["Cell1", "Cell2"]),
1526 Row::new(vec!["Cell3", "Cell4"]),
1527 ];
1528 let table = Table::new(rows, [Constraint::Length(5); 2])
1529 .row_highlight_style(Style::new().red())
1530 .highlight_symbol(">>");
1531 let mut state = TableState::new().with_selected(Some(0));
1532 StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
1533 let expected = Buffer::with_lines([
1534 ">>Cell1 Cell2 ".red(),
1535 " Cell3 Cell4 ".into(),
1536 " ".into(),
1537 ]);
1538 assert_eq!(buf, expected);
1539 }
1540
1541 #[test]
1542 fn render_with_selected_column() {
1543 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1544 let rows = vec![
1545 Row::new(vec!["Cell1", "Cell2"]),
1546 Row::new(vec!["Cell3", "Cell4"]),
1547 ];
1548 let table = Table::new(rows, [Constraint::Length(5); 2])
1549 .column_highlight_style(Style::new().blue())
1550 .highlight_symbol(">>");
1551 let mut state = TableState::new().with_selected_column(Some(1));
1552 StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
1553 let expected = Buffer::with_lines::<[Line; 3]>([
1554 Line::from(vec![
1555 "Cell1".into(),
1556 " ".into(),
1557 "Cell2".blue(),
1558 " ".into(),
1559 ]),
1560 Line::from(vec![
1561 "Cell3".into(),
1562 " ".into(),
1563 "Cell4".blue(),
1564 " ".into(),
1565 ]),
1566 Line::from(vec![" ".into(), " ".blue(), " ".into()]),
1567 ]);
1568 assert_eq!(buf, expected);
1569 }
1570
1571 #[test]
1572 fn render_with_selected_cell() {
1573 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
1574 let rows = vec![
1575 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
1576 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
1577 Row::new(vec!["Cell7", "Cell8", "Cell9"]),
1578 ];
1579 let table = Table::new(rows, [Constraint::Length(5); 3])
1580 .highlight_symbol(">>")
1581 .cell_highlight_style(Style::new().green());
1582 let mut state = TableState::new().with_selected_cell((1, 2));
1583 StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
1584 let expected = Buffer::with_lines::<[Line; 4]>([
1585 Line::from(vec![" Cell1 ".into(), "Cell2 ".into(), "Cell3".into()]),
1586 Line::from(vec![">>Cell4 Cell5 ".into(), "Cell6".green(), " ".into()]),
1587 Line::from(vec![" Cell7 ".into(), "Cell8 ".into(), "Cell9".into()]),
1588 Line::from(vec![" ".into()]),
1589 ]);
1590 assert_eq!(buf, expected);
1591 }
1592
1593 #[test]
1594 fn render_with_selected_row_and_column() {
1595 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
1596 let rows = vec![
1597 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
1598 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
1599 Row::new(vec!["Cell7", "Cell8", "Cell9"]),
1600 ];
1601 let table = Table::new(rows, [Constraint::Length(5); 3])
1602 .highlight_symbol(">>")
1603 .row_highlight_style(Style::new().red())
1604 .column_highlight_style(Style::new().blue());
1605 let mut state = TableState::new().with_selected(1).with_selected_column(2);
1606 StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
1607 let expected = Buffer::with_lines::<[Line; 4]>([
1608 Line::from(vec![" Cell1 ".into(), "Cell2 ".into(), "Cell3".blue()]),
1609 Line::from(vec![">>Cell4 Cell5 ".red(), "Cell6".blue(), " ".red()]),
1610 Line::from(vec![" Cell7 ".into(), "Cell8 ".into(), "Cell9".blue()]),
1611 Line::from(vec![" ".into(), " ".blue(), " ".into()]),
1612 ]);
1613 assert_eq!(buf, expected);
1614 }
1615
1616 #[test]
1617 fn render_with_selected_row_and_column_and_cell() {
1618 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
1619 let rows = vec![
1620 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
1621 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
1622 Row::new(vec!["Cell7", "Cell8", "Cell9"]),
1623 ];
1624 let table = Table::new(rows, [Constraint::Length(5); 3])
1625 .highlight_symbol(">>")
1626 .row_highlight_style(Style::new().red())
1627 .column_highlight_style(Style::new().blue())
1628 .cell_highlight_style(Style::new().green());
1629 let mut state = TableState::new().with_selected(1).with_selected_column(2);
1630 StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
1631 let expected = Buffer::with_lines::<[Line; 4]>([
1632 Line::from(vec![" Cell1 ".into(), "Cell2 ".into(), "Cell3".blue()]),
1633 Line::from(vec![">>Cell4 Cell5 ".red(), "Cell6".green(), " ".red()]),
1634 Line::from(vec![" Cell7 ".into(), "Cell8 ".into(), "Cell9".blue()]),
1635 Line::from(vec![" ".into(), " ".blue(), " ".into()]),
1636 ]);
1637 assert_eq!(buf, expected);
1638 }
1639
1640 #[rstest]
1644 #[case::no_selection(None, 50, ["50", "51", "52", "53", "54"])]
1645 #[case::selection_before_offset(20, 20, ["20", "21", "22", "23", "24"])]
1646 #[case::selection_immediately_before_offset(49, 49, ["49", "50", "51", "52", "53"])]
1647 #[case::selection_at_start_of_offset(50, 50, ["50", "51", "52", "53", "54"])]
1648 #[case::selection_at_end_of_offset(54, 50, ["50", "51", "52", "53", "54"])]
1649 #[case::selection_immediately_after_offset(55, 51, ["51", "52", "53", "54", "55"])]
1650 #[case::selection_after_offset(80, 76, ["76", "77", "78", "79", "80"])]
1651 fn render_with_selection_and_offset<T: Into<Option<usize>>>(
1652 #[case] selected_row: T,
1653 #[case] expected_offset: usize,
1654 #[case] expected_items: [&str; 5],
1655 ) {
1656 let rows = (0..100).map(|i| Row::new([i.to_string()]));
1658 let table = Table::new(rows, [Constraint::Length(2)]);
1659 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 5));
1660 let mut state = TableState::new()
1661 .with_offset(50)
1662 .with_selected(selected_row.into());
1663
1664 StatefulWidget::render(table.clone(), Rect::new(0, 0, 5, 5), &mut buf, &mut state);
1665
1666 assert_eq!(buf, Buffer::with_lines(expected_items));
1667 assert_eq!(state.offset, expected_offset);
1668 }
1669 }
1670
1671 mod column_widths {
1673 use super::*;
1674
1675 #[test]
1676 fn length_constraint() {
1677 let table = Table::default().widths([Length(4), Length(4)]);
1679 assert_eq!(table.get_column_widths(20, 0, 0), [(0, 4), (5, 4)]);
1680
1681 let table = Table::default().widths([Length(4), Length(4)]);
1683 assert_eq!(table.get_column_widths(20, 3, 0), [(3, 4), (8, 4)]);
1684
1685 let table = Table::default().widths([Length(4), Length(4)]);
1687 assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]);
1688
1689 let table = Table::default().widths([Length(4), Length(4)]);
1696 assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]);
1697 }
1698
1699 #[test]
1700 fn max_constraint() {
1701 let table = Table::default().widths([Max(4), Max(4)]);
1703 assert_eq!(table.get_column_widths(20, 0, 0), [(0, 4), (5, 4)]);
1704
1705 let table = Table::default().widths([Max(4), Max(4)]);
1707 assert_eq!(table.get_column_widths(20, 3, 0), [(3, 4), (8, 4)]);
1708
1709 let table = Table::default().widths([Max(4), Max(4)]);
1711 assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]);
1712
1713 let table = Table::default().widths([Max(4), Max(4)]);
1715 assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]);
1716 }
1717
1718 #[test]
1719 fn min_constraint() {
1720 let table = Table::default().widths([Min(4), Min(4)]);
1726 assert_eq!(table.get_column_widths(20, 0, 0), [(0, 10), (11, 9)]);
1727
1728 let table = Table::default().widths([Min(4), Min(4)]);
1730 assert_eq!(table.get_column_widths(20, 3, 0), [(3, 8), (12, 8)]);
1731
1732 let table = Table::default().widths([Min(4), Min(4)]);
1735 assert_eq!(table.get_column_widths(7, 0, 0), [(0, 3), (4, 3)]);
1736
1737 let table = Table::default().widths([Min(4), Min(4)]);
1740 assert_eq!(table.get_column_widths(7, 3, 0), [(3, 2), (6, 1)]);
1741 }
1742
1743 #[test]
1744 fn percentage_constraint() {
1745 let table = Table::default().widths([Percentage(30), Percentage(30)]);
1747 assert_eq!(table.get_column_widths(20, 0, 0), [(0, 6), (7, 6)]);
1748
1749 let table = Table::default().widths([Percentage(30), Percentage(30)]);
1751 assert_eq!(table.get_column_widths(20, 3, 0), [(3, 5), (9, 5)]);
1752
1753 let table = Table::default().widths([Percentage(30), Percentage(30)]);
1756 assert_eq!(table.get_column_widths(7, 0, 0), [(0, 2), (3, 2)]);
1757
1758 let table = Table::default().widths([Percentage(30), Percentage(30)]);
1761 assert_eq!(table.get_column_widths(7, 3, 0), [(3, 1), (5, 1)]);
1762 }
1763
1764 #[test]
1765 fn ratio_constraint() {
1766 let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
1769 assert_eq!(table.get_column_widths(20, 0, 0), [(0, 7), (8, 6)]);
1770
1771 let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
1774 assert_eq!(table.get_column_widths(20, 3, 0), [(3, 6), (10, 5)]);
1775
1776 let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
1779 assert_eq!(table.get_column_widths(7, 0, 0), [(0, 2), (3, 3)]);
1780
1781 let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
1784 assert_eq!(table.get_column_widths(7, 3, 0), [(3, 1), (5, 2)]);
1785 }
1786
1787 #[test]
1789 fn underconstrained_flex() {
1790 let table = Table::default().widths([Min(10), Min(10), Min(1)]);
1791 assert_eq!(
1792 table.get_column_widths(62, 0, 0),
1793 &[(0, 20), (21, 20), (42, 20)]
1794 );
1795
1796 let table = Table::default()
1797 .widths([Min(10), Min(10), Min(1)])
1798 .flex(Flex::Legacy);
1799 assert_eq!(
1800 table.get_column_widths(62, 0, 0),
1801 &[(0, 10), (11, 10), (22, 40)]
1802 );
1803
1804 let table = Table::default()
1805 .widths([Min(10), Min(10), Min(1)])
1806 .flex(Flex::SpaceBetween);
1807 assert_eq!(
1808 table.get_column_widths(62, 0, 0),
1809 &[(0, 20), (21, 20), (42, 20)]
1810 );
1811 }
1812
1813 #[test]
1814 fn underconstrained_segment_size() {
1815 let table = Table::default().widths([Min(10), Min(10), Min(1)]);
1816 assert_eq!(
1817 table.get_column_widths(62, 0, 0),
1818 &[(0, 20), (21, 20), (42, 20)]
1819 );
1820
1821 let table = Table::default()
1822 .widths([Min(10), Min(10), Min(1)])
1823 .flex(Flex::Legacy);
1824 assert_eq!(
1825 table.get_column_widths(62, 0, 0),
1826 &[(0, 10), (11, 10), (22, 40)]
1827 );
1828 }
1829
1830 #[test]
1831 fn no_constraint_with_rows() {
1832 let table = Table::default()
1833 .rows(vec![
1834 Row::new(vec!["a", "b"]),
1835 Row::new(vec!["c", "d", "e"]),
1836 ])
1837 .header(Row::new(vec!["f", "g"]))
1839 .footer(Row::new(vec!["h", "i"]))
1840 .column_spacing(0);
1841 assert_eq!(
1842 table.get_column_widths(30, 0, 3),
1843 &[(0, 10), (10, 10), (20, 10)]
1844 );
1845 }
1846
1847 #[test]
1848 fn no_constraint_with_header() {
1849 let table = Table::default()
1850 .rows(vec![])
1851 .header(Row::new(vec!["f", "g"]))
1852 .column_spacing(0);
1853 assert_eq!(table.get_column_widths(10, 0, 2), [(0, 5), (5, 5)]);
1854 }
1855
1856 #[test]
1857 fn no_constraint_with_footer() {
1858 let table = Table::default()
1859 .rows(vec![])
1860 .footer(Row::new(vec!["h", "i"]))
1861 .column_spacing(0);
1862 assert_eq!(table.get_column_widths(10, 0, 2), [(0, 5), (5, 5)]);
1863 }
1864
1865 #[track_caller]
1866 fn test_table_with_selection<'line, Lines>(
1867 highlight_spacing: HighlightSpacing,
1868 columns: u16,
1869 spacing: u16,
1870 selection: Option<usize>,
1871 expected: Lines,
1872 ) where
1873 Lines: IntoIterator,
1874 Lines::Item: Into<Line<'line>>,
1875 {
1876 let table = Table::default()
1877 .rows(vec![Row::new(vec!["ABCDE", "12345"])])
1878 .highlight_spacing(highlight_spacing)
1879 .highlight_symbol(">>>")
1880 .column_spacing(spacing);
1881 let area = Rect::new(0, 0, columns, 3);
1882 let mut buf = Buffer::empty(area);
1883 let mut state = TableState::default().with_selected(selection);
1884 StatefulWidget::render(table, area, &mut buf, &mut state);
1885 assert_eq!(buf, Buffer::with_lines(expected));
1886 }
1887
1888 #[test]
1889 fn excess_area_highlight_symbol_and_column_spacing_allocation() {
1890 test_table_with_selection(
1892 HighlightSpacing::Never,
1893 15, 0, None, [
1897 "ABCDE 12345 ", " ", " ", ],
1903 );
1904
1905 let table = Table::default()
1906 .rows(vec![Row::new(vec!["ABCDE", "12345"])])
1907 .widths([5, 5])
1908 .column_spacing(0);
1909 let area = Rect::new(0, 0, 15, 3);
1910 let mut buf = Buffer::empty(area);
1911 Widget::render(table, area, &mut buf);
1912 let expected = Buffer::with_lines([
1913 "ABCDE12345 ", " ", " ", ]);
1918 assert_eq!(buf, expected);
1919
1920 test_table_with_selection(
1922 HighlightSpacing::Never,
1923 15, 0, Some(0), [
1927 "ABCDE 12345 ", " ", " ", ],
1931 );
1932
1933 test_table_with_selection(
1935 HighlightSpacing::WhenSelected,
1936 15, 0, None, [
1940 "ABCDE 12345 ", " ", " ", ],
1944 );
1945 test_table_with_selection(
1947 HighlightSpacing::WhenSelected,
1948 15, 0, Some(0), [
1952 ">>>ABCDE 12345 ", " ", " ", ],
1956 );
1957
1958 test_table_with_selection(
1960 HighlightSpacing::Always,
1961 15, 0, None, [
1965 " ABCDE 12345 ", " ", " ", ],
1969 );
1970
1971 test_table_with_selection(
1973 HighlightSpacing::Always,
1974 15, 0, Some(0), [
1978 ">>>ABCDE 12345 ", " ", " ", ],
1982 );
1983 }
1984
1985 #[expect(clippy::too_many_lines)]
1986 #[test]
1987 fn insufficient_area_highlight_symbol_and_column_spacing_allocation() {
1988 test_table_with_selection(
1990 HighlightSpacing::Never,
1991 10, 1, None, [
1995 "ABCDE 1234", " ", " ", ],
1999 );
2000 test_table_with_selection(
2001 HighlightSpacing::WhenSelected,
2002 10, 1, None, [
2006 "ABCDE 1234", " ", " ", ],
2010 );
2011
2012 test_table_with_selection(
2021 HighlightSpacing::Always,
2022 10, 1, None, [
2026 " ABC 123", " ", " ", ],
2030 );
2031
2032 test_table_with_selection(
2034 HighlightSpacing::Always,
2035 9, 1, None, [
2039 " ABC 12", " ", " ", ],
2043 );
2044 test_table_with_selection(
2045 HighlightSpacing::Always,
2046 8, 1, None, [
2050 " AB 12", " ", " ", ],
2054 );
2055 test_table_with_selection(
2056 HighlightSpacing::Always,
2057 7, 1, None, [
2061 " AB 1", " ", " ", ],
2065 );
2066
2067 let table = Table::default()
2068 .rows(vec![Row::new(vec!["ABCDE", "12345"])])
2069 .highlight_spacing(HighlightSpacing::Always)
2070 .flex(Flex::Legacy)
2071 .highlight_symbol(">>>")
2072 .column_spacing(1);
2073 let area = Rect::new(0, 0, 10, 3);
2074 let mut buf = Buffer::empty(area);
2075 Widget::render(table, area, &mut buf);
2076 #[rustfmt::skip]
2078 let expected = Buffer::with_lines([
2079 " ABCDE 1",
2080 " ",
2081 " ",
2082 ]);
2083 assert_eq!(buf, expected);
2084
2085 let table = Table::default()
2086 .rows(vec![Row::new(vec!["ABCDE", "12345"])])
2087 .highlight_spacing(HighlightSpacing::Always)
2088 .flex(Flex::Start)
2089 .highlight_symbol(">>>")
2090 .column_spacing(1);
2091 let area = Rect::new(0, 0, 10, 3);
2092 let mut buf = Buffer::empty(area);
2093 Widget::render(table, area, &mut buf);
2094 #[rustfmt::skip]
2096 let expected = Buffer::with_lines([
2097 " ABC 123",
2098 " ",
2099 " ",
2100 ]);
2101 assert_eq!(buf, expected);
2102
2103 test_table_with_selection(
2104 HighlightSpacing::Never,
2105 10, 1, Some(0), [
2109 "ABCDE 1234", " ",
2111 " ",
2112 ],
2113 );
2114
2115 test_table_with_selection(
2116 HighlightSpacing::WhenSelected,
2117 10, 1, Some(0), [
2121 ">>>ABC 123", " ", " ", ],
2125 );
2126
2127 test_table_with_selection(
2128 HighlightSpacing::Always,
2129 10, 1, Some(0), [
2133 ">>>ABC 123", " ", " ", ],
2137 );
2138 }
2139
2140 #[test]
2141 fn insufficient_area_highlight_symbol_allocation_with_no_column_spacing() {
2142 test_table_with_selection(
2143 HighlightSpacing::Never,
2144 10, 0, None, [
2148 "ABCDE12345", " ", " ", ],
2152 );
2153 test_table_with_selection(
2154 HighlightSpacing::WhenSelected,
2155 10, 0, None, [
2159 "ABCDE12345", " ", " ", ],
2163 );
2164 test_table_with_selection(
2169 HighlightSpacing::Always,
2170 10, 0, None, [
2174 " ABCD123", " ", " ", ],
2178 );
2179 test_table_with_selection(
2180 HighlightSpacing::Never,
2181 10, 0, Some(0), [
2185 "ABCDE12345", " ", " ", ],
2189 );
2190 test_table_with_selection(
2191 HighlightSpacing::WhenSelected,
2192 10, 0, Some(0), [
2196 ">>>ABCD123", " ", " ", ],
2200 );
2201 test_table_with_selection(
2202 HighlightSpacing::Always,
2203 10, 0, Some(0), [
2207 ">>>ABCD123", " ", " ", ],
2211 );
2212 }
2213 }
2214
2215 #[test]
2216 fn stylize() {
2217 assert_eq!(
2218 Table::new(vec![Row::new(vec![Cell::from("")])], [Percentage(100)])
2219 .black()
2220 .on_white()
2221 .bold()
2222 .not_crossed_out()
2223 .style,
2224 Style::default()
2225 .fg(Color::Black)
2226 .bg(Color::White)
2227 .add_modifier(Modifier::BOLD)
2228 .remove_modifier(Modifier::CROSSED_OUT)
2229 );
2230 }
2231
2232 #[rstest]
2233 #[case::no_columns(vec![], vec![], vec![], 0)]
2234 #[case::only_header(vec!["H1", "H2"], vec![], vec![], 2)]
2235 #[case::only_rows(
2236 vec![],
2237 vec![vec!["C1", "C2"], vec!["C1", "C2", "C3"]],
2238 vec![],
2239 3
2240 )]
2241 #[case::only_footer(vec![], vec![], vec!["F1", "F2", "F3", "F4"], 4)]
2242 #[case::rows_longer(
2243 vec!["H1", "H2", "H3", "H4"],
2244 vec![vec!["C1", "C2"],vec!["C1", "C2", "C3"]],
2245 vec!["F1", "F2"],
2246 4
2247 )]
2248 #[case::rows_longer(
2249 vec!["H1", "H2"],
2250 vec![vec!["C1", "C2"], vec!["C1", "C2", "C3", "C4"]],
2251 vec!["F1", "F2"],
2252 4
2253 )]
2254 #[case::footer_longer(
2255 vec!["H1", "H2"],
2256 vec![vec!["C1", "C2"], vec!["C1", "C2", "C3"]],
2257 vec!["F1", "F2", "F3", "F4"],
2258 4
2259 )]
2260
2261 fn column_count(
2262 #[case] header: Vec<&str>,
2263 #[case] rows: Vec<Vec<&str>>,
2264 #[case] footer: Vec<&str>,
2265 #[case] expected: usize,
2266 ) {
2267 let header = Row::new(header);
2268 let footer = Row::new(footer);
2269 let rows: Vec<Row> = rows.into_iter().map(Row::new).collect();
2270 let table = Table::new(rows, Vec::<Constraint>::new())
2271 .header(header)
2272 .footer(footer);
2273 let column_count = table.column_count();
2274 assert_eq!(column_count, expected);
2275 }
2276
2277 #[test]
2278 fn render_in_minimal_buffer() {
2279 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2280 let rows = vec![
2281 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
2282 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
2283 ];
2284 let table = Table::new(rows, [Constraint::Length(10); 3])
2285 .header(Row::new(vec!["Header1", "Header2", "Header3"]))
2286 .footer(Row::new(vec!["Footer1", "Footer2", "Footer3"]));
2287 Widget::render(table, buffer.area, &mut buffer);
2289 assert_eq!(buffer, Buffer::with_lines([" "]));
2290 }
2291
2292 #[test]
2293 fn render_in_zero_size_buffer() {
2294 let mut buffer = Buffer::empty(Rect::ZERO);
2295 let rows = vec![
2296 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
2297 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
2298 ];
2299 let table = Table::new(rows, [Constraint::Length(10); 3])
2300 .header(Row::new(vec!["Header1", "Header2", "Header3"]))
2301 .footer(Row::new(vec!["Footer1", "Footer2", "Footer3"]));
2302 Widget::render(table, buffer.area, &mut buffer);
2304 }
2305}