use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
use crate::{block::{focusable_block, render_scrollbar}, Theme};
pub struct ListState {
pub selected: usize,
offset: usize,
}
impl ListState {
pub fn new() -> Self {
Self { selected: 0, offset: 0 }
}
pub fn select_next(&mut self, item_count: usize) {
if item_count == 0 {
return;
}
self.selected = (self.selected + 1) % item_count;
}
pub fn select_prev(&mut self) {
if self.selected == 0 {
return;
}
self.selected = self.selected.saturating_sub(1);
}
pub fn selected(&self) -> usize {
self.selected
}
pub fn offset(&self) -> usize {
self.offset
}
fn clamp_offset(&mut self, visible_height: usize) {
if visible_height == 0 {
return;
}
if self.selected < self.offset {
self.offset = self.selected;
} else if self.selected >= self.offset + visible_height {
self.offset = self.selected - visible_height + 1;
}
}
}
impl Default for ListState {
fn default() -> Self {
Self::new()
}
}
pub struct ListItem {
pub primary: String,
pub secondary: Option<String>,
}
pub fn render_list(
f: &mut Frame,
area: Rect,
title: &str,
shortcut: Option<u8>,
items: &[ListItem],
state: &mut ListState,
focused: bool,
theme: &Theme,
) {
let block = focusable_block(title, shortcut, focused, theme);
let inner = block.inner(area);
f.render_widget(block, area);
render_scrollbar(f, area, items.len(), state.offset);
let visible_height = inner.height as usize;
if !items.is_empty() && state.selected >= items.len() {
state.selected = items.len() - 1;
}
state.clamp_offset(visible_height);
if items.is_empty() || visible_height == 0 {
return;
}
let visible_items = items
.iter()
.enumerate()
.skip(state.offset)
.take(visible_height);
for (idx, item) in visible_items {
let row_y = inner.y + (idx - state.offset) as u16;
let row_area = Rect {
x: inner.x,
y: row_y,
width: inner.width,
height: 1,
};
let is_selected = idx == state.selected;
let row_style = if is_selected { theme.selection } else { theme.body };
match &item.secondary {
None => {
let para = Paragraph::new(Line::from(Span::styled(
item.primary.clone(),
row_style,
)));
f.render_widget(para, row_area);
}
Some(sec) => {
let sec_width = (sec.chars().count() as u16).min(inner.width.saturating_sub(1));
let prim_width = inner.width.saturating_sub(sec_width);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(prim_width),
Constraint::Length(sec_width),
])
.split(row_area);
let prim_style = if is_selected { theme.selection } else { theme.body };
let sec_style = if is_selected { theme.selection } else { theme.hint };
let prim_para = Paragraph::new(Line::from(Span::styled(
item.primary.clone(),
prim_style,
)));
let sec_para = Paragraph::new(Line::from(Span::styled(
sec.clone(),
sec_style,
)))
.alignment(ratatui::layout::Alignment::Right);
f.render_widget(prim_para, chunks[0]);
f.render_widget(sec_para, chunks[1]);
}
}
}
}