Skip to main content

tui_realm_stdlib/components/
table.rs

1//! `Table` represents a read-only textual table component which can be scrollable through arrows or inactive.
2
3use std::cmp::max;
4
5use tuirealm::command::{Cmd, CmdResult, Direction, Position};
6use tuirealm::component::Component;
7use tuirealm::props::{
8    AttrValue, Attribute, Borders, Color, LineStatic, PropPayload, PropValue, Props, QueryResult,
9    Style, Table as PropTable, TextModifiers, Title,
10};
11use tuirealm::ratatui::Frame;
12use tuirealm::ratatui::layout::{Constraint, Rect};
13use tuirealm::ratatui::widgets::{Cell, Row, Table as TuiTable, TableState};
14use tuirealm::state::{State, StateValue};
15
16use super::props::TABLE_COLUMN_SPACING;
17use crate::prop_ext::{CommonHighlight, CommonProps};
18use crate::utils::borrow_clone_line;
19
20// -- States
21
22/// The state that has to be kept for the [`Table`] component.
23#[derive(Default)]
24pub struct TableStates {
25    /// Index of selected item in textarea
26    pub list_index: usize,
27    /// Lines in text area
28    pub list_len: usize,
29}
30
31impl TableStates {
32    /// Set list length.
33    pub fn set_list_len(&mut self, len: usize) {
34        self.list_len = len;
35    }
36
37    /// Incremenet list index.
38    pub fn incr_list_index(&mut self, rewind: bool) {
39        // Check if index is at last element
40        if self.list_index + 1 < self.list_len {
41            self.list_index += 1;
42        } else if rewind {
43            self.list_index = 0;
44        }
45    }
46
47    /// Decrement list index.
48    pub fn decr_list_index(&mut self, rewind: bool) {
49        // Check if index is bigger than 0
50        if self.list_index > 0 {
51            self.list_index -= 1;
52        } else if rewind && self.list_len > 0 {
53            self.list_index = self.list_len - 1;
54        }
55    }
56
57    /// Keep index if possible, otherwise set to `lenght - 1`.
58    pub fn fix_list_index(&mut self) {
59        if self.list_index >= self.list_len && self.list_len > 0 {
60            self.list_index = self.list_len - 1;
61        } else if self.list_len == 0 {
62            self.list_index = 0;
63        }
64    }
65
66    /// Set list index to the first item in the list.
67    pub fn list_index_at_first(&mut self) {
68        self.list_index = 0;
69    }
70
71    /// Set list index at the last item of the list.
72    pub fn list_index_at_last(&mut self) {
73        if self.list_len > 0 {
74            self.list_index = self.list_len - 1;
75        } else {
76            self.list_index = 0;
77        }
78    }
79
80    /// Calculate the max step ahead to scroll list.
81    #[must_use]
82    pub fn calc_max_step_ahead(&self, max: usize) -> usize {
83        let remaining: usize = match self.list_len {
84            0 => 0,
85            len => len - 1 - self.list_index,
86        };
87        if remaining > max { max } else { remaining }
88    }
89
90    /// Calculate the max step ahead to scroll list.
91    #[must_use]
92    pub fn calc_max_step_behind(&self, max: usize) -> usize {
93        if self.list_index > max {
94            max
95        } else {
96            self.list_index
97        }
98    }
99}
100
101// -- Component
102
103/// `Table` represents a read-only textual table component which can be scrollable through arrows or inactive.
104#[derive(Default)]
105#[must_use]
106pub struct Table {
107    common: CommonProps,
108    common_hg: CommonHighlight,
109    props: Props,
110    pub states: TableStates,
111}
112
113impl Table {
114    /// Set the main foreground color. This may get overwritten by individual text styles.
115    pub fn foreground(mut self, fg: Color) -> Self {
116        self.attr(Attribute::Foreground, AttrValue::Color(fg));
117        self
118    }
119
120    /// Set the main background color. This may get overwritten by individual text styles.
121    pub fn background(mut self, bg: Color) -> Self {
122        self.attr(Attribute::Background, AttrValue::Color(bg));
123        self
124    }
125
126    /// Set the main text modifiers. This may get overwritten by individual text styles.
127    pub fn modifiers(mut self, m: TextModifiers) -> Self {
128        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
129        self
130    }
131
132    /// Set the main style. This may get overwritten by individual text styles.
133    ///
134    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
135    pub fn style(mut self, style: Style) -> Self {
136        self.attr(Attribute::Style, AttrValue::Style(style));
137        self
138    }
139
140    /// Set a custom style for the border when the component is unfocused.
141    pub fn inactive(mut self, s: Style) -> Self {
142        self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
143        self
144    }
145
146    /// Add a border to the component.
147    pub fn borders(mut self, b: Borders) -> Self {
148        self.attr(Attribute::Borders, AttrValue::Borders(b));
149        self
150    }
151
152    /// Add a title to the component.
153    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
154        self.attr(Attribute::Title, AttrValue::Title(title.into()));
155        self
156    }
157
158    /// Set the scroll stepping to use on `Cmd::Scroll(Direction::Up)` or `Cmd::Scroll(Direction::Down)`.
159    pub fn step(mut self, step: usize) -> Self {
160        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
161        self
162    }
163
164    /// Should the list be scrollable or always show only the top (0th) element?
165    pub fn scroll(mut self, scrollable: bool) -> Self {
166        self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
167        self
168    }
169
170    /// Set the Symbol and Style for the indicator of the current line.
171    pub fn highlight_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
172        self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
173        self
174    }
175
176    /// Set a custom highlight style that is patched on-top of the normal style.
177    ///
178    /// By default the highlight style is just `Style::new().add_modifier(Modifier::REVERSED)`.
179    pub fn highlight_style(mut self, s: Style) -> Self {
180        self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
181        self
182    }
183
184    /// Set a custom highlight style that is patched on-top of the highlight style when unfocused.
185    pub fn highlight_style_inactive(mut self, s: Style) -> Self {
186        self.attr(Attribute::HighlightStyleUnfocused, AttrValue::Style(s));
187        self
188    }
189
190    /// Set custom spacing between columns.
191    pub fn column_spacing(mut self, w: u16) -> Self {
192        self.attr(Attribute::Custom(TABLE_COLUMN_SPACING), AttrValue::Size(w));
193        self
194    }
195
196    /// Set a custom height for all rows.
197    ///
198    /// Default: `1`
199    pub fn row_height(mut self, h: u16) -> Self {
200        self.attr(Attribute::Height, AttrValue::Size(h));
201        self
202    }
203
204    /// Set the widths of each column.
205    pub fn widths(mut self, w: &[u16]) -> Self {
206        // TODO: should this maybe be "Layout"?
207        self.attr(
208            Attribute::Width,
209            AttrValue::Payload(PropPayload::Vec(
210                w.iter().map(|x| PropValue::U16(*x)).collect(),
211            )),
212        );
213        self
214    }
215
216    /// Set headers for columns.
217    pub fn headers<S: Into<String>>(mut self, headers: impl IntoIterator<Item = S>) -> Self {
218        // TODO: we should consider using Spans or Lines
219        self.attr(
220            Attribute::Text,
221            AttrValue::Payload(PropPayload::Vec(
222                headers
223                    .into_iter()
224                    .map(|v| PropValue::Str(v.into()))
225                    .collect(),
226            )),
227        );
228        self
229    }
230
231    /// Set the data for the table.
232    pub fn table(mut self, t: PropTable) -> Self {
233        self.attr(Attribute::Content, AttrValue::Table(t));
234        self
235    }
236
237    /// Set whether wraparound should be possible (down on the last choice wraps around to 0, and the other way around).
238    pub fn rewind(mut self, r: bool) -> Self {
239        self.attr(Attribute::Rewind, AttrValue::Flag(r));
240        self
241    }
242
243    /// Set the initially selected line.
244    pub fn selected_line(mut self, line: usize) -> Self {
245        self.attr(
246            Attribute::Value,
247            AttrValue::Payload(PropPayload::Single(PropValue::Usize(line))),
248        );
249        self
250    }
251
252    /// Set the current component to be always active (show highligh even if unfocused)
253    pub fn always_active(mut self) -> Self {
254        self.attr(Attribute::AlwaysActive, AttrValue::Flag(true));
255        self
256    }
257
258    /// ### scrollable
259    ///
260    /// returns the value of the scrollable flag; by default is false
261    fn is_scrollable(&self) -> bool {
262        self.props
263            .get(Attribute::Scroll)
264            .and_then(AttrValue::as_flag)
265            .unwrap_or_default()
266    }
267
268    fn rewindable(&self) -> bool {
269        self.props
270            .get(Attribute::Rewind)
271            .and_then(AttrValue::as_flag)
272            .unwrap_or_default()
273    }
274
275    /// ### layout
276    ///
277    /// Returns layout based on properties.
278    /// If layout is not set in properties, they'll be divided by rows number
279    fn layout(&self) -> Vec<Constraint> {
280        if let Some(widths) = self
281            .props
282            .get(Attribute::Width)
283            .and_then(AttrValue::as_payload)
284            .and_then(PropPayload::as_vec)
285        {
286            widths
287                .iter()
288                .cloned()
289                .map(|x| x.unwrap_u16())
290                .map(Constraint::Percentage)
291                .collect()
292        } else {
293            // Get amount of columns (maximum len of row elements)
294            let columns: usize = self
295                .props
296                .get(Attribute::Content)
297                .and_then(AttrValue::as_table)
298                .and_then(|rows| rows.iter().map(|col| col.len()).max())
299                .unwrap_or(1);
300            // Calc width in equal way, make sure not to divide by zero (this can happen when rows is [[]])
301            let width: u16 = (100 / max(columns, 1)) as u16;
302            (0..columns)
303                .map(|_| Constraint::Percentage(width))
304                .collect()
305        }
306    }
307
308    /// Generate [`Row`]s from a 2d vector of [`TextSpan`](tuirealm::props::TextSpan)s in props [`Attribute::Content`].
309    fn make_rows(&self, row_height: u16) -> Vec<Row<'_>> {
310        let Some(table) = self
311            .props
312            .get(Attribute::Content)
313            .and_then(|x| x.as_table())
314        else {
315            return Vec::new();
316        };
317
318        table
319            .iter()
320            .map(|row| {
321                let columns: Vec<Cell> = row
322                    .iter()
323                    .map(|col| Cell::from(borrow_clone_line(col)))
324                    .collect();
325                Row::new(columns).height(row_height)
326            })
327            .collect() // Make List item from TextSpan
328    }
329}
330
331impl Component for Table {
332    fn view(&mut self, render: &mut Frame, area: Rect) {
333        if !self.common.display {
334            return;
335        }
336
337        let row_height = self
338            .props
339            .get(Attribute::Height)
340            .and_then(AttrValue::as_size)
341            .unwrap_or(1);
342        // Make rows
343        let rows: Vec<Row> = self.make_rows(row_height);
344        let widths: Vec<Constraint> = self.layout();
345
346        let mut widget = TuiTable::new(rows, &widths)
347            .style(self.common.style)
348            .row_highlight_style(
349                self.common_hg
350                    .get_style_focus(self.common.style, self.common.is_active()),
351            );
352
353        if let Some(block) = self.common.get_block() {
354            widget = widget.block(block);
355        }
356
357        // Highlighted symbol
358        if let Some(symbol) = self.common_hg.get_symbol() {
359            widget = widget.highlight_symbol(symbol);
360        }
361
362        // Col spacing
363        if let Some(spacing) = self
364            .props
365            .get(Attribute::Custom(TABLE_COLUMN_SPACING))
366            .and_then(AttrValue::as_size)
367        {
368            widget = widget.column_spacing(spacing);
369        }
370        // Header
371        let headers: Vec<&str> = self
372            .props
373            .get(Attribute::Text)
374            .and_then(AttrValue::as_payload)
375            .and_then(PropPayload::as_vec)
376            .map(|v| {
377                v.iter()
378                    .filter_map(|v| v.as_str().map(|v| v.as_str()))
379                    .collect()
380            })
381            .unwrap_or_default();
382        if !headers.is_empty() {
383            widget = widget.header(
384                Row::new(headers)
385                    .style(self.common.style)
386                    .height(row_height),
387            );
388        }
389        if self.is_scrollable() {
390            let mut state: TableState = TableState::default();
391            state.select(Some(self.states.list_index));
392            render.render_stateful_widget(widget, area, &mut state);
393        } else {
394            render.render_widget(widget, area);
395        }
396    }
397
398    fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
399        if let Some(value) = self
400            .common
401            .get_for_query(attr)
402            .or_else(|| self.common_hg.get_for_query(attr))
403        {
404            return Some(value);
405        }
406
407        self.props.get_for_query(attr)
408    }
409
410    fn attr(&mut self, attr: Attribute, value: AttrValue) {
411        if let Some(value) = self
412            .common
413            .set(attr, value)
414            .and_then(|value| self.common_hg.set(attr, value))
415        {
416            self.props.set(attr, value);
417            if matches!(attr, Attribute::Content) {
418                // Update list len and fix index
419                self.states.set_list_len(
420                    self.props
421                        .get(Attribute::Content)
422                        .and_then(AttrValue::as_table)
423                        .map(|spans| spans.len())
424                        .unwrap_or_default(),
425                );
426                self.states.fix_list_index();
427            } else if matches!(attr, Attribute::Value) && self.is_scrollable() {
428                self.states.list_index = self
429                    .props
430                    .get(Attribute::Value)
431                    .and_then(AttrValue::as_payload)
432                    .and_then(PropPayload::as_single)
433                    .and_then(PropValue::as_usize)
434                    .unwrap_or_default();
435                self.states.fix_list_index();
436            }
437        }
438    }
439
440    fn state(&self) -> State {
441        if self.is_scrollable() {
442            State::Single(StateValue::Usize(self.states.list_index))
443        } else {
444            State::None
445        }
446    }
447
448    fn perform(&mut self, cmd: Cmd) -> CmdResult {
449        match cmd {
450            Cmd::Move(Direction::Down) => {
451                let prev = self.states.list_index;
452                self.states.incr_list_index(self.rewindable());
453                if prev == self.states.list_index {
454                    CmdResult::NoChange
455                } else {
456                    CmdResult::Changed(self.state())
457                }
458            }
459            Cmd::Move(Direction::Up) => {
460                let prev = self.states.list_index;
461                self.states.decr_list_index(self.rewindable());
462                if prev == self.states.list_index {
463                    CmdResult::NoChange
464                } else {
465                    CmdResult::Changed(self.state())
466                }
467            }
468            Cmd::Scroll(Direction::Down) => {
469                let prev = self.states.list_index;
470                let step = self
471                    .props
472                    .get(Attribute::ScrollStep)
473                    .and_then(AttrValue::as_length)
474                    .unwrap_or(8);
475                let step: usize = self.states.calc_max_step_ahead(step);
476                (0..step).for_each(|_| self.states.incr_list_index(false));
477                if prev == self.states.list_index {
478                    CmdResult::NoChange
479                } else {
480                    CmdResult::Changed(self.state())
481                }
482            }
483            Cmd::Scroll(Direction::Up) => {
484                let prev = self.states.list_index;
485                let step = self
486                    .props
487                    .get(Attribute::ScrollStep)
488                    .and_then(AttrValue::as_length)
489                    .unwrap_or(8);
490                let step: usize = self.states.calc_max_step_behind(step);
491                (0..step).for_each(|_| self.states.decr_list_index(false));
492                if prev == self.states.list_index {
493                    CmdResult::NoChange
494                } else {
495                    CmdResult::Changed(self.state())
496                }
497            }
498            Cmd::GoTo(Position::Begin) => {
499                let prev = self.states.list_index;
500                self.states.list_index_at_first();
501                if prev == self.states.list_index {
502                    CmdResult::NoChange
503                } else {
504                    CmdResult::Changed(self.state())
505                }
506            }
507            Cmd::GoTo(Position::End) => {
508                let prev = self.states.list_index;
509                self.states.list_index_at_last();
510                if prev == self.states.list_index {
511                    CmdResult::NoChange
512                } else {
513                    CmdResult::Changed(self.state())
514                }
515            }
516            _ => CmdResult::Invalid(cmd),
517        }
518    }
519}
520
521#[cfg(test)]
522mod tests {
523
524    use pretty_assertions::assert_eq;
525    use tuirealm::props::{HorizontalAlignment, TableBuilder};
526    use tuirealm::ratatui::text::Line;
527
528    use super::*;
529
530    #[test]
531    fn table_states() {
532        let mut states = TableStates::default();
533        assert_eq!(states.list_index, 0);
534        assert_eq!(states.list_len, 0);
535        states.set_list_len(5);
536        assert_eq!(states.list_index, 0);
537        assert_eq!(states.list_len, 5);
538        // Incr
539        states.incr_list_index(true);
540        assert_eq!(states.list_index, 1);
541        states.list_index = 4;
542        states.incr_list_index(false);
543        assert_eq!(states.list_index, 4);
544        states.incr_list_index(true);
545        assert_eq!(states.list_index, 0);
546        // Decr
547        states.decr_list_index(false);
548        assert_eq!(states.list_index, 0);
549        states.decr_list_index(true);
550        assert_eq!(states.list_index, 4);
551        states.decr_list_index(true);
552        assert_eq!(states.list_index, 3);
553        // Begin
554        states.list_index_at_first();
555        assert_eq!(states.list_index, 0);
556        states.list_index_at_last();
557        assert_eq!(states.list_index, 4);
558        // Fix
559        states.set_list_len(3);
560        states.fix_list_index();
561        assert_eq!(states.list_index, 2);
562    }
563
564    #[test]
565    fn test_component_table_scrolling() {
566        // Make component
567        let mut component = Table::default()
568            .foreground(Color::Red)
569            .background(Color::Blue)
570            .highlight_style(Style::new().fg(Color::Yellow))
571            .highlight_str("🚀")
572            .modifiers(TextModifiers::BOLD)
573            .scroll(true)
574            .step(4)
575            .borders(Borders::default())
576            .title(Title::from("events").alignment(HorizontalAlignment::Center))
577            .column_spacing(4)
578            .widths(&[25, 25, 25, 25])
579            .row_height(3)
580            .headers(["Event", "Message", "Behaviour", "???"])
581            .table(
582                TableBuilder::default()
583                    .add_col(Line::from("KeyCode::Down"))
584                    .add_col(Line::from("OnKey"))
585                    .add_col(Line::from("Move cursor down"))
586                    .add_row()
587                    .add_col(Line::from("KeyCode::Up"))
588                    .add_col(Line::from("OnKey"))
589                    .add_col(Line::from("Move cursor up"))
590                    .add_row()
591                    .add_col(Line::from("KeyCode::PageDown"))
592                    .add_col(Line::from("OnKey"))
593                    .add_col(Line::from("Move cursor down by 8"))
594                    .add_row()
595                    .add_col(Line::from("KeyCode::PageUp"))
596                    .add_col(Line::from("OnKey"))
597                    .add_col(Line::from("ove cursor up by 8"))
598                    .add_row()
599                    .add_col(Line::from("KeyCode::End"))
600                    .add_col(Line::from("OnKey"))
601                    .add_col(Line::from("Move cursor to last item"))
602                    .add_row()
603                    .add_col(Line::from("KeyCode::Home"))
604                    .add_col(Line::from("OnKey"))
605                    .add_col(Line::from("Move cursor to first item"))
606                    .add_row()
607                    .add_col(Line::from("KeyCode::Char(_)"))
608                    .add_col(Line::from("OnKey"))
609                    .add_col(Line::from("Return pressed key"))
610                    .add_col(Line::from("4th mysterious columns"))
611                    .build(),
612            );
613        assert_eq!(component.states.list_len, 7);
614        assert_eq!(component.states.list_index, 0);
615        // Own funcs
616        assert_eq!(component.layout().len(), 4);
617        // Increment list index
618        component.states.list_index += 1;
619        assert_eq!(component.states.list_index, 1);
620        // Check messages
621        // Handle inputs
622        assert_eq!(
623            component.perform(Cmd::Move(Direction::Down)),
624            CmdResult::Changed(State::Single(StateValue::Usize(2)))
625        );
626        // Index should be incremented
627        assert_eq!(component.states.list_index, 2);
628        // Index should be decremented
629        assert_eq!(
630            component.perform(Cmd::Move(Direction::Up)),
631            CmdResult::Changed(State::Single(StateValue::Usize(1)))
632        );
633        // Index should be incremented
634        assert_eq!(component.states.list_index, 1);
635        // Index should be 2
636        assert_eq!(
637            component.perform(Cmd::Scroll(Direction::Down)),
638            CmdResult::Changed(State::Single(StateValue::Usize(5)))
639        );
640        // Index should be incremented
641        assert_eq!(component.states.list_index, 5);
642        assert_eq!(
643            component.perform(Cmd::Scroll(Direction::Down)),
644            CmdResult::Changed(State::Single(StateValue::Usize(6)))
645        );
646        // Index should be incremented
647        assert_eq!(component.states.list_index, 6);
648        // Index should be 0
649        assert_eq!(
650            component.perform(Cmd::Scroll(Direction::Up)),
651            CmdResult::Changed(State::Single(StateValue::Usize(2)))
652        );
653        assert_eq!(component.states.list_index, 2);
654        assert_eq!(
655            component.perform(Cmd::Scroll(Direction::Up)),
656            CmdResult::Changed(State::Single(StateValue::Usize(0)))
657        );
658        assert_eq!(component.states.list_index, 0);
659        // End
660        assert_eq!(
661            component.perform(Cmd::GoTo(Position::End)),
662            CmdResult::Changed(State::Single(StateValue::Usize(6)))
663        );
664        assert_eq!(component.states.list_index, 6);
665        // Home
666        assert_eq!(
667            component.perform(Cmd::GoTo(Position::Begin)),
668            CmdResult::Changed(State::Single(StateValue::Usize(0)))
669        );
670        assert_eq!(component.states.list_index, 0);
671        // Update
672        component.attr(
673            Attribute::Content,
674            AttrValue::Table(
675                TableBuilder::default()
676                    .add_col(Line::from("name"))
677                    .add_col(Line::from("age"))
678                    .add_col(Line::from("birthdate"))
679                    .build(),
680            ),
681        );
682        assert_eq!(component.states.list_len, 1);
683        assert_eq!(component.states.list_index, 0);
684        // Get value
685        assert_eq!(component.state(), State::Single(StateValue::Usize(0)));
686    }
687
688    #[test]
689    fn test_component_table_with_empty_rows_and_no_width_set() {
690        // Make component
691        let component = Table::default().table(TableBuilder::default().build());
692
693        assert_eq!(component.states.list_len, 1);
694        assert_eq!(component.states.list_index, 0);
695        // calculating layout would fail if no widths and using "empty" TableBuilder
696        assert_eq!(component.layout().len(), 0);
697    }
698
699    #[test]
700    fn test_components_table() {
701        // Make component
702        let component = Table::default()
703            .foreground(Color::Red)
704            .background(Color::Blue)
705            .highlight_style(Style::new().fg(Color::Yellow))
706            .highlight_str("🚀")
707            .modifiers(TextModifiers::BOLD)
708            .borders(Borders::default())
709            .title(Title::from("events").alignment(HorizontalAlignment::Center))
710            .column_spacing(4)
711            .widths(&[33, 33, 33])
712            .row_height(3)
713            .headers(["Event", "Message", "Behaviour"])
714            .table(
715                TableBuilder::default()
716                    .add_col(Line::from("KeyCode::Down"))
717                    .add_col(Line::from("OnKey"))
718                    .add_col(Line::from("Move cursor down"))
719                    .add_row()
720                    .add_col(Line::from("KeyCode::Up"))
721                    .add_col(Line::from("OnKey"))
722                    .add_col(Line::from("Move cursor up"))
723                    .add_row()
724                    .add_col(Line::from("KeyCode::PageDown"))
725                    .add_col(Line::from("OnKey"))
726                    .add_col(Line::from("Move cursor down by 8"))
727                    .add_row()
728                    .add_col(Line::from("KeyCode::PageUp"))
729                    .add_col(Line::from("OnKey"))
730                    .add_col(Line::from("ove cursor up by 8"))
731                    .add_row()
732                    .add_col(Line::from("KeyCode::End"))
733                    .add_col(Line::from("OnKey"))
734                    .add_col(Line::from("Move cursor to last item"))
735                    .add_row()
736                    .add_col(Line::from("KeyCode::Home"))
737                    .add_col(Line::from("OnKey"))
738                    .add_col(Line::from("Move cursor to first item"))
739                    .add_row()
740                    .add_col(Line::from("KeyCode::Char(_)"))
741                    .add_col(Line::from("OnKey"))
742                    .add_col(Line::from("Return pressed key"))
743                    .build(),
744            );
745        // Get value (not scrollable)
746        assert_eq!(component.state(), State::None);
747    }
748
749    #[test]
750    fn should_init_list_value() {
751        let mut component = Table::default()
752            .foreground(Color::Red)
753            .background(Color::Blue)
754            .highlight_style(Style::new().fg(Color::Yellow))
755            .highlight_str("🚀")
756            .modifiers(TextModifiers::BOLD)
757            .borders(Borders::default())
758            .title(Title::from("events").alignment(HorizontalAlignment::Center))
759            .table(
760                TableBuilder::default()
761                    .add_col(Line::from("KeyCode::Down"))
762                    .add_col(Line::from("OnKey"))
763                    .add_col(Line::from("Move cursor down"))
764                    .add_row()
765                    .add_col(Line::from("KeyCode::Up"))
766                    .add_col(Line::from("OnKey"))
767                    .add_col(Line::from("Move cursor up"))
768                    .add_row()
769                    .add_col(Line::from("KeyCode::PageDown"))
770                    .add_col(Line::from("OnKey"))
771                    .add_col(Line::from("Move cursor down by 8"))
772                    .add_row()
773                    .add_col(Line::from("KeyCode::PageUp"))
774                    .add_col(Line::from("OnKey"))
775                    .add_col(Line::from("ove cursor up by 8"))
776                    .add_row()
777                    .add_col(Line::from("KeyCode::End"))
778                    .add_col(Line::from("OnKey"))
779                    .add_col(Line::from("Move cursor to last item"))
780                    .add_row()
781                    .add_col(Line::from("KeyCode::Home"))
782                    .add_col(Line::from("OnKey"))
783                    .add_col(Line::from("Move cursor to first item"))
784                    .add_row()
785                    .add_col(Line::from("KeyCode::Char(_)"))
786                    .add_col(Line::from("OnKey"))
787                    .add_col(Line::from("Return pressed key"))
788                    .build(),
789            )
790            .scroll(true)
791            .selected_line(2);
792        assert_eq!(component.states.list_index, 2);
793        // Index out of bounds
794        component.attr(
795            Attribute::Value,
796            AttrValue::Payload(PropPayload::Single(PropValue::Usize(50))),
797        );
798        assert_eq!(component.states.list_index, 6);
799    }
800
801    #[test]
802    fn various_header_types() {
803        // static array of static strings
804        let _ = Table::default().headers(["hello"]);
805        // static array of strings
806        let _ = Table::default().headers(["hello".to_string()]);
807        // vec of static strings
808        let _ = Table::default().headers(vec!["hello"]);
809        // vec of strings
810        let _ = Table::default().headers(vec!["hello".to_string()]);
811        // boxed array of static strings
812        let _ = Table::default().headers(vec!["hello"].into_boxed_slice());
813        // boxed array of strings
814        let _ = Table::default().headers(vec!["hello".to_string()].into_boxed_slice());
815    }
816}