Init
This commit is contained in:
201
src/views/main_view.rs
Normal file
201
src/views/main_view.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
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) {
|
||||
let [left_area, _spacer, vu_area] = Layout::horizontal([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(8),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let [scope_area, sequencer_area, preview_area] = Layout::vertical([
|
||||
Constraint::Length(8),
|
||||
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);
|
||||
}
|
||||
|
||||
fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||
if area.width < 50 {
|
||||
let msg = Paragraph::new("Terminal too narrow")
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::new().fg(Color::Rgb(120, 125, 135)));
|
||||
frame.render_widget(msg, area);
|
||||
return;
|
||||
}
|
||||
|
||||
let pattern = app.current_edit_pattern();
|
||||
let length = pattern.length;
|
||||
let num_rows = match length {
|
||||
0..=8 => 1,
|
||||
9..=16 => 2,
|
||||
17..=24 => 3,
|
||||
_ => 4,
|
||||
};
|
||||
let steps_per_row = length.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;
|
||||
|
||||
let row_constraints: Vec<Constraint> = (0..num_rows * 2 - 1)
|
||||
.map(|i| {
|
||||
if i % 2 == 0 {
|
||||
Constraint::Length(row_height)
|
||||
} else {
|
||||
Constraint::Length(1)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let rows = Layout::vertical(row_constraints).split(area);
|
||||
|
||||
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 cols_in_row = end_step - start_step;
|
||||
|
||||
let col_constraints: Vec<Constraint> = (0..cols_in_row * 2 - 1)
|
||||
.map(|i| {
|
||||
if i % 2 == 0 {
|
||||
Constraint::Fill(1)
|
||||
} else if i == cols_in_row - 1 {
|
||||
Constraint::Length(2)
|
||||
} else {
|
||||
Constraint::Length(1)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let cols = Layout::horizontal(col_constraints).split(row_area);
|
||||
|
||||
for col_idx in 0..cols_in_row {
|
||||
let step_idx = start_step + col_idx;
|
||||
if step_idx < length {
|
||||
render_tile(frame, cols[col_idx * 2], app, snapshot, step_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tile(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
app: &App,
|
||||
snapshot: &SequencerSnapshot,
|
||||
step_idx: usize,
|
||||
) {
|
||||
let pattern = app.current_edit_pattern();
|
||||
let step = pattern.step(step_idx);
|
||||
let is_active = step.map(|s| s.active).unwrap_or(false);
|
||||
let is_linked = step.map(|s| s.source.is_some()).unwrap_or(false);
|
||||
let is_selected = step_idx == app.editor_ctx.step;
|
||||
|
||||
let is_playing = if app.playback.playing {
|
||||
snapshot.get_step(app.editor_ctx.bank, app.editor_ctx.pattern) == Some(step_idx)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let (bg, fg) = match (is_playing, is_active, is_selected, is_linked) {
|
||||
(true, true, _, _) => (Color::Rgb(195, 85, 65), Color::White),
|
||||
(true, false, _, _) => (Color::Rgb(180, 120, 45), Color::Black),
|
||||
(false, true, true, true) => (Color::Rgb(180, 140, 220), Color::Black),
|
||||
(false, true, true, false) => (Color::Rgb(0, 220, 180), Color::Black),
|
||||
(false, true, false, true) => (Color::Rgb(90, 70, 120), Color::White),
|
||||
(false, true, false, false) => (Color::Rgb(45, 106, 95), Color::White),
|
||||
(false, false, true, _) => (Color::Rgb(80, 180, 255), Color::Black),
|
||||
(false, false, false, _) => (Color::Rgb(45, 48, 55), Color::Rgb(120, 125, 135)),
|
||||
};
|
||||
|
||||
let symbol = if is_playing {
|
||||
"▶".to_string()
|
||||
} else if let Some(source) = step.and_then(|s| s.source) {
|
||||
format!("→{:02}", source + 1)
|
||||
} else {
|
||||
format!("{:02}", step_idx + 1)
|
||||
};
|
||||
|
||||
let tile = Paragraph::new(symbol)
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
|
||||
|
||||
frame.render_widget(tile, area);
|
||||
}
|
||||
|
||||
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let scope = Scope::new(&app.metrics.scope)
|
||||
.orientation(Orientation::Horizontal)
|
||||
.color(Color::Green);
|
||||
frame.render_widget(scope, area);
|
||||
}
|
||||
|
||||
fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let vu = VuMeter::new(app.metrics.peak_left, app.metrics.peak_right);
|
||||
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)
|
||||
} 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);
|
||||
}
|
||||
Reference in New Issue
Block a user