use super::ScrollViewState;
use ratatui::{
buffer::Buffer,
layout::{Position, Rect, Size},
widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget},
};
use ratatui_kit_macros::Props;
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ScrollbarVisibility {
#[default]
Automatic,
Always,
Never,
}
#[derive(Props, Clone, Hash)]
pub struct Scrollbars<'a> {
pub vertical_scrollbar_visibility: ScrollbarVisibility,
pub horizontal_scrollbar_visibility: ScrollbarVisibility,
pub vertical_scrollbar: Scrollbar<'a>,
pub horizontal_scrollbar: Scrollbar<'a>,
pub over_border: bool,
}
impl Default for Scrollbars<'_> {
fn default() -> Self {
Self {
vertical_scrollbar_visibility: ScrollbarVisibility::Automatic,
horizontal_scrollbar_visibility: ScrollbarVisibility::Automatic,
vertical_scrollbar: Scrollbar::new(ScrollbarOrientation::VerticalRight),
horizontal_scrollbar: Scrollbar::new(ScrollbarOrientation::HorizontalBottom),
over_border: true,
}
}
}
impl Scrollbars<'_> {
fn axis_show(visibility: ScrollbarVisibility, space: i32) -> bool {
match visibility {
ScrollbarVisibility::Always => true,
ScrollbarVisibility::Never => false,
ScrollbarVisibility::Automatic => space < 0,
}
}
pub(crate) fn content_area(&self, inner: Rect, content: Size, ring: bool) -> Rect {
if ring {
return inner;
}
let horizontal_space = inner.width as i32 - content.width as i32;
let vertical_space = inner.height as i32 - content.height as i32;
let (show_horizontal, show_vertical) =
self.visible_scrollbars(horizontal_space, vertical_space);
Rect {
width: inner.width.saturating_sub(show_vertical as u16),
height: inner.height.saturating_sub(show_horizontal as u16),
..inner
}
}
fn render_visible_area(&self, dst: Rect, buf: &mut Buffer, src: Rect, scroll_buffer: &Buffer) {
for (src_row, dst_row) in src.rows().zip(dst.rows()) {
for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
buf[dst_col] = scroll_buffer[src_col].clone();
}
}
}
fn render_scrollbar(
scrollbar: &Scrollbar<'_>,
orientation: ScrollbarOrientation,
area: Rect,
buf: &mut Buffer,
position: u16,
content_len: u16,
viewport_len: u16,
) {
let hidden = content_len.saturating_sub(viewport_len);
let mut scrollbar_state = ScrollbarState::new(hidden as usize).position(position as usize);
scrollbar
.clone()
.orientation(orientation)
.render(area, buf, &mut scrollbar_state);
}
pub fn visible_scrollbars(&self, horizontal_space: i32, vertical_space: i32) -> (bool, bool) {
type V = ScrollbarVisibility;
match (
self.horizontal_scrollbar_visibility,
self.vertical_scrollbar_visibility,
) {
(V::Always, V::Always) => (true, true),
(V::Never, V::Never) => (false, false),
(V::Always, V::Never) => (true, false),
(V::Never, V::Always) => (false, true),
(V::Automatic, V::Never) => (horizontal_space < 0, false),
(V::Never, V::Automatic) => (false, vertical_space < 0),
(V::Always, V::Automatic) => (true, vertical_space <= 0),
(V::Automatic, V::Always) => (horizontal_space <= 0, true),
(V::Automatic, V::Automatic) => {
if horizontal_space >= 0 && vertical_space >= 0 {
(false, false)
} else if horizontal_space < 0 && vertical_space < 0 {
(true, true)
} else if horizontal_space > 0 && vertical_space < 0 {
(false, true)
} else if horizontal_space < 0 && vertical_space > 0 {
(true, false)
} else {
(true, true)
}
}
}
}
pub(crate) fn ring(&self, outer: Rect, inner: Rect) -> bool {
self.over_border && inner.right() < outer.right() && inner.bottom() < outer.bottom()
}
pub fn render_ref(
&self,
outer: Rect,
inner: Rect,
buf: &mut Buffer,
state: &mut ScrollViewState,
scroll_buffer: &Buffer,
) {
let content = scroll_buffer.area.as_size();
let ring = self.ring(outer, inner);
let horizontal_space = inner.width as i32 - content.width as i32;
let vertical_space = inner.height as i32 - content.height as i32;
let (show_horizontal, show_vertical) = if ring {
(
Self::axis_show(self.horizontal_scrollbar_visibility, horizontal_space),
Self::axis_show(self.vertical_scrollbar_visibility, vertical_space),
)
} else {
self.visible_scrollbars(horizontal_space, vertical_space)
};
let viewport = if ring {
inner.as_size()
} else {
Size::new(
inner.width.saturating_sub(show_vertical as u16),
inner.height.saturating_sub(show_horizontal as u16),
)
};
let x = state
.offset
.x
.min(content.width.saturating_sub(viewport.width));
let y = state
.offset
.y
.min(content.height.saturating_sub(viewport.height));
state.offset = Position::new(x, y);
state.size = Some(content);
state.page_size = Some(viewport);
let src = Rect::new(x, y, viewport.width, viewport.height).intersection(scroll_buffer.area);
let dst = Rect::new(inner.x, inner.y, viewport.width, viewport.height);
self.render_visible_area(dst, buf, src, scroll_buffer);
if show_vertical {
let area = Rect::new(inner.x + viewport.width, inner.y, 1, viewport.height);
Self::render_scrollbar(
&self.vertical_scrollbar,
ScrollbarOrientation::VerticalRight,
area,
buf,
y,
content.height,
viewport.height,
);
}
if show_horizontal {
let area = Rect::new(inner.x, inner.y + viewport.height, viewport.width, 1);
Self::render_scrollbar(
&self.horizontal_scrollbar,
ScrollbarOrientation::HorizontalBottom,
area,
buf,
x,
content.width,
viewport.width,
);
}
}
}