1use crate::cells::cell_len;
11use crate::style::Style;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Segment {
16 pub text: crate::CompactString,
19 pub style: Option<Style>,
20 pub control: bool,
22}
23
24impl Segment {
25 pub fn new(text: impl Into<crate::CompactString>, style: Option<Style>) -> Self {
27 Segment {
28 text: text.into(),
29 style,
30 control: false,
31 }
32 }
33
34 pub fn line() -> Self {
36 Segment {
37 text: crate::CompactString::const_new("\n"),
38 style: None,
39 control: false,
40 }
41 }
42
43 pub fn control(text: impl Into<crate::CompactString>) -> Self {
45 Segment {
46 text: text.into(),
47 style: None,
48 control: true,
49 }
50 }
51
52 pub fn cell_length(&self) -> usize {
54 if self.control {
55 0
56 } else {
57 cell_len(&self.text)
58 }
59 }
60
61 pub fn simplify(segments: &[Segment]) -> Vec<Segment> {
64 let mut out: Vec<Segment> = Vec::with_capacity(segments.len());
65 for segment in segments {
66 match out.last_mut() {
67 Some(last) if last.style == segment.style && last.control == segment.control => {
68 last.text.push_str(&segment.text);
69 }
70 _ => out.push(segment.clone()),
71 }
72 }
73 out
74 }
75
76 pub fn remove_color(segments: &[Segment]) -> Vec<Segment> {
79 segments
80 .iter()
81 .map(|segment| Segment {
82 text: segment.text.clone(),
83 style: segment.style.as_ref().map(Style::without_color),
84 control: segment.control,
85 })
86 .collect()
87 }
88
89 pub fn apply_style(segments: &[Segment], style: &Style) -> Vec<Segment> {
97 segments
98 .iter()
99 .map(|segment| {
100 if segment.control || segment.text == "\n" {
101 segment.clone()
102 } else {
103 let combined = match &segment.style {
104 Some(own) => style.combine(own),
105 None => style.clone(),
106 };
107 Segment {
108 text: segment.text.clone(),
109 style: Some(combined),
110 control: false,
111 }
112 }
113 })
114 .collect()
115 }
116
117 pub fn split_lines(segments: &[Segment]) -> Vec<Vec<Segment>> {
125 let mut lines: Vec<Vec<Segment>> = Vec::new();
126 let mut current: Vec<Segment> = Vec::new();
127 let mut empty_last_line = false;
129 for segment in segments {
130 if segment.control || !segment.text.contains('\n') {
131 if !segment.text.is_empty() {
132 current.push(segment.clone());
133 } else if !segment.control && current.is_empty() && !lines.is_empty() {
134 empty_last_line = true;
135 }
136 continue;
137 }
138 empty_last_line = false;
139 let mut parts = segment.text.split('\n').peekable();
140 while let Some(part) = parts.next() {
141 if !part.is_empty() {
142 current.push(Segment::new(part, segment.style.clone()));
143 }
144 if parts.peek().is_some() {
145 lines.push(std::mem::take(&mut current));
147 }
148 }
149 }
150 if !current.is_empty() || empty_last_line {
151 lines.push(current);
152 }
153 lines
154 }
155
156 pub(crate) fn split_lines_owned(segments: Vec<Segment>) -> Vec<Vec<Segment>> {
161 let mut lines: Vec<Vec<Segment>> = Vec::new();
162 let mut current: Vec<Segment> = Vec::new();
163 let mut empty_last_line = false;
164 for segment in segments {
165 if segment.control || !segment.text.contains('\n') {
166 if !segment.text.is_empty() {
167 current.push(segment);
168 } else if !segment.control && current.is_empty() && !lines.is_empty() {
169 empty_last_line = true;
170 }
171 continue;
172 }
173 empty_last_line = false;
174 let mut parts = segment.text.split('\n').peekable();
175 while let Some(part) = parts.next() {
176 if !part.is_empty() {
177 current.push(Segment::new(part, segment.style.clone()));
178 }
179 if parts.peek().is_some() {
180 lines.push(std::mem::take(&mut current));
181 }
182 }
183 }
184 if !current.is_empty() || empty_last_line {
185 lines.push(current);
186 }
187 lines
188 }
189
190 pub fn set_shape(lines: Vec<Vec<Segment>>, width: usize, height: usize) -> Vec<Vec<Segment>> {
194 let mut shaped: Vec<Vec<Segment>> = lines
195 .into_iter()
196 .take(height)
197 .map(|line| Segment::adjust_line_length(&line, width, None))
198 .collect();
199 while shaped.len() < height {
200 shaped.push(vec![Segment::new(" ".repeat(width), None)]);
201 }
202 shaped
203 }
204
205 pub fn fold_lines_words(segments: &[Segment], width: usize) -> Vec<Segment> {
218 if width == 0 {
219 return segments.to_vec();
220 }
221 let mut out = Vec::new();
222 let lines = Self::split_lines(segments);
223 let last = lines.len().saturating_sub(1);
224 for (index, line) in lines.into_iter().enumerate() {
225 let plain: String = line
226 .iter()
227 .filter(|segment| !segment.control)
228 .map(|segment| segment.text.as_str())
229 .collect();
230 let breaks = crate::wrap::divide_line(&plain, width, true);
231
232 let mut char_pos = 0usize;
233 let mut next_break = 0usize;
234 for segment in line {
235 if segment.control {
236 out.push(segment);
237 continue;
238 }
239 let mut buf = String::new();
240 for ch in segment.text.chars() {
241 while next_break < breaks.len() && char_pos == breaks[next_break] {
242 if !buf.is_empty() {
243 out.push(Segment::new(buf.clone(), segment.style.clone()));
244 buf.clear();
245 }
246 out.push(Segment::line());
247 next_break += 1;
248 }
249 buf.push(ch);
250 char_pos += 1;
251 }
252 if !buf.is_empty() {
253 out.push(Segment::new(buf, segment.style.clone()));
254 }
255 }
256 if index != last {
257 out.push(Segment::line());
258 }
259 }
260 out
261 }
262
263 pub fn fold_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
278 if width == 0 {
279 return segments.to_vec();
280 }
281 let mut out = Vec::new();
282 let lines = Self::split_lines(segments);
283 let last = lines.len().saturating_sub(1);
284 for (index, line) in lines.into_iter().enumerate() {
285 let mut used = 0usize;
286 for segment in line {
287 if segment.control {
288 out.push(segment);
289 continue;
290 }
291 let mut remaining = segment.text.as_str();
294 while !remaining.is_empty() {
295 let room = width.saturating_sub(used);
296 if room == 0 {
297 out.push(Segment::line());
298 used = 0;
299 continue;
300 }
301 let chunks = crate::cells::chop_cells(remaining, room);
302 let mut head = chunks.first().cloned().unwrap_or_default();
303 if head.is_empty() {
304 if used > 0 {
305 out.push(Segment::line());
308 used = 0;
309 continue;
310 }
311 let (spans, _) = crate::cells::split_graphemes(remaining);
326 let take = spans.first().map_or(remaining.len(), |span| span.1);
327 head = remaining[..take].to_string();
328 }
329 used += crate::cells::cell_len(&head);
330 remaining = &remaining[head.len()..];
331 out.push(Segment::new(head, segment.style.clone()));
332 if !remaining.is_empty() {
333 out.push(Segment::line());
334 used = 0;
335 }
336 }
337 }
338 if index != last {
339 out.push(Segment::line());
340 }
341 }
342 out
343 }
344
345 pub fn crop_lines(segments: &[Segment], width: usize) -> Vec<Segment> {
360 let mut result: Vec<Segment> = Vec::with_capacity(segments.len());
361 let mut line: Vec<Segment> = Vec::new();
362 for segment in segments {
363 if !segment.control && segment.text.contains('\n') {
364 let mut pieces = segment.text.split('\n').peekable();
365 while let Some(piece) = pieces.next() {
366 if !piece.is_empty() {
367 line.push(Segment::new(piece, segment.style.clone()));
368 }
369 if pieces.peek().is_some() {
370 result.extend(Segment::crop_line(&line, width));
371 result.push(Segment::line());
372 line.clear();
373 }
374 }
375 } else {
376 line.push(segment.clone());
377 }
378 }
379 if !line.is_empty() {
380 result.extend(Segment::crop_line(&line, width));
381 }
382 result
383 }
384
385 fn crop_line(line: &[Segment], length: usize) -> Vec<Segment> {
390 let line_length: usize = line.iter().map(Segment::cell_length).sum();
391 if line_length <= length {
392 return line.to_vec();
393 }
394 let mut new_line: Vec<Segment> = Vec::new();
395 let mut used = 0usize;
396 for segment in line {
397 let segment_length = segment.cell_length();
398 if used + segment_length < length || segment.control {
399 new_line.push(segment.clone());
400 used += segment_length;
401 } else {
402 let cropped = crate::cells::set_cell_size(&segment.text, length - used);
403 new_line.push(Segment::new(cropped, segment.style.clone()));
404 break;
405 }
406 }
407 new_line
408 }
409
410 pub fn adjust_line_length(
413 line: &[Segment],
414 length: usize,
415 style: Option<Style>,
416 ) -> Vec<Segment> {
417 let line_length: usize = line.iter().map(Segment::cell_length).sum();
418 if line_length < length {
419 let mut new_line = line.to_vec();
420 new_line.push(Segment::new(" ".repeat(length - line_length), style));
421 new_line
422 } else {
423 Segment::crop_line(line, length)
424 }
425 }
426}
427
428impl Segment {
429 pub fn is_control(&self) -> bool {
432 self.control
433 }
434
435 pub fn split_cells(&self, cut: usize) -> (Segment, Segment) {
439 let with_text = |text: String| Segment {
440 text: text.into(),
441 style: self.style.clone(),
442 control: self.control,
443 };
444 let chars: Vec<char> = self.text.chars().collect();
445 if chars.iter().all(|&c| crate::cells::char_cell_width(c) == 1) {
446 if cut >= chars.len() {
447 return (self.clone(), with_text(String::new()));
448 }
449 return (
450 with_text(chars[..cut].iter().collect()),
451 with_text(chars[cut..].iter().collect()),
452 );
453 }
454 let cell_length = self.cell_length();
455 if cut >= cell_length {
456 return (self.clone(), with_text(String::new()));
457 }
458 let size = |c: char| crate::cells::char_cell_width(c);
459 let text = |range: std::ops::Range<usize>| -> String { chars[range].iter().collect() };
460 let mut pos = ((cut as f64 / cell_length as f64) * chars.len() as f64) as usize;
461 for _ in 0..=chars.len() * 2 + 2 {
465 let cell_pos = crate::cells::cell_len(&text(0..pos));
466 if cell_pos == cut {
467 return (with_text(text(0..pos)), with_text(text(pos..chars.len())));
468 }
469 if cell_pos + 1 == cut && pos < chars.len() && size(chars[pos]) == 2 {
470 return (
471 with_text(text(0..pos) + " "),
472 with_text(" ".to_string() + &text(pos + 1..chars.len())),
473 );
474 }
475 if cell_pos == cut + 1 && pos > 0 && size(chars[pos - 1]) == 2 {
476 return (
477 with_text(text(0..pos - 1) + " "),
478 with_text(" ".to_string() + &text(pos..chars.len())),
479 );
480 }
481 if cell_pos < cut {
482 pos = (pos + 1).min(chars.len());
483 } else {
484 pos = pos.saturating_sub(1);
485 }
486 }
487 let (before, after) = crate::cells::split_text(&self.text, cut);
488 (with_text(before), with_text(after))
489 }
490
491 pub fn apply_style_with(
496 segments: &[Segment],
497 style: Option<&Style>,
498 post_style: Option<&Style>,
499 ) -> Vec<Segment> {
500 let style = style.filter(|style| !style.is_null());
501 let post_style = post_style.filter(|style| !style.is_null());
502 segments
503 .iter()
504 .map(|segment| {
505 let mut own = segment.style.clone();
506 if let Some(style) = style {
507 own = if segment.control {
508 None
509 } else {
510 Some(match &own {
511 Some(own) => style.combine(own),
512 None => style.clone(),
513 })
514 };
515 }
516 if let Some(post_style) = post_style {
517 own = if segment.control {
518 None
519 } else {
520 Some(match own.filter(|own| !own.is_null()) {
521 Some(own) => own.combine(post_style),
522 None => post_style.clone(),
523 })
524 };
525 }
526 Segment {
527 text: segment.text.clone(),
528 style: own,
529 control: segment.control,
530 }
531 })
532 .collect()
533 }
534
535 pub fn filter_control(segments: &[Segment], is_control: bool) -> Vec<Segment> {
538 segments
539 .iter()
540 .filter(|segment| segment.control == is_control)
541 .cloned()
542 .collect()
543 }
544
545 pub fn split_lines_terminator(segments: &[Segment]) -> Vec<(Vec<Segment>, bool)> {
549 let mut lines = Vec::new();
550 let mut line: Vec<Segment> = Vec::new();
551 for segment in segments {
552 if segment.text.contains('\n') && !segment.control {
553 let mut pieces = segment.text.split('\n').peekable();
554 while let Some(piece) = pieces.next() {
555 if !piece.is_empty() {
556 line.push(Segment::new(piece, segment.style.clone()));
557 }
558 if pieces.peek().is_some() {
559 lines.push((std::mem::take(&mut line), true));
560 }
561 }
562 } else {
563 line.push(segment.clone());
564 }
565 }
566 if !line.is_empty() {
567 lines.push((line, false));
568 }
569 lines
570 }
571
572 pub fn split_and_crop_lines(
577 segments: &[Segment],
578 length: usize,
579 style: Option<&Style>,
580 pad: bool,
581 include_new_lines: bool,
582 ) -> Vec<Vec<Segment>> {
583 Segment::split_lines_terminator(segments)
584 .into_iter()
585 .map(|(line, terminated)| {
586 let mut cropped =
587 Segment::adjust_line_length_with(&line, length, style.cloned(), pad);
588 if terminated && include_new_lines {
589 cropped.push(Segment::line());
590 }
591 cropped
592 })
593 .collect()
594 }
595
596 pub fn adjust_line_length_with(
600 line: &[Segment],
601 length: usize,
602 style: Option<Style>,
603 pad: bool,
604 ) -> Vec<Segment> {
605 let line_length: usize = line.iter().map(Segment::cell_length).sum();
606 if line_length < length && !pad {
607 return line.to_vec();
608 }
609 Segment::adjust_line_length(line, length, style)
610 }
611
612 pub fn get_line_length(line: &[Segment]) -> usize {
615 line.iter()
616 .filter(|segment| !segment.control)
617 .map(|segment| cell_len(&segment.text))
618 .sum()
619 }
620
621 pub fn get_shape(lines: &[Vec<Segment>]) -> (usize, usize) {
624 let width = lines
625 .iter()
626 .map(|line| Segment::get_line_length(line))
627 .max()
628 .unwrap_or(0);
629 (width, lines.len())
630 }
631
632 pub fn set_shape_with(
637 lines: &[Vec<Segment>],
638 width: usize,
639 height: Option<usize>,
640 style: Option<&Style>,
641 new_lines: bool,
642 ) -> Vec<Vec<Segment>> {
643 let height = height.filter(|&height| height > 0).unwrap_or(lines.len());
644 let mut shaped: Vec<Vec<Segment>> = lines
645 .iter()
646 .map(|line| Segment::adjust_line_length(line, width, style.cloned()))
647 .collect();
648 while shaped.len() < height {
649 shaped.push(vec![Segment::blank(width, style, new_lines)]);
650 }
651 shaped
652 }
653
654 fn blank(width: usize, style: Option<&Style>, new_lines: bool) -> Segment {
656 let mut text = " ".repeat(width);
657 if new_lines {
658 text.push('\n');
659 }
660 Segment::new(text, style.cloned())
661 }
662
663 pub fn align_top(
666 lines: &[Vec<Segment>],
667 width: usize,
668 height: usize,
669 style: Option<&Style>,
670 new_lines: bool,
671 ) -> Vec<Vec<Segment>> {
672 Segment::align_vertical(lines, width, height, style, new_lines, |extra| (0, extra))
673 }
674
675 pub fn align_bottom(
678 lines: &[Vec<Segment>],
679 width: usize,
680 height: usize,
681 style: Option<&Style>,
682 new_lines: bool,
683 ) -> Vec<Vec<Segment>> {
684 Segment::align_vertical(lines, width, height, style, new_lines, |extra| (extra, 0))
685 }
686
687 pub fn align_middle(
690 lines: &[Vec<Segment>],
691 width: usize,
692 height: usize,
693 style: Option<&Style>,
694 new_lines: bool,
695 ) -> Vec<Vec<Segment>> {
696 Segment::align_vertical(lines, width, height, style, new_lines, |extra| {
697 (extra / 2, extra - extra / 2)
698 })
699 }
700
701 fn align_vertical(
705 lines: &[Vec<Segment>],
706 width: usize,
707 height: usize,
708 style: Option<&Style>,
709 new_lines: bool,
710 split: impl Fn(usize) -> (usize, usize),
711 ) -> Vec<Vec<Segment>> {
712 if lines.len() >= height {
713 return lines[..height].to_vec();
714 }
715 let (above, below) = split(height - lines.len());
716 let blank = vec![Segment::blank(width, style, new_lines)];
717 let mut aligned = vec![blank.clone(); above];
718 aligned.extend(lines.iter().cloned());
719 aligned.extend(std::iter::repeat_n(blank, below));
720 aligned
721 }
722
723 pub fn strip_links(segments: &[Segment]) -> Vec<Segment> {
726 segments
727 .iter()
728 .map(|segment| match &segment.style {
729 Some(style) if !segment.control => Segment {
730 text: segment.text.clone(),
731 style: (!style.is_null()).then(|| style.update_link(None)),
732 control: false,
733 },
734 _ => segment.clone(),
735 })
736 .collect()
737 }
738
739 pub fn strip_styles(segments: &[Segment]) -> Vec<Segment> {
741 segments
742 .iter()
743 .map(|segment| Segment {
744 text: segment.text.clone(),
745 style: None,
746 control: segment.control,
747 })
748 .collect()
749 }
750
751 pub fn divide(segments: &[Segment], cuts: &[usize]) -> Vec<Vec<Segment>> {
755 let mut result = Vec::new();
756 let mut cuts = cuts.iter().copied();
757 let mut cut = loop {
758 match cuts.next() {
759 None => return result,
760 Some(0) => result.push(Vec::new()),
761 Some(cut) => break cut,
762 }
763 };
764 let mut pieces: Vec<Segment> = Vec::new();
765 let mut pos = 0usize;
766 for segment in segments {
767 let mut segment = segment.clone();
768 while !segment.text.is_empty() {
769 let end_pos = if segment.control {
770 pos
771 } else {
772 pos + cell_len(&segment.text)
773 };
774 if end_pos < cut {
775 pieces.push(segment.clone());
776 pos = end_pos;
777 break;
778 }
779 if end_pos == cut {
780 pieces.push(segment.clone());
781 result.push(std::mem::take(&mut pieces));
782 pos = end_pos;
783 match cuts.next() {
784 Some(next) => cut = next,
785 None => {
786 if !pieces.is_empty() {
787 result.push(pieces);
788 }
789 return result;
790 }
791 }
792 break;
793 }
794 let (before, after) = segment.split_cells(cut - pos);
795 segment = after;
796 pieces.push(before);
797 result.push(std::mem::take(&mut pieces));
798 pos = cut;
799 match cuts.next() {
800 Some(next) => cut = next,
801 None => {
802 if !pieces.is_empty() {
803 result.push(pieces);
804 }
805 return result;
806 }
807 }
808 }
809 }
810 result.push(pieces);
811 result
812 }
813}
814
815#[derive(Clone, Debug, Default)]
818pub struct Segments {
819 pub segments: Vec<Segment>,
820 pub new_lines: bool,
821}
822
823impl Segments {
824 pub fn new(segments: Vec<Segment>, new_lines: bool) -> Self {
825 Segments {
826 segments,
827 new_lines,
828 }
829 }
830}
831
832impl crate::protocol::Renderable for Segments {
833 fn rich_render(
837 &self,
838 _console: &crate::console::Console,
839 _options: &crate::console::ConsoleOptions,
840 ) -> Vec<Segment> {
841 if !self.new_lines {
842 return self.segments.clone();
843 }
844 let mut out = Vec::with_capacity(self.segments.len() * 2);
845 for (index, segment) in self.segments.iter().enumerate() {
846 if index > 0 {
847 out.push(Segment::line());
848 }
849 out.push(segment.clone());
850 }
851 out
852 }
853}
854
855#[derive(Clone, Debug, Default)]
858pub struct SegmentLines {
859 pub lines: Vec<Vec<Segment>>,
860 pub new_lines: bool,
861}
862
863impl SegmentLines {
864 pub fn new(lines: Vec<Vec<Segment>>, new_lines: bool) -> Self {
865 SegmentLines { lines, new_lines }
866 }
867}
868
869impl crate::protocol::Renderable for SegmentLines {
870 fn rich_render(
872 &self,
873 _console: &crate::console::Console,
874 _options: &crate::console::ConsoleOptions,
875 ) -> Vec<Segment> {
876 let mut out = Vec::new();
877 for (index, line) in self.lines.iter().enumerate() {
878 if index > 0 && self.new_lines {
879 out.push(Segment::line());
880 }
881 out.extend(line.iter().cloned());
882 }
883 out
884 }
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890
891 #[test]
894 fn split_lines_owned_matches_split_lines() {
895 let bold = Style::parse("bold").unwrap();
896 let kinds = [
897 Segment::new("ab", None),
898 Segment::new("", None),
899 Segment::new("\n", Some(bold.clone())),
900 Segment::new("x\ny\n", Some(bold)),
901 Segment::new("\n\nz", None),
902 Segment::line(),
903 Segment::control("\x1b[2J"),
904 ];
905 let mut sequences: Vec<Vec<Segment>> = vec![Vec::new()];
906 for _ in 0..5 {
907 sequences = sequences
908 .iter()
909 .flat_map(|sequence| {
910 kinds.iter().map(move |kind| {
911 let mut next = sequence.clone();
912 next.push(kind.clone());
913 next
914 })
915 })
916 .collect();
917 for sequence in &sequences {
918 assert_eq!(
919 Segment::split_lines_owned(sequence.clone()),
920 Segment::split_lines(sequence),
921 "{sequence:?}"
922 );
923 }
924 }
925 }
926
927 #[test]
930 fn crop_lines_cuts_each_line_independently() {
931 let segments = vec![
932 Segment::new("hello world", None),
933 Segment::line(),
934 Segment::new("hi", None),
935 Segment::line(),
936 Segment::control("\x1b[2A"),
937 Segment::new("abcdefgh", None),
938 ];
939 let cropped = Segment::crop_lines(&segments, 5);
940 let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
941 assert_eq!(texts, vec!["hello", "\n", "hi", "\n", "\x1b[2A", "abcde"]);
942 }
943
944 #[test]
948 fn crop_lines_keeps_a_newline_inside_a_segment() {
949 let segments = vec![Segment::new("xy", None), Segment::new("!!\n", None)];
950 let cropped = Segment::crop_lines(&segments, 3);
951 let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
952 assert_eq!(texts, vec!["xy", "!", "\n"]);
953 }
954
955 #[test]
958 fn crop_lines_drops_zero_width_characters_past_the_edge() {
959 let segments = vec![
960 Segment::new("abc", None),
961 Segment::new("\u{200b}", None),
962 Segment::new("d", None),
963 ];
964 let cropped = Segment::crop_lines(&segments, 3);
965 let texts: Vec<&str> = cropped.iter().map(|s| s.text.as_str()).collect();
966 assert_eq!(texts, vec!["abc"]);
967 let fits = Segment::crop_lines(&segments[..2], 3);
969 assert_eq!(fits.len(), 2);
970 }
971
972 #[test]
975 fn crop_lines_pads_a_split_wide_character() {
976 let segments = vec![Segment::new("aa你好", None)];
977 let cropped = Segment::crop_lines(&segments, 5);
978 assert_eq!(cropped[0].text, "aa你 ");
979 }
980
981 #[test]
984 fn crop_lines_preserves_styles_and_drops_the_tail() {
985 let bold = Style::parse("bold").unwrap();
986 let segments = vec![
987 Segment::new("abc", Some(bold.clone())),
988 Segment::new("defgh", None),
989 ];
990 let cropped = Segment::crop_lines(&segments, 3);
991 assert_eq!(cropped.len(), 1);
992 assert_eq!(cropped[0].text, "abc");
993 assert_eq!(cropped[0].style, Some(bold));
994 }
995
996 #[test]
997 fn cell_length_ignores_control() {
998 assert_eq!(Segment::new("abc", None).cell_length(), 3);
999 assert_eq!(Segment::control("\x1b[2J").cell_length(), 0);
1000 }
1001
1002 #[test]
1003 fn fold_lines_carries_the_overflow_instead_of_dropping_it() {
1004 let segments = vec![Segment::new("abcdefghij", None)];
1005 let folded = Segment::fold_lines(&segments, 4);
1006 let text: String = folded.iter().map(|s| s.text.as_str()).collect();
1007 assert_eq!(text.replace('\n', ""), "abcdefghij");
1009 assert_eq!(Segment::split_lines(&folded).len(), 3);
1010 }
1011
1012 #[test]
1013 fn fold_lines_preserves_styles_across_a_break() {
1014 let style = Style::parse("bold").expect("valid style");
1015 let segments = vec![Segment::new("abcdef", Some(style.clone()))];
1016 let folded = Segment::fold_lines(&segments, 3);
1017 for segment in folded.iter().filter(|s| !s.text.contains('\n')) {
1018 assert_eq!(segment.style.as_ref(), Some(&style), "style lost on fold");
1019 }
1020 }
1021
1022 #[test]
1027 fn fold_lines_never_splits_a_grapheme() {
1028 let heart = "\u{2764}\u{fe0f}";
1029 let segments = vec![Segment::new(heart.repeat(3), None)];
1030 let folded = Segment::fold_lines(&segments, 1);
1031 let rows: Vec<String> = Segment::split_lines(&folded)
1032 .iter()
1033 .map(|line| line.iter().map(|s| s.text.as_str()).collect())
1034 .collect();
1035 assert_eq!(rows, vec![heart, heart, heart]);
1036 }
1037
1038 #[test]
1039 fn crop_lines_still_drops_the_overflow() {
1040 let segments = vec![Segment::new("abcdefghij", None)];
1043 let cropped = Segment::crop_lines(&segments, 4);
1044 let text: String = cropped.iter().map(|s| s.text.as_str()).collect();
1045 assert_eq!(text, "abcd");
1046 }
1047
1048 #[test]
1049 fn fold_lines_terminates_when_a_glyph_is_wider_than_the_width() {
1050 let segments = vec![Segment::new("\u{4f60}\u{4f60}", None)];
1054 let folded = Segment::fold_lines(&segments, 1);
1055 let text: String = folded.iter().map(|s| s.text.as_str()).collect();
1056 assert_eq!(
1057 text.matches('\u{4f60}').count(),
1058 2,
1059 "both characters should survive, overflowing rather than looping"
1060 );
1061 }
1062
1063 #[test]
1067 fn fold_lines_words_breaks_between_words() {
1068 let segments = vec![Segment::new("the quick brown fox", None)];
1069 let folded = Segment::fold_lines_words(&segments, 12);
1072 let lines: Vec<String> = Segment::split_lines(&folded)
1073 .iter()
1074 .map(|line| line.iter().map(|s| s.text.as_str()).collect())
1075 .collect();
1076 assert_eq!(lines, vec!["the quick ", "brown fox"]);
1077 }
1078
1079 #[test]
1082 fn fold_lines_words_preserves_styles_across_a_break() {
1083 let green = Style::parse("green").expect("valid style");
1084 let segments = vec![
1085 Segment::new("key: ", None),
1086 Segment::new("alpha beta gamma", Some(green.clone())),
1087 ];
1088 let folded = Segment::fold_lines_words(&segments, 12);
1089 let styled: String = folded
1090 .iter()
1091 .filter(|s| s.style.as_ref() == Some(&green))
1092 .map(|s| s.text.as_str())
1093 .collect();
1094 assert_eq!(styled, "alpha beta gamma", "style lost across the break");
1095 }
1096
1097 #[test]
1100 fn fold_lines_words_keeps_every_character() {
1101 let segments = vec![
1102 Segment::new("short ", None),
1103 Segment::new("z".repeat(25), None),
1104 Segment::new(" tail", None),
1105 ];
1106 for width in 1..=30 {
1107 let folded = Segment::fold_lines_words(&segments, width);
1108 let text: String = folded.iter().map(|s| s.text.as_str()).collect();
1109 assert_eq!(
1110 text.replace('\n', ""),
1111 format!("short {} tail", "z".repeat(25)),
1112 "width {width} lost or reordered characters"
1113 );
1114 }
1115 }
1116
1117 #[test]
1120 fn fold_lines_words_keeps_control_segments() {
1121 let segments = vec![
1122 Segment::control("\x1b]8;;http://x\x1b\\"),
1123 Segment::new("alpha beta", None),
1124 ];
1125 let folded = Segment::fold_lines_words(&segments, 8);
1126 assert_eq!(folded.iter().filter(|s| s.control).count(), 1);
1127 let text: String = folded
1128 .iter()
1129 .filter(|s| !s.control)
1130 .map(|s| s.text.as_str())
1131 .collect();
1132 assert_eq!(text, "alpha \nbeta");
1133 }
1134
1135 #[test]
1136 fn fold_lines_terminates_at_every_narrow_width() {
1137 let sample = "a\u{4f60}b\u{1f600}c";
1139 for width in 1..=6 {
1140 let folded = Segment::fold_lines(&[Segment::new(sample, None)], width);
1141 let text: String = folded.iter().map(|s| s.text.as_str()).collect();
1142 assert!(
1143 text.contains('c'),
1144 "width {width} lost the tail, or did not terminate"
1145 );
1146 }
1147 }
1148}