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    pub fn calc_max_step_ahead(&self, max: usize) -> usize {
93        let remaining: usize = match self.list_len {
94            0 => 0,
95            len => len - 1 - self.list_index,
96        };
97        if remaining > max {
98            max
99        } else {
100            remaining
101        }
102    }
103
104    /// ### calc_max_step_ahead
105    ///
106    /// Calculate the max step ahead to scroll list
107    pub fn calc_max_step_behind(&self, max: usize) -> usize {
108        if self.list_index > max {
109            max
110        } else {
111            self.list_index
112        }
113    }
114}
115
116// -- Component
117
118/// ## Table
119///
120/// represents a read-only text component without any container.
121#[derive(Default)]
122pub struct Table {
123    props: Props,
124    pub states: TableStates,
125    hg_str: Option<String>, // CRAP CRAP CRAP
126    headers: Vec<String>,   // CRAP CRAP CRAP
127}
128
129impl Table {
130    pub fn foreground(mut self, fg: Color) -> Self {
131        self.attr(Attribute::Foreground, AttrValue::Color(fg));
132        self
133    }
134
135    pub fn background(mut self, bg: Color) -> Self {
136        self.attr(Attribute::Background, AttrValue::Color(bg));
137        self
138    }
139
140    pub fn inactive(mut self, s: Style) -> Self {
141        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
142        self
143    }
144
145    pub fn modifiers(mut self, m: TextModifiers) -> Self {
146        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
147        self
148    }
149
150    pub fn borders(mut self, b: Borders) -> Self {
151        self.attr(Attribute::Borders, AttrValue::Borders(b));
152        self
153    }
154
155    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
156        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
157        self
158    }
159
160    pub fn step(mut self, step: usize) -> Self {
161        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
162        self
163    }
164
165    pub fn scroll(mut self, scrollable: bool) -> Self {
166        self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
167        self
168    }
169
170    pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
171        self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
172        self
173    }
174
175    pub fn highlighted_color(mut self, c: Color) -> Self {
176        self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
177        self
178    }
179
180    pub fn column_spacing(mut self, w: u16) -> Self {
181        self.attr(Attribute::Custom(TABLE_COLUMN_SPACING), AttrValue::Size(w));
182        self
183    }
184
185    pub fn row_height(mut self, h: u16) -> Self {
186        self.attr(Attribute::Height, AttrValue::Size(h));
187        self
188    }
189
190    pub fn widths(mut self, w: &[u16]) -> Self {
191        self.attr(
192            Attribute::Width,
193            AttrValue::Payload(PropPayload::Vec(
194                w.iter().map(|x| PropValue::U16(*x)).collect(),
195            )),
196        );
197        self
198    }
199
200    pub fn headers<S: AsRef<str>>(mut self, headers: &[S]) -> Self {
201        self.attr(
202            Attribute::Text,
203            AttrValue::Payload(PropPayload::Vec(
204                headers
205                    .iter()
206                    .map(|x| PropValue::Str(x.as_ref().to_string()))
207                    .collect(),
208            )),
209        );
210        self
211    }
212
213    pub fn table(mut self, t: PropTable) -> Self {
214        self.attr(Attribute::Content, AttrValue::Table(t));
215        self
216    }
217
218    pub fn rewind(mut self, r: bool) -> Self {
219        self.attr(Attribute::Rewind, AttrValue::Flag(r));
220        self
221    }
222
223    /// Set initial selected line
224    /// This method must be called after `rows` and `scrollable` in order to work
225    pub fn selected_line(mut self, line: usize) -> Self {
226        self.attr(
227            Attribute::Value,
228            AttrValue::Payload(PropPayload::One(PropValue::Usize(line))),
229        );
230        self
231    }
232
233    /// ### scrollable
234    ///
235    /// returns the value of the scrollable flag; by default is false
236    fn is_scrollable(&self) -> bool {
237        self.props
238            .get_or(Attribute::Scroll, AttrValue::Flag(false))
239            .unwrap_flag()
240    }
241
242    fn rewindable(&self) -> bool {
243        self.props
244            .get_or(Attribute::Rewind, AttrValue::Flag(false))
245            .unwrap_flag()
246    }
247
248    /// ### layout
249    ///
250    /// Returns layout based on properties.
251    /// If layout is not set in properties, they'll be divided by rows number
252    fn layout(&self) -> Vec<Constraint> {
253        match self.props.get(Attribute::Width).map(|x| x.unwrap_payload()) {
254            Some(PropPayload::Vec(widths)) => widths
255                .iter()
256                .cloned()
257                .map(|x| x.unwrap_u16())
258                .map(Constraint::Percentage)
259                .collect(),
260            _ => {
261                // Get amount of columns (maximum len of row elements)
262                let columns: usize =
263                    match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
264                        Some(rows) => rows.iter().map(|col| col.len()).max().unwrap_or(1),
265                        _ => 1,
266                    };
267                // Calc width in equal way, make sure not to divide by zero (this can happen when rows is [[]])
268                let width: u16 = (100 / max(columns, 1)) as u16;
269                (0..columns)
270                    .map(|_| Constraint::Percentage(width))
271                    .collect()
272            }
273        }
274    }
275}
276
277impl MockComponent for Table {
278    fn view(&mut self, render: &mut Frame, area: Rect) {
279        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
280            let foreground = self
281                .props
282                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
283                .unwrap_color();
284            let background = self
285                .props
286                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
287                .unwrap_color();
288            let modifiers = self
289                .props
290                .get_or(
291                    Attribute::TextProps,
292                    AttrValue::TextModifiers(TextModifiers::empty()),
293                )
294                .unwrap_text_modifiers();
295            let title = self
296                .props
297                .get_or(
298                    Attribute::Title,
299                    AttrValue::Title((String::default(), Alignment::Center)),
300                )
301                .unwrap_title();
302            let borders = self
303                .props
304                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
305                .unwrap_borders();
306            let focus = self
307                .props
308                .get_or(Attribute::Focus, AttrValue::Flag(false))
309                .unwrap_flag();
310            let inactive_style = self
311                .props
312                .get(Attribute::FocusStyle)
313                .map(|x| x.unwrap_style());
314            let row_height = self
315                .props
316                .get_or(Attribute::Height, AttrValue::Size(1))
317                .unwrap_size();
318            // Make rows
319            let rows: Vec<Row> = match self.props.get(Attribute::Content).map(|x| x.unwrap_table())
320            {
321                Some(table) => table
322                    .iter()
323                    .map(|row| {
324                        let columns: Vec<Cell> = row
325                            .iter()
326                            .map(|col| {
327                                let (fg, bg, modifiers) =
328                                    crate::utils::use_or_default_styles(&self.props, col);
329                                Cell::from(Span::styled(
330                                    col.content.clone(),
331                                    Style::default().add_modifier(modifiers).fg(fg).bg(bg),
332                                ))
333                            })
334                            .collect();
335                        Row::new(columns).height(row_height)
336                    })
337                    .collect(), // Make List item from TextSpan
338                _ => Vec::new(),
339            };
340            let highlighted_color = self
341                .props
342                .get(Attribute::HighlightedColor)
343                .map(|x| x.unwrap_color());
344            let widths: Vec<Constraint> = self.layout();
345
346            let mut table = TuiTable::new(rows, &widths).block(crate::utils::get_block(
347                borders,
348                Some(title),
349                focus,
350                inactive_style,
351            ));
352            if let Some(highlighted_color) = highlighted_color {
353                table = table.highlight_style(Style::default().fg(highlighted_color).add_modifier(
354                    match focus {
355                        true => modifiers | TextModifiers::REVERSED,
356                        false => modifiers,
357                    },
358                ));
359            }
360            // Highlighted symbol
361            self.hg_str = self
362                .props
363                .get(Attribute::HighlightedStr)
364                .map(|x| x.unwrap_string());
365            if let Some(hg_str) = &self.hg_str {
366                table = table.highlight_symbol(hg_str.as_str());
367            }
368            // Col spacing
369            if let Some(spacing) = self
370                .props
371                .get(Attribute::Custom(TABLE_COLUMN_SPACING))
372                .map(|x| x.unwrap_size())
373            {
374                table = table.column_spacing(spacing);
375            }
376            // Header
377            self.headers = self
378                .props
379                .get(Attribute::Text)
380                .map(|x| {
381                    x.unwrap_payload()
382                        .unwrap_vec()
383                        .into_iter()
384                        .map(|x| x.unwrap_str())
385                        .collect()
386                })
387                .unwrap_or_default();
388            if !self.headers.is_empty() {
389                let headers: Vec<&str> = self.headers.iter().map(|x| x.as_str()).collect();
390                table = table.header(
391                    Row::new(headers)
392                        .style(
393                            Style::default()
394                                .fg(foreground)
395                                .bg(background)
396                                .add_modifier(modifiers),
397                        )
398                        .height(row_height),
399                );
400            }
401            if self.is_scrollable() {
402                let mut state: TableState = TableState::default();
403                state.select(Some(self.states.list_index));
404                render.render_stateful_widget(table, area, &mut state);
405            } else {
406                render.render_widget(table, area);
407            }
408        }
409    }
410
411    fn query(&self, attr: Attribute) -> Option<AttrValue> {
412        self.props.get(attr)
413    }
414
415    fn attr(&mut self, attr: Attribute, value: AttrValue) {
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                match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
421                    Some(spans) => spans.len(),
422                    _ => 0,
423                },
424            );
425            self.states.fix_list_index();
426        } else if matches!(attr, Attribute::Value) && self.is_scrollable() {
427            self.states.list_index = self
428                .props
429                .get(Attribute::Value)
430                .map(|x| x.unwrap_payload().unwrap_one().unwrap_usize())
431                .unwrap_or(0);
432            self.states.fix_list_index();
433        }
434    }
435
436    fn state(&self) -> State {
437        match self.is_scrollable() {
438            true => State::One(StateValue::Usize(self.states.list_index)),
439            false => State::None,
440        }
441    }
442
443    fn perform(&mut self, cmd: Cmd) -> CmdResult {
444        match cmd {
445            Cmd::Move(Direction::Down) => {
446                let prev = self.states.list_index;
447                self.states.incr_list_index(self.rewindable());
448                if prev != self.states.list_index {
449                    CmdResult::Changed(self.state())
450                } else {
451                    CmdResult::None
452                }
453            }
454            Cmd::Move(Direction::Up) => {
455                let prev = self.states.list_index;
456                self.states.decr_list_index(self.rewindable());
457                if prev != self.states.list_index {
458                    CmdResult::Changed(self.state())
459                } else {
460                    CmdResult::None
461                }
462            }
463            Cmd::Scroll(Direction::Down) => {
464                let prev = self.states.list_index;
465                let step = self
466                    .props
467                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
468                    .unwrap_length();
469                let step: usize = self.states.calc_max_step_ahead(step);
470                (0..step).for_each(|_| self.states.incr_list_index(false));
471                if prev != self.states.list_index {
472                    CmdResult::Changed(self.state())
473                } else {
474                    CmdResult::None
475                }
476            }
477            Cmd::Scroll(Direction::Up) => {
478                let prev = self.states.list_index;
479                let step = self
480                    .props
481                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
482                    .unwrap_length();
483                let step: usize = self.states.calc_max_step_behind(step);
484                (0..step).for_each(|_| self.states.decr_list_index(false));
485                if prev != self.states.list_index {
486                    CmdResult::Changed(self.state())
487                } else {
488                    CmdResult::None
489                }
490            }
491            Cmd::GoTo(Position::Begin) => {
492                let prev = self.states.list_index;
493                self.states.list_index_at_first();
494                if prev != self.states.list_index {
495                    CmdResult::Changed(self.state())
496                } else {
497                    CmdResult::None
498                }
499            }
500            Cmd::GoTo(Position::End) => {
501                let prev = self.states.list_index;
502                self.states.list_index_at_last();
503                if prev != self.states.list_index {
504                    CmdResult::Changed(self.state())
505                } else {
506                    CmdResult::None
507                }
508            }
509            _ => CmdResult::None,
510        }
511    }
512}
513
514#[cfg(test)]
515mod tests {
516
517    use super::*;
518    use pretty_assertions::assert_eq;
519    use tuirealm::props::{TableBuilder, TextSpan};
520
521    #[test]
522    fn table_states() {
523        let mut states = TableStates::default();
524        assert_eq!(states.list_index, 0);
525        assert_eq!(states.list_len, 0);
526        states.set_list_len(5);
527        assert_eq!(states.list_index, 0);
528        assert_eq!(states.list_len, 5);
529        // Incr
530        states.incr_list_index(true);
531        assert_eq!(states.list_index, 1);
532        states.list_index = 4;
533        states.incr_list_index(false);
534        assert_eq!(states.list_index, 4);
535        states.incr_list_index(true);
536        assert_eq!(states.list_index, 0);
537        // Decr
538        states.decr_list_index(false);
539        assert_eq!(states.list_index, 0);
540        states.decr_list_index(true);
541        assert_eq!(states.list_index, 4);
542        states.decr_list_index(true);
543        assert_eq!(states.list_index, 3);
544        // Begin
545        states.list_index_at_first();
546        assert_eq!(states.list_index, 0);
547        states.list_index_at_last();
548        assert_eq!(states.list_index, 4);
549        // Fix
550        states.set_list_len(3);
551        states.fix_list_index();
552        assert_eq!(states.list_index, 2);
553    }
554
555    #[test]
556    fn test_component_table_scrolling() {
557        // Make component
558        let mut component = Table::default()
559            .foreground(Color::Red)
560            .background(Color::Blue)
561            .highlighted_color(Color::Yellow)
562            .highlighted_str("🚀")
563            .modifiers(TextModifiers::BOLD)
564            .scroll(true)
565            .step(4)
566            .borders(Borders::default())
567            .title("events", Alignment::Center)
568            .column_spacing(4)
569            .widths(&[25, 25, 25, 25])
570            .row_height(3)
571            .headers(&["Event", "Message", "Behaviour", "???"])
572            .table(
573                TableBuilder::default()
574                    .add_col(TextSpan::from("KeyCode::Down"))
575                    .add_col(TextSpan::from("OnKey"))
576                    .add_col(TextSpan::from("Move cursor down"))
577                    .add_row()
578                    .add_col(TextSpan::from("KeyCode::Up"))
579                    .add_col(TextSpan::from("OnKey"))
580                    .add_col(TextSpan::from("Move cursor up"))
581                    .add_row()
582                    .add_col(TextSpan::from("KeyCode::PageDown"))
583                    .add_col(TextSpan::from("OnKey"))
584                    .add_col(TextSpan::from("Move cursor down by 8"))
585                    .add_row()
586                    .add_col(TextSpan::from("KeyCode::PageUp"))
587                    .add_col(TextSpan::from("OnKey"))
588                    .add_col(TextSpan::from("ove cursor up by 8"))
589                    .add_row()
590                    .add_col(TextSpan::from("KeyCode::End"))
591                    .add_col(TextSpan::from("OnKey"))
592                    .add_col(TextSpan::from("Move cursor to last item"))
593                    .add_row()
594                    .add_col(TextSpan::from("KeyCode::Home"))
595                    .add_col(TextSpan::from("OnKey"))
596                    .add_col(TextSpan::from("Move cursor to first item"))
597                    .add_row()
598                    .add_col(TextSpan::from("KeyCode::Char(_)"))
599                    .add_col(TextSpan::from("OnKey"))
600                    .add_col(TextSpan::from("Return pressed key"))
601                    .add_col(TextSpan::from("4th mysterious columns"))
602                    .build(),
603            );
604        assert_eq!(component.states.list_len, 7);
605        assert_eq!(component.states.list_index, 0);
606        // Own funcs
607        assert_eq!(component.layout().len(), 4);
608        // Increment list index
609        component.states.list_index += 1;
610        assert_eq!(component.states.list_index, 1);
611        // Check messages
612        // Handle inputs
613        assert_eq!(
614            component.perform(Cmd::Move(Direction::Down)),
615            CmdResult::Changed(State::One(StateValue::Usize(2)))
616        );
617        // Index should be incremented
618        assert_eq!(component.states.list_index, 2);
619        // Index should be decremented
620        assert_eq!(
621            component.perform(Cmd::Move(Direction::Up)),
622            CmdResult::Changed(State::One(StateValue::Usize(1)))
623        );
624        // Index should be incremented
625        assert_eq!(component.states.list_index, 1);
626        // Index should be 2
627        assert_eq!(
628            component.perform(Cmd::Scroll(Direction::Down)),
629            CmdResult::Changed(State::One(StateValue::Usize(5)))
630        );
631        // Index should be incremented
632        assert_eq!(component.states.list_index, 5);
633        assert_eq!(
634            component.perform(Cmd::Scroll(Direction::Down)),
635            CmdResult::Changed(State::One(StateValue::Usize(6)))
636        );
637        // Index should be incremented
638        assert_eq!(component.states.list_index, 6);
639        // Index should be 0
640        assert_eq!(
641            component.perform(Cmd::Scroll(Direction::Up)),
642            CmdResult::Changed(State::One(StateValue::Usize(2)))
643        );
644        assert_eq!(component.states.list_index, 2);
645        assert_eq!(
646            component.perform(Cmd::Scroll(Direction::Up)),
647            CmdResult::Changed(State::One(StateValue::Usize(0)))
648        );
649        assert_eq!(component.states.list_index, 0);
650        // End
651        assert_eq!(
652            component.perform(Cmd::GoTo(Position::End)),
653            CmdResult::Changed(State::One(StateValue::Usize(6)))
654        );
655        assert_eq!(component.states.list_index, 6);
656        // Home
657        assert_eq!(
658            component.perform(Cmd::GoTo(Position::Begin)),
659            CmdResult::Changed(State::One(StateValue::Usize(0)))
660        );
661        assert_eq!(component.states.list_index, 0);
662        // Update
663        component.attr(
664            Attribute::Content,
665            AttrValue::Table(
666                TableBuilder::default()
667                    .add_col(TextSpan::from("name"))
668                    .add_col(TextSpan::from("age"))
669                    .add_col(TextSpan::from("birthdate"))
670                    .build(),
671            ),
672        );
673        assert_eq!(component.states.list_len, 1);
674        assert_eq!(component.states.list_index, 0);
675        // Get value
676        assert_eq!(component.state(), State::One(StateValue::Usize(0)));
677    }
678
679    #[test]
680    fn test_component_table_with_empty_rows_and_no_width_set() {
681        // Make component
682        let component = Table::default().table(TableBuilder::default().build());
683
684        assert_eq!(component.states.list_len, 1);
685        assert_eq!(component.states.list_index, 0);
686        // calculating layout would fail if no widths and using "empty" TableBuilder
687        assert_eq!(component.layout().len(), 0);
688    }
689
690    #[test]
691    fn test_components_table() {
692        // Make component
693        let component = Table::default()
694            .foreground(Color::Red)
695            .background(Color::Blue)
696            .highlighted_color(Color::Yellow)
697            .highlighted_str("🚀")
698            .modifiers(TextModifiers::BOLD)
699            .borders(Borders::default())
700            .title("events", Alignment::Center)
701            .column_spacing(4)
702            .widths(&[33, 33, 33])
703            .row_height(3)
704            .headers(&["Event", "Message", "Behaviour"])
705            .table(
706                TableBuilder::default()
707                    .add_col(TextSpan::from("KeyCode::Down"))
708                    .add_col(TextSpan::from("OnKey"))
709                    .add_col(TextSpan::from("Move cursor down"))
710                    .add_row()
711                    .add_col(TextSpan::from("KeyCode::Up"))
712                    .add_col(TextSpan::from("OnKey"))
713                    .add_col(TextSpan::from("Move cursor up"))
714                    .add_row()
715                    .add_col(TextSpan::from("KeyCode::PageDown"))
716                    .add_col(TextSpan::from("OnKey"))
717                    .add_col(TextSpan::from("Move cursor down by 8"))
718                    .add_row()
719                    .add_col(TextSpan::from("KeyCode::PageUp"))
720                    .add_col(TextSpan::from("OnKey"))
721                    .add_col(TextSpan::from("ove cursor up by 8"))
722                    .add_row()
723                    .add_col(TextSpan::from("KeyCode::End"))
724                    .add_col(TextSpan::from("OnKey"))
725                    .add_col(TextSpan::from("Move cursor to last item"))
726                    .add_row()
727                    .add_col(TextSpan::from("KeyCode::Home"))
728                    .add_col(TextSpan::from("OnKey"))
729                    .add_col(TextSpan::from("Move cursor to first item"))
730                    .add_row()
731                    .add_col(TextSpan::from("KeyCode::Char(_)"))
732                    .add_col(TextSpan::from("OnKey"))
733                    .add_col(TextSpan::from("Return pressed key"))
734                    .build(),
735            );
736        // Get value (not scrollable)
737        assert_eq!(component.state(), State::None);
738    }
739
740    #[test]
741    fn should_init_list_value() {
742        let mut component = Table::default()
743            .foreground(Color::Red)
744            .background(Color::Blue)
745            .highlighted_color(Color::Yellow)
746            .highlighted_str("🚀")
747            .modifiers(TextModifiers::BOLD)
748            .borders(Borders::default())
749            .title("events", Alignment::Center)
750            .table(
751                TableBuilder::default()
752                    .add_col(TextSpan::from("KeyCode::Down"))
753                    .add_col(TextSpan::from("OnKey"))
754                    .add_col(TextSpan::from("Move cursor down"))
755                    .add_row()
756                    .add_col(TextSpan::from("KeyCode::Up"))
757                    .add_col(TextSpan::from("OnKey"))
758                    .add_col(TextSpan::from("Move cursor up"))
759                    .add_row()
760                    .add_col(TextSpan::from("KeyCode::PageDown"))
761                    .add_col(TextSpan::from("OnKey"))
762                    .add_col(TextSpan::from("Move cursor down by 8"))
763                    .add_row()
764                    .add_col(TextSpan::from("KeyCode::PageUp"))
765                    .add_col(TextSpan::from("OnKey"))
766                    .add_col(TextSpan::from("ove cursor up by 8"))
767                    .add_row()
768                    .add_col(TextSpan::from("KeyCode::End"))
769                    .add_col(TextSpan::from("OnKey"))
770                    .add_col(TextSpan::from("Move cursor to last item"))
771                    .add_row()
772                    .add_col(TextSpan::from("KeyCode::Home"))
773                    .add_col(TextSpan::from("OnKey"))
774                    .add_col(TextSpan::from("Move cursor to first item"))
775                    .add_row()
776                    .add_col(TextSpan::from("KeyCode::Char(_)"))
777                    .add_col(TextSpan::from("OnKey"))
778                    .add_col(TextSpan::from("Return pressed key"))
779                    .build(),
780            )
781            .scroll(true)
782            .selected_line(2);
783        assert_eq!(component.states.list_index, 2);
784        // Index out of bounds
785        component.attr(
786            Attribute::Value,
787            AttrValue::Payload(PropPayload::One(PropValue::Usize(50))),
788        );
789        assert_eq!(component.states.list_index, 6);
790    }
791}