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::Right if alt || ctrl => {
234 self.word_end_forward();
235 Some(vec![])
236 }
237 KeyCode::Delete => {
238 self.delete_after_cursor();
239 Some(vec![])
240 }
241 KeyCode::Char(c) if !ctrl => {
242 self.insert_at_cursor(c);
243 Some(vec![])
244 }
245 KeyCode::Backspace => {
246 self.delete_before_cursor();
247 Some(vec![])
248 }
249 KeyCode::Left => {
250 self.cursor_pos =
251 self.value[..self.cursor_pos].char_indices().next_back().map_or(0, |(i, _)| i);
252 Some(vec![])
253 }
254 KeyCode::Right => {
255 if let Some(c) = self.value[self.cursor_pos..].chars().next() {
256 self.cursor_pos += c.len_utf8();
257 }
258 Some(vec![])
259 }
260 KeyCode::Home => {
261 self.cursor_pos = 0;
262 Some(vec![])
263 }
264 KeyCode::End => {
265 self.cursor_pos = self.value.len();
266 Some(vec![])
267 }
268 KeyCode::Up => {
269 self.move_cursor_up(self.content_width);
270 Some(vec![])
271 }
272 KeyCode::Down => {
273 self.move_cursor_down(self.content_width);
274 Some(vec![])
275 }
276 _ => None,
277 }
278 }
279 Event::Paste(text) => {
280 self.insert_str_at_cursor(text);
281 Some(vec![])
282 }
283 _ => None,
284 }
285 }
286
287 fn render(&mut self, context: &ViewContext) -> Frame {
288 Frame::new(self.render_field(context, true))
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crossterm::event::{KeyEvent, KeyModifiers};
296
297 fn key(code: KeyCode) -> KeyEvent {
298 KeyEvent::new(code, KeyModifiers::NONE)
299 }
300 fn ctrl(code: KeyCode) -> KeyEvent {
301 KeyEvent::new(code, KeyModifiers::CONTROL)
302 }
303 fn alt(code: KeyCode) -> KeyEvent {
304 KeyEvent::new(code, KeyModifiers::ALT)
305 }
306 fn field(text: &str) -> TextField {
307 TextField::new(text.to_string())
308 }
309 fn field_at(text: &str, cursor: usize) -> TextField {
310 let mut f = field(text);
311 f.set_cursor_pos(cursor);
312 f
313 }
314
315 async fn send(f: &mut TextField, evt: Event) -> Option<Vec<()>> {
316 f.on_event(&evt).await
317 }
318 async fn send_key(f: &mut TextField, k: KeyEvent) -> Option<Vec<()>> {
319 send(f, Event::Key(k)).await
320 }
321
322 fn assert_state(f: &TextField, value: &str, cursor: usize) {
324 assert_eq!(f.value, value, "value mismatch");
325 assert_eq!(f.cursor_pos(), cursor, "cursor mismatch");
326 }
327
328 #[tokio::test]
329 async fn typing_appends_characters() {
330 let mut f = field("");
331 send_key(&mut f, key(KeyCode::Char('h'))).await;
332 send_key(&mut f, key(KeyCode::Char('i'))).await;
333 assert_eq!(f.value, "hi");
334 }
335
336 #[tokio::test]
337 async fn backspace_variants() {
338 let mut f = field("abc");
340 send_key(&mut f, key(KeyCode::Backspace)).await;
341 assert_eq!(f.value, "ab");
342
343 let mut f = field("");
345 send_key(&mut f, key(KeyCode::Backspace)).await;
346 assert_eq!(f.value, "");
347
348 let mut f = field_at("hello", 3);
350 send_key(&mut f, key(KeyCode::Backspace)).await;
351 assert_state(&f, "helo", 2);
352 }
353
354 #[test]
355 fn to_json_returns_string_value() {
356 assert_eq!(field("hello").to_json(), serde_json::json!("hello"));
357 }
358
359 #[tokio::test]
360 async fn unhandled_keys_are_ignored() {
361 let mut f = field("");
362 assert!(send_key(&mut f, key(KeyCode::F(1))).await.is_none());
363 }
364
365 #[tokio::test]
366 async fn paste_variants() {
367 let mut f = field("");
369 let outcome = send(&mut f, Event::Paste("hello".into())).await;
370 assert!(outcome.is_some());
371 assert_eq!(f.value, "hello");
372
373 let mut f = field_at("hd", 1);
375 send(&mut f, Event::Paste("ello worl".into())).await;
376 assert_state(&f, "hello world", 10);
377 }
378
379 #[test]
380 fn cursor_starts_at_end() {
381 assert_eq!(field("hello").cursor_pos(), 5);
382 }
383
384 #[tokio::test]
385 async fn cursor_movement_single_keys() {
386 let cases: Vec<(&str, Option<usize>, KeyEvent, usize)> = vec![
388 ("hello", None, key(KeyCode::Left), 4),
389 ("hello", None, key(KeyCode::Right), 5),
390 ("", None, key(KeyCode::Left), 0),
391 ("hello", None, key(KeyCode::Home), 0),
392 ("hello", Some(0), key(KeyCode::End), 5),
393 ("hello", None, ctrl(KeyCode::Char('a')), 0),
394 ("hello", Some(0), ctrl(KeyCode::Char('e')), 5),
395 ];
396 for (text, cursor, k, expected) in cases {
397 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
398 send_key(&mut f, k).await;
399 assert_eq!(f.cursor_pos(), expected, "failed for key {k:?} on {text:?}");
400 }
401 }
402
403 #[tokio::test]
404 async fn insert_at_middle() {
405 let mut f = field_at("hllo", 1);
406 send_key(&mut f, key(KeyCode::Char('e'))).await;
407 assert_state(&f, "hello", 2);
408 }
409
410 #[tokio::test]
411 async fn multibyte_utf8_navigation() {
412 let mut f = field("a中b");
413 assert_eq!(f.cursor_pos(), 5);
414 for expected in [4, 1, 0] {
415 send_key(&mut f, key(KeyCode::Left)).await;
416 assert_eq!(f.cursor_pos(), expected);
417 }
418 for expected in [1, 4] {
419 send_key(&mut f, key(KeyCode::Right)).await;
420 assert_eq!(f.cursor_pos(), expected);
421 }
422 }
423
424 #[test]
425 fn set_value_moves_cursor_to_end() {
426 let mut f = field("");
427 f.set_value("hello".to_string());
428 assert_state(&f, "hello", 5);
429 }
430
431 #[test]
432 fn clear_resets_cursor() {
433 let mut f = field("hello");
434 f.clear();
435 assert_state(&f, "", 0);
436 }
437
438 #[tokio::test]
439 async fn delete_forward_variants() {
440 let mut f = field_at("hello", 2);
442 send_key(&mut f, key(KeyCode::Delete)).await;
443 assert_state(&f, "helo", 2);
444
445 let mut f = field("hello");
447 send_key(&mut f, key(KeyCode::Delete)).await;
448 assert_eq!(f.value, "hello");
449
450 let mut f = field_at("a中b", 1);
452 send_key(&mut f, key(KeyCode::Delete)).await;
453 assert_state(&f, "ab", 1);
454 }
455
456 #[tokio::test]
457 async fn ctrl_w_variants() {
458 let cases: Vec<(&str, Option<usize>, &str, usize)> = vec![
460 ("hello world", None, "hello ", 6),
461 ("hello ", None, "", 0),
462 ("hello", Some(0), "hello", 0),
463 ("hello world", Some(8), "hello rld", 6),
464 ("", None, "", 0),
465 ];
466 for (text, cursor, exp_val, exp_cur) in cases {
467 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
468 send_key(&mut f, ctrl(KeyCode::Char('w'))).await;
469 assert_state(&f, exp_val, exp_cur);
470 }
471 }
472
473 #[tokio::test]
474 async fn alt_backspace_deletes_word() {
475 let mut f = field("hello world");
476 send_key(&mut f, alt(KeyCode::Backspace)).await;
477 assert_eq!(f.value, "hello ");
478 }
479
480 #[tokio::test]
481 async fn ctrl_u_variants() {
482 let mut f = field_at("hello world", 5);
483 send_key(&mut f, ctrl(KeyCode::Char('u'))).await;
484 assert_state(&f, " world", 0);
485
486 let mut f = field_at("hello", 0);
488 send_key(&mut f, ctrl(KeyCode::Char('u'))).await;
489 assert_state(&f, "hello", 0);
490 }
491
492 #[tokio::test]
493 async fn ctrl_k_variants() {
494 let mut f = field_at("hello world", 5);
495 send_key(&mut f, ctrl(KeyCode::Char('k'))).await;
496 assert_state(&f, "hello", 5);
497
498 let mut f = field("hello");
500 send_key(&mut f, ctrl(KeyCode::Char('k'))).await;
501 assert_eq!(f.value, "hello");
502 }
503
504 #[tokio::test]
505 async fn word_navigation() {
506 let cases: Vec<(&str, Option<usize>, KeyEvent, usize)> = vec![
508 ("hello world", None, alt(KeyCode::Left), 6),
509 ("hello world", Some(8), alt(KeyCode::Left), 6),
510 ("hello", Some(0), alt(KeyCode::Left), 0),
511 ("hello world", None, ctrl(KeyCode::Left), 6),
512 ("hello world", Some(0), alt(KeyCode::Right), 6),
513 ("hello", None, alt(KeyCode::Right), 5),
514 ("a中 b", Some(0), alt(KeyCode::Right), 5),
515 ("hello world", Some(0), ctrl(KeyCode::Right), 6),
516 ];
517 for (text, cursor, k, expected) in cases {
518 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
519 send_key(&mut f, k).await;
520 assert_eq!(f.cursor_pos(), expected, "failed for {k:?} on {text:?} at {cursor:?}");
521 }
522 }
523
524 #[test]
525 fn move_cursor_up_cases() {
526 let cases: Vec<(&str, Option<usize>, usize, usize)> = vec![
528 ("hello world", Some(3), 10, 0), ("hello world", Some(8), 5, 3), ("hello", Some(3), 0, 3), ];
532 for (text, cursor, width, expected) in cases {
533 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
534 f.move_cursor_up(width);
535 assert_eq!(f.cursor_pos(), expected, "up failed: {text:?} cursor={cursor:?} w={width}");
536 }
537 }
538
539 #[test]
540 fn move_cursor_up_wide_chars() {
541 let mut f = field("中中中中中");
546 f.move_cursor_up(5);
547 assert_eq!(f.cursor_pos(), 9);
548 }
549
550 #[test]
551 fn move_cursor_down_cases() {
552 let cases: Vec<(&str, Option<usize>, usize, usize)> = vec![
554 ("hello world", Some(0), 20, 11), ("hello world", Some(3), 5, 8), ("hello world", Some(8), 5, 11), ("", None, 10, 0), ];
559 for (text, cursor, width, expected) in cases {
560 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
561 f.move_cursor_down(width);
562 assert_eq!(f.cursor_pos(), expected, "down failed: {text:?} cursor={cursor:?} w={width}");
563 }
564 }
565
566 #[test]
567 fn is_cursor_on_first_visual_line() {
568 let mut f = field_at("hello world", 3);
570 f.set_content_width(5);
571 assert!(f.is_cursor_on_first_visual_line()); let mut f = field_at("hello world", 8);
574 f.set_content_width(5);
575 assert!(!f.is_cursor_on_first_visual_line()); }
577
578 #[test]
579 fn is_cursor_on_last_visual_line() {
580 let mut f = field_at("hello world", 11);
582 f.set_content_width(5);
583 assert!(f.is_cursor_on_last_visual_line()); let mut f = field_at("hello world", 3);
586 f.set_content_width(5);
587 assert!(!f.is_cursor_on_last_visual_line()); let mut f = field_at("hello world", 8);
590 f.set_content_width(5);
591 assert!(!f.is_cursor_on_last_visual_line()); }
593
594 #[test]
595 fn single_line_is_both_first_and_last() {
596 let mut f = field_at("hello", 3);
597 f.set_content_width(20);
598 assert!(f.is_cursor_on_first_visual_line());
599 assert!(f.is_cursor_on_last_visual_line());
600 }
601
602 #[test]
603 fn empty_field_is_both_first_and_last() {
604 let mut f = field("");
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}