ratatui/widgets/list/
state.rs

1/// State of the [`List`] widget
2///
3/// This state can be used to scroll through items and select one. When the list is rendered as a
4/// stateful widget, the selected item will be highlighted and the list will be shifted to ensure
5/// that the selected item is visible. This will modify the [`ListState`] object passed to the
6/// [`Frame::render_stateful_widget`](crate::terminal::Frame::render_stateful_widget) method.
7///
8/// The state consists of two fields:
9/// - [`offset`]: the index of the first item to be displayed
10/// - [`selected`]: the index of the selected item, which can be `None` if no item is selected
11///
12/// [`offset`]: ListState::offset()
13/// [`selected`]: ListState::selected()
14///
15/// See the list in the [Examples] directory for a more in depth example of the various
16/// configuration options and for how to handle state.
17///
18/// [Examples]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
19///
20/// # Example
21///
22/// ```rust
23/// use ratatui::{
24///     layout::Rect,
25///     widgets::{List, ListState},
26///     Frame,
27/// };
28///
29/// # fn ui(frame: &mut Frame) {
30/// # let area = Rect::default();
31/// let items = ["Item 1"];
32/// let list = List::new(items);
33///
34/// // This should be stored outside of the function in your application state.
35/// let mut state = ListState::default();
36///
37/// *state.offset_mut() = 1; // display the second item and onwards
38/// state.select(Some(3)); // select the forth item (0-indexed)
39///
40/// frame.render_stateful_widget(list, area, &mut state);
41/// # }
42/// ```
43///
44/// [`List`]: crate::widgets::List
45#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub struct ListState {
48    pub(crate) offset: usize,
49    pub(crate) selected: Option<usize>,
50}
51
52impl ListState {
53    /// Sets the index of the first item to be displayed
54    ///
55    /// This is a fluent setter method which must be chained or used as it consumes self
56    ///
57    /// # Examples
58    ///
59    /// ```rust
60    /// use ratatui::widgets::ListState;
61    ///
62    /// let state = ListState::default().with_offset(1);
63    /// ```
64    #[must_use = "method moves the value of self and returns the modified value"]
65    pub const fn with_offset(mut self, offset: usize) -> Self {
66        self.offset = offset;
67        self
68    }
69
70    /// Sets the index of the selected item
71    ///
72    /// This is a fluent setter method which must be chained or used as it consumes self
73    ///
74    /// # Examples
75    ///
76    /// ```rust
77    /// use ratatui::widgets::ListState;
78    ///
79    /// let state = ListState::default().with_selected(Some(1));
80    /// ```
81    #[must_use = "method moves the value of self and returns the modified value"]
82    pub const fn with_selected(mut self, selected: Option<usize>) -> Self {
83        self.selected = selected;
84        self
85    }
86
87    /// Index of the first item to be displayed
88    ///
89    /// # Examples
90    ///
91    /// ```rust
92    /// use ratatui::widgets::ListState;
93    ///
94    /// let state = ListState::default();
95    /// assert_eq!(state.offset(), 0);
96    /// ```
97    pub const fn offset(&self) -> usize {
98        self.offset
99    }
100
101    /// Mutable reference to the index of the first item to be displayed
102    ///
103    /// # Examples
104    ///
105    /// ```rust
106    /// use ratatui::widgets::ListState;
107    ///
108    /// let mut state = ListState::default();
109    /// *state.offset_mut() = 1;
110    /// ```
111    pub fn offset_mut(&mut self) -> &mut usize {
112        &mut self.offset
113    }
114
115    /// Index of the selected item
116    ///
117    /// Returns `None` if no item is selected
118    ///
119    /// # Examples
120    ///
121    /// ```rust
122    /// use ratatui::widgets::ListState;
123    ///
124    /// let state = ListState::default();
125    /// assert_eq!(state.selected(), None);
126    /// ```
127    pub const fn selected(&self) -> Option<usize> {
128        self.selected
129    }
130
131    /// Mutable reference to the index of the selected item
132    ///
133    /// Returns `None` if no item is selected
134    ///
135    /// # Examples
136    ///
137    /// ```rust
138    /// use ratatui::widgets::ListState;
139    ///
140    /// let mut state = ListState::default();
141    /// *state.selected_mut() = Some(1);
142    /// ```
143    pub fn selected_mut(&mut self) -> &mut Option<usize> {
144        &mut self.selected
145    }
146
147    /// Sets the index of the selected item
148    ///
149    /// Set to `None` if no item is selected. This will also reset the offset to `0`.
150    ///
151    /// # Examples
152    ///
153    /// ```rust
154    /// use ratatui::widgets::ListState;
155    ///
156    /// let mut state = ListState::default();
157    /// state.select(Some(1));
158    /// ```
159    pub fn select(&mut self, index: Option<usize>) {
160        self.selected = index;
161        if index.is_none() {
162            self.offset = 0;
163        }
164    }
165
166    /// Selects the next item or the first one if no item is selected
167    ///
168    /// Note: until the list is rendered, the number of items is not known, so the index is set to
169    /// `0` and will be corrected when the list is rendered
170    ///
171    /// # Examples
172    ///
173    /// ```rust
174    /// use ratatui::widgets::ListState;
175    ///
176    /// let mut state = ListState::default();
177    /// state.select_next();
178    /// ```
179    pub fn select_next(&mut self) {
180        let next = self.selected.map_or(0, |i| i.saturating_add(1));
181        self.select(Some(next));
182    }
183
184    /// Selects the previous item or the last one if no item is selected
185    ///
186    /// Note: until the list is rendered, the number of items is not known, so the index is set to
187    /// `usize::MAX` and will be corrected when the list is rendered
188    ///
189    /// # Examples
190    ///
191    /// ```rust
192    /// use ratatui::widgets::ListState;
193    ///
194    /// let mut state = ListState::default();
195    /// state.select_previous();
196    /// ```
197    pub fn select_previous(&mut self) {
198        let previous = self.selected.map_or(usize::MAX, |i| i.saturating_sub(1));
199        self.select(Some(previous));
200    }
201
202    /// Selects the first item
203    ///
204    /// Note: until the list is rendered, the number of items is not known, so the index is set to
205    /// `0` and will be corrected when the list is rendered
206    ///
207    /// # Examples
208    ///
209    /// ```rust
210    /// use ratatui::widgets::ListState;
211    ///
212    /// let mut state = ListState::default();
213    /// state.select_first();
214    /// ```
215    pub fn select_first(&mut self) {
216        self.select(Some(0));
217    }
218
219    /// Selects the last item
220    ///
221    /// Note: until the list is rendered, the number of items is not known, so the index is set to
222    /// `usize::MAX` and will be corrected when the list is rendered
223    ///
224    /// # Examples
225    ///
226    /// ```rust
227    /// use ratatui::widgets::ListState;
228    ///
229    /// let mut state = ListState::default();
230    /// state.select_last();
231    /// ```
232    pub fn select_last(&mut self) {
233        self.select(Some(usize::MAX));
234    }
235
236    /// Scrolls down by a specified `amount` in the list.
237    ///
238    /// This method updates the selected index by moving it down by the given `amount`.
239    /// If the `amount` causes the index to go out of bounds (i.e., if the index is greater than
240    /// the length of the list), the last item in the list will be selected.
241    ///
242    /// # Examples
243    ///
244    /// ```rust
245    /// use ratatui::widgets::ListState;
246    ///
247    /// let mut state = ListState::default();
248    /// state.scroll_down_by(4);
249    /// ```
250    pub fn scroll_down_by(&mut self, amount: u16) {
251        let selected = self.selected.unwrap_or_default();
252        self.select(Some(selected.saturating_add(amount as usize)));
253    }
254
255    /// Scrolls up by a specified `amount` in the list.
256    ///
257    /// This method updates the selected index by moving it up by the given `amount`.
258    /// If the `amount` causes the index to go out of bounds (i.e., less than zero),
259    /// the first item in the list will be selected.
260    ///
261    /// # Examples
262    ///
263    /// ```rust
264    /// use ratatui::widgets::ListState;
265    ///
266    /// let mut state = ListState::default();
267    /// state.scroll_up_by(4);
268    /// ```
269    pub fn scroll_up_by(&mut self, amount: u16) {
270        let selected = self.selected.unwrap_or_default();
271        self.select(Some(selected.saturating_sub(amount as usize)));
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use pretty_assertions::assert_eq;
278
279    use crate::widgets::ListState;
280
281    #[test]
282    fn selected() {
283        let mut state = ListState::default();
284        assert_eq!(state.selected(), None);
285
286        state.select(Some(1));
287        assert_eq!(state.selected(), Some(1));
288
289        state.select(None);
290        assert_eq!(state.selected(), None);
291    }
292
293    #[test]
294    fn select() {
295        let mut state = ListState::default();
296        assert_eq!(state.selected, None);
297        assert_eq!(state.offset, 0);
298
299        state.select(Some(2));
300        assert_eq!(state.selected, Some(2));
301        assert_eq!(state.offset, 0);
302
303        state.select(None);
304        assert_eq!(state.selected, None);
305        assert_eq!(state.offset, 0);
306    }
307
308    #[test]
309    fn state_navigation() {
310        let mut state = ListState::default();
311        state.select_first();
312        assert_eq!(state.selected, Some(0));
313
314        state.select_previous(); // should not go below 0
315        assert_eq!(state.selected, Some(0));
316
317        state.select_next();
318        assert_eq!(state.selected, Some(1));
319
320        state.select_previous();
321        assert_eq!(state.selected, Some(0));
322
323        state.select_last();
324        assert_eq!(state.selected, Some(usize::MAX));
325
326        state.select_next(); // should not go above usize::MAX
327        assert_eq!(state.selected, Some(usize::MAX));
328
329        state.select_previous();
330        assert_eq!(state.selected, Some(usize::MAX - 1));
331
332        state.select_next();
333        assert_eq!(state.selected, Some(usize::MAX));
334
335        let mut state = ListState::default();
336        state.select_next();
337        assert_eq!(state.selected, Some(0));
338
339        let mut state = ListState::default();
340        state.select_previous();
341        assert_eq!(state.selected, Some(usize::MAX));
342
343        let mut state = ListState::default();
344        state.select(Some(2));
345        state.scroll_down_by(4);
346        assert_eq!(state.selected, Some(6));
347
348        let mut state = ListState::default();
349        state.scroll_up_by(3);
350        assert_eq!(state.selected, Some(0));
351
352        state.select(Some(6));
353        state.scroll_up_by(4);
354        assert_eq!(state.selected, Some(2));
355
356        state.scroll_up_by(4);
357        assert_eq!(state.selected, Some(0));
358    }
359}