#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScrollOrientation {
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ScrollSize {
pub width: u16,
pub height: u16,
}
impl ScrollSize {
pub const fn new(width: u16, height: u16) -> Self {
Self { width, height }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ScrollOffset {
pub x: u16,
pub y: u16,
}
impl ScrollOffset {
pub const fn new(x: u16, y: u16) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrollState {
content: ScrollSize,
viewport: ScrollSize,
offset: ScrollOffset,
}
impl Default for ScrollState {
fn default() -> Self {
Self::new(ScrollSize::new(0, 0), ScrollSize::new(0, 0))
}
}
impl ScrollState {
pub fn new(content: ScrollSize, viewport: ScrollSize) -> Self {
let mut this = Self {
content,
viewport,
offset: ScrollOffset::new(0, 0),
};
this.clamp();
this
}
pub fn content_size(&self) -> ScrollSize {
self.content
}
pub fn viewport_size(&self) -> ScrollSize {
self.viewport
}
pub fn offset(&self) -> ScrollOffset {
self.offset
}
pub fn offset_for(&self, orientation: ScrollOrientation) -> u16 {
match orientation {
ScrollOrientation::Vertical => self.offset.y,
ScrollOrientation::Horizontal => self.offset.x,
}
}
pub fn set_content_size(&mut self, size: ScrollSize) {
self.content = size;
self.clamp();
}
pub fn set_viewport_size(&mut self, size: ScrollSize) {
self.viewport = size;
self.clamp();
}
pub fn set_offset(&mut self, offset: ScrollOffset) {
self.offset = offset;
self.clamp();
}
pub fn set_offset_for(&mut self, orientation: ScrollOrientation, value: u16) {
match orientation {
ScrollOrientation::Vertical => self.offset.y = value,
ScrollOrientation::Horizontal => self.offset.x = value,
}
self.clamp();
}
pub fn scroll_by(&mut self, orientation: ScrollOrientation, delta: i16) {
match orientation {
ScrollOrientation::Vertical => {
if delta < 0 {
self.offset.y = self.offset.y.saturating_sub((-delta) as u16);
} else {
self.offset.y = self.offset.y.saturating_add(delta as u16);
}
}
ScrollOrientation::Horizontal => {
if delta < 0 {
self.offset.x = self.offset.x.saturating_sub((-delta) as u16);
} else {
self.offset.x = self.offset.x.saturating_add(delta as u16);
}
}
}
self.clamp();
}
pub fn scroll_to(&mut self, orientation: ScrollOrientation, value: u16) {
self.set_offset_for(orientation, value);
}
pub fn scroll_to_start(&mut self, orientation: ScrollOrientation) {
self.set_offset_for(orientation, 0);
}
pub fn scroll_to_end(&mut self, orientation: ScrollOrientation) {
let max = self.max_offset_for(orientation);
self.set_offset_for(orientation, max);
}
pub fn max_offset(&self) -> ScrollOffset {
ScrollOffset::new(self.max_offset_x(), self.max_offset_y())
}
pub fn max_offset_for(&self, orientation: ScrollOrientation) -> u16 {
match orientation {
ScrollOrientation::Vertical => self.max_offset_y(),
ScrollOrientation::Horizontal => self.max_offset_x(),
}
}
pub fn max_offset_x(&self) -> u16 {
self.content.width.saturating_sub(self.viewport.width)
}
pub fn max_offset_y(&self) -> u16 {
self.content.height.saturating_sub(self.viewport.height)
}
pub fn can_scroll(&self, orientation: ScrollOrientation) -> bool {
self.max_offset_for(orientation) > 0
}
pub fn can_scroll_vertical(&self) -> bool {
self.can_scroll(ScrollOrientation::Vertical)
}
pub fn can_scroll_horizontal(&self) -> bool {
self.can_scroll(ScrollOrientation::Horizontal)
}
pub fn at_start(&self, orientation: ScrollOrientation) -> bool {
self.offset_for(orientation) == 0
}
pub fn at_end(&self, orientation: ScrollOrientation) -> bool {
self.offset_for(orientation) >= self.max_offset_for(orientation)
}
pub fn scroll_ratio(&self, orientation: ScrollOrientation) -> f32 {
let max = self.max_offset_for(orientation);
if max == 0 {
return 0.0;
}
self.offset_for(orientation) as f32 / max as f32
}
pub fn visible_range(&self) -> (u16, u16, u16, u16) {
let start_x = self.offset.x;
let start_y = self.offset.y;
let end_x = self
.offset
.x
.saturating_add(self.viewport.width)
.min(self.content.width);
let end_y = self
.offset
.y
.saturating_add(self.viewport.height)
.min(self.content.height);
(start_x, start_y, end_x, end_y)
}
pub fn clamp(&mut self) {
let max_x = self.max_offset_x();
let max_y = self.max_offset_y();
self.offset.x = self.offset.x.min(max_x);
self.offset.y = self.offset.y.min(max_y);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clamps_offsets_on_set_sizes() {
let mut s = ScrollState::new(ScrollSize::new(100, 50), ScrollSize::new(20, 10));
s.set_offset(ScrollOffset::new(999, 999));
assert_eq!(s.offset(), ScrollOffset::new(80, 40));
s.set_viewport_size(ScrollSize::new(200, 100));
assert_eq!(s.max_offset(), ScrollOffset::new(0, 0));
assert_eq!(s.offset(), ScrollOffset::new(0, 0));
}
#[test]
fn scroll_by_saturates_and_clamps() {
let mut s = ScrollState::new(ScrollSize::new(10, 10), ScrollSize::new(5, 5));
s.scroll_by(ScrollOrientation::Vertical, 3);
assert_eq!(s.offset().y, 3);
s.scroll_by(ScrollOrientation::Vertical, -10);
assert_eq!(s.offset().y, 0);
s.scroll_by(ScrollOrientation::Vertical, 999);
assert_eq!(s.offset().y, 5);
}
#[test]
fn ratios_behave() {
let s = ScrollState::new(ScrollSize::new(10, 10), ScrollSize::new(10, 10));
assert_eq!(s.scroll_ratio(ScrollOrientation::Vertical), 0.0);
let mut s = ScrollState::new(ScrollSize::new(10, 10), ScrollSize::new(5, 5));
s.scroll_to(ScrollOrientation::Horizontal, 2);
assert!((s.scroll_ratio(ScrollOrientation::Horizontal) - 0.4).abs() < 1e-6);
}
}