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]
name = "tui-tree-widget"
description = "Tree Widget for ratatui"
version = "0.23.0"
name = "managarr-tree-widget"
description = "Tree Widget for Managarr"
version = "0.24.0"
license = "MIT"
repository = "https://github.com/EdJoPaTo/tui-rs-tree-widget"
authors = ["EdJoPaTo <tui-tree-widget-rust-crate@edjopato.de>"]
repository = "https://github.com/Dark-Alex-17/managarr-tree-widget"
authors = ["EdJoPaTo <tui-tree-widget-rust-crate@edjopato.de>", "Dark-Alex-17 <alex.j.tusa@gmail.com>"]
edition = "2021"
keywords = ["tui", "terminal", "tree", "widget"]
keywords = ["tui", "terminal", "tree", "widget", "managarr"]
categories = ["command-line-interface"]
include = ["src/**/*", "examples/**/*", "benches/**/*", "README.md"]
# 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"
[target.'cfg(target_family = "unix")'.dev-dependencies]
pprof = { version = "0.13", features = ["criterion", "flamegraph"] }
pprof = { version = "0.14.0", features = ["criterion", "flamegraph"] }
[[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.
![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::layout::Rect;
use ratatui::widgets::StatefulWidget;
use tui_tree_widget::{Tree, TreeItem, TreeState};
use managarr_tree_widget::{Tree, TreeItem, TreeState};
#[must_use]
fn example_items() -> Vec<TreeItem<'static, &'static str>> {
fn example_items() -> Vec<TreeItem<&'static str, String>> {
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()),
]
}
@@ -75,14 +75,14 @@ fn init(criterion: &mut Criterion) {
group.bench_function("empty", |bencher| {
bencher.iter(|| {
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| {
bencher.iter(|| {
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);
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 mut state = TreeState::default();
bencher.iter_batched(
+7 -20
View File
@@ -7,7 +7,7 @@ use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;
use ratatui::widgets::{Block, Scrollbar, ScrollbarOrientation};
use ratatui::{crossterm, Frame, Terminal};
use tui_tree_widget::{Tree, TreeItem, TreeState};
use managarr_tree_widget::{Tree, TreeItem, TreeState};
#[must_use]
struct App {
@@ -82,24 +82,6 @@ impl App {
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(
@@ -113,7 +95,12 @@ impl App {
.track_symbol(None)
.end_symbol(None),
))
.highlight_style(style)
.highlight_style(
Style::new()
.fg(Color::Black)
.bg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
frame.render_stateful_widget(widget, area, &mut self.state);
}
+5 -3
View File
@@ -10,7 +10,8 @@ use crate::tree_item::TreeItem;
#[must_use]
pub struct Flattened<'a, Identifier, T>
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 item: &'a TreeItem<Identifier, T>,
@@ -18,7 +19,8 @@ where
impl<'a, Identifier, T> Flattened<'a, Identifier, T>
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.
#[must_use]
@@ -38,7 +40,7 @@ pub fn flatten<'a, Identifier, T>(
) -> Vec<Flattened<'a, Identifier, T>>
where
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();
for item in items {
+8 -7
View File
@@ -30,7 +30,7 @@ mod tree_state;
/// # Example
///
/// ```
/// # use tui_tree_widget::{Tree, TreeItem, TreeState};
/// # use managarr_tree_widget::{Tree, TreeItem, TreeState};
/// # use ratatui::backend::TestBackend;
/// # use ratatui::Terminal;
/// # use ratatui::widgets::Block;
@@ -41,7 +41,7 @@ mod tree_state;
/// let items = vec![item];
///
/// terminal.draw(|frame| {
/// let area = frame.size();
/// let area = frame.area();
///
/// let tree_widget = Tree::new(&items)
/// .expect("all item identifiers are unique")
@@ -55,7 +55,8 @@ mod tree_state;
#[derive(Debug, Clone)]
pub struct Tree<'a, Identifier, T>
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>],
@@ -80,7 +81,7 @@ where
impl<'a, Identifier, T> Tree<'a, Identifier, T>
where
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`.
///
@@ -121,7 +122,7 @@ where
/// Show the scrollbar when rendering this widget.
///
/// 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>
pub const fn experimental_scrollbar(mut self, scrollbar: Option<Scrollbar<'a>>) -> Self {
self.scrollbar = scrollbar;
@@ -171,7 +172,7 @@ fn tree_new_errors_with_duplicate_identifiers() {
impl<'a, Identifier, T> StatefulWidget for Tree<'a, Identifier, T>
where
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>;
@@ -341,7 +342,7 @@ where
impl<'a, Identifier, T> Widget for Tree<'a, Identifier, T>
where
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) {
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`.
/// 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.
///
/// 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
///
/// ```
/// # use tui_tree_widget::TreeItem;
/// # use managarr_tree_widget::TreeItem;
/// let a = TreeItem::new_leaf("l", "Leaf");
/// let b = TreeItem::new("r", "Root", vec![a])?;
/// # Ok::<(), std::io::Error>(())
@@ -36,7 +36,8 @@ use ratatui::text::Text;
#[derive(Debug, Clone)]
pub struct TreeItem<Identifier, T>
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) content: T,
@@ -46,11 +47,11 @@ where
impl<Identifier, T> TreeItem<Identifier, T>
where
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.
#[must_use]
pub fn new_leaf(identifier: Identifier, content: T) -> Self {
pub const fn new_leaf(identifier: Identifier, content: T) -> Self {
Self {
identifier,
content,
@@ -107,7 +108,7 @@ where
/// 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]
pub fn child_mut(&mut self, index: usize) -> Option<&mut Self> {
self.children.get_mut(index)
+8 -8
View File
@@ -14,7 +14,7 @@ use crate::tree_item::TreeItem;
/// # Example
///
/// ```
/// # use tui_tree_widget::TreeState;
/// # use managarr_tree_widget::TreeState;
/// type Identifier = usize;
///
/// let mut state = TreeState::<Identifier>::default();
@@ -67,7 +67,7 @@ where
items: &'a [TreeItem<Identifier, T>],
) -> Vec<Flattened<'a, Identifier, T>>
where
T: for<'b> Into<Text<'b>> + Clone,
T: for<'b> Into<Text<'b>> + Clone + Default,
{
flatten(&self.opened, items, &[])
}
@@ -79,7 +79,7 @@ where
/// Clear the selection by passing an empty identifier vector:
///
/// ```rust
/// # use tui_tree_widget::TreeState;
/// # use managarr_tree_widget::TreeState;
/// # let mut state = TreeState::<usize>::default();
/// state.select(Vec::new());
/// ```
@@ -195,13 +195,13 @@ where
/// # Example
///
/// ```
/// # use tui_tree_widget::TreeState;
/// # use managarr_tree_widget::TreeState;
/// # type Identifier = usize;
/// # let mut state = TreeState::<Identifier>::default();
/// // Move the selection one down
/// state.select_visible_relative(|current| {
/// state.select_relative(|current| {
/// // 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))
/// });
/// ```
@@ -230,13 +230,13 @@ where
/// # Example
///
/// ```
/// # use tui_tree_widget::TreeState;
/// # use managarr_tree_widget::TreeState;
/// # type Identifier = usize;
/// # let mut state = TreeState::<Identifier>::default();
/// // Move the selection one down
/// state.select_relative(|current| {
/// // 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))
/// });
/// ```