supercode_frontend_tui/input/
scroll_state.rs1#[derive(Debug, Default, Clone, Copy)]
21pub struct ScrollState {
22 pub selected_idx: Option<usize>,
23 pub scroll_top: usize,
24}
25
26impl ScrollState {
27 pub fn new() -> Self {
28 Self {
29 selected_idx: None,
30 scroll_top: 0,
31 }
32 }
33
34 pub fn reset(&mut self) {
36 self.selected_idx = None;
37 self.scroll_top = 0;
38 }
39
40 pub fn clamp_selection(&mut self, len: usize) {
42 if self.clear_if_empty(len) {
43 return;
44 }
45 self.selected_idx = Some(self.selected_idx.unwrap_or(0).min(len - 1));
46 }
47
48 pub fn move_up_wrap(&mut self, len: usize) {
50 if self.clear_if_empty(len) {
51 return;
52 }
53 self.selected_idx = Some(match self.selected_idx {
54 Some(idx) if idx > 0 => idx - 1,
55 Some(_) => len - 1,
56 None => 0,
57 });
58 }
59
60 pub fn move_down_wrap(&mut self, len: usize) {
62 if self.clear_if_empty(len) {
63 return;
64 }
65 self.selected_idx = Some(match self.selected_idx {
66 Some(idx) if idx + 1 < len => idx + 1,
67 _ => 0,
68 });
69 }
70
71 pub fn page_up_clamped(&mut self, len: usize, visible_rows: usize) {
77 if self.clear_if_empty(len) {
78 return;
79 }
80 let step = visible_rows.max(1);
81 let current = self.selected_idx.unwrap_or(0).min(len - 1);
82 self.selected_idx = Some(current.saturating_sub(step));
83 self.ensure_visible(len, visible_rows);
84 }
85
86 pub fn page_down_clamped(&mut self, len: usize, visible_rows: usize) {
92 if self.clear_if_empty(len) {
93 return;
94 }
95 let step = visible_rows.max(1);
96 let current = self.selected_idx.unwrap_or(0).min(len - 1);
97 self.selected_idx = Some(current.saturating_add(step).min(len - 1));
98 self.ensure_visible(len, visible_rows);
99 }
100
101 pub fn jump_top(&mut self, len: usize, visible_rows: usize) {
103 if self.clear_if_empty(len) {
104 return;
105 }
106 self.selected_idx = Some(0);
107 self.ensure_visible(len, visible_rows);
108 }
109
110 pub fn jump_bottom(&mut self, len: usize, visible_rows: usize) {
112 if self.clear_if_empty(len) {
113 return;
114 }
115 self.selected_idx = Some(len - 1);
116 self.ensure_visible(len, visible_rows);
117 }
118
119 fn clear_if_empty(&mut self, len: usize) -> bool {
120 if len != 0 {
121 return false;
122 }
123 self.selected_idx = None;
124 self.scroll_top = 0;
125 true
126 }
127
128 pub fn ensure_visible(&mut self, len: usize, visible_rows: usize) {
131 if len == 0 || visible_rows == 0 {
132 self.scroll_top = 0;
133 return;
134 }
135 if let Some(sel) = self.selected_idx {
136 if sel < self.scroll_top {
137 self.scroll_top = sel;
138 } else {
139 let bottom = self.scroll_top + visible_rows - 1;
140 if sel > bottom {
141 self.scroll_top = sel + 1 - visible_rows;
142 }
143 }
144 } else {
145 self.scroll_top = 0;
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::ScrollState;
153
154 #[test]
155 fn wrap_navigation_and_visibility() {
156 let mut s = ScrollState::new();
157 let len = 10;
158 let vis = 5;
159
160 s.clamp_selection(len);
161 assert_eq!(s.selected_idx, Some(0));
162 s.ensure_visible(len, vis);
163 assert_eq!(s.scroll_top, 0);
164
165 s.move_up_wrap(len);
166 s.ensure_visible(len, vis);
167 assert_eq!(s.selected_idx, Some(len - 1));
168 match s.selected_idx {
169 Some(sel) => assert!(s.scroll_top <= sel),
170 None => panic!("expected Some(selected_idx) after wrap"),
171 }
172
173 s.move_down_wrap(len);
174 s.ensure_visible(len, vis);
175 assert_eq!(s.selected_idx, Some(0));
176 assert_eq!(s.scroll_top, 0);
177 }
178
179 #[test]
180 fn page_and_jump_navigation_clamps() {
181 let mut s = ScrollState::new();
182 let len = 10;
183 let vis = 4;
184
185 s.clamp_selection(len);
186 s.page_down_clamped(len, vis);
187 assert_eq!(s.selected_idx, Some(4));
188 assert_eq!(s.scroll_top, 1);
189
190 s.page_down_clamped(len, vis);
191 assert_eq!(s.selected_idx, Some(8));
192 assert_eq!(s.scroll_top, 5);
193
194 s.page_down_clamped(len, vis);
195 assert_eq!(s.selected_idx, Some(9));
196 assert_eq!(s.scroll_top, 6);
197
198 s.page_up_clamped(len, vis);
199 assert_eq!(s.selected_idx, Some(5));
200 assert_eq!(s.scroll_top, 5);
201
202 s.jump_top(len, vis);
203 assert_eq!(s.selected_idx, Some(0));
204 assert_eq!(s.scroll_top, 0);
205
206 s.jump_bottom(len, vis);
207 assert_eq!(s.selected_idx, Some(9));
208 assert_eq!(s.scroll_top, 6);
209 }
210}