ratatui_form/field/
select.rs1use crossterm::event::{KeyCode, KeyEvent};
4use ratatui::buffer::Buffer;
5use ratatui::layout::Rect;
6use ratatui::style::{Color, Style};
7use ratatui::text::{Line, Span};
8use ratatui::widgets::Widget;
9use serde_json::Value;
10use unicode_width::UnicodeWidthStr;
11
12use crate::field::Field;
13use crate::style::FormStyle;
14use crate::validation::ValidationError;
15
16pub struct Select {
18 id: String,
19 label: String,
20 options: Vec<(String, String)>, selected_index: Option<usize>,
22 is_open: bool,
23 highlighted_index: usize,
24 required: bool,
25}
26
27impl Select {
28 pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
30 Self {
31 id: id.into(),
32 label: label.into(),
33 options: Vec::new(),
34 selected_index: None,
35 is_open: false,
36 highlighted_index: 0,
37 required: false,
38 }
39 }
40
41 pub fn option(mut self, value: impl Into<String>, display: impl Into<String>) -> Self {
43 self.options.push((value.into(), display.into()));
44 self
45 }
46
47 pub fn options(mut self, options: Vec<(impl Into<String>, impl Into<String>)>) -> Self {
49 for (value, display) in options {
50 self.options.push((value.into(), display.into()));
51 }
52 self
53 }
54
55 pub fn required(mut self) -> Self {
57 self.required = true;
58 self
59 }
60
61 pub fn initial_value(mut self, value: &str) -> Self {
63 for (i, (v, _)) in self.options.iter().enumerate() {
64 if v == value {
65 self.selected_index = Some(i);
66 self.highlighted_index = i;
67 break;
68 }
69 }
70 self
71 }
72
73 fn toggle_open(&mut self) {
74 self.is_open = !self.is_open;
75 if self.is_open {
76 if let Some(idx) = self.selected_index {
77 self.highlighted_index = idx;
78 }
79 }
80 }
81
82 fn select_highlighted(&mut self) {
83 if !self.options.is_empty() {
84 self.selected_index = Some(self.highlighted_index);
85 }
86 self.is_open = false;
87 }
88
89 fn move_highlight_up(&mut self) {
90 if self.highlighted_index > 0 {
91 self.highlighted_index -= 1;
92 }
93 }
94
95 fn move_highlight_down(&mut self) {
96 if self.highlighted_index < self.options.len().saturating_sub(1) {
97 self.highlighted_index += 1;
98 }
99 }
100}
101
102impl Field for Select {
103 fn id(&self) -> &str {
104 &self.id
105 }
106
107 fn label(&self) -> &str {
108 &self.label
109 }
110
111 fn render(&self, area: Rect, buf: &mut Buffer, focused: bool, style: &FormStyle) {
112 if area.height < 1 || area.width < 1 {
113 return;
114 }
115
116 let label_style = if focused {
118 style.label_focused
119 } else {
120 style.label
121 };
122
123 let required_marker = if self.required { "*" } else { "" };
124 let label_text = format!("{}{}: ", self.label, required_marker);
125 let label_width = label_text.width().min(area.width as usize);
126
127 let label_span = Span::styled(&label_text, label_style);
128 let label_line = Line::from(label_span);
129 let label_area = Rect {
130 x: area.x,
131 y: area.y,
132 width: label_width as u16,
133 height: 1,
134 };
135 label_line.render(label_area, buf);
136
137 let input_x = area.x + label_width as u16;
139 let input_width = area.width.saturating_sub(label_width as u16);
140
141 if input_width == 0 {
142 return;
143 }
144
145 let display_text = self
147 .selected_index
148 .and_then(|i| self.options.get(i))
149 .map(|(_, display)| display.as_str())
150 .unwrap_or("-- Select --");
151
152 let input_style = if focused {
154 style.input_focused
155 } else {
156 style.input
157 };
158
159 for x in input_x..input_x + input_width {
161 buf[(x, area.y)].set_style(input_style);
162 buf[(x, area.y)].set_char(' ');
163 }
164
165 let arrow = if self.is_open { " ▲" } else { " ▼" };
167 let max_text_width = input_width.saturating_sub(2) as usize;
168 let truncated_text: String = display_text.chars().take(max_text_width).collect();
169
170 for (i, c) in truncated_text.chars().enumerate() {
171 if input_x + i as u16 >= area.x + area.width - 2 {
172 break;
173 }
174 buf[(input_x + i as u16, area.y)].set_char(c);
175 }
176
177 let arrow_x = input_x + input_width - 2;
179 for (i, c) in arrow.chars().enumerate() {
180 if arrow_x + (i as u16) < area.x + area.width {
181 buf[(arrow_x + i as u16, area.y)].set_char(c);
182 }
183 }
184
185 if self.is_open && area.height > 1 {
187 let max_dropdown_height = (area.height - 1).min(self.options.len() as u16);
188
189 for (i, (_, display)) in self.options.iter().enumerate() {
190 if i >= max_dropdown_height as usize {
191 break;
192 }
193
194 let y = area.y + 1 + i as u16;
195 let is_highlighted = i == self.highlighted_index;
196 let is_selected = Some(i) == self.selected_index;
197
198 let option_style = if is_highlighted {
199 Style::default().bg(Color::Blue).fg(Color::White)
200 } else if is_selected {
201 Style::default().bg(Color::DarkGray).fg(Color::White)
202 } else {
203 style.input
204 };
205
206 for x in input_x..input_x + input_width {
208 buf[(x, y)].set_style(option_style);
209 buf[(x, y)].set_char(' ');
210 }
211
212 let prefix = if is_selected { "● " } else { " " };
214 for (j, c) in prefix.chars().enumerate() {
215 buf[(input_x + j as u16, y)].set_char(c);
216 }
217
218 let text_start = input_x + 2;
219 for (j, c) in display.chars().enumerate() {
220 if text_start + j as u16 >= input_x + input_width {
221 break;
222 }
223 buf[(text_start + j as u16, y)].set_char(c);
224 }
225 }
226 }
227 }
228
229 fn handle_input(&mut self, event: &KeyEvent) -> bool {
230 match event.code {
231 KeyCode::Enter | KeyCode::Char(' ') => {
232 if self.is_open {
233 self.select_highlighted();
234 } else {
235 self.toggle_open();
236 }
237 true
238 }
239 KeyCode::Esc => {
240 if self.is_open {
241 self.is_open = false;
242 true
243 } else {
244 false
245 }
246 }
247 KeyCode::Up => {
248 if self.is_open {
249 self.move_highlight_up();
250 true
251 } else {
252 false
253 }
254 }
255 KeyCode::Down => {
256 if self.is_open {
257 self.move_highlight_down();
258 true
259 } else {
260 self.toggle_open();
261 true
262 }
263 }
264 _ => false,
265 }
266 }
267
268 fn value(&self) -> Value {
269 self.selected_index
270 .and_then(|i| self.options.get(i))
271 .map(|(v, _)| Value::String(v.clone()))
272 .unwrap_or(Value::Null)
273 }
274
275 fn validate(&self) -> Result<(), Vec<ValidationError>> {
276 if self.required && self.selected_index.is_none() {
277 Err(vec![ValidationError {
278 field_id: self.id.clone(),
279 message: format!("{} is required", self.label),
280 }])
281 } else {
282 Ok(())
283 }
284 }
285
286 fn height(&self) -> u16 {
287 if self.is_open {
288 1 + self.options.len().min(10) as u16
289 } else {
290 1
291 }
292 }
293
294 fn is_required(&self) -> bool {
295 self.required
296 }
297}