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}