use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavScroll {
dir: PathBuf,
offset: usize,
}
impl NavScroll {
pub fn update(
slot: &mut Option<NavScroll>,
dir: &Path,
selected: usize,
len: usize,
height: usize,
margin: usize,
) -> usize {
let previous = slot
.as_ref()
.filter(|s| s.dir == dir)
.map(|s| s.offset)
.unwrap_or_else(|| selected.saturating_sub(height / 2));
let offset = clamp(previous, selected, len, height, margin);
match slot {
Some(s) if s.dir == dir => s.offset = offset,
_ => {
*slot = Some(NavScroll {
dir: dir.to_owned(),
offset,
})
}
}
offset
}
}
fn clamp(offset: usize, selected: usize, len: usize, height: usize, margin: usize) -> usize {
if height == 0 || len <= height {
return 0;
}
let margin = margin.min(height.saturating_sub(1) / 2);
let selected = selected.min(len - 1);
let lowest = (selected + margin + 1).saturating_sub(height);
let highest = selected.saturating_sub(margin);
offset.clamp(lowest, highest).min(len - height)
}
#[cfg(test)]
mod tests {
use super::*;
fn row(offset: usize, selected: usize) -> usize {
selected - offset
}
#[test]
fn a_list_that_fits_never_scrolls() {
assert_eq!(clamp(5, 9, 10, 10, 3), 0);
assert_eq!(clamp(0, 0, 0, 10, 3), 0);
}
#[test]
fn moving_down_stops_margin_rows_short_of_the_bottom() {
let mut offset = 0;
for selected in 0..60 {
offset = clamp(offset, selected, 100, 20, 3);
assert!(row(offset, selected) <= 16, "selection {selected} too low");
}
assert_eq!(row(offset, 59), 16);
}
#[test]
fn moving_up_from_the_bottom_leaves_the_selection_where_it_is() {
let mut offset = clamp(0, 59, 100, 20, 3);
let start = row(offset, 59);
for selected in (50..59).rev() {
offset = clamp(offset, selected, 100, 20, 3);
assert_eq!(
offset,
clamp(0, 59, 100, 20, 3),
"view scrolled at {selected}"
);
}
assert_eq!(row(offset, 50), start - 9);
}
#[test]
fn moving_up_stops_margin_rows_short_of_the_top() {
let mut offset = 80;
for selected in (10..99).rev() {
offset = clamp(offset, selected, 100, 20, 3);
assert!(row(offset, selected) >= 3, "selection {selected} too high");
}
}
#[test]
fn the_margin_collapses_at_the_ends_of_the_list() {
assert_eq!(row(clamp(40, 0, 100, 20, 3), 0), 0, "first entry on row 0");
assert_eq!(
row(clamp(0, 99, 100, 20, 3), 99),
19,
"last entry on the last row"
);
}
#[test]
fn a_zero_margin_lets_the_selection_reach_either_edge() {
assert_eq!(row(clamp(0, 19, 100, 20, 0), 19), 19);
assert_eq!(row(clamp(19, 19, 100, 20, 0), 19), 0);
}
#[test]
fn an_oversized_margin_centres_instead_of_jittering() {
let mut offset = 0;
for selected in 10..80 {
offset = clamp(offset, selected, 100, 21, 50);
assert_eq!(row(offset, selected), 10);
}
}
#[test]
fn a_new_directory_centres_its_selection() {
let mut slot = None;
let offset = NavScroll::update(&mut slot, Path::new("/a"), 50, 100, 20, 3);
assert_eq!(row(offset, 50), 10);
}
#[test]
fn the_same_directory_keeps_its_offset_and_another_does_not() {
let mut slot = None;
let first = NavScroll::update(&mut slot, Path::new("/a"), 50, 100, 20, 3);
assert_eq!(
NavScroll::update(&mut slot, Path::new("/a"), 51, 100, 20, 3),
first
);
let other = NavScroll::update(&mut slot, Path::new("/b"), 5, 100, 20, 3);
assert_eq!(other, 0);
}
}