tui/widgets/
paragraph.rs

1use crate::{
2    buffer::Buffer,
3    layout::{Alignment, Rect},
4    style::Style,
5    text::{StyledGrapheme, Text},
6    widgets::{
7        reflow::{LineComposer, LineTruncator, WordWrapper},
8        Block, Widget,
9    },
10};
11use std::iter;
12use unicode_width::UnicodeWidthStr;
13
14fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
15    match alignment {
16        Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
17        Alignment::Right => text_area_width.saturating_sub(line_width),
18        Alignment::Left => 0,
19    }
20}
21
22/// A widget to display some text.
23///
24/// # Examples
25///
26/// ```
27/// # use tui::text::{Text, Spans, Span};
28/// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
29/// # use tui::style::{Style, Color, Modifier};
30/// # use tui::layout::{Alignment};
31/// let text = vec![
32///     Spans::from(vec![
33///         Span::raw("First"),
34///         Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
35///         Span::raw("."),
36///     ]),
37///     Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))),
38/// ];
39/// Paragraph::new(text)
40///     .block(Block::default().title("Paragraph").borders(Borders::ALL))
41///     .style(Style::default().fg(Color::White).bg(Color::Black))
42///     .alignment(Alignment::Center)
43///     .wrap(Wrap { trim: true });
44/// ```
45#[derive(Debug, Clone)]
46pub struct Paragraph<'a> {
47    /// A block to wrap the widget in
48    block: Option<Block<'a>>,
49    /// Widget style
50    style: Style,
51    /// How to wrap the text
52    wrap: Option<Wrap>,
53    /// The text to display
54    text: Text<'a>,
55    /// Scroll
56    scroll: (u16, u16),
57    /// Alignment of the text
58    alignment: Alignment,
59}
60
61/// Describes how to wrap text across lines.
62///
63/// ## Examples
64///
65/// ```
66/// # use tui::widgets::{Paragraph, Wrap};
67/// # use tui::text::Text;
68/// let bullet_points = Text::from(r#"Some indented points:
69///     - First thing goes here and is long so that it wraps
70///     - Here is another point that is long enough to wrap"#);
71///
72/// // With leading spaces trimmed (window width of 30 chars):
73/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
74/// // Some indented points:
75/// // - First thing goes here and is
76/// // long so that it wraps
77/// // - Here is another point that
78/// // is long enough to wrap
79///
80/// // But without trimming, indentation is preserved:
81/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
82/// // Some indented points:
83/// //     - First thing goes here
84/// // and is long so that it wraps
85/// //     - Here is another point
86/// // that is long enough to wrap
87/// ```
88#[derive(Debug, Clone, Copy)]
89pub struct Wrap {
90    /// Should leading whitespace be trimmed
91    pub trim: bool,
92}
93
94impl<'a> Paragraph<'a> {
95    pub fn new<T>(text: T) -> Paragraph<'a>
96    where
97        T: Into<Text<'a>>,
98    {
99        Paragraph {
100            block: None,
101            style: Default::default(),
102            wrap: None,
103            text: text.into(),
104            scroll: (0, 0),
105            alignment: Alignment::Left,
106        }
107    }
108
109    pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
110        self.block = Some(block);
111        self
112    }
113
114    pub fn style(mut self, style: Style) -> Paragraph<'a> {
115        self.style = style;
116        self
117    }
118
119    pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
120        self.wrap = Some(wrap);
121        self
122    }
123
124    pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
125        self.scroll = offset;
126        self
127    }
128
129    pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
130        self.alignment = alignment;
131        self
132    }
133}
134
135impl<'a> Widget for Paragraph<'a> {
136    fn render(mut self, area: Rect, buf: &mut Buffer) {
137        buf.set_style(area, self.style);
138        let text_area = match self.block.take() {
139            Some(b) => {
140                let inner_area = b.inner(area);
141                b.render(area, buf);
142                inner_area
143            }
144            None => area,
145        };
146
147        if text_area.height < 1 {
148            return;
149        }
150
151        let style = self.style;
152        let mut styled = self.text.lines.iter().flat_map(|spans| {
153            spans
154                .0
155                .iter()
156                .flat_map(|span| span.styled_graphemes(style))
157                // Required given the way composers work but might be refactored out if we change
158                // composers to operate on lines instead of a stream of graphemes.
159                .chain(iter::once(StyledGrapheme {
160                    symbol: "\n",
161                    style: self.style,
162                }))
163        });
164
165        let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
166            Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
167        } else {
168            let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
169            if let Alignment::Left = self.alignment {
170                line_composer.set_horizontal_offset(self.scroll.1);
171            }
172            line_composer
173        };
174        let mut y = 0;
175        while let Some((current_line, current_line_width)) = line_composer.next_line() {
176            if y >= self.scroll.0 {
177                let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
178                for StyledGrapheme { symbol, style } in current_line {
179                    buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
180                        .set_symbol(if symbol.is_empty() {
181                            // If the symbol is empty, the last char which rendered last time will
182                            // leave on the line. It's a quick fix.
183                            " "
184                        } else {
185                            symbol
186                        })
187                        .set_style(*style);
188                    x += symbol.width() as u16;
189                }
190            }
191            y += 1;
192            if y >= text_area.height + self.scroll.0 {
193                break;
194            }
195        }
196    }
197}