From b914819283b53e6792970074f2b0978fb98168f2 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Mon, 30 Oct 2023 13:18:58 +0100 Subject: [PATCH] feat: generic identifier (#27) --- examples/example.rs | 23 +++-- examples/util/mod.rs | 8 +- src/flatten.rs | 96 +++++++++---------- src/identifier.rs | 23 ++--- src/lib.rs | 223 ++++++++++++++++++++++++++++++++++--------- 5 files changed, 253 insertions(+), 120 deletions(-) diff --git a/examples/example.rs b/examples/example.rs index b9672b9..d4a14e5 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -25,16 +25,26 @@ impl<'a> App<'a> { fn new() -> Self { Self { tree: StatefulTree::with_items(vec![ - TreeItem::new_leaf("a"), + TreeItem::new_leaf("a", "Alfa"), TreeItem::new( "b", + "Bravo", vec![ - TreeItem::new_leaf("c"), - TreeItem::new("d", vec![TreeItem::new_leaf("e"), TreeItem::new_leaf("f")]), - TreeItem::new_leaf("g"), + 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"), ], - ), - TreeItem::new_leaf("h"), + ) + .expect("all item identifiers are unique"), + TreeItem::new_leaf("h", "Hotel"), ]), } } @@ -74,6 +84,7 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( let area = f.size(); let items = Tree::new(app.tree.items.clone()) + .expect("all item identifiers are unique") .block( Block::new() .borders(Borders::ALL) diff --git a/examples/util/mod.rs b/examples/util/mod.rs index d022fe2..33a4a95 100644 --- a/examples/util/mod.rs +++ b/examples/util/mod.rs @@ -1,8 +1,8 @@ use tui_tree_widget::{TreeItem, TreeState}; pub struct StatefulTree<'a> { - pub state: TreeState, - pub items: Vec>, + pub state: TreeState<&'static str>, + pub items: Vec>, } impl<'a> StatefulTree<'a> { @@ -14,7 +14,7 @@ impl<'a> StatefulTree<'a> { } } - pub fn with_items(items: Vec>) -> Self { + pub fn with_items(items: Vec>) -> Self { Self { state: TreeState::default(), items, @@ -22,7 +22,7 @@ impl<'a> StatefulTree<'a> { } pub fn first(&mut self) { - self.state.select_first(); + self.state.select_first(&self.items); } pub fn last(&mut self) { diff --git a/src/flatten.rs b/src/flatten.rs index 0184677..959afed 100644 --- a/src/flatten.rs +++ b/src/flatten.rs @@ -1,15 +1,14 @@ -use crate::identifier::{TreeIdentifier, TreeIdentifierVec}; use crate::TreeItem; /// A flattened item of all visible [`TreeItem`s](TreeItem). /// /// Generated via [`flatten`]. -pub struct Flattened<'a> { - pub identifier: Vec, - pub item: &'a TreeItem<'a>, +pub struct Flattened<'a, Identifier> { + pub identifier: Vec, + pub item: &'a TreeItem<'a, Identifier>, } -impl<'a> Flattened<'a> { +impl<'a, Identifier> Flattened<'a, Identifier> { #[must_use] pub fn depth(&self) -> usize { self.identifier.len() - 1 @@ -18,21 +17,30 @@ impl<'a> Flattened<'a> { /// Get a flat list of all visible [`TreeItem`s](TreeItem). #[must_use] -pub fn flatten<'a>(opened: &[TreeIdentifierVec], items: &'a [TreeItem<'a>]) -> Vec> { +pub fn flatten<'a, Identifier>( + opened: &[Vec], + items: &'a [TreeItem<'a, Identifier>], +) -> Vec> +where + Identifier: Clone + PartialEq, +{ internal(opened, items, &[]) } #[must_use] -fn internal<'a>( - opened: &[TreeIdentifierVec], - items: &'a [TreeItem<'a>], - current: TreeIdentifier, -) -> Vec> { +fn internal<'a, Identifier>( + opened: &[Vec], + items: &'a [TreeItem<'a, Identifier>], + current: &[Identifier], +) -> Vec> +where + Identifier: Clone + PartialEq, +{ let mut result = Vec::new(); - for (index, item) in items.iter().enumerate() { + for item in items { let mut child_identifier = current.to_vec(); - child_identifier.push(index); + child_identifier.push(item.identifier.clone()); result.push(Flattened { item, @@ -49,30 +57,28 @@ fn internal<'a>( } #[cfg(test)] -fn get_naive_string_from_text(text: &ratatui::text::Text<'_>) -> String { - text.lines - .first() - .unwrap() - .spans - .first() - .unwrap() - .content - .to_string() -} - -#[cfg(test)] -fn get_example_tree_items() -> Vec> { +fn get_example_tree_items() -> Vec> { vec![ - TreeItem::new_leaf("a"), + TreeItem::new_leaf("a", "Alfa"), TreeItem::new( "b", + "Bravo", vec![ - TreeItem::new_leaf("c"), - TreeItem::new("d", vec![TreeItem::new_leaf("e"), TreeItem::new_leaf("f")]), - TreeItem::new_leaf("g"), + 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"), ], - ), - TreeItem::new_leaf("h"), + ) + .expect("all item identifiers are unique"), + TreeItem::new_leaf("h", "Hotel"), ] } @@ -80,45 +86,33 @@ fn get_example_tree_items() -> Vec> { fn get_opened_nothing_opened_is_top_level() { let items = get_example_tree_items(); let result = flatten(&[], &items); - let result_text = result - .iter() - .map(|o| get_naive_string_from_text(&o.item.text)) - .collect::>(); + let result_text = result.iter().map(|o| o.item.identifier).collect::>(); assert_eq!(result_text, ["a", "b", "h"]); } #[test] fn get_opened_wrong_opened_is_only_top_level() { let items = get_example_tree_items(); - let opened = [vec![0], vec![1, 1]]; + let opened = [vec!["a"], vec!["b", "d"]]; let result = flatten(&opened, &items); - let result_text = result - .iter() - .map(|o| get_naive_string_from_text(&o.item.text)) - .collect::>(); + let result_text = result.iter().map(|o| o.item.identifier).collect::>(); assert_eq!(result_text, ["a", "b", "h"]); } #[test] fn get_opened_one_is_opened() { let items = get_example_tree_items(); - let opened = [vec![1]]; + let opened = [vec!["b"]]; let result = flatten(&opened, &items); - let result_text = result - .iter() - .map(|o| get_naive_string_from_text(&o.item.text)) - .collect::>(); + let result_text = result.iter().map(|o| o.item.identifier).collect::>(); assert_eq!(result_text, ["a", "b", "c", "d", "g", "h"]); } #[test] fn get_opened_all_opened() { let items = get_example_tree_items(); - let opened = [vec![1], vec![1, 1]]; + let opened = [vec!["b"], vec!["b", "d"]]; let result = flatten(&opened, &items); - let result_text = result - .iter() - .map(|o| get_naive_string_from_text(&o.item.text)) - .collect::>(); + let result_text = result.iter().map(|o| o.item.identifier).collect::>(); assert_eq!(result_text, ["a", "b", "c", "d", "e", "f", "g", "h"]); } diff --git a/src/identifier.rs b/src/identifier.rs index b3ea9c6..860a8c1 100644 --- a/src/identifier.rs +++ b/src/identifier.rs @@ -1,11 +1,4 @@ -#![allow(clippy::module_name_repetitions)] - -/// Reference to a [`TreeItem`](crate::TreeItem) in a [`Tree`](crate::Tree) -pub type TreeIdentifier<'a> = &'a [usize]; -/// Reference to a [`TreeItem`](crate::TreeItem) in a [`Tree`](crate::Tree) -pub type TreeIdentifierVec = Vec; - -/// Split a [`TreeIdentifier`] into its branch and leaf. +/// Split an `Identifier` into its branch and leaf. /// /// # Examples /// @@ -13,20 +6,22 @@ pub type TreeIdentifierVec = Vec; /// # use tui_tree_widget::get_identifier_without_leaf; /// let (branch, leaf) = get_identifier_without_leaf(&[2, 4, 6]); /// assert_eq!(branch, [2, 4]); -/// assert_eq!(leaf, Some(6)); +/// assert_eq!(leaf, Some(&6)); /// /// let (branch, leaf) = get_identifier_without_leaf(&[2]); /// assert_eq!(branch, []); -/// assert_eq!(leaf, Some(2)); +/// assert_eq!(leaf, Some(&2)); /// -/// let (branch, leaf) = get_identifier_without_leaf(&[]); +/// let (branch, leaf) = get_identifier_without_leaf::(&[]); /// assert_eq!(branch, []); /// assert_eq!(leaf, None); /// ``` #[must_use] -pub const fn get_without_leaf(identifier: TreeIdentifier) -> (TreeIdentifier, Option) { +pub const fn get_without_leaf( + identifier: &[Identifier], +) -> (&[Identifier], Option<&Identifier>) { match identifier { - [branch @ .., leaf] => (branch, Some(*leaf)), - [] => (&[] as &[usize], None), + [branch @ .., leaf] => (branch, Some(leaf)), + [] => (&[] as &[Identifier], None), } } diff --git a/src/lib.rs b/src/lib.rs index ca6a5d2..dfe7ba9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,42 +20,48 @@ mod flatten; mod identifier; pub use crate::flatten::{flatten, Flattened}; -pub use crate::identifier::{ - get_without_leaf as get_identifier_without_leaf, TreeIdentifier, TreeIdentifierVec, -}; +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; -/// let mut state = TreeState::default(); +/// type Identifier = usize; +/// +/// let mut state = TreeState::::default(); /// ``` #[derive(Debug, Default, Clone)] -pub struct TreeState { +pub struct TreeState { offset: usize, - opened: HashSet, - selected: TreeIdentifierVec, + opened: HashSet>, + selected: Vec, } -impl TreeState { +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 { + pub fn get_all_opened(&self) -> Vec> { self.opened.iter().cloned().collect() } #[must_use] - pub fn selected(&self) -> Vec { + pub fn selected(&self) -> Vec { self.selected.clone() } - pub fn select(&mut self, identifier: Vec) { + pub fn select(&mut self, identifier: Vec) { self.selected = identifier; // TODO: ListState does this. Is this relevant? @@ -67,7 +73,7 @@ impl TreeState { /// 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: TreeIdentifierVec) -> bool { + pub fn open(&mut self, identifier: Vec) -> bool { if identifier.is_empty() { false } else { @@ -78,13 +84,13 @@ impl TreeState { /// 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: TreeIdentifier) -> bool { + 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: TreeIdentifierVec) { + pub fn toggle(&mut self, identifier: Vec) { if self.opened.contains(&identifier) { self.close(&identifier); } else { @@ -103,12 +109,16 @@ impl TreeState { } /// Select the first node. - pub fn select_first(&mut self) { - self.select(vec![0]); + pub fn select_first(&mut self, items: &[TreeItem]) { + let identifier = items + .first() + .map(|o| vec![o.identifier.clone()]) + .unwrap_or_default(); + self.select(identifier); } /// Select the last visible node. - pub fn select_last(&mut self, items: &[TreeItem]) { + pub fn select_last(&mut self, items: &[TreeItem]) { let visible = flatten(&self.get_all_opened(), items); let new_identifier = visible .last() @@ -122,7 +132,11 @@ impl TreeState { /// 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 { + pub fn select_visible_index( + &mut self, + items: &[TreeItem], + new_index: usize, + ) -> bool { let current_identifier = self.selected(); let visible = flatten(&self.get_all_opened(), items); let new_index = new_index.min(visible.len().saturating_sub(1)); @@ -144,7 +158,8 @@ impl TreeState { /// ``` /// # use tui_tree_widget::TreeState; /// # let items = vec![]; - /// # let mut state = TreeState::default(); + /// # 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)) @@ -153,7 +168,7 @@ impl TreeState { /// /// 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 + pub fn select_visible_relative(&mut self, items: &[TreeItem], f: F) -> bool where F: FnOnce(Option) -> usize, { @@ -174,7 +189,7 @@ impl TreeState { /// Handles the up arrow key. /// Moves up in the current depth or to its parent. - pub fn key_up(&mut self, items: &[TreeItem]) { + pub fn key_up(&mut self, items: &[TreeItem]) { self.select_visible_relative(items, |current| { current.map_or(usize::MAX, |current| current.saturating_sub(1)) }); @@ -182,7 +197,7 @@ impl TreeState { /// Handles the down arrow key. /// Moves down in the current depth or into a child node. - pub fn key_down(&mut self, items: &[TreeItem]) { + pub fn key_down(&mut self, items: &[TreeItem]) { self.select_visible_relative(items, |current| { current.map_or(0, |current| current.saturating_add(1)) }); @@ -209,55 +224,105 @@ impl TreeState { /// /// 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("leaf"); -/// let b = TreeItem::new("root", vec![a]); +/// 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> { +pub struct TreeItem<'a, Identifier> { + identifier: Identifier, text: Text<'a>, style: Style, - children: Vec>, + children: Vec>, } -impl<'a> TreeItem<'a> { +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(text: T) -> Self + pub fn new_leaf(identifier: Identifier, text: T) -> Self where T: Into>, { Self { + identifier, text: text.into(), style: Style::new(), children: Vec::new(), } } - #[must_use] - pub fn new(text: T, children: Vec>) -> Self + /// 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>, { - Self { + 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] { + 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) @@ -274,13 +339,51 @@ impl<'a> TreeItem<'a> { self } - pub fn add_child(&mut self, child: TreeItem<'a>) { + /// 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(); +} + /// A `Tree` which can be rendered. /// +/// 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 /// /// ``` @@ -291,13 +394,14 @@ impl<'a> TreeItem<'a> { /// # let mut terminal = Terminal::new(TestBackend::new(32, 32)).unwrap(); /// let mut state = TreeState::default(); /// -/// let item = TreeItem::new_leaf("leaf"); +/// let item = TreeItem::new_leaf("l", "leaf"); /// let items = vec![item]; /// /// terminal.draw(|f| { /// let area = f.size(); /// /// let tree_widget = Tree::new(items) +/// .expect("all item identifiers are unique") /// .block(Block::new().borders(Borders::ALL).title("Tree Widget")); /// /// f.render_stateful_widget(tree_widget, area, &mut state); @@ -305,8 +409,8 @@ impl<'a> TreeItem<'a> { /// # Ok::<(), std::io::Error>(()) /// ``` #[derive(Debug, Clone)] -pub struct Tree<'a> { - items: Vec>, +pub struct Tree<'a, Identifier> { + items: Vec>, block: Option>, start_corner: Corner, @@ -326,10 +430,25 @@ pub struct Tree<'a> { node_no_children_symbol: &'a str, } -impl<'a> Tree<'a> { - #[must_use] - pub const fn new(items: Vec>) -> Self { - Self { +impl<'a, Identifier> Tree<'a, Identifier> +where + Identifier: Clone + PartialEq + Eq + core::hash::Hash, +{ + /// Create a new `Tree`. + /// + /// # Errors + /// + /// Errors when there are duplicate identifiers in the children. + pub fn new(items: Vec>) -> std::io::Result { + let identifiers = items.iter().map(|o| &o.identifier).collect::>(); + if identifiers.len() != items.len() { + return Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + "The items contain duplicate identifiers", + )); + } + + Ok(Self { items, block: None, start_corner: Corner::TopLeft, @@ -339,7 +458,7 @@ impl<'a> Tree<'a> { node_closed_symbol: "\u{25b6} ", // Arrow to right node_open_symbol: "\u{25bc} ", // Arrow down node_no_children_symbol: " ", - } + }) } #[allow(clippy::missing_const_for_fn)] @@ -392,8 +511,19 @@ impl<'a> Tree<'a> { } } -impl<'a> StatefulWidget for Tree<'a> { - type State = TreeState; +#[test] +#[should_panic = "duplicate identifiers"] +fn tree_new_errors_with_duplicate_identifiers() { + let a = TreeItem::new_leaf("same", "text"); + let b = a.clone(); + Tree::new(vec![a, b]).unwrap(); +} + +impl<'a, Identifier> StatefulWidget for Tree<'a, Identifier> +where + Identifier: Clone + PartialEq + Eq + core::hash::Hash, +{ + type State = TreeState; #[allow(clippy::too_many_lines)] fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { @@ -522,7 +652,10 @@ impl<'a> StatefulWidget for Tree<'a> { } } -impl<'a> Widget for Tree<'a> { +impl<'a, Identifier> Widget for Tree<'a, Identifier> +where + Identifier: Clone + Default + Eq + core::hash::Hash, +{ fn render(self, area: Rect, buf: &mut Buffer) { let mut state = TreeState::default(); StatefulWidget::render(self, area, buf, &mut state);