Skip to main content

datui_lib/widgets/
retype.rs

1//! The type picker and the combine form: one Surface each over the table.
2
3use crate::app::modals::retype_modal::{CombineField, CombineModal, RetypeModal, Stage};
4use crate::render::context::RenderContext;
5use crate::widgets::ui::{FormLine, FormValue, FormView, HintBar, Picker, Surface};
6use datui_cli::keys::Context;
7use ratatui::buffer::Buffer;
8use ratatui::layout::Rect;
9use ratatui::style::Style;
10use ratatui::text::{Line, Span};
11use ratatui::widgets::{Paragraph, Widget};
12
13/// The type picker over `area`: the types, or a type's formats with what each makes
14/// of the column's first value, and a format typed with what it makes of it.
15pub fn render_retype(area: Rect, buf: &mut Buffer, modal: &RetypeModal, ctx: &RenderContext) {
16    let picker = &modal.picker;
17    let widest = picker
18        .items()
19        .iter()
20        .map(|item| crate::glyphs::display_width(item))
21        .max()
22        .unwrap_or(0) as u16;
23    let width = (widest + 6).clamp(40, 60).min(area.width);
24    // Frame, filter line, the list, the blank above the footer and the footer.
25    let height = (picker.items().len() as u16 + 5).min(23).min(area.height);
26    if width < 10 || height < 6 {
27        return;
28    }
29    let popup = Rect {
30        x: area.x + area.width.saturating_sub(width) / 2,
31        y: area.y + area.height.saturating_sub(height) / 3,
32        width,
33        height,
34    };
35    crate::app::pointer::record(popup, crate::app::pointer::Hit::Modal);
36    let back = match modal.stage {
37        Stage::Type => "Cancel",
38        Stage::Format { .. } => "Back",
39    };
40    let footer = HintBar::from_ctx(ctx)
41        .screen(Context::Retype)
42        .key("Enter")
43        .key_as("Esc", back);
44    let title = modal.title();
45    let inner = Surface::new(&title).footer(&footer).render(popup, buf, ctx);
46    if inner.height < 3 {
47        return;
48    }
49    let hint = match modal.stage {
50        Stage::Type => "type to narrow",
51        Stage::Format { .. } => "type to narrow, or a format such as %d.%m.%Y",
52    };
53    let filter = if picker.filter.is_empty() {
54        Line::from(Span::styled(hint, Style::default().fg(ctx.dimmed)))
55    } else {
56        Line::from(Span::raw(picker.filter.clone()))
57    };
58    Paragraph::new(filter).render(Rect { height: 1, ..inner }, buf);
59    let list = Rect {
60        y: inner.y + 1,
61        height: inner.height.saturating_sub(1),
62        ..inner
63    };
64    if picker.filtered().is_empty() {
65        let said = match modal.typed_format() {
66            Some((format, Some(read))) => {
67                let arrow = crate::glyphs::get().arrow_right;
68                let first = modal.examples.first().cloned().unwrap_or_default();
69                format!("{format}  {first} {arrow} {read}")
70            }
71            Some((format, None)) => format!("{format}: does not read the first value"),
72            None => "No type matches".to_string(),
73        };
74        Paragraph::new(Span::styled(said, Style::default().fg(ctx.text_secondary)))
75            .render(Rect { height: 1, ..list }, buf);
76    } else {
77        Picker::from_state(picker, true).render(list, buf, ctx);
78    }
79}
80
81fn label(field: CombineField) -> &'static str {
82    match field {
83        CombineField::Date => "Date:",
84        CombineField::Time => "Time:",
85        CombineField::Offset => "UTC offset:",
86        CombineField::Kind => "As:",
87        CombineField::Name => "Name:",
88    }
89}
90
91/// Past the longest label, "UTC offset:", plus air.
92const LABEL_WIDTH: u16 = 13;
93
94/// The combine form over `area`.
95pub fn render_combine(area: Rect, buf: &mut Buffer, modal: &CombineModal, ctx: &RenderContext) {
96    let fields: Vec<CombineField> = crate::app::form::Form::fields(modal)
97        .into_iter()
98        .map(|(f, _)| f)
99        .collect();
100    let width = (area.width * 3 / 4).clamp(30, 56).min(area.width);
101    let wanted = if modal.picker.is_some() {
102        16
103    } else {
104        fields.len() as u16 + 6
105    };
106    let height = wanted.min(area.height);
107    if height < 6 {
108        return;
109    }
110    let popup = crate::render::layout::centered_rect(area, width, height);
111    let footer = HintBar::from_ctx(ctx)
112        .screen(Context::Combine)
113        .group("Fields")
114        .key("Enter")
115        .weight(3)
116        .key("Space")
117        .weight(2)
118        .key("Tab")
119        .weight(1)
120        .key("Esc")
121        .weight(4);
122    let kinds: Vec<&str> = crate::formats::column_types::DerivedKind::ALL
123        .iter()
124        .map(|k| k.name())
125        .collect();
126    let kind_at = crate::formats::column_types::DerivedKind::ALL
127        .iter()
128        .position(|k| *k == modal.kind)
129        .unwrap_or(0);
130    let none = || FormValue::Placeholder(crate::app::modals::retype_modal::NONE);
131    let rows = fields
132        .into_iter()
133        .map(|field| {
134            let value = match field {
135                CombineField::Date => FormValue::Choice(&modal.date),
136                CombineField::Time => modal.time.as_deref().map_or_else(none, FormValue::Choice),
137                CombineField::Offset => {
138                    modal.offset.as_deref().map_or_else(none, FormValue::Choice)
139                }
140                CombineField::Kind => FormValue::Options {
141                    items: &kinds,
142                    selected: kind_at,
143                    clicks: None,
144                },
145                CombineField::Name => FormValue::Input(&modal.name),
146            };
147            FormLine::Field(field, label(field), value)
148        })
149        .collect();
150    let status = match &modal.problem {
151        Some(problem) => (problem.clone(), Style::default().fg(ctx.warning)),
152        None => (modal.spec_line(), Style::default().fg(ctx.text_primary)),
153    };
154    FormView {
155        title: "Combine into Datetime",
156        screen: Context::Combine,
157        footer: Some(footer),
158        label_width: LABEL_WIDTH,
159        rows,
160        focused: Some(modal.focus),
161        picker: modal.picker.as_ref().map(|(_, state)| state),
162        status: Some(status),
163        shields: true,
164    }
165    .render::<CombineModal>(popup, buf, ctx);
166}
167
168#[cfg(test)]
169mod tests;