Compare commits
10 Commits
735f111866
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f289d1075 | |||
| 45523fa08e | |||
|
|
6a324736e5 | ||
| b0d8d9f0bf | |||
| 959b60de32 | |||
| 6baef469c8 | |||
| 6c10db760d | |||
| 8a91f662dd | |||
| 37d7b77f90 | |||
| 3e23a73f6b |
+13
-10
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user