tui_widget_list/
state.rs

1use ratatui::widgets::ScrollbarState;
2
3use crate::{ListBuildContext, ListBuilder, ScrollAxis};
4
5#[allow(clippy::module_name_repetitions)]
6#[derive(Debug, Clone)]
7pub struct ListState {
8    /// The selected item. If `None`, no item is currently selected.
9    pub selected: Option<usize>,
10
11    /// The total number of elements in the list. This is necessary to correctly
12    /// handle item selection.
13    pub(crate) num_elements: usize,
14
15    /// Indicates if the selection is circular. If true, calling `next` on the last
16    /// element returns the first, and calling `previous` on the first returns the last.
17    ///
18    /// True by default.
19    pub(crate) infinite_scrolling: bool,
20
21    /// The state for the viewport. Keeps track which item to show
22    /// first and how much it is truncated.
23    pub(crate) view_state: ViewState,
24
25    /// The scrollbar state. This is only used if the view is
26    /// initialzed with a scrollbar.
27    pub(crate) scrollbar_state: ScrollbarState,
28}
29
30#[derive(Debug, Clone, Default, Eq, PartialEq)]
31pub(crate) struct ViewState {
32    /// The index of the first item displayed on the screen.
33    pub(crate) offset: usize,
34
35    /// The truncation in rows/columns of the first item displayed on the screen.
36    pub(crate) first_truncated: u16,
37}
38
39impl Default for ListState {
40    fn default() -> Self {
41        Self {
42            selected: None,
43            num_elements: 0,
44            infinite_scrolling: true,
45            view_state: ViewState::default(),
46            scrollbar_state: ScrollbarState::new(0).position(0),
47        }
48    }
49}
50
51impl ListState {
52    pub(crate) fn set_infinite_scrolling(&mut self, infinite_scrolling: bool) {
53        self.infinite_scrolling = infinite_scrolling;
54    }
55
56    /// Returns the index of the currently selected item, if any.
57    #[must_use]
58    #[deprecated(since = "0.9.0", note = "Use ListState's selected field instead.")]
59    pub fn selected(&self) -> Option<usize> {
60        self.selected
61    }
62
63    /// Selects an item by its index.
64    pub fn select(&mut self, index: Option<usize>) {
65        self.selected = index;
66        if index.is_none() {
67            self.view_state.offset = 0;
68            self.scrollbar_state = self.scrollbar_state.position(0);
69        }
70    }
71
72    /// Selects the next element of the list. If circular is true,
73    /// calling next on the last element selects the first.
74    ///
75    /// # Example
76    ///
77    /// ```rust
78    /// use tui_widget_list::ListState;
79    ///
80    /// let mut list_state = ListState::default();
81    /// list_state.next();
82    /// ```
83    pub fn next(&mut self) {
84        if self.num_elements == 0 {
85            return;
86        }
87        let i = match self.selected {
88            Some(i) => {
89                if i >= self.num_elements - 1 {
90                    if self.infinite_scrolling {
91                        0
92                    } else {
93                        i
94                    }
95                } else {
96                    i + 1
97                }
98            }
99            None => 0,
100        };
101        self.select(Some(i));
102    }
103
104    /// Selects the previous element of the list. If circular is true,
105    /// calling previous on the first element selects the last.
106    ///
107    /// # Example
108    ///
109    /// ```rust
110    /// use tui_widget_list::ListState;
111    ///
112    /// let mut list_state = ListState::default();
113    /// list_state.previous();
114    /// ```
115    pub fn previous(&mut self) {
116        if self.num_elements == 0 {
117            return;
118        }
119        let i = match self.selected {
120            Some(i) => {
121                if i == 0 {
122                    if self.infinite_scrolling {
123                        self.num_elements - 1
124                    } else {
125                        i
126                    }
127                } else {
128                    i - 1
129                }
130            }
131            None => 0,
132        };
133        self.select(Some(i));
134    }
135
136    /// Updates the number of elements that are present in the list.
137    pub(crate) fn set_num_elements(&mut self, num_elements: usize) {
138        self.num_elements = num_elements;
139    }
140
141    /// Updates the current scrollbar content length and position.
142    pub(crate) fn update_scrollbar_state<T>(
143        &mut self,
144        builder: &ListBuilder<T>,
145        item_count: usize,
146        main_axis_size: u16,
147        cross_axis_size: u16,
148        scroll_axis: ScrollAxis,
149    ) {
150        let mut max_scrollbar_position = 0;
151        let mut cumulative_size = 0;
152
153        for index in (0..item_count).rev() {
154            let context = ListBuildContext {
155                index,
156                is_selected: self.selected == Some(index),
157                scroll_axis,
158                cross_axis_size,
159            };
160            let (_, widget_size) = builder.call_closure(&context);
161            cumulative_size += widget_size;
162
163            if cumulative_size > main_axis_size {
164                max_scrollbar_position = index + 1;
165                break;
166            }
167        }
168
169        self.scrollbar_state = self.scrollbar_state.content_length(max_scrollbar_position);
170        self.scrollbar_state = self.scrollbar_state.position(self.view_state.offset);
171    }
172
173    /// Returns the index of the first item currently displayed on the screen.
174    #[must_use]
175    pub fn scroll_offset_index(&self) -> usize {
176        self.view_state.offset
177    }
178
179    /// Returns the number of rows/columns of the first visible item that are scrolled off the top/left.
180    ///
181    /// When the first visible item is partially scrolled out of view, this returns how many
182    /// rows (for vertical lists) or columns (for horizontal lists) are hidden above/left of
183    /// the viewport. Returns 0 if the first visible item is fully visible.
184    ///
185    /// # Example
186    ///
187    /// If message #5 is the first visible item but its first 2 rows are scrolled off the top,
188    /// this returns 2. Combined with `scroll_offset_index()`, you can calculate the exact
189    /// scroll position in pixels/rows.
190    #[must_use]
191    pub fn scroll_truncation(&self) -> u16 {
192        self.view_state.first_truncated
193    }
194}