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}