diff --git a/src/flatten.rs b/src/flatten.rs index 6896e88..6be16ff 100644 --- a/src/flatten.rs +++ b/src/flatten.rs @@ -1,13 +1,13 @@ use std::collections::HashSet; -use crate::TreeItem; +use crate::item::Item; -/// A flattened item of all visible [`TreeItem`s](TreeItem). +/// A flattened item of all visible [`TreeItem`s](Item). /// /// Generated via [`TreeState::flatten`](crate::TreeState::flatten). pub struct Flattened<'a, Identifier> { pub identifier: Vec, - pub item: &'a TreeItem<'a, Identifier>, + pub item: &'a Item<'a, Identifier>, } impl<'a, Identifier> Flattened<'a, Identifier> { @@ -21,7 +21,7 @@ impl<'a, Identifier> Flattened<'a, Identifier> { #[must_use] pub fn flatten<'a, Identifier>( opened: &HashSet>, - items: &'a [TreeItem<'a, Identifier>], + items: &'a [Item<'a, Identifier>], ) -> Vec> where Identifier: Clone + PartialEq + Eq + core::hash::Hash, @@ -32,7 +32,7 @@ where #[must_use] fn internal<'a, Identifier>( opened: &HashSet>, - items: &'a [TreeItem<'a, Identifier>], + items: &'a [Item<'a, Identifier>], current: &[Identifier], ) -> Vec> where @@ -57,28 +57,25 @@ where } #[cfg(test)] -fn get_example_tree_items() -> Vec> { +fn get_example_tree_items() -> Vec> { vec![ - TreeItem::new_leaf("a", "Alfa"), - TreeItem::new( + Item::new_leaf("a", "Alfa"), + Item::new( "b", "Bravo", vec![ - TreeItem::new_leaf("c", "Charlie"), - TreeItem::new( + Item::new_leaf("c", "Charlie"), + Item::new( "d", "Delta", - vec![ - TreeItem::new_leaf("e", "Echo"), - TreeItem::new_leaf("f", "Foxtrot"), - ], + vec![Item::new_leaf("e", "Echo"), Item::new_leaf("f", "Foxtrot")], ) .expect("all item identifiers are unique"), - TreeItem::new_leaf("g", "Golf"), + Item::new_leaf("g", "Golf"), ], ) .expect("all item identifiers are unique"), - TreeItem::new_leaf("h", "Hotel"), + Item::new_leaf("h", "Hotel"), ] } diff --git a/src/item.rs b/src/item.rs new file mode 100644 index 0000000..1c3c1e5 --- /dev/null +++ b/src/item.rs @@ -0,0 +1,163 @@ +use std::collections::HashSet; + +use ratatui::style::Style; +use ratatui::text::Text; + +/// One item inside a [`Tree`](crate::Tree). +/// +/// Can have zero or more `children`. +/// +/// # Identifier +/// +/// The generic argument `Identifier` is used to keep the state like the currently selected or opened [`TreeItem`s](Item) in the [`TreeState`](crate::TreeState). +/// +/// It needs to be unique among its siblings but can be used again on parent or child [`TreeItem`s](Item). +/// A common example would be a filename which has to be unique in its directory while it can exist in another. +/// +/// The `text` can be different from its `identifier`. +/// To repeat the filename analogy: File browsers sometimes hide file extensions. +/// The filename `main.rs` is the identifier while its shown as `main`. +/// Two files `main.rs` and `main.toml` can exist in the same directory and can both be displayed as `main` but their identifier is different. +/// +/// Just like every file in a file system can be uniquely identified with its file and directory names each [`TreeItem`](Item) in a [`Tree`](crate::Tree) can be with these identifiers. +/// As an example the following two identifiers describe the main file in a Rust cargo project: `vec!["src", "main.rs"]`. +/// +/// The identifier does not need to be a `String` and is therefore generic. +/// Until version 0.14 this crate used `usize` and indices. +/// This might still be perfect for your use case. +/// +/// # Example +/// +/// ``` +/// # use tui_tree_widget::TreeItem; +/// let a = TreeItem::new_leaf("l", "Leaf"); +/// let b = TreeItem::new("r", "Root", vec![a])?; +/// # Ok::<(), std::io::Error>(()) +/// ``` +#[derive(Debug, Clone)] +pub struct Item<'a, Identifier> { + pub(super) identifier: Identifier, + pub(super) text: Text<'a>, + pub(super) style: Style, + pub(super) children: Vec>, +} + +impl<'a, Identifier> Item<'a, Identifier> +where + Identifier: Clone + PartialEq + Eq + core::hash::Hash, +{ + /// Create a new `TreeItem` without children. + #[must_use] + pub fn new_leaf(identifier: Identifier, text: T) -> Self + where + T: Into>, + { + Self { + identifier, + text: text.into(), + style: Style::new(), + children: Vec::new(), + } + } + + /// Create a new `TreeItem` with children. + /// + /// # Errors + /// + /// Errors when there are duplicate identifiers in the children. + pub fn new( + identifier: Identifier, + text: T, + children: Vec>, + ) -> std::io::Result + where + T: Into>, + { + let identifiers = children + .iter() + .map(|o| &o.identifier) + .collect::>(); + if identifiers.len() != children.len() { + return Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + "The children contain duplicate identifiers", + )); + } + + Ok(Self { + identifier, + text: text.into(), + style: Style::new(), + children, + }) + } + + #[must_use] + pub fn children(&self) -> &[Item] { + &self.children + } + + /// Get a reference to a child by index. + #[must_use] + pub fn child(&self, index: usize) -> Option<&Self> { + self.children.get(index) + } + + /// Get a mutable reference to a child by index. + /// + /// When you choose to change the `identifier` the [`TreeState`](crate::TreeState) might not work as expected afterwards. + #[must_use] + pub fn child_mut(&mut self, index: usize) -> Option<&mut Self> { + self.children.get_mut(index) + } + + #[must_use] + pub fn height(&self) -> usize { + self.text.height() + } + + #[must_use] + pub const fn style(mut self, style: Style) -> Self { + self.style = style; + self + } + + /// Add a child to the `TreeItem`. + /// + /// # Errors + /// + /// Errors when the `identifier` of the `child` already exists in the children. + pub fn add_child(&mut self, child: Item<'a, Identifier>) -> std::io::Result<()> { + let existing = self + .children + .iter() + .map(|o| &o.identifier) + .collect::>(); + if existing.contains(&child.identifier) { + return Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + "identifier already exists in the children", + )); + } + + self.children.push(child); + Ok(()) + } +} + +#[test] +#[should_panic = "duplicate identifiers"] +fn tree_item_new_errors_with_duplicate_identifiers() { + let a = Item::new_leaf("same", "text"); + let b = a.clone(); + Item::new("root", "Root", vec![a, b]).unwrap(); +} + +#[test] +#[should_panic = "identifier already exists"] +fn tree_item_add_child_errors_with_duplicate_identifiers() { + let a = Item::new_leaf("same", "text"); + let b = a.clone(); + let mut root = Item::new("root", "Root", vec![a]).unwrap(); + root.add_child(b).unwrap(); +} diff --git a/src/lib.rs b/src/lib.rs index cfbd592..bc5c4e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,410 +12,18 @@ use std::collections::HashSet; use ratatui::buffer::Buffer; use ratatui::layout::{Corner, Rect}; use ratatui::style::Style; -use ratatui::text::Text; use ratatui::widgets::{Block, StatefulWidget, Widget}; use unicode_width::UnicodeWidthStr; mod flatten; mod identifier; +mod item; +mod state; -use crate::flatten::flatten; pub use crate::flatten::Flattened; pub use crate::identifier::get_without_leaf as get_identifier_without_leaf; - -/// Keeps the state of what is currently selected and what was opened in a [`Tree`]. -/// -/// The generic argument `Identifier` is used to keep the state like the currently selected or opened [`TreeItem`s](TreeItem) in the [`TreeState`]. -/// For more information see [`TreeItem`]. -/// -/// # Example -/// -/// ``` -/// # use tui_tree_widget::TreeState; -/// type Identifier = usize; -/// -/// let mut state = TreeState::::default(); -/// ``` -#[derive(Debug, Default, Clone)] -pub struct TreeState { - offset: usize, - opened: HashSet>, - selected: Vec, - ensure_selected_in_view_on_next_render: bool, -} - -impl TreeState -where - Identifier: Clone + PartialEq + Eq + core::hash::Hash, -{ - #[must_use] - pub const fn get_offset(&self) -> usize { - self.offset - } - - #[must_use] - pub fn get_all_opened(&self) -> Vec> { - self.opened.iter().cloned().collect() - } - - /// Get a flat list of all visible [`TreeItem`s](TreeItem) with this `TreeState`. - #[must_use] - pub fn flatten<'a>( - &self, - items: &'a [TreeItem<'a, Identifier>], - ) -> Vec> { - flatten(&self.opened, items) - } - - #[must_use] - pub fn selected(&self) -> Vec { - self.selected.clone() - } - - /// Selects the given identifier. - /// - /// Returns `true` when the selection changed. - /// - /// Clear the selection by passing an empty identifier vector: - /// - /// ```rust - /// # use tui_tree_widget::TreeState; - /// # let mut state = TreeState::::default(); - /// state.select(Vec::new()); - /// ``` - pub fn select(&mut self, identifier: Vec) -> bool { - let changed = self.selected != identifier; - self.selected = identifier; - self.ensure_selected_in_view_on_next_render = true; - changed - } - - /// Open a tree node. - /// Returns `true` if the node was closed and has been opened. - /// Returns `false` if the node was already open. - pub fn open(&mut self, identifier: Vec) -> bool { - if identifier.is_empty() { - false - } else { - self.opened.insert(identifier) - } - } - - /// Close a tree node. - /// Returns `true` if the node was open and has been closed. - /// Returns `false` if the node was already closed. - pub fn close(&mut self, identifier: &[Identifier]) -> bool { - self.opened.remove(identifier) - } - - /// Toggles a tree node. - /// If the node is in opened then it calls `close()`. Otherwise it calls `open()`. - pub fn toggle(&mut self, identifier: Vec) { - if self.opened.contains(&identifier) { - self.close(&identifier); - } else { - self.open(identifier); - } - } - - /// Toggles the currently selected tree node. - /// See also [`toggle`](TreeState::toggle) - pub fn toggle_selected(&mut self) { - self.toggle(self.selected()); - self.ensure_selected_in_view_on_next_render = true; - } - - pub fn close_all(&mut self) { - self.opened.clear(); - } - - /// Select the first node. - /// - /// Returns `true` when the selection changed. - pub fn select_first(&mut self, items: &[TreeItem]) -> bool { - let identifier = items - .first() - .map(|o| vec![o.identifier.clone()]) - .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 - .last() - .map(|o| o.identifier.clone()) - .unwrap_or_default(); - self.select(new_identifier) - } - - /// Select the node visible on the given index. - /// - /// 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 - .get(new_index) - .map(|o| o.identifier.clone()) - .unwrap_or_default(); - self.select(new_identifier) - } - - /// Move the current selection with the direction/amount by the given function. - /// - /// Returns `true` when the selection changed. - /// - /// # Example - /// - /// ``` - /// # 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| { - /// current.map_or(0, |current| current.saturating_add(1)) - /// }); - /// ``` - /// - /// For more examples take a look into the source code of [`TreeState::key_up`] or [`TreeState::key_down`]. - /// They are implemented with this method. - pub fn select_visible_relative(&mut self, items: &[TreeItem], f: F) -> bool - where - F: FnOnce(Option) -> usize, - { - let visible = self.flatten(items); - let current_identifier = self.selected(); - let current_index = visible - .iter() - .position(|o| o.identifier == current_identifier); - let new_index = f(current_index).min(visible.len().saturating_sub(1)); - let new_identifier = visible - .get(new_index) - .map(|o| o.identifier.clone()) - .unwrap_or_default(); - self.select(new_identifier) - } - - /// Ensure the selected [`TreeItem`] is visible on next render - pub fn scroll_selected_into_view(&mut self) { - self.ensure_selected_in_view_on_next_render = true; - } - - /// Scroll the specified amount of lines up - pub fn scroll_up(&mut self, lines: usize) { - self.offset = self.offset.saturating_sub(lines); - } - - /// Scroll the specified amount of lines down - pub fn scroll_down(&mut self, lines: usize) { - self.offset = self.offset.saturating_add(lines); - } - - /// Handles the up arrow key. - /// Moves up in the current depth or to its parent. - pub fn key_up(&mut self, items: &[TreeItem]) { - self.select_visible_relative(items, |current| { - current.map_or(usize::MAX, |current| current.saturating_sub(1)) - }); - } - - /// Handles the down arrow key. - /// Moves down in the current depth or into a child node. - pub fn key_down(&mut self, items: &[TreeItem]) { - self.select_visible_relative(items, |current| { - current.map_or(0, |current| current.saturating_add(1)) - }); - } - - /// Handles the left arrow key. - /// Closes the currently selected or moves to its parent. - pub fn key_left(&mut self) { - // Reimplement self.close because of multiple different borrows - let changed = self.opened.remove(&self.selected); - if !changed { - // Select the parent by removing the leaf from selection - self.selected.pop(); - } - self.ensure_selected_in_view_on_next_render = true; - } - - /// Handles the right arrow key. - /// Opens the currently selected. - pub fn key_right(&mut self) { - self.open(self.selected()); - self.ensure_selected_in_view_on_next_render = true; - } -} - -/// One item inside a [`Tree`]. -/// -/// Can have zero or more `children`. -/// -/// # Identifier -/// -/// The generic argument `Identifier` is used to keep the state like the currently selected or opened [`TreeItem`s](TreeItem) in the [`TreeState`]. -/// -/// It needs to be unique among its siblings but can be used again on parent or child [`TreeItem`s](TreeItem). -/// A common example would be a filename which has to be unique in its directory while it can exist in another. -/// -/// The `text` can be different from its `identifier`. -/// To repeat the filename analogy: File browsers sometimes hide file extensions. -/// The filename `main.rs` is the identifier while its shown as `main`. -/// Two files `main.rs` and `main.toml` can exist in the same directory and can both be displayed as `main` but their identifier is different. -/// -/// Just like every file in a file system can be uniquely identified with its file and directory names each [`TreeItem`] in a [`Tree`] can be with these identifiers. -/// As an example the following two identifiers describe the main file in a Rust cargo project: `vec!["src", "main.rs"]`. -/// -/// The identifier does not need to be a `String` and is therefore generic. -/// Until version 0.14 this crate used `usize` and indices. -/// This might still be perfect for your use case. -/// -/// # Example -/// -/// ``` -/// # use tui_tree_widget::TreeItem; -/// let a = TreeItem::new_leaf("l", "Leaf"); -/// let b = TreeItem::new("r", "Root", vec![a])?; -/// # Ok::<(), std::io::Error>(()) -/// ``` -#[derive(Debug, Clone)] -pub struct TreeItem<'a, Identifier> { - identifier: Identifier, - text: Text<'a>, - style: Style, - children: Vec>, -} - -impl<'a, Identifier> TreeItem<'a, Identifier> -where - Identifier: Clone + PartialEq + Eq + core::hash::Hash, -{ - /// Create a new `TreeItem` without children. - #[must_use] - pub fn new_leaf(identifier: Identifier, text: T) -> Self - where - T: Into>, - { - Self { - identifier, - text: text.into(), - style: Style::new(), - children: Vec::new(), - } - } - - /// Create a new `TreeItem` with children. - /// - /// # Errors - /// - /// Errors when there are duplicate identifiers in the children. - pub fn new( - identifier: Identifier, - text: T, - children: Vec>, - ) -> std::io::Result - where - T: Into>, - { - let identifiers = children - .iter() - .map(|o| &o.identifier) - .collect::>(); - if identifiers.len() != children.len() { - return Err(std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - "The children contain duplicate identifiers", - )); - } - - Ok(Self { - identifier, - text: text.into(), - style: Style::new(), - children, - }) - } - - #[must_use] - pub fn children(&self) -> &[TreeItem] { - &self.children - } - - /// Get a reference to a child by index. - #[must_use] - pub fn child(&self, index: usize) -> Option<&Self> { - self.children.get(index) - } - - /// Get a mutable reference to a child by index. - /// - /// When you choose to change the `identifier` the [`TreeState`] might not work as expected afterwards. - #[must_use] - pub fn child_mut(&mut self, index: usize) -> Option<&mut Self> { - self.children.get_mut(index) - } - - #[must_use] - pub fn height(&self) -> usize { - self.text.height() - } - - #[must_use] - pub const fn style(mut self, style: Style) -> Self { - self.style = style; - self - } - - /// Add a child to the `TreeItem`. - /// - /// # Errors - /// - /// Errors when the `identifier` of the `child` already exists in the children. - pub fn add_child(&mut self, child: TreeItem<'a, Identifier>) -> std::io::Result<()> { - let existing = self - .children - .iter() - .map(|o| &o.identifier) - .collect::>(); - if existing.contains(&child.identifier) { - return Err(std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - "identifier already exists in the children", - )); - } - - self.children.push(child); - Ok(()) - } -} - -#[test] -#[should_panic = "duplicate identifiers"] -fn tree_item_new_errors_with_duplicate_identifiers() { - let a = TreeItem::new_leaf("same", "text"); - let b = a.clone(); - TreeItem::new("root", "Root", vec![a, b]).unwrap(); -} - -#[test] -#[should_panic = "identifier already exists"] -fn tree_item_add_child_errors_with_duplicate_identifiers() { - let a = TreeItem::new_leaf("same", "text"); - let b = a.clone(); - let mut root = TreeItem::new("root", "Root", vec![a]).unwrap(); - root.add_child(b).unwrap(); -} +pub use crate::item::Item as TreeItem; +pub use crate::state::State as TreeState; /// A `Tree` which can be rendered. /// diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..0a18256 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,232 @@ +use std::collections::HashSet; + +use crate::flatten::{flatten, Flattened}; +use crate::item::Item; + +/// Keeps the state of what is currently selected and what was opened in a [`Tree`](crate::Tree). +/// +/// The generic argument `Identifier` is used to keep the state like the currently selected or opened [`TreeItem`s](Item) in the [`TreeState`](State). +/// For more information see [`TreeItem`](Item). +/// +/// # Example +/// +/// ``` +/// # use tui_tree_widget::TreeState; +/// type Identifier = usize; +/// +/// let mut state = TreeState::::default(); +/// ``` +#[derive(Debug, Default, Clone)] +pub struct State { + pub(super) offset: usize, + pub(super) opened: HashSet>, + pub(super) selected: Vec, + pub(super) ensure_selected_in_view_on_next_render: bool, +} + +impl State +where + Identifier: Clone + PartialEq + Eq + core::hash::Hash, +{ + #[must_use] + pub const fn get_offset(&self) -> usize { + self.offset + } + + #[must_use] + pub fn get_all_opened(&self) -> Vec> { + self.opened.iter().cloned().collect() + } + + /// Get a flat list of all visible (= below open) [`TreeItem`s](Item) with this `TreeState`. + #[must_use] + pub fn flatten<'a>(&self, items: &'a [Item<'a, Identifier>]) -> Vec> { + flatten(&self.opened, items) + } + + #[must_use] + pub fn selected(&self) -> Vec { + self.selected.clone() + } + + /// Selects the given identifier. + /// + /// Returns `true` when the selection changed. + /// + /// Clear the selection by passing an empty identifier vector: + /// + /// ```rust + /// # use tui_tree_widget::TreeState; + /// # let mut state = TreeState::::default(); + /// state.select(Vec::new()); + /// ``` + pub fn select(&mut self, identifier: Vec) -> bool { + let changed = self.selected != identifier; + self.selected = identifier; + self.ensure_selected_in_view_on_next_render = true; + changed + } + + /// Open a tree node. + /// Returns `true` if the node was closed and has been opened. + /// Returns `false` if the node was already open. + pub fn open(&mut self, identifier: Vec) -> bool { + if identifier.is_empty() { + false + } else { + self.opened.insert(identifier) + } + } + + /// Close a tree node. + /// Returns `true` if the node was open and has been closed. + /// Returns `false` if the node was already closed. + pub fn close(&mut self, identifier: &[Identifier]) -> bool { + self.opened.remove(identifier) + } + + /// Toggles a tree node. + /// If the node is in opened then it calls [`close`](State::close). Otherwise it calls [`open`](State::open). + pub fn toggle(&mut self, identifier: Vec) { + if self.opened.contains(&identifier) { + self.close(&identifier); + } else { + self.open(identifier); + } + } + + /// Toggles the currently selected tree node. + /// See also [`toggle`](State::toggle) + pub fn toggle_selected(&mut self) { + self.toggle(self.selected()); + self.ensure_selected_in_view_on_next_render = true; + } + + pub fn close_all(&mut self) { + self.opened.clear(); + } + + /// Select the first node. + /// + /// Returns `true` when the selection changed. + pub fn select_first(&mut self, items: &[Item]) -> bool { + let identifier = items + .first() + .map(|o| vec![o.identifier.clone()]) + .unwrap_or_default(); + self.select(identifier) + } + + /// Select the last visible node. + /// + /// Returns `true` when the selection changed. + pub fn select_last(&mut self, items: &[Item]) -> bool { + let visible = self.flatten(items); + let new_identifier = visible + .last() + .map(|o| o.identifier.clone()) + .unwrap_or_default(); + self.select(new_identifier) + } + + /// Select the node visible on the given index. + /// + /// Returns `true` when the selection changed. + /// + /// This can be useful for mouse clicks. + pub fn select_visible_index(&mut self, items: &[Item], new_index: usize) -> bool { + let visible = self.flatten(items); + let new_index = new_index.min(visible.len().saturating_sub(1)); + let new_identifier = visible + .get(new_index) + .map(|o| o.identifier.clone()) + .unwrap_or_default(); + self.select(new_identifier) + } + + /// Move the current selection with the direction/amount by the given function. + /// + /// Returns `true` when the selection changed. + /// + /// # Example + /// + /// ``` + /// # 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| { + /// current.map_or(0, |current| current.saturating_add(1)) + /// }); + /// ``` + /// + /// For more examples take a look into the source code of [`key_up`](State::key_up) or [`key_down`](State::key_down). + /// They are implemented with this method. + pub fn select_visible_relative(&mut self, items: &[Item], f: F) -> bool + where + F: FnOnce(Option) -> usize, + { + let visible = self.flatten(items); + let current_identifier = self.selected(); + let current_index = visible + .iter() + .position(|o| o.identifier == current_identifier); + let new_index = f(current_index).min(visible.len().saturating_sub(1)); + let new_identifier = visible + .get(new_index) + .map(|o| o.identifier.clone()) + .unwrap_or_default(); + self.select(new_identifier) + } + + /// Ensure the selected [`TreeItem`](Item) is visible on next render + pub fn scroll_selected_into_view(&mut self) { + self.ensure_selected_in_view_on_next_render = true; + } + + /// Scroll the specified amount of lines up + pub fn scroll_up(&mut self, lines: usize) { + self.offset = self.offset.saturating_sub(lines); + } + + /// Scroll the specified amount of lines down + pub fn scroll_down(&mut self, lines: usize) { + self.offset = self.offset.saturating_add(lines); + } + + /// Handles the up arrow key. + /// Moves up in the current depth or to its parent. + pub fn key_up(&mut self, items: &[Item]) { + self.select_visible_relative(items, |current| { + current.map_or(usize::MAX, |current| current.saturating_sub(1)) + }); + } + + /// Handles the down arrow key. + /// Moves down in the current depth or into a child node. + pub fn key_down(&mut self, items: &[Item]) { + self.select_visible_relative(items, |current| { + current.map_or(0, |current| current.saturating_add(1)) + }); + } + + /// Handles the left arrow key. + /// Closes the currently selected or moves to its parent. + pub fn key_left(&mut self) { + // Reimplement self.close because of multiple different borrows + let changed = self.opened.remove(&self.selected); + if !changed { + // Select the parent by removing the leaf from selection + self.selected.pop(); + } + self.ensure_selected_in_view_on_next_render = true; + } + + /// Handles the right arrow key. + /// Opens the currently selected. + pub fn key_right(&mut self) { + self.open(self.selected()); + self.ensure_selected_in_view_on_next_render = true; + } +}