tui_realm_stdlib/components/
paragraph.rs

1//! ## Paragraph
2//!
3//! `Paragraph` represents a read-only text component inside a container, the text is wrapped inside the container automatically
4//! using the [textwrap](https://docs.rs/textwrap/0.13.4/textwrap/) crate.
5//! The textarea supports multi-style spans.
6//! The component is not scrollable and doesn't handle any input. The text must then fit into the area.
7//! If you want scroll support, use a `Textarea` instead.
8
9use tuirealm::command::{Cmd, CmdResult};
10use tuirealm::props::{
11    Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
12    TextModifiers, TextSpan,
13};
14use tuirealm::ratatui::text::Line as Spans;
15use tuirealm::ratatui::{
16    layout::Rect,
17    text::Span,
18    widgets::{Paragraph as TuiParagraph, Wrap},
19};
20use tuirealm::{Frame, MockComponent, State};
21
22// -- Component
23
24/// ## Paragraph
25///
26/// represents a read-only text component without any container.
27#[derive(Default)]
28#[must_use]
29pub struct Paragraph {
30    props: Props,
31}
32
33impl Paragraph {
34    pub fn foreground(mut self, fg: Color) -> Self {
35        self.attr(Attribute::Foreground, AttrValue::Color(fg));
36        self
37    }
38
39    pub fn background(mut self, bg: Color) -> Self {
40        self.attr(Attribute::Background, AttrValue::Color(bg));
41        self
42    }
43
44    pub fn modifiers(mut self, m: TextModifiers) -> Self {
45        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
46        self
47    }
48
49    pub fn borders(mut self, b: Borders) -> Self {
50        self.attr(Attribute::Borders, AttrValue::Borders(b));
51        self
52    }
53
54    pub fn alignment(mut self, a: Alignment) -> Self {
55        self.attr(Attribute::Alignment, AttrValue::Alignment(a));
56        self
57    }
58
59    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
60        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
61        self
62    }
63
64    /// Set the text of the [`Paragraph`].
65    // This method takes a `IntoIterator` so that it is up to the user when a clone is necessary
66    pub fn text(mut self, s: impl IntoIterator<Item = TextSpan>) -> Self {
67        self.attr(
68            Attribute::Text,
69            AttrValue::Payload(PropPayload::Vec(
70                s.into_iter().map(PropValue::TextSpan).collect(),
71            )),
72        );
73        self
74    }
75
76    pub fn wrap(mut self, wrap: bool) -> Self {
77        self.attr(Attribute::TextWrap, AttrValue::Flag(wrap));
78        self
79    }
80}
81
82impl MockComponent for Paragraph {
83    fn view(&mut self, render: &mut Frame, area: Rect) {
84        // Make a Span
85        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
86            // Make text items
87            let payload = self
88                .props
89                .get_ref(Attribute::Text)
90                .and_then(|x| x.as_payload());
91            let text: Vec<Spans> = match payload {
92                Some(PropPayload::Vec(spans)) => spans
93                    .iter()
94                    // this will skip any "PropValue" that is not a "TextSpan", instead of panicing
95                    .filter_map(|x| x.as_text_span())
96                    .map(|x| {
97                        let (fg, bg, modifiers) =
98                            crate::utils::use_or_default_styles(&self.props, x);
99                        Spans::from(vec![Span::styled(
100                            &x.content,
101                            Style::default().add_modifier(modifiers).fg(fg).bg(bg),
102                        )])
103                    })
104                    .collect(),
105                _ => Vec::new(),
106            };
107            // Text properties
108            let alignment: Alignment = self
109                .props
110                .get_or(Attribute::Alignment, AttrValue::Alignment(Alignment::Left))
111                .unwrap_alignment();
112            // Wrap
113            let trim = self
114                .props
115                .get_or(Attribute::TextWrap, AttrValue::Flag(false))
116                .unwrap_flag();
117            let foreground = self
118                .props
119                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
120                .unwrap_color();
121            let background = self
122                .props
123                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
124                .unwrap_color();
125            let modifiers = self
126                .props
127                .get_or(
128                    Attribute::TextProps,
129                    AttrValue::TextModifiers(TextModifiers::empty()),
130                )
131                .unwrap_text_modifiers();
132            let borders = self
133                .props
134                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
135                .unwrap_borders();
136            let title = self
137                .props
138                .get_ref(Attribute::Title)
139                .and_then(|x| x.as_title());
140            let div = crate::utils::get_block(borders, title, true, None);
141            render.render_widget(
142                TuiParagraph::new(text)
143                    .block(div)
144                    .style(
145                        Style::default()
146                            .fg(foreground)
147                            .bg(background)
148                            .add_modifier(modifiers),
149                    )
150                    .alignment(alignment)
151                    .wrap(Wrap { trim }),
152                area,
153            );
154        }
155    }
156
157    fn query(&self, attr: Attribute) -> Option<AttrValue> {
158        self.props.get(attr)
159    }
160
161    fn attr(&mut self, attr: Attribute, value: AttrValue) {
162        self.props.set(attr, value);
163    }
164
165    fn state(&self) -> State {
166        State::None
167    }
168
169    fn perform(&mut self, _cmd: Cmd) -> CmdResult {
170        CmdResult::None
171    }
172}
173
174#[cfg(test)]
175mod tests {
176
177    use super::*;
178
179    use pretty_assertions::assert_eq;
180
181    #[test]
182    fn test_components_paragraph() {
183        let component = Paragraph::default()
184            .background(Color::Blue)
185            .foreground(Color::Red)
186            .modifiers(TextModifiers::BOLD)
187            .alignment(Alignment::Center)
188            .text([
189                TextSpan::from("Press "),
190                TextSpan::from("<ESC>").fg(Color::Cyan).bold(),
191                TextSpan::from(" to quit"),
192            ])
193            .wrap(true)
194            .title("title", Alignment::Center);
195        // Get value
196        assert_eq!(component.state(), State::None);
197    }
198
199    #[test]
200    fn various_text_types() {
201        // Vec
202        let _ = Paragraph::default().text(vec![TextSpan::new("hello")]);
203        // static array
204        let _ = Paragraph::default().text([TextSpan::new("hello")]);
205        // boxed array
206        let _ = Paragraph::default().text(vec![TextSpan::new("hello")].into_boxed_slice());
207        // already a iterator
208        let _ = Paragraph::default().text(["Hello"].map(TextSpan::new));
209    }
210}