tui_widget_list/state.rs
1use ratatui::widgets::ScrollbarState;
2
3use crate::{ListBuildContext, ListBuilder, ScrollAxis};
4
5#[allow(clippy::module_name_repetitions)]
6#[derive(Debug, Clone)]
7pub struct ListState {
8 /// The selected item. If `None`, no item is currently selected.
9 pub selected: Option<usize>,
10
11 /// The total number of elements in the list. This is necessary to correctly
12 /// handle item selection.
13 pub(crate) num_elements: usize,
14
15 /// Indicates if the selection is circular. If true, calling `next` on the last
16 /// element returns the first, and calling `previous` on the first returns the last.
17 ///
18 /// True by default.
19 pub(crate) infinite_scrolling: bool,
20
21 /// The state for the viewport. Keeps track which item to show
22 /// first and how much it is truncated.
23 pub(crate) view_state: ViewState,
24
25 /// The scrollbar state. This is only used if the view is
26 /// initialzed with a scrollbar.
27 pub(crate) scrollbar_state: ScrollbarState,
28}
29
30#[derive(Debug, Clone, Default, Eq, PartialEq)]
31pub(crate) struct ViewState {
32 /// The index of the first item displayed on the screen.
33 pub(crate) offset: usize,
34
35 /// The truncation in rows/columns of the first item displayed on the screen.
36 pub(crate) first_truncated: u16,
37}
38
39impl Default for ListState {
40 fn default() -> Self {
41 Self {
42 selected: None,
43 num_elements: 0,
44 infinite_scrolling: true,
45 view_state: ViewState::default(),
46 scrollbar_state: ScrollbarState::new(0).position(0),
47 }
48 }
49}
50
51impl ListState {
52 pub(crate) fn set_infinite_scrolling(&mut self, infinite_scrolling: bool) {
53 self.infinite_scrolling = infinite_scrolling;
54 }
55
56 /// Returns the index of the currently selected item, if any.
57 #[must_use]
58 #[deprecated(since = "0.9.0", note = "Use ListState's selected field instead.")]
59 pub fn selected(&self) -> Option<usize> {
60 self.selected
61 }
62
63 /// Selects an item by its index.
64 pub fn select(&mut self, index: Option<usize>) {
65 self.selected = index;
66 if index.is_none() {
67 self.view_state.offset = 0;
68 self.scrollbar_state = self.scrollbar_state.position(0);
69 }
70 }
71
72 /// Selects the next element of the list. If circular is true,
73 /// calling next on the last element selects the first.
74 ///
75 /// # Example
76 ///
77 /// ```rust
78 /// use tui_widget_list::ListState;
79 ///
80 /// let mut list_state = ListState::default();
81 /// list_state.next();
82 /// ```
83 pub fn next(&mut self) {
84 if self.num_elements == 0 {
85 return;
86 }
87 let i = match self.selected {
88 Some(i) => {
89 if i >= self.num_elements - 1 {
90 if self.infinite_scrolling {
91 0
92 } else {
93 i
94 }
95 } else {
96 i + 1
97 }
98 }
99 None => 0,
100 };
101 self.select(Some(i));
102 }
103
104 /// Selects the previous element of the list. If circular is true,
105 /// calling previous on the first element selects the last.
106 ///
107 /// # Example
108 ///
109 /// ```rust
110 /// use tui_widget_list::ListState;
111 ///
112 /// let mut list_state = ListState::default();
113 /// list_state.previous();
114 /// ```
115 pub fn previous(&mut self) {
116 if self.num_elements == 0 {
117 return;
118 }
119 let i = match self.selected {
120 Some(i) => {
121 if i == 0 {
122 if self.infinite_scrolling {
123 self.num_elements - 1
124 } else {
125 i
126 }
127 } else {
128 i - 1
129 }
130 }
131 None => 0,
132 };
133 self.select(Some(i));
134 }
135
136 /// Updates the number of elements that are present in the list.
137 pub(crate) fn set_num_elements(&mut self, num_elements: usize) {
138 self.num_elements = num_elements;
139 }
140
141 /// Updates the current scrollbar content length and position.
142 pub(crate) fn update_scrollbar_state<T>(
143 &mut self,
144 builder: &ListBuilder<T>,
145 item_count: usize,
146 main_axis_size: u16,
147 cross_axis_size: u16,
148 scroll_axis: ScrollAxis,
149 ) {
150 let mut max_scrollbar_position = 0;
151 let mut cumulative_size = 0;
152
153 for index in (0..item_count).rev() {
154 let context = ListBuildContext {
155 index,
156 is_selected: self.selected == Some(index),
157 scroll_axis,
158 cross_axis_size,
159 };
160 let (_, widget_size) = builder.call_closure(&context);
161 cumulative_size += widget_size;
162
163 if cumulative_size > main_axis_size {
164 max_scrollbar_position = index + 1;
165 break;
166 }
167 }
168
169 self.scrollbar_state = self.scrollbar_state.content_length(max_scrollbar_position);
170 self.scrollbar_state = self.scrollbar_state.position(self.view_state.offset);
171 }
172
173 /// Returns the index of the first item currently displayed on the screen.
174 #[must_use]
175 pub fn scroll_offset_index(&self) -> usize {
176 self.view_state.offset
177 }
178
179 /// Returns the number of rows/columns of the first visible item that are scrolled off the top/left.
180 ///
181 /// When the first visible item is partially scrolled out of view, this returns how many
182 /// rows (for vertical lists) or columns (for horizontal lists) are hidden above/left of
183 /// the viewport. Returns 0 if the first visible item is fully visible.
184 ///
185 /// # Example
186 ///
187 /// If message #5 is the first visible item but its first 2 rows are scrolled off the top,
188 /// this returns 2. Combined with `scroll_offset_index()`, you can calculate the exact
189 /// scroll position in pixels/rows.
190 #[must_use]
191 pub fn scroll_truncation(&self) -> u16 {
192 self.view_state.first_truncated
193 }
194}