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}