178 lines
5.6 KiB
Rust
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);
|
|
}
|
|
}
|