use super::ScrollViewState;
use ratatui::{
buffer::Buffer,
layout::{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>,
}
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),
}
}
}
impl ScrollBars<'_> {
fn render_visible_area(
&self,
area: Rect,
buf: &mut Buffer,
visible_area: Rect,
scroll_buffer: &Buffer,
) {
for (src_row, dst_row) in visible_area.rows().zip(area.rows()) {
for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
buf[dst_col] = scroll_buffer[src_col].clone();
}
}
}
fn render_vertical_scrollbar(
&self,
area: Rect,
buf: &mut Buffer,
state: &ScrollViewState,
scroll_size: Size,
) {
let scrollbar_height = scroll_size.height.saturating_sub(area.height);
let mut scrollbar_state =
ScrollbarState::new(scrollbar_height as usize).position(state.offset.y as usize);
self.vertical_scrollbar
.clone()
.render(area, buf, &mut scrollbar_state);
}
fn render_horizontal_scrollbar(
&self,
area: Rect,
buf: &mut Buffer,
state: &ScrollViewState,
scroll_size: Size,
) {
let scrollbar_width = scroll_size.width.saturating_sub(area.width);
let mut scrollbar_state =
ScrollbarState::new(scrollbar_width as usize).position(state.offset.x as usize);
self.horizontal_scrollbar
.clone()
.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)
}
}
}
}
fn render_scrollbars(
&self,
area: Rect,
buf: &mut Buffer,
state: &mut ScrollViewState,
scroll_buffer: &Buffer,
) -> Rect {
let size: ratatui::prelude::Size = scroll_buffer.area.as_size();
let horizontal_space = area.width as i32 - size.width as i32;
let vertical_space = area.height as i32 - size.height as i32;
if horizontal_space > 0 {
state.offset.x = 0;
}
if vertical_space > 0 {
state.offset.y = 0;
}
let (show_horizontal, show_vertical) =
self.visible_scrollbars(horizontal_space, vertical_space);
let new_height = if show_horizontal {
let width = area.width.saturating_sub(show_vertical as u16);
let render_area = Rect { width, ..area };
self.render_horizontal_scrollbar(render_area, buf, state, size);
area.height.saturating_sub(1)
} else {
area.height
};
let new_width = if show_vertical {
let height = area.height.saturating_sub(show_horizontal as u16);
let render_area = Rect { height, ..area };
self.render_vertical_scrollbar(render_area, buf, state, size);
area.width.saturating_sub(1)
} else {
area.width
};
Rect::new(state.offset.x, state.offset.y, new_width, new_height)
}
pub fn render_ref(
&self,
area: Rect,
buf: &mut Buffer,
state: &mut ScrollViewState,
scroll_buffer: &Buffer,
) {
let (mut x, mut y) = state.offset.into();
let max_x_offset = scroll_buffer.area.width.saturating_sub(area.width);
let max_y_offset = scroll_buffer.area.height.saturating_sub(area.height);
x = x.min(max_x_offset);
y = y.min(max_y_offset);
state.offset = (x, y).into();
state.size = Some(scroll_buffer.area.as_size());
state.page_size = Some(area.into());
let visible_area = self
.render_scrollbars(area, buf, state, scroll_buffer)
.intersection(scroll_buffer.area);
self.render_visible_area(area, buf, visible_area, scroll_buffer);
}
}