use crate::layout::Rect;
use crate::render::{Buffer, Cell};
use crate::style::Color;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
pub struct ScrollView {
content_height: u16,
scroll_offset: u16,
show_scrollbar: bool,
scrollbar_fg: Option<Color>,
scrollbar_bg: Option<Color>,
min_width: u16,
min_height: u16,
max_width: u16,
max_height: u16,
props: WidgetProps,
}
impl ScrollView {
pub fn new() -> Self {
Self {
content_height: 0,
scroll_offset: 0,
show_scrollbar: true,
scrollbar_fg: Some(Color::WHITE),
scrollbar_bg: Some(Color::rgb(64, 64, 64)),
min_width: 0,
min_height: 0,
max_width: 0,
max_height: 0,
props: WidgetProps::new(),
}
}
pub fn content_height(mut self, height: u16) -> Self {
self.content_height = height;
self
}
pub fn scroll_offset(mut self, offset: u16) -> Self {
self.scroll_offset = offset;
self
}
pub fn show_scrollbar(mut self, show: bool) -> Self {
self.show_scrollbar = show;
self
}
pub fn scrollbar_style(mut self, fg: Color, bg: Color) -> Self {
self.scrollbar_fg = Some(fg);
self.scrollbar_bg = Some(bg);
self
}
pub fn min_width(mut self, width: u16) -> Self {
self.min_width = width;
self
}
pub fn min_height(mut self, height: u16) -> Self {
self.min_height = height;
self
}
pub fn max_width(mut self, width: u16) -> Self {
self.max_width = width;
self
}
pub fn max_height(mut self, height: u16) -> Self {
self.max_height = height;
self
}
pub fn min_size(self, width: u16, height: u16) -> Self {
self.min_width(width).min_height(height)
}
pub fn max_size(self, width: u16, height: u16) -> Self {
self.max_width(width).max_height(height)
}
pub fn constrain(self, min_w: u16, min_h: u16, max_w: u16, max_h: u16) -> Self {
self.min_width(min_w)
.min_height(min_h)
.max_width(max_w)
.max_height(max_h)
}
fn apply_constraints(&self, area: Rect) -> Rect {
let eff_max_w = if self.max_width > 0 {
self.max_width.max(self.min_width)
} else {
u16::MAX
};
let eff_max_h = if self.max_height > 0 {
self.max_height.max(self.min_height)
} else {
u16::MAX
};
let width = area.width.clamp(self.min_width, eff_max_w);
let height = area.height.clamp(self.min_height, eff_max_h);
Rect::new(area.x, area.y, width, height)
}
pub fn offset(&self) -> u16 {
self.scroll_offset
}
pub fn set_offset(&mut self, offset: u16, viewport_height: u16) {
let max_offset = self.content_height.saturating_sub(viewport_height);
self.scroll_offset = offset.min(max_offset);
}
pub fn scroll_down(&mut self, lines: u16, viewport_height: u16) {
let max_offset = self.content_height.saturating_sub(viewport_height);
self.scroll_offset = (self.scroll_offset + lines).min(max_offset);
}
pub fn scroll_up(&mut self, lines: u16) {
self.scroll_offset = self.scroll_offset.saturating_sub(lines);
}
pub fn scroll_to_top(&mut self) {
self.scroll_offset = 0;
}
pub fn scroll_to_bottom(&mut self, viewport_height: u16) {
let max_offset = self.content_height.saturating_sub(viewport_height);
self.scroll_offset = max_offset;
}
pub fn page_down(&mut self, viewport_height: u16) {
self.scroll_down(viewport_height.saturating_sub(1), viewport_height);
}
pub fn page_up(&mut self, viewport_height: u16) {
self.scroll_up(viewport_height.saturating_sub(1));
}
pub fn handle_mouse(&mut self, event: &crate::event::MouseEvent, viewport_height: u16) -> bool {
use crate::event::MouseEventKind;
let old_offset = self.scroll_offset;
match event.kind {
MouseEventKind::ScrollUp => {
self.scroll_up(3);
}
MouseEventKind::ScrollDown => {
self.scroll_down(3, viewport_height);
}
_ => {}
}
old_offset != self.scroll_offset
}
pub fn handle_key(&mut self, key: &crate::event::Key, viewport_height: u16) -> bool {
use crate::event::Key;
let old_offset = self.scroll_offset;
match key {
Key::Up | Key::Char('k') => {
self.scroll_up(1);
}
Key::Down | Key::Char('j') => {
self.scroll_down(1, viewport_height);
}
Key::PageUp => {
self.page_up(viewport_height);
}
Key::PageDown => {
self.page_down(viewport_height);
}
Key::Home => {
self.scroll_to_top();
}
Key::End => {
self.scroll_to_bottom(viewport_height);
}
_ => {}
}
old_offset != self.scroll_offset
}
pub fn is_scrollable(&self, viewport_height: u16) -> bool {
self.content_height > viewport_height
}
pub fn scroll_percentage(&self, viewport_height: u16) -> f32 {
let max_offset = self.content_height.saturating_sub(viewport_height);
if max_offset == 0 {
0.0
} else {
self.scroll_offset as f32 / max_offset as f32
}
}
pub fn render_scrollbar(&self, ctx: &mut RenderContext) {
if !self.show_scrollbar {
return;
}
let area = self.apply_constraints(ctx.area);
if area.width < 1 || area.height < 1 {
return;
}
let viewport_height = area.height;
if self.content_height <= viewport_height {
return; }
let scrollbar_x = area.width - 1;
let thumb_height = ((viewport_height as f32 / self.content_height as f32)
* viewport_height as f32)
.max(1.0) as u16;
let thumb_height = thumb_height.max(1).min(viewport_height);
let max_offset = self.content_height.saturating_sub(viewport_height);
let scroll_ratio = if max_offset > 0 {
self.scroll_offset as f32 / max_offset as f32
} else {
0.0
};
let thumb_position = ((viewport_height - thumb_height) as f32 * scroll_ratio)
.max(0.0)
.min((viewport_height - thumb_height) as f32) as u16;
for y in 0..viewport_height {
let mut cell = Cell::new('│');
cell.fg = self.scrollbar_bg;
ctx.set(scrollbar_x, y, cell);
}
for y in thumb_position..(thumb_position + thumb_height).min(viewport_height) {
let mut cell = Cell::new('â–ˆ');
cell.fg = self.scrollbar_fg;
ctx.set(scrollbar_x, y, cell);
}
}
pub fn content_area(&self, area: Rect) -> Rect {
if self.show_scrollbar && self.content_height > area.height {
Rect {
x: area.x,
y: area.y,
width: area.width.saturating_sub(1),
height: area.height,
}
} else {
area
}
}
pub fn create_content_buffer(&self, width: u16) -> Buffer {
Buffer::new(width, self.content_height)
}
pub fn render_content(&self, ctx: &mut RenderContext, content_buffer: &Buffer) {
let area = self.content_area(ctx.area);
let viewport_height = area.height;
for y in 0..viewport_height {
let content_y = self.scroll_offset + y;
if content_y >= self.content_height {
break;
}
for x in 0..area.width {
if let Some(cell) = content_buffer.get(x, content_y) {
ctx.set(x, y, *cell);
}
}
}
self.render_scrollbar(ctx);
}
}
impl Default for ScrollView {
fn default() -> Self {
Self::new()
}
}
impl View for ScrollView {
crate::impl_view_meta!("ScrollView");
fn render(&self, ctx: &mut RenderContext) {
let _area = self.apply_constraints(ctx.area);
self.render_scrollbar(ctx);
}
}
impl_styled_view!(ScrollView);
impl_props_builders!(ScrollView);
pub fn scroll_view() -> ScrollView {
ScrollView::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scroll_view_new() {
let s = ScrollView::new();
assert_eq!(s.offset(), 0);
assert_eq!(s.content_height, 0);
assert!(s.show_scrollbar);
}
#[test]
fn test_scroll_view_scroll_down_up() {
let mut s = ScrollView::new().content_height(100);
s.scroll_down(10, 20);
assert_eq!(s.offset(), 10);
s.scroll_up(5);
assert_eq!(s.offset(), 5);
}
#[test]
fn test_scroll_view_scroll_bounds() {
let mut s = ScrollView::new().content_height(50);
s.scroll_down(100, 20); assert_eq!(s.offset(), 30); s.scroll_up(100); assert_eq!(s.offset(), 0);
}
#[test]
fn test_scroll_view_page_down_up() {
let mut s = ScrollView::new().content_height(100);
s.page_down(20);
assert_eq!(s.offset(), 19); s.page_up(20);
assert_eq!(s.offset(), 0);
}
#[test]
fn test_scroll_view_scroll_to_top_bottom() {
let mut s = ScrollView::new().content_height(100);
s.scroll_to_bottom(20);
assert_eq!(s.offset(), 80);
s.scroll_to_top();
assert_eq!(s.offset(), 0);
}
#[test]
fn test_scroll_view_set_offset() {
let mut s = ScrollView::new().content_height(50);
s.set_offset(25, 20);
assert_eq!(s.offset(), 25);
s.set_offset(100, 20); assert_eq!(s.offset(), 30);
}
#[test]
fn test_scroll_view_is_scrollable() {
let s = ScrollView::new().content_height(50);
assert!(s.is_scrollable(20));
assert!(!s.is_scrollable(50));
assert!(!s.is_scrollable(60));
}
#[test]
fn test_scroll_view_percentage() {
let mut s = ScrollView::new().content_height(100);
assert_eq!(s.scroll_percentage(20), 0.0);
s.scroll_to_bottom(20);
assert_eq!(s.scroll_percentage(20), 1.0);
}
#[test]
fn test_scroll_view_handle_key() {
use crate::event::Key;
let mut s = ScrollView::new().content_height(100);
assert!(s.handle_key(&Key::Down, 20));
assert_eq!(s.offset(), 1);
assert!(s.handle_key(&Key::PageDown, 20));
assert!(s.offset() > 1);
assert!(s.handle_key(&Key::Home, 20));
assert_eq!(s.offset(), 0);
assert!(!s.handle_key(&Key::Char('x'), 20));
}
#[test]
fn test_scroll_view_content_area() {
let s = ScrollView::new().content_height(50);
let area = Rect::new(0, 0, 40, 20);
let content = s.content_area(area);
assert_eq!(content.width, 39);
}
#[test]
fn test_scroll_view_content_area_no_scrollbar_needed() {
let s = ScrollView::new().content_height(10);
let area = Rect::new(0, 0, 40, 20);
let content = s.content_area(area);
assert_eq!(content.width, 40); }
#[test]
fn test_scroll_view_render_no_panic() {
let mut buf = Buffer::new(40, 20);
let area = Rect::new(0, 0, 40, 20);
let mut ctx = RenderContext::new(&mut buf, area);
let s = ScrollView::new().content_height(50);
s.render(&mut ctx);
}
#[test]
fn test_scroll_view_default() {
let s = ScrollView::default();
assert_eq!(s.offset(), 0);
}
#[test]
fn test_scroll_view_helper_fn() {
let s = scroll_view();
assert_eq!(s.offset(), 0);
}
}