• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

veeso / tui-realm-stdlib / 20434720435

22 Dec 2025 02:26PM UTC coverage: 69.86% (-0.3%) from 70.169%
20434720435

Pull #46

github

web-flow
Merge c1529bfb4 into 9528c3045
Pull Request #46: Apply changes for core `TextSpan` changes

221 of 271 new or added lines in 6 files covered. (81.55%)

3 existing lines in 2 files now uncovered.

3187 of 4562 relevant lines covered (69.86%)

2.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.09
/src/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

7
extern crate unicode_width;
8

9
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
10
use tuirealm::props::{
11
    Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, SpanStatic,
12
    Style, TextModifiers,
13
};
14
use tuirealm::ratatui::{
15
    layout::Rect,
16
    widgets::{List, ListItem, ListState},
17
};
18
use tuirealm::{Frame, MockComponent, State};
19
use unicode_width::UnicodeWidthStr;
20

21
// -- States
22

23
#[derive(Default)]
24
pub struct TextareaStates {
25
    pub list_index: usize, // Index of selected item in textarea
26
    pub list_len: usize,   // Lines in text area
27
}
28

29
impl TextareaStates {
30
    /// ### set_list_len
31
    ///
32
    /// Set list length and fix list index
33
    pub fn set_list_len(&mut self, len: usize) {
18✔
34
        self.list_len = len;
18✔
35
        self.fix_list_index();
18✔
36
    }
18✔
37

38
    /// ### incr_list_index
39
    ///
40
    /// Incremenet list index
41
    pub fn incr_list_index(&mut self) {
2✔
42
        // Check if index is at last element
43
        if self.list_index + 1 < self.list_len {
2✔
44
            self.list_index += 1;
2✔
45
        }
2✔
46
    }
2✔
47

48
    /// ### decr_list_index
49
    ///
50
    /// Decrement list index
51
    pub fn decr_list_index(&mut self) {
3✔
52
        // Check if index is bigger than 0
53
        if self.list_index > 0 {
3✔
54
            self.list_index -= 1;
3✔
55
        }
3✔
56
    }
3✔
57

58
    /// ### fix_list_index
59
    ///
60
    /// Keep index if possible, otherwise set to lenght - 1
61
    pub fn fix_list_index(&mut self) {
31✔
62
        if self.list_index >= self.list_len && self.list_len > 0 {
31✔
63
            self.list_index = self.list_len - 1;
×
64
        } else if self.list_len == 0 {
31✔
65
            self.list_index = 0;
14✔
66
        }
17✔
67
    }
31✔
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) {
1✔
73
        self.list_index = 0;
1✔
74
    }
1✔
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) {
1✔
80
        if self.list_len > 0 {
1✔
81
            self.list_index = self.list_len - 1;
1✔
82
        } else {
1✔
83
            self.list_index = 0;
×
84
        }
×
85
    }
1✔
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 {
1✔
91
        let remaining: usize = match self.list_len {
1✔
92
            0 => 0,
×
93
            len => len - 1 - self.list_index,
1✔
94
        };
95
        if remaining > max { max } else { remaining }
1✔
96
    }
1✔
97

98
    /// ### calc_max_step_ahead
99
    ///
100
    /// Calculate the max step ahead to scroll list
101
    fn calc_max_step_behind(&self, max: usize) -> usize {
1✔
102
        if self.list_index > max {
1✔
103
            max
×
104
        } else {
105
            self.list_index
1✔
106
        }
107
    }
1✔
108
}
109

110
// -- Component
111

112
/// ## Textarea
113
///
114
/// represents a read-only text component without any container.
115
#[derive(Default)]
116
#[must_use]
117
pub struct Textarea {
118
    props: Props,
119
    pub states: TextareaStates,
120
}
121

122
impl Textarea {
123
    pub fn foreground(mut self, fg: Color) -> Self {
1✔
124
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
1✔
125
        self
1✔
126
    }
1✔
127

128
    pub fn background(mut self, bg: Color) -> Self {
1✔
129
        self.attr(Attribute::Background, AttrValue::Color(bg));
1✔
130
        self
1✔
131
    }
1✔
132

133
    pub fn inactive(mut self, s: Style) -> Self {
×
134
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
×
135
        self
×
136
    }
×
137

138
    pub fn modifiers(mut self, m: TextModifiers) -> Self {
1✔
139
        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
1✔
140
        self
1✔
141
    }
1✔
142

143
    pub fn borders(mut self, b: Borders) -> Self {
1✔
144
        self.attr(Attribute::Borders, AttrValue::Borders(b));
1✔
145
        self
1✔
146
    }
1✔
147

148
    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
1✔
149
        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
1✔
150
        self
1✔
151
    }
1✔
152

153
    pub fn step(mut self, step: usize) -> Self {
1✔
154
        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
1✔
155
        self
1✔
156
    }
1✔
157

158
    pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
1✔
159
        self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
1✔
160
        self
1✔
161
    }
1✔
162

163
    pub fn text_rows(mut self, s: impl IntoIterator<Item = SpanStatic>) -> Self {
5✔
164
        let rows: Vec<PropValue> = s.into_iter().map(PropValue::TextSpan).collect();
5✔
165
        self.states.set_list_len(rows.len());
5✔
166
        self.attr(Attribute::Text, AttrValue::Payload(PropPayload::Vec(rows)));
5✔
167
        self
5✔
168
    }
5✔
169
}
170

171
impl MockComponent for Textarea {
172
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
173
        // Make a Span
174
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
175
            // Make text items
176
            // Highlighted symbol
177
            let hg_str = self
×
178
                .props
×
179
                .get_ref(Attribute::HighlightedStr)
×
180
                .and_then(|x| x.as_string());
×
181
            // NOTE: wrap width is width of area minus 2 (block) minus width of highlighting string
182
            let wrap_width = (area.width as usize) - hg_str.as_ref().map_or(0, |x| x.width()) - 2;
×
183
            // TODO: refactor to use "Text"?
184
            let lines: Vec<ListItem> = match self
×
185
                .props
×
186
                .get_ref(Attribute::Text)
×
187
                .and_then(|x| x.as_payload())
×
188
            {
189
                Some(PropPayload::Vec(spans)) => spans
×
190
                    .iter()
×
191
                    // this will skip any "PropValue" that is not a "TextSpan", instead of panicing
NEW
192
                    .filter_map(|x| x.as_textspan())
×
NEW
193
                    .map(|x| crate::utils::wrap_spans(&[x], wrap_width))
×
194
                    .map(ListItem::new)
×
195
                    .collect(),
×
196
                _ => Vec::new(),
×
197
            };
198
            let foreground = self
×
199
                .props
×
200
                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
201
                .unwrap_color();
×
202
            let background = self
×
203
                .props
×
204
                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
205
                .unwrap_color();
×
206
            let modifiers = self
×
207
                .props
×
208
                .get_or(
×
209
                    Attribute::TextProps,
×
210
                    AttrValue::TextModifiers(TextModifiers::empty()),
×
211
                )
212
                .unwrap_text_modifiers();
×
213
            let title = crate::utils::get_title_or_center(&self.props);
×
214
            let borders = self
×
215
                .props
×
216
                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
217
                .unwrap_borders();
×
218
            let focus = self
×
219
                .props
×
220
                .get_or(Attribute::Focus, AttrValue::Flag(false))
×
221
                .unwrap_flag();
×
222
            let inactive_style = self
×
223
                .props
×
224
                .get(Attribute::FocusStyle)
×
225
                .map(|x| x.unwrap_style());
×
226
            let mut state: ListState = ListState::default();
×
227
            state.select(Some(self.states.list_index));
×
228
            // Make component
229

230
            let mut list = List::new(lines)
×
231
                .block(crate::utils::get_block(
×
232
                    borders,
×
233
                    Some(&title),
×
234
                    focus,
×
235
                    inactive_style,
×
236
                ))
237
                .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
×
238
                .style(
×
239
                    Style::default()
×
240
                        .fg(foreground)
×
241
                        .bg(background)
×
242
                        .add_modifier(modifiers),
×
243
                );
244

245
            if let Some(hg_str) = hg_str {
×
246
                list = list.highlight_symbol(hg_str);
×
247
            }
×
248
            render.render_stateful_widget(list, area, &mut state);
×
249
        }
×
250
    }
×
251

252
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
253
        self.props.get(attr)
×
254
    }
×
255

256
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
13✔
257
        self.props.set(attr, value);
13✔
258
        // Update list len and fix index
259
        self.states.set_list_len(
13✔
260
            match self.props.get(Attribute::Text).map(|x| x.unwrap_payload()) {
13✔
261
                Some(PropPayload::Vec(spans)) => spans.len(),
6✔
262
                _ => 0,
7✔
263
            },
264
        );
265
        self.states.fix_list_index();
13✔
266
    }
13✔
267

268
    fn state(&self) -> State {
1✔
269
        State::None
1✔
270
    }
1✔
271

272
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
7✔
273
        match cmd {
2✔
274
            Cmd::Move(Direction::Down) => {
1✔
275
                self.states.incr_list_index();
1✔
276
            }
1✔
277
            Cmd::Move(Direction::Up) => {
1✔
278
                self.states.decr_list_index();
1✔
279
            }
1✔
280
            Cmd::Scroll(Direction::Down) => {
281
                let step = self
1✔
282
                    .props
1✔
283
                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
1✔
284
                    .unwrap_length();
1✔
285
                let step = self.states.calc_max_step_ahead(step);
1✔
286
                (0..step).for_each(|_| self.states.incr_list_index());
1✔
287
            }
288
            Cmd::Scroll(Direction::Up) => {
289
                let step = self
1✔
290
                    .props
1✔
291
                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
1✔
292
                    .unwrap_length();
1✔
293
                let step = self.states.calc_max_step_behind(step);
1✔
294
                (0..step).for_each(|_| self.states.decr_list_index());
2✔
295
            }
296
            Cmd::GoTo(Position::Begin) => {
1✔
297
                self.states.list_index_at_first();
1✔
298
            }
1✔
299
            Cmd::GoTo(Position::End) => {
1✔
300
                self.states.list_index_at_last();
1✔
301
            }
1✔
302
            _ => {}
1✔
303
        }
304
        CmdResult::None
7✔
305
    }
7✔
306
}
307

308
#[cfg(test)]
309
mod tests {
310

311
    use super::*;
312

313
    use pretty_assertions::assert_eq;
314
    use tuirealm::ratatui::text::Span;
315

316
    #[test]
317
    fn test_components_textarea() {
1✔
318
        // Make component
319
        let mut component = Textarea::default()
1✔
320
            .foreground(Color::Red)
1✔
321
            .background(Color::Blue)
1✔
322
            .modifiers(TextModifiers::BOLD)
1✔
323
            .borders(Borders::default())
1✔
324
            .highlighted_str("🚀")
1✔
325
            .step(4)
1✔
326
            .title("textarea", Alignment::Center)
1✔
327
            .text_rows([Span::from("welcome to "), Span::from("tui-realm")]);
1✔
328
        // Increment list index
329
        component.states.list_index += 1;
1✔
330
        assert_eq!(component.states.list_index, 1);
1✔
331
        // Add one row
332
        component.attr(
1✔
333
            Attribute::Text,
1✔
334
            AttrValue::Payload(PropPayload::Vec(vec![
1✔
335
                PropValue::TextSpan(Span::from("welcome")),
1✔
336
                PropValue::TextSpan(Span::from("to")),
1✔
337
                PropValue::TextSpan(Span::from("tui-realm")),
1✔
338
            ])),
1✔
339
        );
340
        // Verify states
341
        assert_eq!(component.states.list_index, 1); // Kept
1✔
342
        assert_eq!(component.states.list_len, 3);
1✔
343
        // get value
344
        assert_eq!(component.state(), State::None);
1✔
345
        // Render
346
        assert_eq!(component.states.list_index, 1);
1✔
347
        // Handle inputs
348
        assert_eq!(
1✔
349
            component.perform(Cmd::Move(Direction::Down)),
1✔
350
            CmdResult::None
351
        );
352
        // Index should be incremented
353
        assert_eq!(component.states.list_index, 2);
1✔
354
        // Index should be decremented
355
        assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
1✔
356
        // Index should be incremented
357
        assert_eq!(component.states.list_index, 1);
1✔
358
        // Index should be 2
359
        assert_eq!(
1✔
360
            component.perform(Cmd::Scroll(Direction::Down)),
1✔
361
            CmdResult::None
362
        );
363
        // Index should be incremented
364
        assert_eq!(component.states.list_index, 2);
1✔
365
        // Index should be 0
366
        assert_eq!(
1✔
367
            component.perform(Cmd::Scroll(Direction::Up)),
1✔
368
            CmdResult::None
369
        );
370
        // End
371
        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
1✔
372
        assert_eq!(component.states.list_index, 2);
1✔
373
        // Home
374
        assert_eq!(
1✔
375
            component.perform(Cmd::GoTo(Position::Begin)),
1✔
376
            CmdResult::None
377
        );
378
        // Index should be incremented
379
        assert_eq!(component.states.list_index, 0);
1✔
380
        // On key
381
        assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
1✔
382
    }
1✔
383

384
    #[test]
385
    fn various_textrows_types() {
1✔
386
        // Vec
387
        let _ = Textarea::default().text_rows(vec![Span::raw("hello")]);
1✔
388
        // static array
389
        let _ = Textarea::default().text_rows([Span::raw("hello")]);
1✔
390
        // boxed array
391
        let _ = Textarea::default().text_rows(vec![Span::raw("hello")].into_boxed_slice());
1✔
392
        // already a iterator
393
        let _ = Textarea::default().text_rows(["Hello"].map(Span::raw));
1✔
394
    }
1✔
395
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc