use std::{
cell::Cell,
hash::{DefaultHasher, Hash, Hasher},
marker::PhantomData,
rc::Rc,
};
use crate::{
buffer::Buffer,
geometry::Vec2Range,
prelude::{Direction, Rect, Vec2},
style::Style,
widgets::{Element, LayoutNode, Widget},
};
#[derive(Debug, Clone, PartialEq)]
pub struct Scrollbar<M: 'static = ()> {
track_char: char,
track_style: Style,
thumb_char: char,
thumb_style: Style,
direction: Direction,
state: Rc<Cell<ScrollbarState>>,
_marker: PhantomData<M>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct ScrollbarState {
pub content_len: usize,
pub offset: usize,
}
impl<M> Scrollbar<M> {
#[must_use]
pub fn vertical(state: Rc<Cell<ScrollbarState>>) -> Self {
Self {
state,
..Default::default()
}
}
#[must_use]
pub fn horizontal(state: Rc<Cell<ScrollbarState>>) -> Self {
Self {
direction: Direction::Horizontal,
track_char: '─',
thumb_char: '━',
state,
..Default::default()
}
}
#[must_use]
pub fn track_char(mut self, track_char: char) -> Self {
self.track_char = track_char;
self
}
#[must_use]
pub fn track_style<T>(mut self, style: T) -> Self
where
T: Into<Style>,
{
self.track_style = style.into();
self
}
#[must_use]
pub fn thumb_char(mut self, thumb_char: char) -> Self {
self.thumb_char = thumb_char;
self
}
#[must_use]
pub fn thumb_style<T>(mut self, style: T) -> Self
where
T: Into<Style>,
{
self.thumb_style = style.into();
self
}
#[must_use]
pub fn direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
pub fn offset(&self, offset: usize) {
self.state.set(self.state.get().offset(offset));
}
pub fn content_len(&self, content_len: usize) {
self.state.set(self.state.get().content_len(content_len));
}
pub fn get_state(&self) -> ScrollbarState {
self.state.get()
}
}
impl ScrollbarState {
#[must_use]
pub fn new(offset: usize) -> Self {
Self {
content_len: 0,
offset,
}
}
#[must_use]
pub fn offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
#[must_use]
pub fn content_len(mut self, content_len: usize) -> Self {
self.content_len = content_len;
self
}
pub fn next(self) -> Self {
self.advance(1)
}
pub fn advance(mut self, n: usize) -> Self {
self.offset =
(self.offset + n).min(self.content_len.saturating_sub(1));
self
}
pub fn prev(self) -> Self {
self.retreat(1)
}
pub fn retreat(mut self, n: usize) -> Self {
self.offset = self.offset.saturating_sub(n);
self
}
pub fn first(mut self) -> Self {
self.offset = 0;
self
}
pub fn last(mut self) -> Self {
self.offset = self.content_len.saturating_sub(1);
self
}
}
impl<M: Clone + 'static> Widget<M> for Scrollbar<M> {
fn render(&self, buffer: &mut Buffer, layout: &LayoutNode) {
let rect = layout.area;
match self.direction {
Direction::Vertical => self.ver_render(buffer, &rect),
Direction::Horizontal => self.hor_render(buffer, &rect),
}
}
fn height(&self, size: &Vec2) -> usize {
let total = self.state.get().content_len;
match self.direction {
Direction::Vertical => size.y,
Direction::Horizontal => (total > size.y) as usize,
}
}
fn width(&self, size: &Vec2) -> usize {
let total = self.state.get().content_len;
match self.direction {
Direction::Vertical => (total > size.x) as usize,
Direction::Horizontal => size.x,
}
}
fn layout_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.direction.hash(&mut hasher);
hasher.finish()
}
}
impl<M> Scrollbar<M> {
fn ver_render(&self, buffer: &mut Buffer, rect: &Rect) {
let Some((size, pos)) = self.calc_thumb(rect.height()) else {
return;
};
self.render_track(
buffer,
rect.pos().to(Vec2::new(rect.x() + 1, rect.bottom() + 1)),
);
let start = Vec2::new(rect.x(), rect.y() + pos);
let end = Vec2::new(rect.x() + 1, rect.y() + pos + size);
self.render_thumb(buffer, start.to(end));
}
fn hor_render(&self, buffer: &mut Buffer, rect: &Rect) {
let Some((size, pos)) = self.calc_thumb(rect.width()) else {
return;
};
self.render_track(
buffer,
rect.pos().to(Vec2::new(rect.right() + 1, rect.y() + 1)),
);
let start = Vec2::new(rect.x() + pos, rect.y());
let end = Vec2::new(rect.x() + pos + size, rect.y() + 1);
self.render_thumb(buffer, start.to(end));
}
fn calc_thumb(&self, visible: usize) -> Option<(usize, usize)> {
let total = self.state.get().content_len;
if total <= visible || visible == 0 {
self.state.set(self.state.get().offset(0));
return None;
}
let mut thumb_size =
((visible * visible) as f64 / total as f64).round() as usize;
thumb_size = thumb_size.max(1);
let max_offset = total - visible;
let mut state = self.state.get();
if state.offset > max_offset {
state = state.offset(max_offset);
self.state.set(state);
}
let pos = (state.offset as f64 / max_offset as f64
* (visible - thumb_size) as f64)
.round() as usize;
Some((thumb_size, pos))
}
fn render_track(&self, buffer: &mut Buffer, pos_range: Vec2Range) {
for pos in pos_range {
buffer[pos].char(self.track_char).style(self.track_style);
}
}
fn render_thumb(&self, buffer: &mut Buffer, pos_range: Vec2Range) {
for pos in pos_range {
buffer[pos].char(self.thumb_char).style(self.thumb_style);
}
}
}
impl<M> Default for Scrollbar<M> {
fn default() -> Self {
Self {
track_char: '│',
track_style: Default::default(),
thumb_char: '┃',
thumb_style: Default::default(),
direction: Default::default(),
state: Default::default(),
_marker: PhantomData,
}
}
}
impl<M: Clone + 'static> From<Scrollbar<M>> for Box<dyn Widget<M>> {
fn from(value: Scrollbar<M>) -> Self {
Box::new(value)
}
}
impl<M: Clone + 'static> From<Scrollbar<M>> for Element<M> {
fn from(value: Scrollbar<M>) -> Self {
Element::new(value)
}
}