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}