Init
This commit is contained in:
297
src/views/patterns_view.rs
Normal file
297
src/views/patterns_view.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::SequencerSnapshot;
|
||||
use crate::state::PatternsColumn;
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||
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 [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 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
|
||||
.active_patterns
|
||||
.iter()
|
||||
.map(|p| p.bank)
|
||||
.collect();
|
||||
|
||||
let banks_with_queued: Vec<usize> = app
|
||||
.playback
|
||||
.queued_changes
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
crate::engine::PatternChange::Start { bank, .. } => Some(*bank),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
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 {
|
||||
let y = inner.y + top_padding + (idx as u16) * row_height;
|
||||
if y >= inner.y + inner.height {
|
||||
break;
|
||||
}
|
||||
|
||||
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;
|
||||
let is_edit = idx == app.editor_ctx.bank;
|
||||
let is_playing = banks_with_playback.contains(&idx);
|
||||
let is_queued = banks_with_queued.contains(&idx);
|
||||
|
||||
let (bg, fg, prefix) = match (is_cursor, is_playing, is_queued) {
|
||||
(true, _, _) => (Color::Cyan, Color::Black, ""),
|
||||
(false, true, _) => (Color::Rgb(45, 80, 45), Color::Green, "> "),
|
||||
(false, false, true) => (Color::Rgb(80, 80, 45), Color::Yellow, "? "),
|
||||
(false, false, false) if is_selected => (Color::Rgb(60, 65, 75), Color::White, ""),
|
||||
(false, false, false) if is_edit => (Color::Rgb(45, 106, 95), Color::White, ""),
|
||||
(false, false, false) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
||||
};
|
||||
|
||||
let name = app.project_state.project.banks[idx]
|
||||
.name
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
let label = 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 {
|
||||
style.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
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, 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 [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 bank = app.patterns_nav.bank_cursor;
|
||||
let bank_name = app.project_state.project.banks[bank].name.as_deref();
|
||||
let title_text = match bank_name {
|
||||
Some(name) => format!("Patterns ({name})"),
|
||||
None => format!("Patterns (Bank {:02})", bank + 1),
|
||||
};
|
||||
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
|
||||
.active_patterns
|
||||
.iter()
|
||||
.filter(|p| p.bank == bank)
|
||||
.map(|p| p.pattern)
|
||||
.collect();
|
||||
|
||||
let queued_to_play: Vec<usize> = app
|
||||
.playback
|
||||
.queued_changes
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
crate::engine::PatternChange::Start {
|
||||
bank: b, pattern, ..
|
||||
} if *b == bank => Some(*pattern),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let queued_to_stop: Vec<usize> = app
|
||||
.playback
|
||||
.queued_changes
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
crate::engine::PatternChange::Stop {
|
||||
bank: b,
|
||||
pattern,
|
||||
} if *b == bank => Some(*pattern),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let edit_pattern = if app.editor_ctx.bank == bank {
|
||||
Some(app.editor_ctx.pattern)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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 {
|
||||
let y = inner.y + top_padding + (idx as u16) * row_height;
|
||||
if y >= inner.y + inner.height {
|
||||
break;
|
||||
}
|
||||
|
||||
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;
|
||||
let is_edit = edit_pattern == Some(idx);
|
||||
let is_playing = playing_patterns.contains(&idx);
|
||||
let is_queued_play = queued_to_play.contains(&idx);
|
||||
let is_queued_stop = queued_to_stop.contains(&idx);
|
||||
|
||||
let (bg, fg, prefix) = match (is_cursor, is_playing, is_queued_play, is_queued_stop) {
|
||||
(true, _, _, _) => (Color::Cyan, Color::Black, ""),
|
||||
(false, true, _, true) => (Color::Rgb(120, 90, 30), Color::Yellow, "x "),
|
||||
(false, true, _, false) => (Color::Rgb(45, 80, 45), Color::Green, "> "),
|
||||
(false, false, true, _) => (Color::Rgb(80, 80, 45), Color::Yellow, "? "),
|
||||
(false, false, false, _) if is_selected => (Color::Rgb(60, 65, 75), Color::White, ""),
|
||||
(false, false, false, _) if is_edit => (Color::Rgb(45, 106, 95), Color::White, ""),
|
||||
(false, false, false, _) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
||||
};
|
||||
|
||||
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 name_style = if is_playing || is_queued_play {
|
||||
bold_style
|
||||
} else {
|
||||
base_style
|
||||
};
|
||||
frame.render_widget(Paragraph::new(name_text).style(name_style), name_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user