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