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::{Column, Frame, Value, frame_builders, style};
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).
441#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
442pub struct VirtualTable<S> {
443    source: S,
444    columns: Vec<Column>,
445    frame: Frame,
446    offset: usize,
447    height: usize,
448    widths: Vec<Option<usize>>,
449    sample: usize,
450    max_width: usize,
451    wrap: bool,
452    position: bool,
453    row_numbers: bool,
454    fit_window: bool,
455    null: Option<String>,
456    footnote: Option<String>,
457    /// Each column's sampled width, once sampled.
458    sampled: Mutex<Vec<Option<usize>>>,
459    window: ColumnWindow,
460}
461
462/// Which columns a [`VirtualTable`] shows: see
463/// [`visible_columns`](VirtualTable::visible_columns).
464#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
465struct ColumnWindow {
466    offset: usize,
467    count: Option<usize>,
468    fit: bool,
469    frozen: usize,
470}
471
472impl ColumnWindow {
473    /// Whether it leaves any columns out, or could.
474    fn on(&self) -> bool {
475        self.offset > 0 || self.count.is_some() || self.fit || self.frozen > 0
476    }
477}
478
479impl<S> std::fmt::Debug for VirtualTable<S> {
480    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481        f.debug_struct("VirtualTable")
482            .field("columns", &self.columns)
483            .field("offset", &self.offset)
484            .field("height", &self.height)
485            .field("widths", &self.widths)
486            .field("sample", &self.sample)
487            .field("window", &self.window)
488            .finish_non_exhaustive()
489    }
490}
491
492frame_builders!([S] VirtualTable<S>);
493
494impl<S: VirtualRows> VirtualTable<S> {
495    /// A window of 20 rows from the top of `source`, under `columns`.
496    pub fn new(columns: impl IntoIterator<Item = Column>, source: S) -> Self {
497        VirtualTable {
498            source,
499            columns: columns.into_iter().collect(),
500            frame: Frame::default(),
501            offset: 0,
502            height: 20,
503            widths: Vec::new(),
504            sample: DEFAULT_SAMPLE,
505            max_width: DEFAULT_MAX_COLUMN_WIDTH,
506            wrap: false,
507            position: true,
508            row_numbers: false,
509            fit_window: false,
510            null: None,
511            footnote: None,
512            sampled: Mutex::new(Vec::new()),
513            window: ColumnWindow::default(),
514        }
515    }
516
517    /// Start the window at row `offset` (0-based).
518    pub fn offset(mut self, offset: usize) -> Self {
519        self.offset = offset;
520        self
521    }
522
523    /// Show `rows` rows (at most [`MAX_HEIGHT`]).
524    pub fn height(mut self, rows: usize) -> Self {
525        self.set_height(rows);
526        self
527    }
528
529    /// Fix the column widths, in cells, in column order. A missing or `None`
530    /// entry leaves that column to its `ColumnOptions::width`, or to the
531    /// sample.
532    pub fn widths(mut self, widths: impl IntoIterator<Item = impl Into<Option<usize>>>) -> Self {
533        self.widths = widths.into_iter().map(Into::into).collect();
534        self.resample();
535        self
536    }
537
538    /// Sample the header and the first `rows` rows for column widths
539    /// (default [`DEFAULT_SAMPLE`], at most [`MAX_SAMPLE`]). Zero samples
540    /// only the header.
541    pub fn sample(mut self, rows: usize) -> Self {
542        self.sample = rows.min(MAX_SAMPLE);
543        self.resample();
544        self
545    }
546
547    /// Cap sampled widths at `cells` (default [`DEFAULT_MAX_COLUMN_WIDTH`],
548    /// at least 1). Fixed widths are not capped.
549    pub fn max_column_width(mut self, cells: usize) -> Self {
550        self.max_width = cells.max(1);
551        self.resample();
552        self
553    }
554
555    /// Wrap long cells onto more lines instead of cutting them short with an
556    /// ellipsis (default off, so a row is one line).
557    pub fn wrap(mut self, wrap: bool) -> Self {
558        self.wrap = wrap;
559        self
560    }
561
562    /// Show the position line under the table (default on).
563    pub fn show_position(mut self, show: bool) -> Self {
564        self.position = show;
565        self
566    }
567
568    /// Also widen sampled columns to fit the rows shown (default off), up to
569    /// [`max_column_width`](Self::max_column_width). For a window that is
570    /// rendered once: scrolling such a table can change its widths.
571    pub fn fit_window(mut self, fit: bool) -> Self {
572        self.fit_window = fit;
573        self
574    }
575
576    /// Show each row's 1-based number in a first `#` column (default off).
577    pub fn row_numbers(mut self, show: bool) -> Self {
578        self.row_numbers = show;
579        self
580    }
581
582    /// Show null cells as `marker` in the `table.null` style, instead of
583    /// empty like an empty string. Column formatters are not called for
584    /// them.
585    pub fn null_marker(mut self, marker: impl Into<String>) -> Self {
586        self.null = Some(marker.into());
587        self.resample();
588        self
589    }
590
591    /// A line under the position line, in the `table.position` style: a row
592    /// count, a timing, a note.
593    pub fn footnote(mut self, note: impl Into<String>) -> Self {
594        self.footnote = Some(note.into());
595        self
596    }
597
598    /// Start the column window at column `offset`, counted from 0 after the
599    /// [frozen](Self::frozen_columns) ones. With a column window only the
600    /// columns it shows are fetched, sampled and laid out; the position
601    /// line adds `columns 41–60 of 2,000` when some are left out.
602    pub fn column_offset(mut self, offset: usize) -> Self {
603        self.window.offset = offset;
604        self
605    }
606
607    /// Show `count` columns (at least 1) from the
608    /// [column offset](Self::column_offset), besides the frozen ones. With
609    /// [`fit_columns`](Self::fit_columns), at most `count`.
610    pub fn visible_columns(mut self, count: usize) -> Self {
611        self.window.count = Some(count.max(1));
612        self
613    }
614
615    /// Show as many columns from the [column offset](Self::column_offset)
616    /// as fit the width, besides the frozen ones, and always one (default
617    /// off). Columns are measured by their widths (fixed, or sampled),
618    /// sampled a few at a time, so a fit samples at most one column past
619    /// those it shows.
620    pub fn fit_columns(mut self, fit: bool) -> Self {
621        self.window.fit = fit;
622        self
623    }
624
625    /// Keep the first `count` columns on the left, whatever the column
626    /// offset (an id column, a name): they are always shown and never
627    /// scrolled.
628    pub fn frozen_columns(mut self, count: usize) -> Self {
629        self.window.frozen = count;
630        self
631    }
632
633    /// Move the column window to column `offset` (after the frozen ones).
634    pub fn set_column_offset(&mut self, offset: usize) {
635        self.window.offset = offset;
636    }
637
638    /// Show `count` columns from the offset, or `None` for all of them (or
639    /// as many as [fit](Self::fit_columns)).
640    pub fn set_visible_columns(&mut self, count: Option<usize>) {
641        self.window.count = count.map(|c| c.max(1));
642    }
643
644    /// Move the column window by `columns` (negative is left), stopping at
645    /// the first column and at the [last offset](Self::max_column_offset).
646    pub fn scroll_columns_by(&mut self, columns: isize) {
647        let offset = self.window.offset.saturating_add_signed(columns);
648        self.window.offset = offset.min(self.max_column_offset());
649    }
650
651    /// The column window's first column, after the frozen ones, as set.
652    pub fn window_column_offset(&self) -> usize {
653        self.window.offset
654    }
655
656    /// The largest column offset: the one whose window of
657    /// [`visible_columns`](Self::visible_columns) ends at the last column,
658    /// or that shows the last column alone without a count.
659    pub fn max_column_offset(&self) -> usize {
660        let scrollable = self.columns.len().saturating_sub(self.window.frozen);
661        scrollable.saturating_sub(self.window.count.unwrap_or(1))
662    }
663
664    /// The 0-based indices of the columns a render `width` cells wide
665    /// shows: the frozen ones, then the window's. Every column without a
666    /// column window.
667    pub fn shown_columns(&self, width: usize) -> Vec<usize> {
668        let n = self.columns.len();
669        if !self.window.on() {
670            return (0..n).collect();
671        }
672        let frozen = self.window.frozen.min(n);
673        let scrollable = n - frozen;
674        let start = frozen + self.window.offset.min(scrollable.saturating_sub(1));
675        let end = match self.window.count {
676            Some(count) => start.saturating_add(count).min(n),
677            None => n,
678        };
679        let mut shown: Vec<usize> = (0..frozen).collect();
680        if !self.window.fit || start >= end {
681            shown.extend(start..end);
682            return shown;
683        }
684        // Fit: add columns while the table fits, sampling a batch at a time
685        // that surely fits at the cap, and at least one.
686        let mut widths = self.widths_of(&shown);
687        if self.row_numbers {
688            let last = self
689                .row_count()
690                .unwrap_or(self.offset.saturating_add(self.height));
691            widths.insert(0, last.max(1).to_string().len());
692        }
693        let mut next = start;
694        'fit: while next < end {
695            let room = width.saturating_sub(self.table_width(&widths));
696            let batch = (room / (self.max_width + 3)).max(1);
697            let candidates: Vec<usize> = (next..end.min(next + batch)).collect();
698            for (index, w) in candidates.iter().zip(self.widths_of(&candidates)) {
699                widths.push(w);
700                if shown.len() > frozen && self.table_width(&widths) > width {
701                    break 'fit;
702                }
703                shown.push(*index);
704            }
705            next += candidates.len();
706        }
707        shown
708    }
709
710    /// The cells a table of columns `widths` wide takes, its borders and
711    /// padding included.
712    fn table_width(&self, widths: &[usize]) -> usize {
713        let boxed = self.frame.box_set.is_some();
714        let edges = if boxed && self.frame.show_edge { 2 } else { 0 };
715        let dividers = if boxed {
716            widths.len().saturating_sub(1)
717        } else {
718            0
719        };
720        widths.iter().map(|w| w + 2).sum::<usize>() + dividers + edges
721    }
722
723    /// Move the window to row `offset` (0-based).
724    pub fn set_offset(&mut self, offset: usize) {
725        self.offset = offset;
726    }
727
728    /// Change how many rows the window shows (at most [`MAX_HEIGHT`]).
729    pub fn set_height(&mut self, rows: usize) {
730        self.height = rows.min(MAX_HEIGHT);
731    }
732
733    /// Move the window by `rows` (negative is up), stopping at the top and,
734    /// when the source knows its length, at the last full window.
735    pub fn scroll_by(&mut self, rows: isize) {
736        let offset = self.offset.saturating_add_signed(rows);
737        self.offset = match self.max_offset() {
738            Some(max) => offset.min(max),
739            None => offset,
740        };
741    }
742
743    /// The window's first row (0-based), as set.
744    pub fn window_offset(&self) -> usize {
745        self.offset
746    }
747
748    /// How many rows the window shows.
749    pub fn window_height(&self) -> usize {
750        self.height
751    }
752
753    /// The source's row count, when it knows.
754    pub fn row_count(&self) -> Option<usize> {
755        self.source.row_count()
756    }
757
758    /// The offset of the last full window, when the source knows its length.
759    pub fn max_offset(&self) -> Option<usize> {
760        self.row_count()
761            .map(|count| count.saturating_sub(self.height))
762    }
763
764    /// The columns.
765    pub fn columns(&self) -> &[Column] {
766        &self.columns
767    }
768
769    /// The source.
770    pub fn source(&self) -> &S {
771        &self.source
772    }
773
774    /// The source, to change. Call [`resample`](Self::resample) if the first
775    /// rows changed.
776    pub fn source_mut(&mut self) -> &mut S {
777        &mut self.source
778    }
779
780    /// Forget the sampled widths, so the next render samples again.
781    pub fn resample(&self) {
782        self.sampled
783            .lock()
784            .unwrap_or_else(PoisonError::into_inner)
785            .clear();
786    }
787
788    /// The rows the window shows, fetched from the source. An offset past
789    /// the end of a source that knows its length moves back to the last full
790    /// window; one past the end of a source that does not shows no rows.
791    /// A window that [fits its columns](Self::fit_columns) to the width is
792    /// laid out for 80 cells; [`page_at`](Self::page_at) takes the width.
793    pub fn page(&self) -> Page {
794        self.page_at(80)
795    }
796
797    /// The rows the window shows, with the columns a render `width` cells
798    /// wide shows ([`shown_columns`](Self::shown_columns)): only those
799    /// cells are fetched.
800    pub fn page_at(&self, width: usize) -> Page {
801        let columns = self.shown_columns(width);
802        let count = self.source.row_count();
803        let start = match count {
804            Some(count) => self.offset.min(count.saturating_sub(self.height)),
805            None => self.offset,
806        };
807        // One extra row says whether more follow when the source cannot count.
808        let fetch = if count.is_some() {
809            self.height
810        } else {
811            self.height + 1
812        };
813        let mut rows = if self.window.on() {
814            self.source.rows_columns(start, fetch, &columns)
815        } else {
816            self.source.rows(start, fetch)
817        };
818        rows.truncate(fetch);
819        let more = match count {
820            Some(count) => start + rows.len().min(self.height) < count,
821            None => rows.len() > self.height,
822        };
823        rows.truncate(self.height);
824        let width = columns.len();
825        let rows: Vec<Vec<Value>> = rows.into_iter().map(|row| normalize(row, width)).collect();
826        let total = count.or_else(|| (!more && !rows.is_empty()).then(|| start + rows.len()));
827        Page {
828            start,
829            rows,
830            total,
831            more,
832            frozen: if self.window.on() {
833                self.window.frozen.min(self.columns.len())
834            } else {
835                0
836            },
837            column_total: self.columns.len(),
838            columns,
839        }
840    }
841
842    /// Each column's width in cells: fixed, or sampled from the header and
843    /// the first rows (and cached until [`resample`](Self::resample)). This
844    /// samples every column; a column window samples only those it shows.
845    pub fn column_widths(&self) -> Vec<usize> {
846        let all: Vec<usize> = (0..self.columns.len()).collect();
847        self.widths_of(&all)
848    }
849
850    /// Column `index`'s fixed width, if it has one.
851    fn fixed_width(&self, index: usize) -> Option<usize> {
852        self.widths
853            .get(index)
854            .copied()
855            .flatten()
856            .or(self.columns[index].column_options().width)
857    }
858
859    /// The widths of `indices`' columns, sampling those not sampled yet in
860    /// one fetch of their cells.
861    fn widths_of(&self, indices: &[usize]) -> Vec<usize> {
862        let mut sampled = self.sampled.lock().unwrap_or_else(PoisonError::into_inner);
863        if sampled.len() != self.columns.len() {
864            *sampled = vec![None; self.columns.len()];
865        }
866        let missing: Vec<usize> = indices
867            .iter()
868            .copied()
869            .filter(|&i| sampled[i].is_none() && self.fixed_width(i).is_none())
870            .collect();
871        if !missing.is_empty() {
872            let mut widths: Vec<usize> = missing
873                .iter()
874                .map(|&i| Text::new(self.columns[i].header()).measurement().1)
875                .collect();
876            let rows = if missing.len() == self.columns.len() {
877                // Every column: the rows as the source gives them.
878                self.source.rows(0, self.sample)
879            } else {
880                self.source.rows_columns(0, self.sample, &missing)
881            };
882            for row in rows.into_iter().take(self.sample) {
883                // Pad short rows as `page` does, so a missing cell is
884                // sampled as the null marker it will show.
885                let row = normalize(row, missing.len());
886                for ((width, &index), value) in widths.iter_mut().zip(&missing).zip(&row) {
887                    let cell = self.cell(&self.columns[index], value).measurement().1;
888                    *width = (*width).max(cell);
889                }
890            }
891            for (width, &index) in widths.into_iter().zip(&missing) {
892                sampled[index] = Some(width.clamp(1, self.max_width));
893            }
894        }
895        indices
896            .iter()
897            .map(|&i| self.fixed_width(i).or(sampled[i]).unwrap_or(self.max_width))
898            .collect()
899    }
900
901    /// How many lines the table takes around its rows at `width` cells: the
902    /// title, edges, header, caption, position line and footnote. A viewport
903    /// of `lines` lines holds `lines - chrome_lines` rows when cells do not
904    /// wrap.
905    pub fn chrome_lines(&self, console: &Console, width: usize) -> usize {
906        let options = console.options().update_width(width.max(1));
907        let columns = self.shown_columns(width);
908        let empty = Page {
909            start: 0,
910            rows: Vec::new(),
911            total: None,
912            more: false,
913            frozen: 0,
914            column_total: self.columns.len(),
915            columns,
916        };
917        let table = self.build(console, &empty);
918        table_lines(&table, console, &options).len()
919            + usize::from(self.position)
920            + usize::from(self.footnote.is_some())
921    }
922
923    /// The window as a core table (without the position line and footnote),
924    /// its columns as many as the console's width shows.
925    pub fn to_table(&self, console: &Console) -> Table {
926        self.build(console, &self.page_at(console.width()))
927    }
928
929    fn cell(&self, column: &Column, value: &Value) -> Text {
930        match (&self.null, value) {
931            (Some(marker), Value::Null) => Text::new(marker.clone()),
932            _ => column.cell(value),
933        }
934    }
935
936    fn build(&self, console: &Console, page: &Page) -> Table {
937        let mut widths = self.widths_of(&page.columns);
938        if self.fit_window {
939            for (k, (&index, width)) in page.columns.iter().zip(widths.iter_mut()).enumerate() {
940                let column = &self.columns[index];
941                if self.fixed_width(index).is_some() {
942                    continue;
943                }
944                for row in &page.rows {
945                    let cell = self.cell(column, &row[k]).measurement().1;
946                    *width = (*width).max(cell.min(self.max_width));
947                }
948            }
949        }
950        let mut columns: Vec<Column> = Vec::with_capacity(page.columns.len() + 1);
951        if self.row_numbers {
952            let last = page
953                .total
954                .unwrap_or(page.start + page.rows.len())
955                .max(page.start + page.rows.len());
956            let options = rich::ColumnOptions {
957                justify: Justify::Right,
958                width: Some(last.max(1).to_string().len()),
959                no_wrap: true,
960                style: style(console, "table.row_number"),
961                ..Default::default()
962            };
963            columns.push(Column::new("#").options(options));
964        }
965        for (&index, width) in page.columns.iter().zip(&widths) {
966            let column = &self.columns[index];
967            let mut options = column.column_options().clone();
968            options.width = Some(*width);
969            if !self.wrap {
970                options.no_wrap = true;
971                if options.overflow == Overflow::Fold {
972                    options.overflow = Overflow::Ellipsis;
973                }
974            }
975            columns.push(Column::new(column.header()).options(options));
976        }
977        let headers: Vec<Text> = columns.iter().map(|c| Text::new(c.header())).collect();
978        let mut table = self.frame.table(&columns, &headers, true, true);
979        let null_style = style(console, "table.null");
980        for (index, row) in page.rows.iter().enumerate() {
981            let mut cells = Vec::with_capacity(columns.len());
982            if self.row_numbers {
983                cells.push(Text::new((page.start + index + 1).to_string()));
984            }
985            for (&index, value) in page.columns.iter().zip(row) {
986                let mut text = self.cell(&self.columns[index], value);
987                if self.null.is_some() && matches!(value, Value::Null) {
988                    let len = text.plain().len();
989                    text.stylize(null_style.clone(), 0, len);
990                }
991                cells.push(text);
992            }
993            table.add_row_text(cells);
994        }
995        table
996    }
997
998    fn note_lines(
999        &self,
1000        console: &Console,
1001        options: &ConsoleOptions,
1002        text: String,
1003    ) -> Vec<Vec<Segment>> {
1004        let text = Text::styled(text, style(console, "table.position"))
1005            .no_wrap(true)
1006            .overflow(Overflow::Ellipsis);
1007        let mut options = options.clone();
1008        options.height = None;
1009        console.render_lines(&text, &options, false)
1010    }
1011
1012    /// The position line: the rows, and the columns when some are left
1013    /// out.
1014    fn position_text(page: &Page, ascii: bool) -> String {
1015        let rows = page.position(ascii);
1016        match page.column_position(ascii) {
1017            Some(columns) => format!("{rows}, {columns}"),
1018            None => rows,
1019        }
1020    }
1021
1022    fn render_lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Vec<Segment>> {
1023        let page = self.page_at(options.max_width);
1024        let table = self.build(console, &page);
1025        let mut lines = table_lines(&table, console, options);
1026        if self.position {
1027            let text = Self::position_text(&page, console.ascii_only());
1028            lines.extend(self.note_lines(console, options, text));
1029        }
1030        if let Some(note) = &self.footnote {
1031            lines.extend(self.note_lines(console, options, note.clone()));
1032        }
1033        lines
1034    }
1035}
1036
1037impl<S: VirtualRows> Renderable for VirtualTable<S> {
1038    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1039        crate::event::flatten(self.render_lines(console, options))
1040    }
1041
1042    /// The table's measurement, widened to the position line and footnote:
1043    /// every line `rich_render` emits.
1044    fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
1045        let page = self.page_at(options.max_width);
1046        let mut measurement = self.build(console, &page).measure(console, options);
1047        let notes = self
1048            .position
1049            .then(|| Self::position_text(&page, console.ascii_only()))
1050            .into_iter()
1051            .chain(self.footnote.clone());
1052        for note in notes {
1053            measurement.maximum = measurement.maximum.max(Text::new(note).cell_len());
1054        }
1055        measurement
1056    }
1057}
1058
1059impl<S: VirtualRows> crate::a11y::AccessibleText for VirtualTable<S> {
1060    fn accessible_text(&self, width: usize) -> String {
1061        let console = Console::builder().width(width.max(1)).build();
1062        self.to_table(&console).accessible_text(width)
1063    }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068    use super::*;
1069
1070    fn render(table: &impl Renderable, width: usize) -> String {
1071        Console::builder()
1072            .width(width)
1073            .color_system(None)
1074            .build()
1075            .render_export(table)
1076    }
1077
1078    fn numbered(count: Option<usize>) -> FnRows<impl Fn(usize) -> Option<Vec<Value>>> {
1079        FnRows::new(count, |i| {
1080            Some(vec![Value::from(i), format!("r{i}").into()])
1081        })
1082    }
1083
1084    #[test]
1085    fn an_offset_past_the_end_shows_the_last_window() {
1086        let rows: Vec<Vec<Value>> = (0..10).map(|i| vec![Value::from(i)]).collect();
1087        let table = VirtualTable::new([Column::new("n")], rows)
1088            .offset(50)
1089            .height(3);
1090        let page = table.page();
1091        assert_eq!(page.range(), 7..10);
1092        assert_eq!(page.total, Some(10));
1093        assert!(!page.more);
1094        assert_eq!(page.position(false), "rows 8–10 of 10");
1095    }
1096
1097    #[test]
1098    fn an_uncounted_source_reports_what_it_knows() {
1099        let rows = FnRows::new(None, |i| (i < 5).then(|| vec![Value::from(i)]));
1100        let table = VirtualTable::new([Column::new("n")], rows).height(2);
1101        assert_eq!(table.page().position(false), "rows 1–2 of 3+");
1102        let mut table = table;
1103        table.set_offset(3);
1104        let page = table.page();
1105        assert_eq!((page.total, page.more), (Some(5), false));
1106        assert_eq!(page.position(true), "rows 4-5 of 5");
1107        table.set_offset(9);
1108        assert_eq!(table.page().position(false), "no rows from 10");
1109    }
1110
1111    #[test]
1112    fn ragged_rows_are_padded_and_cut() {
1113        let rows = vec![
1114            vec![Value::from("a")],
1115            vec![Value::from("b"), Value::from(2), Value::from("extra")],
1116            vec![],
1117        ];
1118        let table = VirtualTable::new([Column::new("x"), Column::new("y")], rows);
1119        let page = table.page();
1120        assert!(page.rows.iter().all(|row| row.len() == 2));
1121        let out = render(&table, 30);
1122        assert!(out.contains("│ b │ 2 │"), "{out}");
1123    }
1124
1125    #[test]
1126    fn widths_hold_still_while_scrolling() {
1127        let mut table = VirtualTable::new([Column::new("n"), Column::new("s")], numbered(None))
1128            .sample(10)
1129            .height(2);
1130        let first = render(&table, 40);
1131        table.scroll_by(500);
1132        let later = render(&table, 40);
1133        let width = |out: &str| out.lines().next().unwrap().chars().count();
1134        assert_eq!(width(&first), width(&later));
1135        // `n` was sampled from 0..10: one cell wide, so `500` is cut short.
1136        assert!(later.contains("│ … │ r… │"), "{later}");
1137        assert_eq!(table.column_widths(), [1, 2]);
1138    }
1139
1140    #[test]
1141    fn fixed_widths_win_and_the_cap_bounds_samples() {
1142        let long = vec![vec![Value::from("x".repeat(500)), Value::from("y")]];
1143        let table = VirtualTable::new([Column::new("a"), Column::new("b")], long.clone())
1144            .max_column_width(8);
1145        assert_eq!(table.column_widths(), [8, 1]);
1146        let table =
1147            VirtualTable::new([Column::new("a"), Column::new("b")], long).widths([None, Some(5)]);
1148        assert_eq!(table.column_widths(), [40, 5]);
1149    }
1150
1151    #[test]
1152    fn scrolling_stops_at_both_ends() {
1153        let mut table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(10);
1154        table.scroll_by(-5);
1155        assert_eq!(table.window_offset(), 0);
1156        table.scroll_by(1_000);
1157        assert_eq!(table.window_offset(), 90);
1158        assert_eq!(table.max_offset(), Some(90));
1159    }
1160
1161    #[test]
1162    fn nulls_can_be_marked_apart_from_empty_text() {
1163        let rows = vec![vec![Value::Null], vec![Value::from("")]];
1164        let table = VirtualTable::new([Column::new("v")], rows)
1165            .null_marker("NULL")
1166            .show_position(false);
1167        assert_eq!(
1168            render(&table, 20),
1169            "┏━━━━━━┓\n┃ v    ┃\n┡━━━━━━┩\n│ NULL │\n│      │\n└──────┘\n"
1170        );
1171    }
1172
1173    #[test]
1174    fn row_numbers_and_footnote() {
1175        let table = VirtualTable::new([Column::new("s")], numbered(Some(1_000)))
1176            .offset(998)
1177            .height(2)
1178            .row_numbers(true)
1179            .widths([3])
1180            .footnote("(1,000 rows)");
1181        let out = render(&table, 40);
1182        assert!(out.contains("│  999 │ 998 │"), "{out}");
1183        assert!(
1184            out.ends_with("rows 999–1,000 of 1,000\n(1,000 rows)\n"),
1185            "{out}"
1186        );
1187    }
1188
1189    #[test]
1190    fn chrome_lines_count_everything_but_rows() {
1191        let table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(5);
1192        let console = Console::builder().width(40).build();
1193        let out = render(&table, 40);
1194        assert_eq!(out.lines().count(), 5 + table.chrome_lines(&console, 40));
1195    }
1196
1197    #[test]
1198    fn an_empty_source_renders_a_header_and_no_rows() {
1199        let table = VirtualTable::new([Column::new("n")], Vec::<Vec<Value>>::new());
1200        let out = render(&table, 20);
1201        assert!(out.ends_with("no rows\n"), "{out}");
1202        let table = VirtualTable::new(Vec::<Column>::new(), numbered(Some(3)));
1203        render(&table, 20);
1204        // A column window over no columns shows none.
1205        let table = VirtualTable::new(Vec::<Column>::new(), numbered(Some(3))).fit_columns(true);
1206        assert_eq!(table.shown_columns(20), Vec::<usize>::new());
1207        render(&table, 20);
1208    }
1209
1210    /// `rows` rows of `columns` cells `r{row}c{column}`, counting the cells
1211    /// fetched and noting their columns.
1212    struct Counting {
1213        rows: usize,
1214        columns: usize,
1215        cells: std::sync::atomic::AtomicUsize,
1216        seen: Mutex<std::collections::BTreeSet<usize>>,
1217    }
1218
1219    impl Counting {
1220        fn new(rows: usize, columns: usize) -> Self {
1221            Counting {
1222                rows,
1223                columns,
1224                cells: Default::default(),
1225                seen: Default::default(),
1226            }
1227        }
1228
1229        fn seen(&self) -> Vec<usize> {
1230            self.seen.lock().unwrap().iter().copied().collect()
1231        }
1232
1233        fn cells(&self) -> usize {
1234            self.cells.load(std::sync::atomic::Ordering::Relaxed)
1235        }
1236    }
1237
1238    impl VirtualRows for Counting {
1239        fn row_count(&self) -> Option<usize> {
1240            Some(self.rows)
1241        }
1242
1243        fn row(&self, index: usize) -> Option<Vec<Value>> {
1244            let all: Vec<usize> = (0..self.columns).collect();
1245            self.rows_columns(index, 1, &all).pop()
1246        }
1247
1248        fn rows_columns(&self, start: usize, len: usize, columns: &[usize]) -> Vec<Vec<Value>> {
1249            let end = start.saturating_add(len).min(self.rows);
1250            let rows: Vec<Vec<Value>> = (start.min(end)..end)
1251                .map(|r| {
1252                    columns
1253                        .iter()
1254                        .map(|c| Value::from(format!("r{r}c{c}")))
1255                        .collect()
1256                })
1257                .collect();
1258            self.cells.fetch_add(
1259                rows.len() * columns.len(),
1260                std::sync::atomic::Ordering::Relaxed,
1261            );
1262            self.seen.lock().unwrap().extend(columns.iter().copied());
1263            rows
1264        }
1265    }
1266
1267    fn twelve() -> Vec<Column> {
1268        (0..12).map(|c| Column::new(format!("c{c}"))).collect()
1269    }
1270
1271    #[test]
1272    fn a_column_window_fetches_only_its_columns() {
1273        let source = Counting::new(5, 12);
1274        let table = VirtualTable::new(twelve(), &source)
1275            .height(2)
1276            .column_offset(3)
1277            .visible_columns(2);
1278        assert_eq!(
1279            render(&table, 60),
1280            "\
1281┏━━━━━━┳━━━━━━┓
1282┃ c3   ┃ c4   ┃
1283┡━━━━━━╇━━━━━━┩
1284│ r0c3 │ r0c4 │
1285│ r1c3 │ r1c4 │
1286└──────┴──────┘
1287rows 1–2 of 5, columns 4–5 of 12
1288"
1289        );
1290        assert_eq!(source.seen(), vec![3, 4]);
1291        // The width sample (5 rows) and the window (2 rows), two columns.
1292        assert_eq!(source.cells(), 5 * 2 + 2 * 2);
1293        let page = table.page();
1294        assert_eq!(
1295            (page.columns.clone(), page.frozen, page.column_total),
1296            (vec![3, 4], 0, 12)
1297        );
1298        assert_eq!(page.column_range(), 3..5);
1299    }
1300
1301    #[test]
1302    fn frozen_columns_stay_on_the_left() {
1303        let source = Counting::new(5, 12);
1304        let mut table = VirtualTable::new(twelve(), &source)
1305            .height(1)
1306            .frozen_columns(1)
1307            .column_offset(5)
1308            .visible_columns(2);
1309        assert_eq!(
1310            render(&table, 60),
1311            "\
1312┏━━━━━━┳━━━━━━┳━━━━━━┓
1313┃ c0   ┃ c6   ┃ c7   ┃
1314┡━━━━━━╇━━━━━━╇━━━━━━┩
1315│ r0c0 │ r0c6 │ r0c7 │
1316└──────┴──────┴──────┘
1317rows 1–1 of 5, columns 7–8 of 12
1318"
1319        );
1320        assert_eq!(source.seen(), vec![0, 6, 7]);
1321        // Scrolling stops at the first column and at the last full window.
1322        table.scroll_columns_by(-100);
1323        assert_eq!(table.window_column_offset(), 0);
1324        table.scroll_columns_by(100);
1325        assert_eq!(table.max_column_offset(), 9);
1326        assert_eq!(table.window_column_offset(), 9);
1327        assert_eq!(table.shown_columns(60), vec![0, 10, 11]);
1328        // One column: `column`, singular.
1329        table.set_visible_columns(Some(1));
1330        table.set_column_offset(50);
1331        assert_eq!(table.shown_columns(60), vec![0, 11]);
1332        assert_eq!(
1333            table.page().column_position(true).as_deref(),
1334            Some("column 12 of 12")
1335        );
1336    }
1337
1338    #[test]
1339    fn fit_to_width_shows_what_fits_from_the_offset() {
1340        let source = Counting::new(5, 12);
1341        let table = VirtualTable::new(twelve(), &source)
1342            .height(1)
1343            .frozen_columns(1)
1344            .column_offset(2)
1345            .fit_columns(true);
1346        // Each column is 4 cells, 7 with its padding and border: 30 cells
1347        // hold four.
1348        assert_eq!(table.shown_columns(30), vec![0, 3, 4, 5]);
1349        assert_eq!(
1350            render(&table, 34),
1351            "\
1352┏━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┓
1353┃ c0   ┃ c3   ┃ c4   ┃ c5   ┃
1354┡━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━┩
1355│ r0c0 │ r0c3 │ r0c4 │ r0c5 │
1356└──────┴──────┴──────┴──────┘
1357rows 1–1 of 5, columns 4–6 of 12
1358"
1359        );
1360        // Sampling looked one column past the window, no further.
1361        assert_eq!(source.seen(), vec![0, 3, 4, 5, 6]);
1362        // Narrower than one column: still one, past the frozen ones.
1363        assert_eq!(table.shown_columns(5), vec![0, 3]);
1364        // Wide enough for all: no column note.
1365        let all = VirtualTable::new(twelve(), &source)
1366            .height(1)
1367            .fit_columns(true);
1368        assert_eq!(all.shown_columns(200).len(), 12);
1369        assert_eq!(all.page_at(200).column_position(false), None);
1370        // A count caps the fit.
1371        let capped = VirtualTable::new(twelve(), &source)
1372            .fit_columns(true)
1373            .visible_columns(2);
1374        assert_eq!(capped.shown_columns(200), vec![0, 1]);
1375    }
1376
1377    #[test]
1378    fn a_window_reads_its_columns_aloud() {
1379        use crate::a11y::AccessibleText;
1380        let table = VirtualTable::new(twelve(), Counting::new(3, 12))
1381            .height(1)
1382            .column_offset(10);
1383        assert_eq!(
1384            table.accessible_text(80),
1385            "Table with 1 row, columns: c10, c11\nRow 1: c10: r0c10; c11: r0c11"
1386        );
1387    }
1388
1389    #[test]
1390    fn the_guides_wide_table() {
1391        let cells = FnCells::new(100_000, 2_000, |r, c| Value::from(r * c));
1392        let columns = (0..2_000).map(|c| Column::new(format!("c{c}")));
1393        let table = VirtualTable::new(columns, cells)
1394            .height(2)
1395            .frozen_columns(1)
1396            .column_offset(40)
1397            .fit_columns(true);
1398        assert_eq!(
1399            render(&table, 80),
1400            "\
1401┏━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┳━━━━━━┓
1402┃ c0 ┃ c41  ┃ c42  ┃ c43  ┃ c44  ┃ c45  ┃ c46  ┃ c47  ┃ c48  ┃ c49  ┃ c50  ┃
1403┡━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━╇━━━━━━┩
1404│ 0  │ 0    │ 0    │ 0    │ 0    │ 0    │ 0    │ 0    │ 0    │ 0    │ 0    │
1405│ 0  │ 41   │ 42   │ 43   │ 44   │ 45   │ 46   │ 47   │ 48   │ 49   │ 50   │
1406└────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
1407rows 1–2 of 100,000, columns 42–51 of 2,000
1408"
1409        );
1410    }
1411
1412    #[test]
1413    fn fn_cells_compute_only_what_is_asked() {
1414        let calls = std::sync::atomic::AtomicUsize::new(0);
1415        let grid = FnCells::new(100_000, 2_000, |r, c| {
1416            calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1417            Value::from(r + c)
1418        });
1419        let table = VirtualTable::new((0..2_000).map(|c| Column::new(format!("{c}"))), &grid)
1420            .offset(50_000)
1421            .height(3)
1422            .column_offset(1_000)
1423            .visible_columns(3)
1424            .sample(10);
1425        let out = render(&table, 60);
1426        assert!(
1427            out.ends_with("rows 50,001–50,003 of 100,000, columns 1,001–1,003 of 2,000\n"),
1428            "{out}"
1429        );
1430        // Ten sample rows and three window rows, three columns each.
1431        assert_eq!(
1432            calls.load(std::sync::atomic::Ordering::Relaxed),
1433            (10 + 3) * 3
1434        );
1435        assert_eq!(grid.row(3).map(|r| r.len()), Some(2_000));
1436        assert_eq!(grid.rows_columns(99_999, 5, &[0, 5_000]).len(), 1);
1437        assert_eq!(grid.rows_columns(0, 1, &[5_000]), vec![vec![Value::Null]]);
1438    }
1439}