1use 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 cx.use_command(
25 KeyBinding::key(KeyCode::F(1)),
26 with!(show_help => move || show_help.update(|v| *v = !*v)),
27 );
28
29 let selected = state!(cx, || vec![0usize]);
31
32 let expanded_paths = state!(cx, || {
34 vec![
35 vec![0], vec![0, 0], ]
38 });
39
40 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 let mut paths = expanded_paths.get().clone();
50 if let Some(pos) = paths.iter().position(|p| *p == path) {
51 paths.remove(pos);
53 } else {
54 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(¤t_items[idx]);
177 current_items = ¤t_items[idx].children;
178 } else {
179 return None;
180 }
181 }
182
183 result
184}