1use unicode_segmentation::UnicodeSegmentation;
10use unicode_width::UnicodeWidthStr;
11
12#[derive(Debug, Default, Clone, PartialEq, Eq)]
13pub struct TextInput {
14 chars: Vec<String>,
17 cursor: usize,
18 sel_anchor: Option<usize>,
20 scroll: usize,
21 max_len: usize,
22 max_bytes: usize,
23}
24
25#[derive(Debug, Clone)]
28pub struct View {
29 pub text: String,
30 pub cursor_col: u16,
31 pub sel_cols: Option<(u16, u16)>,
33}
34
35#[derive(Debug, Clone)]
37pub struct WrappedLine {
38 pub text: String,
39 pub sel_cols: Option<(u16, u16)>,
40 pub start: usize,
42 pub end: usize,
43}
44
45#[derive(Debug, Clone)]
47pub struct WrappedView {
48 pub lines: Vec<WrappedLine>,
49 pub cursor_row: u16,
51 pub cursor_col: u16,
52}
53
54pub fn wrap_breaks(chars: &[String], width: usize) -> Vec<(usize, usize)> {
57 if chars.is_empty() {
58 return vec![(0, 0)];
59 }
60 let width = width.max(1);
61 let mut lines = Vec::new();
62 let mut start = 0usize;
63 let mut col = 0usize;
64 let mut last_ws: Option<usize> = None;
65 let mut i = 0usize;
66
67 while i < chars.len() {
68 let w = chars[i].width();
69 if w > width {
70 if i > start {
72 lines.push((start, i));
73 }
74 lines.push((i, i + 1));
75 i += 1;
76 start = i;
77 col = 0;
78 last_ws = None;
79 continue;
80 }
81 if col + w > width && i > start {
82 let end = last_ws
84 .filter(|&ws| ws >= start)
85 .map(|ws| ws + 1)
86 .unwrap_or(i);
87 let end = end.max(start + 1);
88 lines.push((start, end));
89 start = end;
90 i = start;
91 col = 0;
92 last_ws = None;
93 continue;
94 }
95 if is_whitespace(&chars[i]) {
96 last_ws = Some(i);
97 }
98 col += w;
99 i += 1;
100 }
101 if start <= chars.len() {
102 lines.push((start, chars.len()));
103 }
104 if lines.is_empty() {
105 lines.push((0, 0));
106 }
107 lines
108}
109
110fn is_whitespace(grapheme: &str) -> bool {
111 !grapheme.is_empty() && grapheme.chars().all(char::is_whitespace)
112}
113
114impl TextInput {
115 pub fn new(initial: &str, max_len: usize) -> Self {
116 let max_bytes = crate::model::text_byte_limit(max_len);
117 let chars = bounded_graphemes(initial, max_len, max_bytes);
118 let cursor = chars.len();
119 Self {
120 chars,
121 cursor,
122 sel_anchor: None,
123 scroll: 0,
124 max_len,
125 max_bytes,
126 }
127 }
128
129 pub fn value(&self) -> String {
130 self.chars.concat()
131 }
132
133 pub fn is_empty(&self) -> bool {
134 self.chars.is_empty()
135 }
136
137 pub fn cursor(&self) -> usize {
138 self.cursor
139 }
140
141 pub fn len(&self) -> usize {
142 self.chars.len()
143 }
144
145 pub fn slice(&self, start: usize, end: usize) -> String {
146 let start = start.min(self.chars.len());
147 let end = end.max(start).min(self.chars.len());
148 self.chars[start..end].concat()
149 }
150
151 pub fn clear_selection(&mut self) {
152 self.sel_anchor = None;
153 }
154
155 pub fn has_selection(&self) -> bool {
156 self.selection_range().is_some()
157 }
158
159 pub fn selection_range(&self) -> Option<(usize, usize)> {
161 let a = self.sel_anchor?;
162 let (lo, hi) = if a <= self.cursor {
163 (a, self.cursor)
164 } else {
165 (self.cursor, a)
166 };
167 (lo < hi).then_some((lo, hi))
168 }
169
170 pub fn selected_text(&self) -> Option<String> {
171 let (lo, hi) = self.selection_range()?;
172 Some(self.chars[lo..hi].concat())
173 }
174
175 pub fn delete_selection(&mut self) -> bool {
177 let Some((lo, hi)) = self.selection_range() else {
178 return false;
179 };
180 self.replace_range(lo, hi, "");
181 true
182 }
183
184 pub fn select_word(&mut self) {
186 if self.chars.is_empty() {
187 self.sel_anchor = None;
188 return;
189 }
190 let n = self.chars.len();
191 let i = self.cursor.min(n);
192 if i < n && !is_whitespace(&self.chars[i]) {
195 let mut start = i;
196 while start > 0 && !is_whitespace(&self.chars[start - 1]) {
197 start -= 1;
198 }
199 let mut end = i;
200 while end < n && !is_whitespace(&self.chars[end]) {
201 end += 1;
202 }
203 self.sel_anchor = Some(start);
204 self.cursor = end;
205 return;
206 }
207 if i > 0 && !is_whitespace(&self.chars[i - 1]) {
208 let mut start = i;
209 while start > 0 && !is_whitespace(&self.chars[start - 1]) {
210 start -= 1;
211 }
212 self.sel_anchor = Some(start);
213 self.cursor = i;
214 return;
215 }
216 let mut start = i;
218 while start < n && is_whitespace(&self.chars[start]) {
219 start += 1;
220 }
221 if start < n {
222 let mut end = start;
223 while end < n && !is_whitespace(&self.chars[end]) {
224 end += 1;
225 }
226 self.sel_anchor = Some(start);
227 self.cursor = end;
228 return;
229 }
230 if i > 0 {
232 let mut end = i;
233 while end > 0 && is_whitespace(&self.chars[end - 1]) {
234 end -= 1;
235 }
236 let mut start = end;
237 while start > 0 && !is_whitespace(&self.chars[start - 1]) {
238 start -= 1;
239 }
240 if start < end {
241 self.sel_anchor = Some(start);
242 self.cursor = end;
243 return;
244 }
245 }
246 self.sel_anchor = None;
247 }
248
249 pub fn set_cursor(&mut self, cursor: usize) {
250 self.clear_selection();
251 self.cursor = cursor.min(self.chars.len());
252 }
253
254 pub fn set_cursor_from_col(&mut self, col: usize) {
258 self.clear_selection();
259 let mut used = 0;
260 let mut cursor = self.scroll;
261 for c in &self.chars[self.scroll.min(self.chars.len())..] {
262 let w = c.width();
263 if used + w > col {
264 break;
265 }
266 used += w;
267 cursor += 1;
268 }
269 self.cursor = cursor.min(self.chars.len());
270 }
271
272 pub fn at_start(&self) -> bool {
273 self.cursor == 0
274 }
275
276 pub fn at_end(&self) -> bool {
277 self.cursor == self.chars.len()
278 }
279
280 pub fn split_off_at_cursor(&mut self) -> Self {
282 self.clear_selection();
283 let tail: Vec<String> = self.chars.split_off(self.cursor);
284 Self {
285 chars: tail,
286 cursor: 0,
287 sel_anchor: None,
288 scroll: 0,
289 max_len: self.max_len,
290 max_bytes: self.max_bytes,
291 }
292 }
293
294 pub fn append(&mut self, other: &Self) -> bool {
298 let left = self.value();
299 let cursor = left.graphemes(true).count();
300 let right = other.value();
301 if left.len().saturating_add(right.len()) > self.max_bytes {
302 return false;
303 }
304 let joined = left + &right;
305 let chars: Vec<String> = joined.graphemes(true).map(str::to_string).collect();
306 if chars.len() > self.max_len {
307 return false;
308 }
309 self.clear_selection();
310 self.chars = chars;
311 self.cursor = cursor.min(self.chars.len());
312 self.scroll = self.scroll.min(self.cursor);
313 true
314 }
315
316 fn replace_range(&mut self, lo: usize, hi: usize, replacement: &str) {
317 let lo = lo.min(self.chars.len());
318 let hi = hi.max(lo).min(self.chars.len());
319 let prefix = self.chars[..lo].concat();
320 let suffix = self.chars[hi..].concat();
321 let before_cursor = format!("{prefix}{replacement}");
322 let cursor = before_cursor.graphemes(true).count();
323 let joined = before_cursor + &suffix;
324 self.chars = joined.graphemes(true).map(str::to_string).collect();
325 self.cursor = cursor.min(self.chars.len());
326 self.sel_anchor = None;
327 self.scroll = self.scroll.min(self.cursor);
328 }
329
330 pub fn insert(&mut self, c: char) {
331 self.insert_str(&c.to_string());
332 }
333
334 pub fn insert_str(&mut self, text: &str) {
338 let (lo, hi) = self.selection_range().unwrap_or((self.cursor, self.cursor));
339 let prefix = self.chars[..lo].concat();
340 let suffix = self.chars[hi..].concat();
341 let available = self
342 .max_bytes
343 .saturating_sub(prefix.len().saturating_add(suffix.len()));
344 let mut flattened = String::with_capacity(text.len().min(available));
345 for character in text.chars().filter_map(|character| match character {
346 '\t' | '\n' | '\r' => Some(' '),
347 character if character.is_control() => None,
348 _ => Some(character),
349 }) {
350 if flattened.len().saturating_add(character.len_utf8()) > available {
351 break;
352 }
353 flattened.push(character);
354 }
355 if flattened.is_empty() {
356 return;
357 }
358 let mut accepted = String::new();
359 for grapheme in flattened.graphemes(true) {
360 let candidate = format!("{prefix}{accepted}{grapheme}{suffix}");
361 if candidate.len() > self.max_bytes || candidate.graphemes(true).count() > self.max_len
362 {
363 break;
364 }
365 accepted.push_str(grapheme);
366 }
367 if !accepted.is_empty() {
368 self.replace_range(lo, hi, &accepted);
369 }
370 }
371
372 pub fn backspace(&mut self) {
373 if self.delete_selection() {
374 return;
375 }
376 if self.cursor > 0 {
377 self.replace_range(self.cursor - 1, self.cursor, "");
378 }
379 }
380
381 pub fn delete(&mut self) {
382 if self.delete_selection() {
383 return;
384 }
385 if self.cursor < self.chars.len() {
386 self.replace_range(self.cursor, self.cursor + 1, "");
387 }
388 }
389
390 pub fn delete_to_start(&mut self) {
391 if self.delete_selection() {
392 return;
393 }
394 self.replace_range(0, self.cursor, "");
395 }
396
397 pub fn delete_to_end(&mut self) {
398 if self.delete_selection() {
399 return;
400 }
401 self.replace_range(self.cursor, self.chars.len(), "");
402 }
403
404 pub fn delete_word_left(&mut self) {
405 if self.delete_selection() {
406 return;
407 }
408 let target = self.word_left_index();
409 self.replace_range(target, self.cursor, "");
410 }
411
412 fn move_to(&mut self, pos: usize, extend: bool) {
413 if extend {
414 if self.sel_anchor.is_none() {
415 self.sel_anchor = Some(self.cursor);
416 }
417 } else {
418 self.sel_anchor = None;
419 }
420 self.cursor = pos.min(self.chars.len());
421 }
422
423 pub fn left(&mut self) {
424 self.move_to(self.cursor.saturating_sub(1), false);
425 }
426
427 pub fn right(&mut self) {
428 self.move_to((self.cursor + 1).min(self.chars.len()), false);
429 }
430
431 pub fn select_left(&mut self) {
432 self.move_to(self.cursor.saturating_sub(1), true);
433 }
434
435 pub fn select_right(&mut self) {
436 self.move_to((self.cursor + 1).min(self.chars.len()), true);
437 }
438
439 pub fn word_left(&mut self) {
440 self.move_to(self.word_left_index(), false);
441 }
442
443 pub fn word_right(&mut self) {
444 self.move_to(self.word_right_index(), false);
445 }
446
447 pub fn select_word_left(&mut self) {
448 self.move_to(self.word_left_index(), true);
449 }
450
451 pub fn select_word_right(&mut self) {
452 self.move_to(self.word_right_index(), true);
453 }
454
455 pub fn home(&mut self) {
456 self.move_to(0, false);
457 }
458
459 pub fn end(&mut self) {
460 self.move_to(self.chars.len(), false);
461 }
462
463 pub fn select_home(&mut self) {
464 self.move_to(0, true);
465 }
466
467 pub fn select_end(&mut self) {
468 self.move_to(self.chars.len(), true);
469 }
470
471 pub fn word_left_index(&self) -> usize {
474 let mut i = self.cursor;
475 while i > 0 && is_whitespace(&self.chars[i - 1]) {
476 i -= 1;
477 }
478 while i > 0 && !is_whitespace(&self.chars[i - 1]) {
479 i -= 1;
480 }
481 i
482 }
483
484 pub fn word_right_index(&self) -> usize {
486 let mut i = self.cursor;
487 let n = self.chars.len();
488 while i < n && is_whitespace(&self.chars[i]) {
489 i += 1;
490 }
491 while i < n && !is_whitespace(&self.chars[i]) {
492 i += 1;
493 }
494 i
495 }
496
497 pub fn wrap_breaks(&self, width: usize) -> Vec<(usize, usize)> {
501 wrap_breaks(&self.chars, width)
502 }
503
504 pub fn wrap_height(&self, width: usize) -> usize {
506 self.wrap_breaks(width).len().max(1)
507 }
508
509 pub fn wrap_cursor_from_breaks(&self, breaks: &[(usize, usize)]) -> (usize, u16) {
511 for (row, &(start, end)) in breaks.iter().enumerate() {
512 if self.cursor < end || (self.cursor == end && row + 1 == breaks.len()) {
513 let col: usize = self.chars[start..self.cursor.min(end)]
514 .iter()
515 .map(|c| c.width())
516 .sum();
517 if self.cursor == end && row + 1 < breaks.len() && end < self.chars.len() {
520 return (row + 1, 0);
521 }
522 return (row, col as u16);
523 }
524 }
525 let last = breaks.len().saturating_sub(1);
526 (last, 0)
527 }
528
529 pub fn wrap_cursor(&self, width: usize) -> (usize, u16) {
531 self.wrap_cursor_from_breaks(&self.wrap_breaks(width))
532 }
533
534 pub fn set_cursor_from_wrap(&mut self, width: usize, row: usize, col: usize) {
536 self.clear_selection();
537 let breaks = self.wrap_breaks(width);
538 if breaks.is_empty() {
539 self.cursor = 0;
540 return;
541 }
542 let row = row.min(breaks.len() - 1);
543 let (start, end) = breaks[row];
544 let mut used = 0usize;
545 let mut cursor = start;
546 for c in &self.chars[start..end] {
547 let w = c.width();
548 if used + w > col {
549 break;
550 }
551 used += w;
552 cursor += 1;
553 }
554 self.cursor = cursor.min(self.chars.len());
555 }
556
557 pub fn wrap_up(&mut self, width: usize, prefer_col: u16) -> bool {
560 let (row, col) = self.wrap_cursor(width);
561 let prefer = if prefer_col == u16::MAX {
562 col
563 } else {
564 prefer_col
565 };
566 if row == 0 {
567 return false;
568 }
569 self.set_cursor_from_wrap(width, row - 1, prefer as usize);
570 true
571 }
572
573 pub fn wrap_down(&mut self, width: usize, prefer_col: u16) -> bool {
576 let (row, col) = self.wrap_cursor(width);
577 let prefer = if prefer_col == u16::MAX {
578 col
579 } else {
580 prefer_col
581 };
582 let height = self.wrap_height(width);
583 if row + 1 >= height {
584 return false;
585 }
586 self.set_cursor_from_wrap(width, row + 1, prefer as usize);
587 true
588 }
589
590 pub fn wrapped(&self, width: usize) -> WrappedView {
592 self.wrapped_with_sel(width, self.selection_range())
593 }
594
595 pub fn wrapped_with_sel(&self, width: usize, sel: Option<(usize, usize)>) -> WrappedView {
598 self.wrapped_from_breaks(&self.wrap_breaks(width), sel)
599 }
600
601 pub fn wrapped_from_breaks(
603 &self,
604 breaks: &[(usize, usize)],
605 sel: Option<(usize, usize)>,
606 ) -> WrappedView {
607 let (cursor_row, cursor_col) = self.wrap_cursor_from_breaks(breaks);
608 let lines = breaks
609 .iter()
610 .map(|&(start, end)| {
611 let text = self.chars[start..end].concat();
612 let sel_cols = sel.and_then(|(lo, hi)| {
613 let vis_lo = lo.max(start);
614 let vis_hi = hi.min(end);
615 if vis_lo >= vis_hi {
616 return None;
617 }
618 let col = |idx: usize| -> u16 {
619 self.chars[start..idx]
620 .iter()
621 .map(|c| c.width())
622 .sum::<usize>() as u16
623 };
624 Some((col(vis_lo), col(vis_hi)))
625 });
626 WrappedLine {
627 text,
628 sel_cols,
629 start,
630 end,
631 }
632 })
633 .collect();
634 WrappedView {
635 lines,
636 cursor_row: cursor_row as u16,
637 cursor_col,
638 }
639 }
640
641 pub fn place_cursor(&mut self, cursor: usize) {
644 self.cursor = cursor.min(self.chars.len());
645 self.sel_anchor = None;
646 }
647
648 pub fn visible(&mut self, width: usize) -> View {
652 if width == 0 {
653 return View {
654 text: String::new(),
655 cursor_col: 0,
656 sel_cols: None,
657 };
658 }
659 if self.cursor < self.scroll {
660 self.scroll = self.cursor;
661 }
662 loop {
665 let used: usize = self.chars[self.scroll..self.cursor]
666 .iter()
667 .map(|c| c.width())
668 .sum();
669 if used <= width || self.scroll >= self.cursor {
670 break;
671 }
672 self.scroll += 1;
673 }
674 let mut text = String::new();
675 let mut used = 0usize;
676 let mut end_idx = self.scroll;
677 for c in &self.chars[self.scroll..] {
678 let w = c.width();
679 if used + w > width {
680 break;
681 }
682 used += w;
683 text.push_str(c);
684 end_idx += 1;
685 }
686 let cursor_col: usize = self.chars[self.scroll..self.cursor]
687 .iter()
688 .map(|c| c.width())
689 .sum();
690
691 let sel_cols = self.selection_range().and_then(|(lo, hi)| {
692 let vis_lo = lo.max(self.scroll);
693 let vis_hi = hi.min(end_idx);
694 if vis_lo >= vis_hi {
695 return None;
696 }
697 let col = |idx: usize| -> u16 {
698 self.chars[self.scroll..idx]
699 .iter()
700 .map(|c| c.width())
701 .sum::<usize>() as u16
702 };
703 Some((col(vis_lo), col(vis_hi)))
704 });
705
706 View {
707 text,
708 cursor_col: cursor_col.min(width) as u16,
709 sel_cols,
710 }
711 }
712}
713
714fn bounded_graphemes(value: &str, max_len: usize, max_bytes: usize) -> Vec<String> {
715 let mut result = Vec::with_capacity(value.len().min(max_len));
716 let mut bytes = 0usize;
717 for grapheme in value.graphemes(true).take(max_len) {
718 if bytes.saturating_add(grapheme.len()) > max_bytes {
719 break;
720 }
721 bytes += grapheme.len();
722 result.push(grapheme.to_string());
723 }
724 result
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730
731 fn input(text: &str) -> TextInput {
732 TextInput::new(text, 256)
733 }
734
735 #[test]
736 fn edits_at_the_cursor() {
737 let mut i = input("hello");
738 i.left();
739 i.insert('!');
740 assert_eq!(i.value(), "hell!o");
741 i.backspace();
742 assert_eq!(i.value(), "hello");
743 i.delete();
744 assert_eq!(i.value(), "hell");
745 }
746
747 #[test]
748 fn moves_and_deletes_by_word() {
749 let mut i = input("one two three");
750 i.word_left();
751 assert_eq!(i.cursor, 8);
752 i.word_left();
753 assert_eq!(i.cursor, 4);
754 i.delete_word_left();
755 assert_eq!(i.value(), "two three");
756 i.end();
757 i.delete_to_start();
758 assert!(i.is_empty());
759 }
760
761 #[test]
762 fn honours_the_length_limit() {
763 let mut i = TextInput::new("ab", 2);
764 i.insert('c');
765 assert_eq!(i.value(), "ab");
766 }
767
768 #[test]
769 fn combining_sequences_cannot_bypass_the_byte_limit() {
770 let mut typed = TextInput::new("", 1);
771 typed.insert('e');
772 for _ in 0..1_000 {
773 typed.insert('\u{301}');
774 }
775 assert_eq!(typed.len(), 1);
776 assert!(typed.value().len() <= crate::model::text_byte_limit(1));
777
778 let pasted = format!("e{}", "\u{301}".repeat(1_000));
779 let pasted = TextInput::new(&pasted, 1);
780 assert!(pasted.value().len() <= crate::model::text_byte_limit(1));
781 }
782
783 #[test]
784 fn appending_inputs_preserves_the_same_limits_as_typing() {
785 let mut input = TextInput::new("a", 1);
786 assert!(!input.append(&TextInput::new("b", 1)));
787 assert_eq!(input.value(), "a");
788 }
789
790 #[test]
791 fn scrolls_to_keep_the_cursor_in_view() {
792 let mut i = input("abcdefghij");
793 let view = i.visible(4);
794 assert_eq!(view.text, "ghij");
795 assert_eq!(view.cursor_col, 4);
796 i.home();
797 let view = i.visible(4);
798 assert_eq!(view.text, "abcd");
799 assert_eq!(view.cursor_col, 0);
800 }
801
802 #[test]
803 fn measures_wide_characters_in_columns() {
804 let mut i = input("买菜");
805 let view = i.visible(4);
806 assert_eq!(view.text, "买菜");
807 assert_eq!(view.cursor_col, 4);
808 }
809
810 #[test]
811 fn cursor_and_deletion_treat_combining_sequences_as_one_grapheme() {
812 let mut i = input("e\u{301}x");
813 assert_eq!(i.len(), 2);
814 i.left();
815 i.backspace();
816 assert_eq!(i.value(), "x");
817 assert_eq!(i.cursor(), 0);
818 }
819
820 #[test]
821 fn cursor_and_selection_do_not_split_zwj_emoji() {
822 let family = "👨👩👧👦";
823 let mut i = input(&format!("{family}!"));
824 assert_eq!(i.len(), 2);
825 i.home();
826 i.select_right();
827 assert_eq!(i.selected_text().as_deref(), Some(family));
828 i.delete_selection();
829 assert_eq!(i.value(), "!");
830 }
831
832 #[test]
833 fn paste_keeps_graphemes_but_strips_terminal_controls() {
834 let family = "👨👩👧👦";
835 let mut i = TextInput::new("", 64);
836 i.insert_str(&format!("e\u{301}\t{family}\u{1b}\u{7f}\u{85}!"));
837
838 assert_eq!(i.value(), format!("e\u{301} {family}!"));
839 assert_eq!(i.len(), 4);
840 assert!(!i.value().chars().any(char::is_control));
841 }
842
843 #[test]
844 fn separately_typed_combining_marks_merge_with_the_previous_grapheme() {
845 let mut i = TextInput::new("", 1);
846 i.insert('e');
847 i.insert('\u{301}');
848
849 assert_eq!(i.value(), "e\u{301}");
850 assert_eq!(i.len(), 1);
851 }
852
853 #[test]
854 fn select_word_picks_the_word_under_the_cursor() {
855 let mut i = input("one two three");
856 i.home();
857 i.word_right(); i.right(); i.select_word();
860 assert_eq!(i.selected_text().as_deref(), Some("two"));
861 assert_eq!(i.selection_range(), Some((4, 7)));
862 }
863
864 #[test]
865 fn shift_arrows_extend_the_selection() {
866 let mut i = input("hello");
867 i.home();
868 i.select_right();
869 i.select_right();
870 assert_eq!(i.selected_text().as_deref(), Some("he"));
871 i.delete_selection();
872 assert_eq!(i.value(), "llo");
873 }
874
875 #[test]
876 fn typing_replaces_the_selection() {
877 let mut i = input("hello");
878 i.home();
879 i.select_word();
880 i.insert('x');
881 assert_eq!(i.value(), "x");
882 }
883
884 #[test]
885 fn wraps_on_spaces_then_hard_breaks() {
886 let s: Vec<String> = "one two three"
887 .graphemes(true)
888 .map(str::to_string)
889 .collect();
890 let breaks = wrap_breaks(&s, 8);
891 let parts: Vec<String> = breaks.iter().map(|&(a, b)| s[a..b].concat()).collect();
892 assert!(parts.len() >= 2);
894 assert!(parts[0].starts_with("one"));
895 assert!(parts.iter().any(|p| p.contains("three")));
896 }
897
898 #[test]
899 fn wrap_ranges_preserve_every_space_and_accept_every_caret_position() {
900 let mut input = input("word next");
901 let breaks = input.wrap_breaks(5);
902 assert_eq!(breaks.first().map(|range| range.0), Some(0));
903 assert_eq!(breaks.last().map(|range| range.1), Some(input.len()));
904 assert!(breaks.windows(2).all(|pair| pair[0].1 == pair[1].0));
905 assert_eq!(
906 breaks
907 .iter()
908 .map(|&(start, end)| input.slice(start, end))
909 .collect::<String>(),
910 input.value()
911 );
912
913 for cursor in 0..=input.len() {
914 input.set_cursor(cursor);
915 let _ = input.wrap_cursor_from_breaks(&breaks);
916 }
917 }
918
919 #[test]
920 fn wrap_height_is_at_least_one() {
921 let i = input("");
922 assert_eq!(i.wrap_height(10), 1);
923 let i = input("hello world again");
924 assert!(i.wrap_height(6) >= 2);
925 }
926
927 #[test]
928 fn wrap_cursor_tracks_rows() {
929 let mut i = input("aaaa bbbb cccc");
930 i.home();
931 let (row, col) = i.wrap_cursor(5);
933 assert_eq!((row, col), (0, 0));
934 i.end();
935 let (row, _) = i.wrap_cursor(5);
936 assert!(row >= 1);
937 }
938}