1use polars::prelude::DataType;
16use std::collections::HashMap;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum WidthChoice {
21 #[default]
23 Auto,
24 Manual(u16),
27 Fit,
30}
31
32pub const MIN_WIDTH: u16 = 4;
34pub const MAX_WIDTH: u16 = 240;
35pub const WIDTH_STEP: u16 = 4;
37pub const UNSEEN_WIDTH: u16 = 12;
39
40impl WidthChoice {
41 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 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
67pub 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#[derive(Debug, Clone, Copy, Default)]
78pub struct PageMeasure {
79 pub header: u16,
81 pub type_label: u16,
83 pub values: u16,
85 pub has_values: bool,
87 pub clips: bool,
89}
90
91impl PageMeasure {
92 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 learned: u16,
107 settled: bool,
109 choice: WidthChoice,
110 shown: Option<u16>,
112 filled: Option<u16>,
115 current: bool,
118}
119
120#[derive(Debug, Clone, Default)]
125pub struct ColumnWidths {
126 by_name: HashMap<String, Vec<Entry>>,
127 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 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 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 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 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 pub fn shown(&self, name: &str, dtype: &DataType) -> Option<u16> {
208 self.entry(name, dtype).and_then(|e| e.shown)
209 }
210
211 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 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 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 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 pub fn relearn(&mut self) {
254 self.relearn = true;
255 }
256
257 pub fn keep_learned(&mut self) {
260 self.relearn = false;
261 }
262
263 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 widths.rows_arrived();
417 assert_eq!(widths.width("d", &s, text(5), 32), 20);
418 }
419
420 #[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 #[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}