1use crate::buffer::ScreenBuffer;
7use crate::cell::Cell;
8use crate::event::{Event, KeyCode, KeyEvent};
9use crate::geometry::Rect;
10use crate::style::Style;
11use crate::text::truncate_to_display_width;
12use crate::widget::label::Alignment;
13use unicode_width::UnicodeWidthStr;
14
15use super::{BorderStyle, EventResult, InteractiveWidget, Widget};
16
17#[derive(Clone, Debug)]
19pub struct Column {
20 pub header: String,
22 pub width: u16,
24 pub alignment: Alignment,
26}
27
28impl Column {
29 pub fn new(header: &str, width: u16) -> Self {
31 Self {
32 header: header.to_string(),
33 width,
34 alignment: Alignment::Left,
35 }
36 }
37
38 #[must_use]
40 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
41 self.alignment = alignment;
42 self
43 }
44}
45
46pub struct DataTable {
51 columns: Vec<Column>,
53 rows: Vec<Vec<String>>,
55 selected_row: usize,
57 row_offset: usize,
59 col_offset: u16,
61 header_style: Style,
63 row_style: Style,
65 selected_style: Style,
67 border: BorderStyle,
69 sort_state: Option<(usize, bool)>,
71 resizable_columns: bool,
73 original_order: Vec<usize>,
75}
76
77impl DataTable {
78 pub fn new(columns: Vec<Column>) -> Self {
80 Self {
81 columns,
82 rows: Vec::new(),
83 selected_row: 0,
84 row_offset: 0,
85 col_offset: 0,
86 header_style: Style::default().bold(true),
87 row_style: Style::default(),
88 selected_style: Style::default().reverse(true),
89 border: BorderStyle::None,
90 sort_state: None,
91 resizable_columns: false,
92 original_order: Vec::new(),
93 }
94 }
95
96 #[must_use]
98 pub fn with_header_style(mut self, style: Style) -> Self {
99 self.header_style = style;
100 self
101 }
102
103 #[must_use]
105 pub fn with_row_style(mut self, style: Style) -> Self {
106 self.row_style = style;
107 self
108 }
109
110 #[must_use]
112 pub fn with_selected_style(mut self, style: Style) -> Self {
113 self.selected_style = style;
114 self
115 }
116
117 #[must_use]
119 pub fn with_border(mut self, border: BorderStyle) -> Self {
120 self.border = border;
121 self
122 }
123
124 pub fn push_row(&mut self, row: Vec<String>) {
126 self.rows.push(row);
127 }
128
129 pub fn set_rows(&mut self, rows: Vec<Vec<String>>) {
131 self.rows = rows;
132 self.selected_row = 0;
133 self.row_offset = 0;
134 self.sort_state = None;
135 self.original_order.clear();
136 }
137
138 pub fn row_count(&self) -> usize {
140 self.rows.len()
141 }
142
143 pub fn column_count(&self) -> usize {
145 self.columns.len()
146 }
147
148 pub fn selected_row(&self) -> usize {
150 self.selected_row
151 }
152
153 pub fn set_selected_row(&mut self, idx: usize) {
155 if self.rows.is_empty() {
156 self.selected_row = 0;
157 } else {
158 self.selected_row = idx.min(self.rows.len().saturating_sub(1));
159 }
160 }
161
162 pub fn selected_row_data(&self) -> Option<&[String]> {
164 self.rows.get(self.selected_row).map(|r| r.as_slice())
165 }
166
167 pub fn columns(&self) -> &[Column] {
169 &self.columns
170 }
171
172 pub fn col_offset(&self) -> u16 {
174 self.col_offset
175 }
176
177 #[must_use]
181 pub fn with_resizable_columns(mut self, enabled: bool) -> Self {
182 self.resizable_columns = enabled;
183 self
184 }
185
186 pub fn sort_by_column(&mut self, col_idx: usize) {
191 if col_idx >= self.columns.len() {
192 return;
193 }
194
195 if self.original_order.is_empty() {
197 self.original_order = (0..self.rows.len()).collect();
198 }
199
200 let ascending = match self.sort_state {
201 Some((prev_col, prev_asc)) if prev_col == col_idx => !prev_asc,
202 _ => true,
203 };
204
205 self.sort_state = Some((col_idx, ascending));
206
207 let col = col_idx;
209 self.rows.sort_by(|a, b| {
210 let va = a.get(col).map(|s| s.as_str()).unwrap_or("");
211 let vb = b.get(col).map(|s| s.as_str()).unwrap_or("");
212 if ascending { va.cmp(vb) } else { vb.cmp(va) }
213 });
214
215 self.selected_row = 0;
217 self.row_offset = 0;
218 }
219
220 pub fn clear_sort(&mut self) {
222 if self.original_order.is_empty() || self.sort_state.is_none() {
223 self.sort_state = None;
224 return;
225 }
226
227 let mut indexed: Vec<(usize, Vec<String>)> = self
229 .original_order
230 .iter()
231 .zip(self.rows.drain(..))
232 .map(|(&orig_idx, row)| (orig_idx, row))
233 .collect();
234 indexed.sort_by_key(|(idx, _)| *idx);
235 self.rows = indexed.into_iter().map(|(_, row)| row).collect();
236
237 self.sort_state = None;
238 self.original_order.clear();
239 self.selected_row = 0;
240 self.row_offset = 0;
241 }
242
243 pub fn sort_state(&self) -> Option<(usize, bool)> {
245 self.sort_state
246 }
247
248 pub fn set_column_width(&mut self, col_idx: usize, width: u16) {
250 if let Some(col) = self.columns.get_mut(col_idx) {
251 col.width = width.clamp(3, 50);
252 }
253 }
254
255 pub fn column_width(&self, col_idx: usize) -> Option<u16> {
257 self.columns.get(col_idx).map(|c| c.width)
258 }
259
260 fn total_columns_width(&self) -> u16 {
262 if self.columns.is_empty() {
263 return 0;
264 }
265 let sum: u16 = self.columns.iter().map(|c| c.width).sum();
267 let separators = self.columns.len().saturating_sub(1) as u16;
268 sum.saturating_add(separators)
269 }
270
271 fn render_row(
273 &self,
274 cells: &[String],
275 y: u16,
276 x_start: u16,
277 available_width: u16,
278 style: &Style,
279 buf: &mut ScreenBuffer,
280 ) {
281 let mut x_offset: u16 = 0;
282 let col_off = self.col_offset;
283
284 for (col_idx, col) in self.columns.iter().enumerate() {
285 let cell_text = cells.get(col_idx).map(|s| s.as_str()).unwrap_or("");
286 let col_w = col.width as usize;
287
288 let col_start = x_offset;
290 let col_end = x_offset.saturating_add(col.width);
291
292 let next_x = if col_idx + 1 < self.columns.len() {
294 col_end.saturating_add(1)
295 } else {
296 col_end
297 };
298
299 if col_end > col_off && col_start < col_off.saturating_add(available_width) {
301 let vis_start = col_start.saturating_sub(col_off);
303 let screen_x = x_start.saturating_add(vis_start);
304
305 let truncated = truncate_to_display_width(cell_text, col_w);
306 let text_width = UnicodeWidthStr::width(truncated);
307
308 let padding = col_w.saturating_sub(text_width);
310 let (left_pad, _right_pad) = match col.alignment {
311 Alignment::Left => (0, padding),
312 Alignment::Center => (padding / 2, padding.saturating_sub(padding / 2)),
313 Alignment::Right => (padding, 0),
314 };
315
316 let mut cx = screen_x;
318 for _ in 0..left_pad {
320 if cx < x_start.saturating_add(available_width) {
321 buf.set(cx, y, Cell::new(" ", style.clone()));
322 cx += 1;
323 }
324 }
325 for ch in truncated.chars() {
327 let char_w = UnicodeWidthStr::width(ch.encode_utf8(&mut [0; 4]) as &str);
328 if cx as usize + char_w > (x_start + available_width) as usize {
329 break;
330 }
331 buf.set(cx, y, Cell::new(ch.to_string(), style.clone()));
332 cx += char_w as u16;
333 }
334 while cx < screen_x.saturating_add(col.width) && cx < x_start + available_width {
336 buf.set(cx, y, Cell::new(" ", style.clone()));
337 cx += 1;
338 }
339
340 if col_idx + 1 < self.columns.len() && cx < x_start.saturating_add(available_width)
342 {
343 buf.set(cx, y, Cell::new("\u{2502}", style.clone()));
344 }
345 }
346
347 x_offset = next_x;
348 }
349 }
350
351 fn ensure_selected_visible(&mut self, visible_height: usize) {
353 if visible_height == 0 {
354 return;
355 }
356 if self.selected_row < self.row_offset {
357 self.row_offset = self.selected_row;
358 }
359 if self.selected_row >= self.row_offset + visible_height {
360 self.row_offset = self
361 .selected_row
362 .saturating_sub(visible_height.saturating_sub(1));
363 }
364 }
365}
366
367impl Widget for DataTable {
368 fn render(&self, area: Rect, buf: &mut ScreenBuffer) {
369 if area.size.width == 0 || area.size.height == 0 {
370 return;
371 }
372
373 super::border::render_border(area, self.border, self.row_style.clone(), buf);
374
375 let inner = super::border::inner_area(area, self.border);
376 if inner.size.width == 0 || inner.size.height == 0 {
377 return;
378 }
379
380 let available_width = inner.size.width;
381 let total_height = inner.size.height as usize;
382
383 if total_height > 0 {
385 let headers: Vec<String> = self
386 .columns
387 .iter()
388 .enumerate()
389 .map(|(idx, c)| {
390 if let Some((sort_col, ascending)) = self.sort_state
391 && sort_col == idx
392 {
393 let indicator = if ascending { "\u{2191}" } else { "\u{2193}" };
394 return format!("{}{indicator}", c.header);
395 }
396 c.header.clone()
397 })
398 .collect();
399 self.render_row(
400 &headers,
401 inner.position.y,
402 inner.position.x,
403 available_width,
404 &self.header_style,
405 buf,
406 );
407 }
408
409 let data_height = total_height.saturating_sub(1);
411 if data_height == 0 {
412 return;
413 }
414
415 let max_offset = self.rows.len().saturating_sub(data_height.max(1));
416 let scroll = self.row_offset.min(max_offset);
417 let visible_end = (scroll + data_height).min(self.rows.len());
418
419 for (row_idx, data_idx) in (scroll..visible_end).enumerate() {
420 let y = inner.position.y + 1 + row_idx as u16;
421 if let Some(row_data) = self.rows.get(data_idx) {
422 let is_selected = data_idx == self.selected_row;
423 let style = if is_selected {
424 &self.selected_style
425 } else {
426 &self.row_style
427 };
428
429 if is_selected {
431 for col in 0..available_width {
432 buf.set(inner.position.x + col, y, Cell::new(" ", style.clone()));
433 }
434 }
435
436 self.render_row(row_data, y, inner.position.x, available_width, style, buf);
437 }
438 }
439 }
440}
441
442impl InteractiveWidget for DataTable {
443 fn handle_event(&mut self, event: &Event) -> EventResult {
444 let Event::Key(KeyEvent { code, modifiers }) = event else {
445 return EventResult::Ignored;
446 };
447
448 match code {
449 KeyCode::Up => {
450 if self.selected_row > 0 {
451 self.selected_row -= 1;
452 self.ensure_selected_visible(20);
453 }
454 EventResult::Consumed
455 }
456 KeyCode::Down => {
457 if !self.rows.is_empty() && self.selected_row < self.rows.len().saturating_sub(1) {
458 self.selected_row += 1;
459 self.ensure_selected_visible(20);
460 }
461 EventResult::Consumed
462 }
463 KeyCode::Left => {
464 let has_ctrl = modifiers.contains(crate::event::Modifiers::CTRL);
465 let has_shift = modifiers.contains(crate::event::Modifiers::SHIFT);
466 if has_ctrl && has_shift && self.resizable_columns {
467 let max_col = self.columns.len().saturating_sub(1);
469 let target = self.selected_row.min(max_col);
470 if let Some(col) = self.columns.get_mut(target) {
471 col.width = col.width.saturating_sub(1).max(3);
472 }
473 } else if has_ctrl {
474 self.col_offset = 0;
475 } else {
476 self.col_offset = self.col_offset.saturating_sub(1);
477 }
478 EventResult::Consumed
479 }
480 KeyCode::Right => {
481 let has_ctrl = modifiers.contains(crate::event::Modifiers::CTRL);
482 let has_shift = modifiers.contains(crate::event::Modifiers::SHIFT);
483 if has_ctrl && has_shift && self.resizable_columns {
484 let max_col = self.columns.len().saturating_sub(1);
486 let target = self.selected_row.min(max_col);
487 if let Some(col) = self.columns.get_mut(target) {
488 col.width = (col.width + 1).min(50);
489 }
490 } else if has_ctrl {
491 self.col_offset = self.total_columns_width();
492 } else {
493 self.col_offset = self.col_offset.saturating_add(1);
494 }
495 EventResult::Consumed
496 }
497 KeyCode::PageUp => {
498 let page = 20;
499 self.selected_row = self.selected_row.saturating_sub(page);
500 self.ensure_selected_visible(20);
501 EventResult::Consumed
502 }
503 KeyCode::PageDown => {
504 let page = 20;
505 if !self.rows.is_empty() {
506 self.selected_row =
507 (self.selected_row + page).min(self.rows.len().saturating_sub(1));
508 self.ensure_selected_visible(20);
509 }
510 EventResult::Consumed
511 }
512 KeyCode::Home => {
513 self.selected_row = 0;
514 self.row_offset = 0;
515 EventResult::Consumed
516 }
517 KeyCode::End => {
518 if !self.rows.is_empty() {
519 self.selected_row = self.rows.len().saturating_sub(1);
520 self.ensure_selected_visible(20);
521 }
522 EventResult::Consumed
523 }
524 KeyCode::Char('0') if modifiers.contains(crate::event::Modifiers::CTRL) => {
526 self.clear_sort();
527 EventResult::Consumed
528 }
529 KeyCode::Char(ch)
531 if modifiers.contains(crate::event::Modifiers::CTRL)
532 && ('1'..='9').contains(ch) =>
533 {
534 let col_idx = (*ch as usize) - ('1' as usize);
535 if col_idx < self.columns.len() {
536 self.sort_by_column(col_idx);
537 }
538 EventResult::Consumed
539 }
540 _ => EventResult::Ignored,
541 }
542 }
543}
544
545#[cfg(test)]
546#[allow(clippy::unwrap_used)]
547mod tests {
548 use super::*;
549 use crate::geometry::Size;
550
551 fn make_test_table() -> DataTable {
552 let cols = vec![
553 Column::new("Name", 10),
554 Column::new("Age", 5),
555 Column::new("City", 12),
556 ];
557 let mut table = DataTable::new(cols);
558 table.push_row(vec!["Alice".into(), "30".into(), "New York".into()]);
559 table.push_row(vec!["Bob".into(), "25".into(), "London".into()]);
560 table.push_row(vec!["Charlie".into(), "35".into(), "Tokyo".into()]);
561 table
562 }
563
564 #[test]
565 fn create_table_with_columns() {
566 let table = make_test_table();
567 assert_eq!(table.column_count(), 3);
568 assert_eq!(table.row_count(), 3);
569 }
570
571 #[test]
572 fn add_rows() {
573 let mut table = DataTable::new(vec![Column::new("X", 5)]);
574 assert_eq!(table.row_count(), 0);
575 table.push_row(vec!["a".into()]);
576 table.push_row(vec!["b".into()]);
577 assert_eq!(table.row_count(), 2);
578 }
579
580 #[test]
581 fn render_empty_table_shows_headers() {
582 let table = DataTable::new(vec![Column::new("Name", 10), Column::new("Age", 5)]);
583 let mut buf = ScreenBuffer::new(Size::new(20, 5));
584 table.render(Rect::new(0, 0, 20, 5), &mut buf);
585
586 assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("N"));
588 assert_eq!(buf.get(3, 0).map(|c| c.grapheme.as_str()), Some("e"));
589 }
590
591 #[test]
592 fn render_with_rows() {
593 let table = make_test_table();
594 let mut buf = ScreenBuffer::new(Size::new(30, 10));
595 table.render(Rect::new(0, 0, 30, 10), &mut buf);
596
597 assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("N"));
599 assert_eq!(buf.get(0, 1).map(|c| c.grapheme.as_str()), Some("A"));
601 assert_eq!(buf.get(0, 2).map(|c| c.grapheme.as_str()), Some("B"));
603 }
604
605 #[test]
606 fn selected_row_highlighted() {
607 let mut table = make_test_table();
608 table.selected_style = Style::default().bold(true);
609 table.row_style = Style::default();
610 table.set_selected_row(1); let mut buf = ScreenBuffer::new(Size::new(30, 10));
613 table.render(Rect::new(0, 0, 30, 10), &mut buf);
614
615 let cell = buf.get(0, 2);
617 assert!(cell.is_some());
618 assert!(cell.map(|c| c.style.bold).unwrap_or(false));
619
620 let cell_a = buf.get(0, 1);
622 assert!(cell_a.is_some());
623 assert!(!cell_a.map(|c| c.style.bold).unwrap_or(true));
624 }
625
626 #[test]
627 fn column_alignment_left() {
628 let table = DataTable::new(vec![Column::new("H", 10).with_alignment(Alignment::Left)]);
629 let mut t = table;
630 t.push_row(vec!["Hi".into()]);
631
632 let mut buf = ScreenBuffer::new(Size::new(15, 5));
633 t.render(Rect::new(0, 0, 15, 5), &mut buf);
634
635 assert_eq!(buf.get(0, 1).map(|c| c.grapheme.as_str()), Some("H"));
637 assert_eq!(buf.get(1, 1).map(|c| c.grapheme.as_str()), Some("i"));
638 }
639
640 #[test]
641 fn column_alignment_right() {
642 let col = Column::new("H", 10).with_alignment(Alignment::Right);
643 let mut table = DataTable::new(vec![col]);
644 table.push_row(vec!["Hi".into()]);
645
646 let mut buf = ScreenBuffer::new(Size::new(15, 5));
647 table.render(Rect::new(0, 0, 15, 5), &mut buf);
648
649 assert_eq!(buf.get(8, 1).map(|c| c.grapheme.as_str()), Some("H"));
651 assert_eq!(buf.get(9, 1).map(|c| c.grapheme.as_str()), Some("i"));
652 }
653
654 #[test]
655 fn column_alignment_center() {
656 let col = Column::new("H", 10).with_alignment(Alignment::Center);
657 let mut table = DataTable::new(vec![col]);
658 table.push_row(vec!["Hi".into()]);
659
660 let mut buf = ScreenBuffer::new(Size::new(15, 5));
661 table.render(Rect::new(0, 0, 15, 5), &mut buf);
662
663 assert_eq!(buf.get(4, 1).map(|c| c.grapheme.as_str()), Some("H"));
665 assert_eq!(buf.get(5, 1).map(|c| c.grapheme.as_str()), Some("i"));
666 }
667
668 #[test]
669 fn utf8_safe_truncation_in_cells() {
670 let col = Column::new("H", 5);
671 let mut table = DataTable::new(vec![col]);
672 table.push_row(vec!["你好世界人".into()]);
673
674 let mut buf = ScreenBuffer::new(Size::new(10, 5));
675 table.render(Rect::new(0, 0, 10, 5), &mut buf);
676
677 assert_eq!(buf.get(0, 1).map(|c| c.grapheme.as_str()), Some("你"));
679 assert_eq!(buf.get(2, 1).map(|c| c.grapheme.as_str()), Some("好"));
680 }
681
682 #[test]
683 fn vertical_scrolling_with_navigation() {
684 let mut table = make_test_table();
685
686 let down = Event::Key(KeyEvent {
687 code: KeyCode::Down,
688 modifiers: crate::event::Modifiers::NONE,
689 });
690 let up = Event::Key(KeyEvent {
691 code: KeyCode::Up,
692 modifiers: crate::event::Modifiers::NONE,
693 });
694
695 assert_eq!(table.selected_row(), 0);
696 table.handle_event(&down);
697 assert_eq!(table.selected_row(), 1);
698 table.handle_event(&down);
699 assert_eq!(table.selected_row(), 2);
700 table.handle_event(&down); assert_eq!(table.selected_row(), 2);
702 table.handle_event(&up);
703 assert_eq!(table.selected_row(), 1);
704 }
705
706 #[test]
707 fn horizontal_scrolling() {
708 let mut table = make_test_table();
709
710 let right = Event::Key(KeyEvent {
711 code: KeyCode::Right,
712 modifiers: crate::event::Modifiers::NONE,
713 });
714 let left = Event::Key(KeyEvent {
715 code: KeyCode::Left,
716 modifiers: crate::event::Modifiers::NONE,
717 });
718
719 assert_eq!(table.col_offset(), 0);
720 table.handle_event(&right);
721 assert_eq!(table.col_offset(), 1);
722 table.handle_event(&left);
723 assert_eq!(table.col_offset(), 0);
724 table.handle_event(&left); assert_eq!(table.col_offset(), 0);
726 }
727
728 #[test]
729 fn page_up_down() {
730 let cols = vec![Column::new("N", 5)];
731 let mut table = DataTable::new(cols);
732 for i in 0..50 {
733 table.push_row(vec![format!("Row {i}")]);
734 }
735
736 let page_down = Event::Key(KeyEvent {
737 code: KeyCode::PageDown,
738 modifiers: crate::event::Modifiers::NONE,
739 });
740 let page_up = Event::Key(KeyEvent {
741 code: KeyCode::PageUp,
742 modifiers: crate::event::Modifiers::NONE,
743 });
744
745 table.handle_event(&page_down);
746 assert_eq!(table.selected_row(), 20);
747 table.handle_event(&page_up);
748 assert_eq!(table.selected_row(), 0);
749 }
750
751 #[test]
752 fn home_end_navigation() {
753 let mut table = make_test_table();
754
755 let end = Event::Key(KeyEvent {
756 code: KeyCode::End,
757 modifiers: crate::event::Modifiers::NONE,
758 });
759 let home = Event::Key(KeyEvent {
760 code: KeyCode::Home,
761 modifiers: crate::event::Modifiers::NONE,
762 });
763
764 table.handle_event(&end);
765 assert_eq!(table.selected_row(), 2);
766 table.handle_event(&home);
767 assert_eq!(table.selected_row(), 0);
768 }
769
770 #[test]
771 fn render_with_border() {
772 let table = make_test_table();
773 let table = DataTable {
774 border: BorderStyle::Single,
775 ..table
776 };
777
778 let mut buf = ScreenBuffer::new(Size::new(35, 8));
779 table.render(Rect::new(0, 0, 35, 8), &mut buf);
780
781 assert_eq!(buf.get(0, 0).map(|c| c.grapheme.as_str()), Some("\u{250c}"));
782 assert_eq!(buf.get(1, 1).map(|c| c.grapheme.as_str()), Some("N"));
784 }
785
786 #[test]
787 fn empty_table_with_columns() {
788 let table = DataTable::new(vec![Column::new("A", 5), Column::new("B", 5)]);
789 assert_eq!(table.row_count(), 0);
790 assert_eq!(table.column_count(), 2);
791 assert!(table.selected_row_data().is_none());
792
793 let mut buf = ScreenBuffer::new(Size::new(15, 5));
795 table.render(Rect::new(0, 0, 15, 5), &mut buf);
796 }
797
798 #[test]
799 fn selected_row_data_access() {
800 let table = make_test_table();
801 match table.selected_row_data() {
802 Some(data) => {
803 assert_eq!(data.len(), 3);
804 assert_eq!(data[0], "Alice");
805 }
806 None => unreachable!("should have data"),
807 }
808 }
809
810 #[test]
811 fn set_rows_resets_selection() {
812 let mut table = make_test_table();
813 table.set_selected_row(2);
814 assert_eq!(table.selected_row(), 2);
815
816 table.set_rows(vec![vec!["X".into()]]);
817 assert_eq!(table.selected_row(), 0);
818 assert_eq!(table.row_count(), 1);
819 }
820
821 #[test]
822 fn builder_pattern() {
823 let table = DataTable::new(vec![Column::new("H", 10)])
824 .with_header_style(Style::default().bold(true))
825 .with_row_style(Style::default().dim(true))
826 .with_selected_style(Style::default().italic(true))
827 .with_border(BorderStyle::Rounded);
828
829 assert!(table.header_style.bold);
830 assert!(table.row_style.dim);
831 assert!(table.selected_style.italic);
832 }
833
834 #[test]
835 fn unhandled_event_ignored() {
836 let mut table = make_test_table();
837 let tab = Event::Key(KeyEvent {
838 code: KeyCode::Tab,
839 modifiers: crate::event::Modifiers::NONE,
840 });
841 assert_eq!(table.handle_event(&tab), EventResult::Ignored);
842 }
843
844 #[test]
847 fn sort_by_column_ascending() {
848 let mut table = make_test_table();
849 table.sort_by_column(0); assert_eq!(table.sort_state(), Some((0, true)));
852 match table.rows.first().map(|r| r[0].as_str()) {
853 Some("Alice") => {}
854 other => panic!("Expected Alice first, got {other:?}"),
855 }
856 }
857
858 #[test]
859 fn sort_toggle_descending() {
860 let mut table = make_test_table();
861 table.sort_by_column(0); assert_eq!(table.sort_state(), Some((0, true)));
863 table.sort_by_column(0); assert_eq!(table.sort_state(), Some((0, false)));
865 match table.rows.first().map(|r| r[0].as_str()) {
866 Some("Charlie") => {}
867 other => panic!("Expected Charlie first (descending), got {other:?}"),
868 }
869 }
870
871 #[test]
872 fn sort_indicator_in_header() {
873 let mut table = make_test_table();
874 table.sort_by_column(0); let mut buf = ScreenBuffer::new(Size::new(35, 10));
877 table.render(Rect::new(0, 0, 35, 10), &mut buf);
878
879 assert_eq!(buf.get(4, 0).map(|c| c.grapheme.as_str()), Some("\u{2191}"));
882 }
883
884 #[test]
885 fn sort_descending_indicator() {
886 let mut table = make_test_table();
887 table.sort_by_column(0);
888 table.sort_by_column(0); let mut buf = ScreenBuffer::new(Size::new(35, 10));
891 table.render(Rect::new(0, 0, 35, 10), &mut buf);
892
893 assert_eq!(buf.get(4, 0).map(|c| c.grapheme.as_str()), Some("\u{2193}"));
895 }
896
897 #[test]
898 fn clear_sort_restores_order() {
899 let mut table = make_test_table();
900 table.sort_by_column(0); table.sort_by_column(0); table.clear_sort();
904 assert!(table.sort_state().is_none());
905 }
906
907 #[test]
908 fn column_resize_increase() {
909 let mut table = make_test_table();
910 let original_width = table.column_width(0);
911 assert_eq!(original_width, Some(10));
912
913 table.set_column_width(0, 15);
914 assert_eq!(table.column_width(0), Some(15));
915 }
916
917 #[test]
918 fn column_resize_clamping() {
919 let mut table = make_test_table();
920
921 table.set_column_width(0, 1);
923 assert_eq!(table.column_width(0), Some(3));
924
925 table.set_column_width(0, 100);
927 assert_eq!(table.column_width(0), Some(50));
928 }
929
930 #[test]
931 fn keyboard_sort_ctrl_1() {
932 let mut table = make_test_table();
933
934 let ctrl_1 = Event::Key(KeyEvent {
935 code: KeyCode::Char('1'),
936 modifiers: crate::event::Modifiers::CTRL,
937 });
938
939 assert_eq!(table.handle_event(&ctrl_1), EventResult::Consumed);
940 assert_eq!(table.sort_state(), Some((0, true)));
941 }
942
943 #[test]
944 fn keyboard_sort_ctrl_0_clears() {
945 let mut table = make_test_table();
946 table.sort_by_column(0);
947 assert!(table.sort_state().is_some());
948
949 let ctrl_0 = Event::Key(KeyEvent {
950 code: KeyCode::Char('0'),
951 modifiers: crate::event::Modifiers::CTRL,
952 });
953
954 assert_eq!(table.handle_event(&ctrl_0), EventResult::Consumed);
955 assert!(table.sort_state().is_none());
956 }
957
958 #[test]
959 fn keyboard_resize_ctrl_shift_right() {
960 let mut table = make_test_table().with_resizable_columns(true);
961
962 let original = table.column_width(0);
963 assert_eq!(original, Some(10));
964
965 let ctrl_shift_right = Event::Key(KeyEvent {
966 code: KeyCode::Right,
967 modifiers: crate::event::Modifiers::CTRL | crate::event::Modifiers::SHIFT,
968 });
969
970 table.handle_event(&ctrl_shift_right);
971 assert_eq!(table.column_width(0), Some(11));
972 }
973
974 #[test]
975 fn keyboard_resize_ctrl_shift_left() {
976 let mut table = make_test_table().with_resizable_columns(true);
977
978 let ctrl_shift_left = Event::Key(KeyEvent {
979 code: KeyCode::Left,
980 modifiers: crate::event::Modifiers::CTRL | crate::event::Modifiers::SHIFT,
981 });
982
983 table.handle_event(&ctrl_shift_left);
984 assert_eq!(table.column_width(0), Some(9));
985 }
986
987 #[test]
988 fn empty_table_sorting_no_crash() {
989 let mut table = DataTable::new(vec![Column::new("X", 5)]);
990 table.sort_by_column(0);
991 assert_eq!(table.sort_state(), Some((0, true)));
992 table.clear_sort();
993 assert!(table.sort_state().is_none());
994 }
995
996 #[test]
997 fn sort_by_column_resets_selection() {
998 let mut table = make_test_table();
999 table.set_selected_row(2);
1000 table.sort_by_column(0);
1001 assert_eq!(table.selected_row(), 0);
1002 }
1003}