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