1use crate::ansi::hyperlink::parse_hyperlink;
64use crate::ansi::params::Params;
65use crate::ansi::text::{Token, string_width, tokenize};
66use crate::buffer::{Bounded, Surface, SurfaceMut};
67use crate::cell::Cell;
68use crate::layout::{Position, Rect, overruns};
69use crate::style::{Style, read_style};
70
71use super::{TextSurface, WidthMode, WrapMode};
72
73pub struct Painter<'s, S: TextSurface + ?Sized> {
82 target: &'s mut S,
83 mode: WidthMode,
85 eaw_wide: bool,
88}
89
90impl<'s, S: TextSurface + ?Sized> Painter<'s, S> {
91 pub fn new(target: &'s mut S) -> Self {
105 let mode = target.width_mode();
106 let eaw_wide = target.eaw_wide();
107 Self {
108 target,
109 mode,
110 eaw_wide,
111 }
112 }
113
114 fn paint_truncate(
119 &mut self,
120 start: Position,
121 clip: Rect,
122 s: &str,
123 s_style: Style,
124 tail_text: &str,
125 tail_style: Style,
126 ) -> Position {
127 if clip.is_empty() {
128 return start;
129 }
130 let tail_w = string_width(tail_text.as_bytes(), self.mode, self.eaw_wide) as u16;
131 let tail = if tail_w == 0 || tail_w > clip.width {
132 None
133 } else {
134 Some(Tail {
135 text: tail_text,
136 style: &tail_style,
137 width: tail_w,
138 })
139 };
140 self.paint_inner(start, clip, s, WrapMode::Truncate, tail, s_style)
141 }
142
143 fn paint_tail(&mut self, tail: Tail<'_>, clip: Rect, y: u16) {
146 let tail_x = clip.right().saturating_sub(tail.width);
147 let sub = Rect::new(tail_x, y, tail.width, 1).intersection(clip);
148 self.paint_inner(
149 Position::new(tail_x, y),
150 sub,
151 tail.text,
152 WrapMode::Truncate,
153 None,
154 tail.style.clone(),
155 );
156 }
157
158 fn paint(
159 &mut self,
160 start: Position,
161 clip: Rect,
162 s: &str,
163 wrap: WrapMode,
164 style: Style,
165 ) -> Position {
166 self.paint_inner(start, clip, s, wrap, None, style)
167 }
168
169 fn paint_inner(
170 &mut self,
171 start: Position,
172 clip: Rect,
173 s: &str,
174 wrap: WrapMode,
175 tail: Option<Tail<'_>>,
176 base: Style,
177 ) -> Position {
178 if clip.is_empty() {
179 return start;
180 }
181 if start.y >= clip.bottom() {
183 return start;
184 }
185 let mut x = start.x;
186 let mut y = start.y;
187 let mut pen = Style::default();
194 let mut pending: Option<(u16, u16, String, u8)> = None;
195 let mut truncated = false;
199
200 for tok in tokenize(s.as_bytes(), self.mode, self.eaw_wide) {
201 if !matches!(tok, Token::Text { width: 0, .. })
205 && let Some((px, py, content, w)) = pending.take()
206 && clip.contains(Position::new(px, py))
207 {
208 let cell = Cell::new(&content, w);
209 self.target
210 .set_cell(Position::new(px, py), &cell.style(pen.inherit(&base)));
211 }
212
213 match tok {
214 Token::Text { text, width: 0 } => {
220 if let Some((_, _, ref mut content, _)) = pending {
221 debug_assert!(std::str::from_utf8(text).is_ok());
222 content.push_str(unsafe { std::str::from_utf8_unchecked(text) });
223 }
224 }
225 Token::Text { text, width } => {
226 debug_assert!(std::str::from_utf8(text).is_ok());
227 if truncated {
228 continue;
229 }
230 let g = unsafe { std::str::from_utf8_unchecked(text) };
231 let cw = width as u8;
232 if overruns(x, u16::from(cw), clip.right()) {
233 match wrap {
234 WrapMode::Truncate => {
235 if let Some(tail) = tail {
236 self.paint_tail(tail, clip, y);
237 x = clip.right();
238 }
239 truncated = true;
240 continue;
241 }
242 WrapMode::Wrap => {
243 y = y.saturating_add(1);
244 x = clip.left();
245 if y >= clip.bottom() {
246 return Position::new(x, y);
247 }
248 if overruns(x, u16::from(cw), clip.right()) {
249 return Position::new(x, y);
250 }
251 }
252 }
253 }
254 pending = Some((x, y, g.to_string(), cw));
255 x += cw as u16;
256 }
257 Token::Escape(seq) => {
258 if seq.last() == Some(&b'm')
259 && let Some(body) = csi_body(seq)
260 {
261 read_style(Params::from_raw(body), &mut pen);
262 } else if let Some(body) = osc_body(seq)
263 && let Some((params, url)) = parse_hyperlink(body)
264 {
265 pen = pen.link(url, params);
266 }
267 }
268 Token::Control(0x0A) => {
269 y = y.saturating_add(1);
270 x = clip.left();
271 truncated = false;
272 if y >= clip.bottom() {
273 return Position::new(x, y);
274 }
275 }
276 Token::Control(0x0D) => {
277 x = clip.left();
278 truncated = false;
279 }
280 Token::Control(_) => {}
281 }
282 }
283
284 if let Some((px, py, content, w)) = pending.take()
286 && clip.contains(Position::new(px, py))
287 {
288 let cell = Cell::new(&content, w);
289 self.target
290 .set_cell(Position::new(px, py), &cell.style(pen.inherit(&base)));
291 }
292 Position::new(x, y)
293 }
294}
295
296impl<'s, S: TextSurface + ?Sized> Bounded for Painter<'s, S> {
297 fn bounds(&self) -> Rect {
298 self.target.bounds()
299 }
300}
301
302impl<'s, S: TextSurface + ?Sized> Surface for Painter<'s, S> {
303 fn cell(&self, pos: Position) -> Option<&Cell> {
304 self.target.cell(pos)
305 }
306}
307
308impl<'s, S: TextSurface + ?Sized> SurfaceMut for Painter<'s, S> {
309 fn set_cell(&mut self, pos: Position, cell: &Cell) {
310 self.target.set_cell(pos, cell);
311 }
312
313 fn cell_mut(&mut self, pos: Position) -> Option<&mut Cell> {
314 self.target.cell_mut(pos)
315 }
316
317 fn insert_lines(&mut self, y: u16, count: u16, bounds_bottom: u16, fill: &Cell) {
318 self.target.insert_lines(y, count, bounds_bottom, fill);
319 }
320
321 fn delete_lines(&mut self, y: u16, count: u16, bounds_bottom: u16, fill: &Cell) {
322 self.target.delete_lines(y, count, bounds_bottom, fill);
323 }
324
325 fn insert_cells(&mut self, pos: Position, count: u16, bounds_right: u16, fill: &Cell) {
326 self.target.insert_cells(pos, count, bounds_right, fill);
327 }
328
329 fn delete_cells(&mut self, pos: Position, count: u16, bounds_right: u16, fill: &Cell) {
330 self.target.delete_cells(pos, count, bounds_right, fill);
331 }
332}
333
334impl<'s, S: TextSurface + ?Sized> TextSurface for Painter<'s, S> {
339 fn width_mode(&self) -> WidthMode {
340 self.mode
341 }
342
343 fn eaw_wide(&self) -> bool {
344 self.eaw_wide
345 }
346
347 fn str_width(&self, s: &str) -> u16 {
352 string_width(s.as_bytes(), self.mode, self.eaw_wide).min(u16::MAX as usize) as u16
353 }
354
355 fn set_str(&mut self, pos: impl Into<Position>, s: &str, style: impl Into<Style>) -> Position {
380 let clip = self.target.bounds();
381 self.paint(pos.into(), clip, s, WrapMode::default(), style.into())
382 }
383
384 fn set_str_wrap(
407 &mut self,
408 pos: impl Into<Position>,
409 s: &str,
410 wrap: WrapMode,
411 style: impl Into<Style>,
412 ) -> Position {
413 let clip = self.target.bounds();
414 self.paint(pos.into(), clip, s, wrap, style.into())
415 }
416
417 fn set_str_rect(
438 &mut self,
439 rect: impl Into<Rect>,
440 s: &str,
441 style: impl Into<Style>,
442 ) -> Position {
443 let rect = rect.into();
444 let clip = rect.intersection(self.target.bounds());
445 self.paint(rect.position(), clip, s, WrapMode::default(), style.into())
446 }
447
448 fn set_str_rect_wrap(
470 &mut self,
471 rect: impl Into<Rect>,
472 s: &str,
473 wrap: WrapMode,
474 style: impl Into<Style>,
475 ) -> Position {
476 let rect = rect.into();
477 let clip = rect.intersection(self.target.bounds());
478 self.paint(rect.position(), clip, s, wrap, style.into())
479 }
480
481 fn set_str_truncate(
513 &mut self,
514 pos: impl Into<Position>,
515 s: &str,
516 s_style: impl Into<Style>,
517 tail: &str,
518 tail_style: impl Into<Style>,
519 ) -> Position {
520 let clip = self.target.bounds();
521 self.paint_truncate(pos.into(), clip, s, s_style.into(), tail, tail_style.into())
522 }
523
524 fn set_str_rect_truncate(
548 &mut self,
549 rect: impl Into<Rect>,
550 s: &str,
551 s_style: impl Into<Style>,
552 tail: &str,
553 tail_style: impl Into<Style>,
554 ) -> Position {
555 let rect = rect.into();
556 let clip = rect.intersection(self.target.bounds());
557 self.paint_truncate(
558 rect.position(),
559 clip,
560 s,
561 s_style.into(),
562 tail,
563 tail_style.into(),
564 )
565 }
566}
567
568#[derive(Clone, Copy)]
572struct Tail<'a> {
573 text: &'a str,
574 style: &'a Style,
575 width: u16,
576}
577
578fn csi_body(seq: &[u8]) -> Option<&[u8]> {
584 let body_start = if seq.len() >= 2 && seq[0] == 0x1b && seq[1] == b'[' {
585 2
586 } else if !seq.is_empty() && seq[0] == 0x9b {
587 1
588 } else {
589 return None;
590 };
591 let last = *seq.last()?;
592 if !(0x40..=0x7e).contains(&last) || seq.len() <= body_start {
593 return None;
594 }
595 Some(&seq[body_start..seq.len() - 1])
596}
597
598fn osc_body(seq: &[u8]) -> Option<&[u8]> {
604 let body_start = if seq.len() >= 2 && seq[0] == 0x1b && seq[1] == b']' {
605 2
606 } else if !seq.is_empty() && seq[0] == 0x9d {
607 1
608 } else {
609 return None;
610 };
611 if seq.len() <= body_start {
612 return Some(&[]);
613 }
614 let end = if seq.ends_with(b"\x1b\\") {
615 seq.len() - 2
616 } else if matches!(seq.last(), Some(0x07 | 0x9c)) {
617 seq.len() - 1
618 } else {
619 seq.len()
620 };
621 if end < body_start {
622 return Some(&[]);
623 }
624 Some(&seq[body_start..end])
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use crate::buffer::{Surface, TextBuffer};
631 use crate::color::Color;
632 use crate::style::AttrFlags;
633
634 fn buf(width: u16, height: u16) -> TextBuffer {
635 TextBuffer::new(width, height)
636 }
637
638 fn cell_at(b: &TextBuffer, x: u16, y: u16) -> Cell {
639 b.cell(Position::new(x, y)).cloned().unwrap()
640 }
641
642 fn link_of(s: &crate::style::Style) -> Option<(&str, &str)> {
643 s.link
644 .as_deref()
645 .map(|l| (l.url.as_str(), l.params.as_str()))
646 }
647
648 #[test]
649 fn plain_text() {
650 let mut b = buf(10, 1);
651 let end =
652 Painter::new(&mut b).set_str_wrap((0, 0), "abc", WrapMode::Truncate, Style::default());
653 assert_eq!(end, Position::new(3, 0));
654 assert_eq!(cell_at(&b, 0, 0).content(), "a");
655 assert_eq!(cell_at(&b, 2, 0).content(), "c");
656 }
657
658 #[test]
659 fn sgr_updates_style_mid_stream() {
660 let mut b = buf(10, 1);
661 let mut p = Painter::new(&mut b);
662 let end = p.set_str_wrap(
663 (0, 0),
664 "a\x1b[1mb\x1b[mc",
665 WrapMode::Truncate,
666 Style::default(),
667 );
668 assert_eq!(end, Position::new(3, 0));
669 let c0 = cell_at(&b, 0, 0);
670 let c1 = cell_at(&b, 1, 0);
671 let c2 = cell_at(&b, 2, 0);
672 assert!(!c0.style.attrs.contains(AttrFlags::BOLD));
673 assert!(c1.style.attrs.contains(AttrFlags::BOLD));
674 assert!(!c2.style.attrs.contains(AttrFlags::BOLD));
675 }
676
677 #[test]
678 fn sgr_color() {
679 let mut b = buf(5, 1);
680 Painter::new(&mut b).set_str_wrap(
681 (0, 0),
682 "\x1b[31mr",
683 WrapMode::Truncate,
684 Style::default(),
685 );
686 assert_eq!(cell_at(&b, 0, 0).style.fg, Some(Color::Red));
687 }
688
689 #[test]
697 fn utf8_payloads_in_sequences_paint_the_text_after_them() {
698 for input in [
699 "\x1b]0;\u{2705}\x07ab",
700 "\x1b]0;x\u{9c}y\x07ab",
701 "\x1b]0;x\u{9d}y\x07ab",
702 "\x1bP1$r\u{2705}\x1b\\ab",
703 "\x1b_G\u{2705}\x1b\\ab",
704 "\x1b#\u{2705}ab",
705 ] {
706 let mut b = buf(10, 1);
707 let end = Painter::new(&mut b).set_str_wrap(
708 (0, 0),
709 input,
710 WrapMode::Truncate,
711 Style::default(),
712 );
713 assert_eq!(
714 end,
715 Position::new(2, 0),
716 "{input:?} painted the wrong width"
717 );
718 assert_eq!(cell_at(&b, 0, 0).content(), "a", "{input:?}");
719 assert_eq!(cell_at(&b, 1, 0).content(), "b", "{input:?}");
720 }
721 }
722
723 #[test]
726 fn osc8_with_a_utf8_url() {
727 let mut b = buf(10, 1);
728 Painter::new(&mut b).set_str_wrap(
729 (0, 0),
730 "\x1b]8;;https://x/\u{2705}\x1b\\a\x1b]8;;\x1b\\b",
731 WrapMode::Truncate,
732 Style::default(),
733 );
734 assert_eq!(
735 link_of(&cell_at(&b, 0, 0).style),
736 Some(("https://x/\u{2705}", ""))
737 );
738 assert_eq!(cell_at(&b, 0, 0).content(), "a");
739 assert!(cell_at(&b, 1, 0).style.link.is_none());
740 assert_eq!(cell_at(&b, 1, 0).content(), "b");
741 }
742
743 #[test]
744 fn osc8_toggles_link() {
745 let mut b = buf(10, 1);
746 Painter::new(&mut b).set_str_wrap(
747 (0, 0),
748 "\x1b]8;;https://x\x1b\\a\x1b]8;;\x1b\\b",
749 WrapMode::Truncate,
750 Style::default(),
751 );
752 assert_eq!(link_of(&cell_at(&b, 0, 0).style), Some(("https://x", "")));
753 assert!(cell_at(&b, 1, 0).style.link.is_none());
754 }
755
756 #[test]
757 fn osc8_malformed_ignored() {
758 let mut b = buf(10, 1);
761 let mut p = Painter::new(&mut b);
762 p.set_str_wrap(
763 (0, 0),
764 "\x1b]8;;https://x\x1b\\a\x1b]8;garbage\x1b\\b",
765 WrapMode::Truncate,
766 Style::default(),
767 );
768 assert_eq!(link_of(&cell_at(&b, 0, 0).style), Some(("https://x", "")));
769 assert_eq!(link_of(&cell_at(&b, 1, 0).style), Some(("https://x", "")));
770 }
771
772 #[test]
773 fn newline_advances_row() {
774 let mut b = buf(5, 3);
775 let end = Painter::new(&mut b).set_str_wrap(
776 (0, 0),
777 "ab\ncd",
778 WrapMode::Truncate,
779 Style::default(),
780 );
781 assert_eq!(cell_at(&b, 0, 0).content(), "a");
782 assert_eq!(cell_at(&b, 1, 0).content(), "b");
783 assert_eq!(cell_at(&b, 0, 1).content(), "c");
784 assert_eq!(cell_at(&b, 1, 1).content(), "d");
785 assert_eq!(end, Position::new(2, 1));
786 }
787
788 #[test]
789 fn cr_returns_to_left() {
790 let mut b = buf(5, 1);
791 Painter::new(&mut b).set_str_wrap((0, 0), "abc\rXY", WrapMode::Truncate, Style::default());
792 assert_eq!(cell_at(&b, 0, 0).content(), "X");
794 assert_eq!(cell_at(&b, 1, 0).content(), "Y");
795 assert_eq!(cell_at(&b, 2, 0).content(), "c");
796 }
797
798 #[test]
799 fn newline_past_bottom_returns() {
800 let mut b = buf(5, 2);
801 let end = Painter::new(&mut b).set_str_wrap(
802 (0, 0),
803 "a\nb\nc",
804 WrapMode::Truncate,
805 Style::default(),
806 );
807 assert_eq!(end, Position::new(0, 2));
808 assert_eq!(cell_at(&b, 0, 0).content(), "a");
809 assert_eq!(cell_at(&b, 0, 1).content(), "b");
810 }
812
813 #[test]
814 fn truncate_at_right_edge() {
815 let mut b = buf(3, 1);
816 let end = Painter::new(&mut b).set_str_wrap(
817 (0, 0),
818 "abcdef",
819 WrapMode::Truncate,
820 Style::default(),
821 );
822 assert_eq!(end, Position::new(3, 0));
823 assert_eq!(cell_at(&b, 0, 0).content(), "a");
824 assert_eq!(cell_at(&b, 2, 0).content(), "c");
825 }
826
827 #[test]
828 fn truncate_resumes_on_next_row() {
829 let mut b = buf(3, 2);
830 let end = Painter::new(&mut b).set_str_wrap(
831 (0, 0),
832 "abcdef\nxy",
833 WrapMode::Truncate,
834 Style::default(),
835 );
836 assert_eq!(cell_at(&b, 2, 0).content(), "c");
837 assert_eq!(cell_at(&b, 0, 1).content(), "x");
838 assert_eq!(cell_at(&b, 1, 1).content(), "y");
839 assert_eq!(end, Position::new(2, 1));
840 }
841
842 #[test]
843 fn truncate_tail_stamped_per_row() {
844 let mut b = buf(4, 2);
845 Painter::new(&mut b).set_str_truncate(
846 (0, 0),
847 "abcdef\nghijkl",
848 Style::default(),
849 "…",
850 Style::default(),
851 );
852 assert_eq!(cell_at(&b, 3, 0).content(), "…");
853 assert_eq!(cell_at(&b, 0, 1).content(), "g");
854 assert_eq!(cell_at(&b, 3, 1).content(), "…");
855 }
856
857 #[test]
858 fn literal_truncate_resumes_on_next_row() {
859 let mut b = buf(3, 2);
860 b.set_str((0, 0), "abcdef\nxy", Style::default());
861 assert_eq!(cell_at(&b, 2, 0).content(), "c");
862 assert_eq!(cell_at(&b, 0, 1).content(), "x");
863 assert_eq!(cell_at(&b, 1, 1).content(), "y");
864 }
865
866 #[test]
867 fn cr_clears_truncation_for_the_row() {
868 let mut b = buf(3, 1);
869 Painter::new(&mut b).set_str_wrap(
870 (0, 0),
871 "abcdef\rXY",
872 WrapMode::Truncate,
873 Style::default(),
874 );
875 assert_eq!(cell_at(&b, 0, 0).content(), "X");
876 assert_eq!(cell_at(&b, 1, 0).content(), "Y");
877 assert_eq!(cell_at(&b, 2, 0).content(), "c");
878 }
879
880 #[test]
881 fn overflow_drops_rest_of_row_without_backfill() {
882 let mut b = buf(3, 1);
883 let end = Painter::new(&mut b).set_str_wrap(
884 (0, 0),
885 "ab中c",
886 WrapMode::Truncate,
887 Style::default(),
888 );
889 assert_eq!(cell_at(&b, 0, 0).content(), "a");
890 assert_eq!(cell_at(&b, 1, 0).content(), "b");
891 assert_eq!(cell_at(&b, 2, 0).content(), " ");
894 assert_eq!(end, Position::new(2, 0));
895 }
896
897 #[test]
898 fn literal_crlf_breaks_the_line() {
899 let mut b = buf(3, 2);
902 b.set_str((0, 0), "abcdef\r\nxy", Style::default());
903 assert_eq!(cell_at(&b, 2, 0).content(), "c");
904 assert_eq!(cell_at(&b, 0, 1).content(), "x");
905 assert_eq!(cell_at(&b, 1, 1).content(), "y");
906 }
907
908 #[test]
909 fn crlf_breaks_the_line() {
910 let mut b = buf(3, 2);
911 Painter::new(&mut b).set_str_wrap(
912 (0, 0),
913 "abcdef\r\nxy",
914 WrapMode::Truncate,
915 Style::default(),
916 );
917 assert_eq!(cell_at(&b, 2, 0).content(), "c");
918 assert_eq!(cell_at(&b, 0, 1).content(), "x");
919 assert_eq!(cell_at(&b, 1, 1).content(), "y");
920 }
921
922 #[test]
923 fn escapes_still_apply_across_a_truncated_row() {
924 let mut b = buf(2, 2);
927 Painter::new(&mut b).set_str_wrap(
928 (0, 0),
929 "ab\x1b[1mcd\x1b]8;;https://x\x1b\\\nz",
930 WrapMode::Truncate,
931 Style::default(),
932 );
933 assert!(!cell_at(&b, 0, 0).style.attrs.contains(AttrFlags::BOLD));
934 let z = cell_at(&b, 0, 1);
935 assert_eq!(z.content(), "z");
936 assert!(z.style.attrs.contains(AttrFlags::BOLD));
937 assert_eq!(link_of(&z.style), Some(("https://x", "")));
938 }
939
940 #[test]
941 fn start_below_clip_paints_nothing() {
942 let mut b = buf(3, 2);
943 let end = Painter::new(&mut b).set_str_wrap(
944 (0, 5),
945 "abcdef\nghi",
946 WrapMode::Truncate,
947 Style::default(),
948 );
949 assert_eq!(end, Position::new(0, 5));
950 for y in 0..2 {
951 for x in 0..3 {
952 assert_eq!(cell_at(&b, x, y).content(), " ");
953 }
954 }
955 }
956
957 #[test]
958 fn literal_start_below_clip_paints_nothing() {
959 let mut b = buf(3, 2);
960 let end = b.set_str((0, 5), "abcdef\nghi", Style::default());
961 assert_eq!(end, Position::new(0, 5));
962 assert_eq!(cell_at(&b, 0, 0).content(), " ");
963 assert_eq!(cell_at(&b, 0, 1).content(), " ");
964 }
965
966 #[test]
967 fn wrap_breaks_on_crlf() {
968 let mut b = buf(4, 3);
971 Painter::new(&mut b).set_str_wrap((0, 0), "ab\r\ncd", WrapMode::Wrap, Style::default());
972 assert_eq!(cell_at(&b, 0, 0).content(), "a");
973 assert_eq!(cell_at(&b, 1, 0).content(), "b");
974 assert_eq!(cell_at(&b, 0, 1).content(), "c");
975 assert_eq!(cell_at(&b, 1, 1).content(), "d");
976 }
977
978 #[test]
979 fn literal_wrap_breaks_on_crlf() {
980 let mut b = buf(4, 3);
981 b.set_str_wrap((0, 0), "ab\r\ncd", WrapMode::Wrap, Style::default());
982 assert_eq!(cell_at(&b, 0, 0).content(), "a");
983 assert_eq!(cell_at(&b, 1, 0).content(), "b");
984 assert_eq!(cell_at(&b, 0, 1).content(), "c");
985 assert_eq!(cell_at(&b, 1, 1).content(), "d");
986 }
987
988 #[test]
989 fn wrap_at_right_edge() {
990 let mut b = buf(3, 3);
991 let end =
992 Painter::new(&mut b).set_str_wrap((0, 0), "abcdef", WrapMode::Wrap, Style::default());
993 assert_eq!(end, Position::new(3, 1));
994 assert_eq!(cell_at(&b, 0, 0).content(), "a");
995 assert_eq!(cell_at(&b, 2, 0).content(), "c");
996 assert_eq!(cell_at(&b, 0, 1).content(), "d");
997 assert_eq!(cell_at(&b, 2, 1).content(), "f");
998 }
999
1000 #[test]
1001 fn rect_clip_and_origin() {
1002 let mut b = buf(10, 5);
1003 let end = Painter::new(&mut b).set_str_rect_wrap(
1004 Rect::new(2, 1, 3, 2),
1005 "abcdef",
1006 WrapMode::Wrap,
1007 Style::default(),
1008 );
1009 assert_eq!(end, Position::new(5, 2));
1010 assert_eq!(cell_at(&b, 2, 1).content(), "a");
1011 assert_eq!(cell_at(&b, 4, 1).content(), "c");
1012 assert_eq!(cell_at(&b, 2, 2).content(), "d");
1013 assert_eq!(cell_at(&b, 4, 2).content(), "f");
1014 assert_eq!(cell_at(&b, 0, 0).content(), " ");
1016 assert_eq!(cell_at(&b, 5, 1).content(), " ");
1017 }
1018
1019 #[test]
1020 fn rect_newline_uses_rect_left() {
1021 let mut b = buf(10, 5);
1022 Painter::new(&mut b).set_str_rect_wrap(
1023 Rect::new(2, 1, 4, 3),
1024 "ab\ncd",
1025 WrapMode::Truncate,
1026 Style::default(),
1027 );
1028 assert_eq!(cell_at(&b, 2, 1).content(), "a");
1029 assert_eq!(cell_at(&b, 3, 1).content(), "b");
1030 assert_eq!(cell_at(&b, 2, 2).content(), "c");
1032 assert_eq!(cell_at(&b, 3, 2).content(), "d");
1033 assert_eq!(cell_at(&b, 0, 2).content(), " ");
1034 }
1035
1036 #[test]
1037 fn with_resets_style_and_link() {
1038 let mut b = buf(10, 1);
1039 Painter::new(&mut b).set_str_wrap(
1041 (0, 0),
1042 "a",
1043 WrapMode::Truncate,
1044 Style::default().bold().link("https://x", ""),
1045 );
1046 assert!(cell_at(&b, 0, 0).style.attrs.contains(AttrFlags::BOLD));
1047 assert_eq!(link_of(&cell_at(&b, 0, 0).style), Some(("https://x", "")));
1048 Painter::new(&mut b).set_str_wrap((1, 0), "b", WrapMode::Truncate, Style::default());
1050 assert!(!cell_at(&b, 1, 0).style.attrs.contains(AttrFlags::BOLD));
1051 assert!(cell_at(&b, 1, 0).style.link.is_none());
1052 }
1053
1054 #[test]
1055 fn calls_start_from_their_own_base() {
1056 let mut b = buf(10, 1);
1057 let mut p = Painter::new(&mut b);
1058 p.set_str_wrap((0, 0), "\x1b[1ma", WrapMode::Truncate, Style::default());
1060 p.set_str_wrap((1, 0), "b", WrapMode::Truncate, Style::default());
1062 assert!(cell_at(&b, 0, 0).style.attrs.contains(AttrFlags::BOLD));
1063 assert!(!cell_at(&b, 1, 0).style.attrs.contains(AttrFlags::BOLD));
1064 }
1065
1066 #[test]
1067 fn base_applies_and_inline_reset_returns_to_base() {
1068 let mut b = buf(10, 1);
1069 let base = Style::default().fg(Color::Red);
1070 Painter::new(&mut b).set_str_wrap((0, 0), "a\x1b[1mb\x1b[0mc", WrapMode::Truncate, base);
1074 let red = Some(Color::Red);
1075 assert_eq!(cell_at(&b, 0, 0).style.fg, red);
1076 assert!(!cell_at(&b, 0, 0).style.attrs.contains(AttrFlags::BOLD));
1077 assert_eq!(cell_at(&b, 1, 0).style.fg, red);
1078 assert!(cell_at(&b, 1, 0).style.attrs.contains(AttrFlags::BOLD));
1079 assert_eq!(cell_at(&b, 2, 0).style.fg, red);
1080 assert!(!cell_at(&b, 2, 0).style.attrs.contains(AttrFlags::BOLD));
1081 }
1082
1083 #[test]
1084 fn position_and_rect_match_when_rect_covers_bounds() {
1085 let mut a = buf(5, 2);
1086 let mut b = buf(5, 2);
1087 let e1 =
1088 Painter::new(&mut a).set_str_wrap((0, 0), "abc", WrapMode::Truncate, Style::default());
1089 let e2 = Painter::new(&mut b).set_str_rect_wrap(
1090 Rect::new(0, 0, 5, 2),
1091 "abc",
1092 WrapMode::Truncate,
1093 Style::default(),
1094 );
1095 assert_eq!(e1, e2);
1096 assert_eq!(cell_at(&a, 2, 0).content(), cell_at(&b, 2, 0).content());
1097 }
1098
1099 fn row(b: &TextBuffer, y: u16) -> String {
1100 (0..b.width())
1101 .map(|x| cell_at(b, x, y).content().to_string())
1102 .collect()
1103 }
1104
1105 #[test]
1106 fn truncate_tail_not_shown_when_text_fits() {
1107 let mut b = buf(5, 1);
1108 let end = Painter::new(&mut b).set_str_truncate(
1109 (0, 0),
1110 "abc",
1111 Style::default(),
1112 "…",
1113 Style::default(),
1114 );
1115 assert_eq!(row(&b, 0), "abc ");
1117 assert_eq!(end, Position::new(3, 0));
1118 }
1119
1120 #[test]
1121 fn truncate_single_cell_tail_on_overflow() {
1122 let mut b = buf(5, 1);
1123 let end = Painter::new(&mut b).set_str_truncate(
1124 (0, 0),
1125 "abcdefgh",
1126 Style::default(),
1127 "…",
1128 Style::default(),
1129 );
1130 assert_eq!(row(&b, 0), "abcd…");
1132 assert_eq!(end, Position::new(5, 0));
1133 }
1134
1135 #[test]
1136 fn truncate_multi_cell_tail_reserves_its_width() {
1137 let mut b = buf(8, 1);
1138 Painter::new(&mut b).set_str_truncate(
1139 (0, 0),
1140 "abcdefghij",
1141 Style::default(),
1142 " more",
1143 Style::default(),
1144 );
1145 assert_eq!(row(&b, 0), "abc more");
1147 }
1148
1149 #[test]
1150 fn truncate_tail_carries_its_style() {
1151 let mut b = buf(5, 1);
1152 Painter::new(&mut b).set_str_truncate(
1153 (0, 0),
1154 "abcdefgh",
1155 Style::default(),
1156 "…",
1157 Style::default().fg(Color::Red),
1158 );
1159 assert_eq!(cell_at(&b, 4, 0).style.fg, Some(Color::Red));
1161 }
1162
1163 #[test]
1164 fn truncate_methods_keep_text_and_tail_styles_separate() {
1165 fn paint(
1166 target: &mut impl TextSurface,
1167 rect: bool,
1168 text: &str,
1169 style: &Style,
1170 tail: &str,
1171 tail_style: &Style,
1172 ) -> Position {
1173 if rect {
1174 target.set_str_rect_truncate(Rect::new(1, 1, 5, 2), text, style, tail, tail_style)
1175 } else {
1176 target.set_str_truncate((0, 0), text, style, tail, tail_style)
1177 }
1178 }
1179
1180 let style = Style::new().fg(Color::Green).bold();
1181 let tail_style = Style::new().fg(Color::Red).italic();
1182 for painter in [false, true] {
1183 for rect in [false, true] {
1184 for (text, tail, expected) in [
1185 ("abc", ".", "abc "),
1186 ("abcde", ".", "abcde"),
1187 ("abcdefgh", ".", "abcd."),
1188 ("abcdefgh", "", "abcde"),
1189 ("abcdefgh", " more!", "abcde"),
1190 ] {
1191 let mut b = if rect { buf(7, 4) } else { buf(5, 2) };
1192 let text = format!("{text}\n{text}");
1193 let end = if painter {
1194 paint(
1195 &mut Painter::new(&mut b),
1196 rect,
1197 &text,
1198 &style,
1199 tail,
1200 &tail_style,
1201 )
1202 } else {
1203 paint(&mut b, rect, &text, &style, tail, &tail_style)
1204 };
1205 let offset = u16::from(rect);
1206 assert_eq!(
1207 end,
1208 Position::new(offset + expected.trim_end().len() as u16, offset + 1)
1209 );
1210 for y in 0..b.height() {
1211 for x in 0..b.width() {
1212 let expected_cell = if (offset..offset + 2).contains(&y)
1213 && (offset..offset + 5).contains(&x)
1214 {
1215 let ch = expected.as_bytes()[usize::from(x - offset)] as char;
1216 match ch {
1217 ' ' => Cell::BLANK,
1218 '.' => Cell::new(".", 1).style(&tail_style),
1219 _ => Cell::new(ch.to_string(), 1).style(&style),
1220 }
1221 } else {
1222 Cell::BLANK
1223 };
1224 assert_eq!(cell_at(&b, x, y), expected_cell);
1225 }
1226 }
1227 }
1228 }
1229 }
1230 }
1231
1232 #[test]
1233 fn truncate_inline_styles_start_from_independent_base_styles() {
1234 for rect in [false, true] {
1235 let mut b = buf(5, 1);
1236 let text_style = Style::new().fg(Color::Green);
1237 let tail_style = Style::new().fg(Color::Red);
1238 let mut painter = Painter::new(&mut b);
1239 if rect {
1240 painter.set_str_rect_truncate(
1241 Rect::new(0, 0, 5, 1),
1242 "\x1b[1mabcdefgh",
1243 &text_style,
1244 "\x1b[3m.",
1245 &tail_style,
1246 );
1247 } else {
1248 painter.set_str_truncate(
1249 (0, 0),
1250 "\x1b[1mabcdefgh",
1251 &text_style,
1252 "\x1b[3m.",
1253 &tail_style,
1254 );
1255 }
1256 for x in 0..4 {
1257 assert_eq!(cell_at(&b, x, 0).style, text_style.clone().bold());
1258 }
1259 assert_eq!(cell_at(&b, 4, 0).style, tail_style.italic());
1260 }
1261 }
1262
1263 #[test]
1264 fn truncate_tail_inline_escapes_apply() {
1265 let mut b = buf(5, 1);
1266 Painter::new(&mut b).set_str_truncate(
1267 (0, 0),
1268 "abcdefgh",
1269 Style::default(),
1270 "\x1b[1m…",
1271 Style::default(),
1272 );
1273 assert!(cell_at(&b, 4, 0).style.attrs.contains(AttrFlags::BOLD));
1275 }
1276
1277 #[test]
1278 fn truncate_wide_tail_too_big_hard_truncates() {
1279 let mut b = buf(3, 1);
1280 let end = Painter::new(&mut b).set_str_truncate(
1281 (0, 0),
1282 "abcdef",
1283 Style::default(),
1284 " more",
1285 Style::default(),
1286 );
1287 assert_eq!(row(&b, 0), "abc");
1290 assert_eq!(end, Position::new(3, 0));
1291 }
1292
1293 #[test]
1296 fn a_cluster_measuring_four_claims_four_columns() {
1297 let mut b = buf(6, 1).with_width_mode(WidthMode::Wc);
1298 Painter::new(&mut b).set_str((0, 0), "\u{1f44d}\u{1f3fd}", Style::default());
1301
1302 assert_eq!(cell_at(&b, 0, 0).content(), "\u{1f44d}\u{1f3fd}");
1303 assert_eq!(cell_at(&b, 0, 0).width(), 4);
1304 for x in 1..4 {
1305 assert!(
1306 cell_at(&b, x, 0).is_continuation(),
1307 "column {x} belongs to the cluster"
1308 );
1309 }
1310 assert!(cell_at(&b, 4, 0).is_blank(), "the cluster stops at four");
1311 }
1312
1313 #[test]
1314 fn truncate_tail_overwrites_split_wide_cell() {
1315 let mut b = buf(5, 1);
1318 Painter::new(&mut b).set_str_truncate(
1319 (0, 0),
1320 "ab中def",
1321 Style::default(),
1322 "…",
1323 Style::default(),
1324 );
1325 assert_eq!(cell_at(&b, 4, 0).content(), "…");
1329 assert!(!cell_at(&b, 3, 0).is_wide());
1330 }
1331}