Feat: begin sample explorer overhaul
All checks were successful
Deploy Website / deploy (push) Has been skipped
All checks were successful
Deploy Website / deploy (push) Has been skipped
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user