Skip to main content

rumatui_tui/widgets/
paragraph.rs

1use std::cell::Cell;
2use std::rc::Rc;
3
4use either::Either;
5use unicode_segmentation::UnicodeSegmentation;
6use unicode_width::UnicodeWidthStr;
7
8use crate::buffer::Buffer;
9use crate::layout::{Alignment, Rect, ScrollMode};
10use crate::style::Style;
11use crate::widgets::reflow::{LineComposer, LineTruncator, Styled, WordWrapper};
12use crate::widgets::scroll::{OffsetScroller, ScrolledLine, Scroller, TailScroller};
13use crate::widgets::{Block, Text, Widget};
14
15fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
16    match alignment {
17        Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
18        Alignment::Right => text_area_width.saturating_sub(line_width),
19        Alignment::Left => 0,
20    }
21}
22
23/// A widget to display some text.
24///
25/// # Examples
26///
27/// ```
28/// # use rumatui_tui::widgets::{Block, Borders, Paragraph, Text};
29/// # use rumatui_tui::style::{Style, Color};
30/// # use rumatui_tui::layout::{Alignment};
31/// let text = [
32///     Text::raw("First line\n"),
33///     Text::styled("Second line\n", Style::default().fg(Color::Red))
34/// ];
35/// Paragraph::new(text.iter())
36///     .block(Block::default().title("Paragraph").borders(Borders::ALL))
37///     .style(Style::default().fg(Color::White).bg(Color::Black))
38///     .alignment(Alignment::Center)
39///     .wrap(true);
40/// ```
41pub struct Paragraph<'a, 't, T>
42where
43    T: Iterator<Item = &'t Text<'t>>,
44{
45    /// A block to wrap the widget in
46    block: Option<Block<'a>>,
47    /// Widget style
48    style: Style,
49    /// Wrap the text or not
50    wrapping: bool,
51    /// The text to display
52    text: T,
53    /// Should we parse the text for embedded commands
54    raw: bool,
55    /// Scroll offset in number of lines
56    scroll: u16,
57    /// Indicates if scroll offset starts from top or bottom of content
58    scroll_mode: ScrollMode,
59    scroll_overflow_char: Option<char>,
60    /// Aligenment of the text
61    alignment: Alignment,
62    /// A flag that is passed in to inform the caller when the buffer
63    /// has overflown.
64    has_overflown: Option<Rc<Cell<bool>>>,
65    at_top: Option<Rc<Cell<bool>>>,
66}
67
68impl<'a, 't, T> Paragraph<'a, 't, T>
69where
70    T: Iterator<Item = &'t Text<'t>>,
71{
72    pub fn new(text: T) -> Paragraph<'a, 't, T> {
73        Paragraph {
74            block: None,
75            style: Default::default(),
76            wrapping: false,
77            raw: false,
78            text,
79            scroll: 0,
80            scroll_mode: ScrollMode::Normal,
81            scroll_overflow_char: None,
82            alignment: Alignment::Left,
83            has_overflown: None,
84            at_top: None,
85        }
86    }
87
88    pub fn block(mut self, block: Block<'a>) -> Paragraph<'a, 't, T> {
89        self.block = Some(block);
90        self
91    }
92
93    pub fn style(mut self, style: Style) -> Paragraph<'a, 't, T> {
94        self.style = style;
95        self
96    }
97
98    pub fn wrap(mut self, flag: bool) -> Paragraph<'a, 't, T> {
99        self.wrapping = flag;
100        self
101    }
102
103    pub fn raw(mut self, flag: bool) -> Paragraph<'a, 't, T> {
104        self.raw = flag;
105        self
106    }
107
108    pub fn scroll(mut self, offset: u16) -> Paragraph<'a, 't, T> {
109        self.scroll = offset;
110        self
111    }
112
113    pub fn scroll_mode(mut self, scroll_mode: ScrollMode) -> Paragraph<'a, 't, T> {
114        self.scroll_mode = scroll_mode;
115        self
116    }
117
118    pub fn scroll_overflow_char(
119        mut self,
120        scroll_overflow_char: Option<char>,
121    ) -> Paragraph<'a, 't, T> {
122        self.scroll_overflow_char = scroll_overflow_char;
123        self
124    }
125
126    pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a, 't, T> {
127        self.alignment = alignment;
128        self
129    }
130
131    pub fn did_overflow(mut self, over: Rc<Cell<bool>>) -> Paragraph<'a, 't, T> {
132        self.has_overflown = Some(over);
133        self
134    }
135
136    pub fn at_top(mut self, top: Rc<Cell<bool>>) -> Paragraph<'a, 't, T> {
137        self.at_top = Some(top);
138        self
139    }
140}
141
142impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T>
143where
144    T: Iterator<Item = &'t Text<'t>>,
145{
146    fn render(mut self, area: Rect, buf: &mut Buffer) {
147        let text_area = match self.block {
148            Some(ref mut b) => {
149                b.render(area, buf);
150                b.inner(area)
151            }
152            None => area,
153        };
154
155        if text_area.height < 1 {
156            return;
157        }
158
159        buf.set_background(text_area, self.style.bg);
160
161        let style = self.style;
162        let mut styled = self.text.by_ref().flat_map(|t| match *t {
163            Text::Raw(ref d) => {
164                let data: &'t str = d; // coerce to &str
165                Either::Left(UnicodeSegmentation::graphemes(data, true).map(|g| Styled(g, style)))
166            }
167            Text::Styled(ref d, s) => {
168                let data: &'t str = d; // coerce to &str
169                Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| Styled(g, s)))
170            }
171        });
172
173        let line_composer: Box<dyn LineComposer> = if self.wrapping {
174            Box::new(WordWrapper::new(&mut styled, text_area.width))
175        } else {
176            Box::new(LineTruncator::new(&mut styled, text_area.width))
177        };
178
179        let mut scrolled_lines: Box<dyn Scroller<'t>> = match self.scroll_mode {
180            ScrollMode::Normal => {
181                let scroller = OffsetScroller::new(self.scroll, line_composer);
182                Box::new(scroller)
183            }
184            ScrollMode::Tail => {
185                let over = self
186                    .has_overflown
187                    .unwrap_or_else(|| Rc::new(Cell::new(false)));
188
189                let scroller = TailScroller::new(
190                    self.scroll,
191                    line_composer,
192                    text_area.height,
193                    Rc::clone(&over),
194                );
195                Box::new(scroller)
196            }
197        };
198
199        for y in 0..text_area.height {
200            match scrolled_lines.next_line() {
201                Some(ScrolledLine::Line(current_line, current_line_width)) => {
202                    let mut x =
203                        get_line_offset(current_line_width, text_area.width, self.alignment);
204                    for Styled(symbol, style) in current_line {
205                        buf.get_mut(text_area.left() + x, text_area.top() + y)
206                            .set_symbol(symbol)
207                            .set_style(style);
208                        x += symbol.width() as u16;
209                    }
210                }
211                Some(ScrolledLine::Overflow) => {
212                    if let Some(top) = self.at_top.as_ref() {
213                        top.set(true);
214                    }
215
216                    if let Some(c) = self.scroll_overflow_char {
217                        buf.get_mut(text_area.left(), text_area.top() + y)
218                            .set_symbol(&c.to_string())
219                            .set_style(style);
220                    }
221                }
222                None => {}
223            }
224        }
225    }
226}