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),
}
}
}
pub(crate) struct ScrollbarLayout {
pub show_horizontal: bool,
pub show_vertical: bool,
pub visible_area: Rect,
}
impl ScrollBars<'_> {
pub(crate) fn layout_for(&self, area: Rect, content_size: Size) -> ScrollbarLayout {
let horizontal_space = area.width as i32 - content_size.width as i32;
let vertical_space = area.height as i32 - content_size.height as i32;
let (show_horizontal, show_vertical) =
self.visible_scrollbars(horizontal_space, vertical_space);
ScrollbarLayout {
show_horizontal,
show_vertical,
visible_area: Rect {
width: area.width.saturating_sub(show_vertical as u16),
height: area.height.saturating_sub(show_horizontal as u16),
..area
},
}
}
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 layout = self.layout_for(area, 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;
}
if layout.show_horizontal {
let width = area.width.saturating_sub(layout.show_vertical as u16);
let render_area = Rect { width, ..area };
self.render_horizontal_scrollbar(render_area, buf, state, size);
}
if layout.show_vertical {
let height = area.height.saturating_sub(layout.show_horizontal as u16);
let render_area = Rect { height, ..area };
self.render_vertical_scrollbar(render_area, buf, state, size);
}
Rect::new(
state.offset.x,
state.offset.y,
layout.visible_area.width,
layout.visible_area.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);
}
}