tui_realm_treeview/
lib.rs

1//! # tui-realm-treeview
2//!
3//! [tui-realm-treeview](https://github.com/veeso/tui-realm-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 = "2"
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 = "2", 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` mock 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//! extern crate tui_realm_treeview;
82//! extern crate tuirealm;
83//!
84//! use tuirealm::{
85//!     command::{Cmd, CmdResult, Direction, Position},
86//!     event::{Event, Key, KeyEvent, KeyModifiers},
87//!     props::{Alignment, BorderType, Borders, Color, Style},
88//!     Component, MockComponent, NoUserEvent, State, StateValue,
89//! };
90//! // treeview
91//! use tui_realm_treeview::{Node, Tree, TreeView, TREE_CMD_CLOSE, TREE_CMD_OPEN};
92//!
93//! #[derive(Debug, PartialEq)]
94//! pub enum Msg {
95//!     ExtendDir(String),
96//!     GoToUpperDir,
97//!     None,
98//! }
99//!
100//! #[derive(MockComponent)]
101//! pub struct FsTree {
102//!     component: TreeView<String>,
103//! }
104//!
105//! impl FsTree {
106//!     pub fn new(tree: Tree<String>, initial_node: Option<String>) -> Self {
107//!         // Preserve initial node if exists
108//!         let initial_node = match initial_node {
109//!             Some(id) if tree.root().query(&id).is_some() => id,
110//!             _ => tree.root().id().to_string(),
111//!         };
112//!         FsTree {
113//!             component: TreeView::default()
114//!                 .foreground(Color::Reset)
115//!                 .borders(
116//!                     Borders::default()
117//!                         .color(Color::LightYellow)
118//!                         .modifiers(BorderType::Rounded),
119//!                 )
120//!                 .inactive(Style::default().fg(Color::Gray))
121//!                 .indent_size(3)
122//!                 .scroll_step(6)
123//!                 .title(tree.root().id(), Alignment::Left)
124//!                 .highlighted_color(Color::LightYellow)
125//!                 .highlight_symbol("🦄")
126//!                 .with_tree(tree)
127//!                 .initial_node(initial_node),
128//!         }
129//!     }
130//! }
131//!
132//! impl Component<Msg, NoUserEvent> for FsTree {
133//!     fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
134//!         let result = match ev {
135//!             Event::Keyboard(KeyEvent {
136//!                 code: Key::Left,
137//!                 modifiers: KeyModifiers::NONE,
138//!             }) => self.perform(Cmd::Custom(TREE_CMD_CLOSE)),
139//!             Event::Keyboard(KeyEvent {
140//!                 code: Key::Right,
141//!                 modifiers: KeyModifiers::NONE,
142//!             }) => self.perform(Cmd::Custom(TREE_CMD_OPEN)),
143//!             Event::Keyboard(KeyEvent {
144//!                 code: Key::PageDown,
145//!                 modifiers: KeyModifiers::NONE,
146//!             }) => self.perform(Cmd::Scroll(Direction::Down)),
147//!             Event::Keyboard(KeyEvent {
148//!                 code: Key::PageUp,
149//!                 modifiers: KeyModifiers::NONE,
150//!             }) => self.perform(Cmd::Scroll(Direction::Up)),
151//!             Event::Keyboard(KeyEvent {
152//!                 code: Key::Down,
153//!                 modifiers: KeyModifiers::NONE,
154//!             }) => self.perform(Cmd::Move(Direction::Down)),
155//!             Event::Keyboard(KeyEvent {
156//!                 code: Key::Up,
157//!                 modifiers: KeyModifiers::NONE,
158//!             }) => self.perform(Cmd::Move(Direction::Up)),
159//!             Event::Keyboard(KeyEvent {
160//!                 code: Key::Home,
161//!                 modifiers: KeyModifiers::NONE,
162//!             }) => self.perform(Cmd::GoTo(Position::Begin)),
163//!             Event::Keyboard(KeyEvent {
164//!                 code: Key::End,
165//!                 modifiers: KeyModifiers::NONE,
166//!             }) => self.perform(Cmd::GoTo(Position::End)),
167//!             Event::Keyboard(KeyEvent {
168//!                 code: Key::Enter,
169//!                 modifiers: KeyModifiers::NONE,
170//!             }) => self.perform(Cmd::Submit),
171//!             Event::Keyboard(KeyEvent {
172//!                 code: Key::Backspace,
173//!                 modifiers: KeyModifiers::NONE,
174//!             }) => return Some(Msg::GoToUpperDir),
175//!             _ => return None,
176//!         };
177//!         match result {
178//!             CmdResult::Submit(State::One(StateValue::String(node))) => Some(Msg::ExtendDir(node)),
179//!             _ => Some(Msg::None),
180//!         }
181//!     }
182//! }
183//!
184//! ```
185//!
186//! ---
187//!
188//! ## Tree widget
189//!
190//! If you want, you can also implement your own version of a tree view mock component using the `TreeWidget`
191//! in order to render a tree.
192//! Keep in mind that if you want to create a stateful tree (with highlighted item), you'll need to render it
193//! as a stateful widget, passing to it a `TreeState`, which is provided by this library.
194//!
195
196#![doc(html_playground_url = "https://play.rust-lang.org")]
197#![doc(
198    html_favicon_url = "https://raw.githubusercontent.com/veeso/tui-realm-treeview/main/docs/images/cargo/tui-realm-treeview-128.png"
199)]
200#![doc(
201    html_logo_url = "https://raw.githubusercontent.com/veeso/tui-realm-treeview/main/docs/images/cargo/tui-realm-treeview-512.png"
202)]
203
204// -- mock
205#[cfg(test)]
206pub(crate) mod mock;
207// -- modules
208mod tree_state;
209mod widget;
210
211use std::iter;
212
213// deps
214pub use orange_trees::{Node as OrangeNode, Tree as OrangeTree};
215// internal
216pub use tree_state::TreeState;
217use tuirealm::command::{Cmd, CmdResult, Direction, Position};
218use tuirealm::props::{
219    Alignment, AttrValue, Attribute, Borders, Color, Props, Style, TextModifiers, TextSpan,
220};
221use tuirealm::ratatui::layout::Rect;
222use tuirealm::ratatui::widgets::Block;
223use tuirealm::{Frame, MockComponent, State, StateValue};
224pub use widget::TreeWidget;
225
226/// [`Tree`] node value.
227pub trait NodeValue: Default {
228    /// Return iterator over render parts - text with it style.
229    /// If style is `None`, then it will be inherited from widget style.
230    fn render_parts_iter(&self) -> impl Iterator<Item = (&str, Option<Style>)>;
231}
232
233impl NodeValue for String {
234    fn render_parts_iter(&self) -> impl Iterator<Item = (&str, Option<Style>)> {
235        iter::once((self.as_str(), None))
236    }
237}
238
239impl NodeValue for Vec<TextSpan> {
240    fn render_parts_iter(&self) -> impl Iterator<Item = (&str, Option<Style>)> {
241        self.iter().map(|span| {
242            (
243                span.content.as_str(),
244                Some(
245                    Style::new()
246                        .fg(span.fg)
247                        .bg(span.bg)
248                        .add_modifier(span.modifiers),
249                ),
250            )
251        })
252    }
253}
254
255// -- type override
256pub type Node<V> = OrangeNode<String, V>;
257pub type Tree<V> = OrangeTree<String, V>;
258
259// -- props
260
261pub const TREE_INDENT_SIZE: &str = "indent-size";
262pub const TREE_INITIAL_NODE: &str = "initial-mode";
263pub const TREE_PRESERVE_STATE: &str = "preserve-state";
264
265// -- Cmd
266
267pub const TREE_CMD_OPEN: &str = "o";
268pub const TREE_CMD_CLOSE: &str = "c";
269
270// -- component
271
272/// ## TreeView
273///
274/// Tree view Mock component for tui-realm
275pub struct TreeView<V: NodeValue> {
276    props: Props,
277    states: TreeState,
278    /// The actual Tree data structure. You can access this from your Component to operate on it
279    /// for example after a certain events.
280    tree: Tree<V>,
281}
282
283impl<V: NodeValue> Default for TreeView<V> {
284    fn default() -> Self {
285        Self {
286            props: Props::default(),
287            states: TreeState::default(),
288            tree: Tree::new(Node::new(String::new(), V::default())),
289        }
290    }
291}
292
293impl<V: NodeValue> TreeView<V> {
294    /// Set widget foreground
295    pub fn foreground(mut self, fg: Color) -> Self {
296        self.attr(Attribute::Foreground, AttrValue::Color(fg));
297        self
298    }
299
300    /// Set widget background
301    pub fn background(mut self, bg: Color) -> Self {
302        self.attr(Attribute::Background, AttrValue::Color(bg));
303        self
304    }
305
306    /// Set another style from default to use when component is inactive
307    pub fn inactive(mut self, s: Style) -> Self {
308        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
309        self
310    }
311
312    /// Set widget border properties
313    pub fn borders(mut self, b: Borders) -> Self {
314        self.attr(Attribute::Borders, AttrValue::Borders(b));
315        self
316    }
317
318    /// Set widget text modifiers
319    pub fn modifiers(mut self, m: TextModifiers) -> Self {
320        self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
321        self
322    }
323
324    /// Set widget title
325    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
326        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
327        self
328    }
329
330    /// Set symbol to prepend to highlighted node
331    pub fn highlight_symbol<S: Into<String>>(mut self, symbol: S) -> Self {
332        self.attr(Attribute::HighlightedStr, AttrValue::String(symbol.into()));
333        self
334    }
335
336    /// Set color to apply to highlighted item
337    pub fn highlighted_color(mut self, color: Color) -> Self {
338        self.attr(Attribute::HighlightedColor, AttrValue::Color(color));
339        self
340    }
341
342    /// Set initial node for tree state.
343    /// NOTE: this must be specified after `with_tree`
344    pub fn initial_node<S: Into<String>>(mut self, node: S) -> Self {
345        self.attr(
346            Attribute::Custom(TREE_INITIAL_NODE),
347            AttrValue::String(node.into()),
348        );
349        self
350    }
351
352    /// Set whether to preserve state on tree change
353    pub fn preserve_state(mut self, preserve: bool) -> Self {
354        self.attr(
355            Attribute::Custom(TREE_PRESERVE_STATE),
356            AttrValue::Flag(preserve),
357        );
358        self
359    }
360
361    /// Set indent size for widget for each level of depth
362    pub fn indent_size(mut self, sz: u16) -> Self {
363        self.attr(Attribute::Custom(TREE_INDENT_SIZE), AttrValue::Size(sz));
364        self
365    }
366
367    /// Set scroll step for scrolling command
368    pub fn scroll_step(mut self, step: usize) -> Self {
369        self.attr(Attribute::ScrollStep, AttrValue::Length(step));
370        self
371    }
372
373    /// Set tree to use as data
374    pub fn with_tree(mut self, tree: Tree<V>) -> Self {
375        self.tree = tree;
376        self
377    }
378
379    /// Get a reference to tree
380    pub fn tree(&self) -> &Tree<V> {
381        &self.tree
382    }
383
384    /// Get mutable reference to tree
385    pub fn tree_mut(&mut self) -> &mut Tree<V> {
386        &mut self.tree
387    }
388
389    /// Set new tree in component.
390    /// Current state is preserved if `PRESERVE_STATE` is set to `AttrValue::Flag(true)`
391    pub fn set_tree(&mut self, tree: Tree<V>) {
392        self.tree = tree;
393        self.states.tree_changed(
394            self.tree.root(),
395            self.props
396                .get_or(
397                    Attribute::Custom(TREE_PRESERVE_STATE),
398                    AttrValue::Flag(false),
399                )
400                .unwrap_flag(),
401        );
402    }
403
404    /// Get a reference to the current tree state
405    pub fn tree_state(&self) -> &TreeState {
406        &self.states
407    }
408
409    /// Returns whether selectd node has changed
410    fn changed(&self, prev: Option<&str>) -> CmdResult {
411        match self.states.selected() {
412            None => CmdResult::None,
413            id if id != prev => CmdResult::Changed(self.state()),
414            _ => CmdResult::None,
415        }
416    }
417
418    fn get_block(
419        props: Borders,
420        title: (&str, Alignment),
421        focus: bool,
422        inactive_style: Option<Style>,
423    ) -> Block<'_> {
424        Block::default()
425            .borders(props.sides)
426            .border_style(match focus {
427                true => props.style(),
428                false => inactive_style
429                    .unwrap_or_else(|| Style::default().fg(Color::Reset).bg(Color::Reset)),
430            })
431            .border_type(props.modifiers)
432            .title(title.0)
433            .title_alignment(title.1)
434    }
435}
436
437// -- mock
438
439impl<V: NodeValue> MockComponent for TreeView<V> {
440    fn view(&mut self, frame: &mut Frame, area: Rect) {
441        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
442            let foreground = self
443                .props
444                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
445                .unwrap_color();
446            let background = self
447                .props
448                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
449                .unwrap_color();
450            let modifiers = self
451                .props
452                .get_or(
453                    Attribute::TextProps,
454                    AttrValue::TextModifiers(TextModifiers::empty()),
455                )
456                .unwrap_text_modifiers();
457            let title = self
458                .props
459                .get_ref(Attribute::Title)
460                .and_then(|v| v.as_title())
461                .map(|v| (v.0.as_str(), v.1))
462                .unwrap_or(("", Alignment::Center));
463            let borders = self
464                .props
465                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
466                .unwrap_borders();
467            let focus = self
468                .props
469                .get_or(Attribute::Focus, AttrValue::Flag(false))
470                .unwrap_flag();
471            let inactive_style = self
472                .props
473                .get(Attribute::FocusStyle)
474                .map(|x| x.unwrap_style());
475            let indent_size = self
476                .props
477                .get_or(Attribute::Custom(TREE_INDENT_SIZE), AttrValue::Size(4))
478                .unwrap_size();
479            let hg_color = self
480                .props
481                .get_or(Attribute::HighlightedColor, AttrValue::Color(foreground))
482                .unwrap_color();
483            let hg_style = match focus {
484                true => Style::default().bg(hg_color).fg(Color::Black),
485                false => Style::default().fg(hg_color),
486            }
487            .add_modifier(modifiers);
488            let hg_str = self
489                .props
490                .get_ref(Attribute::HighlightedStr)
491                .and_then(|x| x.as_string());
492            let div = Self::get_block(borders, title, focus, inactive_style);
493            // Make widget
494            let mut tree = TreeWidget::new(self.tree())
495                .block(div)
496                .highlight_style(hg_style)
497                .indent_size(indent_size.into())
498                .style(
499                    Style::default()
500                        .fg(foreground)
501                        .bg(background)
502                        .add_modifier(modifiers),
503                );
504            if let Some(hg_str) = hg_str {
505                tree = tree.highlight_symbol(hg_str.as_str());
506            }
507            let mut state = self.states.clone();
508            frame.render_stateful_widget(tree, area, &mut state);
509        }
510    }
511
512    fn query(&self, attr: Attribute) -> Option<AttrValue> {
513        self.props.get(attr)
514    }
515
516    fn attr(&mut self, attr: Attribute, value: AttrValue) {
517        // Initial node
518        if matches!(attr, Attribute::Custom(TREE_INITIAL_NODE)) {
519            // Select node if exists
520            if let Some(node) = self.tree.root().query(&value.unwrap_string()) {
521                self.states.select(self.tree.root(), node);
522            }
523        } else {
524            self.props.set(attr, value);
525        }
526    }
527
528    fn state(&self) -> State {
529        match self.states.selected() {
530            None => State::None,
531            Some(id) => State::One(StateValue::String(id.to_string())),
532        }
533    }
534
535    fn perform(&mut self, cmd: Cmd) -> CmdResult {
536        match cmd {
537            Cmd::GoTo(Position::Begin) => {
538                let prev = self.states.selected().map(|x| x.to_string());
539                // Get first sibling of current node
540                if let Some(first) = self.states.first_sibling(self.tree.root()) {
541                    self.states.select(self.tree.root(), first);
542                }
543                self.changed(prev.as_deref())
544            }
545            Cmd::GoTo(Position::End) => {
546                let prev = self.states.selected().map(|x| x.to_string());
547                // Get first sibling of current node
548                if let Some(last) = self.states.last_sibling(self.tree.root()) {
549                    self.states.select(self.tree.root(), last);
550                }
551                self.changed(prev.as_deref())
552            }
553            Cmd::Move(Direction::Down) => {
554                let prev = self.states.selected().map(|x| x.to_string());
555                self.states.move_down(self.tree.root());
556                self.changed(prev.as_deref())
557            }
558            Cmd::Move(Direction::Up) => {
559                let prev = self.states.selected().map(|x| x.to_string());
560                self.states.move_up(self.tree.root());
561                self.changed(prev.as_deref())
562            }
563            Cmd::Scroll(Direction::Down) => {
564                let prev = self.states.selected().map(|x| x.to_string());
565                let step = self
566                    .props
567                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
568                    .unwrap_length();
569                (0..step).for_each(|_| self.states.move_down(self.tree.root()));
570                self.changed(prev.as_deref())
571            }
572            Cmd::Scroll(Direction::Up) => {
573                let prev = self.states.selected().map(|x| x.to_string());
574                let step = self
575                    .props
576                    .get_or(Attribute::ScrollStep, AttrValue::Length(8))
577                    .unwrap_length();
578                (0..step).for_each(|_| self.states.move_up(self.tree.root()));
579                self.changed(prev.as_deref())
580            }
581            Cmd::Submit => CmdResult::Submit(self.state()),
582            Cmd::Custom(TREE_CMD_CLOSE) => {
583                // close selected node
584                self.states.close(self.tree.root());
585                CmdResult::None
586            }
587            Cmd::Custom(TREE_CMD_OPEN) => {
588                // close selected node
589                self.states.open(self.tree.root());
590                CmdResult::None
591            }
592            _ => CmdResult::None,
593        }
594    }
595}
596
597#[cfg(test)]
598mod test {
599
600    use pretty_assertions::assert_eq;
601
602    use super::*;
603    use crate::mock::mock_tree;
604
605    #[test]
606    fn should_initialize_component() {
607        let mut component = TreeView::default()
608            .background(Color::White)
609            .foreground(Color::Cyan)
610            .borders(Borders::default())
611            .inactive(Style::default())
612            .indent_size(4)
613            .modifiers(TextModifiers::all())
614            .preserve_state(true)
615            .scroll_step(4)
616            .title("My tree", Alignment::Center)
617            .with_tree(mock_tree())
618            .initial_node("aB1");
619        // Check tree
620        assert_eq!(component.tree_state().selected().unwrap(), "aB1");
621        assert!(component.tree().root().query(&String::from("aB")).is_some());
622        component
623            .tree_mut()
624            .root_mut()
625            .add_child(Node::new(String::from("d"), String::from("d")));
626    }
627
628    #[test]
629    fn should_return_consistent_state() {
630        let component = TreeView::default().with_tree(mock_tree());
631        assert_eq!(component.state(), State::None);
632        let component = TreeView::default()
633            .with_tree(mock_tree())
634            .initial_node("aA");
635        assert_eq!(
636            component.state(),
637            State::One(StateValue::String(String::from("aA")))
638        );
639    }
640
641    #[test]
642    fn should_perform_go_to_begin() {
643        let mut component = TreeView::default()
644            .with_tree(mock_tree())
645            .initial_node("bB3");
646        // GoTo begin (changed)
647        assert_eq!(
648            component.perform(Cmd::GoTo(Position::Begin)),
649            CmdResult::Changed(State::One(StateValue::String(String::from("bB0"))))
650        );
651        // GoTo begin (unchanged)
652        assert_eq!(
653            component.perform(Cmd::GoTo(Position::Begin)),
654            CmdResult::None
655        );
656    }
657
658    #[test]
659    fn should_perform_go_to_end() {
660        let mut component = TreeView::default()
661            .with_tree(mock_tree())
662            .initial_node("bB1");
663        // GoTo end (changed)
664        assert_eq!(
665            component.perform(Cmd::GoTo(Position::End)),
666            CmdResult::Changed(State::One(StateValue::String(String::from("bB5"))))
667        );
668        // GoTo end (unchanged)
669        assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
670    }
671
672    #[test]
673    fn should_perform_move_down() {
674        let mut component = TreeView::default()
675            .with_tree(mock_tree())
676            .initial_node("cA1");
677        // Move down (changed)
678        assert_eq!(
679            component.perform(Cmd::Move(Direction::Down)),
680            CmdResult::Changed(State::One(StateValue::String(String::from("cA2"))))
681        );
682        // Move down (unchanged)
683        assert_eq!(
684            component.perform(Cmd::Move(Direction::Down)),
685            CmdResult::None
686        );
687    }
688
689    #[test]
690    fn should_perform_move_up() {
691        let mut component = TreeView::default().with_tree(mock_tree()).initial_node("a");
692        // Move up (changed)
693        assert_eq!(
694            component.perform(Cmd::Move(Direction::Up)),
695            CmdResult::Changed(State::One(StateValue::String(String::from("/"))))
696        );
697        // Move up (unchanged)
698        assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
699    }
700
701    #[test]
702    fn should_perform_scroll_down() {
703        let mut component = TreeView::default()
704            .scroll_step(2)
705            .with_tree(mock_tree())
706            .initial_node("cA0");
707        // Scroll down (changed)
708        assert_eq!(
709            component.perform(Cmd::Scroll(Direction::Down)),
710            CmdResult::Changed(State::One(StateValue::String(String::from("cA2"))))
711        );
712        // Scroll down (unchanged)
713        assert_eq!(
714            component.perform(Cmd::Scroll(Direction::Down)),
715            CmdResult::None
716        );
717    }
718
719    #[test]
720    fn should_perform_scroll_up() {
721        let mut component = TreeView::default()
722            .scroll_step(4)
723            .with_tree(mock_tree())
724            .initial_node("aA1");
725        // Scroll Up (changed)
726        assert_eq!(
727            component.perform(Cmd::Scroll(Direction::Up)),
728            CmdResult::Changed(State::One(StateValue::String(String::from("/"))))
729        );
730        // Scroll Up (unchanged)
731        assert_eq!(
732            component.perform(Cmd::Scroll(Direction::Up)),
733            CmdResult::None
734        );
735    }
736
737    #[test]
738    fn should_perform_submit() {
739        let mut component = TreeView::default()
740            .with_tree(mock_tree())
741            .initial_node("aA1");
742        assert_eq!(
743            component.perform(Cmd::Submit),
744            CmdResult::Submit(State::One(StateValue::String(String::from("aA1"))))
745        );
746    }
747
748    #[test]
749    fn should_perform_close() {
750        let mut component = TreeView::default()
751            .with_tree(mock_tree())
752            .initial_node("aA1");
753        component.states.open(component.tree.root());
754        assert_eq!(
755            component.perform(Cmd::Custom(TREE_CMD_CLOSE)),
756            CmdResult::None
757        );
758        assert!(
759            component
760                .tree_state()
761                .is_closed(component.tree().root().query(&String::from("aA1")).unwrap())
762        );
763    }
764
765    #[test]
766    fn should_perform_open() {
767        let mut component = TreeView::default()
768            .with_tree(mock_tree())
769            .initial_node("aA");
770        assert_eq!(
771            component.perform(Cmd::Custom(TREE_CMD_OPEN)),
772            CmdResult::None
773        );
774        assert!(
775            component
776                .tree_state()
777                .is_open(component.tree().root().query(&String::from("aA")).unwrap())
778        );
779    }
780
781    #[test]
782    fn should_update_tree() {
783        let mut component = TreeView::default()
784            .with_tree(mock_tree())
785            .preserve_state(true)
786            .initial_node("aA");
787        // open 'bB'
788        component.states.select(
789            component.tree.root(),
790            component.tree.root().query(&String::from("bB")).unwrap(),
791        );
792        component.states.open(component.tree.root());
793        // re-selecte 'aA'
794        component.states.select(
795            component.tree.root(),
796            component.tree.root().query(&String::from("aA")).unwrap(),
797        );
798        // Create new tree
799        let mut new_tree = mock_tree();
800        new_tree.root_mut().remove_child(&String::from("a"));
801        // Set new tree
802        component.set_tree(new_tree);
803        // selected item should be root
804        assert_eq!(component.states.selected().unwrap(), "/");
805    }
806}