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, 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    /// Render a directory tree with guide lines and tree connectors.
111    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}