slt/context/widgets_interactive/
tree_widgets.rs1use super::*;
2use crate::{DirectoryTreeState, TreeNode};
3
4impl Context {
5 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 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 = ¤t_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 = ¤t_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}