ok
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::SequencerSnapshot;
|
||||
use crate::views::highlight::{highlight_line, highlight_line_with_runtime};
|
||||
use crate::widgets::{Orientation, Scope, VuMeter};
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &mut App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||
@@ -17,19 +15,19 @@ pub fn render(frame: &mut Frame, app: &mut App, snapshot: &SequencerSnapshot, ar
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let [scope_area, sequencer_area, preview_area] = Layout::vertical([
|
||||
Constraint::Length(8),
|
||||
let [scope_area, sequencer_area] = Layout::vertical([
|
||||
Constraint::Length(14),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.areas(left_area);
|
||||
|
||||
render_scope(frame, app, scope_area);
|
||||
render_sequencer(frame, app, snapshot, sequencer_area);
|
||||
render_step_preview(frame, app, snapshot, preview_area);
|
||||
render_vu_meter(frame, app, vu_area);
|
||||
}
|
||||
|
||||
const STEPS_PER_PAGE: usize = 32;
|
||||
|
||||
fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||
if area.width < 50 {
|
||||
let msg = Paragraph::new("Terminal too narrow")
|
||||
@@ -41,8 +39,12 @@ fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot,
|
||||
|
||||
let pattern = app.current_edit_pattern();
|
||||
let length = pattern.length;
|
||||
let num_rows = length.div_ceil(8);
|
||||
let steps_per_row = length.div_ceil(num_rows);
|
||||
let page = app.editor_ctx.step / STEPS_PER_PAGE;
|
||||
let page_start = page * STEPS_PER_PAGE;
|
||||
let steps_on_page = (page_start + STEPS_PER_PAGE).min(length) - page_start;
|
||||
|
||||
let num_rows = steps_on_page.div_ceil(8);
|
||||
let steps_per_row = steps_on_page.div_ceil(num_rows);
|
||||
|
||||
let spacing = num_rows.saturating_sub(1) as u16;
|
||||
let row_height = area.height.saturating_sub(spacing) / num_rows as u16;
|
||||
@@ -61,7 +63,7 @@ fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot,
|
||||
for row_idx in 0..num_rows {
|
||||
let row_area = rows[row_idx * 2];
|
||||
let start_step = row_idx * steps_per_row;
|
||||
let end_step = (start_step + steps_per_row).min(length);
|
||||
let end_step = (start_step + steps_per_row).min(steps_on_page);
|
||||
let cols_in_row = end_step - start_step;
|
||||
|
||||
let col_constraints: Vec<Constraint> = (0..cols_in_row * 2 - 1)
|
||||
@@ -78,12 +80,13 @@ fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot,
|
||||
let cols = Layout::horizontal(col_constraints).split(row_area);
|
||||
|
||||
for col_idx in 0..cols_in_row {
|
||||
let step_idx = start_step + col_idx;
|
||||
let step_idx = page_start + start_step + col_idx;
|
||||
if step_idx < length {
|
||||
render_tile(frame, cols[col_idx * 2], app, snapshot, step_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn render_tile(
|
||||
@@ -168,54 +171,3 @@ fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
|
||||
frame.render_widget(vu, area);
|
||||
}
|
||||
|
||||
fn render_step_preview(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||
let pattern = app.current_edit_pattern();
|
||||
let step_idx = app.editor_ctx.step;
|
||||
let step = pattern.step(step_idx);
|
||||
|
||||
let [title_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
|
||||
|
||||
let is_linked = step.map(|s| s.source.is_some()).unwrap_or(false);
|
||||
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 title_color = if is_linked {
|
||||
Color::Rgb(180, 140, 220)
|
||||
} else {
|
||||
Color::Rgb(120, 125, 135)
|
||||
};
|
||||
let title_p = Paragraph::new(title).style(Style::new().fg(title_color));
|
||||
frame.render_widget(title_p, title_area);
|
||||
|
||||
let script = pattern.resolve_script(step_idx).unwrap_or("");
|
||||
if script.is_empty() {
|
||||
let empty = Paragraph::new(" (empty)").style(Style::new().fg(Color::Rgb(80, 85, 95)));
|
||||
frame.render_widget(empty, content_area);
|
||||
return;
|
||||
}
|
||||
|
||||
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 spans: Vec<_> = if let Some(traces) = runtime_spans {
|
||||
highlight_line_with_runtime(script, traces)
|
||||
} else {
|
||||
highlight_line(script)
|
||||
}
|
||||
.into_iter()
|
||||
.map(|(style, text)| ratatui::text::Span::styled(text, style))
|
||||
.collect();
|
||||
let mut line_spans = vec![ratatui::text::Span::raw(" ")];
|
||||
line_spans.extend(spans);
|
||||
let line = Line::from(line_spans);
|
||||
let paragraph = Paragraph::new(line);
|
||||
frame.render_widget(paragraph, content_area);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user