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