270 lines
8.6 KiB
Rust
270 lines
8.6 KiB
Rust
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||
use ratatui::style::{Color, Modifier, Style};
|
||
use ratatui::text::{Line, Span};
|
||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||
use ratatui::Frame;
|
||
|
||
use crate::app::App;
|
||
use crate::engine::SequencerSnapshot;
|
||
use crate::state::PatternsViewLevel;
|
||
|
||
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||
match app.patterns_view_level {
|
||
PatternsViewLevel::Banks => render_banks(frame, app, snapshot, area),
|
||
PatternsViewLevel::Patterns { bank } => render_patterns(frame, app, snapshot, area, bank),
|
||
}
|
||
}
|
||
|
||
fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||
let block = Block::default()
|
||
.borders(Borders::ALL)
|
||
.border_style(Style::new().fg(Color::Rgb(100, 160, 180)))
|
||
.title("Banks");
|
||
|
||
let inner = block.inner(area);
|
||
frame.render_widget(block, area);
|
||
|
||
if inner.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, inner);
|
||
return;
|
||
}
|
||
|
||
let banks_with_playback: Vec<usize> = snapshot
|
||
.slot_data
|
||
.iter()
|
||
.filter(|s| s.active)
|
||
.map(|s| s.bank)
|
||
.collect();
|
||
|
||
let bank_names: Vec<Option<&str>> = app
|
||
.project_state
|
||
.project
|
||
.banks
|
||
.iter()
|
||
.map(|b| b.name.as_deref())
|
||
.collect();
|
||
|
||
render_grid(
|
||
frame,
|
||
inner,
|
||
app.patterns_cursor,
|
||
app.editor_ctx.bank,
|
||
&banks_with_playback,
|
||
&bank_names,
|
||
);
|
||
}
|
||
|
||
fn render_patterns(
|
||
frame: &mut Frame,
|
||
app: &App,
|
||
snapshot: &SequencerSnapshot,
|
||
area: Rect,
|
||
bank: usize,
|
||
) {
|
||
let bank_name = app.project_state.project.banks[bank].name.as_deref();
|
||
let title_text = match bank_name {
|
||
Some(name) => format!("{name} › Patterns"),
|
||
None => format!("Bank {:02} › Patterns", bank + 1),
|
||
};
|
||
let title = Line::from(vec![
|
||
Span::raw(title_text),
|
||
Span::styled(" [Esc]←", Style::new().fg(Color::Rgb(120, 125, 135))),
|
||
]);
|
||
|
||
let block = Block::default()
|
||
.borders(Borders::ALL)
|
||
.border_style(Style::new().fg(Color::Rgb(100, 160, 180)))
|
||
.title(title);
|
||
|
||
let inner = block.inner(area);
|
||
frame.render_widget(block, area);
|
||
|
||
if inner.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, inner);
|
||
return;
|
||
}
|
||
|
||
let playing_patterns: Vec<usize> = snapshot
|
||
.slot_data
|
||
.iter()
|
||
.filter(|s| s.active && s.bank == bank)
|
||
.map(|s| s.pattern)
|
||
.collect();
|
||
|
||
let edit_pattern = if app.editor_ctx.bank == bank {
|
||
app.editor_ctx.pattern
|
||
} else {
|
||
usize::MAX
|
||
};
|
||
|
||
let pattern_names: Vec<Option<&str>> = app.project_state.project.banks[bank]
|
||
.patterns
|
||
.iter()
|
||
.map(|p| p.name.as_deref())
|
||
.collect();
|
||
|
||
render_pattern_grid(
|
||
frame,
|
||
app,
|
||
snapshot,
|
||
inner,
|
||
bank,
|
||
app.patterns_cursor,
|
||
edit_pattern,
|
||
&playing_patterns,
|
||
&pattern_names,
|
||
);
|
||
}
|
||
|
||
fn render_grid(
|
||
frame: &mut Frame,
|
||
area: Rect,
|
||
cursor: usize,
|
||
edit_pos: usize,
|
||
playing_positions: &[usize],
|
||
names: &[Option<&str>],
|
||
) {
|
||
let rows = Layout::vertical([
|
||
Constraint::Fill(1),
|
||
Constraint::Fill(1),
|
||
Constraint::Fill(1),
|
||
Constraint::Fill(1),
|
||
])
|
||
.split(area);
|
||
|
||
for row in 0..4 {
|
||
let cols = Layout::horizontal(vec![Constraint::Fill(1); 4]).split(rows[row]);
|
||
for col in 0..4 {
|
||
let idx = row * 4 + col;
|
||
let is_cursor = idx == cursor;
|
||
let is_edit = idx == edit_pos;
|
||
let is_playing = playing_positions.contains(&idx);
|
||
|
||
let (bg, fg) = match (is_cursor, is_edit, is_playing) {
|
||
(true, _, _) => (Color::Cyan, Color::Black),
|
||
(false, true, _) => (Color::Rgb(45, 106, 95), Color::White),
|
||
(false, false, true) => (Color::Rgb(45, 80, 45), Color::Green),
|
||
(false, false, false) => (Color::Rgb(45, 48, 55), Color::Rgb(120, 125, 135)),
|
||
};
|
||
|
||
let name = names.get(idx).and_then(|n| *n).unwrap_or("");
|
||
let number = format!("{:02}", idx + 1);
|
||
let cell = cols[col];
|
||
|
||
// Fill background
|
||
frame.render_widget(Block::default().style(Style::new().bg(bg)), cell);
|
||
|
||
let top_area = Rect::new(cell.x, cell.y, cell.width, 1);
|
||
let center_y = cell.y + cell.height / 2;
|
||
let center_area = Rect::new(cell.x, center_y, cell.width, 1);
|
||
|
||
if name.is_empty() {
|
||
// Number centered
|
||
frame.render_widget(
|
||
Paragraph::new(number)
|
||
.alignment(Alignment::Center)
|
||
.style(Style::new().fg(fg).add_modifier(Modifier::BOLD)),
|
||
center_area,
|
||
);
|
||
} else {
|
||
// Number centered at top
|
||
frame.render_widget(
|
||
Paragraph::new(number)
|
||
.alignment(Alignment::Center)
|
||
.style(Style::new().fg(fg).add_modifier(Modifier::DIM)),
|
||
top_area,
|
||
);
|
||
// Name centered in middle
|
||
frame.render_widget(
|
||
Paragraph::new(name)
|
||
.alignment(Alignment::Center)
|
||
.style(Style::new().fg(fg).add_modifier(Modifier::BOLD)),
|
||
center_area,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn render_pattern_grid(
|
||
frame: &mut Frame,
|
||
app: &App,
|
||
snapshot: &SequencerSnapshot,
|
||
area: Rect,
|
||
bank: usize,
|
||
cursor: usize,
|
||
edit_pos: usize,
|
||
playing_positions: &[usize],
|
||
names: &[Option<&str>],
|
||
) {
|
||
let rows = Layout::vertical([
|
||
Constraint::Fill(1),
|
||
Constraint::Fill(1),
|
||
Constraint::Fill(1),
|
||
Constraint::Fill(1),
|
||
])
|
||
.split(area);
|
||
|
||
for row in 0..4 {
|
||
let cols = Layout::horizontal(vec![Constraint::Fill(1); 4]).split(rows[row]);
|
||
for col in 0..4 {
|
||
let idx = row * 4 + col;
|
||
let is_cursor = idx == cursor;
|
||
let is_edit = idx == edit_pos;
|
||
let is_playing = playing_positions.contains(&idx);
|
||
let queued = app.is_pattern_queued(bank, idx, snapshot);
|
||
|
||
let (bg, fg, prefix) = match (is_cursor, is_playing, queued) {
|
||
(true, _, _) => (Color::Cyan, Color::Black, ""),
|
||
(false, true, Some(false)) => (Color::Rgb(120, 90, 30), Color::Yellow, "×"),
|
||
(false, true, _) => (Color::Rgb(45, 80, 45), Color::Green, "▶"),
|
||
(false, false, Some(true)) => (Color::Rgb(80, 80, 45), Color::Yellow, "?"),
|
||
(false, false, _) if is_edit => (Color::Rgb(45, 106, 95), Color::White, ""),
|
||
(false, false, _) => (Color::Rgb(45, 48, 55), Color::Rgb(120, 125, 135), ""),
|
||
};
|
||
|
||
let name = names.get(idx).and_then(|n| *n).unwrap_or("");
|
||
let number = format!("{}{:02}", prefix, idx + 1);
|
||
let cell = cols[col];
|
||
|
||
// Fill background
|
||
frame.render_widget(Block::default().style(Style::new().bg(bg)), cell);
|
||
|
||
let top_area = Rect::new(cell.x, cell.y, cell.width, 1);
|
||
let center_y = cell.y + cell.height / 2;
|
||
let center_area = Rect::new(cell.x, center_y, cell.width, 1);
|
||
|
||
if name.is_empty() {
|
||
// Number centered
|
||
frame.render_widget(
|
||
Paragraph::new(number)
|
||
.alignment(Alignment::Center)
|
||
.style(Style::new().fg(fg).add_modifier(Modifier::BOLD)),
|
||
center_area,
|
||
);
|
||
} else {
|
||
// Number centered at top
|
||
frame.render_widget(
|
||
Paragraph::new(number)
|
||
.alignment(Alignment::Center)
|
||
.style(Style::new().fg(fg).add_modifier(Modifier::DIM)),
|
||
top_area,
|
||
);
|
||
// Name centered in middle
|
||
frame.render_widget(
|
||
Paragraph::new(name)
|
||
.alignment(Alignment::Center)
|
||
.style(Style::new().fg(fg).add_modifier(Modifier::BOLD)),
|
||
center_area,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|