1use crate::{
2 buffer::Buffer,
3 layout::{Alignment, Direction, Margin, Rect},
4 style::Style,
5 text::{StyledGrapheme, Text},
6 widgets::{
7 reflow::{LineComposer, LineTruncator, WordWrapper},
8 Block, Borders, Scrollbar, ScrollbarOrientation, 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#[derive(Debug, Clone)]
46pub struct Paragraph<'a> {
47 block: Option<Block<'a>>,
49 scrollbar: Option<Scrollbar<'a>>,
51 scrollbar_direction: Option<Direction>,
52 margin: Margin,
53 style: Style,
55 wrap: Option<Wrap>,
57 text: Text<'a>,
59 scroll: (u16, u16),
61 alignment: Alignment,
63 content_height: u16,
65 content_width: u16,
66 text_was_updated: bool,
67}
68
69#[derive(Debug, Clone, Copy)]
97pub struct Wrap {
98 pub trim: bool,
100}
101
102impl<'a> Paragraph<'a> {
103 pub fn new<T>(text: T) -> Paragraph<'a>
104 where
105 T: Into<Text<'a>>,
106 {
107 Paragraph {
108 block: None,
109 scrollbar: None,
110 scrollbar_direction: None,
111 margin: Margin::default(),
112 style: Default::default(),
113 wrap: None,
114 text: text.into(),
115 scroll: (0, 0),
116 alignment: Alignment::Left,
117 content_height: 0,
118 content_width: 0,
119 text_was_updated: true,
120 }
121 }
122
123 pub fn text<T>(&mut self, text: T)
124 where
125 T: Into<Text<'a>>,
126 {
127 self.text = text.into();
128 self.text_was_updated = true;
129 }
130
131 pub fn block(&mut self, block: Block<'a>) {
132 self.block = Some(block);
133 }
134
135 pub fn scrollbar(&mut self, scrollbar: Scrollbar<'a>) {
146 self.scrollbar_direction = match scrollbar.show_orientation() {
147 ScrollbarOrientation::VerticalRight | ScrollbarOrientation::VerticalLeft => {
148 Some(Direction::Vertical)
149 }
150 _ => Some(Direction::Horizontal),
151 };
152
153 if self.block.is_none() {
154 self.block = Some(Block::default().borders(Borders::NONE));
155 }
156
157 self.scrollbar = Some(scrollbar);
158 }
159
160 pub fn text_style(&mut self, style: Style) {
161 self.text.patch_style(style);
162 }
163
164 pub fn style(&mut self, style: Style) {
165 self.style = style;
166 }
167
168 pub fn wrap(&mut self, wrap: Wrap) {
169 self.wrap = Some(wrap);
170 }
171
172 pub fn margin(&mut self, margin: u16) {
173 self.margin = Margin {
174 horizontal: margin,
175 vertical: margin,
176 };
177 }
178
179 pub fn horizontal_margin(&mut self, horizontal: u16) {
180 self.margin.horizontal = horizontal;
181 }
182
183 pub fn vertical_margin(&mut self, vertical: u16) {
184 self.margin.vertical = vertical;
185 }
186
187 pub fn scroll(&mut self, offset: (u16, u16)) {
201 self.scroll = offset;
202 }
203
204 pub fn alignment(&mut self, alignment: Alignment) {
205 self.alignment = alignment;
206 }
207
208 pub fn content_height(&self) -> Option<u16> {
210 match self.scrollbar_direction.as_ref() {
211 Some(Direction::Vertical) => Some(self.content_height),
212 _ => None,
213 }
214 }
215
216 pub fn content_width(&self) -> Option<u16> {
220 match self.scrollbar_direction.as_ref() {
221 Some(Direction::Horizontal) => Some(self.content_width),
222 _ => None,
223 }
224 }
225}
226
227impl<'a> Widget for Paragraph<'a> {
228 fn render(&mut self, area: Rect, buf: &mut Buffer) {
229 buf.set_style(area, self.style);
230 let text_area = match self.block.as_mut() {
231 Some(b) => {
232 let inner_area = b.inner(area);
233 b.render(area, buf);
234 inner_area
235 }
236 None => area,
237 };
238
239 let text_area = text_area.inner(&self.margin);
240
241 if !self.text_was_updated && self.scrollbar.is_some() {
242 self.scroll = (
243 self.scroll
244 .0
245 .min(self.content_height.saturating_sub(text_area.height)),
246 self.scroll
247 .1
248 .min(self.content_width.saturating_sub(text_area.width)),
249 )
250 }
251
252 if text_area.height < 1 {
253 return;
254 }
255
256 let style = self.style;
257 let mut styled = self.text.lines.iter().flat_map(|spans| {
258 spans
259 .0
260 .iter()
261 .flat_map(|span| span.styled_graphemes(style))
262 .chain(iter::once(StyledGrapheme {
265 symbol: "\n",
266 style: self.style,
267 }))
268 });
269
270 let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
271 Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
272 } else {
273 let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
274 if let Alignment::Left = self.alignment {
275 line_composer.set_horizontal_offset(self.scroll.1);
276 }
277 line_composer
278 };
279 let mut y = 0;
280 let mut max_line_width = 0;
281 let height = text_area.height + self.scroll.0;
282
283 while let Some((current_line, current_line_width)) = line_composer.next_line() {
284 if y >= self.scroll.0 && y < height {
285 let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
286 for StyledGrapheme { symbol, style } in current_line {
287 buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
288 .set_symbol(if symbol.is_empty() {
289 " "
292 } else {
293 symbol
294 })
295 .set_style(*style);
296 x += symbol.width() as u16;
297 }
298 }
299 y += 1;
300
301 if current_line_width > max_line_width && !self.text_was_updated {
302 max_line_width = current_line_width;
303 }
304
305 if y >= height && !self.text_was_updated {
306 break;
307 }
308 }
309
310 if self.text_was_updated {
311 self.content_height = y;
312 self.content_width = max_line_width;
313 self.text_was_updated = false;
314 }
315
316 if let (Some(scrollbar), Some(dir)) = (self.scrollbar.as_mut(), &self.scrollbar_direction) {
317 match dir {
318 Direction::Horizontal => {
319 scrollbar.offset(self.scroll.1);
320 scrollbar.content_length(self.content_width);
321 }
322 Direction::Vertical => {
323 scrollbar.offset(self.scroll.0);
324 scrollbar.content_length(self.content_height);
325 }
326 }
327 scrollbar.render(area, buf);
328 }
329 }
330}