Skip to main content

slt/context/widgets_interactive/
tree_widgets.rs

1use super::*;
2use crate::{DirectoryTreeState, TreeNode};
3
4impl Context {
5    /// Render a tree view. Left/Right to collapse/expand, Up/Down to navigate.
6    pub fn tree(&mut self, state: &mut TreeState) -> Response {
7        let entries = state.flatten();
8        if entries.is_empty() {
9            return Response::none();
10        }
11        state.selected = state.selected.min(entries.len().saturating_sub(1));
12        let old_selected = state.selected;
13        let focused = self.register_focusable();
14        let interaction_id = self.next_interaction_id();
15        let mut response = self.response_for(interaction_id);
16        response.focused = focused;
17        let mut changed = false;
18
19        if focused {
20            let mut consumed_indices = Vec::new();
21            for (i, event) in self.events.iter().enumerate() {
22                if self.consumed[i] {
23                    continue;
24                }
25                if let Event::Key(key) = event {
26                    if key.kind != KeyEventKind::Press {
27                        continue;
28                    }
29                    match key.code {
30                        KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
31                            let max_index = state.flatten().len().saturating_sub(1);
32                            let _ = handle_vertical_nav(
33                                &mut state.selected,
34                                max_index,
35                                key.code.clone(),
36                            );
37                            changed = changed || state.selected != old_selected;
38                            consumed_indices.push(i);
39                        }
40                        KeyCode::Right | KeyCode::Enter | KeyCode::Char(' ') => {
41                            state.toggle_at(state.selected);
42                            changed = true;
43                            consumed_indices.push(i);
44                        }
45                        KeyCode::Left => {
46                            let entry = &entries[state.selected.min(entries.len() - 1)];
47                            if entry.expanded {
48                                state.toggle_at(state.selected);
49                                changed = true;
50                            }
51                            consumed_indices.push(i);
52                        }
53                        _ => {}
54                    }
55                }
56            }
57            for idx in consumed_indices {
58                self.consumed[idx] = true;
59            }
60        }
61
62        self.commands.push(Command::BeginContainer {
63            direction: Direction::Column,
64            gap: 0,
65            align: Align::Start,
66            align_self: None,
67            justify: Justify::Start,
68            border: None,
69            border_sides: BorderSides::all(),
70            border_style: Style::new().fg(self.theme.border),
71            bg_color: None,
72            padding: Padding::default(),
73            margin: Margin::default(),
74            constraints: Constraints::default(),
75            title: None,
76            grow: 0,
77            group_name: None,
78        });
79
80        let entries = state.flatten();
81        for (idx, entry) in entries.iter().enumerate() {
82            let indent = "  ".repeat(entry.depth);
83            let icon = if entry.is_leaf {
84                "  "
85            } else if entry.expanded {
86                "▾ "
87            } else {
88                "▸ "
89            };
90            let is_selected = idx == state.selected;
91            let style = if is_selected && focused {
92                Style::new().bold().fg(self.theme.primary)
93            } else if is_selected {
94                Style::new().fg(self.theme.primary)
95            } else {
96                Style::new().fg(self.theme.text)
97            };
98            let cursor = if is_selected && focused { "▸" } else { " " };
99            let mut row =
100                String::with_capacity(cursor.len() + indent.len() + icon.len() + entry.label.len());
101            row.push_str(cursor);
102            row.push_str(&indent);
103            row.push_str(icon);
104            row.push_str(&entry.label);
105            self.styled(row, style);
106        }
107
108        self.commands.push(Command::EndContainer);
109        self.last_text_idx = None;
110        response.changed = changed || state.selected != old_selected;
111        response
112    }
113
114    /// Render a directory tree with guide lines and tree connectors.
115    pub fn directory_tree(&mut self, state: &mut DirectoryTreeState) -> Response {
116        let entries = state.tree.flatten();
117        if entries.is_empty() {
118            return Response::none();
119        }
120        state.tree.selected = state.tree.selected.min(entries.len().saturating_sub(1));
121        let old_selected = state.tree.selected;
122        let focused = self.register_focusable();
123        let interaction_id = self.next_interaction_id();
124        let mut response = self.response_for(interaction_id);
125        response.focused = focused;
126        let mut changed = false;
127
128        if focused {
129            let mut consumed_indices = Vec::new();
130            for (i, event) in self.events.iter().enumerate() {
131                if self.consumed[i] {
132                    continue;
133                }
134                if let Event::Key(key) = event {
135                    if key.kind != KeyEventKind::Press {
136                        continue;
137                    }
138                    match key.code {
139                        KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
140                            let max_index = state.tree.flatten().len().saturating_sub(1);
141                            let _ = handle_vertical_nav(
142                                &mut state.tree.selected,
143                                max_index,
144                                key.code.clone(),
145                            );
146                            changed = changed || state.tree.selected != old_selected;
147                            consumed_indices.push(i);
148                        }
149                        KeyCode::Right => {
150                            let current_entries = state.tree.flatten();
151                            let entry = &current_entries
152                                [state.tree.selected.min(current_entries.len() - 1)];
153                            if !entry.is_leaf && !entry.expanded {
154                                state.tree.toggle_at(state.tree.selected);
155                                changed = true;
156                            }
157                            consumed_indices.push(i);
158                        }
159                        KeyCode::Enter | KeyCode::Char(' ') => {
160                            state.tree.toggle_at(state.tree.selected);
161                            changed = true;
162                            consumed_indices.push(i);
163                        }
164                        KeyCode::Left => {
165                            let current_entries = state.tree.flatten();
166                            let entry = &current_entries
167                                [state.tree.selected.min(current_entries.len() - 1)];
168                            if entry.expanded {
169                                state.tree.toggle_at(state.tree.selected);
170                                changed = true;
171                            }
172                            consumed_indices.push(i);
173                        }
174                        _ => {}
175                    }
176                }
177            }
178            for idx in consumed_indices {
179                self.consumed[idx] = true;
180            }
181        }
182
183        self.commands.push(Command::BeginContainer {
184            direction: Direction::Column,
185            gap: 0,
186            align: Align::Start,
187            align_self: None,
188            justify: Justify::Start,
189            border: None,
190            border_sides: BorderSides::all(),
191            border_style: Style::new().fg(self.theme.border),
192            bg_color: None,
193            padding: Padding::default(),
194            margin: Margin::default(),
195            constraints: Constraints::default(),
196            title: None,
197            grow: 0,
198            group_name: None,
199        });
200
201        let mut rows = Vec::new();
202        flatten_directory_rows(&state.tree.nodes, Vec::new(), &mut rows);
203        for (idx, row_entry) in rows.iter().enumerate() {
204            let mut row = String::new();
205            let cursor = if idx == state.tree.selected && focused {
206                "▸"
207            } else {
208                " "
209            };
210            row.push_str(cursor);
211            row.push(' ');
212
213            if row_entry.depth > 0 {
214                for has_more in &row_entry.branch_mask {
215                    if *has_more {
216                        row.push_str("│   ");
217                    } else {
218                        row.push_str("    ");
219                    }
220                }
221                if row_entry.is_last {
222                    row.push_str("└── ");
223                } else {
224                    row.push_str("├── ");
225                }
226            }
227
228            let icon = if row_entry.is_leaf {
229                "  "
230            } else if row_entry.expanded {
231                "▾ "
232            } else {
233                "▸ "
234            };
235            if state.show_icons {
236                row.push_str(icon);
237            }
238            row.push_str(&row_entry.label);
239
240            let style = if idx == state.tree.selected && focused {
241                Style::new().bold().fg(self.theme.primary)
242            } else if idx == state.tree.selected {
243                Style::new().fg(self.theme.primary)
244            } else {
245                Style::new().fg(self.theme.text)
246            };
247            self.styled(row, style);
248        }
249
250        self.commands.push(Command::EndContainer);
251        self.last_text_idx = None;
252        response.changed = changed || state.tree.selected != old_selected;
253        response
254    }
255}
256
257struct DirectoryRenderRow {
258    depth: usize,
259    label: String,
260    is_leaf: bool,
261    expanded: bool,
262    is_last: bool,
263    branch_mask: Vec<bool>,
264}
265
266fn flatten_directory_rows(
267    nodes: &[TreeNode],
268    branch_mask: Vec<bool>,
269    out: &mut Vec<DirectoryRenderRow>,
270) {
271    for (idx, node) in nodes.iter().enumerate() {
272        let is_last = idx + 1 == nodes.len();
273        out.push(DirectoryRenderRow {
274            depth: branch_mask.len(),
275            label: node.label.clone(),
276            is_leaf: node.children.is_empty(),
277            expanded: node.expanded,
278            is_last,
279            branch_mask: branch_mask.clone(),
280        });
281
282        if node.expanded && !node.children.is_empty() {
283            let mut next_mask = branch_mask.clone();
284            next_mask.push(!is_last);
285            flatten_directory_rows(&node.children, next_mask, out);
286        }
287    }
288}