Skip to main content

rich_ext/table/
virtualized.rs

1//! Virtualised tables (#260): one window of a large row source.
2//!
3//! A [`VirtualTable`] renders the rows `offset..offset + height` of a
4//! [`VirtualRows`] source and nothing else, so a million-row source costs one
5//! window of memory. The source is anything that can fetch a row by index: a
6//! `Vec` of rows, a closure ([`FnRows`]), or a caller's own type (a file
7//! index, a query cursor, `rs-rich-data`'s rows).
8//!
9//! Column widths do not follow the window, so scrolling never makes columns
10//! jump: each is fixed by the caller ([`widths`](VirtualTable::widths), or a
11//! core `ColumnOptions::width`) or sampled once from the header and the first
12//! [`sample`](VirtualTable::sample) rows, capped at
13//! [`max_column_width`](VirtualTable::max_column_width). Cells do not wrap by
14//! default, so a row is one line and a viewport of N lines holds a known
15//! number of rows ([`chrome_lines`](VirtualTable::chrome_lines)); longer
16//! text ends in an ellipsis. A position line under the table says where the
17//! window is (`rows 1,001–1,040 of 1,000,000`), or how many rows are known to
18//! exist when the source cannot count them (`rows 1–40 of 41+`).
19//!
20//! A very wide table gets a window of columns too (#702): a
21//! [column offset](VirtualTable::column_offset) and a
22//! [count](VirtualTable::visible_columns) or
23//! [as many as fit](VirtualTable::fit_columns), with
24//! [frozen columns](VirtualTable::frozen_columns) kept on the left. Only
25//! the columns shown are fetched ([`VirtualRows::rows_columns`]), sampled
26//! and laid out, and the position line adds `columns 41–60 of 2,000`.
27//!
28//! This is a static renderer: interactive scrolling belongs to
29//! `rs-rich-interact`'s viewport, which moves the window with
30//! [`set_offset`](VirtualTable::set_offset) or
31//! [`scroll_by`](VirtualTable::scroll_by) (and across with
32//! [`scroll_columns_by`](VirtualTable::scroll_columns_by)) and renders
33//! again.
34//!
35//! ```
36//! use rich::{Console, Justify};
37//! use rich_ext::table::{Column, FnRows, Value, VirtualTable};
38//!
39//! // Ten million rows, computed on demand: none of them is ever stored.
40//! let rows = FnRows::new(Some(10_000_000), |i| {
41//!     Some(vec![Value::Int(i as i64 + 1), format!("item {i}").into()])
42//! });
43//! let table = VirtualTable::new(
44//!     [Column::new("id").justify(Justify::Right), Column::new("name")],
45//!     rows,
46//! )
47//! .widths([8, 10])
48//! .offset(1_000)
49//! .height(3);
50//!
51//! let out = Console::builder().width(40).build().render_export(&table);
52//! assert_eq!(
53//!     out,
54//!     "\
55//! ┏━━━━━━━━━━┳━━━━━━━━━━━━┓
56//! ┃       id ┃ name       ┃
57//! ┡━━━━━━━━━━╇━━━━━━━━━━━━┩
58//! │     1001 │ item 1000  │
59//! │     1002 │ item 1001  │
60//! │     1003 │ item 1002  │
61//! └──────────┴────────────┘
62//! rows 1,001–1,003 of 10,000,000
63//! "
64//! );
65//! ```
66//!
67//! Without [`widths`](VirtualTable::widths), the widths would be sampled
68//! from the header and the first 100 rows (`id` up to `100`, `name` up to
69//! `item 99`), and the window's longer values would end in an ellipsis
70//! (`10…`). Fix widths when the caller knows better, or sample more rows.
71
72use 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
80/// The most rows one window shows; a larger [`height`](VirtualTable::height)
81/// is cut to this.
82pub const MAX_HEIGHT: usize = 10_000;
83
84/// The most rows sampled for column widths.
85pub const MAX_SAMPLE: usize = 10_000;
86
87/// Rows sampled for column widths by default.
88pub const DEFAULT_SAMPLE: usize = 100;
89
90/// The widest a sampled column gets by default, in cells.
91pub const DEFAULT_MAX_COLUMN_WIDTH: usize = 40;
92
93/// Rows fetched by index, for a [`VirtualTable`].
94///
95/// Only [`row`](VirtualRows::row) is required. A source that can fetch a
96/// range more cheaply than row by row (a file index, a cursor) overrides
97/// [`rows`](VirtualRows::rows); one that knows its length overrides
98/// [`row_count`](VirtualRows::row_count). A row may have more or fewer
99/// cells than the table has columns: missing cells are null and extra cells
100/// are dropped.
101///
102/// The methods take `&self` because rendering does. A source that reads
103/// from something stateful (a cursor) keeps that state behind a lock.
104pub trait VirtualRows {
105    /// How many rows there are, when the source knows.
106    fn row_count(&self) -> Option<usize> {
107        None
108    }
109
110    /// The row at `index` (0-based), or `None` past the end.
111    fn row(&self, index: usize) -> Option<Vec<Value>>;
112
113    /// Up to `len` rows from `start`, fewer at the end. The default calls
114    /// [`row`](VirtualRows::row) until it returns `None`.
115    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    /// Up to `len` rows from `start`, each with only the cells of
122    /// `columns` (0-based, in that order; a cell past the row's end is
123    /// null): what a [column window](VirtualTable::visible_columns) of a
124    /// wide table fetches. The default calls [`rows`](VirtualRows::rows)
125    /// and picks the cells; a source that can read some columns without
126    /// the rest (a columnar file, a query, cells computed on demand such
127    /// as [`FnCells`]) overrides it.
128    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
136/// The cells of `columns` from `row`, null past its end.
137fn 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
215/// Cells computed by a closure: `cell(row, column)` for `rows` rows of
216/// `columns` columns. Only the cells a window shows (and the width sample
217/// of the columns it shows) are ever computed, so a table can be as wide
218/// as it is long.
219///
220/// ```
221/// use rich_ext::table::{FnCells, Value, VirtualRows};
222///
223/// let grid = FnCells::new(1_000_000, 2_000, |r, c| Value::from(r * c));
224/// assert_eq!(grid.rows_columns(10, 2, &[3, 1_999]), vec![
225///     vec![Value::from(30), Value::from(19_990)],
226///     vec![Value::from(33), Value::from(21_989)],
227/// ]);
228/// assert_eq!(grid.row(1_000_000), None);
229/// ```
230pub 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    /// `rows` rows of `columns` cells from `cell(row, column)`.
241    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
290/// Rows computed by a closure: `row(index)` returns the row at `index`, or
291/// `None` past the end. The count is optional.
292///
293/// ```
294/// use rich_ext::table::{FnRows, Value, VirtualRows};
295///
296/// let squares = FnRows::new(None, |i| (i < 5).then(|| vec![Value::from(i * i)]));
297/// assert_eq!(squares.row(3), Some(vec![Value::Int(9)]));
298/// assert_eq!(squares.rows(3, 10).len(), 2);
299/// assert_eq!(squares.row_count(), None);
300/// ```
301pub 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    /// Rows from `row`, `count` of them when known.
311    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/// The rows one window shows, from [`VirtualTable::page`].
341#[derive(Clone, Debug, PartialEq)]
342#[non_exhaustive]
343pub struct Page {
344    /// The 0-based index of the first row shown. It is the table's offset,
345    /// moved back when that is past the end of a source that knows its
346    /// length.
347    pub start: usize,
348    /// The rows shown, each with one cell per column shown (see
349    /// [`columns`](Self::columns)).
350    pub rows: Vec<Vec<Value>>,
351    /// How many rows the source has: its own count, or the end when this
352    /// window reached it.
353    pub total: Option<usize>,
354    /// Whether a row is known to follow the window.
355    pub more: bool,
356    /// The 0-based indices of the columns shown, in order: the frozen ones,
357    /// then the window's. Every column when the table has no column window.
358    pub columns: Vec<usize>,
359    /// How many of [`columns`](Self::columns) are frozen.
360    pub frozen: usize,
361    /// How many columns the table has.
362    pub column_total: usize,
363}
364
365impl Page {
366    /// The 0-based indices of the rows shown.
367    pub fn range(&self) -> std::ops::Range<usize> {
368        self.start..self.start + self.rows.len()
369    }
370
371    /// The 0-based indices of the columns shown after the frozen ones.
372    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    /// Where the column window is, in words, when it leaves columns out:
381    /// `columns 41–60 of 2,000` (counted from 1, frozen columns aside), or
382    /// `column 7 of 12`. `None` when every column is shown. `ascii` writes
383    /// the range with `-` instead of `–`.
384    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    /// Where the window is, in words: `rows 1,001–1,040 of 1,000,000`, `rows
406    /// 1–40 of 41+` when the source cannot count past the window, `no rows`,
407    /// or `no rows from 1,001` past the end of an uncounted source. `ascii`
408    /// writes the range with `-` instead of `–`.
409    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
436/// One window of a [`VirtualRows`] source as a table; see the
437/// [module docs](self).
438///
439/// The frame (title, caption, box, edges, expand, border style) is set with
440/// the same builders as [`TableData`](super::TableData).
441pub 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    /// Each column's sampled width, once sampled.
457    sampled: Mutex<Vec<Option<usize>>>,
458    window: ColumnWindow,
459}
460
461/// Which columns a [`VirtualTable`] shows: see
462/// [`visible_columns`](VirtualTable::visible_columns).
463#[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    /// Whether it leaves any columns out, or could.
473    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    /// A window of 20 rows from the top of `source`, under `columns`.
495    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    /// Start the window at row `offset` (0-based).
517    pub fn offset(mut self, offset: usize) -> Self {
518        self.offset = offset;
519        self
520    }
521
522    /// Show `rows` rows (at most [`MAX_HEIGHT`]).
523    pub fn height(mut self, rows: usize) -> Self {
524        self.set_height(rows);
525        self
526    }
527
528    /// Fix the column widths, in cells, in column order. A missing or `None`
529    /// entry leaves that column to its `ColumnOptions::width`, or to the
530    /// sample.
531    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    /// Sample the header and the first `rows` rows for column widths
538    /// (default [`DEFAULT_SAMPLE`], at most [`MAX_SAMPLE`]). Zero samples
539    /// only the header.
540    pub fn sample(mut self, rows: usize) -> Self {
541        self.sample = rows.min(MAX_SAMPLE);
542        self.resample();
543        self
544    }
545
546    /// Cap sampled widths at `cells` (default [`DEFAULT_MAX_COLUMN_WIDTH`],
547    /// at least 1). Fixed widths are not capped.
548    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    /// Wrap long cells onto more lines instead of cutting them short with an
555    /// ellipsis (default off, so a row is one line).
556    pub fn wrap(mut self, wrap: bool) -> Self {
557        self.wrap = wrap;
558        self
559    }
560
561    /// Show the position line under the table (default on).
562    pub fn show_position(mut self, show: bool) -> Self {
563        self.position = show;
564        self
565    }
566
567    /// Also widen sampled columns to fit the rows shown (default off), up to
568    /// [`max_column_width`](Self::max_column_width). For a window that is
569    /// rendered once: scrolling such a table can change its widths.
570    pub fn fit_window(mut self, fit: bool) -> Self {
571        self.fit_window = fit;
572        self
573    }
574
575    /// Show each row's 1-based number in a first `#` column (default off).
576    pub fn row_numbers(mut self, show: bool) -> Self {
577        self.row_numbers = show;
578        self
579    }
580
581    /// Show null cells as `marker` in the `table.null` style, instead of
582    /// empty like an empty string. Column formatters are not called for
583    /// them.
584    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    /// A line under the position line, in the `table.position` style: a row
591    /// count, a timing, a note.
592    pub fn footnote(mut self, note: impl Into<String>) -> Self {
593        self.footnote = Some(note.into());
594        self
595    }
596
597    /// Start the column window at column `offset`, counted from 0 after the
598    /// [frozen](Self::frozen_columns) ones. With a column window only the
599    /// columns it shows are fetched, sampled and laid out; the position
600    /// line adds `columns 41–60 of 2,000` when some are left out.
601    pub fn column_offset(mut self, offset: usize) -> Self {
602        self.window.offset = offset;
603        self
604    }
605
606    /// Show `count` columns (at least 1) from the
607    /// [column offset](Self::column_offset), besides the frozen ones. With
608    /// [`fit_columns`](Self::fit_columns), at most `count`.
609    pub fn visible_columns(mut self, count: usize) -> Self {
610        self.window.count = Some(count.max(1));
611        self
612    }
613
614    /// Show as many columns from the [column offset](Self::column_offset)
615    /// as fit the width, besides the frozen ones, and always one (default
616    /// off). Columns are measured by their widths (fixed, or sampled),
617    /// sampled a few at a time, so a fit samples at most one column past
618    /// those it shows.
619    pub fn fit_columns(mut self, fit: bool) -> Self {
620        self.window.fit = fit;
621        self
622    }
623
624    /// Keep the first `count` columns on the left, whatever the column
625    /// offset (an id column, a name): they are always shown and never
626    /// scrolled.
627    pub fn frozen_columns(mut self, count: usize) -> Self {
628        self.window.frozen = count;
629        self
630    }
631
632    /// Move the column window to column `offset` (after the frozen ones).
633    pub fn set_column_offset(&mut self, offset: usize) {
634        self.window.offset = offset;
635    }
636
637    /// Show `count` columns from the offset, or `None` for all of them (or
638    /// as many as [fit](Self::fit_columns)).
639    pub fn set_visible_columns(&mut self, count: Option<usize>) {
640        self.window.count = count.map(|c| c.max(1));
641    }
642
643    /// Move the column window by `columns` (negative is left), stopping at
644    /// the first column and at the [last offset](Self::max_column_offset).
645    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    /// The column window's first column, after the frozen ones, as set.
651    pub fn window_column_offset(&self) -> usize {
652        self.window.offset
653    }
654
655    /// The largest column offset: the one whose window of
656    /// [`visible_columns`](Self::visible_columns) ends at the last column,
657    /// or that shows the last column alone without a count.
658    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    /// The 0-based indices of the columns a render `width` cells wide
664    /// shows: the frozen ones, then the window's. Every column without a
665    /// column window.
666    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        // Fit: add columns while the table fits, sampling a batch at a time
684        // that surely fits at the cap, and at least one.
685        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    /// The cells a table of columns `widths` wide takes, its borders and
710    /// padding included.
711    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    /// Move the window to row `offset` (0-based).
723    pub fn set_offset(&mut self, offset: usize) {
724        self.offset = offset;
725    }
726
727    /// Change how many rows the window shows (at most [`MAX_HEIGHT`]).
728    pub fn set_height(&mut self, rows: usize) {
729        self.height = rows.min(MAX_HEIGHT);
730    }
731
732    /// Move the window by `rows` (negative is up), stopping at the top and,
733    /// when the source knows its length, at the last full window.
734    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    /// The window's first row (0-based), as set.
743    pub fn window_offset(&self) -> usize {
744        self.offset
745    }
746
747    /// How many rows the window shows.
748    pub fn window_height(&self) -> usize {
749        self.height
750    }
751
752    /// The source's row count, when it knows.
753    pub fn row_count(&self) -> Option<usize> {
754        self.source.row_count()
755    }
756
757    /// The offset of the last full window, when the source knows its length.
758    pub fn max_offset(&self) -> Option<usize> {
759        self.row_count()
760            .map(|count| count.saturating_sub(self.height))
761    }
762
763    /// The columns.
764    pub fn columns(&self) -> &[Column] {
765        &self.columns
766    }
767
768    /// The source.
769    pub fn source(&self) -> &S {
770        &self.source
771    }
772
773    /// The source, to change. Call [`resample`](Self::resample) if the first
774    /// rows changed.
775    pub fn source_mut(&mut self) -> &mut S {
776        &mut self.source
777    }
778
779    /// Forget the sampled widths, so the next render samples again.
780    pub fn resample(&self) {
781        self.sampled
782            .lock()
783            .unwrap_or_else(PoisonError::into_inner)
784            .clear();
785    }
786
787    /// The rows the window shows, fetched from the source. An offset past
788    /// the end of a source that knows its length moves back to the last full
789    /// window; one past the end of a source that does not shows no rows.
790    /// A window that [fits its columns](Self::fit_columns) to the width is
791    /// laid out for 80 cells; [`page_at`](Self::page_at) takes the width.
792    pub fn page(&self) -> Page {
793        self.page_at(80)
794    }
795
796    /// The rows the window shows, with the columns a render `width` cells
797    /// wide shows ([`shown_columns`](Self::shown_columns)): only those
798    /// cells are fetched.
799    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        // One extra row says whether more follow when the source cannot count.
807        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    /// Each column's width in cells: fixed, or sampled from the header and
842    /// the first rows (and cached until [`resample`](Self::resample)). This
843    /// samples every column; a column window samples only those it shows.
844    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    /// Column `index`'s fixed width, if it has one.
850    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    /// The widths of `indices`' columns, sampling those not sampled yet in
859    /// one fetch of their cells.
860    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                // Every column: the rows as the source gives them.
877                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                // Pad short rows as `page` does, so a missing cell is
883                // sampled as the null marker it will show.
884                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    /// How many lines the table takes around its rows at `width` cells: the
901    /// title, edges, header, caption, position line and footnote. A viewport
902    /// of `lines` lines holds `lines - chrome_lines` rows when cells do not
903    /// wrap.
904    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    /// The window as a core table (without the position line and footnote),
923    /// its columns as many as the console's width shows.
924    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    /// The position line: the rows, and the columns when some are left
1012    /// out.
1013    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    /// The table's measurement, widened to the position line and footnote:
1042    /// every line `rich_render` emits.
1043    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        // `n` was sampled from 0..10: one cell wide, so `500` is cut short.
1135        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        // A column window over no columns shows none.
1204        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    /// `rows` rows of `columns` cells `r{row}c{column}`, counting the cells
1210    /// fetched and noting their columns.
1211    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        // The width sample (5 rows) and the window (2 rows), two columns.
1291        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        // Scrolling stops at the first column and at the last full window.
1321        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        // One column: `column`, singular.
1328        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        // Each column is 4 cells, 7 with its padding and border: 30 cells
1346        // hold four.
1347        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        // Sampling looked one column past the window, no further.
1360        assert_eq!(source.seen(), vec![0, 3, 4, 5, 6]);
1361        // Narrower than one column: still one, past the frozen ones.
1362        assert_eq!(table.shown_columns(5), vec![0, 3]);
1363        // Wide enough for all: no column note.
1364        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        // A count caps the fit.
1370        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        // Ten sample rows and three window rows, three columns each.
1430        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}