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