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