tui_scrollview/state.rs
1use ratatui_core::layout::{Position, Size};
2
3#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
4pub struct ScrollViewState {
5 /// The offset is the number of rows and columns to shift the scroll view by.
6 pub(crate) offset: Position,
7 /// The size of the scroll view. Not set until the first render call.
8 pub(crate) size: Option<Size>,
9 /// The size of a page of the scroll view. Not set until the first render call.
10 pub(crate) page_size: Option<Size>,
11}
12
13impl ScrollViewState {
14 /// Create a new scroll view state with an offset of (0, 0)
15 pub fn new() -> Self {
16 Self::default()
17 }
18
19 /// Create a new scroll view state with the given offset
20 pub fn with_offset(offset: Position) -> Self {
21 Self {
22 offset,
23 ..Default::default()
24 }
25 }
26
27 /// Set the offset of the scroll view state
28 pub const fn set_offset(&mut self, offset: Position) {
29 self.offset = offset;
30 }
31
32 /// Get the offset of the scroll view state
33 pub const fn offset(&self) -> Position {
34 self.offset
35 }
36
37 /// Move the scroll view state up by one row
38 pub const fn scroll_up(&mut self) {
39 self.offset.y = self.offset.y.saturating_sub(1);
40 }
41
42 /// Move the scroll view state down by one row
43 pub const fn scroll_down(&mut self) {
44 self.offset.y = self.offset.y.saturating_add(1);
45 }
46
47 /// Move the scroll view state down by one page
48 pub fn scroll_page_down(&mut self) {
49 let page_size = self.page_size.map_or(1, |size| size.height);
50 // we subtract 1 to ensure that there is a one row overlap between pages
51 self.offset.y = self.offset.y.saturating_add(page_size).saturating_sub(1);
52 }
53
54 /// Move the scroll view state up by one page
55 pub fn scroll_page_up(&mut self) {
56 let page_size = self.page_size.map_or(1, |size| size.height);
57 // we add 1 to ensure that there is a one row overlap between pages
58 self.offset.y = self.offset.y.saturating_add(1).saturating_sub(page_size);
59 }
60
61 /// Move the scroll view state left by one column
62 pub const fn scroll_left(&mut self) {
63 self.offset.x = self.offset.x.saturating_sub(1);
64 }
65
66 /// Move the scroll view state right by one column
67 pub const fn scroll_right(&mut self) {
68 self.offset.x = self.offset.x.saturating_add(1);
69 }
70
71 /// Move the scroll view state to the top of the buffer
72 pub const fn scroll_to_top(&mut self) {
73 self.offset = Position::ORIGIN;
74 }
75
76 /// Move the scroll view state to the bottom of the buffer
77 ///
78 /// If the buffer size is not yet computed (done during the first rendering), it will not
79 /// be taken into account and the scroll offset will be set to the maximum value: `u16::MAX`
80 pub fn scroll_to_bottom(&mut self) {
81 // the render call will adjust the offset to ensure that we don't scroll past the end of
82 // the buffer, so we can set the offset to the maximum value here
83 let bottom = self
84 .size
85 .map_or(u16::MAX, |size| size.height.saturating_sub(1));
86 self.offset.y = bottom;
87 }
88
89 /// True if the scroll view state is at the bottom of the buffer
90 ///
91 /// This takes the page size into account. It returns true if the last row in the buffer is
92 /// visible in the current page.
93 ///
94 /// The buffer and the page size are unknown until computed during the first rendering. If the
95 /// buffer size is not yet known, this function always returns true. If the page size is not yet
96 /// known, the current row is treated as a one-row page.
97 ///
98 /// Saturating arithmetic prevents large offsets from overflowing when they are combined with
99 /// the page size.
100 pub fn is_at_bottom(&self) -> bool {
101 let Some(size) = self.size else {
102 return true;
103 };
104 let bottom = size.height.saturating_sub(1);
105 let page_size = self.page_size.map_or(1, |size| size.height);
106 self.offset.y.saturating_add(page_size) > bottom
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn is_at_bottom_requires_the_last_row_to_be_visible() {
116 let mut state = ScrollViewState {
117 offset: Position::new(0, 4),
118 size: Some(Size::new(1, 10)),
119 page_size: Some(Size::new(1, 5)),
120 };
121
122 assert!(!state.is_at_bottom());
123
124 state.offset.y = 5;
125
126 assert!(state.is_at_bottom());
127 }
128}