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