109 lines
3.2 KiB
Rust
109 lines
3.2 KiB
Rust
//! Scrollable single-select list widget with cursor highlight.
|
|
|
|
use crate::theme;
|
|
use ratatui::layout::Rect;
|
|
use ratatui::style::{Modifier, Style};
|
|
use ratatui::text::{Line, Span};
|
|
use ratatui::widgets::Paragraph;
|
|
use ratatui::Frame;
|
|
|
|
/// Scrollable list with a highlighted cursor and selected-item marker.
|
|
pub struct ListSelect<'a> {
|
|
items: &'a [String],
|
|
selected: usize,
|
|
cursor: usize,
|
|
focused: bool,
|
|
visible_count: usize,
|
|
scroll_offset: usize,
|
|
}
|
|
|
|
impl<'a> ListSelect<'a> {
|
|
pub fn new(items: &'a [String], selected: usize, cursor: usize) -> Self {
|
|
Self {
|
|
items,
|
|
selected,
|
|
cursor,
|
|
focused: false,
|
|
visible_count: 5,
|
|
scroll_offset: 0,
|
|
}
|
|
}
|
|
|
|
pub fn focused(mut self, focused: bool) -> Self {
|
|
self.focused = focused;
|
|
self
|
|
}
|
|
|
|
pub fn scroll_offset(mut self, offset: usize) -> Self {
|
|
self.scroll_offset = offset;
|
|
self
|
|
}
|
|
|
|
pub fn visible_count(mut self, n: usize) -> Self {
|
|
self.visible_count = n;
|
|
self
|
|
}
|
|
|
|
pub fn height(&self) -> u16 {
|
|
let item_lines = self.items.len().min(self.visible_count) as u16;
|
|
if self.items.len() > self.visible_count {
|
|
item_lines + 1
|
|
} else {
|
|
item_lines
|
|
}
|
|
}
|
|
|
|
pub fn render(self, frame: &mut Frame, area: Rect) {
|
|
let colors = theme::get();
|
|
let cursor_style = Style::new().fg(colors.hint.key).add_modifier(Modifier::BOLD);
|
|
let selected_style = Style::new().fg(colors.ui.accent);
|
|
let normal_style = Style::default();
|
|
let indicator_style = Style::new().fg(colors.ui.text_dim);
|
|
|
|
let visible_end = (self.scroll_offset + self.visible_count).min(self.items.len());
|
|
let has_above = self.scroll_offset > 0;
|
|
let has_below = visible_end < self.items.len();
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
|
|
for i in self.scroll_offset..visible_end {
|
|
let name = &self.items[i];
|
|
let is_cursor = self.focused && i == self.cursor;
|
|
let is_selected = i == self.selected;
|
|
|
|
let style = if is_cursor {
|
|
cursor_style
|
|
} else if is_selected {
|
|
selected_style
|
|
} else {
|
|
normal_style
|
|
};
|
|
|
|
let prefix = if is_selected { "x " } else { " " };
|
|
let mut spans = vec![
|
|
Span::styled(prefix.to_string(), style),
|
|
Span::styled(name.clone(), style),
|
|
];
|
|
|
|
if has_above && i == self.scroll_offset {
|
|
spans.push(Span::styled(" ▲", indicator_style));
|
|
} else if has_below && i == visible_end - 1 {
|
|
spans.push(Span::styled(" ▼", indicator_style));
|
|
}
|
|
|
|
lines.push(Line::from(spans));
|
|
}
|
|
|
|
if self.items.len() > self.visible_count {
|
|
let position = self.cursor + 1;
|
|
let total = self.items.len();
|
|
lines.push(Line::from(Span::styled(
|
|
format!(" ({position}/{total})"),
|
|
indicator_style,
|
|
)));
|
|
}
|
|
|
|
frame.render_widget(Paragraph::new(lines), area);
|
|
}
|
|
}
|