#[derive(Debug, Clone, Copy)]
pub enum ScrollDirection {
Vertical,
Horizontal,
}
impl PartialEq for ScrollDirection {
fn eq(&self, other: &Self) -> bool {
matches!(
(self, other),
(ScrollDirection::Vertical, ScrollDirection::Vertical)
| (ScrollDirection::Horizontal, ScrollDirection::Horizontal)
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Scroller {
offset: u16,
last_scroll_direction: Option<ScrollDirection>,
scroll_buffer_count: u8,
invert_scroll_vertical: bool,
invert_scroll_horizontal: bool,
}
impl Scroller {
pub fn new() -> Self {
Scroller {
offset: 0,
last_scroll_direction: None,
scroll_buffer_count: 0,
invert_scroll_vertical: false,
invert_scroll_horizontal: false,
}
}
pub fn set_invert_scroll_vertical(&mut self, invert: bool) {
self.invert_scroll_vertical = invert;
}
pub fn is_scroll_vertical_inverted(&self) -> bool {
self.invert_scroll_vertical
}
pub fn set_invert_scroll_horizontal(&mut self, invert: bool) {
self.invert_scroll_horizontal = invert;
}
pub fn is_scroll_horizontal_inverted(&self) -> bool {
self.invert_scroll_horizontal
}
pub fn scroll_by(&mut self, delta: i16, max_offset: u16) {
if delta.is_negative() {
self.offset = self.offset.saturating_sub((-delta) as u16);
} else {
self.offset = self.offset.saturating_add(delta as u16).min(max_offset);
}
}
pub fn scroll_to(&mut self, position: u16, max_offset: u16) {
self.offset = position.min(max_offset);
}
pub fn scroll_to_top(&mut self) {
self.offset = 0;
}
pub fn scroll_to_bottom(&mut self, max_offset: u16) {
self.offset = max_offset;
}
pub fn offset(&self) -> u16 {
self.offset
}
pub fn set_offset(&mut self, offset: u16, max_offset: u16) {
self.offset = offset.min(max_offset);
}
pub fn can_scroll(&self, max_offset: u16) -> bool {
max_offset > 0
}
pub fn can_scroll_up(&self) -> bool {
self.offset > 0
}
pub fn can_scroll_down(&self, max_offset: u16) -> bool {
self.offset < max_offset
}
pub fn handle_scroll_event(&mut self, delta: i8, max_offset: u16) -> bool {
let final_delta = if self.invert_scroll_vertical {
-delta
} else {
delta
};
self.handle_scroll_with_direction(ScrollDirection::Vertical, final_delta, max_offset)
}
pub fn handle_scroll_horizontal(&mut self, delta: i8, max_offset: u16) -> bool {
let final_delta = if self.invert_scroll_horizontal {
-delta
} else {
delta
};
self.handle_scroll_with_direction(ScrollDirection::Horizontal, final_delta, max_offset)
}
fn handle_scroll_with_direction(
&mut self,
direction: ScrollDirection,
delta: i8,
max_offset: u16,
) -> bool {
const BUFFER_THRESHOLD: u8 = 2;
match self.last_scroll_direction {
None => {
self.last_scroll_direction = Some(direction);
self.scroll_buffer_count = 0;
let old_offset = self.offset;
self.scroll_by((delta as i16) * -3, max_offset);
old_offset != self.offset
}
Some(last_dir) if last_dir == direction => {
self.scroll_buffer_count = 0;
let old_offset = self.offset;
self.scroll_by((delta as i16) * -3, max_offset);
old_offset != self.offset
}
Some(_) => {
self.scroll_buffer_count += 1;
if self.scroll_buffer_count >= BUFFER_THRESHOLD {
self.last_scroll_direction = Some(direction);
self.scroll_buffer_count = 0;
let old_offset = self.offset;
self.scroll_by((delta as i16) * -3, max_offset);
old_offset != self.offset
} else {
false
}
}
}
}
pub fn calculate_scroll_from_position(
position: u16,
scrollbar_height: u16,
max_offset: u16,
) -> u16 {
if max_offset == 0 || scrollbar_height == 0 {
return 0;
}
((position as f32 / scrollbar_height as f32) * max_offset as f32).round() as u16
}
pub fn handle_scrollbar_event(
&mut self,
click_x: u16,
click_y: u16,
scrollbar_x: u16,
content_height: u16,
content_start_y: u16,
max_offset: u16,
) -> bool {
if click_x >= scrollbar_x.saturating_sub(1)
&& click_x <= scrollbar_x + 1
&& click_y >= content_start_y
&& click_y < content_start_y + content_height
{
let old_offset = self.offset;
let relative_y = click_y
.saturating_sub(content_start_y)
.min(content_height.saturating_sub(1));
let new_offset =
Self::calculate_scroll_from_position(relative_y, content_height, max_offset);
self.scroll_to(new_offset, max_offset);
return old_offset != self.offset;
}
false
}
pub fn handle_drag_event(
&mut self,
drag_x: u16,
drag_y: u16,
scrollbar_x: u16,
content_height: u16,
content_start_y: u16,
max_offset: u16,
) -> bool {
self.handle_scrollbar_event(
drag_x,
drag_y,
scrollbar_x,
content_height,
content_start_y,
max_offset,
)
}
}
impl Default for Scroller {
fn default() -> Self {
Self::new()
}
}
impl Default for ScrollDirection {
fn default() -> Self {
ScrollDirection::Vertical
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scroll_basic() {
let mut scroller = Scroller::new();
assert_eq!(scroller.offset(), 0);
scroller.scroll_by(5, 100);
assert_eq!(scroller.offset(), 5);
scroller.scroll_by(-3, 100);
assert_eq!(scroller.offset(), 2);
}
#[test]
fn test_scroll_bounds() {
let mut scroller = Scroller::new();
scroller.scroll_by(50, 20); assert_eq!(scroller.offset(), 20);
scroller.scroll_by(-100, 20); assert_eq!(scroller.offset(), 0); }
#[test]
fn test_scroll_to() {
let mut scroller = Scroller::new();
scroller.scroll_to(50, 100);
assert_eq!(scroller.offset(), 50);
scroller.scroll_to(150, 100); assert_eq!(scroller.offset(), 100);
}
#[test]
fn test_scroll_boundaries() {
let mut scroller = Scroller::new();
scroller.scroll_to_bottom(100);
assert_eq!(scroller.offset(), 100);
scroller.scroll_to_top();
assert_eq!(scroller.offset(), 0);
}
#[test]
fn test_can_scroll() {
let scroller = Scroller::new();
assert!(!scroller.can_scroll(0)); assert!(scroller.can_scroll(10)); }
#[test]
fn test_can_scroll_directions() {
let mut scroller = Scroller::new();
assert!(!scroller.can_scroll_up()); assert!(scroller.can_scroll_down(100));
scroller.scroll_to_bottom(100);
assert!(scroller.can_scroll_up()); assert!(!scroller.can_scroll_down(100)); }
#[test]
fn test_mouse_scroll_event() {
let mut scroller = Scroller::new();
assert!(scroller.handle_scroll_event(-1, 100)); assert_eq!(scroller.offset(), 3);
assert!(scroller.handle_scroll_event(1, 100)); assert_eq!(scroller.offset(), 0); }
#[test]
fn test_calculate_scroll_from_position() {
let offset = Scroller::calculate_scroll_from_position(50, 100, 200);
assert_eq!(offset, 100);
let offset = Scroller::calculate_scroll_from_position(25, 100, 200);
assert_eq!(offset, 50);
}
#[test]
fn test_handle_scrollbar_event() {
let mut scroller = Scroller::new();
let max_offset = 100;
let scrollbar_x = 79; let content_height = 20;
let content_start_y = 2;
assert!(scroller.handle_scrollbar_event(
scrollbar_x,
content_start_y + 10,
scrollbar_x,
content_height,
content_start_y,
max_offset
));
assert!(scroller.offset() > 40 && scroller.offset() < 60);
}
#[test]
fn test_scroll_direction_tracking() {
let mut scroller = Scroller::new();
let vertical_result = scroller.handle_scroll_event(-1, 100);
assert!(vertical_result);
let _horizontal = scroller.handle_scroll_horizontal(-1, 100);
}
#[test]
fn test_scroll_invert_vertical() {
let mut scroller = Scroller::new();
let result1 = scroller.handle_scroll_event(-1, 100);
let offset1 = scroller.offset();
assert!(result1);
assert!(offset1 > 0);
scroller.scroll_to_top();
assert_eq!(scroller.offset(), 0);
scroller.scroll_by(50, 100);
assert_eq!(scroller.offset(), 50);
scroller.set_invert_scroll_vertical(true);
let result2 = scroller.handle_scroll_event(-1, 100);
assert!(result2);
assert!(scroller.offset() < 50); }
#[test]
fn test_scroll_invert_horizontal() {
let mut scroller = Scroller::new();
assert!(!scroller.is_scroll_horizontal_inverted());
scroller.set_invert_scroll_horizontal(true);
assert!(scroller.is_scroll_horizontal_inverted());
let _result = scroller.handle_scroll_horizontal(-1, 100);
}
#[test]
fn test_scroll_invert_flags_independent() {
let mut scroller = Scroller::new();
scroller.set_invert_scroll_vertical(true);
assert!(scroller.is_scroll_vertical_inverted());
assert!(!scroller.is_scroll_horizontal_inverted());
scroller.set_invert_scroll_horizontal(true);
assert!(scroller.is_scroll_vertical_inverted());
assert!(scroller.is_scroll_horizontal_inverted());
scroller.set_invert_scroll_vertical(false);
assert!(!scroller.is_scroll_vertical_inverted());
assert!(scroller.is_scroll_horizontal_inverted());
}
}