From 735f111866335353ab43112b8c26813376ea3a49 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 13 Nov 2024 13:06:15 -0700 Subject: [PATCH] feat: Changed the tree to accept any type that implements Into so I can use the tree to fully store structs in the TUI as well as listing them --- examples/example.rs | 79 ++++++++++++++++++++++++++------------------- src/flatten.rs | 21 ++++++++---- src/lib.rs | 25 ++++++++------ src/tree_item.rs | 49 ++++++++++++++-------------- src/tree_state.rs | 10 ++++-- 5 files changed, 109 insertions(+), 75 deletions(-) diff --git a/examples/example.rs b/examples/example.rs index 8da9a87..4d2a1a4 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -12,7 +12,7 @@ use tui_tree_widget::{Tree, TreeItem, TreeState}; #[must_use] struct App { state: TreeState<&'static str>, - items: Vec>, + items: Vec>, } impl App { @@ -20,68 +20,86 @@ impl App { Self { state: TreeState::default(), items: vec![ - TreeItem::new_leaf("a", "Alfa"), + TreeItem::new_leaf("a", "Alfa".to_owned()), TreeItem::new( "b", - "Bravo", + "Bravo".to_owned(), vec![ - TreeItem::new_leaf("c", "Charlie"), + TreeItem::new_leaf("c", "Charlie".to_owned()), TreeItem::new( "d", - "Delta", + "Delta".to_owned(), vec![ - TreeItem::new_leaf("e", "Echo"), - TreeItem::new_leaf("f", "Foxtrot"), + TreeItem::new_leaf("e", "Echo".to_owned()), + TreeItem::new_leaf("f", "Foxtrot".to_owned()), ], ) .expect("all item identifiers are unique"), - TreeItem::new_leaf("g", "Golf"), + TreeItem::new_leaf("g", "Golf".to_owned()), ], ) .expect("all item identifiers are unique"), - TreeItem::new_leaf("h", "Hotel"), + TreeItem::new_leaf("h", "Hotel".to_owned()), TreeItem::new( "i", - "India", + "India".to_owned(), vec![ - TreeItem::new_leaf("j", "Juliett"), - TreeItem::new_leaf("k", "Kilo"), - TreeItem::new_leaf("l", "Lima"), - TreeItem::new_leaf("m", "Mike"), - TreeItem::new_leaf("n", "November"), + TreeItem::new_leaf("j", "Juliett".to_owned()), + TreeItem::new_leaf("k", "Kilo".to_owned()), + TreeItem::new_leaf("l", "Lima".to_owned()), + TreeItem::new_leaf("m", "Mike".to_owned()), + TreeItem::new_leaf("n", "November".to_owned()), ], ) .expect("all item identifiers are unique"), - TreeItem::new_leaf("o", "Oscar"), + TreeItem::new_leaf("o", "Oscar".to_owned()), TreeItem::new( "p", - "Papa", + "Papa".to_owned(), vec![ - TreeItem::new_leaf("q", "Quebec"), - TreeItem::new_leaf("r", "Romeo"), - TreeItem::new_leaf("s", "Sierra"), - TreeItem::new_leaf("t", "Tango"), - TreeItem::new_leaf("u", "Uniform"), + TreeItem::new_leaf("q", "Quebec".to_owned()), + TreeItem::new_leaf("r", "Romeo".to_owned()), + TreeItem::new_leaf("s", "Sierra".to_owned()), + TreeItem::new_leaf("t", "Tango".to_owned()), + TreeItem::new_leaf("u", "Uniform".to_owned()), TreeItem::new( "v", - "Victor", + "Victor".to_owned(), vec![ - TreeItem::new_leaf("w", "Whiskey"), - TreeItem::new_leaf("x", "Xray"), - TreeItem::new_leaf("y", "Yankee"), + TreeItem::new_leaf("w", "Whiskey".to_owned()), + TreeItem::new_leaf("x", "Xray".to_owned()), + TreeItem::new_leaf("y", "Yankee".to_owned()), ], ) .expect("all item identifiers are unique"), ], ) .expect("all item identifiers are unique"), - TreeItem::new_leaf("z", "Zulu"), + TreeItem::new_leaf("z", "Zulu".to_owned()), ], } } fn draw(&mut self, frame: &mut Frame) { let area = frame.area(); + let selected = self.state.selected(); + let flatten = self.state.flatten(&self.items); + let current_selection = flatten + .iter() + .find(|i| self.state.selected() == i.identifier); + let is_selected = current_selection.is_some() + && current_selection.unwrap().item.content().to_string() == *"Echo"; + let style = if is_selected { + Style::new() + .fg(Color::Black) + .bg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else { + Style::new() + .fg(Color::Black) + .bg(Color::LightGreen) + .add_modifier(Modifier::BOLD) + }; let widget = Tree::new(&self.items) .expect("all item identifiers are unique") .block( @@ -95,12 +113,7 @@ impl App { .track_symbol(None) .end_symbol(None), )) - .highlight_style( - Style::new() - .fg(Color::Black) - .bg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ) + .highlight_style(style) .highlight_symbol(">> "); frame.render_stateful_widget(widget, area, &mut self.state); } diff --git a/src/flatten.rs b/src/flatten.rs index fffb167..e623aeb 100644 --- a/src/flatten.rs +++ b/src/flatten.rs @@ -1,17 +1,25 @@ use std::collections::HashSet; +use ratatui::text::Text; + use crate::tree_item::TreeItem; /// A flattened item of all visible [`TreeItem`]s. /// /// Generated via [`TreeState::flatten`](crate::TreeState::flatten). #[must_use] -pub struct Flattened<'text, Identifier> { +pub struct Flattened<'a, Identifier, T> +where + T: for<'b> Into> + Clone, +{ pub identifier: Vec, - pub item: &'text TreeItem<'text, Identifier>, + pub item: &'a TreeItem, } -impl Flattened<'_, Identifier> { +impl<'a, Identifier, T> Flattened<'a, Identifier, T> +where + T: for<'b> Into> + Clone, +{ /// Zero based depth. Depth 0 means top level with 0 indentation. #[must_use] pub fn depth(&self) -> usize { @@ -23,13 +31,14 @@ impl Flattened<'_, Identifier> { /// /// `current` starts empty: `&[]` #[must_use] -pub fn flatten<'text, Identifier>( +pub fn flatten<'a, Identifier, T>( open_identifiers: &HashSet>, - items: &'text [TreeItem<'text, Identifier>], + items: &'a [TreeItem], current: &[Identifier], -) -> Vec> +) -> Vec> where Identifier: Clone + PartialEq + Eq + core::hash::Hash, + T: for<'b> Into> + Clone, { let mut result = Vec::new(); for item in items { diff --git a/src/lib.rs b/src/lib.rs index 931fa17..989af33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ use std::collections::HashSet; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Style; +use ratatui::text::Text; use ratatui::widgets::{Block, Scrollbar, ScrollbarState, StatefulWidget, Widget}; use unicode_width::UnicodeWidthStr; @@ -52,8 +53,11 @@ mod tree_state; /// ``` #[must_use] #[derive(Debug, Clone)] -pub struct Tree<'a, Identifier> { - items: &'a [TreeItem<'a, Identifier>], +pub struct Tree<'a, Identifier, T> +where + T: for<'b> Into> + Clone, +{ + items: &'a [TreeItem], block: Option>, scrollbar: Option>, @@ -73,16 +77,17 @@ pub struct Tree<'a, Identifier> { node_no_children_symbol: &'a str, } -impl<'a, Identifier> Tree<'a, Identifier> +impl<'a, Identifier, T> Tree<'a, Identifier, T> where Identifier: Clone + PartialEq + Eq + core::hash::Hash, + T: for<'b> Into> + Clone, { /// Create a new `Tree`. /// /// # Errors /// /// Errors when there are duplicate identifiers in the children. - pub fn new(items: &'a [TreeItem<'a, Identifier>]) -> std::io::Result { + pub fn new(items: &'a [TreeItem]) -> std::io::Result { let identifiers = items .iter() .map(|item| &item.identifier) @@ -160,12 +165,13 @@ fn tree_new_errors_with_duplicate_identifiers() { let item = TreeItem::new_leaf("same", "text"); let another = item.clone(); let items = [item, another]; - let _: Tree<_> = Tree::new(&items).unwrap(); + let _ = Tree::new(&items).unwrap(); } -impl StatefulWidget for Tree<'_, Identifier> +impl<'a, Identifier, T> StatefulWidget for Tree<'a, Identifier, T> where Identifier: Clone + PartialEq + Eq + core::hash::Hash, + T: for<'b> Into> + Clone, { type State = TreeState; @@ -272,7 +278,7 @@ where height, }; - let text = &item.text; + let text = item.content.clone().into(); let item_style = text.style; let is_selected = state.selected == *identifier; @@ -299,7 +305,7 @@ where ); let symbol = if item.children.is_empty() { self.node_no_children_symbol - } else if state.opened.contains(identifier) { + } else if state.opened.contains(identifier.as_slice()) { self.node_open_symbol } else { self.node_closed_symbol @@ -332,9 +338,10 @@ where } } -impl Widget for Tree<'_, Identifier> +impl<'a, Identifier, T> Widget for Tree<'a, Identifier, T> where Identifier: Clone + Default + Eq + core::hash::Hash, + T: for<'b> Into> + Clone, { fn render(self, area: Rect, buf: &mut Buffer) { let mut state = TreeState::default(); diff --git a/src/tree_item.rs b/src/tree_item.rs index 130b6f2..0daa115 100644 --- a/src/tree_item.rs +++ b/src/tree_item.rs @@ -34,25 +34,26 @@ use ratatui::text::Text; /// # Ok::<(), std::io::Error>(()) /// ``` #[derive(Debug, Clone)] -pub struct TreeItem<'text, Identifier> { +pub struct TreeItem +where + T: for<'a> Into> + Clone, +{ pub(super) identifier: Identifier, - pub(super) text: Text<'text>, + pub(super) content: T, pub(super) children: Vec, } -impl<'text, Identifier> TreeItem<'text, Identifier> +impl TreeItem where Identifier: Clone + PartialEq + Eq + core::hash::Hash, + T: for<'a> Into> + Clone, { /// Create a new `TreeItem` without children. #[must_use] - pub fn new_leaf(identifier: Identifier, text: T) -> Self - where - T: Into>, - { + pub fn new_leaf(identifier: Identifier, content: T) -> Self { Self { identifier, - text: text.into(), + content, children: Vec::new(), } } @@ -62,10 +63,7 @@ where /// # 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>, - { + pub fn new(identifier: Identifier, content: T, children: Vec) -> std::io::Result { let identifiers = children .iter() .map(|item| &item.identifier) @@ -79,7 +77,7 @@ where Ok(Self { identifier, - text: text.into(), + content, children, }) } @@ -92,8 +90,8 @@ where /// Get a reference to the text. #[must_use] - pub const fn text(&self) -> &Text<'text> { - &self.text + pub const fn content(&self) -> &T { + &self.content } #[must_use] @@ -117,7 +115,7 @@ where #[must_use] pub fn height(&self) -> usize { - self.text.height() + self.content.clone().into().height() } /// Add a child to the `TreeItem`. @@ -143,28 +141,31 @@ where } } -impl TreeItem<'static, &'static str> { +impl TreeItem<&'static str, String> { #[cfg(test)] #[must_use] pub(crate) fn example() -> Vec { vec![ - Self::new_leaf("a", "Alfa"), + Self::new_leaf("a", "Alfa".to_owned()), Self::new( "b", - "Bravo", + "Bravo".to_owned(), vec![ - Self::new_leaf("c", "Charlie"), + Self::new_leaf("c", "Charlie".to_owned()), Self::new( "d", - "Delta", - vec![Self::new_leaf("e", "Echo"), Self::new_leaf("f", "Foxtrot")], + "Delta".to_owned(), + vec![ + Self::new_leaf("e", "Echo".to_owned()), + Self::new_leaf("f", "Foxtrot".to_owned()), + ], ) .expect("all item identifiers are unique"), - Self::new_leaf("g", "Golf"), + Self::new_leaf("g", "Golf".to_owned()), ], ) .expect("all item identifiers are unique"), - Self::new_leaf("h", "Hotel"), + Self::new_leaf("h", "Hotel".to_owned()), ] } } diff --git a/src/tree_state.rs b/src/tree_state.rs index e314641..2be9c3e 100644 --- a/src/tree_state.rs +++ b/src/tree_state.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use ratatui::layout::{Position, Rect}; +use ratatui::text::Text; use crate::flatten::{flatten, Flattened}; use crate::tree_item::TreeItem; @@ -61,10 +62,13 @@ where /// Get a flat list of all currently viewable (including by scrolling) [`TreeItem`]s with this `TreeState`. #[must_use] - pub fn flatten<'text>( + pub fn flatten<'a, T>( &self, - items: &'text [TreeItem<'text, Identifier>], - ) -> Vec> { + items: &'a [TreeItem], + ) -> Vec> + where + T: for<'b> Into> + Clone, + { flatten(&self.opened, items, &[]) }