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;