use crate::theme; use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::Paragraph; use ratatui::Frame; 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 = 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); } }