use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Window {
offset: usize,
len: usize,
total: usize,
}
impl Window {
pub(crate) fn containing(total: usize, height: usize, selected: usize) -> Self {
let offset = if height == 0 || total <= height || selected < height {
0
} else {
(selected + 1 - height).min(total - height)
};
Self::new(total, height, offset)
}
pub(crate) fn at(total: usize, height: usize, scroll: usize) -> Self {
Self::new(total, height, scroll.min(Self::max_scroll(total, height)))
}
fn new(total: usize, height: usize, offset: usize) -> Self {
Self {
offset,
len: height.min(total.saturating_sub(offset)),
total,
}
}
pub(crate) fn max_scroll(total: usize, height: usize) -> usize {
total.saturating_sub(height)
}
pub(crate) fn offset(self) -> usize {
self.offset
}
pub(crate) fn total(self) -> usize {
self.total
}
pub(crate) fn range(self) -> Range<usize> {
self.offset..self.offset + self.len
}
pub(crate) fn is_clipped(self) -> bool {
self.len < self.total
}
pub(crate) fn range_label(self) -> Option<String> {
(self.len > 0).then(|| {
format!(
"{}-{}/{}",
self.offset + 1,
self.offset + self.len,
self.total
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_window_over_content_that_fits_shows_all_of_it() {
let window = Window::containing(5, 10, 4);
assert_eq!(window.offset(), 0);
assert_eq!(window.range(), 0..5);
assert!(!window.is_clipped());
}
#[test]
fn a_selection_inside_the_first_window_does_not_scroll() {
for selected in 0..5 {
assert_eq!(
Window::containing(20, 5, selected).offset(),
0,
"{selected}"
);
}
}
#[test]
fn a_selection_below_the_window_scrolls_it_by_exactly_one_row() {
assert_eq!(Window::containing(20, 5, 5).offset(), 1);
assert_eq!(Window::containing(20, 5, 6).offset(), 2);
}
#[test]
fn a_window_never_scrolls_past_the_end_of_the_content() {
let last = Window::containing(20, 5, 19);
assert_eq!(last.offset(), 15);
assert_eq!(last.range(), 15..20);
}
#[test]
fn every_selection_is_visible_at_every_size() {
for total in 0..25usize {
for height in 1..12usize {
for selected in 0..total {
let window = Window::containing(total, height, selected);
assert!(
window.range().contains(&selected),
"total={total} height={height} selected={selected} \
window={:?}",
window.range()
);
assert!(window.range().end <= total);
}
}
}
}
#[test]
fn an_empty_list_yields_an_empty_window() {
let window = Window::containing(0, 10, 0);
assert!(window.range().is_empty());
assert!(!window.is_clipped());
assert_eq!(window.range_label(), None);
}
#[test]
fn a_zero_height_viewport_yields_an_empty_window() {
let window = Window::containing(20, 0, 7);
assert!(window.range().is_empty());
assert!(window.is_clipped());
assert_eq!(window.range_label(), None);
}
#[test]
fn a_one_row_viewport_shows_the_selected_row_alone() {
let window = Window::containing(20, 1, 7);
assert_eq!(window.range(), 7..8);
assert!(window.is_clipped());
assert_eq!(window.range_label().as_deref(), Some("8-8/20"));
}
#[test]
fn a_selection_past_the_end_clamps_to_the_last_window() {
let window = Window::containing(20, 5, 99);
assert_eq!(window.range(), 15..20);
}
#[test]
fn a_scrolled_window_starts_where_it_was_asked_to() {
let window = Window::at(20, 5, 3);
assert_eq!(window.range(), 3..8);
assert!(window.is_clipped());
}
#[test]
fn a_scrolled_window_is_clamped_to_the_content() {
assert_eq!(Window::at(20, 5, 99).range(), 15..20);
assert_eq!(Window::at(20, 30, 15).range(), 0..20);
assert_eq!(Window::at(0, 5, 3).range(), 0..0);
}
#[test]
fn max_scroll_is_zero_when_everything_fits() {
assert_eq!(Window::max_scroll(5, 10), 0);
assert_eq!(Window::max_scroll(20, 5), 15);
assert_eq!(Window::max_scroll(0, 0), 0);
}
#[test]
fn a_range_label_counts_from_one() {
assert_eq!(
Window::containing(42, 10, 0).range_label().as_deref(),
Some("1-10/42")
);
assert_eq!(
Window::containing(42, 10, 41).range_label().as_deref(),
Some("33-42/42")
);
}
}