From fdd2c5436ae5a98b45b845e856c458970ebbd2a4 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Wed, 1 May 2024 01:45:54 +0200 Subject: [PATCH] feat!: cache last tree structure to simplify events Things like key up/down dont require the items anymore to be used. Instead a cached last state from last render is used. --- examples/example.rs | 8 ++-- src/flatten.rs | 34 ++--------------- src/lib.rs | 14 ++++--- src/tree_item.rs | 28 ++++++++++++++ src/tree_state.rs | 92 ++++++++++++++++++++++----------------------- 5 files changed, 88 insertions(+), 88 deletions(-) diff --git a/examples/example.rs b/examples/example.rs index 7cde724..16f9a63 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -142,11 +142,11 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> std::io::Res KeyCode::Char('\n' | ' ') => app.state.toggle_selected(), KeyCode::Left => app.state.key_left(), KeyCode::Right => app.state.key_right(), - KeyCode::Down => app.state.key_down(&app.items), - KeyCode::Up => app.state.key_up(&app.items), + KeyCode::Down => app.state.key_down(), + KeyCode::Up => app.state.key_up(), KeyCode::Esc => app.state.select(Vec::new()), - KeyCode::Home => app.state.select_first(&app.items), - KeyCode::End => app.state.select_last(&app.items), + KeyCode::Home => app.state.select_first(), + KeyCode::End => app.state.select_last(), KeyCode::PageDown => app.state.scroll_down(3), KeyCode::PageUp => app.state.scroll_up(3), _ => false, diff --git a/src/flatten.rs b/src/flatten.rs index 905ec18..9efe03e 100644 --- a/src/flatten.rs +++ b/src/flatten.rs @@ -56,35 +56,9 @@ where result } -#[cfg(test)] -fn get_example_tree_items() -> Vec> { - vec![ - TreeItem::new_leaf("a", "Alfa"), - TreeItem::new( - "b", - "Bravo", - vec![ - TreeItem::new_leaf("c", "Charlie"), - TreeItem::new( - "d", - "Delta", - vec![ - TreeItem::new_leaf("e", "Echo"), - TreeItem::new_leaf("f", "Foxtrot"), - ], - ) - .expect("all item identifiers are unique"), - TreeItem::new_leaf("g", "Golf"), - ], - ) - .expect("all item identifiers are unique"), - TreeItem::new_leaf("h", "Hotel"), - ] -} - #[test] fn get_opened_nothing_opened_is_top_level() { - let items = get_example_tree_items(); + let items = TreeItem::example(); let opened = HashSet::new(); let result = flatten(&opened, &items); let result_text = result @@ -96,7 +70,7 @@ fn get_opened_nothing_opened_is_top_level() { #[test] fn get_opened_wrong_opened_is_only_top_level() { - let items = get_example_tree_items(); + let items = TreeItem::example(); let mut opened = HashSet::new(); opened.insert(vec!["a"]); opened.insert(vec!["b", "d"]); @@ -110,7 +84,7 @@ fn get_opened_wrong_opened_is_only_top_level() { #[test] fn get_opened_one_is_opened() { - let items = get_example_tree_items(); + let items = TreeItem::example(); let mut opened = HashSet::new(); opened.insert(vec!["b"]); let result = flatten(&opened, &items); @@ -123,7 +97,7 @@ fn get_opened_one_is_opened() { #[test] fn get_opened_all_opened() { - let items = get_example_tree_items(); + let items = TreeItem::example(); let mut opened = HashSet::new(); opened.insert(vec!["b"]); opened.insert(vec!["b", "d"]); diff --git a/src/lib.rs b/src/lib.rs index b1ca916..ef96de3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,6 +191,7 @@ where } let visible = state.flatten(&self.items); + state.last_biggest_index = visible.len().saturating_sub(1); if visible.is_empty() { return; } @@ -206,7 +207,7 @@ where }; // Ensure last line is still visible - let mut start = state.offset.min(visible.len().saturating_sub(1)); + let mut start = state.offset.min(state.last_biggest_index); if let Some(ensure_index_in_view) = ensure_index_in_view { start = start.min(ensure_index_in_view); @@ -260,11 +261,8 @@ where let mut current_height = 0; let has_selection = !state.selected.is_empty(); #[allow(clippy::cast_possible_truncation)] - for flattened in visible.into_iter().skip(state.offset).take(end - start) { - let Flattened { - ref identifier, - item, - } = flattened; + for flattened in visible.iter().skip(state.offset).take(end - start) { + let Flattened { identifier, item } = flattened; let x = area.x; let y = area.y + current_height; @@ -324,6 +322,10 @@ where buf.set_style(area, self.highlight_style); } } + state.last_visible_identifiers = visible + .into_iter() + .map(|flattened| flattened.identifier) + .collect(); } } diff --git a/src/tree_item.rs b/src/tree_item.rs index abd3c85..c29f586 100644 --- a/src/tree_item.rs +++ b/src/tree_item.rs @@ -141,6 +141,34 @@ where } } +impl TreeItem<'static, &'static str> { + #[cfg(test)] + pub(crate) fn example() -> Vec { + vec![ + TreeItem::new_leaf("a", "Alfa"), + TreeItem::new( + "b", + "Bravo", + vec![ + TreeItem::new_leaf("c", "Charlie"), + TreeItem::new( + "d", + "Delta", + vec![ + TreeItem::new_leaf("e", "Echo"), + TreeItem::new_leaf("f", "Foxtrot"), + ], + ) + .expect("all item identifiers are unique"), + TreeItem::new_leaf("g", "Golf"), + ], + ) + .expect("all item identifiers are unique"), + TreeItem::new_leaf("h", "Hotel"), + ] + } +} + #[test] #[should_panic = "duplicate identifiers"] fn tree_item_new_errors_with_duplicate_identifiers() { diff --git a/src/tree_state.rs b/src/tree_state.rs index a3a4086..0f70c33 100644 --- a/src/tree_state.rs +++ b/src/tree_state.rs @@ -16,12 +16,14 @@ use crate::tree_item::TreeItem; /// /// let mut state = TreeState::::default(); /// ``` -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default)] pub struct TreeState { pub(super) offset: usize, pub(super) opened: HashSet>, pub(super) selected: Vec, pub(super) ensure_selected_in_view_on_next_render: bool, + pub(super) last_biggest_index: usize, + pub(super) last_visible_identifiers: Vec>, } impl TreeState @@ -38,6 +40,11 @@ where self.opened.iter().cloned().collect() } + #[must_use] + pub fn selected(&self) -> Vec { + self.selected.clone() + } + /// Get a flat list of all visible (= below open) [`TreeItem`]s with this `TreeState`. #[must_use] pub fn flatten<'a>( @@ -47,11 +54,6 @@ where flatten(&self.opened, items) } - #[must_use] - pub fn selected(&self) -> Vec { - self.selected.clone() - } - /// Selects the given identifier. /// /// Returns `true` when the selection changed. @@ -128,22 +130,24 @@ where /// Select the first node. /// /// Returns `true` when the selection changed. - pub fn select_first(&mut self, items: &[TreeItem]) -> bool { - let identifier = items + pub fn select_first(&mut self) -> bool { + let identifier = self + .last_visible_identifiers .first() - .map_or(Vec::new(), |item| vec![item.identifier.clone()]); + .cloned() + .unwrap_or_default(); self.select(identifier) } /// Select the last visible node. /// /// Returns `true` when the selection changed. - pub fn select_last(&mut self, items: &[TreeItem]) -> bool { - let visible = self.flatten(items); - let new_identifier = visible - .into_iter() + pub fn select_last(&mut self) -> bool { + let new_identifier = self + .last_visible_identifiers .last() - .map_or(Vec::new(), |flattened| flattened.identifier); + .cloned() + .unwrap_or_default(); self.select(new_identifier) } @@ -152,17 +156,13 @@ where /// Returns `true` when the selection changed. /// /// This can be useful for mouse clicks. - pub fn select_visible_index( - &mut self, - items: &[TreeItem], - new_index: usize, - ) -> bool { - let visible = self.flatten(items); - let new_index = new_index.min(visible.len().saturating_sub(1)); - let new_identifier = visible - .into_iter() - .nth(new_index) - .map_or(Vec::new(), |flattened| flattened.identifier); + pub fn select_visible_index(&mut self, new_index: usize) -> bool { + let new_index = new_index.min(self.last_biggest_index); + let new_identifier = self + .last_visible_identifiers + .get(new_index) + .cloned() + .unwrap_or_default(); self.select(new_identifier) } @@ -174,35 +174,27 @@ where /// /// ``` /// # use tui_tree_widget::TreeState; - /// # let items = vec![]; /// # type Identifier = usize; /// # let mut state = TreeState::::default(); /// // Move the selection one down - /// state.select_visible_relative(&items, |current| { + /// state.select_visible_relative(|current| { /// current.map_or(0, |current| current.saturating_add(1)) /// }); /// ``` /// /// For more examples take a look into the source code of [`key_up`](Self::key_up) or [`key_down`](Self::key_down). /// They are implemented with this method. - pub fn select_visible_relative( - &mut self, - items: &[TreeItem], - change_function: F, - ) -> bool + pub fn select_visible_relative(&mut self, change_function: F) -> bool where F: FnOnce(Option) -> usize, { - let visible = self.flatten(items); - let current_identifier = self.selected(); + let visible = &self.last_visible_identifiers; + let current_identifier = &self.selected; let current_index = visible .iter() - .position(|flattened| flattened.identifier == current_identifier); - let new_index = change_function(current_index).min(visible.len().saturating_sub(1)); - let new_identifier = visible - .into_iter() - .nth(new_index) - .map_or(Vec::new(), |flattened| flattened.identifier); + .position(|identifier| identifier == current_identifier); + let new_index = change_function(current_index).min(self.last_biggest_index); + let new_identifier = visible.get(new_index).cloned().unwrap_or_default(); self.select(new_identifier) } @@ -223,19 +215,23 @@ where /// Scroll the specified amount of lines down /// - /// In contrast to [`scroll_up()`](Self::scroll_up) this can not return whether the view position changed or not as the actual change is determined on render. - /// Always returns `true`. + /// Returns `true` when the scroll position changed. + /// Returns `false` when the scrolling has reached the last [`TreeItem`]. pub fn scroll_down(&mut self, lines: usize) -> bool { - self.offset = self.offset.saturating_add(lines); - true + let before = self.offset; + self.offset = self + .offset + .saturating_add(lines) + .min(self.last_biggest_index); + before != self.offset } /// Handles the up arrow key. /// Moves up in the current depth or to its parent. /// /// Returns `true` when the selection changed. - pub fn key_up(&mut self, items: &[TreeItem]) -> bool { - self.select_visible_relative(items, |current| { + pub fn key_up(&mut self) -> bool { + self.select_visible_relative(|current| { current.map_or(usize::MAX, |current| current.saturating_sub(1)) }) } @@ -244,8 +240,8 @@ where /// Moves down in the current depth or into a child node. /// /// Returns `true` when the selection changed. - pub fn key_down(&mut self, items: &[TreeItem]) -> bool { - self.select_visible_relative(items, |current| { + pub fn key_down(&mut self) -> bool { + self.select_visible_relative(|current| { current.map_or(0, |current| current.saturating_add(1)) }) }