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}