Skip to main content

slt/widgets/
selection.rs

1/// State for a dropdown select widget.
2///
3/// Renders as a single-line button showing the selected option. When activated,
4/// expands into a vertical list overlay for picking an option.
5#[derive(Debug, Clone, Default)]
6pub struct SelectState {
7    /// Selectable option labels.
8    pub items: Vec<String>,
9    /// Selected option index.
10    pub selected: usize,
11    /// Whether the dropdown list is currently open.
12    pub open: bool,
13    /// Placeholder text shown when `items` is empty.
14    pub placeholder: String,
15    cursor: usize,
16}
17
18impl SelectState {
19    /// Create select state with the provided options.
20    pub fn new(items: Vec<impl Into<String>>) -> Self {
21        Self {
22            items: items.into_iter().map(Into::into).collect(),
23            selected: 0,
24            open: false,
25            placeholder: String::new(),
26            cursor: 0,
27        }
28    }
29
30    /// Set placeholder text shown when no item can be displayed.
31    pub fn placeholder(mut self, p: impl Into<String>) -> Self {
32        self.placeholder = p.into();
33        self
34    }
35
36    /// Returns the currently selected item label, or `None` if empty.
37    pub fn selected_item(&self) -> Option<&str> {
38        self.items.get(self.selected).map(String::as_str)
39    }
40
41    pub(crate) fn cursor(&self) -> usize {
42        self.cursor
43    }
44
45    pub(crate) fn set_cursor(&mut self, c: usize) {
46        self.cursor = c;
47    }
48}
49
50// ── Radio ─────────────────────────────────────────────────────────────
51
52/// State for a radio button group.
53///
54/// Renders a vertical list of mutually-exclusive options with `●`/`○` markers.
55#[derive(Debug, Clone, Default)]
56pub struct RadioState {
57    /// Radio option labels.
58    pub items: Vec<String>,
59    /// Selected option index.
60    pub selected: usize,
61}
62
63impl RadioState {
64    /// Create radio state with the provided options.
65    pub fn new(items: Vec<impl Into<String>>) -> Self {
66        Self {
67            items: items.into_iter().map(Into::into).collect(),
68            selected: 0,
69        }
70    }
71
72    /// Returns the currently selected option label, or `None` if empty.
73    pub fn selected_item(&self) -> Option<&str> {
74        self.items.get(self.selected).map(String::as_str)
75    }
76}
77
78// ── Multi-Select ──────────────────────────────────────────────────────
79
80/// State for a multi-select list.
81///
82/// Like [`ListState`] but allows toggling multiple items with Space.
83#[derive(Debug, Clone)]
84pub struct MultiSelectState {
85    /// Multi-select option labels.
86    pub items: Vec<String>,
87    /// Focused option index used for keyboard navigation.
88    pub cursor: usize,
89    /// Set of selected option indices.
90    pub selected: HashSet<usize>,
91}
92
93impl MultiSelectState {
94    /// Create multi-select state with the provided options.
95    pub fn new(items: Vec<impl Into<String>>) -> Self {
96        Self {
97            items: items.into_iter().map(Into::into).collect(),
98            cursor: 0,
99            selected: HashSet::new(),
100        }
101    }
102
103    /// Return selected item labels in ascending index order.
104    pub fn selected_items(&self) -> Vec<&str> {
105        let mut indices: Vec<usize> = self.selected.iter().copied().collect();
106        indices.sort();
107        indices
108            .iter()
109            .filter_map(|&i| self.items.get(i).map(String::as_str))
110            .collect()
111    }
112
113    /// Toggle selection state for `index`.
114    pub fn toggle(&mut self, index: usize) {
115        if self.selected.contains(&index) {
116            self.selected.remove(&index);
117        } else {
118            self.selected.insert(index);
119        }
120    }
121}
122
123// ── Tree ──────────────────────────────────────────────────────────────
124
125/// A node in a tree view.
126#[derive(Debug, Clone)]
127pub struct TreeNode {
128    /// Display label for this node.
129    pub label: String,
130    /// Child nodes.
131    pub children: Vec<TreeNode>,
132    /// Whether the node is expanded in the tree view.
133    pub expanded: bool,
134}
135
136impl TreeNode {
137    /// Create a collapsed tree node with no children.
138    pub fn new(label: impl Into<String>) -> Self {
139        Self {
140            label: label.into(),
141            children: Vec::new(),
142            expanded: false,
143        }
144    }
145
146    /// Mark this node as expanded.
147    pub fn expanded(mut self) -> Self {
148        self.expanded = true;
149        self
150    }
151
152    /// Set child nodes for this node.
153    pub fn children(mut self, children: Vec<TreeNode>) -> Self {
154        self.children = children;
155        self
156    }
157
158    /// Returns `true` when this node has no children.
159    pub fn is_leaf(&self) -> bool {
160        self.children.is_empty()
161    }
162
163    fn flatten(&self, depth: usize, out: &mut Vec<FlatTreeEntry>) {
164        out.push(FlatTreeEntry {
165            depth,
166            label: self.label.clone(),
167            is_leaf: self.is_leaf(),
168            expanded: self.expanded,
169        });
170        if self.expanded {
171            for child in &self.children {
172                child.flatten(depth + 1, out);
173            }
174        }
175    }
176}
177
178pub(crate) struct FlatTreeEntry {
179    pub depth: usize,
180    pub label: String,
181    pub is_leaf: bool,
182    pub expanded: bool,
183}
184
185/// State for a hierarchical tree view widget.
186#[derive(Debug, Clone)]
187pub struct TreeState {
188    /// Root nodes of the tree.
189    pub nodes: Vec<TreeNode>,
190    /// Selected row index in the flattened visible tree.
191    pub selected: usize,
192}
193
194impl TreeState {
195    /// Create tree state from root nodes.
196    pub fn new(nodes: Vec<TreeNode>) -> Self {
197        Self { nodes, selected: 0 }
198    }
199
200    pub(crate) fn flatten(&self) -> Vec<FlatTreeEntry> {
201        let mut entries = Vec::new();
202        for node in &self.nodes {
203            node.flatten(0, &mut entries);
204        }
205        entries
206    }
207
208    pub(crate) fn toggle_at(&mut self, flat_index: usize) {
209        let mut counter = 0usize;
210        Self::toggle_recursive(&mut self.nodes, flat_index, &mut counter);
211    }
212
213    fn toggle_recursive(nodes: &mut [TreeNode], target: usize, counter: &mut usize) -> bool {
214        for node in nodes.iter_mut() {
215            if *counter == target {
216                if !node.is_leaf() {
217                    node.expanded = !node.expanded;
218                }
219                return true;
220            }
221            *counter += 1;
222            if node.expanded && Self::toggle_recursive(&mut node.children, target, counter) {
223                return true;
224            }
225        }
226        false
227    }
228}
229
230/// State for the directory tree widget.
231#[derive(Debug, Clone)]
232pub struct DirectoryTreeState {
233    /// The underlying tree state (reuses existing TreeState).
234    pub tree: TreeState,
235    /// Whether to show file/folder icons.
236    pub show_icons: bool,
237}
238
239impl DirectoryTreeState {
240    /// Create directory tree state from root nodes.
241    pub fn new(nodes: Vec<TreeNode>) -> Self {
242        Self {
243            tree: TreeState::new(nodes),
244            show_icons: true,
245        }
246    }
247
248    /// Build a directory tree from slash-delimited paths.
249    pub fn from_paths(paths: &[&str]) -> Self {
250        let mut roots: Vec<TreeNode> = Vec::new();
251
252        for raw_path in paths {
253            let parts: Vec<&str> = raw_path
254                .split('/')
255                .filter(|part| !part.is_empty())
256                .collect();
257            if parts.is_empty() {
258                continue;
259            }
260            insert_path(&mut roots, &parts, 0);
261        }
262
263        Self::new(roots)
264    }
265
266    /// Return selected node label if a node is selected.
267    pub fn selected_label(&self) -> Option<&str> {
268        let mut cursor = 0usize;
269        selected_label_in_nodes(&self.tree.nodes, self.tree.selected, &mut cursor)
270    }
271}
272
273impl Default for DirectoryTreeState {
274    fn default() -> Self {
275        Self::new(Vec::<TreeNode>::new())
276    }
277}
278
279fn insert_path(nodes: &mut Vec<TreeNode>, parts: &[&str], depth: usize) {
280    let Some(label) = parts.get(depth) else {
281        return;
282    };
283
284    let is_last = depth + 1 == parts.len();
285    let idx = nodes
286        .iter()
287        .position(|node| node.label == *label)
288        .unwrap_or_else(|| {
289            let mut node = TreeNode::new(*label);
290            if !is_last {
291                node.expanded = true;
292            }
293            nodes.push(node);
294            nodes.len() - 1
295        });
296
297    if is_last {
298        return;
299    }
300
301    nodes[idx].expanded = true;
302    insert_path(&mut nodes[idx].children, parts, depth + 1);
303}
304
305fn selected_label_in_nodes<'a>(
306    nodes: &'a [TreeNode],
307    target: usize,
308    cursor: &mut usize,
309) -> Option<&'a str> {
310    for node in nodes {
311        if *cursor == target {
312            return Some(node.label.as_str());
313        }
314        *cursor += 1;
315        if node.expanded {
316            if let Some(found) = selected_label_in_nodes(&node.children, target, cursor) {
317                return Some(found);
318            }
319        }
320    }
321    None
322}
323
324// ── Command Palette ───────────────────────────────────────────────────
325
326/// A single command entry in the palette.
327#[derive(Debug, Clone)]
328pub struct PaletteCommand {
329    /// Primary command label.
330    pub label: String,
331    /// Supplemental command description.
332    pub description: String,
333    /// Optional keyboard shortcut hint.
334    pub shortcut: Option<String>,
335}
336
337impl PaletteCommand {
338    /// Create a new palette command.
339    pub fn new(label: impl Into<String>, description: impl Into<String>) -> Self {
340        Self {
341            label: label.into(),
342            description: description.into(),
343            shortcut: None,
344        }
345    }
346
347    /// Set a shortcut hint displayed alongside the command.
348    pub fn shortcut(mut self, s: impl Into<String>) -> Self {
349        self.shortcut = Some(s.into());
350        self
351    }
352}