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:
+26
-31
@@ -7,64 +7,59 @@ use ratatui::widgets::StatefulWidget;
|
||||
use managarr_tree_widget::{Tree, TreeItem, TreeState};
|
||||
|
||||
#[must_use]
|
||||
fn example_items() -> Vec<TreeItem<&'static str, &'static str>> {
|
||||
fn example_items() -> Vec<TreeItem<&'static str>> {
|
||||
vec![
|
||||
TreeItem::new_leaf("a", "Alfa"),
|
||||
TreeItem::new_leaf( "Alfa"),
|
||||
TreeItem::new(
|
||||
"b",
|
||||
"Bravo",
|
||||
vec![
|
||||
TreeItem::new_leaf("c", "Charlie"),
|
||||
TreeItem::new_leaf( "Charlie"),
|
||||
TreeItem::new(
|
||||
"d",
|
||||
"Delta",
|
||||
vec![
|
||||
TreeItem::new_leaf("e", "Echo"),
|
||||
TreeItem::new_leaf("f", "Foxtrot"),
|
||||
TreeItem::new_leaf( "Echo"),
|
||||
TreeItem::new_leaf( "Foxtrot"),
|
||||
],
|
||||
)
|
||||
.expect("all item identifiers are unique"),
|
||||
TreeItem::new_leaf("g", "Golf"),
|
||||
TreeItem::new_leaf( "Golf"),
|
||||
],
|
||||
)
|
||||
.expect("all item identifiers are unique"),
|
||||
TreeItem::new_leaf("h", "Hotel"),
|
||||
TreeItem::new_leaf( "Hotel"),
|
||||
TreeItem::new(
|
||||
"i",
|
||||
"India",
|
||||
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( "Juliet"),
|
||||
TreeItem::new_leaf("Kilo"),
|
||||
TreeItem::new_leaf("Lima"),
|
||||
TreeItem::new_leaf("Mike"),
|
||||
TreeItem::new_leaf("November"),
|
||||
],
|
||||
)
|
||||
.expect("all item identifiers are unique"),
|
||||
TreeItem::new_leaf("o", "Oscar"),
|
||||
TreeItem::new_leaf( "Oscar"),
|
||||
TreeItem::new(
|
||||
"p",
|
||||
"Papa",
|
||||
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( "Quebec"),
|
||||
TreeItem::new_leaf( "Romeo"),
|
||||
TreeItem::new_leaf( "Sierra"),
|
||||
TreeItem::new_leaf( "Tango"),
|
||||
TreeItem::new_leaf( "Uniform"),
|
||||
TreeItem::new(
|
||||
"v",
|
||||
"Victor",
|
||||
vec![
|
||||
TreeItem::new_leaf("w", "Whiskey"),
|
||||
TreeItem::new_leaf("x", "Xray"),
|
||||
TreeItem::new_leaf("y", "Yankee"),
|
||||
TreeItem::new_leaf("Whiskey"),
|
||||
TreeItem::new_leaf ("Xray"),
|
||||
TreeItem::new_leaf( "Yankee"),
|
||||
],
|
||||
)
|
||||
.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| {
|
||||
bencher.iter(|| {
|
||||
let items = Vec::<TreeItem<String, String>>::new();
|
||||
let items = Vec::<TreeItem<String>>::new();
|
||||
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);
|
||||
|
||||
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 mut state = TreeState::default();
|
||||
bencher.iter_batched(
|
||||
@@ -112,8 +107,8 @@ fn renders(criterion: &mut Criterion) {
|
||||
let items = example_items();
|
||||
let tree = Tree::new(&items).unwrap();
|
||||
let mut state = TreeState::default();
|
||||
state.open(vec!["b"]);
|
||||
state.open(vec!["b", "d"]);
|
||||
state.open(vec![2]);
|
||||
state.open(vec![2, 4]);
|
||||
bencher.iter_batched(
|
||||
|| (tree.clone(), Buffer::empty(buffer_size)),
|
||||
|(tree, mut buffer)| {
|
||||
|
||||
+23
-28
@@ -11,8 +11,8 @@ use managarr_tree_widget::{Tree, TreeItem, TreeState};
|
||||
|
||||
#[must_use]
|
||||
struct App {
|
||||
state: TreeState<&'static str>,
|
||||
items: Vec<TreeItem<&'static str, &'static str>>,
|
||||
state: TreeState,
|
||||
items: Vec<TreeItem<&'static str>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -20,62 +20,57 @@ impl App {
|
||||
Self {
|
||||
state: TreeState::default(),
|
||||
items: vec![
|
||||
TreeItem::new_leaf("a", "Alfa"),
|
||||
TreeItem::new_leaf( "Alfa"),
|
||||
TreeItem::new(
|
||||
"b",
|
||||
"Bravo",
|
||||
vec![
|
||||
TreeItem::new_leaf("c", "Charlie"),
|
||||
TreeItem::new_leaf( "Charlie"),
|
||||
TreeItem::new(
|
||||
"d",
|
||||
"Delta",
|
||||
vec![
|
||||
TreeItem::new_leaf("e", "Echo"),
|
||||
TreeItem::new_leaf("f", "Foxtrot"),
|
||||
TreeItem::new_leaf("Echo"),
|
||||
TreeItem::new_leaf( "Foxtrot"),
|
||||
],
|
||||
)
|
||||
.expect("all item identifiers are unique"),
|
||||
TreeItem::new_leaf("g", "Golf"),
|
||||
TreeItem::new_leaf( "Golf"),
|
||||
],
|
||||
)
|
||||
.expect("all item identifiers are unique"),
|
||||
TreeItem::new_leaf("h", "Hotel"),
|
||||
TreeItem::new_leaf("Hotel"),
|
||||
TreeItem::new(
|
||||
"i",
|
||||
"India",
|
||||
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( "Juliet"),
|
||||
TreeItem::new_leaf( "Kilo"),
|
||||
TreeItem::new_leaf( "Lima"),
|
||||
TreeItem::new_leaf( "Mike"),
|
||||
TreeItem::new_leaf( "November"),
|
||||
],
|
||||
)
|
||||
.expect("all item identifiers are unique"),
|
||||
TreeItem::new_leaf("o", "Oscar"),
|
||||
TreeItem::new_leaf( "Oscar"),
|
||||
TreeItem::new(
|
||||
"p",
|
||||
"Papa",
|
||||
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( "Quebec"),
|
||||
TreeItem::new_leaf( "Romeo"),
|
||||
TreeItem::new_leaf( "Sierra"),
|
||||
TreeItem::new_leaf( "Tango"),
|
||||
TreeItem::new_leaf( "Uniform"),
|
||||
TreeItem::new(
|
||||
"v",
|
||||
"Victor",
|
||||
vec![
|
||||
TreeItem::new_leaf("w", "Whiskey"),
|
||||
TreeItem::new_leaf("x", "Xray"),
|
||||
TreeItem::new_leaf("y", "Yankee"),
|
||||
TreeItem::new_leaf( "Whiskey"),
|
||||
TreeItem::new_leaf( "Xray"),
|
||||
TreeItem::new_leaf( "Yankee"),
|
||||
],
|
||||
)
|
||||
.expect("all item identifiers are unique"),
|
||||
],
|
||||
)
|
||||
.expect("all item identifiers are unique"),
|
||||
TreeItem::new_leaf("z", "Zulu"),
|
||||
TreeItem::new_leaf( "Zulu"),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
+57
-29
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
use crate::tree_item::TreeItem;
|
||||
use ratatui::text::ToText;
|
||||
|
||||
@@ -7,19 +8,17 @@ use ratatui::text::ToText;
|
||||
///
|
||||
/// Generated via [`TreeState::flatten`](crate::TreeState::flatten).
|
||||
#[must_use]
|
||||
pub struct Flattened<'a, Identifier, T>
|
||||
pub struct Flattened<'a, T>
|
||||
where
|
||||
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
pub identifier: Vec<Identifier>,
|
||||
pub item: &'a TreeItem<Identifier, T>,
|
||||
pub identifier: Vec<u64>,
|
||||
pub item: &'a TreeItem<T>,
|
||||
}
|
||||
|
||||
impl<'a, Identifier, T> Flattened<'a, Identifier, T>
|
||||
impl<'a, T> Flattened<'a, T>
|
||||
where
|
||||
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
/// Zero based depth. Depth 0 means top level with 0 indentation.
|
||||
#[must_use]
|
||||
@@ -32,19 +31,18 @@ where
|
||||
///
|
||||
/// `current` starts empty: `&[]`
|
||||
#[must_use]
|
||||
pub fn flatten<'a, Identifier, T>(
|
||||
open_identifiers: &HashSet<Vec<Identifier>>,
|
||||
items: &'a [TreeItem<Identifier, T>],
|
||||
current: &[Identifier],
|
||||
) -> Vec<Flattened<'a, Identifier, T>>
|
||||
pub fn flatten<'a, T>(
|
||||
open_identifiers: &HashSet<Vec<u64>>,
|
||||
items: &'a [TreeItem<T>],
|
||||
current: &[u64],
|
||||
) -> Vec<Flattened<'a, T>>
|
||||
where
|
||||
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
let mut result = Vec::new();
|
||||
for item in items {
|
||||
let mut child_identifier = current.to_vec();
|
||||
child_identifier.push(item.identifier.clone());
|
||||
child_identifier.push(item.identifier);
|
||||
|
||||
let child_result = open_identifiers
|
||||
.contains(&child_identifier)
|
||||
@@ -64,9 +62,15 @@ where
|
||||
|
||||
#[test]
|
||||
fn depth_works() {
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
let mut open = HashSet::new();
|
||||
open.insert(vec!["b"]);
|
||||
open.insert(vec!["b", "d"]);
|
||||
let hash = |s: &str| {
|
||||
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(), &[])
|
||||
.into_iter()
|
||||
.map(|flattened| flattened.depth())
|
||||
@@ -75,7 +79,7 @@ fn depth_works() {
|
||||
}
|
||||
|
||||
#[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 result = flatten(open, &items, &[]);
|
||||
let actual = result
|
||||
@@ -87,29 +91,53 @@ fn flatten_works(open: &HashSet<Vec<&'static str>>, expected: &[&str]) {
|
||||
|
||||
#[test]
|
||||
fn flatten_nothing_open_is_top_level() {
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
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]
|
||||
fn flatten_wrong_open_is_only_top_level() {
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
let mut open = HashSet::new();
|
||||
open.insert(vec!["a"]);
|
||||
open.insert(vec!["b", "d"]);
|
||||
flatten_works(&open, &["a", "b", "h"]);
|
||||
let hash = |s: &str| {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
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]
|
||||
fn flatten_one_is_open() {
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
let mut open = HashSet::new();
|
||||
open.insert(vec!["b"]);
|
||||
flatten_works(&open, &["a", "b", "c", "d", "g", "h"]);
|
||||
let hash = |s: &str| {
|
||||
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]
|
||||
fn flatten_all_open() {
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
let mut open = HashSet::new();
|
||||
open.insert(vec!["b"]);
|
||||
open.insert(vec!["b", "d"]);
|
||||
flatten_works(&open, &["a", "b", "c", "d", "e", "f", "g", "h"]);
|
||||
let hash = |s: &str| {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
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
@@ -6,7 +6,8 @@ The user interaction state (like the current selection) is stored in the [`TreeS
|
||||
*/
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
@@ -24,9 +25,6 @@ mod tree_state;
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// ```
|
||||
@@ -37,7 +35,7 @@ mod tree_state;
|
||||
/// # let mut terminal = Terminal::new(TestBackend::new(32, 32)).unwrap();
|
||||
/// let mut state = TreeState::default();
|
||||
///
|
||||
/// let item = TreeItem::new_leaf("l", "leaf");
|
||||
/// let item = TreeItem::new_leaf("leaf");
|
||||
/// let items = vec![item];
|
||||
///
|
||||
/// terminal.draw(|frame| {
|
||||
@@ -53,12 +51,11 @@ mod tree_state;
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tree<'a, Identifier, T>
|
||||
pub struct Tree<'a, T>
|
||||
where
|
||||
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
items: &'a [TreeItem<Identifier, T>],
|
||||
items: &'a [TreeItem<T>],
|
||||
|
||||
block: Option<Block<'a>>,
|
||||
scrollbar: Option<Scrollbar<'a>>,
|
||||
@@ -78,17 +75,16 @@ where
|
||||
node_no_children_symbol: &'a str,
|
||||
}
|
||||
|
||||
impl<'a, Identifier, T> Tree<'a, Identifier, T>
|
||||
impl<'a, T> Tree<'a, T>
|
||||
where
|
||||
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
/// Create a new `Tree`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 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
|
||||
.iter()
|
||||
.map(|item| &item.identifier)
|
||||
@@ -163,18 +159,17 @@ where
|
||||
#[test]
|
||||
#[should_panic = "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 items = [item, another];
|
||||
let _ = Tree::new(&items).unwrap();
|
||||
}
|
||||
|
||||
impl<'a, Identifier, T> StatefulWidget for Tree<'a, Identifier, T>
|
||||
impl<'a, T> StatefulWidget for Tree<'a, T>
|
||||
where
|
||||
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
type State = TreeState<Identifier>;
|
||||
type State = TreeState;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
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
|
||||
Identifier: Clone + Default + Eq + core::hash::Hash,
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = TreeState::default();
|
||||
@@ -352,11 +346,12 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod render_tests {
|
||||
use std::hash::{DefaultHasher, Hasher};
|
||||
use super::*;
|
||||
|
||||
#[must_use]
|
||||
#[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 tree = Tree::new(&items).unwrap();
|
||||
let area = Rect::new(0, 0, width, height);
|
||||
@@ -389,7 +384,9 @@ mod render_tests {
|
||||
#[test]
|
||||
fn depth_one() {
|
||||
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 expected = Buffer::with_lines([
|
||||
" Alfa ",
|
||||
@@ -406,8 +403,13 @@ mod render_tests {
|
||||
#[test]
|
||||
fn depth_two() {
|
||||
let mut state = TreeState::default();
|
||||
state.open(vec!["b"]);
|
||||
state.open(vec!["b", "d"]);
|
||||
let mut hasher = DefaultHasher::new();
|
||||
"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 expected = Buffer::with_lines([
|
||||
" Alfa ",
|
||||
|
||||
+42
-39
@@ -1,14 +1,15 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use ratatui::text::ToText;
|
||||
|
||||
/// One item inside a [`Tree`](crate::Tree).
|
||||
///
|
||||
/// 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.
|
||||
/// 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;
|
||||
/// let a = TreeItem::new_leaf("l", "Leaf");
|
||||
/// let b = TreeItem::new("r", "Root", vec![a])?;
|
||||
/// let a = TreeItem::new_leaf("Leaf");
|
||||
/// let b = TreeItem::new("Root", vec![a])?;
|
||||
/// # Ok::<(), std::io::Error>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TreeItem<Identifier, T>
|
||||
pub struct TreeItem<T>
|
||||
where
|
||||
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
pub(super) identifier: Identifier,
|
||||
pub(super) identifier: u64,
|
||||
pub(super) content: T,
|
||||
pub(super) children: Vec<Self>,
|
||||
}
|
||||
|
||||
impl<Identifier, T> TreeItem<Identifier, T>
|
||||
impl<T> TreeItem<T>
|
||||
where
|
||||
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
/// 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.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 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
|
||||
.iter()
|
||||
.map(|item| &item.identifier)
|
||||
@@ -76,17 +65,33 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
content.hash(&mut hasher);
|
||||
|
||||
Ok(Self {
|
||||
identifier,
|
||||
identifier: hasher.finish(),
|
||||
content,
|
||||
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.
|
||||
#[must_use]
|
||||
pub const fn identifier(&self) -> &Identifier {
|
||||
&self.identifier
|
||||
pub const fn identifier(&self) -> u64 {
|
||||
self.identifier
|
||||
}
|
||||
|
||||
/// Get a reference to the text.
|
||||
@@ -142,31 +147,29 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeItem<&'static str, &'static str> {
|
||||
impl TreeItem<&'static str> {
|
||||
#[cfg(test)]
|
||||
#[must_use]
|
||||
pub(crate) fn example() -> Vec<Self> {
|
||||
vec![
|
||||
Self::new_leaf("a", "Alfa"),
|
||||
Self::new_leaf("Alfa"),
|
||||
Self::new(
|
||||
"b",
|
||||
"Bravo",
|
||||
vec![
|
||||
Self::new_leaf("c", "Charlie"),
|
||||
Self::new_leaf("Charlie"),
|
||||
Self::new(
|
||||
"d",
|
||||
"Delta",
|
||||
vec![
|
||||
Self::new_leaf("e", "Echo"),
|
||||
Self::new_leaf("f", "Foxtrot"),
|
||||
Self::new_leaf("Echo"),
|
||||
Self::new_leaf( "Foxtrot"),
|
||||
],
|
||||
)
|
||||
.expect("all item identifiers are unique"),
|
||||
Self::new_leaf("g", "Golf"),
|
||||
Self::new_leaf("Golf"),
|
||||
],
|
||||
)
|
||||
.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]
|
||||
#[should_panic = "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();
|
||||
TreeItem::new("root", "Root", vec![item, another]).unwrap();
|
||||
TreeItem::new("Root", vec![item, another]).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "identifier already exists"]
|
||||
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 mut root = TreeItem::new("root", "Root", vec![item]).unwrap();
|
||||
let mut root = TreeItem::new( "Root", vec![item]).unwrap();
|
||||
root.add_child(another).unwrap();
|
||||
}
|
||||
|
||||
+23
-31
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
use ratatui::layout::{Position, Rect};
|
||||
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).
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ```
|
||||
/// # use managarr_tree_widget::TreeState;
|
||||
/// type Identifier = usize;
|
||||
///
|
||||
/// let mut state = TreeState::<Identifier>::default();
|
||||
/// let mut state = TreeState::default();
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TreeState<Identifier> {
|
||||
pub struct TreeState {
|
||||
pub(super) offset: usize,
|
||||
pub(super) opened: HashSet<Vec<Identifier>>,
|
||||
pub(super) selected: Vec<Identifier>,
|
||||
pub(super) opened: HashSet<Vec<u64>>,
|
||||
pub(super) selected: Vec<u64>,
|
||||
pub(super) ensure_selected_in_view_on_next_render: bool,
|
||||
|
||||
pub(super) last_area: Rect,
|
||||
pub(super) last_biggest_index: usize,
|
||||
/// 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
|
||||
pub(super) last_rendered_identifiers: Vec<(u16, Vec<Identifier>)>,
|
||||
pub(super) last_rendered_identifiers: Vec<(u16, Vec<u64>)>,
|
||||
}
|
||||
|
||||
impl<Identifier> TreeState<Identifier>
|
||||
where
|
||||
Identifier: Clone + PartialEq + Eq + core::hash::Hash,
|
||||
{
|
||||
impl TreeState {
|
||||
#[must_use]
|
||||
pub const fn get_offset(&self) -> usize {
|
||||
self.offset
|
||||
@@ -46,17 +40,17 @@ where
|
||||
|
||||
#[must_use]
|
||||
#[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()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn opened(&self) -> &HashSet<Vec<Identifier>> {
|
||||
pub const fn opened(&self) -> &HashSet<Vec<u64>> {
|
||||
&self.opened
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn selected(&self) -> &[Identifier] {
|
||||
pub fn selected(&self) -> &[u64] {
|
||||
&self.selected
|
||||
}
|
||||
|
||||
@@ -64,10 +58,10 @@ where
|
||||
#[must_use]
|
||||
pub fn flatten<'a, T>(
|
||||
&self,
|
||||
items: &'a [TreeItem<Identifier, T>],
|
||||
) -> Vec<Flattened<'a, Identifier, T>>
|
||||
items: &'a [TreeItem<T>],
|
||||
) -> Vec<Flattened<'a, T>>
|
||||
where
|
||||
T: ToText + Clone + Default,
|
||||
T: ToText + Clone + Default + Display + Hash,
|
||||
{
|
||||
flatten(&self.opened, items, &[])
|
||||
}
|
||||
@@ -80,10 +74,10 @@ where
|
||||
///
|
||||
/// ```rust
|
||||
/// # use managarr_tree_widget::TreeState;
|
||||
/// # let mut state = TreeState::<usize>::default();
|
||||
/// # let mut state = TreeState::default();
|
||||
/// 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;
|
||||
let changed = self.selected != identifier;
|
||||
self.selected = identifier;
|
||||
@@ -93,7 +87,7 @@ where
|
||||
/// Open a tree node.
|
||||
/// Returns `true` when it was closed and has been opened.
|
||||
/// 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() {
|
||||
false
|
||||
} else {
|
||||
@@ -104,7 +98,7 @@ where
|
||||
/// Close a tree node.
|
||||
/// Returns `true` when it was open and has been 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)
|
||||
}
|
||||
|
||||
@@ -113,7 +107,7 @@ where
|
||||
///
|
||||
/// Returns `true` when a node is opened / closed.
|
||||
/// 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() {
|
||||
false
|
||||
} else if self.opened.contains(&identifier) {
|
||||
@@ -196,8 +190,7 @@ where
|
||||
///
|
||||
/// ```
|
||||
/// # use managarr_tree_widget::TreeState;
|
||||
/// # type Identifier = usize;
|
||||
/// # let mut state = TreeState::<Identifier>::default();
|
||||
/// # let mut state = TreeState::default();
|
||||
/// // Move the selection one down
|
||||
/// state.select_relative(|current| {
|
||||
/// // When nothing is currently selected, select index 0
|
||||
@@ -231,8 +224,7 @@ where
|
||||
///
|
||||
/// ```
|
||||
/// # use managarr_tree_widget::TreeState;
|
||||
/// # type Identifier = usize;
|
||||
/// # let mut state = TreeState::<Identifier>::default();
|
||||
/// # let mut state = TreeState::default();
|
||||
/// // Move the selection one down
|
||||
/// state.select_relative(|current| {
|
||||
/// // 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.
|
||||
#[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) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user