Files
doux-copy/seq/src/views/patterns_view.rs
2026-01-20 03:30:48 +01:00

270 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
);
}
}
}
}