tui_tree_widget/tree_state.rs
1use std::collections::HashSet;
2
3use ratatui::layout::{Position, Rect};
4
5use crate::flatten::{flatten, Flattened};
6use crate::tree_item::TreeItem;
7
8/// Keeps the state of what is currently selected and what was opened in a [`Tree`](crate::Tree).
9///
10/// The generic argument `Identifier` is used to keep the state like the currently selected or opened [`TreeItem`]s in the [`TreeState`].
11/// For more information see [`TreeItem`].
12///
13/// # Example
14///
15/// ```
16/// # use tui_tree_widget::TreeState;
17/// type Identifier = usize;
18///
19/// let mut state = TreeState::<Identifier>::default();
20/// ```
21#[must_use]
22#[derive(Debug)]
23pub struct TreeState<Identifier> {
24 pub(super) offset: usize,
25 pub(super) opened: HashSet<Vec<Identifier>>,
26 pub(super) selected: Vec<Identifier>,
27 pub(super) ensure_selected_in_view_on_next_render: bool,
28
29 pub(super) last_area: Rect,
30 pub(super) last_biggest_index: usize,
31 /// All identifiers open on last render
32 pub(super) last_identifiers: Vec<Vec<Identifier>>,
33 /// Identifier rendered at `y` on last render
34 pub(super) last_rendered_identifiers: Vec<(u16, Vec<Identifier>)>,
35}
36
37impl<Identifier> Default for TreeState<Identifier> {
38 fn default() -> Self {
39 Self {
40 offset: 0,
41 opened: HashSet::new(),
42 selected: Vec::new(),
43 ensure_selected_in_view_on_next_render: false,
44
45 last_area: Rect::ZERO,
46 last_biggest_index: 0,
47 last_identifiers: Vec::new(),
48 last_rendered_identifiers: Vec::new(),
49 }
50 }
51}
52
53impl<Identifier> TreeState<Identifier>
54where
55 Identifier: Clone + PartialEq + Eq + core::hash::Hash,
56{
57 #[must_use]
58 pub const fn get_offset(&self) -> usize {
59 self.offset
60 }
61
62 #[must_use]
63 #[deprecated = "Use self.opened()"]
64 pub fn get_all_opened(&self) -> Vec<Vec<Identifier>> {
65 self.opened.iter().cloned().collect()
66 }
67
68 #[must_use]
69 pub const fn opened(&self) -> &HashSet<Vec<Identifier>> {
70 &self.opened
71 }
72
73 #[allow(clippy::missing_const_for_fn)] // False positive
74 #[must_use]
75 pub fn selected(&self) -> &[Identifier] {
76 &self.selected
77 }
78
79 /// Get a flat list of all currently viewable (including by scrolling) [`TreeItem`]s with this `TreeState`.
80 #[must_use]
81 pub fn flatten<'text>(
82 &self,
83 items: &'text [TreeItem<'text, Identifier>],
84 ) -> Vec<Flattened<'text, Identifier>> {
85 flatten(&self.opened, items, &[])
86 }
87
88 /// Selects the given identifier.
89 ///
90 /// Returns `true` when the selection changed.
91 ///
92 /// Clear the selection by passing an empty identifier vector:
93 ///
94 /// ```rust
95 /// # use tui_tree_widget::TreeState;
96 /// # let mut state = TreeState::<usize>::default();
97 /// state.select(Vec::new());
98 /// ```
99 pub fn select(&mut self, identifier: Vec<Identifier>) -> bool {
100 self.ensure_selected_in_view_on_next_render = true;
101 let changed = self.selected != identifier;
102 self.selected = identifier;
103 changed
104 }
105
106 /// Open a tree node.
107 /// Returns `true` when it was closed and has been opened.
108 /// Returns `false` when it was already open.
109 pub fn open(&mut self, identifier: Vec<Identifier>) -> bool {
110 if identifier.is_empty() {
111 false
112 } else {
113 self.opened.insert(identifier)
114 }
115 }
116
117 /// Close a tree node.
118 /// Returns `true` when it was open and has been closed.
119 /// Returns `false` when it was already closed.
120 pub fn close(&mut self, identifier: &[Identifier]) -> bool {
121 self.opened.remove(identifier)
122 }
123
124 /// Toggles a tree node open/close state.
125 /// When it is currently open, then [`close`](Self::close) is called. Otherwise [`open`](Self::open).
126 ///
127 /// Returns `true` when a node is opened / closed.
128 /// As toggle always changes something, this only returns `false` when an empty identifier is given.
129 pub fn toggle(&mut self, identifier: Vec<Identifier>) -> bool {
130 if identifier.is_empty() {
131 false
132 } else if self.opened.contains(&identifier) {
133 self.close(&identifier)
134 } else {
135 self.open(identifier)
136 }
137 }
138
139 /// Toggles the currently selected tree node open/close state.
140 /// See also [`toggle`](Self::toggle)
141 ///
142 /// Returns `true` when a node is opened / closed.
143 /// As toggle always changes something, this only returns `false` when nothing is selected.
144 pub fn toggle_selected(&mut self) -> bool {
145 if self.selected.is_empty() {
146 return false;
147 }
148
149 self.ensure_selected_in_view_on_next_render = true;
150
151 // Reimplement self.close because of multiple different borrows
152 let was_open = self.opened.remove(&self.selected);
153 if was_open {
154 return true;
155 }
156
157 self.open(self.selected.clone())
158 }
159
160 /// Closes all open nodes.
161 ///
162 /// Returns `true` when any node was closed.
163 pub fn close_all(&mut self) -> bool {
164 if self.opened.is_empty() {
165 false
166 } else {
167 self.opened.clear();
168 true
169 }
170 }
171
172 /// Select the first node.
173 ///
174 /// Returns `true` when the selection changed.
175 pub fn select_first(&mut self) -> bool {
176 let identifier = self.last_identifiers.first().cloned().unwrap_or_default();
177 self.select(identifier)
178 }
179
180 /// Select the last node.
181 ///
182 /// Returns `true` when the selection changed.
183 pub fn select_last(&mut self) -> bool {
184 let new_identifier = self.last_identifiers.last().cloned().unwrap_or_default();
185 self.select(new_identifier)
186 }
187
188 /// Select the node on the given index.
189 ///
190 /// Returns `true` when the selection changed.
191 ///
192 /// This can be useful for mouse clicks.
193 #[deprecated = "Prefer self.click_at or self.rendered_at as visible index is hard to predict with height != 1"]
194 pub fn select_visible_index(&mut self, new_index: usize) -> bool {
195 let new_index = new_index.min(self.last_biggest_index);
196 let new_identifier = self
197 .last_identifiers
198 .get(new_index)
199 .cloned()
200 .unwrap_or_default();
201 self.select(new_identifier)
202 }
203
204 /// Move the current selection with the direction/amount by the given function.
205 ///
206 /// Returns `true` when the selection changed.
207 ///
208 /// # Example
209 ///
210 /// ```
211 /// # use tui_tree_widget::TreeState;
212 /// # type Identifier = usize;
213 /// # let mut state = TreeState::<Identifier>::default();
214 /// // Move the selection one down
215 /// state.select_visible_relative(|current| {
216 /// // When nothing is currently selected, select index 0
217 /// // Otherwise select current + 1 (without panicking)
218 /// current.map_or(0, |current| current.saturating_add(1))
219 /// });
220 /// ```
221 ///
222 /// For more examples take a look into the source code of [`key_up`](Self::key_up) or [`key_down`](Self::key_down).
223 /// They are implemented with this method.
224 #[deprecated = "renamed to select_relative"]
225 pub fn select_visible_relative<F>(&mut self, change_function: F) -> bool
226 where
227 F: FnOnce(Option<usize>) -> usize,
228 {
229 let identifiers = &self.last_identifiers;
230 let current_identifier = &self.selected;
231 let current_index = identifiers
232 .iter()
233 .position(|identifier| identifier == current_identifier);
234 let new_index = change_function(current_index).min(self.last_biggest_index);
235 let new_identifier = identifiers.get(new_index).cloned().unwrap_or_default();
236 self.select(new_identifier)
237 }
238
239 /// Move the current selection with the direction/amount by the given function.
240 ///
241 /// Returns `true` when the selection changed.
242 ///
243 /// # Example
244 ///
245 /// ```
246 /// # use tui_tree_widget::TreeState;
247 /// # type Identifier = usize;
248 /// # let mut state = TreeState::<Identifier>::default();
249 /// // Move the selection one down
250 /// state.select_relative(|current| {
251 /// // When nothing is currently selected, select index 0
252 /// // Otherwise select current + 1 (without panicking)
253 /// current.map_or(0, |current| current.saturating_add(1))
254 /// });
255 /// ```
256 ///
257 /// For more examples take a look into the source code of [`key_up`](Self::key_up) or [`key_down`](Self::key_down).
258 /// They are implemented with this method.
259 pub fn select_relative<F>(&mut self, change_function: F) -> bool
260 where
261 F: FnOnce(Option<usize>) -> usize,
262 {
263 let identifiers = &self.last_identifiers;
264 let current_identifier = &self.selected;
265 let current_index = identifiers
266 .iter()
267 .position(|identifier| identifier == current_identifier);
268 let new_index = change_function(current_index).min(self.last_biggest_index);
269 let new_identifier = identifiers.get(new_index).cloned().unwrap_or_default();
270 self.select(new_identifier)
271 }
272
273 /// Get the identifier that was rendered for the given position on last render.
274 #[must_use]
275 pub fn rendered_at(&self, position: Position) -> Option<&[Identifier]> {
276 if !self.last_area.contains(position) {
277 return None;
278 }
279
280 self.last_rendered_identifiers
281 .iter()
282 .rev()
283 .find(|(y, _)| position.y >= *y)
284 .map(|(_, identifier)| identifier.as_ref())
285 }
286
287 /// Select what was rendered at the given position on last render.
288 /// When it is already selected, toggle it.
289 ///
290 /// Returns `true` when the state changed.
291 /// Returns `false` when there was nothing at the given position.
292 pub fn click_at(&mut self, position: Position) -> bool {
293 if let Some(identifier) = self.rendered_at(position) {
294 if identifier == self.selected {
295 self.toggle_selected()
296 } else {
297 self.select(identifier.to_vec())
298 }
299 } else {
300 false
301 }
302 }
303
304 /// Ensure the selected [`TreeItem`] is in view on next render
305 pub fn scroll_selected_into_view(&mut self) {
306 self.ensure_selected_in_view_on_next_render = true;
307 }
308
309 /// Scroll the specified amount of lines up
310 ///
311 /// Returns `true` when the scroll position changed.
312 /// Returns `false` when the scrolling has reached the top.
313 pub fn scroll_up(&mut self, lines: usize) -> bool {
314 let before = self.offset;
315 self.offset = self.offset.saturating_sub(lines);
316 before != self.offset
317 }
318
319 /// Scroll the specified amount of lines down
320 ///
321 /// Returns `true` when the scroll position changed.
322 /// Returns `false` when the scrolling has reached the last [`TreeItem`].
323 pub fn scroll_down(&mut self, lines: usize) -> bool {
324 let before = self.offset;
325 self.offset = self
326 .offset
327 .saturating_add(lines)
328 .min(self.last_biggest_index);
329 before != self.offset
330 }
331
332 /// Handles the up arrow key.
333 /// Moves up in the current depth or to its parent.
334 ///
335 /// Returns `true` when the selection changed.
336 pub fn key_up(&mut self) -> bool {
337 self.select_relative(|current| {
338 // When nothing is selected, fall back to end
339 current.map_or(usize::MAX, |current| current.saturating_sub(1))
340 })
341 }
342
343 /// Handles the down arrow key.
344 /// Moves down in the current depth or into a child node.
345 ///
346 /// Returns `true` when the selection changed.
347 pub fn key_down(&mut self) -> bool {
348 self.select_relative(|current| {
349 // When nothing is selected, fall back to start
350 current.map_or(0, |current| current.saturating_add(1))
351 })
352 }
353
354 /// Handles the left arrow key.
355 /// Closes the currently selected or moves to its parent.
356 ///
357 /// Returns `true` when the selection or the open state changed.
358 pub fn key_left(&mut self) -> bool {
359 self.ensure_selected_in_view_on_next_render = true;
360 // Reimplement self.close because of multiple different borrows
361 let mut changed = self.opened.remove(&self.selected);
362 if !changed {
363 // Select the parent by removing the leaf from selection
364 let popped = self.selected.pop();
365 changed = popped.is_some();
366 }
367 changed
368 }
369
370 /// Handles the right arrow key.
371 /// Opens the currently selected.
372 ///
373 /// Returns `true` when it was closed and has been opened.
374 /// Returns `false` when it was already open or nothing being selected.
375 pub fn key_right(&mut self) -> bool {
376 if self.selected.is_empty() {
377 false
378 } else {
379 self.ensure_selected_in_view_on_next_render = true;
380 self.open(self.selected.clone())
381 }
382 }
383}