1#![forbid(unsafe_code)]
2
3use crate::grapheme_width;
30use ftui_style::Style;
31use smallvec::SmallVec;
32use std::borrow::Cow;
33use unicode_segmentation::UnicodeSegmentation;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum ControlCode {
41 CarriageReturn,
43 LineFeed,
45 Bell,
47 Backspace,
49 Tab,
51 Home,
53 ClearToEndOfLine,
55 ClearLine,
57}
58
59impl ControlCode {
60 #[inline]
62 #[must_use]
63 pub const fn is_newline(&self) -> bool {
64 matches!(self, Self::LineFeed)
65 }
66
67 #[inline]
69 #[must_use]
70 pub const fn is_cr(&self) -> bool {
71 matches!(self, Self::CarriageReturn)
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Segment<'a> {
85 pub text: Cow<'a, str>,
87 pub style: Option<Style>,
89 pub link: Option<Cow<'a, str>>,
91 pub control: Option<SmallVec<[ControlCode; 2]>>,
93}
94
95impl<'a> Segment<'a> {
96 #[inline]
98 #[must_use]
99 pub fn text(s: impl Into<Cow<'a, str>>) -> Self {
100 Self {
101 text: s.into(),
102 style: None,
103 link: None,
104 control: None,
105 }
106 }
107
108 #[inline]
110 #[must_use]
111 pub fn styled(s: impl Into<Cow<'a, str>>, style: Style) -> Self {
112 Self {
113 text: s.into(),
114 style: Some(style),
115 link: None,
116 control: None,
117 }
118 }
119
120 #[inline]
122 #[must_use]
123 pub fn control(code: ControlCode) -> Self {
124 let mut codes = SmallVec::new();
125 codes.push(code);
126 Self {
127 text: Cow::Borrowed(""),
128 style: None,
129 link: None,
130 control: Some(codes),
131 }
132 }
133
134 #[inline]
136 #[must_use]
137 pub fn newline() -> Self {
138 Self::control(ControlCode::LineFeed)
139 }
140
141 #[inline]
143 #[must_use]
144 pub const fn empty() -> Self {
145 Self {
146 text: Cow::Borrowed(""),
147 style: None,
148 link: None,
149 control: None,
150 }
151 }
152
153 #[inline]
155 #[must_use]
156 pub fn as_str(&self) -> &str {
157 &self.text
158 }
159
160 #[inline]
162 #[must_use]
163 pub fn is_empty(&self) -> bool {
164 self.text.is_empty() && self.control.is_none()
165 }
166
167 #[inline]
169 #[must_use]
170 pub fn has_text(&self) -> bool {
171 !self.text.is_empty()
172 }
173
174 #[inline]
176 #[must_use]
177 pub fn is_control(&self) -> bool {
178 self.control.is_some() && self.text.is_empty()
179 }
180
181 #[inline]
183 #[must_use]
184 pub fn is_newline(&self) -> bool {
185 self.control
186 .as_ref()
187 .is_some_and(|codes| codes.iter().any(|c| c.is_newline()))
188 }
189
190 #[inline]
195 #[must_use]
196 pub fn cell_length(&self) -> usize {
197 if self.is_control() {
198 return 0;
199 }
200 crate::display_width(&self.text)
201 }
202
203 #[inline]
208 #[must_use]
209 pub fn cell_length_with<F>(&self, width_fn: F) -> usize
210 where
211 F: Fn(&str) -> usize,
212 {
213 if self.is_control() {
214 return 0;
215 }
216 width_fn(&self.text)
217 }
218
219 #[must_use]
232 pub fn split_at_cell(&self, cell_pos: usize) -> (Self, Self) {
233 if self.is_control() {
235 if cell_pos == 0 {
236 return (Self::empty(), self.clone());
237 }
238 return (self.clone(), Self::empty());
239 }
240
241 if self.text.is_empty() || cell_pos == 0 {
243 return (
244 Self {
245 text: Cow::Borrowed(""),
246 style: self.style,
247 link: self.link.clone(),
248 control: None,
249 },
250 self.clone(),
251 );
252 }
253
254 let total_width = self.cell_length();
255 if cell_pos >= total_width {
256 return (
257 self.clone(),
258 Self {
259 text: Cow::Borrowed(""),
260 style: self.style,
261 link: self.link.clone(),
262 control: None,
263 },
264 );
265 }
266
267 let (byte_pos, _actual_width) = find_cell_boundary(&self.text, cell_pos);
269
270 let left_text = &self.text[..byte_pos];
271 let right_text = &self.text[byte_pos..];
272
273 (
274 Self {
275 text: Cow::Owned(left_text.to_string()),
276 style: self.style,
277 link: self.link.clone(),
278 control: None,
279 },
280 Self {
281 text: Cow::Owned(right_text.to_string()),
282 style: self.style,
283 link: self.link.clone(),
284 control: None,
285 },
286 )
287 }
288
289 #[inline]
291 #[must_use]
292 pub fn with_style(mut self, style: Style) -> Self {
293 self.style = Some(style);
294 self
295 }
296
297 #[must_use]
299 pub fn into_owned(self) -> Segment<'static> {
300 Segment {
301 text: Cow::Owned(self.text.into_owned()),
302 style: self.style,
303 control: self.control,
304 link: self.link.map(|l| std::borrow::Cow::Owned(l.into_owned())),
305 }
306 }
307
308 #[must_use]
310 pub fn with_control(mut self, code: ControlCode) -> Self {
311 if let Some(ref mut codes) = self.control {
312 codes.push(code);
313 } else {
314 let mut codes = SmallVec::new();
315 codes.push(code);
316 self.control = Some(codes);
317 }
318 self
319 }
320}
321
322impl<'a> Default for Segment<'a> {
323 fn default() -> Self {
324 Self::empty()
325 }
326}
327
328impl<'a> From<&'a str> for Segment<'a> {
329 fn from(s: &'a str) -> Self {
330 Self::text(s)
331 }
332}
333
334impl From<String> for Segment<'static> {
335 fn from(s: String) -> Self {
336 Self::text(s)
337 }
338}
339
340pub fn find_cell_boundary(text: &str, target_cells: usize) -> (usize, usize) {
346 let mut current_cells = 0;
347 let mut byte_pos = 0;
348
349 for grapheme in text.graphemes(true) {
350 let grapheme_width = grapheme_width(grapheme);
351
352 if current_cells + grapheme_width > target_cells {
354 break;
356 }
357
358 current_cells += grapheme_width;
359 byte_pos += grapheme.len();
360
361 if current_cells >= target_cells {
362 break;
363 }
364 }
365
366 (byte_pos, current_cells)
367}
368
369#[derive(Debug, Clone, Default, PartialEq, Eq)]
373pub struct SegmentLine<'a> {
374 segments: Vec<Segment<'a>>,
375}
376
377impl<'a> SegmentLine<'a> {
378 #[inline]
380 #[must_use]
381 pub const fn new() -> Self {
382 Self {
383 segments: Vec::new(),
384 }
385 }
386
387 #[inline]
389 #[must_use]
390 pub fn from_segments(segments: Vec<Segment<'a>>) -> Self {
391 Self { segments }
392 }
393
394 #[inline]
396 #[must_use]
397 pub fn from_segment(segment: Segment<'a>) -> Self {
398 Self {
399 segments: vec![segment],
400 }
401 }
402
403 #[inline]
405 #[must_use]
406 pub fn is_empty(&self) -> bool {
407 self.segments.is_empty() || self.segments.iter().all(|s| s.is_empty())
408 }
409
410 #[inline]
412 #[must_use]
413 pub fn len(&self) -> usize {
414 self.segments.len()
415 }
416
417 #[must_use]
419 pub fn cell_length(&self) -> usize {
420 self.segments.iter().map(|s| s.cell_length()).sum()
421 }
422
423 #[inline]
425 pub fn push(&mut self, segment: Segment<'a>) {
426 self.segments.push(segment);
427 }
428
429 #[inline]
431 #[must_use]
432 pub fn segments(&self) -> &[Segment<'a>] {
433 &self.segments
434 }
435
436 #[inline]
438 pub fn segments_mut(&mut self) -> &mut Vec<Segment<'a>> {
439 &mut self.segments
440 }
441
442 #[inline]
444 pub fn iter(&self) -> impl Iterator<Item = &Segment<'a>> {
445 self.segments.iter()
446 }
447
448 #[must_use]
452 pub fn split_at_cell(&self, cell_pos: usize) -> (Self, Self) {
453 if cell_pos == 0 {
454 return (Self::new(), self.clone());
455 }
456
457 let total_width = self.cell_length();
458 if cell_pos >= total_width {
459 return (self.clone(), Self::new());
460 }
461
462 let mut left_segments = Vec::new();
463 let mut right_segments = Vec::new();
464 let mut consumed = 0;
465 let mut found_split = false;
466
467 for segment in &self.segments {
468 if found_split {
469 right_segments.push(segment.clone());
470 continue;
471 }
472
473 let seg_width = segment.cell_length();
474 if consumed + seg_width <= cell_pos {
475 left_segments.push(segment.clone());
477 consumed += seg_width;
478 } else if consumed >= cell_pos {
479 right_segments.push(segment.clone());
481 found_split = true;
482 } else {
483 let split_at = cell_pos - consumed;
485 let (left, right) = segment.split_at_cell(split_at);
486 if left.has_text() {
487 left_segments.push(left);
488 }
489 if right.has_text() {
490 right_segments.push(right);
491 }
492 found_split = true;
493 }
494 }
495
496 (
497 Self::from_segments(left_segments),
498 Self::from_segments(right_segments),
499 )
500 }
501
502 #[must_use]
504 pub fn to_plain_text(&self) -> String {
505 self.segments.iter().map(|s| s.as_str()).collect()
506 }
507
508 #[must_use]
510 pub fn into_owned(self) -> SegmentLine<'static> {
511 SegmentLine {
512 segments: self.segments.into_iter().map(|s| s.into_owned()).collect(),
513 }
514 }
515}
516
517impl<'a> IntoIterator for SegmentLine<'a> {
518 type Item = Segment<'a>;
519 type IntoIter = std::vec::IntoIter<Segment<'a>>;
520
521 fn into_iter(self) -> Self::IntoIter {
522 self.segments.into_iter()
523 }
524}
525
526impl<'a, 'b> IntoIterator for &'b SegmentLine<'a> {
527 type Item = &'b Segment<'a>;
528 type IntoIter = std::slice::Iter<'b, Segment<'a>>;
529
530 fn into_iter(self) -> Self::IntoIter {
531 self.segments.iter()
532 }
533}
534
535#[derive(Debug, Clone, Default, PartialEq, Eq)]
537pub struct SegmentLines<'a> {
538 lines: Vec<SegmentLine<'a>>,
539}
540
541impl<'a> SegmentLines<'a> {
542 #[inline]
544 #[must_use]
545 pub const fn new() -> Self {
546 Self { lines: Vec::new() }
547 }
548
549 #[inline]
551 #[must_use]
552 pub fn from_lines(lines: Vec<SegmentLine<'a>>) -> Self {
553 Self { lines }
554 }
555
556 #[inline]
558 #[must_use]
559 pub fn is_empty(&self) -> bool {
560 self.lines.is_empty()
561 }
562
563 #[inline]
565 #[must_use]
566 pub fn len(&self) -> usize {
567 self.lines.len()
568 }
569
570 #[inline]
572 pub fn push(&mut self, line: SegmentLine<'a>) {
573 self.lines.push(line);
574 }
575
576 #[inline]
578 #[must_use]
579 pub fn lines(&self) -> &[SegmentLine<'a>] {
580 &self.lines
581 }
582
583 #[inline]
585 pub fn iter(&self) -> impl Iterator<Item = &SegmentLine<'a>> {
586 self.lines.iter()
587 }
588
589 #[must_use]
591 pub fn max_width(&self) -> usize {
592 self.lines
593 .iter()
594 .map(|l| l.cell_length())
595 .max()
596 .unwrap_or(0)
597 }
598
599 #[must_use]
601 pub fn into_owned(self) -> SegmentLines<'static> {
602 SegmentLines {
603 lines: self.lines.into_iter().map(|l| l.into_owned()).collect(),
604 }
605 }
606}
607
608impl<'a> IntoIterator for SegmentLines<'a> {
609 type Item = SegmentLine<'a>;
610 type IntoIter = std::vec::IntoIter<SegmentLine<'a>>;
611
612 fn into_iter(self) -> Self::IntoIter {
613 self.lines.into_iter()
614 }
615}
616
617#[must_use]
625pub fn split_into_lines<'a>(segments: impl IntoIterator<Item = Segment<'a>>) -> SegmentLines<'a> {
626 let mut lines = SegmentLines::new();
627 let mut current_line = SegmentLine::new();
628 let mut has_content = false;
629
630 for segment in segments {
631 has_content = true;
632 if segment.is_newline() {
633 lines.push(std::mem::take(&mut current_line));
634 } else if segment.has_text() {
635 let text = segment.as_str();
637 if text.contains('\n') {
638 let parts: Vec<&str> = text.split('\n').collect();
640 for (i, part) in parts.iter().enumerate() {
641 if !part.is_empty() {
642 current_line.push(Segment {
643 text: Cow::Owned((*part).to_string()),
644 style: segment.style,
645 control: None,
646 link: segment.link.clone(),
647 });
648 }
649 if i < parts.len() - 1 {
651 lines.push(std::mem::take(&mut current_line));
652 }
653 }
654 } else {
655 current_line.push(segment);
656 }
657 } else if !segment.is_empty() {
658 current_line.push(segment);
659 }
660 }
661
662 if has_content || lines.is_empty() {
665 lines.push(current_line);
666 }
667
668 lines
669}
670
671pub fn join_lines<'a>(lines: &SegmentLines<'a>) -> Vec<Segment<'a>> {
673 let mut result = Vec::new();
674 let line_count = lines.len();
675
676 for (i, line) in lines.iter().enumerate() {
677 for segment in line.iter() {
678 result.push(segment.clone());
679 }
680 if i < line_count - 1 {
681 result.push(Segment::newline());
682 }
683 }
684
685 result
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
697 fn segment_text_creates_unstyled_segment() {
698 let seg = Segment::text("hello");
699 assert_eq!(seg.as_str(), "hello");
700 assert!(seg.style.is_none());
701 assert!(seg.control.is_none());
702 }
703
704 #[test]
705 fn segment_styled_creates_styled_segment() {
706 let style = Style::new().bold();
707 let seg = Segment::styled("hello", style);
708 assert_eq!(seg.as_str(), "hello");
709 assert_eq!(seg.style, Some(style));
710 }
711
712 #[test]
713 fn segment_control_creates_control_segment() {
714 let seg = Segment::control(ControlCode::LineFeed);
715 assert!(seg.is_control());
716 assert!(seg.is_newline());
717 assert_eq!(seg.cell_length(), 0);
718 }
719
720 #[test]
721 fn segment_empty_is_empty() {
722 let seg = Segment::empty();
723 assert!(seg.is_empty());
724 assert!(!seg.has_text());
725 assert_eq!(seg.cell_length(), 0);
726 }
727
728 #[test]
733 fn cell_length_ascii() {
734 let seg = Segment::text("hello");
735 assert_eq!(seg.cell_length(), 5);
736 }
737
738 #[test]
739 fn cell_length_cjk() {
740 let seg = Segment::text("你好");
742 assert_eq!(seg.cell_length(), 4); }
744
745 #[test]
746 fn cell_length_mixed() {
747 let seg = Segment::text("hi你好");
748 assert_eq!(seg.cell_length(), 6); }
750
751 #[test]
752 fn cell_length_emoji() {
753 let seg = Segment::text("😀");
755 assert!(seg.cell_length() >= 1);
757 }
758
759 #[test]
760 fn cell_length_zwj_sequence() {
761 let seg = Segment::text("👨👩👧");
763 let _width = seg.cell_length();
765 }
766
767 #[test]
768 fn cell_length_control_is_zero() {
769 let seg = Segment::control(ControlCode::Bell);
770 assert_eq!(seg.cell_length(), 0);
771 }
772
773 #[test]
778 fn split_at_cell_ascii() {
779 let seg = Segment::text("hello world");
780 let (left, right) = seg.split_at_cell(5);
781 assert_eq!(left.as_str(), "hello");
782 assert_eq!(right.as_str(), " world");
783 }
784
785 #[test]
786 fn split_at_cell_zero() {
787 let seg = Segment::text("hello");
788 let (left, right) = seg.split_at_cell(0);
789 assert_eq!(left.as_str(), "");
790 assert_eq!(right.as_str(), "hello");
791 }
792
793 #[test]
794 fn split_at_cell_beyond_length() {
795 let seg = Segment::text("hi");
796 let (left, right) = seg.split_at_cell(10);
797 assert_eq!(left.as_str(), "hi");
798 assert_eq!(right.as_str(), "");
799 }
800
801 #[test]
802 fn split_at_cell_cjk() {
803 let seg = Segment::text("你好世界");
805 let (left, right) = seg.split_at_cell(2);
806 assert_eq!(left.as_str(), "你");
807 assert_eq!(right.as_str(), "好世界");
808 }
809
810 #[test]
811 fn split_at_cell_cjk_mid_char() {
812 let seg = Segment::text("你好");
815 let (left, right) = seg.split_at_cell(1);
816 assert_eq!(left.as_str(), "");
818 assert_eq!(right.as_str(), "你好");
819 }
820
821 #[test]
822 fn split_at_cell_mixed() {
823 let seg = Segment::text("hi你");
824 let (left, right) = seg.split_at_cell(2);
825 assert_eq!(left.as_str(), "hi");
826 assert_eq!(right.as_str(), "你");
827 }
828
829 #[test]
830 fn split_at_cell_preserves_style() {
831 let style = Style::new().bold();
832 let seg = Segment::styled("hello", style);
833 let (left, right) = seg.split_at_cell(2);
834 assert_eq!(left.style, Some(style));
835 assert_eq!(right.style, Some(style));
836 }
837
838 #[test]
839 fn split_at_cell_control_segment() {
840 let seg = Segment::control(ControlCode::LineFeed);
841 let (left, right) = seg.split_at_cell(0);
842 assert!(left.is_empty());
843 assert!(right.is_control());
844 }
845
846 #[test]
851 fn segment_line_cell_length() {
852 let mut line = SegmentLine::new();
853 line.push(Segment::text("hello "));
854 line.push(Segment::text("world"));
855 assert_eq!(line.cell_length(), 11);
856 }
857
858 #[test]
859 fn segment_line_split_at_cell() {
860 let mut line = SegmentLine::new();
861 line.push(Segment::text("hello "));
862 line.push(Segment::text("world"));
863
864 let (left, right) = line.split_at_cell(8);
865 assert_eq!(left.to_plain_text(), "hello wo");
866 assert_eq!(right.to_plain_text(), "rld");
867 }
868
869 #[test]
870 fn segment_line_split_at_segment_boundary() {
871 let mut line = SegmentLine::new();
872 line.push(Segment::text("hello"));
873 line.push(Segment::text(" world"));
874
875 let (left, right) = line.split_at_cell(5);
876 assert_eq!(left.to_plain_text(), "hello");
877 assert_eq!(right.to_plain_text(), " world");
878 }
879
880 #[test]
885 fn split_into_lines_single_line() {
886 let segments = vec![Segment::text("hello world")];
887 let lines = split_into_lines(segments);
888 assert_eq!(lines.len(), 1);
889 assert_eq!(lines.lines()[0].to_plain_text(), "hello world");
890 }
891
892 #[test]
893 fn split_into_lines_with_newline_control() {
894 let segments = vec![
895 Segment::text("line one"),
896 Segment::newline(),
897 Segment::text("line two"),
898 ];
899 let lines = split_into_lines(segments);
900 assert_eq!(lines.len(), 2);
901 assert_eq!(lines.lines()[0].to_plain_text(), "line one");
902 assert_eq!(lines.lines()[1].to_plain_text(), "line two");
903 }
904
905 #[test]
906 fn split_into_lines_with_embedded_newline() {
907 let segments = vec![Segment::text("line one\nline two")];
908 let lines = split_into_lines(segments);
909 assert_eq!(lines.len(), 2);
910 assert_eq!(lines.lines()[0].to_plain_text(), "line one");
911 assert_eq!(lines.lines()[1].to_plain_text(), "line two");
912 }
913
914 #[test]
915 fn split_into_lines_empty_input() {
916 let segments: Vec<Segment> = vec![];
917 let lines = split_into_lines(segments);
918 assert_eq!(lines.len(), 1); assert!(lines.lines()[0].is_empty());
920 }
921
922 #[test]
927 fn join_lines_roundtrip() {
928 let segments = vec![
929 Segment::text("line one"),
930 Segment::newline(),
931 Segment::text("line two"),
932 ];
933 let lines = split_into_lines(segments);
934 let joined = join_lines(&lines);
935
936 assert_eq!(joined.len(), 3);
938 assert_eq!(joined[0].as_str(), "line one");
939 assert!(joined[1].is_newline());
940 assert_eq!(joined[2].as_str(), "line two");
941 }
942
943 #[test]
948 fn segment_into_owned() {
949 let s = String::from("hello");
950 let seg: Segment = Segment::text(&s[..]);
951 let owned: Segment<'static> = seg.into_owned();
952 assert_eq!(owned.as_str(), "hello");
953 }
954
955 #[test]
956 fn segment_from_string() {
957 let seg: Segment<'static> = Segment::from(String::from("hello"));
958 assert_eq!(seg.as_str(), "hello");
959 }
960
961 #[test]
962 fn segment_from_str() {
963 let seg: Segment = Segment::from("hello");
964 assert_eq!(seg.as_str(), "hello");
965 }
966
967 #[test]
972 fn control_code_is_newline() {
973 assert!(ControlCode::LineFeed.is_newline());
974 assert!(!ControlCode::CarriageReturn.is_newline());
975 assert!(!ControlCode::Bell.is_newline());
976 }
977
978 #[test]
979 fn control_code_is_cr() {
980 assert!(ControlCode::CarriageReturn.is_cr());
981 assert!(!ControlCode::LineFeed.is_cr());
982 }
983
984 #[test]
985 fn segment_with_control() {
986 let seg = Segment::text("hello").with_control(ControlCode::Bell);
987 assert!(seg.control.is_some());
988 assert_eq!(seg.control.as_ref().unwrap().len(), 1);
989 }
990
991 #[test]
996 fn split_empty_segment() {
997 let seg = Segment::text("");
998 let (left, right) = seg.split_at_cell(5);
999 assert_eq!(left.as_str(), "");
1000 assert_eq!(right.as_str(), "");
1001 }
1002
1003 #[test]
1004 fn combining_characters() {
1005 let seg = Segment::text("e\u{0301}"); let width = seg.cell_length();
1009 assert!(width >= 1);
1010
1011 let (left, right) = seg.split_at_cell(1);
1013 assert_eq!(left.cell_length() + right.cell_length(), width);
1014 }
1015
1016 #[test]
1017 fn segment_line_is_empty() {
1018 let line = SegmentLine::new();
1019 assert!(line.is_empty());
1020
1021 let mut line2 = SegmentLine::new();
1022 line2.push(Segment::empty());
1023 assert!(line2.is_empty());
1024
1025 let mut line3 = SegmentLine::new();
1026 line3.push(Segment::text("x"));
1027 assert!(!line3.is_empty());
1028 }
1029
1030 #[test]
1035 fn cow_borrowed_from_static_str() {
1036 let seg = Segment::text("static string");
1037 assert!(matches!(seg.text, Cow::Borrowed(_)));
1039 }
1040
1041 #[test]
1042 fn cow_owned_from_string() {
1043 let owned = String::from("owned string");
1044 let seg = Segment::text(owned);
1045 assert!(matches!(seg.text, Cow::Owned(_)));
1047 }
1048
1049 #[test]
1050 fn cow_borrowed_reference() {
1051 let s = String::from("reference");
1052 let seg = Segment::text(&s[..]);
1053 assert!(matches!(seg.text, Cow::Borrowed(_)));
1055 }
1056
1057 #[test]
1058 fn into_owned_converts_borrowed_to_owned() {
1059 let seg = Segment::text("borrowed");
1060 assert!(matches!(seg.text, Cow::Borrowed(_)));
1061
1062 let owned = seg.into_owned();
1063 assert!(matches!(owned.text, Cow::Owned(_)));
1065 assert_eq!(owned.as_str(), "borrowed");
1066 }
1067
1068 #[test]
1069 fn clone_borrowed_segment_stays_borrowed() {
1070 let seg = Segment::text("static");
1071 let cloned = seg.clone();
1072 assert!(matches!(cloned.text, Cow::Borrowed(_)));
1074 }
1075
1076 #[test]
1077 fn clone_owned_segment_allocates() {
1078 let owned = String::from("owned");
1079 let seg = Segment::text(owned);
1080 let cloned = seg.clone();
1081 assert!(matches!(cloned.text, Cow::Owned(_)));
1083 assert_eq!(cloned.as_str(), "owned");
1084 }
1085
1086 #[test]
1091 fn segment_default_is_empty() {
1092 let seg = Segment::default();
1093 assert!(seg.is_empty());
1094 assert_eq!(seg.as_str(), "");
1095 assert!(seg.style.is_none());
1096 assert!(seg.control.is_none());
1097 }
1098
1099 #[test]
1100 fn segment_line_default_is_empty() {
1101 let line = SegmentLine::default();
1102 assert!(line.is_empty());
1103 assert_eq!(line.len(), 0);
1104 }
1105
1106 #[test]
1107 fn segment_lines_default_is_empty() {
1108 let lines = SegmentLines::default();
1109 assert!(lines.is_empty());
1110 assert_eq!(lines.len(), 0);
1111 }
1112
1113 #[test]
1118 fn segment_debug_impl() {
1119 let seg = Segment::text("hello");
1120 let debug = format!("{:?}", seg);
1121 assert!(debug.contains("Segment"));
1122 assert!(debug.contains("hello"));
1123 }
1124
1125 #[test]
1126 fn control_code_debug_impl() {
1127 let code = ControlCode::LineFeed;
1128 let debug = format!("{:?}", code);
1129 assert!(debug.contains("LineFeed"));
1130 }
1131
1132 #[test]
1133 fn segment_line_debug_impl() {
1134 let line = SegmentLine::from_segment(Segment::text("test"));
1135 let debug = format!("{:?}", line);
1136 assert!(debug.contains("SegmentLine"));
1137 }
1138
1139 #[test]
1144 fn segment_line_into_owned() {
1145 let s = String::from("test");
1146 let mut line = SegmentLine::new();
1147 line.push(Segment::text(&s[..]));
1148
1149 let owned = line.into_owned();
1150 drop(s); assert_eq!(owned.to_plain_text(), "test");
1152 }
1153
1154 #[test]
1155 fn segment_line_segments_mut() {
1156 let mut line = SegmentLine::from_segment(Segment::text("hello"));
1157 line.segments_mut().push(Segment::text(" world"));
1158 assert_eq!(line.to_plain_text(), "hello world");
1159 }
1160
1161 #[test]
1162 fn segment_line_iter() {
1163 let line = SegmentLine::from_segments(vec![Segment::text("a"), Segment::text("b")]);
1164 let collected: Vec<_> = line.iter().collect();
1165 assert_eq!(collected.len(), 2);
1166 }
1167
1168 #[test]
1169 fn segment_line_into_iter_ref() {
1170 let line = SegmentLine::from_segments(vec![Segment::text("x"), Segment::text("y")]);
1171 let mut count = 0;
1172 for _seg in &line {
1173 count += 1;
1174 }
1175 assert_eq!(count, 2);
1176 }
1177
1178 #[test]
1183 fn segment_lines_into_owned() {
1184 let s = String::from("line one");
1185 let mut lines = SegmentLines::new();
1186 let mut line = SegmentLine::new();
1187 line.push(Segment::text(&s[..]));
1188 lines.push(line);
1189
1190 let owned = lines.into_owned();
1191 drop(s);
1192 assert_eq!(owned.lines()[0].to_plain_text(), "line one");
1193 }
1194
1195 #[test]
1196 fn segment_lines_iter() {
1197 let mut lines = SegmentLines::new();
1198 lines.push(SegmentLine::from_segment(Segment::text("a")));
1199 lines.push(SegmentLine::from_segment(Segment::text("b")));
1200
1201 let collected: Vec<_> = lines.iter().collect();
1202 assert_eq!(collected.len(), 2);
1203 }
1204
1205 #[test]
1206 fn segment_lines_max_width() {
1207 let mut lines = SegmentLines::new();
1208 lines.push(SegmentLine::from_segment(Segment::text("short")));
1209 lines.push(SegmentLine::from_segment(Segment::text("longer line here")));
1210 lines.push(SegmentLine::from_segment(Segment::text("med")));
1211
1212 assert_eq!(lines.max_width(), 16);
1213 }
1214
1215 #[test]
1216 fn segment_lines_max_width_empty() {
1217 let lines = SegmentLines::new();
1218 assert_eq!(lines.max_width(), 0);
1219 }
1220
1221 #[test]
1226 fn segment_with_style_applies_style() {
1227 let style = Style::new().bold();
1228 let seg = Segment::text("hello").with_style(style);
1229 assert_eq!(seg.style, Some(style));
1230 }
1231
1232 #[test]
1233 fn segment_with_multiple_controls() {
1234 let seg = Segment::text("x")
1235 .with_control(ControlCode::Bell)
1236 .with_control(ControlCode::Tab);
1237 let codes = seg.control.unwrap();
1238 assert_eq!(codes.len(), 2);
1239 }
1240
1241 #[test]
1246 fn cell_length_with_custom_width() {
1247 let seg = Segment::text("hello");
1248 let width = seg.cell_length_with(|s| s.len() * 2);
1250 assert_eq!(width, 10);
1251 }
1252
1253 #[test]
1254 fn cell_length_with_on_control_is_zero() {
1255 let seg = Segment::control(ControlCode::Bell);
1256 let width = seg.cell_length_with(|_| 100);
1257 assert_eq!(width, 0);
1258 }
1259
1260 #[test]
1265 fn control_code_equality() {
1266 assert_eq!(ControlCode::LineFeed, ControlCode::LineFeed);
1267 assert_ne!(ControlCode::LineFeed, ControlCode::CarriageReturn);
1268 }
1269
1270 #[test]
1271 fn control_code_hash_consistency() {
1272 use std::collections::HashSet;
1273 let mut set = HashSet::new();
1274 set.insert(ControlCode::LineFeed);
1275 set.insert(ControlCode::CarriageReturn);
1276 assert_eq!(set.len(), 2);
1277 assert!(set.contains(&ControlCode::LineFeed));
1278 }
1279
1280 #[test]
1285 fn split_at_cell_exact_boundary() {
1286 let seg = Segment::text("abcde");
1287 let (left, right) = seg.split_at_cell(5);
1288 assert_eq!(left.as_str(), "abcde");
1289 assert_eq!(right.as_str(), "");
1290 }
1291
1292 #[test]
1293 fn segment_line_split_at_zero() {
1294 let line = SegmentLine::from_segment(Segment::text("hello"));
1295 let (left, right) = line.split_at_cell(0);
1296 assert!(left.is_empty());
1297 assert_eq!(right.to_plain_text(), "hello");
1298 }
1299
1300 #[test]
1301 fn segment_line_split_at_end() {
1302 let line = SegmentLine::from_segment(Segment::text("hello"));
1303 let (left, right) = line.split_at_cell(100);
1304 assert_eq!(left.to_plain_text(), "hello");
1305 assert!(right.is_empty());
1306 }
1307
1308 #[test]
1309 fn join_lines_single_line() {
1310 let mut lines = SegmentLines::new();
1311 lines.push(SegmentLine::from_segment(Segment::text("only")));
1312 let joined = join_lines(&lines);
1313 assert_eq!(joined.len(), 1);
1314 assert_eq!(joined[0].as_str(), "only");
1315 }
1316
1317 #[test]
1318 fn join_lines_empty() {
1319 let lines = SegmentLines::new();
1320 let joined = join_lines(&lines);
1321 assert!(joined.is_empty());
1322 }
1323
1324 #[test]
1325 fn split_into_lines_multiple_newlines() {
1326 let segments = vec![
1327 Segment::text("a"),
1328 Segment::newline(),
1329 Segment::newline(),
1330 Segment::text("b"),
1331 ];
1332 let lines = split_into_lines(segments);
1333 assert_eq!(lines.len(), 3);
1334 assert_eq!(lines.lines()[0].to_plain_text(), "a");
1335 assert!(lines.lines()[1].is_empty());
1336 assert_eq!(lines.lines()[2].to_plain_text(), "b");
1337 }
1338
1339 #[test]
1340 fn split_into_lines_trailing_newline() {
1341 let segments = vec![Segment::text("hello"), Segment::newline()];
1342 let lines = split_into_lines(segments);
1343 assert_eq!(lines.len(), 2);
1344 assert_eq!(lines.lines()[0].to_plain_text(), "hello");
1345 assert!(lines.lines()[1].is_empty());
1346 }
1347}
1348
1349#[cfg(test)]
1350mod proptests {
1351 use super::*;
1352 use proptest::prelude::*;
1353
1354 proptest! {
1355 #[test]
1356 fn split_preserves_total_width(s in "[a-zA-Z0-9 ]{1,100}", pos in 0usize..200) {
1357 let seg = Segment::text(s);
1358 let total = seg.cell_length();
1359 let (left, right) = seg.split_at_cell(pos);
1360
1361 prop_assert_eq!(left.cell_length() + right.cell_length(), total);
1363 }
1364
1365 #[test]
1366 fn split_preserves_content(s in "[a-zA-Z0-9 ]{1,100}", pos in 0usize..200) {
1367 let seg = Segment::text(s.clone());
1368 let (left, right) = seg.split_at_cell(pos);
1369
1370 let combined = format!("{}{}", left.as_str(), right.as_str());
1372 prop_assert_eq!(combined, s);
1373 }
1374
1375 #[test]
1376 fn cell_length_matches_display_width(s in "[a-zA-Z0-9 ]{1,100}") {
1377 let seg = Segment::text(s.clone());
1378 let expected = crate::display_width(s.as_str());
1379 prop_assert_eq!(seg.cell_length(), expected);
1380 }
1381
1382 #[test]
1383 fn line_split_preserves_total_width(
1384 parts in prop::collection::vec("[a-z]{1,10}", 1..5),
1385 pos in 0usize..100
1386 ) {
1387 let mut line = SegmentLine::new();
1388 for part in &parts {
1389 line.push(Segment::text(part.as_str()));
1390 }
1391
1392 let total = line.cell_length();
1393 let (left, right) = line.split_at_cell(pos);
1394
1395 prop_assert_eq!(left.cell_length() + right.cell_length(), total);
1396 }
1397
1398 #[test]
1399 fn split_into_lines_preserves_content(s in "[a-zA-Z0-9 \n]{1,200}") {
1400 let segments = vec![Segment::text(s.clone())];
1401 let lines = split_into_lines(segments);
1402
1403 let mut result = String::new();
1405 for (i, line) in lines.lines().iter().enumerate() {
1406 if i > 0 {
1407 result.push('\n');
1408 }
1409 result.push_str(&line.to_plain_text());
1410 }
1411
1412 prop_assert_eq!(result, s);
1413 }
1414 }
1415}