datui_lib/widgets/
retype.rs1use 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
13pub 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 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
91const LABEL_WIDTH: u16 = 13;
93
94pub 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;