Skip to content

Commit dd52e9c

Browse files
committed
Separate the scroll state
1 parent 0f71a15 commit dd52e9c

File tree

3 files changed

+135
-74
lines changed

3 files changed

+135
-74
lines changed

src/list.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::app_state::AppState;
1616

1717
use self::state::{Filter, ListState};
1818

19+
mod scroll_state;
1920
mod state;
2021

2122
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {

src/list/scroll_state.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
pub struct ScrollState {
2+
n_rows: usize,
3+
max_n_rows_to_display: usize,
4+
selected: Option<usize>,
5+
offset: usize,
6+
scroll_padding: usize,
7+
max_scroll_padding: usize,
8+
}
9+
10+
impl ScrollState {
11+
pub fn new(n_rows: usize, selected: Option<usize>, max_scroll_padding: usize) -> Self {
12+
Self {
13+
n_rows,
14+
max_n_rows_to_display: 0,
15+
selected,
16+
offset: selected.map_or(0, |selected| selected.saturating_sub(max_scroll_padding)),
17+
scroll_padding: 0,
18+
max_scroll_padding,
19+
}
20+
}
21+
22+
#[inline]
23+
pub fn offset(&self) -> usize {
24+
self.offset
25+
}
26+
27+
fn update_offset(&mut self) {
28+
let Some(selected) = self.selected else {
29+
return;
30+
};
31+
32+
let min_offset = (selected + self.scroll_padding)
33+
.saturating_sub(self.max_n_rows_to_display.saturating_sub(1));
34+
let max_offset = selected.saturating_sub(self.scroll_padding);
35+
let global_max_offset = self.n_rows.saturating_sub(self.max_n_rows_to_display);
36+
37+
self.offset = self
38+
.offset
39+
.max(min_offset)
40+
.min(max_offset)
41+
.min(global_max_offset);
42+
}
43+
44+
#[inline]
45+
pub fn selected(&self) -> Option<usize> {
46+
self.selected
47+
}
48+
49+
fn set_selected(&mut self, selected: usize) {
50+
self.selected = Some(selected);
51+
self.update_offset();
52+
}
53+
54+
pub fn select_next(&mut self) {
55+
if let Some(selected) = self.selected {
56+
self.set_selected((selected + 1).min(self.n_rows - 1));
57+
}
58+
}
59+
60+
pub fn select_previous(&mut self) {
61+
if let Some(selected) = self.selected {
62+
self.set_selected(selected.saturating_sub(1));
63+
}
64+
}
65+
66+
pub fn select_first(&mut self) {
67+
if self.n_rows > 0 {
68+
self.set_selected(0);
69+
}
70+
}
71+
72+
pub fn select_last(&mut self) {
73+
if self.n_rows > 0 {
74+
self.set_selected(self.n_rows - 1);
75+
}
76+
}
77+
78+
pub fn set_n_rows(&mut self, n_rows: usize) {
79+
self.n_rows = n_rows;
80+
81+
if self.n_rows == 0 {
82+
self.selected = None;
83+
return;
84+
}
85+
86+
self.set_selected(self.selected.map_or(0, |selected| selected.min(n_rows - 1)));
87+
}
88+
89+
#[inline]
90+
fn update_scroll_padding(&mut self) {
91+
self.scroll_padding = (self.max_n_rows_to_display / 4).min(self.max_scroll_padding);
92+
}
93+
94+
#[inline]
95+
pub fn max_n_rows_to_display(&self) -> usize {
96+
self.max_n_rows_to_display
97+
}
98+
99+
pub fn set_max_n_rows_to_display(&mut self, max_n_rows_to_display: usize) {
100+
self.max_n_rows_to_display = max_n_rows_to_display;
101+
self.update_scroll_padding();
102+
self.update_offset();
103+
}
104+
}

src/list/state.rs

Lines changed: 30 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ use crate::{
1717
MAX_EXERCISE_NAME_LEN,
1818
};
1919

20-
const MAX_SCROLL_PADDING: usize = 5;
20+
use super::scroll_state::ScrollState;
21+
2122
// +1 for column padding.
2223
const SPACE: &[u8] = &[b' '; MAX_EXERCISE_NAME_LEN + 1];
2324

@@ -39,19 +40,14 @@ pub struct ListState<'a> {
3940
/// Footer message to be displayed if not empty.
4041
pub message: String,
4142
app_state: &'a mut AppState,
43+
scroll_state: ScrollState,
4244
name_col_width: usize,
4345
filter: Filter,
44-
n_rows_with_filter: usize,
45-
/// Selected row out of the filtered ones.
46-
selected_row: Option<usize>,
47-
row_offset: usize,
4846
term_width: u16,
4947
term_height: u16,
5048
separator_line: Vec<u8>,
5149
narrow_term: bool,
5250
show_footer: bool,
53-
max_n_rows_to_display: usize,
54-
scroll_padding: usize,
5551
}
5652

5753
impl<'a> ListState<'a> {
@@ -70,50 +66,29 @@ impl<'a> ListState<'a> {
7066
let n_rows_with_filter = app_state.exercises().len();
7167
let selected = app_state.current_exercise_ind();
7268

69+
let (width, height) = terminal::size()?;
70+
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
71+
7372
let mut slf = Self {
7473
message: String::with_capacity(128),
7574
app_state,
75+
scroll_state,
7676
name_col_width,
7777
filter,
78-
n_rows_with_filter,
79-
selected_row: Some(selected),
80-
row_offset: selected.saturating_sub(MAX_SCROLL_PADDING),
8178
// Set by `set_term_size`
8279
term_width: 0,
8380
term_height: 0,
8481
separator_line: Vec::new(),
8582
narrow_term: false,
8683
show_footer: true,
87-
max_n_rows_to_display: 0,
88-
scroll_padding: 0,
8984
};
9085

91-
let (width, height) = terminal::size()?;
9286
slf.set_term_size(width, height);
9387
slf.draw(stdout)?;
9488

9589
Ok(slf)
9690
}
9791

98-
fn update_offset(&mut self) {
99-
let Some(selected) = self.selected_row else {
100-
return;
101-
};
102-
103-
let min_offset = (selected + self.scroll_padding)
104-
.saturating_sub(self.max_n_rows_to_display.saturating_sub(1));
105-
let max_offset = selected.saturating_sub(self.scroll_padding);
106-
let global_max_offset = self
107-
.n_rows_with_filter
108-
.saturating_sub(self.max_n_rows_to_display);
109-
110-
self.row_offset = self
111-
.row_offset
112-
.max(min_offset)
113-
.min(max_offset)
114-
.min(global_max_offset);
115-
}
116-
11792
pub fn set_term_size(&mut self, width: u16, height: u16) {
11893
self.term_width = width;
11994
self.term_height = height;
@@ -124,7 +99,7 @@ impl<'a> ListState<'a> {
12499

125100
let wide_help_footer_width = 95;
126101
// The help footer is shorter when nothing is selected.
127-
self.narrow_term = width < wide_help_footer_width && self.selected_row.is_some();
102+
self.narrow_term = width < wide_help_footer_width && self.scroll_state.selected().is_some();
128103

129104
let header_height = 1;
130105
// 2 separator, 1 progress bar, 1-2 footer message.
@@ -135,13 +110,10 @@ impl<'a> ListState<'a> {
135110
self.separator_line = "─".as_bytes().repeat(width as usize);
136111
}
137112

138-
self.max_n_rows_to_display = height
139-
.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
140-
as usize;
141-
142-
self.scroll_padding = (self.max_n_rows_to_display / 4).min(MAX_SCROLL_PADDING);
143-
144-
self.update_offset();
113+
self.scroll_state.set_max_n_rows_to_display(
114+
height.saturating_sub(header_height + u16::from(self.show_footer) * footer_height)
115+
as usize,
116+
);
145117
}
146118

147119
fn draw_rows(
@@ -150,15 +122,16 @@ impl<'a> ListState<'a> {
150122
filtered_exercises: impl Iterator<Item = (usize, &'a Exercise)>,
151123
) -> io::Result<usize> {
152124
let current_exercise_ind = self.app_state.current_exercise_ind();
125+
let row_offset = self.scroll_state.offset();
153126
let mut n_displayed_rows = 0;
154127

155128
for (exercise_ind, exercise) in filtered_exercises
156-
.skip(self.row_offset)
157-
.take(self.max_n_rows_to_display)
129+
.skip(row_offset)
130+
.take(self.scroll_state.max_n_rows_to_display())
158131
{
159132
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
160133

161-
if self.selected_row == Some(self.row_offset + n_displayed_rows) {
134+
if self.scroll_state.selected() == Some(row_offset + n_displayed_rows) {
162135
writer.stdout.queue(SetBackgroundColor(Color::Rgb {
163136
r: 40,
164137
g: 40,
@@ -225,7 +198,7 @@ impl<'a> ListState<'a> {
225198
Filter::None => self.draw_rows(stdout, iter)?,
226199
};
227200

228-
for _ in 0..self.max_n_rows_to_display - n_displayed_rows {
201+
for _ in 0..self.scroll_state.max_n_rows_to_display() - n_displayed_rows {
229202
next_ln(stdout)?;
230203
}
231204

@@ -247,7 +220,7 @@ impl<'a> ListState<'a> {
247220
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
248221
if self.message.is_empty() {
249222
// Help footer message
250-
if self.selected_row.is_some() {
223+
if self.scroll_state.selected().is_some() {
251224
writer.write_str("↓/j ↑/k home/g end/G | <c>ontinue at | <r>eset exercise")?;
252225
if self.narrow_term {
253226
next_ln(stdout)?;
@@ -298,13 +271,8 @@ impl<'a> ListState<'a> {
298271
stdout.queue(EndSynchronizedUpdate)?.flush()
299272
}
300273

301-
fn set_selected(&mut self, selected: usize) {
302-
self.selected_row = Some(selected);
303-
self.update_offset();
304-
}
305-
306274
fn update_rows(&mut self) {
307-
self.n_rows_with_filter = match self.filter {
275+
let n_rows = match self.filter {
308276
Filter::Done => self
309277
.app_state
310278
.exercises()
@@ -320,15 +288,7 @@ impl<'a> ListState<'a> {
320288
Filter::None => self.app_state.exercises().len(),
321289
};
322290

323-
if self.n_rows_with_filter == 0 {
324-
self.selected_row = None;
325-
return;
326-
}
327-
328-
self.set_selected(
329-
self.selected_row
330-
.map_or(0, |selected| selected.min(self.n_rows_with_filter - 1)),
331-
);
291+
self.scroll_state.set_n_rows(n_rows);
332292
}
333293

334294
#[inline]
@@ -341,28 +301,24 @@ impl<'a> ListState<'a> {
341301
self.update_rows();
342302
}
343303

304+
#[inline]
344305
pub fn select_next(&mut self) {
345-
if let Some(selected) = self.selected_row {
346-
self.set_selected((selected + 1).min(self.n_rows_with_filter - 1));
347-
}
306+
self.scroll_state.select_next();
348307
}
349308

309+
#[inline]
350310
pub fn select_previous(&mut self) {
351-
if let Some(selected) = self.selected_row {
352-
self.set_selected(selected.saturating_sub(1));
353-
}
311+
self.scroll_state.select_previous();
354312
}
355313

314+
#[inline]
356315
pub fn select_first(&mut self) {
357-
if self.n_rows_with_filter > 0 {
358-
self.set_selected(0);
359-
}
316+
self.scroll_state.select_first();
360317
}
361318

319+
#[inline]
362320
pub fn select_last(&mut self) {
363-
if self.n_rows_with_filter > 0 {
364-
self.set_selected(self.n_rows_with_filter - 1);
365-
}
321+
self.scroll_state.select_last();
366322
}
367323

368324
fn selected_to_exercise_ind(&self, selected: usize) -> Result<usize> {
@@ -390,7 +346,7 @@ impl<'a> ListState<'a> {
390346
}
391347

392348
pub fn reset_selected(&mut self) -> Result<()> {
393-
let Some(selected) = self.selected_row else {
349+
let Some(selected) = self.scroll_state.selected() else {
394350
self.message.push_str("Nothing selected to reset!");
395351
return Ok(());
396352
};
@@ -408,7 +364,7 @@ impl<'a> ListState<'a> {
408364

409365
// Return `true` if there was something to select.
410366
pub fn selected_to_current_exercise(&mut self) -> Result<bool> {
411-
let Some(selected) = self.selected_row else {
367+
let Some(selected) = self.scroll_state.selected() else {
412368
self.message.push_str("Nothing selected to continue at!");
413369
return Ok(false);
414370
};

0 commit comments

Comments
 (0)