Files
Cagire/crates/ratatui/src/sample_browser.rs
2026-02-08 01:33:50 +01:00

178 lines
5.6 KiB
Rust

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<Line> = 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);
}
}