use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, Default)]
pub struct ScrollState {
pub offset_y: usize,
pub offset_x: usize,
pub content_height: usize,
pub content_width: usize,
pub viewport_height: usize,
pub viewport_width: usize,
}
impl ScrollState {
pub fn new() -> Self {
Self::default()
}
pub fn with_viewport(viewport_width: usize, viewport_height: usize) -> Self {
Self {
viewport_width,
viewport_height,
..Default::default()
}
}
pub fn set_content_size(&mut self, width: usize, height: usize) {
self.content_width = width;
self.content_height = height;
self.clamp_offset();
}
pub fn set_viewport_size(&mut self, width: usize, height: usize) {
self.viewport_width = width;
self.viewport_height = height;
self.clamp_offset();
}
pub fn scroll_up(&mut self, lines: usize) {
self.offset_y = self.offset_y.saturating_sub(lines);
}
pub fn scroll_down(&mut self, lines: usize) {
self.offset_y = self.offset_y.saturating_add(lines);
self.clamp_offset();
}
pub fn scroll_left(&mut self, cols: usize) {
self.offset_x = self.offset_x.saturating_sub(cols);
}
pub fn scroll_right(&mut self, cols: usize) {
self.offset_x = self.offset_x.saturating_add(cols);
self.clamp_offset();
}
pub fn scroll_to_y(&mut self, offset: usize) {
self.offset_y = offset;
self.clamp_offset();
}
pub fn scroll_to_x(&mut self, offset: usize) {
self.offset_x = offset;
self.clamp_offset();
}
pub fn scroll_to_top(&mut self) {
self.offset_y = 0;
}
pub fn scroll_to_bottom(&mut self) {
if self.content_height > self.viewport_height {
self.offset_y = self.content_height - self.viewport_height;
} else {
self.offset_y = 0;
}
}
pub fn page_up(&mut self) {
self.scroll_up(self.viewport_height.max(1));
}
pub fn page_down(&mut self) {
self.scroll_down(self.viewport_height.max(1));
}
pub fn scroll_to_item(&mut self, index: usize) {
if index < self.offset_y {
self.offset_y = index;
} else if index >= self.offset_y + self.viewport_height {
self.offset_y = index.saturating_sub(self.viewport_height - 1);
}
}
pub fn max_offset_y(&self) -> usize {
self.content_height.saturating_sub(self.viewport_height)
}
pub fn max_offset_x(&self) -> usize {
self.content_width.saturating_sub(self.viewport_width)
}
pub fn can_scroll_up(&self) -> bool {
self.offset_y > 0
}
pub fn can_scroll_down(&self) -> bool {
self.offset_y < self.max_offset_y()
}
pub fn can_scroll_left(&self) -> bool {
self.offset_x > 0
}
pub fn can_scroll_right(&self) -> bool {
self.offset_x < self.max_offset_x()
}
pub fn scroll_percent_y(&self) -> f32 {
let max = self.max_offset_y();
if max == 0 {
0.0
} else {
self.offset_y as f32 / max as f32
}
}
pub fn scroll_percent_x(&self) -> f32 {
let max = self.max_offset_x();
if max == 0 {
0.0
} else {
self.offset_x as f32 / max as f32
}
}
pub fn visible_range(&self) -> (usize, usize) {
let start = self.offset_y;
let end = (self.offset_y + self.viewport_height).min(self.content_height);
(start, end)
}
fn clamp_offset(&mut self) {
self.offset_y = self.offset_y.min(self.max_offset_y());
self.offset_x = self.offset_x.min(self.max_offset_x());
}
}
#[derive(Clone)]
pub struct ScrollHandle {
state: Arc<RwLock<ScrollState>>,
}
impl ScrollHandle {
pub fn get(&self) -> ScrollState {
self.state.read().unwrap().clone()
}
pub fn offset_y(&self) -> usize {
self.state.read().unwrap().offset_y
}
pub fn offset_x(&self) -> usize {
self.state.read().unwrap().offset_x
}
pub fn set_content_size(&self, width: usize, height: usize) {
self.state.write().unwrap().set_content_size(width, height);
}
pub fn set_viewport_size(&self, width: usize, height: usize) {
self.state.write().unwrap().set_viewport_size(width, height);
}
pub fn scroll_up(&self, lines: usize) {
self.state.write().unwrap().scroll_up(lines);
}
pub fn scroll_down(&self, lines: usize) {
self.state.write().unwrap().scroll_down(lines);
}
pub fn scroll_left(&self, cols: usize) {
self.state.write().unwrap().scroll_left(cols);
}
pub fn scroll_right(&self, cols: usize) {
self.state.write().unwrap().scroll_right(cols);
}
pub fn scroll_to_y(&self, offset: usize) {
self.state.write().unwrap().scroll_to_y(offset);
}
pub fn scroll_to_x(&self, offset: usize) {
self.state.write().unwrap().scroll_to_x(offset);
}
pub fn scroll_to_top(&self) {
self.state.write().unwrap().scroll_to_top();
}
pub fn scroll_to_bottom(&self) {
self.state.write().unwrap().scroll_to_bottom();
}
pub fn page_up(&self) {
self.state.write().unwrap().page_up();
}
pub fn page_down(&self) {
self.state.write().unwrap().page_down();
}
pub fn scroll_to_item(&self, index: usize) {
self.state.write().unwrap().scroll_to_item(index);
}
pub fn can_scroll_up(&self) -> bool {
self.state.read().unwrap().can_scroll_up()
}
pub fn can_scroll_down(&self) -> bool {
self.state.read().unwrap().can_scroll_down()
}
pub fn scroll_percent_y(&self) -> f32 {
self.state.read().unwrap().scroll_percent_y()
}
pub fn visible_range(&self) -> (usize, usize) {
self.state.read().unwrap().visible_range()
}
pub fn try_get(&self) -> Option<ScrollState> {
self.state.read().ok().map(|g| g.clone())
}
pub fn try_offset_y(&self) -> Option<usize> {
self.state.read().ok().map(|g| g.offset_y)
}
pub fn try_offset_x(&self) -> Option<usize> {
self.state.read().ok().map(|g| g.offset_x)
}
pub fn try_set_content_size(&self, width: usize, height: usize) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.set_content_size(width, height);
true
} else {
false
}
}
pub fn try_set_viewport_size(&self, width: usize, height: usize) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.set_viewport_size(width, height);
true
} else {
false
}
}
pub fn try_scroll_up(&self, lines: usize) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.scroll_up(lines);
true
} else {
false
}
}
pub fn try_scroll_down(&self, lines: usize) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.scroll_down(lines);
true
} else {
false
}
}
pub fn try_scroll_left(&self, cols: usize) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.scroll_left(cols);
true
} else {
false
}
}
pub fn try_scroll_right(&self, cols: usize) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.scroll_right(cols);
true
} else {
false
}
}
pub fn try_scroll_to_y(&self, offset: usize) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.scroll_to_y(offset);
true
} else {
false
}
}
pub fn try_scroll_to_x(&self, offset: usize) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.scroll_to_x(offset);
true
} else {
false
}
}
pub fn try_scroll_to_top(&self) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.scroll_to_top();
true
} else {
false
}
}
pub fn try_scroll_to_bottom(&self) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.scroll_to_bottom();
true
} else {
false
}
}
pub fn try_page_up(&self) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.page_up();
true
} else {
false
}
}
pub fn try_page_down(&self) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.page_down();
true
} else {
false
}
}
pub fn try_scroll_to_item(&self, index: usize) -> bool {
if let Ok(mut guard) = self.state.write() {
guard.scroll_to_item(index);
true
} else {
false
}
}
pub fn try_can_scroll_up(&self) -> Option<bool> {
self.state.read().ok().map(|g| g.can_scroll_up())
}
pub fn try_can_scroll_down(&self) -> Option<bool> {
self.state.read().ok().map(|g| g.can_scroll_down())
}
pub fn try_scroll_percent_y(&self) -> Option<f32> {
self.state.read().ok().map(|g| g.scroll_percent_y())
}
pub fn try_visible_range(&self) -> Option<(usize, usize)> {
self.state.read().ok().map(|g| g.visible_range())
}
}
pub fn use_scroll() -> ScrollHandle {
use crate::hooks::context::current_context;
let Some(ctx) = current_context() else {
return ScrollHandle {
state: Arc::new(RwLock::new(ScrollState::new())),
};
};
let Ok(mut ctx_ref) = ctx.write() else {
return ScrollHandle {
state: Arc::new(RwLock::new(ScrollState::new())),
};
};
let storage = ctx_ref.use_hook(|| Arc::new(RwLock::new(ScrollState::new())));
let state = storage
.get::<Arc<RwLock<ScrollState>>>()
.unwrap_or_else(|| Arc::new(RwLock::new(ScrollState::new())));
ScrollHandle { state }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hooks::context::{HookContext, with_hooks};
use std::sync::{Arc, RwLock};
#[test]
fn test_scroll_state_basic() {
let mut state = ScrollState::new();
state.set_content_size(100, 50);
state.set_viewport_size(80, 10);
assert_eq!(state.offset_y, 0);
assert_eq!(state.max_offset_y(), 40);
}
#[test]
fn test_scroll_down() {
let mut state = ScrollState::new();
state.set_content_size(100, 50);
state.set_viewport_size(80, 10);
state.scroll_down(5);
assert_eq!(state.offset_y, 5);
state.scroll_down(100);
assert_eq!(state.offset_y, 40); }
#[test]
fn test_scroll_up() {
let mut state = ScrollState::new();
state.set_content_size(100, 50);
state.set_viewport_size(80, 10);
state.scroll_down(20);
state.scroll_up(5);
assert_eq!(state.offset_y, 15);
state.scroll_up(100);
assert_eq!(state.offset_y, 0);
}
#[test]
fn test_page_navigation() {
let mut state = ScrollState::new();
state.set_content_size(100, 50);
state.set_viewport_size(80, 10);
state.page_down();
assert_eq!(state.offset_y, 10);
state.page_up();
assert_eq!(state.offset_y, 0);
}
#[test]
fn test_scroll_to_item() {
let mut state = ScrollState::new();
state.set_content_size(100, 50);
state.set_viewport_size(80, 10);
state.scroll_to_item(15);
assert_eq!(state.offset_y, 6);
state.scroll_to_item(3);
assert_eq!(state.offset_y, 3);
}
#[test]
fn test_visible_range() {
let mut state = ScrollState::new();
state.set_content_size(100, 50);
state.set_viewport_size(80, 10);
state.scroll_down(5);
let (start, end) = state.visible_range();
assert_eq!(start, 5);
assert_eq!(end, 15);
}
#[test]
fn test_scroll_percent() {
let mut state = ScrollState::new();
state.set_content_size(100, 50);
state.set_viewport_size(80, 10);
assert_eq!(state.scroll_percent_y(), 0.0);
state.scroll_to_bottom();
assert_eq!(state.scroll_percent_y(), 1.0);
state.scroll_to_y(20);
assert!((state.scroll_percent_y() - 0.5).abs() < 0.01);
}
#[test]
fn test_can_scroll() {
let mut state = ScrollState::new();
state.set_content_size(100, 50);
state.set_viewport_size(80, 10);
assert!(!state.can_scroll_up());
assert!(state.can_scroll_down());
state.scroll_to_bottom();
assert!(state.can_scroll_up());
assert!(!state.can_scroll_down());
}
#[test]
fn test_use_scroll_without_context_does_not_panic() {
let scroll = use_scroll();
scroll.set_content_size(100, 50);
scroll.set_viewport_size(80, 10);
scroll.scroll_down(7);
assert_eq!(scroll.offset_y(), 7);
}
#[test]
fn test_use_scroll_preserves_state_in_context() {
let ctx = Arc::new(RwLock::new(HookContext::new()));
let scroll1 = with_hooks(ctx.clone(), use_scroll);
scroll1.set_content_size(100, 50);
scroll1.set_viewport_size(80, 10);
scroll1.scroll_down(9);
let scroll2 = with_hooks(ctx, use_scroll);
assert_eq!(scroll2.offset_y(), 9);
}
}