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

veeso / tui-realm-stdlib / 15137208932

20 May 2025 12:13PM UTC coverage: 67.595% (-0.7%) from 68.289%
15137208932

push

github

web-flow
Fix clippy lints, apply some pedantic fixes and general small improvements (#32)

* style(examples): directly have values as a type instead of casting

* style(examples/utils): ignore unused warnings

* style: run clippy auto fix

and remove redundant tests from "label"

* style: apply some clippy pedantic auto fixes

* test: set specific strings for "should_panic"

So that other panics are catched as failed tests.

* style(bar_chart): remove casting to "u64" when "usize" is directly provided and needed

* refactor(table): move making rows to own function

To appease "clippy::too_many_lines" and slightly reduce nesting.

* style: add "#[must_use]" where applicable

To hint not to forget a value.

104 of 192 new or added lines in 19 files covered. (54.17%)

12 existing lines in 2 files now uncovered.

2985 of 4416 relevant lines covered (67.6%)

1.66 hits per line

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

66.52
/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, Style,
12
    TextModifiers, TextSpan,
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) {
10✔
34
        self.list_len = len;
10✔
35
        self.fix_list_index();
10✔
36
    }
10✔
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) {
19✔
62
        if self.list_index >= self.list_len && self.list_len > 0 {
19✔
63
            self.list_index = self.list_len - 1;
×
64
        } else if self.list_len == 0 {
19✔
65
            self.list_index = 0;
14✔
66
        }
14✔
67
    }
19✔
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 {
1✔
96
            max
×
97
        } else {
98
            remaining
1✔
99
        }
100
    }
1✔
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 {
1✔
106
        if self.list_index > max {
1✔
107
            max
×
108
        } else {
109
            self.list_index
1✔
110
        }
111
    }
1✔
112
}
113

114
// -- Component
115

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

126
impl Textarea {
127
    pub fn foreground(mut self, fg: Color) -> Self {
1✔
128
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
1✔
129
        self
1✔
130
    }
1✔
131

132
    pub fn background(mut self, bg: Color) -> Self {
1✔
133
        self.attr(Attribute::Background, AttrValue::Color(bg));
1✔
134
        self
1✔
135
    }
1✔
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 {
1✔
143
        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
1✔
144
        self
1✔
145
    }
1✔
146

147
    pub fn borders(mut self, b: Borders) -> Self {
1✔
148
        self.attr(Attribute::Borders, AttrValue::Borders(b));
1✔
149
        self
1✔
150
    }
1✔
151

152
    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
1✔
153
        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
1✔
154
        self
1✔
155
    }
1✔
156

157
    pub fn step(mut self, step: usize) -> Self {
1✔
158
        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
1✔
159
        self
1✔
160
    }
1✔
161

162
    pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
1✔
163
        self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
1✔
164
        self
1✔
165
    }
1✔
166

167
    pub fn text_rows(mut self, rows: &[TextSpan]) -> Self {
1✔
168
        self.states.set_list_len(rows.len());
1✔
169
        self.attr(
1✔
170
            Attribute::Text,
1✔
171
            AttrValue::Payload(PropPayload::Vec(
1✔
172
                rows.iter().cloned().map(PropValue::TextSpan).collect(),
1✔
173
            )),
1✔
174
        );
175
        self
1✔
176
    }
1✔
177
}
178

179
impl 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
            let hg_str = self
×
186
                .props
×
187
                .get_ref(Attribute::HighlightedStr)
×
188
                .and_then(|x| x.as_string());
×
189
            // NOTE: wrap width is width of area minus 2 (block) minus width of highlighting string
NEW
190
            let wrap_width = (area.width as usize) - hg_str.as_ref().map_or(0, |x| x.width()) - 2;
×
191
            let lines: Vec<ListItem> = match self
×
192
                .props
×
193
                .get_ref(Attribute::Text)
×
194
                .and_then(|x| x.as_payload())
×
195
            {
196
                Some(PropPayload::Vec(spans)) => spans
×
197
                    .iter()
×
198
                    // this will skip any "PropValue" that is not a "TextSpan", instead of panicing
NEW
199
                    .filter_map(|x| x.as_text_span())
×
200
                    .map(|x| crate::utils::wrap_spans(&[x], wrap_width, &self.props))
×
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 = crate::utils::get_title_or_center(&self.props);
×
221
            let borders = self
×
222
                .props
×
223
                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
224
                .unwrap_borders();
×
225
            let focus = self
×
226
                .props
×
227
                .get_or(Attribute::Focus, AttrValue::Flag(false))
×
228
                .unwrap_flag();
×
229
            let inactive_style = self
×
230
                .props
×
231
                .get(Attribute::FocusStyle)
×
232
                .map(|x| x.unwrap_style());
×
233
            let mut state: ListState = ListState::default();
×
234
            state.select(Some(self.states.list_index));
×
235
            // Make component
236

237
            let mut list = List::new(lines)
×
238
                .block(crate::utils::get_block(
×
239
                    borders,
×
240
                    Some(&title),
×
241
                    focus,
×
242
                    inactive_style,
×
243
                ))
244
                .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
×
245
                .style(
×
246
                    Style::default()
×
247
                        .fg(foreground)
×
248
                        .bg(background)
×
249
                        .add_modifier(modifiers),
×
250
                );
251

252
            if let Some(hg_str) = hg_str {
×
253
                list = list.highlight_symbol(hg_str);
×
254
            }
×
255
            render.render_stateful_widget(list, area, &mut state);
×
256
        }
×
257
    }
×
258

259
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
260
        self.props.get(attr)
×
261
    }
×
262

263
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
9✔
264
        self.props.set(attr, value);
9✔
265
        // Update list len and fix index
266
        self.states.set_list_len(
9✔
267
            match self.props.get(Attribute::Text).map(|x| x.unwrap_payload()) {
9✔
268
                Some(PropPayload::Vec(spans)) => spans.len(),
2✔
269
                _ => 0,
7✔
270
            },
271
        );
272
        self.states.fix_list_index();
9✔
273
    }
9✔
274

275
    fn state(&self) -> State {
1✔
276
        State::None
1✔
277
    }
1✔
278

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

315
#[cfg(test)]
316
mod tests {
317

318
    use super::*;
319

320
    use pretty_assertions::assert_eq;
321

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