datui_lib/widgets/
chart_export_modal.rs1use crate::chart::chart_export_modal::{ChartExportModal, FIELDS};
5use crate::render::context::RenderContext;
6use crate::widgets::ui::{FormLine, FormValue, FormView, HintBar};
7use ratatui::layout::Rect;
8
9const LABEL_WIDTH: u16 = 14;
11
12pub fn height(modal: &ChartExportModal) -> u16 {
15 modal.shown().len() as u16 + STATUS_ROWS + 4
16}
17
18const STATUS_ROWS: u16 = 2;
21
22pub fn render_chart_export_modal(
23 area: Rect,
24 buf: &mut ratatui::buffer::Buffer,
25 modal: &mut ChartExportModal,
26 ctx: &RenderContext,
27) {
28 let footer = HintBar::from_ctx(ctx)
29 .screen(datui_cli::keys::Context::Chart)
30 .group("Export dialog")
31 .key("Enter")
32 .weight(3)
33 .key("Tab")
34 .weight(1)
35 .key("Esc")
36 .weight(4);
37 let focus = modal.focus;
38 for (field, _) in FIELDS {
39 if let Some(input) = modal.input_mut(field) {
40 input.set_focused(field == focus);
41 }
42 }
43 let rows = modal
44 .shown()
45 .into_iter()
46 .filter_map(|field| {
47 let value = match modal.choice(field) {
48 Some(choice) => FormValue::Choice(choice),
49 None => FormValue::Input(modal.input(field)?),
50 };
51 Some(FormLine::Field(field, field.label(), value))
52 })
53 .collect();
54 let error = modal.error.clone().unwrap_or_default();
55 FormView {
56 title: "Export Chart",
57 screen: datui_cli::keys::Context::Chart,
58 footer: Some(footer),
59 label_width: LABEL_WIDTH,
60 rows,
61 focused: Some(focus),
62 picker: None,
63 status: Some((error, ratatui::style::Style::default().fg(ctx.warning))),
64 shields: true,
65 }
66 .render::<ChartExportModal>(area, buf, ctx);
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::chart::chart_export_modal::ExportDefaults;
73 use ratatui::buffer::Buffer;
74
75 fn render_rows(width: u16, height: u16) -> Vec<String> {
76 let ctx = RenderContext::for_test();
77 let config = crate::config::AppConfig::default();
78 let theme = crate::config::Theme::from_config(&config.theme).unwrap();
79 let mut modal = ChartExportModal::new();
80 modal.open(&theme, 1000, ExportDefaults::default());
81 modal.path_input.set_value("out.png");
82 let area = Rect::new(0, 0, width, height);
83 let mut buf = Buffer::empty(area);
84 render_chart_export_modal(area, &mut buf, &mut modal, &ctx);
85 (0..height)
86 .map(|y| {
87 (0..width)
88 .map(|x| buf[(x, y)].symbol().to_string())
89 .collect::<String>()
90 })
91 .collect()
92 }
93
94 #[test]
97 fn one_surface_with_a_row_per_field() {
98 let rows = render_rows(64, height(&ChartExportModal::new()));
99 assert!(rows[0].contains("Export Chart"), "title: {:?}", rows[0]);
100 for row in &rows[1..rows.len() - 1] {
101 assert!(
102 !row.contains('╭') && !row.contains('╰'),
103 "a second border inside the surface: {row:?}"
104 );
105 }
106 let text = rows.join("\n");
107 for label in [
108 "Path:",
109 "Format:",
110 "PNG",
111 "Style:",
112 "Light",
113 "Size:",
114 "Document",
115 "Width:",
116 "1600",
117 "Height:",
118 "1000",
119 "Legend:",
120 "Title:",
121 "Description:",
122 "Notes:",
123 "Source:",
124 "Byline:",
125 ] {
126 assert!(text.contains(label), "{label}:\n{text}");
127 }
128 let footer = &rows[rows.len() - 2];
129 assert!(
130 footer.contains("Enter") && footer.contains("Export") && footer.contains("Esc"),
131 "footer chips: {footer:?}"
132 );
133 }
134
135 #[test]
138 fn the_reason_sits_on_the_status_line() {
139 let ctx = RenderContext::for_test();
140 let mut modal = ChartExportModal::new();
141 modal.error = Some("Enter a file path.".to_string());
142 let area = Rect::new(0, 0, 64, height(&ChartExportModal::new()));
143 let mut buf = Buffer::empty(area);
144 render_chart_export_modal(area, &mut buf, &mut modal, &ctx);
145 let rows: Vec<String> = (0..height(&ChartExportModal::new()))
146 .map(|y| (0..64).map(|x| buf[(x, y)].symbol().to_string()).collect())
147 .collect();
148 let at = rows
149 .iter()
150 .position(|row| row.contains("Enter a file path."));
151 assert_eq!(
152 at,
153 Some(usize::from(height(&ChartExportModal::new())) - 4),
154 "{rows:#?}"
155 );
156 assert!(
157 rows[usize::from(height(&ChartExportModal::new())) - 5]
158 .trim_matches(['│', ' '])
159 .is_empty()
160 );
161 assert!(rows.iter().any(|row| row.contains("Byline:")));
162 }
163
164 #[test]
165 fn a_tiny_area_never_panics() {
166 for (w, h) in [(0, 0), (3, 2), (10, 4), (30, 6), (64, 10)] {
167 let _ = render_rows(w, h);
168 }
169 }
170}