1use std::borrow::Cow;
5use std::collections::HashSet;
6use std::sync::Arc;
7
8use polars::prelude::*;
9use ratatui::{
10 buffer::Buffer,
11 layout::Rect,
12 style::{Color, Modifier, Style},
13 text::{Line, Span, Text},
14 widgets::{
15 Block, Borders, Cell, HighlightSpacing, Padding, Paragraph, Row, StatefulWidget, Table,
16 TableState, Widget,
17 },
18};
19
20use crate::error_display::user_message_from_polars;
21use crate::formats::column_types::dtype_label;
22use crate::numfmt::{self, CellFormatter, NumberFormatSettings};
23use crate::table::{DataTableState, DrawnTable, visible_slice};
24use crate::widgets::column_paging::{OnScreen, Room};
25use crate::widgets::column_widths::{ColumnWidths, PageMeasure, WidthChoice};
26
27pub struct DataTable {
28 pub header_bg: Color,
29 pub header_fg: Color,
30 pub row_numbers_fg: Color,
31 pub separator_fg: Color,
32 pub table_cell_padding: u16,
33 pub alternate_row_bg: Option<Color>,
34 pub column_colors: bool,
36 pub str_col: Option<Color>,
37 pub int_col: Option<Color>,
38 pub float_col: Option<Color>,
39 pub bool_col: Option<Color>,
40 pub temporal_col: Option<Color>,
41 pub binary_col: Option<Color>,
44 pub binary_cols: std::collections::HashSet<String>,
47 pub number_format: NumberFormatSettings,
49 pub dtype_row: bool,
51 pub selected_bg: Option<Color>,
53 pub selection_style: Style,
55 pub accent: Color,
57 pub dimmed: Color,
59 pub drift_rows: Vec<u32>,
62 pub drift_groups: Arc<Vec<crate::formats::schema_union::DriftGroup>>,
64 pub sort_columns: Vec<String>,
67 pub sort_descending: Vec<bool>,
69 pub current_column: Option<String>,
71 pub column_cursor_style: Style,
73 pub cell_cursor_style: Style,
76 pub glyphs: &'static crate::glyphs::Glyphs,
78 pub screen_width: u16,
81 pub find_cell: Option<(usize, String)>,
83 pub find_style: Style,
85 pub(crate) find_column: Option<String>,
87 pub match_cells: Option<std::sync::Arc<crate::find::MatchCells>>,
90 pub(crate) drawn_from: usize,
92 pub(crate) units: Vec<(String, String)>,
95 pub(crate) retyped: Vec<String>,
97}
98
99impl Default for DataTable {
100 fn default() -> Self {
101 Self {
102 header_bg: Color::Reset,
103 header_fg: Color::Reset,
104 row_numbers_fg: Color::Reset,
105 separator_fg: Color::Reset,
106 table_cell_padding: 1,
107 alternate_row_bg: None,
108 column_colors: false,
109 str_col: None,
110 int_col: None,
111 float_col: None,
112 bool_col: None,
113 temporal_col: None,
114 binary_col: None,
115 binary_cols: std::collections::HashSet::new(),
116 number_format: NumberFormatSettings::default(),
117 dtype_row: false,
118 selected_bg: None,
119 selection_style: Style::default(),
120 accent: Color::Reset,
121 dimmed: Color::Reset,
122 drift_rows: Vec::new(),
123 drift_groups: Arc::new(Vec::new()),
124 sort_columns: Vec::new(),
125 sort_descending: Vec::new(),
126 current_column: None,
127 column_cursor_style: Style::default(),
128 cell_cursor_style: Style::default(),
129 glyphs: crate::glyphs::get(),
130 screen_width: 0,
131 find_cell: None,
132 find_style: Style::default(),
133 find_column: None,
134 match_cells: None,
135 drawn_from: 0,
136 units: Vec::new(),
137 retyped: Vec::new(),
138 }
139 }
140}
141
142struct RowNumbersParams {
144 start_row: usize,
145 visible_rows: usize,
146 num_rows: usize,
147 numbers: Vec<usize>,
149 selected_row: Option<usize>,
150}
151
152struct ColumnSlice {
155 name: String,
156 drift_mark: &'static str,
157 sort_mark: &'static str,
158 header_width: u16,
160 type_label: Option<String>,
161 type_width: u16,
162 cells: Arc<Cells>,
163 width: u16,
166 right_align: bool,
167 clips: bool,
170 cell_style: Option<Style>,
171 colour: Option<Color>,
173}
174
175impl ColumnSlice {
176 fn natural_width(&self) -> u16 {
178 self.width
179 }
180
181 fn measure(&self) -> PageMeasure {
182 PageMeasure {
183 header: self.header_width,
184 type_label: self.type_width,
185 values: self.cells.value_width,
186 has_values: self.cells.has_values,
187 clips: self.clips,
188 }
189 }
190}
191
192struct Sizing<'a> {
195 widths: &'a mut ColumnWidths,
196 schema: &'a Schema,
197 cap: u16,
198 page: Option<(&'a mut PageCells, &'a DataFrame, usize)>,
201}
202
203pub(crate) struct Cells {
205 cells: Vec<SliceCell>,
206 value_width: u16,
208 has_values: bool,
210}
211
212#[derive(Default)]
216pub(crate) struct PageCells {
217 columns: std::collections::HashMap<String, KeptCells>,
218}
219
220struct KeptCells {
221 series: Series,
224 offset: usize,
225 rows: usize,
226 format: CellFormatter,
227 glyphs: &'static crate::glyphs::Glyphs,
228 nulls: Vec<&'static str>,
229 drift: Vec<u32>,
230 cells: Arc<Cells>,
231 used: bool,
233}
234
235impl PageCells {
236 pub(crate) fn next_frame(&mut self) {
238 self.columns
239 .retain(|_, kept| std::mem::take(&mut kept.used));
240 }
241}
242
243enum SliceCell {
244 Null(&'static str),
246 Value(String),
247}
248
249struct ScrollCue {
251 area: Rect,
252 more_left: bool,
253 more_right: usize,
255}
256
257struct FittedColumns {
260 cols: Vec<ColumnSlice>,
261 widths: Vec<u16>,
262 rows: usize,
263 hint_cell: bool,
266}
267
268const MIN_SCROLL_RESERVE: u16 = 12;
271
272const MAX_SCROLL_RESERVE: u16 = 40;
273
274fn min_partial_width(g: &crate::glyphs::Glyphs) -> u16 {
277 let marker = u16::try_from(crate::glyphs::cell_width(g.ellipsis)).unwrap_or(u16::MAX);
278 marker.saturating_add(1).max(3)
279}
280
281#[derive(Clone, Copy, PartialEq, Eq)]
284enum Side {
285 Frozen,
288 Scrolling,
291}
292
293fn fit_column(
299 col: &ColumnSlice,
300 remaining: u16,
301 min_partial: u16,
302 first: bool,
303 side: Side,
304) -> Option<u16> {
305 if col.natural_width() <= remaining {
306 return Some(col.natural_width());
307 }
308 if side == Side::Frozen && !first {
309 return None;
310 }
311 if remaining >= min_partial && (col.cells.value_width <= remaining || col.clips) {
312 return Some(remaining);
313 }
314 (side == Side::Scrolling && first && remaining > 0).then_some(remaining)
315}
316
317fn cell_line<'a>(mut spans: Vec<Span<'a>>, width: u16, right: bool) -> Line<'a> {
322 use unicode_width::UnicodeWidthStr;
323 let drawn = |s: &Span| crate::glyphs::cell_width(&s.content);
324 if spans.len() > 1 && spans.iter().any(|s| drawn(s) != s.content.width()) {
325 let style = spans[0].style;
326 let joined: String = spans.iter().map(|s| s.content.as_ref()).collect();
327 spans = vec![Span::styled(joined, style)];
328 }
329 let used: usize = spans.iter().map(drawn).sum();
330 let pad = usize::from(width).saturating_sub(used);
331 if right && pad > 0 {
332 const SPACES: &str = " ";
334 let blank = match SPACES.get(..pad) {
335 Some(blank) => Span::raw(blank),
336 None => Span::raw(" ".repeat(pad)),
337 };
338 spans.insert(0, blank);
339 }
340 Line::from(spans)
341}
342
343fn visible_or_header(df: &DataFrame, offset: usize, len: usize) -> Option<DataFrame> {
347 if df.width() == 0 {
348 return None;
349 }
350 Some(visible_slice(df, offset, len).unwrap_or_else(|| df.clear()))
351}
352
353fn is_truncatable_dtype(dtype: &DataType) -> bool {
357 match dtype {
358 DataType::String | DataType::Binary => true,
359 other => other.is_categorical() || other.is_enum() || other.is_nested(),
360 }
361}
362
363impl DataTable {
364 pub fn new() -> Self {
365 Self::default()
366 }
367
368 pub fn with_colors(
369 mut self,
370 header_bg: Color,
371 header_fg: Color,
372 row_numbers_fg: Color,
373 separator_fg: Color,
374 ) -> Self {
375 self.header_bg = header_bg;
376 self.header_fg = header_fg;
377 self.row_numbers_fg = row_numbers_fg;
378 self.separator_fg = separator_fg;
379 self
380 }
381
382 pub fn with_cell_padding(mut self, padding: u16) -> Self {
383 self.table_cell_padding = padding;
384 self
385 }
386
387 pub fn with_screen_width(mut self, width: u16) -> Self {
389 self.screen_width = width;
390 self
391 }
392
393 pub fn with_alternate_row_bg(mut self, color: Option<Color>) -> Self {
394 self.alternate_row_bg = color;
395 self
396 }
397
398 pub fn with_column_type_colors(
400 mut self,
401 str_col: Color,
402 int_col: Color,
403 float_col: Color,
404 bool_col: Color,
405 temporal_col: Color,
406 ) -> Self {
407 self.column_colors = true;
408 self.str_col = Some(str_col);
409 self.int_col = Some(int_col);
410 self.float_col = Some(float_col);
411 self.bool_col = Some(bool_col);
412 self.temporal_col = Some(temporal_col);
413 self
414 }
415
416 pub fn with_binary_col(mut self, color: Color) -> Self {
418 self.binary_col = Some(color);
419 self
420 }
421
422 pub fn with_binary_columns(mut self, names: std::collections::HashSet<String>) -> Self {
424 self.binary_cols = names;
425 self
426 }
427
428 pub fn with_number_format(mut self, settings: NumberFormatSettings) -> Self {
430 self.number_format = settings;
431 self
432 }
433
434 pub fn with_dtype_row(mut self, on: bool) -> Self {
436 self.dtype_row = on;
437 self
438 }
439
440 pub fn with_drift(
443 mut self,
444 rows: Vec<u32>,
445 groups: Arc<Vec<crate::formats::schema_union::DriftGroup>>,
446 ) -> Self {
447 self.drift_rows = rows;
448 self.drift_groups = groups;
449 self
450 }
451
452 #[cfg(test)]
455 pub fn with_sort(mut self, columns: Vec<String>, descending: Vec<bool>) -> Self {
456 debug_assert_eq!(columns.len(), descending.len());
457 self.sort_columns = columns;
458 self.sort_descending = descending;
459 self
460 }
461
462 fn sort_mark_for(&self, column: &str) -> &'static str {
465 if let Some(i) = self.sort_columns.iter().position(|c| c == column) {
466 let g = self.glyphs;
467 if self.sort_descending.get(i).copied().unwrap_or(false) {
468 g.sort_desc
469 } else {
470 g.sort_asc
471 }
472 } else {
473 ""
474 }
475 }
476
477 fn drift_mark_for(&self, column: &str, drifting: &HashSet<&str>) -> &'static str {
480 if drifting.contains(column) {
481 self.glyphs.drift_mark
482 } else {
483 ""
484 }
485 }
486
487 fn drifting_columns(&self) -> HashSet<&str> {
490 self.drift_groups
491 .iter()
492 .flat_map(|group| group.absent.iter().chain(group.unread.iter()))
493 .map(|name| name.as_str())
494 .collect()
495 }
496
497 fn null_glyphs_for(
501 &self,
502 column: &str,
503 g: &'static crate::glyphs::Glyphs,
504 drifting: &HashSet<&str>,
505 ) -> Vec<&'static str> {
506 if self.drift_rows.is_empty() || !drifting.contains(column) {
507 return Vec::new();
508 }
509 self.drift_groups
510 .iter()
511 .map(|group| {
512 if group.absent.iter().any(|c| c == column) {
513 g.absent
514 } else if group.unread.iter().any(|c| c == column) {
515 g.conflict
516 } else {
517 g.null
518 }
519 })
520 .collect()
521 }
522
523 pub fn with_selection_colors(
526 mut self,
527 selection_style: Style,
528 selected_bg: Option<Color>,
529 accent: Color,
530 dimmed: Color,
531 ) -> Self {
532 self.selection_style = selection_style;
533 self.selected_bg = selected_bg;
534 self.accent = accent;
535 self.dimmed = dimmed;
536 self
537 }
538
539 pub fn with_match_cells(
541 mut self,
542 cells: Option<std::sync::Arc<crate::find::MatchCells>>,
543 ) -> Self {
544 self.match_cells = cells;
545 self
546 }
547
548 pub fn with_find_cell(mut self, cell: Option<(usize, String)>, style: Style) -> Self {
550 self.find_cell = cell;
551 self.find_style = style;
552 self
553 }
554
555 pub fn with_cursor_styles(mut self, column: Style, cell: Style) -> Self {
558 self.column_cursor_style = column;
559 self.cell_cursor_style = cell;
560 self
561 }
562
563 pub fn header_height(&self) -> u16 {
565 if self.dtype_row { 2 } else { 1 }
566 }
567
568 fn highlight_style(&self) -> Style {
570 self.selection_style
571 }
572
573 fn column_type_color(&self, dtype: &DataType) -> Option<Color> {
575 if !self.column_colors {
576 return None;
577 }
578 match dtype {
579 DataType::String => self.str_col,
580 DataType::Int8
581 | DataType::Int16
582 | DataType::Int32
583 | DataType::Int64
584 | DataType::UInt8
585 | DataType::UInt16
586 | DataType::UInt32
587 | DataType::UInt64 => self.int_col,
588 DataType::Float32 | DataType::Float64 => self.float_col,
589 DataType::Boolean => self.bool_col,
590 DataType::Date | DataType::Datetime(_, _) | DataType::Time | DataType::Duration(_) => {
591 self.temporal_col
592 }
593 _ => None,
594 }
595 }
596
597 #[cfg(test)]
601 pub(crate) fn render_dataframe(
602 &self,
603 df: &DataFrame,
604 area: Rect,
605 buf: &mut Buffer,
606 state: &mut TableState,
607 leading_gap: bool,
608 ) -> usize {
609 let mut widths = ColumnWidths::default();
610 let sizing = Sizing {
611 widths: &mut widths,
612 schema: df.schema(),
613 cap: self.text_cap(area.width),
614 page: None,
615 };
616 self.render_scrolling(df, area, buf, state, leading_gap, sizing)
617 .0
618 }
619
620 fn text_cap(&self, table_width: u16) -> u16 {
622 let basis = if self.screen_width > 0 {
623 self.screen_width
624 } else {
625 table_width
626 };
627 crate::widgets::column_widths::text_cap(basis)
628 }
629
630 fn render_scrolling(
633 &self,
634 df: &DataFrame,
635 area: Rect,
636 buf: &mut Buffer,
637 state: &mut TableState,
638 leading_gap: bool,
639 mut sizing: Sizing,
640 ) -> (usize, Vec<(u16, u16, String)>, usize) {
641 let rows = df
642 .height()
643 .min((area.height as usize).saturating_sub(self.header_height() as usize));
644 let lead = u16::from(leading_gap);
645 let mut fitted = self.fit_columns(df, rows, area.width, lead, Side::Scrolling, &mut sizing);
646 let shown = fitted.cols.len();
647 let mut used = fitted
648 .widths
649 .iter()
650 .fold(lead, |used, &w| used.saturating_add(w))
651 + self
652 .table_cell_padding
653 .saturating_mul(u16::try_from(shown.saturating_sub(1)).unwrap_or(u16::MAX));
654 if let Some(filled) =
655 self.fill_last_column(&mut fitted, area.width.saturating_sub(used), &mut sizing)
656 {
657 used = used.saturating_add(filled);
658 }
659 fitted.hint_cell = shown > 0 && shown < df.width() && used >= area.width;
660 let (columns, rows) = self.draw_columns(&fitted, area, buf, state, leading_gap);
661 (shown, columns, rows)
662 }
663
664 fn fill_last_column(
668 &self,
669 fitted: &mut FittedColumns,
670 room: u16,
671 sizing: &mut Sizing,
672 ) -> Option<u16> {
673 let (col, width) = fitted.cols.last().zip(fitted.widths.last_mut())?;
674 if room == 0 || col.right_align || *width < col.natural_width() {
675 return None;
676 }
677 let dtype = sizing.schema.get(col.name.as_str())?;
678 if sizing.widths.choice(&col.name, dtype) != WidthChoice::Auto {
679 return None;
680 }
681 *width = width.saturating_add(room);
682 sizing.widths.fill(&col.name, dtype, *width);
683 Some(room)
684 }
685
686 fn fit_frozen_columns(
691 &self,
692 locked: &DataFrame,
693 rows: usize,
694 width: u16,
695 nothing_else_scrolls: bool,
696 table_width: u16,
697 sizing: &mut Sizing,
698 ) -> FittedColumns {
699 let room = width.saturating_sub(2);
702 if nothing_else_scrolls {
703 let fitted = self.fit_columns(locked, rows, room, 0, Side::Frozen, sizing);
704 if fitted.cols.len() == locked.width() {
705 return fitted;
706 }
707 }
708 let reserve = (table_width / 3)
709 .clamp(MIN_SCROLL_RESERVE, MAX_SCROLL_RESERVE)
710 .min(table_width / 2);
711 self.fit_columns(
712 locked,
713 rows,
714 room.saturating_sub(reserve),
715 0,
716 Side::Frozen,
717 sizing,
718 )
719 }
720
721 fn fit_columns(
725 &self,
726 df: &DataFrame,
727 rows: usize,
728 width: u16,
729 lead: u16,
730 side: Side,
731 sizing: &mut Sizing,
732 ) -> FittedColumns {
733 let drifting = self.drifting_columns();
734 let min_partial = min_partial_width(self.glyphs);
735 let mut scratch = String::new();
737 let mut fitted = FittedColumns {
738 cols: Vec::new(),
739 widths: Vec::new(),
740 rows,
741 hint_cell: false,
742 };
743 let mut used = lead;
744 for col_index in 0..df.width() {
745 let remaining = width.saturating_sub(used);
746 if remaining == 0 {
747 break;
748 }
749 let source = sizing.page.as_mut().map(|(cells, frame, offset)| {
750 let series = frame[col_index].as_materialized_series().clone();
751 (&mut **cells, series, *offset)
752 });
753 let mut col = self.slice_column(df, col_index, rows, &drifting, &mut scratch, source);
754 let dtype = sizing
755 .schema
756 .get(col.name.as_str())
757 .unwrap_or_else(|| df[col_index].dtype());
758 col.width = sizing
759 .widths
760 .width(&col.name, dtype, col.measure(), sizing.cap);
761 let first = fitted.cols.is_empty();
762 let Some(w) = fit_column(&col, remaining, min_partial, first, side) else {
763 break;
764 };
765 let whole = w >= col.natural_width();
766 used = used
767 .saturating_add(w)
768 .saturating_add(self.table_cell_padding);
769 fitted.cols.push(col);
770 fitted.widths.push(w);
771 if !whole {
772 break;
774 }
775 }
776 fitted
777 }
778
779 fn format_cells(
781 &self,
782 col_data: &Column,
783 rows: usize,
784 col_fmt: &CellFormatter,
785 null_glyph_by_group: &[&'static str],
786 scratch: &mut String,
787 ) -> Cells {
788 #[cfg(test)]
789 tests::FORMATTED.with(|n| n.set(n.get() + 1));
790 let g = self.glyphs;
791 let mut cells = Vec::with_capacity(rows);
792 let mut value_width = 0usize;
793 for row_index in 0..rows {
794 let value = col_data.get(row_index).unwrap();
795 if matches!(value, AnyValue::Null) {
796 let glyph = self
797 .drift_rows
798 .get(row_index)
799 .and_then(|group| null_glyph_by_group.get(*group as usize))
800 .copied()
801 .unwrap_or(g.null);
802 value_width = value_width.max(crate::glyphs::cell_width(glyph));
803 cells.push(SliceCell::Null(glyph));
804 continue;
805 }
806 let text = match &value {
809 AnyValue::List(items) => Cow::Owned(crate::exact::list_preview(items)),
810 value => numfmt::format_any_value(col_fmt, value, scratch),
811 };
812 let text = crate::exact::cell_preview(&text, g);
815 value_width = value_width.max(crate::glyphs::cell_width(&text));
816 cells.push(SliceCell::Value(text));
817 }
818 Cells {
819 has_values: cells.iter().any(|c| matches!(c, SliceCell::Value(_))),
820 cells,
821 value_width: u16::try_from(value_width).unwrap_or(u16::MAX),
822 }
823 }
824
825 fn slice_column(
827 &self,
828 df: &DataFrame,
829 col_index: usize,
830 rows: usize,
831 drifting: &HashSet<&str>,
832 scratch: &mut String,
833 source: Option<(&mut PageCells, Series, usize)>,
834 ) -> ColumnSlice {
835 let g = self.glyphs;
836 let col_data = &df[col_index];
837 let name = col_data.name().as_str();
838 let dtype = col_data.dtype();
839 let is_binary = self.binary_cols.contains(name);
842 let cell_style = if is_binary {
843 let mut s = Style::default().add_modifier(Modifier::ITALIC);
844 if let Some(c) = self.binary_col {
845 s = s.fg(c);
846 }
847 Some(s)
848 } else {
849 self.column_type_color(dtype)
850 .map(|c| Style::default().fg(c))
851 };
852 let col_fmt = if is_binary {
855 CellFormatter::Passthrough
856 } else {
857 self.number_format.formatter_for(name, dtype)
858 };
859 let right_align = self.number_format.align_numeric_right
862 && !is_binary
863 && numfmt::is_right_aligned_dtype(dtype);
864 let null_glyph_by_group = self.null_glyphs_for(name, g, drifting);
867
868 let rows = rows.min(col_data.len());
869 let drift = &self.drift_rows[..rows.min(self.drift_rows.len())];
870 let cells = match source {
871 Some((page, series, offset)) => match page.columns.get_mut(name) {
872 Some(kept)
873 if Arc::ptr_eq(&kept.series.0, &series.0)
874 && kept.offset == offset
875 && kept.rows == rows
876 && kept.format == col_fmt
877 && std::ptr::eq(kept.glyphs, g)
878 && kept.nulls == null_glyph_by_group
879 && kept.drift == drift =>
880 {
881 kept.used = true;
882 kept.cells.clone()
883 }
884 _ => {
885 let cells = Arc::new(self.format_cells(
886 col_data,
887 rows,
888 &col_fmt,
889 &null_glyph_by_group,
890 scratch,
891 ));
892 let kept = KeptCells {
893 series,
894 offset,
895 rows,
896 format: col_fmt,
897 glyphs: g,
898 nulls: null_glyph_by_group,
899 drift: drift.to_vec(),
900 cells: cells.clone(),
901 used: true,
902 };
903 page.columns.insert(name.to_string(), kept);
904 cells
905 }
906 },
907 None => {
908 Arc::new(self.format_cells(col_data, rows, &col_fmt, &null_glyph_by_group, scratch))
909 }
910 };
911
912 let drift_mark = self.drift_mark_for(name, drifting);
913 let sort_mark = self.sort_mark_for(name);
914 let header_width = crate::glyphs::cell_width(name)
917 + crate::glyphs::cell_width(drift_mark)
918 + crate::glyphs::cell_width(sort_mark);
919 let type_label = self.dtype_row.then(|| {
922 let label = if is_binary {
923 dtype_label(&DataType::Binary)
924 } else {
925 dtype_label(dtype)
926 };
927 match self.units.iter().find(|(column, _)| column == name) {
928 Some((_, unit)) => format!("{label} {} {unit}", self.glyphs.middot),
929 None => label,
930 }
931 });
932 let type_width = type_label
933 .as_deref()
934 .map(crate::glyphs::cell_width)
935 .unwrap_or(0);
936 let cells_u16 = |w: usize| u16::try_from(w).unwrap_or(u16::MAX);
937 let width = cells_u16(header_width.max(type_width)).max(cells.value_width);
938 ColumnSlice {
939 name: name.to_string(),
940 drift_mark,
941 sort_mark,
942 header_width: cells_u16(header_width),
943 type_label,
944 type_width: cells_u16(type_width),
945 cells,
946 width,
947 right_align,
948 clips: is_binary || is_truncatable_dtype(dtype),
949 cell_style,
950 colour: if is_binary {
951 self.binary_col
952 } else {
953 self.column_type_color(dtype)
954 },
955 }
956 }
957
958 fn draw_columns<'f>(
962 &self,
963 fitted: &'f FittedColumns,
964 area: Rect,
965 buf: &mut Buffer,
966 state: &mut TableState,
967 leading_gap: bool,
968 ) -> (Vec<(u16, u16, String)>, usize) {
969 let g = self.glyphs;
970 let fit = |text: &'f str, width: u16| -> Cow<'f, str> {
972 crate::glyphs::fit_cells(text, usize::from(width), g.ellipsis)
973 };
974 let null_style = Style::default()
977 .fg(self.dimmed)
978 .add_modifier(Modifier::ITALIC);
979 let columns = || fitted.cols.iter().zip(fitted.widths.iter().copied());
980
981 let rows: Vec<Row> = (0..fitted.rows)
982 .map(|row_index| {
983 let cells: Vec<Cell> = columns()
984 .map(|(col, w)| {
985 let span = match col.cells.cells.get(row_index) {
986 Some(SliceCell::Null(glyph)) => Span::styled(fit(glyph, w), null_style),
987 Some(SliceCell::Value(text)) => {
988 let mut style = col.cell_style.unwrap_or_default();
989 if self.match_cells.as_ref().is_some_and(|cells| {
990 cells.get(col.name.as_str()).is_some_and(|rows| {
991 rows.contains(&(self.drawn_from + row_index))
992 })
993 }) {
994 style = style.patch(self.find_style);
995 }
996 Span::styled(fit(text, w), style)
997 }
998 None => return Cell::default(),
999 };
1000 Cell::from(cell_line(vec![span], w, col.right_align))
1001 })
1002 .collect();
1003 let row_style = if row_index % 2 == 1 {
1004 self.alternate_row_bg
1005 .map(|c| Style::default().bg(c))
1006 .unwrap_or_default()
1007 } else {
1008 Style::default()
1009 };
1010 Row::new(cells).style(row_style)
1011 })
1012 .collect();
1013
1014 let header_row_style = if self.header_bg == Color::Reset {
1015 Style::default().fg(self.header_fg)
1016 } else {
1017 Style::default().bg(self.header_bg).fg(self.header_fg)
1018 };
1019 let last = fitted.cols.len().saturating_sub(1);
1022 let cursor = self
1024 .current_column
1025 .as_deref()
1026 .and_then(|name| fitted.cols.iter().position(|c| c.name == name));
1027 let cell_style = if cursor.is_some() && self.find_column == self.current_column {
1030 self.find_style
1031 } else {
1032 self.cell_cursor_style
1033 };
1034 let cell_style = if self
1037 .highlight_style()
1038 .add_modifier
1039 .contains(Modifier::REVERSED)
1040 {
1041 cell_style.remove_modifier(Modifier::REVERSED)
1042 } else {
1043 cell_style
1044 };
1045 let headers: Vec<Cell> = columns()
1046 .enumerate()
1047 .map(|(i, (col, w))| {
1048 let hint = u16::from(fitted.hint_cell && i == last && w > 1);
1051 let (name_w, type_w) = if col.type_label.is_some() {
1052 (w, w - hint)
1053 } else {
1054 (w - hint, w)
1055 };
1056 let name_style = match col.colour {
1057 Some(c) => Style::default().fg(c).add_modifier(Modifier::BOLD),
1058 None => Style::default().add_modifier(Modifier::BOLD),
1059 };
1060 let marks = crate::glyphs::cell_width(col.drift_mark)
1063 + crate::glyphs::cell_width(col.sort_mark);
1064 let mut heading = Vec::with_capacity(3);
1065 match u16::try_from(marks).ok().filter(|&m| m < name_w) {
1066 Some(marks) => {
1067 heading.push(Span::styled(fit(&col.name, name_w - marks), name_style));
1068 if !col.drift_mark.is_empty() {
1069 heading.push(Span::styled(
1070 col.drift_mark,
1071 Style::default().fg(self.dimmed),
1072 ));
1073 }
1074 if !col.sort_mark.is_empty() {
1077 heading.push(Span::styled(col.sort_mark, name_style));
1078 }
1079 }
1080 None => heading.push(Span::styled(fit(&col.name, name_w), name_style)),
1081 }
1082 let mut lines = vec![cell_line(heading, name_w, col.right_align)];
1083 if let Some(label) = &col.type_label {
1084 let type_style = match col.colour {
1085 _ if self.retyped.contains(&col.name) => Style::default().fg(self.accent),
1087 Some(c) => Style::default().fg(c),
1088 None => Style::default().fg(self.dimmed),
1089 };
1090 let label = Span::styled(fit(label, type_w), type_style);
1091 lines.push(cell_line(vec![label], type_w, col.right_align));
1092 }
1093 let cell = Cell::from(Text::from(lines));
1094 if cursor == Some(i) {
1095 cell.style(self.cell_cursor_style)
1096 } else {
1097 cell
1098 }
1099 })
1100 .collect();
1101
1102 let mut table = Table::new(rows, fitted.widths.clone())
1103 .column_spacing(self.table_cell_padding)
1104 .header(
1105 Row::new(headers)
1106 .style(header_row_style)
1107 .height(self.header_height()),
1108 )
1109 .row_highlight_style(self.highlight_style())
1110 .column_highlight_style(self.column_cursor_style)
1111 .cell_highlight_style(cell_style);
1112 if leading_gap {
1113 table = table
1115 .highlight_symbol(" ")
1116 .highlight_spacing(HighlightSpacing::Always);
1117 }
1118 state.select_column(cursor);
1121 StatefulWidget::render(table, area, buf, state);
1122 state.select_column(None);
1123 let lead = u16::from(leading_gap).min(area.width);
1126 let columns_area = Rect {
1127 x: area.x + lead,
1128 width: area.width - lead,
1129 ..area
1130 };
1131 let spans = ratatui::layout::Layout::horizontal(
1132 fitted
1133 .widths
1134 .iter()
1135 .map(|&w| ratatui::layout::Constraint::Length(w)),
1136 )
1137 .flex(ratatui::layout::Flex::Start)
1138 .spacing(self.table_cell_padding)
1139 .split(columns_area);
1140 let columns = spans
1141 .iter()
1142 .zip(&fitted.cols)
1143 .map(|(span, col)| (span.x, span.right(), col.name.clone()))
1144 .collect();
1145 (
1146 columns,
1147 fitted.rows.min(usize::from(
1148 area.height.saturating_sub(self.header_height()),
1149 )),
1150 )
1151 }
1152
1153 fn measure_column(
1156 &self,
1157 state: &mut DataTableState,
1158 name: &str,
1159 offset: usize,
1160 rows: usize,
1161 cap: u16,
1162 ) -> u16 {
1163 if let Some(width) = state.drawn_width(name) {
1164 return width;
1165 }
1166 let Some(page) = state.page_column(name, offset, rows) else {
1167 return crate::widgets::column_widths::UNSEEN_WIDTH;
1168 };
1169 let col = self.slice_column(
1170 &page,
1171 0,
1172 page.height(),
1173 &self.drifting_columns(),
1174 &mut String::new(),
1175 None,
1176 );
1177 let dtype = state.width_dtype(name);
1178 state.widths.width(name, &dtype, col.measure(), cap)
1179 }
1180
1181 fn fit_pending(&self, state: &mut DataTableState, offset: usize, rows: usize, cap: u16) {
1184 let pending = state.widths.fits_pending();
1185 if pending.is_empty() || !state.buffer_on_hand() {
1186 return;
1187 }
1188 let drifting = self.drifting_columns();
1189 let mut scratch = String::new();
1190 for (name, dtype) in pending {
1191 let Some(page) = state.page_column(&name, offset, rows) else {
1192 continue;
1193 };
1194 let col = self.slice_column(&page, 0, page.height(), &drifting, &mut scratch, None);
1195 state.widths.fit(&name, &dtype, col.measure(), cap);
1196 }
1197 }
1198
1199 fn render_row_numbers(&self, area: Rect, buf: &mut Buffer, params: RowNumbersParams) {
1200 let header_style = if self.header_bg == Color::Reset {
1202 Style::default().fg(self.header_fg)
1203 } else {
1204 Style::default().bg(self.header_bg).fg(self.header_fg)
1205 };
1206 let header_h = self.header_height().min(area.height);
1207 let header_fill = " ".repeat(area.width as usize);
1208 for dy in 0..header_h {
1209 Paragraph::new(header_fill.clone())
1210 .style(header_style)
1211 .render(
1212 Rect {
1213 x: area.x,
1214 y: area.y + dy,
1215 width: area.width,
1216 height: 1,
1217 },
1218 buf,
1219 );
1220 }
1221
1222 let rows_to_render = params
1224 .visible_rows
1225 .min(params.num_rows.saturating_sub(params.start_row));
1226
1227 if rows_to_render == 0 {
1228 return;
1229 }
1230
1231 let number = |row_idx: usize| params.numbers.get(row_idx).copied().unwrap_or_default();
1232 let max_row_num = (0..rows_to_render).map(number).max().unwrap_or_default();
1233 let max_width = max_row_num.to_string().len();
1234
1235 for row_idx in 0..rows_to_render.min(area.height.saturating_sub(header_h) as usize) {
1236 let row_num_text = number(row_idx).to_string();
1237
1238 let padding = max_width.saturating_sub(row_num_text.len());
1240 let padded_text = format!("{}{}", " ".repeat(padding), row_num_text);
1241
1242 let is_selected = params.selected_row == Some(row_idx);
1245 let (fg, bg) = if is_selected {
1246 (
1247 Color::Reset,
1248 self.selected_bg
1249 .or(self.alternate_row_bg.filter(|_| row_idx % 2 == 1)),
1250 )
1251 } else {
1252 (
1253 self.row_numbers_fg,
1254 self.alternate_row_bg.filter(|_| row_idx % 2 == 1),
1255 )
1256 };
1257 let row_num_style = match bg {
1258 Some(bg_color) => Style::default().fg(fg).bg(bg_color),
1259 None => Style::default().fg(fg),
1260 };
1261
1262 let y = area.y + row_idx as u16 + header_h;
1263 if y < area.y + area.height {
1264 Paragraph::new(padded_text).style(row_num_style).render(
1265 Rect {
1266 x: area.x,
1267 y,
1268 width: area.width,
1269 height: 1,
1270 },
1271 buf,
1272 );
1273 }
1274 }
1275 }
1276}
1277
1278impl StatefulWidget for DataTable {
1279 type State = DataTableState;
1280
1281 fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
1282 state.page_cells.next_frame();
1283 (self.sort_columns, self.sort_descending) = state.header_sort();
1286 self.current_column = state.current_column().map(str::to_string);
1287 self.units = state.units();
1288 self.retyped = state.retyped_columns();
1289 let cap = self.text_cap(area.width);
1292 let whole = area;
1293 state.drawn = None;
1294 let rail_area = Rect {
1295 x: area.x,
1296 y: area.y,
1297 width: 1.min(area.width),
1298 height: area.height,
1299 };
1300 let area = Rect {
1301 x: area.x.saturating_add(1),
1302 y: area.y,
1303 width: area.width.saturating_sub(1),
1304 height: area.height,
1305 };
1306 let header_h = self.header_height();
1307 state.visible_termcols = area.width as usize;
1308 let new_visible_rows = (area.height as usize).saturating_sub(header_h as usize);
1309 let visible_rows_changed = new_visible_rows != state.visible_rows;
1310 state.visible_rows = new_visible_rows;
1311
1312 if let Some(selected) = state.table_state.selected()
1315 && selected >= state.visible_rows
1316 && state.visible_rows > 0
1317 {
1318 let overflow = selected - (state.visible_rows - 1);
1319 state.view.start_row += overflow;
1320 state.table_state.select(Some(state.visible_rows - 1));
1321 }
1322
1323 if visible_rows_changed && !state.page_on_hand(state.view.start_row) {
1326 state.needs_recollect = true;
1329 }
1330
1331 if let Some(error) = state.error()
1334 && !state.suppress_error_display
1335 {
1336 Paragraph::new(format!("Error: {}", user_message_from_polars(error)))
1337 .centered()
1338 .block(
1339 Block::default()
1340 .borders(Borders::NONE)
1341 .padding(Padding::top(area.height / 2)),
1342 )
1343 .wrap(ratatui::widgets::Wrap { trim: true })
1344 .render(area, buf);
1345 return;
1346 }
1347 let start_row = state.start_to_draw();
1350 self.drawn_from = start_row;
1351 state.on_screen = None;
1352 let selected = state.table_state.selected();
1355 self.find_column = self.find_cell.take().and_then(|(row, name)| {
1356 (row.checked_sub(start_row) == selected && selected.is_some()).then_some(name)
1357 });
1358
1359 let mut scroll_indicator: Option<ScrollCue> = None;
1361
1362 let numbers = if state.row_numbers() {
1364 state.row_numbers_from(start_row, state.visible_rows)
1365 } else {
1366 Vec::new()
1367 };
1368 let row_num_width = if state.row_numbers() {
1369 let widest = numbers.iter().max().copied().unwrap_or(1);
1370 widest.to_string().len().max(1) as u16 + 1 } else {
1372 0
1373 };
1374 let row_num_width = row_num_width.min(area.width);
1375 let data_area = Rect {
1376 x: area.x + row_num_width,
1377 width: area.width - row_num_width,
1378 ..area
1379 };
1380 let row_num_area = Rect {
1381 width: row_num_width,
1382 ..area
1383 };
1384 let visible_rows = state.visible_rows;
1385 let row_numbers = |start_row, num_rows, numbers, selected_row| RowNumbersParams {
1386 start_row,
1387 visible_rows,
1388 num_rows,
1389 numbers,
1390 selected_row,
1391 };
1392 let row_number_params = row_numbers(
1393 start_row,
1394 state.num_rows(),
1395 numbers,
1396 state.table_state.selected(),
1397 );
1398
1399 let offset = start_row.saturating_sub(state.view.buffered_start_row);
1402 let rows_room = (area.height as usize).saturating_sub(header_h as usize);
1403 let locked_slice = state
1404 .view
1405 .locked_df
1406 .as_ref()
1407 .and_then(|df| visible_or_header(df, offset, state.visible_rows));
1408 self.fit_pending(state, offset, state.visible_rows.min(rows_room), cap);
1409
1410 if state.view.df.is_some() || state.view.locked_df.is_some() {
1411 if state.row_numbers() {
1412 self.render_row_numbers(row_num_area, buf, row_number_params);
1413 }
1414 let mut drawn_columns = Vec::new();
1415 let mut drawn_rows = 0;
1416 let mut scroll_area = data_area;
1417 let mut leading_gap = false;
1418 if let Some(locked) = locked_slice {
1419 let asked = state.locked_columns_count();
1420 let rule_bottom = (area.y + header_h)
1423 .saturating_add(locked.height().min(rows_room) as u16)
1424 .min(area.bottom());
1425 let mut fitted = self.fit_frozen_columns(
1426 &locked,
1427 locked.height().min(rows_room),
1428 data_area.width,
1429 state.view.column_order.len() <= asked,
1430 area.width,
1431 &mut Sizing {
1432 widths: &mut state.widths,
1433 schema: &state.view.schema,
1434 cap,
1435 page: state
1436 .view
1437 .locked_df
1438 .as_ref()
1439 .map(|frame| (&mut state.page_cells, frame, offset)),
1440 },
1441 );
1442 state.fit_frozen(fitted.cols.len());
1443 let shown = state.frozen_shown().min(fitted.cols.len());
1446 fitted.cols.truncate(shown);
1447 fitted.widths.truncate(shown);
1448 let mut separator_x = data_area.x;
1449 if shown > 0 {
1450 let gaps = self.table_cell_padding.saturating_mul(shown as u16 - 1);
1451 let columns_width = fitted.widths.iter().sum::<u16>().saturating_add(gaps);
1452 let frozen_area = Rect {
1455 width: columns_width.saturating_add(1).min(data_area.width),
1456 ..data_area
1457 };
1458 let (columns, rows) =
1459 self.draw_columns(&fitted, frozen_area, buf, &mut state.table_state, false);
1460 drawn_columns.extend(columns);
1461 drawn_rows = drawn_rows.max(rows);
1462 separator_x = frozen_area.right();
1463 }
1464 if separator_x < data_area.right() {
1465 let rule = if shown < asked {
1468 self.glyphs.rule_broken
1469 } else {
1470 self.glyphs.rule
1471 };
1472 for y in area.y..rule_bottom {
1473 let cell = &mut buf[(separator_x, y)];
1474 cell.set_symbol(rule);
1475 cell.set_style(Style::default().fg(self.separator_fg));
1476 }
1477 }
1478 let scroll_x = separator_x.saturating_add(1).min(data_area.right());
1479 scroll_area = Rect {
1480 x: scroll_x,
1481 width: data_area.right() - scroll_x,
1482 ..data_area
1483 };
1484 leading_gap = true;
1485 }
1486 let room = Room {
1488 width: scroll_area.width,
1489 lead: u16::from(leading_gap),
1490 padding: self.table_cell_padding,
1491 };
1492 let rows = state.visible_rows.min(rows_room);
1493 state.land_column_moves(room, |state, name| {
1494 self.measure_column(state, name, offset, rows, cap)
1495 });
1496 if let Some(sliced_df) = state
1497 .view
1498 .df
1499 .as_ref()
1500 .and_then(|df| visible_or_header(df, offset, state.visible_rows))
1501 {
1502 let total_cols = sliced_df.width();
1503 let (shown, columns, rows) = self.render_scrolling(
1504 &sliced_df,
1505 scroll_area,
1506 buf,
1507 &mut state.table_state,
1508 leading_gap,
1509 Sizing {
1510 widths: &mut state.widths,
1511 schema: &state.view.schema,
1512 cap,
1513 page: state
1514 .view
1515 .df
1516 .as_ref()
1517 .map(|frame| (&mut state.page_cells, frame, offset)),
1518 },
1519 );
1520 drawn_columns.extend(columns);
1521 drawn_rows = drawn_rows.max(rows);
1522 let more_left = state.termcol_index > 0;
1523 let more_right = total_cols.saturating_sub(shown);
1524 let first = state.frozen_shown() + state.termcol_index + 1;
1525 let total = state.view.column_order.len();
1526 state.on_screen =
1527 state
1528 .cursor_index()
1529 .filter(|_| total > 1)
1530 .map(|cursor| OnScreen {
1531 first,
1532 last: first + shown.saturating_sub(1),
1533 cursor: cursor + 1,
1534 total,
1535 });
1536 scroll_indicator = Some(ScrollCue {
1537 area: scroll_area,
1538 more_left,
1539 more_right,
1540 });
1541 } else {
1542 let total = state.view.column_order.len();
1544 state.on_screen =
1545 state
1546 .cursor_index()
1547 .filter(|_| total > 1)
1548 .map(|cursor| OnScreen {
1549 first: 1,
1550 last: total,
1551 cursor: cursor + 1,
1552 total,
1553 });
1554 }
1555 state.drawn = Some(DrawnTable {
1556 area: whole,
1557 header: header_h,
1558 start_row,
1559 rows: drawn_rows,
1560 columns: drawn_columns,
1561 });
1562 } else if !state.view.column_order.is_empty() {
1563 let empty_columns: Vec<_> = state
1565 .view
1566 .column_order
1567 .iter()
1568 .map(|name| {
1569 let dtype = state
1570 .view
1571 .schema
1572 .get(name.as_str())
1573 .cloned()
1574 .unwrap_or(DataType::String);
1575 Series::new_empty(name.as_str().into(), &dtype).into()
1576 })
1577 .collect();
1578 match DataFrame::new_infer_height(empty_columns) {
1579 Ok(empty_df) => {
1580 if state.row_numbers() {
1581 self.render_row_numbers(
1582 row_num_area,
1583 buf,
1584 row_numbers(0, 0, Vec::new(), None),
1585 );
1586 }
1587 self.render_scrolling(
1588 &empty_df,
1589 data_area,
1590 buf,
1591 &mut state.table_state,
1592 false,
1593 Sizing {
1594 widths: &mut state.widths,
1595 schema: &state.view.schema,
1596 cap,
1597 page: None,
1598 },
1599 );
1600 }
1601 _ => {
1602 Paragraph::new("No data").render(area, buf);
1603 }
1604 }
1605 } else {
1606 Paragraph::new("No data").render(area, buf);
1608 }
1609
1610 let empty = state.view.num_rows_valid
1612 && state.num_rows() == 0
1613 && !state.view.column_order.is_empty();
1614 if empty && area.height > header_h && data_area.width > 0 {
1615 let line = Rect {
1616 y: area.y + header_h,
1617 height: 1,
1618 ..data_area
1619 };
1620 Paragraph::new("No rows")
1621 .style(Style::default().fg(self.dimmed))
1622 .render(line, buf);
1623 }
1624
1625 if rail_area.width > 0 && rail_area.height > 0 {
1628 let g = self.glyphs;
1629 let header_style = if self.header_bg == Color::Reset {
1630 Style::default().fg(self.header_fg)
1631 } else {
1632 Style::default().bg(self.header_bg).fg(self.header_fg)
1633 };
1634 for dy in 0..header_h.min(rail_area.height) {
1635 let cell = &mut buf[(rail_area.x, rail_area.y + dy)];
1636 cell.set_char(' ');
1637 cell.set_style(header_style);
1638 }
1639 if state.view.df.is_some()
1640 && !empty
1641 && let Some(sel) = state.table_state.selected()
1642 {
1643 let y = rail_area.y + header_h + sel as u16;
1644 if y < rail_area.y + rail_area.height {
1645 let cell = &mut buf[(rail_area.x, y)];
1646 cell.set_symbol(g.rail.trim_end());
1647 let mut style = Style::default()
1648 .fg(self.accent)
1649 .add_modifier(Modifier::BOLD);
1650 if let Some(bg) = self.selected_bg {
1651 style = style.bg(bg);
1652 }
1653 cell.set_style(style);
1654 }
1655 }
1656 }
1657
1658 if let Some(cue) = scroll_indicator
1661 && cue.area.width > 0
1662 && cue.area.height > 0
1663 {
1664 let g = self.glyphs;
1665 let scroll_area = cue.area;
1666 let hidden = cue.more_right;
1667 let hint_style = if self.header_bg == Color::Reset {
1668 Style::default()
1669 .fg(self.accent)
1670 .add_modifier(Modifier::BOLD)
1671 } else {
1672 Style::default()
1673 .bg(self.header_bg)
1674 .fg(self.accent)
1675 .add_modifier(Modifier::BOLD)
1676 };
1677 if cue.more_left && rail_area.width > 0 {
1678 let cell = &mut buf[(rail_area.x, rail_area.y)];
1679 cell.set_symbol(g.arrow_left);
1680 cell.set_style(hint_style);
1681 }
1682 if hidden > 0 {
1683 let y = if header_h > 1 {
1684 scroll_area.y + 1
1685 } else {
1686 scroll_area.y
1687 };
1688 let right = scroll_area.x + scroll_area.width;
1691 let free = (scroll_area.x..right)
1692 .rev()
1693 .take_while(|&x| buf[(x, y)].symbol() == " ")
1694 .count();
1695 let mut text = format!(" +{hidden} {}", g.arrow_right);
1696 if text.chars().count() > free {
1697 text = g.arrow_right.to_string();
1698 }
1699 let w = text.chars().count() as u16;
1700 if scroll_area.width >= w {
1701 let x0 = right - w;
1702 for (i, ch) in text.chars().enumerate() {
1703 let cell = &mut buf[(x0 + i as u16, y)];
1704 cell.set_char(ch);
1705 cell.set_style(hint_style);
1706 }
1707 }
1708 }
1709 }
1710 }
1711}
1712
1713#[cfg(test)]
1714pub(crate) mod tests;