mecomp_tui/ui/widgets/tree/
state.rs

1use std::collections::HashSet;
2
3use ratatui::layout::{Position, Rect};
4
5use super::{
6    flatten::{flatten, Flattened},
7    item::CheckTreeItem,
8};
9
10/// Keeps the state of what is currently selected and what was opened in a [`CheckTree`](super::CheckTree)
11#[derive(Debug, Default, Clone)]
12#[allow(clippy::module_name_repetitions)]
13pub struct CheckTreeState<Identifier> {
14    pub(super) offset: usize,
15    pub(super) opened: HashSet<Vec<Identifier>>,
16    pub(super) selected: Vec<Identifier>,
17    pub(super) checked: HashSet<Vec<Identifier>>,
18    pub(super) ensure_selected_in_view_on_next_render: bool,
19
20    pub(super) last_area: Rect,
21    pub(super) last_biggest_index: usize,
22    /// All identifiers open on last render
23    pub(super) last_identifiers: Vec<Vec<Identifier>>,
24    /// Identifier rendered at `y` on last render
25    pub(super) last_rendered_identifiers: Vec<(u16, Vec<Identifier>)>,
26}
27
28impl<Identifier> CheckTreeState<Identifier>
29where
30    Identifier: Clone + PartialEq + Eq + core::hash::Hash,
31{
32    #[must_use]
33    pub const fn get_offset(&self) -> usize {
34        self.offset
35    }
36
37    #[must_use]
38    pub const fn opened(&self) -> &HashSet<Vec<Identifier>> {
39        &self.opened
40    }
41
42    /// Refers to the current cursor selection.
43    #[must_use]
44    pub fn selected(&self) -> &[Identifier] {
45        &self.selected
46    }
47
48    /// Refers to the current checked items.
49    #[must_use]
50    pub const fn checked(&self) -> &HashSet<Vec<Identifier>> {
51        &self.checked
52    }
53
54    /// Get a flat list of all currently viewable (including by scrolling) [`CheckTreeItem`]s with this `CheckTreeState`.
55    #[must_use]
56    pub fn flatten<'text>(
57        &self,
58        items: &'text [CheckTreeItem<'text, Identifier>],
59    ) -> Vec<Flattened<'text, Identifier>> {
60        flatten(&self.opened, items, &[])
61    }
62
63    /// Selects the given identifier.
64    ///
65    /// Returns `true` when the selection changed.
66    ///
67    /// Clear the selection by passing an empty identifier vector:
68    pub fn select(&mut self, identifier: Vec<Identifier>) -> bool {
69        self.ensure_selected_in_view_on_next_render = true;
70        let changed = self.selected != identifier;
71        self.selected = identifier;
72        changed
73    }
74
75    /// Open a tree node.
76    /// Returns `true` when it was closed and has been opened.
77    /// Returns `false` when it was already open.
78    ///
79    /// TODO: This should return `false` when it was a leaf node.
80    pub fn open(&mut self, identifier: Vec<Identifier>) -> bool {
81        if identifier.is_empty() {
82            false
83        } else {
84            self.opened.insert(identifier)
85        }
86    }
87
88    /// Close a tree node.
89    /// Returns `true` when it was open and has been closed.
90    /// Returns `false` when it was already closed.
91    ///
92    /// TODO: This should return `false` when it was a leaf node.
93    pub fn close(&mut self, identifier: &[Identifier]) -> bool {
94        self.opened.remove(identifier)
95    }
96
97    /// Check a tree node
98    /// Returns `true` when it was unchecked and has been checked.
99    /// Returns `false` when it was already checked.
100    ///
101    /// TODO: This should return `false` when it was not a leaf node.
102    pub fn check(&mut self, identifier: Vec<Identifier>) -> bool {
103        if identifier.is_empty() {
104            false
105        } else {
106            // insert the identifier
107            self.checked.insert(identifier)
108        }
109    }
110
111    /// Uncheck a tree node
112    /// Returns `true` when it was checked and has been unchecked.
113    /// Returns `false` when it was already unchecked.
114    ///
115    /// TODO: This should return `false` when it was not a leaf node.
116    pub fn uncheck(&mut self, identifier: &[Identifier]) -> bool {
117        self.checked.remove(identifier)
118    }
119
120    /// Toggles a tree node open/close state.
121    /// When it is currently open, then [`close`](Self::close) is called. Otherwise [`open`](Self::open).
122    ///
123    /// Returns `true` when a node is opened / closed.
124    /// As toggle always changes something, this only returns `false` when an empty identifier is given.
125    ///
126    /// TODO: This should return `false` when it was a leaf node.
127    pub fn toggle(&mut self, identifier: Vec<Identifier>) -> bool {
128        if identifier.is_empty() {
129            false
130        } else if self.opened.contains(&identifier) {
131            self.close(&identifier)
132        } else {
133            self.open(identifier)
134        }
135    }
136
137    /// Toggles the currently selected tree node open/close state.
138    /// See also [`toggle`](Self::toggle)
139    ///
140    /// Returns `true` when a node is opened / closed.
141    /// As toggle always changes something, this only returns `false` when nothing is selected.
142    ///
143    /// TODO: This should return `false` when it was a leaf node.
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    /// Toggles a tree node checked/unchecked state.
161    /// When it is currently checked, then [`uncheck`](Self::uncheck) is called. Otherwise [`check`](Self::check).
162    ///
163    /// Returns `true` when a node is checked / unchecked.
164    /// As toggle always changes something, this only returns `false` when an empty identifier is given.
165    ///
166    /// TODO: This should return `false` when it was not a leaf node.
167    pub fn toggle_check(&mut self, identifier: Vec<Identifier>) -> bool {
168        if identifier.is_empty() {
169            false
170        } else if self.checked.contains(&identifier) {
171            self.uncheck(&identifier)
172        } else {
173            self.check(identifier)
174        }
175    }
176
177    /// Toggles the currently selected tree node checked/unchecked state.
178    /// See also [`toggle_check`](Self::toggle_check)
179    /// Returns `true` when a node is checked / unchecked.
180    /// As toggle always changes something, this only returns `false` when nothing is selected.
181    ///
182    /// TODO: This should return `false` when it was not a leaf node.
183    pub fn toggle_check_selected(&mut self) -> bool {
184        if self.selected.is_empty() {
185            return false;
186        }
187
188        // Reimplement self.uncheck because of multiple different borrows
189        let was_checked = self.checked.remove(&self.selected);
190        if was_checked {
191            return true;
192        }
193
194        self.check(self.selected.clone())
195    }
196
197    /// Closes all open nodes, and uncheck all checked nodes.
198    ///
199    /// Returns `true` when any node was closed or unchecked.
200    pub fn reset(&mut self) -> bool {
201        if self.opened.is_empty() && self.checked.is_empty() {
202            false
203        } else {
204            self.opened.clear();
205            self.checked.clear();
206            true
207        }
208    }
209
210    /// Select the first node.
211    ///
212    /// Returns `true` when the selection changed.
213    pub fn select_first(&mut self) -> bool {
214        let identifier = self.last_identifiers.first().cloned().unwrap_or_default();
215        self.select(identifier)
216    }
217
218    /// Select the last node.
219    ///
220    /// Returns `true` when the selection changed.
221    pub fn select_last(&mut self) -> bool {
222        let new_identifier = self.last_identifiers.last().cloned().unwrap_or_default();
223        self.select(new_identifier)
224    }
225
226    /// Move the current selection with the direction/amount by the given function.
227    ///
228    /// Returns `true` when the selection changed.
229    ///
230    /// For more examples take a look into the source code of [`key_up`](Self::key_up) or [`key_down`](Self::key_down).
231    /// They are implemented with this method.
232    pub fn select_relative<F>(&mut self, change_function: F) -> bool
233    where
234        F: FnOnce(Option<usize>) -> usize,
235    {
236        let identifiers = &self.last_identifiers;
237        let current_identifier = &self.selected;
238        let current_index = identifiers
239            .iter()
240            .position(|identifier| identifier == current_identifier);
241        let new_index = change_function(current_index).min(self.last_biggest_index);
242        let new_identifier = identifiers.get(new_index).cloned().unwrap_or_default();
243        self.select(new_identifier)
244    }
245
246    /// Get the identifier that was rendered for the given position on last render.
247    #[must_use]
248    pub fn rendered_at(&self, position: Position) -> Option<&[Identifier]> {
249        if !self.last_area.contains(position) {
250            return None;
251        }
252
253        self.last_rendered_identifiers
254            .iter()
255            .rev()
256            .find(|(y, _)| position.y == *y)
257            .map(|(_, identifier)| identifier.as_ref())
258    }
259
260    /// Ensure the selected [`CheckTreeItem`] is in view on next render
261    pub fn scroll_selected_into_view(&mut self) {
262        self.ensure_selected_in_view_on_next_render = true;
263    }
264
265    /// Scroll the specified amount of lines up
266    ///
267    /// Returns `true` when the scroll position changed.
268    /// Returns `false` when the scrolling has reached the top.
269    pub fn scroll_up(&mut self, lines: usize) -> bool {
270        let before = self.offset;
271        self.offset = self.offset.saturating_sub(lines);
272        before != self.offset
273    }
274
275    /// Scroll the specified amount of lines down
276    ///
277    /// Returns `true` when the scroll position changed.
278    /// Returns `false` when the scrolling has reached the last [`CheckTreeItem`].
279    pub fn scroll_down(&mut self, lines: usize) -> bool {
280        let before = self.offset;
281        self.offset = self
282            .offset
283            .saturating_add(lines)
284            .min(self.last_biggest_index);
285        before != self.offset
286    }
287
288    /// Handles the up arrow key.
289    /// Moves up in the current depth or to its parent.
290    ///
291    /// Returns `true` when the selection changed.
292    pub fn key_up(&mut self) -> bool {
293        self.select_relative(|current| {
294            // When nothing is selected, fall back to end
295            current.map_or(usize::MAX, |current| current.saturating_sub(1))
296        })
297    }
298
299    /// Handles the down arrow key.
300    /// Moves down in the current depth or into a child node.
301    ///
302    /// Returns `true` when the selection changed.
303    pub fn key_down(&mut self) -> bool {
304        self.select_relative(|current| {
305            // When nothing is selected, fall back to start
306            current.map_or(0, |current| current.saturating_add(1))
307        })
308    }
309
310    /// Handles the left arrow key.
311    /// Closes the currently selected or moves to its parent.
312    ///
313    /// Returns `true` when the selection or the open state changed.
314    pub fn key_left(&mut self) -> bool {
315        self.ensure_selected_in_view_on_next_render = true;
316        // Reimplement self.close because of multiple different borrows
317        let mut changed = self.opened.remove(&self.selected);
318        if !changed {
319            // Select the parent by removing the leaf from selection
320            let popped = self.selected.pop();
321            changed = popped.is_some();
322        }
323        changed
324    }
325
326    /// Handles the right arrow key.
327    /// Opens the currently selected.
328    ///
329    /// Returns `true` when it was closed and has been opened.
330    /// Returns `false` when it was already open or nothing being selected.
331    pub fn key_right(&mut self) -> bool {
332        if self.selected.is_empty() {
333            false
334        } else {
335            self.ensure_selected_in_view_on_next_render = true;
336            self.open(self.selected.clone())
337        }
338    }
339
340    /// Handles the space key.
341    /// Toggles the whether the current item is selected
342    ///
343    /// Returns `true` when the selection changed.
344    pub fn key_space(&mut self) -> bool {
345        self.toggle_check_selected()
346    }
347
348    /// Handles a mouse click.
349    /// Selects the item at the given position.
350    /// If the item is a leaf, it checks it.
351    /// It the item is a branch, it toggles it open/closed.
352    ///
353    /// Returns `true` when the selection or the open state changed.
354    /// Returns `false` when nothing was rendered at the given position (nothing was clicked).
355    pub fn mouse_click(&mut self, position: Position) -> bool {
356        let Some(identifier) = self.rendered_at(position) else {
357            // if we clicked outside of the last render, clear the selection
358            self.selected.clear();
359            return false;
360        };
361        self.select(identifier.to_vec());
362
363        self.ensure_selected_in_view_on_next_render = true;
364
365        // since we set the selection before, we know that one or both of these will return true
366        // TODO: when the todos for the `toggle_check_selected` and `toggle_selected` methods are done, this && needs to be replaced with ||
367        self.toggle_check_selected() && self.toggle_selected()
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use pretty_assertions::assert_eq;
375
376    #[test]
377    fn test_select() {
378        let mut state: CheckTreeState<&str> = CheckTreeState::default();
379
380        let id: &[&str] = &[];
381        assert_eq!(state.select(id.to_vec()), false);
382        assert_eq!(state.selected(), id);
383
384        let id = &["a"];
385        assert_eq!(state.select(id.clone().to_vec()), true);
386        assert_eq!(state.selected(), id);
387
388        let id = &["a", "b"];
389        assert_eq!(state.select(id.clone().to_vec()), true);
390        assert_eq!(state.selected(), id);
391
392        let id: &[&str] = &[];
393        assert_eq!(state.select(id.to_vec()), true);
394        assert_eq!(state.selected(), id);
395
396        let id: &[&str] = &[];
397        assert_eq!(state.select(id.to_vec()), false);
398        assert_eq!(state.selected(), id);
399    }
400
401    #[test]
402    fn test_open() {
403        let mut state: CheckTreeState<&str> = CheckTreeState::default();
404        let mut expected: HashSet<Vec<&str>> = HashSet::default();
405
406        // at first, it's empty
407        assert_eq!(state.opened(), &expected);
408
409        // we get false if we "open" nothing
410        assert_eq!(state.open(vec![]), false);
411
412        // we get true if we open something that isn't already open
413        let id = vec!["a"];
414        assert_eq!(state.open(id.clone()), true);
415        expected.insert(id.clone());
416        assert_eq!(state.opened(), &expected);
417
418        // we get false if we open it again
419        let id = vec!["a"];
420        assert_eq!(state.open(id.clone()), false);
421        assert_eq!(state.opened(), &expected);
422    }
423
424    #[test]
425    fn test_close() {
426        let mut state: CheckTreeState<&str> = CheckTreeState::default();
427
428        assert_eq!(state.close(&["a"]), false);
429
430        state.open(vec!["a"]);
431
432        assert_eq!(state.close(&["a"]), true);
433        assert_eq!(state.close(&["a"]), false);
434    }
435
436    #[test]
437    fn test_reset() {
438        let mut state: CheckTreeState<&str> = CheckTreeState::default();
439
440        assert_eq!(state.reset(), false);
441
442        state.open(vec!["a"]);
443
444        assert_eq!(state.reset(), true);
445        assert_eq!(state.reset(), false);
446
447        state.open(vec!["a"]);
448        state.open(vec!["a", "b"]);
449
450        assert_eq!(state.reset(), true);
451        assert_eq!(state.reset(), false);
452    }
453
454    #[test]
455    fn test_check() {
456        let mut state: CheckTreeState<&str> = CheckTreeState::default();
457        let mut expected: HashSet<Vec<&str>> = HashSet::default();
458
459        // at first, it's empty
460        assert_eq!(state.checked(), &expected);
461
462        // we get false if we "check" nothing
463        assert_eq!(state.check(vec![]), false);
464
465        // we get true if we check something that isn't already open
466        let id = vec!["a"];
467        assert_eq!(state.check(id.clone()), true);
468        expected.insert(id.clone());
469        assert_eq!(state.checked(), &expected);
470
471        // we get false if we check it again
472        let id = vec!["a"];
473        assert_eq!(state.check(id.clone()), false);
474        assert_eq!(state.checked(), &expected);
475    }
476
477    #[test]
478    fn test_uncheck() {
479        let mut state: CheckTreeState<&str> = CheckTreeState::default();
480
481        assert_eq!(state.uncheck(&["a"]), false);
482
483        state.check(vec!["a"]);
484
485        assert_eq!(state.uncheck(&["a"]), true);
486        assert_eq!(state.uncheck(&["a"]), false);
487    }
488
489    #[test]
490    fn test_toggle() {
491        let mut state: CheckTreeState<&str> = CheckTreeState::default();
492        let mut expected: HashSet<Vec<&str>> = HashSet::default();
493
494        assert_eq!(state.toggle(vec![]), false);
495
496        let id = vec!["a"];
497        assert_eq!(state.toggle(id.clone()), true);
498        expected.insert(id.clone());
499        assert_eq!(state.opened(), &expected);
500
501        assert_eq!(state.toggle(id.clone()), true);
502        expected.remove(&id);
503        assert_eq!(state.opened(), &expected);
504    }
505
506    #[test]
507    fn test_toggle_selected() {
508        let mut state: CheckTreeState<&str> = CheckTreeState::default();
509        let mut expected: HashSet<Vec<&str>> = HashSet::default();
510
511        assert_eq!(state.toggle_selected(), false);
512
513        let id = vec!["a"];
514        state.select(id.clone());
515
516        assert_eq!(state.toggle_selected(), true);
517        expected.insert(id.clone());
518        assert_eq!(state.opened(), &expected);
519
520        assert_eq!(state.toggle_selected(), true);
521        expected.remove(&id);
522        assert_eq!(state.opened(), &expected);
523    }
524
525    #[test]
526    fn test_toggle_check() {
527        let mut state: CheckTreeState<&str> = CheckTreeState::default();
528        let mut expected: HashSet<Vec<&str>> = HashSet::default();
529
530        assert_eq!(state.toggle_check(vec![]), false);
531
532        let id = vec!["a"];
533        assert_eq!(state.toggle_check(id.clone()), true);
534        expected.insert(id.clone());
535        assert_eq!(state.checked(), &expected);
536
537        assert_eq!(state.toggle_check(id.clone()), true);
538        expected.remove(&id);
539        assert_eq!(state.checked(), &expected);
540    }
541
542    #[test]
543    fn test_toggle_selected_check() {
544        let mut state: CheckTreeState<&str> = CheckTreeState::default();
545        let mut expected: HashSet<Vec<&str>> = HashSet::default();
546
547        assert_eq!(state.toggle_check_selected(), false);
548
549        let id = vec!["a"];
550        state.select(id.clone());
551
552        assert_eq!(state.toggle_check_selected(), true);
553        expected.insert(id.clone());
554        assert_eq!(state.checked(), &expected);
555
556        assert_eq!(state.toggle_check_selected(), true);
557        expected.remove(&id);
558        assert_eq!(state.checked(), &expected);
559    }
560}