Skip to main content

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