ratatui/widgets/table/
table_state.rs

1/// State of a [`Table`] widget
2///
3/// This state can be used to scroll through the rows and select one of them. When the table is
4/// rendered as a stateful widget, the selected row, column and cell will be highlighted and the
5/// table will be shifted to ensure that the selected row is visible. This will modify the
6/// [`TableState`] object passed to the [`Frame::render_stateful_widget`] method.
7///
8/// The state consists of two fields:
9/// - [`offset`]: the index of the first row to be displayed
10/// - [`selected`]: the index of the selected row, which can be `None` if no row is selected
11/// - [`selected_column`]: the index of the selected column, which can be `None` if no column is
12///   selected
13///
14/// [`offset`]: TableState::offset()
15/// [`selected`]: TableState::selected()
16/// [`selected_column`]: TableState::selected_column()
17///
18/// See the `table` example and the `recipe` and `traceroute` tabs in the demo2 example in the
19/// [Examples] directory for a more in depth example of the various configuration options and for
20/// how to handle state.
21///
22/// [Examples]: https://github.com/ratatui/ratatui/blob/master/examples/README.md
23///
24/// # Example
25///
26/// ```rust
27/// use ratatui::{
28///     layout::{Constraint, Rect},
29///     widgets::{Row, Table, TableState},
30///     Frame,
31/// };
32///
33/// # fn ui(frame: &mut Frame) {
34/// # let area = Rect::default();
35/// let rows = [Row::new(vec!["Cell1", "Cell2"])];
36/// let widths = [Constraint::Length(5), Constraint::Length(5)];
37/// let table = Table::new(rows, widths).widths(widths);
38///
39/// // Note: TableState should be stored in your application state (not constructed in your render
40/// // method) so that the selected row is preserved across renders
41/// let mut table_state = TableState::default();
42/// *table_state.offset_mut() = 1; // display the second row and onwards
43/// table_state.select(Some(3)); // select the forth row (0-indexed)
44/// table_state.select_column(Some(2)); // select the third column (0-indexed)
45///
46/// frame.render_stateful_widget(table, area, &mut table_state);
47/// # }
48/// ```
49///
50/// Note that if [`Table::widths`] is not called before rendering, the rendered columns will have
51/// equal width.
52///
53/// [`Table`]: crate::widgets::Table
54/// [`Table::widths`]: crate::widgets::Table::widths
55/// [`Frame::render_stateful_widget`]: crate::Frame::render_stateful_widget
56#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct TableState {
59    pub(crate) offset: usize,
60    pub(crate) selected: Option<usize>,
61    pub(crate) selected_column: Option<usize>,
62}
63
64impl TableState {
65    /// Creates a new [`TableState`]
66    ///
67    /// # Examples
68    ///
69    /// ```rust
70    /// use ratatui::widgets::TableState;
71    ///
72    /// let state = TableState::new();
73    /// ```
74    pub const fn new() -> Self {
75        Self {
76            offset: 0,
77            selected: None,
78            selected_column: None,
79        }
80    }
81
82    /// Sets the index of the first row to be displayed
83    ///
84    /// This is a fluent setter method which must be chained or used as it consumes self
85    ///
86    /// # Examples
87    ///
88    /// ```rust
89    /// use ratatui::widgets::TableState;
90    ///
91    /// let state = TableState::new().with_offset(1);
92    /// ```
93    #[must_use = "method moves the value of self and returns the modified value"]
94    pub const fn with_offset(mut self, offset: usize) -> Self {
95        self.offset = offset;
96        self
97    }
98
99    /// Sets the index of the selected row
100    ///
101    /// This is a fluent setter method which must be chained or used as it consumes self
102    ///
103    /// # Examples
104    ///
105    /// ```rust
106    /// use ratatui::widgets::TableState;
107    ///
108    /// let state = TableState::new().with_selected(Some(1));
109    /// ```
110    #[must_use = "method moves the value of self and returns the modified value"]
111    pub fn with_selected<T>(mut self, selected: T) -> Self
112    where
113        T: Into<Option<usize>>,
114    {
115        self.selected = selected.into();
116        self
117    }
118
119    /// Sets the index of the selected column
120    ///
121    /// This is a fluent setter method which must be chained or used as it consumes self
122    ///
123    /// # Examples
124    ///
125    /// ```rust
126    /// # use ratatui::widgets::{TableState};
127    /// let state = TableState::new().with_selected_column(Some(1));
128    /// ```
129    #[must_use = "method moves the value of self and returns the modified value"]
130    pub fn with_selected_column<T>(mut self, selected: T) -> Self
131    where
132        T: Into<Option<usize>>,
133    {
134        self.selected_column = selected.into();
135        self
136    }
137
138    /// Sets the indexes of the selected cell
139    ///
140    /// This is a fluent setter method which must be chained or used as it consumes self
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// # use ratatui::widgets::{TableState};
146    /// let state = TableState::new().with_selected_cell(Some((1, 5)));
147    /// ```
148    #[must_use = "method moves the value of self and returns the modified value"]
149    pub fn with_selected_cell<T>(mut self, selected: T) -> Self
150    where
151        T: Into<Option<(usize, usize)>>,
152    {
153        if let Some((r, c)) = selected.into() {
154            self.selected = Some(r);
155            self.selected_column = Some(c);
156        } else {
157            self.selected = None;
158            self.selected_column = None;
159        }
160
161        self
162    }
163
164    /// Index of the first row to be displayed
165    ///
166    /// # Examples
167    ///
168    /// ```rust
169    /// use ratatui::widgets::TableState;
170    ///
171    /// let state = TableState::new();
172    /// assert_eq!(state.offset(), 0);
173    /// ```
174    pub const fn offset(&self) -> usize {
175        self.offset
176    }
177
178    /// Mutable reference to the index of the first row to be displayed
179    ///
180    /// # Examples
181    ///
182    /// ```rust
183    /// use ratatui::widgets::TableState;
184    ///
185    /// let mut state = TableState::default();
186    /// *state.offset_mut() = 1;
187    /// ```
188    pub fn offset_mut(&mut self) -> &mut usize {
189        &mut self.offset
190    }
191
192    /// Index of the selected row
193    ///
194    /// Returns `None` if no row is selected
195    ///
196    /// # Examples
197    ///
198    /// ```rust
199    /// use ratatui::widgets::TableState;
200    ///
201    /// let state = TableState::new();
202    /// assert_eq!(state.selected(), None);
203    /// ```
204    pub const fn selected(&self) -> Option<usize> {
205        self.selected
206    }
207
208    /// Index of the selected column
209    ///
210    /// Returns `None` if no column is selected
211    ///
212    /// # Examples
213    ///
214    /// ```rust
215    /// # use ratatui::widgets::{TableState};
216    /// let state = TableState::new();
217    /// assert_eq!(state.selected_column(), None);
218    /// ```
219    pub const fn selected_column(&self) -> Option<usize> {
220        self.selected_column
221    }
222
223    /// Indexes of the selected cell
224    ///
225    /// Returns `None` if no cell is selected
226    ///
227    /// # Examples
228    ///
229    /// ```rust
230    /// # use ratatui::widgets::{TableState};
231    /// let state = TableState::new();
232    /// assert_eq!(state.selected_cell(), None);
233    /// ```
234    pub const fn selected_cell(&self) -> Option<(usize, usize)> {
235        if let (Some(r), Some(c)) = (self.selected, self.selected_column) {
236            return Some((r, c));
237        }
238        None
239    }
240
241    /// Mutable reference to the index of the selected row
242    ///
243    /// Returns `None` if no row is selected
244    ///
245    /// # Examples
246    ///
247    /// ```rust
248    /// use ratatui::widgets::TableState;
249    ///
250    /// let mut state = TableState::default();
251    /// *state.selected_mut() = Some(1);
252    /// ```
253    pub fn selected_mut(&mut self) -> &mut Option<usize> {
254        &mut self.selected
255    }
256
257    /// Mutable reference to the index of the selected column
258    ///
259    /// Returns `None` if no column is selected
260    ///
261    /// # Examples
262    ///
263    /// ```rust
264    /// # use ratatui::widgets::{TableState};
265    /// let mut state = TableState::default();
266    /// *state.selected_column_mut() = Some(1);
267    /// ```
268    pub fn selected_column_mut(&mut self) -> &mut Option<usize> {
269        &mut self.selected_column
270    }
271
272    /// Sets the index of the selected row
273    ///
274    /// Set to `None` if no row is selected. This will also reset the offset to `0`.
275    ///
276    /// # Examples
277    ///
278    /// ```rust
279    /// use ratatui::widgets::TableState;
280    ///
281    /// let mut state = TableState::default();
282    /// state.select(Some(1));
283    /// ```
284    pub fn select(&mut self, index: Option<usize>) {
285        self.selected = index;
286        if index.is_none() {
287            self.offset = 0;
288        }
289    }
290
291    /// Sets the index of the selected column
292    ///
293    /// # Examples
294    ///
295    /// ```rust
296    /// # use ratatui::widgets::{TableState};
297    /// let mut state = TableState::default();
298    /// state.select_column(Some(1));
299    /// ```
300    pub fn select_column(&mut self, index: Option<usize>) {
301        self.selected_column = index;
302    }
303
304    /// Sets the indexes of the selected cell
305    ///
306    /// Set to `None` if no cell is selected. This will also reset the row offset to `0`.
307    ///
308    /// # Examples
309    ///
310    /// ```rust
311    /// # use ratatui::widgets::{TableState};
312    /// let mut state = TableState::default();
313    /// state.select_cell(Some((1, 5)));
314    /// ```
315    pub fn select_cell(&mut self, indexes: Option<(usize, usize)>) {
316        if let Some((r, c)) = indexes {
317            self.selected = Some(r);
318            self.selected_column = Some(c);
319        } else {
320            self.offset = 0;
321            self.selected = None;
322            self.selected_column = None;
323        }
324    }
325
326    /// Selects the next row or the first one if no row is selected
327    ///
328    /// Note: until the table is rendered, the number of rows is not known, so the index is set to
329    /// `0` and will be corrected when the table is rendered
330    ///
331    /// # Examples
332    ///
333    /// ```rust
334    /// use ratatui::widgets::TableState;
335    ///
336    /// let mut state = TableState::default();
337    /// state.select_next();
338    /// ```
339    pub fn select_next(&mut self) {
340        let next = self.selected.map_or(0, |i| i.saturating_add(1));
341        self.select(Some(next));
342    }
343
344    /// Selects the next column or the first one if no column is selected
345    ///
346    /// Note: until the table is rendered, the number of columns is not known, so the index is set
347    /// to `0` and will be corrected when the table is rendered
348    ///
349    /// # Examples
350    ///
351    /// ```rust
352    /// # use ratatui::widgets::{TableState};
353    /// let mut state = TableState::default();
354    /// state.select_next_column();
355    /// ```
356    pub fn select_next_column(&mut self) {
357        let next = self.selected_column.map_or(0, |i| i.saturating_add(1));
358        self.select_column(Some(next));
359    }
360
361    /// Selects the previous row or the last one if no item is selected
362    ///
363    /// Note: until the table is rendered, the number of rows is not known, so the index is set to
364    /// `usize::MAX` and will be corrected when the table is rendered
365    ///
366    /// # Examples
367    ///
368    /// ```rust
369    /// use ratatui::widgets::TableState;
370    ///
371    /// let mut state = TableState::default();
372    /// state.select_previous();
373    /// ```
374    pub fn select_previous(&mut self) {
375        let previous = self.selected.map_or(usize::MAX, |i| i.saturating_sub(1));
376        self.select(Some(previous));
377    }
378
379    /// Selects the previous column or the last one if no column is selected
380    ///
381    /// Note: until the table is rendered, the number of columns is not known, so the index is set
382    /// to `usize::MAX` and will be corrected when the table is rendered
383    ///
384    /// # Examples
385    ///
386    /// ```rust
387    /// # use ratatui::widgets::{TableState};
388    /// let mut state = TableState::default();
389    /// state.select_previous_column();
390    /// ```
391    pub fn select_previous_column(&mut self) {
392        let previous = self
393            .selected_column
394            .map_or(usize::MAX, |i| i.saturating_sub(1));
395        self.select_column(Some(previous));
396    }
397
398    /// Selects the first row
399    ///
400    /// Note: until the table is rendered, the number of rows is not known, so the index is set to
401    /// `0` and will be corrected when the table is rendered
402    ///
403    /// # Examples
404    ///
405    /// ```rust
406    /// use ratatui::widgets::TableState;
407    ///
408    /// let mut state = TableState::default();
409    /// state.select_first();
410    /// ```
411    pub fn select_first(&mut self) {
412        self.select(Some(0));
413    }
414
415    /// Selects the first column
416    ///
417    /// Note: until the table is rendered, the number of columns is not known, so the index is set
418    /// to `0` and will be corrected when the table is rendered
419    ///
420    /// # Examples
421    ///
422    /// ```rust
423    /// # use ratatui::widgets::{TableState};
424    /// let mut state = TableState::default();
425    /// state.select_first_column();
426    /// ```
427    pub fn select_first_column(&mut self) {
428        self.select_column(Some(0));
429    }
430
431    /// Selects the last row
432    ///
433    /// Note: until the table is rendered, the number of rows is not known, so the index is set to
434    /// `usize::MAX` and will be corrected when the table is rendered
435    ///
436    /// # Examples
437    ///
438    /// ```rust
439    /// use ratatui::widgets::TableState;
440    ///
441    /// let mut state = TableState::default();
442    /// state.select_last();
443    /// ```
444    pub fn select_last(&mut self) {
445        self.select(Some(usize::MAX));
446    }
447
448    /// Selects the last column
449    ///
450    /// Note: until the table is rendered, the number of columns is not known, so the index is set
451    /// to `usize::MAX` and will be corrected when the table is rendered
452    ///
453    /// # Examples
454    ///
455    /// ```rust
456    /// # use ratatui::widgets::{TableState};
457    /// let mut state = TableState::default();
458    /// state.select_last();
459    /// ```
460    pub fn select_last_column(&mut self) {
461        self.select_column(Some(usize::MAX));
462    }
463
464    /// Scrolls down by a specified `amount` in the table.
465    ///
466    /// This method updates the selected index by moving it down by the given `amount`.
467    /// If the `amount` causes the index to go out of bounds (i.e., if the index is greater than
468    /// the number of rows in the table), the last row in the table will be selected.
469    ///
470    /// # Examples
471    ///
472    /// ```rust
473    /// use ratatui::widgets::TableState;
474    ///
475    /// let mut state = TableState::default();
476    /// state.scroll_down_by(4);
477    /// ```
478    pub fn scroll_down_by(&mut self, amount: u16) {
479        let selected = self.selected.unwrap_or_default();
480        self.select(Some(selected.saturating_add(amount as usize)));
481    }
482
483    /// Scrolls up by a specified `amount` in the table.
484    ///
485    /// This method updates the selected index by moving it up by the given `amount`.
486    /// If the `amount` causes the index to go out of bounds (i.e., less than zero),
487    /// the first row in the table will be selected.
488    ///
489    /// # Examples
490    ///
491    /// ```rust
492    /// use ratatui::widgets::TableState;
493    ///
494    /// let mut state = TableState::default();
495    /// state.scroll_up_by(4);
496    /// ```
497    pub fn scroll_up_by(&mut self, amount: u16) {
498        let selected = self.selected.unwrap_or_default();
499        self.select(Some(selected.saturating_sub(amount as usize)));
500    }
501
502    /// Scrolls right by a specified `amount` in the table.
503    ///
504    /// This method updates the selected index by moving it right by the given `amount`.
505    /// If the `amount` causes the index to go out of bounds (i.e., if the index is greater than
506    /// the number of columns in the table), the last column in the table will be selected.
507    ///
508    /// # Examples
509    ///
510    /// ```rust
511    /// # use ratatui::widgets::{TableState};
512    /// let mut state = TableState::default();
513    /// state.scroll_right_by(4);
514    /// ```
515    pub fn scroll_right_by(&mut self, amount: u16) {
516        let selected = self.selected_column.unwrap_or_default();
517        self.select_column(Some(selected.saturating_add(amount as usize)));
518    }
519
520    /// Scrolls left by a specified `amount` in the table.
521    ///
522    /// This method updates the selected index by moving it left by the given `amount`.
523    /// If the `amount` causes the index to go out of bounds (i.e., less than zero),
524    /// the first item in the table will be selected.
525    ///
526    /// # Examples
527    ///
528    /// ```rust
529    /// # use ratatui::widgets::{TableState};
530    /// let mut state = TableState::default();
531    /// state.scroll_left_by(4);
532    /// ```
533    pub fn scroll_left_by(&mut self, amount: u16) {
534        let selected = self.selected_column.unwrap_or_default();
535        self.select_column(Some(selected.saturating_sub(amount as usize)));
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542
543    #[test]
544    fn new() {
545        let state = TableState::new();
546        assert_eq!(state.offset, 0);
547        assert_eq!(state.selected, None);
548        assert_eq!(state.selected_column, None);
549    }
550
551    #[test]
552    fn with_offset() {
553        let state = TableState::new().with_offset(1);
554        assert_eq!(state.offset, 1);
555    }
556
557    #[test]
558    fn with_selected() {
559        let state = TableState::new().with_selected(Some(1));
560        assert_eq!(state.selected, Some(1));
561    }
562
563    #[test]
564    fn with_selected_column() {
565        let state = TableState::new().with_selected_column(Some(1));
566        assert_eq!(state.selected_column, Some(1));
567    }
568
569    #[test]
570    fn with_selected_cell_none() {
571        let state = TableState::new().with_selected_cell(None);
572        assert_eq!(state.selected, None);
573        assert_eq!(state.selected_column, None);
574    }
575
576    #[test]
577    fn offset() {
578        let state = TableState::new();
579        assert_eq!(state.offset(), 0);
580    }
581
582    #[test]
583    fn offset_mut() {
584        let mut state = TableState::new();
585        *state.offset_mut() = 1;
586        assert_eq!(state.offset, 1);
587    }
588
589    #[test]
590    fn selected() {
591        let state = TableState::new();
592        assert_eq!(state.selected(), None);
593    }
594
595    #[test]
596    fn selected_column() {
597        let state = TableState::new();
598        assert_eq!(state.selected_column(), None);
599    }
600
601    #[test]
602    fn selected_cell() {
603        let state = TableState::new();
604        assert_eq!(state.selected_cell(), None);
605    }
606
607    #[test]
608    fn selected_mut() {
609        let mut state = TableState::new();
610        *state.selected_mut() = Some(1);
611        assert_eq!(state.selected, Some(1));
612    }
613
614    #[test]
615    fn selected_column_mut() {
616        let mut state = TableState::new();
617        *state.selected_column_mut() = Some(1);
618        assert_eq!(state.selected_column, Some(1));
619    }
620
621    #[test]
622    fn select() {
623        let mut state = TableState::new();
624        state.select(Some(1));
625        assert_eq!(state.selected, Some(1));
626    }
627
628    #[test]
629    fn select_none() {
630        let mut state = TableState::new().with_selected(Some(1));
631        state.select(None);
632        assert_eq!(state.selected, None);
633    }
634
635    #[test]
636    fn select_column() {
637        let mut state = TableState::new();
638        state.select_column(Some(1));
639        assert_eq!(state.selected_column, Some(1));
640    }
641
642    #[test]
643    fn select_column_none() {
644        let mut state = TableState::new().with_selected_column(Some(1));
645        state.select_column(None);
646        assert_eq!(state.selected_column, None);
647    }
648
649    #[test]
650    fn select_cell() {
651        let mut state = TableState::new();
652        state.select_cell(Some((1, 5)));
653        assert_eq!(state.selected_cell(), Some((1, 5)));
654    }
655
656    #[test]
657    fn select_cell_none() {
658        let mut state = TableState::new().with_selected_cell(Some((1, 5)));
659        state.select_cell(None);
660        assert_eq!(state.selected, None);
661        assert_eq!(state.selected_column, None);
662        assert_eq!(state.selected_cell(), None);
663    }
664
665    #[test]
666    fn test_table_state_navigation() {
667        let mut state = TableState::default();
668        state.select_first();
669        assert_eq!(state.selected, Some(0));
670
671        state.select_previous(); // should not go below 0
672        assert_eq!(state.selected, Some(0));
673
674        state.select_next();
675        assert_eq!(state.selected, Some(1));
676
677        state.select_previous();
678        assert_eq!(state.selected, Some(0));
679
680        state.select_last();
681        assert_eq!(state.selected, Some(usize::MAX));
682
683        state.select_next(); // should not go above usize::MAX
684        assert_eq!(state.selected, Some(usize::MAX));
685
686        state.select_previous();
687        assert_eq!(state.selected, Some(usize::MAX - 1));
688
689        state.select_next();
690        assert_eq!(state.selected, Some(usize::MAX));
691
692        let mut state = TableState::default();
693        state.select_next();
694        assert_eq!(state.selected, Some(0));
695
696        let mut state = TableState::default();
697        state.select_previous();
698        assert_eq!(state.selected, Some(usize::MAX));
699
700        let mut state = TableState::default();
701        state.select(Some(2));
702        state.scroll_down_by(4);
703        assert_eq!(state.selected, Some(6));
704
705        let mut state = TableState::default();
706        state.scroll_up_by(3);
707        assert_eq!(state.selected, Some(0));
708
709        state.select(Some(6));
710        state.scroll_up_by(4);
711        assert_eq!(state.selected, Some(2));
712
713        state.scroll_up_by(4);
714        assert_eq!(state.selected, Some(0));
715
716        let mut state = TableState::default();
717        state.select_first_column();
718        assert_eq!(state.selected_column, Some(0));
719
720        state.select_previous_column();
721        assert_eq!(state.selected_column, Some(0));
722
723        state.select_next_column();
724        assert_eq!(state.selected_column, Some(1));
725
726        state.select_previous_column();
727        assert_eq!(state.selected_column, Some(0));
728
729        state.select_last_column();
730        assert_eq!(state.selected_column, Some(usize::MAX));
731
732        state.select_previous_column();
733        assert_eq!(state.selected_column, Some(usize::MAX - 1));
734
735        let mut state = TableState::default().with_selected_column(Some(12));
736        state.scroll_right_by(4);
737        assert_eq!(state.selected_column, Some(16));
738
739        state.scroll_left_by(20);
740        assert_eq!(state.selected_column, Some(0));
741
742        state.scroll_right_by(100);
743        assert_eq!(state.selected_column, Some(100));
744
745        state.scroll_left_by(20);
746        assert_eq!(state.selected_column, Some(80));
747    }
748}