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

veeso / tui-realm-stdlib / 22143900361

18 Feb 2026 02:31PM UTC coverage: 76.863% (+4.4%) from 72.486%
22143900361

Pull #58

github

web-flow
Merge 82269e219 into 237158c92
Pull Request #58: Remove unnecessary features

588 of 1352 new or added lines in 21 files covered. (43.49%)

40 existing lines in 15 files now uncovered.

3940 of 5126 relevant lines covered (76.86%)

4.91 hits per line

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

79.31
/src/components/table.rs
1
//! `Table` represents a read-only textual table component which can be scrollable through arrows or inactive.
2

3
use std::cmp::max;
4

5
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
6
use tuirealm::props::{
7
    AttrValue, Attribute, Borders, Color, LineStatic, PropPayload, PropValue, Props, Style,
8
    Table as PropTable, TextModifiers, Title,
9
};
10
use tuirealm::ratatui::{
11
    layout::{Constraint, Rect},
12
    text::Line,
13
    widgets::{Cell, Row, Table as TuiTable, TableState},
14
};
15
use tuirealm::{Frame, MockComponent, State, StateValue};
16

17
use super::props::TABLE_COLUMN_SPACING;
18
use crate::prop_ext::CommonProps;
19
use crate::utils::{self, borrow_clone_line};
20

21
// -- States
22

23
/// The state that has to be kept for the [`Table`] component.
24
#[derive(Default)]
25
pub struct TableStates {
26
    /// Index of selected item in textarea
27
    pub list_index: usize,
28
    /// Lines in text area
29
    pub list_len: usize,
30
}
31

32
impl TableStates {
33
    /// Set list length.
34
    pub fn set_list_len(&mut self, len: usize) {
14✔
35
        self.list_len = len;
14✔
36
    }
14✔
37

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

48
    /// Decrement list index.
49
    pub fn decr_list_index(&mut self, rewind: bool) {
20✔
50
        // Check if index is bigger than 0
10✔
51
        if self.list_index > 0 {
20✔
52
            self.list_index -= 1;
16✔
53
        } else if rewind && self.list_len > 0 {
16✔
54
            self.list_index = self.list_len - 1;
2✔
55
        }
2✔
56
    }
20✔
57

58
    /// Keep index if possible, otherwise set to `lenght - 1`.
59
    pub fn fix_list_index(&mut self) {
16✔
60
        if self.list_index >= self.list_len && self.list_len > 0 {
16✔
61
            self.list_index = self.list_len - 1;
4✔
62
        } else if self.list_len == 0 {
12✔
63
            self.list_index = 0;
×
64
        }
12✔
65
    }
16✔
66

67
    /// Set list index to the first item in the list.
68
    pub fn list_index_at_first(&mut self) {
4✔
69
        self.list_index = 0;
4✔
70
    }
4✔
71

72
    /// Set list index at the last item of the list.
73
    pub fn list_index_at_last(&mut self) {
4✔
74
        if self.list_len > 0 {
4✔
75
            self.list_index = self.list_len - 1;
4✔
76
        } else {
4✔
77
            self.list_index = 0;
×
78
        }
×
79
    }
4✔
80

81
    /// Calculate the max step ahead to scroll list.
82
    #[must_use]
83
    pub fn calc_max_step_ahead(&self, max: usize) -> usize {
4✔
84
        let remaining: usize = match self.list_len {
4✔
85
            0 => 0,
×
86
            len => len - 1 - self.list_index,
4✔
87
        };
88
        if remaining > max { max } else { remaining }
4✔
89
    }
4✔
90

91
    /// Calculate the max step ahead to scroll list.
92
    #[must_use]
93
    pub fn calc_max_step_behind(&self, max: usize) -> usize {
4✔
94
        if self.list_index > max {
4✔
95
            max
2✔
96
        } else {
97
            self.list_index
2✔
98
        }
99
    }
4✔
100
}
101

102
// -- Component
103

104
/// `Table` represents a read-only textual table component which can be scrollable through arrows or inactive.
105
#[derive(Default)]
106
#[must_use]
107
pub struct Table {
108
    common: CommonProps,
109
    props: Props,
110
    pub states: TableStates,
111
}
112

113
impl Table {
114
    /// Set the main foreground color. This may get overwritten by individual text styles.
115
    pub fn foreground(mut self, fg: Color) -> Self {
6✔
116
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
6✔
117
        self
6✔
118
    }
6✔
119

120
    /// Set the main background color. This may get overwritten by individual text styles.
121
    pub fn background(mut self, bg: Color) -> Self {
6✔
122
        self.attr(Attribute::Background, AttrValue::Color(bg));
6✔
123
        self
6✔
124
    }
6✔
125

126
    /// Set the main text modifiers. This may get overwritten by individual text styles.
127
    pub fn modifiers(mut self, m: TextModifiers) -> Self {
6✔
128
        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
6✔
129
        self
6✔
130
    }
6✔
131

132
    /// Set the main style. This may get overwritten by individual text styles.
133
    ///
134
    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
NEW
135
    pub fn style(mut self, style: Style) -> Self {
×
NEW
136
        self.attr(Attribute::Style, AttrValue::Style(style));
×
NEW
137
        self
×
NEW
138
    }
×
139

140
    /// Set a custom style for the border when the component is unfocused.
NEW
141
    pub fn inactive(mut self, s: Style) -> Self {
×
NEW
142
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
×
UNCOV
143
        self
×
UNCOV
144
    }
×
145

146
    /// Add a border to the component.
147
    pub fn borders(mut self, b: Borders) -> Self {
6✔
148
        self.attr(Attribute::Borders, AttrValue::Borders(b));
6✔
149
        self
6✔
150
    }
6✔
151

152
    /// Add a title to the component.
153
    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
6✔
154
        self.attr(Attribute::Title, AttrValue::Title(title.into()));
6✔
155
        self
6✔
156
    }
6✔
157

158
    /// Set the scroll stepping to use on `Cmd::Scroll(Direction::Up)` or `Cmd::Scroll(Direction::Down)`.
159
    pub fn step(mut self, step: usize) -> Self {
2✔
160
        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
2✔
161
        self
2✔
162
    }
2✔
163

164
    /// Should the list be scrollable or always show only the top (0th) element?
165
    pub fn scroll(mut self, scrollable: bool) -> Self {
4✔
166
        self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
4✔
167
        self
4✔
168
    }
4✔
169

170
    /// Set the Symbol and Style for the indicator of the current line.
171
    pub fn highlighted_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
6✔
172
        self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
6✔
173
        self
6✔
174
    }
6✔
175

176
    /// Set a custom foreground color for the currently highlighted item.
177
    pub fn highlighted_color(mut self, c: Color) -> Self {
6✔
178
        // TODO: shouldnt this be a highlight style instead?
3✔
179
        self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
6✔
180
        self
6✔
181
    }
6✔
182

183
    /// Set custom spacing between columns.
184
    pub fn column_spacing(mut self, w: u16) -> Self {
4✔
185
        self.attr(Attribute::Custom(TABLE_COLUMN_SPACING), AttrValue::Size(w));
4✔
186
        self
4✔
187
    }
4✔
188

189
    /// Set a custom height for all rows.
190
    ///
191
    /// Default: `1`
192
    pub fn row_height(mut self, h: u16) -> Self {
4✔
193
        self.attr(Attribute::Height, AttrValue::Size(h));
4✔
194
        self
4✔
195
    }
4✔
196

197
    /// Set the widths of each column.
198
    pub fn widths(mut self, w: &[u16]) -> Self {
4✔
199
        // TODO: should this maybe be "Layout"?
2✔
200
        self.attr(
4✔
201
            Attribute::Width,
4✔
202
            AttrValue::Payload(PropPayload::Vec(
2✔
203
                w.iter().map(|x| PropValue::U16(*x)).collect(),
14✔
204
            )),
2✔
205
        );
2✔
206
        self
4✔
207
    }
4✔
208

209
    /// Set headers for columns.
210
    pub fn headers<S: Into<String>>(mut self, headers: impl IntoIterator<Item = S>) -> Self {
16✔
211
        self.attr(
16✔
212
            Attribute::Text,
16✔
213
            AttrValue::Payload(PropPayload::Vec(
8✔
214
                headers
16✔
215
                    .into_iter()
16✔
216
                    .map(|v| PropValue::Str(v.into()))
26✔
217
                    .collect(),
16✔
218
            )),
8✔
219
        );
8✔
220
        self
16✔
221
    }
16✔
222

223
    /// Set the data for the table.
224
    pub fn table(mut self, t: PropTable) -> Self {
8✔
225
        self.attr(Attribute::Content, AttrValue::Table(t));
8✔
226
        self
8✔
227
    }
8✔
228

229
    /// Set whether wraparound should be possible (down on the last choice wraps around to 0, and the other way around).
230
    pub fn rewind(mut self, r: bool) -> Self {
×
231
        self.attr(Attribute::Rewind, AttrValue::Flag(r));
×
232
        self
×
233
    }
×
234

235
    /// Set the initially selected line.
236
    pub fn selected_line(mut self, line: usize) -> Self {
2✔
237
        self.attr(
2✔
238
            Attribute::Value,
2✔
239
            AttrValue::Payload(PropPayload::Single(PropValue::Usize(line))),
2✔
240
        );
1✔
241
        self
2✔
242
    }
2✔
243

244
    /// ### scrollable
245
    ///
246
    /// returns the value of the scrollable flag; by default is false
247
    fn is_scrollable(&self) -> bool {
24✔
248
        self.props
24✔
249
            .get_or(Attribute::Scroll, AttrValue::Flag(false))
24✔
250
            .unwrap_flag()
24✔
251
    }
24✔
252

253
    fn rewindable(&self) -> bool {
4✔
254
        self.props
4✔
255
            .get_or(Attribute::Rewind, AttrValue::Flag(false))
4✔
256
            .unwrap_flag()
4✔
257
    }
4✔
258

259
    /// ### layout
260
    ///
261
    /// Returns layout based on properties.
262
    /// If layout is not set in properties, they'll be divided by rows number
263
    fn layout(&self) -> Vec<Constraint> {
4✔
264
        if let Some(PropPayload::Vec(widths)) =
2✔
265
            self.props.get(Attribute::Width).map(|x| x.unwrap_payload())
4✔
266
        {
267
            widths
2✔
268
                .iter()
2✔
269
                .cloned()
2✔
270
                .map(|x| x.unwrap_u16())
8✔
271
                .map(Constraint::Percentage)
2✔
272
                .collect()
2✔
273
        } else {
274
            // Get amount of columns (maximum len of row elements)
275
            let columns: usize = match self.props.get(Attribute::Content).map(|x| x.unwrap_table())
2✔
276
            {
277
                Some(rows) => rows.iter().map(|col| col.len()).max().unwrap_or(1),
2✔
278
                _ => 1,
×
279
            };
280
            // Calc width in equal way, make sure not to divide by zero (this can happen when rows is [[]])
281
            let width: u16 = (100 / max(columns, 1)) as u16;
2✔
282
            (0..columns)
2✔
283
                .map(|_| Constraint::Percentage(width))
2✔
284
                .collect()
2✔
285
        }
286
    }
4✔
287

288
    /// Generate [`Row`]s from a 2d vector of [`TextSpan`](tuirealm::props::TextSpan)s in props [`Attribute::Content`].
289
    fn make_rows(&self, row_height: u16) -> Vec<Row<'_>> {
×
290
        let Some(table) = self
×
291
            .props
×
292
            .get_ref(Attribute::Content)
×
293
            .and_then(|x| x.as_table())
×
294
        else {
295
            return Vec::new();
×
296
        };
297

298
        table
×
299
            .iter()
×
300
            .map(|row| {
×
301
                let columns: Vec<Cell> = row
×
302
                    .iter()
×
303
                    .map(|col| {
×
304
                        let line = Line::from(
×
305
                            col.spans
×
306
                                .iter()
×
307
                                .map(utils::borrow_clone_span)
×
308
                                .collect::<Vec<_>>(),
×
309
                        );
310
                        Cell::from(line)
×
311
                    })
×
312
                    .collect();
×
313
                Row::new(columns).height(row_height)
×
314
            })
×
315
            .collect() // Make List item from TextSpan
×
316
    }
×
317
}
318

319
impl MockComponent for Table {
320
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
NEW
321
        if !self.common.display {
×
NEW
322
            return;
×
NEW
323
        }
×
324

NEW
325
        let row_height = self
×
NEW
326
            .props
×
NEW
327
            .get_or(Attribute::Height, AttrValue::Size(1))
×
NEW
328
            .unwrap_size();
×
329
        // Make rows
NEW
330
        let rows: Vec<Row> = self.make_rows(row_height);
×
NEW
331
        let widths: Vec<Constraint> = self.layout();
×
332

NEW
333
        let mut widget = TuiTable::new(rows, &widths).style(self.common.style);
×
334

NEW
335
        if let Some(block) = self.common.get_block() {
×
NEW
336
            widget = widget.block(block);
×
NEW
337
        }
×
338

NEW
339
        let highlighted_color = self
×
NEW
340
            .props
×
NEW
341
            .get(Attribute::HighlightedColor)
×
NEW
342
            .map(|x| x.unwrap_color());
×
343

NEW
344
        if let Some(highlighted_color) = highlighted_color {
×
NEW
345
            widget =
×
NEW
346
                widget.row_highlight_style(Style::default().fg(highlighted_color).add_modifier(
×
NEW
347
                    if self.common.focused {
×
NEW
348
                        TextModifiers::REVERSED
×
349
                    } else {
NEW
350
                        TextModifiers::empty()
×
351
                    },
352
                ));
NEW
353
        }
×
354
        // Highlighted symbol
NEW
355
        let hg_str = self
×
NEW
356
            .props
×
NEW
357
            .get_ref(Attribute::HighlightedStr)
×
NEW
358
            .and_then(|x| x.as_textline());
×
NEW
359
        if let Some(hg_str) = hg_str {
×
NEW
360
            widget = widget.highlight_symbol(borrow_clone_line(hg_str));
×
NEW
361
        }
×
362
        // Col spacing
NEW
363
        if let Some(spacing) = self
×
NEW
364
            .props
×
NEW
365
            .get(Attribute::Custom(TABLE_COLUMN_SPACING))
×
NEW
366
            .map(|x| x.unwrap_size())
×
NEW
367
        {
×
NEW
368
            widget = widget.column_spacing(spacing);
×
NEW
369
        }
×
370
        // Header
NEW
371
        let headers: Vec<&str> = self
×
NEW
372
            .props
×
NEW
373
            .get_ref(Attribute::Text)
×
NEW
374
            .and_then(|v| v.as_payload())
×
NEW
375
            .and_then(|v| v.as_vec())
×
NEW
376
            .map(|v| {
×
NEW
377
                v.iter()
×
NEW
378
                    .filter_map(|v| v.as_str().map(|v| v.as_str()))
×
NEW
379
                    .collect()
×
NEW
380
            })
×
NEW
381
            .unwrap_or_default();
×
NEW
382
        if !headers.is_empty() {
×
NEW
383
            widget = widget.header(
×
NEW
384
                Row::new(headers)
×
NEW
385
                    .style(self.common.style)
×
NEW
386
                    .height(row_height),
×
NEW
387
            );
×
NEW
388
        }
×
NEW
389
        if self.is_scrollable() {
×
NEW
390
            let mut state: TableState = TableState::default();
×
NEW
391
            state.select(Some(self.states.list_index));
×
NEW
392
            render.render_stateful_widget(widget, area, &mut state);
×
NEW
393
        } else {
×
NEW
394
            render.render_widget(widget, area);
×
395
        }
×
396
    }
×
397

398
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
NEW
399
        if let Some(value) = self.common.get(attr) {
×
NEW
400
            return Some(value);
×
NEW
401
        }
×
402

403
        self.props.get(attr)
×
404
    }
×
405

406
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
90✔
407
        if let Some(value) = self.common.set(attr, value) {
90✔
408
            self.props.set(attr, value);
60✔
409
            if matches!(attr, Attribute::Content) {
60✔
410
                // Update list len and fix index
411
                self.states.set_list_len(
10✔
412
                    match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
10✔
413
                        Some(spans) => spans.len(),
10✔
NEW
414
                        _ => 0,
×
415
                    },
416
                );
417
                self.states.fix_list_index();
10✔
418
            } else if matches!(attr, Attribute::Value) && self.is_scrollable() {
50✔
419
                self.states.list_index = self
4✔
420
                    .props
4✔
421
                    .get(Attribute::Value)
4✔
422
                    .map_or(0, |x| x.unwrap_payload().unwrap_single().unwrap_usize());
4✔
423
                self.states.fix_list_index();
4✔
424
            }
46✔
425
        }
30✔
426
    }
90✔
427

428
    fn state(&self) -> State {
20✔
429
        if self.is_scrollable() {
20✔
430
            State::Single(StateValue::Usize(self.states.list_index))
18✔
431
        } else {
432
            State::None
2✔
433
        }
434
    }
20✔
435

436
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
16✔
437
        match cmd {
4✔
438
            Cmd::Move(Direction::Down) => {
439
                let prev = self.states.list_index;
2✔
440
                self.states.incr_list_index(self.rewindable());
2✔
441
                if prev == self.states.list_index {
2✔
442
                    CmdResult::None
×
443
                } else {
444
                    CmdResult::Changed(self.state())
2✔
445
                }
446
            }
447
            Cmd::Move(Direction::Up) => {
448
                let prev = self.states.list_index;
2✔
449
                self.states.decr_list_index(self.rewindable());
2✔
450
                if prev == self.states.list_index {
2✔
451
                    CmdResult::None
×
452
                } else {
453
                    CmdResult::Changed(self.state())
2✔
454
                }
455
            }
456
            Cmd::Scroll(Direction::Down) => {
457
                let prev = self.states.list_index;
4✔
458
                let step = self
4✔
459
                    .props
4✔
460
                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
4✔
461
                    .unwrap_length();
4✔
462
                let step: usize = self.states.calc_max_step_ahead(step);
4✔
463
                (0..step).for_each(|_| self.states.incr_list_index(false));
10✔
464
                if prev == self.states.list_index {
4✔
465
                    CmdResult::None
×
466
                } else {
467
                    CmdResult::Changed(self.state())
4✔
468
                }
469
            }
470
            Cmd::Scroll(Direction::Up) => {
471
                let prev = self.states.list_index;
4✔
472
                let step = self
4✔
473
                    .props
4✔
474
                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
4✔
475
                    .unwrap_length();
4✔
476
                let step: usize = self.states.calc_max_step_behind(step);
4✔
477
                (0..step).for_each(|_| self.states.decr_list_index(false));
12✔
478
                if prev == self.states.list_index {
4✔
479
                    CmdResult::None
×
480
                } else {
481
                    CmdResult::Changed(self.state())
4✔
482
                }
483
            }
484
            Cmd::GoTo(Position::Begin) => {
485
                let prev = self.states.list_index;
2✔
486
                self.states.list_index_at_first();
2✔
487
                if prev == self.states.list_index {
2✔
488
                    CmdResult::None
×
489
                } else {
490
                    CmdResult::Changed(self.state())
2✔
491
                }
492
            }
493
            Cmd::GoTo(Position::End) => {
494
                let prev = self.states.list_index;
2✔
495
                self.states.list_index_at_last();
2✔
496
                if prev == self.states.list_index {
2✔
497
                    CmdResult::None
×
498
                } else {
499
                    CmdResult::Changed(self.state())
2✔
500
                }
501
            }
502
            _ => CmdResult::None,
×
503
        }
504
    }
16✔
505
}
506

507
#[cfg(test)]
508
mod tests {
509

510
    use super::*;
511
    use pretty_assertions::assert_eq;
512
    use tuirealm::props::{HorizontalAlignment, TableBuilder};
513

514
    #[test]
515
    fn table_states() {
2✔
516
        let mut states = TableStates::default();
2✔
517
        assert_eq!(states.list_index, 0);
2✔
518
        assert_eq!(states.list_len, 0);
2✔
519
        states.set_list_len(5);
2✔
520
        assert_eq!(states.list_index, 0);
2✔
521
        assert_eq!(states.list_len, 5);
2✔
522
        // Incr
523
        states.incr_list_index(true);
2✔
524
        assert_eq!(states.list_index, 1);
2✔
525
        states.list_index = 4;
2✔
526
        states.incr_list_index(false);
2✔
527
        assert_eq!(states.list_index, 4);
2✔
528
        states.incr_list_index(true);
2✔
529
        assert_eq!(states.list_index, 0);
2✔
530
        // Decr
531
        states.decr_list_index(false);
2✔
532
        assert_eq!(states.list_index, 0);
2✔
533
        states.decr_list_index(true);
2✔
534
        assert_eq!(states.list_index, 4);
2✔
535
        states.decr_list_index(true);
2✔
536
        assert_eq!(states.list_index, 3);
2✔
537
        // Begin
538
        states.list_index_at_first();
2✔
539
        assert_eq!(states.list_index, 0);
2✔
540
        states.list_index_at_last();
2✔
541
        assert_eq!(states.list_index, 4);
2✔
542
        // Fix
543
        states.set_list_len(3);
2✔
544
        states.fix_list_index();
2✔
545
        assert_eq!(states.list_index, 2);
2✔
546
    }
2✔
547

548
    #[test]
549
    fn test_component_table_scrolling() {
2✔
550
        // Make component
1✔
551
        let mut component = Table::default()
2✔
552
            .foreground(Color::Red)
2✔
553
            .background(Color::Blue)
2✔
554
            .highlighted_color(Color::Yellow)
2✔
555
            .highlighted_str("🚀")
2✔
556
            .modifiers(TextModifiers::BOLD)
2✔
557
            .scroll(true)
2✔
558
            .step(4)
2✔
559
            .borders(Borders::default())
2✔
560
            .title(Title::from("events").alignment(HorizontalAlignment::Center))
2✔
561
            .column_spacing(4)
2✔
562
            .widths(&[25, 25, 25, 25])
2✔
563
            .row_height(3)
2✔
564
            .headers(["Event", "Message", "Behaviour", "???"])
2✔
565
            .table(
2✔
566
                TableBuilder::default()
2✔
567
                    .add_col(Line::from("KeyCode::Down"))
2✔
568
                    .add_col(Line::from("OnKey"))
2✔
569
                    .add_col(Line::from("Move cursor down"))
2✔
570
                    .add_row()
2✔
571
                    .add_col(Line::from("KeyCode::Up"))
2✔
572
                    .add_col(Line::from("OnKey"))
2✔
573
                    .add_col(Line::from("Move cursor up"))
2✔
574
                    .add_row()
2✔
575
                    .add_col(Line::from("KeyCode::PageDown"))
2✔
576
                    .add_col(Line::from("OnKey"))
2✔
577
                    .add_col(Line::from("Move cursor down by 8"))
2✔
578
                    .add_row()
2✔
579
                    .add_col(Line::from("KeyCode::PageUp"))
2✔
580
                    .add_col(Line::from("OnKey"))
2✔
581
                    .add_col(Line::from("ove cursor up by 8"))
2✔
582
                    .add_row()
2✔
583
                    .add_col(Line::from("KeyCode::End"))
2✔
584
                    .add_col(Line::from("OnKey"))
2✔
585
                    .add_col(Line::from("Move cursor to last item"))
2✔
586
                    .add_row()
2✔
587
                    .add_col(Line::from("KeyCode::Home"))
2✔
588
                    .add_col(Line::from("OnKey"))
2✔
589
                    .add_col(Line::from("Move cursor to first item"))
2✔
590
                    .add_row()
2✔
591
                    .add_col(Line::from("KeyCode::Char(_)"))
2✔
592
                    .add_col(Line::from("OnKey"))
2✔
593
                    .add_col(Line::from("Return pressed key"))
2✔
594
                    .add_col(Line::from("4th mysterious columns"))
2✔
595
                    .build(),
2✔
596
            );
1✔
597
        assert_eq!(component.states.list_len, 7);
2✔
598
        assert_eq!(component.states.list_index, 0);
2✔
599
        // Own funcs
600
        assert_eq!(component.layout().len(), 4);
2✔
601
        // Increment list index
602
        component.states.list_index += 1;
2✔
603
        assert_eq!(component.states.list_index, 1);
2✔
604
        // Check messages
605
        // Handle inputs
606
        assert_eq!(
2✔
607
            component.perform(Cmd::Move(Direction::Down)),
2✔
608
            CmdResult::Changed(State::Single(StateValue::Usize(2)))
1✔
609
        );
1✔
610
        // Index should be incremented
611
        assert_eq!(component.states.list_index, 2);
2✔
612
        // Index should be decremented
613
        assert_eq!(
2✔
614
            component.perform(Cmd::Move(Direction::Up)),
2✔
615
            CmdResult::Changed(State::Single(StateValue::Usize(1)))
1✔
616
        );
1✔
617
        // Index should be incremented
618
        assert_eq!(component.states.list_index, 1);
2✔
619
        // Index should be 2
620
        assert_eq!(
2✔
621
            component.perform(Cmd::Scroll(Direction::Down)),
2✔
622
            CmdResult::Changed(State::Single(StateValue::Usize(5)))
1✔
623
        );
1✔
624
        // Index should be incremented
625
        assert_eq!(component.states.list_index, 5);
2✔
626
        assert_eq!(
2✔
627
            component.perform(Cmd::Scroll(Direction::Down)),
2✔
628
            CmdResult::Changed(State::Single(StateValue::Usize(6)))
1✔
629
        );
1✔
630
        // Index should be incremented
631
        assert_eq!(component.states.list_index, 6);
2✔
632
        // Index should be 0
633
        assert_eq!(
2✔
634
            component.perform(Cmd::Scroll(Direction::Up)),
2✔
635
            CmdResult::Changed(State::Single(StateValue::Usize(2)))
1✔
636
        );
1✔
637
        assert_eq!(component.states.list_index, 2);
2✔
638
        assert_eq!(
2✔
639
            component.perform(Cmd::Scroll(Direction::Up)),
2✔
640
            CmdResult::Changed(State::Single(StateValue::Usize(0)))
1✔
641
        );
1✔
642
        assert_eq!(component.states.list_index, 0);
2✔
643
        // End
644
        assert_eq!(
2✔
645
            component.perform(Cmd::GoTo(Position::End)),
2✔
646
            CmdResult::Changed(State::Single(StateValue::Usize(6)))
1✔
647
        );
1✔
648
        assert_eq!(component.states.list_index, 6);
2✔
649
        // Home
650
        assert_eq!(
2✔
651
            component.perform(Cmd::GoTo(Position::Begin)),
2✔
652
            CmdResult::Changed(State::Single(StateValue::Usize(0)))
1✔
653
        );
1✔
654
        assert_eq!(component.states.list_index, 0);
2✔
655
        // Update
656
        component.attr(
2✔
657
            Attribute::Content,
2✔
658
            AttrValue::Table(
2✔
659
                TableBuilder::default()
2✔
660
                    .add_col(Line::from("name"))
2✔
661
                    .add_col(Line::from("age"))
2✔
662
                    .add_col(Line::from("birthdate"))
2✔
663
                    .build(),
2✔
664
            ),
2✔
665
        );
1✔
666
        assert_eq!(component.states.list_len, 1);
2✔
667
        assert_eq!(component.states.list_index, 0);
2✔
668
        // Get value
669
        assert_eq!(component.state(), State::Single(StateValue::Usize(0)));
2✔
670
    }
2✔
671

672
    #[test]
673
    fn test_component_table_with_empty_rows_and_no_width_set() {
2✔
674
        // Make component
1✔
675
        let component = Table::default().table(TableBuilder::default().build());
2✔
676

1✔
677
        assert_eq!(component.states.list_len, 1);
2✔
678
        assert_eq!(component.states.list_index, 0);
2✔
679
        // calculating layout would fail if no widths and using "empty" TableBuilder
680
        assert_eq!(component.layout().len(), 0);
2✔
681
    }
2✔
682

683
    #[test]
684
    fn test_components_table() {
2✔
685
        // Make component
1✔
686
        let component = Table::default()
2✔
687
            .foreground(Color::Red)
2✔
688
            .background(Color::Blue)
2✔
689
            .highlighted_color(Color::Yellow)
2✔
690
            .highlighted_str("🚀")
2✔
691
            .modifiers(TextModifiers::BOLD)
2✔
692
            .borders(Borders::default())
2✔
693
            .title(Title::from("events").alignment(HorizontalAlignment::Center))
2✔
694
            .column_spacing(4)
2✔
695
            .widths(&[33, 33, 33])
2✔
696
            .row_height(3)
2✔
697
            .headers(["Event", "Message", "Behaviour"])
2✔
698
            .table(
2✔
699
                TableBuilder::default()
2✔
700
                    .add_col(Line::from("KeyCode::Down"))
2✔
701
                    .add_col(Line::from("OnKey"))
2✔
702
                    .add_col(Line::from("Move cursor down"))
2✔
703
                    .add_row()
2✔
704
                    .add_col(Line::from("KeyCode::Up"))
2✔
705
                    .add_col(Line::from("OnKey"))
2✔
706
                    .add_col(Line::from("Move cursor up"))
2✔
707
                    .add_row()
2✔
708
                    .add_col(Line::from("KeyCode::PageDown"))
2✔
709
                    .add_col(Line::from("OnKey"))
2✔
710
                    .add_col(Line::from("Move cursor down by 8"))
2✔
711
                    .add_row()
2✔
712
                    .add_col(Line::from("KeyCode::PageUp"))
2✔
713
                    .add_col(Line::from("OnKey"))
2✔
714
                    .add_col(Line::from("ove cursor up by 8"))
2✔
715
                    .add_row()
2✔
716
                    .add_col(Line::from("KeyCode::End"))
2✔
717
                    .add_col(Line::from("OnKey"))
2✔
718
                    .add_col(Line::from("Move cursor to last item"))
2✔
719
                    .add_row()
2✔
720
                    .add_col(Line::from("KeyCode::Home"))
2✔
721
                    .add_col(Line::from("OnKey"))
2✔
722
                    .add_col(Line::from("Move cursor to first item"))
2✔
723
                    .add_row()
2✔
724
                    .add_col(Line::from("KeyCode::Char(_)"))
2✔
725
                    .add_col(Line::from("OnKey"))
2✔
726
                    .add_col(Line::from("Return pressed key"))
2✔
727
                    .build(),
2✔
728
            );
1✔
729
        // Get value (not scrollable)
1✔
730
        assert_eq!(component.state(), State::None);
2✔
731
    }
2✔
732

733
    #[test]
734
    fn should_init_list_value() {
2✔
735
        let mut component = Table::default()
2✔
736
            .foreground(Color::Red)
2✔
737
            .background(Color::Blue)
2✔
738
            .highlighted_color(Color::Yellow)
2✔
739
            .highlighted_str("🚀")
2✔
740
            .modifiers(TextModifiers::BOLD)
2✔
741
            .borders(Borders::default())
2✔
742
            .title(Title::from("events").alignment(HorizontalAlignment::Center))
2✔
743
            .table(
2✔
744
                TableBuilder::default()
2✔
745
                    .add_col(Line::from("KeyCode::Down"))
2✔
746
                    .add_col(Line::from("OnKey"))
2✔
747
                    .add_col(Line::from("Move cursor down"))
2✔
748
                    .add_row()
2✔
749
                    .add_col(Line::from("KeyCode::Up"))
2✔
750
                    .add_col(Line::from("OnKey"))
2✔
751
                    .add_col(Line::from("Move cursor up"))
2✔
752
                    .add_row()
2✔
753
                    .add_col(Line::from("KeyCode::PageDown"))
2✔
754
                    .add_col(Line::from("OnKey"))
2✔
755
                    .add_col(Line::from("Move cursor down by 8"))
2✔
756
                    .add_row()
2✔
757
                    .add_col(Line::from("KeyCode::PageUp"))
2✔
758
                    .add_col(Line::from("OnKey"))
2✔
759
                    .add_col(Line::from("ove cursor up by 8"))
2✔
760
                    .add_row()
2✔
761
                    .add_col(Line::from("KeyCode::End"))
2✔
762
                    .add_col(Line::from("OnKey"))
2✔
763
                    .add_col(Line::from("Move cursor to last item"))
2✔
764
                    .add_row()
2✔
765
                    .add_col(Line::from("KeyCode::Home"))
2✔
766
                    .add_col(Line::from("OnKey"))
2✔
767
                    .add_col(Line::from("Move cursor to first item"))
2✔
768
                    .add_row()
2✔
769
                    .add_col(Line::from("KeyCode::Char(_)"))
2✔
770
                    .add_col(Line::from("OnKey"))
2✔
771
                    .add_col(Line::from("Return pressed key"))
2✔
772
                    .build(),
2✔
773
            )
1✔
774
            .scroll(true)
2✔
775
            .selected_line(2);
2✔
776
        assert_eq!(component.states.list_index, 2);
2✔
777
        // Index out of bounds
778
        component.attr(
2✔
779
            Attribute::Value,
2✔
780
            AttrValue::Payload(PropPayload::Single(PropValue::Usize(50))),
2✔
781
        );
1✔
782
        assert_eq!(component.states.list_index, 6);
2✔
783
    }
2✔
784

785
    #[test]
786
    fn various_header_types() {
2✔
787
        // static array of static strings
1✔
788
        let _ = Table::default().headers(["hello"]);
2✔
789
        // static array of strings
1✔
790
        let _ = Table::default().headers(["hello".to_string()]);
2✔
791
        // vec of static strings
1✔
792
        let _ = Table::default().headers(vec!["hello"]);
2✔
793
        // vec of strings
1✔
794
        let _ = Table::default().headers(vec!["hello".to_string()]);
2✔
795
        // boxed array of static strings
1✔
796
        let _ = Table::default().headers(vec!["hello"].into_boxed_slice());
2✔
797
        // boxed array of strings
1✔
798
        let _ = Table::default().headers(vec!["hello".to_string()].into_boxed_slice());
2✔
799
    }
2✔
800
}
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