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, selection_width, state, &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: &[Rect]) {
814 if let Some(ref header) = self.header {
815 buf.set_style(area, header.style);
816 for (cell_area, cell) in column_widths.iter().zip(header.cells.iter()) {
817 let new_x = area.x + cell_area.x;
818 let area_to_render = Rect::new(new_x, area.y, cell_area.width, area.height);
819 cell.render(area_to_render, buf);
820 }
821 }
822 }
823
824 fn render_footer(&self, area: Rect, buf: &mut Buffer, column_widths: &[Rect]) {
829 if let Some(ref footer) = self.footer {
830 buf.set_style(area, footer.style);
831 for (cell_area, cell) in column_widths.iter().zip(footer.cells.iter()) {
832 let new_x = area.x + cell_area.x;
833 let area_to_render = Rect::new(new_x, area.y, cell_area.width, area.height);
834 cell.render(area_to_render, buf);
835 }
836 }
837 }
838
839 fn render_rows(
844 &self,
845 area: Rect,
846 buf: &mut Buffer,
847 selection_width: u16,
848 state: &mut TableState,
849 columns_widths: &[Rect],
850 ) {
851 if self.rows.is_empty() {
852 return;
853 }
854
855 let (start_index, end_index) = self.visible_rows(state, area);
856 state.offset = start_index;
857
858 let mut y_offset = 0;
859
860 let mut selected_row_area = None;
861 for (i, row) in self
862 .rows
863 .iter()
864 .enumerate()
865 .skip(start_index)
866 .take(end_index - start_index)
867 {
868 let y = area.y + y_offset + row.top_margin;
869 let height = (y + row.height).min(area.bottom()).saturating_sub(y);
870 let row_area = Rect { y, height, ..area };
871 buf.set_style(row_area, row.style);
872
873 let is_selected = state.selected.is_some_and(|index| index == i);
874 if selection_width > 0 && is_selected {
875 self.set_selection_style(buf, selection_width, row_area, row);
876 }
877 self.render_row_cells(buf, columns_widths.iter().collect(), &row.cells, row_area);
878 if is_selected {
879 selected_row_area = Some(row_area);
880 }
881 y_offset += row.height_with_margin();
882 }
883
884 let selected_column_area = state.selected_column.and_then(|s| {
885 columns_widths.get(s).map(|cell_area| Rect {
888 x: cell_area.x + area.x,
889 width: cell_area.width,
890 ..area
891 })
892 });
893
894 match (selected_row_area, selected_column_area) {
895 (Some(row_area), Some(col_area)) => {
896 buf.set_style(row_area, self.row_highlight_style);
897 buf.set_style(col_area, self.column_highlight_style);
898 let cell_area = row_area.intersection(col_area);
899 buf.set_style(cell_area, self.cell_highlight_style);
900 }
901 (Some(row_area), None) => {
902 buf.set_style(row_area, self.row_highlight_style);
903 }
904 (None, Some(col_area)) => {
905 buf.set_style(col_area, self.column_highlight_style);
906 }
907 (None, None) => (),
908 }
909 }
910
911 fn render_row_cells(
917 &self,
918 buf: &mut Buffer,
919 column_widths: Vec<&Rect>,
920 cells: &Vec<Cell>,
921 row_area: Rect,
922 ) {
923 let mut column_widths_iterator = column_widths.into_iter();
924 for current_cell in cells {
925 if let Some(cell_area) = Self::get_cell_area(
926 &mut column_widths_iterator,
927 current_cell.column_span,
928 self.column_spacing,
929 ) {
930 let new_x = row_area.x + cell_area.x;
931 let area_to_render = Rect::new(new_x, row_area.y, cell_area.width, row_area.height);
932 current_cell.render(area_to_render, buf);
933 }
934 }
935 }
936
937 fn set_selection_style(
939 &self,
940 buf: &mut Buffer,
941 selection_width: u16,
942 row_area: Rect,
943 row: &Row,
944 ) {
945 let selection_area = Rect {
946 width: selection_width,
947 ..row_area
948 };
949 buf.set_style(selection_area, row.style);
950 (&self.highlight_symbol).render(selection_area, buf);
951 }
952
953 fn get_cell_area<'a, T>(
967 column_widths_iterator: &mut T,
968 cell_column_span: u16,
969 column_spacing: u16,
970 ) -> Option<Rect>
971 where
972 T: Iterator<Item = &'a Rect>,
973 {
974 if cell_column_span == 0 {
975 return None;
976 }
977 let first = column_widths_iterator.next()?;
978 let (n_columns_taken, all_columns_width) = column_widths_iterator
979 .take((cell_column_span - 1).into())
980 .map(|rect| (1, rect.width))
981 .fold((1, first.width), |so_far, next_column| {
982 (next_column.0 + so_far.0, next_column.1 + so_far.1)
983 });
984 let width = all_columns_width + (n_columns_taken - 1) * column_spacing;
985 Some(Rect::new(first.x, first.y, width, 1))
986 }
987
988 fn visible_rows(&self, state: &TableState, area: Rect) -> (usize, usize) {
997 let last_row = self.rows.len().saturating_sub(1);
998 let mut start = state.offset.min(last_row);
999
1000 if let Some(selected) = state.selected {
1001 start = start.min(selected);
1002 }
1003
1004 let mut end = start;
1005 let mut height = 0;
1006
1007 for item in self.rows.iter().skip(start) {
1008 if height + item.height > area.height {
1009 break;
1010 }
1011 height += item.height_with_margin();
1012 end += 1;
1013 }
1014
1015 if let Some(selected) = state.selected {
1016 let selected = selected.min(last_row);
1017
1018 while selected >= end {
1020 height = height.saturating_add(self.rows[end].height_with_margin());
1021 end += 1;
1022 while height > area.height {
1023 height = height.saturating_sub(self.rows[start].height_with_margin());
1024 start += 1;
1025 }
1026 }
1027 }
1028
1029 if height < area.height && end < self.rows.len() {
1031 end += 1;
1032 }
1033
1034 (start, end)
1035 }
1036
1037 fn get_column_widths(
1042 &self,
1043 max_width: u16,
1044 selection_width: u16,
1045 col_count: usize,
1046 ) -> Vec<Rect> {
1047 let widths = if self.widths.is_empty() {
1048 vec![Constraint::Length(max_width / col_count.max(1) as u16); col_count]
1050 } else {
1051 self.widths.clone()
1052 };
1053 let [_selection_area, columns_area] =
1055 Layout::horizontal([Constraint::Length(selection_width), Constraint::Fill(0)])
1056 .areas(Rect::new(0, 0, max_width, 1));
1057 let rects = Layout::horizontal(widths)
1058 .flex(self.flex)
1059 .spacing(self.column_spacing)
1060 .split(columns_area);
1061 rects
1062 .iter()
1063 .map(|c| Rect::new(c.x, 0, c.width, 1))
1064 .collect()
1065 }
1066
1067 fn column_count(&self) -> usize {
1068 self.rows
1069 .iter()
1070 .chain(self.footer.iter())
1071 .chain(self.header.iter())
1072 .map(|r| r.cells.len())
1073 .max()
1074 .unwrap_or_default()
1075 }
1076
1077 fn selection_width(&self, state: &TableState) -> u16 {
1080 let has_selection = state.selected.is_some();
1081 if self.highlight_spacing.should_add(has_selection) {
1082 self.highlight_symbol.width() as u16
1083 } else {
1084 0
1085 }
1086 }
1087}
1088
1089fn ensure_percentages_less_than_100(widths: &[Constraint]) {
1090 for w in widths {
1091 if let Constraint::Percentage(p) = w {
1092 assert!(
1093 *p <= 100,
1094 "Percentages should be between 0 and 100 inclusively."
1095 );
1096 }
1097 }
1098}
1099
1100impl Styled for Table<'_> {
1101 type Item = Self;
1102
1103 fn style(&self) -> Style {
1104 self.style
1105 }
1106
1107 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
1108 self.style(style)
1109 }
1110}
1111
1112impl<'a, Item> FromIterator<Item> for Table<'a>
1113where
1114 Item: Into<Row<'a>>,
1115{
1116 fn from_iter<Iter: IntoIterator<Item = Item>>(rows: Iter) -> Self {
1121 let widths: [Constraint; 0] = [];
1122 Self::new(rows, widths)
1123 }
1124}
1125
1126#[cfg(test)]
1127mod tests {
1128 use alloc::string::ToString;
1129 use alloc::{format, vec};
1130
1131 use ratatui_core::layout::Constraint::*;
1132 use ratatui_core::style::{Color, Modifier, Style, Stylize};
1133 use ratatui_core::text::Line;
1134 use rstest::{fixture, rstest};
1135
1136 use super::*;
1137 use crate::table::Cell;
1138
1139 #[test]
1140 fn new() {
1141 let rows = [Row::new(vec![Cell::from("")])];
1142 let widths = [Constraint::Percentage(100)];
1143 let table = Table::new(rows.clone(), widths);
1144 assert_eq!(table.rows, rows);
1145 assert_eq!(table.header, None);
1146 assert_eq!(table.footer, None);
1147 assert_eq!(table.widths, widths);
1148 assert_eq!(table.column_spacing, 1);
1149 assert_eq!(table.block, None);
1150 assert_eq!(table.style, Style::default());
1151 assert_eq!(table.row_highlight_style, Style::default());
1152 assert_eq!(table.highlight_symbol, Text::default());
1153 assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
1154 assert_eq!(table.flex, Flex::Start);
1155 }
1156
1157 #[test]
1158 fn default() {
1159 let table = Table::default();
1160 assert_eq!(table.rows, []);
1161 assert_eq!(table.header, None);
1162 assert_eq!(table.footer, None);
1163 assert_eq!(table.widths, []);
1164 assert_eq!(table.column_spacing, 1);
1165 assert_eq!(table.block, None);
1166 assert_eq!(table.style, Style::default());
1167 assert_eq!(table.row_highlight_style, Style::default());
1168 assert_eq!(table.highlight_symbol, Text::default());
1169 assert_eq!(table.highlight_spacing, HighlightSpacing::WhenSelected);
1170 assert_eq!(table.flex, Flex::Start);
1171 }
1172
1173 #[test]
1174 fn collect() {
1175 let table = (0..4)
1176 .map(|i| -> Row { (0..4).map(|j| format!("{i}*{j} = {}", i * j)).collect() })
1177 .collect::<Table>()
1178 .widths([Constraint::Percentage(25); 4]);
1179
1180 let expected_rows: Vec<Row> = vec![
1181 Row::new(["0*0 = 0", "0*1 = 0", "0*2 = 0", "0*3 = 0"]),
1182 Row::new(["1*0 = 0", "1*1 = 1", "1*2 = 2", "1*3 = 3"]),
1183 Row::new(["2*0 = 0", "2*1 = 2", "2*2 = 4", "2*3 = 6"]),
1184 Row::new(["3*0 = 0", "3*1 = 3", "3*2 = 6", "3*3 = 9"]),
1185 ];
1186
1187 assert_eq!(table.rows, expected_rows);
1188 assert_eq!(table.widths, [Constraint::Percentage(25); 4]);
1189 }
1190
1191 #[test]
1192 fn widths() {
1193 let table = Table::default().widths([Constraint::Length(100)]);
1194 assert_eq!(table.widths, [Constraint::Length(100)]);
1195
1196 #[expect(clippy::needless_borrows_for_generic_args)]
1199 let table = Table::default().widths(&[Constraint::Length(100)]);
1200 assert_eq!(table.widths, [Constraint::Length(100)]);
1201
1202 let table = Table::default().widths(vec![Constraint::Length(100)]);
1203 assert_eq!(table.widths, [Constraint::Length(100)]);
1204
1205 #[expect(clippy::needless_borrows_for_generic_args)]
1208 let table = Table::default().widths(&vec![Constraint::Length(100)]);
1209 assert_eq!(table.widths, [Constraint::Length(100)]);
1210
1211 let table = Table::default().widths([100].into_iter().map(Constraint::Length));
1212 assert_eq!(table.widths, [Constraint::Length(100)]);
1213 }
1214
1215 #[test]
1216 fn rows() {
1217 let rows = [Row::new(vec![Cell::from("")])];
1218 let table = Table::default().rows(rows.clone());
1219 assert_eq!(table.rows, rows);
1220 }
1221
1222 #[test]
1223 fn column_spacing() {
1224 let table = Table::default().column_spacing(2);
1225 assert_eq!(table.column_spacing, 2);
1226 }
1227
1228 #[test]
1229 fn block() {
1230 let block = Block::bordered().title("Table");
1231 let table = Table::default().block(block.clone());
1232 assert_eq!(table.block, Some(block));
1233 }
1234
1235 #[test]
1236 fn header() {
1237 let header = Row::new(vec![Cell::from("")]);
1238 let table = Table::default().header(header.clone());
1239 assert_eq!(table.header, Some(header));
1240 }
1241
1242 #[test]
1243 fn footer() {
1244 let footer = Row::new(vec![Cell::from("")]);
1245 let table = Table::default().footer(footer.clone());
1246 assert_eq!(table.footer, Some(footer));
1247 }
1248
1249 #[test]
1250 #[expect(deprecated)]
1251 fn highlight_style() {
1252 let style = Style::default().red().italic();
1253 let table = Table::default().highlight_style(style);
1254 assert_eq!(table.row_highlight_style, style);
1255 }
1256
1257 #[test]
1258 fn row_highlight_style() {
1259 let style = Style::default().red().italic();
1260 let table = Table::default().row_highlight_style(style);
1261 assert_eq!(table.row_highlight_style, style);
1262 }
1263
1264 #[test]
1265 fn column_highlight_style() {
1266 let style = Style::default().red().italic();
1267 let table = Table::default().column_highlight_style(style);
1268 assert_eq!(table.column_highlight_style, style);
1269 }
1270
1271 #[test]
1272 fn cell_highlight_style() {
1273 let style = Style::default().red().italic();
1274 let table = Table::default().cell_highlight_style(style);
1275 assert_eq!(table.cell_highlight_style, style);
1276 }
1277
1278 #[test]
1279 fn highlight_symbol() {
1280 let table = Table::default().highlight_symbol(">>");
1281 assert_eq!(table.highlight_symbol, Text::from(">>"));
1282 }
1283
1284 #[test]
1285 fn highlight_spacing() {
1286 let table = Table::default().highlight_spacing(HighlightSpacing::Always);
1287 assert_eq!(table.highlight_spacing, HighlightSpacing::Always);
1288 }
1289
1290 #[test]
1291 #[should_panic = "Percentages should be between 0 and 100 inclusively"]
1292 fn table_invalid_percentages() {
1293 let _ = Table::default().widths([Constraint::Percentage(110)]);
1294 }
1295
1296 #[test]
1297 fn widths_conversions() {
1298 let array = [Constraint::Percentage(100)];
1299 let table = Table::new(Vec::<Row>::new(), array);
1300 assert_eq!(table.widths, [Constraint::Percentage(100)], "array");
1301
1302 let array_ref = &[Constraint::Percentage(100)];
1303 let table = Table::new(Vec::<Row>::new(), array_ref);
1304 assert_eq!(table.widths, [Constraint::Percentage(100)], "array ref");
1305
1306 let vec = vec![Constraint::Percentage(100)];
1307 let slice = vec.as_slice();
1308 let table = Table::new(Vec::<Row>::new(), slice);
1309 assert_eq!(table.widths, [Constraint::Percentage(100)], "slice");
1310
1311 let vec = vec![Constraint::Percentage(100)];
1312 let table = Table::new(Vec::<Row>::new(), vec);
1313 assert_eq!(table.widths, [Constraint::Percentage(100)], "vec");
1314
1315 let vec_ref = &vec![Constraint::Percentage(100)];
1316 let table = Table::new(Vec::<Row>::new(), vec_ref);
1317 assert_eq!(table.widths, [Constraint::Percentage(100)], "vec ref");
1318 }
1319
1320 #[cfg(test)]
1321 mod state {
1322 use ratatui_core::buffer::Buffer;
1323 use ratatui_core::layout::{Constraint, Rect};
1324 use ratatui_core::widgets::StatefulWidget;
1325
1326 use super::*;
1327 use crate::table::{Row, Table, TableState};
1328
1329 #[fixture]
1330 fn table_buf() -> Buffer {
1331 Buffer::empty(Rect::new(0, 0, 10, 10))
1332 }
1333
1334 #[rstest]
1335 fn test_list_state_empty_list(mut table_buf: Buffer) {
1336 let mut state = TableState::default();
1337
1338 let rows: Vec<Row> = Vec::new();
1339 let widths = vec![Constraint::Percentage(100)];
1340 let table = Table::new(rows, widths);
1341 state.select_first();
1342 StatefulWidget::render(table, table_buf.area, &mut table_buf, &mut state);
1343 assert_eq!(state.selected, None);
1344 assert_eq!(state.selected_column, None);
1345 }
1346
1347 #[rstest]
1348 fn test_list_state_single_item(mut table_buf: Buffer) {
1349 let mut state = TableState::default();
1350
1351 let widths = vec![Constraint::Percentage(100)];
1352
1353 let items = vec![Row::new(vec!["Item 1"])];
1354 let table = Table::new(items, widths);
1355 state.select_first();
1356 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1357 assert_eq!(state.selected, Some(0));
1358 assert_eq!(state.selected_column, None);
1359
1360 state.select_last();
1361 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1362 assert_eq!(state.selected, Some(0));
1363 assert_eq!(state.selected_column, None);
1364
1365 state.select_previous();
1366 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1367 assert_eq!(state.selected, Some(0));
1368 assert_eq!(state.selected_column, None);
1369
1370 state.select_next();
1371 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1372 assert_eq!(state.selected, Some(0));
1373 assert_eq!(state.selected_column, None);
1374
1375 let mut state = TableState::default();
1376
1377 state.select_first_column();
1378 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1379 assert_eq!(state.selected_column, Some(0));
1380 assert_eq!(state.selected, None);
1381
1382 state.select_last_column();
1383 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1384 assert_eq!(state.selected_column, Some(0));
1385 assert_eq!(state.selected, None);
1386
1387 state.select_previous_column();
1388 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1389 assert_eq!(state.selected_column, Some(0));
1390 assert_eq!(state.selected, None);
1391
1392 state.select_next_column();
1393 StatefulWidget::render(&table, table_buf.area, &mut table_buf, &mut state);
1394 assert_eq!(state.selected_column, Some(0));
1395 assert_eq!(state.selected, None);
1396 }
1397 }
1398
1399 #[cfg(test)]
1400 mod render {
1401 use ratatui_core::layout::Alignment;
1402
1403 use super::*;
1404
1405 #[test]
1406 fn render_empty_area() {
1407 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1408 let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
1409 let table = Table::new(rows, vec![Constraint::Length(5); 2]);
1410 Widget::render(table, Rect::new(0, 0, 0, 0), &mut buf);
1411 assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 15, 3)));
1412 }
1413
1414 #[test]
1415 fn render_default() {
1416 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1417 let table = Table::default();
1418 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1419 assert_eq!(buf, Buffer::empty(Rect::new(0, 0, 15, 3)));
1420 }
1421
1422 #[test]
1423 fn render_with_block() {
1424 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1425 let rows = vec![
1426 Row::new(vec!["Cell1", "Cell2"]),
1427 Row::new(vec!["Cell3", "Cell4"]),
1428 ];
1429 let block = Block::bordered().title("Block");
1430 let table = Table::new(rows, vec![Constraint::Length(5); 2]).block(block);
1431 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1432 #[rustfmt::skip]
1433 let expected = Buffer::with_lines([
1434 "┌Block────────┐",
1435 "│Cell1 Cell2 │",
1436 "└─────────────┘",
1437 ]);
1438 assert_eq!(buf, expected);
1439 }
1440
1441 #[rstest]
1442 #[case(15, 5, vec![
1443 Row::new(vec![
1444 Cell::new("Cell1").column_span(1),
1445 Cell::new("Cell2").column_span(1),
1446 ]),
1447 Row::new(vec![
1448 Cell::new("Cell3").column_span(1),
1449 Cell::new("Cell4").column_span(1),
1450 ]),
1451 ],
1452 &Buffer::with_lines(["Cell1 Cell2 ", "Cell3 Cell4 "]))]
1453 #[case(15, 5, vec![
1454 Row::new(vec![
1455 Cell::new("Cell1").column_span(0),
1456 Cell::new("Cell2").column_span(1),
1457 ]),
1458 Row::new(vec![
1459 Cell::new("Cell3").column_span(1),
1460 Cell::new("Cell4").column_span(1),
1461 ]),
1462 ], &Buffer::with_lines(["Cell2 ", "Cell3 Cell4 "]))]
1463 #[case(15, 5, vec![
1464 Row::new(vec![
1465 Cell::new("Cell1").column_span(2),
1466 Cell::new("Cell2").column_span(1),
1467 ]),
1468 Row::new(vec![
1469 Cell::new("Cell3").column_span(1),
1470 Cell::new("Cell4").column_span(1),
1471 ]),
1472 ], &Buffer::with_lines(["Cell1 ", "Cell3 Cell4 "]))]
1473 fn test_colspans_2_cols<'rows, Rows>(
1474 #[case] width: u16,
1475 #[case] column_width: u16,
1476 #[case] rows: Rows,
1477 #[case] expected: &Buffer,
1478 ) where
1479 Rows: IntoIterator<Item = Row<'rows>>,
1480 {
1481 let mut buf = Buffer::empty(Rect::new(0, 0, width, 2));
1482 let table = Table::new(rows, [Constraint::Length(column_width); 2]);
1483 Widget::render(table, Rect::new(0, 0, width, 2), &mut buf);
1484 assert_eq!(buf, *expected);
1485 }
1486
1487 #[rstest]
1488 #[case(17, 5, vec![
1489 Row::new(vec![
1490 Cell::new("Cell1").column_span(2),
1491 Cell::new("Cell2").column_span(1),
1492 ]),
1493 Row::new(vec![
1494 Cell::new("Cell3").column_span(1),
1495 Cell::new("Cell4").column_span(1),
1496 Cell::new("Cell5").column_span(1),
1497 ]),
1498 ], &Buffer::with_lines(["Cell1 Cell2", "Cell3 Cell4 Cell5"]))]
1499 #[case(17, 5, vec![
1500 Row::new(vec![
1501 Cell::new("Cell1").column_span(1),
1502 Cell::new("Cell2").column_span(2),
1503 Cell::new("Cell3").column_span(1),
1504 ]),
1505 Row::new(vec![
1506 Cell::new("Cell4").column_span(1),
1507 Cell::new("Cell5").column_span(1),
1508 Cell::new("Cell6").column_span(1),
1509 ]),
1510 ], &Buffer::with_lines(["Cell1 Cell2 ", "Cell4 Cell5 Cell6"]))]
1511 #[case(15, 5, vec![
1512 Row::new(vec![
1513 Cell::new("11111111111111111111").column_span(2),
1514 Cell::new("22222222222222222222").column_span(1),
1515 ]),
1516 Row::new(vec![
1517 Cell::new("33333333333333333333").column_span(1),
1518 Cell::new("44444444444444444444").column_span(2),
1519 Cell::new("55555555555555555555").column_span(1),
1520 ]),
1521 ], &Buffer::with_lines(["1111111111 2222", "3333 4444444444"]))]
1522 fn test_colspans_3_cols<'rows, Rows>(
1523 #[case] width: u16,
1524 #[case] column_width: u16,
1525 #[case] rows: Rows,
1526 #[case] expected: &Buffer,
1527 ) where
1528 Rows: IntoIterator<Item = Row<'rows>>,
1529 {
1530 let mut buf = Buffer::empty(Rect::new(0, 0, width, 2));
1531 let table = Table::new(rows, [Constraint::Length(column_width); 3]);
1532 Widget::render(table, Rect::new(0, 0, width, 2), &mut buf);
1533 assert_eq!(buf, *expected);
1534 }
1535
1536 #[test]
1537 fn render_with_header() {
1538 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1539 let header = Row::new(vec!["Head1", "Head2"]);
1540 let rows = vec![
1541 Row::new(vec!["Cell1", "Cell2"]),
1542 Row::new(vec!["Cell3", "Cell4"]),
1543 ];
1544 let table = Table::new(rows, [Constraint::Length(5); 2]).header(header);
1545 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1546 #[rustfmt::skip]
1547 let expected = Buffer::with_lines([
1548 "Head1 Head2 ",
1549 "Cell1 Cell2 ",
1550 "Cell3 Cell4 ",
1551 ]);
1552 assert_eq!(buf, expected);
1553 }
1554
1555 #[test]
1556 fn render_with_footer() {
1557 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1558 let footer = Row::new(vec!["Foot1", "Foot2"]);
1559 let rows = vec![
1560 Row::new(vec!["Cell1", "Cell2"]),
1561 Row::new(vec!["Cell3", "Cell4"]),
1562 ];
1563 let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
1564 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1565 #[rustfmt::skip]
1566 let expected = Buffer::with_lines([
1567 "Cell1 Cell2 ",
1568 "Cell3 Cell4 ",
1569 "Foot1 Foot2 ",
1570 ]);
1571 assert_eq!(buf, expected);
1572 }
1573
1574 #[test]
1575 fn render_with_header_and_footer() {
1576 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1577 let header = Row::new(vec!["Head1", "Head2"]);
1578 let footer = Row::new(vec!["Foot1", "Foot2"]);
1579 let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
1580 let table = Table::new(rows, [Constraint::Length(5); 2])
1581 .header(header)
1582 .footer(footer);
1583 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1584 #[rustfmt::skip]
1585 let expected = Buffer::with_lines([
1586 "Head1 Head2 ",
1587 "Cell1 Cell2 ",
1588 "Foot1 Foot2 ",
1589 ]);
1590 assert_eq!(buf, expected);
1591 }
1592
1593 #[test]
1594 fn render_with_header_margin() {
1595 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1596 let header = Row::new(vec!["Head1", "Head2"]).bottom_margin(1);
1597 let rows = vec![
1598 Row::new(vec!["Cell1", "Cell2"]),
1599 Row::new(vec!["Cell3", "Cell4"]),
1600 ];
1601 let table = Table::new(rows, [Constraint::Length(5); 2]).header(header);
1602 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1603 #[rustfmt::skip]
1604 let expected = Buffer::with_lines([
1605 "Head1 Head2 ",
1606 " ",
1607 "Cell1 Cell2 ",
1608 ]);
1609 assert_eq!(buf, expected);
1610 }
1611
1612 #[test]
1613 fn render_with_footer_margin() {
1614 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1615 let footer = Row::new(vec!["Foot1", "Foot2"]).top_margin(1);
1616 let rows = vec![Row::new(vec!["Cell1", "Cell2"])];
1617 let table = Table::new(rows, [Constraint::Length(5); 2]).footer(footer);
1618 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1619 #[rustfmt::skip]
1620 let expected = Buffer::with_lines([
1621 "Cell1 Cell2 ",
1622 " ",
1623 "Foot1 Foot2 ",
1624 ]);
1625 assert_eq!(buf, expected);
1626 }
1627
1628 #[test]
1629 fn render_with_row_margin() {
1630 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1631 let rows = vec![
1632 Row::new(vec!["Cell1", "Cell2"]).bottom_margin(1),
1633 Row::new(vec!["Cell3", "Cell4"]),
1634 ];
1635 let table = Table::new(rows, [Constraint::Length(5); 2]);
1636 Widget::render(table, Rect::new(0, 0, 15, 3), &mut buf);
1637 #[rustfmt::skip]
1638 let expected = Buffer::with_lines([
1639 "Cell1 Cell2 ",
1640 " ",
1641 "Cell3 Cell4 ",
1642 ]);
1643 assert_eq!(buf, expected);
1644 }
1645
1646 #[test]
1647 fn render_with_tall_row() {
1648 let mut buf = Buffer::empty(Rect::new(0, 0, 23, 3));
1649 let rows = vec![
1650 Row::new(vec!["Cell1", "Cell2"]),
1651 Row::new(vec![
1652 Text::raw("Cell3-Line1\nCell3-Line2\nCell3-Line3"),
1653 Text::raw("Cell4-Line1\nCell4-Line2\nCell4-Line3"),
1654 ])
1655 .height(3),
1656 ];
1657 let table = Table::new(rows, [Constraint::Length(11); 2]);
1658 Widget::render(table, Rect::new(0, 0, 23, 3), &mut buf);
1659 #[rustfmt::skip]
1660 let expected = Buffer::with_lines([
1661 "Cell1 Cell2 ",
1662 "Cell3-Line1 Cell4-Line1",
1663 "Cell3-Line2 Cell4-Line2",
1664 ]);
1665 assert_eq!(buf, expected);
1666 }
1667
1668 #[test]
1669 fn render_with_alignment() {
1670 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 3));
1671 let rows = vec![
1672 Row::new(vec![Line::from("Left").alignment(Alignment::Left)]),
1673 Row::new(vec![Line::from("Center").alignment(Alignment::Center)]),
1674 Row::new(vec![Line::from("Right").alignment(Alignment::Right)]),
1675 ];
1676 let table = Table::new(rows, [Percentage(100)]);
1677 Widget::render(table, Rect::new(0, 0, 10, 3), &mut buf);
1678 let expected = Buffer::with_lines(["Left ", " Center ", " Right"]);
1679 assert_eq!(buf, expected);
1680 }
1681
1682 #[test]
1683 fn render_with_overflow_does_not_panic() {
1684 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
1685 let table = Table::new(Vec::<Row>::new(), [Constraint::Min(20); 1])
1686 .header(Row::new([Line::from("").alignment(Alignment::Right)]))
1687 .footer(Row::new([Line::from("").alignment(Alignment::Right)]));
1688 Widget::render(table, Rect::new(0, 0, 20, 3), &mut buf);
1689 }
1690
1691 #[test]
1692 fn render_with_selected_column_and_incorrect_width_count_does_not_panic() {
1693 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
1694 let table = Table::new(
1695 vec![Row::new(vec!["Row1", "Row2", "Row3"])],
1696 [Constraint::Length(10); 1],
1697 );
1698 let mut state = TableState::new().with_selected_column(2);
1699 StatefulWidget::render(table, Rect::new(0, 0, 20, 3), &mut buf, &mut state);
1700 }
1701
1702 #[test]
1703 fn render_with_selected() {
1704 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1705 let rows = vec![
1706 Row::new(vec!["Cell1", "Cell2"]),
1707 Row::new(vec!["Cell3", "Cell4"]),
1708 ];
1709 let table = Table::new(rows, [Constraint::Length(5); 2])
1710 .row_highlight_style(Style::new().red())
1711 .highlight_symbol(">>");
1712 let mut state = TableState::new().with_selected(Some(0));
1713 StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
1714 let expected = Buffer::with_lines([
1715 ">>Cell1 Cell2 ".red(),
1716 " Cell3 Cell4 ".into(),
1717 " ".into(),
1718 ]);
1719 assert_eq!(buf, expected);
1720 }
1721
1722 #[test]
1723 fn render_with_selected_column() {
1724 let mut buf = Buffer::empty(Rect::new(0, 0, 15, 3));
1725 let rows = vec![
1726 Row::new(vec!["Cell1", "Cell2"]),
1727 Row::new(vec!["Cell3", "Cell4"]),
1728 ];
1729 let table = Table::new(rows, [Constraint::Length(5); 2])
1730 .column_highlight_style(Style::new().blue())
1731 .highlight_symbol(">>");
1732 let mut state = TableState::new().with_selected_column(Some(1));
1733 StatefulWidget::render(table, Rect::new(0, 0, 15, 3), &mut buf, &mut state);
1734 let expected = Buffer::with_lines::<[Line; 3]>([
1735 Line::from(vec![
1736 "Cell1".into(),
1737 " ".into(),
1738 "Cell2".blue(),
1739 " ".into(),
1740 ]),
1741 Line::from(vec![
1742 "Cell3".into(),
1743 " ".into(),
1744 "Cell4".blue(),
1745 " ".into(),
1746 ]),
1747 Line::from(vec![" ".into(), " ".blue(), " ".into()]),
1748 ]);
1749 assert_eq!(buf, expected);
1750 }
1751
1752 #[test]
1753 fn render_with_selected_cell() {
1754 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
1755 let rows = vec![
1756 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
1757 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
1758 Row::new(vec!["Cell7", "Cell8", "Cell9"]),
1759 ];
1760 let table = Table::new(rows, [Constraint::Length(5); 3])
1761 .highlight_symbol(">>")
1762 .cell_highlight_style(Style::new().green());
1763 let mut state = TableState::new().with_selected_cell((1, 2));
1764 StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
1765 let expected = Buffer::with_lines::<[Line; 4]>([
1766 Line::from(vec![" Cell1 ".into(), "Cell2 ".into(), "Cell3".into()]),
1767 Line::from(vec![">>Cell4 Cell5 ".into(), "Cell6".green(), " ".into()]),
1768 Line::from(vec![" Cell7 ".into(), "Cell8 ".into(), "Cell9".into()]),
1769 Line::from(vec![" ".into()]),
1770 ]);
1771 assert_eq!(buf, expected);
1772 }
1773
1774 #[test]
1775 fn render_with_selected_row_and_column() {
1776 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
1777 let rows = vec![
1778 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
1779 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
1780 Row::new(vec!["Cell7", "Cell8", "Cell9"]),
1781 ];
1782 let table = Table::new(rows, [Constraint::Length(5); 3])
1783 .highlight_symbol(">>")
1784 .row_highlight_style(Style::new().red())
1785 .column_highlight_style(Style::new().blue());
1786 let mut state = TableState::new().with_selected(1).with_selected_column(2);
1787 StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
1788 let expected = Buffer::with_lines::<[Line; 4]>([
1789 Line::from(vec![" Cell1 ".into(), "Cell2 ".into(), "Cell3".blue()]),
1790 Line::from(vec![">>Cell4 Cell5 ".red(), "Cell6".blue(), " ".red()]),
1791 Line::from(vec![" Cell7 ".into(), "Cell8 ".into(), "Cell9".blue()]),
1792 Line::from(vec![" ".into(), " ".blue(), " ".into()]),
1793 ]);
1794 assert_eq!(buf, expected);
1795 }
1796
1797 #[test]
1798 fn render_with_selected_row_and_column_and_cell() {
1799 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 4));
1800 let rows = vec![
1801 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
1802 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
1803 Row::new(vec!["Cell7", "Cell8", "Cell9"]),
1804 ];
1805 let table = Table::new(rows, [Constraint::Length(5); 3])
1806 .highlight_symbol(">>")
1807 .row_highlight_style(Style::new().red())
1808 .column_highlight_style(Style::new().blue())
1809 .cell_highlight_style(Style::new().green());
1810 let mut state = TableState::new().with_selected(1).with_selected_column(2);
1811 StatefulWidget::render(table, Rect::new(0, 0, 20, 4), &mut buf, &mut state);
1812 let expected = Buffer::with_lines::<[Line; 4]>([
1813 Line::from(vec![" Cell1 ".into(), "Cell2 ".into(), "Cell3".blue()]),
1814 Line::from(vec![">>Cell4 Cell5 ".red(), "Cell6".green(), " ".red()]),
1815 Line::from(vec![" Cell7 ".into(), "Cell8 ".into(), "Cell9".blue()]),
1816 Line::from(vec![" ".into(), " ".blue(), " ".into()]),
1817 ]);
1818 assert_eq!(buf, expected);
1819 }
1820
1821 #[rstest]
1825 #[case::no_selection(None, 50, ["50", "51", "52", "53", "54"])]
1826 #[case::selection_before_offset(20, 20, ["20", "21", "22", "23", "24"])]
1827 #[case::selection_immediately_before_offset(49, 49, ["49", "50", "51", "52", "53"])]
1828 #[case::selection_at_start_of_offset(50, 50, ["50", "51", "52", "53", "54"])]
1829 #[case::selection_at_end_of_offset(54, 50, ["50", "51", "52", "53", "54"])]
1830 #[case::selection_immediately_after_offset(55, 51, ["51", "52", "53", "54", "55"])]
1831 #[case::selection_after_offset(80, 76, ["76", "77", "78", "79", "80"])]
1832 fn render_with_selection_and_offset<T: Into<Option<usize>>>(
1833 #[case] selected_row: T,
1834 #[case] expected_offset: usize,
1835 #[case] expected_items: [&str; 5],
1836 ) {
1837 let rows = (0..100).map(|i| Row::new([i.to_string()]));
1839 let table = Table::new(rows, [Constraint::Length(2)]);
1840 let mut buf = Buffer::empty(Rect::new(0, 0, 2, 5));
1841 let mut state = TableState::new()
1842 .with_offset(50)
1843 .with_selected(selected_row.into());
1844
1845 StatefulWidget::render(table.clone(), Rect::new(0, 0, 5, 5), &mut buf, &mut state);
1846
1847 assert_eq!(buf, Buffer::with_lines(expected_items));
1848 assert_eq!(state.offset, expected_offset);
1849 }
1850 }
1851
1852 mod column_widths {
1854 use super::*;
1855
1856 #[test]
1857 fn length_constraint() {
1858 let table = Table::default().widths([Length(4), Length(4)]);
1860 assert_eq!(
1861 table.get_column_widths(20, 0, 0),
1862 [Rect::new(0, 0, 4, 1), Rect::new(5, 0, 4, 1),]
1863 );
1864
1865 let table = Table::default().widths([Length(4), Length(4)]);
1867 assert_eq!(
1868 table.get_column_widths(20, 3, 0),
1869 [Rect::new(3, 0, 4, 1), Rect::new(8, 0, 4, 1)]
1870 );
1871
1872 let table = Table::default().widths([Length(4), Length(4)]);
1874 assert_eq!(
1875 table.get_column_widths(7, 0, 0),
1876 [Rect::new(0, 0, 3, 1), Rect::new(4, 0, 3, 1)]
1877 );
1878
1879 let table = Table::default().widths([Length(4), Length(4)]);
1886 assert_eq!(
1887 table.get_column_widths(7, 3, 0),
1888 [Rect::new(3, 0, 2, 1), Rect::new(6, 0, 1, 1)]
1889 );
1890 }
1891
1892 #[test]
1893 fn max_constraint() {
1894 let table = Table::default().widths([Max(4), Max(4)]);
1896 assert_eq!(
1897 table.get_column_widths(20, 0, 0),
1898 [Rect::new(0, 0, 4, 1), Rect::new(5, 0, 4, 1)]
1899 );
1900
1901 let table = Table::default().widths([Max(4), Max(4)]);
1903 assert_eq!(
1904 table.get_column_widths(20, 3, 0),
1905 [Rect::new(3, 0, 4, 1), Rect::new(8, 0, 4, 1)]
1906 );
1907
1908 let table = Table::default().widths([Max(4), Max(4)]);
1910 assert_eq!(
1911 table.get_column_widths(7, 0, 0),
1912 [Rect::new(0, 0, 3, 1), Rect::new(4, 0, 3, 1)]
1913 );
1914
1915 let table = Table::default().widths([Max(4), Max(4)]);
1917 assert_eq!(
1918 table.get_column_widths(7, 3, 0),
1919 [Rect::new(3, 0, 2, 1), Rect::new(6, 0, 1, 1)]
1920 );
1921 }
1922
1923 #[test]
1924 fn min_constraint() {
1925 let table = Table::default().widths([Min(4), Min(4)]);
1931 assert_eq!(
1932 table.get_column_widths(20, 0, 0),
1933 [Rect::new(0, 0, 10, 1), Rect::new(11, 0, 9, 1)]
1934 );
1935
1936 let table = Table::default().widths([Min(4), Min(4)]);
1938 assert_eq!(
1939 table.get_column_widths(20, 3, 0),
1940 [Rect::new(3, 0, 8, 1), Rect::new(12, 0, 8, 1)]
1941 );
1942
1943 let table = Table::default().widths([Min(4), Min(4)]);
1946 assert_eq!(
1947 table.get_column_widths(7, 0, 0),
1948 [Rect::new(0, 0, 3, 1), Rect::new(4, 0, 3, 1)]
1949 );
1950
1951 let table = Table::default().widths([Min(4), Min(4)]);
1954 assert_eq!(
1955 table.get_column_widths(7, 3, 0),
1956 [Rect::new(3, 0, 2, 1), Rect::new(6, 0, 1, 1)]
1957 );
1958 }
1959
1960 #[test]
1961 fn percentage_constraint() {
1962 let table = Table::default().widths([Percentage(30), Percentage(30)]);
1964 assert_eq!(
1965 table.get_column_widths(20, 0, 0),
1966 [Rect::new(0, 0, 6, 1), Rect::new(7, 0, 6, 1)]
1967 );
1968
1969 let table = Table::default().widths([Percentage(30), Percentage(30)]);
1971 assert_eq!(
1972 table.get_column_widths(20, 3, 0),
1973 [Rect::new(3, 0, 5, 1), Rect::new(9, 0, 5, 1)]
1974 );
1975
1976 let table = Table::default().widths([Percentage(30), Percentage(30)]);
1979 assert_eq!(
1980 table.get_column_widths(7, 0, 0),
1981 [Rect::new(0, 0, 2, 1), Rect::new(3, 0, 2, 1)]
1982 );
1983
1984 let table = Table::default().widths([Percentage(30), Percentage(30)]);
1987 assert_eq!(
1988 table.get_column_widths(7, 3, 0),
1989 [Rect::new(3, 0, 1, 1), Rect::new(5, 0, 1, 1)]
1990 );
1991 }
1992
1993 #[test]
1994 fn ratio_constraint() {
1995 let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
1998 assert_eq!(
1999 table.get_column_widths(20, 0, 0),
2000 [Rect::new(0, 0, 7, 1), Rect::new(8, 0, 6, 1)]
2001 );
2002
2003 let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
2006 assert_eq!(
2007 table.get_column_widths(20, 3, 0),
2008 [Rect::new(3, 0, 6, 1), Rect::new(10, 0, 5, 1)]
2009 );
2010
2011 let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
2014 assert_eq!(
2015 table.get_column_widths(7, 0, 0),
2016 [Rect::new(0, 0, 2, 1), Rect::new(3, 0, 3, 1)]
2017 );
2018
2019 let table = Table::default().widths([Ratio(1, 3), Ratio(1, 3)]);
2022 assert_eq!(
2023 table.get_column_widths(7, 3, 0),
2024 [Rect::new(3, 0, 1, 1), Rect::new(5, 0, 2, 1)]
2025 );
2026 }
2027
2028 #[test]
2030 fn underconstrained_flex() {
2031 let table = Table::default().widths([Min(10), Min(10), Min(1)]);
2032 assert_eq!(
2033 table.get_column_widths(62, 0, 0),
2034 &[
2035 Rect::new(0, 0, 20, 1),
2036 Rect::new(21, 0, 20, 1),
2037 Rect::new(42, 0, 20, 1)
2038 ]
2039 );
2040
2041 let table = Table::default()
2042 .widths([Min(10), Min(10), Min(1)])
2043 .flex(Flex::Legacy);
2044 assert_eq!(
2045 table.get_column_widths(62, 0, 0),
2046 &[
2047 Rect::new(0, 0, 10, 1),
2048 Rect::new(11, 0, 10, 1),
2049 Rect::new(22, 0, 40, 1)
2050 ]
2051 );
2052
2053 let table = Table::default()
2054 .widths([Min(10), Min(10), Min(1)])
2055 .flex(Flex::SpaceBetween);
2056 assert_eq!(
2057 table.get_column_widths(62, 0, 0),
2058 &[
2059 Rect::new(0, 0, 20, 1),
2060 Rect::new(21, 0, 20, 1),
2061 Rect::new(42, 0, 20, 1)
2062 ]
2063 );
2064 }
2065
2066 #[test]
2067 fn underconstrained_segment_size() {
2068 let table = Table::default().widths([Min(10), Min(10), Min(1)]);
2069 assert_eq!(
2070 table.get_column_widths(62, 0, 0),
2071 &[
2072 Rect::new(0, 0, 20, 1),
2073 Rect::new(21, 0, 20, 1),
2074 Rect::new(42, 0, 20, 1)
2075 ]
2076 );
2077
2078 let table = Table::default()
2079 .widths([Min(10), Min(10), Min(1)])
2080 .flex(Flex::Legacy);
2081 assert_eq!(
2082 table.get_column_widths(62, 0, 0),
2083 &[
2084 Rect::new(0, 0, 10, 1),
2085 Rect::new(11, 0, 10, 1),
2086 Rect::new(22, 0, 40, 1)
2087 ]
2088 );
2089 }
2090
2091 #[test]
2092 fn no_constraint_with_rows() {
2093 let table = Table::default()
2094 .rows(vec![
2095 Row::new(vec!["a", "b"]),
2096 Row::new(vec!["c", "d", "e"]),
2097 ])
2098 .header(Row::new(vec!["f", "g"]))
2100 .footer(Row::new(vec!["h", "i"]))
2101 .column_spacing(0);
2102 assert_eq!(
2103 table.get_column_widths(30, 0, 3),
2104 &[
2105 Rect::new(0, 0, 10, 1),
2106 Rect::new(10, 0, 10, 1),
2107 Rect::new(20, 0, 10, 1)
2108 ]
2109 );
2110 }
2111
2112 #[test]
2113 fn no_constraint_with_header() {
2114 let table = Table::default()
2115 .rows(vec![])
2116 .header(Row::new(vec!["f", "g"]))
2117 .column_spacing(0);
2118 assert_eq!(
2119 table.get_column_widths(10, 0, 2),
2120 [Rect::new(0, 0, 5, 1), Rect::new(5, 0, 5, 1)]
2121 );
2122 }
2123
2124 #[test]
2125 fn no_constraint_with_footer() {
2126 let table = Table::default()
2127 .rows(vec![])
2128 .footer(Row::new(vec!["h", "i"]))
2129 .column_spacing(0);
2130 assert_eq!(
2131 table.get_column_widths(10, 0, 2),
2132 [Rect::new(0, 0, 5, 1), Rect::new(5, 0, 5, 1)]
2133 );
2134 }
2135
2136 #[track_caller]
2137 fn test_table_with_selection<'line, Lines>(
2138 highlight_spacing: HighlightSpacing,
2139 columns: u16,
2140 spacing: u16,
2141 selection: Option<usize>,
2142 expected: Lines,
2143 ) where
2144 Lines: IntoIterator,
2145 Lines::Item: Into<Line<'line>>,
2146 {
2147 let table = Table::default()
2148 .rows(vec![Row::new(vec!["ABCDE", "12345"])])
2149 .highlight_spacing(highlight_spacing)
2150 .highlight_symbol(">>>")
2151 .column_spacing(spacing);
2152 let area = Rect::new(0, 0, columns, 3);
2153 let mut buf = Buffer::empty(area);
2154 let mut state = TableState::default().with_selected(selection);
2155 StatefulWidget::render(table, area, &mut buf, &mut state);
2156 assert_eq!(buf, Buffer::with_lines(expected));
2157 }
2158
2159 #[test]
2160 fn excess_area_highlight_symbol_and_column_spacing_allocation() {
2161 test_table_with_selection(
2163 HighlightSpacing::Never,
2164 15, 0, None, [
2168 "ABCDE 12345 ", " ", " ", ],
2174 );
2175
2176 let table = Table::default()
2177 .rows(vec![Row::new(vec!["ABCDE", "12345"])])
2178 .widths([5, 5])
2179 .column_spacing(0);
2180 let area = Rect::new(0, 0, 15, 3);
2181 let mut buf = Buffer::empty(area);
2182 Widget::render(table, area, &mut buf);
2183 let expected = Buffer::with_lines([
2184 "ABCDE12345 ", " ", " ", ]);
2189 assert_eq!(buf, expected);
2190
2191 test_table_with_selection(
2193 HighlightSpacing::Never,
2194 15, 0, Some(0), [
2198 "ABCDE 12345 ", " ", " ", ],
2202 );
2203
2204 test_table_with_selection(
2206 HighlightSpacing::WhenSelected,
2207 15, 0, None, [
2211 "ABCDE 12345 ", " ", " ", ],
2215 );
2216 test_table_with_selection(
2218 HighlightSpacing::WhenSelected,
2219 15, 0, Some(0), [
2223 ">>>ABCDE 12345 ", " ", " ", ],
2227 );
2228
2229 test_table_with_selection(
2231 HighlightSpacing::Always,
2232 15, 0, None, [
2236 " ABCDE 12345 ", " ", " ", ],
2240 );
2241
2242 test_table_with_selection(
2244 HighlightSpacing::Always,
2245 15, 0, Some(0), [
2249 ">>>ABCDE 12345 ", " ", " ", ],
2253 );
2254 }
2255
2256 #[expect(clippy::too_many_lines)]
2257 #[test]
2258 fn insufficient_area_highlight_symbol_and_column_spacing_allocation() {
2259 test_table_with_selection(
2261 HighlightSpacing::Never,
2262 10, 1, None, [
2266 "ABCDE 1234", " ", " ", ],
2270 );
2271 test_table_with_selection(
2272 HighlightSpacing::WhenSelected,
2273 10, 1, None, [
2277 "ABCDE 1234", " ", " ", ],
2281 );
2282
2283 test_table_with_selection(
2292 HighlightSpacing::Always,
2293 10, 1, None, [
2297 " ABC 123", " ", " ", ],
2301 );
2302
2303 test_table_with_selection(
2305 HighlightSpacing::Always,
2306 9, 1, None, [
2310 " ABC 12", " ", " ", ],
2314 );
2315 test_table_with_selection(
2316 HighlightSpacing::Always,
2317 8, 1, None, [
2321 " AB 12", " ", " ", ],
2325 );
2326 test_table_with_selection(
2327 HighlightSpacing::Always,
2328 7, 1, None, [
2332 " AB 1", " ", " ", ],
2336 );
2337
2338 let table = Table::default()
2339 .rows(vec![Row::new(vec!["ABCDE", "12345"])])
2340 .highlight_spacing(HighlightSpacing::Always)
2341 .flex(Flex::Legacy)
2342 .highlight_symbol(">>>")
2343 .column_spacing(1);
2344 let area = Rect::new(0, 0, 10, 3);
2345 let mut buf = Buffer::empty(area);
2346 Widget::render(table, area, &mut buf);
2347 #[rustfmt::skip]
2349 let expected = Buffer::with_lines([
2350 " ABCDE 1",
2351 " ",
2352 " ",
2353 ]);
2354 assert_eq!(buf, expected);
2355
2356 let table = Table::default()
2357 .rows(vec![Row::new(vec!["ABCDE", "12345"])])
2358 .highlight_spacing(HighlightSpacing::Always)
2359 .flex(Flex::Start)
2360 .highlight_symbol(">>>")
2361 .column_spacing(1);
2362 let area = Rect::new(0, 0, 10, 3);
2363 let mut buf = Buffer::empty(area);
2364 Widget::render(table, area, &mut buf);
2365 #[rustfmt::skip]
2367 let expected = Buffer::with_lines([
2368 " ABC 123",
2369 " ",
2370 " ",
2371 ]);
2372 assert_eq!(buf, expected);
2373
2374 test_table_with_selection(
2375 HighlightSpacing::Never,
2376 10, 1, Some(0), [
2380 "ABCDE 1234", " ",
2382 " ",
2383 ],
2384 );
2385
2386 test_table_with_selection(
2387 HighlightSpacing::WhenSelected,
2388 10, 1, Some(0), [
2392 ">>>ABC 123", " ", " ", ],
2396 );
2397
2398 test_table_with_selection(
2399 HighlightSpacing::Always,
2400 10, 1, Some(0), [
2404 ">>>ABC 123", " ", " ", ],
2408 );
2409 }
2410
2411 #[test]
2412 fn insufficient_area_highlight_symbol_allocation_with_no_column_spacing() {
2413 test_table_with_selection(
2414 HighlightSpacing::Never,
2415 10, 0, None, [
2419 "ABCDE12345", " ", " ", ],
2423 );
2424 test_table_with_selection(
2425 HighlightSpacing::WhenSelected,
2426 10, 0, None, [
2430 "ABCDE12345", " ", " ", ],
2434 );
2435 test_table_with_selection(
2440 HighlightSpacing::Always,
2441 10, 0, None, [
2445 " ABCD123", " ", " ", ],
2449 );
2450 test_table_with_selection(
2451 HighlightSpacing::Never,
2452 10, 0, Some(0), [
2456 "ABCDE12345", " ", " ", ],
2460 );
2461 test_table_with_selection(
2462 HighlightSpacing::WhenSelected,
2463 10, 0, Some(0), [
2467 ">>>ABCD123", " ", " ", ],
2471 );
2472 test_table_with_selection(
2473 HighlightSpacing::Always,
2474 10, 0, Some(0), [
2478 ">>>ABCD123", " ", " ", ],
2482 );
2483 }
2484 }
2485
2486 #[test]
2487 fn stylize() {
2488 assert_eq!(
2489 Table::new(vec![Row::new(vec![Cell::from("")])], [Percentage(100)])
2490 .black()
2491 .on_white()
2492 .bold()
2493 .not_crossed_out()
2494 .style,
2495 Style::default()
2496 .fg(Color::Black)
2497 .bg(Color::White)
2498 .add_modifier(Modifier::BOLD)
2499 .remove_modifier(Modifier::CROSSED_OUT)
2500 );
2501 }
2502
2503 #[rstest]
2504 #[case::no_columns(vec![], vec![], vec![], 0)]
2505 #[case::only_header(vec!["H1", "H2"], vec![], vec![], 2)]
2506 #[case::only_rows(
2507 vec![],
2508 vec![vec!["C1", "C2"], vec!["C1", "C2", "C3"]],
2509 vec![],
2510 3
2511 )]
2512 #[case::only_footer(vec![], vec![], vec!["F1", "F2", "F3", "F4"], 4)]
2513 #[case::rows_longer(
2514 vec!["H1", "H2", "H3", "H4"],
2515 vec![vec!["C1", "C2"],vec!["C1", "C2", "C3"]],
2516 vec!["F1", "F2"],
2517 4
2518 )]
2519 #[case::rows_longer(
2520 vec!["H1", "H2"],
2521 vec![vec!["C1", "C2"], vec!["C1", "C2", "C3", "C4"]],
2522 vec!["F1", "F2"],
2523 4
2524 )]
2525 #[case::footer_longer(
2526 vec!["H1", "H2"],
2527 vec![vec!["C1", "C2"], vec!["C1", "C2", "C3"]],
2528 vec!["F1", "F2", "F3", "F4"],
2529 4
2530 )]
2531
2532 fn column_count(
2533 #[case] header: Vec<&str>,
2534 #[case] rows: Vec<Vec<&str>>,
2535 #[case] footer: Vec<&str>,
2536 #[case] expected: usize,
2537 ) {
2538 let header = Row::new(header);
2539 let footer = Row::new(footer);
2540 let rows: Vec<Row> = rows.into_iter().map(Row::new).collect();
2541 let table = Table::new(rows, Vec::<Constraint>::new())
2542 .header(header)
2543 .footer(footer);
2544 let column_count = table.column_count();
2545 assert_eq!(column_count, expected);
2546 }
2547
2548 #[test]
2549 fn render_in_minimal_buffer() {
2550 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2551 let rows = vec![
2552 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
2553 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
2554 ];
2555 let table = Table::new(rows, [Constraint::Length(10); 3])
2556 .header(Row::new(vec!["Header1", "Header2", "Header3"]))
2557 .footer(Row::new(vec!["Footer1", "Footer2", "Footer3"]));
2558 Widget::render(table, buffer.area, &mut buffer);
2560 assert_eq!(buffer, Buffer::with_lines([" "]));
2561 }
2562
2563 #[test]
2564 fn render_in_zero_size_buffer() {
2565 let mut buffer = Buffer::empty(Rect::ZERO);
2566 let rows = vec![
2567 Row::new(vec!["Cell1", "Cell2", "Cell3"]),
2568 Row::new(vec!["Cell4", "Cell5", "Cell6"]),
2569 ];
2570 let table = Table::new(rows, [Constraint::Length(10); 3])
2571 .header(Row::new(vec!["Header1", "Header2", "Header3"]))
2572 .footer(Row::new(vec!["Footer1", "Footer2", "Footer3"]));
2573 Widget::render(table, buffer.area, &mut buffer);
2575 }
2576
2577 #[test]
2578 fn get_area_for_column_span_one_no_more_columns() {
2579 let columns = [];
2580 let column_span = Table::get_cell_area(&mut columns.iter(), 1, 1);
2581 assert!(column_span.is_none());
2582 }
2583
2584 #[test]
2585 fn get_area_for_column_span_two_no_more_columns() {
2586 let columns = [];
2587 let column_span = Table::get_cell_area(&mut columns.iter(), 2, 1);
2588 assert!(column_span.is_none());
2589 }
2590
2591 #[rstest]
2592 #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 2, 5)]
2593 #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 2, 5,)]
2594 #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 1, 2)]
2595 #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 3, 5)]
2596 #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}], 1, 2)]
2597 #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}], 2, 2)]
2598 #[case(&[
2599 Rect{x: 3, width: 2, y: 0, height: 1},
2600 Rect{x: 3, width: 2, y: 0, height: 1},
2601 Rect{x: 3, width: 2, y: 0, height: 1},
2602 Rect{x: 3, width: 2, y: 0, height: 1},
2603 ], 3, 8)]
2604 #[case(&[
2605 Rect{x: 3, width: 2, y: 0, height: 1},
2606 Rect{x: 3, width: 2, y: 0, height: 1},
2607 Rect{x: 3, width: 2, y: 0, height: 1},
2608 ], 3, 8)]
2609 fn test_colspan_width_single_column_spacing(
2610 #[case] columns: &[Rect],
2611 #[case] column_span: u16,
2612 #[case] expected_column_width: u16,
2613 ) {
2614 let column_span = Table::get_cell_area(&mut columns.iter(), column_span, 1);
2615 assert!(column_span.is_some());
2616 assert_eq!(column_span.unwrap().width, expected_column_width);
2617 }
2618
2619 #[rstest]
2620 #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}, Rect{x: 3, width: 2, y: 0, height: 1}], 3, 10)]
2621 #[case(&[Rect{x: 3, width: 2, y: 0, height: 1}], 3, 2)]
2622 fn test_colspan_width_two_column_spacing(
2623 #[case] columns: &[Rect],
2624 #[case] column_span: u16,
2625 #[case] expected_column_width: u16,
2626 ) {
2627 let column_span = Table::get_cell_area(&mut columns.iter(), column_span, 2);
2628 assert!(column_span.is_some());
2629 assert_eq!(column_span.unwrap().width, expected_column_width);
2630 }
2631
2632 #[rstest]
2633 #[case(
2634 HighlightSpacing::Always,
2635 15, 1, None, [
2639 Cell::new("ABCDEFGHIJK").column_span(2),
2640 Cell::new("12345678901"),
2641 Cell::new("XYZXYZXYZXY"),
2642 ],
2643 [
2644 " ABCDEFGH 123",
2645 " ", " ", ])]
2648 #[case(
2649 HighlightSpacing::Always,
2650 15, 1, Some(0), [
2654 Cell::new("ABCDEFGHIJK").column_span(2),
2655 Cell::new("12345678901"),
2656 Cell::new("XYZXYZXYZXY"),
2657 ],
2658 [
2659 ">>>ABCDEFGH 123",
2660 " ", " ", ])]
2663 #[case(
2664 HighlightSpacing::WhenSelected,
2665 15, 1, None, [
2669 Cell::new("ABCDEFGHIJK").column_span(2),
2670 Cell::new("12345678901"),
2671 Cell::new("XYZXYZXYZXY"),
2672 ],
2673 [
2674 "ABCDEFGHIJ 1234",
2675 " ", " ", ])]
2678 #[case(
2679 HighlightSpacing::WhenSelected,
2680 15, 1, Some(0), [
2684 Cell::new("ABCDEFGHIJK").column_span(2),
2685 Cell::new("12345678901"),
2686 Cell::new("XYZXYZXYZXY"),
2687 ],
2688 [
2689 ">>>ABCDEFGH 123",
2690 " ", " ", ])]
2693 fn test_table_with_selection_and_column_spans<'line, 'cell, Lines, Cells>(
2694 #[case] highlight_spacing: HighlightSpacing,
2695 #[case] columns: u16,
2696 #[case] spacing: u16,
2697 #[case] selection: Option<usize>,
2698 #[case] cells: Cells,
2699 #[case] expected: Lines,
2700 ) where
2701 Cells: IntoIterator,
2702 Cells::Item: Into<Cell<'cell>>,
2703 Lines: IntoIterator,
2704 Lines::Item: Into<Line<'line>>,
2705 {
2706 let table = Table::default()
2707 .rows(vec![Row::new(cells)])
2708 .highlight_spacing(highlight_spacing)
2709 .highlight_symbol(">>>")
2710 .column_spacing(spacing);
2711 let area = Rect::new(0, 0, columns, 3);
2712 let mut buf = Buffer::empty(area);
2713 let mut state = TableState::default().with_selected(selection);
2714 StatefulWidget::render(table, area, &mut buf, &mut state);
2715 assert_eq!(buf, Buffer::with_lines(expected));
2716 }
2717}