ratatui_kit/components/scroll_view/
state.rs1use crossterm::event::{Event, KeyCode, KeyEventKind, MouseEventKind};
14use ratatui::layout::{Position, Size};
15
16#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
17pub struct ScrollViewState {
19 pub(crate) offset: Position,
21 pub(crate) size: Option<Size>,
23 pub(crate) page_size: Option<Size>,
25}
26
27impl ScrollViewState {
28 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn with_offset(offset: Position) -> Self {
35 Self {
36 offset,
37 ..Default::default()
38 }
39 }
40
41 pub const fn set_offset(&mut self, offset: Position) {
43 self.offset = offset;
44 }
45
46 pub const fn offset(&self) -> Position {
48 self.offset
49 }
50
51 pub const fn scroll_up(&mut self) {
53 self.offset.y = self.offset.y.saturating_sub(1);
54 }
55
56 pub const fn scroll_down(&mut self) {
58 self.offset.y = self.offset.y.saturating_add(1);
59 }
60
61 pub fn scroll_page_down(&mut self) {
63 let page_size = self.page_size.map_or(1, |size| size.height);
64 self.offset.y = self.offset.y.saturating_add(page_size).saturating_sub(1);
66 }
67
68 pub fn scroll_page_up(&mut self) {
70 let page_size = self.page_size.map_or(1, |size| size.height);
71 self.offset.y = self.offset.y.saturating_add(1).saturating_sub(page_size);
73 }
74
75 pub const fn scroll_left(&mut self) {
77 self.offset.x = self.offset.x.saturating_sub(1);
78 }
79
80 pub const fn scroll_right(&mut self) {
82 self.offset.x = self.offset.x.saturating_add(1);
83 }
84
85 pub const fn scroll_to_top(&mut self) {
87 self.offset = Position::ORIGIN;
88 }
89
90 pub fn scroll_to_bottom(&mut self) {
92 let bottom = self
94 .size
95 .map_or(u16::MAX, |size| size.height.saturating_sub(1));
96 self.offset.y = bottom;
97 }
98
99 pub const fn size(&self) -> Option<Size> {
101 self.size
102 }
103
104 pub const fn page_size(&self) -> Option<Size> {
106 self.page_size
107 }
108
109 pub fn is_at_bottom(&self) -> bool {
114 let Some(size) = self.size else {
115 return true;
116 };
117 let bottom = size.height.saturating_sub(1);
118 let page_size = self.page_size.map_or(1, |size| size.height);
119 self.offset.y.saturating_add(page_size) > bottom
120 }
121
122 pub fn scroll_to_visible(&mut self, y: u16, height: u16) {
128 let page = self.page_size.map_or(u16::MAX, |size| size.height);
129 let top = self.offset.y;
130 let target_bottom = y.saturating_add(height);
131 if y < top {
132 self.offset.y = y;
134 } else if target_bottom > top.saturating_add(page) {
135 self.offset.y = target_bottom.saturating_sub(page);
137 }
138 }
139
140 pub fn handle_event(&mut self, event: &Event) -> bool {
142 match event {
143 Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
144 KeyCode::Up | KeyCode::Char('k') => self.scroll_up(),
145 KeyCode::Down | KeyCode::Char('j') => self.scroll_down(),
146 KeyCode::Left | KeyCode::Char('h') => self.scroll_left(),
147 KeyCode::Right | KeyCode::Char('l') => self.scroll_right(),
148 KeyCode::PageUp => self.scroll_page_up(),
149 KeyCode::PageDown => self.scroll_page_down(),
150 KeyCode::Home => self.scroll_to_top(),
151 KeyCode::End => self.scroll_to_bottom(),
152 _ => return false,
153 },
154 Event::Mouse(event) => match event.kind {
155 MouseEventKind::ScrollDown => self.scroll_down(),
156 MouseEventKind::ScrollUp => self.scroll_up(),
157 MouseEventKind::ScrollLeft => self.scroll_left(),
158 MouseEventKind::ScrollRight => self.scroll_right(),
159 _ => return false,
160 },
161 _ => return false,
162 }
163 true
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn is_at_bottom_requires_the_last_row_to_be_visible() {
173 let mut state = ScrollViewState {
174 offset: Position::new(0, 4),
175 size: Some(Size::new(1, 10)),
176 page_size: Some(Size::new(1, 5)),
177 };
178 assert!(!state.is_at_bottom());
179 state.offset.y = 5;
180 assert!(state.is_at_bottom());
181 }
182
183 #[test]
184 fn is_at_bottom_before_first_render() {
185 let state = ScrollViewState::default();
186 assert!(state.is_at_bottom());
187 }
188
189 #[test]
190 fn scroll_to_visible_only_moves_when_outside_the_page() {
191 let mut state = ScrollViewState {
192 offset: Position::new(0, 2),
193 size: Some(Size::new(1, 20)),
194 page_size: Some(Size::new(1, 5)),
195 };
196 state.scroll_to_visible(3, 1);
198 assert_eq!(state.offset.y, 2);
199 state.scroll_to_visible(9, 1);
201 assert_eq!(state.offset.y, 5); state.scroll_to_visible(1, 1);
204 assert_eq!(state.offset.y, 1);
205 }
206}