use crate::theme::{browser, search}; 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 border_style = if self.focused { Style::new().fg(browser::FOCUSED_BORDER) } else { Style::new().fg(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); } self.render_tree(frame, list_area); } fn render_search(&self, frame: &mut Frame, area: Rect) { let style = if self.search_active { Style::new().fg(search::ACTIVE) } else { Style::new().fg(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) { 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(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} ", browser::FOLDER_ICON) } TreeLineKind::Root { expanded: false } | TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", browser::FOLDER_ICON), TreeLineKind::File => ("\u{266A} ", browser::FILE_ICON), }; let label_style = if is_cursor && self.focused { Style::new().fg(browser::SELECTED).add_modifier(Modifier::BOLD) } else if is_cursor { Style::new().fg(browser::FILE) } else { match entry.kind { TreeLineKind::Root { .. } => { Style::new().fg(browser::ROOT).add_modifier(Modifier::BOLD) } TreeLineKind::Folder { .. } => Style::new().fg(browser::DIRECTORY), TreeLineKind::File => Style::default(), } }; let icon_style = if is_cursor && self.focused { label_style } else { Style::new().fg(icon_color) }; let spans = vec![ Span::raw(indent), Span::styled(icon, icon_style), Span::styled(&entry.label, label_style), ]; lines.push(Line::from(spans)); } frame.render_widget(Paragraph::new(lines), area); } }