Feat: begin sample explorer overhaul
All checks were successful
Deploy Website / deploy (push) Has been skipped

This commit is contained in:
2026-03-05 00:42:39 +01:00
parent 2c8a6794a3
commit 4743c33916
5 changed files with 143 additions and 28 deletions

View File

@@ -23,6 +23,7 @@ pub struct TreeLine {
pub label: String,
pub folder: String,
pub index: usize,
pub child_count: usize,
}
/// Tree-view browser for navigating sample folders.
@@ -163,15 +164,43 @@ impl<'a> SampleBrowser<'a> {
Style::new().fg(icon_color)
};
let prefix_width = indent.len() + 2; // indent + icon
let suffix = match entry.kind {
TreeLineKind::File => format!(" {}", entry.index),
TreeLineKind::Root { expanded: false }
| TreeLineKind::Folder { expanded: false }
if entry.child_count > 0 =>
{
format!(" ({})", entry.child_count)
}
_ => String::new(),
};
let max_label = (area.width as usize)
.saturating_sub(prefix_width)
.saturating_sub(suffix.len());
let label: std::borrow::Cow<str> = if entry.label.len() > max_label && max_label > 1 {
let truncated: String = entry.label.chars().take(max_label - 1).collect();
format!("{}\u{2026}", truncated).into()
} else {
(&entry.label).into()
};
let mut spans = vec![
Span::raw(indent),
Span::styled(icon, icon_style),
Span::styled(&entry.label, label_style),
Span::styled(label, label_style),
];
if matches!(entry.kind, TreeLineKind::File) {
match entry.kind {
TreeLineKind::File => {
let idx_style = Style::new().fg(colors.browser.empty_text);
spans.push(Span::styled(format!(" {}", entry.index), idx_style));
spans.push(Span::styled(suffix, idx_style));
}
_ if !suffix.is_empty() => {
let dim_style = Style::new().fg(colors.browser.empty_text);
spans.push(Span::styled(suffix, dim_style));
}
_ => {}
}
lines.push(Line::from(spans));

View File

@@ -211,6 +211,21 @@ fn handle_scroll(ctx: &mut InputContext, col: u16, row: u16, term: Rect, up: boo
return;
}
// Scroll over side panel area
if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
let (_main_area, side_area) = panel_split(body);
if contains(side_area, col, row) {
if let Some(crate::state::SidePanel::SampleBrowser(state)) = &mut ctx.app.panel.side {
if up {
state.move_up();
} else {
state.move_down();
}
}
return;
}
}
match ctx.app.page {
Page::Main => {
if up {
@@ -356,25 +371,60 @@ fn handle_footer_click(ctx: &mut InputContext, col: u16, row: u16, footer: Rect)
// --- Body ---
fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect, kind: ClickKind) {
// Account for side panel splitting
let page_area = if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
fn panel_split(body: Rect) -> (Rect, Rect) {
if body.width >= 120 {
let panel_width = body.width * 35 / 100;
let [main, _side] =
let [main, side] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
.areas(body);
main
(main, side)
} else {
let panel_height = body.height * 40 / 100;
let [main, _side] =
let [main, side] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)])
.areas(body);
main
(main, side)
}
}
fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect, kind: ClickKind) {
use crate::state::PanelFocus;
if ctx.app.panel.visible && ctx.app.panel.side.is_some() {
let (main_area, side_area) = panel_split(body);
if contains(side_area, col, row) {
ctx.app.panel.focus = PanelFocus::Side;
return;
}
// Click on main area: defocus panel
if contains(main_area, col, row) {
if kind == ClickKind::Double {
ctx.dispatch(AppCommand::ClosePanel);
} else {
body
};
ctx.app.panel.focus = PanelFocus::Main;
}
}
// Fall through to page-specific handler with main_area
if !contains(main_area, col, row) {
return;
}
match ctx.app.page {
Page::Main => handle_main_click(ctx, col, row, main_area, kind),
Page::Patterns => handle_patterns_click(ctx, col, row, main_area, kind),
Page::Help => handle_help_click(ctx, col, row, main_area),
Page::Dict => handle_dict_click(ctx, col, row, main_area),
Page::Options => handle_options_click(ctx, col, row, main_area),
Page::Engine => handle_engine_click(ctx, col, row, main_area, kind),
Page::Script => handle_script_click(ctx, col, row, main_area),
}
return;
}
let page_area = body;
if !contains(page_area, col, row) {
return;

View File

@@ -41,7 +41,7 @@ pub(super) fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> Input
}
KeyCode::Down => {
for _ in 0..10 {
state.move_down(30);
state.move_down();
}
}
_ => {}
@@ -49,7 +49,7 @@ pub(super) fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> Input
} else {
match key.code {
KeyCode::Up | KeyCode::Char('k') => state.move_up(),
KeyCode::Down | KeyCode::Char('j') => state.move_down(30),
KeyCode::Down | KeyCode::Char('j') => state.move_down(),
KeyCode::PageUp => {
for _ in 0..20 {
state.move_up();
@@ -57,7 +57,7 @@ pub(super) fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> Input
}
KeyCode::PageDown => {
for _ in 0..20 {
state.move_down(30);
state.move_down();
}
}
KeyCode::Enter | KeyCode::Right => {
@@ -71,6 +71,10 @@ pub(super) fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> Input
.audio_tx
.load()
.send(AudioCommand::Evaluate { cmd, time: None });
ctx.dispatch(AppCommand::SetStatus(format!(
"\u{25B8} {}/{}",
folder, entry.label
)));
}
_ => state.toggle_expand(),
}

View File

@@ -1,3 +1,4 @@
use std::cell::Cell;
use std::fs;
use std::path::{Path, PathBuf};
@@ -62,12 +63,19 @@ impl SampleNode {
SampleNode::Folder { expanded, .. } => TreeLineKind::Folder { expanded: *expanded },
SampleNode::File { .. } => TreeLineKind::File,
};
let child_count = match self {
SampleNode::Root { children, .. } | SampleNode::Folder { children, .. } => {
children.iter().filter(|c| matches!(c, SampleNode::File { .. })).count()
}
SampleNode::File { .. } => 0,
};
out.push(TreeLine {
depth,
kind,
label: self.label().to_string(),
folder: parent_folder.to_string(),
index: file_index,
child_count,
});
if self.expanded() {
let folder_name = self.label();
@@ -321,6 +329,7 @@ impl SampleTree {
expanded,
} if name == target_name => {
let show_children = !collapsed && *expanded;
let file_count = children.iter().filter(|c| matches!(c, SampleNode::File { .. })).count();
out.push(TreeLine {
depth: 0,
kind: TreeLineKind::Folder {
@@ -329,6 +338,7 @@ impl SampleTree {
label: name.clone(),
folder: String::new(),
index: 0,
child_count: file_count,
});
if show_children {
let mut idx = 0;
@@ -340,6 +350,7 @@ impl SampleTree {
label: fname.clone(),
folder: name.clone(),
index: idx,
child_count: 0,
});
idx += 1;
}
@@ -362,6 +373,7 @@ pub struct SampleBrowserState {
pub scroll_offset: usize,
pub search_query: String,
pub search_active: bool,
pub visible_height: Cell<usize>,
filter: Option<Vec<String>>,
}
@@ -373,6 +385,7 @@ impl SampleBrowserState {
scroll_offset: 0,
search_query: String::new(),
search_active: false,
visible_height: Cell::new(20),
filter: None,
}
}
@@ -427,6 +440,10 @@ impl SampleBrowserState {
if self.scroll_offset > self.cursor {
self.scroll_offset = self.cursor;
}
let vh = self.visible_height.get();
if vh > 0 && self.cursor >= self.scroll_offset + vh {
self.scroll_offset = self.cursor - vh + 1;
}
}
pub fn move_up(&mut self) {
@@ -438,15 +455,16 @@ impl SampleBrowserState {
}
}
pub fn move_down(&mut self, visible_height: usize) {
pub fn move_down(&mut self) {
let count = self.visible_count();
if count == 0 {
return;
}
if self.cursor + 1 < count {
self.cursor += 1;
if self.cursor >= self.scroll_offset + visible_height {
self.scroll_offset = self.cursor - visible_height + 1;
let vh = self.visible_height.get();
if vh > 0 && self.cursor >= self.scroll_offset + vh {
self.scroll_offset = self.cursor - vh + 1;
}
}
}

View File

@@ -299,6 +299,13 @@ fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
let [tree_area, preview_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(6)]).areas(area);
// Compute visible height: tree_area minus borders (2), minus search bar (1) if shown
let mut vh = tree_area.height.saturating_sub(2) as usize;
if state.search_active || !state.search_query.is_empty() {
vh = vh.saturating_sub(1);
}
state.visible_height.set(vh);
let entries = state.entries();
SampleBrowser::new(&entries, state.cursor)
.scroll_offset(state.scroll_offset)
@@ -542,6 +549,13 @@ fn render_footer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, are
])
} else {
let bindings: Vec<(&str, &str)> = match app.page {
Page::Main if app.panel.visible && app.panel.focus == PanelFocus::Side => vec![
("↑↓", "Navigate"),
("", "Expand/Play"),
("", "Collapse"),
("/", "Search"),
("Tab", "Close"),
],
Page::Main => vec![
("Space", "Play"),
("Enter", "Edit"),