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