Skip to main content

tui_dispatch_debug/debug/
scroll_state.rs

1//! Reusable scroll and cursor state for debug overlays.
2
3use crossterm::event::KeyCode;
4
5/// Scroll offset tracker for content rendered via `ScrollView`.
6///
7/// Tracks `offset` (current scroll position) and a cached `page_size`
8/// (visible rows). All navigation methods clamp automatically.
9#[derive(Debug, Clone)]
10pub struct ScrollState {
11    /// Current scroll offset (first visible row)
12    pub offset: usize,
13    /// Cached page size (visible rows in the viewport)
14    pub page_size: usize,
15}
16
17impl Default for ScrollState {
18    fn default() -> Self {
19        Self {
20            offset: 0,
21            page_size: 1,
22        }
23    }
24}
25
26impl ScrollState {
27    /// Create a new scroll state at position 0.
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    fn page_size_value(&self) -> usize {
33        self.page_size.max(1)
34    }
35
36    fn max_offset(&self, content_len: usize) -> usize {
37        content_len.saturating_sub(self.page_size_value())
38    }
39
40    /// Scroll up by one row.
41    pub fn scroll_up(&mut self) {
42        self.offset = self.offset.saturating_sub(1);
43    }
44
45    /// Scroll down by one row, clamped to `content_len`.
46    pub fn scroll_down(&mut self, content_len: usize) {
47        let max = self.max_offset(content_len);
48        self.offset = (self.offset + 1).min(max);
49    }
50
51    /// Jump to the top.
52    pub fn to_top(&mut self) {
53        self.offset = 0;
54    }
55
56    /// Jump to the bottom, clamped to `content_len`.
57    pub fn to_bottom(&mut self, content_len: usize) {
58        self.offset = self.max_offset(content_len);
59    }
60
61    /// Scroll up by one page.
62    pub fn page_up(&mut self) {
63        let ps = self.page_size_value();
64        self.offset = self.offset.saturating_sub(ps);
65    }
66
67    /// Scroll down by one page, clamped to `content_len`.
68    pub fn page_down(&mut self, content_len: usize) {
69        let ps = self.page_size_value();
70        let max = self.max_offset(content_len);
71        self.offset = (self.offset + ps).min(max);
72    }
73
74    /// Handle standard scroll keys (j/k, arrows, g/G, Home/End, PageUp/PageDown).
75    ///
76    /// Returns `true` if the key was consumed.
77    pub fn handle_scroll_key(&mut self, key: KeyCode, content_len: usize) -> bool {
78        match key {
79            KeyCode::Char('j') | KeyCode::Down => {
80                self.scroll_down(content_len);
81                true
82            }
83            KeyCode::Char('k') | KeyCode::Up => {
84                self.scroll_up();
85                true
86            }
87            KeyCode::Char('g') | KeyCode::Home => {
88                self.to_top();
89                true
90            }
91            KeyCode::Char('G') | KeyCode::End => {
92                self.to_bottom(content_len);
93                true
94            }
95            KeyCode::PageDown => {
96                self.page_down(content_len);
97                true
98            }
99            KeyCode::PageUp => {
100                self.page_up();
101                true
102            }
103            _ => false,
104        }
105    }
106
107    /// Reset to initial state.
108    pub fn reset(&mut self) {
109        self.offset = 0;
110        self.page_size = 1;
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn scroll_state_basic_navigation() {
120        let mut s = ScrollState::new();
121        s.page_size = 5;
122
123        s.scroll_down(20);
124        assert_eq!(s.offset, 1);
125
126        s.scroll_up();
127        assert_eq!(s.offset, 0);
128
129        // Can't go below 0
130        s.scroll_up();
131        assert_eq!(s.offset, 0);
132
133        s.to_bottom(20);
134        assert_eq!(s.offset, 15); // 20 - 5
135
136        s.to_top();
137        assert_eq!(s.offset, 0);
138    }
139
140    #[test]
141    fn scroll_state_page_navigation() {
142        let mut s = ScrollState::new();
143        s.page_size = 5;
144
145        s.page_down(20);
146        assert_eq!(s.offset, 5);
147
148        s.page_down(20);
149        assert_eq!(s.offset, 10);
150
151        s.page_up();
152        assert_eq!(s.offset, 5);
153
154        // Clamp at bottom
155        s.offset = 14;
156        s.page_down(20);
157        assert_eq!(s.offset, 15);
158    }
159
160    #[test]
161    fn scroll_state_handle_key() {
162        let mut s = ScrollState::new();
163        s.page_size = 5;
164
165        assert!(s.handle_scroll_key(KeyCode::Char('j'), 10));
166        assert_eq!(s.offset, 1);
167
168        assert!(s.handle_scroll_key(KeyCode::Char('k'), 10));
169        assert_eq!(s.offset, 0);
170
171        assert!(s.handle_scroll_key(KeyCode::Char('G'), 10));
172        assert_eq!(s.offset, 5);
173
174        assert!(s.handle_scroll_key(KeyCode::Char('g'), 10));
175        assert_eq!(s.offset, 0);
176
177        assert!(!s.handle_scroll_key(KeyCode::Char('x'), 10));
178    }
179
180    #[test]
181    fn scroll_state_reset() {
182        let mut s = ScrollState {
183            offset: 5,
184            page_size: 10,
185        };
186        s.reset();
187        assert_eq!(s.offset, 0);
188        assert_eq!(s.page_size, 1);
189    }
190}