This commit is contained in:
2026-01-20 17:12:56 +01:00
parent 1ba74f850c
commit f7797664bd
13 changed files with 86318 additions and 209 deletions

View File

@@ -1,6 +1,7 @@
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use ratatui::Frame;
use crate::app::App;
@@ -8,28 +9,34 @@ use crate::engine::SequencerSnapshot;
use crate::state::PatternsColumn;
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let [banks_area, patterns_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(area);
let [banks_area, gap, patterns_area] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(1),
Constraint::Fill(1),
])
.areas(area);
render_banks(frame, app, snapshot, banks_area);
// gap is just empty space
let _ = gap;
render_patterns(frame, app, snapshot, patterns_area);
}
fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Banks);
let border_color = if is_focused {
let [title_area, inner] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
let title_color = if is_focused {
Color::Rgb(100, 160, 180)
} else {
Color::Rgb(70, 75, 85)
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(border_color))
.title("Banks");
let inner = block.inner(area);
frame.render_widget(block, area);
let title = Paragraph::new("Banks")
.style(Style::new().fg(title_color))
.alignment(ratatui::layout::Alignment::Center);
frame.render_widget(title, title_area);
let banks_with_playback: Vec<usize> = snapshot
.slot_data
@@ -48,14 +55,26 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
})
.collect();
let rows: Vec<Constraint> = (0..16).map(|_| Constraint::Length(1)).collect();
let row_areas = Layout::vertical(rows).split(inner);
let row_height = (inner.height / 16).max(1);
let total_needed = row_height * 16;
let top_padding = if inner.height > total_needed {
(inner.height - total_needed) / 2
} else {
0
};
for idx in 0..16 {
if idx >= row_areas.len() {
let y = inner.y + top_padding + (idx as u16) * row_height;
if y >= inner.y + inner.height {
break;
}
let row_area = row_areas[idx];
let row_area = Rect {
x: inner.x,
y,
width: inner.width,
height: row_height.min(inner.y + inner.height - y),
};
let is_cursor = is_focused && idx == app.patterns_nav.bank_cursor;
let is_selected = idx == app.patterns_nav.bank_cursor;
@@ -89,14 +108,35 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
style
};
// Fill the entire row with background color
let bg_block = Block::default().style(Style::new().bg(bg));
frame.render_widget(bg_block, row_area);
let text_y = if row_height > 1 {
row_area.y + (row_height - 1) / 2
} else {
row_area.y
};
let text_area = Rect {
x: row_area.x,
y: text_y,
width: row_area.width,
height: 1,
};
let para = Paragraph::new(label).style(style);
frame.render_widget(para, row_area);
frame.render_widget(para, text_area);
}
}
fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
use crate::model::PatternSpeed;
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Patterns);
let border_color = if is_focused {
let [title_area, inner] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
let title_color = if is_focused {
Color::Rgb(100, 160, 180)
} else {
Color::Rgb(70, 75, 85)
@@ -104,18 +144,14 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
let bank = app.patterns_nav.bank_cursor;
let bank_name = app.project_state.project.banks[bank].name.as_deref();
let title = match bank_name {
let title_text = match bank_name {
Some(name) => format!("Patterns ({name})"),
None => format!("Patterns (Bank {:02})", bank + 1),
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(border_color))
.title(title);
let inner = block.inner(area);
frame.render_widget(block, area);
let title = Paragraph::new(title_text)
.style(Style::new().fg(title_color))
.alignment(ratatui::layout::Alignment::Center);
frame.render_widget(title, title_area);
let playing_patterns: Vec<usize> = snapshot
.slot_data
@@ -159,14 +195,26 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
None
};
let rows: Vec<Constraint> = (0..16).map(|_| Constraint::Length(1)).collect();
let row_areas = Layout::vertical(rows).split(inner);
let row_height = (inner.height / 16).max(1);
let total_needed = row_height * 16;
let top_padding = if inner.height > total_needed {
(inner.height - total_needed) / 2
} else {
0
};
for idx in 0..16 {
if idx >= row_areas.len() {
let y = inner.y + top_padding + (idx as u16) * row_height;
if y >= inner.y + inner.height {
break;
}
let row_area = row_areas[idx];
let row_area = Rect {
x: inner.x,
y,
width: inner.width,
height: row_height.min(inner.y + inner.height - y),
};
let is_cursor = is_focused && idx == app.patterns_nav.pattern_cursor;
let is_selected = idx == app.patterns_nav.pattern_cursor;
@@ -185,24 +233,70 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
(false, false, false, _) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
};
let name = app.project_state.project.banks[bank].patterns[idx]
.name
.as_deref()
.unwrap_or("");
let label = if name.is_empty() {
let pattern = &app.project_state.project.banks[bank].patterns[idx];
let name = pattern.name.as_deref().unwrap_or("");
let length = pattern.length;
let speed = pattern.speed;
let base_style = Style::new().bg(bg).fg(fg);
let bold_style = base_style.add_modifier(Modifier::BOLD);
// Fill the entire row with background color
let bg_block = Block::default().style(Style::new().bg(bg));
frame.render_widget(bg_block, row_area);
let text_y = if row_height > 1 {
row_area.y + (row_height - 1) / 2
} else {
row_area.y
};
// Split row into columns: [index+name] [length] [speed]
let speed_width: u16 = 14; // "Speed: 1/4x "
let length_width: u16 = 13; // "Length: 16 "
let name_width = row_area
.width
.saturating_sub(speed_width + length_width + 2);
let [name_area, length_area, speed_area] = Layout::horizontal([
Constraint::Length(name_width),
Constraint::Length(length_width),
Constraint::Length(speed_width),
])
.areas(Rect {
x: row_area.x,
y: text_y,
width: row_area.width,
height: 1,
});
// Column 1: prefix + index + name (left-aligned)
let name_text = if name.is_empty() {
format!("{}{:02}", prefix, idx + 1)
} else {
format!("{}{:02} {}", prefix, idx + 1, name)
};
let style = Style::new().bg(bg).fg(fg);
let style = if is_playing || is_queued_play {
style.add_modifier(Modifier::BOLD)
let name_style = if is_playing || is_queued_play {
bold_style
} else {
style
base_style
};
frame.render_widget(Paragraph::new(name_text).style(name_style), name_area);
let para = Paragraph::new(label).style(style);
frame.render_widget(para, row_area);
// Column 2: length
let length_line = Line::from(vec![
Span::styled("Length: ", bold_style),
Span::styled(format!("{}", length), base_style),
]);
frame.render_widget(Paragraph::new(length_line), length_area);
// Column 3: speed (only if non-default)
if speed != PatternSpeed::Normal {
let speed_line = Line::from(vec![
Span::styled("Speed: ", bold_style),
Span::styled(speed.label(), base_style),
]);
frame.render_widget(Paragraph::new(speed_line), speed_area);
}
}
}