Compare commits

...

10 Commits

Author SHA1 Message Date
4f289d1075 chore: Upgrade to Ratatui v0.30.0
Test Coverage / coverage (push) Has been cancelled
Rust / rustfmt (push) Has been cancelled
Rust / clippy (beta) (push) Has been cancelled
Rust / clippy (nightly) (push) Has been cancelled
Rust / clippy (stable) (push) Has been cancelled
Rust / features (macos-latest, beta) (push) Has been cancelled
Rust / features (macos-latest, stable) (push) Has been cancelled
Rust / features (ubuntu-latest, beta) (push) Has been cancelled
Rust / features (ubuntu-latest, stable) (push) Has been cancelled
Rust / features (windows-latest, beta) (push) Has been cancelled
Rust / features (windows-latest, stable) (push) Has been cancelled
Rust / test (macos-latest, beta) (push) Has been cancelled
Rust / test (macos-latest, nightly) (push) Has been cancelled
Rust / test (macos-latest, stable) (push) Has been cancelled
Rust / test (ubuntu-latest, beta) (push) Has been cancelled
Rust / test (ubuntu-latest, nightly) (push) Has been cancelled
Rust / test (ubuntu-latest, stable) (push) Has been cancelled
Rust / test (windows-latest, beta) (push) Has been cancelled
Rust / test (windows-latest, nightly) (push) Has been cancelled
Rust / test (windows-latest, stable) (push) Has been cancelled
Rust / Release aarch64-apple-darwin (push) Has been cancelled
Rust / Release x86_64-apple-darwin (push) Has been cancelled
Rust / Release aarch64-unknown-linux-gnu (push) Has been cancelled
Rust / Release arm-unknown-linux-gnueabihf (push) Has been cancelled
Rust / Release armv7-unknown-linux-gnueabihf (push) Has been cancelled
Rust / Release riscv64gc-unknown-linux-gnu (push) Has been cancelled
Rust / Release x86_64-unknown-linux-gnu (push) Has been cancelled
Rust / Release aarch64-pc-windows-msvc (push) Has been cancelled
Rust / Release x86_64-pc-windows-msvc (push) Has been cancelled
2026-01-07 17:07:14 -07:00
45523fa08e chore: Updated the README to have more detailed instructions on how to use the widget 2024-11-18 14:46:19 -07:00
Alex Clarke
6a324736e5 chore: Updated the README to have installation instructions since this crate is not published to crates.io 2024-11-18 13:14:33 -07:00
b0d8d9f0bf feat(TreeItem): Added PartialEq and Eq constraints to the types that can be passed to the TreeItem struct 2024-11-15 14:40:44 -07:00
959b60de32 style: Applied formatting to all files 2024-11-15 12:50:14 -07:00
6baef469c8 Fixed a typo in the README 2024-11-14 18:39:10 -07:00
6c10db760d 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 2024-11-14 17:35:40 -07:00
8a91f662dd fix: Changed the type constraints to use the ToText Ratatui trait 2024-11-14 16:38:38 -07:00
37d7b77f90 fix: Corrected doctests to pass 2024-11-14 16:04:39 -07:00
3e23a73f6b 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. 2024-11-14 15:38:37 -07:00
8 changed files with 355 additions and 259 deletions
+13 -10
View File
@@ -1,15 +1,18 @@
[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.25.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"]
rust-version = "1.82.0"
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
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"
@@ -23,15 +26,15 @@ debug = true
lto = true lto = true
[dependencies] [dependencies]
ratatui = { version = "0.29", default-features = false } ratatui = { version = "0.30", default-features = false }
unicode-width = "0.2" unicode-width = "0.2"
[dev-dependencies] [dev-dependencies]
criterion = "0.5" criterion = "0.5"
ratatui = "0.29" ratatui = "0.30"
[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"
+78 -2
View File
@@ -1,7 +1,83 @@
# 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). ## Installation
Add this widget to your project using the following command:
```shell
cargo add managarr-tree-widget
```
## Running the example
To run the example widget, simply run:
```shell
cargo run --example example
```
## Usage
The following is an example of how to create a tree of strings (namely one like the one used in the [example](./examples/example.rs)):
```rust
fn draw(&mut self, frame: &mut Frame) {
let area = frame.area();
let tree_items = vec![
TreeItem::new_leaf("Alfa"),
TreeItem::new(
"Bravo",
vec![
TreeItem::new_leaf("Charlie"),
TreeItem::new(
"Delta",
vec![TreeItem::new_leaf("Echo"), TreeItem::new_leaf("Foxtrot")],
)
.expect("all item identifiers are unique"),
TreeItem::new_leaf("Golf"),
],
)
.expect("all item identifiers are unique"),
TreeItem::new_leaf("Hotel"),
];
let widget = Tree::new(&tree_items)
.expect("all item identifiers are unique")
.block(
Block::bordered()
.title("Tree Widget"),
)
.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);
}
```
This will generate the following tree structure:
```
┌── Alfa
├── Bravo
│ ├── Charlie
│ ├── Delta
│ │ ├── Echo
│ │ └── Foxtrot
│ └── Golf
└── Hotel
```
This example assumes the existence of a `self.state` field that is initialized with `TreeState::default()`. The `TreeItem` struct is used to create a tree of items, and the `Tree` struct is used to create the widget itself.
A more detailed and feature-complete example is available in the [example](./examples/example.rs) file.
## Credit
The original project for this widget is the [Ratatui Tree Widget](https://github.com/EdJoPaTo/tui-rs-tree-widget), which was purpose built for the specific use
case of [`mqttui`](https://github.com/EdJoPaTo/mqttui).
The updated version of the tree widget that allows more generic types is created by me, [Alex Clarke](https://github.com/Dark-Alex-17).
+28 -36
View File
@@ -1,70 +1,62 @@
use std::hint::black_box; use std::hint::black_box;
use criterion::{criterion_group, criterion_main, BatchSize, Criterion, Throughput}; use criterion::{criterion_group, criterion_main, BatchSize, Criterion, Throughput};
use managarr_tree_widget::{Tree, TreeItem, TreeState};
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};
#[must_use] #[must_use]
fn example_items() -> Vec<TreeItem<'static, &'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("Echo"), TreeItem::new_leaf("Foxtrot")],
TreeItem::new_leaf("e", "Echo"),
TreeItem::new_leaf("f", "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,15 +66,15 @@ 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::<TreeItem<String>>::new();
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 +88,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<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 +104,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)| {
+40 -58
View File
@@ -1,18 +1,18 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use managarr_tree_widget::{Tree, TreeItem, TreeState};
use ratatui::backend::{Backend, CrosstermBackend}; use ratatui::backend::{Backend, CrosstermBackend};
use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers, MouseEventKind}; use ratatui::crossterm::event::{Event, KeyCode, KeyModifiers, MouseEventKind};
use ratatui::layout::{Position, Rect}; use ratatui::layout::{Position, Rect};
use ratatui::style::{Color, Modifier, Style}; 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::{Frame, Terminal, crossterm};
use tui_tree_widget::{Tree, TreeItem, TreeState};
#[must_use] #[must_use]
struct App { struct App {
state: TreeState<&'static str>, state: TreeState,
items: Vec<TreeItem<&'static str, String>>, items: Vec<TreeItem<&'static str>>,
} }
impl App { impl App {
@@ -20,86 +20,60 @@ impl App {
Self { Self {
state: TreeState::default(), state: TreeState::default(),
items: vec![ items: vec![
TreeItem::new_leaf("a", "Alfa".to_owned()), TreeItem::new_leaf("Alfa"),
TreeItem::new( TreeItem::new(
"b", "Bravo",
"Bravo".to_owned(),
vec![ vec![
TreeItem::new_leaf("c", "Charlie".to_owned()), TreeItem::new_leaf("Charlie"),
TreeItem::new( TreeItem::new(
"d", "Delta",
"Delta".to_owned(), vec![TreeItem::new_leaf("Echo"), TreeItem::new_leaf("Foxtrot")],
vec![
TreeItem::new_leaf("e", "Echo".to_owned()),
TreeItem::new_leaf("f", "Foxtrot".to_owned()),
],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("g", "Golf".to_owned()), TreeItem::new_leaf("Golf"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("h", "Hotel".to_owned()), TreeItem::new_leaf("Hotel"),
TreeItem::new( TreeItem::new(
"i", "India",
"India".to_owned(),
vec![ vec![
TreeItem::new_leaf("j", "Juliett".to_owned()), TreeItem::new_leaf("Juliet"),
TreeItem::new_leaf("k", "Kilo".to_owned()), TreeItem::new_leaf("Kilo"),
TreeItem::new_leaf("l", "Lima".to_owned()), TreeItem::new_leaf("Lima"),
TreeItem::new_leaf("m", "Mike".to_owned()), TreeItem::new_leaf("Mike"),
TreeItem::new_leaf("n", "November".to_owned()), TreeItem::new_leaf("November"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
TreeItem::new_leaf("o", "Oscar".to_owned()), TreeItem::new_leaf("Oscar"),
TreeItem::new( TreeItem::new(
"p", "Papa",
"Papa".to_owned(),
vec![ vec![
TreeItem::new_leaf("q", "Quebec".to_owned()), TreeItem::new_leaf("Quebec"),
TreeItem::new_leaf("r", "Romeo".to_owned()), TreeItem::new_leaf("Romeo"),
TreeItem::new_leaf("s", "Sierra".to_owned()), TreeItem::new_leaf("Sierra"),
TreeItem::new_leaf("t", "Tango".to_owned()), TreeItem::new_leaf("Tango"),
TreeItem::new_leaf("u", "Uniform".to_owned()), TreeItem::new_leaf("Uniform"),
TreeItem::new( TreeItem::new(
"v", "Victor",
"Victor".to_owned(),
vec![ vec![
TreeItem::new_leaf("w", "Whiskey".to_owned()), TreeItem::new_leaf("Whiskey"),
TreeItem::new_leaf("x", "Xray".to_owned()), TreeItem::new_leaf("Xray"),
TreeItem::new_leaf("y", "Yankee".to_owned()), 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".to_owned()), TreeItem::new_leaf("Zulu"),
], ],
} }
} }
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 +87,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);
} }
@@ -150,7 +129,10 @@ fn main() -> std::io::Result<()> {
Ok(()) Ok(())
} }
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> std::io::Result<()> { fn run_app<B: Backend<Error = std::io::Error>>(
terminal: &mut Terminal<B>,
mut app: App,
) -> std::io::Result<()> {
const DEBOUNCE: Duration = Duration::from_millis(20); // 50 FPS const DEBOUNCE: Duration = Duration::from_millis(20); // 50 FPS
let before = Instant::now(); let before = Instant::now();
@@ -165,7 +147,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> std::io::Res
let update = match crossterm::event::read()? { let update = match crossterm::event::read()? {
Event::Key(key) => match key.code { Event::Key(key) => match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
return Ok(()) return Ok(());
} }
KeyCode::Char('q') => return Ok(()), KeyCode::Char('q') => return Ok(()),
KeyCode::Char('\n' | ' ') => app.state.toggle_selected(), KeyCode::Char('\n' | ' ') => app.state.toggle_selected(),
+81 -30
View File
@@ -1,24 +1,24 @@
use std::collections::HashSet;
use ratatui::text::Text;
use crate::tree_item::TreeItem; use crate::tree_item::TreeItem;
use ratatui::text::ToText;
use std::collections::HashSet;
use std::fmt::Display;
use std::hash::Hash;
/// A flattened item of all visible [`TreeItem`]s. /// A flattened item of all visible [`TreeItem`]s.
/// ///
/// 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
T: for<'b> Into<Text<'b>> + Clone, T: ToText + Clone + Default + Display + Hash + PartialEq + Eq,
{ {
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
T: for<'b> Into<Text<'b>> + Clone, T: ToText + Clone + Default + Display + Hash + PartialEq + Eq,
{ {
/// 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]
@@ -31,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 + PartialEq + Eq,
T: for<'b> Into<Text<'b>> + Clone,
{ {
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)
@@ -63,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())
@@ -74,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
@@ -86,29 +91,75 @@ 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"),
],
);
} }
+34 -32
View File
@@ -5,13 +5,14 @@ Tree widget [`Tree`] is generated with [`TreeItem`]s (which itself can contain [
The user interaction state (like the current selection) is stored in the [`TreeState`]. The user interaction state (like the current selection) is stored in the [`TreeState`].
*/ */
use std::collections::HashSet;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::style::Style; use ratatui::style::Style;
use ratatui::text::Text; use ratatui::text::ToText;
use ratatui::widgets::{Block, Scrollbar, ScrollbarState, StatefulWidget, Widget}; use ratatui::widgets::{Block, Scrollbar, ScrollbarState, StatefulWidget, Widget};
use std::collections::HashSet;
use std::fmt::Display;
use std::hash::Hash;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
pub use crate::flatten::Flattened; pub use crate::flatten::Flattened;
@@ -24,40 +25,36 @@ 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
/// ///
/// ``` /// ```
/// # 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;
/// # 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| {
/// 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")
/// .block(Block::bordered().title("Tree Widget")); /// .block(Block::bordered().title("Tree Widget"));
/// ///
/// frame.render_stateful_widget(tree_widget, area, &mut state); /// frame.render_stateful_widget(tree_widget, area, &mut state);
/// })?; /// }).unwrap();
/// # Ok::<(), std::io::Error>(())
/// ``` /// ```
#[must_use] #[must_use]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Tree<'a, Identifier, T> pub struct Tree<'a, T>
where where
T: for<'b> Into<Text<'b>> + Clone, T: ToText + Clone + Default + Display + Hash + PartialEq + Eq,
{ {
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>>,
@@ -77,17 +74,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 + PartialEq + Eq,
T: for<'b> Into<Text<'b>> + Clone,
{ {
/// 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)
@@ -121,7 +117,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;
@@ -162,18 +158,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", "text"); 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 + PartialEq + Eq,
T: for<'b> Into<Text<'b>> + Clone,
{ {
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) {
@@ -278,7 +273,7 @@ where
height, height,
}; };
let text = item.content.clone().into(); let text = item.content.to_text();
let item_style = text.style; let item_style = text.style;
let is_selected = state.selected == *identifier; let is_selected = state.selected == *identifier;
@@ -338,10 +333,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 + PartialEq + Eq,
T: for<'b> Into<Text<'b>> + Clone,
{ {
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,10 +346,11 @@ where
#[cfg(test)] #[cfg(test)]
mod render_tests { mod render_tests {
use super::*; use super::*;
use std::hash::{DefaultHasher, Hasher};
#[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);
@@ -388,7 +383,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 ",
@@ -405,8 +402,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 ",
+49 -48
View File
@@ -1,21 +1,22 @@
use ratatui::text::ToText;
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::Display;
use ratatui::text::Text; use std::hash::{DefaultHasher, Hash, Hasher};
/// 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.
/// ///
/// 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,42 +29,31 @@ 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("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, PartialEq, Eq)]
pub struct TreeItem<Identifier, T> pub struct TreeItem<T>
where where
T: for<'a> Into<Text<'a>> + Clone, T: ToText + Clone + Default + Display + Hash + PartialEq + Eq,
{ {
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 + PartialEq + Eq,
T: for<'a> Into<Text<'a>> + Clone,
{ {
/// Create a new `TreeItem` without children.
#[must_use]
pub 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)
@@ -75,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.
@@ -107,7 +113,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)
@@ -115,7 +121,7 @@ where
#[must_use] #[must_use]
pub fn height(&self) -> usize { pub fn height(&self) -> usize {
self.content.clone().into().height() self.content.clone().to_text().height()
} }
/// Add a child to the `TreeItem`. /// Add a child to the `TreeItem`.
@@ -141,31 +147,26 @@ where
} }
} }
impl TreeItem<&'static str, String> { 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".to_owned()), Self::new_leaf("Alfa"),
Self::new( Self::new(
"b", "Bravo",
"Bravo".to_owned(),
vec![ vec![
Self::new_leaf("c", "Charlie".to_owned()), Self::new_leaf("Charlie"),
Self::new( Self::new(
"d", "Delta",
"Delta".to_owned(), vec![Self::new_leaf("Echo"), Self::new_leaf("Foxtrot")],
vec![
Self::new_leaf("e", "Echo".to_owned()),
Self::new_leaf("f", "Foxtrot".to_owned()),
],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
Self::new_leaf("g", "Golf".to_owned()), Self::new_leaf("Golf"),
], ],
) )
.expect("all item identifiers are unique"), .expect("all item identifiers are unique"),
Self::new_leaf("h", "Hotel".to_owned()), Self::new_leaf("Hotel"),
] ]
} }
} }
@@ -173,16 +174,16 @@ impl TreeItem<&'static str, String> {
#[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();
} }
+32 -43
View File
@@ -1,44 +1,38 @@
use std::collections::HashSet;
use ratatui::layout::{Position, Rect}; use ratatui::layout::{Position, Rect};
use ratatui::text::Text; use ratatui::text::ToText;
use std::collections::HashSet;
use std::fmt::Display;
use std::hash::Hash;
use crate::flatten::{flatten, Flattened}; use crate::flatten::{Flattened, flatten};
use crate::tree_item::TreeItem; 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 tui_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,28 +40,25 @@ 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
} }
/// Get a flat list of all currently viewable (including by scrolling) [`TreeItem`]s with this `TreeState`. /// Get a flat list of all currently viewable (including by scrolling) [`TreeItem`]s with this `TreeState`.
#[must_use] #[must_use]
pub fn flatten<'a, T>( pub fn flatten<'a, T>(&self, items: &'a [TreeItem<T>]) -> Vec<Flattened<'a, T>>
&self,
items: &'a [TreeItem<Identifier, T>],
) -> Vec<Flattened<'a, Identifier, T>>
where where
T: for<'b> Into<Text<'b>> + Clone, T: ToText + Clone + Default + Display + Hash + PartialEq + Eq,
{ {
flatten(&self.opened, items, &[]) flatten(&self.opened, items, &[])
} }
@@ -79,11 +70,11 @@ 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::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 +84,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 +95,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 +104,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) {
@@ -195,13 +186,12 @@ where
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use tui_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_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 +220,12 @@ where
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use tui_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
/// // 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))
/// }); /// });
/// ``` /// ```
@@ -259,7 +248,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;
} }