Skip to main content

mecomp_tui/ui/widgets/tree/
state.rs

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