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};
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    /// The state for the viewport. Keeps track which item to show
25    /// first and how much it is truncated.
26    pub(crate) view_state: ViewState,
27
28    /// The scrollbar state. This is only used if the view is
29    /// initialzed with a scrollbar.
30    pub(crate) scrollbar_state: ScrollbarState,
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub(crate) struct ViewState {
35    /// The index of the first item displayed on the screen.
36    pub(crate) offset: usize,
37
38    /// The truncation in rows/columns of the first item displayed on the screen.
39    pub(crate) first_truncated: u16,
40
41    /// Cached visible sizes from the last render: map from item index to its visible main-axis size.
42    /// This avoids re-evaluating the builder for hit testing and other post-render queries.
43    pub(crate) visible_main_axis_sizes: HashMap<usize, u16>,
44
45    /// The inner area used during the last render (after applying the optional block).
46    pub(crate) inner_area: Rect,
47
48    /// The scroll axis used during the last render.
49    pub(crate) scroll_axis: ScrollAxis,
50}
51
52impl Default for ViewState {
53    fn default() -> Self {
54        Self {
55            offset: 0,
56            first_truncated: 0,
57            visible_main_axis_sizes: HashMap::new(),
58            inner_area: Rect::default(),
59            scroll_axis: ScrollAxis::Vertical,
60        }
61    }
62}
63
64impl Default for ListState {
65    fn default() -> Self {
66        Self {
67            selected: None,
68            num_elements: 0,
69            infinite_scrolling: true,
70            view_state: ViewState::default(),
71            scrollbar_state: ScrollbarState::new(0).position(0),
72        }
73    }
74}
75
76impl ListState {
77    pub(crate) fn set_infinite_scrolling(&mut self, infinite_scrolling: bool) {
78        self.infinite_scrolling = infinite_scrolling;
79    }
80
81    /// Returns the index of the currently selected item, if any.
82    #[must_use]
83    #[deprecated(since = "0.9.0", note = "Use ListState's selected field instead.")]
84    pub fn selected(&self) -> Option<usize> {
85        self.selected
86    }
87
88    /// Selects an item by its index.
89    pub fn select(&mut self, index: Option<usize>) {
90        self.selected = index;
91        if index.is_none() {
92            self.view_state.offset = 0;
93            self.scrollbar_state = self.scrollbar_state.position(0);
94        }
95    }
96
97    /// Selects the next element of the list. If circular is true,
98    /// calling next on the last element selects the first.
99    ///
100    /// # Example
101    ///
102    /// ```rust
103    /// use tui_widget_list::ListState;
104    ///
105    /// let mut list_state = ListState::default();
106    /// list_state.next();
107    /// ```
108    pub fn next(&mut self) {
109        if self.num_elements == 0 {
110            return;
111        }
112        let i = match self.selected {
113            Some(i) => {
114                if i >= self.num_elements - 1 {
115                    if self.infinite_scrolling {
116                        0
117                    } else {
118                        i
119                    }
120                } else {
121                    i + 1
122                }
123            }
124            None => 0,
125        };
126        self.select(Some(i));
127    }
128
129    /// Selects the previous element of the list. If circular is true,
130    /// calling previous on the first element selects the last.
131    ///
132    /// # Example
133    ///
134    /// ```rust
135    /// use tui_widget_list::ListState;
136    ///
137    /// let mut list_state = ListState::default();
138    /// list_state.previous();
139    /// ```
140    pub fn previous(&mut self) {
141        if self.num_elements == 0 {
142            return;
143        }
144        let i = match self.selected {
145            Some(i) => {
146                if i == 0 {
147                    if self.infinite_scrolling {
148                        self.num_elements - 1
149                    } else {
150                        i
151                    }
152                } else {
153                    i - 1
154                }
155            }
156            None => 0,
157        };
158        self.select(Some(i));
159    }
160
161    /// Returns the index of the first item currently displayed on the screen.
162    #[must_use]
163    pub fn scroll_offset_index(&self) -> usize {
164        self.view_state.offset
165    }
166
167    /// Returns the number of rows/columns of the first visible item that are scrolled off the top/left.
168    ///
169    /// When the first visible item is partially scrolled out of view, this returns how many
170    /// rows (for vertical lists) or columns (for horizontal lists) are hidden above/left of
171    /// the viewport. Returns 0 if the first visible item is fully visible.
172    ///
173    /// # Example
174    ///
175    /// If message #5 is the first visible item but its first 2 rows are scrolled off the top,
176    /// this returns 2. Combined with `scroll_offset_index()`, you can calculate the exact
177    /// scroll position in pixels/rows.
178    #[must_use]
179    pub fn scroll_truncation(&self) -> u16 {
180        self.view_state.first_truncated
181    }
182
183    /// Updates the number of elements that are present in the list.
184    pub(crate) fn set_num_elements(&mut self, num_elements: usize) {
185        self.num_elements = num_elements;
186    }
187
188    /// Updates the current scrollbar content length and position.
189    pub(crate) fn update_scrollbar_state<T>(
190        &mut self,
191        builder: &ListBuilder<T>,
192        item_count: usize,
193        main_axis_size: u16,
194        cross_axis_size: u16,
195        scroll_axis: ScrollAxis,
196    ) {
197        let mut max_scrollbar_position = 0;
198        let mut cumulative_size = 0;
199
200        for index in (0..item_count).rev() {
201            let context = ListBuildContext {
202                index,
203                is_selected: self.selected == Some(index),
204                scroll_axis,
205                cross_axis_size,
206            };
207            let (_, widget_size) = builder.call_closure(&context);
208            cumulative_size += widget_size;
209
210            if cumulative_size > main_axis_size {
211                max_scrollbar_position = index + 1;
212                break;
213            }
214        }
215
216        self.scrollbar_state = self.scrollbar_state.content_length(max_scrollbar_position);
217        self.scrollbar_state = self.scrollbar_state.position(self.view_state.offset);
218    }
219
220    /// Replace the cached visible sizes with a new map computed during render.
221    /// The values should be the actually visible size (after truncation) along the main axis.
222    pub(crate) fn set_visible_main_axis_sizes(&mut self, sizes: HashMap<usize, u16>) {
223        self.view_state.visible_main_axis_sizes = sizes;
224    }
225
226    /// Get a reference to the cached visible sizes map from the last render.
227    #[must_use]
228    pub(crate) fn visible_main_axis_sizes(&self) -> &HashMap<usize, u16> {
229        &self.view_state.visible_main_axis_sizes
230    }
231
232    /// Set the inner area used during the last render.
233    pub(crate) fn set_inner_area(&mut self, inner_area: Rect) {
234        self.view_state.inner_area = inner_area;
235    }
236
237    /// Get the inner area used during the last render.
238    #[must_use]
239    pub(crate) fn inner_area(&self) -> Rect {
240        self.view_state.inner_area
241    }
242
243    /// Set the scroll axis used during the last render.
244    pub(crate) fn set_scroll_axis(&mut self, scroll_axis: ScrollAxis) {
245        self.view_state.scroll_axis = scroll_axis;
246    }
247
248    /// Get the scroll axis used during the last render.
249    #[must_use]
250    pub(crate) fn last_scroll_axis(&self) -> ScrollAxis {
251        self.view_state.scroll_axis
252    }
253}