ratatui_widgets/
paragraph.rs

1//! The [`Paragraph`] widget and related types allows displaying a block of text with optional
2//! wrapping, alignment, and block styling.
3use 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/// A widget to display some text.
14///
15/// It is used to display a block of text. The text can be styled and aligned. It can also be
16/// wrapped to the next line if it is too long to fit in the given area.
17///
18/// The text can be any type that can be converted into a [`Text`]. By default, the text is styled
19/// with [`Style::default()`], not wrapped, and aligned to the left.
20///
21/// The text can be wrapped to the next line if it is too long to fit in the given area. The
22/// wrapping can be configured with the [`wrap`] method. For more complex wrapping, consider using
23/// the [Textwrap crate].
24///
25/// The text can be aligned to the left, right, or center. The alignment can be configured with the
26/// [`alignment`] method or with the [`left_aligned`], [`right_aligned`], and [`centered`] methods.
27///
28/// The text can be scrolled to show a specific part of the text. The scroll offset can be set with
29/// the [`scroll`] method.
30///
31/// The text can be surrounded by a [`Block`] with a title and borders. The block can be configured
32/// with the [`block`] method.
33///
34/// The style of the text can be set with the [`style`] method. This style will be applied to the
35/// entire widget, including the block if one is present. Any style set on the block or text will be
36/// added to this style. See the [`Style`] type for more information on how styles are combined.
37///
38/// Note: If neither wrapping or a block is needed, consider rendering the [`Text`], [`Line`], or
39/// [`Span`] widgets directly.
40///
41/// [Textwrap crate]: https://crates.io/crates/textwrap
42/// [`wrap`]: Self::wrap
43/// [`alignment`]: Self::alignment
44/// [`left_aligned`]: Self::left_aligned
45/// [`right_aligned`]: Self::right_aligned
46/// [`centered`]: Self::centered
47/// [`scroll`]: Self::scroll
48/// [`block`]: Self::block
49/// [`style`]: Self::style
50///
51/// # Example
52///
53/// ```
54/// use ratatui::layout::Alignment;
55/// use ratatui::style::{Style, Stylize};
56/// use ratatui::text::{Line, Span};
57/// use ratatui::widgets::{Block, Paragraph, Wrap};
58///
59/// let text = vec![
60///     Line::from(vec![
61///         Span::raw("First"),
62///         Span::styled("line", Style::new().green().italic()),
63///         ".".into(),
64///     ]),
65///     Line::from("Second line".red()),
66///     "Third line".into(),
67/// ];
68/// Paragraph::new(text)
69///     .block(Block::bordered().title("Paragraph"))
70///     .style(Style::new().white().on_black())
71///     .alignment(Alignment::Center)
72///     .wrap(Wrap { trim: true });
73/// ```
74///
75/// [`Span`]: ratatui_core::text::Span
76#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
77pub struct Paragraph<'a> {
78    /// A block to wrap the widget in
79    block: Option<Block<'a>>,
80    /// Widget style
81    style: Style,
82    /// How to wrap the text
83    wrap: Option<Wrap>,
84    /// The text to display
85    text: Text<'a>,
86    /// Scroll
87    scroll: Position,
88    /// Alignment of the text
89    alignment: Alignment,
90}
91
92/// Describes how to wrap text across lines.
93///
94/// ## Examples
95///
96/// ```
97/// use ratatui::text::Text;
98/// use ratatui::widgets::{Paragraph, Wrap};
99///
100/// let bullet_points = Text::from(
101///     r#"Some indented points:
102///     - First thing goes here and is long so that it wraps
103///     - Here is another point that is long enough to wrap"#,
104/// );
105///
106/// // With leading spaces trimmed (window width of 30 chars):
107/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
108/// // Some indented points:
109/// // - First thing goes here and is
110/// // long so that it wraps
111/// // - Here is another point that
112/// // is long enough to wrap
113///
114/// // But without trimming, indentation is preserved:
115/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
116/// // Some indented points:
117/// //     - First thing goes here
118/// // and is long so that it wraps
119/// //     - Here is another point
120/// // that is long enough to wrap
121/// ```
122#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
123pub struct Wrap {
124    /// Should leading whitespace be trimmed
125    pub trim: bool,
126}
127
128type Horizontal = u16;
129type Vertical = u16;
130
131impl<'a> Paragraph<'a> {
132    /// Creates a new [`Paragraph`] widget with the given text.
133    ///
134    /// The `text` parameter can be a [`Text`] or any type that can be converted into a [`Text`]. By
135    /// default, the text is styled with [`Style::default()`], not wrapped, and aligned to the left.
136    ///
137    /// # Examples
138    ///
139    /// ```rust
140    /// use ratatui::style::{Style, Stylize};
141    /// use ratatui::text::{Line, Text};
142    /// use ratatui::widgets::Paragraph;
143    ///
144    /// let paragraph = Paragraph::new("Hello, world!");
145    /// let paragraph = Paragraph::new(String::from("Hello, world!"));
146    /// let paragraph = Paragraph::new(Text::raw("Hello, world!"));
147    /// let paragraph = Paragraph::new(Text::styled("Hello, world!", Style::default()));
148    /// let paragraph = Paragraph::new(Line::from(vec!["Hello, ".into(), "world!".red()]));
149    /// ```
150    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    /// Surrounds the [`Paragraph`] widget with a [`Block`].
165    ///
166    /// # Example
167    ///
168    /// ```rust
169    /// use ratatui::widgets::{Block, Paragraph};
170    ///
171    /// let paragraph = Paragraph::new("Hello, world!").block(Block::bordered().title("Paragraph"));
172    /// ```
173    #[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    /// Sets the style of the entire widget.
180    ///
181    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
182    /// your own type that implements [`Into<Style>`]).
183    ///
184    /// This applies to the entire widget, including the block if one is present. Any style set on
185    /// the block or text will be added to this style.
186    ///
187    /// # Example
188    ///
189    /// ```rust
190    /// use ratatui::style::{Style, Stylize};
191    /// use ratatui::widgets::Paragraph;
192    ///
193    /// let paragraph = Paragraph::new("Hello, world!").style(Style::new().red().on_white());
194    /// ```
195    ///
196    /// [`Color`]: ratatui_core::style::Color
197    #[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    /// Sets the wrapping configuration for the widget.
204    ///
205    /// See [`Wrap`] for more information on the different options.
206    ///
207    /// # Example
208    ///
209    /// ```rust
210    /// use ratatui::widgets::{Paragraph, Wrap};
211    ///
212    /// let paragraph = Paragraph::new("Hello, world!").wrap(Wrap { trim: true });
213    /// ```
214    #[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    /// Set the scroll offset for the given paragraph
221    ///
222    /// The scroll offset is a tuple of (y, x) offset. The y offset is the number of lines to
223    /// scroll, and the x offset is the number of characters to scroll. The scroll offset is applied
224    /// after the text is wrapped and aligned.
225    ///
226    /// Note: the order of the tuple is (y, x) instead of (x, y), which is different from general
227    /// convention across the crate.
228    ///
229    /// For more information about future scrolling design and concerns, see [RFC: Design of
230    /// Scrollable Widgets](https://github.com/ratatui/ratatui/discussions/1924) on GitHub.
231    #[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    /// Set the text alignment for the given paragraph
241    ///
242    /// The alignment is a variant of the [`Alignment`] enum which can be one of Left, Right, or
243    /// Center. If no alignment is specified, the text in a paragraph will be left-aligned.
244    ///
245    /// # Example
246    ///
247    /// ```rust
248    /// use ratatui::layout::Alignment;
249    /// use ratatui::widgets::Paragraph;
250    ///
251    /// let paragraph = Paragraph::new("Hello World").alignment(Alignment::Center);
252    /// ```
253    #[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    /// Left-aligns the text in the given paragraph.
260    ///
261    /// Convenience shortcut for `Paragraph::alignment(Alignment::Left)`.
262    ///
263    /// # Examples
264    ///
265    /// ```rust
266    /// use ratatui::widgets::Paragraph;
267    ///
268    /// let paragraph = Paragraph::new("Hello World").left_aligned();
269    /// ```
270    #[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    /// Center-aligns the text in the given paragraph.
276    ///
277    /// Convenience shortcut for `Paragraph::alignment(Alignment::Center)`.
278    ///
279    /// # Examples
280    ///
281    /// ```rust
282    /// use ratatui::widgets::Paragraph;
283    ///
284    /// let paragraph = Paragraph::new("Hello World").centered();
285    /// ```
286    #[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    /// Right-aligns the text in the given paragraph.
292    ///
293    /// Convenience shortcut for `Paragraph::alignment(Alignment::Right)`.
294    ///
295    /// # Examples
296    ///
297    /// ```rust
298    /// use ratatui::widgets::Paragraph;
299    ///
300    /// let paragraph = Paragraph::new("Hello World").right_aligned();
301    /// ```
302    #[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    /// Calculates the number of lines needed to fully render.
308    ///
309    /// Given a max line width, this method calculates the number of lines that a paragraph will
310    /// need in order to be fully rendered. For paragraphs that do not use wrapping, this count is
311    /// simply the number of lines present in the paragraph.
312    ///
313    /// This method will also account for the [`Block`] if one is set through [`Self::block`].
314    ///
315    /// Note: The design for text wrapping is not stable and might affect this API.
316    ///
317    /// # Example
318    ///
319    /// ```ignore
320    /// use ratatui::{widgets::{Paragraph, Wrap}};
321    ///
322    /// let paragraph = Paragraph::new("Hello World")
323    ///     .wrap(Wrap { trim: false });
324    /// assert_eq!(paragraph.line_count(20), 1);
325    /// assert_eq!(paragraph.line_count(10), 2);
326    /// ```
327    #[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    /// Calculates the shortest line width needed to avoid any word being wrapped or truncated.
367    ///
368    /// Accounts for the [`Block`] if a block is set through [`Self::block`].
369    ///
370    /// Note: The design for text wrapping is not stable and might affect this API.
371    ///
372    /// # Example
373    ///
374    /// ```ignore
375    /// use ratatui::{widgets::Paragraph};
376    ///
377    /// let paragraph = Paragraph::new("Hello World");
378    /// assert_eq!(paragraph.line_width(), 11);
379    ///
380    /// let paragraph = Paragraph::new("Hello World\nhi\nHello World!!!");
381    /// assert_eq!(paragraph.line_width(), 14);
382    /// ```
383    #[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            // compute the lines iteratively until we reach the desired scroll offset.
433            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            // avoid unnecessary work by skipping directly to the relevant line before rendering
441            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        // Make sure to overwrite any previous character with a space (rather than a zero-width)
468        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    /// Tests the [`Paragraph`] widget against the expected [`Buffer`] by rendering it onto an equal
511    /// area and comparing the rendered and expected content.
512    /// This can be used for easy testing of varying configured paragraphs with the same expected
513    /// buffer or any other test case really.
514    #[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(&paragraph, &Buffer::with_lines(["foo"]));
530            test_case(&paragraph, &Buffer::with_lines(["foo   "]));
531            test_case(&paragraph, &Buffer::with_lines(["foo   ", "      "]));
532            test_case(&paragraph, &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(&paragraph, &Buffer::with_lines([" "]));
544            test_case(&paragraph, &Buffer::with_lines(["          "]));
545            test_case(&paragraph, &Buffer::with_lines(["     "; 10]));
546            test_case(&paragraph, &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(&paragraph, &Buffer::with_lines(["Hello, world!  "]));
559            test_case(&paragraph, &Buffer::with_lines(["Hello, world!"]));
560            test_case(
561                &paragraph,
562                &Buffer::with_lines(["Hello, world!  ", "               "]),
563            );
564            test_case(
565                &paragraph,
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                &paragraph,
581                &Buffer::with_lines(["This is a ", "multiline ", "paragraph."]),
582            );
583            test_case(
584                &paragraph,
585                &Buffer::with_lines(["This is a      ", "multiline      ", "paragraph.     "]),
586            );
587            test_case(
588                &paragraph,
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        // We use the slightly unconventional "worlds" instead of "world" here to make sure when we
603        // can truncate this without triggering the typos linter.
604        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(&paragraph, &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(&paragraph, &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            &paragraph,
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            &paragraph,
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(&paragraph, &Buffer::empty(area));
922            test_case(&paragraph.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(&paragraph, &Buffer::empty(area));
937            test_case(&paragraph.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                &paragraph.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(&paragraph, &Buffer::with_lines(["Hello, <world>!"]));
979            test_case(&paragraph, &Buffer::with_lines(["Hello, <world>!     "]));
980            test_case(
981                &paragraph,
982                &Buffer::with_lines(["Hello, <world>!     ", "                    "]),
983            );
984            test_case(
985                &paragraph,
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    /// Regression test for <https://github.com/ratatui/ratatui/issues/990>
1179    ///
1180    /// This test ensures that paragraphs with a block and styled text are rendered correctly.
1181    /// It has been simplified from the original issue but tests the same functionality.
1182    #[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        // This should not panic, even if the buffer is too small to render the paragraph.
1228        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        // This should not panic, even if the buffer has zero size.
1237        paragraph.render(buffer.area, &mut buffer);
1238    }
1239}