This commit is contained in:
2026-01-23 01:18:40 +01:00
parent 1aad52eed1
commit 10e2812e4c
5 changed files with 157 additions and 91 deletions

View File

@@ -6,9 +6,10 @@ use ratatui::Frame;
use crate::app::App;
use crate::engine::{LinkState, SequencerSnapshot};
use crate::model::forth::SourceSpan;
use crate::page::Page;
use crate::state::{Modal, PatternField};
use crate::views::highlight;
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
use crate::widgets::{ConfirmModal, ModalFrame, TextInputModal};
use super::{audio_view, doc_view, main_view, patterns_view, title_view};
@@ -25,16 +26,17 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
}
let padded = Rect {
x: term.x + 1,
x: term.x + 4,
y: term.y + 1,
width: term.width.saturating_sub(2),
width: term.width.saturating_sub(8),
height: term.height.saturating_sub(2),
};
let [header_area, _padding, body_area, footer_area] = Layout::vertical([
let [header_area, _padding, body_area, _bottom_padding, footer_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Length(3),
])
.areas(padded);
@@ -130,7 +132,7 @@ fn render_header(
bank_area,
);
// Pattern block (name + length + speed + iter)
// Pattern block (name + length + speed + page + iter)
let default_pattern_name = format!("Pattern {:02}", app.editor_ctx.pattern + 1);
let pattern_name = pattern.name.as_deref().unwrap_or(&default_pattern_name);
let speed_info = if pattern.speed != PatternSpeed::Normal {
@@ -138,13 +140,20 @@ fn render_header(
} else {
String::new()
};
let total_pages = pattern.length.div_ceil(32);
let page_info = if total_pages > 1 {
let current_page = app.editor_ctx.step / 32 + 1;
format!(" · {current_page}/{total_pages}")
} else {
String::new()
};
let iter_info = snapshot
.get_iter(app.editor_ctx.bank, app.editor_ctx.pattern)
.map(|iter| format!(" · #{}", iter + 1))
.unwrap_or_default();
let pattern_text = format!(
" {} · {} steps{}{} ",
pattern_name, pattern.length, speed_info, iter_info
" {} · {} steps{}{}{} ",
pattern_name, pattern.length, speed_info, page_info, iter_info
);
let pattern_style = Style::new().bg(Color::Rgb(30, 50, 50)).fg(Color::White);
frame.render_widget(
@@ -194,28 +203,37 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
} else {
let bindings: Vec<(&str, &str)> = match app.page {
Page::Main => vec![
("←→↑↓", "nav"),
("t", "toggle"),
("Enter", "edit"),
("<>", "len"),
("[]", "spd"),
("f", "fill"),
("←→↑↓", "Navigate"),
("t", "Toggle"),
("Enter", "Edit"),
("p", "Preview"),
("Space", "Play"),
("<>", "Length"),
("[]", "Speed"),
],
Page::Patterns => vec![
("←→↑↓", "nav"),
("Enter", "select"),
("Space", "play"),
("Esc", "back"),
("←→↑↓", "Navigate"),
("Enter", "Select"),
("Space", "Play"),
("Esc", "Back"),
("r", "Rename"),
("Del", "Reset"),
],
Page::Audio => vec![
("q", "quit"),
("h", "hush"),
("p", "panic"),
("r", "reset"),
("t", "test"),
("C-←→", "page"),
("↑↓", "Navigate"),
("←→", "Adjust"),
("h", "Hush"),
("p", "Panic"),
("r", "Reset"),
("t", "Test"),
("Space", "Play"),
],
Page::Doc => vec![
("↑↓", "Scroll"),
("←→", "Category"),
("Tab", "Topic"),
("PgUp/Dn", "Page"),
],
Page::Doc => vec![("j/k", "topic"), ("PgUp/Dn", "scroll"), ("C-←→", "page")],
};
let page_width = page_indicator.chars().count();
@@ -245,7 +263,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
Style::new().fg(Color::Yellow),
));
spans.push(Span::styled(
format!(" {action}"),
format!(":{action}"),
Style::new().fg(Color::Rgb(120, 125, 135)),
));
@@ -342,6 +360,86 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
.border_color(Color::Magenta)
.render_centered(frame, term);
}
Modal::Preview => {
let width = (term.width * 80 / 100).max(40);
let height = (term.height * 80 / 100).max(10);
let pattern = app.current_edit_pattern();
let step_idx = app.editor_ctx.step;
let step = pattern.step(step_idx);
let source_idx = step.and_then(|s| s.source);
let title = if let Some(src) = source_idx {
format!("Step {:02}{:02}", step_idx + 1, src + 1)
} else {
format!("Step {:02}", step_idx + 1)
};
let inner = ModalFrame::new(&title)
.width(width)
.height(height)
.border_color(Color::Rgb(120, 125, 135))
.render_centered(frame, term);
let script = pattern.resolve_script(step_idx).unwrap_or("");
if script.is_empty() {
let empty = Paragraph::new("(empty)")
.alignment(Alignment::Center)
.style(Style::new().fg(Color::Rgb(80, 85, 95)));
let centered_area = Rect {
y: inner.y + inner.height / 2,
height: 1,
..inner
};
frame.render_widget(empty, centered_area);
} else {
let runtime_spans = if app.ui.runtime_highlight && app.playback.playing {
snapshot.get_trace(
app.editor_ctx.bank,
app.editor_ctx.pattern,
step_idx,
)
} else {
None
};
let mut offset = 0usize;
let lines: Vec<Line> = script
.lines()
.map(|line_str| {
let tokens = if let Some(traces) = runtime_spans {
let shifted: Vec<_> = traces
.iter()
.filter_map(|s| {
let start = s.start.saturating_sub(offset);
let end = s.end.saturating_sub(offset);
if end > 0 && start < line_str.len() {
Some(SourceSpan {
start: start.min(line_str.len()),
end: end.min(line_str.len()),
})
} else {
None
}
})
.collect();
highlight_line_with_runtime(line_str, &shifted)
} else {
highlight_line(line_str)
};
offset += line_str.len() + 1;
let spans: Vec<Span> = tokens
.into_iter()
.map(|(style, text)| Span::styled(text, style))
.collect();
Line::from(spans)
})
.collect();
let paragraph = Paragraph::new(lines);
frame.render_widget(paragraph, inner);
}
}
Modal::Editor => {
let width = (term.width * 80 / 100).max(40);
let height = (term.height * 60 / 100).max(10);