Skip to main content

datui_lib/widgets/
sample_form.rs

1//! The Sample form: which rows every analysis tool reads, and how they are picked.
2//!
3//! Only settings. A value names itself ("All rows (36.8M)", "Random", "100,000
4//! rows"), and a kind of rows that needs telling what to type (partitions, files, a
5//! time range) carries its context on the lines under it. The keys are on the
6//! control bar, like everywhere else.
7
8use crate::render::context::RenderContext;
9use crate::sample_modal::{SampleField, SampleForm};
10use crate::widgets::ui::{FormRow, FormValue, Surface};
11use ratatui::buffer::Buffer;
12use ratatui::layout::Rect;
13use ratatui::style::Style;
14use ratatui::text::{Line, Span};
15use ratatui::widgets::{Paragraph, Widget, Wrap};
16
17/// How many source files the list under the Files row shows at once.
18pub const FILES_SHOWN: usize = 6;
19
20/// Where values start, past the rail gutter and the longest label.
21const LABEL_WIDTH: u16 = 15;
22
23/// `focused` is whether the form has the cursor; a form waiting beside a focused
24/// tool list is drawn without the rail, so one thing looks focused.
25pub fn render(form: &SampleForm, focused: bool, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
26    let dimmed = Style::default().fg(ctx.dimmed);
27    let g = crate::glyphs::get();
28    // The form, top to bottom: a row per setting, and the context lines a setting
29    // brings with it, directly under it.
30    enum Item {
31        Row(SampleField),
32        Context(Line<'static>),
33    }
34    let mut items = Vec::new();
35    for field in form.fields() {
36        items.push(Item::Row(field));
37        match field {
38            SampleField::PartitionValues => {
39                let known = form.partition_values_known();
40                if !known.is_empty() {
41                    let shown = if known.len() > 4 {
42                        format!(
43                            "{}, {}, {} {}",
44                            known[0],
45                            known[1],
46                            g.ellipsis,
47                            known[known.len() - 1]
48                        )
49                    } else {
50                        known.join(", ")
51                    };
52                    items.push(Item::Context(Line::styled(
53                        format!("Holds {shown}"),
54                        dimmed,
55                    )));
56                }
57                // The forms a value can take, shown with values this column holds.
58                let (list, range) = match known.as_slice() {
59                    [first, .., last] if known.len() >= 3 => {
60                        (format!("{first},{last}"), format!("{}..{last}", known[1]))
61                    }
62                    _ => ("2019,2021".to_string(), "2020..2022".to_string()),
63                };
64                items.push(Item::Context(Line::styled(
65                    format!("One, a list {list}, or a range {range}"),
66                    dimmed,
67                )));
68            }
69            SampleField::Files => {
70                let chosen = form.files_chosen();
71                let files = &form.context.files;
72                for (index, name) in files
73                    .iter()
74                    .enumerate()
75                    .skip(form.file_offset)
76                    .take(FILES_SHOWN)
77                {
78                    let mark = if chosen.contains(&(index + 1)) {
79                        g.checkbox_on
80                    } else {
81                        g.checkbox_off
82                    };
83                    items.push(Item::Context(Line::from(vec![
84                        Span::styled(format!("{mark} {:>3}  ", index + 1), dimmed),
85                        Span::styled(short_path(name), Style::default().fg(ctx.text_primary)),
86                    ])));
87                }
88                let rest = files.len().saturating_sub(form.file_offset + FILES_SHOWN);
89                if rest > 0 {
90                    items.push(Item::Context(Line::styled(
91                        format!("{rest} more {}", g.ellipsis),
92                        dimmed,
93                    )));
94                }
95            }
96            SampleField::RangeTo => {
97                if let Some(rows) = form.context.view_rows {
98                    items.push(Item::Context(Line::styled(
99                        format!("Table: {} rows", crate::numfmt::group_chrome(rows)),
100                        dimmed,
101                    )));
102                }
103            }
104            SampleField::TimeBefore => {
105                items.push(Item::Context(Line::styled(
106                    format!("Dates as 2024-01-31 {} Before excluded", g.middot),
107                    dimmed,
108                )));
109            }
110            SampleField::Size => {
111                let per = if matches!(
112                    form.draft.method,
113                    crate::sampling::SampleMethod::PerPartition { .. }
114                ) {
115                    "Rows per value"
116                } else {
117                    "Rows"
118                };
119                // The cost, when the table has measured its rows; the forms a size
120                // takes otherwise.
121                let said = match (form.rows_expected(), form.estimate()) {
122                    (Some(rows), Some(bytes)) => format!(
123                        "{} rows {} ~{}",
124                        crate::numfmt::group_chrome(rows),
125                        g.middot,
126                        crate::widgets::info::format_bytes(bytes)
127                    ),
128                    _ => format!("{per}, like 50000, 50k or 2m"),
129                };
130                items.push(Item::Context(Line::styled(said, dimmed)));
131            }
132            _ => {}
133        }
134    }
135
136    let error_height = u16::from(form.error.is_some()) * 3;
137    let height = items.len() as u16 + error_height + 2;
138    let width = area.width.saturating_sub(4).clamp(40, 72).min(area.width);
139    let frame = Rect {
140        x: area.x + area.width.saturating_sub(width) / 2,
141        y: area.y + area.height.saturating_sub(height) / 2,
142        width,
143        height: height.min(area.height),
144    };
145    let inner = Surface::new("Sample").render(frame, buf, ctx);
146    let bottom = inner.y + inner.height;
147    let mut y = inner.y;
148    for item in &items {
149        if y >= bottom {
150            return;
151        }
152        let line = Rect {
153            y,
154            height: 1,
155            ..inner
156        };
157        match item {
158            Item::Row(field) => {
159                let choice = form.choice(*field);
160                let value = match form.input(*field) {
161                    Some(input) => FormValue::Input(input),
162                    None => FormValue::Choice(&choice),
163                };
164                FormRow {
165                    label: field.label(),
166                    value,
167                    focused: focused && form.field == *field,
168                    label_width: LABEL_WIDTH,
169                }
170                .render(line, buf, ctx);
171                crate::pointer::record_field::<SampleForm>(line, *field);
172            }
173            Item::Context(text) => {
174                // Under the value column, so it reads as belonging to the row above.
175                let indent = LABEL_WIDTH + 1;
176                Paragraph::new(text.clone()).render(
177                    Rect {
178                        x: line.x + indent,
179                        width: line.width.saturating_sub(indent),
180                        ..line
181                    },
182                    buf,
183                );
184            }
185        }
186        y += 1;
187    }
188    if let Some(error) = &form.error
189        && y + 1 < bottom
190    {
191        Paragraph::new(error.as_str())
192            .wrap(Wrap { trim: true })
193            .style(Style::default().fg(ctx.warning))
194            .render(
195                Rect {
196                    y: y + 1,
197                    height: bottom - y - 1,
198                    ..inner
199                },
200                buf,
201            );
202    }
203}
204
205/// A file's path from its last two parts: a hive file is told apart by its
206/// partition directory, and the root every file shares says nothing.
207fn short_path(path: &str) -> String {
208    let parts: Vec<&str> = path.rsplit(['/', '\\']).take(2).collect();
209    parts.into_iter().rev().collect::<Vec<_>>().join("/")
210}