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}