Feat: WIP pattern view redesign

This commit is contained in:
2026-02-22 03:26:48 +01:00
parent c9c8fe4117
commit d3b27e8245
9 changed files with 636 additions and 127 deletions

View File

@@ -27,20 +27,106 @@ pub fn layout(area: Rect) -> [Rect; 3] {
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let [main_area, _, vu_area] = layout(area);
let has_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_preview;
let seq_h = sequencer_height(app.current_edit_pattern().length, app.editor_ctx.step);
let (viz_area, sequencer_area) = viz_seq_split(main_area, app.audio.config.layout, has_viz, seq_h);
if has_viz {
render_viz_area(frame, app, snapshot, viz_area);
if matches!(app.audio.config.layout, MainLayout::Top) {
render_top_layout(frame, app, snapshot, main_area);
} else {
let has_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_preview;
let (viz_area, sequencer_area) =
viz_seq_split(main_area, app.audio.config.layout, has_viz);
if has_viz {
render_viz_area(frame, app, snapshot, viz_area);
}
render_sequencer(frame, app, snapshot, sequencer_area);
}
render_sequencer(frame, app, snapshot, sequencer_area);
render_vu_meter(frame, app, vu_area);
}
fn render_top_layout(
frame: &mut Frame,
app: &App,
snapshot: &SequencerSnapshot,
main_area: Rect,
) {
let has_audio_viz = app.audio.config.show_scope || app.audio.config.show_spectrum;
let has_preview = app.audio.config.show_preview;
let mut constraints = Vec::new();
if has_audio_viz {
constraints.push(Constraint::Fill(1));
}
if has_preview {
constraints.push(Constraint::Length(preview_height(has_audio_viz)));
}
constraints.push(Constraint::Fill(1));
let areas = Layout::vertical(&constraints).split(main_area);
let mut idx = 0;
if has_audio_viz {
render_audio_viz(frame, app, areas[idx]);
idx += 1;
}
if has_preview {
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
if has_prelude {
let [script_area, prelude_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(areas[idx]);
render_script_preview(frame, app, snapshot, script_area);
render_prelude_preview(frame, app, prelude_area);
} else {
render_script_preview(frame, app, snapshot, areas[idx]);
}
idx += 1;
}
render_sequencer(frame, app, snapshot, areas[idx]);
}
fn render_audio_viz(frame: &mut Frame, app: &App, area: Rect) {
match (app.audio.config.show_scope, app.audio.config.show_spectrum) {
(true, true) => {
let [scope_area, spectrum_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(area);
render_scope(frame, app, scope_area, Orientation::Horizontal);
render_spectrum(frame, app, spectrum_area);
}
(true, false) => render_scope(frame, app, area, Orientation::Horizontal),
(false, true) => render_spectrum(frame, app, area),
(false, false) => {}
}
}
fn preview_height(has_audio_viz: bool) -> u16 {
if has_audio_viz { 10 } else { 14 }
}
pub fn sequencer_rect(app: &App, main_area: Rect) -> Rect {
if matches!(app.audio.config.layout, MainLayout::Top) {
let has_audio_viz = app.audio.config.show_scope || app.audio.config.show_spectrum;
let has_preview = app.audio.config.show_preview;
let mut constraints = Vec::new();
if has_audio_viz {
constraints.push(Constraint::Fill(1));
}
if has_preview {
constraints.push(Constraint::Length(preview_height(has_audio_viz)));
}
constraints.push(Constraint::Fill(1));
let areas = Layout::vertical(&constraints).split(main_area);
areas[areas.len() - 1]
} else {
let has_viz = app.audio.config.show_scope
|| app.audio.config.show_spectrum
|| app.audio.config.show_preview;
let (_, seq_area) = viz_seq_split(main_area, app.audio.config.layout, has_viz);
seq_area
}
}
enum VizPanel {
Scope,
Spectrum,
@@ -81,39 +167,49 @@ fn render_viz_area(
match panel {
VizPanel::Scope => render_scope(frame, app, *panel_area, orientation),
VizPanel::Spectrum => render_spectrum(frame, app, *panel_area),
VizPanel::Preview => render_script_preview(frame, app, snapshot, *panel_area),
VizPanel::Preview => {
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
if has_prelude {
let [script_area, prelude_area] = if is_vertical_layout {
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)])
.areas(*panel_area)
} else {
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)])
.areas(*panel_area)
};
render_script_preview(frame, app, snapshot, script_area);
render_prelude_preview(frame, app, prelude_area);
} else {
render_script_preview(frame, app, snapshot, *panel_area);
}
}
}
}
}
const STEPS_PER_PAGE: usize = 32;
const TILE_HEIGHT: u16 = 3;
const ROW_GAP: u16 = 1;
pub fn sequencer_height(pattern_length: usize, current_step: usize) -> u16 {
let page = current_step / STEPS_PER_PAGE;
let page_start = page * STEPS_PER_PAGE;
let steps_on_page = (page_start + STEPS_PER_PAGE).min(pattern_length) - page_start;
if steps_on_page == 0 {
return 0;
pub fn steps_per_page(area_height: u16) -> usize {
if area_height < 5 {
return 8;
}
let num_rows = steps_on_page.div_ceil(8);
let grid_h = (num_rows as u16) * TILE_HEIGHT + (num_rows.saturating_sub(1) as u16) * ROW_GAP;
grid_h + 2
let usable = (area_height - 2) as usize;
let max_rows = (usable + ROW_GAP as usize) / (TILE_HEIGHT as usize + ROW_GAP as usize);
(max_rows * 8).clamp(8, 128)
}
pub fn viz_seq_split(
main_area: Rect,
layout: MainLayout,
has_viz: bool,
seq_h: u16,
) -> (Rect, Rect) {
match layout {
MainLayout::Top => {
if has_viz {
let [viz, seq] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(seq_h),
Constraint::Fill(1),
])
.areas(main_area);
(viz, seq)
@@ -124,7 +220,7 @@ pub fn viz_seq_split(
MainLayout::Bottom => {
if has_viz {
let [seq, viz] = Layout::vertical([
Constraint::Length(seq_h),
Constraint::Fill(1),
Constraint::Fill(1),
])
.areas(main_area);
@@ -226,9 +322,11 @@ fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot,
let pattern = app.current_edit_pattern();
let length = pattern.length;
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 spp = steps_per_page(area.height);
app.editor_ctx.steps_per_page.set(spp);
let page = app.editor_ctx.step / spp;
let page_start = page * spp;
let steps_on_page = (page_start + spp).min(length) - page_start;
for (tile_rect, step_offset) in grid_layout(area, steps_on_page) {
let step_idx = page_start + step_offset;
@@ -470,6 +568,34 @@ fn render_script_preview(
frame.render_widget(Paragraph::new(lines), inner);
}
fn render_prelude_preview(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
let prelude = &app.project_state.project.prelude;
let block = Block::default()
.borders(Borders::ALL)
.title(" Prelude ")
.border_style(Style::new().fg(theme.ui.border));
let inner = block.inner(area);
frame.render_widget(block, area);
let lines: Vec<Line> = prelude
.lines()
.take(inner.height as usize)
.map(|line_str| {
let tokens = highlight_line_with_runtime(line_str, &[], &[], &[], &user_words);
let spans: Vec<Span> = tokens
.into_iter()
.map(|(style, text, _)| Span::styled(text, style))
.collect();
Line::from(spans)
})
.collect();
frame.render_widget(Paragraph::new(lines), inner);
}
fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let block = Block::default()