use crate::theme; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; #[derive(Clone, Copy)] pub enum TreeLineKind { Root { expanded: bool }, Folder { expanded: bool }, File, } #[derive(Clone)] pub struct TreeLine { pub depth: u8, pub kind: TreeLineKind, pub label: String, pub folder: String, pub index: usize, } pub struct SampleBrowser<'a> { entries: &'a [TreeLine], cursor: usize, scroll_offset: usize, search_query: &'a str, search_active: bool, focused: bool, } impl<'a> SampleBrowser<'a> { pub fn new(entries: &'a [TreeLine], cursor: usize) -> Self { Self { entries, cursor, scroll_offset: 0, search_query: "", search_active: false, focused: false, } } pub fn scroll_offset(mut self, offset: usize) -> Self { self.scroll_offset = offset; self } pub fn search(mut self, query: &'a str, active: bool) -> Self { self.search_query = query; self.search_active = active; self } pub fn focused(mut self, focused: bool) -> Self { self.focused = focused; self } pub fn render(self, frame: &mut Frame, area: Rect) { let colors = theme::get(); let border_style = if self.focused { Style::new().fg(colors.browser.focused_border) } else { Style::new().fg(colors.browser.unfocused_border) }; let block = Block::default() .borders(Borders::ALL) .border_style(border_style) .title(" Samples "); let inner = block.inner(area); frame.render_widget(block, area); if inner.height == 0 || inner.width == 0 { return; } let show_search = self.search_active || !self.search_query.is_empty(); let (search_area, list_area) = if show_search { let [s, l] = Layout::vertical([ Constraint::Length(1), Constraint::Fill(1), ]) .areas(inner); (Some(s), l) } else { (None, inner) }; if let Some(sa) = search_area { self.render_search(frame, sa, &colors); } self.render_tree(frame, list_area, &colors); } fn render_search(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) { let style = if self.search_active { Style::new().fg(colors.search.active) } else { Style::new().fg(colors.search.inactive) }; let cursor = if self.search_active { "_" } else { "" }; let text = format!("/{}{}", self.search_query, cursor); let line = Line::from(Span::styled(text, style)); frame.render_widget(Paragraph::new(vec![line]), area); } fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) { let height = area.height as usize; if self.entries.is_empty() { let msg = if self.search_query.is_empty() { "No samples loaded" } else { "No matches" }; let line = Line::from(Span::styled(msg, Style::new().fg(colors.browser.empty_text))); frame.render_widget(Paragraph::new(vec![line]), area); return; } let visible_end = (self.scroll_offset + height).min(self.entries.len()); let mut lines: Vec = Vec::with_capacity(height); for i in self.scroll_offset..visible_end { let entry = &self.entries[i]; let is_cursor = i == self.cursor; let indent = " ".repeat(entry.depth as usize); let (icon, icon_color) = match entry.kind { TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => { ("\u{25BC} ", colors.browser.folder_icon) } TreeLineKind::Root { expanded: false } | TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", colors.browser.folder_icon), TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon), }; let label_style = if is_cursor && self.focused { Style::new().fg(colors.browser.selected).add_modifier(Modifier::BOLD) } else if is_cursor { Style::new().fg(colors.browser.file) } else { match entry.kind { TreeLineKind::Root { .. } => { Style::new().fg(colors.browser.root).add_modifier(Modifier::BOLD) } TreeLineKind::Folder { .. } => Style::new().fg(colors.browser.directory), TreeLineKind::File => Style::default(), } }; let icon_style = if is_cursor && self.focused { label_style } else { Style::new().fg(icon_color) }; let mut spans = vec![ Span::raw(indent), Span::styled(icon, icon_style), Span::styled(&entry.label, label_style), ]; if matches!(entry.kind, TreeLineKind::File) { let idx_style = Style::new().fg(colors.browser.empty_text); spans.push(Span::styled(format!(" {}", entry.index), idx_style)); } lines.push(Line::from(spans)); } frame.render_widget(Paragraph::new(lines), area); } }