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

veeso / tui-realm-stdlib / 20395971316

20 Dec 2025 02:51PM UTC coverage: 68.439% (+0.5%) from 67.987%
20395971316

push

github

web-flow
fix: Fix Styling inconsistencies (#44)

7 of 84 new or added lines in 10 files covered. (8.33%)

6 existing lines in 5 files now uncovered.

3038 of 4439 relevant lines covered (68.44%)

1.84 hits per line

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

74.51
/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::ratatui::{
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)]
23
pub struct TableStates {
24
    pub list_index: usize, // Index of selected item in textarea
25
    pub list_len: usize,   // Lines in text area
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 {
7✔
44
            self.list_index = 0;
1✔
45
        }
1✔
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 {
8✔
56
            self.list_index = self.list_len - 1;
1✔
57
        }
1✔
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
        }
6✔
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 {
2✔
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
    #[must_use]
93
    pub fn calc_max_step_ahead(&self, max: usize) -> usize {
2✔
94
        let remaining: usize = match self.list_len {
2✔
95
            0 => 0,
×
96
            len => len - 1 - self.list_index,
2✔
97
        };
98
        if remaining > max { max } else { remaining }
2✔
99
    }
2✔
100

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

114
// -- Component
115

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

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

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

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

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

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

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

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

162
    pub fn scroll(mut self, scrollable: bool) -> Self {
2✔
163
        self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
2✔
164
        self
2✔
165
    }
2✔
166

167
    pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
3✔
168
        self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
3✔
169
        self
3✔
170
    }
3✔
171

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

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

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

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

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

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

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

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

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

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

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

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

284
        table
×
285
            .iter()
×
286
            .map(|row| {
×
287
                let columns: Vec<Cell> = row
×
288
                    .iter()
×
289
                    .map(|col| {
×
290
                        let (fg, bg, modifiers) =
×
291
                            crate::utils::use_or_default_styles(&self.props, col);
×
292
                        Cell::from(Span::styled(
×
293
                            &col.content,
×
294
                            Style::default().add_modifier(modifiers).fg(fg).bg(bg),
×
295
                        ))
296
                    })
×
297
                    .collect();
×
298
                Row::new(columns).height(row_height)
×
299
            })
×
300
            .collect() // Make List item from TextSpan
×
301
    }
×
302
}
303

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

NEW
323
            let normal_style = Style::default()
×
NEW
324
                .fg(foreground)
×
NEW
325
                .bg(background)
×
NEW
326
                .add_modifier(modifiers);
×
327

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

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

413
    fn query(&self, attr: Attribute) -> Option<AttrValue> {
×
414
        self.props.get(attr)
×
415
    }
×
416

417
    fn attr(&mut self, attr: Attribute, value: AttrValue) {
45✔
418
        self.props.set(attr, value);
45✔
419
        if matches!(attr, Attribute::Content) {
45✔
420
            // Update list len and fix index
421
            self.states.set_list_len(
5✔
422
                match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
5✔
423
                    Some(spans) => spans.len(),
5✔
424
                    _ => 0,
×
425
                },
426
            );
427
            self.states.fix_list_index();
5✔
428
        } else if matches!(attr, Attribute::Value) && self.is_scrollable() {
40✔
429
            self.states.list_index = self
2✔
430
                .props
2✔
431
                .get(Attribute::Value)
2✔
432
                .map_or(0, |x| x.unwrap_payload().unwrap_one().unwrap_usize());
2✔
433
            self.states.fix_list_index();
2✔
434
        }
38✔
435
    }
45✔
436

437
    fn state(&self) -> State {
10✔
438
        if self.is_scrollable() {
10✔
439
            State::One(StateValue::Usize(self.states.list_index))
9✔
440
        } else {
441
            State::None
1✔
442
        }
443
    }
10✔
444

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

516
#[cfg(test)]
517
mod tests {
518

519
    use super::*;
520
    use pretty_assertions::assert_eq;
521
    use tuirealm::props::{TableBuilder, TextSpan};
522

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

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

681
    #[test]
682
    fn test_component_table_with_empty_rows_and_no_width_set() {
1✔
683
        // Make component
684
        let component = Table::default().table(TableBuilder::default().build());
1✔
685

686
        assert_eq!(component.states.list_len, 1);
1✔
687
        assert_eq!(component.states.list_index, 0);
1✔
688
        // calculating layout would fail if no widths and using "empty" TableBuilder
689
        assert_eq!(component.layout().len(), 0);
1✔
690
    }
1✔
691

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

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

794
    #[test]
795
    fn various_header_types() {
1✔
796
        // static array of static strings
797
        let _ = Table::default().headers(["hello"]);
1✔
798
        // static array of strings
799
        let _ = Table::default().headers(["hello".to_string()]);
1✔
800
        // vec of static strings
801
        let _ = Table::default().headers(vec!["hello"]);
1✔
802
        // vec of strings
803
        let _ = Table::default().headers(vec!["hello".to_string()]);
1✔
804
        // boxed array of static strings
805
        let _ = Table::default().headers(vec!["hello"].into_boxed_slice());
1✔
806
        // boxed array of strings
807
        let _ = Table::default().headers(vec!["hello".to_string()].into_boxed_slice());
1✔
808
    }
1✔
809
}
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