1use std::marker::PhantomData;
55
56use ratatui::layout::Constraint;
57use ratatui::prelude::*;
58use ratatui::widgets::{Block, Borders, Cell, Row};
59
60use super::{Component, Focusable};
61
62pub trait TableRow: Clone {
89 fn cells(&self) -> Vec<String>;
94}
95
96#[derive(Clone, Debug)]
112pub struct Column {
113 header: String,
114 width: Constraint,
115 sortable: bool,
116}
117
118impl Column {
119 pub fn new(header: impl Into<String>, width: Constraint) -> Self {
123 Self {
124 header: header.into(),
125 width,
126 sortable: false,
127 }
128 }
129
130 pub fn sortable(mut self) -> Self {
135 self.sortable = true;
136 self
137 }
138
139 pub fn header(&self) -> &str {
141 &self.header
142 }
143
144 pub fn width(&self) -> Constraint {
146 self.width
147 }
148
149 pub fn is_sortable(&self) -> bool {
151 self.sortable
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
157pub enum SortDirection {
158 #[default]
160 Ascending,
161 Descending,
163}
164
165impl SortDirection {
166 pub fn toggle(self) -> Self {
168 match self {
169 SortDirection::Ascending => SortDirection::Descending,
170 SortDirection::Descending => SortDirection::Ascending,
171 }
172 }
173}
174
175#[derive(Clone, Debug, PartialEq, Eq)]
177pub enum TableMessage {
178 Up,
180 Down,
182 First,
184 Last,
186 PageUp(usize),
188 PageDown(usize),
190 Select,
192 SortBy(usize),
198 ClearSort,
200}
201
202#[derive(Clone, Debug, PartialEq, Eq)]
204pub enum TableOutput<T: Clone> {
205 Selected(T),
207 SelectionChanged(usize),
209 Sorted {
211 column: usize,
213 direction: SortDirection,
215 },
216 SortCleared,
218}
219
220#[derive(Clone, Debug)]
224pub struct TableState<T: TableRow> {
225 rows: Vec<T>,
226 columns: Vec<Column>,
227 selected: Option<usize>,
228 sort: Option<(usize, SortDirection)>,
229 display_order: Vec<usize>,
230 focused: bool,
231 disabled: bool,
232}
233
234impl<T: TableRow> Default for TableState<T> {
235 fn default() -> Self {
236 Self {
237 rows: Vec::new(),
238 columns: Vec::new(),
239 selected: None,
240 sort: None,
241 display_order: Vec::new(),
242 focused: false,
243 disabled: false,
244 }
245 }
246}
247
248impl<T: TableRow> TableState<T> {
249 pub fn new(rows: Vec<T>, columns: Vec<Column>) -> Self {
276 let display_order: Vec<usize> = (0..rows.len()).collect();
277 let selected = if rows.is_empty() { None } else { Some(0) };
278 Self {
279 rows,
280 columns,
281 selected,
282 sort: None,
283 display_order,
284 focused: false,
285 disabled: false,
286 }
287 }
288
289 pub fn with_selected(rows: Vec<T>, columns: Vec<Column>, selected: usize) -> Self {
293 let display_order: Vec<usize> = (0..rows.len()).collect();
294 let selected = if rows.is_empty() {
295 None
296 } else {
297 Some(selected.min(rows.len() - 1))
298 };
299 Self {
300 rows,
301 columns,
302 selected,
303 sort: None,
304 display_order,
305 focused: false,
306 disabled: false,
307 }
308 }
309
310 pub fn rows(&self) -> &[T] {
312 &self.rows
313 }
314
315 pub fn columns(&self) -> &[Column] {
317 &self.columns
318 }
319
320 pub fn selected_index(&self) -> Option<usize> {
324 self.selected
325 }
326
327 pub fn selected_row(&self) -> Option<&T> {
331 self.selected
332 .and_then(|i| self.display_order.get(i))
333 .and_then(|&idx| self.rows.get(idx))
334 }
335
336 pub fn sort(&self) -> Option<(usize, SortDirection)> {
340 self.sort
341 }
342
343 pub fn len(&self) -> usize {
345 self.rows.len()
346 }
347
348 pub fn is_empty(&self) -> bool {
350 self.rows.is_empty()
351 }
352
353 pub fn set_rows(&mut self, rows: Vec<T>) {
358 self.rows = rows;
359 self.display_order = (0..self.rows.len()).collect();
360 self.sort = None;
361
362 if self.rows.is_empty() {
363 self.selected = None;
364 } else if let Some(sel) = self.selected {
365 self.selected = Some(sel.min(self.rows.len() - 1));
366 } else {
367 self.selected = Some(0);
368 }
369 }
370
371 pub fn set_selected(&mut self, index: Option<usize>) {
376 match index {
377 Some(i) if i < self.display_order.len() => self.selected = Some(i),
378 Some(_) => {} None => self.selected = None,
380 }
381 }
382
383 pub fn is_disabled(&self) -> bool {
385 self.disabled
386 }
387
388 pub fn set_disabled(&mut self, disabled: bool) {
392 self.disabled = disabled;
393 }
394
395 fn apply_sort(&mut self) {
397 if let Some((col, direction)) = self.sort {
398 self.display_order.sort_by(|&a, &b| {
399 let cells_a = self.rows[a].cells();
400 let cells_b = self.rows[b].cells();
401 let cmp = cells_a.get(col).cmp(&cells_b.get(col));
402 match direction {
403 SortDirection::Ascending => cmp,
404 SortDirection::Descending => cmp.reverse(),
405 }
406 });
407 } else {
408 self.display_order = (0..self.rows.len()).collect();
410 }
411 }
412
413 fn find_display_index(&self, original_index: usize) -> Option<usize> {
415 self.display_order
416 .iter()
417 .position(|&idx| idx == original_index)
418 }
419}
420
421pub struct Table<T: TableRow>(PhantomData<T>);
483
484impl<T: TableRow + 'static> Component for Table<T> {
485 type State = TableState<T>;
486 type Message = TableMessage;
487 type Output = TableOutput<T>;
488
489 fn init() -> Self::State {
490 TableState::default()
491 }
492
493 fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
494 if state.disabled || state.rows.is_empty() {
495 return None;
496 }
497
498 let len = state.display_order.len();
499 let current = state.selected.unwrap_or(0);
500
501 match msg {
502 TableMessage::Up => {
503 if current > 0 {
504 let new_index = current - 1;
505 state.selected = Some(new_index);
506 return Some(TableOutput::SelectionChanged(new_index));
507 }
508 }
509 TableMessage::Down => {
510 if current < len - 1 {
511 let new_index = current + 1;
512 state.selected = Some(new_index);
513 return Some(TableOutput::SelectionChanged(new_index));
514 }
515 }
516 TableMessage::First => {
517 if current != 0 {
518 state.selected = Some(0);
519 return Some(TableOutput::SelectionChanged(0));
520 }
521 }
522 TableMessage::Last => {
523 let last = len - 1;
524 if current != last {
525 state.selected = Some(last);
526 return Some(TableOutput::SelectionChanged(last));
527 }
528 }
529 TableMessage::PageUp(page_size) => {
530 let new_index = current.saturating_sub(page_size);
531 if new_index != current {
532 state.selected = Some(new_index);
533 return Some(TableOutput::SelectionChanged(new_index));
534 }
535 }
536 TableMessage::PageDown(page_size) => {
537 let new_index = (current + page_size).min(len - 1);
538 if new_index != current {
539 state.selected = Some(new_index);
540 return Some(TableOutput::SelectionChanged(new_index));
541 }
542 }
543 TableMessage::Select => {
544 if let Some(row) = state.selected_row().cloned() {
545 return Some(TableOutput::Selected(row));
546 }
547 }
548 TableMessage::SortBy(col) => {
549 if let Some(column) = state.columns.get(col) {
551 if !column.is_sortable() {
552 return None;
553 }
554
555 let selected_original = state
557 .selected
558 .and_then(|i| state.display_order.get(i).copied());
559
560 let new_sort = match state.sort {
562 Some((c, SortDirection::Ascending)) if c == col => {
563 Some((col, SortDirection::Descending))
564 }
565 Some((c, SortDirection::Descending)) if c == col => None,
566 _ => Some((col, SortDirection::Ascending)),
567 };
568
569 state.sort = new_sort;
570 state.apply_sort();
571
572 if let Some(orig) = selected_original {
574 state.selected = state.find_display_index(orig);
575 }
576
577 return match new_sort {
578 Some((column, direction)) => {
579 Some(TableOutput::Sorted { column, direction })
580 }
581 None => Some(TableOutput::SortCleared),
582 };
583 }
584 }
585 TableMessage::ClearSort => {
586 if state.sort.is_some() {
587 let selected_original = state
589 .selected
590 .and_then(|i| state.display_order.get(i).copied());
591
592 state.sort = None;
593 state.apply_sort();
594
595 if let Some(orig) = selected_original {
597 state.selected = state.find_display_index(orig);
598 }
599
600 return Some(TableOutput::SortCleared);
601 }
602 }
603 }
604
605 None
606 }
607
608 fn view(state: &Self::State, frame: &mut Frame, area: Rect) {
609 let header_cells: Vec<Cell> = state
611 .columns
612 .iter()
613 .enumerate()
614 .map(|(i, col)| {
615 let mut text = col.header.clone();
616 if let Some((sort_col, dir)) = state.sort {
617 if sort_col == i {
618 text.push_str(match dir {
619 SortDirection::Ascending => " ↑",
620 SortDirection::Descending => " ↓",
621 });
622 }
623 }
624 Cell::from(text)
625 })
626 .collect();
627
628 let header_style = if state.disabled {
629 Style::default().fg(Color::DarkGray)
630 } else {
631 Style::default().add_modifier(Modifier::BOLD)
632 };
633
634 let header = Row::new(header_cells).style(header_style).bottom_margin(1);
635
636 let rows: Vec<Row> = state
638 .display_order
639 .iter()
640 .map(|&idx| {
641 let cells: Vec<Cell> = state.rows[idx]
642 .cells()
643 .into_iter()
644 .map(Cell::from)
645 .collect();
646 Row::new(cells)
647 })
648 .collect();
649
650 let widths: Vec<Constraint> = state.columns.iter().map(|c| c.width).collect();
651
652 let border_style = if state.focused && !state.disabled {
653 Style::default().fg(Color::Yellow)
654 } else {
655 Style::default()
656 };
657
658 let row_highlight_style = if state.disabled {
659 Style::default().bg(Color::DarkGray)
660 } else if state.focused {
661 Style::default()
662 .bg(Color::Blue)
663 .fg(Color::White)
664 .add_modifier(Modifier::BOLD)
665 } else {
666 Style::default().bg(Color::DarkGray)
667 };
668
669 let table = ratatui::widgets::Table::new(rows, widths)
670 .header(header)
671 .block(
672 Block::default()
673 .borders(Borders::ALL)
674 .border_style(border_style),
675 )
676 .row_highlight_style(row_highlight_style)
677 .highlight_symbol("> ");
678
679 let mut table_state = ratatui::widgets::TableState::default();
681 table_state.select(state.selected);
682 frame.render_stateful_widget(table, area, &mut table_state);
683 }
684}
685
686impl<T: TableRow + 'static> Focusable for Table<T> {
687 fn is_focused(state: &Self::State) -> bool {
688 state.focused
689 }
690
691 fn set_focused(state: &mut Self::State, focused: bool) {
692 state.focused = focused;
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 #[derive(Clone, Debug, PartialEq)]
702 struct TestRow {
703 name: String,
704 value: String,
705 }
706
707 impl TestRow {
708 fn new(name: &str, value: &str) -> Self {
709 Self {
710 name: name.into(),
711 value: value.into(),
712 }
713 }
714 }
715
716 impl TableRow for TestRow {
717 fn cells(&self) -> Vec<String> {
718 vec![self.name.clone(), self.value.clone()]
719 }
720 }
721
722 fn test_columns() -> Vec<Column> {
723 vec![
724 Column::new("Name", Constraint::Length(10)).sortable(),
725 Column::new("Value", Constraint::Length(10)).sortable(),
726 ]
727 }
728
729 fn test_rows() -> Vec<TestRow> {
730 vec![
731 TestRow::new("Charlie", "30"),
732 TestRow::new("Alice", "10"),
733 TestRow::new("Bob", "20"),
734 ]
735 }
736
737 #[test]
740 fn test_tablerow_impl() {
741 let row = TestRow::new("Test", "123");
742 assert_eq!(row.cells(), vec!["Test", "123"]);
743 }
744
745 #[test]
746 fn test_tablerow_empty_cells() {
747 #[derive(Clone)]
748 struct EmptyRow;
749
750 impl TableRow for EmptyRow {
751 fn cells(&self) -> Vec<String> {
752 vec![]
753 }
754 }
755
756 let row = EmptyRow;
757 assert!(row.cells().is_empty());
758 }
759
760 #[test]
763 fn test_column_new() {
764 let col = Column::new("Header", Constraint::Length(15));
765 assert_eq!(col.header(), "Header");
766 assert!(!col.is_sortable());
767 }
768
769 #[test]
770 fn test_column_sortable() {
771 let col = Column::new("Header", Constraint::Length(15)).sortable();
772 assert!(col.is_sortable());
773 }
774
775 #[test]
776 fn test_column_clone() {
777 let col = Column::new("Header", Constraint::Length(15)).sortable();
778 let cloned = col.clone();
779 assert_eq!(cloned.header(), "Header");
780 assert!(cloned.is_sortable());
781 }
782
783 #[test]
784 fn test_column_width() {
785 let col = Column::new("Header", Constraint::Percentage(50));
786 assert_eq!(col.width(), Constraint::Percentage(50));
787 }
788
789 #[test]
792 fn test_sort_direction_toggle() {
793 assert_eq!(SortDirection::Ascending.toggle(), SortDirection::Descending);
794 assert_eq!(SortDirection::Descending.toggle(), SortDirection::Ascending);
795 }
796
797 #[test]
798 fn test_sort_direction_default() {
799 let dir: SortDirection = Default::default();
800 assert_eq!(dir, SortDirection::Ascending);
801 }
802
803 #[test]
806 fn test_new() {
807 let state = TableState::new(test_rows(), test_columns());
808 assert_eq!(state.len(), 3);
809 assert_eq!(state.selected_index(), Some(0));
810 assert!(state.sort().is_none());
811 }
812
813 #[test]
814 fn test_new_empty() {
815 let state: TableState<TestRow> = TableState::new(vec![], test_columns());
816 assert!(state.is_empty());
817 assert_eq!(state.selected_index(), None);
818 }
819
820 #[test]
821 fn test_with_selected() {
822 let state = TableState::with_selected(test_rows(), test_columns(), 2);
823 assert_eq!(state.selected_index(), Some(2));
824 }
825
826 #[test]
827 fn test_with_selected_clamps() {
828 let state = TableState::with_selected(test_rows(), test_columns(), 100);
829 assert_eq!(state.selected_index(), Some(2)); }
831
832 #[test]
833 fn test_default() {
834 let state: TableState<TestRow> = TableState::default();
835 assert!(state.is_empty());
836 assert_eq!(state.selected_index(), None);
837 assert!(state.columns().is_empty());
838 }
839
840 #[test]
843 fn test_rows_accessor() {
844 let state = TableState::new(test_rows(), test_columns());
845 assert_eq!(state.rows().len(), 3);
846 }
847
848 #[test]
849 fn test_columns_accessor() {
850 let state = TableState::new(test_rows(), test_columns());
851 assert_eq!(state.columns().len(), 2);
852 }
853
854 #[test]
855 fn test_selected_index() {
856 let state = TableState::with_selected(test_rows(), test_columns(), 1);
857 assert_eq!(state.selected_index(), Some(1));
858 }
859
860 #[test]
861 fn test_selected_row() {
862 let state = TableState::with_selected(test_rows(), test_columns(), 1);
863 let row = state.selected_row().unwrap();
864 assert_eq!(row.name, "Alice");
865 }
866
867 #[test]
868 fn test_sort() {
869 let state = TableState::new(test_rows(), test_columns());
870 assert!(state.sort().is_none());
871 }
872
873 #[test]
874 fn test_len() {
875 let state = TableState::new(test_rows(), test_columns());
876 assert_eq!(state.len(), 3);
877 }
878
879 #[test]
880 fn test_is_empty() {
881 let empty: TableState<TestRow> = TableState::new(vec![], vec![]);
882 assert!(empty.is_empty());
883
884 let not_empty = TableState::new(test_rows(), test_columns());
885 assert!(!not_empty.is_empty());
886 }
887
888 #[test]
891 fn test_set_rows() {
892 let mut state = TableState::new(test_rows(), test_columns());
893 state.set_rows(vec![TestRow::new("New", "1")]);
894 assert_eq!(state.len(), 1);
895 assert_eq!(state.selected_index(), Some(0));
896 }
897
898 #[test]
899 fn test_set_rows_preserves_selection() {
900 let mut state = TableState::with_selected(test_rows(), test_columns(), 1);
901 state.set_rows(vec![
902 TestRow::new("A", "1"),
903 TestRow::new("B", "2"),
904 TestRow::new("C", "3"),
905 ]);
906 assert_eq!(state.selected_index(), Some(1));
907 }
908
909 #[test]
910 fn test_set_rows_clamps_selection() {
911 let mut state = TableState::with_selected(test_rows(), test_columns(), 2);
912 state.set_rows(vec![TestRow::new("A", "1")]);
913 assert_eq!(state.selected_index(), Some(0)); }
915
916 #[test]
917 fn test_set_selected() {
918 let mut state = TableState::new(test_rows(), test_columns());
919 state.set_selected(Some(2));
920 assert_eq!(state.selected_index(), Some(2));
921
922 state.set_selected(None);
923 assert_eq!(state.selected_index(), None);
924 }
925
926 #[test]
927 fn test_disabled_accessors() {
928 let mut state = TableState::new(test_rows(), test_columns());
929 assert!(!state.is_disabled());
930
931 state.set_disabled(true);
932 assert!(state.is_disabled());
933
934 state.set_disabled(false);
935 assert!(!state.is_disabled());
936 }
937
938 #[test]
941 fn test_down() {
942 let mut state = TableState::new(test_rows(), test_columns());
943 let output = Table::<TestRow>::update(&mut state, TableMessage::Down);
944 assert_eq!(output, Some(TableOutput::SelectionChanged(1)));
945 assert_eq!(state.selected_index(), Some(1));
946 }
947
948 #[test]
949 fn test_down_at_last() {
950 let mut state = TableState::with_selected(test_rows(), test_columns(), 2);
951 let output = Table::<TestRow>::update(&mut state, TableMessage::Down);
952 assert_eq!(output, None);
953 assert_eq!(state.selected_index(), Some(2));
954 }
955
956 #[test]
957 fn test_up() {
958 let mut state = TableState::with_selected(test_rows(), test_columns(), 1);
959 let output = Table::<TestRow>::update(&mut state, TableMessage::Up);
960 assert_eq!(output, Some(TableOutput::SelectionChanged(0)));
961 assert_eq!(state.selected_index(), Some(0));
962 }
963
964 #[test]
965 fn test_up_at_first() {
966 let mut state = TableState::new(test_rows(), test_columns());
967 let output = Table::<TestRow>::update(&mut state, TableMessage::Up);
968 assert_eq!(output, None);
969 assert_eq!(state.selected_index(), Some(0));
970 }
971
972 #[test]
973 fn test_first() {
974 let mut state = TableState::with_selected(test_rows(), test_columns(), 2);
975 let output = Table::<TestRow>::update(&mut state, TableMessage::First);
976 assert_eq!(output, Some(TableOutput::SelectionChanged(0)));
977 assert_eq!(state.selected_index(), Some(0));
978 }
979
980 #[test]
981 fn test_first_already_first() {
982 let mut state = TableState::new(test_rows(), test_columns());
983 let output = Table::<TestRow>::update(&mut state, TableMessage::First);
984 assert_eq!(output, None);
985 }
986
987 #[test]
988 fn test_last() {
989 let mut state = TableState::new(test_rows(), test_columns());
990 let output = Table::<TestRow>::update(&mut state, TableMessage::Last);
991 assert_eq!(output, Some(TableOutput::SelectionChanged(2)));
992 assert_eq!(state.selected_index(), Some(2));
993 }
994
995 #[test]
996 fn test_last_already_last() {
997 let mut state = TableState::with_selected(test_rows(), test_columns(), 2);
998 let output = Table::<TestRow>::update(&mut state, TableMessage::Last);
999 assert_eq!(output, None);
1000 }
1001
1002 #[test]
1003 fn test_page_down() {
1004 let mut state = TableState::new(test_rows(), test_columns());
1005 let output = Table::<TestRow>::update(&mut state, TableMessage::PageDown(2));
1006 assert_eq!(output, Some(TableOutput::SelectionChanged(2)));
1007 }
1008
1009 #[test]
1010 fn test_page_up() {
1011 let mut state = TableState::with_selected(test_rows(), test_columns(), 2);
1012 let output = Table::<TestRow>::update(&mut state, TableMessage::PageUp(2));
1013 assert_eq!(output, Some(TableOutput::SelectionChanged(0)));
1014 }
1015
1016 #[test]
1017 fn test_select() {
1018 let mut state = TableState::with_selected(test_rows(), test_columns(), 1);
1019 let output = Table::<TestRow>::update(&mut state, TableMessage::Select);
1020 assert_eq!(
1021 output,
1022 Some(TableOutput::Selected(TestRow::new("Alice", "10")))
1023 );
1024 }
1025
1026 #[test]
1027 fn test_empty_navigation() {
1028 let mut state: TableState<TestRow> = TableState::new(vec![], test_columns());
1029
1030 assert_eq!(
1031 Table::<TestRow>::update(&mut state, TableMessage::Down),
1032 None
1033 );
1034 assert_eq!(Table::<TestRow>::update(&mut state, TableMessage::Up), None);
1035 assert_eq!(
1036 Table::<TestRow>::update(&mut state, TableMessage::Select),
1037 None
1038 );
1039 }
1040
1041 #[test]
1044 fn test_sort_ascending() {
1045 let mut state = TableState::new(test_rows(), test_columns());
1046 let output = Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1047 assert_eq!(
1048 output,
1049 Some(TableOutput::Sorted {
1050 column: 0,
1051 direction: SortDirection::Ascending,
1052 })
1053 );
1054
1055 assert_eq!(state.rows()[state.display_order[0]].name, "Alice");
1057 assert_eq!(state.rows()[state.display_order[1]].name, "Bob");
1058 assert_eq!(state.rows()[state.display_order[2]].name, "Charlie");
1059 }
1060
1061 #[test]
1062 fn test_sort_descending() {
1063 let mut state = TableState::new(test_rows(), test_columns());
1064 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); let output = Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); assert_eq!(
1067 output,
1068 Some(TableOutput::Sorted {
1069 column: 0,
1070 direction: SortDirection::Descending,
1071 })
1072 );
1073
1074 assert_eq!(state.rows()[state.display_order[0]].name, "Charlie");
1076 assert_eq!(state.rows()[state.display_order[1]].name, "Bob");
1077 assert_eq!(state.rows()[state.display_order[2]].name, "Alice");
1078 }
1079
1080 #[test]
1081 fn test_sort_clear() {
1082 let mut state = TableState::new(test_rows(), test_columns());
1083 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); let output = Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); assert_eq!(output, Some(TableOutput::SortCleared));
1087 assert!(state.sort().is_none());
1088
1089 assert_eq!(state.rows()[state.display_order[0]].name, "Charlie");
1091 assert_eq!(state.rows()[state.display_order[1]].name, "Alice");
1092 assert_eq!(state.rows()[state.display_order[2]].name, "Bob");
1093 }
1094
1095 #[test]
1096 fn test_sort_unsortable_column() {
1097 let columns = vec![
1098 Column::new("Name", Constraint::Length(10)), Column::new("Value", Constraint::Length(10)).sortable(),
1100 ];
1101 let mut state = TableState::new(test_rows(), columns);
1102 let output = Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1103 assert_eq!(output, None);
1104 }
1105
1106 #[test]
1107 fn test_sort_preserves_selection() {
1108 let mut state = TableState::with_selected(test_rows(), test_columns(), 1);
1109 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); let selected = state.selected_row().unwrap();
1115 assert_eq!(selected.name, "Alice");
1116 }
1117
1118 #[test]
1119 fn test_sort_numeric_strings() {
1120 let rows = vec![
1122 TestRow::new("Item", "9"),
1123 TestRow::new("Item", "10"),
1124 TestRow::new("Item", "2"),
1125 ];
1126 let columns = vec![
1127 Column::new("Name", Constraint::Length(10)),
1128 Column::new("Value", Constraint::Length(10)).sortable(),
1129 ];
1130 let mut state = TableState::new(rows, columns);
1131
1132 Table::<TestRow>::update(&mut state, TableMessage::SortBy(1));
1133
1134 assert_eq!(state.rows()[state.display_order[0]].value, "10");
1136 assert_eq!(state.rows()[state.display_order[1]].value, "2");
1137 assert_eq!(state.rows()[state.display_order[2]].value, "9");
1138 }
1139
1140 #[test]
1141 fn test_clear_sort() {
1142 let mut state = TableState::new(test_rows(), test_columns());
1143 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1144 assert!(state.sort().is_some());
1145
1146 let output = Table::<TestRow>::update(&mut state, TableMessage::ClearSort);
1147 assert_eq!(output, Some(TableOutput::SortCleared));
1148 assert!(state.sort().is_none());
1149 }
1150
1151 #[test]
1152 fn test_clear_sort_when_not_sorted() {
1153 let mut state = TableState::new(test_rows(), test_columns());
1154 let output = Table::<TestRow>::update(&mut state, TableMessage::ClearSort);
1155 assert_eq!(output, None);
1156 }
1157
1158 #[test]
1159 fn test_sort_different_column() {
1160 let mut state = TableState::new(test_rows(), test_columns());
1161
1162 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1164 assert_eq!(state.sort(), Some((0, SortDirection::Ascending)));
1165
1166 let output = Table::<TestRow>::update(&mut state, TableMessage::SortBy(1));
1168 assert_eq!(
1169 output,
1170 Some(TableOutput::Sorted {
1171 column: 1,
1172 direction: SortDirection::Ascending,
1173 })
1174 );
1175 }
1176
1177 #[test]
1180 fn test_disabled() {
1181 let mut state = TableState::new(test_rows(), test_columns());
1182 state.set_disabled(true);
1183
1184 assert_eq!(
1185 Table::<TestRow>::update(&mut state, TableMessage::Down),
1186 None
1187 );
1188 assert_eq!(Table::<TestRow>::update(&mut state, TableMessage::Up), None);
1189 assert_eq!(
1190 Table::<TestRow>::update(&mut state, TableMessage::Select),
1191 None
1192 );
1193 assert_eq!(
1194 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)),
1195 None
1196 );
1197 }
1198
1199 #[test]
1202 fn test_focusable() {
1203 let mut state = TableState::new(test_rows(), test_columns());
1204 assert!(!Table::<TestRow>::is_focused(&state));
1205
1206 Table::<TestRow>::set_focused(&mut state, true);
1207 assert!(Table::<TestRow>::is_focused(&state));
1208
1209 Table::<TestRow>::blur(&mut state);
1210 assert!(!Table::<TestRow>::is_focused(&state));
1211
1212 Table::<TestRow>::focus(&mut state);
1213 assert!(Table::<TestRow>::is_focused(&state));
1214 }
1215
1216 #[test]
1219 fn test_view_renders() {
1220 use crate::backend::CaptureBackend;
1221 use ratatui::Terminal;
1222
1223 let state = TableState::new(test_rows(), test_columns());
1224
1225 let backend = CaptureBackend::new(40, 10);
1226 let mut terminal = Terminal::new(backend).unwrap();
1227
1228 terminal
1229 .draw(|frame| {
1230 Table::<TestRow>::view(&state, frame, frame.area());
1231 })
1232 .unwrap();
1233
1234 let output = terminal.backend().to_string();
1235 assert!(output.contains("Name"));
1236 assert!(output.contains("Value"));
1237 assert!(output.contains("Charlie"));
1238 assert!(output.contains("Alice"));
1239 assert!(output.contains("Bob"));
1240 }
1241
1242 #[test]
1243 fn test_view_with_header() {
1244 use crate::backend::CaptureBackend;
1245 use ratatui::Terminal;
1246
1247 let state = TableState::new(test_rows(), test_columns());
1248
1249 let backend = CaptureBackend::new(40, 10);
1250 let mut terminal = Terminal::new(backend).unwrap();
1251
1252 terminal
1253 .draw(|frame| {
1254 Table::<TestRow>::view(&state, frame, frame.area());
1255 })
1256 .unwrap();
1257
1258 let output = terminal.backend().to_string();
1259 assert!(output.contains("Name"));
1260 assert!(output.contains("Value"));
1261 }
1262
1263 #[test]
1264 fn test_view_with_sort_indicator() {
1265 use crate::backend::CaptureBackend;
1266 use ratatui::Terminal;
1267
1268 let mut state = TableState::new(test_rows(), test_columns());
1269 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1270
1271 let backend = CaptureBackend::new(40, 10);
1272 let mut terminal = Terminal::new(backend).unwrap();
1273
1274 terminal
1275 .draw(|frame| {
1276 Table::<TestRow>::view(&state, frame, frame.area());
1277 })
1278 .unwrap();
1279
1280 let output = terminal.backend().to_string();
1281 assert!(output.contains("↑")); }
1283
1284 #[test]
1285 fn test_view_focused() {
1286 use crate::backend::CaptureBackend;
1287 use ratatui::Terminal;
1288
1289 let mut state = TableState::new(test_rows(), test_columns());
1290 state.focused = true;
1291
1292 let backend = CaptureBackend::new(40, 10);
1293 let mut terminal = Terminal::new(backend).unwrap();
1294
1295 terminal
1296 .draw(|frame| {
1297 Table::<TestRow>::view(&state, frame, frame.area());
1298 })
1299 .unwrap();
1300
1301 let _output = terminal.backend().to_string();
1303 }
1304
1305 #[test]
1306 fn test_view_disabled() {
1307 use crate::backend::CaptureBackend;
1308 use ratatui::Terminal;
1309
1310 let mut state = TableState::new(test_rows(), test_columns());
1311 state.disabled = true;
1312
1313 let backend = CaptureBackend::new(40, 10);
1314 let mut terminal = Terminal::new(backend).unwrap();
1315
1316 terminal
1317 .draw(|frame| {
1318 Table::<TestRow>::view(&state, frame, frame.area());
1319 })
1320 .unwrap();
1321
1322 let _output = terminal.backend().to_string();
1324 }
1325
1326 #[test]
1327 fn test_view_empty() {
1328 use crate::backend::CaptureBackend;
1329 use ratatui::Terminal;
1330
1331 let state: TableState<TestRow> = TableState::new(vec![], test_columns());
1332
1333 let backend = CaptureBackend::new(40, 10);
1334 let mut terminal = Terminal::new(backend).unwrap();
1335
1336 terminal
1337 .draw(|frame| {
1338 Table::<TestRow>::view(&state, frame, frame.area());
1339 })
1340 .unwrap();
1341
1342 let output = terminal.backend().to_string();
1344 assert!(output.contains("Name")); }
1346
1347 #[test]
1350 fn test_clone() {
1351 let mut state = TableState::with_selected(test_rows(), test_columns(), 1);
1352 state.focused = true;
1353 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1354
1355 let cloned = state.clone();
1356 assert_eq!(cloned.selected_index(), Some(0)); assert!(cloned.focused);
1358 assert!(cloned.sort().is_some());
1359 }
1360
1361 #[test]
1362 fn test_init() {
1363 let state: TableState<TestRow> = Table::<TestRow>::init();
1364 assert!(state.is_empty());
1365 assert!(!state.focused);
1366 assert!(!state.disabled);
1367 }
1368
1369 #[test]
1370 fn test_full_workflow() {
1371 let mut state = TableState::new(test_rows(), test_columns());
1372 Table::<TestRow>::set_focused(&mut state, true);
1373
1374 Table::<TestRow>::update(&mut state, TableMessage::Down);
1376 Table::<TestRow>::update(&mut state, TableMessage::Down);
1377 assert_eq!(state.selected_index(), Some(2));
1378
1379 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1381 Table::<TestRow>::update(&mut state, TableMessage::First);
1385 assert_eq!(state.selected_row().unwrap().name, "Alice");
1386
1387 let output = Table::<TestRow>::update(&mut state, TableMessage::Select);
1389 assert_eq!(
1390 output,
1391 Some(TableOutput::Selected(TestRow::new("Alice", "10")))
1392 );
1393 }
1394
1395 #[test]
1396 fn test_navigation_with_sort() {
1397 let mut state = TableState::new(test_rows(), test_columns());
1398
1399 Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1403
1404 assert_eq!(state.selected_row().unwrap().name, "Charlie");
1407 assert_eq!(state.selected_index(), Some(2));
1408
1409 Table::<TestRow>::update(&mut state, TableMessage::First);
1411 assert_eq!(state.selected_row().unwrap().name, "Alice");
1412
1413 Table::<TestRow>::update(&mut state, TableMessage::Down);
1414 assert_eq!(state.selected_row().unwrap().name, "Bob");
1415
1416 Table::<TestRow>::update(&mut state, TableMessage::Down);
1417 assert_eq!(state.selected_row().unwrap().name, "Charlie");
1418 }
1419
1420 #[test]
1421 fn test_sort_out_of_bounds_column() {
1422 let mut state = TableState::new(test_rows(), test_columns());
1423 let output = Table::<TestRow>::update(&mut state, TableMessage::SortBy(99));
1424 assert_eq!(output, None);
1425 }
1426
1427 #[test]
1428 fn test_page_navigation_bounds() {
1429 let mut state = TableState::new(test_rows(), test_columns());
1430
1431 let output = Table::<TestRow>::update(&mut state, TableMessage::PageDown(100));
1433 assert_eq!(output, Some(TableOutput::SelectionChanged(2)));
1434
1435 let output = Table::<TestRow>::update(&mut state, TableMessage::PageUp(100));
1437 assert_eq!(output, Some(TableOutput::SelectionChanged(0)));
1438 }
1439}