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 pub selected: Option<usize>,
13
14 pub(crate) num_elements: usize,
17
18 pub(crate) infinite_scrolling: bool,
23
24 pub(crate) item_scroll: u16,
27
28 pub(crate) view_state: ViewState,
31
32 pub(crate) scrollbar_state: ScrollbarState,
35}
36
37#[derive(Debug, Clone, PartialEq)]
38pub(crate) struct ViewState {
39 pub(crate) offset: usize,
41
42 pub(crate) first_truncated: u16,
44
45 pub(crate) visible_main_axis_sizes: HashMap<usize, u16>,
48
49 pub(crate) inner_area: Rect,
51
52 pub(crate) scroll_axis: ScrollAxis,
54
55 pub(crate) scroll_direction: ScrollDirection,
57
58 pub(crate) last_main_axis_size: u16,
60
61 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 #[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 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 pub fn next(&mut self) {
127 if self.num_elements == 0 {
128 return;
129 }
130 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 pub fn previous(&mut self) {
167 if self.num_elements == 0 {
168 return;
169 }
170 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 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 #[must_use]
201 pub fn scroll_offset_index(&self) -> usize {
202 self.view_state.offset
203 }
204
205 #[must_use]
217 pub fn scroll_truncation(&self) -> u16 {
218 self.view_state.first_truncated
219 }
220
221 pub(crate) fn set_num_elements(&mut self, num_elements: usize) {
223 self.num_elements = num_elements;
224 }
225
226 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 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 #[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 pub(crate) fn set_inner_area(&mut self, inner_area: Rect) {
272 self.view_state.inner_area = inner_area;
273 }
274
275 #[must_use]
277 pub(crate) fn inner_area(&self) -> Rect {
278 self.view_state.inner_area
279 }
280
281 pub(crate) fn set_scroll_axis(&mut self, scroll_axis: ScrollAxis) {
283 self.view_state.scroll_axis = scroll_axis;
284 }
285
286 #[must_use]
288 pub(crate) fn last_scroll_axis(&self) -> ScrollAxis {
289 self.view_state.scroll_axis
290 }
291
292 pub(crate) fn set_scroll_direction(&mut self, scroll_direction: ScrollDirection) {
294 self.view_state.scroll_direction = scroll_direction;
295 }
296
297 #[must_use]
299 pub(crate) fn last_scroll_direction(&self) -> ScrollDirection {
300 self.view_state.scroll_direction
301 }
302
303 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 pub(crate) fn set_last_main_axis_size(&mut self, size: u16) {
315 self.view_state.last_main_axis_size = size;
316 }
317
318 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 #[must_use]
325 pub(crate) fn item_scroll(&self) -> u16 {
326 self.item_scroll
327 }
328}