1use crate::{
2 helper::{char_width, chars_width},
3 view::line_wrapper::LineWrapper,
4 Lines,
5};
6use ratatui::layout::Rect;
7
8#[derive(Debug, Clone)]
11pub(crate) struct ViewState {
12 pub(crate) viewport: Offset,
14 pub(crate) num_rows: usize,
16 pub(crate) screen_area: Rect,
21 pub(crate) wrap: bool,
23 pub(crate) tab_width: usize,
25}
26
27impl Default for ViewState {
28 fn default() -> Self {
29 Self {
30 viewport: Offset::default(),
31 num_rows: 0,
32 screen_area: Rect::default(),
33 wrap: true,
34 tab_width: 2,
35 }
36 }
37}
38
39#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
40pub(crate) struct Offset {
41 pub(crate) x: usize,
43 pub(crate) y: usize,
45}
46
47impl Offset {
48 pub(crate) fn new(x: usize, y: usize) -> Self {
49 Self { x, y }
50 }
51}
52
53impl From<Rect> for Offset {
54 fn from(value: Rect) -> Self {
55 Self {
56 x: value.x as usize,
57 y: value.y as usize,
58 }
59 }
60}
61
62impl ViewState {
63 pub(crate) fn set_screen_area<T: Into<Rect>>(&mut self, area: T) {
68 self.screen_area = area.into();
69 }
70
71 pub(crate) fn update_viewport_horizontal(
73 &mut self,
74 width: usize,
75 cursor_col: usize,
76 line: Option<&Vec<char>>,
77 ) -> usize {
78 let Some(line) = line else {
79 self.viewport.x = 0;
80 return self.viewport.x;
81 };
82
83 if cursor_col < self.viewport.x {
85 self.viewport.x = cursor_col;
86 return self.viewport.x;
87 }
88
89 let mut max_cursor_pos = self.viewport.x;
91 let mut current_width = 0;
92 for &ch in line.iter().skip(self.viewport.x) {
93 current_width += char_width(ch, self.tab_width);
94 if current_width >= width {
95 break;
96 }
97 max_cursor_pos += 1;
98 }
99
100 if cursor_col > max_cursor_pos {
102 let mut backward_width = 0;
103 let mut new_viewport_x = cursor_col;
104
105 for i in (0..=cursor_col).rev() {
107 let char_width = match line.get(i) {
108 Some(&ch) => char_width(ch, self.tab_width),
109 None => 1,
110 };
111 backward_width += char_width;
112 if backward_width >= width {
113 break;
114 }
115 new_viewport_x = new_viewport_x.saturating_sub(1);
116 }
117
118 self.viewport.x = new_viewport_x;
119 }
120
121 self.viewport.x
122 }
123
124 pub(crate) fn update_viewport_vertical(&mut self, height: usize, cursor_row: usize) -> usize {
126 let max_cursor_pos = height.saturating_sub(1) + self.viewport.y;
127
128 if cursor_row < self.viewport.y {
130 self.viewport.y = cursor_row;
131 }
132
133 if cursor_row >= max_cursor_pos {
135 self.viewport.y += cursor_row.saturating_sub(max_cursor_pos);
136 }
137
138 self.viewport.y
139 }
140
141 pub(crate) fn update_viewport_vertical_wrap(
143 &mut self,
144 width: usize,
145 height: usize,
146 cursor_row: usize,
147 lines: &Lines,
148 ) -> usize {
149 if cursor_row < self.viewport.y {
151 self.viewport.y = cursor_row;
152 }
153
154 self.scroll_down(lines, width, height, cursor_row);
156
157 self.viewport.y
158 }
159
160 pub(crate) fn update_num_rows(&mut self, num_rows: usize) {
163 self.num_rows = num_rows;
164 }
165
166 fn scroll_down(
178 &mut self,
179 lines: &Lines,
180 max_width: usize,
181 max_height: usize,
182 cursor_row: usize,
183 ) {
184 if cursor_row < self.viewport.y + self.num_rows || self.num_rows == 0 {
186 return;
187 }
188
189 let mut remaining_height = max_height;
190
191 let skip = lines.len().saturating_sub(cursor_row + 1);
192 for (i, line) in lines.iter_row().rev().skip(skip).enumerate() {
193 let line_width = chars_width(line, self.tab_width);
194 let current_row_height = LineWrapper::determine_split(line_width, max_width).len();
195
196 if remaining_height < current_row_height {
198 let first_visible_row = cursor_row.saturating_sub(i.saturating_sub(1));
199 self.viewport.y = first_visible_row;
200 break;
201 }
202
203 remaining_height = remaining_height.saturating_sub(current_row_height);
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 macro_rules! update_view_vertical_test {
214 ($name:ident: {
215 view: $given_view:expr,
216 height: $given_height:expr,
217 cursor: $given_cursor:expr,
218 expected: $expected_offset:expr
219 }) => {
220 #[test]
221 fn $name() {
222 let mut view = $given_view;
224 let height = $given_height;
225 let cursor = $given_cursor;
226
227 let offset = view.update_viewport_vertical(height, cursor);
229
230 assert_eq!(offset, $expected_offset);
232 }
233 };
234 }
235
236 macro_rules! update_view_horizontal_test {
237 ($name:ident: {
238 view: $given_view:expr,
239 width: $given_width:expr,
240 cursor: $given_cursor:expr,
241 expected: $expected_offset:expr
242 }) => {
243 #[test]
244 fn $name() {
245 let mut view = $given_view;
247 let width = $given_width;
248 let cursor = $given_cursor;
249 let line = vec![];
250
251 let offset = view.update_viewport_horizontal(width, cursor, Some(&line));
253
254 assert_eq!(offset, $expected_offset);
256 }
257 };
258 }
259
260 update_view_vertical_test!(
261 scroll_up: {
265 view: ViewState{
266 viewport: Offset::new(0, 1),
267 ..Default::default()
268 },
269 height: 2,
270 cursor: 0,
271 expected: 0
272 }
273 );
274
275 update_view_vertical_test!(
276 scroll_down: {
280 view: ViewState{
281 viewport: Offset::new(0, 0),
282 ..Default::default()
283 },
284 height: 2,
285 cursor: 2,
286 expected: 1
287 }
288 );
289
290 update_view_horizontal_test!(
291 scroll_left: {
292 view: ViewState{
293 viewport: Offset::new(1, 0),
294 ..Default::default()
295 },
296 width: 2,
297 cursor: 0,
298 expected: 0
299 }
300 );
301
302 update_view_horizontal_test!(
303 scroll_right: {
304 view: ViewState{
305 viewport: Offset::new(0, 0),
306 ..Default::default()
307 },
308 width: 2,
309 cursor: 2,
310 expected: 1
311 }
312 );
313}