1use crate::component::{Component, EventCx};
2use crate::event::Event;
3use crate::geom::{Pos, Rect, Size};
4use crate::render::RenderCx;
5use crate::style::Style;
6use crate::text::Text;
7
8pub struct TreeNode {
10 pub label: Text,
11 pub children: Vec<TreeNode>,
12 pub expanded: bool,
13}
14
15struct VisibleEntry {
17 node_path: Vec<usize>, depth: usize,
20 is_last: bool,
21 has_children: bool,
22 expanded: bool,
23 label: Text,
24}
25
26pub struct Tree {
31 nodes: Vec<TreeNode>,
32 selected: usize,
34 show_guides: bool,
35 scroll_offset: usize,
36 rect: Rect,
37 style: Style,
38 select_style: Style,
39}
40
41impl Tree {
42 pub fn new(nodes: Vec<TreeNode>) -> Self {
44 Self {
45 nodes,
46 selected: 0,
47 show_guides: true,
48 scroll_offset: 0,
49 rect: Rect::default(),
50 style: Style::default(),
51 select_style: Style::default(),
52 }
53 }
54
55 pub fn show_guides(mut self, show: bool) -> Self {
56 self.show_guides = show;
57 self
58 }
59
60 pub fn style(mut self, style: Style) -> Self {
61 self.style = style;
62 self
63 }
64
65 pub fn select_style(mut self, style: Style) -> Self {
66 self.select_style = style;
67 self
68 }
69
70 pub fn selected_index(&self) -> usize {
71 self.selected
72 }
73
74 fn flatten(&self) -> Vec<VisibleEntry> {
76 let mut entries = Vec::new();
77 for (i, node) in self.nodes.iter().enumerate() {
78 self.flatten_node(node, &vec![i], 0, i == self.nodes.len() - 1, &mut entries);
79 }
80 entries
81 }
82
83 fn flatten_node(
84 &self,
85 node: &TreeNode,
86 path: &[usize],
87 depth: usize,
88 is_last: bool,
89 entries: &mut Vec<VisibleEntry>,
90 ) {
91 let has_children = !node.children.is_empty();
92 entries.push(VisibleEntry {
93 node_path: path.to_vec(),
94 depth,
95 is_last,
96 has_children,
97 expanded: node.expanded,
98 label: node.label.clone(),
99 });
100
101 if node.expanded {
102 let child_count = node.children.len();
103 for (i, child) in node.children.iter().enumerate() {
104 let mut child_path = path.to_vec();
105 child_path.push(i);
106 self.flatten_node(
107 child,
108 &child_path,
109 depth + 1,
110 i == child_count - 1,
111 entries,
112 );
113 }
114 }
115 }
116
117 fn node_at_path(&mut self, path: &[usize]) -> Option<&mut TreeNode> {
119 if path.is_empty() {
120 return None;
121 }
122 let mut node = self.nodes.get_mut(path[0])?;
123 for &idx in &path[1..] {
124 node = node.children.get_mut(idx)?;
125 }
126 Some(node)
127 }
128}
129
130impl Component for Tree {
131 fn render(&self, cx: &mut RenderCx) {
132 let entries = self.flatten();
133 if entries.is_empty() {
134 return;
135 }
136
137 let visible_height = self.rect.height.max(1) as usize;
138 let start = self.scroll_offset.min(entries.len().saturating_sub(1));
139 let end = (start + visible_height).min(entries.len());
140
141 for i in start..end {
142 let entry = &entries[i];
143 let is_selected = i == self.selected;
144 let row_y = self.rect.y + (i - start) as u16;
145
146 let mut prefix = String::new();
148
149 if self.show_guides {
150 if entry.depth > 0 {
152 let mut has_continuation = vec![false; entry.depth];
154 for a in 0..entry.depth {
155 for j in (i + 1)..entries.len() {
158 if entries[j].depth < a + 1 {
159 break;
160 }
161 if entries[j].depth == a + 1 {
162 has_continuation[a] = true;
163 break;
164 }
165 }
166 }
167
168 for a in 0..entry.depth {
169 if has_continuation[a] {
170 prefix.push_str("│ ");
171 } else {
172 prefix.push_str(" ");
173 }
174 }
175 }
176
177 if entry.depth > 0 {
179 if entry.is_last {
180 prefix.push_str("└─");
181 } else {
182 prefix.push_str("├─");
183 }
184 }
185 } else {
186 for _ in 0..entry.depth {
187 prefix.push_str(" ");
188 }
189 }
190
191 if entry.has_children {
193 if entry.expanded {
194 prefix.push_str("▼ ");
195 } else {
196 prefix.push_str("▶ ");
197 }
198 } else {
199 prefix.push_str(" ");
200 }
201
202 let style = if is_selected {
204 &self.select_style
205 } else {
206 &self.style
207 };
208
209 cx.buffer.write_text(
210 Pos {
211 x: self.rect.x,
212 y: row_y,
213 },
214 self.rect,
215 &prefix,
216 style,
217 );
218
219 let label_text = entry.label.first_text();
220 let prefix_w = crate::widgets::textarea::str_width(&prefix);
221 cx.buffer.write_text(
222 Pos {
223 x: self.rect.x + prefix_w,
224 y: row_y,
225 },
226 self.rect,
227 label_text,
228 style,
229 );
230 }
231 }
232
233 fn measure(
234 &self,
235 _constraint: crate::layout::Constraint,
236 _cx: &mut crate::component::MeasureCx,
237 ) -> Size {
238 let entries = self.flatten();
239 let max_w = entries
240 .iter()
241 .map(|e| (e.depth * 2 + 2) as u16 + e.label.max_width())
242 .max()
243 .unwrap_or(0);
244 Size {
245 width: max_w,
246 height: entries.len().max(1) as u16,
247 }
248 }
249
250 fn event(&mut self, event: &Event, cx: &mut EventCx) {
251 if matches!(event, Event::Focus | Event::Blur | Event::Tick) {
252 return;
253 }
254
255 let entries = self.flatten();
256 if entries.is_empty() {
257 return;
258 }
259
260 if let Event::Key(key_event) = event {
261 match &key_event.key {
262 crate::event::Key::Up => {
263 if self.selected > 0 {
264 self.selected -= 1;
265 self.scroll_to_selected(entries.len());
266 cx.invalidate_paint();
267 }
268 }
269 crate::event::Key::Down => {
270 if self.selected + 1 < entries.len() {
271 self.selected += 1;
272 self.scroll_to_selected(entries.len());
273 cx.invalidate_paint();
274 }
275 }
276 crate::event::Key::Enter | crate::event::Key::Char(' ') => {
277 self.toggle_selected();
278 cx.invalidate_paint();
279 }
280 crate::event::Key::Right => {
281 self.expand_selected();
282 cx.invalidate_paint();
283 }
284 crate::event::Key::Left => {
285 self.collapse_or_parent(&entries);
286 cx.invalidate_paint();
287 }
288 crate::event::Key::Home => {
289 self.selected = 0;
290 self.scroll_offset = 0;
291 cx.invalidate_paint();
292 }
293 crate::event::Key::End => {
294 self.selected = entries.len().saturating_sub(1);
295 self.scroll_to_selected(entries.len());
296 cx.invalidate_paint();
297 }
298 _ => {}
299 }
300 }
301 }
302
303 fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
304 self.rect = rect;
305 }
306
307 fn focusable(&self) -> bool {
308 false
309 }
310
311 fn style(&self) -> Style {
312 self.style.clone()
313 }
314}
315
316impl Tree {
317 fn toggle_selected(&mut self) {
318 let entries = self.flatten();
319 if let Some(entry) = entries.get(self.selected) {
320 if entry.has_children {
321 if let Some(node) = self.node_at_path(&entry.node_path) {
322 node.expanded = !node.expanded;
323 }
324 }
325 }
326 }
327
328 fn expand_selected(&mut self) {
329 let entries = self.flatten();
330 if let Some(entry) = entries.get(self.selected) {
331 if entry.has_children && !entry.expanded {
332 if let Some(node) = self.node_at_path(&entry.node_path) {
333 node.expanded = true;
334 }
335 }
336 }
337 }
338
339 fn collapse_or_parent(&mut self, entries: &[VisibleEntry]) {
340 if let Some(entry) = entries.get(self.selected) {
341 if entry.has_children && entry.expanded {
342 if let Some(node) = self.node_at_path(&entry.node_path) {
344 node.expanded = false;
345 }
346 } else if entry.depth > 0 {
347 let parent_depth = entry.depth - 1;
349 for (i, e) in entries.iter().enumerate() {
350 if e.node_path.len() == entry.node_path.len() - 1
351 && e.depth == parent_depth
352 {
353 self.selected = i;
354 break;
355 }
356 }
357 }
358 }
359 }
360
361 fn scroll_to_selected(&mut self, total_visible: usize) {
362 let visible_height = self.rect.height.max(1) as usize;
363 if self.selected < self.scroll_offset {
364 self.scroll_offset = self.selected;
365 } else if self.selected >= self.scroll_offset + visible_height {
366 self.scroll_offset = self.selected.saturating_sub(visible_height.saturating_sub(1));
367 }
368 self.scroll_offset = self.scroll_offset.min(
369 total_visible.saturating_sub(visible_height),
370 );
371 }
372}