#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StickyScroll {
None,
Top,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrollState {
pub offset: usize,
pub total: usize,
pub viewport: usize,
pub sticky: StickyScroll,
}
impl ScrollState {
pub fn new() -> Self {
Self {
offset: 0,
total: 0,
viewport: 0,
sticky: StickyScroll::None,
}
}
pub fn with_sticky(mut self, sticky: StickyScroll) -> Self {
self.sticky = sticky;
self
}
pub fn set_bounds(&mut self, total: usize, viewport: usize) {
let was_at_bottom = self.is_at_bottom();
self.total = total;
self.viewport = viewport;
match self.sticky {
StickyScroll::Top => self.offset = 0,
StickyScroll::Bottom if was_at_bottom => self.scroll_to_bottom(),
_ => self.clamp(),
}
}
pub fn max_offset(&self) -> usize {
self.total.saturating_sub(self.viewport)
}
pub fn clamp(&mut self) {
self.offset = self.offset.min(self.max_offset());
}
pub fn scroll_up(&mut self, lines: usize) {
self.offset = self.offset.saturating_sub(lines);
}
pub fn scroll_down(&mut self, lines: usize) {
self.offset = (self.offset + lines).min(self.max_offset());
}
pub fn page_up(&mut self) {
self.scroll_up(self.viewport.max(1));
}
pub fn page_down(&mut self) {
self.scroll_down(self.viewport.max(1));
}
pub fn scroll_to_top(&mut self) {
self.offset = 0;
}
pub fn scroll_to_bottom(&mut self) {
self.offset = self.max_offset();
}
pub fn is_at_top(&self) -> bool {
self.offset == 0
}
pub fn is_at_bottom(&self) -> bool {
self.offset >= self.max_offset()
}
pub fn visible_range(&self) -> std::ops::Range<usize> {
let start = self.offset.min(self.total);
let end = (start + self.viewport).min(self.total);
start..end
}
}
impl Default for ScrollState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scroll_state_clamps_offset() {
let mut state = ScrollState::new();
state.set_bounds(100, 10);
state.scroll_down(500);
assert_eq!(state.offset, 90);
state.scroll_up(500);
assert_eq!(state.offset, 0);
}
#[test]
fn scroll_state_visible_range() {
let mut state = ScrollState::new();
state.set_bounds(20, 5);
state.scroll_down(3);
assert_eq!(state.visible_range(), 3..8);
}
#[test]
fn scroll_state_sticky_bottom_tracks_growth() {
let mut state = ScrollState::new().with_sticky(StickyScroll::Bottom);
state.set_bounds(10, 5);
state.scroll_to_bottom();
state.set_bounds(20, 5);
assert_eq!(state.offset, 15);
}
}