#![doc(html_playground_url = "https://play.rust-lang.org")]
#![doc(
html_favicon_url = "https://raw.githubusercontent.com/veeso/tui-realm/main/crates/tuirealm-treeview/docs/images/cargo/tui-realm-treeview-128.png"
)]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/veeso/tui-realm/main/crates/tuirealm-treeview/docs/images/cargo/tui-realm-treeview-128.png"
)]
#[doc(hidden)]
pub mod mock;
pub mod tree_state;
pub mod widget;
use std::iter;
pub use orange_trees::{Node as OrangeNode, Tree as OrangeTree};
use tui_realm_stdlib::prop_ext::{CommonHighlight, CommonProps};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::component::Component;
use tuirealm::props::{
AttrValue, Attribute, Borders, Color, LineStatic, Props, QueryResult, SpanStatic, Style,
TextModifiers, Title,
};
use tuirealm::ratatui::Frame;
use tuirealm::ratatui::layout::Rect;
use tuirealm::state::{State, StateValue};
pub use self::tree_state::TreeState;
pub use self::widget::TreeWidget;
pub trait NodeValue: Default {
fn render_parts_iter(&self) -> impl Iterator<Item = (&str, Option<Style>)>;
}
impl NodeValue for String {
fn render_parts_iter(&self) -> impl Iterator<Item = (&str, Option<Style>)> {
iter::once((self.as_str(), None))
}
}
impl NodeValue for Vec<SpanStatic> {
fn render_parts_iter(&self) -> impl Iterator<Item = (&str, Option<Style>)> {
self.iter()
.map(|span| (span.content.as_ref(), Some(span.style)))
}
}
pub type Node<V> = OrangeNode<String, V>;
pub type Tree<V> = OrangeTree<String, V>;
pub const TREE_INDENT_SIZE: &str = "indent-size";
pub const TREE_INITIAL_NODE: &str = "initial-mode";
pub const TREE_PRESERVE_STATE: &str = "preserve-state";
pub const TREE_CMD_OPEN: &str = "o";
pub const TREE_CMD_CLOSE: &str = "c";
pub struct TreeView<V: NodeValue> {
common: CommonProps,
common_hg: CommonHighlight,
props: Props,
states: TreeState,
tree: Tree<V>,
}
impl<V: NodeValue> Default for TreeView<V> {
fn default() -> Self {
Self {
common: CommonProps::default(),
common_hg: CommonHighlight::default(),
props: Props::default(),
states: TreeState::default(),
tree: Tree::new(Node::new(String::new(), V::default())),
}
}
}
impl<V: NodeValue> TreeView<V> {
pub fn foreground(mut self, fg: Color) -> Self {
self.attr(Attribute::Foreground, AttrValue::Color(fg));
self
}
pub fn background(mut self, bg: Color) -> Self {
self.attr(Attribute::Background, AttrValue::Color(bg));
self
}
pub fn modifiers(mut self, m: TextModifiers) -> Self {
self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
self
}
pub fn style(mut self, style: Style) -> Self {
self.attr(Attribute::Style, AttrValue::Style(style));
self
}
pub fn inactive(mut self, s: Style) -> Self {
self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
self
}
pub fn borders(mut self, b: Borders) -> Self {
self.attr(Attribute::Borders, AttrValue::Borders(b));
self
}
pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
self.attr(Attribute::Title, AttrValue::Title(title.into()));
self
}
pub fn highlight_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
self
}
pub fn highlight_style(mut self, s: Style) -> Self {
self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
self
}
pub fn highlight_style_inactive(mut self, s: Style) -> Self {
self.attr(Attribute::HighlightStyleUnfocused, AttrValue::Style(s));
self
}
pub fn initial_node<S: Into<String>>(mut self, node: S) -> Self {
self.attr(
Attribute::Custom(TREE_INITIAL_NODE),
AttrValue::String(node.into()),
);
self
}
pub fn preserve_state(mut self, preserve: bool) -> Self {
self.attr(
Attribute::Custom(TREE_PRESERVE_STATE),
AttrValue::Flag(preserve),
);
self
}
pub fn indent_size(mut self, sz: u16) -> Self {
self.attr(Attribute::Custom(TREE_INDENT_SIZE), AttrValue::Size(sz));
self
}
pub fn scroll_step(mut self, step: usize) -> Self {
self.attr(Attribute::ScrollStep, AttrValue::Length(step));
self
}
pub fn with_tree(mut self, tree: Tree<V>) -> Self {
self.tree = tree;
self
}
pub fn tree(&self) -> &Tree<V> {
&self.tree
}
pub fn tree_mut(&mut self) -> &mut Tree<V> {
&mut self.tree
}
pub fn set_tree(&mut self, tree: Tree<V>) {
self.tree = tree;
self.states.tree_changed(
self.tree.root(),
self.props
.get(Attribute::Custom(TREE_PRESERVE_STATE))
.and_then(AttrValue::as_flag)
.unwrap_or_default(),
);
}
pub fn tree_state(&self) -> &TreeState {
&self.states
}
fn changed(&self, prev: Option<&str>) -> CmdResult {
match self.states.selected() {
None => CmdResult::NoChange,
id if id != prev => CmdResult::Changed(self.state()),
_ => CmdResult::NoChange,
}
}
}
impl<V: NodeValue> Component for TreeView<V> {
fn view(&mut self, frame: &mut Frame, area: Rect) {
if !self.common.display {
return;
}
let indent_size = self
.props
.get(Attribute::Custom(TREE_INDENT_SIZE))
.and_then(AttrValue::as_size)
.unwrap_or(4);
let block = self.common.get_block();
let mut tree = TreeWidget::new(self.tree())
.indent_size(indent_size.into())
.style(self.common.style)
.highlight_style(
self.common_hg
.get_style_focus(self.common.style, self.common.is_active()),
);
if let Some(block) = block {
tree = tree.block(block);
}
if let Some(symbol) = self.common_hg.get_symbol() {
tree = tree.highlight_str(symbol);
}
let mut state = self.states.clone();
frame.render_stateful_widget(tree, area, &mut state);
}
fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
if let Some(value) = self
.common
.get_for_query(attr)
.or_else(|| self.common_hg.get_for_query(attr))
{
return Some(value);
}
self.props.get_for_query(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
if matches!(attr, Attribute::Custom(TREE_INITIAL_NODE)) {
if let Some(node) = self.tree.root().query(&value.unwrap_string()) {
self.states.select(self.tree.root(), node);
}
} else if let Some(value) = self
.common
.set(attr, value)
.and_then(|value| self.common_hg.set(attr, value))
{
self.props.set(attr, value);
}
}
fn state(&self) -> State {
match self.states.selected() {
None => State::None,
Some(id) => State::Single(StateValue::String(id.to_string())),
}
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::GoTo(Position::Begin) => {
let prev = self.states.selected().map(|x| x.to_string());
if let Some(first) = self.states.first_sibling(self.tree.root()) {
self.states.select(self.tree.root(), first);
}
self.changed(prev.as_deref())
}
Cmd::GoTo(Position::End) => {
let prev = self.states.selected().map(|x| x.to_string());
if let Some(last) = self.states.last_sibling(self.tree.root()) {
self.states.select(self.tree.root(), last);
}
self.changed(prev.as_deref())
}
Cmd::Move(Direction::Down) => {
let prev = self.states.selected().map(|x| x.to_string());
self.states.move_down(self.tree.root());
self.changed(prev.as_deref())
}
Cmd::Move(Direction::Up) => {
let prev = self.states.selected().map(|x| x.to_string());
self.states.move_up(self.tree.root());
self.changed(prev.as_deref())
}
Cmd::Scroll(Direction::Down) => {
let prev = self.states.selected().map(|x| x.to_string());
let step = self
.props
.get(Attribute::ScrollStep)
.and_then(AttrValue::as_length)
.unwrap_or(8);
(0..step).for_each(|_| self.states.move_down(self.tree.root()));
self.changed(prev.as_deref())
}
Cmd::Scroll(Direction::Up) => {
let prev = self.states.selected().map(|x| x.to_string());
let step = self
.props
.get(Attribute::ScrollStep)
.and_then(AttrValue::as_length)
.unwrap_or(8);
(0..step).for_each(|_| self.states.move_up(self.tree.root()));
self.changed(prev.as_deref())
}
Cmd::Submit => CmdResult::Submit(self.state()),
Cmd::Custom(TREE_CMD_CLOSE) => {
self.states.close(self.tree.root());
CmdResult::Visual
}
Cmd::Custom(TREE_CMD_OPEN) => {
self.states.open(self.tree.root());
CmdResult::Visual
}
_ => CmdResult::Invalid(cmd),
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use tuirealm::props::HorizontalAlignment;
use super::*;
use crate::mock::mock_tree;
#[test]
fn should_initialize_component() {
let mut component = TreeView::default()
.background(Color::White)
.foreground(Color::Cyan)
.borders(Borders::default())
.inactive(Style::default())
.indent_size(4)
.modifiers(TextModifiers::all())
.preserve_state(true)
.scroll_step(4)
.title(Title::from("My tree").alignment(HorizontalAlignment::Center))
.with_tree(mock_tree())
.initial_node("aB1");
assert_eq!(component.tree_state().selected().unwrap(), "aB1");
assert!(component.tree().root().query(&String::from("aB")).is_some());
component
.tree_mut()
.root_mut()
.add_child(Node::new(String::from("d"), String::from("d")));
}
#[test]
fn should_return_consistent_state() {
let component = TreeView::default().with_tree(mock_tree());
assert_eq!(component.state(), State::None);
let component = TreeView::default()
.with_tree(mock_tree())
.initial_node("aA");
assert_eq!(
component.state(),
State::Single(StateValue::String(String::from("aA")))
);
}
#[test]
fn should_perform_go_to_begin() {
let mut component = TreeView::default()
.with_tree(mock_tree())
.initial_node("bB3");
assert_eq!(
component.perform(Cmd::GoTo(Position::Begin)),
CmdResult::Changed(State::Single(StateValue::String(String::from("bB0"))))
);
assert_eq!(
component.perform(Cmd::GoTo(Position::Begin)),
CmdResult::NoChange
);
}
#[test]
fn should_perform_go_to_end() {
let mut component = TreeView::default()
.with_tree(mock_tree())
.initial_node("bB1");
assert_eq!(
component.perform(Cmd::GoTo(Position::End)),
CmdResult::Changed(State::Single(StateValue::String(String::from("bB5"))))
);
assert_eq!(
component.perform(Cmd::GoTo(Position::End)),
CmdResult::NoChange
);
}
#[test]
fn should_perform_move_down() {
let mut component = TreeView::default()
.with_tree(mock_tree())
.initial_node("cA1");
assert_eq!(
component.perform(Cmd::Move(Direction::Down)),
CmdResult::Changed(State::Single(StateValue::String(String::from("cA2"))))
);
assert_eq!(
component.perform(Cmd::Move(Direction::Down)),
CmdResult::NoChange
);
}
#[test]
fn should_perform_move_up() {
let mut component = TreeView::default().with_tree(mock_tree()).initial_node("a");
assert_eq!(
component.perform(Cmd::Move(Direction::Up)),
CmdResult::Changed(State::Single(StateValue::String(String::from("/"))))
);
assert_eq!(
component.perform(Cmd::Move(Direction::Up)),
CmdResult::NoChange
);
}
#[test]
fn should_perform_scroll_down() {
let mut component = TreeView::default()
.scroll_step(2)
.with_tree(mock_tree())
.initial_node("cA0");
assert_eq!(
component.perform(Cmd::Scroll(Direction::Down)),
CmdResult::Changed(State::Single(StateValue::String(String::from("cA2"))))
);
assert_eq!(
component.perform(Cmd::Scroll(Direction::Down)),
CmdResult::NoChange
);
}
#[test]
fn should_perform_scroll_up() {
let mut component = TreeView::default()
.scroll_step(4)
.with_tree(mock_tree())
.initial_node("aA1");
assert_eq!(
component.perform(Cmd::Scroll(Direction::Up)),
CmdResult::Changed(State::Single(StateValue::String(String::from("/"))))
);
assert_eq!(
component.perform(Cmd::Scroll(Direction::Up)),
CmdResult::NoChange
);
}
#[test]
fn should_perform_submit() {
let mut component = TreeView::default()
.with_tree(mock_tree())
.initial_node("aA1");
assert_eq!(
component.perform(Cmd::Submit),
CmdResult::Submit(State::Single(StateValue::String(String::from("aA1"))))
);
}
#[test]
fn should_perform_close() {
let mut component = TreeView::default()
.with_tree(mock_tree())
.initial_node("aA1");
component.states.open(component.tree.root());
assert_eq!(
component.perform(Cmd::Custom(TREE_CMD_CLOSE)),
CmdResult::Visual
);
assert!(
component
.tree_state()
.is_closed(component.tree().root().query(&String::from("aA1")).unwrap())
);
}
#[test]
fn should_perform_open() {
let mut component = TreeView::default()
.with_tree(mock_tree())
.initial_node("aA");
assert_eq!(
component.perform(Cmd::Custom(TREE_CMD_OPEN)),
CmdResult::Visual
);
assert!(
component
.tree_state()
.is_open(component.tree().root().query(&String::from("aA")).unwrap())
);
}
#[test]
fn should_update_tree() {
let mut component = TreeView::default()
.with_tree(mock_tree())
.preserve_state(true)
.initial_node("aA");
component.states.select(
component.tree.root(),
component.tree.root().query(&String::from("bB")).unwrap(),
);
component.states.open(component.tree.root());
component.states.select(
component.tree.root(),
component.tree.root().query(&String::from("aA")).unwrap(),
);
let mut new_tree = mock_tree();
new_tree.root_mut().remove_child(&String::from("a"));
component.set_tree(new_tree);
assert_eq!(component.states.selected().unwrap(), "/");
}
}