Skip to main content

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