Skip to main content

16_tree/
16_tree.rs

1//! Example 16: Tree View
2//!
3//! Demonstrates the Tree widget for hierarchical navigation.
4//!
5//! Run with: cargo run -p telex-tui --example 16_tree
6
7use crossterm::event::KeyCode;
8use telex::prelude::*;
9use telex::Color;
10
11telex::require_api!(0, 2);
12
13fn main() {
14    telex::run_with_theme(App, telex::theme::Theme::nord()).unwrap();
15}
16
17struct App;
18
19impl Component for App {
20    fn render(&self, cx: Scope) -> View {
21        let show_help = state!(cx, || false);
22
23        // F1 toggles help
24        cx.use_command(
25            KeyBinding::key(KeyCode::F(1)),
26            with!(show_help => move || show_help.update(|v| *v = !*v)),
27        );
28
29        // Track selected path
30        let selected = state!(cx, || vec![0usize]);
31
32        // Track expanded state for each node (by path prefix)
33        let expanded_paths = state!(cx, || {
34            vec![
35                vec![0],    // src/ expanded
36                vec![0, 0], // src/components/ expanded
37            ]
38        });
39
40        // Build tree items with current expanded state
41        let items = build_tree(&expanded_paths.get());
42
43        let on_select = with!(selected => move |path: TreePath| {
44            selected.set(path);
45        });
46
47        let on_activate = with!(expanded_paths => move |path: TreePath| {
48            // Toggle expand/collapse for the activated item
49            let mut paths = expanded_paths.get().clone();
50            if let Some(pos) = paths.iter().position(|p| *p == path) {
51                // Currently expanded, collapse it
52                paths.remove(pos);
53            } else {
54                // Currently collapsed, expand it
55                paths.push(path.clone());
56            }
57            expanded_paths.set(paths);
58        });
59
60        let selected_label = get_item_at_path(&items, &selected.get())
61            .map(|item| item.label.clone())
62            .unwrap_or_else(|| "Nothing".to_string());
63
64        View::vstack()
65            .child(
66                View::styled_text("File Browser")
67                    .color(Color::Cyan)
68                    .bold()
69                    .build(),
70            )
71            .child(
72                View::styled_text(format!("Selected: {}", selected_label))
73                    .dim()
74                    .build(),
75            )
76            .child(
77                View::boxed()
78                    .flex(1)
79                    .border(true)
80                    .child(
81                        View::tree()
82                            .items(items)
83                            .selected(selected.get().clone())
84                            .on_select(on_select)
85                            .on_activate(on_activate)
86                            .build(),
87                    )
88                    .build(),
89            )
90            .child(
91                View::styled_text(
92                    "↑↓/jk: navigate | Enter: expand/collapse | F1 help | Ctrl+Q: quit",
93                )
94                .dim()
95                .build(),
96            )
97            .child(
98                View::modal()
99                    .visible(show_help.get())
100                    .title("Example 16: Tree View")
101                    .on_dismiss(with!(show_help => move || show_help.set(false)))
102                    .child(
103                        View::vstack()
104                            .child(View::styled_text("What you're seeing").bold().build())
105                            .child(View::text("• Hierarchical tree widget"))
106                            .child(View::text("• Expand/collapse folders"))
107                            .child(View::text("• Path-based selection tracking"))
108                            .child(View::gap(1))
109                            .child(View::styled_text("Key concepts").bold().build())
110                            .child(View::text("• View::tree() for hierarchical data"))
111                            .child(View::text("• TreeItem::new().child() builds hierarchy"))
112                            .child(View::text("• on_select returns TreePath (Vec<usize>)"))
113                            .child(View::text("• on_activate for expand/collapse"))
114                            .child(View::gap(1))
115                            .child(View::styled_text("Try this").bold().build())
116                            .child(View::text("• Navigate with arrow keys"))
117                            .child(View::text("• Press Enter to expand/collapse folders"))
118                            .child(View::text("• Watch the 'Selected:' text update"))
119                            .child(View::gap(1))
120                            .child(View::styled_text("Next up").bold().build())
121                            .child(View::text("→ 17_table: data tables with sorting"))
122                            .child(View::gap(1))
123                            .child(View::styled_text("Press Escape to close").dim().build())
124                            .build(),
125                    )
126                    .build(),
127            )
128            .build()
129    }
130}
131
132fn build_tree(expanded_paths: &[TreePath]) -> Vec<TreeItem> {
133    let is_expanded = |path: &[usize]| expanded_paths.iter().any(|p| p == path);
134
135    vec![
136        TreeItem::new("src")
137            .icon("📁")
138            .expanded(is_expanded(&[0]))
139            .child(
140                TreeItem::new("components")
141                    .icon("📁")
142                    .expanded(is_expanded(&[0, 0]))
143                    .child(TreeItem::new("button.rs").icon("📄"))
144                    .child(TreeItem::new("input.rs").icon("📄"))
145                    .child(TreeItem::new("list.rs").icon("📄")),
146            )
147            .child(
148                TreeItem::new("utils")
149                    .icon("📁")
150                    .expanded(is_expanded(&[0, 1]))
151                    .child(TreeItem::new("helpers.rs").icon("📄"))
152                    .child(TreeItem::new("macros.rs").icon("📄")),
153            )
154            .child(TreeItem::new("main.rs").icon("📄"))
155            .child(TreeItem::new("lib.rs").icon("📄")),
156        TreeItem::new("tests")
157            .icon("📁")
158            .expanded(is_expanded(&[1]))
159            .child(TreeItem::new("integration_tests.rs").icon("📄"))
160            .child(TreeItem::new("unit_tests.rs").icon("📄")),
161        TreeItem::new("Cargo.toml").icon("📦"),
162        TreeItem::new("README.md").icon("📝"),
163    ]
164}
165
166fn get_item_at_path<'a>(items: &'a [TreeItem], path: &[usize]) -> Option<&'a TreeItem> {
167    if path.is_empty() {
168        return None;
169    }
170
171    let mut current_items = items;
172    let mut result = None;
173
174    for &idx in path {
175        if idx < current_items.len() {
176            result = Some(&current_items[idx]);
177            current_items = &current_items[idx].children;
178        } else {
179            return None;
180        }
181    }
182
183    result
184}