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