//! 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(), } } }