1use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, MouseEvent};
4use ratatui::{
5 buffer::Buffer,
6 layout::{Offset, Position, Rect},
7 style::{Color, Style},
8 widgets::{Block, Paragraph, StatefulWidget, Widget},
9};
10use unicode_width::UnicodeWidthChar;
11
12#[derive(Debug, Default)]
14pub struct InputBoxState {
15 text: String,
17 cursor_position: usize,
20 text_length: usize,
22 ps_columns: util::PrefixSumVec,
24 cursor_offset: u16,
26 horizontal_scroll: u16,
28}
29
30impl InputBoxState {
31 #[must_use]
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 #[must_use]
37 pub const fn text(&self) -> &str {
38 self.text.as_str()
39 }
40
41 pub fn set_text(&mut self, new_text: &str) {
42 self.text = String::from(new_text);
43 self.text_length = self.text.chars().count();
44 self.cursor_position = self.text_length;
45 self.ps_columns.clear();
46 for c in self.text.chars() {
47 self.ps_columns
48 .push(UnicodeWidthChar::width(c).unwrap_or_default());
49 }
50 self.horizontal_scroll = 0;
52 }
53
54 pub fn clear(&mut self) {
55 self.text.clear();
56 self.cursor_position = 0;
57 self.text_length = 0;
58 self.ps_columns.clear();
59 self.horizontal_scroll = 0;
60 }
61
62 #[must_use]
63 pub const fn is_empty(&self) -> bool {
64 self.text.is_empty()
65 }
66
67 const fn move_cursor_left(&mut self) {
68 let mut min = self.cursor_position.saturating_sub(1);
69 if min > self.text_length {
70 min = self.text_length;
71 }
72 self.cursor_position = min;
73 }
74
75 const fn move_cursor_right(&mut self) {
76 let mut min = self.cursor_position.saturating_add(1);
77 if min > self.text_length {
78 min = self.text_length;
79 }
80 self.cursor_position = min;
81 }
82
83 const fn move_cursor_to_start(&mut self) {
84 self.cursor_position = 0;
85 }
86
87 const fn move_cursor_to_end(&mut self) {
88 self.cursor_position = self.text_length;
89 }
90
91 const fn update_cursor_offset(&mut self, new_offset: u16) {
92 self.cursor_offset = new_offset;
93 }
94
95 #[must_use]
96 pub const fn cursor_offset(&self) -> Offset {
97 Offset::new(self.cursor_offset as i32, 0)
98 }
99
100 fn enter_char(&mut self, new_char: char) {
101 let cursor_byte_index = self
104 .text
105 .chars()
106 .take(self.cursor_position)
107 .map(char::len_utf8)
108 .sum();
109
110 self.text.insert(cursor_byte_index, new_char);
111 self.text_length += 1;
112 self.ps_columns.insert(
113 self.cursor_position,
114 UnicodeWidthChar::width(new_char).unwrap_or_default(),
115 );
116
117 self.move_cursor_right();
118 }
119
120 fn delete_char(&mut self) {
122 if self.cursor_position == 0 {
123 return;
124 }
125
126 let mut chars = self.text.chars();
130
131 let mut new = chars
133 .by_ref()
134 .take(self.cursor_position - 1)
135 .collect::<String>();
136 chars.next();
138 new.extend(chars);
140
141 self.text = new;
142 self.text_length = self.text_length.saturating_sub(1);
143 self.ps_columns.remove(self.cursor_position - 1);
144 self.move_cursor_left();
145 }
146
147 fn delete_next_char(&mut self) {
149 let mut chars = self.text.chars();
152 let mut new = chars
153 .by_ref()
154 .take(self.cursor_position)
155 .collect::<String>();
156 chars.next();
157 new.extend(chars);
158
159 self.text = new;
160 self.text_length = self.text_length.saturating_sub(1);
161 self.ps_columns.remove(self.cursor_position);
162 }
163
164 const fn update_scroll(&mut self, view_width: u16) {
169 let cursor_column = self.ps_columns.get(self.cursor_position);
170 let scroll = self.horizontal_scroll as usize;
171 let view_end = scroll + view_width as usize;
172
173 #[allow(clippy::cast_possible_truncation)]
174 if cursor_column < scroll {
175 self.horizontal_scroll = cursor_column as u16;
177 } else if cursor_column > view_end {
178 self.horizontal_scroll = (cursor_column.saturating_sub(view_width as usize)) as u16;
181 }
182 }
184
185 const fn column_to_char_index(&self, column: usize) -> usize {
190 let mut left = 0;
193 let mut right = self.text_length;
194
195 while left < right {
196 let mid = (left + right).div_ceil(2);
197 if self.ps_columns.get(mid) <= column {
198 left = mid;
199 } else {
200 right = mid - 1;
201 }
202 }
203
204 left
205 }
206
207 pub fn handle_key_event(&mut self, key: KeyEvent) {
208 if key.kind != KeyEventKind::Press {
209 return;
210 }
211
212 match key.code {
213 KeyCode::Char(to_insert) => {
214 self.enter_char(to_insert);
215 }
216 KeyCode::Backspace => {
217 self.delete_char();
218 }
219 KeyCode::Delete => {
220 self.delete_next_char();
221 }
222 KeyCode::Left => {
223 self.move_cursor_left();
224 }
225 KeyCode::Right => {
226 self.move_cursor_right();
227 }
228 KeyCode::Home => {
229 self.move_cursor_to_start();
230 }
231 KeyCode::End => {
232 self.move_cursor_to_end();
233 }
234 _ => {}
235 }
236 }
237
238 pub fn handle_mouse_event(&mut self, mouse: MouseEvent, area: Rect) {
242 let MouseEvent {
243 kind, column, row, ..
244 } = mouse;
245 let mouse_position = Position::new(column, row);
246
247 if !area.contains(mouse_position) {
248 return;
249 }
250
251 if kind == crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left) {
252 let mouse_x = mouse_position.x.saturating_sub(area.x + 1);
254
255 let actual_column = (mouse_x + self.horizontal_scroll) as usize;
257
258 self.cursor_position = self.column_to_char_index(actual_column);
260 }
261 }
262}
263
264#[derive(Debug, Clone)]
268pub struct InputBox<'a> {
269 border: Option<Block<'a>>,
270 text_color: Color,
271}
272
273impl<'a> InputBox<'a> {
274 #[must_use]
275 pub const fn new() -> Self {
276 Self {
277 border: None,
278 text_color: Color::Reset,
279 }
280 }
281
282 #[must_use]
283 pub fn border(mut self, border: Block<'a>) -> Self {
284 self.border.replace(border);
285 self
286 }
287
288 #[must_use]
289 pub const fn text_color(mut self, color: Color) -> Self {
290 self.text_color = color;
291 self
292 }
293}
294
295impl Default for InputBox<'_> {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301impl StatefulWidget for InputBox<'_> {
302 type State = InputBoxState;
303
304 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
305 let inner_area = self.border.map_or(area, |border| {
307 let inner = border.inner(area);
308 border.render(area, buf);
309 inner
310 });
311
312 state.update_scroll(inner_area.width);
314
315 let cursor_column = state.ps_columns.get(state.cursor_position);
316
317 #[allow(clippy::cast_possible_truncation)]
318 state.update_cursor_offset(cursor_column as u16 - state.horizontal_scroll);
319 let input = Paragraph::new(state.text.as_str())
320 .style(Style::default().fg(self.text_color))
321 .scroll((0, state.horizontal_scroll));
322
323 input.render(inner_area, buf);
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use crate::test_utils::{assert_buffer_eq, setup_test_terminal};
330
331 use super::*;
332 use anyhow::Result;
333 use pretty_assertions::assert_eq;
334 use rstest::rstest;
335
336 #[test]
337 fn test_enter_delete() {
338 let mut input_box = InputBoxState::default();
339
340 input_box.enter_char('a');
341 assert_eq!(input_box.text, "a");
342 assert_eq!(input_box.cursor_position, 1);
343
344 input_box.enter_char('b');
345 assert_eq!(input_box.text, "ab");
346 assert_eq!(input_box.cursor_position, 2);
347
348 input_box.enter_char('c');
349 assert_eq!(input_box.text, "abc");
350 assert_eq!(input_box.cursor_position, 3);
351
352 input_box.move_cursor_left();
353 assert_eq!(input_box.cursor_position, 2);
354
355 input_box.delete_char();
356 assert_eq!(input_box.text, "ac");
357 assert_eq!(input_box.cursor_position, 1);
358
359 input_box.enter_char('d');
360 assert_eq!(input_box.text, "adc");
361 assert_eq!(input_box.cursor_position, 2);
362
363 input_box.move_cursor_right();
364 assert_eq!(input_box.cursor_position, 3);
365
366 input_box.clear();
367 assert_eq!(input_box.text, "");
368 assert_eq!(input_box.cursor_position, 0);
369
370 input_box.delete_char();
371 assert_eq!(input_box.text, "");
372 assert_eq!(input_box.cursor_position, 0);
373
374 input_box.delete_char();
375 assert_eq!(input_box.text, "");
376 assert_eq!(input_box.cursor_position, 0);
377 }
378
379 #[test]
380 fn test_enter_delete_non_ascii_char() {
381 let mut input_box = InputBoxState::default();
382
383 input_box.enter_char('a');
384 assert_eq!(input_box.text, "a");
385 assert_eq!(input_box.cursor_position, 1);
386 assert_eq!(input_box.text_length, 1);
387 assert_eq!(input_box.ps_columns.last(), 1);
388
389 input_box.enter_char('m');
390 assert_eq!(input_box.text, "am");
391 assert_eq!(input_box.cursor_position, 2);
392 assert_eq!(input_box.text_length, 2);
393 assert_eq!(input_box.ps_columns.last(), 2);
394
395 input_box.enter_char('é');
396 assert_eq!(input_box.text, "amé");
397 assert_eq!(input_box.cursor_position, 3);
398 assert_eq!(input_box.text_length, 3);
399 assert_eq!(input_box.ps_columns.last(), 3);
400
401 input_box.enter_char('l');
402 assert_eq!(input_box.text, "amél");
403 assert_eq!(input_box.cursor_position, 4);
404 assert_eq!(input_box.text_length, 4);
405 assert_eq!(input_box.ps_columns.last(), 4);
406
407 input_box.delete_char();
408 assert_eq!(input_box.text, "amé");
409 assert_eq!(input_box.cursor_position, 3);
410 assert_eq!(input_box.text_length, 3);
411 assert_eq!(input_box.ps_columns.last(), 3);
412
413 input_box.delete_char();
414 assert_eq!(input_box.text, "am");
415 assert_eq!(input_box.cursor_position, 2);
416 assert_eq!(input_box.text_length, 2);
417 assert_eq!(input_box.ps_columns.last(), 2);
418 }
419
420 #[test]
421 fn test_enter_delete_wide_characters() {
422 let mut input_box = InputBoxState::default();
423
424 input_box.enter_char('こ');
425 assert_eq!(input_box.text, "こ");
426 assert_eq!(input_box.cursor_position, 1);
427 assert_eq!(input_box.text_length, 1);
428 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 2);
429 assert_eq!(input_box.ps_columns.last(), 2);
430
431 input_box.enter_char('ん');
432 assert_eq!(input_box.text, "こん");
433 assert_eq!(input_box.cursor_position, 2);
434 assert_eq!(input_box.text_length, 2);
435 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 4);
436 assert_eq!(input_box.ps_columns.last(), 4);
437
438 input_box.enter_char('に');
439 assert_eq!(input_box.text, "こんに");
440 assert_eq!(input_box.cursor_position, 3);
441 assert_eq!(input_box.text_length, 3);
442 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 6);
443 assert_eq!(input_box.ps_columns.last(), 6);
444
445 input_box.enter_char('ち');
446 assert_eq!(input_box.text, "こんにち");
447 assert_eq!(input_box.cursor_position, 4);
448 assert_eq!(input_box.text_length, 4);
449 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 8);
450 assert_eq!(input_box.ps_columns.last(), 8);
451
452 input_box.enter_char('は');
453 assert_eq!(input_box.text, "こんにちは");
454 assert_eq!(input_box.cursor_position, 5);
455 assert_eq!(input_box.text_length, 5);
456 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 10);
457 assert_eq!(input_box.ps_columns.last(), 10);
458
459 input_box.delete_char();
460 assert_eq!(input_box.text, "こんにち");
461 assert_eq!(input_box.cursor_position, 4);
462 assert_eq!(input_box.text_length, 4);
463 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 8);
464 assert_eq!(input_box.ps_columns.last(), 8);
465
466 input_box.delete_char();
467 assert_eq!(input_box.text, "こんに");
468 assert_eq!(input_box.cursor_position, 3);
469 assert_eq!(input_box.text_length, 3);
470 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 6);
471 assert_eq!(input_box.ps_columns.last(), 6);
472 }
473
474 #[test]
475 fn test_move_left_right() {
476 let mut input_box = InputBoxState::default();
477
478 input_box.set_text("héこ👨\u{200B}");
484 assert_eq!(input_box.text, "héこ👨");
485 assert_eq!(input_box.cursor_position, 5);
486 assert_eq!(input_box.text_length, 5);
487 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 6);
488 assert_eq!(input_box.ps_columns.last(), 6);
489
490 input_box.move_cursor_left();
491 assert_eq!(input_box.cursor_position, 4);
492 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 6);
493
494 input_box.move_cursor_left();
495 assert_eq!(input_box.cursor_position, 3);
496 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 4);
497 input_box.move_cursor_left();
498 assert_eq!(input_box.cursor_position, 2);
499 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 2);
500 input_box.move_cursor_left();
501 assert_eq!(input_box.cursor_position, 1);
502 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 1);
503 input_box.move_cursor_left();
504 assert_eq!(input_box.cursor_position, 0);
505 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 0);
506 input_box.move_cursor_left();
507 assert_eq!(input_box.cursor_position, 0);
508 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 0);
509 input_box.move_cursor_right();
510 assert_eq!(input_box.cursor_position, 1);
511 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 1);
512 input_box.move_cursor_right();
513 assert_eq!(input_box.cursor_position, 2);
514 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 2);
515 input_box.move_cursor_right();
516 assert_eq!(input_box.cursor_position, 3);
517 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 4);
518 input_box.move_cursor_right();
519 assert_eq!(input_box.cursor_position, 4);
520 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 6);
521 input_box.move_cursor_right();
522 assert_eq!(input_box.cursor_position, 5);
523 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 6);
524 input_box.move_cursor_right();
525 assert_eq!(input_box.cursor_position, 5);
526 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 6);
527 }
528
529 #[test]
530 fn test_enter_delete_middle() {
531 let mut input_box = InputBoxState::default();
532
533 input_box.set_text("ace");
534 assert_eq!(input_box.text, "ace");
535 assert_eq!(input_box.cursor_position, 3);
536
537 input_box.move_cursor_left();
538 input_box.enter_char('Ü');
539 assert_eq!(input_box.text, "acÜe");
540 assert_eq!(input_box.cursor_position, 3);
541 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 3);
542 assert_eq!(input_box.text_length, 4);
543 assert_eq!(input_box.ps_columns.last(), 4);
544
545 input_box.move_cursor_left();
546 input_box.move_cursor_left();
547 input_box.enter_char('X');
548 assert_eq!(input_box.text, "aXcÜe");
549 assert_eq!(input_box.cursor_position, 2);
550 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 2);
551 assert_eq!(input_box.text_length, 5);
552 assert_eq!(input_box.ps_columns.last(), 5);
553
554 input_box.enter_char('こ');
556 input_box.enter_char('い');
557 assert_eq!(input_box.text, "aXこいcÜe");
558 assert_eq!(input_box.cursor_position, 4);
559 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 6);
560 assert_eq!(input_box.text_length, 7);
561 assert_eq!(input_box.ps_columns.last(), 9);
562
563 input_box.move_cursor_left();
564 input_box.delete_char();
565 assert_eq!(input_box.text, "aXいcÜe");
566 assert_eq!(input_box.cursor_position, 2);
567 assert_eq!(input_box.ps_columns.get(input_box.cursor_position), 2);
568 assert_eq!(input_box.text_length, 6);
569 assert_eq!(input_box.ps_columns.last(), 7);
570
571 input_box.delete_next_char();
572 assert_eq!(input_box.text, "aXcÜe");
573 assert_eq!(input_box.cursor_position, 2);
574 assert_eq!(input_box.text_length, 5);
575 assert_eq!(input_box.ps_columns.last(), 5);
576
577 input_box.delete_char();
578 assert_eq!(input_box.text, "acÜe");
579 assert_eq!(input_box.cursor_position, 1);
580 assert_eq!(input_box.text_length, 4);
581 assert_eq!(input_box.ps_columns.last(), 4);
582
583 input_box.move_cursor_right();
584 input_box.delete_next_char();
585 assert_eq!(input_box.text, "ace");
586 assert_eq!(input_box.cursor_position, 2);
587 }
588
589 #[test]
590 fn test_input_box_is_empty() {
591 let input_box = InputBoxState::default();
592 assert!(input_box.is_empty());
593
594 let mut input_box = InputBoxState::default();
595 input_box.set_text("abc");
596
597 assert!(!input_box.is_empty());
598 }
599
600 #[test]
601 fn test_input_box_text() {
602 let mut input_box = InputBoxState::default();
603 input_box.set_text("abc");
604
605 assert_eq!(input_box.text(), "abc");
606 }
607
608 #[rstest]
609 fn test_input_box_render(
610 #[values(10, 20)] width: u16,
611 #[values(1, 2, 3, 4, 5, 6)] height: u16,
612 ) -> Result<()> {
613 use ratatui::{buffer::Buffer, text::Line};
614
615 let (mut terminal, _) = setup_test_terminal(width, height);
616 let mut state = InputBoxState::default();
617 state.set_text("Hello, World!");
618 let area = Rect::new(0, 0, width, height);
619
620 let buffer = terminal
621 .draw(|frame| {
622 frame.render_stateful_widget(
623 InputBox::new().border(Block::bordered()),
624 area,
625 &mut state,
626 )
627 })?
628 .buffer
629 .clone();
630
631 let line_top = Line::raw(String::from("┌") + &"─".repeat((width - 2).into()) + "┐");
632 let line_text = if width > 15 {
633 Line::raw(String::from("│Hello, World!") + &" ".repeat((width - 15).into()) + "│")
634 } else {
635 Line::raw(
636 String::from("│")
637 + &"Hello, World!"
638 .chars()
639 .skip(state.text().len() - (width - 2) as usize)
640 .collect::<String>()
641 + "│",
642 )
643 };
644 let line_empty = Line::raw(String::from("│") + &" ".repeat((width - 2).into()) + "│");
645 let line_bottom = Line::raw(String::from("└") + &"─".repeat((width - 2).into()) + "┘");
646
647 let expected = Buffer::with_lines(match height {
648 0 => unreachable!(),
649 1 => vec![line_top].into_iter(),
650 2 => vec![line_top, line_bottom].into_iter(),
651 3 => vec![line_top, line_text, line_bottom].into_iter(),
652 other => vec![line_top, line_text]
653 .into_iter()
654 .chain(
655 std::iter::repeat_n(line_empty, (other - 3).into())
656 .chain(std::iter::once(line_bottom)),
657 )
658 .collect::<Vec<_>>()
659 .into_iter(),
660 });
661
662 assert_eq!(buffer, expected);
663
664 Ok(())
665 }
666
667 #[rstest]
668 #[case::fits("Hello", 10, "Hello ")]
669 #[case::exact_fit("Hello, World!", 13, "Hello, World!")]
670 #[case::too_small("Hello, World!", 6, "World!")]
671 #[case::too_small_wide("こんにちは世界", 10, "にちは世界")]
672 fn test_keeps_cursor_visible_right(
673 #[case] new_text: &str,
674 #[case] view_width: u16,
675 #[case] expected_visible_text: &str,
676 ) -> Result<()> {
677 use ratatui::{buffer::Buffer, text::Line};
678
679 let (mut terminal, _) = setup_test_terminal(view_width, 1);
680 let mut state = InputBoxState::default();
681 state.set_text(new_text);
682
683 let area = Rect::new(0, 0, view_width, 1);
684 let buffer = terminal
685 .draw(|frame| frame.render_stateful_widget(InputBox::new(), area, &mut state))?
686 .buffer
687 .clone();
688 let line = Line::raw(expected_visible_text.to_string());
689 let expected = Buffer::with_lines(std::iter::once(line));
690 assert_buffer_eq(&buffer, &expected);
691 Ok(())
692 }
693
694 #[test]
695 fn test_column_to_char_index() {
696 let mut input_box = InputBoxState::default();
697
698 input_box.set_text("Hello, World!");
700 assert_eq!(input_box.column_to_char_index(0), 0);
701 assert_eq!(input_box.column_to_char_index(1), 1);
702 assert_eq!(input_box.column_to_char_index(5), 5);
703 assert_eq!(input_box.column_to_char_index(13), 13);
704 assert_eq!(input_box.column_to_char_index(100), 13); input_box.set_text("こんにち");
708 assert_eq!(input_box.column_to_char_index(0), 0);
710 assert_eq!(input_box.column_to_char_index(1), 0); assert_eq!(input_box.column_to_char_index(2), 1); assert_eq!(input_box.column_to_char_index(3), 1); assert_eq!(input_box.column_to_char_index(4), 2); assert_eq!(input_box.column_to_char_index(6), 3); assert_eq!(input_box.column_to_char_index(8), 4); input_box.set_text("aこbに");
719 assert_eq!(input_box.column_to_char_index(0), 0); assert_eq!(input_box.column_to_char_index(1), 1); assert_eq!(input_box.column_to_char_index(2), 1); assert_eq!(input_box.column_to_char_index(3), 2); assert_eq!(input_box.column_to_char_index(4), 3); assert_eq!(input_box.column_to_char_index(5), 3); assert_eq!(input_box.column_to_char_index(6), 4); }
728
729 #[test]
730 fn test_smooth_scrolling_maintains_position() {
731 let mut input_box = InputBoxState::default();
732 input_box.set_text("Hello, World! This is long text!"); assert_eq!(input_box.text_length, 32);
736 assert_eq!(input_box.cursor_position, 32);
737
738 input_box.update_scroll(10);
740 assert_eq!(input_box.horizontal_scroll, 22); for _ in 0..3 {
744 input_box.move_cursor_left();
745 }
746 input_box.update_scroll(10);
747 assert_eq!(input_box.cursor_position, 29);
749 assert_eq!(input_box.horizontal_scroll, 22);
750
751 for _ in 0..5 {
753 input_box.move_cursor_left();
754 }
755 input_box.update_scroll(10);
756 assert_eq!(input_box.cursor_position, 24);
758 assert_eq!(input_box.horizontal_scroll, 22);
759
760 for _ in 0..5 {
762 input_box.move_cursor_left();
763 }
764 input_box.update_scroll(10);
765 assert_eq!(input_box.cursor_position, 19);
767 assert_eq!(input_box.horizontal_scroll, 19);
768 }
769
770 #[test]
771 fn test_smooth_scrolling_right_movement() {
772 let mut input_box = InputBoxState::default();
773 input_box.set_text("Hello, World! This is long text!"); input_box.move_cursor_to_start();
777 assert_eq!(input_box.cursor_position, 0);
778 assert_eq!(input_box.horizontal_scroll, 0);
779
780 for _ in 0..5 {
782 input_box.move_cursor_right();
783 }
784 input_box.update_scroll(10);
785 assert_eq!(input_box.cursor_position, 5);
787 assert_eq!(input_box.horizontal_scroll, 0);
788
789 for _ in 0..4 {
791 input_box.move_cursor_right();
792 }
793 input_box.update_scroll(10);
794 assert_eq!(input_box.cursor_position, 9);
796 assert_eq!(input_box.horizontal_scroll, 0);
797
798 input_box.move_cursor_right();
800 input_box.update_scroll(10);
801 assert_eq!(input_box.cursor_position, 10);
803 assert_eq!(input_box.horizontal_scroll, 0);
804
805 input_box.move_cursor_right();
807 input_box.update_scroll(10);
808 assert_eq!(input_box.cursor_position, 11);
810 assert_eq!(input_box.horizontal_scroll, 1); }
812
813 #[test]
814 fn test_smooth_scrolling_with_wide_chars() {
815 let mut input_box = InputBoxState::default();
816 input_box.set_text("こんにちは世界です。"); assert_eq!(input_box.cursor_position, 10);
820 input_box.update_scroll(10);
821 assert_eq!(input_box.horizontal_scroll, 10); for _ in 0..3 {
826 input_box.move_cursor_left();
827 }
828 input_box.update_scroll(10);
829 assert_eq!(input_box.cursor_position, 7);
831 assert_eq!(input_box.horizontal_scroll, 10);
832
833 for _ in 0..3 {
835 input_box.move_cursor_left();
836 }
837 input_box.update_scroll(10);
838 assert_eq!(input_box.cursor_position, 4);
840 assert_eq!(input_box.horizontal_scroll, 8);
841 }
842
843 #[test]
844 fn test_smooth_scrolling_edge_cases() {
845 let mut input_box = InputBoxState::default();
846
847 input_box.set_text("");
849 input_box.update_scroll(10);
850 assert_eq!(input_box.horizontal_scroll, 0);
851
852 input_box.set_text("Hi");
854 input_box.cursor_position = 2;
855 input_box.update_scroll(10);
856 assert_eq!(input_box.horizontal_scroll, 0);
857
858 input_box.set_text("Long text here");
860 input_box.cursor_position = 14;
861 input_box.update_scroll(5);
862 assert!(input_box.horizontal_scroll > 0);
863 input_box.clear();
864 assert_eq!(input_box.horizontal_scroll, 0);
865 }
866
867 #[test]
868 fn test_mouse_click_no_scroll() {
869 let mut input_box = InputBoxState::default();
870 input_box.set_text("Hello");
871
872 let mouse_event = MouseEvent {
874 kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
875 column: 3, row: 1,
877 modifiers: crossterm::event::KeyModifiers::empty(),
878 };
879 let area = Rect::new(1, 1, 10, 1);
880 input_box.handle_mouse_event(mouse_event, area);
881 assert_eq!(input_box.cursor_position, 1);
882 }
883
884 #[test]
885 fn test_mouse_click_with_scroll_ascii() {
886 let mut input_box = InputBoxState::default();
887 input_box.set_text("Hello, World!");
888
889 for _ in 0..13 {
891 input_box.move_cursor_right();
892 }
893 input_box.update_scroll(10);
894
895 assert_eq!(input_box.horizontal_scroll, 3);
897
898 let mouse_event = MouseEvent {
901 kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
902 column: 6, row: 0,
904 modifiers: crossterm::event::KeyModifiers::empty(),
905 };
906 let area = Rect::new(0, 0, 12, 1);
907 input_box.handle_mouse_event(mouse_event, area);
908 assert_eq!(input_box.cursor_position, 8);
909 }
910
911 #[test]
912 fn test_mouse_click_with_scroll_wide_chars() {
913 let mut input_box = InputBoxState::default();
914 input_box.set_text("こんにちは世界"); for _ in 0..7 {
918 input_box.move_cursor_right();
919 }
920 input_box.update_scroll(10);
921
922 assert_eq!(input_box.horizontal_scroll, 4);
924
925 let mouse_event = MouseEvent {
929 kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
930 column: 7, row: 0,
932 modifiers: crossterm::event::KeyModifiers::empty(),
933 };
934 let area = Rect::new(0, 0, 12, 1);
935 input_box.handle_mouse_event(mouse_event, area);
936 assert_eq!(input_box.cursor_position, 5);
937 }
938
939 #[test]
940 fn test_mouse_click_beyond_text_end() {
941 let mut input_box = InputBoxState::default();
942 input_box.set_text("Hi");
943
944 let mouse_event = MouseEvent {
946 kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
947 column: 20,
948 row: 0,
949 modifiers: crossterm::event::KeyModifiers::empty(),
950 };
951 let area = Rect::new(0, 0, 30, 1);
952 input_box.handle_mouse_event(mouse_event, area);
953 assert_eq!(input_box.cursor_position, 2); }
955
956 #[test]
957 fn test_mouse_click_on_wide_char_boundary() {
958 let mut input_box = InputBoxState::default();
959 input_box.set_text("aこb");
960
961 let mouse_event = MouseEvent {
964 kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
965 column: 3, row: 0,
967 modifiers: crossterm::event::KeyModifiers::empty(),
968 };
969 let area = Rect::new(0, 0, 10, 1);
970 input_box.handle_mouse_event(mouse_event, area);
971 assert_eq!(input_box.cursor_position, 1);
972 }
973}
974
975mod util {
976 #[derive(Debug)]
983 pub struct PrefixSumVec {
984 data: Vec<usize>,
985 }
986 impl PrefixSumVec {
987 pub fn new() -> Self {
988 Self { data: vec![0] }
989 }
990
991 pub const fn last(&self) -> usize {
992 if let [.., last] = self.data.as_slice() {
993 *last
994 } else {
995 unreachable!() }
997 }
998
999 pub fn push(&mut self, value: usize) {
1000 let last = self.last();
1001 self.data.push(last + value);
1002 }
1003
1004 pub fn insert(&mut self, index: usize, value: usize) {
1005 let index = index + 1;
1007
1008 if index >= self.data.len() {
1010 self.push(value);
1011 return;
1012 }
1013
1014 let mut prev = self.data[index - 1];
1018 for i in index..self.data.len() {
1019 let current = self.data[i];
1020 self.data[i] = prev + value;
1021 prev = current;
1022 }
1023 self.data.push(prev + value);
1024 }
1025
1026 pub fn remove(&mut self, index: usize) {
1027 if self.data.len() <= 1 {
1028 return;
1030 }
1031
1032 let index = index + 1;
1034
1035 if index >= self.data.len() {
1037 self.data.pop();
1038 return;
1039 }
1040
1041 for i in index..self.data.len() - 1 {
1043 let prev = self.data[i - 1];
1044 let next = self.data[i + 1];
1045 self.data[i] = prev + (next - self.data[i]);
1046 }
1047 self.data.pop();
1048 }
1049
1050 pub const fn get(&self, index: usize) -> usize {
1051 self.data.as_slice()[index]
1052 }
1053
1054 pub fn clear(&mut self) {
1055 self.data.clear();
1056 self.data.push(0);
1057 }
1058 }
1059 impl Default for PrefixSumVec {
1060 fn default() -> Self {
1061 Self::new()
1062 }
1063 }
1064 #[cfg(test)]
1065 mod tests {
1066 use super::PrefixSumVec;
1067 use pretty_assertions::assert_eq;
1068
1069 #[test]
1070 fn test_prefix_sum_vec_basic_operations() {
1071 let mut psv = PrefixSumVec::new();
1072 assert_eq!(psv.last(), 0);
1073 assert_eq!(psv.data, vec![0]);
1074 assert_eq!(psv.last(), 0);
1075 psv.remove(0); assert_eq!(psv.data, vec![0]);
1077 assert_eq!(psv.last(), 0);
1078 psv.push(3);
1079 assert_eq!(psv.data, vec![0, 3]);
1080 assert_eq!(psv.last(), 3);
1081 psv.push(5);
1082 assert_eq!(psv.data, vec![0, 3, 8]);
1083 assert_eq!(psv.last(), 8);
1084 psv.insert(1, 2); assert_eq!(psv.data, vec![0, 3, 5, 10]);
1086 assert_eq!(psv.last(), 10);
1087 psv.insert(0, 7); assert_eq!(psv.data, vec![0, 7, 10, 12, 17]);
1089 assert_eq!(psv.last(), 17);
1090 psv.remove(2); assert_eq!(psv.data, vec![0, 7, 10, 15]);
1092 assert_eq!(psv.last(), 15);
1093 psv.remove(0); assert_eq!(psv.data, vec![0, 3, 8]);
1095 psv.remove(1); assert_eq!(psv.data, vec![0, 3]);
1097 assert_eq!(psv.get(0), 0);
1098 assert_eq!(psv.get(1), 3);
1099 psv.insert(1, 4); assert_eq!(psv.data, vec![0, 3, 7]);
1101 psv.clear();
1102 assert_eq!(psv.data, vec![0]);
1103 }
1104 }
1105}