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