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