fix: Changed the implementation to not require the direct use of identifiers, and to instead generate them using the hashes of the contents of each tree item

This commit is contained in:
2024-11-14 17:35:40 -07:00
parent 8a91f662dd
commit 6c10db760d
6 changed files with 198 additions and 183 deletions
+26 -31
View File
@@ -7,64 +7,59 @@ use ratatui::widgets::StatefulWidget;
use managarr_tree_widget::{Tree, TreeItem, TreeState}; use managarr_tree_widget::{Tree, TreeItem, TreeState};
#[must_use] #[must_use]
fn example_items() -> Vec<TreeItem<&'static str, &'static str>> { fn example_items() -> Vec<TreeItem<&'static str>> {
vec![ vec![
TreeItem::new_leaf("a", "Alfa"), TreeItem::new_leaf( "Alfa"),
TreeItem::new( TreeItem::new(
"b",
"Bravo", "Bravo",
vec![ vec![
TreeItem::new_leaf("c", "Charlie"), TreeItem::new_leaf( "Charlie"),
TreeItem::new( TreeItem::new(
"d",
"Delta", "Delta",
vec![ vec![
TreeItem::new_leaf("e", "Echo"), TreeItem::new_leaf( "Echo"),
TreeItem::new_leaf("f", "Foxtrot"), TreeItem::new_leaf( "Foxtrot"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("g", "Golf"), TreeItem::new_leaf( "Golf"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("h", "Hotel"), TreeItem::new_leaf( "Hotel"),
TreeItem::new( TreeItem::new(
"i",
"India", "India",
vec![ vec![
TreeItem::new_leaf("j", "Juliett"), TreeItem::new_leaf( "Juliet"),
TreeItem::new_leaf("k", "Kilo"), TreeItem::new_leaf("Kilo"),
TreeItem::new_leaf("l", "Lima"), TreeItem::new_leaf("Lima"),
TreeItem::new_leaf("m", "Mike"), TreeItem::new_leaf("Mike"),
TreeItem::new_leaf("n", "November"), TreeItem::new_leaf("November"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("o", "Oscar"), TreeItem::new_leaf( "Oscar"),
TreeItem::new( TreeItem::new(
"p",
"Papa", "Papa",
vec![ vec![
TreeItem::new_leaf("q", "Quebec"), TreeItem::new_leaf( "Quebec"),
TreeItem::new_leaf("r", "Romeo"), TreeItem::new_leaf( "Romeo"),
TreeItem::new_leaf("s", "Sierra"), TreeItem::new_leaf( "Sierra"),
TreeItem::new_leaf("t", "Tango"), TreeItem::new_leaf( "Tango"),
TreeItem::new_leaf("u", "Uniform"), TreeItem::new_leaf( "Uniform"),
TreeItem::new( TreeItem::new(
"v",
"Victor", "Victor",
vec![ vec![
TreeItem::new_leaf("w", "Whiskey"), TreeItem::new_leaf("Whiskey"),
TreeItem::new_leaf("x", "Xray"), TreeItem::new_leaf ("Xray"),
TreeItem::new_leaf("y", "Yankee"), TreeItem::new_leaf( "Yankee"),
], ],
) )
.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( "Zulu"),
] ]
} }
@@ -74,7 +69,7 @@ fn init(criterion: &mut Criterion) {
group.bench_function("empty", |bencher| { group.bench_function("empty", |bencher| {
bencher.iter(|| { bencher.iter(|| {
let items = Vec::<TreeItem<String, String>>::new(); let items = Vec::<TreeItem<String>>::new();
let _ = black_box(Tree::new(black_box(&items))).unwrap(); let _ = black_box(Tree::new(black_box(&items))).unwrap();
}); });
}); });
@@ -96,7 +91,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, String>> = vec![]; let items: Vec<TreeItem<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(
@@ -112,8 +107,8 @@ fn renders(criterion: &mut Criterion) {
let items = example_items(); let items = example_items();
let tree = Tree::new(&items).unwrap(); let tree = Tree::new(&items).unwrap();
let mut state = TreeState::default(); let mut state = TreeState::default();
state.open(vec!["b"]); state.open(vec![2]);
state.open(vec!["b", "d"]); state.open(vec![2, 4]);
bencher.iter_batched( bencher.iter_batched(
|| (tree.clone(), Buffer::empty(buffer_size)), || (tree.clone(), Buffer::empty(buffer_size)),
|(tree, mut buffer)| { |(tree, mut buffer)| {
+23 -28
View File
@@ -11,8 +11,8 @@ use managarr_tree_widget::{Tree, TreeItem, TreeState};
#[must_use] #[must_use]
struct App { struct App {
state: TreeState<&'static str>, state: TreeState,
items: Vec<TreeItem<&'static str, &'static str>>, items: Vec<TreeItem<&'static str>>,
} }
impl App { impl App {
@@ -20,62 +20,57 @@ impl App {
Self { Self {
state: TreeState::default(), state: TreeState::default(),
items: vec![ items: vec![
TreeItem::new_leaf("a", "Alfa"), TreeItem::new_leaf( "Alfa"),
TreeItem::new( TreeItem::new(
"b",
"Bravo", "Bravo",
vec![ vec![
TreeItem::new_leaf("c", "Charlie"), TreeItem::new_leaf( "Charlie"),
TreeItem::new( TreeItem::new(
"d",
"Delta", "Delta",
vec![ vec![
TreeItem::new_leaf("e", "Echo"), TreeItem::new_leaf("Echo"),
TreeItem::new_leaf("f", "Foxtrot"), TreeItem::new_leaf( "Foxtrot"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("g", "Golf"), TreeItem::new_leaf( "Golf"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("h", "Hotel"), TreeItem::new_leaf("Hotel"),
TreeItem::new( TreeItem::new(
"i",
"India", "India",
vec![ vec![
TreeItem::new_leaf("j", "Juliett"), TreeItem::new_leaf( "Juliet"),
TreeItem::new_leaf("k", "Kilo"), TreeItem::new_leaf( "Kilo"),
TreeItem::new_leaf("l", "Lima"), TreeItem::new_leaf( "Lima"),
TreeItem::new_leaf("m", "Mike"), TreeItem::new_leaf( "Mike"),
TreeItem::new_leaf("n", "November"), TreeItem::new_leaf( "November"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("o", "Oscar"), TreeItem::new_leaf( "Oscar"),
TreeItem::new( TreeItem::new(
"p",
"Papa", "Papa",
vec![ vec![
TreeItem::new_leaf("q", "Quebec"), TreeItem::new_leaf( "Quebec"),
TreeItem::new_leaf("r", "Romeo"), TreeItem::new_leaf( "Romeo"),
TreeItem::new_leaf("s", "Sierra"), TreeItem::new_leaf( "Sierra"),
TreeItem::new_leaf("t", "Tango"), TreeItem::new_leaf( "Tango"),
TreeItem::new_leaf("u", "Uniform"), TreeItem::new_leaf( "Uniform"),
TreeItem::new( TreeItem::new(
"v",
"Victor", "Victor",
vec![ vec![
TreeItem::new_leaf("w", "Whiskey"), TreeItem::new_leaf( "Whiskey"),
TreeItem::new_leaf("x", "Xray"), TreeItem::new_leaf( "Xray"),
TreeItem::new_leaf("y", "Yankee"), TreeItem::new_leaf( "Yankee"),
], ],
) )
.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( "Zulu"),
], ],
} }
} }
+57 -29
View File
@@ -1,5 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::Display;
use std::hash::Hash;
use crate::tree_item::TreeItem; use crate::tree_item::TreeItem;
use ratatui::text::ToText; use ratatui::text::ToText;
@@ -7,19 +8,17 @@ use ratatui::text::ToText;
/// ///
/// Generated via [`TreeState::flatten`](crate::TreeState::flatten). /// Generated via [`TreeState::flatten`](crate::TreeState::flatten).
#[must_use] #[must_use]
pub struct Flattened<'a, Identifier, T> pub struct Flattened<'a, T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, T: ToText + Clone + Default + Display + Hash,
T: ToText + Clone + Default,
{ {
pub identifier: Vec<Identifier>, pub identifier: Vec<u64>,
pub item: &'a TreeItem<Identifier, T>, pub item: &'a TreeItem<T>,
} }
impl<'a, Identifier, T> Flattened<'a, Identifier, T> impl<'a, T> Flattened<'a, T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, T: ToText + Clone + Default + Display + Hash,
T: ToText + 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]
@@ -32,19 +31,18 @@ where
/// ///
/// `current` starts empty: `&[]` /// `current` starts empty: `&[]`
#[must_use] #[must_use]
pub fn flatten<'a, Identifier, T>( pub fn flatten<'a, T>(
open_identifiers: &HashSet<Vec<Identifier>>, open_identifiers: &HashSet<Vec<u64>>,
items: &'a [TreeItem<Identifier, T>], items: &'a [TreeItem<T>],
current: &[Identifier], current: &[u64],
) -> Vec<Flattened<'a, Identifier, T>> ) -> Vec<Flattened<'a, T>>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, T: ToText + Clone + Default + Display + Hash,
T: ToText + Clone + Default,
{ {
let mut result = Vec::new(); let mut result = Vec::new();
for item in items { for item in items {
let mut child_identifier = current.to_vec(); let mut child_identifier = current.to_vec();
child_identifier.push(item.identifier.clone()); child_identifier.push(item.identifier);
let child_result = open_identifiers let child_result = open_identifiers
.contains(&child_identifier) .contains(&child_identifier)
@@ -64,9 +62,15 @@ where
#[test] #[test]
fn depth_works() { fn depth_works() {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut open = HashSet::new(); let mut open = HashSet::new();
open.insert(vec!["b"]); let hash = |s: &str| {
open.insert(vec!["b", "d"]); let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
};
open.insert(vec![hash("Bravo")]);
open.insert(vec![hash("Bravo"), hash("Delta")]);
let depths = flatten(&open, &TreeItem::example(), &[]) let depths = flatten(&open, &TreeItem::example(), &[])
.into_iter() .into_iter()
.map(|flattened| flattened.depth()) .map(|flattened| flattened.depth())
@@ -75,7 +79,7 @@ fn depth_works() {
} }
#[cfg(test)] #[cfg(test)]
fn flatten_works(open: &HashSet<Vec<&'static str>>, expected: &[&str]) { fn flatten_works(open: &HashSet<Vec<u64>>, expected: &[u64]) {
let items = TreeItem::example(); let items = TreeItem::example();
let result = flatten(open, &items, &[]); let result = flatten(open, &items, &[]);
let actual = result let actual = result
@@ -87,29 +91,53 @@ fn flatten_works(open: &HashSet<Vec<&'static str>>, expected: &[&str]) {
#[test] #[test]
fn flatten_nothing_open_is_top_level() { fn flatten_nothing_open_is_top_level() {
use std::hash::{DefaultHasher, Hash, Hasher};
let open = HashSet::new(); let open = HashSet::new();
flatten_works(&open, &["a", "b", "h"]); let hash = |s: &str| {
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
};
flatten_works(&open, &[hash("Alfa"), hash("Bravo"), hash("Hotel")]);
} }
#[test] #[test]
fn flatten_wrong_open_is_only_top_level() { fn flatten_wrong_open_is_only_top_level() {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut open = HashSet::new(); let mut open = HashSet::new();
open.insert(vec!["a"]); let hash = |s: &str| {
open.insert(vec!["b", "d"]); let mut hasher = DefaultHasher::new();
flatten_works(&open, &["a", "b", "h"]); s.hash(&mut hasher);
hasher.finish()
};
open.insert(vec![hash("Alfa")]);
open.insert(vec![hash("Bravo"), hash("Delta")]);
flatten_works(&open, &[hash("Alfa"), hash("Bravo"), hash("Hotel")]);
} }
#[test] #[test]
fn flatten_one_is_open() { fn flatten_one_is_open() {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut open = HashSet::new(); let mut open = HashSet::new();
open.insert(vec!["b"]); let hash = |s: &str| {
flatten_works(&open, &["a", "b", "c", "d", "g", "h"]); let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
};
open.insert(vec![hash("Bravo")]);
flatten_works(&open, &[hash("Alfa"), hash("Bravo"), hash("Charlie"), hash("Delta"), hash("Golf"), hash("Hotel")]);
} }
#[test] #[test]
fn flatten_all_open() { fn flatten_all_open() {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut open = HashSet::new(); let mut open = HashSet::new();
open.insert(vec!["b"]); let hash = |s: &str| {
open.insert(vec!["b", "d"]); let mut hasher = DefaultHasher::new();
flatten_works(&open, &["a", "b", "c", "d", "e", "f", "g", "h"]); s.hash(&mut hasher);
hasher.finish()
};
open.insert(vec![hash("Bravo")]);
open.insert(vec![hash("Bravo"), hash("Delta")]);
flatten_works(&open, &[hash("Alfa"), hash("Bravo"), hash("Charlie"), hash("Delta"), hash("Echo"), hash("Foxtrot"), hash("Golf"), hash("Hotel")]);
} }
+27 -25
View File
@@ -6,7 +6,8 @@ The user interaction state (like the current selection) is stored in the [`TreeS
*/ */
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::Display;
use std::hash::Hash;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::style::Style; use ratatui::style::Style;
@@ -24,9 +25,6 @@ mod tree_state;
/// A `Tree` which can be rendered. /// A `Tree` which can be rendered.
/// ///
/// The generic argument `Identifier` is used to keep the state like the currently selected or opened [`TreeItem`]s in the [`TreeState`].
/// For more information see [`TreeItem`].
///
/// # Example /// # Example
/// ///
/// ``` /// ```
@@ -37,7 +35,7 @@ mod tree_state;
/// # let mut terminal = Terminal::new(TestBackend::new(32, 32)).unwrap(); /// # let mut terminal = Terminal::new(TestBackend::new(32, 32)).unwrap();
/// let mut state = TreeState::default(); /// let mut state = TreeState::default();
/// ///
/// let item = TreeItem::new_leaf("l", "leaf"); /// let item = TreeItem::new_leaf("leaf");
/// let items = vec![item]; /// let items = vec![item];
/// ///
/// terminal.draw(|frame| { /// terminal.draw(|frame| {
@@ -53,12 +51,11 @@ mod tree_state;
/// ``` /// ```
#[must_use] #[must_use]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Tree<'a, Identifier, T> pub struct Tree<'a, T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, T: ToText + Clone + Default + Display + Hash,
T: ToText + Clone + Default,
{ {
items: &'a [TreeItem<Identifier, T>], items: &'a [TreeItem<T>],
block: Option<Block<'a>>, block: Option<Block<'a>>,
scrollbar: Option<Scrollbar<'a>>, scrollbar: Option<Scrollbar<'a>>,
@@ -78,17 +75,16 @@ where
node_no_children_symbol: &'a str, node_no_children_symbol: &'a str,
} }
impl<'a, Identifier, T> Tree<'a, Identifier, T> impl<'a, T> Tree<'a, T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, T: ToText + Clone + Default + Display + Hash,
T: ToText + Clone + Default,
{ {
/// Create a new `Tree`. /// Create a new `Tree`.
/// ///
/// # Errors /// # Errors
/// ///
/// Errors when there are duplicate identifiers in the children. /// Errors when there are duplicate identifiers in the children.
pub fn new(items: &'a [TreeItem<Identifier, T>]) -> std::io::Result<Self> { pub fn new(items: &'a [TreeItem<T>]) -> std::io::Result<Self> {
let identifiers = items let identifiers = items
.iter() .iter()
.map(|item| &item.identifier) .map(|item| &item.identifier)
@@ -163,18 +159,17 @@ where
#[test] #[test]
#[should_panic = "duplicate identifiers"] #[should_panic = "duplicate identifiers"]
fn tree_new_errors_with_duplicate_identifiers() { fn tree_new_errors_with_duplicate_identifiers() {
let item = TreeItem::new_leaf("same".to_owned(), "text".to_owned()); let item = TreeItem::new_leaf("text".to_owned());
let another = item.clone(); let another = item.clone();
let items = [item, another]; let items = [item, another];
let _ = Tree::new(&items).unwrap(); let _ = Tree::new(&items).unwrap();
} }
impl<'a, Identifier, T> StatefulWidget for Tree<'a, Identifier, T> impl<'a, T> StatefulWidget for Tree<'a, T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, T: ToText + Clone + Default + Display + Hash,
T: ToText + Clone + Default,
{ {
type State = TreeState<Identifier>; type State = TreeState;
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn render(self, full_area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, full_area: Rect, buf: &mut Buffer, state: &mut Self::State) {
@@ -339,10 +334,9 @@ where
} }
} }
impl<'a, Identifier, T> Widget for Tree<'a, Identifier, T> impl<'a, T> Widget for Tree<'a, T>
where where
Identifier: Clone + Default + Eq + core::hash::Hash, T: ToText + Clone + Default + Display + Hash,
T: ToText + 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();
@@ -352,11 +346,12 @@ where
#[cfg(test)] #[cfg(test)]
mod render_tests { mod render_tests {
use std::hash::{DefaultHasher, Hasher};
use super::*; use super::*;
#[must_use] #[must_use]
#[track_caller] #[track_caller]
fn render(width: u16, height: u16, state: &mut TreeState<&'static str>) -> Buffer { fn render(width: u16, height: u16, state: &mut TreeState) -> Buffer {
let items = TreeItem::example(); let items = TreeItem::example();
let tree = Tree::new(&items).unwrap(); let tree = Tree::new(&items).unwrap();
let area = Rect::new(0, 0, width, height); let area = Rect::new(0, 0, width, height);
@@ -389,7 +384,9 @@ mod render_tests {
#[test] #[test]
fn depth_one() { fn depth_one() {
let mut state = TreeState::default(); let mut state = TreeState::default();
state.open(vec!["b"]); let mut hasher = DefaultHasher::new();
"Bravo".hash(&mut hasher);
state.open(vec![hasher.finish()]);
let buffer = render(13, 7, &mut state); let buffer = render(13, 7, &mut state);
let expected = Buffer::with_lines([ let expected = Buffer::with_lines([
" Alfa ", " Alfa ",
@@ -406,8 +403,13 @@ mod render_tests {
#[test] #[test]
fn depth_two() { fn depth_two() {
let mut state = TreeState::default(); let mut state = TreeState::default();
state.open(vec!["b"]); let mut hasher = DefaultHasher::new();
state.open(vec!["b", "d"]); "Bravo".hash(&mut hasher);
let bravo_hash = hasher.finish();
let mut hasher = DefaultHasher::new();
"Delta".hash(&mut hasher);
state.open(vec![bravo_hash]);
state.open(vec![bravo_hash, hasher.finish()]);
let buffer = render(15, 9, &mut state); let buffer = render(15, 9, &mut state);
let expected = Buffer::with_lines([ let expected = Buffer::with_lines([
" Alfa ", " Alfa ",
+42 -39
View File
@@ -1,14 +1,15 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::Display;
use std::hash::{DefaultHasher, Hash, Hasher};
use ratatui::text::ToText; use ratatui::text::ToText;
/// One item inside a [`Tree`](crate::Tree). /// One item inside a [`Tree`](crate::Tree).
/// ///
/// Can have zero or more `children`. /// Can have zero or more `children`.
/// ///
/// # Identifier /// # identifier
/// ///
/// The generic argument `Identifier` is used to keep the state like the currently selected or opened [`TreeItem`]s in the [`TreeState`](crate::TreeState). /// The `identifier` is used to keep the state like the currently selected or opened [`TreeItem`]s in the [`TreeState`](crate::TreeState).
/// ///
/// It needs to be unique among its siblings but can be used again on parent or child [`TreeItem`]s. /// It needs to be unique among its siblings but can be used again on parent or child [`TreeItem`]s.
/// A common example would be a filename which has to be unique in its directory while it can exist in another. /// A common example would be a filename which has to be unique in its directory while it can exist in another.
@@ -29,42 +30,30 @@ use ratatui::text::ToText;
/// ///
/// ``` /// ```
/// # use managarr_tree_widget::TreeItem; /// # use managarr_tree_widget::TreeItem;
/// let a = TreeItem::new_leaf("l", "Leaf"); /// let a = TreeItem::new_leaf("Leaf");
/// let b = TreeItem::new("r", "Root", vec![a])?; /// let b = TreeItem::new("Root", vec![a])?;
/// # Ok::<(), std::io::Error>(()) /// # Ok::<(), std::io::Error>(())
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TreeItem<Identifier, T> pub struct TreeItem<T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, T: ToText + Clone + Default + Display + Hash,
T: ToText + Clone + Default,
{ {
pub(super) identifier: Identifier, pub(super) identifier: u64,
pub(super) content: T, pub(super) content: T,
pub(super) children: Vec<Self>, pub(super) children: Vec<Self>,
} }
impl<Identifier, T> TreeItem<Identifier, T> impl<T> TreeItem<T>
where where
Identifier: Clone + PartialEq + Eq + core::hash::Hash, T: ToText + Clone + Default + Display + Hash,
T: ToText + Clone + Default,
{ {
/// Create a new `TreeItem` without children.
#[must_use]
pub const fn new_leaf(identifier: Identifier, content: T) -> Self {
Self {
identifier,
content,
children: Vec::new(),
}
}
/// Create a new `TreeItem` with children. /// Create a new `TreeItem` with children.
/// ///
/// # Errors /// # Errors
/// ///
/// Errors when there are duplicate identifiers in the children. /// Errors when there are duplicate identifiers in the children.
pub fn new(identifier: Identifier, content: T, children: Vec<Self>) -> std::io::Result<Self> { pub fn new(content: T, children: Vec<Self>) -> std::io::Result<Self> {
let identifiers = children let identifiers = children
.iter() .iter()
.map(|item| &item.identifier) .map(|item| &item.identifier)
@@ -76,17 +65,33 @@ where
)); ));
} }
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
Ok(Self { Ok(Self {
identifier, identifier: hasher.finish(),
content, content,
children, children,
}) })
} }
/// Create a new `TreeItem` without children.
#[must_use]
pub fn new_leaf(content: T) -> Self {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
Self {
identifier: hasher.finish(),
content,
children: Vec::new(),
}
}
/// Get a reference to the identifier. /// Get a reference to the identifier.
#[must_use] #[must_use]
pub const fn identifier(&self) -> &Identifier { pub const fn identifier(&self) -> u64 {
&self.identifier self.identifier
} }
/// Get a reference to the text. /// Get a reference to the text.
@@ -142,31 +147,29 @@ where
} }
} }
impl TreeItem<&'static str, &'static str> { impl TreeItem<&'static str> {
#[cfg(test)] #[cfg(test)]
#[must_use] #[must_use]
pub(crate) fn example() -> Vec<Self> { pub(crate) fn example() -> Vec<Self> {
vec![ vec![
Self::new_leaf("a", "Alfa"), Self::new_leaf("Alfa"),
Self::new( Self::new(
"b",
"Bravo", "Bravo",
vec![ vec![
Self::new_leaf("c", "Charlie"), Self::new_leaf("Charlie"),
Self::new( Self::new(
"d",
"Delta", "Delta",
vec![ vec![
Self::new_leaf("e", "Echo"), Self::new_leaf("Echo"),
Self::new_leaf("f", "Foxtrot"), Self::new_leaf( "Foxtrot"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
Self::new_leaf("g", "Golf"), Self::new_leaf("Golf"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
Self::new_leaf("h", "Hotel"), Self::new_leaf( "Hotel"),
] ]
} }
} }
@@ -174,16 +177,16 @@ impl TreeItem<&'static str, &'static str> {
#[test] #[test]
#[should_panic = "duplicate identifiers"] #[should_panic = "duplicate identifiers"]
fn tree_item_new_errors_with_duplicate_identifiers() { fn tree_item_new_errors_with_duplicate_identifiers() {
let item = TreeItem::new_leaf("same", "text"); let item = TreeItem::new_leaf( "text");
let another = item.clone(); let another = item.clone();
TreeItem::new("root", "Root", vec![item, another]).unwrap(); TreeItem::new("Root", vec![item, another]).unwrap();
} }
#[test] #[test]
#[should_panic = "identifier already exists"] #[should_panic = "identifier already exists"]
fn tree_item_add_child_errors_with_duplicate_identifiers() { fn tree_item_add_child_errors_with_duplicate_identifiers() {
let item = TreeItem::new_leaf("same", "text"); let item = TreeItem::new_leaf("text");
let another = item.clone(); let another = item.clone();
let mut root = TreeItem::new("root", "Root", vec![item]).unwrap(); let mut root = TreeItem::new( "Root", vec![item]).unwrap();
root.add_child(another).unwrap(); root.add_child(another).unwrap();
} }
+23 -31
View File
@@ -1,5 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::Display;
use std::hash::Hash;
use ratatui::layout::{Position, Rect}; use ratatui::layout::{Position, Rect};
use ratatui::text::ToText; use ratatui::text::ToText;
@@ -8,37 +9,30 @@ use crate::tree_item::TreeItem;
/// Keeps the state of what is currently selected and what was opened in a [`Tree`](crate::Tree). /// Keeps the state of what is currently selected and what was opened in a [`Tree`](crate::Tree).
/// ///
/// The generic argument `Identifier` is used to keep the state like the currently selected or opened [`TreeItem`]s in the [`TreeState`].
/// For more information see [`TreeItem`].
///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use managarr_tree_widget::TreeState; /// # use managarr_tree_widget::TreeState;
/// type Identifier = usize;
/// ///
/// let mut state = TreeState::<Identifier>::default(); /// let mut state = TreeState::default();
/// ``` /// ```
#[must_use] #[must_use]
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct TreeState<Identifier> { pub struct TreeState {
pub(super) offset: usize, pub(super) offset: usize,
pub(super) opened: HashSet<Vec<Identifier>>, pub(super) opened: HashSet<Vec<u64>>,
pub(super) selected: Vec<Identifier>, pub(super) selected: Vec<u64>,
pub(super) ensure_selected_in_view_on_next_render: bool, pub(super) ensure_selected_in_view_on_next_render: bool,
pub(super) last_area: Rect, pub(super) last_area: Rect,
pub(super) last_biggest_index: usize, pub(super) last_biggest_index: usize,
/// All identifiers open on last render /// All identifiers open on last render
pub(super) last_identifiers: Vec<Vec<Identifier>>, pub(super) last_identifiers: Vec<Vec<u64>>,
/// Identifier rendered at `y` on last render /// Identifier rendered at `y` on last render
pub(super) last_rendered_identifiers: Vec<(u16, Vec<Identifier>)>, pub(super) last_rendered_identifiers: Vec<(u16, Vec<u64>)>,
} }
impl<Identifier> TreeState<Identifier> impl TreeState {
where
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
{
#[must_use] #[must_use]
pub const fn get_offset(&self) -> usize { pub const fn get_offset(&self) -> usize {
self.offset self.offset
@@ -46,17 +40,17 @@ where
#[must_use] #[must_use]
#[deprecated = "Use self.opened()"] #[deprecated = "Use self.opened()"]
pub fn get_all_opened(&self) -> Vec<Vec<Identifier>> { pub fn get_all_opened(&self) -> Vec<Vec<u64>> {
self.opened.iter().cloned().collect() self.opened.iter().cloned().collect()
} }
#[must_use] #[must_use]
pub const fn opened(&self) -> &HashSet<Vec<Identifier>> { pub const fn opened(&self) -> &HashSet<Vec<u64>> {
&self.opened &self.opened
} }
#[must_use] #[must_use]
pub fn selected(&self) -> &[Identifier] { pub fn selected(&self) -> &[u64] {
&self.selected &self.selected
} }
@@ -64,10 +58,10 @@ where
#[must_use] #[must_use]
pub fn flatten<'a, T>( pub fn flatten<'a, T>(
&self, &self,
items: &'a [TreeItem<Identifier, T>], items: &'a [TreeItem<T>],
) -> Vec<Flattened<'a, Identifier, T>> ) -> Vec<Flattened<'a, T>>
where where
T: ToText + Clone + Default, T: ToText + Clone + Default + Display + Hash,
{ {
flatten(&self.opened, items, &[]) flatten(&self.opened, items, &[])
} }
@@ -80,10 +74,10 @@ where
/// ///
/// ```rust /// ```rust
/// # use managarr_tree_widget::TreeState; /// # use managarr_tree_widget::TreeState;
/// # let mut state = TreeState::<usize>::default(); /// # let mut state = TreeState::default();
/// state.select(Vec::new()); /// state.select(Vec::new());
/// ``` /// ```
pub fn select(&mut self, identifier: Vec<Identifier>) -> bool { pub fn select(&mut self, identifier: Vec<u64>) -> bool {
self.ensure_selected_in_view_on_next_render = true; self.ensure_selected_in_view_on_next_render = true;
let changed = self.selected != identifier; let changed = self.selected != identifier;
self.selected = identifier; self.selected = identifier;
@@ -93,7 +87,7 @@ where
/// Open a tree node. /// Open a tree node.
/// Returns `true` when it was closed and has been opened. /// Returns `true` when it was closed and has been opened.
/// Returns `false` when it was already open. /// Returns `false` when it was already open.
pub fn open(&mut self, identifier: Vec<Identifier>) -> bool { pub fn open(&mut self, identifier: Vec<u64>) -> bool {
if identifier.is_empty() { if identifier.is_empty() {
false false
} else { } else {
@@ -104,7 +98,7 @@ where
/// Close a tree node. /// Close a tree node.
/// Returns `true` when it was open and has been closed. /// Returns `true` when it was open and has been closed.
/// Returns `false` when it was already closed. /// Returns `false` when it was already closed.
pub fn close(&mut self, identifier: &[Identifier]) -> bool { pub fn close(&mut self, identifier: &[u64]) -> bool {
self.opened.remove(identifier) self.opened.remove(identifier)
} }
@@ -113,7 +107,7 @@ where
/// ///
/// Returns `true` when a node is opened / closed. /// Returns `true` when a node is opened / closed.
/// As toggle always changes something, this only returns `false` when an empty identifier is given. /// As toggle always changes something, this only returns `false` when an empty identifier is given.
pub fn toggle(&mut self, identifier: Vec<Identifier>) -> bool { pub fn toggle(&mut self, identifier: Vec<u64>) -> bool {
if identifier.is_empty() { if identifier.is_empty() {
false false
} else if self.opened.contains(&identifier) { } else if self.opened.contains(&identifier) {
@@ -196,8 +190,7 @@ where
/// ///
/// ``` /// ```
/// # use managarr_tree_widget::TreeState; /// # use managarr_tree_widget::TreeState;
/// # type Identifier = usize; /// # let mut state = TreeState::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
@@ -231,8 +224,7 @@ where
/// ///
/// ``` /// ```
/// # use managarr_tree_widget::TreeState; /// # use managarr_tree_widget::TreeState;
/// # type Identifier = usize; /// # let mut state = TreeState::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
@@ -259,7 +251,7 @@ where
/// Get the identifier that was rendered for the given position on last render. /// Get the identifier that was rendered for the given position on last render.
#[must_use] #[must_use]
pub fn rendered_at(&self, position: Position) -> Option<&[Identifier]> { pub fn rendered_at(&self, position: Position) -> Option<&[u64]> {
if !self.last_area.contains(position) { if !self.last_area.contains(position) {
return None; return None;
} }