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 stripes_follow_rows: bool,
36 pub column_colors: bool,
38 pub str_col: Option<Color>,
39 pub int_col: Option<Color>,
40 pub float_col: Option<Color>,
41 pub bool_col: Option<Color>,
42 pub temporal_col: Option<Color>,
43 pub binary_col: Option<Color>,
46 pub binary_cols: std::collections::HashSet<String>,
49 pub number_format: NumberFormatSettings,
51 pub dtype_row: bool,
53 pub selected_bg: Option<Color>,
55 pub selection_style: Style,
57 pub accent: Color,
59 pub dimmed: Color,
61 pub drift_rows: Vec<u32>,
64 pub drift_groups: Arc<Vec<crate::formats::schema_union::DriftGroup>>,
66 pub sort_columns: Vec<String>,
69 pub sort_descending: Vec<bool>,
71 pub current_column: Option<String>,
73 pub column_cursor_style: Style,
75 pub cell_cursor_style: Style,
78 pub glyphs: &'static crate::glyphs::Glyphs,
80 pub screen_width: u16,
84 pub find_cell: Option<(usize, String)>,
86 pub find_style: Style,
88 pub(crate) find_column: Option<String>,
90 pub match_cells: Option<std::sync::Arc<crate::find::MatchCells>>,
93 pub(crate) drawn_from: usize,
95 pub(crate) units: Vec<(String, String)>,
98 pub(crate) retyped: Vec<String>,
100}
101
102impl Default for DataTable {
103 fn default() -> Self {
104 Self {
105 header_bg: Color::Reset,
106 header_fg: Color::Reset,
107 row_numbers_fg: Color::Reset,
108 separator_fg: Color::Reset,
109 table_cell_padding: 1,
110 alternate_row_bg: None,
111 stripes_follow_rows: true,
112 column_colors: false,
113 str_col: None,
114 int_col: None,
115 float_col: None,
116 bool_col: None,
117 temporal_col: None,
118 binary_col: None,
119 binary_cols: std::collections::HashSet::new(),
120 number_format: NumberFormatSettings::default(),
121 dtype_row: false,
122 selected_bg: None,
123 selection_style: Style::default(),
124 accent: Color::Reset,
125 dimmed: Color::Reset,
126 drift_rows: Vec::new(),
127 drift_groups: Arc::new(Vec::new()),
128 sort_columns: Vec::new(),
129 sort_descending: Vec::new(),
130 current_column: None,
131 column_cursor_style: Style::default(),
132 cell_cursor_style: Style::default(),
133 glyphs: crate::glyphs::get(),
134 screen_width: 0,
135 find_cell: None,
136 find_style: Style::default(),
137 find_column: None,
138 match_cells: None,
139 drawn_from: 0,
140 units: Vec::new(),
141 retyped: Vec::new(),
142 }
143 }
144}
145
146struct RowNumbersParams {
148 start_row: usize,
149 visible_rows: usize,
150 num_rows: usize,
151 numbers: Vec<usize>,
153 selected_row: Option<usize>,
154}
155
156struct ColumnSlice {
159 name: String,
160 drift_mark: &'static str,
161 sort_mark: &'static str,
162 header_width: u16,
164 type_label: Option<String>,
165 type_width: u16,
166 cells: Arc<Cells>,
167 width: u16,
170 right_align: bool,
171 clips: bool,
174 cell_style: Option<Style>,
175 colour: Option<Color>,
177}
178
179impl ColumnSlice {
180 fn natural_width(&self) -> u16 {
182 self.width
183 }
184
185 fn measure(&self) -> PageMeasure {
186 PageMeasure {
187 header: self.header_width,
188 type_label: self.type_width,
189 values: self.cells.value_width,
190 has_values: self.cells.has_values,
191 clips: self.clips,
192 }
193 }
194}
195
196struct Sizing<'a> {
199 widths: &'a mut ColumnWidths,
200 schema: &'a Schema,
201 cap: u16,
202 page: Option<(&'a mut PageCells, &'a DataFrame, usize)>,
205}
206
207#[derive(Clone)]
209pub(crate) struct Cells {
210 cells: Vec<SliceCell>,
211 widths: Vec<u16>,
213 value_width: u16,
215 has_values: bool,
217}
218
219impl Cells {
220 fn new(cells: Vec<SliceCell>, widths: Vec<u16>) -> Self {
221 Self {
222 value_width: widths.iter().copied().max().unwrap_or(0),
223 has_values: cells.iter().any(|c| matches!(c, SliceCell::Value(_))),
224 cells,
225 widths,
226 }
227 }
228}
229
230#[derive(Default)]
235pub(crate) struct PageCells {
236 columns: std::collections::HashMap<String, KeptCells>,
237}
238
239struct KeptCells {
240 series: Series,
243 offset: usize,
244 rows: usize,
245 format: CellFormatter,
246 cut: usize,
247 glyphs: &'static crate::glyphs::Glyphs,
248 nulls: Vec<&'static str>,
249 drift: Vec<u32>,
250 cells: Arc<Cells>,
251 used: bool,
253}
254
255impl PageCells {
256 pub(crate) fn next_frame(&mut self) {
258 self.columns
259 .retain(|_, kept| std::mem::take(&mut kept.used));
260 }
261}
262
263#[derive(Clone)]
264enum SliceCell {
265 Null(&'static str),
267 Value(String),
268}
269
270struct ScrollCue {
272 area: Rect,
273 more_left: bool,
274 more_right: usize,
276}
277
278struct FittedColumns {
281 cols: Vec<ColumnSlice>,
282 widths: Vec<u16>,
283 rows: usize,
284 hint_cell: bool,
287}
288
289const MIN_SCROLL_RESERVE: u16 = 12;
292
293const MAX_SCROLL_RESERVE: u16 = 40;
294
295fn min_partial_width(g: &crate::glyphs::Glyphs) -> u16 {
298 let marker = u16::try_from(crate::glyphs::cell_width(g.ellipsis)).unwrap_or(u16::MAX);
299 marker.saturating_add(1).max(3)
300}
301
302#[derive(Clone, Copy, PartialEq, Eq)]
305enum Side {
306 Frozen,
309 Scrolling,
312}
313
314fn fit_column(
320 col: &ColumnSlice,
321 remaining: u16,
322 min_partial: u16,
323 first: bool,
324 side: Side,
325) -> Option<u16> {
326 if col.natural_width() <= remaining {
327 return Some(col.natural_width());
328 }
329 if side == Side::Frozen && !first {
330 return None;
331 }
332 if remaining >= min_partial && (col.cells.value_width <= remaining || col.clips) {
333 return Some(remaining);
334 }
335 (side == Side::Scrolling && first && remaining > 0).then_some(remaining)
336}
337
338fn cell_line<'a>(mut spans: Vec<Span<'a>>, width: u16, right: bool) -> Line<'a> {
343 use unicode_width::UnicodeWidthStr;
344 let drawn = |s: &Span| crate::glyphs::cell_width(&s.content);
345 if spans.len() > 1 && spans.iter().any(|s| drawn(s) != s.content.width()) {
346 let style = spans[0].style;
347 let joined: String = spans.iter().map(|s| s.content.as_ref()).collect();
348 spans = vec![Span::styled(joined, style)];
349 }
350 let used: usize = spans.iter().map(drawn).sum();
351 let pad = usize::from(width).saturating_sub(used);
352 if right && pad > 0 {
353 const SPACES: &str = " ";
355 let blank = match SPACES.get(..pad) {
356 Some(blank) => Span::raw(blank),
357 None => Span::raw(" ".repeat(pad)),
358 };
359 spans.insert(0, blank);
360 }
361 Line::from(spans)
362}
363
364fn visible_or_header(df: &DataFrame, offset: usize, len: usize) -> Option<DataFrame> {
368 if df.width() == 0 {
369 return None;
370 }
371 Some(visible_slice(df, offset, len).unwrap_or_else(|| df.clear()))
372}
373
374fn is_truncatable_dtype(dtype: &DataType) -> bool {
378 match dtype {
379 DataType::String | DataType::Binary => true,
380 other => other.is_categorical() || other.is_enum() || other.is_nested(),
381 }
382}
383
384impl DataTable {
385 pub fn new() -> Self {
386 Self::default()
387 }
388
389 pub fn with_colors(
390 mut self,
391 header_bg: Color,
392 header_fg: Color,
393 row_numbers_fg: Color,
394 separator_fg: Color,
395 ) -> Self {
396 self.header_bg = header_bg;
397 self.header_fg = header_fg;
398 self.row_numbers_fg = row_numbers_fg;
399 self.separator_fg = separator_fg;
400 self
401 }
402
403 pub fn with_cell_padding(mut self, padding: u16) -> Self {
404 self.table_cell_padding = padding;
405 self
406 }
407
408 pub fn with_screen_width(mut self, width: u16) -> Self {
410 self.screen_width = width;
411 self
412 }
413
414 pub fn with_alternate_row_bg(mut self, color: Option<Color>) -> Self {
415 self.alternate_row_bg = color;
416 self
417 }
418
419 pub fn with_stripes_follow_rows(mut self, on: bool) -> Self {
421 self.stripes_follow_rows = on;
422 self
423 }
424
425 fn striped(&self, index: usize) -> bool {
429 let first = if self.stripes_follow_rows {
430 self.drawn_from
431 } else {
432 0
433 };
434 (first + index) % 2 == 1
435 }
436
437 pub fn with_column_type_colors(
439 mut self,
440 str_col: Color,
441 int_col: Color,
442 float_col: Color,
443 bool_col: Color,
444 temporal_col: Color,
445 ) -> Self {
446 self.column_colors = true;
447 self.str_col = Some(str_col);
448 self.int_col = Some(int_col);
449 self.float_col = Some(float_col);
450 self.bool_col = Some(bool_col);
451 self.temporal_col = Some(temporal_col);
452 self
453 }
454
455 pub fn with_binary_col(mut self, color: Color) -> Self {
457 self.binary_col = Some(color);
458 self
459 }
460
461 pub fn with_binary_columns(mut self, names: std::collections::HashSet<String>) -> Self {
463 self.binary_cols = names;
464 self
465 }
466
467 pub fn with_number_format(mut self, settings: NumberFormatSettings) -> Self {
469 self.number_format = settings;
470 self
471 }
472
473 pub fn with_dtype_row(mut self, on: bool) -> Self {
475 self.dtype_row = on;
476 self
477 }
478
479 pub fn with_drift(
482 mut self,
483 rows: Vec<u32>,
484 groups: Arc<Vec<crate::formats::schema_union::DriftGroup>>,
485 ) -> Self {
486 self.drift_rows = rows;
487 self.drift_groups = groups;
488 self
489 }
490
491 #[cfg(test)]
494 pub fn with_sort(mut self, columns: Vec<String>, descending: Vec<bool>) -> Self {
495 debug_assert_eq!(columns.len(), descending.len());
496 self.sort_columns = columns;
497 self.sort_descending = descending;
498 self
499 }
500
501 fn sort_mark_for(&self, column: &str) -> &'static str {
504 if let Some(i) = self.sort_columns.iter().position(|c| c == column) {
505 let g = self.glyphs;
506 if self.sort_descending.get(i).copied().unwrap_or(false) {
507 g.sort_desc
508 } else {
509 g.sort_asc
510 }
511 } else {
512 ""
513 }
514 }
515
516 fn drift_mark_for(&self, column: &str, drifting: &HashSet<&str>) -> &'static str {
519 if drifting.contains(column) {
520 self.glyphs.drift_mark
521 } else {
522 ""
523 }
524 }
525
526 fn drifting_columns(&self) -> HashSet<&str> {
529 self.drift_groups
530 .iter()
531 .flat_map(|group| group.absent.iter().chain(group.unread.iter()))
532 .map(|name| name.as_str())
533 .collect()
534 }
535
536 fn null_glyphs_for(
540 &self,
541 column: &str,
542 g: &'static crate::glyphs::Glyphs,
543 drifting: &HashSet<&str>,
544 ) -> Vec<&'static str> {
545 if self.drift_rows.is_empty() || !drifting.contains(column) {
546 return Vec::new();
547 }
548 self.drift_groups
549 .iter()
550 .map(|group| {
551 if group.absent.iter().any(|c| c == column) {
552 g.absent
553 } else if group.unread.iter().any(|c| c == column) {
554 g.conflict
555 } else {
556 g.null
557 }
558 })
559 .collect()
560 }
561
562 pub fn with_selection_colors(
565 mut self,
566 selection_style: Style,
567 selected_bg: Option<Color>,
568 accent: Color,
569 dimmed: Color,
570 ) -> Self {
571 self.selection_style = selection_style;
572 self.selected_bg = selected_bg;
573 self.accent = accent;
574 self.dimmed = dimmed;
575 self
576 }
577
578 pub fn with_match_cells(
580 mut self,
581 cells: Option<std::sync::Arc<crate::find::MatchCells>>,
582 ) -> Self {
583 self.match_cells = cells;
584 self
585 }
586
587 pub fn with_find_cell(mut self, cell: Option<(usize, String)>, style: Style) -> Self {
589 self.find_cell = cell;
590 self.find_style = style;
591 self
592 }
593
594 pub fn with_cursor_styles(mut self, column: Style, cell: Style) -> Self {
597 self.column_cursor_style = column;
598 self.cell_cursor_style = cell;
599 self
600 }
601
602 pub fn header_height(&self) -> u16 {
604 if self.dtype_row { 2 } else { 1 }
605 }
606
607 fn highlight_style(&self) -> Style {
609 self.selection_style
610 }
611
612 fn column_type_color(&self, dtype: &DataType) -> Option<Color> {
614 if !self.column_colors {
615 return None;
616 }
617 match dtype {
618 DataType::String => self.str_col,
619 DataType::Int8
620 | DataType::Int16
621 | DataType::Int32
622 | DataType::Int64
623 | DataType::UInt8
624 | DataType::UInt16
625 | DataType::UInt32
626 | DataType::UInt64 => self.int_col,
627 DataType::Float32 | DataType::Float64 => self.float_col,
628 DataType::Boolean => self.bool_col,
629 DataType::Date | DataType::Datetime(_, _) | DataType::Time | DataType::Duration(_) => {
630 self.temporal_col
631 }
632 _ => None,
633 }
634 }
635
636 #[cfg(test)]
640 pub(crate) fn render_dataframe(
641 &self,
642 df: &DataFrame,
643 area: Rect,
644 buf: &mut Buffer,
645 state: &mut TableState,
646 leading_gap: bool,
647 ) -> usize {
648 let mut widths = ColumnWidths::default();
649 let sizing = Sizing {
650 widths: &mut widths,
651 schema: df.schema(),
652 cap: self.text_cap(area.width),
653 page: None,
654 };
655 self.render_scrolling(df, area, buf, state, leading_gap, sizing)
656 .0
657 }
658
659 fn text_cap(&self, table_width: u16) -> u16 {
661 let basis = if self.screen_width > 0 {
662 self.screen_width
663 } else {
664 table_width
665 };
666 crate::widgets::column_widths::text_cap(basis)
667 }
668
669 fn render_scrolling(
672 &self,
673 df: &DataFrame,
674 area: Rect,
675 buf: &mut Buffer,
676 state: &mut TableState,
677 leading_gap: bool,
678 mut sizing: Sizing,
679 ) -> (usize, Vec<(u16, u16, String)>, usize) {
680 let rows = df
681 .height()
682 .min((area.height as usize).saturating_sub(self.header_height() as usize));
683 let lead = u16::from(leading_gap);
684 let mut fitted = self.fit_columns(df, rows, area.width, lead, Side::Scrolling, &mut sizing);
685 let shown = fitted.cols.len();
686 let mut used = fitted
687 .widths
688 .iter()
689 .fold(lead, |used, &w| used.saturating_add(w))
690 + self
691 .table_cell_padding
692 .saturating_mul(u16::try_from(shown.saturating_sub(1)).unwrap_or(u16::MAX));
693 if let Some(filled) =
694 self.fill_last_column(&mut fitted, area.width.saturating_sub(used), &mut sizing)
695 {
696 used = used.saturating_add(filled);
697 }
698 fitted.hint_cell = shown > 0 && shown < df.width() && used >= area.width;
699 let (columns, rows) = self.draw_columns(&fitted, area, buf, state, leading_gap);
700 (shown, columns, rows)
701 }
702
703 fn fill_last_column(
707 &self,
708 fitted: &mut FittedColumns,
709 room: u16,
710 sizing: &mut Sizing,
711 ) -> Option<u16> {
712 let (col, width) = fitted.cols.last().zip(fitted.widths.last_mut())?;
713 if room == 0 || col.right_align || *width < col.natural_width() {
714 return None;
715 }
716 let dtype = sizing.schema.get(col.name.as_str())?;
717 if sizing.widths.choice(&col.name, dtype) != WidthChoice::Auto {
718 return None;
719 }
720 *width = width.saturating_add(room);
721 sizing.widths.fill(&col.name, dtype, *width);
722 Some(room)
723 }
724
725 fn fit_frozen_columns(
730 &self,
731 locked: &DataFrame,
732 rows: usize,
733 width: u16,
734 nothing_else_scrolls: bool,
735 table_width: u16,
736 sizing: &mut Sizing,
737 ) -> FittedColumns {
738 let room = width.saturating_sub(2);
741 if nothing_else_scrolls {
742 let fitted = self.fit_columns(locked, rows, room, 0, Side::Frozen, sizing);
743 if fitted.cols.len() == locked.width() {
744 return fitted;
745 }
746 }
747 let reserve = (table_width / 3)
748 .clamp(MIN_SCROLL_RESERVE, MAX_SCROLL_RESERVE)
749 .min(table_width / 2);
750 self.fit_columns(
751 locked,
752 rows,
753 room.saturating_sub(reserve),
754 0,
755 Side::Frozen,
756 sizing,
757 )
758 }
759
760 fn fit_columns(
764 &self,
765 df: &DataFrame,
766 rows: usize,
767 width: u16,
768 lead: u16,
769 side: Side,
770 sizing: &mut Sizing,
771 ) -> FittedColumns {
772 let drifting = self.drifting_columns();
773 let min_partial = min_partial_width(self.glyphs);
774 let mut scratch = String::new();
776 let mut fitted = FittedColumns {
777 cols: Vec::new(),
778 widths: Vec::new(),
779 rows,
780 hint_cell: false,
781 };
782 let mut used = lead;
783 for col_index in 0..df.width() {
784 let remaining = width.saturating_sub(used);
785 if remaining == 0 {
786 break;
787 }
788 let source = sizing.page.as_mut().map(|(cells, frame, offset)| {
789 let series = frame[col_index].as_materialized_series().clone();
790 (&mut **cells, series, *offset)
791 });
792 let mut col = self.slice_column(df, col_index, rows, &drifting, &mut scratch, source);
793 let dtype = sizing
794 .schema
795 .get(col.name.as_str())
796 .unwrap_or_else(|| df[col_index].dtype());
797 col.width = sizing
798 .widths
799 .width(&col.name, dtype, col.measure(), sizing.cap);
800 let first = fitted.cols.is_empty();
801 let Some(w) = fit_column(&col, remaining, min_partial, first, side) else {
802 break;
803 };
804 let whole = w >= col.natural_width();
805 used = used
806 .saturating_add(w)
807 .saturating_add(self.table_cell_padding);
808 fitted.cols.push(col);
809 fitted.widths.push(w);
810 if !whole {
811 break;
813 }
814 }
815 fitted
816 }
817
818 fn cell_cut(&self) -> usize {
821 crate::exact::cell_cut(self.screen_width)
822 }
823
824 fn format_cells(
828 &self,
829 col_data: &Column,
830 rows: usize,
831 col_fmt: &CellFormatter,
832 null_glyph_by_group: &[&'static str],
833 scratch: &mut String,
834 kept: Option<(Cells, &[u32], isize)>,
835 ) -> Cells {
836 let mut cells = Vec::with_capacity(rows);
837 let mut widths = Vec::with_capacity(rows);
838 let (mut old, old_drift, shift) = match kept {
839 Some((old, drift, shift)) => (Some(old), drift, shift),
840 None => (None, &[][..], 0),
841 };
842 for row_index in 0..rows {
843 let from = old.as_mut().and_then(|old| {
844 let at = usize::try_from(row_index as isize + shift).ok()?;
845 (at < old.cells.len() && old_drift.get(at) == self.drift_rows.get(row_index)).then(
847 || {
848 let cell = std::mem::replace(&mut old.cells[at], SliceCell::Null(""));
849 (cell, old.widths[at])
850 },
851 )
852 });
853 let (cell, width) = from.unwrap_or_else(|| {
854 self.format_cell(col_data, row_index, col_fmt, null_glyph_by_group, scratch)
855 });
856 cells.push(cell);
857 widths.push(width);
858 }
859 Cells::new(cells, widths)
860 }
861
862 fn format_cell(
864 &self,
865 col_data: &Column,
866 row_index: usize,
867 col_fmt: &CellFormatter,
868 null_glyph_by_group: &[&'static str],
869 scratch: &mut String,
870 ) -> (SliceCell, u16) {
871 #[cfg(test)]
872 tests::FORMATTED.with(|n| n.set(n.get() + 1));
873 let g = self.glyphs;
874 let width = |text: &str| u16::try_from(crate::glyphs::cell_width(text)).unwrap_or(u16::MAX);
875 let value = col_data.get(row_index).unwrap();
876 if matches!(value, AnyValue::Null) {
877 let glyph = self
878 .drift_rows
879 .get(row_index)
880 .and_then(|group| null_glyph_by_group.get(*group as usize))
881 .copied()
882 .unwrap_or(g.null);
883 return (SliceCell::Null(glyph), width(glyph));
884 }
885 let text = match &value {
888 AnyValue::List(items) => Cow::Owned(crate::exact::list_preview(items)),
889 value => numfmt::format_any_value(col_fmt, value, scratch),
890 };
891 let text = crate::exact::cell_text(text, g, self.cell_cut());
894 let w = width(&text);
895 (SliceCell::Value(text), w)
896 }
897
898 fn slice_column(
900 &self,
901 df: &DataFrame,
902 col_index: usize,
903 rows: usize,
904 drifting: &HashSet<&str>,
905 scratch: &mut String,
906 source: Option<(&mut PageCells, Series, usize)>,
907 ) -> ColumnSlice {
908 let g = self.glyphs;
909 let col_data = &df[col_index];
910 let name = col_data.name().as_str();
911 let dtype = col_data.dtype();
912 let is_binary = self.binary_cols.contains(name);
915 let cell_style = if is_binary {
916 let mut s = Style::default().add_modifier(Modifier::ITALIC);
917 if let Some(c) = self.binary_col {
918 s = s.fg(c);
919 }
920 Some(s)
921 } else {
922 self.column_type_color(dtype)
923 .map(|c| Style::default().fg(c))
924 };
925 let col_fmt = if is_binary {
928 CellFormatter::Passthrough
929 } else {
930 self.number_format.formatter_for(name, dtype)
931 };
932 let right_align = self.number_format.align_numeric_right
935 && !is_binary
936 && numfmt::is_right_aligned_dtype(dtype);
937 let null_glyph_by_group = self.null_glyphs_for(name, g, drifting);
940
941 let rows = rows.min(col_data.len());
942 let drift = &self.drift_rows[..rows.min(self.drift_rows.len())];
943 let cut = self.cell_cut();
944 let cells = match source {
945 Some((page, series, offset)) => match page.columns.get_mut(name) {
946 Some(kept)
947 if Arc::ptr_eq(&kept.series.0, &series.0)
948 && kept.format == col_fmt
949 && kept.cut == cut
950 && std::ptr::eq(kept.glyphs, g)
951 && kept.nulls == null_glyph_by_group =>
952 {
953 kept.used = true;
954 if kept.offset != offset || kept.rows != rows || kept.drift != drift {
955 let old = std::mem::replace(
956 &mut kept.cells,
957 Arc::new(Cells::new(Vec::new(), Vec::new())),
958 );
959 let old = Arc::try_unwrap(old).unwrap_or_else(|old| (*old).clone());
961 let shift = offset as isize - kept.offset as isize;
962 kept.cells = Arc::new(self.format_cells(
963 col_data,
964 rows,
965 &col_fmt,
966 &null_glyph_by_group,
967 scratch,
968 Some((old, &kept.drift, shift)),
969 ));
970 kept.offset = offset;
971 kept.rows = rows;
972 kept.drift.clear();
973 kept.drift.extend_from_slice(drift);
974 }
975 kept.cells.clone()
976 }
977 _ => {
978 let cells = Arc::new(self.format_cells(
979 col_data,
980 rows,
981 &col_fmt,
982 &null_glyph_by_group,
983 scratch,
984 None,
985 ));
986 let kept = KeptCells {
987 series,
988 offset,
989 rows,
990 format: col_fmt,
991 cut,
992 glyphs: g,
993 nulls: null_glyph_by_group,
994 drift: drift.to_vec(),
995 cells: cells.clone(),
996 used: true,
997 };
998 page.columns.insert(name.to_string(), kept);
999 cells
1000 }
1001 },
1002 None => Arc::new(self.format_cells(
1003 col_data,
1004 rows,
1005 &col_fmt,
1006 &null_glyph_by_group,
1007 scratch,
1008 None,
1009 )),
1010 };
1011
1012 let drift_mark = self.drift_mark_for(name, drifting);
1013 let sort_mark = self.sort_mark_for(name);
1014 let header_width = crate::glyphs::cell_width(name)
1017 + crate::glyphs::cell_width(drift_mark)
1018 + crate::glyphs::cell_width(sort_mark);
1019 let type_label = self.dtype_row.then(|| {
1022 let label = if is_binary {
1023 dtype_label(&DataType::Binary)
1024 } else {
1025 dtype_label(dtype)
1026 };
1027 match self.units.iter().find(|(column, _)| column == name) {
1028 Some((_, unit)) => format!("{label} {} {unit}", self.glyphs.middot),
1029 None => label,
1030 }
1031 });
1032 let type_width = type_label
1033 .as_deref()
1034 .map(crate::glyphs::cell_width)
1035 .unwrap_or(0);
1036 let cells_u16 = |w: usize| u16::try_from(w).unwrap_or(u16::MAX);
1037 let width = cells_u16(header_width.max(type_width)).max(cells.value_width);
1038 ColumnSlice {
1039 name: name.to_string(),
1040 drift_mark,
1041 sort_mark,
1042 header_width: cells_u16(header_width),
1043 type_label,
1044 type_width: cells_u16(type_width),
1045 cells,
1046 width,
1047 right_align,
1048 clips: is_binary || is_truncatable_dtype(dtype),
1049 cell_style,
1050 colour: if is_binary {
1051 self.binary_col
1052 } else {
1053 self.column_type_color(dtype)
1054 },
1055 }
1056 }
1057
1058 fn draw_columns<'f>(
1062 &self,
1063 fitted: &'f FittedColumns,
1064 area: Rect,
1065 buf: &mut Buffer,
1066 state: &mut TableState,
1067 leading_gap: bool,
1068 ) -> (Vec<(u16, u16, String)>, usize) {
1069 let g = self.glyphs;
1070 let fit = |text: &'f str, width: u16| -> Cow<'f, str> {
1072 crate::glyphs::fit_cells(text, usize::from(width), g.ellipsis)
1073 };
1074 let null_style = Style::default()
1077 .fg(self.dimmed)
1078 .add_modifier(Modifier::ITALIC);
1079 let columns = || fitted.cols.iter().zip(fitted.widths.iter().copied());
1080
1081 let rows: Vec<Row> = (0..fitted.rows)
1082 .map(|row_index| {
1083 let cells: Vec<Cell> = columns()
1084 .map(|(col, w)| {
1085 let span = match col.cells.cells.get(row_index) {
1086 Some(SliceCell::Null(glyph)) => Span::styled(fit(glyph, w), null_style),
1087 Some(SliceCell::Value(text)) => {
1088 let mut style = col.cell_style.unwrap_or_default();
1089 if self.match_cells.as_ref().is_some_and(|cells| {
1090 cells.get(col.name.as_str()).is_some_and(|rows| {
1091 rows.contains(&(self.drawn_from + row_index))
1092 })
1093 }) {
1094 style = style.patch(self.find_style);
1095 }
1096 Span::styled(fit(text, w), style)
1097 }
1098 None => return Cell::default(),
1099 };
1100 let mut spans = Vec::with_capacity(1 + usize::from(col.right_align));
1102 spans.push(span);
1103 Cell::from(cell_line(spans, w, col.right_align))
1104 })
1105 .collect();
1106 let row_style = if self.striped(row_index) {
1107 self.alternate_row_bg
1108 .map(|c| Style::default().bg(c))
1109 .unwrap_or_default()
1110 } else {
1111 Style::default()
1112 };
1113 Row::new(cells).style(row_style)
1114 })
1115 .collect();
1116
1117 let header_row_style = if self.header_bg == Color::Reset {
1118 Style::default().fg(self.header_fg)
1119 } else {
1120 Style::default().bg(self.header_bg).fg(self.header_fg)
1121 };
1122 let last = fitted.cols.len().saturating_sub(1);
1125 let cursor = self
1127 .current_column
1128 .as_deref()
1129 .and_then(|name| fitted.cols.iter().position(|c| c.name == name));
1130 let cell_style = if cursor.is_some() && self.find_column == self.current_column {
1133 self.find_style
1134 } else {
1135 self.cell_cursor_style
1136 };
1137 let cell_style = if self
1140 .highlight_style()
1141 .add_modifier
1142 .contains(Modifier::REVERSED)
1143 {
1144 cell_style.remove_modifier(Modifier::REVERSED)
1145 } else {
1146 cell_style
1147 };
1148 let headers: Vec<Cell> = columns()
1149 .enumerate()
1150 .map(|(i, (col, w))| {
1151 let hint = u16::from(fitted.hint_cell && i == last && w > 1);
1154 let (name_w, type_w) = if col.type_label.is_some() {
1155 (w, w - hint)
1156 } else {
1157 (w - hint, w)
1158 };
1159 let name_style = match col.colour {
1160 Some(c) => Style::default().fg(c).add_modifier(Modifier::BOLD),
1161 None => Style::default().add_modifier(Modifier::BOLD),
1162 };
1163 let marks = crate::glyphs::cell_width(col.drift_mark)
1166 + crate::glyphs::cell_width(col.sort_mark);
1167 let mut heading = Vec::with_capacity(3);
1168 match u16::try_from(marks).ok().filter(|&m| m < name_w) {
1169 Some(marks) => {
1170 heading.push(Span::styled(fit(&col.name, name_w - marks), name_style));
1171 if !col.drift_mark.is_empty() {
1172 heading.push(Span::styled(
1173 col.drift_mark,
1174 Style::default().fg(self.dimmed),
1175 ));
1176 }
1177 if !col.sort_mark.is_empty() {
1180 heading.push(Span::styled(col.sort_mark, name_style));
1181 }
1182 }
1183 None => heading.push(Span::styled(fit(&col.name, name_w), name_style)),
1184 }
1185 let mut lines = vec![cell_line(heading, name_w, col.right_align)];
1186 if let Some(label) = &col.type_label {
1187 let type_style = match col.colour {
1188 _ if self.retyped.contains(&col.name) => Style::default().fg(self.accent),
1190 Some(c) => Style::default().fg(c),
1191 None => Style::default().fg(self.dimmed),
1192 };
1193 let label = Span::styled(fit(label, type_w), type_style);
1194 lines.push(cell_line(vec![label], type_w, col.right_align));
1195 }
1196 let cell = Cell::from(Text::from(lines));
1197 if cursor == Some(i) {
1198 cell.style(self.cell_cursor_style)
1199 } else {
1200 cell
1201 }
1202 })
1203 .collect();
1204
1205 let mut table = Table::new(rows, fitted.widths.clone())
1206 .column_spacing(self.table_cell_padding)
1207 .header(
1208 Row::new(headers)
1209 .style(header_row_style)
1210 .height(self.header_height()),
1211 )
1212 .row_highlight_style(self.highlight_style())
1213 .column_highlight_style(self.column_cursor_style)
1214 .cell_highlight_style(cell_style);
1215 if leading_gap {
1216 table = table
1218 .highlight_symbol(" ")
1219 .highlight_spacing(HighlightSpacing::Always);
1220 }
1221 state.select_column(cursor);
1224 StatefulWidget::render(table, area, buf, state);
1225 state.select_column(None);
1226 let lead = u16::from(leading_gap).min(area.width);
1229 let columns_area = Rect {
1230 x: area.x + lead,
1231 width: area.width - lead,
1232 ..area
1233 };
1234 let spans = ratatui::layout::Layout::horizontal(
1235 fitted
1236 .widths
1237 .iter()
1238 .map(|&w| ratatui::layout::Constraint::Length(w)),
1239 )
1240 .flex(ratatui::layout::Flex::Start)
1241 .spacing(self.table_cell_padding)
1242 .split(columns_area);
1243 let columns = spans
1244 .iter()
1245 .zip(&fitted.cols)
1246 .map(|(span, col)| (span.x, span.right(), col.name.clone()))
1247 .collect();
1248 (
1249 columns,
1250 fitted.rows.min(usize::from(
1251 area.height.saturating_sub(self.header_height()),
1252 )),
1253 )
1254 }
1255
1256 fn measure_column(
1259 &self,
1260 state: &mut DataTableState,
1261 name: &str,
1262 offset: usize,
1263 rows: usize,
1264 cap: u16,
1265 ) -> u16 {
1266 if let Some(width) = state.drawn_width(name) {
1267 return width;
1268 }
1269 let Some(page) = state.page_column(name, offset, rows) else {
1270 return crate::widgets::column_widths::UNSEEN_WIDTH;
1271 };
1272 let col = self.slice_column(
1273 &page,
1274 0,
1275 page.height(),
1276 &self.drifting_columns(),
1277 &mut String::new(),
1278 None,
1279 );
1280 let dtype = state.width_dtype(name);
1281 state.widths.width(name, &dtype, col.measure(), cap)
1282 }
1283
1284 fn fit_pending(&self, state: &mut DataTableState, offset: usize, rows: usize, cap: u16) {
1287 let pending = state.widths.fits_pending();
1288 if pending.is_empty() || !state.buffer_on_hand() {
1289 return;
1290 }
1291 let drifting = self.drifting_columns();
1292 let mut scratch = String::new();
1293 for (name, dtype) in pending {
1294 let Some(page) = state.page_column(&name, offset, rows) else {
1295 continue;
1296 };
1297 let col = self.slice_column(&page, 0, page.height(), &drifting, &mut scratch, None);
1298 state.widths.fit(&name, &dtype, col.measure(), cap);
1299 }
1300 }
1301
1302 fn render_row_numbers(&self, area: Rect, buf: &mut Buffer, params: RowNumbersParams) {
1303 let header_style = if self.header_bg == Color::Reset {
1305 Style::default().fg(self.header_fg)
1306 } else {
1307 Style::default().bg(self.header_bg).fg(self.header_fg)
1308 };
1309 let header_h = self.header_height().min(area.height);
1310 let header_fill = " ".repeat(area.width as usize);
1311 for dy in 0..header_h {
1312 Paragraph::new(header_fill.clone())
1313 .style(header_style)
1314 .render(
1315 Rect {
1316 x: area.x,
1317 y: area.y + dy,
1318 width: area.width,
1319 height: 1,
1320 },
1321 buf,
1322 );
1323 }
1324
1325 let rows_to_render = params
1327 .visible_rows
1328 .min(params.num_rows.saturating_sub(params.start_row));
1329
1330 if rows_to_render == 0 {
1331 return;
1332 }
1333
1334 let number = |row_idx: usize| params.numbers.get(row_idx).copied().unwrap_or_default();
1335 let max_row_num = (0..rows_to_render).map(number).max().unwrap_or_default();
1336 let max_width = max_row_num.checked_ilog10().unwrap_or(0) as usize + 1;
1337 let mut padded_text = String::with_capacity(max_width);
1339
1340 for row_idx in 0..rows_to_render.min(area.height.saturating_sub(header_h) as usize) {
1341 use std::fmt::Write as _;
1342 padded_text.clear();
1343 let _ = write!(padded_text, "{:>max_width$}", number(row_idx));
1344
1345 let is_selected = params.selected_row == Some(row_idx);
1348 let striped = self.striped(row_idx);
1349 let (fg, bg) = if is_selected {
1350 (
1351 Color::Reset,
1352 self.selected_bg
1353 .or(self.alternate_row_bg.filter(|_| striped)),
1354 )
1355 } else {
1356 (
1357 self.row_numbers_fg,
1358 self.alternate_row_bg.filter(|_| striped),
1359 )
1360 };
1361 let row_num_style = match bg {
1362 Some(bg_color) => Style::default().fg(fg).bg(bg_color),
1363 None => Style::default().fg(fg),
1364 };
1365
1366 let y = area.y + row_idx as u16 + header_h;
1367 if y < area.y + area.height {
1368 let line = Rect {
1369 x: area.x,
1370 y,
1371 width: area.width,
1372 height: 1,
1373 };
1374 buf.set_style(line, row_num_style);
1375 buf.set_stringn(
1376 line.x,
1377 y,
1378 &padded_text,
1379 usize::from(line.width),
1380 Style::default(),
1381 );
1382 }
1383 }
1384 }
1385}
1386
1387impl StatefulWidget for DataTable {
1388 type State = DataTableState;
1389
1390 fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
1391 state.page_cells.next_frame();
1392 if self.screen_width == 0 {
1393 self.screen_width = area.width;
1394 }
1395 (self.sort_columns, self.sort_descending) = state.header_sort();
1398 self.current_column = state.current_column().map(str::to_string);
1399 self.units = state.units();
1400 self.retyped = state.retyped_columns();
1401 let cap = self.text_cap(area.width);
1404 let whole = area;
1405 state.drawn = None;
1406 let rail_area = Rect {
1407 x: area.x,
1408 y: area.y,
1409 width: 1.min(area.width),
1410 height: area.height,
1411 };
1412 let area = Rect {
1413 x: area.x.saturating_add(1),
1414 y: area.y,
1415 width: area.width.saturating_sub(1),
1416 height: area.height,
1417 };
1418 let header_h = self.header_height();
1419 state.visible_termcols = area.width as usize;
1420 let new_visible_rows = (area.height as usize).saturating_sub(header_h as usize);
1421 let visible_rows_changed = new_visible_rows != state.visible_rows;
1422 state.visible_rows = new_visible_rows;
1423
1424 if let Some(selected) = state.table_state.selected()
1427 && selected >= state.visible_rows
1428 && state.visible_rows > 0
1429 {
1430 let overflow = selected - (state.visible_rows - 1);
1431 state.view.start_row += overflow;
1432 state.table_state.select(Some(state.visible_rows - 1));
1433 }
1434
1435 if visible_rows_changed && !state.page_on_hand(state.view.start_row) {
1438 state.needs_recollect = true;
1441 }
1442
1443 if let Some(error) = state.error()
1446 && !state.suppress_error_display
1447 {
1448 Paragraph::new(format!("Error: {}", user_message_from_polars(error)))
1449 .centered()
1450 .block(
1451 Block::default()
1452 .borders(Borders::NONE)
1453 .padding(Padding::top(area.height / 2)),
1454 )
1455 .wrap(ratatui::widgets::Wrap { trim: true })
1456 .render(area, buf);
1457 return;
1458 }
1459 let start_row = state.start_to_draw();
1462 self.drawn_from = start_row;
1463 state.on_screen = None;
1464 let selected = state.table_state.selected();
1467 self.find_column = self.find_cell.take().and_then(|(row, name)| {
1468 (row.checked_sub(start_row) == selected && selected.is_some()).then_some(name)
1469 });
1470
1471 let mut scroll_indicator: Option<ScrollCue> = None;
1473
1474 let numbers = if state.row_numbers() {
1476 state.row_numbers_from(start_row, state.visible_rows)
1477 } else {
1478 Vec::new()
1479 };
1480 let row_num_width = if state.row_numbers() {
1481 let widest = numbers.iter().max().copied().unwrap_or(1);
1482 widest.to_string().len().max(1) as u16 + 1 } else {
1484 0
1485 };
1486 let row_num_width = row_num_width.min(area.width);
1487 let data_area = Rect {
1488 x: area.x + row_num_width,
1489 width: area.width - row_num_width,
1490 ..area
1491 };
1492 let row_num_area = Rect {
1493 width: row_num_width,
1494 ..area
1495 };
1496 let visible_rows = state.visible_rows;
1497 let row_numbers = |start_row, num_rows, numbers, selected_row| RowNumbersParams {
1498 start_row,
1499 visible_rows,
1500 num_rows,
1501 numbers,
1502 selected_row,
1503 };
1504 let row_number_params = row_numbers(
1505 start_row,
1506 state.num_rows(),
1507 numbers,
1508 state.table_state.selected(),
1509 );
1510
1511 let offset = start_row.saturating_sub(state.view.buffered_start_row);
1514 let rows_room = (area.height as usize).saturating_sub(header_h as usize);
1515 let locked_slice = state
1516 .view
1517 .locked_df
1518 .as_ref()
1519 .and_then(|df| visible_or_header(df, offset, state.visible_rows));
1520 self.fit_pending(state, offset, state.visible_rows.min(rows_room), cap);
1521
1522 if state.view.df.is_some() || state.view.locked_df.is_some() {
1523 if state.row_numbers() {
1524 self.render_row_numbers(row_num_area, buf, row_number_params);
1525 }
1526 let mut drawn_columns = Vec::new();
1527 let mut drawn_rows = 0;
1528 let mut scroll_area = data_area;
1529 let mut leading_gap = false;
1530 if let Some(locked) = locked_slice {
1531 let asked = state.locked_columns_count();
1532 let rule_bottom = (area.y + header_h)
1535 .saturating_add(locked.height().min(rows_room) as u16)
1536 .min(area.bottom());
1537 let mut fitted = self.fit_frozen_columns(
1538 &locked,
1539 locked.height().min(rows_room),
1540 data_area.width,
1541 state.view.column_order.len() <= asked,
1542 area.width,
1543 &mut Sizing {
1544 widths: &mut state.widths,
1545 schema: &state.view.schema,
1546 cap,
1547 page: state
1548 .view
1549 .locked_df
1550 .as_ref()
1551 .map(|frame| (&mut state.page_cells, frame, offset)),
1552 },
1553 );
1554 state.fit_frozen(fitted.cols.len());
1555 let shown = state.frozen_shown().min(fitted.cols.len());
1558 fitted.cols.truncate(shown);
1559 fitted.widths.truncate(shown);
1560 let mut separator_x = data_area.x;
1561 if shown > 0 {
1562 let gaps = self.table_cell_padding.saturating_mul(shown as u16 - 1);
1563 let columns_width = fitted.widths.iter().sum::<u16>().saturating_add(gaps);
1564 let frozen_area = Rect {
1567 width: columns_width.saturating_add(1).min(data_area.width),
1568 ..data_area
1569 };
1570 let (columns, rows) =
1571 self.draw_columns(&fitted, frozen_area, buf, &mut state.table_state, false);
1572 drawn_columns.extend(columns);
1573 drawn_rows = drawn_rows.max(rows);
1574 separator_x = frozen_area.right();
1575 }
1576 if separator_x < data_area.right() {
1577 let rule = if shown < asked {
1580 self.glyphs.rule_broken
1581 } else {
1582 self.glyphs.rule
1583 };
1584 for y in area.y..rule_bottom {
1585 let cell = &mut buf[(separator_x, y)];
1586 cell.set_symbol(rule);
1587 cell.set_style(Style::default().fg(self.separator_fg));
1588 }
1589 }
1590 let scroll_x = separator_x.saturating_add(1).min(data_area.right());
1591 scroll_area = Rect {
1592 x: scroll_x,
1593 width: data_area.right() - scroll_x,
1594 ..data_area
1595 };
1596 leading_gap = true;
1597 }
1598 let room = Room {
1600 width: scroll_area.width,
1601 lead: u16::from(leading_gap),
1602 padding: self.table_cell_padding,
1603 };
1604 let rows = state.visible_rows.min(rows_room);
1605 state.land_column_moves(room, |state, name| {
1606 self.measure_column(state, name, offset, rows, cap)
1607 });
1608 if let Some(sliced_df) = state
1609 .view
1610 .df
1611 .as_ref()
1612 .and_then(|df| visible_or_header(df, offset, state.visible_rows))
1613 {
1614 let total_cols = sliced_df.width();
1615 let (shown, columns, rows) = self.render_scrolling(
1616 &sliced_df,
1617 scroll_area,
1618 buf,
1619 &mut state.table_state,
1620 leading_gap,
1621 Sizing {
1622 widths: &mut state.widths,
1623 schema: &state.view.schema,
1624 cap,
1625 page: state
1626 .view
1627 .df
1628 .as_ref()
1629 .map(|frame| (&mut state.page_cells, frame, offset)),
1630 },
1631 );
1632 drawn_columns.extend(columns);
1633 drawn_rows = drawn_rows.max(rows);
1634 let more_left = state.termcol_index > 0;
1635 let more_right = total_cols.saturating_sub(shown);
1636 let first = state.frozen_shown() + state.termcol_index + 1;
1637 let total = state.view.column_order.len();
1638 state.on_screen =
1639 state
1640 .cursor_index()
1641 .filter(|_| total > 1)
1642 .map(|cursor| OnScreen {
1643 first,
1644 last: first + shown.saturating_sub(1),
1645 cursor: cursor + 1,
1646 total,
1647 });
1648 scroll_indicator = Some(ScrollCue {
1649 area: scroll_area,
1650 more_left,
1651 more_right,
1652 });
1653 } else {
1654 let total = state.view.column_order.len();
1656 state.on_screen =
1657 state
1658 .cursor_index()
1659 .filter(|_| total > 1)
1660 .map(|cursor| OnScreen {
1661 first: 1,
1662 last: total,
1663 cursor: cursor + 1,
1664 total,
1665 });
1666 }
1667 state.drawn = Some(DrawnTable {
1668 area: whole,
1669 header: header_h,
1670 start_row,
1671 rows: drawn_rows,
1672 columns: drawn_columns,
1673 });
1674 } else if !state.view.column_order.is_empty() {
1675 let empty_columns: Vec<_> = state
1677 .view
1678 .column_order
1679 .iter()
1680 .map(|name| {
1681 let dtype = state
1682 .view
1683 .schema
1684 .get(name.as_str())
1685 .cloned()
1686 .unwrap_or(DataType::String);
1687 Series::new_empty(name.as_str().into(), &dtype).into()
1688 })
1689 .collect();
1690 match DataFrame::new_infer_height(empty_columns) {
1691 Ok(empty_df) => {
1692 if state.row_numbers() {
1693 self.render_row_numbers(
1694 row_num_area,
1695 buf,
1696 row_numbers(0, 0, Vec::new(), None),
1697 );
1698 }
1699 self.render_scrolling(
1700 &empty_df,
1701 data_area,
1702 buf,
1703 &mut state.table_state,
1704 false,
1705 Sizing {
1706 widths: &mut state.widths,
1707 schema: &state.view.schema,
1708 cap,
1709 page: None,
1710 },
1711 );
1712 }
1713 _ => {
1714 Paragraph::new("No data").render(area, buf);
1715 }
1716 }
1717 } else {
1718 Paragraph::new("No data").render(area, buf);
1720 }
1721
1722 let empty = state.view.num_rows_valid
1724 && state.num_rows() == 0
1725 && !state.view.column_order.is_empty();
1726 if empty && area.height > header_h && data_area.width > 0 {
1727 let line = Rect {
1728 y: area.y + header_h,
1729 height: 1,
1730 ..data_area
1731 };
1732 Paragraph::new("No rows")
1733 .style(Style::default().fg(self.dimmed))
1734 .render(line, buf);
1735 }
1736
1737 if rail_area.width > 0 && rail_area.height > 0 {
1740 let g = self.glyphs;
1741 let header_style = if self.header_bg == Color::Reset {
1742 Style::default().fg(self.header_fg)
1743 } else {
1744 Style::default().bg(self.header_bg).fg(self.header_fg)
1745 };
1746 for dy in 0..header_h.min(rail_area.height) {
1747 let cell = &mut buf[(rail_area.x, rail_area.y + dy)];
1748 cell.set_char(' ');
1749 cell.set_style(header_style);
1750 }
1751 if state.view.df.is_some()
1752 && !empty
1753 && let Some(sel) = state.table_state.selected()
1754 {
1755 let y = rail_area.y + header_h + sel as u16;
1756 if y < rail_area.y + rail_area.height {
1757 let cell = &mut buf[(rail_area.x, y)];
1758 cell.set_symbol(g.rail.trim_end());
1759 let mut style = Style::default()
1760 .fg(self.accent)
1761 .add_modifier(Modifier::BOLD);
1762 if let Some(bg) = self.selected_bg {
1763 style = style.bg(bg);
1764 }
1765 cell.set_style(style);
1766 }
1767 }
1768 }
1769
1770 if let Some(cue) = scroll_indicator
1773 && cue.area.width > 0
1774 && cue.area.height > 0
1775 {
1776 let g = self.glyphs;
1777 let scroll_area = cue.area;
1778 let hidden = cue.more_right;
1779 let hint_style = if self.header_bg == Color::Reset {
1780 Style::default()
1781 .fg(self.accent)
1782 .add_modifier(Modifier::BOLD)
1783 } else {
1784 Style::default()
1785 .bg(self.header_bg)
1786 .fg(self.accent)
1787 .add_modifier(Modifier::BOLD)
1788 };
1789 if cue.more_left && rail_area.width > 0 {
1790 let cell = &mut buf[(rail_area.x, rail_area.y)];
1791 cell.set_symbol(g.arrow_left);
1792 cell.set_style(hint_style);
1793 }
1794 if hidden > 0 {
1795 let y = if header_h > 1 {
1796 scroll_area.y + 1
1797 } else {
1798 scroll_area.y
1799 };
1800 let right = scroll_area.x + scroll_area.width;
1803 let free = (scroll_area.x..right)
1804 .rev()
1805 .take_while(|&x| buf[(x, y)].symbol() == " ")
1806 .count();
1807 let mut text = format!(" +{hidden} {}", g.arrow_right);
1808 if text.chars().count() > free {
1809 text = g.arrow_right.to_string();
1810 }
1811 let w = text.chars().count() as u16;
1812 if scroll_area.width >= w {
1813 let x0 = right - w;
1814 for (i, ch) in text.chars().enumerate() {
1815 let cell = &mut buf[(x0 + i as u16, y)];
1816 cell.set_char(ch);
1817 cell.set_style(hint_style);
1818 }
1819 }
1820 }
1821 }
1822 }
1823}
1824
1825#[cfg(test)]
1826pub(crate) mod tests;