1#![forbid(unsafe_code)]
2
3use crate::TextMeasurement;
32use crate::grapheme_width;
33use crate::segment::{Segment, SegmentLine, SegmentLines, split_into_lines};
34use crate::wrap::{WrapMode, graphemes, truncate_to_width_with_info};
35use ftui_style::Style;
36use std::borrow::Cow;
37use unicode_segmentation::UnicodeSegmentation;
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct Span<'a> {
45 pub content: Cow<'a, str>,
47 pub style: Option<Style>,
49 pub link: Option<Cow<'a, str>>,
51}
52
53impl<'a> Span<'a> {
54 #[inline]
56 #[must_use]
57 pub fn raw(content: impl Into<Cow<'a, str>>) -> Self {
58 Self {
59 content: content.into(),
60 style: None,
61 link: None,
62 }
63 }
64
65 #[inline]
67 #[must_use]
68 pub fn styled(content: impl Into<Cow<'a, str>>, style: Style) -> Self {
69 Self {
70 content: content.into(),
71 style: Some(style),
72 link: None,
73 }
74 }
75
76 #[inline]
78 #[must_use]
79 pub fn link(mut self, link: impl Into<Cow<'a, str>>) -> Self {
80 self.link = Some(link.into());
81 self
82 }
83
84 #[inline]
86 #[must_use]
87 pub fn as_str(&self) -> &str {
88 &self.content
89 }
90
91 #[inline]
93 #[must_use]
94 pub fn width(&self) -> usize {
95 crate::display_width(&self.content)
96 }
97
98 #[must_use]
102 pub fn split_at_cell(&self, cell_pos: usize) -> (Self, Self) {
103 if self.content.is_empty() || cell_pos == 0 {
104 return (
105 Self {
106 content: Cow::Borrowed(""),
107 style: self.style,
108 link: self.link.clone(),
109 },
110 self.clone(),
111 );
112 }
113
114 let total_width = self.width();
115 if cell_pos >= total_width {
116 return (
117 self.clone(),
118 Self {
119 content: Cow::Borrowed(""),
120 style: self.style,
121 link: self.link.clone(),
122 },
123 );
124 }
125
126 let (byte_pos, _actual_width) = find_cell_boundary(&self.content, cell_pos);
127
128 let (left_cow, right_cow) = match &self.content {
129 Cow::Borrowed(s) => {
130 let (l, r) = s.split_at(byte_pos);
131 (Cow::Borrowed(l), Cow::Borrowed(r))
132 }
133 Cow::Owned(s) => {
134 let (l, r) = s.split_at(byte_pos);
135 (Cow::Owned(l.to_string()), Cow::Owned(r.to_string()))
136 }
137 };
138
139 (
140 Self {
141 content: left_cow,
142 style: self.style,
143 link: self.link.clone(),
144 },
145 Self {
146 content: right_cow,
147 style: self.style,
148 link: self.link.clone(),
149 },
150 )
151 }
152
153 #[must_use]
155 pub fn measurement(&self) -> TextMeasurement {
156 let width = self.width();
157 TextMeasurement {
158 minimum: width,
159 maximum: width,
160 }
161 }
162
163 #[inline]
165 #[must_use]
166 pub fn is_empty(&self) -> bool {
167 self.content.is_empty()
168 }
169
170 #[inline]
172 #[must_use]
173 pub fn with_style(mut self, style: Style) -> Self {
174 self.style = Some(style);
175 self
176 }
177
178 #[inline]
180 #[must_use]
181 pub fn into_segment(self) -> Segment<'a> {
182 let mut seg = match self.style {
183 Some(style) => Segment::styled(self.content, style),
184 None => Segment::text(self.content),
185 };
186 seg.link = self.link;
187 seg
188 }
189
190 #[must_use]
192 pub fn into_owned(self) -> Span<'static> {
193 Span {
194 content: Cow::Owned(self.content.into_owned()),
195 style: self.style,
196 link: self.link.map(|l| Cow::Owned(l.into_owned())),
197 }
198 }
199}
200
201impl<'a> From<&'a str> for Span<'a> {
202 fn from(s: &'a str) -> Self {
203 Self::raw(s)
204 }
205}
206
207impl From<String> for Span<'static> {
208 fn from(s: String) -> Self {
209 Self::raw(s)
210 }
211}
212
213impl<'a> From<Segment<'a>> for Span<'a> {
214 fn from(seg: Segment<'a>) -> Self {
215 Self {
216 content: seg.text,
217 style: seg.style,
218 link: seg.link,
219 }
220 }
221}
222
223impl Default for Span<'_> {
224 fn default() -> Self {
225 Self::raw("")
226 }
227}
228
229#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
242pub struct Text<'a> {
243 lines: Vec<Line<'a>>,
245}
246
247#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
249pub struct Line<'a> {
250 spans: Vec<Span<'a>>,
251}
252
253impl<'a> Line<'a> {
254 #[inline]
256 #[must_use]
257 pub const fn new() -> Self {
258 Self { spans: Vec::new() }
259 }
260
261 #[must_use]
263 pub fn from_spans(spans: impl IntoIterator<Item = Span<'a>>) -> Self {
264 Self {
265 spans: spans.into_iter().collect(),
266 }
267 }
268
269 #[inline]
271 #[must_use]
272 pub fn raw(content: impl Into<Cow<'a, str>>) -> Self {
273 Self {
274 spans: vec![Span::raw(content)],
275 }
276 }
277
278 #[inline]
280 #[must_use]
281 pub fn styled(content: impl Into<Cow<'a, str>>, style: Style) -> Self {
282 Self {
283 spans: vec![Span::styled(content, style)],
284 }
285 }
286
287 #[inline]
289 #[must_use]
290 pub fn is_empty(&self) -> bool {
291 self.spans.is_empty() || self.spans.iter().all(|s| s.is_empty())
292 }
293
294 #[inline]
296 #[must_use]
297 pub fn len(&self) -> usize {
298 self.spans.len()
299 }
300
301 #[inline]
303 #[must_use]
304 pub fn width(&self) -> usize {
305 self.spans.iter().map(|s| s.width()).sum()
306 }
307
308 #[must_use]
310 pub fn measurement(&self) -> TextMeasurement {
311 let width = self.width();
312 TextMeasurement {
313 minimum: width,
314 maximum: width,
315 }
316 }
317
318 #[inline]
320 #[must_use]
321 pub fn spans(&self) -> &[Span<'a>] {
322 &self.spans
323 }
324
325 #[inline]
327 pub fn push_span(&mut self, span: Span<'a>) {
328 self.spans.push(span);
329 }
330
331 #[inline]
333 #[must_use]
334 pub fn with_span(mut self, span: Span<'a>) -> Self {
335 self.push_span(span);
336 self
337 }
338
339 pub fn apply_base_style(&mut self, base: Style) {
344 for span in &mut self.spans {
345 span.style = Some(match span.style {
346 Some(existing) => existing.merge(&base),
347 None => base,
348 });
349 }
350 }
351
352 #[must_use]
354 pub fn to_plain_text(&self) -> String {
355 self.spans.iter().map(|s| s.as_str()).collect()
356 }
357
358 #[must_use]
360 pub fn wrap(&self, width: usize, mode: WrapMode) -> Vec<Line<'a>> {
361 if mode == WrapMode::None || width == 0 {
362 return vec![self.clone()];
363 }
364
365 if self.is_empty() {
366 return vec![Line::new()];
367 }
368
369 match mode {
370 WrapMode::None => vec![self.clone()],
371 WrapMode::Char => wrap_line_chars(self, width),
372 WrapMode::Word | WrapMode::Optimal => wrap_line_words(self, width, false),
373 WrapMode::WordChar => wrap_line_words(self, width, true),
374 }
375 }
376
377 #[must_use]
379 pub fn into_segments(self) -> Vec<Segment<'a>> {
380 self.spans.into_iter().map(|s| s.into_segment()).collect()
381 }
382
383 #[must_use]
385 pub fn into_segment_line(self) -> SegmentLine<'a> {
386 SegmentLine::from_segments(self.into_segments())
387 }
388
389 pub fn iter(&self) -> impl Iterator<Item = &Span<'a>> {
391 self.spans.iter()
392 }
393}
394
395impl<'a> From<Span<'a>> for Line<'a> {
396 fn from(span: Span<'a>) -> Self {
397 Self { spans: vec![span] }
398 }
399}
400
401impl<'a> From<&'a str> for Line<'a> {
402 fn from(s: &'a str) -> Self {
403 Self::raw(s)
404 }
405}
406
407impl From<String> for Line<'static> {
408 fn from(s: String) -> Self {
409 Self::raw(s)
410 }
411}
412
413impl<'a> IntoIterator for Line<'a> {
414 type Item = Span<'a>;
415 type IntoIter = std::vec::IntoIter<Span<'a>>;
416
417 fn into_iter(self) -> Self::IntoIter {
418 self.spans.into_iter()
419 }
420}
421
422impl<'a> IntoIterator for &'a Line<'a> {
423 type Item = &'a Span<'a>;
424 type IntoIter = std::slice::Iter<'a, Span<'a>>;
425
426 fn into_iter(self) -> Self::IntoIter {
427 self.spans.iter()
428 }
429}
430
431impl<'a> Text<'a> {
432 #[inline]
434 #[must_use]
435 pub const fn new() -> Self {
436 Self { lines: Vec::new() }
437 }
438
439 #[must_use]
441 pub fn raw(content: impl Into<Cow<'a, str>>) -> Self {
442 let content = content.into();
443 if content.is_empty() {
444 return Self::new();
445 }
446
447 let lines: Vec<Line<'a>> = match content {
451 Cow::Borrowed(s) => s.split('\n').map(Line::raw).collect(),
452 Cow::Owned(s) => s
453 .split('\n')
454 .map(|line| Line::raw(line.to_string()))
455 .collect(),
456 };
457
458 Self { lines }
459 }
460
461 #[must_use]
463 pub fn styled(content: impl Into<Cow<'a, str>>, style: Style) -> Self {
464 let content = content.into();
465 if content.is_empty() {
466 return Self::new();
467 }
468
469 let lines: Vec<Line<'a>> = match content {
470 Cow::Borrowed(s) => s.split('\n').map(|l| Line::styled(l, style)).collect(),
471 Cow::Owned(s) => s
472 .split('\n')
473 .map(|l| Line::styled(l.to_string(), style))
474 .collect(),
475 };
476
477 Self { lines }
478 }
479
480 #[inline]
482 #[must_use]
483 pub fn from_line(line: Line<'a>) -> Self {
484 Self { lines: vec![line] }
485 }
486
487 #[must_use]
489 pub fn from_lines(lines: impl IntoIterator<Item = Line<'a>>) -> Self {
490 Self {
491 lines: lines.into_iter().collect(),
492 }
493 }
494
495 #[must_use]
497 pub fn from_spans(spans: impl IntoIterator<Item = Span<'a>>) -> Self {
498 Self {
499 lines: vec![Line::from_spans(spans)],
500 }
501 }
502
503 #[must_use]
505 pub fn from_segments(segments: impl IntoIterator<Item = Segment<'a>>) -> Self {
506 let segment_lines = split_into_lines(segments);
507 let lines: Vec<Line<'a>> = segment_lines
508 .into_iter()
509 .map(|seg_line| Line::from_spans(seg_line.into_iter().map(Span::from)))
510 .collect();
511
512 Self { lines }
513 }
514
515 #[inline]
517 #[must_use]
518 pub fn is_empty(&self) -> bool {
519 self.lines.is_empty() || self.lines.iter().all(|l| l.is_empty())
520 }
521
522 #[inline]
524 #[must_use]
525 pub fn height(&self) -> usize {
526 self.lines.len()
527 }
528
529 #[inline]
531 #[must_use]
532 pub fn height_as_u16(&self) -> u16 {
533 self.lines.len().try_into().unwrap_or(u16::MAX)
534 }
535
536 #[inline]
538 #[must_use]
539 pub fn width(&self) -> usize {
540 self.lines.iter().map(|l| l.width()).max().unwrap_or(0)
541 }
542
543 #[must_use]
545 pub fn measurement(&self) -> TextMeasurement {
546 let width = self.width();
547 TextMeasurement {
548 minimum: width,
549 maximum: width,
550 }
551 }
552
553 #[inline]
555 #[must_use]
556 pub fn lines(&self) -> &[Line<'a>] {
557 &self.lines
558 }
559
560 #[inline]
564 #[must_use]
565 pub fn style(&self) -> Option<Style> {
566 self.lines
567 .first()
568 .and_then(|line| line.spans().first())
569 .and_then(|span| span.style)
570 }
571
572 #[inline]
574 pub fn push_line(&mut self, line: Line<'a>) {
575 self.lines.push(line);
576 }
577
578 #[inline]
580 #[must_use]
581 pub fn with_line(mut self, line: Line<'a>) -> Self {
582 self.push_line(line);
583 self
584 }
585
586 pub fn push_span(&mut self, span: Span<'a>) {
588 if self.lines.is_empty() {
589 self.lines.push(Line::new());
590 }
591 if let Some(last) = self.lines.last_mut() {
592 last.push_span(span);
593 }
594 }
595
596 #[must_use]
598 pub fn with_span(mut self, span: Span<'a>) -> Self {
599 self.push_span(span);
600 self
601 }
602
603 pub fn apply_base_style(&mut self, base: Style) {
608 for line in &mut self.lines {
609 line.apply_base_style(base);
610 }
611 }
612
613 #[must_use]
615 pub fn with_base_style(mut self, base: Style) -> Self {
616 self.apply_base_style(base);
617 self
618 }
619
620 #[must_use]
622 pub fn to_plain_text(&self) -> String {
623 self.lines
624 .iter()
625 .map(|l| l.to_plain_text())
626 .collect::<Vec<_>>()
627 .join("\n")
628 }
629
630 #[must_use]
632 pub fn into_segment_lines(self) -> SegmentLines<'a> {
633 SegmentLines::from_lines(
634 self.lines
635 .into_iter()
636 .map(|l| l.into_segment_line())
637 .collect(),
638 )
639 }
640
641 pub fn iter(&self) -> impl Iterator<Item = &Line<'a>> {
643 self.lines.iter()
644 }
645
646 pub fn truncate(&mut self, max_width: usize, ellipsis: Option<&str>) {
651 let ellipsis_width = ellipsis.map(crate::display_width).unwrap_or(0);
652
653 for line in &mut self.lines {
654 let line_width = line.width();
655 if line_width <= max_width {
656 continue;
657 }
658
659 let (content_width, use_ellipsis) = if ellipsis.is_some() && max_width >= ellipsis_width
661 {
662 (max_width - ellipsis_width, true)
663 } else {
664 (max_width, false)
665 };
666
667 let mut remaining = content_width;
669 let mut new_spans = Vec::new();
670
671 for span in &line.spans {
672 if remaining == 0 {
673 break;
674 }
675
676 let span_width = span.width();
677 if span_width <= remaining {
678 new_spans.push(span.clone());
679 remaining -= span_width;
680 } else {
681 let (truncated, _) = truncate_to_width_with_info(&span.content, remaining);
683 if !truncated.is_empty() {
684 new_spans.push(Span {
685 content: Cow::Owned(truncated.to_string()),
686 style: span.style,
687 link: span.link.clone(),
688 });
689 }
690 remaining = 0;
691 }
692 }
693
694 if use_ellipsis
696 && line_width > max_width
697 && let Some(e) = ellipsis
698 {
699 new_spans.push(Span::raw(e.to_string()));
700 }
701
702 line.spans = new_spans;
703 }
704 }
705
706 #[must_use]
708 pub fn truncated(&self, max_width: usize, ellipsis: Option<&str>) -> Self {
709 let mut text = self.clone();
710 text.truncate(max_width, ellipsis);
711 text
712 }
713}
714
715fn find_cell_boundary(text: &str, target_cells: usize) -> (usize, usize) {
720 let mut current_cells = 0;
721 let mut byte_pos = 0;
722
723 for grapheme in graphemes(text) {
724 let grapheme_width = grapheme_width(grapheme);
725
726 if current_cells + grapheme_width > target_cells {
727 break;
728 }
729
730 current_cells += grapheme_width;
731 byte_pos += grapheme.len();
732
733 if current_cells >= target_cells {
734 break;
735 }
736 }
737
738 (byte_pos, current_cells)
739}
740
741fn span_is_whitespace(span: &Span<'_>) -> bool {
742 span.as_str()
743 .graphemes(true)
744 .all(|g| g.chars().all(|c| c.is_whitespace()))
745}
746
747fn trim_span_start<'a>(span: Span<'a>) -> Span<'a> {
748 let text = span.as_str();
749 let mut start = 0;
750 let mut found = false;
751
752 for (idx, grapheme) in text.grapheme_indices(true) {
753 if grapheme.chars().all(|c| c.is_whitespace()) {
754 start = idx + grapheme.len();
755 continue;
756 }
757 found = true;
758 break;
759 }
760
761 if !found {
762 return Span::raw("");
763 }
764
765 Span {
766 content: Cow::Owned(text[start..].to_string()),
767 style: span.style,
768 link: span.link,
769 }
770}
771
772fn trim_span_end<'a>(span: Span<'a>) -> Span<'a> {
773 let text = span.as_str();
774 let mut end = text.len();
775 let mut found = false;
776
777 for (idx, grapheme) in text.grapheme_indices(true).rev() {
778 if grapheme.chars().all(|c| c.is_whitespace()) {
779 end = idx;
780 continue;
781 }
782 found = true;
783 break;
784 }
785
786 if !found {
787 return Span::raw("");
788 }
789
790 Span {
791 content: Cow::Owned(text[..end].to_string()),
792 style: span.style,
793 link: span.link,
794 }
795}
796
797fn trim_line_trailing<'a>(mut line: Line<'a>) -> Line<'a> {
798 while let Some(last) = line.spans.last().cloned() {
799 let trimmed = trim_span_end(last);
800 if trimmed.is_empty() {
801 line.spans.pop();
802 continue;
803 }
804 let len = line.spans.len();
805 if len > 0 {
806 line.spans[len - 1] = trimmed;
807 }
808 break;
809 }
810 line
811}
812
813fn push_span_merged<'a>(line: &mut Line<'a>, span: Span<'a>) {
814 if span.is_empty() {
815 return;
816 }
817
818 if let Some(last) = line.spans.last_mut()
819 && last.style == span.style
820 && last.link == span.link
821 {
822 let mut merged = String::with_capacity(last.as_str().len() + span.as_str().len());
823 merged.push_str(last.as_str());
824 merged.push_str(span.as_str());
825 last.content = Cow::Owned(merged);
826 return;
827 }
828
829 line.spans.push(span);
830}
831
832fn split_span_words<'a>(span: &Span<'a>) -> Vec<Span<'a>> {
833 let (text, borrowed_base): (&str, Option<&'a str>) = match &span.content {
834 Cow::Borrowed(s) => (*s, Some(*s)),
835 Cow::Owned(s) => (s.as_str(), None),
836 };
837 let mut start = 0;
838 let mut in_whitespace = false;
839 let mut segments = Vec::new();
840
841 for (idx, grapheme) in text.grapheme_indices(true) {
843 let is_ws = grapheme.chars().all(crate::wrap::is_breaking_whitespace);
844 if idx == 0 {
845 in_whitespace = is_ws;
846 }
847
848 if is_ws != in_whitespace {
849 let sub = &text[start..idx];
851 let content = match borrowed_base {
852 Some(base) => Cow::Borrowed(&base[start..idx]),
853 None => Cow::Owned(sub.to_string()),
854 };
855 segments.push(Span {
856 content,
857 style: span.style,
858 link: span.link.clone(),
859 });
860 start = idx;
861 in_whitespace = is_ws;
862 }
863 }
864
865 if start < text.len() {
867 let sub = &text[start..];
868 let content = match borrowed_base {
869 Some(base) => Cow::Borrowed(&base[start..]),
870 None => Cow::Owned(sub.to_string()),
871 };
872 segments.push(Span {
873 content,
874 style: span.style,
875 link: span.link.clone(),
876 });
877 }
878
879 segments
880}
881
882fn wrap_line_chars<'a>(line: &Line<'a>, width: usize) -> Vec<Line<'a>> {
883 let mut lines = Vec::new();
884 let mut current = Line::new();
885 let mut current_width = 0;
886
887 for span in line.spans.iter().cloned() {
888 let mut remaining = span;
889 while !remaining.is_empty() {
890 if current_width >= width && !current.is_empty() {
891 lines.push(trim_line_trailing(current));
892 current = Line::new();
893 current_width = 0;
894 }
895
896 let available = width.saturating_sub(current_width).max(1);
897 let span_width = remaining.width();
898
899 if span_width <= available {
900 current_width += span_width;
901 push_span_merged(&mut current, remaining);
902 break;
903 }
904
905 let (left, right) = remaining.split_at_cell(available);
906
907 let (left, right) = if left.is_empty() && current.is_empty() && !remaining.is_empty() {
910 let first_w = remaining
911 .as_str()
912 .graphemes(true)
913 .next()
914 .map(grapheme_width)
915 .unwrap_or(1);
916 remaining.split_at_cell(first_w.max(1))
917 } else {
918 (left, right)
919 };
920
921 if !left.is_empty() {
922 push_span_merged(&mut current, left);
923 }
924 lines.push(trim_line_trailing(current));
925 current = Line::new();
926 current_width = 0;
927 remaining = right;
928 }
929 }
930
931 if !current.is_empty() || lines.is_empty() {
932 lines.push(trim_line_trailing(current));
933 }
934
935 lines
936}
937
938fn wrap_line_words<'a>(line: &Line<'a>, width: usize, char_fallback: bool) -> Vec<Line<'a>> {
939 let mut pieces: Vec<Span<'a>> = Vec::new();
940 for span in &line.spans {
941 pieces.extend(split_span_words(span));
942 }
943
944 let mut lines = Vec::new();
945 let mut current = Line::new();
946 let mut current_width = 0;
947 let mut first_line = true;
948
949 for piece in pieces {
950 let piece_width = piece.width();
951 let is_ws = span_is_whitespace(&piece);
952
953 if current_width + piece_width <= width {
954 if current_width == 0 && !first_line && is_ws {
955 continue;
956 }
957 current_width += piece_width;
958 push_span_merged(&mut current, piece);
959 continue;
960 }
961
962 if !current.is_empty() {
963 lines.push(trim_line_trailing(current));
964 current = Line::new();
965 current_width = 0;
966 first_line = false;
967 }
968
969 if piece_width > width {
970 if char_fallback {
971 let mut remaining = piece;
972 while !remaining.is_empty() {
973 if current_width >= width && !current.is_empty() {
974 lines.push(trim_line_trailing(current));
975 current = Line::new();
976 current_width = 0;
977 first_line = false;
978 }
979
980 let available = width.saturating_sub(current_width).max(1);
981 let (left, right) = remaining.split_at_cell(available);
982
983 let (left, right) =
986 if left.is_empty() && current.is_empty() && !remaining.is_empty() {
987 let first_w = remaining
988 .as_str()
989 .graphemes(true)
990 .next()
991 .map(grapheme_width)
992 .unwrap_or(1);
993 remaining.split_at_cell(first_w.max(1))
994 } else {
995 (left, right)
996 };
997
998 let mut left = left;
999
1000 if current_width == 0 && !first_line {
1001 left = trim_span_start(left);
1002 }
1003
1004 if !left.is_empty() {
1005 current_width += left.width();
1006 push_span_merged(&mut current, left);
1007 }
1008
1009 if current_width >= width && !current.is_empty() {
1010 lines.push(trim_line_trailing(current));
1011 current = Line::new();
1012 current_width = 0;
1013 first_line = false;
1014 }
1015
1016 remaining = right;
1017 }
1018 } else if !is_ws {
1019 let mut trimmed = piece;
1020 if !first_line {
1021 trimmed = trim_span_start(trimmed);
1022 }
1023 if !trimmed.is_empty() {
1024 push_span_merged(&mut current, trimmed);
1025 }
1026 lines.push(trim_line_trailing(current));
1027 current = Line::new();
1028 current_width = 0;
1029 first_line = false;
1030 }
1031 continue;
1032 }
1033
1034 let mut trimmed = piece;
1035 if !first_line {
1036 trimmed = trim_span_start(trimmed);
1037 }
1038 if !trimmed.is_empty() {
1039 current_width += trimmed.width();
1040 push_span_merged(&mut current, trimmed);
1041 }
1042 }
1043
1044 if !current.is_empty() || lines.is_empty() {
1045 lines.push(trim_line_trailing(current));
1046 }
1047
1048 lines
1049}
1050
1051impl<'a> From<&'a str> for Text<'a> {
1052 fn from(s: &'a str) -> Self {
1053 Self::raw(s)
1054 }
1055}
1056
1057impl From<String> for Text<'static> {
1058 fn from(s: String) -> Self {
1059 Self::raw(s)
1060 }
1061}
1062
1063impl<'a> From<Line<'a>> for Text<'a> {
1064 fn from(line: Line<'a>) -> Self {
1065 Self::from_line(line)
1066 }
1067}
1068
1069impl<'a> FromIterator<Span<'a>> for Text<'a> {
1070 fn from_iter<I: IntoIterator<Item = Span<'a>>>(iter: I) -> Self {
1071 Self::from_spans(iter)
1072 }
1073}
1074
1075impl<'a> FromIterator<Line<'a>> for Text<'a> {
1076 fn from_iter<I: IntoIterator<Item = Line<'a>>>(iter: I) -> Self {
1077 Self::from_lines(iter)
1078 }
1079}
1080
1081impl<'a> IntoIterator for Text<'a> {
1082 type Item = Line<'a>;
1083 type IntoIter = std::vec::IntoIter<Line<'a>>;
1084
1085 fn into_iter(self) -> Self::IntoIter {
1086 self.lines.into_iter()
1087 }
1088}
1089
1090impl<'a> IntoIterator for &'a Text<'a> {
1091 type Item = &'a Line<'a>;
1092 type IntoIter = std::slice::Iter<'a, Line<'a>>;
1093
1094 fn into_iter(self) -> Self::IntoIter {
1095 self.lines.iter()
1096 }
1097}
1098
1099#[cfg(test)]
1100mod tests {
1101 use super::*;
1102 use ftui_style::StyleFlags;
1103
1104 #[test]
1109 fn span_raw_creates_unstyled() {
1110 let span = Span::raw("hello");
1111 assert_eq!(span.as_str(), "hello");
1112 assert!(span.style.is_none());
1113 }
1114
1115 #[test]
1116 fn span_styled_creates_styled() {
1117 let style = Style::new().bold();
1118 let span = Span::styled("hello", style);
1119 assert_eq!(span.as_str(), "hello");
1120 assert_eq!(span.style, Some(style));
1121 }
1122
1123 #[test]
1124 fn span_width_ascii() {
1125 let span = Span::raw("hello");
1126 assert_eq!(span.width(), 5);
1127 }
1128
1129 #[test]
1130 fn span_width_cjk() {
1131 let span = Span::raw("你好");
1132 assert_eq!(span.width(), 4);
1133 }
1134
1135 #[test]
1136 fn span_into_segment() {
1137 let style = Style::new().bold();
1138 let span = Span::styled("hello", style);
1139 let seg = span.into_segment();
1140 assert_eq!(seg.as_str(), "hello");
1141 assert_eq!(seg.style, Some(style));
1142 }
1143
1144 #[test]
1149 fn line_empty() {
1150 let line = Line::new();
1151 assert!(line.is_empty());
1152 assert_eq!(line.width(), 0);
1153 }
1154
1155 #[test]
1156 fn line_raw() {
1157 let line = Line::raw("hello world");
1158 assert_eq!(line.width(), 11);
1159 assert_eq!(line.to_plain_text(), "hello world");
1160 }
1161
1162 #[test]
1163 fn line_styled() {
1164 let style = Style::new().bold();
1165 let line = Line::styled("hello", style);
1166 assert_eq!(line.spans()[0].style, Some(style));
1167 }
1168
1169 #[test]
1170 fn line_from_spans() {
1171 let line = Line::from_spans([Span::raw("hello "), Span::raw("world")]);
1172 assert_eq!(line.len(), 2);
1173 assert_eq!(line.width(), 11);
1174 assert_eq!(line.to_plain_text(), "hello world");
1175 }
1176
1177 #[test]
1178 fn line_push_span() {
1179 let mut line = Line::raw("hello ");
1180 line.push_span(Span::raw("world"));
1181 assert_eq!(line.len(), 2);
1182 assert_eq!(line.to_plain_text(), "hello world");
1183 }
1184
1185 #[test]
1186 fn line_apply_base_style() {
1187 let base = Style::new().bold();
1188 let mut line = Line::from_spans([
1189 Span::raw("hello"),
1190 Span::styled("world", Style::new().italic()),
1191 ]);
1192
1193 line.apply_base_style(base);
1194
1195 assert!(line.spans()[0].style.unwrap().has_attr(StyleFlags::BOLD));
1197
1198 let second_style = line.spans()[1].style.unwrap();
1200 assert!(second_style.has_attr(StyleFlags::BOLD));
1201 assert!(second_style.has_attr(StyleFlags::ITALIC));
1202 }
1203
1204 #[test]
1205 fn line_wrap_preserves_styles_word() {
1206 let bold = Style::new().bold();
1207 let italic = Style::new().italic();
1208 let line = Line::from_spans([Span::styled("Hello", bold), Span::styled(" world", italic)]);
1209
1210 let wrapped = line.wrap(6, WrapMode::Word);
1211 assert_eq!(wrapped.len(), 2);
1212 assert_eq!(wrapped[0].spans()[0].as_str(), "Hello");
1213 assert_eq!(wrapped[0].spans()[0].style, Some(bold));
1214 assert_eq!(wrapped[1].spans()[0].as_str(), "world");
1215 assert_eq!(wrapped[1].spans()[0].style, Some(italic));
1216 }
1217
1218 #[test]
1223 fn text_empty() {
1224 let text = Text::new();
1225 assert!(text.is_empty());
1226 assert_eq!(text.height(), 0);
1227 assert_eq!(text.width(), 0);
1228 }
1229
1230 #[test]
1231 fn text_raw_single_line() {
1232 let text = Text::raw("hello world");
1233 assert_eq!(text.height(), 1);
1234 assert_eq!(text.width(), 11);
1235 assert_eq!(text.to_plain_text(), "hello world");
1236 }
1237
1238 #[test]
1239 fn text_raw_multiline() {
1240 let text = Text::raw("line 1\nline 2\nline 3");
1241 assert_eq!(text.height(), 3);
1242 assert_eq!(text.to_plain_text(), "line 1\nline 2\nline 3");
1243 }
1244
1245 #[test]
1246 fn text_styled() {
1247 let style = Style::new().bold();
1248 let text = Text::styled("hello", style);
1249 assert_eq!(text.lines()[0].spans()[0].style, Some(style));
1250 }
1251
1252 #[test]
1253 fn text_from_spans() {
1254 let text = Text::from_spans([Span::raw("hello "), Span::raw("world")]);
1255 assert_eq!(text.height(), 1);
1256 assert_eq!(text.to_plain_text(), "hello world");
1257 }
1258
1259 #[test]
1260 fn text_from_lines() {
1261 let text = Text::from_lines([Line::raw("line 1"), Line::raw("line 2")]);
1262 assert_eq!(text.height(), 2);
1263 assert_eq!(text.to_plain_text(), "line 1\nline 2");
1264 }
1265
1266 #[test]
1267 fn text_push_line() {
1268 let mut text = Text::raw("line 1");
1269 text.push_line(Line::raw("line 2"));
1270 assert_eq!(text.height(), 2);
1271 }
1272
1273 #[test]
1274 fn text_push_span() {
1275 let mut text = Text::raw("hello ");
1276 text.push_span(Span::raw("world"));
1277 assert_eq!(text.to_plain_text(), "hello world");
1278 }
1279
1280 #[test]
1281 fn text_apply_base_style() {
1282 let base = Style::new().bold();
1283 let mut text = Text::from_lines([
1284 Line::raw("line 1"),
1285 Line::styled("line 2", Style::new().italic()),
1286 ]);
1287
1288 text.apply_base_style(base);
1289
1290 assert!(
1292 text.lines()[0].spans()[0]
1293 .style
1294 .unwrap()
1295 .has_attr(StyleFlags::BOLD)
1296 );
1297
1298 let second_style = text.lines()[1].spans()[0].style.unwrap();
1300 assert!(second_style.has_attr(StyleFlags::BOLD));
1301 assert!(second_style.has_attr(StyleFlags::ITALIC));
1302 }
1303
1304 #[test]
1305 fn text_width_multiline() {
1306 let text = Text::raw("short\nlonger line\nmed");
1307 assert_eq!(text.width(), 11); }
1309
1310 #[test]
1315 fn truncate_no_change_if_fits() {
1316 let mut text = Text::raw("hello");
1317 text.truncate(10, None);
1318 assert_eq!(text.to_plain_text(), "hello");
1319 }
1320
1321 #[test]
1322 fn truncate_simple() {
1323 let mut text = Text::raw("hello world");
1324 text.truncate(5, None);
1325 assert_eq!(text.to_plain_text(), "hello");
1326 }
1327
1328 #[test]
1329 fn truncate_with_ellipsis() {
1330 let mut text = Text::raw("hello world");
1331 text.truncate(8, Some("..."));
1332 assert_eq!(text.to_plain_text(), "hello...");
1333 }
1334
1335 #[test]
1336 fn truncate_multiline() {
1337 let mut text = Text::raw("hello world\nfoo bar baz");
1338 text.truncate(8, Some("..."));
1339 assert_eq!(text.to_plain_text(), "hello...\nfoo b...");
1340 }
1341
1342 #[test]
1343 fn truncate_preserves_style() {
1344 let style = Style::new().bold();
1345 let mut text = Text::styled("hello world", style);
1346 text.truncate(5, None);
1347
1348 assert_eq!(text.lines()[0].spans()[0].style, Some(style));
1349 }
1350
1351 #[test]
1352 fn truncate_cjk() {
1353 let mut text = Text::raw("你好世界"); text.truncate(4, None);
1355 assert_eq!(text.to_plain_text(), "你好");
1356 }
1357
1358 #[test]
1359 fn truncate_cjk_odd_width() {
1360 let mut text = Text::raw("你好世界"); text.truncate(5, None); assert_eq!(text.to_plain_text(), "你好");
1363 }
1364
1365 #[test]
1370 fn text_from_str() {
1371 let text: Text = "hello".into();
1372 assert_eq!(text.to_plain_text(), "hello");
1373 }
1374
1375 #[test]
1376 fn text_from_string() {
1377 let text: Text = String::from("hello").into();
1378 assert_eq!(text.to_plain_text(), "hello");
1379 }
1380
1381 #[test]
1382 fn text_from_empty_string_is_empty() {
1383 let text: Text = String::new().into();
1384 assert!(text.is_empty());
1385 assert_eq!(text.height(), 0);
1386 assert_eq!(text.width(), 0);
1387 }
1388
1389 #[test]
1390 fn text_from_empty_line_preserves_single_empty_line() {
1391 let text: Text = Line::new().into();
1392 assert_eq!(text.height(), 1);
1393 assert!(text.lines()[0].is_empty());
1394 assert_eq!(text.width(), 0);
1395 }
1396
1397 #[test]
1398 fn text_from_lines_empty_iter_is_empty() {
1399 let text = Text::from_lines(Vec::<Line>::new());
1400 assert!(text.is_empty());
1401 assert_eq!(text.height(), 0);
1402 }
1403
1404 #[test]
1405 fn text_from_str_preserves_empty_middle_line() {
1406 let text: Text = "a\n\nb".into();
1407 assert_eq!(text.height(), 3);
1408 assert_eq!(text.lines()[0].to_plain_text(), "a");
1409 assert!(text.lines()[1].is_empty());
1410 assert_eq!(text.lines()[2].to_plain_text(), "b");
1411 assert_eq!(text.to_plain_text(), "a\n\nb");
1412 }
1413
1414 #[test]
1415 fn text_into_segment_lines() {
1416 let text = Text::raw("line 1\nline 2");
1417 let seg_lines = text.into_segment_lines();
1418 assert_eq!(seg_lines.len(), 2);
1419 }
1420
1421 #[test]
1422 fn line_into_iter() {
1423 let line = Line::from_spans([Span::raw("a"), Span::raw("b")]);
1424 let collected: Vec<_> = line.into_iter().collect();
1425 assert_eq!(collected.len(), 2);
1426 }
1427
1428 #[test]
1429 fn text_into_iter() {
1430 let text = Text::from_lines([Line::raw("a"), Line::raw("b")]);
1431 let collected: Vec<_> = text.into_iter().collect();
1432 assert_eq!(collected.len(), 2);
1433 }
1434
1435 #[test]
1436 fn text_collect_from_spans() {
1437 let text: Text = [Span::raw("a"), Span::raw("b")].into_iter().collect();
1438 assert_eq!(text.height(), 1);
1439 assert_eq!(text.to_plain_text(), "ab");
1440 }
1441
1442 #[test]
1443 fn text_collect_from_lines() {
1444 let text: Text = [Line::raw("a"), Line::raw("b")].into_iter().collect();
1445 assert_eq!(text.height(), 2);
1446 }
1447
1448 #[test]
1453 fn empty_string_creates_empty_text() {
1454 let text = Text::raw("");
1455 assert!(text.is_empty());
1456 }
1457
1458 #[test]
1459 fn single_newline_creates_two_empty_lines() {
1460 let text = Text::raw("\n");
1461 assert_eq!(text.height(), 2);
1462 assert!(text.lines()[0].is_empty());
1463 assert!(text.lines()[1].is_empty());
1464 }
1465
1466 #[test]
1467 fn trailing_newline() {
1468 let text = Text::raw("hello\n");
1469 assert_eq!(text.height(), 2);
1470 assert_eq!(text.lines()[0].to_plain_text(), "hello");
1471 assert!(text.lines()[1].is_empty());
1472 }
1473
1474 #[test]
1475 fn leading_newline() {
1476 let text = Text::raw("\nhello");
1477 assert_eq!(text.height(), 2);
1478 assert!(text.lines()[0].is_empty());
1479 assert_eq!(text.lines()[1].to_plain_text(), "hello");
1480 }
1481
1482 #[test]
1483 fn line_with_span_ownership() {
1484 let s = String::from("hello");
1486 let line = Line::raw(s.clone());
1487 drop(s); assert_eq!(line.to_plain_text(), "hello"); }
1490
1491 #[test]
1496 fn span_cow_borrowed_from_static() {
1497 let span = Span::raw("static");
1498 assert!(matches!(span.content, Cow::Borrowed(_)));
1499 }
1500
1501 #[test]
1502 fn span_cow_owned_from_string() {
1503 let span = Span::raw(String::from("owned"));
1504 assert!(matches!(span.content, Cow::Owned(_)));
1505 }
1506
1507 #[test]
1508 fn span_into_owned_converts_borrowed() {
1509 let span = Span::raw("borrowed");
1510 assert!(matches!(span.content, Cow::Borrowed(_)));
1511
1512 let owned = span.into_owned();
1513 assert!(matches!(owned.content, Cow::Owned(_)));
1514 assert_eq!(owned.as_str(), "borrowed");
1515 }
1516
1517 #[test]
1518 fn span_with_link_into_owned() {
1519 let span = Span::raw("text").link("https://example.com");
1520 let owned = span.into_owned();
1521 assert!(owned.link.is_some());
1522 assert!(matches!(owned.link.as_ref().unwrap(), Cow::Owned(_)));
1523 }
1524
1525 #[test]
1530 fn span_link_method() {
1531 let span = Span::raw("click me").link("https://example.com");
1532 assert_eq!(span.link.as_deref(), Some("https://example.com"));
1533 }
1534
1535 #[test]
1536 fn span_measurement() {
1537 let span = Span::raw("hello");
1538 let m = span.measurement();
1539 assert_eq!(m.minimum, 5);
1540 assert_eq!(m.maximum, 5);
1541 }
1542
1543 #[test]
1544 fn span_is_empty() {
1545 assert!(Span::raw("").is_empty());
1546 assert!(!Span::raw("x").is_empty());
1547 }
1548
1549 #[test]
1550 fn span_default_is_empty() {
1551 let span = Span::default();
1552 assert!(span.is_empty());
1553 assert!(span.style.is_none());
1554 assert!(span.link.is_none());
1555 }
1556
1557 #[test]
1558 fn span_with_style() {
1559 let style = Style::new().bold();
1560 let span = Span::raw("text").with_style(style);
1561 assert_eq!(span.style, Some(style));
1562 }
1563
1564 #[test]
1565 fn span_from_segment() {
1566 let style = Style::new().italic();
1567 let seg = Segment::styled("hello", style);
1568 let span: Span = seg.into();
1569 assert_eq!(span.as_str(), "hello");
1570 assert_eq!(span.style, Some(style));
1571 }
1572
1573 #[test]
1574 fn span_debug_impl() {
1575 let span = Span::raw("test");
1576 let debug = format!("{:?}", span);
1577 assert!(debug.contains("Span"));
1578 assert!(debug.contains("test"));
1579 }
1580
1581 #[test]
1586 fn line_measurement() {
1587 let line = Line::raw("hello world");
1588 let m = line.measurement();
1589 assert_eq!(m.minimum, 11);
1590 assert_eq!(m.maximum, 11);
1591 }
1592
1593 #[test]
1594 fn line_from_empty_string_is_empty() {
1595 let line: Line = String::new().into();
1596 assert!(line.is_empty());
1597 assert_eq!(line.width(), 0);
1598 }
1599
1600 #[test]
1601 fn line_width_combining_mark_is_single_cell() {
1602 let line = Line::raw("e\u{301}");
1603 assert_eq!(line.width(), 1);
1604 }
1605
1606 #[test]
1607 fn line_wrap_handles_wide_grapheme_with_tiny_width() {
1608 let line = Line::raw("你好");
1609 let wrapped = line.wrap(1, WrapMode::Char);
1610 assert_eq!(wrapped.len(), 2);
1611 assert_eq!(wrapped[0].to_plain_text(), "你");
1612 assert_eq!(wrapped[1].to_plain_text(), "好");
1613 }
1614
1615 #[test]
1616 fn line_iter() {
1617 let line = Line::from_spans([Span::raw("a"), Span::raw("b"), Span::raw("c")]);
1618 let collected: Vec<_> = line.iter().collect();
1619 assert_eq!(collected.len(), 3);
1620 }
1621
1622 #[test]
1623 fn line_into_segments() {
1624 let style = Style::new().bold();
1625 let line = Line::from_spans([Span::raw("hello"), Span::styled(" world", style)]);
1626 let segments = line.into_segments();
1627 assert_eq!(segments.len(), 2);
1628 assert_eq!(segments[0].style, None);
1629 assert_eq!(segments[1].style, Some(style));
1630 }
1631
1632 #[test]
1633 fn line_into_segment_line() {
1634 let line = Line::raw("test");
1635 let seg_line = line.into_segment_line();
1636 assert_eq!(seg_line.to_plain_text(), "test");
1637 }
1638
1639 #[test]
1640 fn line_with_span_builder() {
1641 let line = Line::raw("hello ").with_span(Span::raw("world"));
1642 assert_eq!(line.to_plain_text(), "hello world");
1643 }
1644
1645 #[test]
1646 fn line_from_span() {
1647 let span = Span::styled("test", Style::new().bold());
1648 let line: Line = span.into();
1649 assert_eq!(line.to_plain_text(), "test");
1650 }
1651
1652 #[test]
1653 fn line_debug_impl() {
1654 let line = Line::raw("test");
1655 let debug = format!("{:?}", line);
1656 assert!(debug.contains("Line"));
1657 }
1658
1659 #[test]
1660 fn line_default_is_empty() {
1661 let line = Line::default();
1662 assert!(line.is_empty());
1663 }
1664
1665 #[test]
1670 fn text_style_returns_first_span_style() {
1671 let style = Style::new().bold();
1672 let text = Text::styled("hello", style);
1673 assert_eq!(text.style(), Some(style));
1674 }
1675
1676 #[test]
1677 fn text_style_returns_none_for_empty() {
1678 let text = Text::new();
1679 assert!(text.style().is_none());
1680 }
1681
1682 #[test]
1683 fn text_style_returns_none_for_unstyled() {
1684 let text = Text::raw("plain");
1685 assert!(text.style().is_none());
1686 }
1687
1688 #[test]
1689 fn text_with_line_builder() {
1690 let text = Text::raw("line 1").with_line(Line::raw("line 2"));
1691 assert_eq!(text.height(), 2);
1692 }
1693
1694 #[test]
1695 fn text_with_span_builder() {
1696 let text = Text::raw("hello ").with_span(Span::raw("world"));
1697 assert_eq!(text.to_plain_text(), "hello world");
1698 }
1699
1700 #[test]
1701 fn text_with_base_style_builder() {
1702 let text = Text::raw("test").with_base_style(Style::new().bold());
1703 assert!(
1704 text.lines()[0].spans()[0]
1705 .style
1706 .unwrap()
1707 .has_attr(StyleFlags::BOLD)
1708 );
1709 }
1710
1711 #[test]
1712 fn text_height_as_u16() {
1713 let text = Text::raw("a\nb\nc");
1714 assert_eq!(text.height_as_u16(), 3);
1715 }
1716
1717 #[test]
1718 fn text_height_as_u16_saturates() {
1719 let text = Text::new();
1722 assert_eq!(text.height_as_u16(), 0);
1723 }
1724
1725 #[test]
1726 fn text_measurement() {
1727 let text = Text::raw("short\nlonger line");
1728 let m = text.measurement();
1729 assert_eq!(m.minimum, 11); assert_eq!(m.maximum, 11);
1731 }
1732
1733 #[test]
1734 fn text_from_segments_with_newlines() {
1735 let segments = vec![
1736 Segment::text("line 1"),
1737 Segment::newline(),
1738 Segment::text("line 2"),
1739 ];
1740 let text = Text::from_segments(segments);
1741 assert_eq!(text.height(), 2);
1742 assert_eq!(text.lines()[0].to_plain_text(), "line 1");
1743 assert_eq!(text.lines()[1].to_plain_text(), "line 2");
1744 }
1745
1746 #[test]
1747 fn text_converts_to_segment_lines_multiline() {
1748 let text = Text::raw("a\nb");
1749 let seg_lines = text.into_segment_lines();
1750 assert_eq!(seg_lines.len(), 2);
1751 }
1752
1753 #[test]
1754 fn text_iter() {
1755 let text = Text::from_lines([Line::raw("a"), Line::raw("b")]);
1756 let collected: Vec<_> = text.iter().collect();
1757 assert_eq!(collected.len(), 2);
1758 }
1759
1760 #[test]
1761 fn text_debug_impl() {
1762 let text = Text::raw("test");
1763 let debug = format!("{:?}", text);
1764 assert!(debug.contains("Text"));
1765 }
1766
1767 #[test]
1768 fn text_default_is_empty() {
1769 let text = Text::default();
1770 assert!(text.is_empty());
1771 }
1772
1773 #[test]
1778 fn truncate_ellipsis_wider_than_max() {
1779 let mut text = Text::raw("ab");
1780 text.truncate(2, Some("...")); assert!(text.width() <= 2);
1783 }
1784
1785 #[test]
1786 fn truncate_exact_width_no_change() {
1787 let mut text = Text::raw("hello");
1788 text.truncate(5, Some("..."));
1789 assert_eq!(text.to_plain_text(), "hello"); }
1791
1792 #[test]
1793 fn truncate_multiple_spans() {
1794 let text = Text::from_spans([
1795 Span::raw("hello "),
1796 Span::styled("world", Style::new().bold()),
1797 ]);
1798 let truncated = text.truncated(8, None);
1799 assert_eq!(truncated.to_plain_text(), "hello wo");
1800 }
1801
1802 #[test]
1803 fn truncate_preserves_link() {
1804 let mut text =
1805 Text::from_spans([Span::raw("click ").link("https://a.com"), Span::raw("here")]);
1806 text.truncate(6, None);
1807 assert!(text.lines()[0].spans()[0].link.is_some());
1809 }
1810
1811 #[test]
1816 fn push_span_on_empty_creates_line() {
1817 let mut text = Text::new();
1818 text.push_span(Span::raw("hello"));
1819 assert_eq!(text.height(), 1);
1820 assert_eq!(text.to_plain_text(), "hello");
1821 }
1822
1823 #[test]
1828 fn text_ref_into_iter() {
1829 let text = Text::from_lines([Line::raw("a"), Line::raw("b")]);
1830 let mut count = 0;
1831 for _line in &text {
1832 count += 1;
1833 }
1834 assert_eq!(count, 2);
1835 }
1836
1837 #[test]
1838 fn line_ref_into_iter() {
1839 let line = Line::from_spans([Span::raw("a"), Span::raw("b")]);
1840 let mut count = 0;
1841 for _span in &line {
1842 count += 1;
1843 }
1844 assert_eq!(count, 2);
1845 }
1846}
1847
1848#[cfg(test)]
1849mod proptests {
1850 use super::*;
1851 use proptest::prelude::*;
1852
1853 proptest! {
1854 #[test]
1855 fn raw_text_roundtrips(s in "[a-zA-Z0-9 \n]{0,100}") {
1856 let text = Text::raw(&s);
1857 let plain = text.to_plain_text();
1858 prop_assert_eq!(plain, s);
1859 }
1860
1861 #[test]
1862 fn truncate_never_exceeds_width(s in "[a-zA-Z0-9]{1,50}", max_width in 1usize..20) {
1863 let mut text = Text::raw(&s);
1864 text.truncate(max_width, None);
1865 prop_assert!(text.width() <= max_width);
1866 }
1867
1868 #[test]
1869 fn truncate_with_ellipsis_never_exceeds_width(s in "[a-zA-Z0-9]{1,50}", max_width in 4usize..20) {
1870 let mut text = Text::raw(&s);
1871 text.truncate(max_width, Some("..."));
1872 prop_assert!(text.width() <= max_width);
1873 }
1874
1875 #[test]
1876 fn height_equals_newline_count_plus_one(s in "[a-zA-Z\n]{1,100}") {
1877 let text = Text::raw(&s);
1878 let newline_count = s.chars().filter(|&c| c == '\n').count();
1879 prop_assert_eq!(text.height(), newline_count + 1);
1880 }
1881
1882 #[test]
1883 fn from_segments_preserves_content(
1884 parts in prop::collection::vec("[a-z]{1,10}", 1..5)
1885 ) {
1886 let segments: Vec<Segment> = parts.iter()
1887 .map(|s| Segment::text(s.as_str()))
1888 .collect();
1889
1890 let text = Text::from_segments(segments);
1891 let plain = text.to_plain_text();
1892 let expected: String = parts.join("");
1893
1894 prop_assert_eq!(plain, expected);
1895 }
1896 }
1897}