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