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}