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}