flesh out sequencer

This commit is contained in:
2026-01-20 14:37:03 +01:00
parent 276107433a
commit ebb82b6f7d
25 changed files with 2069 additions and 771 deletions

View File

@@ -9,7 +9,7 @@ use crate::commands::AppCommand;
use crate::engine::{AudioCommand, LinkState, SequencerSnapshot};
use crate::model::PatternSpeed;
use crate::page::Page;
use crate::state::{AudioFocus, Focus, Modal, PatternField, PatternsViewLevel};
use crate::state::{AudioFocus, Modal, PatternField};
pub enum InputResult {
Continue,
@@ -31,6 +31,11 @@ impl<'a> InputContext<'a> {
}
pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
if ctx.app.ui.show_title {
ctx.app.ui.show_title = false;
return InputResult::Continue;
}
ctx.dispatch(AppCommand::ClearStatus);
if matches!(ctx.app.ui.modal, Modal::None) {
@@ -59,6 +64,49 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
_ => {}
},
Modal::ConfirmDeleteStep {
bank,
pattern,
step,
selected: _,
} => {
let (bank, pattern, step) = (*bank, *pattern, *step);
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
ctx.dispatch(AppCommand::DeleteStep {
bank,
pattern,
step,
});
ctx.dispatch(AppCommand::CloseModal);
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
ctx.dispatch(AppCommand::CloseModal);
}
KeyCode::Left | KeyCode::Right => {
if let Modal::ConfirmDeleteStep { selected, .. } = &mut ctx.app.ui.modal {
*selected = !*selected;
}
}
KeyCode::Enter => {
let do_delete =
if let Modal::ConfirmDeleteStep { selected, .. } = &ctx.app.ui.modal {
*selected
} else {
false
};
if do_delete {
ctx.dispatch(AppCommand::DeleteStep {
bank,
pattern,
step,
});
}
ctx.dispatch(AppCommand::CloseModal);
}
_ => {}
}
}
Modal::SaveAs(path) => match key.code {
KeyCode::Enter => {
let save_path = PathBuf::from(path.as_str());
@@ -77,6 +125,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
let load_path = PathBuf::from(path.as_str());
ctx.dispatch(AppCommand::CloseModal);
ctx.dispatch(AppCommand::Load(load_path));
load_project_samples(ctx);
}
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
KeyCode::Backspace => {
@@ -204,6 +253,23 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::Char(c) => path.push(c),
_ => {}
},
Modal::Editor => {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Esc => {
ctx.dispatch(AppCommand::SaveEditorToStep);
ctx.dispatch(AppCommand::CompileCurrentStep);
ctx.dispatch(AppCommand::CloseModal);
}
KeyCode::Char('e') if ctrl => {
ctx.dispatch(AppCommand::SaveEditorToStep);
ctx.dispatch(AppCommand::CompileCurrentStep);
}
_ => {
ctx.app.editor_ctx.text.input(Event::Key(key));
}
}
}
Modal::None => unreachable!(),
}
InputResult::Continue
@@ -243,64 +309,73 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult {
match ctx.app.editor_ctx.focus {
Focus::Sequencer => match key.code {
KeyCode::Char('q') => {
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
selected: false,
}));
}
KeyCode::Char(' ') => {
ctx.dispatch(AppCommand::TogglePlaying);
ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed);
}
KeyCode::Tab => ctx.dispatch(AppCommand::ToggleFocus),
KeyCode::Left => ctx.dispatch(AppCommand::PrevStep),
KeyCode::Right => ctx.dispatch(AppCommand::NextStep),
KeyCode::Up => ctx.dispatch(AppCommand::StepUp),
KeyCode::Down => ctx.dispatch(AppCommand::StepDown),
KeyCode::Enter => ctx.dispatch(AppCommand::ToggleStep),
KeyCode::Char('s') => {
let default = ctx
.app
.project_state
.file_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "project.buboseq".to_string());
ctx.dispatch(AppCommand::OpenModal(Modal::SaveAs(default)));
}
KeyCode::Char('l') => {
ctx.dispatch(AppCommand::OpenModal(Modal::LoadFrom(String::new())));
}
KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp),
KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown),
KeyCode::Char('<') | KeyCode::Char(',') => ctx.dispatch(AppCommand::LengthDecrease),
KeyCode::Char('>') | KeyCode::Char('.') => ctx.dispatch(AppCommand::LengthIncrease),
KeyCode::Char('[') => ctx.dispatch(AppCommand::SpeedDecrease),
KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease),
KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)),
KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)),
KeyCode::Char('c') if ctrl => ctx.dispatch(AppCommand::CopyStep),
KeyCode::Char('v') if ctrl => ctx.dispatch(AppCommand::PasteStep),
_ => {}
},
Focus::Editor => match key.code {
KeyCode::Tab | KeyCode::Esc => ctx.dispatch(AppCommand::ToggleFocus),
KeyCode::Char('e') if ctrl => {
ctx.dispatch(AppCommand::SaveEditorToStep);
ctx.dispatch(AppCommand::CompileCurrentStep);
}
_ => {
ctx.app.editor_ctx.text.input(Event::Key(key));
}
},
match key.code {
KeyCode::Char('q') => {
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
selected: false,
}));
}
KeyCode::Char(' ') => {
ctx.dispatch(AppCommand::TogglePlaying);
ctx.playing
.store(ctx.app.playback.playing, Ordering::Relaxed);
}
KeyCode::Left => ctx.dispatch(AppCommand::PrevStep),
KeyCode::Right => ctx.dispatch(AppCommand::NextStep),
KeyCode::Up => ctx.dispatch(AppCommand::StepUp),
KeyCode::Down => ctx.dispatch(AppCommand::StepDown),
KeyCode::Enter => ctx.dispatch(AppCommand::OpenModal(Modal::Editor)),
KeyCode::Char('t') => ctx.dispatch(AppCommand::ToggleStep),
KeyCode::Char('s') => {
ctx.dispatch(AppCommand::OpenModal(Modal::SaveAs(String::new())));
}
KeyCode::Char('c') if ctrl => ctx.dispatch(AppCommand::CopyStep),
KeyCode::Char('v') if ctrl => ctx.dispatch(AppCommand::PasteStep),
KeyCode::Char('b') if ctrl => ctx.dispatch(AppCommand::LinkPasteStep),
KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenStep),
KeyCode::Char('l') => {
let default_dir = ctx
.app
.project_state
.file_path
.as_ref()
.and_then(|p| p.parent())
.map(|p| {
let mut s = p.display().to_string();
if !s.ends_with('/') {
s.push('/');
}
s
})
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::LoadFrom(default_dir)));
}
KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp),
KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown),
KeyCode::Char('<') | KeyCode::Char(',') => ctx.dispatch(AppCommand::LengthDecrease),
KeyCode::Char('>') | KeyCode::Char('.') => ctx.dispatch(AppCommand::LengthIncrease),
KeyCode::Char('[') => ctx.dispatch(AppCommand::SpeedDecrease),
KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease),
KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)),
KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)),
KeyCode::Delete | KeyCode::Backspace => {
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
let step = ctx.app.editor_ctx.step;
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmDeleteStep {
bank,
pattern,
step,
selected: false,
}));
}
_ => {}
}
InputResult::Continue
}
fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
use crate::state::PatternsColumn;
match key.code {
KeyCode::Left => ctx.dispatch(AppCommand::PatternsCursorLeft),
KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight),
@@ -308,42 +383,39 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::Down => ctx.dispatch(AppCommand::PatternsCursorDown),
KeyCode::Esc | KeyCode::Backspace => ctx.dispatch(AppCommand::PatternsBack),
KeyCode::Enter => ctx.dispatch(AppCommand::PatternsEnter),
KeyCode::Char(' ') => {
if let PatternsViewLevel::Patterns { bank } = ctx.app.patterns_view_level {
let pattern = ctx.app.patterns_cursor;
ctx.dispatch(AppCommand::TogglePatternPlayback { bank, pattern });
}
}
KeyCode::Char(' ') => ctx.dispatch(AppCommand::PatternsTogglePlay),
KeyCode::Char('q') => {
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
selected: false,
}));
}
KeyCode::Char('r') => match ctx.app.patterns_view_level {
PatternsViewLevel::Banks => {
let bank = ctx.app.patterns_cursor;
let current_name = ctx.app.project_state.project.banks[bank]
.name
.clone()
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::RenameBank {
bank,
name: current_name,
}));
KeyCode::Char('r') => {
let bank = ctx.app.patterns_nav.bank_cursor;
match ctx.app.patterns_nav.column {
PatternsColumn::Banks => {
let current_name = ctx.app.project_state.project.banks[bank]
.name
.clone()
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::RenameBank {
bank,
name: current_name,
}));
}
PatternsColumn::Patterns => {
let pattern = ctx.app.patterns_nav.pattern_cursor;
let current_name = ctx.app.project_state.project.banks[bank].patterns[pattern]
.name
.clone()
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::RenamePattern {
bank,
pattern,
name: current_name,
}));
}
}
PatternsViewLevel::Patterns { bank } => {
let pattern = ctx.app.patterns_cursor;
let current_name = ctx.app.project_state.project.banks[bank].patterns[pattern]
.name
.clone()
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::RenamePattern {
bank,
pattern,
name: current_name,
}));
}
},
}
_ => {}
}
InputResult::Continue
@@ -421,3 +493,29 @@ fn handle_doc_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
InputResult::Continue
}
fn load_project_samples(ctx: &mut InputContext) {
let paths = ctx.app.project_state.project.sample_paths.clone();
if paths.is_empty() {
return;
}
let mut total_count = 0;
for path in &paths {
if path.is_dir() {
let index = doux::loader::scan_samples_dir(path);
let count = index.len();
total_count += count;
let _ = ctx.audio_tx.send(AudioCommand::LoadSamples(index));
}
}
ctx.app.audio.config.sample_paths = paths;
ctx.app.audio.config.sample_count = total_count;
if total_count > 0 {
ctx.dispatch(AppCommand::SetStatus(format!(
"Loaded {total_count} samples from project"
)));
}
}