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 pub fn is_cursor_on_first_visual_line(&self) -> bool {
158 self.cursor_visual_row() == 0
159 }
160
161 pub fn is_cursor_on_last_visual_line(&self) -> bool {
162 self.cursor_visual_row() >= self.max_visual_row()
163 }
164
165 fn cursor_visual_row(&self) -> usize {
166 if self.content_width == 0 {
167 return 0;
168 }
169 self.display_width_up_to(self.cursor_pos) / self.content_width
170 }
171
172 fn max_visual_row(&self) -> usize {
173 if self.content_width == 0 {
174 return 0;
175 }
176 self.display_width_up_to(self.value.len()) / self.content_width
177 }
178
179 fn display_width_up_to(&self, byte_pos: usize) -> usize {
180 display_width_text(&self.value[..byte_pos])
181 }
182
183 fn byte_offset_for_display_width(&self, target_width: usize) -> usize {
184 let mut width = 0;
185 for (i, ch) in self.value.char_indices() {
186 if width >= target_width {
187 return i;
188 }
189 width += UnicodeWidthChar::width(ch).unwrap_or(0);
190 }
191 self.value.len()
192 }
193}
194
195impl Component for TextField {
196 type Message = ();
197
198 async fn on_event(&mut self, event: &Event) -> Option<Vec<Self::Message>> {
199 match event {
200 Event::Key(key) => {
201 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
202 let alt = key.modifiers.contains(KeyModifiers::ALT);
203 match key.code {
204 KeyCode::Char('a') if ctrl => {
205 self.cursor_pos = 0;
206 Some(vec![])
207 }
208 KeyCode::Char('e') if ctrl => {
209 self.cursor_pos = self.value.len();
210 Some(vec![])
211 }
212 KeyCode::Char('w') if ctrl => {
213 self.delete_word_backward();
214 Some(vec![])
215 }
216 KeyCode::Char('u') if ctrl => {
217 self.value.drain(..self.cursor_pos);
218 self.cursor_pos = 0;
219 Some(vec![])
220 }
221 KeyCode::Char('k') if ctrl => {
222 self.value.truncate(self.cursor_pos);
223 Some(vec![])
224 }
225 KeyCode::Backspace if alt => {
226 self.delete_word_backward();
227 Some(vec![])
228 }
229 KeyCode::Left if alt || ctrl => {
230 self.cursor_pos = self.word_start_backward();
231 Some(vec![])
232 }
233 KeyCode::Char('b') if alt => {
234 self.cursor_pos = self.word_start_backward();
235 Some(vec![])
236 }
237 KeyCode::Right if alt || ctrl => {
238 self.word_end_forward();
239 Some(vec![])
240 }
241 KeyCode::Char('f') if alt => {
242 self.word_end_forward();
243 Some(vec![])
244 }
245 KeyCode::Delete => {
246 self.delete_after_cursor();
247 Some(vec![])
248 }
249 KeyCode::Char(c) if !ctrl => {
250 self.insert_at_cursor(c);
251 Some(vec![])
252 }
253 KeyCode::Backspace => {
254 self.delete_before_cursor();
255 Some(vec![])
256 }
257 KeyCode::Left => {
258 self.cursor_pos =
259 self.value[..self.cursor_pos].char_indices().next_back().map_or(0, |(i, _)| i);
260 Some(vec![])
261 }
262 KeyCode::Right => {
263 if let Some(c) = self.value[self.cursor_pos..].chars().next() {
264 self.cursor_pos += c.len_utf8();
265 }
266 Some(vec![])
267 }
268 KeyCode::Home => {
269 self.cursor_pos = 0;
270 Some(vec![])
271 }
272 KeyCode::End => {
273 self.cursor_pos = self.value.len();
274 Some(vec![])
275 }
276 KeyCode::Up => {
277 self.move_cursor_up(self.content_width);
278 Some(vec![])
279 }
280 KeyCode::Down => {
281 self.move_cursor_down(self.content_width);
282 Some(vec![])
283 }
284 _ => None,
285 }
286 }
287 Event::Paste(text) => {
288 self.insert_str_at_cursor(text);
289 Some(vec![])
290 }
291 _ => None,
292 }
293 }
294
295 fn render(&mut self, context: &ViewContext) -> Frame {
296 Frame::new(self.render_field(context, true))
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crossterm::event::{KeyEvent, KeyModifiers};
304
305 fn key(code: KeyCode) -> KeyEvent {
306 KeyEvent::new(code, KeyModifiers::NONE)
307 }
308 fn ctrl(code: KeyCode) -> KeyEvent {
309 KeyEvent::new(code, KeyModifiers::CONTROL)
310 }
311 fn alt(code: KeyCode) -> KeyEvent {
312 KeyEvent::new(code, KeyModifiers::ALT)
313 }
314 fn field(text: &str) -> TextField {
315 TextField::new(text.to_string())
316 }
317 fn field_at(text: &str, cursor: usize) -> TextField {
318 let mut f = field(text);
319 f.set_cursor_pos(cursor);
320 f
321 }
322
323 async fn send(f: &mut TextField, evt: Event) -> Option<Vec<()>> {
324 f.on_event(&evt).await
325 }
326 async fn send_key(f: &mut TextField, k: KeyEvent) -> Option<Vec<()>> {
327 send(f, Event::Key(k)).await
328 }
329
330 fn assert_state(f: &TextField, value: &str, cursor: usize) {
332 assert_eq!(f.value, value, "value mismatch");
333 assert_eq!(f.cursor_pos(), cursor, "cursor mismatch");
334 }
335
336 #[tokio::test]
337 async fn typing_appends_characters() {
338 let mut f = field("");
339 send_key(&mut f, key(KeyCode::Char('h'))).await;
340 send_key(&mut f, key(KeyCode::Char('i'))).await;
341 assert_eq!(f.value, "hi");
342 }
343
344 #[tokio::test]
345 async fn backspace_variants() {
346 let mut f = field("abc");
348 send_key(&mut f, key(KeyCode::Backspace)).await;
349 assert_eq!(f.value, "ab");
350
351 let mut f = field("");
353 send_key(&mut f, key(KeyCode::Backspace)).await;
354 assert_eq!(f.value, "");
355
356 let mut f = field_at("hello", 3);
358 send_key(&mut f, key(KeyCode::Backspace)).await;
359 assert_state(&f, "helo", 2);
360 }
361
362 #[test]
363 fn to_json_returns_string_value() {
364 assert_eq!(field("hello").to_json(), serde_json::json!("hello"));
365 }
366
367 #[tokio::test]
368 async fn unhandled_keys_are_ignored() {
369 let mut f = field("");
370 assert!(send_key(&mut f, key(KeyCode::F(1))).await.is_none());
371 }
372
373 #[tokio::test]
374 async fn paste_variants() {
375 let mut f = field("");
377 let outcome = send(&mut f, Event::Paste("hello".into())).await;
378 assert!(outcome.is_some());
379 assert_eq!(f.value, "hello");
380
381 let mut f = field_at("hd", 1);
383 send(&mut f, Event::Paste("ello worl".into())).await;
384 assert_state(&f, "hello world", 10);
385 }
386
387 #[test]
388 fn cursor_starts_at_end() {
389 assert_eq!(field("hello").cursor_pos(), 5);
390 }
391
392 #[tokio::test]
393 async fn cursor_movement_single_keys() {
394 let cases: Vec<(&str, Option<usize>, KeyEvent, usize)> = vec![
396 ("hello", None, key(KeyCode::Left), 4),
397 ("hello", None, key(KeyCode::Right), 5),
398 ("", None, key(KeyCode::Left), 0),
399 ("hello", None, key(KeyCode::Home), 0),
400 ("hello", Some(0), key(KeyCode::End), 5),
401 ("hello", None, ctrl(KeyCode::Char('a')), 0),
402 ("hello", Some(0), ctrl(KeyCode::Char('e')), 5),
403 ];
404 for (text, cursor, k, expected) in cases {
405 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
406 send_key(&mut f, k).await;
407 assert_eq!(f.cursor_pos(), expected, "failed for key {k:?} on {text:?}");
408 }
409 }
410
411 #[tokio::test]
412 async fn insert_at_middle() {
413 let mut f = field_at("hllo", 1);
414 send_key(&mut f, key(KeyCode::Char('e'))).await;
415 assert_state(&f, "hello", 2);
416 }
417
418 #[tokio::test]
419 async fn multibyte_utf8_navigation() {
420 let mut f = field("a中b");
421 assert_eq!(f.cursor_pos(), 5);
422 for expected in [4, 1, 0] {
423 send_key(&mut f, key(KeyCode::Left)).await;
424 assert_eq!(f.cursor_pos(), expected);
425 }
426 for expected in [1, 4] {
427 send_key(&mut f, key(KeyCode::Right)).await;
428 assert_eq!(f.cursor_pos(), expected);
429 }
430 }
431
432 #[test]
433 fn set_value_moves_cursor_to_end() {
434 let mut f = field("");
435 f.set_value("hello".to_string());
436 assert_state(&f, "hello", 5);
437 }
438
439 #[test]
440 fn clear_resets_cursor() {
441 let mut f = field("hello");
442 f.clear();
443 assert_state(&f, "", 0);
444 }
445
446 #[tokio::test]
447 async fn delete_forward_variants() {
448 let mut f = field_at("hello", 2);
450 send_key(&mut f, key(KeyCode::Delete)).await;
451 assert_state(&f, "helo", 2);
452
453 let mut f = field("hello");
455 send_key(&mut f, key(KeyCode::Delete)).await;
456 assert_eq!(f.value, "hello");
457
458 let mut f = field_at("a中b", 1);
460 send_key(&mut f, key(KeyCode::Delete)).await;
461 assert_state(&f, "ab", 1);
462 }
463
464 #[tokio::test]
465 async fn ctrl_w_variants() {
466 let cases: Vec<(&str, Option<usize>, &str, usize)> = vec![
468 ("hello world", None, "hello ", 6),
469 ("hello ", None, "", 0),
470 ("hello", Some(0), "hello", 0),
471 ("hello world", Some(8), "hello rld", 6),
472 ("", None, "", 0),
473 ];
474 for (text, cursor, exp_val, exp_cur) in cases {
475 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
476 send_key(&mut f, ctrl(KeyCode::Char('w'))).await;
477 assert_state(&f, exp_val, exp_cur);
478 }
479 }
480
481 #[tokio::test]
482 async fn alt_backspace_deletes_word() {
483 let mut f = field("hello world");
484 send_key(&mut f, alt(KeyCode::Backspace)).await;
485 assert_eq!(f.value, "hello ");
486 }
487
488 #[tokio::test]
489 async fn ctrl_u_variants() {
490 let mut f = field_at("hello world", 5);
491 send_key(&mut f, ctrl(KeyCode::Char('u'))).await;
492 assert_state(&f, " world", 0);
493
494 let mut f = field_at("hello", 0);
496 send_key(&mut f, ctrl(KeyCode::Char('u'))).await;
497 assert_state(&f, "hello", 0);
498 }
499
500 #[tokio::test]
501 async fn ctrl_k_variants() {
502 let mut f = field_at("hello world", 5);
503 send_key(&mut f, ctrl(KeyCode::Char('k'))).await;
504 assert_state(&f, "hello", 5);
505
506 let mut f = field("hello");
508 send_key(&mut f, ctrl(KeyCode::Char('k'))).await;
509 assert_eq!(f.value, "hello");
510 }
511
512 #[tokio::test]
513 async fn word_navigation() {
514 let cases: Vec<(&str, Option<usize>, KeyEvent, usize)> = vec![
516 ("hello world", None, alt(KeyCode::Left), 6),
517 ("hello world", Some(8), alt(KeyCode::Left), 6),
518 ("hello", Some(0), alt(KeyCode::Left), 0),
519 ("hello world", None, ctrl(KeyCode::Left), 6),
520 ("hello world", Some(0), alt(KeyCode::Right), 6),
521 ("hello", None, alt(KeyCode::Right), 5),
522 ("a中 b", Some(0), alt(KeyCode::Right), 5),
523 ("hello world", Some(0), ctrl(KeyCode::Right), 6),
524 ];
525 for (text, cursor, k, expected) in cases {
526 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
527 send_key(&mut f, k).await;
528 assert_eq!(f.cursor_pos(), expected, "failed for {k:?} on {text:?} at {cursor:?}");
529 }
530 }
531
532 #[test]
533 fn move_cursor_up_cases() {
534 let cases: Vec<(&str, Option<usize>, usize, usize)> = vec![
536 ("hello world", Some(3), 10, 0), ("hello world", Some(8), 5, 3), ("hello", Some(3), 0, 3), ];
540 for (text, cursor, width, expected) in cases {
541 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
542 f.move_cursor_up(width);
543 assert_eq!(f.cursor_pos(), expected, "up failed: {text:?} cursor={cursor:?} w={width}");
544 }
545 }
546
547 #[test]
548 fn move_cursor_up_wide_chars() {
549 let mut f = field("中中中中中");
554 f.move_cursor_up(5);
555 assert_eq!(f.cursor_pos(), 9);
556 }
557
558 #[test]
559 fn move_cursor_down_cases() {
560 let cases: Vec<(&str, Option<usize>, usize, usize)> = vec![
562 ("hello world", Some(0), 20, 11), ("hello world", Some(3), 5, 8), ("hello world", Some(8), 5, 11), ("", None, 10, 0), ];
567 for (text, cursor, width, expected) in cases {
568 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
569 f.move_cursor_down(width);
570 assert_eq!(f.cursor_pos(), expected, "down failed: {text:?} cursor={cursor:?} w={width}");
571 }
572 }
573
574 #[test]
575 fn is_cursor_on_first_visual_line() {
576 let mut f = field_at("hello world", 3);
578 f.set_content_width(5);
579 assert!(f.is_cursor_on_first_visual_line()); let mut f = field_at("hello world", 8);
582 f.set_content_width(5);
583 assert!(!f.is_cursor_on_first_visual_line()); }
585
586 #[test]
587 fn is_cursor_on_last_visual_line() {
588 let mut f = field_at("hello world", 11);
590 f.set_content_width(5);
591 assert!(f.is_cursor_on_last_visual_line()); let mut f = field_at("hello world", 3);
594 f.set_content_width(5);
595 assert!(!f.is_cursor_on_last_visual_line()); let mut f = field_at("hello world", 8);
598 f.set_content_width(5);
599 assert!(!f.is_cursor_on_last_visual_line()); }
601
602 #[test]
603 fn single_line_is_both_first_and_last() {
604 let mut f = field_at("hello", 3);
605 f.set_content_width(20);
606 assert!(f.is_cursor_on_first_visual_line());
607 assert!(f.is_cursor_on_last_visual_line());
608 }
609
610 #[test]
611 fn empty_field_is_both_first_and_last() {
612 let mut f = field("");
613 f.set_content_width(20);
614 assert!(f.is_cursor_on_first_visual_line());
615 assert!(f.is_cursor_on_last_visual_line());
616 }
617}