1use crate::datatable::{DataTable, DataValue};
2use crossterm::event::{KeyCode, KeyEvent};
3use ratatui::layout::Constraint;
4use ratatui::style::{Color, Modifier, Style};
5use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table};
6
7#[derive(Debug, Clone)]
9pub struct SortConfig {
10 pub column_index: usize,
11 pub order: SortOrder,
12}
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum SortOrder {
16 Ascending,
17 Descending,
18}
19
20#[derive(Debug, Clone)]
22pub struct FilterConfig {
23 pub pattern: String,
24 pub column_index: Option<usize>, pub case_sensitive: bool,
26}
27
28#[derive(Debug, Clone)]
30pub struct SearchState {
31 pub pattern: String,
32 pub current_match: Option<(usize, usize)>, pub matches: Vec<(usize, usize)>,
34 pub case_sensitive: bool,
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub enum ViewMode {
40 Normal, Filtering, Searching, Sorting, }
45
46#[derive(Debug, Clone)]
48pub struct SimpleInput {
49 pub text: String,
50 pub cursor_position: usize,
51}
52
53impl Default for SimpleInput {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl SimpleInput {
60 #[must_use]
61 pub fn new() -> Self {
62 Self {
63 text: String::new(),
64 cursor_position: 0,
65 }
66 }
67
68 pub fn clear(&mut self) {
69 self.text.clear();
70 self.cursor_position = 0;
71 }
72
73 pub fn insert_char(&mut self, ch: char) {
74 self.text.insert(self.cursor_position, ch);
75 self.cursor_position += 1;
76 }
77
78 pub fn delete_char(&mut self) {
79 if self.cursor_position > 0 {
80 self.cursor_position -= 1;
81 self.text.remove(self.cursor_position);
82 }
83 }
84
85 pub fn move_cursor_left(&mut self) {
86 if self.cursor_position > 0 {
87 self.cursor_position -= 1;
88 }
89 }
90
91 pub fn move_cursor_right(&mut self) {
92 if self.cursor_position < self.text.len() {
93 self.cursor_position += 1;
94 }
95 }
96
97 pub fn move_cursor_home(&mut self) {
98 self.cursor_position = 0;
99 }
100
101 pub fn move_cursor_end(&mut self) {
102 self.cursor_position = self.text.len();
103 }
104}
105
106#[derive(Clone)]
108pub struct DataTableView {
109 table: DataTable,
111
112 mode: ViewMode,
114
115 sort: Option<SortConfig>,
117 filter: Option<FilterConfig>,
118 search: Option<SearchState>,
119
120 filter_input: SimpleInput,
122 search_input: SimpleInput,
123
124 pub visible_rows: Vec<usize>, pub column_widths: Vec<u16>, pub selected_row: usize,
130 pub selected_col: usize,
131 pub scroll_offset: usize, pub horizontal_scroll: usize, pub page_size: usize, pub visible_col_start: usize, pub visible_col_end: usize, }
137
138impl DataTableView {
139 #[must_use]
141 pub fn new(table: DataTable) -> Self {
142 let visible_rows: Vec<usize> = (0..table.row_count()).collect();
143 let column_widths = Self::calculate_column_widths(&table, &visible_rows);
144 let column_count = table.column_count();
145
146 Self {
147 table,
148 mode: ViewMode::Normal,
149 sort: None,
150 filter: None,
151 search: None,
152 filter_input: SimpleInput::new(),
153 search_input: SimpleInput::new(),
154 visible_rows,
155 column_widths,
156 selected_row: 0,
157 selected_col: 0,
158 scroll_offset: 0,
159 horizontal_scroll: 0,
160 page_size: 30, visible_col_start: 0,
162 visible_col_end: column_count, }
164 }
165
166 #[must_use]
168 pub fn table(&self) -> &DataTable {
169 &self.table
170 }
171
172 pub fn table_mut(&mut self) -> &mut DataTable {
174 &mut self.table
175 }
176
177 #[must_use]
179 pub fn get_datatable(&self) -> &DataTable {
180 &self.table
181 }
182
183 pub fn get_datatable_mut(&mut self) -> &mut DataTable {
185 &mut self.table
186 }
187
188 pub fn update_viewport(&mut self, terminal_width: u16, terminal_height: u16) {
190 let mut total_width = 0u16;
192 let mut end_col = self.visible_col_start;
193
194 for i in self.visible_col_start..self.column_widths.len() {
195 let col_width = self.column_widths[i];
196 if total_width + col_width + 1 > terminal_width.saturating_sub(2) {
197 break; }
199 total_width += col_width + 1;
200 end_col = i + 1;
201 }
202
203 if end_col == self.visible_col_start && self.visible_col_start < self.column_widths.len() {
205 end_col = self.visible_col_start + 1;
206 }
207
208 self.visible_col_end = end_col;
209
210 self.page_size = (terminal_height.saturating_sub(9) as usize).max(10);
213 }
214
215 #[must_use]
217 pub fn mode(&self) -> ViewMode {
218 self.mode.clone()
219 }
220
221 #[must_use]
223 pub fn visible_row_count(&self) -> usize {
224 self.visible_rows.len()
225 }
226
227 pub fn apply_filter(
229 &mut self,
230 pattern: String,
231 column_index: Option<usize>,
232 case_sensitive: bool,
233 ) {
234 self.filter = Some(FilterConfig {
235 pattern: pattern.clone(),
236 column_index,
237 case_sensitive,
238 });
239
240 self.update_visible_rows();
241 self.selected_row = 0; self.scroll_offset = 0;
243 }
244
245 pub fn clear_filter(&mut self) {
247 self.filter = None;
248 self.update_visible_rows();
249 self.selected_row = 0;
250 self.scroll_offset = 0;
251 }
252
253 pub fn apply_sort(&mut self, column_index: usize, order: SortOrder) {
255 self.sort = Some(SortConfig {
256 column_index,
257 order,
258 });
259 self.update_visible_rows();
260 self.selected_row = 0;
261 self.scroll_offset = 0;
262 }
263
264 pub fn clear_sort(&mut self) {
266 self.sort = None;
267 self.update_visible_rows();
268 }
269
270 pub fn start_search(&mut self, pattern: String, case_sensitive: bool) {
272 let matches = self.find_matches(&pattern, case_sensitive);
273 let current_match = matches.first().copied();
274
275 self.search = Some(SearchState {
276 pattern,
277 current_match,
278 matches,
279 case_sensitive,
280 });
281
282 if let Some((row_idx, _)) = current_match {
284 if let Some(visible_pos) = self.visible_rows.iter().position(|&r| r == row_idx) {
285 self.selected_row = visible_pos;
286 self.ensure_row_visible(visible_pos);
287 }
288 }
289 }
290
291 pub fn next_search_match(&mut self) {
293 if let Some(ref mut search) = self.search {
294 if let Some(current) = search.current_match {
295 if let Some(current_idx) = search.matches.iter().position(|&m| m == current) {
296 let next_idx = (current_idx + 1) % search.matches.len();
297 search.current_match = search.matches.get(next_idx).copied();
298
299 if let Some((row_idx, _)) = search.current_match {
300 if let Some(visible_pos) =
301 self.visible_rows.iter().position(|&r| r == row_idx)
302 {
303 self.selected_row = visible_pos;
304 self.ensure_row_visible(visible_pos);
305 }
306 }
307 }
308 }
309 }
310 }
311
312 pub fn prev_search_match(&mut self) {
314 if let Some(ref mut search) = self.search {
315 if let Some(current) = search.current_match {
316 if let Some(current_idx) = search.matches.iter().position(|&m| m == current) {
317 let prev_idx = if current_idx == 0 {
318 search.matches.len() - 1
319 } else {
320 current_idx - 1
321 };
322 search.current_match = search.matches.get(prev_idx).copied();
323
324 if let Some((row_idx, _)) = search.current_match {
325 if let Some(visible_pos) =
326 self.visible_rows.iter().position(|&r| r == row_idx)
327 {
328 self.selected_row = visible_pos;
329 self.ensure_row_visible(visible_pos);
330 }
331 }
332 }
333 }
334 }
335 }
336
337 pub fn clear_search(&mut self) {
339 self.search = None;
340 }
341
342 pub fn enter_filter_mode(&mut self) {
344 self.mode = ViewMode::Filtering;
345 self.filter_input.clear();
346 }
347
348 pub fn enter_search_mode(&mut self) {
350 self.mode = ViewMode::Searching;
351 self.search_input.clear();
352 }
353
354 pub fn exit_special_mode(&mut self) {
356 self.mode = ViewMode::Normal;
357 }
358
359 pub fn handle_navigation(&mut self, key: KeyEvent) -> bool {
361 if self.mode != ViewMode::Normal {
362 return false;
363 }
364
365 match key.code {
366 KeyCode::Up => {
367 if self.selected_row > 0 {
368 self.selected_row -= 1;
369 self.ensure_row_visible(self.selected_row);
370 }
371 true
372 }
373 KeyCode::Down => {
374 if self.selected_row + 1 < self.visible_rows.len() {
375 self.selected_row += 1;
376 self.ensure_row_visible(self.selected_row);
377 }
378 true
379 }
380 KeyCode::Left => {
381 if self.selected_col > 0 {
382 self.selected_col -= 1;
383 self.ensure_column_visible(self.selected_col);
384 }
385 true
386 }
387 KeyCode::Right => {
388 if self.selected_col + 1 < self.table.column_count() {
389 self.selected_col += 1;
390 self.ensure_column_visible(self.selected_col);
391 }
392 true
393 }
394 KeyCode::PageUp => {
395 let jump = self.page_size.min(self.selected_row);
396 self.selected_row -= jump;
397 self.ensure_row_visible(self.selected_row);
398 true
399 }
400 KeyCode::PageDown => {
401 let jump = self
402 .page_size
403 .min(self.visible_rows.len() - self.selected_row - 1);
404 self.selected_row += jump;
405 self.ensure_row_visible(self.selected_row);
406 true
407 }
408 KeyCode::Home => {
409 self.selected_row = 0;
410 self.scroll_offset = 0;
411 true
412 }
413 KeyCode::End => {
414 if !self.visible_rows.is_empty() {
415 self.selected_row = self.visible_rows.len() - 1;
416 self.ensure_row_visible(self.selected_row);
417 }
418 true
419 }
420 _ => false,
421 }
422 }
423
424 pub fn handle_filter_input(&mut self, key: KeyEvent) -> bool {
426 if self.mode != ViewMode::Filtering {
427 return false;
428 }
429
430 match key.code {
431 KeyCode::Char(c) => {
432 self.filter_input.insert_char(c);
433 true
434 }
435 KeyCode::Backspace => {
436 self.filter_input.delete_char();
437 true
438 }
439 KeyCode::Left => {
440 self.filter_input.move_cursor_left();
441 true
442 }
443 KeyCode::Right => {
444 self.filter_input.move_cursor_right();
445 true
446 }
447 KeyCode::Home => {
448 self.filter_input.move_cursor_home();
449 true
450 }
451 KeyCode::End => {
452 self.filter_input.move_cursor_end();
453 true
454 }
455 KeyCode::Enter => {
456 self.apply_filter(self.filter_input.text.clone(), None, false);
458 self.exit_special_mode();
459 true
460 }
461 KeyCode::Esc => {
462 self.exit_special_mode();
463 true
464 }
465 _ => false,
466 }
467 }
468
469 pub fn handle_search_input(&mut self, key: KeyEvent) -> bool {
471 if self.mode != ViewMode::Searching {
472 return false;
473 }
474
475 match key.code {
476 KeyCode::Char(c) => {
477 self.search_input.insert_char(c);
478 true
479 }
480 KeyCode::Backspace => {
481 self.search_input.delete_char();
482 true
483 }
484 KeyCode::Left => {
485 self.search_input.move_cursor_left();
486 true
487 }
488 KeyCode::Right => {
489 self.search_input.move_cursor_right();
490 true
491 }
492 KeyCode::Home => {
493 self.search_input.move_cursor_home();
494 true
495 }
496 KeyCode::End => {
497 self.search_input.move_cursor_end();
498 true
499 }
500 KeyCode::Enter => {
501 self.start_search(self.search_input.text.clone(), false);
503 self.exit_special_mode();
504 true
505 }
506 KeyCode::Esc => {
507 self.exit_special_mode();
508 true
509 }
510 _ => false,
511 }
512 }
513
514 #[must_use]
516 pub fn get_selected_value(&self) -> Option<&DataValue> {
517 let visible_row = *self.visible_rows.get(self.selected_row)?;
518 self.table.get_value(visible_row, self.selected_col)
519 }
520
521 #[must_use]
523 pub fn get_selected_column(&self) -> usize {
524 self.selected_col
525 }
526
527 #[must_use]
529 pub fn get_status_info(&self) -> String {
530 let total_rows = self.table.row_count();
531 let visible_rows = self.visible_rows.len();
532 let current_row = self.selected_row + 1;
533
534 let mut status = format!("Row {current_row}/{visible_rows}");
535
536 if visible_rows != total_rows {
537 status.push_str(&format!(" (filtered from {total_rows})"));
538 }
539
540 if let Some(ref filter) = self.filter {
541 status.push_str(&format!(" | Filter: '{}'", filter.pattern));
542 }
543
544 if let Some(ref search) = self.search {
545 status.push_str(&format!(
546 " | Search: '{}' ({} matches)",
547 search.pattern,
548 search.matches.len()
549 ));
550 }
551
552 if let Some(ref sort) = self.sort {
553 let col_name = &self.table.columns[sort.column_index].name;
554 let order = match sort.order {
555 SortOrder::Ascending => "↑",
556 SortOrder::Descending => "↓",
557 };
558 status.push_str(&format!(" | Sort: {col_name} {order}"));
559 }
560
561 status
562 }
563
564 #[must_use]
566 pub fn create_table_widget(&self) -> Table<'_> {
567 let header = Row::new(
569 self.table.columns[self.visible_col_start..self.visible_col_end]
570 .iter()
571 .enumerate()
572 .map(|(i, col)| {
573 let actual_col_idx = self.visible_col_start + i;
574 let mut style = Style::default().add_modifier(Modifier::BOLD);
575 if actual_col_idx == self.selected_col {
576 style = style.bg(Color::Blue);
577 }
578 Cell::from(col.name.as_str()).style(style)
580 }),
581 )
582 .style(Style::default().bg(Color::DarkGray));
583
584 let start = self.scroll_offset;
586 let end = (start + self.page_size).min(self.visible_rows.len());
587
588 let rows: Vec<Row> = (start..end)
589 .map(|visible_idx| {
590 let row_idx = self.visible_rows[visible_idx];
591 let is_selected = visible_idx == self.selected_row;
592 let is_search_match = self.is_search_match(row_idx);
593
594 let cells: Vec<Cell> = (self.visible_col_start..self.visible_col_end)
596 .map(|col_idx| {
597 let value = self
598 .table
599 .get_value(row_idx, col_idx)
600 .map(std::string::ToString::to_string)
601 .unwrap_or_default();
602
603 let mut style = Style::default();
604
605 if is_selected && col_idx == self.selected_col {
606 style = style.bg(Color::Yellow).fg(Color::Black);
607 } else if is_selected {
608 style = style.bg(Color::Blue).fg(Color::White);
609 } else if is_search_match && self.is_cell_search_match(row_idx, col_idx) {
610 style = style.bg(Color::Green).fg(Color::Black);
611 }
612
613 Cell::from(value).style(style)
614 })
615 .collect();
616
617 Row::new(cells)
618 })
619 .collect();
620
621 let constraints: Vec<Constraint> = self.column_widths
623 [self.visible_col_start..self.visible_col_end]
624 .iter()
625 .map(|&width| Constraint::Length(width))
626 .collect();
627
628 Table::new(rows, constraints)
629 .header(header)
630 .block(Block::default().borders(Borders::ALL).title("Data"))
631 .row_highlight_style(Style::default().bg(Color::Blue))
632 }
633
634 #[must_use]
636 pub fn create_input_widget(&self) -> Option<Paragraph<'_>> {
637 match self.mode {
638 ViewMode::Filtering => Some(
639 Paragraph::new(format!("Filter: {}", self.filter_input.text))
640 .block(Block::default().borders(Borders::ALL).title("Filter")),
641 ),
642 ViewMode::Searching => Some(
643 Paragraph::new(format!("Search: {}", self.search_input.text))
644 .block(Block::default().borders(Borders::ALL).title("Search")),
645 ),
646 _ => None,
647 }
648 }
649
650 fn update_visible_rows(&mut self) {
653 let mut visible: Vec<usize> = (0..self.table.row_count()).collect();
655
656 if let Some(ref filter) = self.filter {
658 visible.retain(|&row_idx| self.matches_filter(row_idx, filter));
659 }
660
661 if let Some(ref sort) = self.sort {
663 visible.sort_by(|&a, &b| self.compare_rows(a, b, sort));
664 }
665
666 self.visible_rows = visible;
667 self.column_widths = Self::calculate_column_widths(&self.table, &self.visible_rows);
668 }
669
670 fn matches_filter(&self, row_idx: usize, filter: &FilterConfig) -> bool {
671 let pattern = if filter.case_sensitive {
672 filter.pattern.clone()
673 } else {
674 filter.pattern.to_lowercase()
675 };
676
677 if let Some(col_idx) = filter.column_index {
678 if let Some(value) = self.table.get_value(row_idx, col_idx) {
680 let text = if filter.case_sensitive {
681 value.to_string()
682 } else {
683 value.to_string().to_lowercase()
684 };
685 text.contains(&pattern)
686 } else {
687 false
688 }
689 } else {
690 (0..self.table.column_count()).any(|col_idx| {
692 if let Some(value) = self.table.get_value(row_idx, col_idx) {
693 let text = if filter.case_sensitive {
694 value.to_string()
695 } else {
696 value.to_string().to_lowercase()
697 };
698 text.contains(&pattern)
699 } else {
700 false
701 }
702 })
703 }
704 }
705
706 fn compare_rows(&self, a: usize, b: usize, sort: &SortConfig) -> std::cmp::Ordering {
707 use std::cmp::Ordering;
708
709 let val_a = self.table.get_value(a, sort.column_index);
710 let val_b = self.table.get_value(b, sort.column_index);
711
712 let result = match (val_a, val_b) {
713 (Some(a), Some(b)) => self.compare_values(a, b),
714 (Some(_), None) => Ordering::Less,
715 (None, Some(_)) => Ordering::Greater,
716 (None, None) => Ordering::Equal,
717 };
718
719 match sort.order {
720 SortOrder::Ascending => result,
721 SortOrder::Descending => result.reverse(),
722 }
723 }
724
725 fn compare_values(&self, a: &DataValue, b: &DataValue) -> std::cmp::Ordering {
726 use crate::datatable::DataValue;
727 use std::cmp::Ordering;
728
729 match (a, b) {
730 (DataValue::Integer(a), DataValue::Integer(b)) => a.cmp(b),
731 (DataValue::Float(a), DataValue::Float(b)) => {
732 a.partial_cmp(b).unwrap_or(Ordering::Equal)
733 }
734 (DataValue::String(a), DataValue::String(b)) => a.cmp(b),
735 (DataValue::Boolean(a), DataValue::Boolean(b)) => a.cmp(b),
736 (DataValue::DateTime(a), DataValue::DateTime(b)) => a.cmp(b),
737 (DataValue::Null, DataValue::Null) => Ordering::Equal,
738 (DataValue::Null, _) => Ordering::Greater,
739 (_, DataValue::Null) => Ordering::Less,
740 (a, b) => a.to_string().cmp(&b.to_string()),
742 }
743 }
744
745 fn find_matches(&self, pattern: &str, case_sensitive: bool) -> Vec<(usize, usize)> {
746 let search_pattern = if case_sensitive {
747 pattern.to_string()
748 } else {
749 pattern.to_lowercase()
750 };
751
752 let mut matches = Vec::new();
753
754 for &row_idx in &self.visible_rows {
755 for col_idx in 0..self.table.column_count() {
756 if let Some(value) = self.table.get_value(row_idx, col_idx) {
757 let text = if case_sensitive {
758 value.to_string()
759 } else {
760 value.to_string().to_lowercase()
761 };
762
763 if text.contains(&search_pattern) {
764 matches.push((row_idx, col_idx));
765 }
766 }
767 }
768 }
769
770 matches
771 }
772
773 fn is_search_match(&self, row_idx: usize) -> bool {
774 if let Some(ref search) = self.search {
775 search.matches.iter().any(|(r, _)| *r == row_idx)
776 } else {
777 false
778 }
779 }
780
781 fn is_cell_search_match(&self, row_idx: usize, col_idx: usize) -> bool {
782 if let Some(ref search) = self.search {
783 search.matches.contains(&(row_idx, col_idx))
784 } else {
785 false
786 }
787 }
788
789 fn ensure_row_visible(&mut self, row_idx: usize) {
790 if row_idx < self.scroll_offset {
791 self.scroll_offset = row_idx;
792 } else if row_idx >= self.scroll_offset + self.page_size {
793 self.scroll_offset = row_idx - self.page_size + 1;
794 }
795 }
796
797 fn ensure_column_visible(&mut self, col_idx: usize) {
798 if col_idx >= self.visible_col_start && col_idx < self.visible_col_end {
800 return;
801 }
802
803 if col_idx < self.visible_col_start {
804 self.visible_col_start = col_idx;
806 } else if col_idx >= self.visible_col_end {
808 self.visible_col_start =
810 col_idx - (self.visible_col_end - self.visible_col_start - 1).min(col_idx);
811 }
813
814 self.horizontal_scroll = self.visible_col_start;
815 }
816
817 fn calculate_column_widths(table: &DataTable, visible_rows: &[usize]) -> Vec<u16> {
818 let mut widths = Vec::new();
819
820 const MIN_WIDTH: u16 = 4; const MAX_WIDTH: u16 = 50; const PADDING: u16 = 2; const MAX_ROWS_TO_CHECK: usize = 100; let total_rows = if visible_rows.is_empty() {
828 table.row_count()
829 } else {
830 visible_rows.len()
831 };
832
833 let rows_to_check: Vec<usize> = if total_rows <= MAX_ROWS_TO_CHECK {
834 if visible_rows.is_empty() {
836 (0..total_rows).collect()
837 } else {
838 visible_rows.iter().take(total_rows).copied().collect()
839 }
840 } else {
841 let step = total_rows / MAX_ROWS_TO_CHECK;
843 (0..MAX_ROWS_TO_CHECK)
844 .map(|i| {
845 let idx = (i * step).min(total_rows - 1);
846 if visible_rows.is_empty() {
847 idx
848 } else {
849 visible_rows[idx]
850 }
851 })
852 .collect()
853 };
854
855 for (col_idx, column) in table.columns.iter().enumerate() {
856 let mut max_width = column.name.len();
858
859 for &row_idx in &rows_to_check {
861 if let Some(value) = table.get_value(row_idx, col_idx) {
862 let display_len = value.to_string().len();
863 max_width = max_width.max(display_len);
864 }
865 }
866
867 let optimal_width = (max_width + PADDING as usize)
869 .max(MIN_WIDTH as usize)
870 .min(MAX_WIDTH as usize) as u16;
871
872 widths.push(optimal_width);
873 }
874
875 widths
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882 use crate::datatable::{DataColumn, DataRow, DataType, DataValue};
883 use crossterm::event::KeyModifiers;
884
885 fn create_test_table() -> DataTable {
886 let mut table = DataTable::new("test");
887
888 table.add_column(DataColumn::new("id").with_type(DataType::Integer));
889 table.add_column(DataColumn::new("name").with_type(DataType::String));
890 table.add_column(DataColumn::new("score").with_type(DataType::Float));
891
892 table
893 .add_row(DataRow::new(vec![
894 DataValue::Integer(1),
895 DataValue::String("Alice".to_string()),
896 DataValue::Float(95.5),
897 ]))
898 .unwrap();
899
900 table
901 .add_row(DataRow::new(vec![
902 DataValue::Integer(2),
903 DataValue::String("Bob".to_string()),
904 DataValue::Float(87.3),
905 ]))
906 .unwrap();
907
908 table
909 .add_row(DataRow::new(vec![
910 DataValue::Integer(3),
911 DataValue::String("Charlie".to_string()),
912 DataValue::Float(92.1),
913 ]))
914 .unwrap();
915
916 table
917 }
918
919 #[test]
920 fn test_datatable_view_creation() {
921 let table = create_test_table();
922 let view = DataTableView::new(table);
923
924 assert_eq!(view.visible_row_count(), 3);
925 assert_eq!(view.mode(), ViewMode::Normal);
926 assert!(view.filter.is_none());
927 assert!(view.search.is_none());
928 assert!(view.sort.is_none());
929 }
930
931 #[test]
932 fn test_filter() {
933 let table = create_test_table();
934 let mut view = DataTableView::new(table);
935
936 view.apply_filter("li".to_string(), None, false);
938
939 assert_eq!(view.visible_row_count(), 2); assert!(view.filter.is_some());
941 }
942
943 #[test]
944 fn test_sort() {
945 let table = create_test_table();
946 let mut view = DataTableView::new(table);
947
948 view.apply_sort(2, SortOrder::Descending);
950
951 assert_eq!(view.visible_row_count(), 3);
952
953 let first_visible_row = view.visible_rows[0];
955 let first_value = view.table().get_value(first_visible_row, 1).unwrap();
956 assert_eq!(first_value.to_string(), "Alice");
957 }
958
959 #[test]
960 fn test_search() {
961 let table = create_test_table();
962 let mut view = DataTableView::new(table);
963
964 view.start_search("Bob".to_string(), false);
965
966 assert!(view.search.is_some());
967 let search = view.search.as_ref().unwrap();
968 assert_eq!(search.matches.len(), 1);
969 assert_eq!(search.current_match, Some((1, 1))); }
971
972 #[test]
973 fn test_navigation() {
974 let table = create_test_table();
975 let mut view = DataTableView::new(table);
976
977 assert_eq!(view.selected_row, 0);
978 assert_eq!(view.selected_col, 0);
979
980 view.handle_navigation(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
982 assert_eq!(view.selected_row, 1);
983
984 view.handle_navigation(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE));
986 assert_eq!(view.selected_col, 1);
987 }
988}