1use crate::app::pointer::{FieldId, Hit};
6use crate::render::context::RenderContext;
7use crate::widgets::text_input::TextInput;
8use ratatui::buffer::Buffer;
9use ratatui::layout::Rect;
10use ratatui::style::{Modifier, Style};
11use ratatui::text::{Line, Span};
12use ratatui::widgets::{Paragraph, Widget};
13
14pub enum FormValue<'a> {
16 Input(&'a TextInput),
19 Toggle(bool),
21 Choice(&'a str),
24 Placeholder(&'a str),
26 Options {
32 items: &'a [&'a str],
33 selected: usize,
34 clicks: Option<FieldId>,
35 },
36 Spans { spans: Vec<Span<'a>>, dimmed: bool },
39}
40
41pub struct FormRow<'a> {
42 pub label: &'a str,
43 pub value: FormValue<'a>,
44 pub focused: bool,
45 pub label_width: u16,
48}
49
50impl FormRow<'_> {
51 pub fn render(&self, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
52 self.render_picking(area, buf, ctx, false);
53 }
54
55 pub fn render_picking(&self, area: Rect, buf: &mut Buffer, ctx: &RenderContext, picking: bool) {
59 if area.height == 0 || area.width == 0 {
60 return;
61 }
62 let g = crate::glyphs::get();
67 let rail = if self.focused && !picking {
68 g.rail
69 } else {
70 " "
71 };
72 Paragraph::new(rail)
73 .style(Style::default().fg(ctx.accent))
74 .render(Rect { width: 1, ..area }, buf);
75 self.render_body(
76 Rect {
77 x: area.x + 1,
78 width: area.width - 1,
79 ..area
80 },
81 buf,
82 ctx,
83 );
84 }
85
86 pub fn render_body(&self, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
88 if area.width == 0 || area.height == 0 {
89 return;
90 }
91 let g = crate::glyphs::get();
92 let dimmed = matches!(self.value, FormValue::Spans { dimmed: true, .. });
93 let label_style = if dimmed {
94 Style::default().fg(ctx.dimmed)
95 } else if self.focused {
96 Style::default().fg(ctx.accent).add_modifier(Modifier::BOLD)
97 } else {
98 Style::default().fg(ctx.label)
99 };
100 let label_w = self.label_width.min(area.width);
101 let air = label_w.saturating_sub(crate::glyphs::display_width(self.label) as u16);
103 Paragraph::new(self.label).style(label_style).render(
104 Rect {
105 width: label_w,
106 ..area
107 },
108 buf,
109 );
110
111 let value_area = Rect {
112 x: area.x + label_w,
113 width: area.width.saturating_sub(label_w),
114 ..area
115 };
116 if value_area.width == 0 {
117 return;
118 }
119 match &self.value {
120 FormValue::Input(input) => (*input).render(value_area, buf),
121 FormValue::Toggle(on) => {
122 let marker = if *on { g.checkbox_on } else { g.checkbox_off };
123 Paragraph::new(marker)
124 .style(Style::default().fg(ctx.text_primary))
125 .render(value_area, buf);
126 }
127 FormValue::Choice(value) => {
128 Paragraph::new(*value)
129 .style(Style::default().fg(ctx.text_primary))
130 .render(value_area, buf);
131 }
132 FormValue::Placeholder(value) => {
133 Paragraph::new(*value)
134 .style(Style::default().fg(ctx.dimmed))
135 .render(value_area, buf);
136 }
137 FormValue::Spans { spans, dimmed } => {
138 let spans: Vec<Span> = spans
139 .iter()
140 .map(|s| match dimmed {
141 true => Span::styled(s.content.clone(), Style::default().fg(ctx.dimmed)),
142 false => s.clone(),
143 })
144 .collect();
145 Paragraph::new(Line::from(spans)).render(value_area, buf);
146 }
147 FormValue::Options {
148 items,
149 selected,
150 clicks,
151 } => OptionsRow {
152 items,
153 selected: *selected,
154 clicks: clicks.as_ref(),
155 focused: self.focused,
156 air,
157 }
158 .render(value_area, buf, ctx),
159 }
160 }
161}
162
163struct OptionsRow<'a> {
165 items: &'a [&'a str],
166 selected: usize,
167 clicks: Option<&'a FieldId>,
168 focused: bool,
169 air: u16,
172}
173
174impl OptionsRow<'_> {
175 fn render(&self, value_area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
176 let chosen = Style::default()
179 .fg(ctx.text_primary)
180 .add_modifier(Modifier::BOLD)
181 .patch(ctx.highlight_style());
182 let other = Style::default().fg(if self.focused {
183 ctx.text_secondary
184 } else {
185 ctx.dimmed
186 });
187 let hit = |index: usize, current: usize| {
188 self.clicks.map(|field| Hit::Option {
189 field: Some(field.clone()),
190 index,
191 current,
192 })
193 };
194 let full: usize = self
198 .items
199 .iter()
200 .map(|item| crate::glyphs::display_width(item) + 2)
201 .sum();
202 let compact = full > usize::from(value_area.width + self.air.min(1));
203 let lead = self.air.min(if compact { 2 } else { 1 });
204 let mut spans = Vec::new();
205 let mut hits = Vec::new();
206 if !compact {
207 for (i, item) in self.items.iter().enumerate() {
208 if let Some(hit) = hit(i, self.selected) {
209 hits.push((spans.len(), hit));
210 }
211 let style = if i == self.selected { chosen } else { other };
212 spans.push(Span::styled(format!(" {item} "), style));
213 }
214 } else {
215 let g = crate::glyphs::get();
218 let name = self.items.get(self.selected).copied().unwrap_or("");
219 let mark = Style::default().fg(ctx.text_secondary);
220 spans.push(Span::styled(g.choice_prev, mark));
221 spans.push(Span::styled(format!(" {name} "), chosen));
222 spans.push(Span::styled(g.choice_next, mark));
223 if let (Some(back), Some(on)) = (hit(0, 1), hit(1, 0)) {
224 hits.push((0, back));
225 hits.push((2, on));
226 }
227 }
228 let area = Rect {
229 x: value_area.x - lead,
230 width: value_area.width + lead,
231 ..value_area
232 };
233 let line = Line::from(spans);
234 crate::app::pointer::record_spans(area, &line, hits);
235 Paragraph::new(line).render(area, buf);
236 }
237}
238
239#[cfg(test)]
240mod tests;