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 = snapshot .slot_data .iter() .filter(|s| s.active) .map(|s| s.bank) .collect(); let bank_names: Vec> = 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 = 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> = 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, ); } } } }