Skip to main content

datui_lib/widgets/
column_widths.rs

1//! The width each column of the main table is drawn at, kept by column identity so
2//! the layout holds still while the view pages, scrolls, reorders and resizes.
3//!
4//! An automatic width is learned from the first page a column is drawn on, from
5//! values the renderer formats anyway: nothing is read for it. Text keeps that width
6//! on later pages, bounded by [`text_cap`], and a longer value is clipped behind the
7//! marker. A number, date or flag can't be clipped without reading as another value,
8//! so its width only grows, to the widest value seen. Widths set by hand in the
9//! Columns sidebar outrank both.
10//!
11//! A deliberate change to what the view shows (a query, reshape, drill, sort or
12//! filter) learns every automatic width again, from the first rows the new view
13//! reads. Paging and scrolling never do.
14
15use polars::prelude::DataType;
16use std::collections::HashMap;
17
18/// How a column's width is chosen.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum WidthChoice {
21    /// Learned from the rows on screen, text and headings bounded by the cap.
22    #[default]
23    Auto,
24    /// Set by hand. Text and headings get exactly this many cells; a number column
25    /// is never narrower than its widest value on screen.
26    Manual(u16),
27    /// Fit to the rows on screen the next time the table is drawn, which makes it
28    /// [`WidthChoice::Manual`].
29    Fit,
30}
31
32/// The narrowest and widest a width set by hand may be.
33pub const MIN_WIDTH: u16 = 4;
34pub const MAX_WIDTH: u16 = 240;
35/// Cells one narrower or wider press moves a width.
36pub const WIDTH_STEP: u16 = 4;
37/// Where narrower or wider starts on a column that has not been drawn yet.
38pub const UNSEEN_WIDTH: u16 = 12;
39
40impl WidthChoice {
41    /// One step narrower, from the width set by hand or else from the width drawn.
42    pub fn narrower(self, shown: Option<u16>) -> Self {
43        Self::Manual(
44            self.base(shown)
45                .saturating_sub(WIDTH_STEP)
46                .clamp(MIN_WIDTH, MAX_WIDTH),
47        )
48    }
49
50    /// One step wider, from the width set by hand or else from the width drawn.
51    pub fn wider(self, shown: Option<u16>) -> Self {
52        Self::Manual(
53            self.base(shown)
54                .saturating_add(WIDTH_STEP)
55                .clamp(MIN_WIDTH, MAX_WIDTH),
56        )
57    }
58
59    fn base(self, shown: Option<u16>) -> u16 {
60        match self {
61            Self::Manual(width) => width,
62            Self::Auto | Self::Fit => shown.unwrap_or(UNSEEN_WIDTH),
63        }
64    }
65}
66
67/// The most cells an automatic width gives text or a heading: two fifths of the
68/// terminal (32 at 80 columns, 48 at 120), between 16 and 64. Taken from the
69/// terminal rather than the table, so opening a sidebar moves no column; a resize
70/// moves them, as it should.
71pub fn text_cap(screen_width: u16) -> u16 {
72    let fifths = u32::from(screen_width) * 2 / 5;
73    u16::try_from(fifths).unwrap_or(u16::MAX).clamp(16, 64)
74}
75
76/// One column measured on the rows on screen, in cells, as the table draws it.
77#[derive(Debug, Clone, Copy, Default)]
78pub struct PageMeasure {
79    /// The name and its marks.
80    pub header: u16,
81    /// The type row's label, or 0 without the type row.
82    pub type_label: u16,
83    /// The widest value or null glyph.
84    pub values: u16,
85    /// Whether any row on screen holds a value rather than a null.
86    pub has_values: bool,
87    /// Whether a value may be drawn clipped: text, not a number.
88    pub clips: bool,
89}
90
91impl PageMeasure {
92    /// The width that shows this page whole, heading bounded by `cap`.
93    fn fitted(&self, cap: u16) -> u16 {
94        self.values
95            .max(self.type_label)
96            .max(self.header.min(cap))
97            .clamp(1, MAX_WIDTH)
98    }
99}
100
101#[derive(Debug, Clone)]
102struct Entry {
103    dtype: DataType,
104    /// The values' width: for text, of the first page with a value in it; for the
105    /// rest, the widest page so far.
106    learned: u16,
107    /// Whether text has learned from a page with a value, rather than only nulls.
108    settled: bool,
109    choice: WidthChoice,
110    /// The width the column was last drawn at.
111    shown: Option<u16>,
112    /// The width it was last drawn at with the room to the table's right edge, as
113    /// the last column drawn: what it showed, though not what it is planned with.
114    filled: Option<u16>,
115    /// Whether `shown` was drawn since automatic widths were last relearned, so it
116    /// is the width the column draws at now rather than in the view before.
117    current: bool,
118}
119
120/// Display widths by column identity: a name and its type. A column whose type
121/// changes, as a query or reading it as text can do, is a new column and starts
122/// afresh; one that comes back with its type keeps its width. Separate from the
123/// footer's byte estimate, which plans the buffer and says nothing about cells.
124#[derive(Debug, Clone, Default)]
125pub struct ColumnWidths {
126    by_name: HashMap<String, Vec<Entry>>,
127    /// Automatic widths are to be learned again from the next rows read. Waits for
128    /// them: until they arrive the old rows are still drawn, and they would teach
129    /// the new view their widths.
130    relearn: bool,
131}
132
133impl ColumnWidths {
134    fn entry(&self, name: &str, dtype: &DataType) -> Option<&Entry> {
135        self.by_name.get(name)?.iter().find(|e| &e.dtype == dtype)
136    }
137
138    fn entry_mut(&mut self, name: &str, dtype: &DataType) -> &mut Entry {
139        // Looked up before inserting: drawing calls this per column per frame, and
140        // the name is allocated only the first time.
141        if !self.by_name.contains_key(name) {
142            self.by_name.insert(name.to_string(), Vec::new());
143        }
144        let entries = self.by_name.get_mut(name).expect("inserted above");
145        let at = match entries.iter().position(|e| &e.dtype == dtype) {
146            Some(at) => at,
147            None => {
148                entries.push(Entry {
149                    dtype: dtype.clone(),
150                    learned: 0,
151                    settled: false,
152                    choice: WidthChoice::Auto,
153                    shown: None,
154                    filled: None,
155                    current: false,
156                });
157                entries.len() - 1
158            }
159        };
160        &mut entries[at]
161    }
162
163    /// The width to draw the column at on this page, learning from the page as
164    /// automatic widths do. `cap` bounds automatic text and headings.
165    pub fn width(&mut self, name: &str, dtype: &DataType, page: PageMeasure, cap: u16) -> u16 {
166        let entry = self.entry_mut(name, dtype);
167        if page.clips {
168            if !entry.settled {
169                entry.learned = entry.learned.max(page.values);
170                entry.settled = page.has_values;
171            }
172        } else {
173            entry.learned = entry.learned.max(page.values);
174        }
175        let width = match entry.choice {
176            WidthChoice::Manual(width) if page.clips => width,
177            WidthChoice::Manual(width) => width.max(page.values),
178            WidthChoice::Auto | WidthChoice::Fit => {
179                let values = if page.clips {
180                    entry.learned.min(cap)
181                } else {
182                    entry.learned
183                };
184                values.max(page.type_label).max(page.header.min(cap))
185            }
186        };
187        entry.shown = Some(width);
188        entry.filled = None;
189        entry.current = true;
190        width
191    }
192
193    /// The column, drawn last at `width`, was widened to `filled` to reach the table's
194    /// right edge. Only an automatic width fills: one set by hand is drawn as set.
195    pub fn fill(&mut self, name: &str, dtype: &DataType, filled: u16) {
196        let entry = self.entry_mut(name, dtype);
197        entry.filled = Some(filled);
198    }
199
200    /// How the column's width is chosen.
201    pub fn choice(&self, name: &str, dtype: &DataType) -> WidthChoice {
202        self.entry(name, dtype)
203            .map_or(WidthChoice::Auto, |e| e.choice)
204    }
205
206    /// The width the column was last drawn at, if it has been.
207    pub fn shown(&self, name: &str, dtype: &DataType) -> Option<u16> {
208        self.entry(name, dtype).and_then(|e| e.shown)
209    }
210
211    /// [`Self::shown`], with the room the column filled at the right edge: what a
212    /// step narrower or wider starts from, so a wider column never draws narrower.
213    pub fn on_screen(&self, name: &str, dtype: &DataType) -> Option<u16> {
214        self.entry(name, dtype).and_then(|e| e.filled.or(e.shown))
215    }
216
217    /// The width the column was last drawn at, if it has been drawn since the
218    /// widths were last relearned: what a sideways page can be planned with.
219    pub fn drawn(&self, name: &str, dtype: &DataType) -> Option<u16> {
220        self.entry(name, dtype)
221            .filter(|e| e.current)
222            .and_then(|e| e.shown)
223    }
224
225    pub fn set_choice(&mut self, name: &str, dtype: &DataType, choice: WidthChoice) {
226        let choice = match choice {
227            WidthChoice::Manual(width) => WidthChoice::Manual(width.clamp(MIN_WIDTH, MAX_WIDTH)),
228            other => other,
229        };
230        self.entry_mut(name, dtype).choice = choice;
231    }
232
233    /// The columns waiting to be fitted to the rows on screen.
234    pub fn fits_pending(&self) -> Vec<(String, DataType)> {
235        self.by_name
236            .iter()
237            .flat_map(|(name, entries)| {
238                entries
239                    .iter()
240                    .filter(|e| e.choice == WidthChoice::Fit)
241                    .map(move |e| (name.clone(), e.dtype.clone()))
242            })
243            .collect()
244    }
245
246    /// Fit the column to `page`: its values and type whole, its heading up to `cap`.
247    pub fn fit(&mut self, name: &str, dtype: &DataType, page: PageMeasure, cap: u16) {
248        self.entry_mut(name, dtype).choice = WidthChoice::Manual(page.fitted(cap));
249    }
250
251    /// The view changed: learn every automatic width again from the next rows read
252    /// (see [`Self::rows_arrived`]). Widths set by hand are kept.
253    pub fn relearn(&mut self) {
254        self.relearn = true;
255    }
256
257    /// The change asked to relearn never showed (it failed and the view was put
258    /// back), so the widths learned for the view on screen stand.
259    pub fn keep_learned(&mut self) {
260        self.relearn = false;
261    }
262
263    /// Rows read for the view are about to replace the ones on screen. After
264    /// [`Self::relearn`], every automatic width starts again from them: text from
265    /// its first page with a value, and the rest from nothing, so they may narrow.
266    pub fn rows_arrived(&mut self) {
267        if std::mem::take(&mut self.relearn) {
268            for entry in self.by_name.values_mut().flatten() {
269                entry.learned = 0;
270                entry.settled = false;
271                entry.current = false;
272            }
273        }
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    fn text(values: u16) -> PageMeasure {
282        PageMeasure {
283            header: 4,
284            type_label: 3,
285            values,
286            has_values: true,
287            clips: true,
288        }
289    }
290
291    fn number(values: u16) -> PageMeasure {
292        PageMeasure {
293            clips: false,
294            ..text(values)
295        }
296    }
297
298    #[test]
299    fn the_cap_is_two_fifths_of_the_terminal_within_bounds() {
300        assert_eq!(text_cap(80), 32);
301        assert_eq!(text_cap(120), 48);
302        assert_eq!(text_cap(60), 24);
303        assert_eq!(text_cap(20), 16);
304        assert_eq!(text_cap(400), 64);
305    }
306
307    /// Text keeps the width of the first page it showed a value on; later pages
308    /// neither widen nor narrow it.
309    #[test]
310    fn text_keeps_its_first_page_width() {
311        let mut widths = ColumnWidths::default();
312        let s = DataType::String;
313        assert_eq!(widths.width("d", &s, text(10), 32), 10);
314        assert_eq!(widths.width("d", &s, text(200), 32), 10);
315        assert_eq!(widths.width("d", &s, text(2), 32), 10);
316    }
317
318    /// A first page of nulls teaches nothing: the first page with a value does.
319    #[test]
320    fn a_page_of_nulls_does_not_settle_text() {
321        let mut widths = ColumnWidths::default();
322        let s = DataType::String;
323        let nulls = PageMeasure {
324            values: 1,
325            has_values: false,
326            ..text(1)
327        };
328        assert_eq!(widths.width("d", &s, nulls, 32), 4);
329        assert_eq!(widths.width("d", &s, text(12), 32), 12);
330        assert_eq!(widths.width("d", &s, text(20), 32), 12);
331    }
332
333    /// Long text and long headings are bounded by the cap.
334    #[test]
335    fn automatic_text_and_headings_stop_at_the_cap() {
336        let mut widths = ColumnWidths::default();
337        let s = DataType::String;
338        assert_eq!(widths.width("d", &s, text(215), 32), 32);
339        let long_heading = PageMeasure {
340            header: 105,
341            ..number(3)
342        };
343        assert_eq!(widths.width("n", &DataType::Int64, long_heading, 32), 32);
344    }
345
346    /// A number never draws narrower than its widest value seen, and does not shrink
347    /// back on a page of narrower ones.
348    #[test]
349    fn numbers_widen_and_stay_wide() {
350        let mut widths = ColumnWidths::default();
351        let i = DataType::Int64;
352        assert_eq!(widths.width("n", &i, number(5), 32), 5);
353        assert_eq!(widths.width("n", &i, number(7), 32), 7);
354        assert_eq!(widths.width("n", &i, number(2), 32), 7);
355    }
356
357    /// A width set by hand is exact for text, and a floor for numbers.
358    #[test]
359    fn a_manual_width_is_exact_for_text_and_a_floor_for_numbers() {
360        let mut widths = ColumnWidths::default();
361        widths.set_choice("d", &DataType::String, WidthChoice::Manual(6));
362        assert_eq!(widths.width("d", &DataType::String, text(30), 32), 6);
363        widths.set_choice("n", &DataType::Int64, WidthChoice::Manual(6));
364        assert_eq!(widths.width("n", &DataType::Int64, number(3), 32), 6);
365        assert_eq!(widths.width("n", &DataType::Int64, number(9), 32), 9);
366        widths.set_choice("d", &DataType::String, WidthChoice::Manual(1));
367        assert_eq!(
368            widths.choice("d", &DataType::String),
369            WidthChoice::Manual(MIN_WIDTH)
370        );
371    }
372
373    /// The same name with another type is another column: it starts afresh, and
374    /// the first comes back with its own width.
375    #[test]
376    fn a_column_is_its_name_and_type() {
377        let mut widths = ColumnWidths::default();
378        widths.set_choice("x", &DataType::Int64, WidthChoice::Manual(20));
379        assert_eq!(widths.width("x", &DataType::String, text(5), 32), 5);
380        assert_eq!(widths.choice("x", &DataType::String), WidthChoice::Auto);
381        assert_eq!(widths.width("x", &DataType::Int64, number(3), 32), 20);
382    }
383
384    #[test]
385    fn fit_takes_the_page_and_automatic_returns_to_the_learned_width() {
386        let mut widths = ColumnWidths::default();
387        let s = DataType::String;
388        assert_eq!(widths.width("d", &s, text(10), 32), 10);
389        widths.set_choice("d", &s, WidthChoice::Fit);
390        assert_eq!(widths.fits_pending(), vec![("d".to_string(), s.clone())]);
391        widths.fit("d", &s, text(90), 32);
392        assert!(widths.fits_pending().is_empty());
393        assert_eq!(widths.choice("d", &s), WidthChoice::Manual(90));
394        assert_eq!(widths.width("d", &s, text(3), 32), 90);
395        widths.set_choice("d", &s, WidthChoice::Auto);
396        assert_eq!(widths.width("d", &s, text(3), 32), 10);
397    }
398
399    /// A relearn waits for the new view's rows: the old ones drawn meanwhile teach
400    /// nothing that lasts. Then text and numbers start again, and manual widths stay.
401    #[test]
402    fn a_relearn_starts_again_from_the_next_rows() {
403        let mut widths = ColumnWidths::default();
404        let (s, i) = (DataType::String, DataType::Int64);
405        assert_eq!(widths.width("d", &s, text(10), 32), 10);
406        assert_eq!(widths.width("n", &i, number(9), 32), 9);
407        widths.set_choice("m", &s, WidthChoice::Manual(7));
408        widths.relearn();
409        assert_eq!(widths.width("d", &s, text(20), 32), 10);
410        widths.rows_arrived();
411        assert_eq!(widths.width("d", &s, text(20), 32), 20);
412        assert_eq!(widths.width("d", &s, text(30), 32), 20);
413        assert_eq!(widths.width("n", &i, number(3), 32), 4);
414        assert_eq!(widths.width("m", &s, text(30), 32), 7);
415        // Only once: later rows are paging.
416        widths.rows_arrived();
417        assert_eq!(widths.width("d", &s, text(5), 32), 20);
418    }
419
420    /// A width drawn before a relearn is not one to plan a page with until the
421    /// column is drawn again; it is still the width the sidebar steps from.
422    #[test]
423    fn a_relearn_makes_drawn_widths_unknown_until_drawn_again() {
424        let mut widths = ColumnWidths::default();
425        let s = DataType::String;
426        assert_eq!(widths.drawn("d", &s), None);
427        assert_eq!(widths.width("d", &s, text(10), 32), 10);
428        assert_eq!(widths.drawn("d", &s), Some(10));
429        widths.relearn();
430        widths.rows_arrived();
431        assert_eq!(widths.drawn("d", &s), None);
432        assert_eq!(widths.shown("d", &s), Some(10));
433        assert_eq!(widths.width("d", &s, text(4), 32), 4);
434        assert_eq!(widths.drawn("d", &s), Some(4));
435    }
436
437    /// A relearn whose view never showed is dropped.
438    #[test]
439    fn a_relearn_kept_back_changes_nothing() {
440        let mut widths = ColumnWidths::default();
441        let s = DataType::String;
442        assert_eq!(widths.width("d", &s, text(10), 32), 10);
443        widths.relearn();
444        widths.keep_learned();
445        widths.rows_arrived();
446        assert_eq!(widths.width("d", &s, text(20), 32), 10);
447    }
448
449    #[test]
450    fn narrower_and_wider_step_from_what_is_drawn() {
451        assert_eq!(
452            WidthChoice::Auto.wider(Some(10)),
453            WidthChoice::Manual(10 + WIDTH_STEP)
454        );
455        assert_eq!(
456            WidthChoice::Auto.narrower(Some(10)),
457            WidthChoice::Manual(10 - WIDTH_STEP)
458        );
459        assert_eq!(
460            WidthChoice::Manual(MIN_WIDTH).narrower(None),
461            WidthChoice::Manual(MIN_WIDTH)
462        );
463        assert_eq!(
464            WidthChoice::Manual(MAX_WIDTH).wider(None),
465            WidthChoice::Manual(MAX_WIDTH)
466        );
467        assert_eq!(
468            WidthChoice::Fit.wider(None),
469            WidthChoice::Manual(UNSEEN_WIDTH + WIDTH_STEP)
470        );
471    }
472}