Skip to main content

tui_realm_treeview/
lib.rs

1//! # tui-realm-treeview
2//!
3//! [tui-realm-treeview](https://github.com/veeso/tui-realm/tree/feature/main/crates/tuirealm-treeview) is a
4//! [tui-realm](https://github.com/veeso/tui-realm) implementation of a treeview component.
5//! The tree engine is based on [Orange-trees](https://docs.rs/orange-trees/).
6//!
7//! ## Get Started
8//!
9//! ### Adding `tui-realm-treeview` as dependency
10//!
11//! ```toml
12//! tui-realm-treeview = "4"
13//! ```
14//!
15//! Or if you don't use **Crossterm**, define the backend as you would do with tui-realm:
16//!
17//! ```toml
18//! tui-realm-treeview = { version = "4", default-features = false, features = [ "termion" ] }
19//! ```
20//!
21//! ## Component API
22//!
23//! **Commands**:
24//!
25//! | Cmd                       | Result            | Behaviour                                            |
26//! |---------------------------|-------------------|------------------------------------------------------|
27//! | `Custom($TREE_CMD_CLOSE)` | `None`            | Close selected node                                  |
28//! | `Custom($TREE_CMD_OPEN)`  | `None`            | Open selected node                                   |
29//! | `GoTo(Begin)`             | `Changed \| None` | Move cursor to the top of the current tree node      |
30//! | `GoTo(End)`               | `Changed \| None` | Move cursor to the bottom of the current tree node   |
31//! | `Move(Down)`              | `Changed \| None` | Go to next element                                   |
32//! | `Move(Up)`                | `Changed \| None` | Go to previous element                               |
33//! | `Scroll(Down)`            | `Changed \| None` | Move cursor down by defined max steps or end of node |
34//! | `Scroll(Up)`              | `Changed \| None` | Move cursor up by defined max steps or begin of node |
35//! | `Submit`                  | `Submit`          | Just returns submit result with current state        |
36//!
37//! **State**: the state returned is a `One(String)` containing the id of the selected node. If no node is selected `None` is returned.
38//!
39//! **Properties**:
40//!
41//! - `Background(Color)`: background color. The background color will be used as background for unselected entry, but will be used as foreground for the selected entry when focus is true
42//! - `Borders(Borders)`: set borders properties for component
43//! - `Custom($TREE_IDENT_SIZE, Size)`: Set space to render for each each depth level
44//! - `Custom($TREE_INITIAL_NODE, String)`: Select initial node in the tree. This option has priority over `keep_state`
45//! - `Custom($TREE_PRESERVE_STATE, Flag)`: If true, the selected entry will be kept after an update of the tree (obviously if the entry still exists in the tree).
46//! - `FocusStyle(Style)`: inactive style
47//! - `Foreground(Color)`: foreground color. The foreground will be used as foreground for the selected item, when focus is false, otherwise as background
48//! - `HighlightedColor(Color)`: The provided color will be used to highlight the selected node. `Foreground` will be used if unset.
49//! - `HighlightedStr(String)`: The provided string will be displayed on the left side of the selected entry in the tree
50//! - `ScrollStep(Length)`: Defines the maximum amount of rows to scroll
51//! - `TextProps(TextModifiers)`: set text modifiers
52//! - `Title(Title)`: Set box title
53//!
54//! ### Updating the tree
55//!
56//! The tree in this component is not inside the `props`, but is a member of the `TreeView` component structure.
57//! In order to update and work with the tree you've got basically two ways to do this.
58//!
59//! #### Remounting the component
60//!
61//! In situation where you need to update the tree on the update routine (as happens in the example),
62//! the best way to update the tree is to remount the component from scratch.
63//!
64//! #### Updating the tree from the "on" method
65//!
66//! This method is probably better than remounting, but it is not always possible to use this.
67//! When you implement `Component` for your treeview, you have a mutable reference to the component, and so here you can call these methods to operate on the tree:
68//!
69//! - `pub fn tree(&self) -> &Tree`: returns a reference to the tree
70//! - `pub fn tree_mut(&mut self) -> &mut Tree`: returns a mutable reference to the tree; which allows you to operate on it
71//! - `pub fn set_tree(&mut self, tree: Tree)`: update the current tree with another
72//! - `pub fn tree_state(&self) -> &TreeState`: get a reference to the current tree state. (See tree state docs)
73//!
74//! You can access these methods from the `on()` method as said before. So these methods can be handy when you update the tree after a certain events or maybe even better, you can set the tree if you receive it from a `UserEvent` produced by a **Port**.
75//!
76//! ---
77//!
78//! ## Setup a tree component
79//!
80//! ```rust
81//! # use tuirealm::{
82//! #     command::{Cmd, CmdResult, Direction, Position},
83//! #     component::{AppComponent, Component},
84//! #     event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent},
85//! #     props::{Title, HorizontalAlignment, BorderType, Borders, Color, Style},
86//! #     state::{State, StateValue},
87//! # };
88//! # use tui_realm_treeview::{Node, Tree, TreeView, TREE_CMD_CLOSE, TREE_CMD_OPEN};
89//! #
90//! #[derive(Debug, PartialEq)]
91//! pub enum Msg {
92//!     ExtendDir(String),
93//!     GoToUpperDir,
94//!     Redraw,
95//! }
96//!
97//! #[derive(Component)]
98//! pub struct FsTree {
99//!     component: TreeView<String>,
100//! }
101//!
102//! impl FsTree {
103//!     pub fn new(tree: Tree<String>, initial_node: Option<String>) -> Self {
104//!         // Preserve initial node if exists
105//!         let initial_node = match initial_node {
106//!             Some(id) if tree.root().query(&id).is_some() => id,
107//!             _ => tree.root().id().to_string(),
108//!         };
109//!         FsTree {
110//!             component: TreeView::default()
111//!                 .foreground(Color::Reset)
112//!                 .borders(
113//!                     Borders::default()
114//!                         .color(Color::LightYellow)
115//!                         .modifiers(BorderType::Rounded),
116//!                 )
117//!                 .inactive(Style::default().fg(Color::Gray))
118//!                 .indent_size(3)
119//!                 .scroll_step(6)
120//!                 .title(Title::from(tree.root().id().to_string()).alignment(HorizontalAlignment::Left))
121//!                 .highlight_style(Style::new().fg(Color::LightYellow))
122//!                 .highlight_str("🦄")
123//!                 .with_tree(tree)
124//!                 .initial_node(initial_node),
125//!         }
126//!     }
127//! }
128//!
129//! impl AppComponent<Msg, NoUserEvent> for FsTree {
130//!     fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
131//!         let result = match ev {
132//!             Event::Keyboard(KeyEvent {
133//!                 code: Key::Left,
134//!                 modifiers: KeyModifiers::NONE,
135//!             }) => self.perform(Cmd::Custom(TREE_CMD_CLOSE)),
136//!             Event::Keyboard(KeyEvent {
137//!                 code: Key::Right,
138//!                 modifiers: KeyModifiers::NONE,
139//!             }) => self.perform(Cmd::Custom(TREE_CMD_OPEN)),
140//!             Event::Keyboard(KeyEvent {
141//!                 code: Key::PageDown,
142//!                 modifiers: KeyModifiers::NONE,
143//!             }) => self.perform(Cmd::Scroll(Direction::Down)),
144//!             Event::Keyboard(KeyEvent {
145//!                 code: Key::PageUp,
146//!                 modifiers: KeyModifiers::NONE,
147//!             }) => self.perform(Cmd::Scroll(Direction::Up)),
148//!             Event::Keyboard(KeyEvent {
149//!                 code: Key::Down,
150//!                 modifiers: KeyModifiers::NONE,
151//!             }) => self.perform(Cmd::Move(Direction::Down)),
152//!             Event::Keyboard(KeyEvent {
153//!                 code: Key::Up,
154//!                 modifiers: KeyModifiers::NONE,
155//!             }) => self.perform(Cmd::Move(Direction::Up)),
156//!             Event::Keyboard(KeyEvent {
157//!                 code: Key::Home,
158//!                 modifiers: KeyModifiers::NONE,
159//!             }) => self.perform(Cmd::GoTo(Position::Begin)),
160//!             Event::Keyboard(KeyEvent {
161//!                 code: Key::End,
162//!                 modifiers: KeyModifiers::NONE,
163//!             }) => self.perform(Cmd::GoTo(Position::End)),
164//!             Event::Keyboard(KeyEvent {
165//!                 code: Key::Enter,
166//!                 modifiers: KeyModifiers::NONE,
167//!             }) => self.perform(Cmd::Submit),
168//!             Event::Keyboard(KeyEvent {
169//!                 code: Key::Backspace,
170//!                 modifiers: KeyModifiers::NONE,
171//!             }) => return Some(Msg::GoToUpperDir),
172//!             _ => return None,
173//!         };
174//!         match result {
175//!             CmdResult::Submit(State::Single(StateValue::String(node))) => Some(Msg::ExtendDir(node)),
176//!             _ => Some(Msg::Redraw),
177//!         }
178//!     }
179//! }
180//!
181//! ```
182//!
183//! ---
184//!
185//! ## Tree widget
186//!
187//! If you want, you can also implement your own version of a tree view component using the `TreeWidget`
188//! in order to render a tree.
189//! Keep in mind that if you want to create a stateful tree (with highlighted item), you'll need to render it
190//! as a stateful widget, passing to it a `TreeState`, which is provided by this library.
191//!
192
193#![doc(html_playground_url = "https://play.rust-lang.org")]
194#![doc(
195    html_favicon_url = "https://raw.githubusercontent.com/veeso/tui-realm/main/crates/tuirealm-treeview/docs/images/cargo/tui-realm-treeview-128.png"
196)]
197#![doc(
198    html_logo_url = "https://raw.githubusercontent.com/veeso/tui-realm/main/crates/tuirealm-treeview/docs/images/cargo/tui-realm-treeview-128.png"
199)]
200
201#[doc(hidden)]
202pub mod mock;
203pub mod tree_state;
204pub mod widget;
205
206use std::iter;
207
208pub use orange_trees::{Node as OrangeNode, Tree as OrangeTree};
209use tui_realm_stdlib::prop_ext::{CommonHighlight, CommonProps};
210use tuirealm::command::{Cmd, CmdResult, Direction, Position};
211use tuirealm::component::Component;
212use tuirealm::props::{
213    AttrValue, Attribute, Borders, Color, LineStatic, Props, QueryResult, SpanStatic, Style,
214    TextModifiers, Title,
215};
216use tuirealm::ratatui::Frame;
217use tuirealm::ratatui::layout::Rect;
218use tuirealm::state::{State, StateValue};
219
220pub use self::tree_state::TreeState;
221pub use self::widget::TreeWidget;
222
223/// [`Tree`] node value.
224pub trait NodeValue: Default {
225    /// Return iterator over render parts - text with it style.
226    /// If style is `None`, then it will be inherited from widget style.
227    fn render_parts_iter(&self) -> impl Iterator<Item = (&str, Option<Style>)>;
228}
229
230impl NodeValue for String {
231    fn render_parts_iter(&self) -> impl Iterator<Item = (&str, Option<Style>)> {
232        iter::once((self.as_str(), None))
233    }
234}
235
236impl NodeValue for Vec<SpanStatic> {
237    fn render_parts_iter(&self) -> impl Iterator<Item = (&str, Option<Style>)> {
238        self.iter()
239            .map(|span| (span.content.as_ref(), Some(span.style)))
240    }
241}
242
243// -- type override
244pub type Node<V> = OrangeNode<String, V>;
245pub type Tree<V> = OrangeTree<String, V>;
246
247// -- props
248
249pub const TREE_INDENT_SIZE: &str = "indent-size";
250pub const TREE_INITIAL_NODE: &str = "initial-mode";
251pub const TREE_PRESERVE_STATE: &str = "preserve-state";
252
253// -- Cmd
254
255pub const TREE_CMD_OPEN: &str = "o";
256pub const TREE_CMD_CLOSE: &str = "c";
257
258// -- component
259
260/// ## TreeView
261///
262/// Tree view component for tui-realm
263pub struct TreeView<V: NodeValue> {
264    common: CommonProps,
265    common_hg: CommonHighlight,
266    props: Props,
267    states: TreeState,
268    /// The actual Tree data structure. You can access this from your Component to operate on it
269    /// for example after a certain events.
270    tree: Tree<V>,
271}
272
273impl<V: NodeValue> Default for TreeView<V> {
274    fn default() -> Self {
275        Self {
276            common: CommonProps::default(),
277            common_hg: CommonHighlight::default(),
278            props: Props::default(),
279            states: TreeState::default(),
280            tree: Tree::new(Node::new(String::new(), V::default())),
281        }
282    }
283}
284
285impl<V: NodeValue> TreeView<V> {
286    /// Set the main foreground color. This may get overwritten by individual text styles.
287    pub fn foreground(mut self, fg: Color) -> Self {
288        self.attr(Attribute::Foreground, AttrValue::Color(fg));
289        self
290    }
291
292    /// Set the main background color. This may get overwritten by individual text styles.
293    pub fn background(mut self, bg: Color) -> Self {
294        self.attr(Attribute::Background, AttrValue::Color(bg));
295        self
296    }
297
298    /// Set the main text modifiers. This may get overwritten by individual text styles.
299    pub fn modifiers(mut self, m: TextModifiers) -> Self {
300        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
301        self
302    }
303
304    /// Set the main style. This may get overwritten by individual text styles.
305    ///
306    /// This option will overwrite any previous [`foreground`](Self::foreground), [`background`](Self::background) and [`modifiers`](Self::modifiers)!
307    pub fn style(mut self, style: Style) -> Self {
308        self.attr(Attribute::Style, AttrValue::Style(style));
309        self
310    }
311
312    /// Set a custom style for the border when the component is unfocused.
313    pub fn inactive(mut self, s: Style) -> Self {
314        self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
315        self
316    }
317
318    /// Set widget border properties
319    pub fn borders(mut self, b: Borders) -> Self {
320        self.attr(Attribute::Borders, AttrValue::Borders(b));
321        self
322    }
323
324    /// Add a title to the component.
325    pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
326        self.attr(Attribute::Title, AttrValue::Title(title.into()));
327        self
328    }
329
330    /// Set the Symbol and Style for the indicator of the current line.
331    pub fn highlight_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
332        self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
333        self
334    }
335
336    /// Set a custom highlight style that is patched on-top of the normal style.
337    ///
338    /// By default the highlight style is just `Style::new().add_modifier(Modifier::REVERSED)`.
339    pub fn highlight_style(mut self, s: Style) -> Self {
340        self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
341        self
342    }
343
344    /// Set a custom highlight style that is patched on-top of the highlight style when unfocused.
345    pub fn highlight_style_inactive(mut self, s: Style) -> Self {
346        self.attr(Attribute::HighlightStyleUnfocused, AttrValue::Style(s));
347        self
348    }
349
350    /// Set initial node for tree state.
351    /// NOTE: this must be specified after `with_tree`
352    pub fn initial_node<S: Into<String>>(mut self, node: S) -> Self {
353        self.attr(
354            Attribute::Custom(TREE_INITIAL_NODE),
355            AttrValue::String(node.into()),
356        );
357        self
358    }
359
360    /// Set whether to preserve state on tree change
361    pub fn preserve_state(mut self, preserve: bool) -> Self {
362        self.attr(
363            Attribute::Custom(TREE_PRESERVE_STATE),
364            AttrValue::Flag(preserve),
365        );
366        self
367    }
368
369    /// Set indent size for widget for each level of depth
370    pub fn indent_size(mut self, sz: u16) -> Self {
371        self.attr(Attribute::Custom(TREE_INDENT_SIZE), AttrValue::Size(sz));
372        self
373    }
374
375    /// Set scroll step for scrolling command
376    pub fn scroll_step(mut self, step: usize) -> Self {
377        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
378        self
379    }
380
381    /// Set tree to use as data
382    pub fn with_tree(mut self, tree: Tree<V>) -> Self {
383        self.tree = tree;
384        self
385    }
386
387    /// Get a reference to tree
388    pub fn tree(&self) -> &Tree<V> {
389        &self.tree
390    }
391
392    /// Get mutable reference to tree
393    pub fn tree_mut(&mut self) -> &mut Tree<V> {
394        &mut self.tree
395    }
396
397    /// Set new tree in component.
398    /// Current state is preserved if `PRESERVE_STATE` is set to `AttrValue::Flag(true)`
399    pub fn set_tree(&mut self, tree: Tree<V>) {
400        self.tree = tree;
401        self.states.tree_changed(
402            self.tree.root(),
403            self.props
404                .get(Attribute::Custom(TREE_PRESERVE_STATE))
405                .and_then(AttrValue::as_flag)
406                .unwrap_or_default(),
407        );
408    }
409
410    /// Get a reference to the current tree state
411    pub fn tree_state(&self) -> &TreeState {
412        &self.states
413    }
414
415    /// Returns whether selectd node has changed
416    fn changed(&self, prev: Option<&str>) -> CmdResult {
417        match self.states.selected() {
418            None => CmdResult::NoChange,
419            id if id != prev => CmdResult::Changed(self.state()),
420            _ => CmdResult::NoChange,
421        }
422    }
423}
424
425impl<V: NodeValue> Component for TreeView<V> {
426    fn view(&mut self, frame: &mut Frame, area: Rect) {
427        if !self.common.display {
428            return;
429        }
430
431        let indent_size = self
432            .props
433            .get(Attribute::Custom(TREE_INDENT_SIZE))
434            .and_then(AttrValue::as_size)
435            .unwrap_or(4);
436
437        let block = self.common.get_block();
438
439        // Make widget
440        let mut tree = TreeWidget::new(self.tree())
441            .indent_size(indent_size.into())
442            .style(self.common.style)
443            .highlight_style(
444                self.common_hg
445                    .get_style_focus(self.common.style, self.common.is_active()),
446            );
447
448        if let Some(block) = block {
449            tree = tree.block(block);
450        }
451        if let Some(symbol) = self.common_hg.get_symbol() {
452            tree = tree.highlight_str(symbol);
453        }
454
455        let mut state = self.states.clone();
456        frame.render_stateful_widget(tree, area, &mut state);
457    }
458
459    fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
460        if let Some(value) = self
461            .common
462            .get_for_query(attr)
463            .or_else(|| self.common_hg.get_for_query(attr))
464        {
465            return Some(value);
466        }
467
468        self.props.get_for_query(attr)
469    }
470
471    fn attr(&mut self, attr: Attribute, value: AttrValue) {
472        // Initial node
473        if matches!(attr, Attribute::Custom(TREE_INITIAL_NODE)) {
474            // Select node if exists
475            if let Some(node) = self.tree.root().query(&value.unwrap_string()) {
476                self.states.select(self.tree.root(), node);
477            }
478        } else if let Some(value) = self
479            .common
480            .set(attr, value)
481            .and_then(|value| self.common_hg.set(attr, value))
482        {
483            self.props.set(attr, value);
484        }
485    }
486
487    fn state(&self) -> State {
488        match self.states.selected() {
489            None => State::None,
490            Some(id) => State::Single(StateValue::String(id.to_string())),
491        }
492    }
493
494    fn perform(&mut self, cmd: Cmd) -> CmdResult {
495        match cmd {
496            Cmd::GoTo(Position::Begin) => {
497                let prev = self.states.selected().map(|x| x.to_string());
498                // Get first sibling of current node
499                if let Some(first) = self.states.first_sibling(self.tree.root()) {
500                    self.states.select(self.tree.root(), first);
501                }
502                self.changed(prev.as_deref())
503            }
504            Cmd::GoTo(Position::End) => {
505                let prev = self.states.selected().map(|x| x.to_string());
506                // Get first sibling of current node
507                if let Some(last) = self.states.last_sibling(self.tree.root()) {
508                    self.states.select(self.tree.root(), last);
509                }
510                self.changed(prev.as_deref())
511            }
512            Cmd::Move(Direction::Down) => {
513                let prev = self.states.selected().map(|x| x.to_string());
514                self.states.move_down(self.tree.root());
515                self.changed(prev.as_deref())
516            }
517            Cmd::Move(Direction::Up) => {
518                let prev = self.states.selected().map(|x| x.to_string());
519                self.states.move_up(self.tree.root());
520                self.changed(prev.as_deref())
521            }
522            Cmd::Scroll(Direction::Down) => {
523                let prev = self.states.selected().map(|x| x.to_string());
524                let step = self
525                    .props
526                    .get(Attribute::ScrollStep)
527                    .and_then(AttrValue::as_length)
528                    .unwrap_or(8);
529                (0..step).for_each(|_| self.states.move_down(self.tree.root()));
530                self.changed(prev.as_deref())
531            }
532            Cmd::Scroll(Direction::Up) => {
533                let prev = self.states.selected().map(|x| x.to_string());
534                let step = self
535                    .props
536                    .get(Attribute::ScrollStep)
537                    .and_then(AttrValue::as_length)
538                    .unwrap_or(8);
539                (0..step).for_each(|_| self.states.move_up(self.tree.root()));
540                self.changed(prev.as_deref())
541            }
542            Cmd::Submit => CmdResult::Submit(self.state()),
543            Cmd::Custom(TREE_CMD_CLOSE) => {
544                // close selected node
545                self.states.close(self.tree.root());
546                CmdResult::Visual
547            }
548            Cmd::Custom(TREE_CMD_OPEN) => {
549                // close selected node
550                self.states.open(self.tree.root());
551                CmdResult::Visual
552            }
553            _ => CmdResult::Invalid(cmd),
554        }
555    }
556}
557
558#[cfg(test)]
559mod test {
560
561    use pretty_assertions::assert_eq;
562    use tuirealm::props::HorizontalAlignment;
563
564    use super::*;
565    use crate::mock::mock_tree;
566
567    #[test]
568    fn should_initialize_component() {
569        let mut component = TreeView::default()
570            .background(Color::White)
571            .foreground(Color::Cyan)
572            .borders(Borders::default())
573            .inactive(Style::default())
574            .indent_size(4)
575            .modifiers(TextModifiers::all())
576            .preserve_state(true)
577            .scroll_step(4)
578            .title(Title::from("My tree").alignment(HorizontalAlignment::Center))
579            .with_tree(mock_tree())
580            .initial_node("aB1");
581        // Check tree
582        assert_eq!(component.tree_state().selected().unwrap(), "aB1");
583        assert!(component.tree().root().query(&String::from("aB")).is_some());
584        component
585            .tree_mut()
586            .root_mut()
587            .add_child(Node::new(String::from("d"), String::from("d")));
588    }
589
590    #[test]
591    fn should_return_consistent_state() {
592        let component = TreeView::default().with_tree(mock_tree());
593        assert_eq!(component.state(), State::None);
594        let component = TreeView::default()
595            .with_tree(mock_tree())
596            .initial_node("aA");
597        assert_eq!(
598            component.state(),
599            State::Single(StateValue::String(String::from("aA")))
600        );
601    }
602
603    #[test]
604    fn should_perform_go_to_begin() {
605        let mut component = TreeView::default()
606            .with_tree(mock_tree())
607            .initial_node("bB3");
608        // GoTo begin (changed)
609        assert_eq!(
610            component.perform(Cmd::GoTo(Position::Begin)),
611            CmdResult::Changed(State::Single(StateValue::String(String::from("bB0"))))
612        );
613        // GoTo begin (unchanged)
614        assert_eq!(
615            component.perform(Cmd::GoTo(Position::Begin)),
616            CmdResult::NoChange
617        );
618    }
619
620    #[test]
621    fn should_perform_go_to_end() {
622        let mut component = TreeView::default()
623            .with_tree(mock_tree())
624            .initial_node("bB1");
625        // GoTo end (changed)
626        assert_eq!(
627            component.perform(Cmd::GoTo(Position::End)),
628            CmdResult::Changed(State::Single(StateValue::String(String::from("bB5"))))
629        );
630        // GoTo end (unchanged)
631        assert_eq!(
632            component.perform(Cmd::GoTo(Position::End)),
633            CmdResult::NoChange
634        );
635    }
636
637    #[test]
638    fn should_perform_move_down() {
639        let mut component = TreeView::default()
640            .with_tree(mock_tree())
641            .initial_node("cA1");
642        // Move down (changed)
643        assert_eq!(
644            component.perform(Cmd::Move(Direction::Down)),
645            CmdResult::Changed(State::Single(StateValue::String(String::from("cA2"))))
646        );
647        // Move down (unchanged)
648        assert_eq!(
649            component.perform(Cmd::Move(Direction::Down)),
650            CmdResult::NoChange
651        );
652    }
653
654    #[test]
655    fn should_perform_move_up() {
656        let mut component = TreeView::default().with_tree(mock_tree()).initial_node("a");
657        // Move up (changed)
658        assert_eq!(
659            component.perform(Cmd::Move(Direction::Up)),
660            CmdResult::Changed(State::Single(StateValue::String(String::from("/"))))
661        );
662        // Move up (unchanged)
663        assert_eq!(
664            component.perform(Cmd::Move(Direction::Up)),
665            CmdResult::NoChange
666        );
667    }
668
669    #[test]
670    fn should_perform_scroll_down() {
671        let mut component = TreeView::default()
672            .scroll_step(2)
673            .with_tree(mock_tree())
674            .initial_node("cA0");
675        // Scroll down (changed)
676        assert_eq!(
677            component.perform(Cmd::Scroll(Direction::Down)),
678            CmdResult::Changed(State::Single(StateValue::String(String::from("cA2"))))
679        );
680        // Scroll down (unchanged)
681        assert_eq!(
682            component.perform(Cmd::Scroll(Direction::Down)),
683            CmdResult::NoChange
684        );
685    }
686
687    #[test]
688    fn should_perform_scroll_up() {
689        let mut component = TreeView::default()
690            .scroll_step(4)
691            .with_tree(mock_tree())
692            .initial_node("aA1");
693        // Scroll Up (changed)
694        assert_eq!(
695            component.perform(Cmd::Scroll(Direction::Up)),
696            CmdResult::Changed(State::Single(StateValue::String(String::from("/"))))
697        );
698        // Scroll Up (unchanged)
699        assert_eq!(
700            component.perform(Cmd::Scroll(Direction::Up)),
701            CmdResult::NoChange
702        );
703    }
704
705    #[test]
706    fn should_perform_submit() {
707        let mut component = TreeView::default()
708            .with_tree(mock_tree())
709            .initial_node("aA1");
710        assert_eq!(
711            component.perform(Cmd::Submit),
712            CmdResult::Submit(State::Single(StateValue::String(String::from("aA1"))))
713        );
714    }
715
716    #[test]
717    fn should_perform_close() {
718        let mut component = TreeView::default()
719            .with_tree(mock_tree())
720            .initial_node("aA1");
721        component.states.open(component.tree.root());
722        assert_eq!(
723            component.perform(Cmd::Custom(TREE_CMD_CLOSE)),
724            CmdResult::Visual
725        );
726        assert!(
727            component
728                .tree_state()
729                .is_closed(component.tree().root().query(&String::from("aA1")).unwrap())
730        );
731    }
732
733    #[test]
734    fn should_perform_open() {
735        let mut component = TreeView::default()
736            .with_tree(mock_tree())
737            .initial_node("aA");
738        assert_eq!(
739            component.perform(Cmd::Custom(TREE_CMD_OPEN)),
740            CmdResult::Visual
741        );
742        assert!(
743            component
744                .tree_state()
745                .is_open(component.tree().root().query(&String::from("aA")).unwrap())
746        );
747    }
748
749    #[test]
750    fn should_update_tree() {
751        let mut component = TreeView::default()
752            .with_tree(mock_tree())
753            .preserve_state(true)
754            .initial_node("aA");
755        // open 'bB'
756        component.states.select(
757            component.tree.root(),
758            component.tree.root().query(&String::from("bB")).unwrap(),
759        );
760        component.states.open(component.tree.root());
761        // re-selecte 'aA'
762        component.states.select(
763            component.tree.root(),
764            component.tree.root().query(&String::from("aA")).unwrap(),
765        );
766        // Create new tree
767        let mut new_tree = mock_tree();
768        new_tree.root_mut().remove_child(&String::from("a"));
769        // Set new tree
770        component.set_tree(new_tree);
771        // selected item should be root
772        assert_eq!(component.states.selected().unwrap(), "/");
773    }
774}