Fixes
This commit is contained in:
553
src/input/modal.rs
Normal file
553
src/input/modal.rs
Normal file
@@ -0,0 +1,553 @@
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::SeqCommand;
|
||||
use crate::model::PatternSpeed;
|
||||
use crate::state::{
|
||||
ConfirmAction, EditorTarget, EuclideanField, Modal, PatternField,
|
||||
PatternPropsField, RenameTarget,
|
||||
};
|
||||
|
||||
pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match &mut ctx.app.ui.modal {
|
||||
Modal::Confirm { action, selected } => {
|
||||
let (action, confirmed) = (action.clone(), *selected);
|
||||
match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => return execute_confirm(ctx, &action),
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Left | KeyCode::Right => {
|
||||
if let Modal::Confirm { selected, .. } = &mut ctx.app.ui.modal {
|
||||
*selected = !*selected;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if confirmed {
|
||||
return execute_confirm(ctx, &action);
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::FileBrowser(state) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
use crate::state::file_browser::FileBrowserMode;
|
||||
let mode = state.mode.clone();
|
||||
if let Some(path) = state.confirm() {
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
match mode {
|
||||
FileBrowserMode::Save => ctx.dispatch(AppCommand::Save(path)),
|
||||
FileBrowserMode::Load => {
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::ResetScriptState);
|
||||
ctx.dispatch(AppCommand::Load(path));
|
||||
super::load_project_samples(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Tab => state.autocomplete(),
|
||||
KeyCode::Left => state.go_up(),
|
||||
KeyCode::Right => state.enter_selected(),
|
||||
KeyCode::Up => state.select_prev(12),
|
||||
KeyCode::Down => state.select_next(12),
|
||||
KeyCode::Backspace => state.backspace(),
|
||||
KeyCode::Char(c) => {
|
||||
state.input.push(c);
|
||||
state.refresh_entries();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Modal::Rename { target, name } => {
|
||||
let target = target.clone();
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
let new_name = if name.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.clone())
|
||||
};
|
||||
ctx.dispatch(rename_command(&target, new_name));
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
if let Modal::Rename { name, .. } = &mut ctx.app.ui.modal {
|
||||
name.pop();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
if let Modal::Rename { name, .. } = &mut ctx.app.ui.modal {
|
||||
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/3, 2/5, 1x, 2x)".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(state) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let sample_path = if let Some(entry) = state.entries.get(state.selected) {
|
||||
if entry.is_dir && entry.name != ".." {
|
||||
Some(state.current_dir().join(&entry.name))
|
||||
} else if entry.is_dir {
|
||||
state.enter_selected();
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let dir = state.current_dir();
|
||||
if dir.is_dir() {
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(path) = sample_path {
|
||||
let index = doux::sampling::scan_samples_dir(&path);
|
||||
let count = index.len();
|
||||
let preload_entries: Vec<(String, std::path::PathBuf)> = index
|
||||
.iter()
|
||||
.map(|e| (e.name.clone(), e.path.clone()))
|
||||
.collect();
|
||||
let _ = ctx.audio_tx.load().send(crate::engine::AudioCommand::LoadSamples(index));
|
||||
ctx.app.audio.config.sample_count += count;
|
||||
ctx.app.audio.add_sample_path(path);
|
||||
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
||||
let sr = ctx.app.audio.config.sample_rate;
|
||||
std::thread::Builder::new()
|
||||
.name("sample-preload".into())
|
||||
.spawn(move || {
|
||||
crate::init::preload_sample_heads(preload_entries, sr, ®istry);
|
||||
})
|
||||
.expect("failed to spawn preload thread");
|
||||
}
|
||||
ctx.dispatch(AppCommand::SetStatus(format!("Added {count} samples")));
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Tab => state.autocomplete(),
|
||||
KeyCode::Left => state.go_up(),
|
||||
KeyCode::Right => state.enter_selected(),
|
||||
KeyCode::Up => state.select_prev(14),
|
||||
KeyCode::Down => state.select_next(14),
|
||||
KeyCode::Backspace => state.backspace(),
|
||||
KeyCode::Char(c) => {
|
||||
state.input.push(c);
|
||||
state.refresh_entries();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Modal::Editor => {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
let editor = &mut ctx.app.editor_ctx.editor;
|
||||
|
||||
if editor.search_active() {
|
||||
match key.code {
|
||||
KeyCode::Esc => editor.search_clear(),
|
||||
KeyCode::Enter => editor.search_confirm(),
|
||||
KeyCode::Backspace => editor.search_backspace(),
|
||||
KeyCode::Char(c) if !ctrl => editor.search_input(c),
|
||||
_ => {}
|
||||
}
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
if editor.sample_finder_active() {
|
||||
match key.code {
|
||||
KeyCode::Esc => editor.dismiss_sample_finder(),
|
||||
KeyCode::Tab | KeyCode::Enter => editor.accept_sample_finder(),
|
||||
KeyCode::Backspace => editor.sample_finder_backspace(),
|
||||
KeyCode::Char('n') if ctrl => editor.sample_finder_next(),
|
||||
KeyCode::Char('p') if ctrl => editor.sample_finder_prev(),
|
||||
KeyCode::Char(c) if !ctrl => editor.sample_finder_input(c),
|
||||
_ => {}
|
||||
}
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
if editor.is_selecting() {
|
||||
editor.cancel_selection();
|
||||
} else if editor.completion_active() {
|
||||
editor.dismiss_completion();
|
||||
} else {
|
||||
match ctx.app.editor_ctx.target {
|
||||
EditorTarget::Step => {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
}
|
||||
EditorTarget::Prelude => {
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('e') if ctrl => match ctx.app.editor_ctx.target {
|
||||
EditorTarget::Step => {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
}
|
||||
EditorTarget::Prelude => {
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
},
|
||||
KeyCode::Char('b') if ctrl => {
|
||||
editor.activate_sample_finder();
|
||||
}
|
||||
KeyCode::Char('f') if ctrl => {
|
||||
editor.activate_search();
|
||||
}
|
||||
KeyCode::Char('n') if ctrl => {
|
||||
if editor.completion_active() {
|
||||
editor.completion_next();
|
||||
} else if editor.sample_finder_active() {
|
||||
editor.sample_finder_next();
|
||||
} else {
|
||||
editor.search_next();
|
||||
}
|
||||
}
|
||||
KeyCode::Char('p') if ctrl => {
|
||||
if editor.completion_active() {
|
||||
editor.completion_prev();
|
||||
} else if editor.sample_finder_active() {
|
||||
editor.sample_finder_prev();
|
||||
} else {
|
||||
editor.search_prev();
|
||||
}
|
||||
}
|
||||
KeyCode::Char('s') if ctrl => {
|
||||
ctx.dispatch(AppCommand::ToggleEditorStack);
|
||||
}
|
||||
KeyCode::Char('r') if ctrl => {
|
||||
let script = ctx.app.editor_ctx.editor.lines().join("\n");
|
||||
match ctx
|
||||
.app
|
||||
.execute_script_oneshot(&script, ctx.link, ctx.audio_tx)
|
||||
{
|
||||
Ok(()) => ctx
|
||||
.app
|
||||
.ui
|
||||
.flash("Executed", 100, crate::state::FlashKind::Info),
|
||||
Err(e) => ctx.app.ui.flash(
|
||||
&format!("Error: {e}"),
|
||||
200,
|
||||
crate::state::FlashKind::Error,
|
||||
),
|
||||
}
|
||||
}
|
||||
KeyCode::Char('a') if ctrl => {
|
||||
editor.select_all();
|
||||
}
|
||||
KeyCode::Char('c') if ctrl => {
|
||||
editor.copy();
|
||||
}
|
||||
KeyCode::Char('x') if ctrl => {
|
||||
editor.cut();
|
||||
}
|
||||
KeyCode::Char('v') if ctrl => {
|
||||
editor.paste();
|
||||
}
|
||||
KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down if shift => {
|
||||
if !editor.is_selecting() {
|
||||
editor.start_selection();
|
||||
}
|
||||
editor.input(Event::Key(key));
|
||||
}
|
||||
_ => {
|
||||
editor.input(Event::Key(key));
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.app.editor_ctx.show_stack {
|
||||
crate::services::stack_preview::update_cache(&ctx.app.editor_ctx);
|
||||
}
|
||||
}
|
||||
Modal::Preview => match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('p') => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Left => ctx.dispatch(AppCommand::PrevStep),
|
||||
KeyCode::Right => ctx.dispatch(AppCommand::NextStep),
|
||||
KeyCode::Up => ctx.dispatch(AppCommand::StepUp),
|
||||
KeyCode::Down => ctx.dispatch(AppCommand::StepDown),
|
||||
_ => {}
|
||||
},
|
||||
Modal::PatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
field,
|
||||
name,
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
} => {
|
||||
let (bank, pattern) = (*bank, *pattern);
|
||||
match key.code {
|
||||
KeyCode::Up => *field = field.prev(),
|
||||
KeyCode::Down | KeyCode::Tab => *field = field.next(),
|
||||
KeyCode::Left => match field {
|
||||
PatternPropsField::Speed => *speed = speed.prev(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.prev(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Right => match field {
|
||||
PatternPropsField::Speed => *speed = speed.next(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.next(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Char(c) => match field {
|
||||
PatternPropsField::Name => name.push(c),
|
||||
PatternPropsField::Length if c.is_ascii_digit() => length.push(c),
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Backspace => match field {
|
||||
PatternPropsField::Name => {
|
||||
name.pop();
|
||||
}
|
||||
PatternPropsField::Length => {
|
||||
length.pop();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
let name_val = if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.clone())
|
||||
};
|
||||
let length_val = length.parse().ok();
|
||||
let speed_val = *speed;
|
||||
let quant_val = *quantization;
|
||||
let sync_val = *sync_mode;
|
||||
ctx.dispatch(AppCommand::StagePatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
name: name_val,
|
||||
length: length_val,
|
||||
speed: speed_val,
|
||||
quantization: quant_val,
|
||||
sync_mode: sync_val,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => {
|
||||
let bindings_count = crate::views::keybindings::bindings_for(ctx.app.page).len();
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('?') => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
*scroll = scroll.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
*scroll = (*scroll + 1).min(bindings_count.saturating_sub(1));
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
*scroll = scroll.saturating_sub(10);
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
*scroll = (*scroll + 10).min(bindings_count.saturating_sub(1));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::EuclideanDistribution {
|
||||
bank,
|
||||
pattern,
|
||||
source_step,
|
||||
field,
|
||||
pulses,
|
||||
steps,
|
||||
rotation,
|
||||
} => {
|
||||
let (bank_val, pattern_val, source_step_val) = (*bank, *pattern, *source_step);
|
||||
match key.code {
|
||||
KeyCode::Up => *field = field.prev(),
|
||||
KeyCode::Down | KeyCode::Tab => *field = field.next(),
|
||||
KeyCode::Left => {
|
||||
let target = match field {
|
||||
EuclideanField::Pulses => pulses,
|
||||
EuclideanField::Steps => steps,
|
||||
EuclideanField::Rotation => rotation,
|
||||
};
|
||||
if let Ok(val) = target.parse::<usize>() {
|
||||
*target = val.saturating_sub(1).to_string();
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
let target = match field {
|
||||
EuclideanField::Pulses => pulses,
|
||||
EuclideanField::Steps => steps,
|
||||
EuclideanField::Rotation => rotation,
|
||||
};
|
||||
if let Ok(val) = target.parse::<usize>() {
|
||||
*target = (val + 1).min(128).to_string();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) if c.is_ascii_digit() => match field {
|
||||
EuclideanField::Pulses => pulses.push(c),
|
||||
EuclideanField::Steps => steps.push(c),
|
||||
EuclideanField::Rotation => rotation.push(c),
|
||||
},
|
||||
KeyCode::Backspace => match field {
|
||||
EuclideanField::Pulses => {
|
||||
pulses.pop();
|
||||
}
|
||||
EuclideanField::Steps => {
|
||||
steps.pop();
|
||||
}
|
||||
EuclideanField::Rotation => {
|
||||
rotation.pop();
|
||||
}
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
let pulses_val: usize = pulses.parse().unwrap_or(0);
|
||||
let steps_val: usize = steps.parse().unwrap_or(0);
|
||||
let rotation_val: usize = rotation.parse().unwrap_or(0);
|
||||
if pulses_val > 0 && steps_val > 0 && pulses_val <= steps_val {
|
||||
ctx.dispatch(AppCommand::ApplyEuclideanDistribution {
|
||||
bank: bank_val,
|
||||
pattern: pattern_val,
|
||||
source_step: source_step_val,
|
||||
pulses: pulses_val,
|
||||
steps: steps_val,
|
||||
rotation: rotation_val,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SetStatus(
|
||||
"Invalid: pulses must be > 0 and <= steps".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::None => unreachable!(),
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn execute_confirm(ctx: &mut InputContext, action: &ConfirmAction) -> InputResult {
|
||||
match action {
|
||||
ConfirmAction::Quit => return InputResult::Quit,
|
||||
ConfirmAction::DeleteStep { bank, pattern, step } => {
|
||||
ctx.dispatch(AppCommand::DeleteStep { bank: *bank, pattern: *pattern, step: *step });
|
||||
}
|
||||
ConfirmAction::DeleteSteps { bank, pattern, steps } => {
|
||||
ctx.dispatch(AppCommand::DeleteSteps { bank: *bank, pattern: *pattern, steps: steps.clone() });
|
||||
}
|
||||
ConfirmAction::ResetPattern { bank, pattern } => {
|
||||
ctx.dispatch(AppCommand::ResetPattern { bank: *bank, pattern: *pattern });
|
||||
}
|
||||
ConfirmAction::ResetBank { bank } => {
|
||||
ctx.dispatch(AppCommand::ResetBank { bank: *bank });
|
||||
}
|
||||
ConfirmAction::ResetPatterns { bank, patterns } => {
|
||||
ctx.dispatch(AppCommand::ResetPatterns { bank: *bank, patterns: patterns.clone() });
|
||||
}
|
||||
ConfirmAction::ResetBanks { banks } => {
|
||||
ctx.dispatch(AppCommand::ResetBanks { banks: banks.clone() });
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn rename_command(target: &RenameTarget, name: Option<String>) -> AppCommand {
|
||||
match target {
|
||||
RenameTarget::Bank { bank } => AppCommand::RenameBank { bank: *bank, name },
|
||||
RenameTarget::Pattern { bank, pattern } => AppCommand::RenamePattern {
|
||||
bank: *bank, pattern: *pattern, name,
|
||||
},
|
||||
RenameTarget::Step { bank, pattern, step } => AppCommand::RenameStep {
|
||||
bank: *bank, pattern: *pattern, step: *step, name,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user