Skip to main content

datui_lib/widgets/
column_widths.rs

1//! Each main-table column's drawn width, kept by identity so the layout holds through
2//! paging, scrolling, reordering and resizing. Automatic widths are learned from the
3//! first page drawn (values already formatted, nothing read): text keeps it, bounded by
4//! [`text_cap`] and clipped beyond; numbers, dates and flags only grow (clipped they
5//! would read wrong). Sidebar-set widths win. A query, reshape, drill, sort or filter
6//! relearns automatic widths; paging and scrolling never do.
7
8use polars::prelude::DataType;
9use std::collections::HashMap;
10
11/// How a column's width is chosen.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum WidthChoice {
14    /// Learned from the rows on screen, text and headings bounded by the cap.
15    #[default]
16    Auto,
17    /// Set by hand. Text and headings get exactly this many cells; a number column
18    /// is never narrower than its widest value on screen.
19    Manual(u16),
20    /// Fit to the rows on screen the next time the table is drawn, which makes it
21    /// [`WidthChoice::Manual`].
22    Fit,
23}
24
25/// The narrowest and widest a width set by hand may be.
26pub const MIN_WIDTH: u16 = 4;
27pub const MAX_WIDTH: u16 = 240;
28/// Cells one narrower or wider press moves a width.
29pub const WIDTH_STEP: u16 = 4;
30/// Where narrower or wider starts on a column that has not been drawn yet.
31pub const UNSEEN_WIDTH: u16 = 12;
32
33impl WidthChoice {
34    /// One step narrower, from the width set by hand or else from the width drawn.
35    pub fn narrower(self, shown: Option<u16>) -> Self {
36        Self::Manual(
37            self.base(shown)
38                .saturating_sub(WIDTH_STEP)
39                .clamp(MIN_WIDTH, MAX_WIDTH),
40        )
41    }
42
43    /// One step wider, from the width set by hand or else from the width drawn.
44    pub fn wider(self, shown: Option<u16>) -> Self {
45        Self::Manual(
46            self.base(shown)
47                .saturating_add(WIDTH_STEP)
48                .clamp(MIN_WIDTH, MAX_WIDTH),
49        )
50    }
51
52    fn base(self, shown: Option<u16>) -> u16 {
53        match self {
54            Self::Manual(width) => width,
55            Self::Auto | Self::Fit => shown.unwrap_or(UNSEEN_WIDTH),
56        }
57    }
58}
59
60/// The most cells an automatic width gives text or a heading: two fifths of the
61/// terminal (32 at 80 columns), between 16 and 64. From the terminal, not the table, so
62/// a sidebar moves nothing; a resize does.
63pub fn text_cap(screen_width: u16) -> u16 {
64    let fifths = u32::from(screen_width) * 2 / 5;
65    u16::try_from(fifths).unwrap_or(u16::MAX).clamp(16, 64)
66}
67
68/// One column measured on the rows on screen, in cells, as the table draws it.
69#[derive(Debug, Clone, Copy, Default)]
70pub struct PageMeasure {
71    /// The name and its marks.
72    pub header: u16,
73    /// The type row's label, or 0 without the type row.
74    pub type_label: u16,
75    /// The widest value or null glyph.
76    pub values: u16,
77    /// Whether any row on screen holds a value rather than a null.
78    pub has_values: bool,
79    /// Whether a value may be drawn clipped: text, not a number.
80    pub clips: bool,
81}
82
83impl PageMeasure {
84    /// The width that shows this page whole, heading bounded by `cap`.
85    fn fitted(&self, cap: u16) -> u16 {
86        self.values
87            .max(self.type_label)
88            .max(self.header.min(cap))
89            .clamp(1, MAX_WIDTH)
90    }
91}
92
93#[derive(Debug, Clone)]
94struct Entry {
95    dtype: DataType,
96    /// The values' width: for text, of the first page with a value in it; for the
97    /// rest, the widest page so far.
98    learned: u16,
99    /// Whether text has learned from a page with a value, rather than only nulls.
100    settled: bool,
101    choice: WidthChoice,
102    /// The width the column was last drawn at.
103    shown: Option<u16>,
104    /// The width it was last drawn at with the room to the table's right edge, as
105    /// the last column drawn: what it showed, though not what it is planned with.
106    filled: Option<u16>,
107    /// Whether `shown` was drawn since automatic widths were last relearned, so it
108    /// is the width the column draws at now rather than in the view before.
109    current: bool,
110}
111
112/// Display widths by column identity (name and type): a retyped column starts afresh,
113/// one returning with its type keeps its width. Unrelated to the byte estimate that
114/// plans buffers.
115#[derive(Debug, Clone, Default)]
116pub struct ColumnWidths {
117    by_name: HashMap<String, Vec<Entry>>,
118    /// Automatic widths are to be learned again from the next rows read. Waits for
119    /// them: until they arrive the old rows are still drawn, and they would teach
120    /// the new view their widths.
121    relearn: bool,
122}
123
124impl ColumnWidths {
125    fn entry(&self, name: &str, dtype: &DataType) -> Option<&Entry> {
126        self.by_name.get(name)?.iter().find(|e| &e.dtype == dtype)
127    }
128
129    fn entry_mut(&mut self, name: &str, dtype: &DataType) -> &mut Entry {
130        // Looked up before inserting: drawing calls this per column per frame, and
131        // the name is allocated only the first time.
132        if !self.by_name.contains_key(name) {
133            self.by_name.insert(name.to_string(), Vec::new());
134        }
135        let entries = self.by_name.get_mut(name).expect("inserted above");
136        let at = match entries.iter().position(|e| &e.dtype == dtype) {
137            Some(at) => at,
138            None => {
139                entries.push(Entry {
140                    dtype: dtype.clone(),
141                    learned: 0,
142                    settled: false,
143                    choice: WidthChoice::Auto,
144                    shown: None,
145                    filled: None,
146                    current: false,
147                });
148                entries.len() - 1
149            }
150        };
151        &mut entries[at]
152    }
153
154    /// The width to draw the column at on this page, learning from the page as
155    /// automatic widths do. `cap` bounds automatic text and headings.
156    pub fn width(&mut self, name: &str, dtype: &DataType, page: PageMeasure, cap: u16) -> u16 {
157        let entry = self.entry_mut(name, dtype);
158        if page.clips {
159            if !entry.settled {
160                entry.learned = entry.learned.max(page.values);
161                entry.settled = page.has_values;
162            }
163        } else {
164            entry.learned = entry.learned.max(page.values);
165        }
166        let width = match entry.choice {
167            WidthChoice::Manual(width) if page.clips => width,
168            WidthChoice::Manual(width) => width.max(page.values),
169            WidthChoice::Auto | WidthChoice::Fit => {
170                let values = if page.clips {
171                    entry.learned.min(cap)
172                } else {
173                    entry.learned
174                };
175                values.max(page.type_label).max(page.header.min(cap))
176            }
177        };
178        entry.shown = Some(width);
179        entry.filled = None;
180        entry.current = true;
181        width
182    }
183
184    /// The column, drawn last at `width`, was widened to `filled` to reach the table's
185    /// right edge. Only an automatic width fills: one set by hand is drawn as set.
186    pub fn fill(&mut self, name: &str, dtype: &DataType, filled: u16) {
187        let entry = self.entry_mut(name, dtype);
188        entry.filled = Some(filled);
189    }
190
191    /// How the column's width is chosen.
192    pub fn choice(&self, name: &str, dtype: &DataType) -> WidthChoice {
193        self.entry(name, dtype)
194            .map_or(WidthChoice::Auto, |e| e.choice)
195    }
196
197    /// The width the column was last drawn at, if it has been.
198    pub fn shown(&self, name: &str, dtype: &DataType) -> Option<u16> {
199        self.entry(name, dtype).and_then(|e| e.shown)
200    }
201
202    /// [`Self::shown`], with the room the column filled at the right edge: what a
203    /// step narrower or wider starts from, so a wider column never draws narrower.
204    pub fn on_screen(&self, name: &str, dtype: &DataType) -> Option<u16> {
205        self.entry(name, dtype).and_then(|e| e.filled.or(e.shown))
206    }
207
208    /// The width the column was last drawn at, if it has been drawn since the
209    /// widths were last relearned: what a sideways page can be planned with.
210    pub fn drawn(&self, name: &str, dtype: &DataType) -> Option<u16> {
211        self.entry(name, dtype)
212            .filter(|e| e.current)
213            .and_then(|e| e.shown)
214    }
215
216    pub fn set_choice(&mut self, name: &str, dtype: &DataType, choice: WidthChoice) {
217        let choice = match choice {
218            WidthChoice::Manual(width) => WidthChoice::Manual(width.clamp(MIN_WIDTH, MAX_WIDTH)),
219            other => other,
220        };
221        self.entry_mut(name, dtype).choice = choice;
222    }
223
224    /// The columns waiting to be fitted to the rows on screen.
225    pub fn fits_pending(&self) -> Vec<(String, DataType)> {
226        self.by_name
227            .iter()
228            .flat_map(|(name, entries)| {
229                entries
230                    .iter()
231                    .filter(|e| e.choice == WidthChoice::Fit)
232                    .map(move |e| (name.clone(), e.dtype.clone()))
233            })
234            .collect()
235    }
236
237    /// Fit the column to `page`: its values and type whole, its heading up to `cap`.
238    pub fn fit(&mut self, name: &str, dtype: &DataType, page: PageMeasure, cap: u16) {
239        self.entry_mut(name, dtype).choice = WidthChoice::Manual(page.fitted(cap));
240    }
241
242    /// The view changed: learn every automatic width again from the next rows read
243    /// (see [`Self::rows_arrived`]). Widths set by hand are kept.
244    pub fn relearn(&mut self) {
245        self.relearn = true;
246    }
247
248    /// The change asked to relearn never showed (it failed and the view was put
249    /// back), so the widths learned for the view on screen stand.
250    pub fn keep_learned(&mut self) {
251        self.relearn = false;
252    }
253
254    /// Rows read for the view are about to replace the ones on screen. After
255    /// [`Self::relearn`], every automatic width starts again from them: text from
256    /// its first page with a value, and the rest from nothing, so they may narrow.
257    pub fn rows_arrived(&mut self) {
258        if std::mem::take(&mut self.relearn) {
259            for entry in self.by_name.values_mut().flatten() {
260                entry.learned = 0;
261                entry.settled = false;
262                entry.current = false;
263            }
264        }
265    }
266}
267
268#[cfg(test)]
269mod tests;