1use std::cell::RefCell;
16use std::sync::Arc;
17
18use ratatui::style::{Color, Modifier, Style};
19use ratatui::text::{Line, Span};
20
21use crate::wrap::{wrap_line, wrap_line_window, wrapped_row_count};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub struct WrapMarker {
36 pub glyph: char,
39 pub style: Style,
42}
43
44impl Default for WrapMarker {
45 fn default() -> Self {
49 Self::builder().build()
50 }
51}
52
53impl WrapMarker {
54 pub fn builder() -> WraperMarkerBuilder {
55 WraperMarkerBuilder {
56 glyph: '↵',
57 style: Style::new().fg(Color::DarkGray).add_modifier(Modifier::DIM),
58 }
59 }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub struct WraperMarkerBuilder {
65 pub glyph: char,
68 pub style: Style,
71}
72
73impl WraperMarkerBuilder {
74 pub fn build(self) -> WrapMarker {
75 WrapMarker {
76 glyph: self.glyph,
77 style: self.style,
78 }
79 }
80
81 pub fn style(mut self, style: Style) -> WraperMarkerBuilder {
83 self.style = style;
84 self
85 }
86
87 pub fn glyph(mut self, glyph: char) -> WraperMarkerBuilder {
89 self.glyph = glyph;
90 self
91 }
92}
93
94fn effective_wrap_width(width: usize, mode: WrapMode, has_marker: bool) -> usize {
100 if has_marker && mode == WrapMode::Wrap && width >= 2 {
101 width - 1
102 } else {
103 width
104 }
105}
106
107#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
109pub enum WrapMode {
110 #[default]
113 Wrap,
114 Clip,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
127pub struct TextPos {
128 pub line: usize,
129 pub col: usize,
130}
131
132impl TextPos {
133 pub fn new(line: usize, col: usize) -> Self {
134 Self { line, col }
135 }
136}
137
138type LineStyles = Vec<Vec<(usize, usize, Style)>>;
142
143struct LineRows {
152 cum: Vec<u32>,
153 lens: Vec<usize>,
154}
155
156impl LineRows {
157 fn build(char_lens: impl Iterator<Item = usize>, width: usize, mode: WrapMode) -> Self {
158 let mut cum = vec![0u32];
159 let mut lens = Vec::new();
160 let mut total = 0u32;
161 for len in char_lens {
162 let rows = match mode {
163 WrapMode::Wrap => wrapped_row_count(len, width) as u32,
164 WrapMode::Clip => 1,
166 };
167 total += rows;
168 cum.push(total);
169 lens.push(len);
170 }
171 Self { cum, lens }
172 }
173
174 fn total_rows(&self) -> u32 {
175 (*self.cum.last().unwrap_or(&0)).max(1)
176 }
177
178 fn line_count(&self) -> usize {
179 self.cum.len().saturating_sub(1)
180 }
181
182 fn locate(&self, row: u32) -> (usize, u32) {
186 if self.cum.len() <= 1 {
187 return (0, 0);
188 }
189 let idx = self.cum.partition_point(|&c| c <= row);
192 let line = idx.saturating_sub(1).min(self.cum.len() - 2);
193 (line, row - self.cum[line])
194 }
195}
196
197pub struct PanelWrap {
200 raw: Arc<str>,
205 source: Arc<str>,
209 line_ranges: Vec<(usize, usize)>,
212 rows: LineRows,
213 width: usize,
214 wrap_width: usize,
220 mode: WrapMode,
221 marker: Option<WrapMarker>,
224 line_styles: Option<LineStyles>,
229 last_window: RefCell<Option<(u16, u16, Vec<Line<'static>>)>>,
236}
237
238impl PanelWrap {
239 pub fn build(source: Arc<str>, width: usize) -> Self {
244 Self::build_with(source, width, WrapMode::Wrap)
245 }
246
247 pub fn build_with(source: Arc<str>, width: usize, mode: WrapMode) -> Self {
249 Self::build_with_marker(source, width, mode, None)
250 }
251
252 pub fn build_with_marker(
256 source: Arc<str>,
257 width: usize,
258 mode: WrapMode,
259 marker: Option<WrapMarker>,
260 ) -> Self {
261 let wrap_width = effective_wrap_width(width, mode, marker.is_some());
262 let line_ranges = Self::split_line_ranges(&source);
263 let rows = LineRows::build(
264 line_ranges
265 .iter()
266 .map(|&(s, e)| source[s..e].chars().count()),
267 wrap_width,
268 mode,
269 );
270 Self {
271 raw: Arc::clone(&source),
272 source,
273 line_ranges,
274 rows,
275 width,
276 wrap_width,
277 mode,
278 marker,
279 line_styles: None,
280 last_window: RefCell::new(None),
281 }
282 }
283
284 fn split_line_ranges(source: &str) -> Vec<(usize, usize)> {
287 let mut line_ranges = Vec::new();
288 let bytes = source.as_bytes();
289 let mut start = 0usize;
290 for (i, &b) in bytes.iter().enumerate() {
291 if b == b'\n' {
292 let mut end = i;
293 if end > start && bytes[end - 1] == b'\r' {
294 end -= 1;
295 }
296 line_ranges.push((start, end));
297 start = i + 1;
298 }
299 }
300 if start < bytes.len() || line_ranges.is_empty() {
301 line_ranges.push((start, bytes.len()));
302 }
303 line_ranges
304 }
305
306 #[cfg(feature = "ansi")]
311 pub fn build_ansi(raw: Arc<str>, width: usize, mode: WrapMode) -> Self {
312 Self::build_ansi_with_marker(raw, width, mode, None)
313 }
314
315 #[cfg(feature = "ansi")]
318 pub fn build_ansi_with_marker(
319 raw: Arc<str>,
320 width: usize,
321 mode: WrapMode,
322 marker: Option<WrapMarker>,
323 ) -> Self {
324 let wrap_width = effective_wrap_width(width, mode, marker.is_some());
325 let (plain_lines, styles) = parse_ansi(&raw);
326 let source: Arc<str> = Arc::from(plain_lines.join("\n"));
327 let mut line_ranges = Vec::with_capacity(plain_lines.len().max(1));
330 let mut pos = 0usize;
331 for line in &plain_lines {
332 let start = pos;
333 let end = start + line.len();
334 line_ranges.push((start, end));
335 pos = end + 1; }
337 if line_ranges.is_empty() {
338 line_ranges.push((0, 0));
339 }
340 let rows = LineRows::build(
341 plain_lines.iter().map(|l| l.chars().count()),
342 wrap_width,
343 mode,
344 );
345 Self {
346 raw,
347 source,
348 line_ranges,
349 rows,
350 width,
351 wrap_width,
352 mode,
353 marker,
354 line_styles: Some(styles),
355 last_window: RefCell::new(None),
356 }
357 }
358
359 pub fn rebuild_if_needed(cache: &mut Option<PanelWrap>, source: &Arc<str>, width: usize) {
365 Self::rebuild_if_needed_with(cache, source, width, WrapMode::Wrap);
366 }
367
368 pub fn rebuild_if_needed_with(
372 cache: &mut Option<PanelWrap>,
373 source: &Arc<str>,
374 width: usize,
375 mode: WrapMode,
376 ) {
377 Self::rebuild_if_needed_marker(cache, source, width, mode, None);
378 }
379
380 pub fn rebuild_if_needed_marker(
384 cache: &mut Option<PanelWrap>,
385 source: &Arc<str>,
386 width: usize,
387 mode: WrapMode,
388 marker: Option<WrapMarker>,
389 ) {
390 let stale = match cache {
391 Some(c) => {
392 !Arc::ptr_eq(&c.raw, source)
393 || c.width != width
394 || c.mode != mode
395 || c.marker != marker
396 || c.line_styles.is_some()
397 }
398 None => true,
399 };
400 if stale {
401 *cache = Some(PanelWrap::build_with_marker(
402 Arc::clone(source),
403 width,
404 mode,
405 marker,
406 ));
407 }
408 }
409
410 #[cfg(feature = "ansi")]
414 pub fn rebuild_if_needed_ansi(
415 cache: &mut Option<PanelWrap>,
416 raw: &Arc<str>,
417 width: usize,
418 mode: WrapMode,
419 ) {
420 Self::rebuild_if_needed_ansi_marker(cache, raw, width, mode, None);
421 }
422
423 #[cfg(feature = "ansi")]
427 pub fn rebuild_if_needed_ansi_marker(
428 cache: &mut Option<PanelWrap>,
429 raw: &Arc<str>,
430 width: usize,
431 mode: WrapMode,
432 marker: Option<WrapMarker>,
433 ) {
434 let stale = match cache {
435 Some(c) => {
436 !Arc::ptr_eq(&c.raw, raw)
437 || c.width != width
438 || c.mode != mode
439 || c.marker != marker
440 || c.line_styles.is_none()
441 }
442 None => true,
443 };
444 if stale {
445 *cache = Some(PanelWrap::build_ansi_with_marker(
446 Arc::clone(raw),
447 width,
448 mode,
449 marker,
450 ));
451 }
452 }
453
454 pub fn mode(&self) -> WrapMode {
456 self.mode
457 }
458
459 pub fn wrap_width(&self) -> usize {
464 self.wrap_width
465 }
466
467 pub fn marker(&self) -> Option<WrapMarker> {
469 self.marker
470 }
471
472 pub fn line_count(&self) -> usize {
473 self.rows.line_count()
474 }
475
476 pub fn source(&self) -> &str {
480 &self.source
481 }
482
483 pub fn line_text(&self, idx: usize) -> &str {
484 let (s, e) = self.line_ranges[idx];
485 &self.source[s..e]
486 }
487
488 pub fn line_char_len(&self, idx: usize) -> usize {
489 self.rows.lens.get(idx).copied().unwrap_or(0)
490 }
491
492 pub fn total_rows(&self) -> u32 {
493 self.rows.total_rows()
494 }
495
496 pub fn visible_window(&self, scroll: u16, height: u16) -> Vec<Line<'static>> {
504 if height == 0 || self.line_count() == 0 {
505 return Vec::new();
506 }
507 if let Some((cached_scroll, cached_height, cached)) = self.last_window.borrow().as_ref()
508 && *cached_scroll == scroll
509 && *cached_height == height
510 {
511 return cached.clone();
512 }
513 let out = match self.mode {
514 WrapMode::Clip => self.visible_window_clip(scroll, height),
515 WrapMode::Wrap => self.visible_window_wrap(scroll, height),
516 };
517 *self.last_window.borrow_mut() = Some((scroll, height, out.clone()));
518 out
519 }
520
521 fn visible_window_wrap(&self, scroll: u16, height: u16) -> Vec<Line<'static>> {
523 let (start_line, row_in_line) = self.rows.locate(scroll as u32);
524 let height_usize = height as usize;
525 let mut out: Vec<Line<'static>> = Vec::with_capacity(height_usize);
526 let mut skip = row_in_line as usize;
527 for idx in start_line..self.line_count() {
528 if out.len() >= height_usize {
529 break;
530 }
531 let budget = height_usize - out.len();
532 let mut rows = if self.line_styles.is_none() {
533 wrap_line_window(self.line_text(idx), self.wrap_width, skip, budget)
534 } else {
535 self.wrap_line_window_styled(idx, skip, budget)
536 };
537 self.mark_continued_rows(idx, skip, &mut rows);
543 out.extend(rows);
544 skip = 0;
545 }
546 out.truncate(height_usize);
547 out
548 }
549
550 fn mark_continued_rows(&self, idx: usize, first_row: usize, rows: &mut [Line<'static>]) {
556 let Some(marker) = self.marker else {
557 return;
558 };
559 if self.wrap_width >= self.width {
563 return;
564 }
565 let total_in_line = wrapped_row_count(self.line_char_len(idx), self.wrap_width);
566 for (k, line) in rows.iter_mut().enumerate() {
567 let row_in_line = first_row + k;
568 if row_in_line + 1 < total_in_line {
569 line.spans
570 .push(Span::styled(marker.glyph.to_string(), marker.style));
571 }
572 }
573 }
574
575 fn visible_window_clip(&self, scroll: u16, height: u16) -> Vec<Line<'static>> {
579 let start = scroll as usize;
580 let height_usize = height as usize;
581 let mut out: Vec<Line<'static>> = Vec::with_capacity(height_usize);
582 for idx in start..self.line_count() {
583 if out.len() >= height_usize {
584 break;
585 }
586 let end = self.line_char_len(idx).min(self.width);
587 out.push(Line::from(self.styled_spans(idx, 0, end)));
588 }
589 out
590 }
591
592 fn wrap_line_window_styled(
596 &self,
597 idx: usize,
598 skip_rows: usize,
599 max_rows: usize,
600 ) -> Vec<Line<'static>> {
601 if max_rows == 0 {
602 return Vec::new();
603 }
604 if self.wrap_width == 0 {
605 return if skip_rows == 0 {
606 vec![Line::from(self.styled_spans(
607 idx,
608 0,
609 self.line_char_len(idx),
610 ))]
611 } else {
612 Vec::new()
613 };
614 }
615 let c0 = skip_rows.saturating_mul(self.wrap_width);
616 let c1 = c0.saturating_add(max_rows.saturating_mul(self.wrap_width));
617 let spans = self.styled_spans(idx, c0, c1);
618 if spans.is_empty() {
619 return Vec::new();
620 }
621 wrap_line(Line::from(spans), self.wrap_width)
622 }
623
624 fn styled_spans(&self, idx: usize, c0: usize, c1: usize) -> Vec<Span<'static>> {
628 if c1 <= c0 {
629 return Vec::new();
630 }
631 let text = self.line_text(idx);
632 let slice: String = text.chars().skip(c0).take(c1 - c0).collect();
633 if slice.is_empty() {
634 return Vec::new();
635 }
636 let runs = match &self.line_styles {
637 None => return vec![Span::raw(slice)],
638 Some(all) => all.get(idx).map(|v| v.as_slice()).unwrap_or(&[]),
639 };
640 if runs.is_empty() {
641 return vec![Span::raw(slice)];
642 }
643 let style_at = |abs: usize| {
644 runs.iter()
645 .find(|&&(s, e, _)| abs >= s && abs < e)
646 .map(|&(_, _, st)| st)
647 .unwrap_or_default()
648 };
649 let chars: Vec<char> = slice.chars().collect();
650 let mut spans = Vec::new();
651 let mut i = 0usize;
652 while i < chars.len() {
653 let style = style_at(c0 + i);
654 let mut j = i + 1;
655 while j < chars.len() && style_at(c0 + j) == style {
656 j += 1;
657 }
658 let seg: String = chars[i..j].iter().collect();
659 spans.push(Span::styled(seg, style));
660 i = j;
661 }
662 spans
663 }
664
665 pub fn textpos_to_row_col(&self, pos: TextPos) -> (u32, usize) {
670 if self.line_count() == 0 {
671 return (0, 0);
672 }
673 let line = pos.line.min(self.line_count() - 1);
674 let len = self.line_char_len(line);
675 let col = pos.col.min(len);
676 if self.mode == WrapMode::Clip || self.wrap_width == 0 {
679 return (self.rows.cum[line], col);
680 }
681 let rows_in_line = wrapped_row_count(len, self.wrap_width) as u32;
682 let row_in_line = ((col / self.wrap_width) as u32).min(rows_in_line.saturating_sub(1));
683 let col_in_row = col.saturating_sub(row_in_line as usize * self.wrap_width);
684 (self.rows.cum[line] + row_in_line, col_in_row)
685 }
686
687 pub fn row_col_to_textpos(&self, row: u32, col: usize) -> TextPos {
692 if self.line_count() == 0 {
693 return TextPos::new(0, 0);
694 }
695 let (line, row_in_line) = self.rows.locate(row);
696 let len = self.line_char_len(line);
697 let base = if self.wrap_width == 0 {
698 0
699 } else {
700 row_in_line as usize * self.wrap_width
701 };
702 TextPos::new(line, base.saturating_add(col).min(len))
708 }
709}
710
711#[cfg(feature = "ansi")]
715fn parse_ansi(raw: &str) -> (Vec<String>, LineStyles) {
716 use ansi_to_tui::IntoText;
717 use ratatui::text::Text;
718
719 let text = raw
720 .into_text()
721 .unwrap_or_else(|_| Text::raw(raw.to_string()));
722 let mut plain_lines: Vec<String> = Vec::with_capacity(text.lines.len().max(1));
723 let mut styles: LineStyles = Vec::with_capacity(text.lines.len().max(1));
724 for line in &text.lines {
725 let mut plain = String::new();
726 let mut runs: Vec<(usize, usize, Style)> = Vec::new();
727 let mut col = 0usize;
728 for span in &line.spans {
729 let content: &str = span.content.as_ref();
730 let n = content.chars().count();
731 if n == 0 {
732 continue;
733 }
734 runs.push((col, col + n, line.style.patch(span.style)));
735 plain.push_str(content);
736 col += n;
737 }
738 if plain.ends_with('\r') {
741 plain.pop();
742 let new_len = plain.chars().count();
743 if let Some(last) = runs.last_mut() {
744 last.1 = last.1.min(new_len);
745 if last.0 >= last.1 {
746 runs.pop();
747 }
748 }
749 }
750 plain_lines.push(plain);
751 styles.push(runs);
752 }
753 if plain_lines.is_empty() {
754 plain_lines.push(String::new());
755 styles.push(Vec::new());
756 }
757 (plain_lines, styles)
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 fn wrap(text: &str, width: usize) -> PanelWrap {
765 PanelWrap::build(Arc::from(text), width)
766 }
767
768 fn clip(text: &str, width: usize) -> PanelWrap {
769 PanelWrap::build_with(Arc::from(text), width, WrapMode::Clip)
770 }
771
772 fn row_text(line: &Line<'static>) -> String {
773 line.spans.iter().map(|s| s.content.as_ref()).collect()
774 }
775
776 #[test]
777 fn clip_mode_maps_one_row_per_line_regardless_of_length() {
778 let w = clip("0123456789ABCDE\nshort", 10);
780 assert_eq!(w.line_count(), 2);
781 assert_eq!(w.total_rows(), 2, "one row per raw line, no wrapping");
782 let rows = w.visible_window(0, 5);
785 assert_eq!(rows.len(), 2);
786 assert_eq!(row_text(&rows[0]), "0123456789", "clipped to width");
787 assert_eq!(row_text(&rows[1]), "short");
788 }
789
790 #[test]
791 fn clip_mode_row_and_textpos_map_straight_through() {
792 let w = clip("0123456789ABCDE\nsecond", 10);
793 assert_eq!(w.textpos_to_row_col(TextPos::new(1, 3)), (1, 3));
795 assert_eq!(w.row_col_to_textpos(1, 3), TextPos::new(1, 3));
796 assert_eq!(w.row_col_to_textpos(0, 4), TextPos::new(0, 4));
798 }
799
800 #[test]
801 fn clip_mode_scrolls_by_whole_lines() {
802 let body: String = (0..1000).map(|i| format!("line {i}\n")).collect();
803 let w = clip(&body, 4); let rows = w.visible_window(500, 3);
805 assert_eq!(rows.len(), 3);
806 assert_eq!(row_text(&rows[0]), "line");
807 assert_eq!(w.total_rows(), 1000);
809 }
810
811 #[test]
812 fn splits_lines_like_str_lines_including_trailing_newline_and_crlf() {
813 let w = wrap("a\r\nb\nc", 10);
814 assert_eq!(w.line_count(), 3);
815 assert_eq!(w.line_text(0), "a");
816 assert_eq!(w.line_text(1), "b");
817 assert_eq!(w.line_text(2), "c");
818
819 let w2 = wrap("a\nb\n", 10);
820 assert_eq!(
821 w2.line_count(),
822 2,
823 "no trailing empty line after a final \\n, matching str::lines()"
824 );
825 }
826
827 #[test]
828 fn empty_body_has_one_line_and_one_row() {
829 let w = wrap("", 10);
830 assert_eq!(w.line_count(), 1);
831 assert_eq!(w.total_rows(), 1);
832 }
833
834 #[test]
835 fn total_rows_accounts_for_wrapping_long_lines() {
836 let w = wrap("0123456789ABCDE\n", 10);
838 assert_eq!(w.total_rows(), 2);
839 }
840
841 #[test]
842 fn row_col_and_textpos_roundtrip_for_a_wrapped_line() {
843 let w = wrap("0123456789ABCDE", 10); assert_eq!(w.row_col_to_textpos(0, 3), TextPos::new(0, 3));
845 assert_eq!(w.row_col_to_textpos(1, 2), TextPos::new(0, 12));
846 assert_eq!(w.textpos_to_row_col(TextPos::new(0, 3)), (0, 3));
847 assert_eq!(w.textpos_to_row_col(TextPos::new(0, 12)), (1, 2));
848 assert_eq!(w.textpos_to_row_col(TextPos::new(0, 15)), (1, 5));
850 }
851
852 #[test]
853 fn locate_binary_search_finds_the_right_line_for_a_huge_body() {
854 let body: String = (0..100_000).map(|i| format!("line {i}\n")).collect();
855 let w = wrap(&body, 20);
856 assert_eq!(w.row_col_to_textpos(50_000, 0), TextPos::new(50_000, 0));
859 }
860
861 #[test]
862 fn visible_window_only_wraps_the_requested_rows() {
863 let body: String = (0..1000).map(|i| format!("line {i}\n")).collect();
864 let w = wrap(&body, 20);
865 let rows = w.visible_window(500, 5);
866 assert_eq!(rows.len(), 5);
867 let text: Vec<String> = rows
868 .iter()
869 .map(|l| l.spans.iter().map(|s| s.content.as_ref()).collect())
870 .collect();
871 assert_eq!(
872 text,
873 vec!["line 500", "line 501", "line 502", "line 503", "line 504"]
874 );
875 }
876
877 #[test]
884 fn visible_window_is_correct_for_a_single_enormous_unbroken_line() {
885 let body: String = "abcdefghij".repeat(200_000); let w = wrap(&body, 10);
887
888 let top = w.visible_window(0, 3);
889 assert_eq!(top.len(), 3);
890 let row0: String = top[0].spans.iter().map(|s| s.content.as_ref()).collect();
891 assert_eq!(row0, "abcdefghij", "row 0 is chars [0, 10)");
892 let row2: String = top[2].spans.iter().map(|s| s.content.as_ref()).collect();
893 assert_eq!(
894 row2, "abcdefghij",
895 "row 2 (chars [20, 30)) lands mid-repeat but still aligned"
896 );
897
898 let mid = w.visible_window(50_000, 2);
900 assert_eq!(mid.len(), 2);
901 let mid_row: String = mid[0].spans.iter().map(|s| s.content.as_ref()).collect();
902 assert_eq!(mid_row, "abcdefghij");
903
904 let again = w.visible_window(50_000, 2);
907 let again_text: Vec<String> = again
908 .iter()
909 .map(|l| l.spans.iter().map(|s| s.content.as_ref()).collect())
910 .collect();
911 let mid_text: Vec<String> = mid
912 .iter()
913 .map(|l| l.spans.iter().map(|s| s.content.as_ref()).collect())
914 .collect();
915 assert_eq!(again_text, mid_text);
916 }
917
918 #[test]
928 fn visible_window_stays_fast_across_many_redraws_of_a_single_huge_line() {
929 use std::time::{Duration, Instant};
930 let body: String = "x".repeat(5_000_000);
931 let w = wrap(&body, 78);
932
933 let start = Instant::now();
934 for _ in 0..200 {
935 let rows = w.visible_window(0, 30);
936 assert_eq!(
937 rows.len(),
938 30,
939 "the first 30 wrapped rows of a 5,000,000-char line at width 78"
940 );
941 }
942 let elapsed = start.elapsed();
943 assert!(
944 elapsed < Duration::from_secs(2),
945 "200 redraws of a single 5MB line took {elapsed:?} — expected a small fraction of a second"
946 );
947 }
948
949 #[test]
950 fn rebuild_if_needed_skips_rebuilding_on_an_unchanged_pointer_and_width() {
951 let source: Arc<str> = Arc::from("hello\nworld");
952 let mut cache: Option<PanelWrap> = None;
953 PanelWrap::rebuild_if_needed(&mut cache, &source, 10);
954 let first_ptr = cache.as_ref().unwrap().source.as_ptr();
955 PanelWrap::rebuild_if_needed(&mut cache, &source, 10);
957 assert_eq!(cache.as_ref().unwrap().source.as_ptr(), first_ptr);
958 PanelWrap::rebuild_if_needed(&mut cache, &source, 20);
960 assert_eq!(cache.as_ref().unwrap().width, 20);
961 let source2: Arc<str> = Arc::from("hello\nworld");
964 PanelWrap::rebuild_if_needed(&mut cache, &source2, 20);
965 assert!(Arc::ptr_eq(&cache.as_ref().unwrap().source, &source2));
966 }
967}
968
969#[cfg(all(test, feature = "ansi"))]
970mod ansi_tests {
971 use super::*;
972 use ratatui::style::Color;
973
974 fn row_text(line: &Line<'static>) -> String {
975 line.spans.iter().map(|s| s.content.as_ref()).collect()
976 }
977
978 const RED_THEN_PLAIN: &str = "\x1b[31mred\x1b[0m plain";
979
980 #[test]
981 fn geometry_and_copy_use_the_stripped_text() {
982 let w = PanelWrap::build_ansi(Arc::from(RED_THEN_PLAIN), 40, WrapMode::Wrap);
983 assert_eq!(w.line_count(), 1);
985 assert_eq!(w.line_text(0), "red plain");
986 assert_eq!(w.line_char_len(0), 9);
987 }
988
989 #[test]
990 fn rendered_rows_keep_their_colour() {
991 let w = PanelWrap::build_ansi(Arc::from(RED_THEN_PLAIN), 40, WrapMode::Wrap);
992 let rows = w.visible_window(0, 1);
993 assert_eq!(rows.len(), 1);
994 assert_eq!(row_text(&rows[0]), "red plain");
995 assert_eq!(rows[0].spans[0].content.as_ref(), "red");
997 assert_eq!(rows[0].spans[0].style.fg, Some(Color::Red));
998 let plain: String = rows[0].spans[1..]
999 .iter()
1000 .map(|s| s.content.as_ref())
1001 .collect();
1002 assert_eq!(plain, " plain");
1003 assert_ne!(
1004 rows[0].spans[1].style.fg,
1005 Some(Color::Red),
1006 "the reset run is not red"
1007 );
1008 }
1009
1010 #[test]
1011 fn colour_survives_wrapping_across_a_row_boundary() {
1012 let w = PanelWrap::build_ansi(Arc::from(RED_THEN_PLAIN), 4, WrapMode::Wrap);
1014 assert_eq!(w.total_rows(), 3);
1015 let rows = w.visible_window(0, 3);
1016 assert_eq!(row_text(&rows[0]), "red ");
1017 assert_eq!(rows[0].spans[0].content.as_ref(), "red");
1019 assert_eq!(rows[0].spans[0].style.fg, Some(Color::Red));
1020 }
1021
1022 #[test]
1023 fn clip_mode_keeps_colour_on_the_single_clipped_row() {
1024 let w = PanelWrap::build_ansi(Arc::from(RED_THEN_PLAIN), 4, WrapMode::Clip);
1025 assert_eq!(w.total_rows(), 1);
1026 let rows = w.visible_window(0, 5);
1027 assert_eq!(rows.len(), 1);
1028 assert_eq!(row_text(&rows[0]), "red ", "clipped to width 4");
1029 assert_eq!(rows[0].spans[0].style.fg, Some(Color::Red));
1030 }
1031
1032 #[test]
1033 fn ansi_and_plain_switch_forces_a_rebuild() {
1034 let raw: Arc<str> = Arc::from(RED_THEN_PLAIN);
1035 let mut cache: Option<PanelWrap> = None;
1036 PanelWrap::rebuild_if_needed_ansi(&mut cache, &raw, 40, WrapMode::Wrap);
1037 assert!(cache.as_ref().unwrap().line_styles.is_some());
1038 let ptr = cache.as_ref().unwrap().source.as_ptr();
1040 PanelWrap::rebuild_if_needed_ansi(&mut cache, &raw, 40, WrapMode::Wrap);
1041 assert_eq!(cache.as_ref().unwrap().source.as_ptr(), ptr);
1042 PanelWrap::rebuild_if_needed_with(&mut cache, &raw, 40, WrapMode::Wrap);
1044 assert!(cache.as_ref().unwrap().line_styles.is_none());
1045 }
1046}