ratatui/widgets/
paragraph.rs

1use unicode_width::UnicodeWidthStr;
2
3use crate::{
4    buffer::Buffer,
5    layout::{Alignment, Position, Rect},
6    style::{Style, Styled},
7    text::{Line, StyledGrapheme, Text},
8    widgets::{
9        block::BlockExt,
10        reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
11        Block, Widget, WidgetRef,
12    },
13};
14
15const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
16    match alignment {
17        Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
18        Alignment::Right => text_area_width.saturating_sub(line_width),
19        Alignment::Left => 0,
20    }
21}
22
23/// A widget to display some text.
24///
25/// It is used to display a block of text. The text can be styled and aligned. It can also be
26/// wrapped to the next line if it is too long to fit in the given area.
27///
28/// The text can be any type that can be converted into a [`Text`]. By default, the text is styled
29/// with [`Style::default()`], not wrapped, and aligned to the left.
30///
31/// The text can be wrapped to the next line if it is too long to fit in the given area. The
32/// wrapping can be configured with the [`wrap`] method. For more complex wrapping, consider using
33/// the [Textwrap crate].
34///
35/// The text can be aligned to the left, right, or center. The alignment can be configured with the
36/// [`alignment`] method or with the [`left_aligned`], [`right_aligned`], and [`centered`] methods.
37///
38/// The text can be scrolled to show a specific part of the text. The scroll offset can be set with
39/// the [`scroll`] method.
40///
41/// The text can be surrounded by a [`Block`] with a title and borders. The block can be configured
42/// with the [`block`] method.
43///
44/// The style of the text can be set with the [`style`] method. This style will be applied to the
45/// entire widget, including the block if one is present. Any style set on the block or text will be
46/// added to this style. See the [`Style`] type for more information on how styles are combined.
47///
48/// Note: If neither wrapping or a block is needed, consider rendering the [`Text`], [`Line`], or
49/// [`Span`] widgets directly.
50///
51/// [Textwrap crate]: https://crates.io/crates/textwrap
52/// [`wrap`]: Self::wrap
53/// [`alignment`]: Self::alignment
54/// [`left_aligned`]: Self::left_aligned
55/// [`right_aligned`]: Self::right_aligned
56/// [`centered`]: Self::centered
57/// [`scroll`]: Self::scroll
58/// [`block`]: Self::block
59/// [`style`]: Self::style
60///
61/// # Example
62///
63/// ```
64/// use ratatui::{
65///     layout::Alignment,
66///     style::{Style, Stylize},
67///     text::{Line, Span},
68///     widgets::{Block, Paragraph, Wrap},
69/// };
70///
71/// let text = vec![
72///     Line::from(vec![
73///         Span::raw("First"),
74///         Span::styled("line", Style::new().green().italic()),
75///         ".".into(),
76///     ]),
77///     Line::from("Second line".red()),
78///     "Third line".into(),
79/// ];
80/// Paragraph::new(text)
81///     .block(Block::bordered().title("Paragraph"))
82///     .style(Style::new().white().on_black())
83///     .alignment(Alignment::Center)
84///     .wrap(Wrap { trim: true });
85/// ```
86///
87/// [`Span`]: crate::text::Span
88#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
89pub struct Paragraph<'a> {
90    /// A block to wrap the widget in
91    block: Option<Block<'a>>,
92    /// Widget style
93    style: Style,
94    /// How to wrap the text
95    wrap: Option<Wrap>,
96    /// The text to display
97    text: Text<'a>,
98    /// Scroll
99    scroll: Position,
100    /// Alignment of the text
101    alignment: Alignment,
102}
103
104/// Describes how to wrap text across lines.
105///
106/// ## Examples
107///
108/// ```
109/// use ratatui::{
110///     text::Text,
111///     widgets::{Paragraph, Wrap},
112/// };
113///
114/// let bullet_points = Text::from(
115///     r#"Some indented points:
116///     - First thing goes here and is long so that it wraps
117///     - Here is another point that is long enough to wrap"#,
118/// );
119///
120/// // With leading spaces trimmed (window width of 30 chars):
121/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
122/// // Some indented points:
123/// // - First thing goes here and is
124/// // long so that it wraps
125/// // - Here is another point that
126/// // is long enough to wrap
127///
128/// // But without trimming, indentation is preserved:
129/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
130/// // Some indented points:
131/// //     - First thing goes here
132/// // and is long so that it wraps
133/// //     - Here is another point
134/// // that is long enough to wrap
135/// ```
136#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
137pub struct Wrap {
138    /// Should leading whitespace be trimmed
139    pub trim: bool,
140}
141
142type Horizontal = u16;
143type Vertical = u16;
144
145impl<'a> Paragraph<'a> {
146    /// Creates a new [`Paragraph`] widget with the given text.
147    ///
148    /// The `text` parameter can be a [`Text`] or any type that can be converted into a [`Text`]. By
149    /// default, the text is styled with [`Style::default()`], not wrapped, and aligned to the left.
150    ///
151    /// # Examples
152    ///
153    /// ```rust
154    /// use ratatui::{
155    ///     style::{Style, Stylize},
156    ///     text::{Line, Text},
157    ///     widgets::Paragraph,
158    /// };
159    ///
160    /// let paragraph = Paragraph::new("Hello, world!");
161    /// let paragraph = Paragraph::new(String::from("Hello, world!"));
162    /// let paragraph = Paragraph::new(Text::raw("Hello, world!"));
163    /// let paragraph = Paragraph::new(Text::styled("Hello, world!", Style::default()));
164    /// let paragraph = Paragraph::new(Line::from(vec!["Hello, ".into(), "world!".red()]));
165    /// ```
166    pub fn new<T>(text: T) -> Self
167    where
168        T: Into<Text<'a>>,
169    {
170        Self {
171            block: None,
172            style: Style::default(),
173            wrap: None,
174            text: text.into(),
175            scroll: Position::ORIGIN,
176            alignment: Alignment::Left,
177        }
178    }
179
180    /// Surrounds the [`Paragraph`] widget with a [`Block`].
181    ///
182    /// # Example
183    ///
184    /// ```rust
185    /// use ratatui::widgets::{Block, Paragraph};
186    ///
187    /// let paragraph = Paragraph::new("Hello, world!").block(Block::bordered().title("Paragraph"));
188    /// ```
189    #[must_use = "method moves the value of self and returns the modified value"]
190    pub fn block(mut self, block: Block<'a>) -> Self {
191        self.block = Some(block);
192        self
193    }
194
195    /// Sets the style of the entire widget.
196    ///
197    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
198    /// your own type that implements [`Into<Style>`]).
199    ///
200    /// This applies to the entire widget, including the block if one is present. Any style set on
201    /// the block or text will be added to this style.
202    ///
203    /// # Example
204    ///
205    /// ```rust
206    /// use ratatui::{
207    ///     style::{Style, Stylize},
208    ///     widgets::Paragraph,
209    /// };
210    ///
211    /// let paragraph = Paragraph::new("Hello, world!").style(Style::new().red().on_white());
212    /// ```
213    ///
214    /// [`Color`]: crate::style::Color
215    #[must_use = "method moves the value of self and returns the modified value"]
216    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
217        self.style = style.into();
218        self
219    }
220
221    /// Sets the wrapping configuration for the widget.
222    ///
223    /// See [`Wrap`] for more information on the different options.
224    ///
225    /// # Example
226    ///
227    /// ```rust
228    /// use ratatui::widgets::{Paragraph, Wrap};
229    ///
230    /// let paragraph = Paragraph::new("Hello, world!").wrap(Wrap { trim: true });
231    /// ```
232    #[must_use = "method moves the value of self and returns the modified value"]
233    pub const fn wrap(mut self, wrap: Wrap) -> Self {
234        self.wrap = Some(wrap);
235        self
236    }
237
238    /// Set the scroll offset for the given paragraph
239    ///
240    /// The scroll offset is a tuple of (y, x) offset. The y offset is the number of lines to
241    /// scroll, and the x offset is the number of characters to scroll. The scroll offset is applied
242    /// after the text is wrapped and aligned.
243    ///
244    /// Note: the order of the tuple is (y, x) instead of (x, y), which is different from general
245    /// convention across the crate.
246    ///
247    /// For more information about future scrolling design and concerns, see [RFC: Design of
248    /// Scrollable Widgets](https://github.com/ratatui/ratatui/issues/174) on GitHub.
249    #[must_use = "method moves the value of self and returns the modified value"]
250    pub const fn scroll(mut self, offset: (Vertical, Horizontal)) -> Self {
251        self.scroll = Position {
252            x: offset.1,
253            y: offset.0,
254        };
255        self
256    }
257
258    /// Set the text alignment for the given paragraph
259    ///
260    /// The alignment is a variant of the [`Alignment`] enum which can be one of Left, Right, or
261    /// Center. If no alignment is specified, the text in a paragraph will be left-aligned.
262    ///
263    /// # Example
264    ///
265    /// ```rust
266    /// use ratatui::{layout::Alignment, widgets::Paragraph};
267    ///
268    /// let paragraph = Paragraph::new("Hello World").alignment(Alignment::Center);
269    /// ```
270    #[must_use = "method moves the value of self and returns the modified value"]
271    pub const fn alignment(mut self, alignment: Alignment) -> Self {
272        self.alignment = alignment;
273        self
274    }
275
276    /// Left-aligns the text in the given paragraph.
277    ///
278    /// Convenience shortcut for `Paragraph::alignment(Alignment::Left)`.
279    ///
280    /// # Examples
281    ///
282    /// ```rust
283    /// use ratatui::widgets::Paragraph;
284    ///
285    /// let paragraph = Paragraph::new("Hello World").left_aligned();
286    /// ```
287    #[must_use = "method moves the value of self and returns the modified value"]
288    pub const fn left_aligned(self) -> Self {
289        self.alignment(Alignment::Left)
290    }
291
292    /// Center-aligns the text in the given paragraph.
293    ///
294    /// Convenience shortcut for `Paragraph::alignment(Alignment::Center)`.
295    ///
296    /// # Examples
297    ///
298    /// ```rust
299    /// use ratatui::widgets::Paragraph;
300    ///
301    /// let paragraph = Paragraph::new("Hello World").centered();
302    /// ```
303    #[must_use = "method moves the value of self and returns the modified value"]
304    pub const fn centered(self) -> Self {
305        self.alignment(Alignment::Center)
306    }
307
308    /// Right-aligns the text in the given paragraph.
309    ///
310    /// Convenience shortcut for `Paragraph::alignment(Alignment::Right)`.
311    ///
312    /// # Examples
313    ///
314    /// ```rust
315    /// use ratatui::widgets::Paragraph;
316    ///
317    /// let paragraph = Paragraph::new("Hello World").right_aligned();
318    /// ```
319    #[must_use = "method moves the value of self and returns the modified value"]
320    pub const fn right_aligned(self) -> Self {
321        self.alignment(Alignment::Right)
322    }
323
324    /// Calculates the number of lines needed to fully render.
325    ///
326    /// Given a max line width, this method calculates the number of lines that a paragraph will
327    /// need in order to be fully rendered. For paragraphs that do not use wrapping, this count is
328    /// simply the number of lines present in the paragraph.
329    ///
330    /// This method will also account for the [`Block`] if one is set through [`Self::block`].
331    ///
332    /// Note: The design for text wrapping is not stable and might affect this API.
333    ///
334    /// # Example
335    ///
336    /// ```ignore
337    /// use ratatui::{widgets::{Paragraph, Wrap}};
338    ///
339    /// let paragraph = Paragraph::new("Hello World")
340    ///     .wrap(Wrap { trim: false });
341    /// assert_eq!(paragraph.line_count(20), 1);
342    /// assert_eq!(paragraph.line_count(10), 2);
343    /// ```
344    #[instability::unstable(
345        feature = "rendered-line-info",
346        issue = "https://github.com/ratatui/ratatui/issues/293"
347    )]
348    pub fn line_count(&self, width: u16) -> usize {
349        if width < 1 {
350            return 0;
351        }
352
353        let (top, bottom) = self
354            .block
355            .as_ref()
356            .map(Block::vertical_space)
357            .unwrap_or_default();
358
359        let count = if let Some(Wrap { trim }) = self.wrap {
360            let styled = self.text.iter().map(|line| {
361                let graphemes = line
362                    .spans
363                    .iter()
364                    .flat_map(|span| span.styled_graphemes(self.style));
365                let alignment = line.alignment.unwrap_or(self.alignment);
366                (graphemes, alignment)
367            });
368            let mut line_composer = WordWrapper::new(styled, width, trim);
369            let mut count = 0;
370            while line_composer.next_line().is_some() {
371                count += 1;
372            }
373            count
374        } else {
375            self.text.height()
376        };
377
378        count
379            .saturating_add(top as usize)
380            .saturating_add(bottom as usize)
381    }
382
383    /// Calculates the shortest line width needed to avoid any word being wrapped or truncated.
384    ///
385    /// Accounts for the [`Block`] if a block is set through [`Self::block`].
386    ///
387    /// Note: The design for text wrapping is not stable and might affect this API.
388    ///
389    /// # Example
390    ///
391    /// ```ignore
392    /// use ratatui::{widgets::Paragraph};
393    ///
394    /// let paragraph = Paragraph::new("Hello World");
395    /// assert_eq!(paragraph.line_width(), 11);
396    ///
397    /// let paragraph = Paragraph::new("Hello World\nhi\nHello World!!!");
398    /// assert_eq!(paragraph.line_width(), 14);
399    /// ```
400    #[instability::unstable(
401        feature = "rendered-line-info",
402        issue = "https://github.com/ratatui/ratatui/issues/293"
403    )]
404    pub fn line_width(&self) -> usize {
405        let width = self.text.iter().map(Line::width).max().unwrap_or_default();
406        let (left, right) = self
407            .block
408            .as_ref()
409            .map(Block::horizontal_space)
410            .unwrap_or_default();
411
412        width
413            .saturating_add(left as usize)
414            .saturating_add(right as usize)
415    }
416}
417
418impl Widget for Paragraph<'_> {
419    fn render(self, area: Rect, buf: &mut Buffer) {
420        self.render_ref(area, buf);
421    }
422}
423
424impl WidgetRef for Paragraph<'_> {
425    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
426        buf.set_style(area, self.style);
427        self.block.render_ref(area, buf);
428        let inner = self.block.inner_if_some(area);
429        self.render_paragraph(inner, buf);
430    }
431}
432
433impl Paragraph<'_> {
434    fn render_paragraph(&self, text_area: Rect, buf: &mut Buffer) {
435        if text_area.is_empty() {
436            return;
437        }
438
439        buf.set_style(text_area, self.style);
440        let styled = self.text.iter().map(|line| {
441            let graphemes = line.styled_graphemes(self.text.style);
442            let alignment = line.alignment.unwrap_or(self.alignment);
443            (graphemes, alignment)
444        });
445
446        if let Some(Wrap { trim }) = self.wrap {
447            let line_composer = WordWrapper::new(styled, text_area.width, trim);
448            self.render_text(line_composer, text_area, buf);
449        } else {
450            let mut line_composer = LineTruncator::new(styled, text_area.width);
451            line_composer.set_horizontal_offset(self.scroll.x);
452            self.render_text(line_composer, text_area, buf);
453        }
454    }
455}
456
457impl<'a> Paragraph<'a> {
458    fn render_text<C: LineComposer<'a>>(&self, mut composer: C, area: Rect, buf: &mut Buffer) {
459        let mut y = 0;
460        while let Some(WrappedLine {
461            line: current_line,
462            width: current_line_width,
463            alignment: current_line_alignment,
464        }) = composer.next_line()
465        {
466            if y >= self.scroll.y {
467                let mut x = get_line_offset(current_line_width, area.width, current_line_alignment);
468                for StyledGrapheme { symbol, style } in current_line {
469                    let width = symbol.width();
470                    if width == 0 {
471                        continue;
472                    }
473                    // If the symbol is empty, the last char which rendered last time will
474                    // leave on the line. It's a quick fix.
475                    let symbol = if symbol.is_empty() { " " } else { symbol };
476                    buf[(area.left() + x, area.top() + y - self.scroll.y)]
477                        .set_symbol(symbol)
478                        .set_style(*style);
479                    x += width as u16;
480                }
481            }
482            y += 1;
483            if y >= area.height + self.scroll.y {
484                break;
485            }
486        }
487    }
488}
489
490impl<'a> Styled for Paragraph<'a> {
491    type Item = Self;
492
493    fn style(&self) -> Style {
494        self.style
495    }
496
497    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
498        self.style(style)
499    }
500}
501
502#[cfg(test)]
503mod test {
504    use super::*;
505    use crate::{
506        backend::TestBackend,
507        buffer::Buffer,
508        layout::{Alignment, Rect},
509        style::{Color, Modifier, Style, Stylize},
510        text::{Line, Span, Text},
511        widgets::{block::Position, Borders, Widget},
512        Terminal,
513    };
514
515    /// Tests the [`Paragraph`] widget against the expected [`Buffer`] by rendering it onto an equal
516    /// area and comparing the rendered and expected content.
517    /// This can be used for easy testing of varying configured paragraphs with the same expected
518    /// buffer or any other test case really.
519    #[track_caller]
520    fn test_case(paragraph: &Paragraph, expected: &Buffer) {
521        let backend = TestBackend::new(expected.area.width, expected.area.height);
522        let mut terminal = Terminal::new(backend).unwrap();
523        terminal
524            .draw(|f| f.render_widget(paragraph.clone(), f.area()))
525            .unwrap();
526        terminal.backend().assert_buffer(expected);
527    }
528
529    #[test]
530    fn zero_width_char_at_end_of_line() {
531        let line = "foo\u{200B}";
532        for paragraph in [
533            Paragraph::new(line),
534            Paragraph::new(line).wrap(Wrap { trim: false }),
535            Paragraph::new(line).wrap(Wrap { trim: true }),
536        ] {
537            test_case(&paragraph, &Buffer::with_lines(["foo"]));
538            test_case(&paragraph, &Buffer::with_lines(["foo   "]));
539            test_case(&paragraph, &Buffer::with_lines(["foo   ", "      "]));
540            test_case(&paragraph, &Buffer::with_lines(["foo", "   "]));
541        }
542    }
543
544    #[test]
545    fn test_render_empty_paragraph() {
546        for paragraph in [
547            Paragraph::new(""),
548            Paragraph::new("").wrap(Wrap { trim: false }),
549            Paragraph::new("").wrap(Wrap { trim: true }),
550        ] {
551            test_case(&paragraph, &Buffer::with_lines([" "]));
552            test_case(&paragraph, &Buffer::with_lines(["          "]));
553            test_case(&paragraph, &Buffer::with_lines(["     "; 10]));
554            test_case(&paragraph, &Buffer::with_lines([" ", " "]));
555        }
556    }
557
558    #[test]
559    fn test_render_single_line_paragraph() {
560        let text = "Hello, world!";
561        for paragraph in [
562            Paragraph::new(text),
563            Paragraph::new(text).wrap(Wrap { trim: false }),
564            Paragraph::new(text).wrap(Wrap { trim: true }),
565        ] {
566            test_case(&paragraph, &Buffer::with_lines(["Hello, world!  "]));
567            test_case(&paragraph, &Buffer::with_lines(["Hello, world!"]));
568            test_case(
569                &paragraph,
570                &Buffer::with_lines(["Hello, world!  ", "               "]),
571            );
572            test_case(
573                &paragraph,
574                &Buffer::with_lines(["Hello, world!", "             "]),
575            );
576        }
577    }
578
579    #[test]
580    fn test_render_multi_line_paragraph() {
581        let text = "This is a\nmultiline\nparagraph.";
582        for paragraph in [
583            Paragraph::new(text),
584            Paragraph::new(text).wrap(Wrap { trim: false }),
585            Paragraph::new(text).wrap(Wrap { trim: true }),
586        ] {
587            test_case(
588                &paragraph,
589                &Buffer::with_lines(["This is a ", "multiline ", "paragraph."]),
590            );
591            test_case(
592                &paragraph,
593                &Buffer::with_lines(["This is a      ", "multiline      ", "paragraph.     "]),
594            );
595            test_case(
596                &paragraph,
597                &Buffer::with_lines([
598                    "This is a      ",
599                    "multiline      ",
600                    "paragraph.     ",
601                    "               ",
602                    "               ",
603                ]),
604            );
605        }
606    }
607
608    #[test]
609    fn test_render_paragraph_with_block() {
610        // We use the slightly unconventional "worlds" instead of "world" here to make sure when we
611        // can truncate this without triggering the typos linter.
612        let text = "Hello, worlds!";
613        let truncated_paragraph = Paragraph::new(text).block(Block::bordered().title("Title"));
614        let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
615        let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
616
617        for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
618            #[rustfmt::skip]
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            test_case(
636                paragraph,
637                &Buffer::with_lines([
638                    "┌Title────────────┐",
639                    "│Hello, worlds!   │",
640                    "│                 │",
641                    "└─────────────────┘",
642                ]),
643            );
644        }
645
646        test_case(
647            &truncated_paragraph,
648            &Buffer::with_lines([
649                "┌Title───────┐",
650                "│Hello, world│",
651                "│            │",
652                "└────────────┘",
653            ]),
654        );
655        test_case(
656            &wrapped_paragraph,
657            &Buffer::with_lines([
658                "┌Title──────┐",
659                "│Hello,     │",
660                "│worlds!    │",
661                "└───────────┘",
662            ]),
663        );
664        test_case(
665            &trimmed_paragraph,
666            &Buffer::with_lines([
667                "┌Title──────┐",
668                "│Hello,     │",
669                "│worlds!    │",
670                "└───────────┘",
671            ]),
672        );
673    }
674
675    #[test]
676    fn test_render_line_styled() {
677        let l0 = Line::raw("unformatted");
678        let l1 = Line::styled("bold text", Style::new().bold());
679        let l2 = Line::styled("cyan text", Style::new().cyan());
680        let l3 = Line::styled("dim text", Style::new().dim());
681        let paragraph = Paragraph::new(vec![l0, l1, l2, l3]);
682
683        let mut expected =
684            Buffer::with_lines(["unformatted", "bold text", "cyan text", "dim text"]);
685        expected.set_style(Rect::new(0, 1, 9, 1), Style::new().bold());
686        expected.set_style(Rect::new(0, 2, 9, 1), Style::new().cyan());
687        expected.set_style(Rect::new(0, 3, 8, 1), Style::new().dim());
688
689        test_case(&paragraph, &expected);
690    }
691
692    #[test]
693    fn test_render_line_spans_styled() {
694        let l0 = Line::default().spans([
695            Span::styled("bold", Style::new().bold()),
696            Span::raw(" and "),
697            Span::styled("cyan", Style::new().cyan()),
698        ]);
699        let l1 = Line::default().spans([Span::raw("unformatted")]);
700        let paragraph = Paragraph::new(vec![l0, l1]);
701
702        let mut expected = Buffer::with_lines(["bold and cyan", "unformatted"]);
703        expected.set_style(Rect::new(0, 0, 4, 1), Style::new().bold());
704        expected.set_style(Rect::new(9, 0, 4, 1), Style::new().cyan());
705
706        test_case(&paragraph, &expected);
707    }
708
709    #[test]
710    fn test_render_paragraph_with_block_with_bottom_title_and_border() {
711        let block = Block::new()
712            .borders(Borders::BOTTOM)
713            .title_position(Position::Bottom)
714            .title("Title");
715        let paragraph = Paragraph::new("Hello, world!").block(block);
716        test_case(
717            &paragraph,
718            &Buffer::with_lines(["Hello, world!  ", "Title──────────"]),
719        );
720    }
721
722    #[test]
723    fn test_render_paragraph_with_word_wrap() {
724        let text = "This is a long line of text that should wrap      and contains a superultramegagigalong word.";
725        let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
726        let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
727
728        test_case(
729            &wrapped_paragraph,
730            &Buffer::with_lines([
731                "This is a long line",
732                "of text that should",
733                "wrap      and      ",
734                "contains a         ",
735                "superultramegagigal",
736                "ong word.          ",
737            ]),
738        );
739        test_case(
740            &wrapped_paragraph,
741            &Buffer::with_lines([
742                "This is a   ",
743                "long line of",
744                "text that   ",
745                "should wrap ",
746                "    and     ",
747                "contains a  ",
748                "superultrame",
749                "gagigalong  ",
750                "word.       ",
751            ]),
752        );
753
754        test_case(
755            &trimmed_paragraph,
756            &Buffer::with_lines([
757                "This is a long line",
758                "of text that should",
759                "wrap      and      ",
760                "contains a         ",
761                "superultramegagigal",
762                "ong word.          ",
763            ]),
764        );
765        test_case(
766            &trimmed_paragraph,
767            &Buffer::with_lines([
768                "This is a   ",
769                "long line of",
770                "text that   ",
771                "should wrap ",
772                "and contains",
773                "a           ",
774                "superultrame",
775                "gagigalong  ",
776                "word.       ",
777            ]),
778        );
779    }
780
781    #[test]
782    fn test_render_paragraph_with_line_truncation() {
783        let text = "This is a long line of text that should be truncated.";
784        let truncated_paragraph = Paragraph::new(text);
785
786        test_case(
787            &truncated_paragraph,
788            &Buffer::with_lines(["This is a long line of"]),
789        );
790        test_case(
791            &truncated_paragraph,
792            &Buffer::with_lines(["This is a long line of te"]),
793        );
794        test_case(
795            &truncated_paragraph,
796            &Buffer::with_lines(["This is a long line of "]),
797        );
798        test_case(
799            &truncated_paragraph.clone().scroll((0, 2)),
800            &Buffer::with_lines(["is is a long line of te"]),
801        );
802    }
803
804    #[test]
805    fn test_render_paragraph_with_left_alignment() {
806        let text = "Hello, world!";
807        let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Left);
808        let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
809        let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
810
811        for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
812            test_case(paragraph, &Buffer::with_lines(["Hello, world!  "]));
813            test_case(paragraph, &Buffer::with_lines(["Hello, world!"]));
814        }
815
816        test_case(&truncated_paragraph, &Buffer::with_lines(["Hello, wor"]));
817        test_case(
818            &wrapped_paragraph,
819            &Buffer::with_lines(["Hello,    ", "world!    "]),
820        );
821        test_case(
822            &trimmed_paragraph,
823            &Buffer::with_lines(["Hello,    ", "world!    "]),
824        );
825    }
826
827    #[test]
828    fn test_render_paragraph_with_center_alignment() {
829        let text = "Hello, world!";
830        let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Center);
831        let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
832        let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
833
834        for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
835            test_case(paragraph, &Buffer::with_lines([" Hello, world! "]));
836            test_case(paragraph, &Buffer::with_lines(["  Hello, world! "]));
837            test_case(paragraph, &Buffer::with_lines(["  Hello, world!  "]));
838            test_case(paragraph, &Buffer::with_lines(["Hello, world!"]));
839        }
840
841        test_case(&truncated_paragraph, &Buffer::with_lines(["Hello, wor"]));
842        test_case(
843            &wrapped_paragraph,
844            &Buffer::with_lines(["  Hello,  ", "  world!  "]),
845        );
846        test_case(
847            &trimmed_paragraph,
848            &Buffer::with_lines(["  Hello,  ", "  world!  "]),
849        );
850    }
851
852    #[test]
853    fn test_render_paragraph_with_right_alignment() {
854        let text = "Hello, world!";
855        let truncated_paragraph = Paragraph::new(text).alignment(Alignment::Right);
856        let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
857        let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
858
859        for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
860            test_case(paragraph, &Buffer::with_lines(["  Hello, world!"]));
861            test_case(paragraph, &Buffer::with_lines(["Hello, world!"]));
862        }
863
864        test_case(&truncated_paragraph, &Buffer::with_lines(["Hello, wor"]));
865        test_case(
866            &wrapped_paragraph,
867            &Buffer::with_lines(["    Hello,", "    world!"]),
868        );
869        test_case(
870            &trimmed_paragraph,
871            &Buffer::with_lines(["    Hello,", "    world!"]),
872        );
873    }
874
875    #[test]
876    fn test_render_paragraph_with_scroll_offset() {
877        let text = "This is a\ncool\nmultiline\nparagraph.";
878        let truncated_paragraph = Paragraph::new(text).scroll((2, 0));
879        let wrapped_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: false });
880        let trimmed_paragraph = truncated_paragraph.clone().wrap(Wrap { trim: true });
881
882        for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
883            test_case(
884                paragraph,
885                &Buffer::with_lines(["multiline   ", "paragraph.  ", "            "]),
886            );
887            test_case(paragraph, &Buffer::with_lines(["multiline   "]));
888        }
889
890        test_case(
891            &truncated_paragraph.clone().scroll((2, 4)),
892            &Buffer::with_lines(["iline   ", "graph.  "]),
893        );
894        test_case(
895            &wrapped_paragraph,
896            &Buffer::with_lines(["cool   ", "multili", "ne     "]),
897        );
898    }
899
900    #[test]
901    fn test_render_paragraph_with_zero_width_area() {
902        let text = "Hello, world!";
903        let area = Rect::new(0, 0, 0, 3);
904
905        for paragraph in [
906            Paragraph::new(text),
907            Paragraph::new(text).wrap(Wrap { trim: false }),
908            Paragraph::new(text).wrap(Wrap { trim: true }),
909        ] {
910            test_case(&paragraph, &Buffer::empty(area));
911            test_case(&paragraph.clone().scroll((2, 4)), &Buffer::empty(area));
912        }
913    }
914
915    #[test]
916    fn test_render_paragraph_with_zero_height_area() {
917        let text = "Hello, world!";
918        let area = Rect::new(0, 0, 10, 0);
919
920        for paragraph in [
921            Paragraph::new(text),
922            Paragraph::new(text).wrap(Wrap { trim: false }),
923            Paragraph::new(text).wrap(Wrap { trim: true }),
924        ] {
925            test_case(&paragraph, &Buffer::empty(area));
926            test_case(&paragraph.clone().scroll((2, 4)), &Buffer::empty(area));
927        }
928    }
929
930    #[test]
931    fn test_render_paragraph_with_styled_text() {
932        let text = Line::from(vec![
933            Span::styled("Hello, ", Style::default().fg(Color::Red)),
934            Span::styled("world!", Style::default().fg(Color::Blue)),
935        ]);
936
937        let mut expected_buffer = Buffer::with_lines(["Hello, world!"]);
938        expected_buffer.set_style(
939            Rect::new(0, 0, 7, 1),
940            Style::default().fg(Color::Red).bg(Color::Green),
941        );
942        expected_buffer.set_style(
943            Rect::new(7, 0, 6, 1),
944            Style::default().fg(Color::Blue).bg(Color::Green),
945        );
946
947        for paragraph in [
948            Paragraph::new(text.clone()),
949            Paragraph::new(text.clone()).wrap(Wrap { trim: false }),
950            Paragraph::new(text.clone()).wrap(Wrap { trim: true }),
951        ] {
952            test_case(
953                &paragraph.style(Style::default().bg(Color::Green)),
954                &expected_buffer,
955            );
956        }
957    }
958
959    #[test]
960    fn test_render_paragraph_with_special_characters() {
961        let text = "Hello, <world>!";
962        for paragraph in [
963            Paragraph::new(text),
964            Paragraph::new(text).wrap(Wrap { trim: false }),
965            Paragraph::new(text).wrap(Wrap { trim: true }),
966        ] {
967            test_case(&paragraph, &Buffer::with_lines(["Hello, <world>!"]));
968            test_case(&paragraph, &Buffer::with_lines(["Hello, <world>!     "]));
969            test_case(
970                &paragraph,
971                &Buffer::with_lines(["Hello, <world>!     ", "                    "]),
972            );
973            test_case(
974                &paragraph,
975                &Buffer::with_lines(["Hello, <world>!", "               "]),
976            );
977        }
978    }
979
980    #[test]
981    fn test_render_paragraph_with_unicode_characters() {
982        let text = "こんにちは, 世界! 😃";
983        let truncated_paragraph = Paragraph::new(text);
984        let wrapped_paragraph = Paragraph::new(text).wrap(Wrap { trim: false });
985        let trimmed_paragraph = Paragraph::new(text).wrap(Wrap { trim: true });
986
987        for paragraph in [&truncated_paragraph, &wrapped_paragraph, &trimmed_paragraph] {
988            test_case(paragraph, &Buffer::with_lines(["こんにちは, 世界! 😃"]));
989            test_case(
990                paragraph,
991                &Buffer::with_lines(["こんにちは, 世界! 😃     "]),
992            );
993        }
994
995        test_case(
996            &truncated_paragraph,
997            &Buffer::with_lines(["こんにちは, 世 "]),
998        );
999        test_case(
1000            &wrapped_paragraph,
1001            &Buffer::with_lines(["こんにちは,    ", "世界! 😃      "]),
1002        );
1003        test_case(
1004            &trimmed_paragraph,
1005            &Buffer::with_lines(["こんにちは,    ", "世界! 😃      "]),
1006        );
1007    }
1008
1009    #[test]
1010    fn can_be_stylized() {
1011        assert_eq!(
1012            Paragraph::new("").black().on_white().bold().not_dim().style,
1013            Style::default()
1014                .fg(Color::Black)
1015                .bg(Color::White)
1016                .add_modifier(Modifier::BOLD)
1017                .remove_modifier(Modifier::DIM)
1018        );
1019    }
1020
1021    #[test]
1022    fn widgets_paragraph_count_rendered_lines() {
1023        let paragraph = Paragraph::new("Hello World");
1024        assert_eq!(paragraph.line_count(20), 1);
1025        assert_eq!(paragraph.line_count(10), 1);
1026        let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false });
1027        assert_eq!(paragraph.line_count(20), 1);
1028        assert_eq!(paragraph.line_count(10), 2);
1029        let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true });
1030        assert_eq!(paragraph.line_count(20), 1);
1031        assert_eq!(paragraph.line_count(10), 2);
1032
1033        let text = "Hello World ".repeat(100);
1034        let paragraph = Paragraph::new(text.trim());
1035        assert_eq!(paragraph.line_count(11), 1);
1036        assert_eq!(paragraph.line_count(6), 1);
1037        let paragraph = paragraph.wrap(Wrap { trim: false });
1038        assert_eq!(paragraph.line_count(11), 100);
1039        assert_eq!(paragraph.line_count(6), 200);
1040        let paragraph = paragraph.wrap(Wrap { trim: true });
1041        assert_eq!(paragraph.line_count(11), 100);
1042        assert_eq!(paragraph.line_count(6), 200);
1043    }
1044
1045    #[test]
1046    fn widgets_paragraph_rendered_line_count_accounts_block() {
1047        let block = Block::new();
1048        let paragraph = Paragraph::new("Hello World").block(block);
1049        assert_eq!(paragraph.line_count(20), 1);
1050        assert_eq!(paragraph.line_count(10), 1);
1051
1052        let block = Block::new().borders(Borders::TOP);
1053        let paragraph = paragraph.block(block);
1054        assert_eq!(paragraph.line_count(20), 2);
1055        assert_eq!(paragraph.line_count(10), 2);
1056
1057        let block = Block::new().borders(Borders::BOTTOM);
1058        let paragraph = paragraph.block(block);
1059        assert_eq!(paragraph.line_count(20), 2);
1060        assert_eq!(paragraph.line_count(10), 2);
1061
1062        let block = Block::new().borders(Borders::TOP | Borders::BOTTOM);
1063        let paragraph = paragraph.block(block);
1064        assert_eq!(paragraph.line_count(20), 3);
1065        assert_eq!(paragraph.line_count(10), 3);
1066
1067        let block = Block::bordered();
1068        let paragraph = paragraph.block(block);
1069        assert_eq!(paragraph.line_count(20), 3);
1070        assert_eq!(paragraph.line_count(10), 3);
1071
1072        let block = Block::bordered();
1073        let paragraph = paragraph.block(block).wrap(Wrap { trim: true });
1074        assert_eq!(paragraph.line_count(20), 3);
1075        assert_eq!(paragraph.line_count(10), 4);
1076
1077        let block = Block::bordered();
1078        let paragraph = paragraph.block(block).wrap(Wrap { trim: false });
1079        assert_eq!(paragraph.line_count(20), 3);
1080        assert_eq!(paragraph.line_count(10), 4);
1081
1082        let text = "Hello World ".repeat(100);
1083        let block = Block::new();
1084        let paragraph = Paragraph::new(text.trim()).block(block);
1085        assert_eq!(paragraph.line_count(11), 1);
1086
1087        let block = Block::bordered();
1088        let paragraph = paragraph.block(block);
1089        assert_eq!(paragraph.line_count(11), 3);
1090        assert_eq!(paragraph.line_count(6), 3);
1091
1092        let block = Block::new().borders(Borders::TOP);
1093        let paragraph = paragraph.block(block);
1094        assert_eq!(paragraph.line_count(11), 2);
1095        assert_eq!(paragraph.line_count(6), 2);
1096
1097        let block = Block::new().borders(Borders::BOTTOM);
1098        let paragraph = paragraph.block(block);
1099        assert_eq!(paragraph.line_count(11), 2);
1100        assert_eq!(paragraph.line_count(6), 2);
1101
1102        let block = Block::new().borders(Borders::LEFT | Borders::RIGHT);
1103        let paragraph = paragraph.block(block);
1104        assert_eq!(paragraph.line_count(11), 1);
1105        assert_eq!(paragraph.line_count(6), 1);
1106    }
1107
1108    #[test]
1109    fn widgets_paragraph_line_width() {
1110        let paragraph = Paragraph::new("Hello World");
1111        assert_eq!(paragraph.line_width(), 11);
1112        let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: false });
1113        assert_eq!(paragraph.line_width(), 11);
1114        let paragraph = Paragraph::new("Hello World").wrap(Wrap { trim: true });
1115        assert_eq!(paragraph.line_width(), 11);
1116
1117        let text = "Hello World ".repeat(100);
1118        let paragraph = Paragraph::new(text);
1119        assert_eq!(paragraph.line_width(), 1200);
1120        let paragraph = paragraph.wrap(Wrap { trim: false });
1121        assert_eq!(paragraph.line_width(), 1200);
1122        let paragraph = paragraph.wrap(Wrap { trim: true });
1123        assert_eq!(paragraph.line_width(), 1200);
1124    }
1125
1126    #[test]
1127    fn widgets_paragraph_line_width_accounts_for_block() {
1128        let block = Block::bordered();
1129        let paragraph = Paragraph::new("Hello World").block(block);
1130        assert_eq!(paragraph.line_width(), 13);
1131
1132        let block = Block::new().borders(Borders::LEFT);
1133        let paragraph = Paragraph::new("Hello World").block(block);
1134        assert_eq!(paragraph.line_width(), 12);
1135
1136        let block = Block::new().borders(Borders::LEFT);
1137        let paragraph = Paragraph::new("Hello World")
1138            .block(block)
1139            .wrap(Wrap { trim: true });
1140        assert_eq!(paragraph.line_width(), 12);
1141
1142        let block = Block::new().borders(Borders::LEFT);
1143        let paragraph = Paragraph::new("Hello World")
1144            .block(block)
1145            .wrap(Wrap { trim: false });
1146        assert_eq!(paragraph.line_width(), 12);
1147    }
1148
1149    #[test]
1150    fn left_aligned() {
1151        let p = Paragraph::new("Hello, world!").left_aligned();
1152        assert_eq!(p.alignment, Alignment::Left);
1153    }
1154
1155    #[test]
1156    fn centered() {
1157        let p = Paragraph::new("Hello, world!").centered();
1158        assert_eq!(p.alignment, Alignment::Center);
1159    }
1160
1161    #[test]
1162    fn right_aligned() {
1163        let p = Paragraph::new("Hello, world!").right_aligned();
1164        assert_eq!(p.alignment, Alignment::Right);
1165    }
1166
1167    /// Regression test for <https://github.com/ratatui/ratatui/issues/990>
1168    ///
1169    /// This test ensures that paragraphs with a block and styled text are rendered correctly.
1170    /// It has been simplified from the original issue but tests the same functionality.
1171    #[test]
1172    fn paragraph_block_text_style() {
1173        let text = Text::styled("Styled text", Color::Green);
1174        let paragraph = Paragraph::new(text).block(Block::bordered());
1175
1176        let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
1177        paragraph.render(Rect::new(0, 0, 20, 3), &mut buf);
1178
1179        let mut expected = Buffer::with_lines([
1180            "┌──────────────────┐",
1181            "│Styled text       │",
1182            "└──────────────────┘",
1183        ]);
1184        expected.set_style(Rect::new(1, 1, 11, 1), Style::default().fg(Color::Green));
1185        assert_eq!(buf, expected);
1186    }
1187}