Skip to main content

sbom_tools/tui/
state.rs

1//! Shared state definitions for TUI views.
2//!
3//! This module provides common traits and utilities for list/table navigation
4//! that are shared between diff mode and view mode.
5
6/// Trait for list-based navigation state.
7///
8/// Provides common selection and navigation methods for any view
9/// that displays a selectable list of items.
10pub trait ListNavigation {
11    /// Get the current selection index.
12    fn selected(&self) -> usize;
13
14    /// Set the selection index.
15    fn set_selected(&mut self, idx: usize);
16
17    /// Get the total number of items.
18    fn total(&self) -> usize;
19
20    /// Set the total number of items.
21    fn set_total(&mut self, total: usize);
22
23    /// Move selection to the next item.
24    fn select_next(&mut self) {
25        let total = self.total();
26        let selected = self.selected();
27        if total > 0 && selected < total.saturating_sub(1) {
28            self.set_selected(selected + 1);
29        }
30    }
31
32    /// Move selection to the previous item.
33    fn select_prev(&mut self) {
34        let selected = self.selected();
35        if selected > 0 {
36            self.set_selected(selected - 1);
37        }
38    }
39
40    /// Ensure selection is within valid bounds.
41    fn clamp_selection(&mut self) {
42        let total = self.total();
43        let selected = self.selected();
44        if total == 0 {
45            self.set_selected(0);
46        } else if selected >= total {
47            self.set_selected(total.saturating_sub(1));
48        }
49    }
50
51    /// Move selection up by a page.
52    fn page_up(&mut self) {
53        use super::constants::PAGE_SIZE;
54        let selected = self.selected();
55        self.set_selected(selected.saturating_sub(PAGE_SIZE));
56    }
57
58    /// Move selection down by a page.
59    fn page_down(&mut self) {
60        use super::constants::PAGE_SIZE;
61        let total = self.total();
62        let selected = self.selected();
63        if total > 0 {
64            self.set_selected((selected + PAGE_SIZE).min(total.saturating_sub(1)));
65        }
66    }
67
68    /// Move to the first item.
69    fn go_first(&mut self) {
70        self.set_selected(0);
71    }
72
73    /// Move to the last item.
74    fn go_last(&mut self) {
75        let total = self.total();
76        if total > 0 {
77            self.set_selected(total.saturating_sub(1));
78        }
79    }
80}
81
82/// Base state for simple list navigation.
83///
84/// Can be embedded in more complex state structs to provide
85/// common navigation functionality.
86#[derive(Debug, Clone, Default)]
87pub struct ListState {
88    pub selected: usize,
89    pub total: usize,
90    pub scroll_offset: usize,
91}
92
93impl ListState {
94    #[must_use]
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    #[must_use]
100    pub const fn with_total(total: usize) -> Self {
101        Self {
102            selected: 0,
103            total,
104            scroll_offset: 0,
105        }
106    }
107}
108
109impl ListNavigation for ListState {
110    fn selected(&self) -> usize {
111        self.selected
112    }
113
114    fn set_selected(&mut self, idx: usize) {
115        self.selected = idx;
116    }
117
118    fn total(&self) -> usize {
119        self.total
120    }
121
122    fn set_total(&mut self, total: usize) {
123        self.total = total;
124    }
125}
126
127/// Trait for tree-based navigation state.
128///
129/// Extends list navigation with expand/collapse functionality
130/// for hierarchical views.
131pub trait TreeNavigation: ListNavigation {
132    /// Check if a node is expanded.
133    fn is_expanded(&self, node_id: &str) -> bool;
134
135    /// Expand a node.
136    fn expand(&mut self, node_id: &str);
137
138    /// Collapse a node.
139    fn collapse(&mut self, node_id: &str);
140
141    /// Toggle a node's expanded state.
142    fn toggle_expand(&mut self, node_id: &str) {
143        if self.is_expanded(node_id) {
144            self.collapse(node_id);
145        } else {
146            self.expand(node_id);
147        }
148    }
149
150    /// Expand all nodes.
151    fn expand_all(&mut self);
152
153    /// Collapse all nodes.
154    fn collapse_all(&mut self);
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_list_state_navigation() {
163        let mut state = ListState::with_total(10);
164
165        assert_eq!(state.selected(), 0);
166
167        state.select_next();
168        assert_eq!(state.selected(), 1);
169
170        state.select_prev();
171        assert_eq!(state.selected(), 0);
172
173        // Can't go below 0
174        state.select_prev();
175        assert_eq!(state.selected(), 0);
176
177        // Go to last
178        state.go_last();
179        assert_eq!(state.selected(), 9);
180
181        // Can't go past end
182        state.select_next();
183        assert_eq!(state.selected(), 9);
184
185        // Go to first
186        state.go_first();
187        assert_eq!(state.selected(), 0);
188    }
189
190    #[test]
191    fn test_list_state_page_navigation() {
192        let mut state = ListState::with_total(50);
193
194        state.page_down();
195        assert_eq!(state.selected(), 10);
196
197        state.page_down();
198        assert_eq!(state.selected(), 20);
199
200        state.page_up();
201        assert_eq!(state.selected(), 10);
202
203        state.page_up();
204        assert_eq!(state.selected(), 0);
205
206        // Can't go below 0
207        state.page_up();
208        assert_eq!(state.selected(), 0);
209    }
210
211    #[test]
212    fn test_list_state_clamp() {
213        let mut state = ListState::with_total(10);
214        state.selected = 15;
215
216        state.clamp_selection();
217        assert_eq!(state.selected(), 9);
218
219        state.set_total(0);
220        state.clamp_selection();
221        assert_eq!(state.selected(), 0);
222    }
223}