use std::sync::{Arc, Mutex, PoisonError};
use rich::{Console, ConsoleOptions, Justify, Overflow, Renderable, Segment, Table, Text};
use super::data::normalize;
use super::stream::table_lines;
use super::{Column, Frame, Value, frame_builders, style};
pub const MAX_HEIGHT: usize = 10_000;
pub const MAX_SAMPLE: usize = 10_000;
pub const DEFAULT_SAMPLE: usize = 100;
pub const DEFAULT_MAX_COLUMN_WIDTH: usize = 40;
pub trait VirtualRows {
fn row_count(&self) -> Option<usize> {
None
}
fn row(&self, index: usize) -> Option<Vec<Value>>;
fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
(start..start.saturating_add(len))
.map_while(|index| self.row(index))
.collect()
}
fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
self.rows(start, len)
.iter()
.map(|row| pick(row, columns))
.collect()
}
}
fn pick(row: &[Value], columns: &[usize]) -> Vec<Value> {
columns
.iter()
.map(|&c| row.get(c).cloned().unwrap_or(Value::Null))
.collect()
}
impl VirtualRows for [Vec<Value>] {
fn row_count(&self) -> Option<usize> {
Some(self.len())
}
fn row(&self, index: usize) -> Option<Vec<Value>> {
self.get(index).cloned()
}
fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
let start = start.min(self.len());
let end = start.saturating_add(len).min(self.len());
self[start..end].to_vec()
}
fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
let start = start.min(self.len());
let end = start.saturating_add(len).min(self.len());
self[start..end]
.iter()
.map(|row| pick(row, columns))
.collect()
}
}
impl VirtualRows for Vec<Vec<Value>> {
fn row_count(&self) -> Option<usize> {
Some(self.len())
}
fn row(&self, index: usize) -> Option<Vec<Value>> {
self.as_slice().row(index)
}
fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
self.as_slice().rows(start, len)
}
fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
self.as_slice().rows_columns(start, len, columns)
}
}
macro_rules! forward_rows {
($($ptr:ty),*) => {$(
impl<T: VirtualRows + ?Sized> VirtualRows for $ptr {
fn row_count(&self) -> Option<usize> {
(**self).row_count()
}
fn row(&self, index: usize) -> Option<Vec<Value>> {
(**self).row(index)
}
fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
(**self).rows(start, len)
}
fn rows_columns(
&self,
start: usize,
len: usize,
columns: &[usize],
) -> Vec<Vec<Value>> {
(**self).rows_columns(start, len, columns)
}
}
)*};
}
forward_rows!(&T, Box<T>, Arc<T>);
pub struct FnCells<F> {
rows: usize,
columns: usize,
cell: F,
}
impl<F> FnCells<F>
where
F: Fn(usize, usize) -> Value,
{
pub fn new(rows: usize, columns: usize, cell: F) -> Self {
FnCells {
rows,
columns,
cell,
}
}
}
impl<F> std::fmt::Debug for FnCells<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FnCells")
.field("rows", &self.rows)
.field("columns", &self.columns)
.finish_non_exhaustive()
}
}
impl<F> VirtualRows for FnCells<F>
where
F: Fn(usize, usize) -> Value,
{
fn row_count(&self) -> Option<usize> {
Some(self.rows)
}
fn row(&self, index: usize) -> Option<Vec<Value>> {
(index < self.rows).then(|| (0..self.columns).map(|c| (self.cell)(index, c)).collect())
}
fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
let end = start.saturating_add(len).min(self.rows);
(start.min(end)..end)
.map(|r| {
columns
.iter()
.map(|&c| {
if c < self.columns {
(self.cell)(r, c)
} else {
Value::Null
}
})
.collect()
})
.collect()
}
}
pub struct FnRows<F> {
count: Option<usize>,
row: F,
}
impl<F> FnRows<F>
where
F: Fn(usize) -> Option<Vec<Value>>,
{
pub fn new(count: Option<usize>, row: F) -> Self {
FnRows { count, row }
}
}
impl<F> std::fmt::Debug for FnRows<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FnRows")
.field("count", &self.count)
.finish_non_exhaustive()
}
}
impl<F> VirtualRows for FnRows<F>
where
F: Fn(usize) -> Option<Vec<Value>>,
{
fn row_count(&self) -> Option<usize> {
self.count
}
fn row(&self, index: usize) -> Option<Vec<Value>> {
if self.count.is_some_and(|count| index >= count) {
return None;
}
(self.row)(index)
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Page {
pub start: usize,
pub rows: Vec<Vec<Value>>,
pub total: Option<usize>,
pub more: bool,
pub columns: Vec<usize>,
pub frozen: usize,
pub column_total: usize,
}
impl Page {
pub fn range(&self) -> std::ops::Range<usize> {
self.start..self.start + self.rows.len()
}
pub fn column_range(&self) -> std::ops::Range<usize> {
match self.columns.get(self.frozen..) {
Some([first, .., last]) => *first..*last + 1,
Some([only]) => *only..*only + 1,
_ => 0..0,
}
}
pub fn column_position(&self, ascii: bool) -> Option<String> {
if self.columns.len() >= self.column_total {
return None;
}
let number = |n: usize| crate::format::number(i64::try_from(n).unwrap_or(i64::MAX));
let range = self.column_range();
let total = number(self.column_total);
Some(match range.len() {
0 => format!("no columns of {total}"),
1 => format!("column {} of {total}", number(range.start + 1)),
_ => {
let dash = if ascii { "-" } else { "–" };
format!(
"columns {}{dash}{} of {total}",
number(range.start + 1),
number(range.end)
)
}
})
}
pub fn position(&self, ascii: bool) -> String {
let number = |n: usize| crate::format::number(i64::try_from(n).unwrap_or(i64::MAX));
if self.rows.is_empty() {
return match self.start {
0 => "no rows".to_string(),
start => format!("no rows from {}", number(start + 1)),
};
}
let dash = if ascii { "-" } else { "–" };
let range = format!(
"rows {}{dash}{}",
number(self.start + 1),
number(self.start + self.rows.len())
);
match self.total {
Some(total) => format!("{range} of {}", number(total)),
None if self.more => {
format!(
"{range} of {}+",
number(self.start.saturating_add(self.rows.len() + 1))
)
}
None => range,
}
}
}
#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
pub struct VirtualTable<S> {
source: S,
columns: Vec<Column>,
frame: Frame,
offset: usize,
height: usize,
widths: Vec<Option<usize>>,
sample: usize,
max_width: usize,
wrap: bool,
position: bool,
row_numbers: bool,
fit_window: bool,
null: Option<String>,
footnote: Option<String>,
sampled: Mutex<Vec<Option<usize>>>,
window: ColumnWindow,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct ColumnWindow {
offset: usize,
count: Option<usize>,
fit: bool,
frozen: usize,
}
impl ColumnWindow {
fn on(&self) -> bool {
self.offset > 0 || self.count.is_some() || self.fit || self.frozen > 0
}
}
impl<S> std::fmt::Debug for VirtualTable<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VirtualTable")
.field("columns", &self.columns)
.field("offset", &self.offset)
.field("height", &self.height)
.field("widths", &self.widths)
.field("sample", &self.sample)
.field("window", &self.window)
.finish_non_exhaustive()
}
}
frame_builders!([S] VirtualTable<S>);
impl<S: VirtualRows> VirtualTable<S> {
pub fn new(columns: impl IntoIterator<Item = Column>, source: S) -> Self {
VirtualTable {
source,
columns: columns.into_iter().collect(),
frame: Frame::default(),
offset: 0,
height: 20,
widths: Vec::new(),
sample: DEFAULT_SAMPLE,
max_width: DEFAULT_MAX_COLUMN_WIDTH,
wrap: false,
position: true,
row_numbers: false,
fit_window: false,
null: None,
footnote: None,
sampled: Mutex::new(Vec::new()),
window: ColumnWindow::default(),
}
}
pub fn offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
pub fn height(mut self, rows: usize) -> Self {
self.set_height(rows);
self
}
pub fn widths(mut self, widths: impl IntoIterator<Item = impl Into<Option<usize>>>) -> Self {
self.widths = widths.into_iter().map(Into::into).collect();
self.resample();
self
}
pub fn sample(mut self, rows: usize) -> Self {
self.sample = rows.min(MAX_SAMPLE);
self.resample();
self
}
pub fn max_column_width(mut self, cells: usize) -> Self {
self.max_width = cells.max(1);
self.resample();
self
}
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
pub fn show_position(mut self, show: bool) -> Self {
self.position = show;
self
}
pub fn fit_window(mut self, fit: bool) -> Self {
self.fit_window = fit;
self
}
pub fn row_numbers(mut self, show: bool) -> Self {
self.row_numbers = show;
self
}
pub fn null_marker(mut self, marker: impl Into<String>) -> Self {
self.null = Some(marker.into());
self.resample();
self
}
pub fn footnote(mut self, note: impl Into<String>) -> Self {
self.footnote = Some(note.into());
self
}
pub fn column_offset(mut self, offset: usize) -> Self {
self.window.offset = offset;
self
}
pub fn visible_columns(mut self, count: usize) -> Self {
self.window.count = Some(count.max(1));
self
}
pub fn fit_columns(mut self, fit: bool) -> Self {
self.window.fit = fit;
self
}
pub fn frozen_columns(mut self, count: usize) -> Self {
self.window.frozen = count;
self
}
pub fn set_column_offset(&mut self, offset: usize) {
self.window.offset = offset;
}
pub fn set_visible_columns(&mut self, count: Option<usize>) {
self.window.count = count.map(|c| c.max(1));
}
pub fn scroll_columns_by(&mut self, columns: isize) {
let offset = self.window.offset.saturating_add_signed(columns);
self.window.offset = offset.min(self.max_column_offset());
}
pub fn window_column_offset(&self) -> usize {
self.window.offset
}
pub fn max_column_offset(&self) -> usize {
let scrollable = self.columns.len().saturating_sub(self.window.frozen);
scrollable.saturating_sub(self.window.count.unwrap_or(1))
}
pub fn shown_columns(&self, width: usize) -> Vec<usize> {
let n = self.columns.len();
if !self.window.on() {
return (0..n).collect();
}
let frozen = self.window.frozen.min(n);
let scrollable = n - frozen;
let start = frozen + self.window.offset.min(scrollable.saturating_sub(1));
let end = match self.window.count {
Some(count) => start.saturating_add(count).min(n),
None => n,
};
let mut shown: Vec<usize> = (0..frozen).collect();
if !self.window.fit || start >= end {
shown.extend(start..end);
return shown;
}
let mut widths = self.widths_of(&shown);
if self.row_numbers {
let last = self
.row_count()
.unwrap_or(self.offset.saturating_add(self.height));
widths.insert(0, last.max(1).to_string().len());
}
let mut next = start;
'fit: while next < end {
let room = width.saturating_sub(self.table_width(&widths));
let batch = (room / (self.max_width + 3)).max(1);
let candidates: Vec<usize> = (next..end.min(next + batch)).collect();
for (index, w) in candidates.iter().zip(self.widths_of(&candidates)) {
widths.push(w);
if shown.len() > frozen && self.table_width(&widths) > width {
break 'fit;
}
shown.push(*index);
}
next += candidates.len();
}
shown
}
fn table_width(&self, widths: &[usize]) -> usize {
let boxed = self.frame.box_set.is_some();
let edges = if boxed && self.frame.show_edge { 2 } else { 0 };
let dividers = if boxed {
widths.len().saturating_sub(1)
} else {
0
};
widths.iter().map(|w| w + 2).sum::<usize>() + dividers + edges
}
pub fn set_offset(&mut self, offset: usize) {
self.offset = offset;
}
pub fn set_height(&mut self, rows: usize) {
self.height = rows.min(MAX_HEIGHT);
}
pub fn scroll_by(&mut self, rows: isize) {
let offset = self.offset.saturating_add_signed(rows);
self.offset = match self.max_offset() {
Some(max) => offset.min(max),
None => offset,
};
}
pub fn window_offset(&self) -> usize {
self.offset
}
pub fn window_height(&self) -> usize {
self.height
}
pub fn row_count(&self) -> Option<usize> {
self.source.row_count()
}
pub fn max_offset(&self) -> Option<usize> {
self.row_count()
.map(|count| count.saturating_sub(self.height))
}
pub fn columns(&self) -> &[Column] {
&self.columns
}
pub fn source(&self) -> &S {
&self.source
}
pub fn source_mut(&mut self) -> &mut S {
&mut self.source
}
pub fn resample(&self) {
self.sampled
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clear();
}
pub fn page(&self) -> Page {
self.page_at(80)
}
pub fn page_at(&self, width: usize) -> Page {
let columns = self.shown_columns(width);
let count = self.source.row_count();
let start = match count {
Some(count) => self.offset.min(count.saturating_sub(self.height)),
None => self.offset,
};
let fetch = if count.is_some() {
self.height
} else {
self.height + 1
};
let mut rows = if self.window.on() {
self.source.rows_columns(start, fetch, &columns)
} else {
self.source.rows(start, fetch)
};
rows.truncate(fetch);
let more = match count {
Some(count) => start + rows.len().min(self.height) < count,
None => rows.len() > self.height,
};
rows.truncate(self.height);
let width = columns.len();
let rows: Vec<Vec<Value>> = rows.into_iter().map(|row| normalize(row, width)).collect();
let total = count.or_else(|| (!more && !rows.is_empty()).then(|| start + rows.len()));
Page {
start,
rows,
total,
more,
frozen: if self.window.on() {
self.window.frozen.min(self.columns.len())
} else {
0
},
column_total: self.columns.len(),
columns,
}
}
pub fn column_widths(&self) -> Vec<usize> {
let all: Vec<usize> = (0..self.columns.len()).collect();
self.widths_of(&all)
}
fn fixed_width(&self, index: usize) -> Option<usize> {
self.widths
.get(index)
.copied()
.flatten()
.or(self.columns[index].column_options().width)
}
fn widths_of(&self, indices: &[usize]) -> Vec<usize> {
let mut sampled = self.sampled.lock().unwrap_or_else(PoisonError::into_inner);
if sampled.len() != self.columns.len() {
*sampled = vec![None; self.columns.len()];
}
let missing: Vec<usize> = indices
.iter()
.copied()
.filter(|&i| sampled[i].is_none() && self.fixed_width(i).is_none())
.collect();
if !missing.is_empty() {
let mut widths: Vec<usize> = missing
.iter()
.map(|&i| Text::new(self.columns[i].header()).measurement().1)
.collect();
let rows = if missing.len() == self.columns.len() {
self.source.rows(0, self.sample)
} else {
self.source.rows_columns(0, self.sample, &missing)
};
for row in rows.into_iter().take(self.sample) {
let row = normalize(row, missing.len());
for ((width, &index), value) in widths.iter_mut().zip(&missing).zip(&row) {
let cell = self.cell(&self.columns[index], value).measurement().1;
*width = (*width).max(cell);
}
}
for (width, &index) in widths.into_iter().zip(&missing) {
sampled[index] = Some(width.clamp(1, self.max_width));
}
}
indices
.iter()
.map(|&i| self.fixed_width(i).or(sampled[i]).unwrap_or(self.max_width))
.collect()
}
pub fn chrome_lines(&self, console: &Console, width: usize) -> usize {
let options = console.options().update_width(width.max(1));
let columns = self.shown_columns(width);
let empty = Page {
start: 0,
rows: Vec::new(),
total: None,
more: false,
frozen: 0,
column_total: self.columns.len(),
columns,
};
let table = self.build(console, &empty);
table_lines(&table, console, &options).len()
+ usize::from(self.position)
+ usize::from(self.footnote.is_some())
}
pub fn to_table(&self, console: &Console) -> Table {
self.build(console, &self.page_at(console.width()))
}
fn cell(&self, column: &Column, value: &Value) -> Text {
match (&self.null, value) {
(Some(marker), Value::Null) => Text::new(marker.clone()),
_ => column.cell(value),
}
}
fn build(&self, console: &Console, page: &Page) -> Table {
let mut widths = self.widths_of(&page.columns);
if self.fit_window {
for (k, (&index, width)) in page.columns.iter().zip(widths.iter_mut()).enumerate() {
let column = &self.columns[index];
if self.fixed_width(index).is_some() {
continue;
}
for row in &page.rows {
let cell = self.cell(column, &row[k]).measurement().1;
*width = (*width).max(cell.min(self.max_width));
}
}
}
let mut columns: Vec<Column> = Vec::with_capacity(page.columns.len() + 1);
if self.row_numbers {
let last = page
.total
.unwrap_or(page.start + page.rows.len())
.max(page.start + page.rows.len());
let options = rich::ColumnOptions {
justify: Justify::Right,
width: Some(last.max(1).to_string().len()),
no_wrap: true,
style: style(console, "table.row_number"),
..Default::default()
};
columns.push(Column::new("#").options(options));
}
for (&index, width) in page.columns.iter().zip(&widths) {
let column = &self.columns[index];
let mut options = column.column_options().clone();
options.width = Some(*width);
if !self.wrap {
options.no_wrap = true;
if options.overflow == Overflow::Fold {
options.overflow = Overflow::Ellipsis;
}
}
columns.push(Column::new(column.header()).options(options));
}
let headers: Vec<Text> = columns.iter().map(|c| Text::new(c.header())).collect();
let mut table = self.frame.table(&columns, &headers, true, true);
let null_style = style(console, "table.null");
for (index, row) in page.rows.iter().enumerate() {
let mut cells = Vec::with_capacity(columns.len());
if self.row_numbers {
cells.push(Text::new((page.start + index + 1).to_string()));
}
for (&index, value) in page.columns.iter().zip(row) {
let mut text = self.cell(&self.columns[index], value);
if self.null.is_some() && matches!(value, Value::Null) {
let len = text.plain().len();
text.stylize(null_style.clone(), 0, len);
}
cells.push(text);
}
table.add_row_text(cells);
}
table
}
fn note_lines(
&self,
console: &Console,
options: &ConsoleOptions,
text: String,
) -> Vec<Vec<Segment>> {
let text = Text::styled(text, style(console, "table.position"))
.no_wrap(true)
.overflow(Overflow::Ellipsis);
let mut options = options.clone();
options.height = None;
console.render_lines(&text, &options, false)
}
fn position_text(page: &Page, ascii: bool) -> String {
let rows = page.position(ascii);
match page.column_position(ascii) {
Some(columns) => format!("{rows}, {columns}"),
None => rows,
}
}
fn render_lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Vec<Segment>> {
let page = self.page_at(options.max_width);
let table = self.build(console, &page);
let mut lines = table_lines(&table, console, options);
if self.position {
let text = Self::position_text(&page, console.ascii_only());
lines.extend(self.note_lines(console, options, text));
}
if let Some(note) = &self.footnote {
lines.extend(self.note_lines(console, options, note.clone()));
}
lines
}
}
impl<S: VirtualRows> Renderable for VirtualTable<S> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
crate::event::flatten(self.render_lines(console, options))
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
let page = self.page_at(options.max_width);
let mut measurement = self.build(console, &page).measure(console, options);
let notes = self
.position
.then(|| Self::position_text(&page, console.ascii_only()))
.into_iter()
.chain(self.footnote.clone());
for note in notes {
measurement.maximum = measurement.maximum.max(Text::new(note).cell_len());
}
measurement
}
}
impl<S: VirtualRows> crate::a11y::AccessibleText for VirtualTable<S> {
fn accessible_text(&self, width: usize) -> String {
let console = Console::builder().width(width.max(1)).build();
self.to_table(&console).accessible_text(width)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn render(table: &impl Renderable, width: usize) -> String {
Console::builder()
.width(width)
.color_system(None)
.build()
.render_export(table)
}
fn numbered(count: Option<usize>) -> FnRows<impl Fn(usize) -> Option<Vec<Value>>> {
FnRows::new(count, |i| {
Some(vec![Value::from(i), format!("r{i}").into()])
})
}
#[test]
fn an_offset_past_the_end_shows_the_last_window() {
let rows: Vec<Vec<Value>> = (0..10).map(|i| vec![Value::from(i)]).collect();
let table = VirtualTable::new([Column::new("n")], rows)
.offset(50)
.height(3);
let page = table.page();
assert_eq!(page.range(), 7..10);
assert_eq!(page.total, Some(10));
assert!(!page.more);
assert_eq!(page.position(false), "rows 8–10 of 10");
}
#[test]
fn an_uncounted_source_reports_what_it_knows() {
let rows = FnRows::new(None, |i| (i < 5).then(|| vec![Value::from(i)]));
let table = VirtualTable::new([Column::new("n")], rows).height(2);
assert_eq!(table.page().position(false), "rows 1–2 of 3+");
let mut table = table;
table.set_offset(3);
let page = table.page();
assert_eq!((page.total, page.more), (Some(5), false));
assert_eq!(page.position(true), "rows 4-5 of 5");
table.set_offset(9);
assert_eq!(table.page().position(false), "no rows from 10");
}
#[test]
fn ragged_rows_are_padded_and_cut() {
let rows = vec![
vec![Value::from("a")],
vec![Value::from("b"), Value::from(2), Value::from("extra")],
vec![],
];
let table = VirtualTable::new([Column::new("x"), Column::new("y")], rows);
let page = table.page();
assert!(page.rows.iter().all(|row| row.len() == 2));
let out = render(&table, 30);
assert!(out.contains("│ b │ 2 │"), "{out}");
}
#[test]
fn widths_hold_still_while_scrolling() {
let mut table = VirtualTable::new([Column::new("n"), Column::new("s")], numbered(None))
.sample(10)
.height(2);
let first = render(&table, 40);
table.scroll_by(500);
let later = render(&table, 40);
let width = |out: &str| out.lines().next().unwrap().chars().count();
assert_eq!(width(&first), width(&later));
assert!(later.contains("│ … │ r… │"), "{later}");
assert_eq!(table.column_widths(), [1, 2]);
}
#[test]
fn fixed_widths_win_and_the_cap_bounds_samples() {
let long = vec![vec![Value::from("x".repeat(500)), Value::from("y")]];
let table = VirtualTable::new([Column::new("a"), Column::new("b")], long.clone())
.max_column_width(8);
assert_eq!(table.column_widths(), [8, 1]);
let table =
VirtualTable::new([Column::new("a"), Column::new("b")], long).widths([None, Some(5)]);
assert_eq!(table.column_widths(), [40, 5]);
}
#[test]
fn scrolling_stops_at_both_ends() {
let mut table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(10);
table.scroll_by(-5);
assert_eq!(table.window_offset(), 0);
table.scroll_by(1_000);
assert_eq!(table.window_offset(), 90);
assert_eq!(table.max_offset(), Some(90));
}
#[test]
fn nulls_can_be_marked_apart_from_empty_text() {
let rows = vec![vec![Value::Null], vec![Value::from("")]];
let table = VirtualTable::new([Column::new("v")], rows)
.null_marker("NULL")
.show_position(false);
assert_eq!(
render(&table, 20),
"┏━━━━━━┓\n┃ v ┃\n┡━━━━━━┩\n│ NULL │\n│ │\n└──────┘\n"
);
}
#[test]
fn row_numbers_and_footnote() {
let table = VirtualTable::new([Column::new("s")], numbered(Some(1_000)))
.offset(998)
.height(2)
.row_numbers(true)
.widths([3])
.footnote("(1,000 rows)");
let out = render(&table, 40);
assert!(out.contains("│ 999 │ 998 │"), "{out}");
assert!(
out.ends_with("rows 999–1,000 of 1,000\n(1,000 rows)\n"),
"{out}"
);
}
#[test]
fn chrome_lines_count_everything_but_rows() {
let table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(5);
let console = Console::builder().width(40).build();
let out = render(&table, 40);
assert_eq!(out.lines().count(), 5 + table.chrome_lines(&console, 40));
}
#[test]
fn an_empty_source_renders_a_header_and_no_rows() {
let table = VirtualTable::new([Column::new("n")], Vec::<Vec<Value>>::new());
let out = render(&table, 20);
assert!(out.ends_with("no rows\n"), "{out}");
let table = VirtualTable::new(Vec::<Column>::new(), numbered(Some(3)));
render(&table, 20);
let table = VirtualTable::new(Vec::<Column>::new(), numbered(Some(3))).fit_columns(true);
assert_eq!(table.shown_columns(20), Vec::<usize>::new());
render(&table, 20);
}
struct Counting {
rows: usize,
columns: usize,
cells: std::sync::atomic::AtomicUsize,
seen: Mutex<std::collections::BTreeSet<usize>>,
}
impl Counting {
fn new(rows: usize, columns: usize) -> Self {
Counting {
rows,
columns,
cells: Default::default(),
seen: Default::default(),
}
}
fn seen(&self) -> Vec<usize> {
self.seen.lock().unwrap().iter().copied().collect()
}
fn cells(&self) -> usize {
self.cells.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl VirtualRows for Counting {
fn row_count(&self) -> Option<usize> {
Some(self.rows)
}
fn row(&self, index: usize) -> Option<Vec<Value>> {
let all: Vec<usize> = (0..self.columns).collect();
self.rows_columns(index, 1, &all).pop()
}
fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
let end = start.saturating_add(len).min(self.rows);
let rows: Vec<Vec<Value>> = (start.min(end)..end)
.map(|r| {
columns
.iter()
.map(|c| Value::from(format!("r{r}c{c}")))
.collect()
})
.collect();
self.cells.fetch_add(
rows.len() * columns.len(),
std::sync::atomic::Ordering::Relaxed,
);
self.seen.lock().unwrap().extend(columns.iter().copied());
rows
}
}
fn twelve() -> Vec<Column> {
(0..12).map(|c| Column::new(format!("c{c}"))).collect()
}
#[test]
fn a_column_window_fetches_only_its_columns() {
let source = Counting::new(5, 12);
let table = VirtualTable::new(twelve(), &source)
.height(2)
.column_offset(3)
.visible_columns(2);
assert_eq!(
render(&table, 60),
"\
┏━━━━━━┳━━━━━━┓
┃ c3 ┃ c4 ┃
┡━━━━━━╇━━━━━━┩
│ r0c3 │ r0c4 │
│ r1c3 │ r1c4 │
└──────┴──────┘
rows 1–2 of 5, columns 4–5 of 12
"
);
assert_eq!(source.seen(), vec![3, 4]);
assert_eq!(source.cells(), 5 * 2 + 2 * 2);
let page = table.page();
assert_eq!(
(page.columns.clone(), page.frozen, page.column_total),
(vec![3, 4], 0, 12)
);
assert_eq!(page.column_range(), 3..5);
}
#[test]
fn frozen_columns_stay_on_the_left() {
let source = Counting::new(5, 12);
let mut table = VirtualTable::new(twelve(), &source)
.height(1)
.frozen_columns(1)
.column_offset(5)
.visible_columns(2);
assert_eq!(
render(&table, 60),
"\
┏━━━━━━┳━━━━━━┳━━━━━━┓
┃ c0 ┃ c6 ┃ c7 ┃
┡━━━━━━╇━━━━━━╇━━━━━━┩
│ r0c0 │ r0c6 │ r0c7 │
└──────┴──────┴──────┘
rows 1–1 of 5, columns 7–8 of 12
"
);
assert_eq!(source.seen(), vec![0, 6, 7]);
table.scroll_columns_by(-100);
assert_eq!(table.window_column_offset(), 0);
table.scroll_columns_by(100);
assert_eq!(table.max_column_offset(), 9);
assert_eq!(table.window_column_offset(), 9);
assert_eq!(table.shown_columns(60), vec![0, 10, 11]);
table.set_visible_columns(Some(1));
table.set_column_offset(50);
assert_eq!(table.shown_columns(60), vec![0, 11]);
assert_eq!(
table.page().column_position(true).as_deref(),
Some("column 12 of 12")
);
}
#[test]
fn fit_to_width_shows_what_fits_from_the_offset() {
let source = Counting::new(5, 12);
let table = VirtualTable::new(twelve(), &source)
.height(1)
.frozen_columns(1)
.column_offset(2)
.fit_columns(true);
assert_eq!(table.shown_columns(30), vec![0, 3, 4, 5]);
assert_eq!(
render(&table, 34),
"\
┏━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┓
┃ c0 ┃ c3 ┃ c4 ┃ c5 ┃
┡━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━┩
│ r0c0 │ r0c3 │ r0c4 │ r0c5 │
└──────┴──────┴──────┴──────┘
rows 1–1 of 5, columns 4–6 of 12
"
);
assert_eq!(source.seen(), vec![0, 3, 4, 5, 6]);
assert_eq!(table.shown_columns(5), vec![0, 3]);
let all = VirtualTable::new(twelve(), &source)
.height(1)
.fit_columns(true);
assert_eq!(all.shown_columns(200).len(), 12);
assert_eq!(all.page_at(200).column_position(false), None);
let capped = VirtualTable::new(twelve(), &source)
.fit_columns(true)
.visible_columns(2);
assert_eq!(capped.shown_columns(200), vec![0, 1]);
}
#[test]
fn a_window_reads_its_columns_aloud() {
use crate::a11y::AccessibleText;
let table = VirtualTable::new(twelve(), Counting::new(3, 12))
.height(1)
.column_offset(10);
assert_eq!(
table.accessible_text(80),
"Table with 1 row, columns: c10, c11\nRow 1: c10: r0c10; c11: r0c11"
);
}
#[test]
fn the_guides_wide_table() {
let cells = FnCells::new(100_000, 2_000, |r, c| Value::from(r * c));
let columns = (0..2_000).map(|c| Column::new(format!("c{c}")));
let table = VirtualTable::new(columns, cells)
.height(2)
.frozen_columns(1)
.column_offset(40)
.fit_columns(true);
assert_eq!(
render(&table, 80),
"\
┏━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┓
┃ c0 ┃ c41 ┃ c42 ┃ c43 ┃ c44 ┃ c45 ┃ c46 ┃ c47 ┃ c48 ┃ c49 ┃ c50 ┃
┡━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━┩
│ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │
│ 0 │ 41 │ 42 │ 43 │ 44 │ 45 │ 46 │ 47 │ 48 │ 49 │ 50 │
└────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
rows 1–2 of 100,000, columns 42–51 of 2,000
"
);
}
#[test]
fn fn_cells_compute_only_what_is_asked() {
let calls = std::sync::atomic::AtomicUsize::new(0);
let grid = FnCells::new(100_000, 2_000, |r, c| {
calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Value::from(r + c)
});
let table = VirtualTable::new((0..2_000).map(|c| Column::new(format!("{c}"))), &grid)
.offset(50_000)
.height(3)
.column_offset(1_000)
.visible_columns(3)
.sample(10);
let out = render(&table, 60);
assert!(
out.ends_with("rows 50,001–50,003 of 100,000, columns 1,001–1,003 of 2,000\n"),
"{out}"
);
assert_eq!(
calls.load(std::sync::atomic::Ordering::Relaxed),
(10 + 3) * 3
);
assert_eq!(grid.row(3).map(|r| r.len()), Some(2_000));
assert_eq!(grid.rows_columns(99_999, 5, &[0, 5_000]).len(), 1);
assert_eq!(grid.rows_columns(0, 1, &[5_000]), vec![vec![Value::Null]]);
}
}