tui_widget_list/state.rs
1use std::collections::HashMap;
2
3use ratatui_core::layout::Rect;
4use ratatui_widgets::scrollbar::ScrollbarState;
5
6use crate::{ListBuildContext, ListBuilder, ScrollAxis, ScrollDirection};
7
8#[allow(clippy::module_name_repetitions)]
9#[derive(Debug, Clone)]
10pub struct ListState {
11 /// The selected item. If `None`, no item is currently selected.
12 pub selected: Option<usize>,
13
14 /// The total number of elements in the list. This is necessary to correctly
15 /// handle item selection.
16 pub(crate) num_elements: usize,
17
18 /// Indicates if infinite scrolling is enabled.
19 /// If true, calling `next` on the last element returns the first,
20 /// and calling `previous` on the first returns the last.
21 ///
22 /// True by default.
23 pub(crate) infinite_scrolling: bool,
24
25 /// Scroll offset within the currently selected item. When an item is larger
26 /// than the viewport, this tracks how far we've scrolled into it.
27 pub(crate) item_scroll: u16,
28
29 /// The state for the viewport. Keeps track which item to show
30 /// first and how much it is truncated.
31 pub(crate) view_state: ViewState,
32
33 /// The scrollbar state. This is only used if the view is
34 /// initialzed with a scrollbar.
35 pub(crate) scrollbar_state: ScrollbarState,
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub(crate) struct ViewState {
40 /// The index of the first item displayed on the screen.
41 pub(crate) offset: usize,
42
43 /// The truncation in rows/columns of the first item displayed on the screen.
44 pub(crate) first_truncated: u16,
45
46 /// Cached visible sizes from the last render: map from item index to its visible main-axis size.
47 /// This avoids re-evaluating the builder for hit testing and other post-render queries.
48 pub(crate) visible_main_axis_sizes: HashMap<usize, u16>,
49
50 /// The inner area used during the last render (after applying the optional block).
51 pub(crate) inner_area: Rect,
52
53 /// The scroll axis used during the last render.
54 pub(crate) scroll_axis: ScrollAxis,
55
56 /// The scroll direction used during the last render.
57 pub(crate) scroll_direction: ScrollDirection,
58
59 /// The viewport's main axis size from the last render.
60 pub(crate) last_main_axis_size: u16,
61
62 /// Full (untruncated) main-axis sizes of items from the last render.
63 pub(crate) total_main_axis_sizes: HashMap<usize, u16>,
64}
65
66impl Default for ViewState {
67 fn default() -> Self {
68 Self {
69 offset: 0,
70 first_truncated: 0,
71 visible_main_axis_sizes: HashMap::new(),
72 inner_area: Rect::default(),
73 scroll_axis: ScrollAxis::Vertical,
74 scroll_direction: ScrollDirection::Forward,
75 last_main_axis_size: 0,
76 total_main_axis_sizes: HashMap::new(),
77 }
78 }
79}
80
81impl Default for ListState {
82 fn default() -> Self {
83 Self {
84 selected: None,
85 num_elements: 0,
86 infinite_scrolling: true,
87 item_scroll: 0,
88 view_state: ViewState::default(),
89 scrollbar_state: ScrollbarState::new(0).position(0),
90 }
91 }
92}
93
94impl ListState {
95 /// Creates a new `ListState` with the given initial index.
96 ///
97 /// # Example
98 /// ```rust
99 /// # use tui_widget_list::ListState;
100 ///
101 /// let list_state = ListState::new_with_index(Some(1));
102 ///
103 /// assert_eq!(list_state.selected, Some(1));
104 /// ```
105 pub fn new_with_index(index: Option<usize>) -> Self {
106 let mut state = Self::default();
107 state.select(index);
108 state
109 }
110
111 pub(crate) fn set_infinite_scrolling(&mut self, infinite_scrolling: bool) {
112 self.infinite_scrolling = infinite_scrolling;
113 }
114
115 /// Returns the index of the currently selected item, if any.
116 #[must_use]
117 #[deprecated(since = "0.9.0", note = "Use ListState's selected field instead.")]
118 pub fn selected(&self) -> Option<usize> {
119 self.selected
120 }
121
122 /// Selects an item by its index.
123 pub fn select(&mut self, index: Option<usize>) {
124 self.selected = index;
125 self.item_scroll = 0;
126 if index.is_none() {
127 self.view_state.offset = 0;
128 self.scrollbar_state = self.scrollbar_state.position(0);
129 }
130 }
131
132 /// Selects the next element of the list. If `inifinite_scrolling`
133 /// is true, calling next on the last element selects the first.
134 ///
135 /// # Example
136 ///
137 /// ```rust
138 /// use tui_widget_list::ListState;
139 ///
140 /// let mut list_state = ListState::default();
141 /// list_state.next();
142 /// ```
143 pub fn next(&mut self) {
144 if self.num_elements == 0 {
145 return;
146 }
147 // If the current item overflows the viewport, scroll within it first
148 if let Some(selected) = self.selected {
149 let overflow = self.item_overflow(selected);
150 if overflow > 0 && self.item_scroll < overflow {
151 self.item_scroll += 1;
152 return;
153 }
154 }
155 let i = match self.selected {
156 Some(i) => {
157 if i >= self.num_elements - 1 {
158 if self.infinite_scrolling {
159 0
160 } else {
161 i
162 }
163 } else {
164 i + 1
165 }
166 }
167 None => 0,
168 };
169 self.select(Some(i));
170 }
171
172 /// Selects the previous element of the list. If `infinite_scrolling`
173 /// is true, calling previous on the first element selects the last.
174 ///
175 /// # Example
176 ///
177 /// ```rust
178 /// use tui_widget_list::ListState;
179 ///
180 /// let mut list_state = ListState::default();
181 /// list_state.previous();
182 /// ```
183 pub fn previous(&mut self) {
184 if self.num_elements == 0 {
185 return;
186 }
187 // If the current item overflows the viewport, scroll back within it first
188 if self.item_scroll > 0 {
189 self.item_scroll -= 1;
190 return;
191 }
192 let i = match self.selected {
193 Some(i) => {
194 if i == 0 {
195 if self.infinite_scrolling {
196 self.num_elements - 1
197 } else {
198 i
199 }
200 } else {
201 i - 1
202 }
203 }
204 None => self.num_elements - 1,
205 };
206 // If the previous item overflows the viewport, start at its bottom
207 let overflow = self.item_overflow(i);
208 if overflow > 0 {
209 self.selected = Some(i);
210 self.item_scroll = overflow;
211 return;
212 }
213 self.select(Some(i));
214 }
215
216 /// Returns the index of the first item currently displayed on the screen.
217 #[must_use]
218 pub fn scroll_offset_index(&self) -> usize {
219 self.view_state.offset
220 }
221
222 /// Returns the number of rows/columns of the first visible item that are scrolled off the top/left.
223 ///
224 /// When the first visible item is partially scrolled out of view, this returns how many
225 /// rows (for vertical lists) or columns (for horizontal lists) are hidden above/left of
226 /// the viewport. Returns 0 if the first visible item is fully visible.
227 ///
228 /// # Example
229 ///
230 /// If message #5 is the first visible item but its first 2 rows are scrolled off the top,
231 /// this returns 2. Combined with `scroll_offset_index()`, you can calculate the exact
232 /// scroll position in pixels/rows.
233 #[must_use]
234 pub fn scroll_truncation(&self) -> u16 {
235 self.view_state.first_truncated
236 }
237
238 /// Updates the number of elements that are present in the list.
239 pub(crate) fn set_num_elements(&mut self, num_elements: usize) {
240 self.num_elements = num_elements;
241 }
242
243 /// Updates the current scrollbar content length and position.
244 pub(crate) fn update_scrollbar_state<T>(
245 &mut self,
246 builder: &ListBuilder<T>,
247 item_count: usize,
248 main_axis_size: u16,
249 cross_axis_size: u16,
250 scroll_axis: ScrollAxis,
251 ) {
252 let mut max_scrollbar_position = 0;
253 let mut cumulative_size = 0;
254
255 for index in (0..item_count).rev() {
256 let context = ListBuildContext {
257 index,
258 is_selected: self.selected == Some(index),
259 scroll_axis,
260 cross_axis_size,
261 };
262 let (_, widget_size) = builder.call_closure(&context);
263 cumulative_size += widget_size;
264
265 if cumulative_size > main_axis_size {
266 max_scrollbar_position = index + 1;
267 break;
268 }
269 }
270
271 self.scrollbar_state = self.scrollbar_state.content_length(max_scrollbar_position);
272 self.scrollbar_state = self.scrollbar_state.position(self.view_state.offset);
273 }
274
275 /// Replace the cached visible sizes with a new map computed during render.
276 /// The values should be the actually visible size (after truncation) along the main axis.
277 pub(crate) fn set_visible_main_axis_sizes(&mut self, sizes: HashMap<usize, u16>) {
278 self.view_state.visible_main_axis_sizes = sizes;
279 }
280
281 /// Get a reference to the cached visible sizes map from the last render.
282 #[must_use]
283 pub(crate) fn visible_main_axis_sizes(&self) -> &HashMap<usize, u16> {
284 &self.view_state.visible_main_axis_sizes
285 }
286
287 /// Set the inner area used during the last render.
288 pub(crate) fn set_inner_area(&mut self, inner_area: Rect) {
289 self.view_state.inner_area = inner_area;
290 }
291
292 /// Get the inner area used during the last render.
293 #[must_use]
294 pub(crate) fn inner_area(&self) -> Rect {
295 self.view_state.inner_area
296 }
297
298 /// Set the scroll axis used during the last render.
299 pub(crate) fn set_scroll_axis(&mut self, scroll_axis: ScrollAxis) {
300 self.view_state.scroll_axis = scroll_axis;
301 }
302
303 /// Get the scroll axis used during the last render.
304 #[must_use]
305 pub(crate) fn last_scroll_axis(&self) -> ScrollAxis {
306 self.view_state.scroll_axis
307 }
308
309 /// Set the scroll direction used during the last render.
310 pub(crate) fn set_scroll_direction(&mut self, scroll_direction: ScrollDirection) {
311 self.view_state.scroll_direction = scroll_direction;
312 }
313
314 /// Get the scroll direction used during the last render.
315 #[must_use]
316 pub(crate) fn last_scroll_direction(&self) -> ScrollDirection {
317 self.view_state.scroll_direction
318 }
319
320 /// Returns how many rows/cols of the item extend beyond the viewport,
321 /// or 0 if the item fits or its size is not cached.
322 fn item_overflow(&self, index: usize) -> u16 {
323 self.view_state
324 .total_main_axis_sizes
325 .get(&index)
326 .map(|&total| total.saturating_sub(self.view_state.last_main_axis_size))
327 .unwrap_or(0)
328 }
329
330 /// Set the viewport's main axis size from the last render.
331 pub(crate) fn set_last_main_axis_size(&mut self, size: u16) {
332 self.view_state.last_main_axis_size = size;
333 }
334
335 /// Set the full (untruncated) main-axis sizes of items from the last render.
336 pub(crate) fn set_total_main_axis_sizes(&mut self, sizes: HashMap<usize, u16>) {
337 self.view_state.total_main_axis_sizes = sizes;
338 }
339
340 /// Get the scroll offset within the currently selected item.
341 #[must_use]
342 pub(crate) fn item_scroll(&self) -> u16 {
343 self.item_scroll
344 }
345}