1use ratatui_core::buffer::Buffer;
4use ratatui_core::layout::{Alignment, Position, Rect};
5use ratatui_core::style::{Style, Styled};
6use ratatui_core::text::{Line, StyledGrapheme, Text};
7use ratatui_core::widgets::Widget;
8use unicode_width::UnicodeWidthStr;
9
10use crate::block::{Block, BlockExt};
11use crate::reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine};
12
13#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
77pub struct Paragraph<'a> {
78 block: Option<Block<'a>>,
80 style: Style,
82 wrap: Option<Wrap>,
84 text: Text<'a>,
86 scroll: Position,
88 alignment: Alignment,
90}
91
92#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
123pub struct Wrap {
124 pub trim: bool,
126}
127
128type Horizontal = u16;
129type Vertical = u16;
130
131impl<'a> Paragraph<'a> {
132 pub fn new<T>(text: T) -> Self
151 where
152 T: Into<Text<'a>>,
153 {
154 Self {
155 block: None,
156 style: Style::default(),
157 wrap: None,
158 text: text.into(),
159 scroll: Position::ORIGIN,
160 alignment: Alignment::Left,
161 }
162 }
163
164 #[must_use = "method moves the value of self and returns the modified value"]
174 pub fn block(mut self, block: Block<'a>) -> Self {
175 self.block = Some(block);
176 self
177 }
178
179 #[must_use = "method moves the value of self and returns the modified value"]
198 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
199 self.style = style.into();
200 self
201 }
202
203 #[must_use = "method moves the value of self and returns the modified value"]
215 pub const fn wrap(mut self, wrap: Wrap) -> Self {
216 self.wrap = Some(wrap);
217 self
218 }
219
220 #[must_use = "method moves the value of self and returns the modified value"]
232 pub const fn scroll(mut self, offset: (Vertical, Horizontal)) -> Self {
233 self.scroll = Position {
234 x: offset.1,
235 y: offset.0,
236 };
237 self
238 }
239
240 #[must_use = "method moves the value of self and returns the modified value"]
254 pub const fn alignment(mut self, alignment: Alignment) -> Self {
255 self.alignment = alignment;
256 self
257 }
258
259 #[must_use = "method moves the value of self and returns the modified value"]
271 pub const fn left_aligned(self) -> Self {
272 self.alignment(Alignment::Left)
273 }
274
275 #[must_use = "method moves the value of self and returns the modified value"]
287 pub const fn centered(self) -> Self {
288 self.alignment(Alignment::Center)
289 }
290
291 #[must_use = "method moves the value of self and returns the modified value"]
303 pub const fn right_aligned(self) -> Self {
304 self.alignment(Alignment::Right)
305 }
306
307 #[instability::unstable(
328 feature = "rendered-line-info",
329 issue = "https://github.com/ratatui/ratatui/issues/293"
330 )]
331 pub fn line_count(&self, width: u16) -> usize {
332 if width < 1 {
333 return 0;
334 }
335
336 let (top, bottom) = self
337 .block
338 .as_ref()
339 .map(Block::vertical_space)
340 .unwrap_or_default();
341
342 let count = if let Some(Wrap { trim }) = self.wrap {
343 let styled = self.text.iter().map(|line| {
344 let graphemes = line
345 .spans
346 .iter()
347 .flat_map(|span| span.styled_graphemes(self.style));
348 let alignment = line.alignment.unwrap_or(self.alignment);
349 (graphemes, alignment)
350 });
351 let mut line_composer = WordWrapper::new(styled, width, trim);
352 let mut count = 0;
353 while line_composer.next_line().is_some() {
354 count += 1;
355 }
356 count
357 } else {
358 self.text.height()
359 };
360
361 count
362 .saturating_add(top as usize)
363 .saturating_add(bottom as usize)
364 }
365
366 #[instability::unstable(
384 feature = "rendered-line-info",
385 issue = "https://github.com/ratatui/ratatui/issues/293"
386 )]
387 pub fn line_width(&self) -> usize {
388 let width = self.text.iter().map(Line::width).max().unwrap_or_default();
389 let (left, right) = self
390 .block
391 .as_ref()
392 .map(Block::horizontal_space)
393 .unwrap_or_default();
394
395 width
396 .saturating_add(left as usize)
397 .saturating_add(right as usize)
398 }
399}
400
401impl Widget for Paragraph<'_> {
402 fn render(self, area: Rect, buf: &mut Buffer) {
403 Widget::render(&self, area, buf);
404 }
405}
406
407impl Widget for &Paragraph<'_> {
408 fn render(self, area: Rect, buf: &mut Buffer) {
409 let area = area.intersection(buf.area);
410 buf.set_style(area, self.style);
411 self.block.as_ref().render(area, buf);
412 let inner = self.block.inner_if_some(area);
413 self.render_paragraph(inner, buf);
414 }
415}
416
417impl Paragraph<'_> {
418 fn render_paragraph(&self, text_area: Rect, buf: &mut Buffer) {
419 if text_area.is_empty() {
420 return;
421 }
422
423 buf.set_style(text_area, self.style);
424 let styled = self.text.iter().map(|line| {
425 let graphemes = line.styled_graphemes(self.text.style);
426 let alignment = line.alignment.unwrap_or(self.alignment);
427 (graphemes, alignment)
428 });
429
430 if let Some(Wrap { trim }) = self.wrap {
431 let mut line_composer = WordWrapper::new(styled, text_area.width, trim);
432 for _ in 0..self.scroll.y {
434 if line_composer.next_line().is_none() {
435 return;
436 }
437 }
438 render_lines(line_composer, text_area, buf);
439 } else {
440 let lines = styled.skip(self.scroll.y as usize);
442 let mut line_composer = LineTruncator::new(lines, text_area.width);
443 line_composer.set_horizontal_offset(self.scroll.x);
444 render_lines(line_composer, text_area, buf);
445 }
446 }
447}
448
449fn render_lines<'a, C: LineComposer<'a>>(mut composer: C, area: Rect, buf: &mut Buffer) {
450 let mut y = 0;
451 while let Some(ref wrapped) = composer.next_line() {
452 render_line(wrapped, area, buf, y);
453 y += 1;
454 if y >= area.height {
455 break;
456 }
457 }
458}
459
460fn render_line(wrapped: &WrappedLine<'_, '_>, area: Rect, buf: &mut Buffer, y: u16) {
461 let mut x = get_line_offset(wrapped.width, area.width, wrapped.alignment);
462 for StyledGrapheme { symbol, style } in wrapped.graphemes {
463 let width = symbol.width();
464 if width == 0 {
465 continue;
466 }
467 let symbol = if symbol.is_empty() { " " } else { symbol };
469 let position = Position::new(area.left() + x, area.top() + y);
470 buf[position].set_symbol(symbol).set_style(*style);
471 x += u16::try_from(width).unwrap_or(u16::MAX);
472 }
473}
474
475const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
476 match alignment {
477 Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
478 Alignment::Right => text_area_width.saturating_sub(line_width),
479 Alignment::Left => 0,
480 }
481}
482
483impl Styled for Paragraph<'_> {
484 type Item = Self;
485
486 fn style(&self) -> Style {
487 self.style
488 }
489
490 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
491 self.style(style)
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use alloc::vec;
498
499 use ratatui_core::buffer::Buffer;
500 use ratatui_core::layout::{Alignment, Rect};
501 use ratatui_core::style::{Color, Modifier, Style, Stylize};
502 use ratatui_core::text::{Line, Span, Text};
503 use ratatui_core::widgets::Widget;
504 use rstest::rstest;
505
506 use super::*;
507 use crate::block::TitlePosition;
508 use crate::borders::Borders;
509
510 #[track_caller]
515 fn test_case(paragraph: &Paragraph, expected: &Buffer) {
516 let mut buffer = Buffer::empty(Rect::new(0, 0, expected.area.width, expected.area.height));
517 paragraph.render(buffer.area, &mut buffer);
518 assert_eq!(buffer, *expected);
519 }
520
521 #[test]
522 fn zero_width_char_at_end_of_line() {
523 let line = "foo\u{200B}";
524 for paragraph in [
525 Paragraph::new(line),
526 Paragraph::new(line).wrap(Wrap { trim: false }),
527 Paragraph::new(line).wrap(Wrap { trim: true }),
528 ] {
529 test_case(¶graph, &Buffer::with_lines(["foo"]));
530 test_case(¶graph, &Buffer::with_lines(["foo "]));
531 test_case(¶graph, &Buffer::with_lines(["foo ", " "]));
532 test_case(¶graph, &Buffer::with_lines(["foo", " "]));
533 }
534 }
535
536 #[test]
537 fn test_render_empty_paragraph() {
538 for paragraph in [
539 Paragraph::new(""),
540 Paragraph::new("").wrap(Wrap { trim: false }),
541 Paragraph::new("").wrap(Wrap { trim: true }),
542 ] {
543 test_case(¶graph, &Buffer::with_lines([" "]));
544 test_case(¶graph, &Buffer::with_lines([" "]));
545 test_case(¶graph, &Buffer::with_lines([" "; 10]));
546 test_case(¶graph, &Buffer::with_lines([" ", " "]));
547 }
548 }
549
550 #[test]
551 fn test_render_single_line_paragraph() {
552 let text = "Hello, world!";
553 for paragraph in [
554 Paragraph::new(text),
555 Paragraph::new(text).wrap(Wrap { trim: false }),
556 Paragraph::new(text).wrap(Wrap { trim: true }),
557 ] {
558 test_case(¶graph, &Buffer::with_lines(["Hello, world! "]));
559 test_case(¶graph, &Buffer::with_lines(["Hello, world!"]));
560 test_case(
561 ¶graph,
562 &Buffer::with_lines(["Hello, world! ", " "]),
563 );
564 test_case(
565 ¶graph,
566 &Buffer::with_lines(["Hello, world!", " "]),
567 );
568 }
569 }
570
571 #[test]
572 fn test_render_multi_line_paragraph() {
573 let text = "This is a\nmultiline\nparagraph.";
574 for paragraph in [
575 Paragraph::new(text),
576 Paragraph::new(text).wrap(Wrap { trim: false }),
577 Paragraph::new(text).wrap(Wrap { trim: true }),
578 ] {
579 test_case(
580 ¶graph,
581 &Buffer::with_lines(["This is a ", "multiline ", "paragraph."]),
582 );
583 test_case(
584 ¶graph,
585 &Buffer::with_lines(["This is a ", "multiline ", "paragraph. "]),
586 );
587 test_case(
588 ¶graph,
589 &Buffer::with_lines([
590 "This is a ",
591 "multiline ",
592 "paragraph. ",
593 " ",
594 " ",
595 ]),
596 );
597 }
598 }
599
600 #[test]
601 fn test_render_paragraph_with_block() {
602 let text = "Hello, worlds!";
605 let truncated_paragraph = Paragraph::new(text).block(Block::bordered().title("Title"));
606 let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
607 let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
608
609 for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
610 #[rustfmt::skip]
611 test_case(
612 paragraph,
613 &Buffer::with_lines([
614 "┌Title─────────┐",
615 "│Hello, worlds!│",
616 "└──────────────┘",
617 ]),
618 );
619 test_case(
620 paragraph,
621 &Buffer::with_lines([
622 "┌Title───────────┐",
623 "│Hello, worlds! │",
624 "└────────────────┘",
625 ]),
626 );
627 test_case(
628 paragraph,
629 &Buffer::with_lines([
630 "┌Title────────────┐",
631 "│Hello, worlds! │",
632 "│ │",
633 "└─────────────────┘",
634 ]),
635 );
636 }
637
638 test_case(
639 &truncated_paragraph,
640 &Buffer::with_lines([
641 "┌Title───────┐",
642 "│Hello, world│",
643 "│ │",
644 "└────────────┘",
645 ]),
646 );
647 test_case(
648 &wrapped_paragraph,
649 &Buffer::with_lines([
650 "┌Title──────┐",
651 "│Hello, │",
652 "│worlds! │",
653 "└───────────┘",
654 ]),
655 );
656 test_case(
657 &trimmed_paragraph,
658 &Buffer::with_lines([
659 "┌Title──────┐",
660 "│Hello, │",
661 "│worlds! │",
662 "└───────────┘",
663 ]),
664 );
665 }
666
667 #[test]
668 fn test_render_line_styled() {
669 let l0 = Line::raw("unformatted");
670 let l1 = Line::styled("bold text", Style::new().bold());
671 let l2 = Line::styled("cyan text", Style::new().cyan());
672 let l3 = Line::styled("dim text", Style::new().dim());
673 let paragraph = Paragraph::new(vec![l0, l1, l2, l3]);
674
675 let mut expected =
676 Buffer::with_lines(["unformatted", "bold text", "cyan text", "dim text"]);
677 expected.set_style(Rect::new(0, 1, 9, 1), Style::new().bold());
678 expected.set_style(Rect::new(0, 2, 9, 1), Style::new().cyan());
679 expected.set_style(Rect::new(0, 3, 8, 1), Style::new().dim());
680
681 test_case(¶graph, &expected);
682 }
683
684 #[test]
685 fn test_render_line_spans_styled() {
686 let l0 = Line::default().spans([
687 Span::styled("bold", Style::new().bold()),
688 Span::raw(" and "),
689 Span::styled("cyan", Style::new().cyan()),
690 ]);
691 let l1 = Line::default().spans([Span::raw("unformatted")]);
692 let paragraph = Paragraph::new(vec![l0, l1]);
693
694 let mut expected = Buffer::with_lines(["bold and cyan", "unformatted"]);
695 expected.set_style(Rect::new(0, 0, 4, 1), Style::new().bold());
696 expected.set_style(Rect::new(9, 0, 4, 1), Style::new().cyan());
697
698 test_case(¶graph, &expected);
699 }
700
701 #[test]
702 fn test_render_paragraph_with_block_with_bottom_title_and_border() {
703 let block = Block::new()
704 .borders(Borders::BOTTOM)
705 .title_position(TitlePosition::Bottom)
706 .title("Title");
707 let paragraph = Paragraph::new("Hello, world!").block(block);
708 test_case(
709 ¶graph,
710 &Buffer::with_lines(["Hello, world! ", "Title──────────"]),
711 );
712 }
713
714 #[test]
715 fn test_render_paragraph_with_word_wrap() {
716 let text = "This is a long line of text that should wrap and contains a superultramegagigalong word.";
717 let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
718 let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
719
720 test_case(
721 &wrapped_paragraph,
722 &Buffer::with_lines([
723 "This is a long line",
724 "of text that should",
725 "wrap and ",
726 "contains a ",
727 "superultramegagigal",
728 "ong word. ",
729 ]),
730 );
731 test_case(
732 &wrapped_paragraph,
733 &Buffer::with_lines([
734 "This is a ",
735 "long line of",
736 "text that ",
737 "should wrap ",
738 " and ",
739 "contains a ",
740 "superultrame",
741 "gagigalong ",
742 "word. ",
743 ]),
744 );
745
746 test_case(
747 &trimmed_paragraph,
748 &Buffer::with_lines([
749 "This is a long line",
750 "of text that should",
751 "wrap and ",
752 "contains a ",
753 "superultramegagigal",
754 "ong word. ",
755 ]),
756 );
757 test_case(
758 &trimmed_paragraph,
759 &Buffer::with_lines([
760 "This is a ",
761 "long line of",
762 "text that ",
763 "should wrap ",
764 "and contains",
765 "a ",
766 "superultrame",
767 "gagigalong ",
768 "word. ",
769 ]),
770 );
771 }
772
773 #[test]
774 fn test_render_wrapped_paragraph_with_whitespace_only_line() {
775 let text: Text = ["A", " ", "B", " a", "C"]
776 .into_iter()
777 .map(Line::from)
778 .collect();
779 let paragraph = Paragraph::new(text.clone()).wrap(Wrap { trim: false });
780 let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
781
782 test_case(
783 ¶graph,
784 &Buffer::with_lines(["A", " ", "B", " a", "C"]),
785 );
786 test_case(
787 &trimmed_paragraph,
788 &Buffer::with_lines(["A", "", "B", "a", "C"]),
789 );
790 }
791
792 #[test]
793 fn test_render_paragraph_with_line_truncation() {
794 let text = "This is a long line of text that should be truncated.";
795 let truncated_paragraph = Paragraph::new(text);
796
797 test_case(
798 &truncated_paragraph,
799 &Buffer::with_lines(["This is a long line of"]),
800 );
801 test_case(
802 &truncated_paragraph,
803 &Buffer::with_lines(["This is a long line of te"]),
804 );
805 test_case(
806 &truncated_paragraph,
807 &Buffer::with_lines(["This is a long line of "]),
808 );
809 test_case(
810 &truncated_paragraph.clone().scroll((0, 2)),
811 &Buffer::with_lines(["is is a long line of te"]),
812 );
813 }
814
815 #[test]
816 fn test_render_paragraph_with_left_alignment() {
817 let text = "Hello, world!";
818 let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Left);
819 let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
820 let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
821
822 for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
823 test_case(paragraph, &Buffer::with_lines(["Hello, world! "]));
824 test_case(paragraph, &Buffer::with_lines(["Hello, world!"]));
825 }
826
827 test_case(&truncated_paragraph, &Buffer::with_lines(["Hello, wor"]));
828 test_case(
829 &wrapped_paragraph,
830 &Buffer::with_lines(["Hello, ", "world! "]),
831 );
832 test_case(
833 &trimmed_paragraph,
834 &Buffer::with_lines(["Hello, ", "world! "]),
835 );
836 }
837
838 #[test]
839 fn test_render_paragraph_with_center_alignment() {
840 let text = "Hello, world!";
841 let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Center);
842 let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
843 let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
844
845 for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
846 test_case(paragraph, &Buffer::with_lines([" Hello, world! "]));
847 test_case(paragraph, &Buffer::with_lines([" Hello, world! "]));
848 test_case(paragraph, &Buffer::with_lines([" Hello, world! "]));
849 test_case(paragraph, &Buffer::with_lines(["Hello, world!"]));
850 }
851
852 test_case(&truncated_paragraph, &Buffer::with_lines(["Hello, wor"]));
853 test_case(
854 &wrapped_paragraph,
855 &Buffer::with_lines([" Hello, ", " world! "]),
856 );
857 test_case(
858 &trimmed_paragraph,
859 &Buffer::with_lines([" Hello, ", " world! "]),
860 );
861 }
862
863 #[test]
864 fn test_render_paragraph_with_right_alignment() {
865 let text = "Hello, world!";
866 let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Right);
867 let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
868 let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
869
870 for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
871 test_case(paragraph, &Buffer::with_lines([" Hello, world!"]));
872 test_case(paragraph, &Buffer::with_lines(["Hello, world!"]));
873 }
874
875 test_case(&truncated_paragraph, &Buffer::with_lines(["Hello, wor"]));
876 test_case(
877 &wrapped_paragraph,
878 &Buffer::with_lines([" Hello,", " world!"]),
879 );
880 test_case(
881 &trimmed_paragraph,
882 &Buffer::with_lines([" Hello,", " world!"]),
883 );
884 }
885
886 #[test]
887 fn test_render_paragraph_with_scroll_offset() {
888 let text = "This is a\ncool\nmultiline\nparagraph.";
889 let truncated_paragraph = Paragraph::new(text).scroll((2, 0));
890 let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
891 let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
892
893 for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
894 test_case(
895 paragraph,
896 &Buffer::with_lines(["multiline ", "paragraph. ", " "]),
897 );
898 test_case(paragraph, &Buffer::with_lines(["multiline "]));
899 }
900
901 test_case(
902 &truncated_paragraph.clone().scroll((2, 4)),
903 &Buffer::with_lines(["iline ", "graph. "]),
904 );
905 test_case(
906 &wrapped_paragraph,
907 &Buffer::with_lines(["cool ", "multili", "ne "]),
908 );
909 }
910
911 #[test]
912 fn test_render_paragraph_with_zero_width_area() {
913 let text = "Hello, world!";
914 let area = Rect::new(0, 0, 0, 3);
915
916 for paragraph in [
917 Paragraph::new(text),
918 Paragraph::new(text).wrap(Wrap { trim: false }),
919 Paragraph::new(text).wrap(Wrap { trim: true }),
920 ] {
921 test_case(¶graph, &Buffer::empty(area));
922 test_case(¶graph.clone().scroll((2, 4)), &Buffer::empty(area));
923 }
924 }
925
926 #[test]
927 fn test_render_paragraph_with_zero_height_area() {
928 let text = "Hello, world!";
929 let area = Rect::new(0, 0, 10, 0);
930
931 for paragraph in [
932 Paragraph::new(text),
933 Paragraph::new(text).wrap(Wrap { trim: false }),
934 Paragraph::new(text).wrap(Wrap { trim: true }),
935 ] {
936 test_case(¶graph, &Buffer::empty(area));
937 test_case(¶graph.clone().scroll((2, 4)), &Buffer::empty(area));
938 }
939 }
940
941 #[test]
942 fn test_render_paragraph_with_styled_text() {
943 let text = Line::from(vec![
944 Span::styled("Hello, ", Style::default().fg(Color::Red)),
945 Span::styled("world!", Style::default().fg(Color::Blue)),
946 ]);
947
948 let mut expected_buffer = Buffer::with_lines(["Hello, world!"]);
949 expected_buffer.set_style(
950 Rect::new(0, 0, 7, 1),
951 Style::default().fg(Color::Red).bg(Color::Green),
952 );
953 expected_buffer.set_style(
954 Rect::new(7, 0, 6, 1),
955 Style::default().fg(Color::Blue).bg(Color::Green),
956 );
957
958 for paragraph in [
959 Paragraph::new(text.clone()),
960 Paragraph::new(text.clone()).wrap(Wrap { trim: false }),
961 Paragraph::new(text.clone()).wrap(Wrap { trim: true }),
962 ] {
963 test_case(
964 ¶graph.style(Style::default().bg(Color::Green)),
965 &expected_buffer,
966 );
967 }
968 }
969
970 #[test]
971 fn test_render_paragraph_with_special_characters() {
972 let text = "Hello, <world>!";
973 for paragraph in [
974 Paragraph::new(text),
975 Paragraph::new(text).wrap(Wrap { trim: false }),
976 Paragraph::new(text).wrap(Wrap { trim: true }),
977 ] {
978 test_case(¶graph, &Buffer::with_lines(["Hello, <world>!"]));
979 test_case(¶graph, &Buffer::with_lines(["Hello, <world>! "]));
980 test_case(
981 ¶graph,
982 &Buffer::with_lines(["Hello, <world>! ", " "]),
983 );
984 test_case(
985 ¶graph,
986 &Buffer::with_lines(["Hello, <world>!", " "]),
987 );
988 }
989 }
990
991 #[test]
992 fn test_render_paragraph_with_unicode_characters() {
993 let text = "こんにちは, 世界! 😃";
994 let truncated_paragraph = Paragraph::new(text);
995 let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
996 let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
997
998 for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
999 test_case(paragraph, &Buffer::with_lines(["こんにちは, 世界! 😃"]));
1000 test_case(
1001 paragraph,
1002 &Buffer::with_lines(["こんにちは, 世界! 😃 "]),
1003 );
1004 }
1005
1006 test_case(
1007 &truncated_paragraph,
1008 &Buffer::with_lines(["こんにちは, 世 "]),
1009 );
1010 test_case(
1011 &wrapped_paragraph,
1012 &Buffer::with_lines(["こんにちは, ", "世界! 😃 "]),
1013 );
1014 test_case(
1015 &trimmed_paragraph,
1016 &Buffer::with_lines(["こんにちは, ", "世界! 😃 "]),
1017 );
1018 }
1019
1020 #[test]
1021 fn can_be_stylized() {
1022 assert_eq!(
1023 Paragraph::new("").black().on_white().bold().not_dim().style,
1024 Style::default()
1025 .fg(Color::Black)
1026 .bg(Color::White)
1027 .add_modifier(Modifier::BOLD)
1028 .remove_modifier(Modifier::DIM)
1029 );
1030 }
1031
1032 #[test]
1033 fn widgets_paragraph_count_rendered_lines() {
1034 let paragraph = Paragraph::new("Hello World");
1035 assert_eq!(paragraph.line_count(20), 1);
1036 assert_eq!(paragraph.line_count(10), 1);
1037 let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false });
1038 assert_eq!(paragraph.line_count(20), 1);
1039 assert_eq!(paragraph.line_count(10), 2);
1040 let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true });
1041 assert_eq!(paragraph.line_count(20), 1);
1042 assert_eq!(paragraph.line_count(10), 2);
1043
1044 let text = "Hello World ".repeat(100);
1045 let paragraph = Paragraph::new(text.trim());
1046 assert_eq!(paragraph.line_count(11), 1);
1047 assert_eq!(paragraph.line_count(6), 1);
1048 let paragraph = paragraph.wrap(Wrap { trim: false });
1049 assert_eq!(paragraph.line_count(11), 100);
1050 assert_eq!(paragraph.line_count(6), 200);
1051 let paragraph = paragraph.wrap(Wrap { trim: true });
1052 assert_eq!(paragraph.line_count(11), 100);
1053 assert_eq!(paragraph.line_count(6), 200);
1054 }
1055
1056 #[test]
1057 fn widgets_paragraph_rendered_line_count_accounts_block() {
1058 let block = Block::new();
1059 let paragraph = Paragraph::new("Hello World").block(block);
1060 assert_eq!(paragraph.line_count(20), 1);
1061 assert_eq!(paragraph.line_count(10), 1);
1062
1063 let block = Block::new().borders(Borders::TOP);
1064 let paragraph = paragraph.block(block);
1065 assert_eq!(paragraph.line_count(20), 2);
1066 assert_eq!(paragraph.line_count(10), 2);
1067
1068 let block = Block::new().borders(Borders::BOTTOM);
1069 let paragraph = paragraph.block(block);
1070 assert_eq!(paragraph.line_count(20), 2);
1071 assert_eq!(paragraph.line_count(10), 2);
1072
1073 let block = Block::new().borders(Borders::TOP | Borders::BOTTOM);
1074 let paragraph = paragraph.block(block);
1075 assert_eq!(paragraph.line_count(20), 3);
1076 assert_eq!(paragraph.line_count(10), 3);
1077
1078 let block = Block::bordered();
1079 let paragraph = paragraph.block(block);
1080 assert_eq!(paragraph.line_count(20), 3);
1081 assert_eq!(paragraph.line_count(10), 3);
1082
1083 let block = Block::bordered();
1084 let paragraph = paragraph.block(block).wrap(Wrap { trim: true });
1085 assert_eq!(paragraph.line_count(20), 3);
1086 assert_eq!(paragraph.line_count(10), 4);
1087
1088 let block = Block::bordered();
1089 let paragraph = paragraph.block(block).wrap(Wrap { trim: false });
1090 assert_eq!(paragraph.line_count(20), 3);
1091 assert_eq!(paragraph.line_count(10), 4);
1092
1093 let text = "Hello World ".repeat(100);
1094 let block = Block::new();
1095 let paragraph = Paragraph::new(text.trim()).block(block);
1096 assert_eq!(paragraph.line_count(11), 1);
1097
1098 let block = Block::bordered();
1099 let paragraph = paragraph.block(block);
1100 assert_eq!(paragraph.line_count(11), 3);
1101 assert_eq!(paragraph.line_count(6), 3);
1102
1103 let block = Block::new().borders(Borders::TOP);
1104 let paragraph = paragraph.block(block);
1105 assert_eq!(paragraph.line_count(11), 2);
1106 assert_eq!(paragraph.line_count(6), 2);
1107
1108 let block = Block::new().borders(Borders::BOTTOM);
1109 let paragraph = paragraph.block(block);
1110 assert_eq!(paragraph.line_count(11), 2);
1111 assert_eq!(paragraph.line_count(6), 2);
1112
1113 let block = Block::new().borders(Borders::LEFT | Borders::RIGHT);
1114 let paragraph = paragraph.block(block);
1115 assert_eq!(paragraph.line_count(11), 1);
1116 assert_eq!(paragraph.line_count(6), 1);
1117 }
1118
1119 #[test]
1120 fn widgets_paragraph_line_width() {
1121 let paragraph = Paragraph::new("Hello World");
1122 assert_eq!(paragraph.line_width(), 11);
1123 let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false });
1124 assert_eq!(paragraph.line_width(), 11);
1125 let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true });
1126 assert_eq!(paragraph.line_width(), 11);
1127
1128 let text = "Hello World ".repeat(100);
1129 let paragraph = Paragraph::new(text);
1130 assert_eq!(paragraph.line_width(), 1200);
1131 let paragraph = paragraph.wrap(Wrap { trim: false });
1132 assert_eq!(paragraph.line_width(), 1200);
1133 let paragraph = paragraph.wrap(Wrap { trim: true });
1134 assert_eq!(paragraph.line_width(), 1200);
1135 }
1136
1137 #[test]
1138 fn widgets_paragraph_line_width_accounts_for_block() {
1139 let block = Block::bordered();
1140 let paragraph = Paragraph::new("Hello World").block(block);
1141 assert_eq!(paragraph.line_width(), 13);
1142
1143 let block = Block::new().borders(Borders::LEFT);
1144 let paragraph = Paragraph::new("Hello World").block(block);
1145 assert_eq!(paragraph.line_width(), 12);
1146
1147 let block = Block::new().borders(Borders::LEFT);
1148 let paragraph = Paragraph::new("Hello World")
1149 .block(block)
1150 .wrap(Wrap { trim: true });
1151 assert_eq!(paragraph.line_width(), 12);
1152
1153 let block = Block::new().borders(Borders::LEFT);
1154 let paragraph = Paragraph::new("Hello World")
1155 .block(block)
1156 .wrap(Wrap { trim: false });
1157 assert_eq!(paragraph.line_width(), 12);
1158 }
1159
1160 #[test]
1161 fn left_aligned() {
1162 let p = Paragraph::new("Hello, world!").left_aligned();
1163 assert_eq!(p.alignment, Alignment::Left);
1164 }
1165
1166 #[test]
1167 fn centered() {
1168 let p = Paragraph::new("Hello, world!").centered();
1169 assert_eq!(p.alignment, Alignment::Center);
1170 }
1171
1172 #[test]
1173 fn right_aligned() {
1174 let p = Paragraph::new("Hello, world!").right_aligned();
1175 assert_eq!(p.alignment, Alignment::Right);
1176 }
1177
1178 #[test]
1183 fn paragraph_block_text_style() {
1184 let text = Text::styled("Styled text", Color::Green);
1185 let paragraph = Paragraph::new(text).block(Block::bordered());
1186
1187 let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
1188 paragraph.render(Rect::new(0, 0, 20, 3), &mut buf);
1189
1190 let mut expected = Buffer::with_lines([
1191 "┌──────────────────┐",
1192 "│Styled text │",
1193 "└──────────────────┘",
1194 ]);
1195 expected.set_style(Rect::new(1, 1, 11, 1), Style::default().fg(Color::Green));
1196 assert_eq!(buf, expected);
1197 }
1198
1199 #[rstest]
1200 #[case::bottom(Rect::new(0, 5, 15, 1))]
1201 #[case::right(Rect::new(20, 0, 15, 1))]
1202 #[case::bottom_right(Rect::new(20, 5, 15, 1))]
1203 fn test_render_paragraph_out_of_bounds(#[case] area: Rect) {
1204 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1205 Paragraph::new("Beyond the pale").render(area, &mut buffer);
1206 assert_eq!(buffer, Buffer::with_lines(vec![" "; 3]));
1207 }
1208
1209 #[test]
1210 fn partial_out_of_bounds() {
1211 let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
1212 Paragraph::new("Hello World").render(Rect::new(10, 0, 10, 3), &mut buffer);
1213 assert_eq!(
1214 buffer,
1215 Buffer::with_lines(vec![
1216 " Hello",
1217 " ",
1218 " ",
1219 ])
1220 );
1221 }
1222
1223 #[test]
1224 fn render_in_minimal_buffer() {
1225 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
1226 let paragraph = Paragraph::new("Lorem ipsum");
1227 paragraph.render(buffer.area, &mut buffer);
1229 assert_eq!(buffer, Buffer::with_lines(["L"]));
1230 }
1231
1232 #[test]
1233 fn render_in_zero_size_buffer() {
1234 let mut buffer = Buffer::empty(Rect::ZERO);
1235 let paragraph = Paragraph::new("Lorem ipsum");
1236 paragraph.render(buffer.area, &mut buffer);
1238 }
1239}