envision/component/
table.rs

1//! A data table component with row selection and column sorting.
2//!
3//! `Table` provides a tabular data display with keyboard navigation,
4//! row selection, and column sorting capabilities.
5//!
6//! # Example
7//!
8//! ```rust
9//! use envision::component::{
10//!     Column, Component, Focusable, SortDirection, Table, TableMessage, TableOutput,
11//!     TableRow, TableState,
12//! };
13//! use ratatui::layout::Constraint;
14//!
15//! // Define your row type
16//! #[derive(Clone, Debug, PartialEq)]
17//! struct User {
18//!     name: String,
19//!     email: String,
20//! }
21//!
22//! impl TableRow for User {
23//!     fn cells(&self) -> Vec<String> {
24//!         vec![self.name.clone(), self.email.clone()]
25//!     }
26//! }
27//!
28//! // Create table state
29//! let users = vec![
30//!     User { name: "Alice".into(), email: "alice@example.com".into() },
31//!     User { name: "Bob".into(), email: "bob@example.com".into() },
32//! ];
33//!
34//! let columns = vec![
35//!     Column::new("Name", Constraint::Length(15)).sortable(),
36//!     Column::new("Email", Constraint::Length(25)),
37//! ];
38//!
39//! let mut state = TableState::new(users, columns);
40//! Table::set_focused(&mut state, true);
41//!
42//! // Navigate down
43//! let output = Table::<User>::update(&mut state, TableMessage::Down);
44//! assert_eq!(output, Some(TableOutput::SelectionChanged(1)));
45//!
46//! // Sort by name column
47//! let output = Table::<User>::update(&mut state, TableMessage::SortBy(0));
48//! assert_eq!(output, Some(TableOutput::Sorted {
49//!     column: 0,
50//!     direction: SortDirection::Ascending,
51//! }));
52//! ```
53
54use std::marker::PhantomData;
55
56use ratatui::layout::Constraint;
57use ratatui::prelude::*;
58use ratatui::widgets::{Block, Borders, Cell, Row};
59
60use super::{Component, Focusable};
61
62/// Trait for types that can be displayed as table rows.
63///
64/// Implement this trait for your data types to use them with `Table`.
65///
66/// # Example
67///
68/// ```rust
69/// use envision::component::TableRow;
70///
71/// #[derive(Clone)]
72/// struct Product {
73///     name: String,
74///     price: f64,
75///     quantity: u32,
76/// }
77///
78/// impl TableRow for Product {
79///     fn cells(&self) -> Vec<String> {
80///         vec![
81///             self.name.clone(),
82///             format!("${:.2}", self.price),
83///             self.quantity.to_string(),
84///         ]
85///     }
86/// }
87/// ```
88pub trait TableRow: Clone {
89    /// Returns the cell values for this row.
90    ///
91    /// The order of values should match the order of columns
92    /// defined in the table.
93    fn cells(&self) -> Vec<String>;
94}
95
96/// Column definition for a table.
97///
98/// Columns define the header text, width, and whether the column
99/// is sortable.
100///
101/// # Example
102///
103/// ```rust
104/// use envision::component::Column;
105/// use ratatui::layout::Constraint;
106///
107/// let col = Column::new("Name", Constraint::Length(20)).sortable();
108/// assert_eq!(col.header(), "Name");
109/// assert!(col.is_sortable());
110/// ```
111#[derive(Clone, Debug)]
112pub struct Column {
113    header: String,
114    width: Constraint,
115    sortable: bool,
116}
117
118impl Column {
119    /// Creates a new column with the given header and width.
120    ///
121    /// The column is not sortable by default.
122    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    /// Makes this column sortable.
131    ///
132    /// Sortable columns can be sorted by clicking/selecting the header
133    /// or using `TableMessage::SortBy`.
134    pub fn sortable(mut self) -> Self {
135        self.sortable = true;
136        self
137    }
138
139    /// Returns the column header text.
140    pub fn header(&self) -> &str {
141        &self.header
142    }
143
144    /// Returns the column width constraint.
145    pub fn width(&self) -> Constraint {
146        self.width
147    }
148
149    /// Returns whether this column is sortable.
150    pub fn is_sortable(&self) -> bool {
151        self.sortable
152    }
153}
154
155/// Sort direction for table columns.
156#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
157pub enum SortDirection {
158    /// Sort in ascending order (A-Z, 0-9).
159    #[default]
160    Ascending,
161    /// Sort in descending order (Z-A, 9-0).
162    Descending,
163}
164
165impl SortDirection {
166    /// Returns the opposite sort direction.
167    pub fn toggle(self) -> Self {
168        match self {
169            SortDirection::Ascending => SortDirection::Descending,
170            SortDirection::Descending => SortDirection::Ascending,
171        }
172    }
173}
174
175/// Messages that can be sent to a Table component.
176#[derive(Clone, Debug, PartialEq, Eq)]
177pub enum TableMessage {
178    /// Move selection up by one row.
179    Up,
180    /// Move selection down by one row.
181    Down,
182    /// Move selection to the first row.
183    First,
184    /// Move selection to the last row.
185    Last,
186    /// Move selection up by a page.
187    PageUp(usize),
188    /// Move selection down by a page.
189    PageDown(usize),
190    /// Confirm the current selection.
191    Select,
192    /// Sort by the given column index.
193    ///
194    /// If already sorted by this column, toggles direction.
195    /// If sorted ascending, becomes descending.
196    /// If sorted descending, clears the sort.
197    SortBy(usize),
198    /// Clear the current sort, returning to original order.
199    ClearSort,
200}
201
202/// Output messages from a Table component.
203#[derive(Clone, Debug, PartialEq, Eq)]
204pub enum TableOutput<T: Clone> {
205    /// A row was selected (e.g., Enter pressed).
206    Selected(T),
207    /// The selection changed to a new row index.
208    SelectionChanged(usize),
209    /// The sort changed.
210    Sorted {
211        /// The column being sorted by.
212        column: usize,
213        /// The sort direction.
214        direction: SortDirection,
215    },
216    /// Sort was cleared.
217    SortCleared,
218}
219
220/// State for a Table component.
221///
222/// Holds the rows, columns, selection state, and sort configuration.
223#[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    /// Creates a new table state with the given rows and columns.
250    ///
251    /// If there are rows, the first row is selected by default.
252    ///
253    /// # Example
254    ///
255    /// ```rust
256    /// use envision::component::{Column, TableRow, TableState};
257    /// use ratatui::layout::Constraint;
258    ///
259    /// #[derive(Clone)]
260    /// struct Item { name: String }
261    ///
262    /// impl TableRow for Item {
263    ///     fn cells(&self) -> Vec<String> {
264    ///         vec![self.name.clone()]
265    ///     }
266    /// }
267    ///
268    /// let state = TableState::new(
269    ///     vec![Item { name: "A".into() }, Item { name: "B".into() }],
270    ///     vec![Column::new("Name", Constraint::Length(10))],
271    /// );
272    /// assert_eq!(state.len(), 2);
273    /// assert_eq!(state.selected_index(), Some(0));
274    /// ```
275    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    /// Creates a table state with a specific row selected.
290    ///
291    /// The index is clamped to the valid range.
292    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    /// Returns a reference to the rows.
311    pub fn rows(&self) -> &[T] {
312        &self.rows
313    }
314
315    /// Returns a reference to the columns.
316    pub fn columns(&self) -> &[Column] {
317        &self.columns
318    }
319
320    /// Returns the currently selected display index.
321    ///
322    /// This is the index in the display order, not the original row index.
323    pub fn selected_index(&self) -> Option<usize> {
324        self.selected
325    }
326
327    /// Returns a reference to the currently selected row.
328    ///
329    /// Returns `None` if no row is selected or the table is empty.
330    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    /// Returns the current sort configuration.
337    ///
338    /// Returns `None` if no sort is applied.
339    pub fn sort(&self) -> Option<(usize, SortDirection)> {
340        self.sort
341    }
342
343    /// Returns the number of rows.
344    pub fn len(&self) -> usize {
345        self.rows.len()
346    }
347
348    /// Returns `true` if the table has no rows.
349    pub fn is_empty(&self) -> bool {
350        self.rows.is_empty()
351    }
352
353    /// Sets the rows, resetting sort and adjusting selection.
354    ///
355    /// If there were rows selected, the selection is preserved if valid,
356    /// otherwise clamped to the last row.
357    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    /// Sets the selected row by display index.
372    ///
373    /// Pass `None` to clear the selection.
374    /// Out of bounds indices are ignored.
375    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(_) => {} // Out of bounds, ignore
379            None => self.selected = None,
380        }
381    }
382
383    /// Returns `true` if the table is disabled.
384    pub fn is_disabled(&self) -> bool {
385        self.disabled
386    }
387
388    /// Sets the disabled state.
389    ///
390    /// Disabled tables do not respond to messages.
391    pub fn set_disabled(&mut self, disabled: bool) {
392        self.disabled = disabled;
393    }
394
395    /// Applies the current sort to the display order.
396    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            // Reset to original order
409            self.display_order = (0..self.rows.len()).collect();
410        }
411    }
412
413    /// Finds the display index of the given original row index.
414    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
421/// A data table component with row selection and column sorting.
422///
423/// `Table` displays tabular data with support for keyboard navigation,
424/// single row selection, and column sorting. It uses a generic row type
425/// that implements the [`TableRow`] trait.
426///
427/// # Type Parameters
428///
429/// - `T`: The row data type. Must implement [`TableRow`] and `Clone`.
430///
431/// # Navigation
432///
433/// - `Up` / `Down` - Move selection by one row
434/// - `First` / `Last` - Jump to beginning/end
435/// - `PageUp` / `PageDown` - Move by page size
436/// - `Select` - Confirm the current selection
437/// - `SortBy(column)` - Sort by the given column
438/// - `ClearSort` - Clear the current sort
439///
440/// # Sorting
441///
442/// Clicking the same column cycles through: Ascending -> Descending -> None.
443/// Only columns marked as `sortable()` can be sorted.
444///
445/// # Example
446///
447/// ```rust
448/// use envision::component::{
449///     Column, Component, Table, TableMessage, TableRow, TableState,
450/// };
451/// use ratatui::layout::Constraint;
452///
453/// #[derive(Clone, Debug, PartialEq)]
454/// struct Person {
455///     name: String,
456///     age: u32,
457/// }
458///
459/// impl TableRow for Person {
460///     fn cells(&self) -> Vec<String> {
461///         vec![self.name.clone(), self.age.to_string()]
462///     }
463/// }
464///
465/// let people = vec![
466///     Person { name: "Alice".into(), age: 30 },
467///     Person { name: "Bob".into(), age: 25 },
468/// ];
469///
470/// let columns = vec![
471///     Column::new("Name", Constraint::Length(15)).sortable(),
472///     Column::new("Age", Constraint::Length(5)).sortable(),
473/// ];
474///
475/// let mut state = TableState::new(people, columns);
476///
477/// // Navigate and select
478/// Table::<Person>::update(&mut state, TableMessage::Down);
479/// let output = Table::<Person>::update(&mut state, TableMessage::Select);
480/// // output is Some(TableOutput::Selected(Person { name: "Bob", age: 25 }))
481/// ```
482pub 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                // Check if column exists and is sortable
550                if let Some(column) = state.columns.get(col) {
551                    if !column.is_sortable() {
552                        return None;
553                    }
554
555                    // Get the currently selected row's original index
556                    let selected_original = state
557                        .selected
558                        .and_then(|i| state.display_order.get(i).copied());
559
560                    // Toggle sort: None -> Asc -> Desc -> None
561                    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                    // Restore selection to the same row
573                    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                    // Get the currently selected row's original index
588                    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                    // Restore selection to the same row
596                    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        // Build header row with sort indicators
610        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        // Build data rows using display_order
637        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        // Use TableState for stateful rendering
680        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    // Test row type
701    #[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    // TableRow Trait Tests
738
739    #[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    // Column Tests
761
762    #[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    // SortDirection Tests
790
791    #[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    // State Creation Tests
804
805    #[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)); // Clamped to last
830    }
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    // Accessors Tests
841
842    #[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    // Mutators Tests
889
890    #[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)); // Clamped
914    }
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    // Navigation Tests
939
940    #[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    // Sorting Tests
1042
1043    #[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        // Check order: Alice, Bob, Charlie
1056        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)); // Ascending
1065        let output = Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); // Descending
1066        assert_eq!(
1067            output,
1068            Some(TableOutput::Sorted {
1069                column: 0,
1070                direction: SortDirection::Descending,
1071            })
1072        );
1073
1074        // Check order: Charlie, Bob, Alice
1075        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)); // Ascending
1084        Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); // Descending
1085        let output = Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); // Clear
1086        assert_eq!(output, Some(TableOutput::SortCleared));
1087        assert!(state.sort().is_none());
1088
1089        // Back to original order: Charlie, Alice, Bob
1090        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)), // Not sortable
1099            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        // Initially selected: Alice (index 1 in original order)
1110
1111        Table::<TestRow>::update(&mut state, TableMessage::SortBy(0)); // Sort ascending
1112
1113        // After sort, Alice should still be selected but at a different display index
1114        let selected = state.selected_row().unwrap();
1115        assert_eq!(selected.name, "Alice");
1116    }
1117
1118    #[test]
1119    fn test_sort_numeric_strings() {
1120        // Numeric strings sort lexicographically, not numerically
1121        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        // Lexicographic: "10" < "2" < "9"
1135        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        // Sort by column 0
1163        Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1164        assert_eq!(state.sort(), Some((0, SortDirection::Ascending)));
1165
1166        // Sort by column 1 - should reset to ascending on new column
1167        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    // Disabled State Tests
1178
1179    #[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    // Focus Tests
1200
1201    #[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    // View Tests
1217
1218    #[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("↑")); // Ascending indicator
1282    }
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        // Should render without panicking
1302        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        // Should render without panicking
1323        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        // Should render without panicking
1343        let output = terminal.backend().to_string();
1344        assert!(output.contains("Name")); // Headers still shown
1345    }
1346
1347    // Integration Tests
1348
1349    #[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)); // Alice is now at position 0 after sort
1357        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        // Navigate
1375        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        // Sort
1380        Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1381        // Selection should follow the row, not the position
1382
1383        // Navigate after sort
1384        Table::<TestRow>::update(&mut state, TableMessage::First);
1385        assert_eq!(state.selected_row().unwrap().name, "Alice");
1386
1387        // Select
1388        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        // Initially selected: Charlie (position 0 in original order)
1400
1401        // Sort ascending by name
1402        Table::<TestRow>::update(&mut state, TableMessage::SortBy(0));
1403
1404        // Now display order is: Alice, Bob, Charlie
1405        // But selection is preserved on the same ROW (Charlie), now at position 2
1406        assert_eq!(state.selected_row().unwrap().name, "Charlie");
1407        assert_eq!(state.selected_index(), Some(2));
1408
1409        // Navigate to first to get to Alice
1410        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        // PageDown beyond end
1432        let output = Table::<TestRow>::update(&mut state, TableMessage::PageDown(100));
1433        assert_eq!(output, Some(TableOutput::SelectionChanged(2)));
1434
1435        // PageUp beyond start
1436        let output = Table::<TestRow>::update(&mut state, TableMessage::PageUp(100));
1437        assert_eq!(output, Some(TableOutput::SelectionChanged(0)));
1438    }
1439}