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

veeso / tui-realm-stdlib / 21209471148

21 Jan 2026 12:24PM UTC coverage: 72.486% (-0.1%) from 72.632%
21209471148

Pull #53

github

web-flow
Merge 237158c92 into 95c949ee0
Pull Request #53: Apply changes for ratatui 0.30

43 of 90 new or added lines in 9 files covered. (47.78%)

1 existing line in 1 file now uncovered.

3662 of 5052 relevant lines covered (72.49%)

4.17 hits per line

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

75.75
/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::utils::{self, borrow_clone_line};
21

22
// -- States
23

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

30
impl TableStates {
31
    /// ### set_list_len
32
    ///
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
    /// ### incr_list_index
39
    ///
40
    /// Incremenet list index
41
    pub fn incr_list_index(&mut self, rewind: bool) {
18✔
42
        // Check if index is at last element
9✔
43
        if self.list_index + 1 < self.list_len {
18✔
44
            self.list_index += 1;
14✔
45
        } else if rewind {
14✔
46
            self.list_index = 0;
2✔
47
        }
2✔
48
    }
18✔
49

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

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

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

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

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

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

116
// -- Component
117

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

128
impl Table {
129
    pub fn foreground(mut self, fg: Color) -> Self {
6✔
130
        self.attr(Attribute::Foreground, AttrValue::Color(fg));
6✔
131
        self
6✔
132
    }
6✔
133

134
    pub fn background(mut self, bg: Color) -> Self {
6✔
135
        self.attr(Attribute::Background, AttrValue::Color(bg));
6✔
136
        self
6✔
137
    }
6✔
138

139
    pub fn inactive(mut self, s: Style) -> Self {
×
140
        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
×
141
        self
×
142
    }
×
143

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
    pub fn borders(mut self, b: Borders) -> Self {
6✔
150
        self.attr(Attribute::Borders, AttrValue::Borders(b));
6✔
151
        self
6✔
152
    }
6✔
153

154
    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
6✔
155
        self.attr(Attribute::Title, AttrValue::Title(title.into()));
6✔
156
        self
6✔
157
    }
6✔
158

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
    pub fn scroll(mut self, scrollable: bool) -> Self {
4✔
165
        self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
4✔
166
        self
4✔
167
    }
4✔
168

169
    pub fn highlighted_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
6✔
170
        self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
6✔
171
        self
6✔
172
    }
6✔
173

174
    pub fn highlighted_color(mut self, c: Color) -> Self {
6✔
175
        self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
6✔
176
        self
6✔
177
    }
6✔
178

179
    pub fn column_spacing(mut self, w: u16) -> Self {
4✔
180
        self.attr(Attribute::Custom(TABLE_COLUMN_SPACING), AttrValue::Size(w));
4✔
181
        self
4✔
182
    }
4✔
183

184
    pub fn row_height(mut self, h: u16) -> Self {
4✔
185
        self.attr(Attribute::Height, AttrValue::Size(h));
4✔
186
        self
4✔
187
    }
4✔
188

189
    pub fn widths(mut self, w: &[u16]) -> Self {
4✔
190
        self.attr(
4✔
191
            Attribute::Width,
4✔
192
            AttrValue::Payload(PropPayload::Vec(
2✔
193
                w.iter().map(|x| PropValue::U16(*x)).collect(),
14✔
194
            )),
2✔
195
        );
2✔
196
        self
4✔
197
    }
4✔
198

199
    pub fn headers<S: Into<String>>(mut self, headers: impl IntoIterator<Item = S>) -> Self {
16✔
200
        self.attr(
16✔
201
            Attribute::Text,
16✔
202
            AttrValue::Payload(PropPayload::Vec(
8✔
203
                headers
16✔
204
                    .into_iter()
16✔
205
                    .map(|v| PropValue::Str(v.into()))
26✔
206
                    .collect(),
16✔
207
            )),
8✔
208
        );
8✔
209
        self
16✔
210
    }
16✔
211

212
    pub fn table(mut self, t: PropTable) -> Self {
8✔
213
        self.attr(Attribute::Content, AttrValue::Table(t));
8✔
214
        self
8✔
215
    }
8✔
216

217
    pub fn rewind(mut self, r: bool) -> Self {
×
218
        self.attr(Attribute::Rewind, AttrValue::Flag(r));
×
219
        self
×
220
    }
×
221

222
    /// Set initial selected line
223
    /// This method must be called after `rows` and `scrollable` in order to work
224
    pub fn selected_line(mut self, line: usize) -> Self {
2✔
225
        self.attr(
2✔
226
            Attribute::Value,
2✔
227
            AttrValue::Payload(PropPayload::One(PropValue::Usize(line))),
2✔
228
        );
1✔
229
        self
2✔
230
    }
2✔
231

232
    /// ### scrollable
233
    ///
234
    /// returns the value of the scrollable flag; by default is false
235
    fn is_scrollable(&self) -> bool {
24✔
236
        self.props
24✔
237
            .get_or(Attribute::Scroll, AttrValue::Flag(false))
24✔
238
            .unwrap_flag()
24✔
239
    }
24✔
240

241
    fn rewindable(&self) -> bool {
4✔
242
        self.props
4✔
243
            .get_or(Attribute::Rewind, AttrValue::Flag(false))
4✔
244
            .unwrap_flag()
4✔
245
    }
4✔
246

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

276
    /// Generate [`Row`]s from a 2d vector of [`TextSpan`](tuirealm::props::TextSpan)s in props [`Attribute::Content`].
277
    fn make_rows(&self, row_height: u16) -> Vec<Row<'_>> {
×
278
        let Some(table) = self
×
279
            .props
×
280
            .get_ref(Attribute::Content)
×
281
            .and_then(|x| x.as_table())
×
282
        else {
283
            return Vec::new();
×
284
        };
285

286
        table
×
287
            .iter()
×
288
            .map(|row| {
×
289
                let columns: Vec<Cell> = row
×
290
                    .iter()
×
291
                    .map(|col| {
×
292
                        let line = Line::from(
×
293
                            col.spans
×
294
                                .iter()
×
295
                                .map(utils::borrow_clone_span)
×
296
                                .collect::<Vec<_>>(),
×
297
                        );
298
                        Cell::from(line)
×
299
                    })
×
300
                    .collect();
×
301
                Row::new(columns).height(row_height)
×
302
            })
×
303
            .collect() // Make List item from TextSpan
×
304
    }
×
305
}
306

307
impl MockComponent for Table {
308
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
309
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
310
            let foreground = self
×
311
                .props
×
312
                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
313
                .unwrap_color();
×
314
            let background = self
×
315
                .props
×
316
                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
317
                .unwrap_color();
×
318
            let modifiers = self
×
319
                .props
×
320
                .get_or(
×
321
                    Attribute::TextProps,
×
322
                    AttrValue::TextModifiers(TextModifiers::empty()),
×
323
                )
324
                .unwrap_text_modifiers();
×
325

326
            let normal_style = Style::default()
×
327
                .fg(foreground)
×
328
                .bg(background)
×
329
                .add_modifier(modifiers);
×
330

331
            let title = self
×
332
                .props
×
333
                .get_ref(Attribute::Title)
×
334
                .and_then(|v| v.as_title());
×
335
            let borders = self
×
336
                .props
×
337
                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
338
                .unwrap_borders();
×
339
            let focus = self
×
340
                .props
×
341
                .get_or(Attribute::Focus, AttrValue::Flag(false))
×
342
                .unwrap_flag();
×
343
            let inactive_style = self
×
344
                .props
×
345
                .get(Attribute::FocusStyle)
×
346
                .map(|x| x.unwrap_style());
×
347
            let row_height = self
×
348
                .props
×
349
                .get_or(Attribute::Height, AttrValue::Size(1))
×
350
                .unwrap_size();
×
351
            // Make rows
352
            let rows: Vec<Row> = self.make_rows(row_height);
×
353
            let highlighted_color = self
×
354
                .props
×
355
                .get(Attribute::HighlightedColor)
×
356
                .map(|x| x.unwrap_color());
×
357
            let widths: Vec<Constraint> = self.layout();
×
358

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

419
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
420
        self.props.get(attr)
×
421
    }
×
422

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

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

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

522
#[cfg(test)]
523
mod tests {
524

525
    use super::*;
526
    use pretty_assertions::assert_eq;
527
    use tuirealm::props::{Alignment, TableBuilder};
528

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

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

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

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

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

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

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