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