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