#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrollView {
pub total: usize,
pub offset: usize,
pub viewport: usize,
}
pub fn cycle(cursor: usize, len: usize, delta: isize) -> usize {
if len == 0 {
return 0;
}
(cursor as isize + delta).rem_euclid(len as isize) as usize
}
pub fn step_clamped(cursor: usize, len: usize, delta: isize) -> usize {
if len == 0 {
return 0;
}
let last = (len - 1) as isize;
(cursor as isize + delta).clamp(0, last) as usize
}
pub fn keep_visible(view: ScrollView, selected: usize) -> usize {
if view.viewport == 0 || view.total == 0 {
return 0;
}
let max_offset = view.total.saturating_sub(view.viewport);
let mut offset = view.offset.min(max_offset);
if selected < offset {
offset = selected;
} else if selected >= offset + view.viewport {
offset = selected + 1 - view.viewport;
}
offset.min(max_offset)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cycle_wraps_at_both_ends() {
assert_eq!(cycle(0, 3, -1), 2);
assert_eq!(cycle(2, 3, 1), 0);
assert_eq!(cycle(0, 0, 1), 0);
}
#[test]
fn step_clamped_stops_at_the_edges() {
assert_eq!(step_clamped(0, 3, -1), 0);
assert_eq!(step_clamped(2, 3, 5), 2);
}
#[test]
fn keep_visible_follows_the_cursor() {
let view = |offset| ScrollView {
total: 10,
offset,
viewport: 3,
};
assert_eq!(keep_visible(view(0), 5), 3);
assert_eq!(keep_visible(view(5), 2), 2);
assert_eq!(keep_visible(view(0), 0), 0);
}
#[test]
fn keep_visible_handles_empty_and_zero_viewport() {
let empty = ScrollView {
total: 0,
offset: 0,
viewport: 3,
};
assert_eq!(keep_visible(empty, 0), 0);
let no_viewport = ScrollView {
total: 10,
offset: 4,
viewport: 0,
};
assert_eq!(keep_visible(no_viewport, 5), 0);
}
}