use ratatui::{layout::Rect, widgets::StatefulWidget};
use std::ops::Range;
pub struct ScrollbarState {
content_length: usize,
position: usize,
viewport_content_length: usize,
}
impl ScrollbarState {
pub fn new(content_length: usize) -> Self {
Self {
content_length,
position: 0,
viewport_content_length: 0,
}
}
pub fn viewport_content_length(self, viewport_content_length: usize) -> Self {
Self {
viewport_content_length,
..self
}
}
pub fn position(self, position: usize) -> Self {
Self { position, ..self }
}
fn thumb(&self) -> Thumb<usize> {
Thumb {
start: self.position.min(
self.content_length
.saturating_sub(self.viewport_content_length),
),
length: self.viewport_content_length.min(self.content_length),
}
}
fn to_subpixels(&self, track_height: u16) -> impl Fn(usize) -> Subpixel {
move |u| {
let mut u = (u as f64) / (self.content_length as f64);
u *= (track_height as f64) * 8.0;
(u.round() as u32).into()
}
}
}
pub struct Scrollbar;
impl StatefulWidget for Scrollbar {
type State = ScrollbarState;
fn render(self, track: Rect, buf: &mut ratatui::buffer::Buffer, state: &mut Self::State) {
use ratatui::style::Style;
if track.width == 0 {
return;
}
let Some(max_thumb) = track.height.checked_sub(1).map(Subpixel::from) else {
return;
};
let thumb = state.thumb().map(state.to_subpixels(track.height));
let thumb = Range::from(Thumb {
start: thumb.start.min(max_thumb),
length: thumb.length.max(1u16.into()),
});
buf[(track.x, track.y + thumb.start.pixel)]
.set_char(subpixels_char_top(thumb.start.subpixel));
for i in (thumb.start.pixel + 1)..thumb.end.pixel {
buf[(track.x, track.y + i)].set_char('\u{2588}');
}
if thumb.end.subpixel > 0 {
buf[(track.x, track.y + thumb.end.pixel)]
.set_char(subpixels_char_bottom(thumb.end.subpixel));
buf[(track.x, track.y + thumb.end.pixel)].set_style(Style::default().reversed());
}
}
}
struct Thumb<T> {
start: T,
length: T,
}
impl<T> Thumb<T> {
fn map<U>(self, f: impl Fn(T) -> U) -> Thumb<U> {
Thumb {
start: f(self.start),
length: f(self.length),
}
}
}
impl<T: Copy + std::ops::Add<Output = T>> From<Thumb<T>> for Range<T> {
fn from(thumb: Thumb<T>) -> Self {
Self {
start: thumb.start,
end: thumb.start + thumb.length,
}
}
}
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
struct Subpixel {
pixel: u16,
subpixel: u16,
}
impl From<u32> for Subpixel {
fn from(total_subpixels: u32) -> Self {
Self {
pixel: (total_subpixels / 8) as u16,
subpixel: (total_subpixels % 8) as u16,
}
}
}
impl From<u16> for Subpixel {
fn from(pixels: u16) -> Self {
Self {
pixel: pixels,
subpixel: 0,
}
}
}
impl std::ops::Add for Subpixel {
type Output = Self;
fn add(self, rhs: Subpixel) -> Self::Output {
let total_subpixels = self.subpixel + rhs.subpixel;
Self {
pixel: self.pixel + rhs.pixel + total_subpixels / 8,
subpixel: total_subpixels % 8,
}
}
}
fn subpixels_char_top(subpixels: u16) -> char {
match subpixels {
0 => '\u{2588}',
1 => '\u{2587}',
2 => '\u{2586}',
3 => '\u{2585}',
4 => '\u{2584}',
5 => '\u{2583}',
6 => '\u{2582}',
7 => '\u{2581}',
_ => '?',
}
}
fn subpixels_char_bottom(subpixels: u16) -> char {
match subpixels {
0 => '\u{2588}',
1 => '\u{2587}',
2 => '\u{2586}',
3 => '\u{2585}',
4 => '\u{2584}',
5 => '\u{2583}',
6 => '\u{2582}',
7 => '\u{2581}',
_ => '?',
}
}