Files
Cagire/src/app/dispatch.rs
2026-02-25 20:31:36 +01:00

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