tui_realm_stdlib/components/
table.rs

1//! ## Table
2//!
3//! `Table` represents a read-only textual table component which can be scrollable through arrows or inactive
4
5use super::props::TABLE_COLUMN_SPACING;
6use std::cmp::max;
7
8use tuirealm::command::{Cmd, CmdResult, Direction, Position};
9use tuirealm::props::{
10    Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
11    Table as PropTable, TextModifiers,
12};
13use tuirealm::ratatui::{
14    layout::{Constraint, Rect},
15    text::Span,
16    widgets::{Cell, Row, Table as TuiTable, TableState},
17};
18use tuirealm::{Frame, MockComponent, State, StateValue};
19
20// -- States
21
22#[derive(Default)]
23pub struct TableStates {
24    pub list_index: usize, // Index of selected item in textarea
25    pub list_len: usize,   // Lines in text area
26}
27
28impl TableStates {
29    /// ### set_list_len
30    ///
31    /// Set list length
32    pub fn set_list_len(&mut self, len: usize) {
33        self.list_len = len;
34    }
35
36    /// ### incr_list_index
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    /// ### decr_list_index
49    ///
50    /// Decrement list index
51    pub fn decr_list_index(&mut self, rewind: bool) {
52        // Check if index is bigger than 0
53        if self.list_index > 0 {
54            self.list_index -= 1;
55        } else if rewind && self.list_len > 0 {
56            self.list_index = self.list_len - 1;
57        }
58    }
59
60    /// ### fix_list_index
61    ///
62    /// Keep index if possible, otherwise set to lenght - 1
63    pub fn fix_list_index(&mut self) {
64        if self.list_index >= self.list_len && self.list_len > 0 {
65            self.list_index = self.list_len - 1;
66        } else if self.list_len == 0 {
67            self.list_index = 0;
68        }
69    }
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) {
75        self.list_index = 0;
76    }
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) {
82        if self.list_len > 0 {
83            self.list_index = self.list_len - 1;
84        } else {
85            self.list_index = 0;
86        }
87    }
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 {
94        let remaining: usize = match self.list_len {
95            0 => 0,
96            len => len - 1 - self.list_index,
97        };
98        if remaining > max { max } else { remaining }
99    }
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 {
106        if self.list_index > max {
107            max
108        } else {
109            self.list_index
110        }
111    }
112}
113
114// -- Component
115
116/// ## Table
117///
118/// represents a read-only text component without any container.
119#[derive(Default)]
120#[must_use]
121pub struct Table {
122    props: Props,
123    pub states: TableStates,
124}
125
126impl Table {
127    pub fn foreground(mut self, fg: Color) -> Self {
128        self.attr(Attribute::Foreground, AttrValue::Color(fg));
129        self
130    }
131
132    pub fn background(mut self, bg: Color) -> Self {
133        self.attr(Attribute::Background, AttrValue::Color(bg));
134        self
135    }
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 {
143        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
144        self
145    }
146
147    pub fn borders(mut self, b: Borders) -> Self {
148        self.attr(Attribute::Borders, AttrValue::Borders(b));
149        self
150    }
151
152    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
153        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
154        self
155    }
156
157    pub fn step(mut self, step: usize) -> Self {
158        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
159        self
160    }
161
162    pub fn scroll(mut self, scrollable: bool) -> Self {
163        self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
164        self
165    }
166
167    pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
168        self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
169        self
170    }
171
172    pub fn highlighted_color(mut self, c: Color) -> Self {
173        self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
174        self
175    }
176
177    pub fn column_spacing(mut self, w: u16) -> Self {
178        self.attr(Attribute::Custom(TABLE_COLUMN_SPACING), AttrValue::Size(w));
179        self
180    }
181
182    pub fn row_height(mut self, h: u16) -> Self {
183        self.attr(Attribute::Height, AttrValue::Size(h));
184        self
185    }
186
187    pub fn widths(mut self, w: &[u16]) -> Self {
188        self.attr(
189            Attribute::Width,
190            AttrValue::Payload(PropPayload::Vec(
191                w.iter().map(|x| PropValue::U16(*x)).collect(),
192            )),
193        );
194        self
195    }
196
197    pub fn headers<S: Into<String>>(mut self, headers: impl IntoIterator<Item = S>) -> Self {
198        self.attr(
199            Attribute::Text,
200            AttrValue::Payload(PropPayload::Vec(
201                headers
202                    .into_iter()
203                    .map(|v| PropValue::Str(v.into()))
204                    .collect(),
205            )),
206        );
207        self
208    }
209
210    pub fn table(mut self, t: PropTable) -> Self {
211        self.attr(Attribute::Content, AttrValue::Table(t));
212        self
213    }
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 {
223        self.attr(
224            Attribute::Value,
225            AttrValue::Payload(PropPayload::One(PropValue::Usize(line))),
226        );
227        self
228    }
229
230    /// ### scrollable
231    ///
232    /// returns the value of the scrollable flag; by default is false
233    fn is_scrollable(&self) -> bool {
234        self.props
235            .get_or(Attribute::Scroll, AttrValue::Flag(false))
236            .unwrap_flag()
237    }
238
239    fn rewindable(&self) -> bool {
240        self.props
241            .get_or(Attribute::Rewind, AttrValue::Flag(false))
242            .unwrap_flag()
243    }
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> {
250        if let Some(PropPayload::Vec(widths)) =
251            self.props.get(Attribute::Width).map(|x| x.unwrap_payload())
252        {
253            widths
254                .iter()
255                .cloned()
256                .map(|x| x.unwrap_u16())
257                .map(Constraint::Percentage)
258                .collect()
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())
262            {
263                Some(rows) => rows.iter().map(|col| col.len()).max().unwrap_or(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;
268            (0..columns)
269                .map(|_| Constraint::Percentage(width))
270                .collect()
271        }
272    }
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
304impl 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            let title = crate::utils::get_title_or_center(&self.props);
323            let borders = self
324                .props
325                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
326                .unwrap_borders();
327            let focus = self
328                .props
329                .get_or(Attribute::Focus, AttrValue::Flag(false))
330                .unwrap_flag();
331            let inactive_style = self
332                .props
333                .get(Attribute::FocusStyle)
334                .map(|x| x.unwrap_style());
335            let row_height = self
336                .props
337                .get_or(Attribute::Height, AttrValue::Size(1))
338                .unwrap_size();
339            // Make rows
340            let rows: Vec<Row> = self.make_rows(row_height);
341            let highlighted_color = self
342                .props
343                .get(Attribute::HighlightedColor)
344                .map(|x| x.unwrap_color());
345            let widths: Vec<Constraint> = self.layout();
346
347            let mut table = TuiTable::new(rows, &widths).block(crate::utils::get_block(
348                borders,
349                Some(&title),
350                focus,
351                inactive_style,
352            ));
353            if let Some(highlighted_color) = highlighted_color {
354                table =
355                    table.row_highlight_style(Style::default().fg(highlighted_color).add_modifier(
356                        if focus {
357                            modifiers | TextModifiers::REVERSED
358                        } else {
359                            modifiers
360                        },
361                    ));
362            }
363            // Highlighted symbol
364            let hg_str = self
365                .props
366                .get_ref(Attribute::HighlightedStr)
367                .and_then(|x| x.as_string());
368            if let Some(hg_str) = hg_str {
369                table = table.highlight_symbol(hg_str.as_str());
370            }
371            // Col spacing
372            if let Some(spacing) = self
373                .props
374                .get(Attribute::Custom(TABLE_COLUMN_SPACING))
375                .map(|x| x.unwrap_size())
376            {
377                table = table.column_spacing(spacing);
378            }
379            // Header
380            let headers: Vec<&str> = self
381                .props
382                .get_ref(Attribute::Text)
383                .and_then(|v| v.as_payload())
384                .and_then(|v| v.as_vec())
385                .map(|v| {
386                    v.iter()
387                        .filter_map(|v| v.as_str().map(|v| v.as_str()))
388                        .collect()
389                })
390                .unwrap_or_default();
391            if !headers.is_empty() {
392                table = table.header(
393                    Row::new(headers)
394                        .style(
395                            Style::default()
396                                .fg(foreground)
397                                .bg(background)
398                                .add_modifier(modifiers),
399                        )
400                        .height(row_height),
401                );
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) {
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                match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
423                    Some(spans) => spans.len(),
424                    _ => 0,
425                },
426            );
427            self.states.fix_list_index();
428        } else if matches!(attr, Attribute::Value) && self.is_scrollable() {
429            self.states.list_index = self
430                .props
431                .get(Attribute::Value)
432                .map_or(0, |x| x.unwrap_payload().unwrap_one().unwrap_usize());
433            self.states.fix_list_index();
434        }
435    }
436
437    fn state(&self) -> State {
438        if self.is_scrollable() {
439            State::One(StateValue::Usize(self.states.list_index))
440        } else {
441            State::None
442        }
443    }
444
445    fn perform(&mut self, cmd: Cmd) -> CmdResult {
446        match cmd {
447            Cmd::Move(Direction::Down) => {
448                let prev = self.states.list_index;
449                self.states.incr_list_index(self.rewindable());
450                if prev == self.states.list_index {
451                    CmdResult::None
452                } else {
453                    CmdResult::Changed(self.state())
454                }
455            }
456            Cmd::Move(Direction::Up) => {
457                let prev = self.states.list_index;
458                self.states.decr_list_index(self.rewindable());
459                if prev == self.states.list_index {
460                    CmdResult::None
461                } else {
462                    CmdResult::Changed(self.state())
463                }
464            }
465            Cmd::Scroll(Direction::Down) => {
466                let prev = self.states.list_index;
467                let step = self
468                    .props
469                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
470                    .unwrap_length();
471                let step: usize = self.states.calc_max_step_ahead(step);
472                (0..step).for_each(|_| self.states.incr_list_index(false));
473                if prev == self.states.list_index {
474                    CmdResult::None
475                } else {
476                    CmdResult::Changed(self.state())
477                }
478            }
479            Cmd::Scroll(Direction::Up) => {
480                let prev = self.states.list_index;
481                let step = self
482                    .props
483                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
484                    .unwrap_length();
485                let step: usize = self.states.calc_max_step_behind(step);
486                (0..step).for_each(|_| self.states.decr_list_index(false));
487                if prev == self.states.list_index {
488                    CmdResult::None
489                } else {
490                    CmdResult::Changed(self.state())
491                }
492            }
493            Cmd::GoTo(Position::Begin) => {
494                let prev = self.states.list_index;
495                self.states.list_index_at_first();
496                if prev == self.states.list_index {
497                    CmdResult::None
498                } else {
499                    CmdResult::Changed(self.state())
500                }
501            }
502            Cmd::GoTo(Position::End) => {
503                let prev = self.states.list_index;
504                self.states.list_index_at_last();
505                if prev == self.states.list_index {
506                    CmdResult::None
507                } else {
508                    CmdResult::Changed(self.state())
509                }
510            }
511            _ => CmdResult::None,
512        }
513    }
514}
515
516#[cfg(test)]
517mod tests {
518
519    use super::*;
520    use pretty_assertions::assert_eq;
521    use tuirealm::props::{TableBuilder, TextSpan};
522
523    #[test]
524    fn table_states() {
525        let mut states = TableStates::default();
526        assert_eq!(states.list_index, 0);
527        assert_eq!(states.list_len, 0);
528        states.set_list_len(5);
529        assert_eq!(states.list_index, 0);
530        assert_eq!(states.list_len, 5);
531        // Incr
532        states.incr_list_index(true);
533        assert_eq!(states.list_index, 1);
534        states.list_index = 4;
535        states.incr_list_index(false);
536        assert_eq!(states.list_index, 4);
537        states.incr_list_index(true);
538        assert_eq!(states.list_index, 0);
539        // Decr
540        states.decr_list_index(false);
541        assert_eq!(states.list_index, 0);
542        states.decr_list_index(true);
543        assert_eq!(states.list_index, 4);
544        states.decr_list_index(true);
545        assert_eq!(states.list_index, 3);
546        // Begin
547        states.list_index_at_first();
548        assert_eq!(states.list_index, 0);
549        states.list_index_at_last();
550        assert_eq!(states.list_index, 4);
551        // Fix
552        states.set_list_len(3);
553        states.fix_list_index();
554        assert_eq!(states.list_index, 2);
555    }
556
557    #[test]
558    fn test_component_table_scrolling() {
559        // Make component
560        let mut component = Table::default()
561            .foreground(Color::Red)
562            .background(Color::Blue)
563            .highlighted_color(Color::Yellow)
564            .highlighted_str("🚀")
565            .modifiers(TextModifiers::BOLD)
566            .scroll(true)
567            .step(4)
568            .borders(Borders::default())
569            .title("events", Alignment::Center)
570            .column_spacing(4)
571            .widths(&[25, 25, 25, 25])
572            .row_height(3)
573            .headers(["Event", "Message", "Behaviour", "???"])
574            .table(
575                TableBuilder::default()
576                    .add_col(TextSpan::from("KeyCode::Down"))
577                    .add_col(TextSpan::from("OnKey"))
578                    .add_col(TextSpan::from("Move cursor down"))
579                    .add_row()
580                    .add_col(TextSpan::from("KeyCode::Up"))
581                    .add_col(TextSpan::from("OnKey"))
582                    .add_col(TextSpan::from("Move cursor up"))
583                    .add_row()
584                    .add_col(TextSpan::from("KeyCode::PageDown"))
585                    .add_col(TextSpan::from("OnKey"))
586                    .add_col(TextSpan::from("Move cursor down by 8"))
587                    .add_row()
588                    .add_col(TextSpan::from("KeyCode::PageUp"))
589                    .add_col(TextSpan::from("OnKey"))
590                    .add_col(TextSpan::from("ove cursor up by 8"))
591                    .add_row()
592                    .add_col(TextSpan::from("KeyCode::End"))
593                    .add_col(TextSpan::from("OnKey"))
594                    .add_col(TextSpan::from("Move cursor to last item"))
595                    .add_row()
596                    .add_col(TextSpan::from("KeyCode::Home"))
597                    .add_col(TextSpan::from("OnKey"))
598                    .add_col(TextSpan::from("Move cursor to first item"))
599                    .add_row()
600                    .add_col(TextSpan::from("KeyCode::Char(_)"))
601                    .add_col(TextSpan::from("OnKey"))
602                    .add_col(TextSpan::from("Return pressed key"))
603                    .add_col(TextSpan::from("4th mysterious columns"))
604                    .build(),
605            );
606        assert_eq!(component.states.list_len, 7);
607        assert_eq!(component.states.list_index, 0);
608        // Own funcs
609        assert_eq!(component.layout().len(), 4);
610        // Increment list index
611        component.states.list_index += 1;
612        assert_eq!(component.states.list_index, 1);
613        // Check messages
614        // Handle inputs
615        assert_eq!(
616            component.perform(Cmd::Move(Direction::Down)),
617            CmdResult::Changed(State::One(StateValue::Usize(2)))
618        );
619        // Index should be incremented
620        assert_eq!(component.states.list_index, 2);
621        // Index should be decremented
622        assert_eq!(
623            component.perform(Cmd::Move(Direction::Up)),
624            CmdResult::Changed(State::One(StateValue::Usize(1)))
625        );
626        // Index should be incremented
627        assert_eq!(component.states.list_index, 1);
628        // Index should be 2
629        assert_eq!(
630            component.perform(Cmd::Scroll(Direction::Down)),
631            CmdResult::Changed(State::One(StateValue::Usize(5)))
632        );
633        // Index should be incremented
634        assert_eq!(component.states.list_index, 5);
635        assert_eq!(
636            component.perform(Cmd::Scroll(Direction::Down)),
637            CmdResult::Changed(State::One(StateValue::Usize(6)))
638        );
639        // Index should be incremented
640        assert_eq!(component.states.list_index, 6);
641        // Index should be 0
642        assert_eq!(
643            component.perform(Cmd::Scroll(Direction::Up)),
644            CmdResult::Changed(State::One(StateValue::Usize(2)))
645        );
646        assert_eq!(component.states.list_index, 2);
647        assert_eq!(
648            component.perform(Cmd::Scroll(Direction::Up)),
649            CmdResult::Changed(State::One(StateValue::Usize(0)))
650        );
651        assert_eq!(component.states.list_index, 0);
652        // End
653        assert_eq!(
654            component.perform(Cmd::GoTo(Position::End)),
655            CmdResult::Changed(State::One(StateValue::Usize(6)))
656        );
657        assert_eq!(component.states.list_index, 6);
658        // Home
659        assert_eq!(
660            component.perform(Cmd::GoTo(Position::Begin)),
661            CmdResult::Changed(State::One(StateValue::Usize(0)))
662        );
663        assert_eq!(component.states.list_index, 0);
664        // Update
665        component.attr(
666            Attribute::Content,
667            AttrValue::Table(
668                TableBuilder::default()
669                    .add_col(TextSpan::from("name"))
670                    .add_col(TextSpan::from("age"))
671                    .add_col(TextSpan::from("birthdate"))
672                    .build(),
673            ),
674        );
675        assert_eq!(component.states.list_len, 1);
676        assert_eq!(component.states.list_index, 0);
677        // Get value
678        assert_eq!(component.state(), State::One(StateValue::Usize(0)));
679    }
680
681    #[test]
682    fn test_component_table_with_empty_rows_and_no_width_set() {
683        // Make component
684        let component = Table::default().table(TableBuilder::default().build());
685
686        assert_eq!(component.states.list_len, 1);
687        assert_eq!(component.states.list_index, 0);
688        // calculating layout would fail if no widths and using "empty" TableBuilder
689        assert_eq!(component.layout().len(), 0);
690    }
691
692    #[test]
693    fn test_components_table() {
694        // Make component
695        let component = Table::default()
696            .foreground(Color::Red)
697            .background(Color::Blue)
698            .highlighted_color(Color::Yellow)
699            .highlighted_str("🚀")
700            .modifiers(TextModifiers::BOLD)
701            .borders(Borders::default())
702            .title("events", Alignment::Center)
703            .column_spacing(4)
704            .widths(&[33, 33, 33])
705            .row_height(3)
706            .headers(["Event", "Message", "Behaviour"])
707            .table(
708                TableBuilder::default()
709                    .add_col(TextSpan::from("KeyCode::Down"))
710                    .add_col(TextSpan::from("OnKey"))
711                    .add_col(TextSpan::from("Move cursor down"))
712                    .add_row()
713                    .add_col(TextSpan::from("KeyCode::Up"))
714                    .add_col(TextSpan::from("OnKey"))
715                    .add_col(TextSpan::from("Move cursor up"))
716                    .add_row()
717                    .add_col(TextSpan::from("KeyCode::PageDown"))
718                    .add_col(TextSpan::from("OnKey"))
719                    .add_col(TextSpan::from("Move cursor down by 8"))
720                    .add_row()
721                    .add_col(TextSpan::from("KeyCode::PageUp"))
722                    .add_col(TextSpan::from("OnKey"))
723                    .add_col(TextSpan::from("ove cursor up by 8"))
724                    .add_row()
725                    .add_col(TextSpan::from("KeyCode::End"))
726                    .add_col(TextSpan::from("OnKey"))
727                    .add_col(TextSpan::from("Move cursor to last item"))
728                    .add_row()
729                    .add_col(TextSpan::from("KeyCode::Home"))
730                    .add_col(TextSpan::from("OnKey"))
731                    .add_col(TextSpan::from("Move cursor to first item"))
732                    .add_row()
733                    .add_col(TextSpan::from("KeyCode::Char(_)"))
734                    .add_col(TextSpan::from("OnKey"))
735                    .add_col(TextSpan::from("Return pressed key"))
736                    .build(),
737            );
738        // Get value (not scrollable)
739        assert_eq!(component.state(), State::None);
740    }
741
742    #[test]
743    fn should_init_list_value() {
744        let mut component = Table::default()
745            .foreground(Color::Red)
746            .background(Color::Blue)
747            .highlighted_color(Color::Yellow)
748            .highlighted_str("🚀")
749            .modifiers(TextModifiers::BOLD)
750            .borders(Borders::default())
751            .title("events", Alignment::Center)
752            .table(
753                TableBuilder::default()
754                    .add_col(TextSpan::from("KeyCode::Down"))
755                    .add_col(TextSpan::from("OnKey"))
756                    .add_col(TextSpan::from("Move cursor down"))
757                    .add_row()
758                    .add_col(TextSpan::from("KeyCode::Up"))
759                    .add_col(TextSpan::from("OnKey"))
760                    .add_col(TextSpan::from("Move cursor up"))
761                    .add_row()
762                    .add_col(TextSpan::from("KeyCode::PageDown"))
763                    .add_col(TextSpan::from("OnKey"))
764                    .add_col(TextSpan::from("Move cursor down by 8"))
765                    .add_row()
766                    .add_col(TextSpan::from("KeyCode::PageUp"))
767                    .add_col(TextSpan::from("OnKey"))
768                    .add_col(TextSpan::from("ove cursor up by 8"))
769                    .add_row()
770                    .add_col(TextSpan::from("KeyCode::End"))
771                    .add_col(TextSpan::from("OnKey"))
772                    .add_col(TextSpan::from("Move cursor to last item"))
773                    .add_row()
774                    .add_col(TextSpan::from("KeyCode::Home"))
775                    .add_col(TextSpan::from("OnKey"))
776                    .add_col(TextSpan::from("Move cursor to first item"))
777                    .add_row()
778                    .add_col(TextSpan::from("KeyCode::Char(_)"))
779                    .add_col(TextSpan::from("OnKey"))
780                    .add_col(TextSpan::from("Return pressed key"))
781                    .build(),
782            )
783            .scroll(true)
784            .selected_line(2);
785        assert_eq!(component.states.list_index, 2);
786        // Index out of bounds
787        component.attr(
788            Attribute::Value,
789            AttrValue::Payload(PropPayload::One(PropValue::Usize(50))),
790        );
791        assert_eq!(component.states.list_index, 6);
792    }
793
794    #[test]
795    fn various_header_types() {
796        // static array of static strings
797        let _ = Table::default().headers(["hello"]);
798        // static array of strings
799        let _ = Table::default().headers(["hello".to_string()]);
800        // vec of static strings
801        let _ = Table::default().headers(vec!["hello"]);
802        // vec of strings
803        let _ = Table::default().headers(vec!["hello".to_string()]);
804        // boxed array of static strings
805        let _ = Table::default().headers(vec!["hello"].into_boxed_slice());
806        // boxed array of strings
807        let _ = Table::default().headers(vec!["hello".to_string()].into_boxed_slice());
808    }
809}