tui_treelistview/state/
navigation.rs1use std::hash::Hash;
2
3use crate::projection::ProjectedNode;
4use crate::style::TreeScrollPolicy;
5
6use super::TreeListViewState;
7
8impl<Id: Copy + Eq + Hash> TreeListViewState<Id> {
9 #[must_use]
11 pub const fn selected_id(&self) -> Option<Id> {
12 self.selected
13 }
14
15 #[must_use]
17 pub const fn selected_index(&self) -> Option<usize> {
18 self.selected_row
19 }
20
21 pub fn select_id(&mut self, selected: Option<Id>) -> bool {
23 let index = selected.and_then(|id| self.projection.index_of(id));
24 self.set_selection(index)
25 }
26
27 pub fn select_index(&mut self, index: Option<usize>) -> bool {
29 self.set_selection(index)
30 }
31
32 pub fn select_first(&mut self) -> bool {
34 self.select_index((!self.projection.is_empty()).then_some(0))
35 }
36
37 pub fn select_last(&mut self) -> bool {
39 self.select_index(
40 (!self.projection.is_empty()).then_some(self.projection.len().saturating_sub(1)),
41 )
42 }
43
44 pub fn select_prev(&mut self) -> bool {
46 if self.projection.is_empty() {
47 return self.set_selection(None);
48 }
49 let index = self.selected_index().map_or_else(
50 || self.projection.len().saturating_sub(1),
51 |index| index.saturating_sub(1),
52 );
53 self.select_index(Some(index))
54 }
55
56 pub fn select_next(&mut self) -> bool {
58 if self.projection.is_empty() {
59 return self.set_selection(None);
60 }
61 let index = self.selected_index().map_or(0, |index| {
62 index.saturating_add(1).min(self.projection.len() - 1)
63 });
64 self.select_index(Some(index))
65 }
66
67 pub fn select_parent(&mut self) -> bool {
69 let parent = self.selected_node().and_then(ProjectedNode::parent_index);
70 parent.is_some() && self.set_selection(parent)
71 }
72
73 pub fn select_first_child(&mut self) -> bool {
75 let Some(index) = self.selected_index() else {
76 return false;
77 };
78 let Some(parent) = self.projection.nodes().get(index).copied() else {
79 return false;
80 };
81 let child = self
82 .projection
83 .nodes()
84 .get(index.saturating_add(1))
85 .filter(|candidate| candidate.level() == parent.level().saturating_add(1))
86 .map(|_| index.saturating_add(1));
87 child.is_some() && self.set_selection(child)
88 }
89
90 #[must_use]
92 pub fn selected_parent_id(&self) -> Option<Id> {
93 self.selected_node().and_then(ProjectedNode::parent)
94 }
95
96 #[must_use]
97 pub fn selected_level(&self) -> Option<usize> {
98 self.selected_node().map(ProjectedNode::level)
99 }
100
101 #[must_use]
102 pub const fn visible_len(&self) -> usize {
103 self.projection.len()
104 }
105
106 #[must_use]
107 pub const fn is_empty(&self) -> bool {
108 self.projection.is_empty()
109 }
110
111 pub fn visible_ids(&self) -> impl Iterator<Item = Id> + '_ {
112 self.projection.nodes().iter().map(|node| node.id())
113 }
114
115 #[must_use]
116 pub fn visible_index_of(&self, id: Id) -> Option<usize> {
117 self.projection.index_of(id)
118 }
119
120 #[must_use]
121 pub fn visible_contains(&self, id: Id) -> bool {
122 self.projection.index_of(id).is_some()
123 }
124
125 #[must_use]
127 pub const fn offset(&self) -> usize {
128 self.offset
129 }
130
131 pub fn set_offset(&mut self, offset: usize) -> bool {
133 let offset = offset.min(self.projection.len().saturating_sub(1));
134 let changed = self.offset != offset;
135 self.offset = offset;
136 self.selection_needs_visibility = false;
137 changed
138 }
139
140 pub fn scroll_view_by(&mut self, amount: isize) -> bool {
142 let offset = if amount.is_negative() {
143 self.offset.saturating_sub(amount.unsigned_abs())
144 } else {
145 self.offset.saturating_add(amount.cast_unsigned())
146 };
147 self.set_offset(offset)
148 }
149
150 #[must_use]
151 pub const fn horizontal_offset(&self) -> u16 {
152 self.horizontal_offset
153 }
154
155 pub const fn set_horizontal_offset(&mut self, offset: u16) -> bool {
156 let changed = self.horizontal_offset != offset;
157 self.horizontal_offset = offset;
158 self.column_needs_visibility = false;
159 changed
160 }
161
162 pub const fn scroll_horizontal_by(&mut self, amount: i16) -> bool {
163 let offset = if amount.is_negative() {
164 self.horizontal_offset.saturating_sub(amount.unsigned_abs())
165 } else {
166 self.horizontal_offset
167 .saturating_add(amount.cast_unsigned())
168 };
169 self.set_horizontal_offset(offset)
170 }
171
172 pub(crate) fn clamp_horizontal_offset(&mut self, maximum: u16) {
173 self.horizontal_offset = self.horizontal_offset.min(maximum);
174 }
175
176 #[must_use]
177 pub const fn selected_column(&self) -> Option<usize> {
178 self.selected_column
179 }
180
181 pub fn select_column(&mut self, column: Option<usize>, column_count: usize) -> bool {
182 let column = column.filter(|column| *column < column_count);
183 let changed = self.selected_column != column;
184 self.selected_column = column;
185 if changed {
186 self.column_needs_visibility = column.is_some();
187 }
188 changed
189 }
190
191 pub fn select_column_left(&mut self, column_count: usize) -> bool {
192 if column_count == 0 {
193 return self.select_column(None, 0);
194 }
195 let column = self
196 .selected_column
197 .filter(|column| *column < column_count)
198 .map_or(column_count - 1, |column| column.saturating_sub(1));
199 self.select_column(Some(column), column_count)
200 }
201
202 pub fn select_column_right(&mut self, column_count: usize) -> bool {
203 if column_count == 0 {
204 return self.select_column(None, 0);
205 }
206 let column = self
207 .selected_column
208 .filter(|column| *column < column_count)
209 .map_or(0, |column| column.saturating_add(1).min(column_count - 1));
210 self.select_column(Some(column), column_count)
211 }
212
213 pub(crate) fn ensure_selection_visible(
214 &mut self,
215 viewport_height: usize,
216 policy: TreeScrollPolicy,
217 ) {
218 if !self.selection_needs_visibility {
219 return;
220 }
221 let Some(selected) = self.selected_index() else {
222 self.selection_needs_visibility = false;
223 return;
224 };
225 let height = viewport_height.max(1);
226 match policy {
227 TreeScrollPolicy::KeepInView => {
228 if selected < self.offset {
229 self.offset = selected;
230 } else if selected >= self.offset.saturating_add(height) {
231 self.offset = selected.saturating_add(1).saturating_sub(height);
232 }
233 }
234 TreeScrollPolicy::CenterOnSelect => {
235 self.offset = selected.saturating_sub(height / 2);
236 }
237 }
238 self.offset = self
239 .offset
240 .min(self.projection.len().saturating_sub(height));
241 self.selection_needs_visibility = false;
242 }
243
244 pub(crate) fn clamp_offset_to_viewport(&mut self, viewport_height: usize) {
245 let maximum = self.projection.len().saturating_sub(viewport_height.max(1));
246 self.offset = self.offset.min(maximum);
247 }
248
249 pub(crate) const fn ensure_column_visible(
250 &mut self,
251 start: u16,
252 width: u16,
253 viewport_width: u16,
254 ) {
255 if !self.column_needs_visibility || viewport_width == 0 {
256 return;
257 }
258 let end = start.saturating_add(width);
259 if start < self.horizontal_offset {
260 self.horizontal_offset = start;
261 } else if end > self.horizontal_offset.saturating_add(viewport_width) {
262 self.horizontal_offset = end.saturating_sub(viewport_width);
263 }
264 self.column_needs_visibility = false;
265 }
266
267 fn set_selection(&mut self, selected_row: Option<usize>) -> bool {
268 let selected_row = selected_row.filter(|&index| index < self.projection.len());
269 let selected = selected_row
270 .and_then(|index| self.projection.nodes().get(index))
271 .map(|node| node.id());
272 let changed = self.selected != selected || self.selected_row != selected_row;
273 self.selected = selected;
274 self.selected_row = selected_row;
275 if changed {
276 self.selection_needs_visibility = selected.is_some();
277 }
278 changed
279 }
280}