This commit is contained in:
2026-03-05 18:24:09 +01:00
parent 4743c33916
commit 0097777449
18 changed files with 177 additions and 276 deletions

View File

@@ -6,6 +6,7 @@ pub mod main_view;
pub mod options_view;
pub mod patterns_view;
mod render;
pub mod sample_explorer_view;
pub mod script_view;
pub mod title_view;

View File

@@ -14,19 +14,18 @@ use crate::engine::{LinkState, SequencerSnapshot};
use crate::model::{ExecutionTrace, SourceSpan};
use crate::page::Page;
use crate::state::{
EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, RenameTarget,
SidePanel,
EditorTarget, EuclideanField, FlashKind, Modal, PatternField, RenameTarget,
};
use crate::theme;
use crate::views::highlight::{self, highlight_line_with_runtime};
use crate::widgets::{
hint_line, render_props_form, render_search_bar, ConfirmModal, ModalFrame, NavMinimap, NavTile,
SampleBrowser, TextInputModal,
TextInputModal,
};
use super::{
dict_view, engine_view, help_view, main_view, options_view, patterns_view, script_view,
title_view,
dict_view, engine_view, help_view, main_view, options_view, patterns_view,
sample_explorer_view, script_view, title_view,
};
fn clip_span(span: SourceSpan, line_start: usize, line_len: usize) -> Option<SourceSpan> {
@@ -164,28 +163,15 @@ pub fn render(
render_header(frame, app, link, snapshot, header_area);
}
let (page_area, panel_area) = if app.panel.visible && app.panel.side.is_some() {
let panel_width = body_area.width * 35 / 100;
let [main, side] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
.areas(body_area);
(main, Some(side))
} else {
(body_area, None)
};
match app.page {
Page::Main => main_view::render(frame, app, snapshot, page_area),
Page::Patterns => patterns_view::render(frame, app, snapshot, page_area),
Page::Engine => engine_view::render(frame, app, link, page_area),
Page::Options => options_view::render(frame, app, page_area),
Page::Help => help_view::render(frame, app, page_area),
Page::Dict => dict_view::render(frame, app, page_area),
Page::Script => script_view::render(frame, app, snapshot, page_area),
}
if let Some(side_area) = panel_area {
render_side_panel(frame, app, side_area);
Page::Main => main_view::render(frame, app, snapshot, body_area),
Page::Patterns => patterns_view::render(frame, app, snapshot, body_area),
Page::Engine => engine_view::render(frame, app, link, body_area),
Page::Options => options_view::render(frame, app, body_area),
Page::Help => help_view::render(frame, app, body_area),
Page::Dict => dict_view::render(frame, app, body_area),
Page::Script => script_view::render(frame, app, snapshot, body_area),
Page::SampleExplorer => sample_explorer_view::render(frame, app, body_area),
}
if !perf {
@@ -292,69 +278,6 @@ fn header_height(_width: u16) -> u16 {
3
}
fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
let focused = app.panel.focus == PanelFocus::Side;
match &app.panel.side {
Some(SidePanel::SampleBrowser(state)) => {
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)
.search(&state.search_query, state.search_active)
.focused(focused)
.render(frame, tree_area);
if let Some(sample) = state
.sample_key()
.and_then(|key| app.audio.sample_registry.as_ref()?.get(&key))
.filter(|s| s.frame_count >= s.total_frames)
{
use crate::widgets::Waveform;
use std::cell::RefCell;
thread_local! {
static MONO_BUF: RefCell<Vec<f32>> = const { RefCell::new(Vec::new()) };
}
let [wave_area, info_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)])
.areas(preview_area);
MONO_BUF.with(|buf| {
let mut buf = buf.borrow_mut();
let channels = sample.channels as usize;
let frame_count = sample.frame_count as usize;
buf.clear();
buf.reserve(frame_count);
for i in 0..frame_count {
buf.push(sample.frames[i * channels]);
}
frame.render_widget(Waveform::new(&buf), wave_area);
});
let duration = sample.total_frames as f32 / app.audio.config.sample_rate;
let ch_label = if sample.channels == 1 {
"mono"
} else {
"stereo"
};
let info = Paragraph::new(format!(" {duration:.1}s · {ch_label}"))
.style(Style::new().fg(theme::get().ui.text_dim));
frame.render_widget(info, info_area);
}
}
None => {}
}
}
fn render_header(
frame: &mut Frame,
app: &App,
@@ -527,6 +450,7 @@ fn render_footer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, are
Page::Help => " HELP ",
Page::Dict => " DICT ",
Page::Script => " SCRIPT ",
Page::SampleExplorer => " SAMPLES ",
};
let content = if let Some(ref msg) = app.ui.status_message {
@@ -549,10 +473,10 @@ 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"),
Page::SampleExplorer => vec![
("\u{2191}\u{2193}", "Navigate"),
("\u{2192}", "Expand/Play"),
("\u{2190}", "Collapse"),
("/", "Search"),
("Tab", "Close"),
],

View File

@@ -0,0 +1,87 @@
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::Style;
use ratatui::Frame;
use crate::app::App;
use crate::theme;
use crate::widgets::{SampleBrowser, Waveform};
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
render_browser(frame, app, area);
}
fn render_browser(frame: &mut Frame, app: &App, area: Rect) {
let state = match &app.sample_browser {
Some(s) => s,
None => return,
};
let [tree_area, preview_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(6)]).areas(area);
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)
.search(&state.search_query, state.search_active)
.focused(true)
.render(frame, tree_area);
render_waveform_preview(frame, app, state, preview_area);
}
fn render_waveform_preview(
frame: &mut Frame,
app: &App,
state: &crate::state::SampleBrowserState,
area: Rect,
) {
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use std::cell::RefCell;
let sample = match state
.sample_key()
.and_then(|key| app.audio.sample_registry.as_ref()?.get(&key))
.filter(|s| s.frame_count >= s.total_frames)
{
Some(s) => s,
None => return,
};
thread_local! {
static MONO_BUF: RefCell<Vec<f32>> = const { RefCell::new(Vec::new()) };
}
let [wave_area, info_area] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
MONO_BUF.with(|buf| {
let mut buf = buf.borrow_mut();
let channels = sample.channels as usize;
let frame_count = sample.frame_count as usize;
buf.clear();
buf.reserve(frame_count);
for i in 0..frame_count {
buf.push(sample.frames[i * channels]);
}
frame.render_widget(Waveform::new(&buf), wave_area);
});
let duration = sample.total_frames as f32 / app.audio.config.sample_rate;
let ch_label = if sample.channels == 1 {
"mono"
} else {
"stereo"
};
let info = Paragraph::new(Line::from(Span::styled(
format!(" {duration:.1}s \u{00B7} {ch_label}"),
Style::new().fg(theme::get().ui.text_dim),
)));
frame.render_widget(info, info_area);
}