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 (default 10 items).
52    fn page_up(&mut self) {
53        let selected = self.selected();
54        self.set_selected(selected.saturating_sub(10));
55    }
56
57    /// Move selection down by a page (default 10 items).
58    fn page_down(&mut self) {
59        let total = self.total();
60        let selected = self.selected();
61        if total > 0 {
62            self.set_selected((selected + 10).min(total.saturating_sub(1)));
63        }
64    }
65
66    /// Move to the first item.
67    fn go_first(&mut self) {
68        self.set_selected(0);
69    }
70
71    /// Move to the last item.
72    fn go_last(&mut self) {
73        let total = self.total();
74        if total > 0 {
75            self.set_selected(total.saturating_sub(1));
76        }
77    }
78}
79
80/// Base state for simple list navigation.
81///
82/// Can be embedded in more complex state structs to provide
83/// common navigation functionality.
84#[derive(Debug, Clone, Default)]
85pub struct ListState {
86    pub selected: usize,
87    pub total: usize,
88    pub scroll_offset: usize,
89}
90
91impl ListState {
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    pub fn with_total(total: usize) -> Self {
97        Self {
98            selected: 0,
99            total,
100            scroll_offset: 0,
101        }
102    }
103}
104
105impl ListNavigation for ListState {
106    fn selected(&self) -> usize {
107        self.selected
108    }
109
110    fn set_selected(&mut self, idx: usize) {
111        self.selected = idx;
112    }
113
114    fn total(&self) -> usize {
115        self.total
116    }
117
118    fn set_total(&mut self, total: usize) {
119        self.total = total;
120    }
121}
122
123/// Trait for tree-based navigation state.
124///
125/// Extends list navigation with expand/collapse functionality
126/// for hierarchical views.
127pub trait TreeNavigation: ListNavigation {
128    /// Check if a node is expanded.
129    fn is_expanded(&self, node_id: &str) -> bool;
130
131    /// Expand a node.
132    fn expand(&mut self, node_id: &str);
133
134    /// Collapse a node.
135    fn collapse(&mut self, node_id: &str);
136
137    /// Toggle a node's expanded state.
138    fn toggle_expand(&mut self, node_id: &str) {
139        if self.is_expanded(node_id) {
140            self.collapse(node_id);
141        } else {
142            self.expand(node_id);
143        }
144    }
145
146    /// Expand all nodes.
147    fn expand_all(&mut self);
148
149    /// Collapse all nodes.
150    fn collapse_all(&mut self);
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_list_state_navigation() {
159        let mut state = ListState::with_total(10);
160
161        assert_eq!(state.selected(), 0);
162
163        state.select_next();
164        assert_eq!(state.selected(), 1);
165
166        state.select_prev();
167        assert_eq!(state.selected(), 0);
168
169        // Can't go below 0
170        state.select_prev();
171        assert_eq!(state.selected(), 0);
172
173        // Go to last
174        state.go_last();
175        assert_eq!(state.selected(), 9);
176
177        // Can't go past end
178        state.select_next();
179        assert_eq!(state.selected(), 9);
180
181        // Go to first
182        state.go_first();
183        assert_eq!(state.selected(), 0);
184    }
185
186    #[test]
187    fn test_list_state_page_navigation() {
188        let mut state = ListState::with_total(50);
189
190        state.page_down();
191        assert_eq!(state.selected(), 10);
192
193        state.page_down();
194        assert_eq!(state.selected(), 20);
195
196        state.page_up();
197        assert_eq!(state.selected(), 10);
198
199        state.page_up();
200        assert_eq!(state.selected(), 0);
201
202        // Can't go below 0
203        state.page_up();
204        assert_eq!(state.selected(), 0);
205    }
206
207    #[test]
208    fn test_list_state_clamp() {
209        let mut state = ListState::with_total(10);
210        state.selected = 15;
211
212        state.clamp_selection();
213        assert_eq!(state.selected(), 9);
214
215        state.set_total(0);
216        state.clamp_selection();
217        assert_eq!(state.selected(), 0);
218    }
219}