Init
This commit is contained in:
705
src/input.rs
Normal file
705
src/input.rs
Normal file
@@ -0,0 +1,705 @@
|
||||
use crossbeam_channel::Sender;
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::{AudioCommand, LinkState, SequencerSnapshot};
|
||||
use crate::model::PatternSpeed;
|
||||
use crate::page::Page;
|
||||
use crate::state::{AudioFocus, Modal, PatternField};
|
||||
|
||||
pub enum InputResult {
|
||||
Continue,
|
||||
Quit,
|
||||
}
|
||||
|
||||
pub struct InputContext<'a> {
|
||||
pub app: &'a mut App,
|
||||
pub link: &'a LinkState,
|
||||
pub snapshot: &'a SequencerSnapshot,
|
||||
pub playing: &'a Arc<AtomicBool>,
|
||||
pub audio_tx: &'a Sender<AudioCommand>,
|
||||
}
|
||||
|
||||
impl<'a> InputContext<'a> {
|
||||
fn dispatch(&mut self, cmd: AppCommand) {
|
||||
self.app.dispatch(cmd, self.link, self.snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
if handle_live_keys(ctx, &key) {
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
if key.kind == KeyEventKind::Release {
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
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) {
|
||||
handle_normal_input(ctx, key)
|
||||
} else {
|
||||
handle_modal_input(ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
|
||||
match (key.code, key.kind) {
|
||||
_ if !matches!(ctx.app.ui.modal, Modal::None) => false,
|
||||
(KeyCode::Char('f'), KeyEventKind::Press) => {
|
||||
ctx.app.live_keys.flip_fill();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match &mut ctx.app.ui.modal {
|
||||
Modal::ConfirmQuit { selected } => match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => return InputResult::Quit,
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Left | KeyCode::Right => {
|
||||
*selected = !*selected;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if *selected {
|
||||
return InputResult::Quit;
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
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::ConfirmResetPattern {
|
||||
bank,
|
||||
pattern,
|
||||
selected: _,
|
||||
} => {
|
||||
let (bank, pattern) = (*bank, *pattern);
|
||||
match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||
ctx.dispatch(AppCommand::ResetPattern { bank, pattern });
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Left | KeyCode::Right => {
|
||||
if let Modal::ConfirmResetPattern { selected, .. } = &mut ctx.app.ui.modal {
|
||||
*selected = !*selected;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let do_reset =
|
||||
if let Modal::ConfirmResetPattern { selected, .. } = &ctx.app.ui.modal {
|
||||
*selected
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if do_reset {
|
||||
ctx.dispatch(AppCommand::ResetPattern { bank, pattern });
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::ConfirmResetBank { bank, selected: _ } => {
|
||||
let bank = *bank;
|
||||
match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||
ctx.dispatch(AppCommand::ResetBank { bank });
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Left | KeyCode::Right => {
|
||||
if let Modal::ConfirmResetBank { selected, .. } = &mut ctx.app.ui.modal {
|
||||
*selected = !*selected;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let do_reset =
|
||||
if let Modal::ConfirmResetBank { selected, .. } = &ctx.app.ui.modal {
|
||||
*selected
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if do_reset {
|
||||
ctx.dispatch(AppCommand::ResetBank { bank });
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::SaveAs(path) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let save_path = PathBuf::from(path.as_str());
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
ctx.dispatch(AppCommand::Save(save_path));
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
path.pop();
|
||||
}
|
||||
KeyCode::Char(c) => path.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::LoadFrom(path) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
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 => {
|
||||
path.pop();
|
||||
}
|
||||
KeyCode::Char(c) => path.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::RenameBank { bank, name } => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let bank_idx = *bank;
|
||||
let new_name = if name.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.clone())
|
||||
};
|
||||
ctx.dispatch(AppCommand::RenameBank {
|
||||
bank: bank_idx,
|
||||
name: new_name,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
name.pop();
|
||||
}
|
||||
KeyCode::Char(c) => name.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::RenamePattern {
|
||||
bank,
|
||||
pattern,
|
||||
name,
|
||||
} => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let (bank_idx, pattern_idx) = (*bank, *pattern);
|
||||
let new_name = if name.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.clone())
|
||||
};
|
||||
ctx.dispatch(AppCommand::RenamePattern {
|
||||
bank: bank_idx,
|
||||
pattern: pattern_idx,
|
||||
name: new_name,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
name.pop();
|
||||
}
|
||||
KeyCode::Char(c) => name.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::SetPattern { field, input } => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let field = *field;
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
match field {
|
||||
PatternField::Length => {
|
||||
if let Ok(len) = input.parse::<usize>() {
|
||||
ctx.dispatch(AppCommand::SetLength {
|
||||
bank,
|
||||
pattern,
|
||||
length: len,
|
||||
});
|
||||
let new_len = ctx
|
||||
.app
|
||||
.project_state
|
||||
.project
|
||||
.pattern_at(bank, pattern)
|
||||
.length;
|
||||
ctx.dispatch(AppCommand::SetStatus(format!("Length set to {new_len}")));
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SetStatus("Invalid length".to_string()));
|
||||
}
|
||||
}
|
||||
PatternField::Speed => {
|
||||
if let Some(speed) = PatternSpeed::from_label(input) {
|
||||
ctx.dispatch(AppCommand::SetSpeed {
|
||||
bank,
|
||||
pattern,
|
||||
speed,
|
||||
});
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"Speed set to {}",
|
||||
speed.label()
|
||||
)));
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SetStatus(
|
||||
"Invalid speed (try 1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x)".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
input.pop();
|
||||
}
|
||||
KeyCode::Char(c) => input.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::SetTempo(input) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
if let Ok(tempo) = input.parse::<f64>() {
|
||||
let tempo = tempo.clamp(20.0, 300.0);
|
||||
ctx.link.set_tempo(tempo);
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"Tempo set to {tempo:.1} BPM"
|
||||
)));
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SetStatus("Invalid tempo".to_string()));
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
input.pop();
|
||||
}
|
||||
KeyCode::Char(c) if c.is_ascii_digit() || c == '.' => input.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::AddSamplePath(path) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let sample_path = PathBuf::from(path.as_str());
|
||||
if sample_path.is_dir() {
|
||||
let index = doux::loader::scan_samples_dir(&sample_path);
|
||||
let count = index.len();
|
||||
let _ = ctx.audio_tx.send(AudioCommand::LoadSamples(index));
|
||||
ctx.app.audio.config.sample_count += count;
|
||||
ctx.app.audio.add_sample_path(sample_path);
|
||||
ctx.dispatch(AppCommand::SetStatus(format!("Added {count} samples")));
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SetStatus("Path is not a directory".to_string()));
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
path.pop();
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
if ctrl {
|
||||
match key.code {
|
||||
KeyCode::Left => {
|
||||
ctx.dispatch(AppCommand::PageLeft);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
ctx.dispatch(AppCommand::PageRight);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
ctx.dispatch(AppCommand::PageUp);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
ctx.dispatch(AppCommand::PageDown);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match ctx.app.page {
|
||||
Page::Main => handle_main_page(ctx, key, ctrl),
|
||||
Page::Patterns => handle_patterns_page(ctx, key),
|
||||
Page::Audio => handle_audio_page(ctx, key),
|
||||
Page::Doc => handle_doc_page(ctx, key),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult {
|
||||
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('T') => {
|
||||
let current = format!("{:.1}", ctx.link.tempo());
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current)));
|
||||
}
|
||||
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;
|
||||
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
match key.code {
|
||||
KeyCode::Left => ctx.dispatch(AppCommand::PatternsCursorLeft),
|
||||
KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight),
|
||||
KeyCode::Up => ctx.dispatch(AppCommand::PatternsCursorUp),
|
||||
KeyCode::Down => ctx.dispatch(AppCommand::PatternsCursorDown),
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::PatternsBack),
|
||||
KeyCode::Enter => ctx.dispatch(AppCommand::PatternsEnter),
|
||||
KeyCode::Char(' ') => ctx.dispatch(AppCommand::PatternsTogglePlay),
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('c') if ctrl => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
ctx.dispatch(AppCommand::CopyBank { bank });
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::CopyPattern { bank, pattern });
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('v') if ctrl => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
ctx.dispatch(AppCommand::PasteBank { bank });
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::PastePattern { bank, pattern });
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete | KeyCode::Backspace => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetBank {
|
||||
bank,
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetPattern {
|
||||
bank,
|
||||
pattern,
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => ctx.app.audio.prev_focus(),
|
||||
KeyCode::Down | KeyCode::Char('j') => ctx.app.audio.next_focus(),
|
||||
KeyCode::Left => {
|
||||
match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice => ctx.app.audio.prev_output_device(),
|
||||
AudioFocus::InputDevice => ctx.app.audio.prev_input_device(),
|
||||
AudioFocus::Channels => ctx.app.audio.adjust_channels(-1),
|
||||
AudioFocus::BufferSize => ctx.app.audio.adjust_buffer_size(-64),
|
||||
AudioFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(),
|
||||
AudioFocus::RuntimeHighlight => {
|
||||
ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight
|
||||
}
|
||||
AudioFocus::SamplePaths => ctx.app.audio.remove_last_sample_path(),
|
||||
AudioFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
AudioFocus::StartStopSync => ctx
|
||||
.link
|
||||
.set_start_stop_sync_enabled(!ctx.link.is_start_stop_sync_enabled()),
|
||||
AudioFocus::Quantum => ctx.link.set_quantum(ctx.link.quantum() - 1.0),
|
||||
}
|
||||
if ctx.app.audio.focus != AudioFocus::SamplePaths {
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice => ctx.app.audio.next_output_device(),
|
||||
AudioFocus::InputDevice => ctx.app.audio.next_input_device(),
|
||||
AudioFocus::Channels => ctx.app.audio.adjust_channels(1),
|
||||
AudioFocus::BufferSize => ctx.app.audio.adjust_buffer_size(64),
|
||||
AudioFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(),
|
||||
AudioFocus::RuntimeHighlight => {
|
||||
ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight
|
||||
}
|
||||
AudioFocus::SamplePaths => {}
|
||||
AudioFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
AudioFocus::StartStopSync => ctx
|
||||
.link
|
||||
.set_start_stop_sync_enabled(!ctx.link.is_start_stop_sync_enabled()),
|
||||
AudioFocus::Quantum => ctx.link.set_quantum(ctx.link.quantum() + 1.0),
|
||||
}
|
||||
if ctx.app.audio.focus != AudioFocus::SamplePaths {
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('R') => ctx.app.audio.trigger_restart(),
|
||||
KeyCode::Char('A') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::AddSamplePath(String::new())));
|
||||
}
|
||||
KeyCode::Char('D') => {
|
||||
ctx.app.audio.refresh_devices();
|
||||
let out_count = ctx.app.audio.output_devices.len();
|
||||
let in_count = ctx.app.audio.input_devices.len();
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"Found {out_count} output, {in_count} input devices"
|
||||
)));
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
let _ = ctx.audio_tx.send(AudioCommand::Hush);
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
let _ = ctx.audio_tx.send(AudioCommand::Panic);
|
||||
}
|
||||
KeyCode::Char('r') => ctx.app.metrics.peak_voices = 0,
|
||||
KeyCode::Char('t') => {
|
||||
let _ = ctx
|
||||
.audio_tx
|
||||
.send(AudioCommand::Evaluate("sin 440 * 0.3".into()));
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn handle_doc_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('j') | KeyCode::Down => ctx.dispatch(AppCommand::DocScrollDown(1)),
|
||||
KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::DocScrollUp(1)),
|
||||
KeyCode::Char('h') | KeyCode::Left => ctx.dispatch(AppCommand::DocPrevCategory),
|
||||
KeyCode::Char('l') | KeyCode::Right => ctx.dispatch(AppCommand::DocNextCategory),
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::DocNextTopic),
|
||||
KeyCode::BackTab => ctx.dispatch(AppCommand::DocPrevTopic),
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::DocScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::DocScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
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"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user