1use crate::ansi::hyperlink::parse_hyperlink;
53use crate::ansi::params::Params;
54use crate::ansi::text::{Token, string_width, tokenize};
55use crate::buffer::{Bounded, Surface, SurfaceMut};
56use crate::cell::Cell;
57use crate::layout::{Position, Rect};
58use crate::style::{Style, read_style};
59
60use super::{TextSurface, WidthMode, WrapMode};
61
62pub struct Painter<'s, S: TextSurface + ?Sized> {
71 target: &'s mut S,
72 mode: WidthMode,
74 eaw_wide: bool,
77}
78
79impl<'s, S: TextSurface + ?Sized> Painter<'s, S> {
80 pub fn new(target: &'s mut S) -> Self {
94 let mode = target.width_mode();
95 let eaw_wide = target.eaw_wide();
96 Self {
97 target,
98 mode,
99 eaw_wide,
100 }
101 }
102
103 fn paint_truncate(
108 &mut self,
109 start: Position,
110 clip: Rect,
111 s: &str,
112 tail_text: &str,
113 tail_style: Style,
114 style: Style,
115 ) -> Position {
116 if clip.is_empty() {
117 return start;
118 }
119 let tail_w = string_width(tail_text.as_bytes(), self.mode, self.eaw_wide) as u16;
120 let tail = if tail_w == 0 || tail_w > clip.width {
121 None
122 } else {
123 Some(Tail {
124 text: tail_text,
125 style: &tail_style,
126 width: tail_w,
127 })
128 };
129 self.paint_inner(start, clip, s, WrapMode::Truncate, tail, style)
130 }
131
132 fn paint_tail(&mut self, tail: Tail<'_>, clip: Rect, y: u16) {
135 let tail_x = clip.right().saturating_sub(tail.width);
136 let sub = Rect::new(tail_x, y, tail.width, 1).intersection(clip);
137 self.paint_inner(
138 Position::new(tail_x, y),
139 sub,
140 tail.text,
141 WrapMode::Truncate,
142 None,
143 tail.style.clone(),
144 );
145 }
146
147 fn paint(
148 &mut self,
149 start: Position,
150 clip: Rect,
151 s: &str,
152 wrap: WrapMode,
153 style: Style,
154 ) -> Position {
155 self.paint_inner(start, clip, s, wrap, None, style)
156 }
157
158 fn paint_inner(
159 &mut self,
160 start: Position,
161 clip: Rect,
162 s: &str,
163 wrap: WrapMode,
164 tail: Option<Tail<'_>>,
165 base: Style,
166 ) -> Position {
167 if clip.is_empty() {
168 return start;
169 }
170 if start.y >= clip.bottom() {
172 return start;
173 }
174 let mut x = start.x;
175 let mut y = start.y;
176 let mut pen = Style::default();
183 let mut pending: Option<(u16, u16, String, u8)> = None;
184 let mut truncated = false;
188
189 for tok in tokenize(s.as_bytes(), self.mode, self.eaw_wide) {
190 if !matches!(tok, Token::Text { width: 0, .. })
194 && let Some((px, py, content, w)) = pending.take()
195 && clip.contains(Position::new(px, py))
196 {
197 let cell = if w == 2 {
198 Cell::wide(&content)
199 } else {
200 Cell::narrow(&content)
201 };
202 self.target
203 .set_cell(Position::new(px, py), &cell.style(pen.inherit(&base)));
204 }
205
206 match tok {
207 Token::Text { text, width: 0 } => {
213 if let Some((_, _, ref mut content, _)) = pending {
214 debug_assert!(std::str::from_utf8(text).is_ok());
215 content.push_str(unsafe { std::str::from_utf8_unchecked(text) });
216 }
217 }
218 Token::Text { text, width } => {
219 debug_assert!(std::str::from_utf8(text).is_ok());
220 if truncated {
221 continue;
222 }
223 let g = unsafe { std::str::from_utf8_unchecked(text) };
224 let cw = width as u8;
225 if x + cw as u16 > clip.right() {
226 match wrap {
227 WrapMode::Truncate => {
228 if let Some(tail) = tail {
229 self.paint_tail(tail, clip, y);
230 x = clip.right();
231 }
232 truncated = true;
233 continue;
234 }
235 WrapMode::Wrap => {
236 y = y.saturating_add(1);
237 x = clip.left();
238 if y >= clip.bottom() {
239 return Position::new(x, y);
240 }
241 if x + cw as u16 > clip.right() {
242 return Position::new(x, y);
243 }
244 }
245 }
246 }
247 pending = Some((x, y, g.to_string(), cw));
248 x += cw as u16;
249 }
250 Token::Escape(seq) => {
251 if seq.last() == Some(&b'm')
252 && let Some(body) = csi_body(seq)
253 {
254 read_style(Params::from_raw(body), &mut pen);
255 } else if let Some(body) = osc_body(seq)
256 && let Some((params, url)) = parse_hyperlink(body)
257 {
258 pen = pen.link(url, params);
259 }
260 }
261 Token::Control(0x0A) => {
262 y = y.saturating_add(1);
263 x = clip.left();
264 truncated = false;
265 if y >= clip.bottom() {
266 return Position::new(x, y);
267 }
268 }
269 Token::Control(0x0D) => {
270 x = clip.left();
271 truncated = false;
272 }
273 Token::Control(_) => {}
274 }
275 }
276
277 if let Some((px, py, content, w)) = pending.take()
279 && clip.contains(Position::new(px, py))
280 {
281 let cell = if w == 2 {
282 Cell::wide(&content)
283 } else {
284 Cell::narrow(&content)
285 };
286 self.target
287 .set_cell(Position::new(px, py), &cell.style(pen.inherit(&base)));
288 }
289 Position::new(x, y)
290 }
291}
292
293impl<'s, S: TextSurface + ?Sized> Bounded for Painter<'s, S> {
294 fn bounds(&self) -> Rect {
295 self.target.bounds()
296 }
297}
298
299impl<'s, S: TextSurface + ?Sized> Surface for Painter<'s, S> {
300 fn cell(&self, pos: Position) -> Option<&Cell> {
301 self.target.cell(pos)
302 }
303}
304
305impl<'s, S: TextSurface + ?Sized> SurfaceMut for Painter<'s, S> {
306 fn set_cell(&mut self, pos: Position, cell: &Cell) {
307 self.target.set_cell(pos, cell);
308 }
309
310 fn cell_mut(&mut self, pos: Position) -> Option<&mut Cell> {
311 self.target.cell_mut(pos)
312 }
313
314 fn insert_lines(&mut self, y: u16, count: u16, bounds_bottom: u16, fill: &Cell) {
315 self.target.insert_lines(y, count, bounds_bottom, fill);
316 }
317
318 fn delete_lines(&mut self, y: u16, count: u16, bounds_bottom: u16, fill: &Cell) {
319 self.target.delete_lines(y, count, bounds_bottom, fill);
320 }
321
322 fn insert_cells(&mut self, pos: Position, count: u16, bounds_right: u16, fill: &Cell) {
323 self.target.insert_cells(pos, count, bounds_right, fill);
324 }
325
326 fn delete_cells(&mut self, pos: Position, count: u16, bounds_right: u16, fill: &Cell) {
327 self.target.delete_cells(pos, count, bounds_right, fill);
328 }
329}
330
331impl<'s, S: TextSurface + ?Sized> TextSurface for Painter<'s, S> {
336 fn width_mode(&self) -> WidthMode {
337 self.mode
338 }
339
340 fn eaw_wide(&self) -> bool {
341 self.eaw_wide
342 }
343
344 fn str_width(&self, s: &str) -> u16 {
349 string_width(s.as_bytes(), self.mode, self.eaw_wide).min(u16::MAX as usize) as u16
350 }
351
352 fn set_str(&mut self, pos: impl Into<Position>, s: &str, style: impl Into<Style>) -> Position {
377 let clip = self.target.bounds();
378 self.paint(pos.into(), clip, s, WrapMode::default(), style.into())
379 }
380
381 fn set_str_wrap(
404 &mut self,
405 pos: impl Into<Position>,
406 s: &str,
407 wrap: WrapMode,
408 style: impl Into<Style>,
409 ) -> Position {
410 let clip = self.target.bounds();
411 self.paint(pos.into(), clip, s, wrap, style.into())
412 }
413
414 fn set_str_rect(
435 &mut self,
436 rect: impl Into<Rect>,
437 s: &str,
438 style: impl Into<Style>,
439 ) -> Position {
440 let rect = rect.into();
441 let clip = rect.intersection(self.target.bounds());
442 self.paint(rect.position(), clip, s, WrapMode::default(), style.into())
443 }
444
445 fn set_str_rect_wrap(
467 &mut self,
468 rect: impl Into<Rect>,
469 s: &str,
470 wrap: WrapMode,
471 style: impl Into<Style>,
472 ) -> Position {
473 let rect = rect.into();
474 let clip = rect.intersection(self.target.bounds());
475 self.paint(rect.position(), clip, s, wrap, style.into())
476 }
477
478 fn set_str_truncate(
509 &mut self,
510 pos: impl Into<Position>,
511 s: &str,
512 tail: &str,
513 tail_style: impl Into<Style>,
514 ) -> Position {
515 let clip = self.target.bounds();
516 self.paint_truncate(
517 pos.into(),
518 clip,
519 s,
520 tail,
521 tail_style.into(),
522 Style::default(),
523 )
524 }
525
526 fn set_str_rect_truncate(
549 &mut self,
550 rect: impl Into<Rect>,
551 s: &str,
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 tail,
562 tail_style.into(),
563 Style::default(),
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((0, 0), "abcdef\nghijkl", "…", Style::default());
846 assert_eq!(cell_at(&b, 3, 0).content(), "…");
847 assert_eq!(cell_at(&b, 0, 1).content(), "g");
848 assert_eq!(cell_at(&b, 3, 1).content(), "…");
849 }
850
851 #[test]
852 fn literal_truncate_resumes_on_next_row() {
853 let mut b = buf(3, 2);
854 b.set_str((0, 0), "abcdef\nxy", Style::default());
855 assert_eq!(cell_at(&b, 2, 0).content(), "c");
856 assert_eq!(cell_at(&b, 0, 1).content(), "x");
857 assert_eq!(cell_at(&b, 1, 1).content(), "y");
858 }
859
860 #[test]
861 fn cr_clears_truncation_for_the_row() {
862 let mut b = buf(3, 1);
863 Painter::new(&mut b).set_str_wrap(
864 (0, 0),
865 "abcdef\rXY",
866 WrapMode::Truncate,
867 Style::default(),
868 );
869 assert_eq!(cell_at(&b, 0, 0).content(), "X");
870 assert_eq!(cell_at(&b, 1, 0).content(), "Y");
871 assert_eq!(cell_at(&b, 2, 0).content(), "c");
872 }
873
874 #[test]
875 fn overflow_drops_rest_of_row_without_backfill() {
876 let mut b = buf(3, 1);
877 let end = Painter::new(&mut b).set_str_wrap(
878 (0, 0),
879 "ab中c",
880 WrapMode::Truncate,
881 Style::default(),
882 );
883 assert_eq!(cell_at(&b, 0, 0).content(), "a");
884 assert_eq!(cell_at(&b, 1, 0).content(), "b");
885 assert_eq!(cell_at(&b, 2, 0).content(), " ");
888 assert_eq!(end, Position::new(2, 0));
889 }
890
891 #[test]
892 fn literal_crlf_breaks_the_line() {
893 let mut b = buf(3, 2);
896 b.set_str((0, 0), "abcdef\r\nxy", Style::default());
897 assert_eq!(cell_at(&b, 2, 0).content(), "c");
898 assert_eq!(cell_at(&b, 0, 1).content(), "x");
899 assert_eq!(cell_at(&b, 1, 1).content(), "y");
900 }
901
902 #[test]
903 fn crlf_breaks_the_line() {
904 let mut b = buf(3, 2);
905 Painter::new(&mut b).set_str_wrap(
906 (0, 0),
907 "abcdef\r\nxy",
908 WrapMode::Truncate,
909 Style::default(),
910 );
911 assert_eq!(cell_at(&b, 2, 0).content(), "c");
912 assert_eq!(cell_at(&b, 0, 1).content(), "x");
913 assert_eq!(cell_at(&b, 1, 1).content(), "y");
914 }
915
916 #[test]
917 fn escapes_still_apply_across_a_truncated_row() {
918 let mut b = buf(2, 2);
921 Painter::new(&mut b).set_str_wrap(
922 (0, 0),
923 "ab\x1b[1mcd\x1b]8;;https://x\x1b\\\nz",
924 WrapMode::Truncate,
925 Style::default(),
926 );
927 assert!(!cell_at(&b, 0, 0).style.attrs.contains(AttrFlags::BOLD));
928 let z = cell_at(&b, 0, 1);
929 assert_eq!(z.content(), "z");
930 assert!(z.style.attrs.contains(AttrFlags::BOLD));
931 assert_eq!(link_of(&z.style), Some(("https://x", "")));
932 }
933
934 #[test]
935 fn start_below_clip_paints_nothing() {
936 let mut b = buf(3, 2);
937 let end = Painter::new(&mut b).set_str_wrap(
938 (0, 5),
939 "abcdef\nghi",
940 WrapMode::Truncate,
941 Style::default(),
942 );
943 assert_eq!(end, Position::new(0, 5));
944 for y in 0..2 {
945 for x in 0..3 {
946 assert_eq!(cell_at(&b, x, y).content(), " ");
947 }
948 }
949 }
950
951 #[test]
952 fn literal_start_below_clip_paints_nothing() {
953 let mut b = buf(3, 2);
954 let end = b.set_str((0, 5), "abcdef\nghi", Style::default());
955 assert_eq!(end, Position::new(0, 5));
956 assert_eq!(cell_at(&b, 0, 0).content(), " ");
957 assert_eq!(cell_at(&b, 0, 1).content(), " ");
958 }
959
960 #[test]
961 fn wrap_breaks_on_crlf() {
962 let mut b = buf(4, 3);
965 Painter::new(&mut b).set_str_wrap((0, 0), "ab\r\ncd", WrapMode::Wrap, Style::default());
966 assert_eq!(cell_at(&b, 0, 0).content(), "a");
967 assert_eq!(cell_at(&b, 1, 0).content(), "b");
968 assert_eq!(cell_at(&b, 0, 1).content(), "c");
969 assert_eq!(cell_at(&b, 1, 1).content(), "d");
970 }
971
972 #[test]
973 fn literal_wrap_breaks_on_crlf() {
974 let mut b = buf(4, 3);
975 b.set_str_wrap((0, 0), "ab\r\ncd", WrapMode::Wrap, Style::default());
976 assert_eq!(cell_at(&b, 0, 0).content(), "a");
977 assert_eq!(cell_at(&b, 1, 0).content(), "b");
978 assert_eq!(cell_at(&b, 0, 1).content(), "c");
979 assert_eq!(cell_at(&b, 1, 1).content(), "d");
980 }
981
982 #[test]
983 fn wrap_at_right_edge() {
984 let mut b = buf(3, 3);
985 let end =
986 Painter::new(&mut b).set_str_wrap((0, 0), "abcdef", WrapMode::Wrap, Style::default());
987 assert_eq!(end, Position::new(3, 1));
988 assert_eq!(cell_at(&b, 0, 0).content(), "a");
989 assert_eq!(cell_at(&b, 2, 0).content(), "c");
990 assert_eq!(cell_at(&b, 0, 1).content(), "d");
991 assert_eq!(cell_at(&b, 2, 1).content(), "f");
992 }
993
994 #[test]
995 fn rect_clip_and_origin() {
996 let mut b = buf(10, 5);
997 let end = Painter::new(&mut b).set_str_rect_wrap(
998 Rect::new(2, 1, 3, 2),
999 "abcdef",
1000 WrapMode::Wrap,
1001 Style::default(),
1002 );
1003 assert_eq!(end, Position::new(5, 2));
1004 assert_eq!(cell_at(&b, 2, 1).content(), "a");
1005 assert_eq!(cell_at(&b, 4, 1).content(), "c");
1006 assert_eq!(cell_at(&b, 2, 2).content(), "d");
1007 assert_eq!(cell_at(&b, 4, 2).content(), "f");
1008 assert_eq!(cell_at(&b, 0, 0).content(), " ");
1010 assert_eq!(cell_at(&b, 5, 1).content(), " ");
1011 }
1012
1013 #[test]
1014 fn rect_newline_uses_rect_left() {
1015 let mut b = buf(10, 5);
1016 Painter::new(&mut b).set_str_rect_wrap(
1017 Rect::new(2, 1, 4, 3),
1018 "ab\ncd",
1019 WrapMode::Truncate,
1020 Style::default(),
1021 );
1022 assert_eq!(cell_at(&b, 2, 1).content(), "a");
1023 assert_eq!(cell_at(&b, 3, 1).content(), "b");
1024 assert_eq!(cell_at(&b, 2, 2).content(), "c");
1026 assert_eq!(cell_at(&b, 3, 2).content(), "d");
1027 assert_eq!(cell_at(&b, 0, 2).content(), " ");
1028 }
1029
1030 #[test]
1031 fn with_resets_style_and_link() {
1032 let mut b = buf(10, 1);
1033 Painter::new(&mut b).set_str_wrap(
1035 (0, 0),
1036 "a",
1037 WrapMode::Truncate,
1038 Style::default().bold().link("https://x", ""),
1039 );
1040 assert!(cell_at(&b, 0, 0).style.attrs.contains(AttrFlags::BOLD));
1041 assert_eq!(link_of(&cell_at(&b, 0, 0).style), Some(("https://x", "")));
1042 Painter::new(&mut b).set_str_wrap((1, 0), "b", WrapMode::Truncate, Style::default());
1044 assert!(!cell_at(&b, 1, 0).style.attrs.contains(AttrFlags::BOLD));
1045 assert!(cell_at(&b, 1, 0).style.link.is_none());
1046 }
1047
1048 #[test]
1049 fn calls_start_from_their_own_base() {
1050 let mut b = buf(10, 1);
1051 let mut p = Painter::new(&mut b);
1052 p.set_str_wrap((0, 0), "\x1b[1ma", WrapMode::Truncate, Style::default());
1054 p.set_str_wrap((1, 0), "b", WrapMode::Truncate, Style::default());
1056 assert!(cell_at(&b, 0, 0).style.attrs.contains(AttrFlags::BOLD));
1057 assert!(!cell_at(&b, 1, 0).style.attrs.contains(AttrFlags::BOLD));
1058 }
1059
1060 #[test]
1061 fn base_applies_and_inline_reset_returns_to_base() {
1062 let mut b = buf(10, 1);
1063 let base = Style::default().fg(Color::Red);
1064 Painter::new(&mut b).set_str_wrap((0, 0), "a\x1b[1mb\x1b[0mc", WrapMode::Truncate, base);
1068 let red = Some(Color::Red);
1069 assert_eq!(cell_at(&b, 0, 0).style.fg, red);
1070 assert!(!cell_at(&b, 0, 0).style.attrs.contains(AttrFlags::BOLD));
1071 assert_eq!(cell_at(&b, 1, 0).style.fg, red);
1072 assert!(cell_at(&b, 1, 0).style.attrs.contains(AttrFlags::BOLD));
1073 assert_eq!(cell_at(&b, 2, 0).style.fg, red);
1074 assert!(!cell_at(&b, 2, 0).style.attrs.contains(AttrFlags::BOLD));
1075 }
1076
1077 #[test]
1078 fn position_and_rect_match_when_rect_covers_bounds() {
1079 let mut a = buf(5, 2);
1080 let mut b = buf(5, 2);
1081 let e1 =
1082 Painter::new(&mut a).set_str_wrap((0, 0), "abc", WrapMode::Truncate, Style::default());
1083 let e2 = Painter::new(&mut b).set_str_rect_wrap(
1084 Rect::new(0, 0, 5, 2),
1085 "abc",
1086 WrapMode::Truncate,
1087 Style::default(),
1088 );
1089 assert_eq!(e1, e2);
1090 assert_eq!(cell_at(&a, 2, 0).content(), cell_at(&b, 2, 0).content());
1091 }
1092
1093 fn row(b: &TextBuffer, y: u16) -> String {
1094 (0..b.width())
1095 .map(|x| cell_at(b, x, y).content().to_string())
1096 .collect()
1097 }
1098
1099 #[test]
1100 fn truncate_tail_not_shown_when_text_fits() {
1101 let mut b = buf(5, 1);
1102 let end = Painter::new(&mut b).set_str_truncate((0, 0), "abc", "…", Style::default());
1103 assert_eq!(row(&b, 0), "abc ");
1105 assert_eq!(end, Position::new(3, 0));
1106 }
1107
1108 #[test]
1109 fn truncate_single_cell_tail_on_overflow() {
1110 let mut b = buf(5, 1);
1111 let end = Painter::new(&mut b).set_str_truncate((0, 0), "abcdefgh", "…", Style::default());
1112 assert_eq!(row(&b, 0), "abcd…");
1114 assert_eq!(end, Position::new(5, 0));
1115 }
1116
1117 #[test]
1118 fn truncate_multi_cell_tail_reserves_its_width() {
1119 let mut b = buf(8, 1);
1120 Painter::new(&mut b).set_str_truncate((0, 0), "abcdefghij", " more", Style::default());
1121 assert_eq!(row(&b, 0), "abc more");
1123 }
1124
1125 #[test]
1126 fn truncate_tail_carries_its_style() {
1127 let mut b = buf(5, 1);
1128 Painter::new(&mut b).set_str_truncate(
1129 (0, 0),
1130 "abcdefgh",
1131 "…",
1132 Style::default().fg(Color::Red),
1133 );
1134 assert_eq!(cell_at(&b, 4, 0).style.fg, Some(Color::Red));
1136 }
1137
1138 #[test]
1139 fn truncate_tail_inline_escapes_apply() {
1140 let mut b = buf(5, 1);
1141 Painter::new(&mut b).set_str_truncate((0, 0), "abcdefgh", "\x1b[1m…", Style::default());
1142 assert!(cell_at(&b, 4, 0).style.attrs.contains(AttrFlags::BOLD));
1144 }
1145
1146 #[test]
1147 fn truncate_wide_tail_too_big_hard_truncates() {
1148 let mut b = buf(3, 1);
1149 let end =
1150 Painter::new(&mut b).set_str_truncate((0, 0), "abcdef", " more", Style::default());
1151 assert_eq!(row(&b, 0), "abc");
1154 assert_eq!(end, Position::new(3, 0));
1155 }
1156
1157 #[test]
1158 fn truncate_tail_overwrites_split_wide_cell() {
1159 let mut b = buf(5, 1);
1162 Painter::new(&mut b).set_str_truncate((0, 0), "ab中def", "…", Style::default());
1163 assert_eq!(cell_at(&b, 4, 0).content(), "…");
1167 assert!(!cell_at(&b, 3, 0).is_wide());
1168 }
1169}