Skip to main content

ratatui_form/field/
text.rs

1//! Text input field.
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4use ratatui::buffer::Buffer;
5use ratatui::layout::Rect;
6use ratatui::style::{Color, Modifier, 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, Validator};
15
16/// A single-line text input field.
17pub struct TextInput {
18    id: String,
19    label: String,
20    value: String,
21    cursor_position: usize,
22    placeholder: Option<String>,
23    required: bool,
24    validators: Vec<Box<dyn Validator>>,
25    validation_errors: Vec<ValidationError>,
26}
27
28impl TextInput {
29    /// Creates a new text input field.
30    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
31        Self {
32            id: id.into(),
33            label: label.into(),
34            value: String::new(),
35            cursor_position: 0,
36            placeholder: None,
37            required: false,
38            validators: Vec::new(),
39            validation_errors: Vec::new(),
40        }
41    }
42
43    /// Sets a placeholder text.
44    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
45        self.placeholder = Some(placeholder.into());
46        self
47    }
48
49    /// Marks this field as required.
50    pub fn required(mut self) -> Self {
51        self.required = true;
52        self
53    }
54
55    /// Adds a validator to this field.
56    pub fn validator(mut self, validator: Box<dyn Validator>) -> Self {
57        self.validators.push(validator);
58        self
59    }
60
61    /// Sets the initial value.
62    pub fn initial_value(mut self, value: impl Into<String>) -> Self {
63        self.value = value.into();
64        self.cursor_position = self.value.len();
65        self
66    }
67
68    fn insert_char(&mut self, c: char) {
69        self.value.insert(self.cursor_position, c);
70        self.cursor_position += c.len_utf8();
71    }
72
73    fn delete_char_before_cursor(&mut self) {
74        if self.cursor_position > 0 {
75            let prev_char_boundary = self.value[..self.cursor_position]
76                .char_indices()
77                .last()
78                .map(|(i, _)| i)
79                .unwrap_or(0);
80            self.value.remove(prev_char_boundary);
81            self.cursor_position = prev_char_boundary;
82        }
83    }
84
85    fn delete_char_at_cursor(&mut self) {
86        if self.cursor_position < self.value.len() {
87            self.value.remove(self.cursor_position);
88        }
89    }
90
91    fn move_cursor_left(&mut self) {
92        if self.cursor_position > 0 {
93            self.cursor_position = self.value[..self.cursor_position]
94                .char_indices()
95                .last()
96                .map(|(i, _)| i)
97                .unwrap_or(0);
98        }
99    }
100
101    fn move_cursor_right(&mut self) {
102        if self.cursor_position < self.value.len() {
103            self.cursor_position = self.value[self.cursor_position..]
104                .char_indices()
105                .nth(1)
106                .map(|(i, _)| self.cursor_position + i)
107                .unwrap_or(self.value.len());
108        }
109    }
110
111    fn move_cursor_home(&mut self) {
112        self.cursor_position = 0;
113    }
114
115    fn move_cursor_end(&mut self) {
116        self.cursor_position = self.value.len();
117    }
118}
119
120impl Field for TextInput {
121    fn id(&self) -> &str {
122        &self.id
123    }
124
125    fn label(&self) -> &str {
126        &self.label
127    }
128
129    fn render(&self, area: Rect, buf: &mut Buffer, focused: bool, style: &FormStyle) {
130        if area.height < 1 || area.width < 1 {
131            return;
132        }
133
134        // Render label
135        let label_style = if focused {
136            style.label_focused
137        } else {
138            style.label
139        };
140
141        let required_marker = if self.required { "*" } else { "" };
142        let label_text = format!("{}{}: ", self.label, required_marker);
143        let label_width = label_text.width().min(area.width as usize);
144
145        let label_span = Span::styled(&label_text, label_style);
146        let label_line = Line::from(label_span);
147        let label_area = Rect {
148            x: area.x,
149            y: area.y,
150            width: label_width as u16,
151            height: 1,
152        };
153        label_line.render(label_area, buf);
154
155        // Calculate input area
156        let input_x = area.x + label_width as u16;
157        let input_width = area.width.saturating_sub(label_width as u16);
158
159        if input_width == 0 {
160            return;
161        }
162
163        // Determine what to display
164        let (display_text, display_style) = if self.value.is_empty() {
165            if let Some(ref placeholder) = self.placeholder {
166                (placeholder.as_str(), style.placeholder)
167            } else {
168                ("", style.input)
169            }
170        } else {
171            (self.value.as_str(), style.input)
172        };
173
174        // Render input value with background
175        let input_bg_style = if focused {
176            style.input_focused
177        } else {
178            style.input
179        };
180
181        // Fill input area with background
182        for x in input_x..input_x + input_width {
183            buf[(x, area.y)].set_style(input_bg_style);
184            buf[(x, area.y)].set_char(' ');
185        }
186
187        // Render the text
188        let visible_text: String = display_text.chars().take(input_width as usize).collect();
189        for (i, c) in visible_text.chars().enumerate() {
190            if input_x + i as u16 >= area.x + area.width {
191                break;
192            }
193            buf[(input_x + i as u16, area.y)].set_char(c);
194            buf[(input_x + i as u16, area.y)].set_style(display_style);
195        }
196
197        // Render cursor if focused
198        if focused {
199            let cursor_x = input_x + self.value[..self.cursor_position].width() as u16;
200            if cursor_x < area.x + area.width {
201                buf[(cursor_x, area.y)].set_style(
202                    Style::default()
203                        .bg(Color::White)
204                        .fg(Color::Black)
205                        .add_modifier(Modifier::SLOW_BLINK),
206                );
207            }
208        }
209
210        // Render validation errors if any
211        if !self.validation_errors.is_empty() && area.height > 1 {
212            let error_msg = &self.validation_errors[0].message;
213            let error_span = Span::styled(error_msg, style.error);
214            let error_line = Line::from(error_span);
215            let error_area = Rect {
216                x: input_x,
217                y: area.y + 1,
218                width: input_width,
219                height: 1,
220            };
221            error_line.render(error_area, buf);
222        }
223    }
224
225    fn handle_input(&mut self, event: &KeyEvent) -> bool {
226        match event.code {
227            KeyCode::Char(c) => {
228                if event.modifiers.contains(KeyModifiers::CONTROL) {
229                    match c {
230                        'a' => self.move_cursor_home(),
231                        'e' => self.move_cursor_end(),
232                        'u' => {
233                            self.value.clear();
234                            self.cursor_position = 0;
235                        }
236                        _ => return false,
237                    }
238                } else {
239                    self.insert_char(c);
240                }
241                true
242            }
243            KeyCode::Backspace => {
244                self.delete_char_before_cursor();
245                true
246            }
247            KeyCode::Delete => {
248                self.delete_char_at_cursor();
249                true
250            }
251            KeyCode::Left => {
252                self.move_cursor_left();
253                true
254            }
255            KeyCode::Right => {
256                self.move_cursor_right();
257                true
258            }
259            KeyCode::Home => {
260                self.move_cursor_home();
261                true
262            }
263            KeyCode::End => {
264                self.move_cursor_end();
265                true
266            }
267            _ => false,
268        }
269    }
270
271    fn value(&self) -> Value {
272        Value::String(self.value.clone())
273    }
274
275    fn validate(&self) -> Result<(), Vec<ValidationError>> {
276        let mut errors = Vec::new();
277
278        // Check required
279        if self.required && self.value.trim().is_empty() {
280            errors.push(ValidationError {
281                field_id: self.id.clone(),
282                message: format!("{} is required", self.label),
283            });
284        }
285
286        // Run validators
287        for validator in &self.validators {
288            if let Err(msg) = validator.validate(&self.value) {
289                errors.push(ValidationError {
290                    field_id: self.id.clone(),
291                    message: msg,
292                });
293            }
294        }
295
296        if errors.is_empty() {
297            Ok(())
298        } else {
299            Err(errors)
300        }
301    }
302
303    fn height(&self) -> u16 {
304        if self.validation_errors.is_empty() {
305            1
306        } else {
307            2
308        }
309    }
310
311    fn is_required(&self) -> bool {
312        self.required
313    }
314}