466 lines
21 KiB
Rust
466 lines
21 KiB
Rust
//! Routes `AppCommand` variants to the appropriate `App` methods.
|
|
|
|
use crate::commands::AppCommand;
|
|
use crate::engine::{LinkState, SequencerSnapshot};
|
|
use crate::model::bp_label;
|
|
use crate::services::{dict_nav, euclidean, help_nav, pattern_editor};
|
|
use crate::state::{undo::UndoEntry, FlashKind, Modal, StagedPropChange};
|
|
|
|
use super::App;
|
|
|
|
impl App {
|
|
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
|
match cmd {
|
|
AppCommand::Undo => {
|
|
if let Some(entry) = self.undo.pop_undo() {
|
|
let reverse = self.apply_undo_entry(entry);
|
|
self.undo.push_redo(reverse);
|
|
self.ui.flash("Undo", 100, FlashKind::Info);
|
|
}
|
|
return;
|
|
}
|
|
AppCommand::Redo => {
|
|
if let Some(entry) = self.undo.pop_redo() {
|
|
let reverse = self.apply_undo_entry(entry);
|
|
self.undo.undo_stack.push(reverse);
|
|
self.ui.flash("Redo", 100, FlashKind::Info);
|
|
}
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if let Some(scope) = self.undoable_scope(&cmd) {
|
|
let cursor = (self.editor_ctx.bank, self.editor_ctx.pattern, self.editor_ctx.step);
|
|
self.undo.push(UndoEntry { scope, cursor });
|
|
}
|
|
|
|
match cmd {
|
|
AppCommand::Undo | AppCommand::Redo => unreachable!(),
|
|
|
|
// Playback
|
|
AppCommand::TogglePlaying => self.toggle_playing(link),
|
|
AppCommand::TempoUp => self.tempo_up(link),
|
|
AppCommand::TempoDown => self.tempo_down(link),
|
|
|
|
// Navigation
|
|
AppCommand::NextStep => self.next_step(),
|
|
AppCommand::PrevStep => self.prev_step(),
|
|
AppCommand::StepUp => self.step_up(),
|
|
AppCommand::StepDown => self.step_down(),
|
|
|
|
// Pattern editing
|
|
AppCommand::ToggleSteps => self.toggle_steps(),
|
|
AppCommand::LengthIncrease => self.length_increase(),
|
|
AppCommand::LengthDecrease => self.length_decrease(),
|
|
AppCommand::SpeedIncrease => self.speed_increase(),
|
|
AppCommand::SpeedDecrease => self.speed_decrease(),
|
|
AppCommand::SetLength {
|
|
bank,
|
|
pattern,
|
|
length,
|
|
} => {
|
|
let (change, new_len) = pattern_editor::set_length(
|
|
&mut self.project_state.project,
|
|
bank,
|
|
pattern,
|
|
length,
|
|
);
|
|
if self.editor_ctx.bank == bank
|
|
&& self.editor_ctx.pattern == pattern
|
|
&& self.editor_ctx.step >= new_len
|
|
{
|
|
self.editor_ctx.step = new_len - 1;
|
|
}
|
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
|
}
|
|
AppCommand::SetSpeed {
|
|
bank,
|
|
pattern,
|
|
speed,
|
|
} => {
|
|
let change = pattern_editor::set_speed(
|
|
&mut self.project_state.project,
|
|
bank,
|
|
pattern,
|
|
speed,
|
|
);
|
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
|
}
|
|
|
|
// Script editing
|
|
AppCommand::SaveEditorToStep => self.save_editor_to_step(),
|
|
AppCommand::CompileCurrentStep => self.compile_current_step(link),
|
|
AppCommand::DeleteStep {
|
|
bank,
|
|
pattern,
|
|
step,
|
|
} => self.delete_step(bank, pattern, step),
|
|
AppCommand::DeleteSteps {
|
|
bank,
|
|
pattern,
|
|
steps,
|
|
} => self.delete_steps(bank, pattern, &steps),
|
|
AppCommand::ResetPattern { bank, pattern } => self.reset_pattern(bank, pattern),
|
|
AppCommand::ResetBank { bank } => self.reset_bank(bank),
|
|
AppCommand::CopyPattern { bank, pattern } => self.copy_pattern(bank, pattern),
|
|
AppCommand::PastePattern { bank, pattern } => self.paste_pattern(bank, pattern),
|
|
AppCommand::CopyPatterns { bank, patterns } => self.copy_patterns(bank, &patterns),
|
|
AppCommand::PastePatterns { bank, start } => self.paste_patterns(bank, start),
|
|
AppCommand::CopyBank { bank } => self.copy_bank(bank),
|
|
AppCommand::PasteBank { bank } => self.paste_bank(bank),
|
|
AppCommand::CopyBanks { banks } => self.copy_banks(&banks),
|
|
AppCommand::PasteBanks { start } => self.paste_banks(start),
|
|
AppCommand::ResetPatterns { bank, patterns } => self.reset_patterns(bank, &patterns),
|
|
AppCommand::ResetBanks { banks } => self.reset_banks(&banks),
|
|
|
|
// Reorder
|
|
AppCommand::ShiftPatternsUp => self.shift_patterns_up(),
|
|
AppCommand::ShiftPatternsDown => self.shift_patterns_down(),
|
|
|
|
// Clipboard
|
|
AppCommand::HardenSteps => self.harden_steps(),
|
|
AppCommand::CopySteps => self.copy_steps(),
|
|
AppCommand::PasteSteps => self.paste_steps(link),
|
|
AppCommand::LinkPasteSteps => self.link_paste_steps(),
|
|
AppCommand::DuplicateSteps => self.duplicate_steps(link),
|
|
|
|
// Pattern playback (staging)
|
|
AppCommand::ClearStagedChanges => self.clear_staged_changes(),
|
|
|
|
// Project
|
|
AppCommand::RenameBank { bank, name } => {
|
|
self.project_state.project.banks[bank].name = name;
|
|
}
|
|
AppCommand::RenamePattern {
|
|
bank,
|
|
pattern,
|
|
name,
|
|
} => {
|
|
self.project_state.project.banks[bank].patterns[pattern].name = name;
|
|
}
|
|
AppCommand::DescribePattern {
|
|
bank,
|
|
pattern,
|
|
description,
|
|
} => {
|
|
self.project_state.project.banks[bank].patterns[pattern].description = description;
|
|
}
|
|
AppCommand::RenameStep {
|
|
bank,
|
|
pattern,
|
|
step,
|
|
name,
|
|
} => {
|
|
if let Some(s) =
|
|
self.project_state.project.banks[bank].patterns[pattern].step_mut(step)
|
|
{
|
|
s.name = name;
|
|
}
|
|
self.project_state.mark_dirty(bank, pattern);
|
|
}
|
|
AppCommand::Save(path) => self.save(path, link, snapshot),
|
|
AppCommand::Load(path) => self.load(path, link),
|
|
|
|
// UI
|
|
AppCommand::SetStatus(msg) => self.ui.set_status(msg),
|
|
AppCommand::ClearStatus => self.ui.clear_status(),
|
|
AppCommand::OpenModal(modal) => {
|
|
if matches!(modal, Modal::Editor) {
|
|
let pattern = &self.project_state.project.banks[self.editor_ctx.bank].patterns
|
|
[self.editor_ctx.pattern];
|
|
if let Some(source) = pattern.step(self.editor_ctx.step).and_then(|s| s.source)
|
|
{
|
|
self.editor_ctx.step = source as usize;
|
|
}
|
|
self.load_step_to_editor();
|
|
}
|
|
self.ui.modal = modal;
|
|
}
|
|
AppCommand::CloseModal => self.ui.modal = Modal::None,
|
|
AppCommand::OpenPatternModal(field) => self.open_pattern_modal(field),
|
|
AppCommand::OpenPatternPropsModal { bank, pattern } => {
|
|
self.open_pattern_props_modal(bank, pattern);
|
|
}
|
|
AppCommand::StagePatternProps {
|
|
bank,
|
|
pattern,
|
|
name,
|
|
description,
|
|
length,
|
|
speed,
|
|
quantization,
|
|
sync_mode,
|
|
follow_up,
|
|
} => {
|
|
self.playback.staged_prop_changes.insert(
|
|
(bank, pattern),
|
|
StagedPropChange {
|
|
name,
|
|
description,
|
|
length,
|
|
speed,
|
|
quantization,
|
|
sync_mode,
|
|
follow_up,
|
|
},
|
|
);
|
|
self.ui
|
|
.set_status(format!("{} props staged", bp_label(bank, pattern)));
|
|
}
|
|
|
|
// Page navigation
|
|
AppCommand::PageLeft => {
|
|
self.page.left();
|
|
self.maybe_show_onboarding();
|
|
}
|
|
AppCommand::PageRight => {
|
|
self.page.right();
|
|
self.maybe_show_onboarding();
|
|
}
|
|
AppCommand::PageUp => {
|
|
self.page.up();
|
|
self.maybe_show_onboarding();
|
|
}
|
|
AppCommand::PageDown => {
|
|
self.page.down();
|
|
self.maybe_show_onboarding();
|
|
}
|
|
AppCommand::GoToPage(page) => {
|
|
self.page = page;
|
|
self.maybe_show_onboarding();
|
|
}
|
|
|
|
// Help navigation
|
|
AppCommand::HelpToggleFocus => help_nav::toggle_focus(&mut self.ui),
|
|
AppCommand::HelpNextTopic(n) => help_nav::next_topic(&mut self.ui, n),
|
|
AppCommand::HelpPrevTopic(n) => help_nav::prev_topic(&mut self.ui, n),
|
|
AppCommand::HelpScrollDown(n) => help_nav::scroll_down(&mut self.ui, n),
|
|
AppCommand::HelpScrollUp(n) => help_nav::scroll_up(&mut self.ui, n),
|
|
AppCommand::HelpActivateSearch => help_nav::activate_search(&mut self.ui),
|
|
AppCommand::HelpClearSearch => help_nav::clear_search(&mut self.ui),
|
|
AppCommand::HelpSearchInput(c) => help_nav::search_input(&mut self.ui, c),
|
|
AppCommand::HelpSearchBackspace => help_nav::search_backspace(&mut self.ui),
|
|
AppCommand::HelpSearchConfirm => help_nav::search_confirm(&mut self.ui),
|
|
|
|
// Dictionary navigation
|
|
AppCommand::DictToggleFocus => dict_nav::toggle_focus(&mut self.ui),
|
|
AppCommand::DictNextCategory => dict_nav::next_category(&mut self.ui),
|
|
AppCommand::DictPrevCategory => dict_nav::prev_category(&mut self.ui),
|
|
AppCommand::DictScrollDown(n) => dict_nav::scroll_down(&mut self.ui, n),
|
|
AppCommand::DictScrollUp(n) => dict_nav::scroll_up(&mut self.ui, n),
|
|
AppCommand::DictActivateSearch => dict_nav::activate_search(&mut self.ui),
|
|
AppCommand::DictClearSearch => dict_nav::clear_search(&mut self.ui),
|
|
AppCommand::DictSearchInput(c) => dict_nav::search_input(&mut self.ui, c),
|
|
AppCommand::DictSearchBackspace => dict_nav::search_backspace(&mut self.ui),
|
|
AppCommand::DictSearchConfirm => dict_nav::search_confirm(&mut self.ui),
|
|
|
|
// Patterns view
|
|
AppCommand::PatternsCursorLeft => self.patterns_nav.move_left(),
|
|
AppCommand::PatternsCursorRight => self.patterns_nav.move_right(),
|
|
AppCommand::PatternsCursorUp => self.patterns_nav.move_up(),
|
|
AppCommand::PatternsCursorDown => self.patterns_nav.move_down(),
|
|
AppCommand::PatternsEnter => {
|
|
let bank = self.patterns_nav.selected_bank();
|
|
let pattern = self.patterns_nav.selected_pattern();
|
|
self.select_edit_bank(bank);
|
|
self.select_edit_pattern(pattern);
|
|
self.page.down();
|
|
self.maybe_show_onboarding();
|
|
}
|
|
AppCommand::PatternsBack => {
|
|
self.page.down();
|
|
self.maybe_show_onboarding();
|
|
}
|
|
|
|
// Mute/Solo (staged)
|
|
AppCommand::StageMute { bank, pattern } => self.playback.stage_mute(bank, pattern),
|
|
AppCommand::StageSolo { bank, pattern } => self.playback.stage_solo(bank, pattern),
|
|
AppCommand::ClearMutes => {
|
|
self.playback.clear_staged_mutes();
|
|
self.mute.clear_mute();
|
|
}
|
|
AppCommand::ClearSolos => {
|
|
self.playback.clear_staged_solos();
|
|
self.mute.clear_solo();
|
|
}
|
|
|
|
// UI state
|
|
AppCommand::ClearMinimap => self.ui.dismiss_minimap(),
|
|
AppCommand::HideTitle => {
|
|
self.ui.show_title = false;
|
|
self.maybe_show_onboarding();
|
|
}
|
|
AppCommand::ToggleEditorStack => {
|
|
self.editor_ctx.show_stack = !self.editor_ctx.show_stack;
|
|
if self.editor_ctx.show_stack {
|
|
crate::services::stack_preview::update_cache(&self.editor_ctx);
|
|
}
|
|
}
|
|
AppCommand::SetColorScheme(scheme) => {
|
|
self.ui.color_scheme = scheme;
|
|
let palette = scheme.to_palette();
|
|
let rotated = cagire_ratatui::theme::transform::rotate_palette(&palette, self.ui.hue_rotation);
|
|
crate::theme::set(rotated);
|
|
self.ui.invalidate_help_cache();
|
|
}
|
|
AppCommand::SetHueRotation(degrees) => {
|
|
self.ui.hue_rotation = degrees;
|
|
let palette = self.ui.color_scheme.to_palette();
|
|
let rotated = cagire_ratatui::theme::transform::rotate_palette(&palette, degrees);
|
|
crate::theme::set(rotated);
|
|
self.ui.invalidate_help_cache();
|
|
}
|
|
AppCommand::ToggleRuntimeHighlight => {
|
|
self.ui.runtime_highlight = !self.ui.runtime_highlight;
|
|
}
|
|
AppCommand::ToggleCompletion => {
|
|
self.ui.show_completion = !self.ui.show_completion;
|
|
self.editor_ctx
|
|
.editor
|
|
.set_completion_enabled(self.ui.show_completion);
|
|
}
|
|
AppCommand::SetFont(f) => self.ui.font = f,
|
|
AppCommand::SetZoomFactor(z) => self.ui.zoom_factor = z,
|
|
AppCommand::SetWindowSize(w, h) => {
|
|
self.ui.window_width = w;
|
|
self.ui.window_height = h;
|
|
}
|
|
AppCommand::ToggleLiveKeysFill => self.live_keys.flip_fill(),
|
|
|
|
// Panel
|
|
AppCommand::ClosePanel => {
|
|
self.panel.visible = false;
|
|
self.panel.focus = crate::state::PanelFocus::Main;
|
|
}
|
|
|
|
// Direct navigation (mouse)
|
|
AppCommand::GoToStep(step) => {
|
|
let len = self.current_edit_pattern().length;
|
|
if step < len {
|
|
self.editor_ctx.step = step;
|
|
self.editor_ctx.clear_selection();
|
|
self.load_step_to_editor();
|
|
}
|
|
}
|
|
AppCommand::PatternsSelectBank(bank) => {
|
|
self.patterns_nav.bank_cursor = bank;
|
|
self.patterns_nav.clear_selection();
|
|
}
|
|
AppCommand::PatternsSelectPattern(pattern) => {
|
|
self.patterns_nav.pattern_cursor = pattern;
|
|
self.patterns_nav.clear_selection();
|
|
}
|
|
AppCommand::HelpSelectTopic(i) => help_nav::select_topic(&mut self.ui, i),
|
|
AppCommand::DictSelectCategory(i) => dict_nav::select_category(&mut self.ui, i),
|
|
|
|
// Selection
|
|
AppCommand::SetSelectionAnchor(step) => {
|
|
self.editor_ctx.selection_anchor = Some(step);
|
|
}
|
|
|
|
// Audio settings (engine page)
|
|
AppCommand::AudioSetSection(section) => self.audio.section = section,
|
|
AppCommand::AudioNextSection => self.audio.next_section(self.plugin_mode),
|
|
AppCommand::AudioPrevSection => self.audio.prev_section(self.plugin_mode),
|
|
AppCommand::AudioOutputListUp => self.audio.output_list.move_up(),
|
|
AppCommand::AudioOutputListDown(count) => self.audio.output_list.move_down(count),
|
|
AppCommand::AudioOutputPageUp => self.audio.output_list.page_up(),
|
|
AppCommand::AudioOutputPageDown(count) => self.audio.output_list.page_down(count),
|
|
AppCommand::AudioInputListUp => self.audio.input_list.move_up(),
|
|
AppCommand::AudioInputListDown(count) => self.audio.input_list.move_down(count),
|
|
AppCommand::AudioInputPageDown(count) => self.audio.input_list.page_down(count),
|
|
AppCommand::AudioSettingNext => self.audio.next_setting(self.plugin_mode),
|
|
AppCommand::AudioSettingPrev => self.audio.prev_setting(self.plugin_mode),
|
|
AppCommand::SetOutputDevice(name) => self.audio.config.output_device = Some(name),
|
|
AppCommand::SetInputDevice(name) => self.audio.config.input_device = Some(name),
|
|
AppCommand::SetDeviceKind(kind) => self.audio.device_kind = kind,
|
|
AppCommand::AdjustAudioSetting { setting, delta } => {
|
|
use crate::state::SettingKind;
|
|
match setting {
|
|
SettingKind::Channels => self.audio.adjust_channels(delta as i16),
|
|
SettingKind::BufferSize => self.audio.adjust_buffer_size(delta),
|
|
SettingKind::Polyphony => self.audio.adjust_max_voices(delta),
|
|
SettingKind::Nudge => {
|
|
self.metrics.nudge_ms =
|
|
(self.metrics.nudge_ms + delta as f64).clamp(-50.0, 50.0);
|
|
}
|
|
}
|
|
}
|
|
AppCommand::AudioTriggerRestart => self.audio.trigger_restart(),
|
|
AppCommand::RemoveLastSamplePath => self.audio.remove_last_sample_path(),
|
|
AppCommand::AudioRefreshDevices => self.audio.refresh_devices(),
|
|
|
|
// Options page
|
|
AppCommand::OptionsNextFocus => self.options.next_focus(self.plugin_mode),
|
|
AppCommand::OptionsPrevFocus => self.options.prev_focus(self.plugin_mode),
|
|
AppCommand::OptionsSetFocus(focus) => self.options.focus = focus,
|
|
AppCommand::ToggleRefreshRate => self.audio.toggle_refresh_rate(),
|
|
AppCommand::ToggleScope => self.audio.config.show_scope = !self.audio.config.show_scope,
|
|
AppCommand::ToggleSpectrum => self.audio.config.show_spectrum = !self.audio.config.show_spectrum,
|
|
AppCommand::ToggleLissajous => self.audio.config.show_lissajous = !self.audio.config.show_lissajous,
|
|
AppCommand::TogglePreview => self.audio.config.show_preview = !self.audio.config.show_preview,
|
|
AppCommand::SetGainBoost(g) => self.audio.config.gain_boost = g,
|
|
AppCommand::ToggleNormalizeViz => self.audio.config.normalize_viz = !self.audio.config.normalize_viz,
|
|
AppCommand::TogglePerformanceMode => self.ui.performance_mode = !self.ui.performance_mode,
|
|
|
|
// Metrics
|
|
AppCommand::ResetPeakVoices => self.metrics.peak_voices = 0,
|
|
|
|
// Euclidean distribution
|
|
AppCommand::ApplyEuclideanDistribution {
|
|
bank,
|
|
pattern,
|
|
source_step,
|
|
pulses,
|
|
steps,
|
|
rotation,
|
|
} => {
|
|
let targets = euclidean::apply_distribution(
|
|
&mut self.project_state.project,
|
|
bank,
|
|
pattern,
|
|
source_step,
|
|
pulses,
|
|
steps,
|
|
rotation,
|
|
);
|
|
let created_count = targets.len();
|
|
self.project_state.mark_dirty(bank, pattern);
|
|
for &target in &targets {
|
|
let saved = self.editor_ctx.step;
|
|
self.editor_ctx.step = target;
|
|
self.compile_current_step(link);
|
|
self.editor_ctx.step = saved;
|
|
}
|
|
self.load_step_to_editor();
|
|
self.ui.flash(
|
|
&format!("Created {created_count} linked steps (E({pulses},{steps}))"),
|
|
200,
|
|
FlashKind::Success,
|
|
);
|
|
}
|
|
|
|
// Onboarding
|
|
AppCommand::DismissOnboarding => {
|
|
let name = self.page.name().to_string();
|
|
if !self.ui.onboarding_dismissed.contains(&name) {
|
|
self.ui.onboarding_dismissed.push(name);
|
|
}
|
|
}
|
|
AppCommand::ResetOnboarding => self.ui.onboarding_dismissed.clear(),
|
|
AppCommand::GoToHelpTopic(topic) => {
|
|
self.ui.modal = Modal::None;
|
|
self.page = crate::page::Page::Help;
|
|
self.ui.help_topic = topic;
|
|
}
|
|
|
|
// Prelude
|
|
AppCommand::OpenPreludeEditor => self.open_prelude_editor(),
|
|
AppCommand::SavePrelude => self.save_prelude(),
|
|
AppCommand::EvaluatePrelude => self.evaluate_prelude(link),
|
|
AppCommand::ClosePreludeEditor => self.close_prelude_editor(),
|
|
}
|
|
}
|
|
}
|