feat: Changed the tree to accept any type that implements Into<Text> so I can use the tree to fully store structs in the TUI as well as listing them

This commit is contained in:
2024-11-13 13:06:15 -07:00
parent 6e66e3e398
commit 735f111866
5 changed files with 109 additions and 75 deletions
+46 -33
View File
@@ -12,7 +12,7 @@ use tui_tree_widget::{Tree, TreeItem, TreeState};
#[must_use]
struct App {
state: TreeState<&'static str>,
items: Vec<TreeItem<'static, &'static str>>,
items: Vec<TreeItem<&'static str, String>>,
}
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);
}
+15 -6
View File
@@ -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<Text<'b>> + Clone,
{
pub identifier: Vec<Identifier>,
pub item: &'text TreeItem<'text, Identifier>,
pub item: &'a TreeItem<Identifier, T>,
}
impl<Identifier> Flattened<'_, Identifier> {
impl<'a, Identifier, T> Flattened<'a, Identifier, T>
where
T: for<'b> Into<Text<'b>> + Clone,
{
/// Zero based depth. Depth 0 means top level with 0 indentation.
#[must_use]
pub fn depth(&self) -> usize {
@@ -23,13 +31,14 @@ impl<Identifier> Flattened<'_, Identifier> {
///
/// `current` starts empty: `&[]`
#[must_use]
pub fn flatten<'text, Identifier>(
pub fn flatten<'a, Identifier, T>(
open_identifiers: &HashSet<Vec<Identifier>>,
items: &'text [TreeItem<'text, Identifier>],
items: &'a [TreeItem<Identifier, T>],
current: &[Identifier],
) -> Vec<Flattened<'text, Identifier>>
) -> Vec<Flattened<'a, Identifier, T>>
where
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'b> Into<Text<'b>> + Clone,
{
let mut result = Vec::new();
for item in items {
+16 -9
View File
@@ -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<Text<'b>> + Clone,
{
items: &'a [TreeItem<Identifier, T>],
block: Option<Block<'a>>,
scrollbar: Option<Scrollbar<'a>>,
@@ -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<Text<'b>> + 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<Self> {
pub fn new(items: &'a [TreeItem<Identifier, T>]) -> std::io::Result<Self> {
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<Identifier> 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<Text<'b>> + Clone,
{
type State = TreeState<Identifier>;
@@ -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<Identifier> 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<Text<'b>> + Clone,
{
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TreeState::default();
+27 -26
View File
@@ -34,25 +34,26 @@ use ratatui::text::Text;
/// # Ok::<(), std::io::Error>(())
/// ```
#[derive(Debug, Clone)]
pub struct TreeItem<'text, Identifier> {
pub struct TreeItem<Identifier, T>
where
T: for<'a> Into<Text<'a>> + Clone,
{
pub(super) identifier: Identifier,
pub(super) text: Text<'text>,
pub(super) content: T,
pub(super) children: Vec<Self>,
}
impl<'text, Identifier> TreeItem<'text, Identifier>
impl<Identifier, T> TreeItem<Identifier, T>
where
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
T: for<'a> Into<Text<'a>> + Clone,
{
/// Create a new `TreeItem` without children.
#[must_use]
pub fn new_leaf<T>(identifier: Identifier, text: T) -> Self
where
T: Into<Text<'text>>,
{
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<T>(identifier: Identifier, text: T, children: Vec<Self>) -> std::io::Result<Self>
where
T: Into<Text<'text>>,
{
pub fn new(identifier: Identifier, content: T, children: Vec<Self>) -> std::io::Result<Self> {
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<Self> {
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")],
)
.expect("all item identifiers are unique"),
Self::new_leaf("g", "Golf"),
"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("h", "Hotel"),
Self::new_leaf("g", "Golf".to_owned()),
],
)
.expect("all item identifiers are unique"),
Self::new_leaf("h", "Hotel".to_owned()),
]
}
}
+7 -3
View File
@@ -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<Flattened<'text, Identifier>> {
items: &'a [TreeItem<Identifier, T>],
) -> Vec<Flattened<'a, Identifier, T>>
where
T: for<'b> Into<Text<'b>> + Clone,
{
flatten(&self.opened, items, &[])
}