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