1use crossterm::event::{KeyCode, KeyModifiers};
2use unicode_width::UnicodeWidthChar;
3
4use crate::components::{Component, Event, ViewContext};
5use crate::line::Line;
6use crate::rendering::frame::Frame;
7use crate::rendering::soft_wrap::display_width_text;
8
9pub struct TextField {
11 pub value: String,
12 cursor_pos: usize,
13 content_width: usize,
14}
15
16impl TextField {
17 pub fn new(value: String) -> Self {
18 let cursor_pos = value.len();
19 Self { value, cursor_pos, content_width: usize::MAX }
20 }
21
22 pub fn set_content_width(&mut self, width: usize) {
23 self.content_width = width.max(1);
24 }
25
26 pub fn cursor_pos(&self) -> usize {
27 self.cursor_pos
28 }
29
30 pub fn set_cursor_pos(&mut self, pos: usize) {
31 self.cursor_pos = pos.min(self.value.len());
32 }
33
34 pub fn insert_at_cursor(&mut self, c: char) {
35 self.value.insert(self.cursor_pos, c);
36 self.cursor_pos += c.len_utf8();
37 }
38
39 pub fn insert_str_at_cursor(&mut self, s: &str) {
40 self.value.insert_str(self.cursor_pos, s);
41 self.cursor_pos += s.len();
42 }
43
44 pub fn delete_before_cursor(&mut self) -> bool {
45 let Some((prev, _)) = self.value[..self.cursor_pos].char_indices().next_back() else {
46 return false;
47 };
48 self.value.drain(prev..self.cursor_pos);
49 self.cursor_pos = prev;
50 true
51 }
52
53 pub fn set_value(&mut self, value: String) {
54 self.cursor_pos = value.len();
55 self.value = value;
56 }
57
58 pub fn clear(&mut self) {
59 self.value.clear();
60 self.cursor_pos = 0;
61 }
62
63 pub fn to_json(&self) -> serde_json::Value {
64 serde_json::Value::String(self.value.clone())
65 }
66
67 pub fn render_field(&self, context: &ViewContext, focused: bool) -> Vec<Line> {
68 let mut line = Line::new(&self.value);
69 if focused {
70 line.push_styled("▏", context.theme.primary());
71 }
72 vec![line]
73 }
74
75 fn delete_after_cursor(&mut self) {
76 if let Some(c) = self.value[self.cursor_pos..].chars().next() {
77 self.value.drain(self.cursor_pos..self.cursor_pos + c.len_utf8());
78 }
79 }
80
81 fn delete_word_backward(&mut self) {
82 let end = self.cursor_pos;
83 let start = self.word_start_backward();
84 self.cursor_pos = start;
85 self.value.drain(start..end);
86 }
87
88 fn word_end_forward(&mut self) {
89 let len = self.value.len();
90 while self.cursor_pos < len {
91 let ch = self.value[self.cursor_pos..].chars().next().unwrap();
92 if ch.is_whitespace() {
93 break;
94 }
95 self.cursor_pos += ch.len_utf8();
96 }
97 while self.cursor_pos < len {
98 let ch = self.value[self.cursor_pos..].chars().next().unwrap();
99 if !ch.is_whitespace() {
100 break;
101 }
102 self.cursor_pos += ch.len_utf8();
103 }
104 }
105
106 fn move_cursor_up(&mut self, content_width: usize) {
107 if content_width == 0 {
108 return;
109 }
110 let cursor_width = self.display_width_up_to(self.cursor_pos);
111 let row = cursor_width / content_width;
112 if row == 0 {
113 self.cursor_pos = 0;
114 } else {
115 let col = cursor_width % content_width;
116 let target = (row - 1) * content_width + col;
117 self.cursor_pos = self.byte_offset_for_display_width(target);
118 }
119 }
120
121 fn move_cursor_down(&mut self, content_width: usize) {
122 if content_width == 0 {
123 return;
124 }
125 let cursor_width = self.display_width_up_to(self.cursor_pos);
126 let total_width = self.display_width_up_to(self.value.len());
127 let row = cursor_width / content_width;
128 let max_row = total_width / content_width;
129 if row >= max_row {
130 self.cursor_pos = self.value.len();
131 } else {
132 let col = cursor_width % content_width;
133 let target = ((row + 1) * content_width + col).min(total_width);
134 self.cursor_pos = self.byte_offset_for_display_width(target);
135 }
136 }
137
138 fn word_start_backward(&self) -> usize {
139 let mut pos = self.cursor_pos;
140 while pos > 0 {
141 let (i, ch) = self.value[..pos].char_indices().next_back().unwrap();
142 if !ch.is_whitespace() {
143 break;
144 }
145 pos = i;
146 }
147 while pos > 0 {
148 let (i, ch) = self.value[..pos].char_indices().next_back().unwrap();
149 if ch.is_whitespace() {
150 break;
151 }
152 pos = i;
153 }
154 pos
155 }
156
157 fn display_width_up_to(&self, byte_pos: usize) -> usize {
158 display_width_text(&self.value[..byte_pos])
159 }
160
161 fn byte_offset_for_display_width(&self, target_width: usize) -> usize {
162 let mut width = 0;
163 for (i, ch) in self.value.char_indices() {
164 if width >= target_width {
165 return i;
166 }
167 width += UnicodeWidthChar::width(ch).unwrap_or(0);
168 }
169 self.value.len()
170 }
171}
172
173impl Component for TextField {
174 type Message = ();
175
176 async fn on_event(&mut self, event: &Event) -> Option<Vec<Self::Message>> {
177 match event {
178 Event::Key(key) => {
179 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
180 let alt = key.modifiers.contains(KeyModifiers::ALT);
181 match key.code {
182 KeyCode::Char('a') if ctrl => {
183 self.cursor_pos = 0;
184 Some(vec![])
185 }
186 KeyCode::Char('e') if ctrl => {
187 self.cursor_pos = self.value.len();
188 Some(vec![])
189 }
190 KeyCode::Char('w') if ctrl => {
191 self.delete_word_backward();
192 Some(vec![])
193 }
194 KeyCode::Char('u') if ctrl => {
195 self.value.drain(..self.cursor_pos);
196 self.cursor_pos = 0;
197 Some(vec![])
198 }
199 KeyCode::Char('k') if ctrl => {
200 self.value.truncate(self.cursor_pos);
201 Some(vec![])
202 }
203 KeyCode::Backspace if alt => {
204 self.delete_word_backward();
205 Some(vec![])
206 }
207 KeyCode::Left if alt || ctrl => {
208 self.cursor_pos = self.word_start_backward();
209 Some(vec![])
210 }
211 KeyCode::Right if alt || ctrl => {
212 self.word_end_forward();
213 Some(vec![])
214 }
215 KeyCode::Delete => {
216 self.delete_after_cursor();
217 Some(vec![])
218 }
219 KeyCode::Char(c) if !ctrl => {
220 self.insert_at_cursor(c);
221 Some(vec![])
222 }
223 KeyCode::Backspace => {
224 self.delete_before_cursor();
225 Some(vec![])
226 }
227 KeyCode::Left => {
228 self.cursor_pos =
229 self.value[..self.cursor_pos].char_indices().next_back().map_or(0, |(i, _)| i);
230 Some(vec![])
231 }
232 KeyCode::Right => {
233 if let Some(c) = self.value[self.cursor_pos..].chars().next() {
234 self.cursor_pos += c.len_utf8();
235 }
236 Some(vec![])
237 }
238 KeyCode::Home => {
239 self.cursor_pos = 0;
240 Some(vec![])
241 }
242 KeyCode::End => {
243 self.cursor_pos = self.value.len();
244 Some(vec![])
245 }
246 KeyCode::Up => {
247 self.move_cursor_up(self.content_width);
248 Some(vec![])
249 }
250 KeyCode::Down => {
251 self.move_cursor_down(self.content_width);
252 Some(vec![])
253 }
254 _ => None,
255 }
256 }
257 Event::Paste(text) => {
258 self.insert_str_at_cursor(text);
259 Some(vec![])
260 }
261 _ => None,
262 }
263 }
264
265 fn render(&mut self, context: &ViewContext) -> Frame {
266 Frame::new(self.render_field(context, true))
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use crossterm::event::{KeyEvent, KeyModifiers};
274
275 fn key(code: KeyCode) -> KeyEvent {
276 KeyEvent::new(code, KeyModifiers::NONE)
277 }
278 fn ctrl(code: KeyCode) -> KeyEvent {
279 KeyEvent::new(code, KeyModifiers::CONTROL)
280 }
281 fn alt(code: KeyCode) -> KeyEvent {
282 KeyEvent::new(code, KeyModifiers::ALT)
283 }
284 fn field(text: &str) -> TextField {
285 TextField::new(text.to_string())
286 }
287 fn field_at(text: &str, cursor: usize) -> TextField {
288 let mut f = field(text);
289 f.set_cursor_pos(cursor);
290 f
291 }
292
293 async fn send(f: &mut TextField, evt: Event) -> Option<Vec<()>> {
294 f.on_event(&evt).await
295 }
296 async fn send_key(f: &mut TextField, k: KeyEvent) -> Option<Vec<()>> {
297 send(f, Event::Key(k)).await
298 }
299
300 fn assert_state(f: &TextField, value: &str, cursor: usize) {
302 assert_eq!(f.value, value, "value mismatch");
303 assert_eq!(f.cursor_pos(), cursor, "cursor mismatch");
304 }
305
306 #[tokio::test]
307 async fn typing_appends_characters() {
308 let mut f = field("");
309 send_key(&mut f, key(KeyCode::Char('h'))).await;
310 send_key(&mut f, key(KeyCode::Char('i'))).await;
311 assert_eq!(f.value, "hi");
312 }
313
314 #[tokio::test]
315 async fn backspace_variants() {
316 let mut f = field("abc");
318 send_key(&mut f, key(KeyCode::Backspace)).await;
319 assert_eq!(f.value, "ab");
320
321 let mut f = field("");
323 send_key(&mut f, key(KeyCode::Backspace)).await;
324 assert_eq!(f.value, "");
325
326 let mut f = field_at("hello", 3);
328 send_key(&mut f, key(KeyCode::Backspace)).await;
329 assert_state(&f, "helo", 2);
330 }
331
332 #[test]
333 fn to_json_returns_string_value() {
334 assert_eq!(field("hello").to_json(), serde_json::json!("hello"));
335 }
336
337 #[tokio::test]
338 async fn unhandled_keys_are_ignored() {
339 let mut f = field("");
340 assert!(send_key(&mut f, key(KeyCode::F(1))).await.is_none());
341 }
342
343 #[tokio::test]
344 async fn paste_variants() {
345 let mut f = field("");
347 let outcome = send(&mut f, Event::Paste("hello".into())).await;
348 assert!(outcome.is_some());
349 assert_eq!(f.value, "hello");
350
351 let mut f = field_at("hd", 1);
353 send(&mut f, Event::Paste("ello worl".into())).await;
354 assert_state(&f, "hello world", 10);
355 }
356
357 #[test]
358 fn cursor_starts_at_end() {
359 assert_eq!(field("hello").cursor_pos(), 5);
360 }
361
362 #[tokio::test]
363 async fn cursor_movement_single_keys() {
364 let cases: Vec<(&str, Option<usize>, KeyEvent, usize)> = vec![
366 ("hello", None, key(KeyCode::Left), 4),
367 ("hello", None, key(KeyCode::Right), 5),
368 ("", None, key(KeyCode::Left), 0),
369 ("hello", None, key(KeyCode::Home), 0),
370 ("hello", Some(0), key(KeyCode::End), 5),
371 ("hello", None, ctrl(KeyCode::Char('a')), 0),
372 ("hello", Some(0), ctrl(KeyCode::Char('e')), 5),
373 ];
374 for (text, cursor, k, expected) in cases {
375 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
376 send_key(&mut f, k).await;
377 assert_eq!(f.cursor_pos(), expected, "failed for key {k:?} on {text:?}");
378 }
379 }
380
381 #[tokio::test]
382 async fn insert_at_middle() {
383 let mut f = field_at("hllo", 1);
384 send_key(&mut f, key(KeyCode::Char('e'))).await;
385 assert_state(&f, "hello", 2);
386 }
387
388 #[tokio::test]
389 async fn multibyte_utf8_navigation() {
390 let mut f = field("a中b");
391 assert_eq!(f.cursor_pos(), 5);
392 for expected in [4, 1, 0] {
393 send_key(&mut f, key(KeyCode::Left)).await;
394 assert_eq!(f.cursor_pos(), expected);
395 }
396 for expected in [1, 4] {
397 send_key(&mut f, key(KeyCode::Right)).await;
398 assert_eq!(f.cursor_pos(), expected);
399 }
400 }
401
402 #[test]
403 fn set_value_moves_cursor_to_end() {
404 let mut f = field("");
405 f.set_value("hello".to_string());
406 assert_state(&f, "hello", 5);
407 }
408
409 #[test]
410 fn clear_resets_cursor() {
411 let mut f = field("hello");
412 f.clear();
413 assert_state(&f, "", 0);
414 }
415
416 #[tokio::test]
417 async fn delete_forward_variants() {
418 let mut f = field_at("hello", 2);
420 send_key(&mut f, key(KeyCode::Delete)).await;
421 assert_state(&f, "helo", 2);
422
423 let mut f = field("hello");
425 send_key(&mut f, key(KeyCode::Delete)).await;
426 assert_eq!(f.value, "hello");
427
428 let mut f = field_at("a中b", 1);
430 send_key(&mut f, key(KeyCode::Delete)).await;
431 assert_state(&f, "ab", 1);
432 }
433
434 #[tokio::test]
435 async fn ctrl_w_variants() {
436 let cases: Vec<(&str, Option<usize>, &str, usize)> = vec![
438 ("hello world", None, "hello ", 6),
439 ("hello ", None, "", 0),
440 ("hello", Some(0), "hello", 0),
441 ("hello world", Some(8), "hello rld", 6),
442 ("", None, "", 0),
443 ];
444 for (text, cursor, exp_val, exp_cur) in cases {
445 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
446 send_key(&mut f, ctrl(KeyCode::Char('w'))).await;
447 assert_state(&f, exp_val, exp_cur);
448 }
449 }
450
451 #[tokio::test]
452 async fn alt_backspace_deletes_word() {
453 let mut f = field("hello world");
454 send_key(&mut f, alt(KeyCode::Backspace)).await;
455 assert_eq!(f.value, "hello ");
456 }
457
458 #[tokio::test]
459 async fn ctrl_u_variants() {
460 let mut f = field_at("hello world", 5);
461 send_key(&mut f, ctrl(KeyCode::Char('u'))).await;
462 assert_state(&f, " world", 0);
463
464 let mut f = field_at("hello", 0);
466 send_key(&mut f, ctrl(KeyCode::Char('u'))).await;
467 assert_state(&f, "hello", 0);
468 }
469
470 #[tokio::test]
471 async fn ctrl_k_variants() {
472 let mut f = field_at("hello world", 5);
473 send_key(&mut f, ctrl(KeyCode::Char('k'))).await;
474 assert_state(&f, "hello", 5);
475
476 let mut f = field("hello");
478 send_key(&mut f, ctrl(KeyCode::Char('k'))).await;
479 assert_eq!(f.value, "hello");
480 }
481
482 #[tokio::test]
483 async fn word_navigation() {
484 let cases: Vec<(&str, Option<usize>, KeyEvent, usize)> = vec![
486 ("hello world", None, alt(KeyCode::Left), 6),
487 ("hello world", Some(8), alt(KeyCode::Left), 6),
488 ("hello", Some(0), alt(KeyCode::Left), 0),
489 ("hello world", None, ctrl(KeyCode::Left), 6),
490 ("hello world", Some(0), alt(KeyCode::Right), 6),
491 ("hello", None, alt(KeyCode::Right), 5),
492 ("a中 b", Some(0), alt(KeyCode::Right), 5),
493 ("hello world", Some(0), ctrl(KeyCode::Right), 6),
494 ];
495 for (text, cursor, k, expected) in cases {
496 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
497 send_key(&mut f, k).await;
498 assert_eq!(f.cursor_pos(), expected, "failed for {k:?} on {text:?} at {cursor:?}");
499 }
500 }
501
502 #[test]
503 fn move_cursor_up_cases() {
504 let cases: Vec<(&str, Option<usize>, usize, usize)> = vec![
506 ("hello world", Some(3), 10, 0), ("hello world", Some(8), 5, 3), ("hello", Some(3), 0, 3), ];
510 for (text, cursor, width, expected) in cases {
511 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
512 f.move_cursor_up(width);
513 assert_eq!(f.cursor_pos(), expected, "up failed: {text:?} cursor={cursor:?} w={width}");
514 }
515 }
516
517 #[test]
518 fn move_cursor_up_wide_chars() {
519 let mut f = field("中中中中中");
524 f.move_cursor_up(5);
525 assert_eq!(f.cursor_pos(), 9);
526 }
527
528 #[test]
529 fn move_cursor_down_cases() {
530 let cases: Vec<(&str, Option<usize>, usize, usize)> = vec![
532 ("hello world", Some(0), 20, 11), ("hello world", Some(3), 5, 8), ("hello world", Some(8), 5, 11), ("", None, 10, 0), ];
537 for (text, cursor, width, expected) in cases {
538 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
539 f.move_cursor_down(width);
540 assert_eq!(f.cursor_pos(), expected, "down failed: {text:?} cursor={cursor:?} w={width}");
541 }
542 }
543}