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