use std::any::Any;
use std::sync::{Arc, Mutex};
use super::component::Component;
use super::layout_node::{LayoutNode, LayoutNodeProvider, ScrollNode};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FollowMode {
None,
#[default]
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OverscrollMode {
#[default]
None,
Chain,
Contain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScrollbarMode {
#[default]
Hidden,
Auto,
Always,
}
#[derive(Debug, Clone, Default)]
pub struct ScrollViewOptions {
pub axis: Option<ScrollAxis>,
pub follow: FollowMode,
pub primary: bool,
pub overscroll: OverscrollMode,
pub scrollbar: ScrollbarMode,
pub scrollbar_hide_delay_ms: Option<u64>,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum ScrollAxis {
#[default]
Vertical,
}
#[derive(Debug, Clone)]
struct ScrollViewState {
scroll_top: usize,
is_following_end: bool,
viewport_height: usize,
content_height: usize,
follow_suppressed_at_end: bool,
transient_scrollbar_visible: bool,
}
impl ScrollViewState {
fn new(follow: FollowMode) -> Self {
Self {
scroll_top: 0,
is_following_end: follow == FollowMode::End,
viewport_height: 0,
content_height: 0,
follow_suppressed_at_end: false,
transient_scrollbar_visible: false,
}
}
}
#[derive(Clone)]
pub struct ScrollView {
child: Arc<dyn Component>,
options: ScrollViewOptions,
state: Arc<Mutex<ScrollViewState>>,
request_render_callback: Arc<Mutex<Option<Arc<dyn Fn() + Send + Sync>>>>,
}
impl ScrollView {
pub fn new(child: Arc<dyn Component>, options: ScrollViewOptions) -> Self {
let follow = options.follow;
Self {
child,
options,
state: Arc::new(Mutex::new(ScrollViewState::new(follow))),
request_render_callback: Arc::new(Mutex::new(None)),
}
}
pub fn simple(child: Arc<dyn Component>) -> Self {
Self::new(child, ScrollViewOptions::default())
}
pub fn primary(child: Arc<dyn Component>) -> Self {
Self::new(child, ScrollViewOptions {
follow: FollowMode::End,
primary: true,
..Default::default()
})
}
pub fn child(&self) -> &Arc<dyn Component> {
&self.child
}
pub fn scroll_top(&self) -> usize {
self.state.lock().map(|s| s.scroll_top).unwrap_or(0)
}
pub fn is_following_end(&self) -> bool {
self.state.lock().map(|s| s.is_following_end).unwrap_or(false)
}
pub fn viewport_height(&self) -> usize {
self.state.lock().map(|s| s.viewport_height).unwrap_or(0)
}
pub fn content_height(&self) -> usize {
self.state.lock().map(|s| s.content_height).unwrap_or(0)
}
pub fn is_primary(&self) -> bool {
self.options.primary
}
pub fn is_scrollbar_visible(&self) -> bool {
match self.options.scrollbar {
ScrollbarMode::Always => self.viewport_height() > 0,
ScrollbarMode::Auto => {
self.content_height() > self.viewport_height()
&& self.state.lock().map(|s| s.transient_scrollbar_visible).unwrap_or(false)
}
ScrollbarMode::Hidden => false,
}
}
pub fn get_content_width(&self, width: usize) -> usize {
if self.options.scrollbar == ScrollbarMode::Always && width > 1 {
width - 1
} else {
width
}
}
pub fn scroll_by(&self, delta: i32) -> i32 {
if delta == 0 {
return 0;
}
if let Ok(mut state) = self.state.lock() {
let max_scroll = state.content_height.saturating_sub(state.viewport_height);
let start = if state.is_following_end { max_scroll } else { state.scroll_top };
let new_top = if delta > 0 {
start.saturating_add(delta as usize)
} else {
start.saturating_sub((-delta) as usize)
};
let clamped = new_top.min(max_scroll);
let moved = (clamped as i32) - (start as i32);
state.scroll_top = clamped;
state.is_following_end = self.options.follow == FollowMode::End && clamped >= max_scroll;
state.follow_suppressed_at_end = false;
if moved != 0 {
state.transient_scrollbar_visible = true;
}
delta - moved
} else {
delta
}
}
pub fn scroll_to(&self, position: usize) {
if let Ok(mut state) = self.state.lock() {
let max_scroll = state.content_height.saturating_sub(state.viewport_height);
state.scroll_top = position.min(max_scroll);
if self.options.follow == FollowMode::End {
state.is_following_end = state.scroll_top >= max_scroll;
}
state.follow_suppressed_at_end = false;
state.transient_scrollbar_visible = true;
}
self.request_render();
}
pub fn scroll_to_start(&self) {
if let Ok(mut state) = self.state.lock() {
let changed = state.scroll_top != 0 || state.is_following_end;
state.scroll_top = 0;
state.is_following_end = false;
state.follow_suppressed_at_end = false;
if changed {
state.transient_scrollbar_visible = true;
}
}
self.request_render();
}
pub fn scroll_to_end(&self) {
if let Ok(mut state) = self.state.lock() {
let max_scroll = state.content_height.saturating_sub(state.viewport_height);
let changed = state.scroll_top != max_scroll || !state.is_following_end;
state.scroll_top = max_scroll;
state.is_following_end = self.options.follow == FollowMode::End;
state.follow_suppressed_at_end = false;
if changed {
state.transient_scrollbar_visible = true;
}
}
self.request_render();
}
pub fn update_layout(&self, content_height: usize, viewport_height: usize, request_render: Option<Arc<dyn Fn() + Send + Sync>>) {
if let Ok(mut state) = self.state.lock() {
state.content_height = content_height;
state.viewport_height = viewport_height;
if let Some(cb) = request_render {
*self.request_render_callback.lock().unwrap() = Some(cb);
}
if self.options.follow == FollowMode::End && state.is_following_end {
let max_scroll = content_height.saturating_sub(viewport_height);
state.scroll_top = max_scroll;
}
let max_scroll = content_height.saturating_sub(viewport_height);
state.scroll_top = state.scroll_top.min(max_scroll);
if state.scroll_top < max_scroll {
state.follow_suppressed_at_end = false;
}
if self.options.follow == FollowMode::End
&& state.scroll_top == max_scroll
&& !state.follow_suppressed_at_end
{
state.is_following_end = true;
}
if content_height <= viewport_height {
state.transient_scrollbar_visible = false;
}
}
}
fn request_render(&self) {
if let Ok(cb) = self.request_render_callback.lock() {
if let Some(callback) = cb.as_ref() {
callback();
}
}
}
pub fn render_with_viewport(&self, width: usize, height: usize) -> Vec<String> {
let content_width = self.get_content_width(width);
let all_lines = self.child.render(content_width);
let content_height = all_lines.len();
self.update_layout(content_height, height, None);
let scroll_top = self.scroll_top();
let visible: Vec<String> = all_lines
.into_iter()
.skip(scroll_top)
.take(height)
.collect();
let mut visible = visible;
while visible.len() < height {
visible.push(String::new());
}
if self.is_scrollbar_visible() {
}
visible
}
pub fn scrollbar_style(&self, text: &str) -> String {
format!("\x1b[100m{}\x1b[49m", text)
}
}
impl Component for ScrollView {
fn render(&self, width: usize) -> Vec<String> {
let content_width = self.get_content_width(width);
self.child.render(content_width)
}
fn invalidate(&self) {
self.child.invalidate();
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl LayoutNodeProvider for ScrollView {
fn layout_node(&self) -> Option<LayoutNode> {
Some(LayoutNode::Scroll(ScrollNode::new(self.child.clone())))
}
}
pub fn render_scroll_view(scroll_view: &ScrollView, width: usize, height: usize) -> Vec<String> {
scroll_view.render_with_viewport(width, height)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Text, Container};
#[test]
fn test_scroll_view_basic() {
let text = Arc::new(Text::new("Line 1\nLine 2\nLine 3\nLine 4\nLine 5", 0, 0));
let scroll = ScrollView::simple(text);
assert_eq!(scroll.scroll_top(), 0);
assert!(scroll.is_following_end());
}
#[test]
fn test_scroll_by() {
let text = Arc::new(Text::new("Line 1\nLine 2\nLine 3\nLine 4\nLine 5", 0, 0));
let scroll = ScrollView::new(text, ScrollViewOptions {
follow: FollowMode::None,
..Default::default()
});
scroll.update_layout(5, 2, None);
let unused = scroll.scroll_by(1);
assert_eq!(unused, 0);
assert_eq!(scroll.scroll_top(), 1);
}
#[test]
fn test_scroll_to_end() {
let text = Arc::new(Text::new("Line 1\nLine 2\nLine 3", 0, 0));
let scroll = ScrollView::new(text, ScrollViewOptions {
follow: FollowMode::End,
..Default::default()
});
scroll.update_layout(3, 1, None);
scroll.scroll_to_end();
assert!(scroll.is_following_end());
assert_eq!(scroll.scroll_top(), 2);
}
#[test]
fn test_scroll_to_start() {
let text = Arc::new(Text::new("Line 1\nLine 2\nLine 3", 0, 0));
let scroll = ScrollView::simple(text);
scroll.update_layout(3, 1, None);
scroll.scroll_by(2);
scroll.scroll_to_start();
assert_eq!(scroll.scroll_top(), 0);
assert!(!scroll.is_following_end());
}
}