use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
text::Span,
widgets::{List, ListItem, ListState, StatefulWidget, Widget},
};
#[derive(Debug)]
pub struct SelectableList<T> {
items: Vec<T>,
cursor: usize,
}
impl<T> SelectableList<T> {
pub fn new(items: Vec<T>) -> Self {
Self { items, cursor: 0 }
}
pub fn set_items(&mut self, items: Vec<T>) {
self.items = items;
self.cursor = 0;
}
#[allow(dead_code)]
pub fn items(&self) -> &[T] {
&self.items
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
#[allow(dead_code)]
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn selected(&self) -> Option<&T> {
self.items.get(self.cursor)
}
pub fn move_up(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn move_down(&mut self) {
if self.cursor + 1 < self.items.len() {
self.cursor += 1;
}
}
pub fn widget<F>(&self, render_item: F) -> SelectableListWidget<'_, T, F>
where
F: Fn(&T, bool) -> ListItem<'static>,
{
SelectableListWidget {
list: self,
render_item,
}
}
}
pub struct SelectableListWidget<'a, T, F> {
list: &'a SelectableList<T>,
render_item: F,
}
impl<T, F> Widget for SelectableListWidget<'_, T, F>
where
F: Fn(&T, bool) -> ListItem<'static>,
{
fn render(self, area: Rect, buf: &mut Buffer) {
let height = area.height as usize;
let cursor = self.list.cursor;
if self.list.items.is_empty() {
Widget::render(
List::new(vec![ListItem::new(Span::styled(
" (empty)",
Style::default().fg(Color::DarkGray),
))]),
area,
buf,
);
return;
}
let scroll = if cursor >= height {
cursor + 1 - height
} else {
0
};
let items: Vec<ListItem<'static>> = self
.list
.items
.iter()
.enumerate()
.skip(scroll)
.take(height)
.map(|(i, item)| (self.render_item)(item, i == cursor))
.collect();
let mut state = ListState::default();
state.select(Some(cursor.saturating_sub(scroll)));
StatefulWidget::render(List::new(items), area, buf, &mut state);
}
}