1use std::sync::{Arc, Mutex, PoisonError};
73
74use rich::{Console, ConsoleOptions, Justify, Overflow, Renderable, Segment, Table, Text};
75
76use super::data::normalize;
77use super::stream::table_lines;
78use super::{frame_builders, style, Column, Frame, Value};
79
80pub const MAX_HEIGHT: usize = 10_000;
83
84pub const MAX_SAMPLE: usize = 10_000;
86
87pub const DEFAULT_SAMPLE: usize = 100;
89
90pub const DEFAULT_MAX_COLUMN_WIDTH: usize = 40;
92
93pub trait VirtualRows {
105 fn row_count(&self) -> Option<usize> {
107 None
108 }
109
110 fn row(&self, index: usize) -> Option<Vec<Value>>;
112
113 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
116 (start..start.saturating_add(len))
117 .map_while(|index| self.row(index))
118 .collect()
119 }
120
121 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
129 self.rows(start, len)
130 .iter()
131 .map(|row| pick(row, columns))
132 .collect()
133 }
134}
135
136fn pick(row: &[Value], columns: &[usize]) -> Vec<Value> {
138 columns
139 .iter()
140 .map(|&c| row.get(c).cloned().unwrap_or(Value::Null))
141 .collect()
142}
143
144impl VirtualRows for [Vec<Value>] {
145 fn row_count(&self) -> Option<usize> {
146 Some(self.len())
147 }
148
149 fn row(&self, index: usize) -> Option<Vec<Value>> {
150 self.get(index).cloned()
151 }
152
153 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
154 let start = start.min(self.len());
155 let end = start.saturating_add(len).min(self.len());
156 self[start..end].to_vec()
157 }
158
159 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
160 let start = start.min(self.len());
161 let end = start.saturating_add(len).min(self.len());
162 self[start..end]
163 .iter()
164 .map(|row| pick(row, columns))
165 .collect()
166 }
167}
168
169impl VirtualRows for Vec<Vec<Value>> {
170 fn row_count(&self) -> Option<usize> {
171 Some(self.len())
172 }
173
174 fn row(&self, index: usize) -> Option<Vec<Value>> {
175 self.as_slice().row(index)
176 }
177
178 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
179 self.as_slice().rows(start, len)
180 }
181
182 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
183 self.as_slice().rows_columns(start, len, columns)
184 }
185}
186
187macro_rules! forward_rows {
188 ($($ptr:ty),*) => {$(
189 impl<T: VirtualRows + ?Sized> VirtualRows for $ptr {
190 fn row_count(&self) -> Option<usize> {
191 (**self).row_count()
192 }
193
194 fn row(&self, index: usize) -> Option<Vec<Value>> {
195 (**self).row(index)
196 }
197
198 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
199 (**self).rows(start, len)
200 }
201
202 fn rows_columns(
203 &self,
204 start: usize,
205 len: usize,
206 columns: &[usize],
207 ) -> Vec<Vec<Value>> {
208 (**self).rows_columns(start, len, columns)
209 }
210 }
211 )*};
212}
213forward_rows!(&T, Box<T>, Arc<T>);
214
215pub struct FnCells<F> {
231 rows: usize,
232 columns: usize,
233 cell: F,
234}
235
236impl<F> FnCells<F>
237where
238 F: Fn(usize, usize) -> Value,
239{
240 pub fn new(rows: usize, columns: usize, cell: F) -> Self {
242 FnCells {
243 rows,
244 columns,
245 cell,
246 }
247 }
248}
249
250impl<F> std::fmt::Debug for FnCells<F> {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.debug_struct("FnCells")
253 .field("rows", &self.rows)
254 .field("columns", &self.columns)
255 .finish_non_exhaustive()
256 }
257}
258
259impl<F> VirtualRows for FnCells<F>
260where
261 F: Fn(usize, usize) -> Value,
262{
263 fn row_count(&self) -> Option<usize> {
264 Some(self.rows)
265 }
266
267 fn row(&self, index: usize) -> Option<Vec<Value>> {
268 (index < self.rows).then(|| (0..self.columns).map(|c| (self.cell)(index, c)).collect())
269 }
270
271 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
272 let end = start.saturating_add(len).min(self.rows);
273 (start.min(end)..end)
274 .map(|r| {
275 columns
276 .iter()
277 .map(|&c| {
278 if c < self.columns {
279 (self.cell)(r, c)
280 } else {
281 Value::Null
282 }
283 })
284 .collect()
285 })
286 .collect()
287 }
288}
289
290pub struct FnRows<F> {
302 count: Option<usize>,
303 row: F,
304}
305
306impl<F> FnRows<F>
307where
308 F: Fn(usize) -> Option<Vec<Value>>,
309{
310 pub fn new(count: Option<usize>, row: F) -> Self {
312 FnRows { count, row }
313 }
314}
315
316impl<F> std::fmt::Debug for FnRows<F> {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.debug_struct("FnRows")
319 .field("count", &self.count)
320 .finish_non_exhaustive()
321 }
322}
323
324impl<F> VirtualRows for FnRows<F>
325where
326 F: Fn(usize) -> Option<Vec<Value>>,
327{
328 fn row_count(&self) -> Option<usize> {
329 self.count
330 }
331
332 fn row(&self, index: usize) -> Option<Vec<Value>> {
333 if self.count.is_some_and(|count| index >= count) {
334 return None;
335 }
336 (self.row)(index)
337 }
338}
339
340#[derive(Clone, Debug, PartialEq)]
342#[non_exhaustive]
343pub struct Page {
344 pub start: usize,
348 pub rows: Vec<Vec<Value>>,
351 pub total: Option<usize>,
354 pub more: bool,
356 pub columns: Vec<usize>,
359 pub frozen: usize,
361 pub column_total: usize,
363}
364
365impl Page {
366 pub fn range(&self) -> std::ops::Range<usize> {
368 self.start..self.start + self.rows.len()
369 }
370
371 pub fn column_range(&self) -> std::ops::Range<usize> {
373 match self.columns.get(self.frozen..) {
374 Some([first, .., last]) => *first..*last + 1,
375 Some([only]) => *only..*only + 1,
376 _ => 0..0,
377 }
378 }
379
380 pub fn column_position(&self, ascii: bool) -> Option<String> {
385 if self.columns.len() >= self.column_total {
386 return None;
387 }
388 let number = |n: usize| crate::format::number(i64::try_from(n).unwrap_or(i64::MAX));
389 let range = self.column_range();
390 let total = number(self.column_total);
391 Some(match range.len() {
392 0 => format!("no columns of {total}"),
393 1 => format!("column {} of {total}", number(range.start + 1)),
394 _ => {
395 let dash = if ascii { "-" } else { "–" };
396 format!(
397 "columns {}{dash}{} of {total}",
398 number(range.start + 1),
399 number(range.end)
400 )
401 }
402 })
403 }
404
405 pub fn position(&self, ascii: bool) -> String {
410 let number = |n: usize| crate::format::number(i64::try_from(n).unwrap_or(i64::MAX));
411 if self.rows.is_empty() {
412 return match self.start {
413 0 => "no rows".to_string(),
414 start => format!("no rows from {}", number(start + 1)),
415 };
416 }
417 let dash = if ascii { "-" } else { "–" };
418 let range = format!(
419 "rows {}{dash}{}",
420 number(self.start + 1),
421 number(self.start + self.rows.len())
422 );
423 match self.total {
424 Some(total) => format!("{range} of {}", number(total)),
425 None if self.more => {
426 format!(
427 "{range} of {}+",
428 number(self.start.saturating_add(self.rows.len() + 1))
429 )
430 }
431 None => range,
432 }
433 }
434}
435
436pub struct VirtualTable<S> {
442 source: S,
443 columns: Vec<Column>,
444 frame: Frame,
445 offset: usize,
446 height: usize,
447 widths: Vec<Option<usize>>,
448 sample: usize,
449 max_width: usize,
450 wrap: bool,
451 position: bool,
452 row_numbers: bool,
453 fit_window: bool,
454 null: Option<String>,
455 footnote: Option<String>,
456 sampled: Mutex<Vec<Option<usize>>>,
458 window: ColumnWindow,
459}
460
461#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
464struct ColumnWindow {
465 offset: usize,
466 count: Option<usize>,
467 fit: bool,
468 frozen: usize,
469}
470
471impl ColumnWindow {
472 fn on(&self) -> bool {
474 self.offset > 0 || self.count.is_some() || self.fit || self.frozen > 0
475 }
476}
477
478impl<S> std::fmt::Debug for VirtualTable<S> {
479 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480 f.debug_struct("VirtualTable")
481 .field("columns", &self.columns)
482 .field("offset", &self.offset)
483 .field("height", &self.height)
484 .field("widths", &self.widths)
485 .field("sample", &self.sample)
486 .field("window", &self.window)
487 .finish_non_exhaustive()
488 }
489}
490
491frame_builders!([S] VirtualTable<S>);
492
493impl<S: VirtualRows> VirtualTable<S> {
494 pub fn new(columns: impl IntoIterator<Item = Column>, source: S) -> Self {
496 VirtualTable {
497 source,
498 columns: columns.into_iter().collect(),
499 frame: Frame::default(),
500 offset: 0,
501 height: 20,
502 widths: Vec::new(),
503 sample: DEFAULT_SAMPLE,
504 max_width: DEFAULT_MAX_COLUMN_WIDTH,
505 wrap: false,
506 position: true,
507 row_numbers: false,
508 fit_window: false,
509 null: None,
510 footnote: None,
511 sampled: Mutex::new(Vec::new()),
512 window: ColumnWindow::default(),
513 }
514 }
515
516 pub fn offset(mut self, offset: usize) -> Self {
518 self.offset = offset;
519 self
520 }
521
522 pub fn height(mut self, rows: usize) -> Self {
524 self.set_height(rows);
525 self
526 }
527
528 pub fn widths(mut self, widths: impl IntoIterator<Item = impl Into<Option<usize>>>) -> Self {
532 self.widths = widths.into_iter().map(Into::into).collect();
533 self.resample();
534 self
535 }
536
537 pub fn sample(mut self, rows: usize) -> Self {
541 self.sample = rows.min(MAX_SAMPLE);
542 self.resample();
543 self
544 }
545
546 pub fn max_column_width(mut self, cells: usize) -> Self {
549 self.max_width = cells.max(1);
550 self.resample();
551 self
552 }
553
554 pub fn wrap(mut self, wrap: bool) -> Self {
557 self.wrap = wrap;
558 self
559 }
560
561 pub fn show_position(mut self, show: bool) -> Self {
563 self.position = show;
564 self
565 }
566
567 pub fn fit_window(mut self, fit: bool) -> Self {
571 self.fit_window = fit;
572 self
573 }
574
575 pub fn row_numbers(mut self, show: bool) -> Self {
577 self.row_numbers = show;
578 self
579 }
580
581 pub fn null_marker(mut self, marker: impl Into<String>) -> Self {
585 self.null = Some(marker.into());
586 self.resample();
587 self
588 }
589
590 pub fn footnote(mut self, note: impl Into<String>) -> Self {
593 self.footnote = Some(note.into());
594 self
595 }
596
597 pub fn column_offset(mut self, offset: usize) -> Self {
602 self.window.offset = offset;
603 self
604 }
605
606 pub fn visible_columns(mut self, count: usize) -> Self {
610 self.window.count = Some(count.max(1));
611 self
612 }
613
614 pub fn fit_columns(mut self, fit: bool) -> Self {
620 self.window.fit = fit;
621 self
622 }
623
624 pub fn frozen_columns(mut self, count: usize) -> Self {
628 self.window.frozen = count;
629 self
630 }
631
632 pub fn set_column_offset(&mut self, offset: usize) {
634 self.window.offset = offset;
635 }
636
637 pub fn set_visible_columns(&mut self, count: Option<usize>) {
640 self.window.count = count.map(|c| c.max(1));
641 }
642
643 pub fn scroll_columns_by(&mut self, columns: isize) {
646 let offset = self.window.offset.saturating_add_signed(columns);
647 self.window.offset = offset.min(self.max_column_offset());
648 }
649
650 pub fn window_column_offset(&self) -> usize {
652 self.window.offset
653 }
654
655 pub fn max_column_offset(&self) -> usize {
659 let scrollable = self.columns.len().saturating_sub(self.window.frozen);
660 scrollable.saturating_sub(self.window.count.unwrap_or(1))
661 }
662
663 pub fn shown_columns(&self, width: usize) -> Vec<usize> {
667 let n = self.columns.len();
668 if !self.window.on() {
669 return (0..n).collect();
670 }
671 let frozen = self.window.frozen.min(n);
672 let scrollable = n - frozen;
673 let start = frozen + self.window.offset.min(scrollable.saturating_sub(1));
674 let end = match self.window.count {
675 Some(count) => start.saturating_add(count).min(n),
676 None => n,
677 };
678 let mut shown: Vec<usize> = (0..frozen).collect();
679 if !self.window.fit || start >= end {
680 shown.extend(start..end);
681 return shown;
682 }
683 let mut widths = self.widths_of(&shown);
686 if self.row_numbers {
687 let last = self
688 .row_count()
689 .unwrap_or(self.offset.saturating_add(self.height));
690 widths.insert(0, last.max(1).to_string().len());
691 }
692 let mut next = start;
693 'fit: while next < end {
694 let room = width.saturating_sub(self.table_width(&widths));
695 let batch = (room / (self.max_width + 3)).max(1);
696 let candidates: Vec<usize> = (next..end.min(next + batch)).collect();
697 for (index, w) in candidates.iter().zip(self.widths_of(&candidates)) {
698 widths.push(w);
699 if shown.len() > frozen && self.table_width(&widths) > width {
700 break 'fit;
701 }
702 shown.push(*index);
703 }
704 next += candidates.len();
705 }
706 shown
707 }
708
709 fn table_width(&self, widths: &[usize]) -> usize {
712 let boxed = self.frame.box_set.is_some();
713 let edges = if boxed && self.frame.show_edge { 2 } else { 0 };
714 let dividers = if boxed {
715 widths.len().saturating_sub(1)
716 } else {
717 0
718 };
719 widths.iter().map(|w| w + 2).sum::<usize>() + dividers + edges
720 }
721
722 pub fn set_offset(&mut self, offset: usize) {
724 self.offset = offset;
725 }
726
727 pub fn set_height(&mut self, rows: usize) {
729 self.height = rows.min(MAX_HEIGHT);
730 }
731
732 pub fn scroll_by(&mut self, rows: isize) {
735 let offset = self.offset.saturating_add_signed(rows);
736 self.offset = match self.max_offset() {
737 Some(max) => offset.min(max),
738 None => offset,
739 };
740 }
741
742 pub fn window_offset(&self) -> usize {
744 self.offset
745 }
746
747 pub fn window_height(&self) -> usize {
749 self.height
750 }
751
752 pub fn row_count(&self) -> Option<usize> {
754 self.source.row_count()
755 }
756
757 pub fn max_offset(&self) -> Option<usize> {
759 self.row_count()
760 .map(|count| count.saturating_sub(self.height))
761 }
762
763 pub fn columns(&self) -> &[Column] {
765 &self.columns
766 }
767
768 pub fn source(&self) -> &S {
770 &self.source
771 }
772
773 pub fn source_mut(&mut self) -> &mut S {
776 &mut self.source
777 }
778
779 pub fn resample(&self) {
781 self.sampled
782 .lock()
783 .unwrap_or_else(PoisonError::into_inner)
784 .clear();
785 }
786
787 pub fn page(&self) -> Page {
793 self.page_at(80)
794 }
795
796 pub fn page_at(&self, width: usize) -> Page {
800 let columns = self.shown_columns(width);
801 let count = self.source.row_count();
802 let start = match count {
803 Some(count) => self.offset.min(count.saturating_sub(self.height)),
804 None => self.offset,
805 };
806 let fetch = if count.is_some() {
808 self.height
809 } else {
810 self.height + 1
811 };
812 let mut rows = if self.window.on() {
813 self.source.rows_columns(start, fetch, &columns)
814 } else {
815 self.source.rows(start, fetch)
816 };
817 rows.truncate(fetch);
818 let more = match count {
819 Some(count) => start + rows.len().min(self.height) < count,
820 None => rows.len() > self.height,
821 };
822 rows.truncate(self.height);
823 let width = columns.len();
824 let rows: Vec<Vec<Value>> = rows.into_iter().map(|row| normalize(row, width)).collect();
825 let total = count.or_else(|| (!more && !rows.is_empty()).then(|| start + rows.len()));
826 Page {
827 start,
828 rows,
829 total,
830 more,
831 frozen: if self.window.on() {
832 self.window.frozen.min(self.columns.len())
833 } else {
834 0
835 },
836 column_total: self.columns.len(),
837 columns,
838 }
839 }
840
841 pub fn column_widths(&self) -> Vec<usize> {
845 let all: Vec<usize> = (0..self.columns.len()).collect();
846 self.widths_of(&all)
847 }
848
849 fn fixed_width(&self, index: usize) -> Option<usize> {
851 self.widths
852 .get(index)
853 .copied()
854 .flatten()
855 .or(self.columns[index].column_options().width)
856 }
857
858 fn widths_of(&self, indices: &[usize]) -> Vec<usize> {
861 let mut sampled = self.sampled.lock().unwrap_or_else(PoisonError::into_inner);
862 if sampled.len() != self.columns.len() {
863 *sampled = vec![None; self.columns.len()];
864 }
865 let missing: Vec<usize> = indices
866 .iter()
867 .copied()
868 .filter(|&i| sampled[i].is_none() && self.fixed_width(i).is_none())
869 .collect();
870 if !missing.is_empty() {
871 let mut widths: Vec<usize> = missing
872 .iter()
873 .map(|&i| Text::new(self.columns[i].header()).measurement().1)
874 .collect();
875 let rows = if missing.len() == self.columns.len() {
876 self.source.rows(0, self.sample)
878 } else {
879 self.source.rows_columns(0, self.sample, &missing)
880 };
881 for row in rows.into_iter().take(self.sample) {
882 let row = normalize(row, missing.len());
885 for ((width, &index), value) in widths.iter_mut().zip(&missing).zip(&row) {
886 let cell = self.cell(&self.columns[index], value).measurement().1;
887 *width = (*width).max(cell);
888 }
889 }
890 for (width, &index) in widths.into_iter().zip(&missing) {
891 sampled[index] = Some(width.clamp(1, self.max_width));
892 }
893 }
894 indices
895 .iter()
896 .map(|&i| self.fixed_width(i).or(sampled[i]).unwrap_or(self.max_width))
897 .collect()
898 }
899
900 pub fn chrome_lines(&self, console: &Console, width: usize) -> usize {
905 let options = console.options().update_width(width.max(1));
906 let columns = self.shown_columns(width);
907 let empty = Page {
908 start: 0,
909 rows: Vec::new(),
910 total: None,
911 more: false,
912 frozen: 0,
913 column_total: self.columns.len(),
914 columns,
915 };
916 let table = self.build(console, &empty);
917 table_lines(&table, console, &options).len()
918 + usize::from(self.position)
919 + usize::from(self.footnote.is_some())
920 }
921
922 pub fn to_table(&self, console: &Console) -> Table {
925 self.build(console, &self.page_at(console.width()))
926 }
927
928 fn cell(&self, column: &Column, value: &Value) -> Text {
929 match (&self.null, value) {
930 (Some(marker), Value::Null) => Text::new(marker.clone()),
931 _ => column.cell(value),
932 }
933 }
934
935 fn build(&self, console: &Console, page: &Page) -> Table {
936 let mut widths = self.widths_of(&page.columns);
937 if self.fit_window {
938 for (k, (&index, width)) in page.columns.iter().zip(widths.iter_mut()).enumerate() {
939 let column = &self.columns[index];
940 if self.fixed_width(index).is_some() {
941 continue;
942 }
943 for row in &page.rows {
944 let cell = self.cell(column, &row[k]).measurement().1;
945 *width = (*width).max(cell.min(self.max_width));
946 }
947 }
948 }
949 let mut columns: Vec<Column> = Vec::with_capacity(page.columns.len() + 1);
950 if self.row_numbers {
951 let last = page
952 .total
953 .unwrap_or(page.start + page.rows.len())
954 .max(page.start + page.rows.len());
955 let options = rich::ColumnOptions {
956 justify: Justify::Right,
957 width: Some(last.max(1).to_string().len()),
958 no_wrap: true,
959 style: style(console, "table.row_number"),
960 ..Default::default()
961 };
962 columns.push(Column::new("#").options(options));
963 }
964 for (&index, width) in page.columns.iter().zip(&widths) {
965 let column = &self.columns[index];
966 let mut options = column.column_options().clone();
967 options.width = Some(*width);
968 if !self.wrap {
969 options.no_wrap = true;
970 if options.overflow == Overflow::Fold {
971 options.overflow = Overflow::Ellipsis;
972 }
973 }
974 columns.push(Column::new(column.header()).options(options));
975 }
976 let headers: Vec<Text> = columns.iter().map(|c| Text::new(c.header())).collect();
977 let mut table = self.frame.table(&columns, &headers, true, true);
978 let null_style = style(console, "table.null");
979 for (index, row) in page.rows.iter().enumerate() {
980 let mut cells = Vec::with_capacity(columns.len());
981 if self.row_numbers {
982 cells.push(Text::new((page.start + index + 1).to_string()));
983 }
984 for (&index, value) in page.columns.iter().zip(row) {
985 let mut text = self.cell(&self.columns[index], value);
986 if self.null.is_some() && matches!(value, Value::Null) {
987 let len = text.plain().len();
988 text.stylize(null_style.clone(), 0, len);
989 }
990 cells.push(text);
991 }
992 table.add_row_text(cells);
993 }
994 table
995 }
996
997 fn note_lines(
998 &self,
999 console: &Console,
1000 options: &ConsoleOptions,
1001 text: String,
1002 ) -> Vec<Vec<Segment>> {
1003 let text = Text::styled(text, style(console, "table.position"))
1004 .no_wrap(true)
1005 .overflow(Overflow::Ellipsis);
1006 let mut options = options.clone();
1007 options.height = None;
1008 console.render_lines(&text, &options, false)
1009 }
1010
1011 fn position_text(page: &Page, ascii: bool) -> String {
1014 let rows = page.position(ascii);
1015 match page.column_position(ascii) {
1016 Some(columns) => format!("{rows}, {columns}"),
1017 None => rows,
1018 }
1019 }
1020
1021 fn render_lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Vec<Segment>> {
1022 let page = self.page_at(options.max_width);
1023 let table = self.build(console, &page);
1024 let mut lines = table_lines(&table, console, options);
1025 if self.position {
1026 let text = Self::position_text(&page, console.ascii_only());
1027 lines.extend(self.note_lines(console, options, text));
1028 }
1029 if let Some(note) = &self.footnote {
1030 lines.extend(self.note_lines(console, options, note.clone()));
1031 }
1032 lines
1033 }
1034}
1035
1036impl<S: VirtualRows> Renderable for VirtualTable<S> {
1037 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1038 crate::event::flatten(self.render_lines(console, options))
1039 }
1040
1041 fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
1044 let page = self.page_at(options.max_width);
1045 let mut measurement = self.build(console, &page).measure(console, options);
1046 let notes = self
1047 .position
1048 .then(|| Self::position_text(&page, console.ascii_only()))
1049 .into_iter()
1050 .chain(self.footnote.clone());
1051 for note in notes {
1052 measurement.maximum = measurement.maximum.max(Text::new(note).cell_len());
1053 }
1054 measurement
1055 }
1056}
1057
1058impl<S: VirtualRows> crate::a11y::AccessibleText for VirtualTable<S> {
1059 fn accessible_text(&self, width: usize) -> String {
1060 let console = Console::builder().width(width.max(1)).build();
1061 self.to_table(&console).accessible_text(width)
1062 }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067 use super::*;
1068
1069 fn render(table: &impl Renderable, width: usize) -> String {
1070 Console::builder()
1071 .width(width)
1072 .color_system(None)
1073 .build()
1074 .render_export(table)
1075 }
1076
1077 fn numbered(count: Option<usize>) -> FnRows<impl Fn(usize) -> Option<Vec<Value>>> {
1078 FnRows::new(count, |i| {
1079 Some(vec![Value::from(i), format!("r{i}").into()])
1080 })
1081 }
1082
1083 #[test]
1084 fn an_offset_past_the_end_shows_the_last_window() {
1085 let rows: Vec<Vec<Value>> = (0..10).map(|i| vec![Value::from(i)]).collect();
1086 let table = VirtualTable::new([Column::new("n")], rows)
1087 .offset(50)
1088 .height(3);
1089 let page = table.page();
1090 assert_eq!(page.range(), 7..10);
1091 assert_eq!(page.total, Some(10));
1092 assert!(!page.more);
1093 assert_eq!(page.position(false), "rows 8–10 of 10");
1094 }
1095
1096 #[test]
1097 fn an_uncounted_source_reports_what_it_knows() {
1098 let rows = FnRows::new(None, |i| (i < 5).then(|| vec![Value::from(i)]));
1099 let table = VirtualTable::new([Column::new("n")], rows).height(2);
1100 assert_eq!(table.page().position(false), "rows 1–2 of 3+");
1101 let mut table = table;
1102 table.set_offset(3);
1103 let page = table.page();
1104 assert_eq!((page.total, page.more), (Some(5), false));
1105 assert_eq!(page.position(true), "rows 4-5 of 5");
1106 table.set_offset(9);
1107 assert_eq!(table.page().position(false), "no rows from 10");
1108 }
1109
1110 #[test]
1111 fn ragged_rows_are_padded_and_cut() {
1112 let rows = vec![
1113 vec![Value::from("a")],
1114 vec![Value::from("b"), Value::from(2), Value::from("extra")],
1115 vec![],
1116 ];
1117 let table = VirtualTable::new([Column::new("x"), Column::new("y")], rows);
1118 let page = table.page();
1119 assert!(page.rows.iter().all(|row| row.len() == 2));
1120 let out = render(&table, 30);
1121 assert!(out.contains("│ b │ 2 │"), "{out}");
1122 }
1123
1124 #[test]
1125 fn widths_hold_still_while_scrolling() {
1126 let mut table = VirtualTable::new([Column::new("n"), Column::new("s")], numbered(None))
1127 .sample(10)
1128 .height(2);
1129 let first = render(&table, 40);
1130 table.scroll_by(500);
1131 let later = render(&table, 40);
1132 let width = |out: &str| out.lines().next().unwrap().chars().count();
1133 assert_eq!(width(&first), width(&later));
1134 assert!(later.contains("│ … │ r… │"), "{later}");
1136 assert_eq!(table.column_widths(), [1, 2]);
1137 }
1138
1139 #[test]
1140 fn fixed_widths_win_and_the_cap_bounds_samples() {
1141 let long = vec![vec![Value::from("x".repeat(500)), Value::from("y")]];
1142 let table = VirtualTable::new([Column::new("a"), Column::new("b")], long.clone())
1143 .max_column_width(8);
1144 assert_eq!(table.column_widths(), [8, 1]);
1145 let table =
1146 VirtualTable::new([Column::new("a"), Column::new("b")], long).widths([None, Some(5)]);
1147 assert_eq!(table.column_widths(), [40, 5]);
1148 }
1149
1150 #[test]
1151 fn scrolling_stops_at_both_ends() {
1152 let mut table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(10);
1153 table.scroll_by(-5);
1154 assert_eq!(table.window_offset(), 0);
1155 table.scroll_by(1_000);
1156 assert_eq!(table.window_offset(), 90);
1157 assert_eq!(table.max_offset(), Some(90));
1158 }
1159
1160 #[test]
1161 fn nulls_can_be_marked_apart_from_empty_text() {
1162 let rows = vec![vec![Value::Null], vec![Value::from("")]];
1163 let table = VirtualTable::new([Column::new("v")], rows)
1164 .null_marker("NULL")
1165 .show_position(false);
1166 assert_eq!(
1167 render(&table, 20),
1168 "┏━━━━━━┓\n┃ v ┃\n┡━━━━━━┩\n│ NULL │\n│ │\n└──────┘\n"
1169 );
1170 }
1171
1172 #[test]
1173 fn row_numbers_and_footnote() {
1174 let table = VirtualTable::new([Column::new("s")], numbered(Some(1_000)))
1175 .offset(998)
1176 .height(2)
1177 .row_numbers(true)
1178 .widths([3])
1179 .footnote("(1,000 rows)");
1180 let out = render(&table, 40);
1181 assert!(out.contains("│ 999 │ 998 │"), "{out}");
1182 assert!(
1183 out.ends_with("rows 999–1,000 of 1,000\n(1,000 rows)\n"),
1184 "{out}"
1185 );
1186 }
1187
1188 #[test]
1189 fn chrome_lines_count_everything_but_rows() {
1190 let table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(5);
1191 let console = Console::builder().width(40).build();
1192 let out = render(&table, 40);
1193 assert_eq!(out.lines().count(), 5 + table.chrome_lines(&console, 40));
1194 }
1195
1196 #[test]
1197 fn an_empty_source_renders_a_header_and_no_rows() {
1198 let table = VirtualTable::new([Column::new("n")], Vec::<Vec<Value>>::new());
1199 let out = render(&table, 20);
1200 assert!(out.ends_with("no rows\n"), "{out}");
1201 let table = VirtualTable::new(Vec::<Column>::new(), numbered(Some(3)));
1202 render(&table, 20);
1203 let table = VirtualTable::new(Vec::<Column>::new(), numbered(Some(3))).fit_columns(true);
1205 assert_eq!(table.shown_columns(20), Vec::<usize>::new());
1206 render(&table, 20);
1207 }
1208
1209 struct Counting {
1212 rows: usize,
1213 columns: usize,
1214 cells: std::sync::atomic::AtomicUsize,
1215 seen: Mutex<std::collections::BTreeSet<usize>>,
1216 }
1217
1218 impl Counting {
1219 fn new(rows: usize, columns: usize) -> Self {
1220 Counting {
1221 rows,
1222 columns,
1223 cells: Default::default(),
1224 seen: Default::default(),
1225 }
1226 }
1227
1228 fn seen(&self) -> Vec<usize> {
1229 self.seen.lock().unwrap().iter().copied().collect()
1230 }
1231
1232 fn cells(&self) -> usize {
1233 self.cells.load(std::sync::atomic::Ordering::Relaxed)
1234 }
1235 }
1236
1237 impl VirtualRows for Counting {
1238 fn row_count(&self) -> Option<usize> {
1239 Some(self.rows)
1240 }
1241
1242 fn row(&self, index: usize) -> Option<Vec<Value>> {
1243 let all: Vec<usize> = (0..self.columns).collect();
1244 self.rows_columns(index, 1, &all).pop()
1245 }
1246
1247 fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
1248 let end = start.saturating_add(len).min(self.rows);
1249 let rows: Vec<Vec<Value>> = (start.min(end)..end)
1250 .map(|r| {
1251 columns
1252 .iter()
1253 .map(|c| Value::from(format!("r{r}c{c}")))
1254 .collect()
1255 })
1256 .collect();
1257 self.cells.fetch_add(
1258 rows.len() * columns.len(),
1259 std::sync::atomic::Ordering::Relaxed,
1260 );
1261 self.seen.lock().unwrap().extend(columns.iter().copied());
1262 rows
1263 }
1264 }
1265
1266 fn twelve() -> Vec<Column> {
1267 (0..12).map(|c| Column::new(format!("c{c}"))).collect()
1268 }
1269
1270 #[test]
1271 fn a_column_window_fetches_only_its_columns() {
1272 let source = Counting::new(5, 12);
1273 let table = VirtualTable::new(twelve(), &source)
1274 .height(2)
1275 .column_offset(3)
1276 .visible_columns(2);
1277 assert_eq!(
1278 render(&table, 60),
1279 "\
1280┏━━━━━━┳━━━━━━┓
1281┃ c3 ┃ c4 ┃
1282┡━━━━━━╇━━━━━━┩
1283│ r0c3 │ r0c4 │
1284│ r1c3 │ r1c4 │
1285└──────┴──────┘
1286rows 1–2 of 5, columns 4–5 of 12
1287"
1288 );
1289 assert_eq!(source.seen(), vec![3, 4]);
1290 assert_eq!(source.cells(), 5 * 2 + 2 * 2);
1292 let page = table.page();
1293 assert_eq!(
1294 (page.columns.clone(), page.frozen, page.column_total),
1295 (vec![3, 4], 0, 12)
1296 );
1297 assert_eq!(page.column_range(), 3..5);
1298 }
1299
1300 #[test]
1301 fn frozen_columns_stay_on_the_left() {
1302 let source = Counting::new(5, 12);
1303 let mut table = VirtualTable::new(twelve(), &source)
1304 .height(1)
1305 .frozen_columns(1)
1306 .column_offset(5)
1307 .visible_columns(2);
1308 assert_eq!(
1309 render(&table, 60),
1310 "\
1311┏━━━━━━┳━━━━━━┳━━━━━━┓
1312┃ c0 ┃ c6 ┃ c7 ┃
1313┡━━━━━━╇━━━━━━╇━━━━━━┩
1314│ r0c0 │ r0c6 │ r0c7 │
1315└──────┴──────┴──────┘
1316rows 1–1 of 5, columns 7–8 of 12
1317"
1318 );
1319 assert_eq!(source.seen(), vec![0, 6, 7]);
1320 table.scroll_columns_by(-100);
1322 assert_eq!(table.window_column_offset(), 0);
1323 table.scroll_columns_by(100);
1324 assert_eq!(table.max_column_offset(), 9);
1325 assert_eq!(table.window_column_offset(), 9);
1326 assert_eq!(table.shown_columns(60), vec![0, 10, 11]);
1327 table.set_visible_columns(Some(1));
1329 table.set_column_offset(50);
1330 assert_eq!(table.shown_columns(60), vec![0, 11]);
1331 assert_eq!(
1332 table.page().column_position(true).as_deref(),
1333 Some("column 12 of 12")
1334 );
1335 }
1336
1337 #[test]
1338 fn fit_to_width_shows_what_fits_from_the_offset() {
1339 let source = Counting::new(5, 12);
1340 let table = VirtualTable::new(twelve(), &source)
1341 .height(1)
1342 .frozen_columns(1)
1343 .column_offset(2)
1344 .fit_columns(true);
1345 assert_eq!(table.shown_columns(30), vec![0, 3, 4, 5]);
1348 assert_eq!(
1349 render(&table, 34),
1350 "\
1351┏━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┓
1352┃ c0 ┃ c3 ┃ c4 ┃ c5 ┃
1353┡━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━┩
1354│ r0c0 │ r0c3 │ r0c4 │ r0c5 │
1355└──────┴──────┴──────┴──────┘
1356rows 1–1 of 5, columns 4–6 of 12
1357"
1358 );
1359 assert_eq!(source.seen(), vec![0, 3, 4, 5, 6]);
1361 assert_eq!(table.shown_columns(5), vec![0, 3]);
1363 let all = VirtualTable::new(twelve(), &source)
1365 .height(1)
1366 .fit_columns(true);
1367 assert_eq!(all.shown_columns(200).len(), 12);
1368 assert_eq!(all.page_at(200).column_position(false), None);
1369 let capped = VirtualTable::new(twelve(), &source)
1371 .fit_columns(true)
1372 .visible_columns(2);
1373 assert_eq!(capped.shown_columns(200), vec![0, 1]);
1374 }
1375
1376 #[test]
1377 fn a_window_reads_its_columns_aloud() {
1378 use crate::a11y::AccessibleText;
1379 let table = VirtualTable::new(twelve(), Counting::new(3, 12))
1380 .height(1)
1381 .column_offset(10);
1382 assert_eq!(
1383 table.accessible_text(80),
1384 "Table with 1 row, columns: c10, c11\nRow 1: c10: r0c10; c11: r0c11"
1385 );
1386 }
1387
1388 #[test]
1389 fn the_guides_wide_table() {
1390 let cells = FnCells::new(100_000, 2_000, |r, c| Value::from(r * c));
1391 let columns = (0..2_000).map(|c| Column::new(format!("c{c}")));
1392 let table = VirtualTable::new(columns, cells)
1393 .height(2)
1394 .frozen_columns(1)
1395 .column_offset(40)
1396 .fit_columns(true);
1397 assert_eq!(
1398 render(&table, 80),
1399 "\
1400┏━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┓
1401┃ c0 ┃ c41 ┃ c42 ┃ c43 ┃ c44 ┃ c45 ┃ c46 ┃ c47 ┃ c48 ┃ c49 ┃ c50 ┃
1402┡━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━┩
1403│ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │
1404│ 0 │ 41 │ 42 │ 43 │ 44 │ 45 │ 46 │ 47 │ 48 │ 49 │ 50 │
1405└────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
1406rows 1–2 of 100,000, columns 42–51 of 2,000
1407"
1408 );
1409 }
1410
1411 #[test]
1412 fn fn_cells_compute_only_what_is_asked() {
1413 let calls = std::sync::atomic::AtomicUsize::new(0);
1414 let grid = FnCells::new(100_000, 2_000, |r, c| {
1415 calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1416 Value::from(r + c)
1417 });
1418 let table = VirtualTable::new((0..2_000).map(|c| Column::new(format!("{c}"))), &grid)
1419 .offset(50_000)
1420 .height(3)
1421 .column_offset(1_000)
1422 .visible_columns(3)
1423 .sample(10);
1424 let out = render(&table, 60);
1425 assert!(
1426 out.ends_with("rows 50,001–50,003 of 100,000, columns 1,001–1,003 of 2,000\n"),
1427 "{out}"
1428 );
1429 assert_eq!(
1431 calls.load(std::sync::atomic::Ordering::Relaxed),
1432 (10 + 3) * 3
1433 );
1434 assert_eq!(grid.row(3).map(|r| r.len()), Some(2_000));
1435 assert_eq!(grid.rows_columns(99_999, 5, &[0, 5_000]).len(), 1);
1436 assert_eq!(grid.rows_columns(0, 1, &[5_000]), vec![vec![Value::Null]]);
1437 }
1438}