1use crate::app::form::Form;
6use crate::render::context::RenderContext;
7use crate::widgets::ui::{FormRow, FormValue, HintBar, Picker, PickerState, Surface};
8use ratatui::buffer::Buffer;
9use ratatui::layout::Rect;
10use ratatui::style::Style;
11use ratatui::text::Line;
12use ratatui::widgets::{Paragraph, Widget};
13
14pub enum FormLine<'a, F> {
16 Field(F, &'a str, FormValue<'a>),
18 Note(Line<'a>),
21}
22
23pub struct FormView<'a, F> {
26 pub title: &'a str,
27 pub screen: datui_cli::keys::Context,
28 pub footer: Option<HintBar>,
29 pub label_width: u16,
31 pub rows: Vec<FormLine<'a, F>>,
33 pub focused: Option<F>,
35 pub picker: Option<&'a PickerState>,
36 pub status: Option<(String, Style)>,
40 pub shields: bool,
44}
45
46impl<'a, F: Copy + PartialEq> FormView<'a, F> {
47 pub fn render<T: Form<Field = F>>(self, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
49 self.draw(area, buf, ctx, Some(crate::app::pointer::record_field::<T>));
50 }
51
52 pub fn render_unrecorded(self, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
54 self.draw(area, buf, ctx, None);
55 }
56
57 fn draw(self, area: Rect, buf: &mut Buffer, ctx: &RenderContext, record: Option<fn(Rect, F)>) {
58 let footer = match (self.picker, self.footer) {
59 (Some(_), _) => Some(
60 HintBar::from_ctx(ctx)
61 .screen(self.screen)
62 .group("Picker")
63 .key("Enter")
64 .weight(3)
65 .key("(type)")
66 .weight(1)
67 .key("Esc")
68 .weight(4),
69 ),
70 (None, footer) => footer,
71 };
72 if self.shields {
73 crate::app::pointer::record(area, crate::app::pointer::Hit::Modal);
74 }
75 let mut surface = Surface::new(self.title);
76 if let Some(footer) = &footer {
77 surface = surface.footer(footer);
78 }
79 let content = surface.render(area, buf, ctx);
80 if content.height == 0 || content.width < 4 {
81 return;
82 }
83 let bottom = content.bottom();
84 let reserved = match self.status {
86 Some(_) if content.height > 2 => 2,
87 _ => 0,
88 };
89 let room = (content.height - reserved) as usize;
90 let focused = self.focused;
92 let at = self
93 .rows
94 .iter()
95 .position(|line| matches!(line, FormLine::Field(f, ..) if Some(*f) == focused))
96 .unwrap_or(0);
97 let mut end = at;
99 while end + 1 - at < room && matches!(self.rows.get(end + 1), Some(FormLine::Note(_))) {
100 end += 1;
101 }
102 let first = end.saturating_sub(room.saturating_sub(1));
103 let picking = self.picker.is_some();
104 let mut y = content.y;
105 for line in self.rows.into_iter().skip(first).take(room) {
106 let row = Rect {
107 y,
108 height: 1,
109 ..content
110 };
111 match line {
112 FormLine::Field(field, label, value) => {
113 if let Some(record) = record {
115 record(row, field);
116 }
117 FormRow {
118 label,
119 value,
120 focused: Some(field) == focused,
121 label_width: self.label_width,
122 }
123 .render_picking(row, buf, ctx, picking);
124 }
125 FormLine::Note(text) => {
126 let indent = self.label_width + 1;
127 Paragraph::new(text).render(
128 Rect {
129 x: row.x + indent,
130 width: row.width.saturating_sub(indent),
131 ..row
132 },
133 buf,
134 );
135 }
136 }
137 y += 1;
138 }
139 let status_y = bottom - 1;
140 if let Some(state) = self.picker {
142 crate::app::pointer::record(content, crate::app::pointer::Hit::Picker);
144 let picker_y = y + 1;
145 if picker_y < status_y {
146 let picker_area = Rect {
147 x: content.x + 2,
148 y: picker_y,
149 width: content.width.saturating_sub(2),
150 height: status_y - picker_y,
151 };
152 Picker::from_state(state, true).render(picker_area, buf, ctx);
153 }
154 }
155 let Some((text, style)) = self.status.filter(|(text, _)| !text.is_empty()) else {
156 return;
157 };
158 if reserved == 0 {
159 return;
160 }
161 let width = content.width as usize;
165 let free = if picking {
166 1
167 } else {
168 bottom.saturating_sub(y + 1).max(1) as usize
169 };
170 let mut lines = crate::widgets::info::wrap_to(&text, width);
171 if lines.len() > free {
172 lines.truncate(free);
173 if let Some(last) = lines.last_mut() {
174 let cut = format!("{last} {}", crate::glyphs::get().ellipsis);
175 *last = crate::glyphs::fit(&cut, width);
176 }
177 }
178 let top = bottom - lines.len() as u16;
179 for (i, line) in lines.into_iter().enumerate() {
180 Paragraph::new(line).style(style).render(
181 Rect {
182 y: top + i as u16,
183 height: 1,
184 ..content
185 },
186 buf,
187 );
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests;