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

veeso / tui-realm-stdlib / 22147789116

09 Feb 2026 04:20PM UTC coverage: 77.303% (+4.8%) from 72.482%
22147789116

push

github

hasezoey
refactor(table): switch to use "CommonProps"

18 of 90 new or added lines in 1 file covered. (20.0%)

405 existing lines in 20 files now uncovered.

3944 of 5102 relevant lines covered (77.3%)

4.92 hits per line

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

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

5
use std::cmp::max;
6

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

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

23
// -- States
24

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

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

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

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

63
    /// ### fix_list_index
64
    ///
65
    /// Keep index if possible, otherwise set to lenght - 1
66
    pub fn fix_list_index(&mut self) {
16✔
67
        if self.list_index >= self.list_len && self.list_len > 0 {
16✔
68
            self.list_index = self.list_len - 1;
4✔
69
        } else if self.list_len == 0 {
12✔
70
            self.list_index = 0;
×
71
        }
12✔
72
    }
16✔
73

74
    /// ### list_index_at_first
75
    ///
76
    /// Set list index to the first item in the list
77
    pub fn list_index_at_first(&mut self) {
4✔
78
        self.list_index = 0;
4✔
79
    }
4✔
80

81
    /// ### list_index_at_last
82
    ///
83
    /// Set list index at the last item of the list
84
    pub fn list_index_at_last(&mut self) {
4✔
85
        if self.list_len > 0 {
4✔
86
            self.list_index = self.list_len - 1;
4✔
87
        } else {
4✔
88
            self.list_index = 0;
×
89
        }
×
90
    }
4✔
91

92
    /// ### calc_max_step_ahead
93
    ///
94
    /// Calculate the max step ahead to scroll list
95
    #[must_use]
96
    pub fn calc_max_step_ahead(&self, max: usize) -> usize {
4✔
97
        let remaining: usize = match self.list_len {
4✔
98
            0 => 0,
×
99
            len => len - 1 - self.list_index,
4✔
100
        };
101
        if remaining > max { max } else { remaining }
4✔
102
    }
4✔
103

104
    /// ### calc_max_step_ahead
105
    ///
106
    /// Calculate the max step ahead to scroll list
107
    #[must_use]
108
    pub fn calc_max_step_behind(&self, max: usize) -> usize {
4✔
109
        if self.list_index > max {
4✔
110
            max
2✔
111
        } else {
112
            self.list_index
2✔
113
        }
114
    }
4✔
115
}
116

117
// -- Component
118

119
/// ## Table
120
///
121
/// represents a read-only text component without any container.
122
#[derive(Default)]
123
#[must_use]
124
pub struct Table {
125
    common: CommonProps,
126
    props: Props,
127
    pub states: TableStates,
128
}
129

130
impl Table {
131
    /// Set the main foreground color. This may get overwritten by individual text styles.
132
    pub fn foreground(mut self, fg: Color) -> Self {
6✔
133
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
6✔
134
        self
6✔
135
    }
6✔
136

137
    /// Set the main background color. This may get overwritten by individual text styles.
138
    pub fn background(mut self, bg: Color) -> Self {
6✔
139
        self.attr(Attribute::Background, AttrValue::Color(bg));
6✔
140
        self
6✔
141
    }
6✔
142

143
    /// Set the main text modifiers. This may get overwritten by individual text styles.
144
    pub fn modifiers(mut self, m: TextModifiers) -> Self {
6✔
145
        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
6✔
146
        self
6✔
147
    }
6✔
148

149
    /// Set the main style. This may get overwritten by individual text styles.
150
    ///
151
    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
NEW
152
    pub fn style(mut self, style: Style) -> Self {
×
NEW
153
        self.attr(Attribute::Style, AttrValue::Style(style));
×
NEW
154
        self
×
NEW
155
    }
×
156

157
    /// Set a custom style for the border when the component is unfocused.
NEW
158
    pub fn inactive(mut self, s: Style) -> Self {
×
NEW
159
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
×
UNCOV
160
        self
×
UNCOV
161
    }
×
162

163
    /// Add a border to the component.
164
    pub fn borders(mut self, b: Borders) -> Self {
6✔
165
        self.attr(Attribute::Borders, AttrValue::Borders(b));
6✔
166
        self
6✔
167
    }
6✔
168

169
    /// Add a title to the component.
170
    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
6✔
171
        self.attr(Attribute::Title, AttrValue::Title(title.into()));
6✔
172
        self
6✔
173
    }
6✔
174

175
    /// Set the scroll stepping to use on `Cmd::Scroll(Direction::Up)` or `Cmd::Scroll(Direction::Down)`.
176
    pub fn step(mut self, step: usize) -> Self {
2✔
177
        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
2✔
178
        self
2✔
179
    }
2✔
180

181
    /// Should the list be scrollable or always show only the top (0th) element?
182
    pub fn scroll(mut self, scrollable: bool) -> Self {
4✔
183
        self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
4✔
184
        self
4✔
185
    }
4✔
186

187
    /// Set the Symbol and Style for the indicator of the current line.
188
    pub fn highlighted_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
6✔
189
        self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
6✔
190
        self
6✔
191
    }
6✔
192

193
    /// Set a custom foreground color for the currently highlighted item.
194
    pub fn highlighted_color(mut self, c: Color) -> Self {
6✔
195
        // TODO: shouldnt this be a highlight style instead?
3✔
196
        self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
6✔
197
        self
6✔
198
    }
6✔
199

200
    /// Set custom spacing between columns.
201
    pub fn column_spacing(mut self, w: u16) -> Self {
4✔
202
        self.attr(Attribute::Custom(TABLE_COLUMN_SPACING), AttrValue::Size(w));
4✔
203
        self
4✔
204
    }
4✔
205

206
    /// Set a custom height for all rows.
207
    ///
208
    /// Default: `1`
209
    pub fn row_height(mut self, h: u16) -> Self {
4✔
210
        self.attr(Attribute::Height, AttrValue::Size(h));
4✔
211
        self
4✔
212
    }
4✔
213

214
    /// Set the widths of each column.
215
    pub fn widths(mut self, w: &[u16]) -> Self {
4✔
216
        // TODO: should this maybe be "Layout"?
2✔
217
        self.attr(
4✔
218
            Attribute::Width,
4✔
219
            AttrValue::Payload(PropPayload::Vec(
2✔
220
                w.iter().map(|x| PropValue::U16(*x)).collect(),
14✔
221
            )),
2✔
222
        );
2✔
223
        self
4✔
224
    }
4✔
225

226
    /// Set headers for columns.
227
    pub fn headers<S: Into<String>>(mut self, headers: impl IntoIterator<Item = S>) -> Self {
16✔
228
        self.attr(
16✔
229
            Attribute::Text,
16✔
230
            AttrValue::Payload(PropPayload::Vec(
8✔
231
                headers
16✔
232
                    .into_iter()
16✔
233
                    .map(|v| PropValue::Str(v.into()))
26✔
234
                    .collect(),
16✔
235
            )),
8✔
236
        );
8✔
237
        self
16✔
238
    }
16✔
239

240
    /// Set the data for the table.
241
    pub fn table(mut self, t: PropTable) -> Self {
8✔
242
        self.attr(Attribute::Content, AttrValue::Table(t));
8✔
243
        self
8✔
244
    }
8✔
245

246
    /// Set whether wraparound should be possible (down on the last choice wraps around to 0, and the other way around).
247
    pub fn rewind(mut self, r: bool) -> Self {
×
248
        self.attr(Attribute::Rewind, AttrValue::Flag(r));
×
249
        self
×
250
    }
×
251

252
    /// Set the initially selected line.
253
    pub fn selected_line(mut self, line: usize) -> Self {
2✔
254
        self.attr(
2✔
255
            Attribute::Value,
2✔
256
            AttrValue::Payload(PropPayload::Single(PropValue::Usize(line))),
2✔
257
        );
1✔
258
        self
2✔
259
    }
2✔
260

261
    /// ### scrollable
262
    ///
263
    /// returns the value of the scrollable flag; by default is false
264
    fn is_scrollable(&self) -> bool {
24✔
265
        self.props
24✔
266
            .get_or(Attribute::Scroll, AttrValue::Flag(false))
24✔
267
            .unwrap_flag()
24✔
268
    }
24✔
269

270
    fn rewindable(&self) -> bool {
4✔
271
        self.props
4✔
272
            .get_or(Attribute::Rewind, AttrValue::Flag(false))
4✔
273
            .unwrap_flag()
4✔
274
    }
4✔
275

276
    /// ### layout
277
    ///
278
    /// Returns layout based on properties.
279
    /// If layout is not set in properties, they'll be divided by rows number
280
    fn layout(&self) -> Vec<Constraint> {
4✔
281
        if let Some(PropPayload::Vec(widths)) =
2✔
282
            self.props.get(Attribute::Width).map(|x| x.unwrap_payload())
4✔
283
        {
284
            widths
2✔
285
                .iter()
2✔
286
                .cloned()
2✔
287
                .map(|x| x.unwrap_u16())
8✔
288
                .map(Constraint::Percentage)
2✔
289
                .collect()
2✔
290
        } else {
291
            // Get amount of columns (maximum len of row elements)
292
            let columns: usize = match self.props.get(Attribute::Content).map(|x| x.unwrap_table())
2✔
293
            {
294
                Some(rows) => rows.iter().map(|col| col.len()).max().unwrap_or(1),
2✔
295
                _ => 1,
×
296
            };
297
            // Calc width in equal way, make sure not to divide by zero (this can happen when rows is [[]])
298
            let width: u16 = (100 / max(columns, 1)) as u16;
2✔
299
            (0..columns)
2✔
300
                .map(|_| Constraint::Percentage(width))
2✔
301
                .collect()
2✔
302
        }
303
    }
4✔
304

305
    /// Generate [`Row`]s from a 2d vector of [`TextSpan`](tuirealm::props::TextSpan)s in props [`Attribute::Content`].
306
    fn make_rows(&self, row_height: u16) -> Vec<Row<'_>> {
×
307
        let Some(table) = self
×
308
            .props
×
309
            .get_ref(Attribute::Content)
×
310
            .and_then(|x| x.as_table())
×
311
        else {
312
            return Vec::new();
×
313
        };
314

315
        table
×
316
            .iter()
×
317
            .map(|row| {
×
318
                let columns: Vec<Cell> = row
×
319
                    .iter()
×
320
                    .map(|col| {
×
321
                        let line = Line::from(
×
322
                            col.spans
×
323
                                .iter()
×
324
                                .map(utils::borrow_clone_span)
×
325
                                .collect::<Vec<_>>(),
×
326
                        );
327
                        Cell::from(line)
×
328
                    })
×
329
                    .collect();
×
330
                Row::new(columns).height(row_height)
×
331
            })
×
332
            .collect() // Make List item from TextSpan
×
333
    }
×
334
}
335

336
impl MockComponent for Table {
337
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
NEW
338
        if !self.common.display {
×
NEW
339
            return;
×
NEW
340
        }
×
341

NEW
342
        let row_height = self
×
NEW
343
            .props
×
NEW
344
            .get_or(Attribute::Height, AttrValue::Size(1))
×
NEW
345
            .unwrap_size();
×
346
        // Make rows
NEW
347
        let rows: Vec<Row> = self.make_rows(row_height);
×
NEW
348
        let widths: Vec<Constraint> = self.layout();
×
349

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

NEW
352
        if let Some(block) = self.common.get_block() {
×
NEW
353
            widget = widget.block(block);
×
NEW
354
        }
×
355

NEW
356
        let highlighted_color = self
×
NEW
357
            .props
×
NEW
358
            .get(Attribute::HighlightedColor)
×
NEW
359
            .map(|x| x.unwrap_color());
×
360

NEW
361
        if let Some(highlighted_color) = highlighted_color {
×
NEW
362
            widget =
×
NEW
363
                widget.row_highlight_style(Style::default().fg(highlighted_color).add_modifier(
×
NEW
364
                    if self.common.focused {
×
NEW
365
                        TextModifiers::REVERSED
×
366
                    } else {
NEW
367
                        TextModifiers::empty()
×
368
                    },
369
                ));
NEW
370
        }
×
371
        // Highlighted symbol
NEW
372
        let hg_str = self
×
NEW
373
            .props
×
NEW
374
            .get_ref(Attribute::HighlightedStr)
×
NEW
375
            .and_then(|x| x.as_textline());
×
NEW
376
        if let Some(hg_str) = hg_str {
×
NEW
377
            widget = widget.highlight_symbol(borrow_clone_line(hg_str));
×
NEW
378
        }
×
379
        // Col spacing
NEW
380
        if let Some(spacing) = self
×
NEW
381
            .props
×
NEW
382
            .get(Attribute::Custom(TABLE_COLUMN_SPACING))
×
NEW
383
            .map(|x| x.unwrap_size())
×
NEW
384
        {
×
NEW
385
            widget = widget.column_spacing(spacing);
×
NEW
386
        }
×
387
        // Header
NEW
388
        let headers: Vec<&str> = self
×
NEW
389
            .props
×
NEW
390
            .get_ref(Attribute::Text)
×
NEW
391
            .and_then(|v| v.as_payload())
×
NEW
392
            .and_then(|v| v.as_vec())
×
NEW
393
            .map(|v| {
×
NEW
394
                v.iter()
×
NEW
395
                    .filter_map(|v| v.as_str().map(|v| v.as_str()))
×
NEW
396
                    .collect()
×
NEW
397
            })
×
NEW
398
            .unwrap_or_default();
×
NEW
399
        if !headers.is_empty() {
×
NEW
400
            widget = widget.header(
×
NEW
401
                Row::new(headers)
×
NEW
402
                    .style(self.common.style)
×
NEW
403
                    .height(row_height),
×
NEW
404
            );
×
NEW
405
        }
×
NEW
406
        if self.is_scrollable() {
×
NEW
407
            let mut state: TableState = TableState::default();
×
NEW
408
            state.select(Some(self.states.list_index));
×
NEW
409
            render.render_stateful_widget(widget, area, &mut state);
×
NEW
410
        } else {
×
NEW
411
            render.render_widget(widget, area);
×
412
        }
×
413
    }
×
414

415
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
NEW
416
        if let Some(value) = self.common.get(attr) {
×
NEW
417
            return Some(value);
×
NEW
418
        }
×
419

420
        self.props.get(attr)
×
421
    }
×
422

423
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
90✔
424
        if let Some(value) = self.common.set(attr, value) {
90✔
425
            self.props.set(attr, value);
60✔
426
            if matches!(attr, Attribute::Content) {
60✔
427
                // Update list len and fix index
428
                self.states.set_list_len(
10✔
429
                    match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
10✔
430
                        Some(spans) => spans.len(),
10✔
NEW
431
                        _ => 0,
×
432
                    },
433
                );
434
                self.states.fix_list_index();
10✔
435
            } else if matches!(attr, Attribute::Value) && self.is_scrollable() {
50✔
436
                self.states.list_index = self
4✔
437
                    .props
4✔
438
                    .get(Attribute::Value)
4✔
439
                    .map_or(0, |x| x.unwrap_payload().unwrap_single().unwrap_usize());
4✔
440
                self.states.fix_list_index();
4✔
441
            }
46✔
442
        }
30✔
443
    }
90✔
444

445
    fn state(&self) -> State {
20✔
446
        if self.is_scrollable() {
20✔
447
            State::Single(StateValue::Usize(self.states.list_index))
18✔
448
        } else {
449
            State::None
2✔
450
        }
451
    }
20✔
452

453
    fn perform(&mut self, cmd: Cmd) -> CmdResult {
16✔
454
        match cmd {
4✔
455
            Cmd::Move(Direction::Down) => {
456
                let prev = self.states.list_index;
2✔
457
                self.states.incr_list_index(self.rewindable());
2✔
458
                if prev == self.states.list_index {
2✔
459
                    CmdResult::None
×
460
                } else {
461
                    CmdResult::Changed(self.state())
2✔
462
                }
463
            }
464
            Cmd::Move(Direction::Up) => {
465
                let prev = self.states.list_index;
2✔
466
                self.states.decr_list_index(self.rewindable());
2✔
467
                if prev == self.states.list_index {
2✔
468
                    CmdResult::None
×
469
                } else {
470
                    CmdResult::Changed(self.state())
2✔
471
                }
472
            }
473
            Cmd::Scroll(Direction::Down) => {
474
                let prev = self.states.list_index;
4✔
475
                let step = self
4✔
476
                    .props
4✔
477
                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
4✔
478
                    .unwrap_length();
4✔
479
                let step: usize = self.states.calc_max_step_ahead(step);
4✔
480
                (0..step).for_each(|_| self.states.incr_list_index(false));
10✔
481
                if prev == self.states.list_index {
4✔
482
                    CmdResult::None
×
483
                } else {
484
                    CmdResult::Changed(self.state())
4✔
485
                }
486
            }
487
            Cmd::Scroll(Direction::Up) => {
488
                let prev = self.states.list_index;
4✔
489
                let step = self
4✔
490
                    .props
4✔
491
                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
4✔
492
                    .unwrap_length();
4✔
493
                let step: usize = self.states.calc_max_step_behind(step);
4✔
494
                (0..step).for_each(|_| self.states.decr_list_index(false));
12✔
495
                if prev == self.states.list_index {
4✔
496
                    CmdResult::None
×
497
                } else {
498
                    CmdResult::Changed(self.state())
4✔
499
                }
500
            }
501
            Cmd::GoTo(Position::Begin) => {
502
                let prev = self.states.list_index;
2✔
503
                self.states.list_index_at_first();
2✔
504
                if prev == self.states.list_index {
2✔
505
                    CmdResult::None
×
506
                } else {
507
                    CmdResult::Changed(self.state())
2✔
508
                }
509
            }
510
            Cmd::GoTo(Position::End) => {
511
                let prev = self.states.list_index;
2✔
512
                self.states.list_index_at_last();
2✔
513
                if prev == self.states.list_index {
2✔
514
                    CmdResult::None
×
515
                } else {
516
                    CmdResult::Changed(self.state())
2✔
517
                }
518
            }
519
            _ => CmdResult::None,
×
520
        }
521
    }
16✔
522
}
523

524
#[cfg(test)]
525
mod tests {
526

527
    use super::*;
528
    use pretty_assertions::assert_eq;
529
    use tuirealm::props::{HorizontalAlignment, TableBuilder};
530

531
    #[test]
532
    fn table_states() {
2✔
533
        let mut states = TableStates::default();
2✔
534
        assert_eq!(states.list_index, 0);
2✔
535
        assert_eq!(states.list_len, 0);
2✔
536
        states.set_list_len(5);
2✔
537
        assert_eq!(states.list_index, 0);
2✔
538
        assert_eq!(states.list_len, 5);
2✔
539
        // Incr
540
        states.incr_list_index(true);
2✔
541
        assert_eq!(states.list_index, 1);
2✔
542
        states.list_index = 4;
2✔
543
        states.incr_list_index(false);
2✔
544
        assert_eq!(states.list_index, 4);
2✔
545
        states.incr_list_index(true);
2✔
546
        assert_eq!(states.list_index, 0);
2✔
547
        // Decr
548
        states.decr_list_index(false);
2✔
549
        assert_eq!(states.list_index, 0);
2✔
550
        states.decr_list_index(true);
2✔
551
        assert_eq!(states.list_index, 4);
2✔
552
        states.decr_list_index(true);
2✔
553
        assert_eq!(states.list_index, 3);
2✔
554
        // Begin
555
        states.list_index_at_first();
2✔
556
        assert_eq!(states.list_index, 0);
2✔
557
        states.list_index_at_last();
2✔
558
        assert_eq!(states.list_index, 4);
2✔
559
        // Fix
560
        states.set_list_len(3);
2✔
561
        states.fix_list_index();
2✔
562
        assert_eq!(states.list_index, 2);
2✔
563
    }
2✔
564

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

689
    #[test]
690
    fn test_component_table_with_empty_rows_and_no_width_set() {
2✔
691
        // Make component
1✔
692
        let component = Table::default().table(TableBuilder::default().build());
2✔
693

1✔
694
        assert_eq!(component.states.list_len, 1);
2✔
695
        assert_eq!(component.states.list_index, 0);
2✔
696
        // calculating layout would fail if no widths and using "empty" TableBuilder
697
        assert_eq!(component.layout().len(), 0);
2✔
698
    }
2✔
699

700
    #[test]
701
    fn test_components_table() {
2✔
702
        // Make component
1✔
703
        let component = Table::default()
2✔
704
            .foreground(Color::Red)
2✔
705
            .background(Color::Blue)
2✔
706
            .highlighted_color(Color::Yellow)
2✔
707
            .highlighted_str("🚀")
2✔
708
            .modifiers(TextModifiers::BOLD)
2✔
709
            .borders(Borders::default())
2✔
710
            .title(Title::from("events").alignment(HorizontalAlignment::Center))
2✔
711
            .column_spacing(4)
2✔
712
            .widths(&[33, 33, 33])
2✔
713
            .row_height(3)
2✔
714
            .headers(["Event", "Message", "Behaviour"])
2✔
715
            .table(
2✔
716
                TableBuilder::default()
2✔
717
                    .add_col(Line::from("KeyCode::Down"))
2✔
718
                    .add_col(Line::from("OnKey"))
2✔
719
                    .add_col(Line::from("Move cursor down"))
2✔
720
                    .add_row()
2✔
721
                    .add_col(Line::from("KeyCode::Up"))
2✔
722
                    .add_col(Line::from("OnKey"))
2✔
723
                    .add_col(Line::from("Move cursor up"))
2✔
724
                    .add_row()
2✔
725
                    .add_col(Line::from("KeyCode::PageDown"))
2✔
726
                    .add_col(Line::from("OnKey"))
2✔
727
                    .add_col(Line::from("Move cursor down by 8"))
2✔
728
                    .add_row()
2✔
729
                    .add_col(Line::from("KeyCode::PageUp"))
2✔
730
                    .add_col(Line::from("OnKey"))
2✔
731
                    .add_col(Line::from("ove cursor up by 8"))
2✔
732
                    .add_row()
2✔
733
                    .add_col(Line::from("KeyCode::End"))
2✔
734
                    .add_col(Line::from("OnKey"))
2✔
735
                    .add_col(Line::from("Move cursor to last item"))
2✔
736
                    .add_row()
2✔
737
                    .add_col(Line::from("KeyCode::Home"))
2✔
738
                    .add_col(Line::from("OnKey"))
2✔
739
                    .add_col(Line::from("Move cursor to first item"))
2✔
740
                    .add_row()
2✔
741
                    .add_col(Line::from("KeyCode::Char(_)"))
2✔
742
                    .add_col(Line::from("OnKey"))
2✔
743
                    .add_col(Line::from("Return pressed key"))
2✔
744
                    .build(),
2✔
745
            );
1✔
746
        // Get value (not scrollable)
1✔
747
        assert_eq!(component.state(), State::None);
2✔
748
    }
2✔
749

750
    #[test]
751
    fn should_init_list_value() {
2✔
752
        let mut component = Table::default()
2✔
753
            .foreground(Color::Red)
2✔
754
            .background(Color::Blue)
2✔
755
            .highlighted_color(Color::Yellow)
2✔
756
            .highlighted_str("🚀")
2✔
757
            .modifiers(TextModifiers::BOLD)
2✔
758
            .borders(Borders::default())
2✔
759
            .title(Title::from("events").alignment(HorizontalAlignment::Center))
2✔
760
            .table(
2✔
761
                TableBuilder::default()
2✔
762
                    .add_col(Line::from("KeyCode::Down"))
2✔
763
                    .add_col(Line::from("OnKey"))
2✔
764
                    .add_col(Line::from("Move cursor down"))
2✔
765
                    .add_row()
2✔
766
                    .add_col(Line::from("KeyCode::Up"))
2✔
767
                    .add_col(Line::from("OnKey"))
2✔
768
                    .add_col(Line::from("Move cursor up"))
2✔
769
                    .add_row()
2✔
770
                    .add_col(Line::from("KeyCode::PageDown"))
2✔
771
                    .add_col(Line::from("OnKey"))
2✔
772
                    .add_col(Line::from("Move cursor down by 8"))
2✔
773
                    .add_row()
2✔
774
                    .add_col(Line::from("KeyCode::PageUp"))
2✔
775
                    .add_col(Line::from("OnKey"))
2✔
776
                    .add_col(Line::from("ove cursor up by 8"))
2✔
777
                    .add_row()
2✔
778
                    .add_col(Line::from("KeyCode::End"))
2✔
779
                    .add_col(Line::from("OnKey"))
2✔
780
                    .add_col(Line::from("Move cursor to last item"))
2✔
781
                    .add_row()
2✔
782
                    .add_col(Line::from("KeyCode::Home"))
2✔
783
                    .add_col(Line::from("OnKey"))
2✔
784
                    .add_col(Line::from("Move cursor to first item"))
2✔
785
                    .add_row()
2✔
786
                    .add_col(Line::from("KeyCode::Char(_)"))
2✔
787
                    .add_col(Line::from("OnKey"))
2✔
788
                    .add_col(Line::from("Return pressed key"))
2✔
789
                    .build(),
2✔
790
            )
1✔
791
            .scroll(true)
2✔
792
            .selected_line(2);
2✔
793
        assert_eq!(component.states.list_index, 2);
2✔
794
        // Index out of bounds
795
        component.attr(
2✔
796
            Attribute::Value,
2✔
797
            AttrValue::Payload(PropPayload::Single(PropValue::Usize(50))),
2✔
798
        );
1✔
799
        assert_eq!(component.states.list_index, 6);
2✔
800
    }
2✔
801

802
    #[test]
803
    fn various_header_types() {
2✔
804
        // static array of static strings
1✔
805
        let _ = Table::default().headers(["hello"]);
2✔
806
        // static array of strings
1✔
807
        let _ = Table::default().headers(["hello".to_string()]);
2✔
808
        // vec of static strings
1✔
809
        let _ = Table::default().headers(vec!["hello"]);
2✔
810
        // vec of strings
1✔
811
        let _ = Table::default().headers(vec!["hello".to_string()]);
2✔
812
        // boxed array of static strings
1✔
813
        let _ = Table::default().headers(vec!["hello"].into_boxed_slice());
2✔
814
        // boxed array of strings
1✔
815
        let _ = Table::default().headers(vec!["hello".to_string()].into_boxed_slice());
2✔
816
    }
2✔
817
}
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