tui_realm_stdlib/components/
textarea.rs

1//! ## Textarea
2//!
3//! `Textarea` 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 and it is scrollable with arrows.
6
7extern crate unicode_width;
8
9use tuirealm::command::{Cmd, CmdResult, Direction, Position};
10use tuirealm::props::{
11    Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
12    TextModifiers, TextSpan,
13};
14use tuirealm::ratatui::{
15    layout::Rect,
16    widgets::{List, ListItem, ListState},
17};
18use tuirealm::{Frame, MockComponent, State};
19use unicode_width::UnicodeWidthStr;
20
21// -- States
22
23#[derive(Default)]
24pub struct TextareaStates {
25    pub list_index: usize, // Index of selected item in textarea
26    pub list_len: usize,   // Lines in text area
27}
28
29impl TextareaStates {
30    /// ### set_list_len
31    ///
32    /// Set list length and fix list index
33    pub fn set_list_len(&mut self, len: usize) {
34        self.list_len = len;
35        self.fix_list_index();
36    }
37
38    /// ### incr_list_index
39    ///
40    /// Incremenet list index
41    pub fn incr_list_index(&mut self) {
42        // Check if index is at last element
43        if self.list_index + 1 < self.list_len {
44            self.list_index += 1;
45        }
46    }
47
48    /// ### decr_list_index
49    ///
50    /// Decrement list index
51    pub fn decr_list_index(&mut self) {
52        // Check if index is bigger than 0
53        if self.list_index > 0 {
54            self.list_index -= 1;
55        }
56    }
57
58    /// ### fix_list_index
59    ///
60    /// Keep index if possible, otherwise set to lenght - 1
61    pub fn fix_list_index(&mut self) {
62        if self.list_index >= self.list_len && self.list_len > 0 {
63            self.list_index = self.list_len - 1;
64        } else if self.list_len == 0 {
65            self.list_index = 0;
66        }
67    }
68
69    /// ### list_index_at_first
70    ///
71    /// Set list index to the first item in the list
72    pub fn list_index_at_first(&mut self) {
73        self.list_index = 0;
74    }
75
76    /// ### list_index_at_last
77    ///
78    /// Set list index at the last item of the list
79    pub fn list_index_at_last(&mut self) {
80        if self.list_len > 0 {
81            self.list_index = self.list_len - 1;
82        } else {
83            self.list_index = 0;
84        }
85    }
86
87    /// ### calc_max_step_ahead
88    ///
89    /// Calculate the max step ahead to scroll list
90    fn calc_max_step_ahead(&self, max: usize) -> usize {
91        let remaining: usize = match self.list_len {
92            0 => 0,
93            len => len - 1 - self.list_index,
94        };
95        if remaining > max {
96            max
97        } else {
98            remaining
99        }
100    }
101
102    /// ### calc_max_step_ahead
103    ///
104    /// Calculate the max step ahead to scroll list
105    fn calc_max_step_behind(&self, max: usize) -> usize {
106        if self.list_index > max {
107            max
108        } else {
109            self.list_index
110        }
111    }
112}
113
114// -- Component
115
116/// ## Textarea
117///
118/// represents a read-only text component without any container.
119#[derive(Default)]
120pub struct Textarea {
121    props: Props,
122    pub states: TextareaStates,
123    hg_str: Option<String>, // CRAP CRAP CRAP
124}
125
126impl Textarea {
127    pub fn foreground(mut self, fg: Color) -> Self {
128        self.attr(Attribute::Foreground, AttrValue::Color(fg));
129        self
130    }
131
132    pub fn background(mut self, bg: Color) -> Self {
133        self.attr(Attribute::Background, AttrValue::Color(bg));
134        self
135    }
136
137    pub fn inactive(mut self, s: Style) -> Self {
138        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
139        self
140    }
141
142    pub fn modifiers(mut self, m: TextModifiers) -> Self {
143        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
144        self
145    }
146
147    pub fn borders(mut self, b: Borders) -> Self {
148        self.attr(Attribute::Borders, AttrValue::Borders(b));
149        self
150    }
151
152    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
153        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
154        self
155    }
156
157    pub fn step(mut self, step: usize) -> Self {
158        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
159        self
160    }
161
162    pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
163        self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
164        self
165    }
166
167    pub fn text_rows(mut self, rows: &[TextSpan]) -> Self {
168        self.states.set_list_len(rows.len());
169        self.attr(
170            Attribute::Text,
171            AttrValue::Payload(PropPayload::Vec(
172                rows.iter().cloned().map(PropValue::TextSpan).collect(),
173            )),
174        );
175        self
176    }
177}
178
179impl MockComponent for Textarea {
180    fn view(&mut self, render: &mut Frame, area: Rect) {
181        // Make a Span
182        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
183            // Make text items
184            // Highlighted symbol
185            self.hg_str = self
186                .props
187                .get(Attribute::HighlightedStr)
188                .map(|x| x.unwrap_string());
189            // NOTE: wrap width is width of area minus 2 (block) minus width of highlighting string
190            let wrap_width =
191                (area.width as usize) - self.hg_str.as_ref().map(|x| x.width()).unwrap_or(0) - 2;
192            let lines: Vec<ListItem> =
193                match self.props.get(Attribute::Text).map(|x| x.unwrap_payload()) {
194                    Some(PropPayload::Vec(spans)) => spans
195                        .iter()
196                        .cloned()
197                        .map(|x| x.unwrap_text_span())
198                        .map(|x| {
199                            crate::utils::wrap_spans(vec![x].as_slice(), wrap_width, &self.props)
200                        })
201                        .map(ListItem::new)
202                        .collect(),
203                    _ => Vec::new(),
204                };
205            let foreground = self
206                .props
207                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
208                .unwrap_color();
209            let background = self
210                .props
211                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
212                .unwrap_color();
213            let modifiers = self
214                .props
215                .get_or(
216                    Attribute::TextProps,
217                    AttrValue::TextModifiers(TextModifiers::empty()),
218                )
219                .unwrap_text_modifiers();
220            let title = self
221                .props
222                .get_or(
223                    Attribute::Title,
224                    AttrValue::Title((String::default(), Alignment::Center)),
225                )
226                .unwrap_title();
227            let borders = self
228                .props
229                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
230                .unwrap_borders();
231            let focus = self
232                .props
233                .get_or(Attribute::Focus, AttrValue::Flag(false))
234                .unwrap_flag();
235            let inactive_style = self
236                .props
237                .get(Attribute::FocusStyle)
238                .map(|x| x.unwrap_style());
239            let mut state: ListState = ListState::default();
240            state.select(Some(self.states.list_index));
241            // Make component
242
243            let mut list = List::new(lines)
244                .block(crate::utils::get_block(
245                    borders,
246                    Some(title),
247                    focus,
248                    inactive_style,
249                ))
250                .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
251                .style(
252                    Style::default()
253                        .fg(foreground)
254                        .bg(background)
255                        .add_modifier(modifiers),
256                );
257
258            if let Some(hg_str) = &self.hg_str {
259                list = list.highlight_symbol(hg_str);
260            }
261            render.render_stateful_widget(list, area, &mut state);
262        }
263    }
264
265    fn query(&self, attr: Attribute) -> Option<AttrValue> {
266        self.props.get(attr)
267    }
268
269    fn attr(&mut self, attr: Attribute, value: AttrValue) {
270        self.props.set(attr, value);
271        // Update list len and fix index
272        self.states.set_list_len(
273            match self.props.get(Attribute::Text).map(|x| x.unwrap_payload()) {
274                Some(PropPayload::Vec(spans)) => spans.len(),
275                _ => 0,
276            },
277        );
278        self.states.fix_list_index();
279    }
280
281    fn state(&self) -> State {
282        State::None
283    }
284
285    fn perform(&mut self, cmd: Cmd) -> CmdResult {
286        match cmd {
287            Cmd::Move(Direction::Down) => {
288                self.states.incr_list_index();
289            }
290            Cmd::Move(Direction::Up) => {
291                self.states.decr_list_index();
292            }
293            Cmd::Scroll(Direction::Down) => {
294                let step = self
295                    .props
296                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
297                    .unwrap_length();
298                let step = self.states.calc_max_step_ahead(step);
299                (0..step).for_each(|_| self.states.incr_list_index());
300            }
301            Cmd::Scroll(Direction::Up) => {
302                let step = self
303                    .props
304                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
305                    .unwrap_length();
306                let step = self.states.calc_max_step_behind(step);
307                (0..step).for_each(|_| self.states.decr_list_index());
308            }
309            Cmd::GoTo(Position::Begin) => {
310                self.states.list_index_at_first();
311            }
312            Cmd::GoTo(Position::End) => {
313                self.states.list_index_at_last();
314            }
315            _ => {}
316        }
317        CmdResult::None
318    }
319}
320
321#[cfg(test)]
322mod tests {
323
324    use super::*;
325
326    use pretty_assertions::assert_eq;
327
328    #[test]
329    fn test_components_textarea() {
330        // Make component
331        let mut component = Textarea::default()
332            .foreground(Color::Red)
333            .background(Color::Blue)
334            .modifiers(TextModifiers::BOLD)
335            .borders(Borders::default())
336            .highlighted_str("🚀")
337            .step(4)
338            .title("textarea", Alignment::Center)
339            .text_rows(&[TextSpan::from("welcome to "), TextSpan::from("tui-realm")]);
340        // Increment list index
341        component.states.list_index += 1;
342        assert_eq!(component.states.list_index, 1);
343        // Add one row
344        component.attr(
345            Attribute::Text,
346            AttrValue::Payload(PropPayload::Vec(vec![
347                PropValue::TextSpan(TextSpan::from("welcome")),
348                PropValue::TextSpan(TextSpan::from("to")),
349                PropValue::TextSpan(TextSpan::from("tui-realm")),
350            ])),
351        );
352        // Verify states
353        assert_eq!(component.states.list_index, 1); // Kept
354        assert_eq!(component.states.list_len, 3);
355        // get value
356        assert_eq!(component.state(), State::None);
357        // Render
358        assert_eq!(component.states.list_index, 1);
359        // Handle inputs
360        assert_eq!(
361            component.perform(Cmd::Move(Direction::Down)),
362            CmdResult::None
363        );
364        // Index should be incremented
365        assert_eq!(component.states.list_index, 2);
366        // Index should be decremented
367        assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
368        // Index should be incremented
369        assert_eq!(component.states.list_index, 1);
370        // Index should be 2
371        assert_eq!(
372            component.perform(Cmd::Scroll(Direction::Down)),
373            CmdResult::None
374        );
375        // Index should be incremented
376        assert_eq!(component.states.list_index, 2);
377        // Index should be 0
378        assert_eq!(
379            component.perform(Cmd::Scroll(Direction::Up)),
380            CmdResult::None
381        );
382        // End
383        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
384        assert_eq!(component.states.list_index, 2);
385        // Home
386        assert_eq!(
387            component.perform(Cmd::GoTo(Position::Begin)),
388            CmdResult::None
389        );
390        // Index should be incremented
391        assert_eq!(component.states.list_index, 0);
392        // On key
393        assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
394    }
395}