use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Rect},
style::Style,
widgets::{Block, Borders, List, ListItem, ListState, StatefulWidget, Widget},
};
use crate::{default_scrollbar, default_scrollbar_state, BORDER_GRAY, DEFAULT_HIGHLIGHT, HIGHLIGHT_SYMBOL};
#[derive(Clone)]
pub struct ScrollListState {
pub list_state: ListState,
pub follow: bool,
}
impl Default for ScrollListState {
fn default() -> Self {
Self {
list_state: ListState::default(),
follow: true,
}
}
}
impl ScrollListState {
pub fn select(&mut self, index: Option<usize>) {
self.list_state.select(index);
}
pub fn next(&mut self, item_count: usize) {
let i = self.list_state.selected().map_or(0, |s| s + 1);
let max = item_count.saturating_sub(1);
self.list_state.select(Some(i.min(max)));
}
pub fn previous(&mut self) {
let i = self.list_state.selected().map_or(0, |s| s.saturating_sub(1));
self.list_state.select(Some(i));
}
pub fn last(&mut self, item_count: usize) {
self.list_state.select(Some(item_count.saturating_sub(1)));
}
}
pub struct ScrollList<'a> {
items: Vec<ListItem<'a>>,
highlight_style: Style,
highlight_symbol: String,
borders: Borders,
border_style: Style,
}
impl<'a> ScrollList<'a> {
pub fn new(items: Vec<String>) -> Self {
Self {
items: items
.into_iter()
.map(|s| ListItem::new(s))
.collect(),
highlight_style: DEFAULT_HIGHLIGHT,
highlight_symbol: HIGHLIGHT_SYMBOL.to_string(),
borders: Borders::NONE,
border_style: Style::new().fg(BORDER_GRAY),
}
}
pub fn new_styled(items: Vec<(String, Style)>) -> Self {
Self {
items: items
.into_iter()
.map(|(s, style)| ListItem::new(s).style(style))
.collect(),
highlight_style: DEFAULT_HIGHLIGHT,
highlight_symbol: HIGHLIGHT_SYMBOL.to_string(),
borders: Borders::NONE,
border_style: Style::new().fg(BORDER_GRAY),
}
}
pub fn new_list(items: Vec<ListItem<'a>>) -> Self {
Self {
items,
highlight_style: DEFAULT_HIGHLIGHT,
highlight_symbol: HIGHLIGHT_SYMBOL.to_string(),
borders: Borders::NONE,
border_style: Style::new().fg(BORDER_GRAY),
}
}
pub fn style(mut self, style: Style) -> Self {
for item in &mut self.items {
*item = item.clone().style(style);
}
self
}
pub fn highlight_style(mut self, style: Style) -> Self {
self.highlight_style = style;
self
}
pub fn highlight_symbol(mut self, symbol: &str) -> Self {
self.highlight_symbol = symbol.to_string();
self
}
pub fn borders(mut self, borders: Borders) -> Self {
self.borders = borders;
self
}
pub fn border_style(mut self, style: Style) -> Self {
self.border_style = style;
self
}
}
impl StatefulWidget for ScrollList<'_> {
type State = ScrollListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if state.follow && !self.items.is_empty() {
state
.list_state
.select(Some(self.items.len().saturating_sub(1)));
}
let inner = if self.borders != Borders::NONE {
let block = Block::bordered()
.borders(self.borders)
.border_style(self.border_style);
let inner = block.inner(area);
block.render(area, buf);
inner
} else {
area
};
let hchunks = Layout::horizontal([
Constraint::Min(1),
Constraint::Length(1),
])
.split(inner);
let list_area = hchunks[0];
let scrollbar_area = hchunks[1];
let total = self.items.len();
let mut list = List::new(self.items)
.highlight_style(self.highlight_style)
.scroll_padding(1);
if !self.highlight_symbol.is_empty() {
list = list.highlight_symbol(self.highlight_symbol);
}
StatefulWidget::render(list, list_area, buf, &mut state.list_state);
if state.list_state.selected().is_none() && total > 0 {
state.list_state.select(Some(0));
}
if total > list_area.height as usize {
let visible = list_area.height as usize;
let max_offset = total.saturating_sub(visible);
let offset = state.list_state.offset();
let position = if max_offset > 0 {
offset * (total - 1) / max_offset
} else {
0
};
let mut scrollbar_state = default_scrollbar_state(total, position);
StatefulWidget::render(
default_scrollbar(),
scrollbar_area,
buf,
&mut scrollbar_state,
);
}
}
}
#[cfg(test)]
mod tests;