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

veeso / tui-realm-stdlib / 8154353584

05 Mar 2024 10:00AM UTC coverage: 73.477% (+0.02%) from 73.457%
8154353584

push

github

web-flow
fix: compatibility with ratatui 0.26 (#21)

1 of 22 new or added lines in 5 files covered. (4.55%)

1 existing line in 1 file now uncovered.

2762 of 3759 relevant lines covered (73.48%)

1.99 hits per line

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

78.71
/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 super::props::TABLE_COLUMN_SPACING;
6
use std::cmp::max;
7

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

20
// -- States
21

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

28
impl TableStates {
29
    /// ### set_list_len
30
    ///
31
    /// Set list length
32
    pub fn set_list_len(&mut self, len: usize) {
7✔
33
        self.list_len = len;
7✔
34
    }
7✔
35

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

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

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

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

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

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

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

116
// -- Component
117

118
/// ## Table
119
///
120
/// represents a read-only text component without any container.
121
#[derive(Default)]
8✔
122
pub struct Table {
123
    props: Props,
4✔
124
    pub states: TableStates,
4✔
125
    hg_str: Option<String>, // CRAP CRAP CRAP
4✔
126
    headers: Vec<String>,   // CRAP CRAP CRAP
4✔
127
}
128

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

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

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

145
    pub fn modifiers(mut self, m: TextModifiers) -> Self {
3✔
146
        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
3✔
147
        self
3✔
148
    }
3✔
149

150
    pub fn borders(mut self, b: Borders) -> Self {
3✔
151
        self.attr(Attribute::Borders, AttrValue::Borders(b));
3✔
152
        self
3✔
153
    }
3✔
154

155
    pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self {
3✔
156
        self.attr(
3✔
157
            Attribute::Title,
3✔
158
            AttrValue::Title((t.as_ref().to_string(), a)),
3✔
159
        );
160
        self
3✔
161
    }
3✔
162

163
    pub fn step(mut self, step: usize) -> Self {
1✔
164
        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
1✔
165
        self
1✔
166
    }
1✔
167

168
    pub fn scroll(mut self, scrollable: bool) -> Self {
2✔
169
        self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
2✔
170
        self
2✔
171
    }
2✔
172

173
    pub fn highlighted_str<S: AsRef<str>>(mut self, s: S) -> Self {
3✔
174
        self.attr(
3✔
175
            Attribute::HighlightedStr,
3✔
176
            AttrValue::String(s.as_ref().to_string()),
3✔
177
        );
178
        self
3✔
179
    }
3✔
180

181
    pub fn highlighted_color(mut self, c: Color) -> Self {
3✔
182
        self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
3✔
183
        self
3✔
184
    }
3✔
185

186
    pub fn column_spacing(mut self, w: u16) -> Self {
2✔
187
        self.attr(Attribute::Custom(TABLE_COLUMN_SPACING), AttrValue::Size(w));
2✔
188
        self
2✔
189
    }
2✔
190

191
    pub fn row_height(mut self, h: u16) -> Self {
2✔
192
        self.attr(Attribute::Height, AttrValue::Size(h));
2✔
193
        self
2✔
194
    }
2✔
195

196
    pub fn widths(mut self, w: &[u16]) -> Self {
2✔
197
        self.attr(
2✔
198
            Attribute::Width,
2✔
199
            AttrValue::Payload(PropPayload::Vec(
2✔
200
                w.iter().map(|x| PropValue::U16(*x)).collect(),
9✔
201
            )),
202
        );
203
        self
2✔
204
    }
2✔
205

206
    pub fn headers<S: AsRef<str>>(mut self, headers: &[S]) -> Self {
2✔
207
        self.attr(
2✔
208
            Attribute::Text,
2✔
209
            AttrValue::Payload(PropPayload::Vec(
2✔
210
                headers
2✔
211
                    .iter()
212
                    .map(|x| PropValue::Str(x.as_ref().to_string()))
7✔
213
                    .collect(),
214
            )),
215
        );
216
        self
2✔
217
    }
2✔
218

219
    pub fn table(mut self, t: PropTable) -> Self {
4✔
220
        self.attr(Attribute::Content, AttrValue::Table(t));
4✔
221
        self
4✔
222
    }
4✔
223

224
    pub fn rewind(mut self, r: bool) -> Self {
×
225
        self.attr(Attribute::Rewind, AttrValue::Flag(r));
×
226
        self
×
227
    }
×
228

229
    /// Set initial selected line
230
    /// This method must be called after `rows` and `scrollable` in order to work
231
    pub fn selected_line(mut self, line: usize) -> Self {
1✔
232
        self.attr(
1✔
233
            Attribute::Value,
1✔
234
            AttrValue::Payload(PropPayload::One(PropValue::Usize(line))),
1✔
235
        );
236
        self
1✔
237
    }
1✔
238

239
    /// ### scrollable
240
    ///
241
    /// returns the value of the scrollable flag; by default is false
242
    fn is_scrollable(&self) -> bool {
12✔
243
        self.props
24✔
244
            .get_or(Attribute::Scroll, AttrValue::Flag(false))
12✔
245
            .unwrap_flag()
246
    }
12✔
247

248
    fn rewindable(&self) -> bool {
2✔
249
        self.props
4✔
250
            .get_or(Attribute::Rewind, AttrValue::Flag(false))
2✔
251
            .unwrap_flag()
252
    }
2✔
253

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

283
impl MockComponent for Table {
284
    fn view(&mut self, render: &mut Frame, area: Rect) {
×
285
        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
×
286
            let foreground = self
×
287
                .props
288
                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
×
289
                .unwrap_color();
290
            let background = self
×
291
                .props
292
                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
×
293
                .unwrap_color();
294
            let modifiers = self
×
295
                .props
296
                .get_or(
297
                    Attribute::TextProps,
×
298
                    AttrValue::TextModifiers(TextModifiers::empty()),
×
299
                )
300
                .unwrap_text_modifiers();
301
            let title = self
×
302
                .props
303
                .get_or(
304
                    Attribute::Title,
×
305
                    AttrValue::Title((String::default(), Alignment::Center)),
×
306
                )
307
                .unwrap_title();
308
            let borders = self
×
309
                .props
310
                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
×
311
                .unwrap_borders();
312
            let focus = self
×
313
                .props
314
                .get_or(Attribute::Focus, AttrValue::Flag(false))
×
315
                .unwrap_flag();
316
            let inactive_style = self
×
317
                .props
318
                .get(Attribute::FocusStyle)
×
319
                .map(|x| x.unwrap_style());
×
320
            let row_height = self
×
321
                .props
322
                .get_or(Attribute::Height, AttrValue::Size(1))
×
323
                .unwrap_size();
324
            // Make rows
325
            let rows: Vec<Row> = match self.props.get(Attribute::Content).map(|x| x.unwrap_table())
×
326
            {
327
                Some(table) => table
×
328
                    .iter()
329
                    .map(|row| {
×
330
                        let columns: Vec<Cell> = row
×
331
                            .iter()
332
                            .map(|col| {
×
333
                                let (fg, bg, modifiers) =
×
334
                                    crate::utils::use_or_default_styles(&self.props, col);
×
335
                                Cell::from(Span::styled(
×
336
                                    col.content.clone(),
×
337
                                    Style::default().add_modifier(modifiers).fg(fg).bg(bg),
×
338
                                ))
339
                            })
×
340
                            .collect();
341
                        Row::new(columns).height(row_height)
×
342
                    })
×
343
                    .collect(), // Make List item from TextSpan
×
344
                _ => Vec::new(),
×
345
            };
×
346
            let highlighted_color = self
×
347
                .props
348
                .get(Attribute::HighlightedColor)
×
349
                .map(|x| x.unwrap_color());
×
350
            let widths: Vec<Constraint> = self.layout();
×
351
            #[cfg(feature = "tui")]
352
            let mut table = TuiTable::new(rows)
353
                .block(crate::utils::get_block(
354
                    borders,
355
                    Some(title),
356
                    focus,
357
                    inactive_style,
358
                ))
359
                .widths(&widths);
360
            #[cfg(feature = "ratatui")]
NEW
361
            let mut table = TuiTable::new(rows, &widths).block(crate::utils::get_block(
×
362
                borders,
NEW
363
                Some(title),
×
364
                focus,
365
                inactive_style,
366
            ));
367
            if let Some(highlighted_color) = highlighted_color {
×
368
                table = table.highlight_style(Style::default().fg(highlighted_color).add_modifier(
×
369
                    match focus {
×
370
                        true => modifiers | TextModifiers::REVERSED,
×
371
                        false => modifiers,
×
372
                    },
373
                ));
374
            }
375
            // Highlighted symbol
376
            self.hg_str = self
×
377
                .props
378
                .get(Attribute::HighlightedStr)
×
379
                .map(|x| x.unwrap_string());
×
380
            if let Some(hg_str) = &self.hg_str {
×
NEW
381
                table = table.highlight_symbol(hg_str.as_str());
×
382
            }
383
            // Col spacing
384
            if let Some(spacing) = self
×
385
                .props
386
                .get(Attribute::Custom(TABLE_COLUMN_SPACING))
×
387
                .map(|x| x.unwrap_size())
×
388
            {
389
                table = table.column_spacing(spacing);
×
390
            }
391
            // Header
392
            self.headers = self
×
393
                .props
394
                .get(Attribute::Text)
×
395
                .map(|x| {
×
396
                    x.unwrap_payload()
×
397
                        .unwrap_vec()
398
                        .into_iter()
399
                        .map(|x| x.unwrap_str())
×
400
                        .collect()
401
                })
×
402
                .unwrap_or_default();
403
            if !self.headers.is_empty() {
×
404
                let headers: Vec<&str> = self.headers.iter().map(|x| x.as_str()).collect();
×
405
                table = table.header(
×
406
                    Row::new(headers)
×
407
                        .style(
408
                            Style::default()
×
409
                                .fg(foreground)
410
                                .bg(background)
411
                                .add_modifier(modifiers),
412
                        )
413
                        .height(row_height),
×
414
                );
415
            }
416
            if self.is_scrollable() {
×
417
                let mut state: TableState = TableState::default();
×
418
                state.select(Some(self.states.list_index));
×
419
                render.render_stateful_widget(table, area, &mut state);
×
420
            } else {
421
                render.render_widget(table, area);
×
422
            }
423
        }
×
424
    }
×
425

426
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
427
        self.props.get(attr)
×
428
    }
×
429

430
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
39✔
431
        self.props.set(attr, value);
39✔
432
        if matches!(attr, Attribute::Content) {
39✔
433
            // Update list len and fix index
434
            self.states.set_list_len(
10✔
435
                match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
10✔
436
                    Some(spans) => spans.len(),
5✔
437
                    _ => 0,
×
438
                },
439
            );
5✔
440
            self.states.fix_list_index();
5✔
441
        } else if matches!(attr, Attribute::Value) && self.is_scrollable() {
34✔
442
            self.states.list_index = self
4✔
443
                .props
444
                .get(Attribute::Value)
2✔
445
                .map(|x| x.unwrap_payload().unwrap_one().unwrap_usize())
2✔
446
                .unwrap_or(0);
447
            self.states.fix_list_index();
2✔
448
        }
449
    }
39✔
450

451
    fn state(&self) -> State {
10✔
452
        match self.is_scrollable() {
10✔
453
            true => State::One(StateValue::Usize(self.states.list_index)),
9✔
454
            false => State::None,
1✔
455
        }
456
    }
10✔
457

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

529
#[cfg(test)]
530
mod tests {
531

532
    use super::*;
533
    use pretty_assertions::assert_eq;
534
    use tuirealm::props::{TableBuilder, TextSpan};
535

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

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

694
    #[test]
695
    fn test_component_table_with_empty_rows_and_no_width_set() {
2✔
696
        // Make component
697
        let component = Table::default().table(TableBuilder::default().build());
1✔
698

699
        assert_eq!(component.states.list_len, 1);
1✔
700
        assert_eq!(component.states.list_index, 0);
1✔
701
        // calculating layout would fail if no widths and using "empty" TableBuilder
702
        assert_eq!(component.layout().len(), 0);
1✔
703
    }
2✔
704

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

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