feat: Updated the Ratatui Tree Widget to allow the tree to story any data type that implements Into<Text>, so that users can also easily fetch data from within the tree based on what's selected for non-text data types.

This commit is contained in:
2024-11-14 15:38:37 -07:00
parent 735f111866
commit 3e23a73f6b
8 changed files with 78 additions and 84 deletions
+7 -7
View File
@@ -1,12 +1,12 @@
[package] [package]
name = "tui-tree-widget" name = "managarr-tree-widget"
description = "Tree Widget for ratatui" description = "Tree Widget for Managarr"
version = "0.23.0" version = "0.24.0"
license = "MIT" license = "MIT"
repository = "https://github.com/EdJoPaTo/tui-rs-tree-widget" repository = "https://github.com/Dark-Alex-17/managarr-tree-widget"
authors = ["EdJoPaTo <tui-tree-widget-rust-crate@edjopato.de>"] authors = ["EdJoPaTo <tui-tree-widget-rust-crate@edjopato.de>", "Dark-Alex-17 <alex.j.tusa@gmail.com>"]
edition = "2021" edition = "2021"
keywords = ["tui", "terminal", "tree", "widget"] keywords = ["tui", "terminal", "tree", "widget", "managarr"]
categories = ["command-line-interface"] categories = ["command-line-interface"]
include = ["src/**/*", "examples/**/*", "benches/**/*", "README.md"] include = ["src/**/*", "examples/**/*", "benches/**/*", "README.md"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -31,7 +31,7 @@ criterion = "0.5"
ratatui = "0.29" ratatui = "0.29"
[target.'cfg(target_family = "unix")'.dev-dependencies] [target.'cfg(target_family = "unix")'.dev-dependencies]
pprof = { version = "0.13", features = ["criterion", "flamegraph"] } pprof = { version = "0.14.0", features = ["criterion", "flamegraph"] }
[[bench]] [[bench]]
name = "bench" name = "bench"
+5 -2
View File
@@ -1,7 +1,10 @@
# Ratatui Tree Widget # Managarr Tree Widget
[Ratatui](https://docs.rs/ratatui) Widget built to show Tree Data structures. [Ratatui](https://docs.rs/ratatui) Widget built to show Tree Data structures.
![Screenshot](media/screenshot.png) ![Screenshot](media/screenshot.png)
Built for the specific use case of [`mqttui`](https://github.com/EdJoPaTo/mqttui).
## Credit
The original project for this widget is the [Ratatui Tree Widget](https://github.com/EdJoPaTo/tui-rs-tree-widget), which was purppose built for the specific use
case of [`mqttui`](https://github.com/EdJoPaTo/mqttui).
+31 -31
View File
@@ -4,67 +4,67 @@ use criterion::{criterion_group, criterion_main, BatchSize, Criterion, Throughpu
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::widgets::StatefulWidget; use ratatui::widgets::StatefulWidget;
use tui_tree_widget::{Tree, TreeItem, TreeState}; use managarr_tree_widget::{Tree, TreeItem, TreeState};
#[must_use] #[must_use]
fn example_items() -> Vec<TreeItem<'static, &'static str>> { fn example_items() -> Vec<TreeItem<&'static str, String>> {
vec![ vec![
TreeItem::new_leaf("a", "Alfa"), TreeItem::new_leaf("a", "Alfa".to_owned()),
TreeItem::new( TreeItem::new(
"b", "b",
"Bravo", "Bravo".to_owned(),
vec![ vec![
TreeItem::new_leaf("c", "Charlie"), TreeItem::new_leaf("c", "Charlie".to_owned()),
TreeItem::new( TreeItem::new(
"d", "d",
"Delta", "Delta".to_owned(),
vec![ vec![
TreeItem::new_leaf("e", "Echo"), TreeItem::new_leaf("e", "Echo".to_owned()),
TreeItem::new_leaf("f", "Foxtrot"), TreeItem::new_leaf("f", "Foxtrot".to_owned()),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("g", "Golf"), TreeItem::new_leaf("g", "Golf".to_owned()),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("h", "Hotel"), TreeItem::new_leaf("h", "Hotel".to_owned()),
TreeItem::new( TreeItem::new(
"i", "i",
"India", "India".to_owned(),
vec![ vec![
TreeItem::new_leaf("j", "Juliett"), TreeItem::new_leaf("j", "Juliett".to_owned()),
TreeItem::new_leaf("k", "Kilo"), TreeItem::new_leaf("k", "Kilo".to_owned()),
TreeItem::new_leaf("l", "Lima"), TreeItem::new_leaf("l", "Lima".to_owned()),
TreeItem::new_leaf("m", "Mike"), TreeItem::new_leaf("m", "Mike".to_owned()),
TreeItem::new_leaf("n", "November"), TreeItem::new_leaf("n", "November".to_owned()),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("o", "Oscar"), TreeItem::new_leaf("o", "Oscar".to_owned()),
TreeItem::new( TreeItem::new(
"p", "p",
"Papa", "Papa".to_owned(),
vec![ vec![
TreeItem::new_leaf("q", "Quebec"), TreeItem::new_leaf("q", "Quebec".to_owned()),
TreeItem::new_leaf("r", "Romeo"), TreeItem::new_leaf("r", "Romeo".to_owned()),
TreeItem::new_leaf("s", "Sierra"), TreeItem::new_leaf("s", "Sierra".to_owned()),
TreeItem::new_leaf("t", "Tango"), TreeItem::new_leaf("t", "Tango".to_owned()),
TreeItem::new_leaf("u", "Uniform"), TreeItem::new_leaf("u", "Uniform".to_owned()),
TreeItem::new( TreeItem::new(
"v", "v",
"Victor", "Victor".to_owned(),
vec![ vec![
TreeItem::new_leaf("w", "Whiskey"), TreeItem::new_leaf("w", "Whiskey".to_owned()),
TreeItem::new_leaf("x", "Xray"), TreeItem::new_leaf("x", "Xray".to_owned()),
TreeItem::new_leaf("y", "Yankee"), TreeItem::new_leaf("y", "Yankee".to_owned()),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("z", "Zulu"), TreeItem::new_leaf("z", "Zulu".to_owned()),
] ]
} }
@@ -75,14 +75,14 @@ fn init(criterion: &mut Criterion) {
group.bench_function("empty", |bencher| { group.bench_function("empty", |bencher| {
bencher.iter(|| { bencher.iter(|| {
let items = vec![]; let items = vec![];
let _: Tree<usize> = black_box(Tree::new(black_box(&items))).unwrap(); let _ = black_box(Tree::new(black_box(&items))).unwrap();
}); });
}); });
group.bench_function("example-items", |bencher| { group.bench_function("example-items", |bencher| {
bencher.iter(|| { bencher.iter(|| {
let items = example_items(); let items = example_items();
let _: Tree<_> = black_box(Tree::new(black_box(&items))).unwrap(); let _ = black_box(Tree::new(black_box(&items))).unwrap();
}); });
}); });
@@ -96,7 +96,7 @@ fn renders(criterion: &mut Criterion) {
let buffer_size = Rect::new(0, 0, 100, 100); let buffer_size = Rect::new(0, 0, 100, 100);
group.bench_function("empty", |bencher| { group.bench_function("empty", |bencher| {
let items: Vec<TreeItem<usize>> = vec![]; let items: Vec<TreeItem<usize, String>> = vec![];
let tree = Tree::new(&items).unwrap(); let tree = Tree::new(&items).unwrap();
let mut state = TreeState::default(); let mut state = TreeState::default();
bencher.iter_batched( bencher.iter_batched(
+7 -20
View File
@@ -7,7 +7,7 @@ use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span; use ratatui::text::Span;
use ratatui::widgets::{Block, Scrollbar, ScrollbarOrientation}; use ratatui::widgets::{Block, Scrollbar, ScrollbarOrientation};
use ratatui::{crossterm, Frame, Terminal}; use ratatui::{crossterm, Frame, Terminal};
use tui_tree_widget::{Tree, TreeItem, TreeState}; use managarr_tree_widget::{Tree, TreeItem, TreeState};
#[must_use] #[must_use]
struct App { struct App {
@@ -82,24 +82,6 @@ impl App {
fn draw(&mut self, frame: &mut Frame) { fn draw(&mut self, frame: &mut Frame) {
let area = frame.area(); 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) let widget = Tree::new(&self.items)
.expect("all item identifiers are unique") .expect("all item identifiers are unique")
.block( .block(
@@ -113,7 +95,12 @@ impl App {
.track_symbol(None) .track_symbol(None)
.end_symbol(None), .end_symbol(None),
)) ))
.highlight_style(style) .highlight_style(
Style::new()
.fg(Color::Black)
.bg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> "); .highlight_symbol(">> ");
frame.render_stateful_widget(widget, area, &mut self.state); frame.render_stateful_widget(widget, area, &mut self.state);
} }
+5 -3
View File
@@ -10,7 +10,8 @@ use crate::tree_item::TreeItem;
#[must_use] #[must_use]
pub struct Flattened<'a, Identifier, T> pub struct Flattened<'a, Identifier, T>
where where
T: for<'b> Into<Text<'b>> + Clone, Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'b> Into<Text<'b>> + Clone + Default,
{ {
pub identifier: Vec<Identifier>, pub identifier: Vec<Identifier>,
pub item: &'a TreeItem<Identifier, T>, pub item: &'a TreeItem<Identifier, T>,
@@ -18,7 +19,8 @@ where
impl<'a, Identifier, T> Flattened<'a, Identifier, T> impl<'a, Identifier, T> Flattened<'a, Identifier, T>
where where
T: for<'b> Into<Text<'b>> + Clone, Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'b> Into<Text<'b>> + Clone + Default,
{ {
/// Zero based depth. Depth 0 means top level with 0 indentation. /// Zero based depth. Depth 0 means top level with 0 indentation.
#[must_use] #[must_use]
@@ -38,7 +40,7 @@ pub fn flatten<'a, Identifier, T>(
) -> Vec<Flattened<'a, Identifier, T>> ) -> Vec<Flattened<'a, Identifier, T>>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'b> Into<Text<'b>> + Clone, T: for<'b> Into<Text<'b>> + Clone + Default,
{ {
let mut result = Vec::new(); let mut result = Vec::new();
for item in items { for item in items {
+8 -7
View File
@@ -30,7 +30,7 @@ mod tree_state;
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use tui_tree_widget::{Tree, TreeItem, TreeState}; /// # use managarr_tree_widget::{Tree, TreeItem, TreeState};
/// # use ratatui::backend::TestBackend; /// # use ratatui::backend::TestBackend;
/// # use ratatui::Terminal; /// # use ratatui::Terminal;
/// # use ratatui::widgets::Block; /// # use ratatui::widgets::Block;
@@ -41,7 +41,7 @@ mod tree_state;
/// let items = vec![item]; /// let items = vec![item];
/// ///
/// terminal.draw(|frame| { /// terminal.draw(|frame| {
/// let area = frame.size(); /// let area = frame.area();
/// ///
/// let tree_widget = Tree::new(&items) /// let tree_widget = Tree::new(&items)
/// .expect("all item identifiers are unique") /// .expect("all item identifiers are unique")
@@ -55,7 +55,8 @@ mod tree_state;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Tree<'a, Identifier, T> pub struct Tree<'a, Identifier, T>
where where
T: for<'b> Into<Text<'b>> + Clone, Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'b> Into<Text<'b>> + Clone + Default,
{ {
items: &'a [TreeItem<Identifier, T>], items: &'a [TreeItem<Identifier, T>],
@@ -80,7 +81,7 @@ where
impl<'a, Identifier, T> Tree<'a, Identifier, T> impl<'a, Identifier, T> Tree<'a, Identifier, T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'b> Into<Text<'b>> + Clone, T: for<'b> Into<Text<'b>> + Clone + Default,
{ {
/// Create a new `Tree`. /// Create a new `Tree`.
/// ///
@@ -121,7 +122,7 @@ where
/// Show the scrollbar when rendering this widget. /// Show the scrollbar when rendering this widget.
/// ///
/// Experimental: Can change on any release without any additional notice. /// Experimental: Can change on any release without any additional notice.
/// Its there to test and experiment with whats possible with scrolling widgets. /// It's there to test and experiment with what's possible with scrolling widgets.
/// Also see <https://github.com/ratatui-org/ratatui/issues/174> /// Also see <https://github.com/ratatui-org/ratatui/issues/174>
pub const fn experimental_scrollbar(mut self, scrollbar: Option<Scrollbar<'a>>) -> Self { pub const fn experimental_scrollbar(mut self, scrollbar: Option<Scrollbar<'a>>) -> Self {
self.scrollbar = scrollbar; self.scrollbar = scrollbar;
@@ -171,7 +172,7 @@ fn tree_new_errors_with_duplicate_identifiers() {
impl<'a, Identifier, T> StatefulWidget for Tree<'a, Identifier, T> impl<'a, Identifier, T> StatefulWidget for Tree<'a, Identifier, T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'b> Into<Text<'b>> + Clone, T: for<'b> Into<Text<'b>> + Clone + Default,
{ {
type State = TreeState<Identifier>; type State = TreeState<Identifier>;
@@ -341,7 +342,7 @@ where
impl<'a, Identifier, T> Widget for Tree<'a, Identifier, T> impl<'a, Identifier, T> Widget for Tree<'a, Identifier, T>
where where
Identifier: Clone + Default + Eq + core::hash::Hash, Identifier: Clone + Default + Eq + core::hash::Hash,
T: for<'b> Into<Text<'b>> + Clone, T: for<'b> Into<Text<'b>> + Clone + Default,
{ {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TreeState::default(); let mut state = TreeState::default();
+7 -6
View File
@@ -15,7 +15,7 @@ use ratatui::text::Text;
/// ///
/// The `text` can be different from its `identifier`. /// The `text` can be different from its `identifier`.
/// To repeat the filename analogy: File browsers sometimes hide file extensions. /// To repeat the filename analogy: File browsers sometimes hide file extensions.
/// The filename `main.rs` is the identifier while its shown as `main`. /// The filename `main.rs` is the identifier while it's 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. /// 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`](crate::Tree) can be with these identifiers. /// Just like every file in a file system can be uniquely identified with its file and directory names each [`TreeItem`] in a [`Tree`](crate::Tree) can be with these identifiers.
@@ -28,7 +28,7 @@ use ratatui::text::Text;
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use tui_tree_widget::TreeItem; /// # use managarr_tree_widget::TreeItem;
/// let a = TreeItem::new_leaf("l", "Leaf"); /// let a = TreeItem::new_leaf("l", "Leaf");
/// let b = TreeItem::new("r", "Root", vec![a])?; /// let b = TreeItem::new("r", "Root", vec![a])?;
/// # Ok::<(), std::io::Error>(()) /// # Ok::<(), std::io::Error>(())
@@ -36,7 +36,8 @@ use ratatui::text::Text;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TreeItem<Identifier, T> pub struct TreeItem<Identifier, T>
where where
T: for<'a> Into<Text<'a>> + Clone, Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'a> Into<Text<'a>> + Clone + Default,
{ {
pub(super) identifier: Identifier, pub(super) identifier: Identifier,
pub(super) content: T, pub(super) content: T,
@@ -46,11 +47,11 @@ where
impl<Identifier, T> TreeItem<Identifier, T> impl<Identifier, T> TreeItem<Identifier, T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'a> Into<Text<'a>> + Clone, T: for<'a> Into<Text<'a>> + Clone + Default,
{ {
/// Create a new `TreeItem` without children. /// Create a new `TreeItem` without children.
#[must_use] #[must_use]
pub fn new_leaf(identifier: Identifier, content: T) -> Self { pub const fn new_leaf(identifier: Identifier, content: T) -> Self {
Self { Self {
identifier, identifier,
content, content,
@@ -107,7 +108,7 @@ where
/// Get a mutable reference to a child by 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. /// When you choose to change the `identifier` the [`TreeState`](crate::TreeState) might not work as expected afterward.
#[must_use] #[must_use]
pub fn child_mut(&mut self, index: usize) -> Option<&mut Self> { pub fn child_mut(&mut self, index: usize) -> Option<&mut Self> {
self.children.get_mut(index) self.children.get_mut(index)
+8 -8
View File
@@ -14,7 +14,7 @@ use crate::tree_item::TreeItem;
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use tui_tree_widget::TreeState; /// # use managarr_tree_widget::TreeState;
/// type Identifier = usize; /// type Identifier = usize;
/// ///
/// let mut state = TreeState::<Identifier>::default(); /// let mut state = TreeState::<Identifier>::default();
@@ -67,7 +67,7 @@ where
items: &'a [TreeItem<Identifier, T>], items: &'a [TreeItem<Identifier, T>],
) -> Vec<Flattened<'a, Identifier, T>> ) -> Vec<Flattened<'a, Identifier, T>>
where where
T: for<'b> Into<Text<'b>> + Clone, T: for<'b> Into<Text<'b>> + Clone + Default,
{ {
flatten(&self.opened, items, &[]) flatten(&self.opened, items, &[])
} }
@@ -79,7 +79,7 @@ where
/// Clear the selection by passing an empty identifier vector: /// Clear the selection by passing an empty identifier vector:
/// ///
/// ```rust /// ```rust
/// # use tui_tree_widget::TreeState; /// # use managarr_tree_widget::TreeState;
/// # let mut state = TreeState::<usize>::default(); /// # let mut state = TreeState::<usize>::default();
/// state.select(Vec::new()); /// state.select(Vec::new());
/// ``` /// ```
@@ -195,13 +195,13 @@ where
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use tui_tree_widget::TreeState; /// # use managarr_tree_widget::TreeState;
/// # type Identifier = usize; /// # type Identifier = usize;
/// # let mut state = TreeState::<Identifier>::default(); /// # let mut state = TreeState::<Identifier>::default();
/// // Move the selection one down /// // Move the selection one down
/// state.select_visible_relative(|current| { /// state.select_relative(|current| {
/// // When nothing is currently selected, select index 0 /// // When nothing is currently selected, select index 0
/// // Otherwise select current + 1 (without panicing) /// // Otherwise select current + 1 (without panicking)
/// current.map_or(0, |current| current.saturating_add(1)) /// current.map_or(0, |current| current.saturating_add(1))
/// }); /// });
/// ``` /// ```
@@ -230,13 +230,13 @@ where
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use tui_tree_widget::TreeState; /// # use managarr_tree_widget::TreeState;
/// # type Identifier = usize; /// # type Identifier = usize;
/// # let mut state = TreeState::<Identifier>::default(); /// # let mut state = TreeState::<Identifier>::default();
/// // Move the selection one down /// // Move the selection one down
/// state.select_relative(|current| { /// state.select_relative(|current| {
/// // When nothing is currently selected, select index 0 /// // When nothing is currently selected, select index 0
/// // Otherwise select current + 1 (without panicing) /// // Otherwise select current + 1 (without panicking)
/// current.map_or(0, |current| current.saturating_add(1)) /// current.map_or(0, |current| current.saturating_add(1))
/// }); /// });
/// ``` /// ```