use rand::rngs::StdRng; use rand::SeedableRng; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use crossbeam_channel::Sender; use crate::commands::AppCommand; use crate::engine::{ LinkState, PatternChange, PatternSnapshot, SeqCommand, SequencerSnapshot, StepSnapshot, }; use crate::midi::MidiState; use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, StepContext, Variables}; use crate::page::Page; use crate::services::pattern_editor; use crate::settings::Settings; use crate::state::{ AudioSettings, DictFocus, EditorContext, FlashKind, Focus, LiveKeyState, Metrics, Modal, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState, ProjectState, StagedChange, UiState, }; use crate::views::{dict_view, help_view}; const STEPS_PER_PAGE: usize = 32; pub struct App { pub project_state: ProjectState, pub ui: UiState, pub playback: PlaybackState, pub page: Page, pub editor_ctx: EditorContext, pub patterns_nav: PatternsNav, pub metrics: Metrics, pub script_engine: ScriptEngine, pub variables: Variables, pub dict: Dictionary, pub rng: Rng, pub live_keys: Arc, pub clipboard: Option, pub copied_pattern: Option, pub copied_bank: Option, pub audio: AudioSettings, pub options: OptionsState, pub panel: PanelState, pub midi: MidiState, } impl Default for App { fn default() -> Self { Self::new() } } impl App { pub fn new() -> Self { let variables = Arc::new(Mutex::new(HashMap::new())); let dict = Arc::new(Mutex::new(HashMap::new())); let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0))); let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), Arc::clone(&rng)); let live_keys = Arc::new(LiveKeyState::new()); Self { project_state: ProjectState::default(), ui: UiState::default(), playback: PlaybackState::default(), page: Page::default(), editor_ctx: EditorContext::default(), patterns_nav: PatternsNav::default(), metrics: Metrics::default(), variables, dict, rng, live_keys, script_engine, clipboard: arboard::Clipboard::new().ok(), copied_pattern: None, copied_bank: None, audio: AudioSettings::default(), options: OptionsState::default(), panel: PanelState::default(), midi: MidiState::new(), } } pub fn save_settings(&self, link: &LinkState) { let settings = Settings { audio: crate::settings::AudioSettings { output_device: self.audio.config.output_device.clone(), input_device: self.audio.config.input_device.clone(), channels: self.audio.config.channels, buffer_size: self.audio.config.buffer_size, max_voices: self.audio.config.max_voices, lookahead_ms: self.audio.config.lookahead_ms, }, display: crate::settings::DisplaySettings { fps: self.audio.config.refresh_rate.to_fps(), runtime_highlight: self.ui.runtime_highlight, show_scope: self.audio.config.show_scope, show_spectrum: self.audio.config.show_spectrum, show_completion: self.ui.show_completion, flash_brightness: self.ui.flash_brightness, color_scheme: self.ui.color_scheme, ..Default::default() }, link: crate::settings::LinkSettings { enabled: link.is_enabled(), tempo: link.tempo(), quantum: link.quantum(), }, midi: crate::settings::MidiSettings { output_devices: { let outputs = crate::midi::list_midi_outputs(); self.midi.selected_outputs.iter() .map(|opt| opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone())).unwrap_or_default()) .collect() }, input_devices: { let inputs = crate::midi::list_midi_inputs(); self.midi.selected_inputs.iter() .map(|opt| opt.and_then(|idx| inputs.get(idx).map(|d| d.name.clone())).unwrap_or_default()) .collect() }, }, }; settings.save(); } fn current_bank_pattern(&self) -> (usize, usize) { (self.editor_ctx.bank, self.editor_ctx.pattern) } pub fn mark_all_patterns_dirty(&mut self) { self.project_state.mark_all_dirty(); } pub fn toggle_playing(&mut self) { self.playback.toggle(); } pub fn tempo_up(&self, link: &LinkState) { let current = link.tempo(); link.set_tempo((current + 1.0).min(300.0)); } pub fn tempo_down(&self, link: &LinkState) { let current = link.tempo(); link.set_tempo((current - 1.0).max(20.0)); } pub fn toggle_focus(&mut self, link: &LinkState) { match self.editor_ctx.focus { Focus::Sequencer => { self.editor_ctx.focus = Focus::Editor; self.load_step_to_editor(); } Focus::Editor => { self.save_editor_to_step(); self.compile_current_step(link); self.editor_ctx.focus = Focus::Sequencer; } } } pub fn current_edit_pattern(&self) -> &Pattern { let (bank, pattern) = self.current_bank_pattern(); self.project_state.project.pattern_at(bank, pattern) } pub fn next_step(&mut self) { let len = self.current_edit_pattern().length; self.editor_ctx.step = (self.editor_ctx.step + 1) % len; self.load_step_to_editor(); } pub fn prev_step(&mut self) { let len = self.current_edit_pattern().length; self.editor_ctx.step = (self.editor_ctx.step + len - 1) % len; self.load_step_to_editor(); } pub fn step_up(&mut self) { let len = self.current_edit_pattern().length; let page_start = (self.editor_ctx.step / STEPS_PER_PAGE) * STEPS_PER_PAGE; let steps_on_page = (page_start + STEPS_PER_PAGE).min(len) - page_start; let num_rows = steps_on_page.div_ceil(8); let steps_per_row = steps_on_page.div_ceil(num_rows); if self.editor_ctx.step >= steps_per_row { self.editor_ctx.step -= steps_per_row; } else { self.editor_ctx.step = (self.editor_ctx.step + len - steps_per_row) % len; } self.load_step_to_editor(); } pub fn step_down(&mut self) { let len = self.current_edit_pattern().length; let page_start = (self.editor_ctx.step / STEPS_PER_PAGE) * STEPS_PER_PAGE; let steps_on_page = (page_start + STEPS_PER_PAGE).min(len) - page_start; let num_rows = steps_on_page.div_ceil(8); let steps_per_row = steps_on_page.div_ceil(num_rows); self.editor_ctx.step = (self.editor_ctx.step + steps_per_row) % len; self.load_step_to_editor(); } pub fn toggle_steps(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let indices: Vec = match self.editor_ctx.selection_range() { Some(range) => range.collect(), None => vec![self.editor_ctx.step], }; for idx in indices { pattern_editor::toggle_step( &mut self.project_state.project, bank, pattern, idx, ); } self.project_state.mark_dirty(bank, pattern); } pub fn length_increase(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let (change, _) = pattern_editor::increase_length(&mut self.project_state.project, bank, pattern); self.project_state.mark_dirty(change.bank, change.pattern); } pub fn length_decrease(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let (change, new_len) = pattern_editor::decrease_length(&mut self.project_state.project, bank, pattern); if self.editor_ctx.step >= new_len { self.editor_ctx.step = new_len - 1; self.load_step_to_editor(); } self.project_state.mark_dirty(change.bank, change.pattern); } pub fn speed_increase(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let change = pattern_editor::increase_speed(&mut self.project_state.project, bank, pattern); self.project_state.mark_dirty(change.bank, change.pattern); } pub fn speed_decrease(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let change = pattern_editor::decrease_speed(&mut self.project_state.project, bank, pattern); self.project_state.mark_dirty(change.bank, change.pattern); } fn load_step_to_editor(&mut self) { let (bank, pattern) = self.current_bank_pattern(); if let Some(script) = pattern_editor::get_step_script( &self.project_state.project, bank, pattern, self.editor_ctx.step, ) { let lines: Vec = if script.is_empty() { vec![String::new()] } else { script.lines().map(String::from).collect() }; self.editor_ctx.editor.set_content(lines); let candidates = model::WORDS .iter() .map(|w| cagire_ratatui::CompletionCandidate { name: w.name.to_string(), signature: w.stack.to_string(), description: w.desc.to_string(), example: w.example.to_string(), }) .collect(); self.editor_ctx.editor.set_candidates(candidates); self.editor_ctx .editor .set_completion_enabled(self.ui.show_completion); } } pub fn save_editor_to_step(&mut self) { let text = self.editor_ctx.editor.content(); let (bank, pattern) = self.current_bank_pattern(); let change = pattern_editor::set_step_script( &mut self.project_state.project, bank, pattern, self.editor_ctx.step, text, ); self.project_state.mark_dirty(change.bank, change.pattern); } pub fn execute_script_oneshot( &self, script: &str, link: &LinkState, audio_tx: &arc_swap::ArcSwap>, ) -> Result<(), String> { let (bank, pattern) = self.current_bank_pattern(); let step_idx = self.editor_ctx.step; let speed = self .project_state .project .pattern_at(bank, pattern) .speed .multiplier(); let ctx = StepContext { step: step_idx, beat: link.beat(), bank, pattern, tempo: link.tempo(), phase: link.phase(), slot: 0, runs: 0, iter: 0, speed, fill: false, nudge_secs: 0.0, cc_memory: None, #[cfg(feature = "desktop")] mouse_x: 0.5, #[cfg(feature = "desktop")] mouse_y: 0.5, #[cfg(feature = "desktop")] mouse_down: 0.0, }; let cmds = self.script_engine.evaluate(script, &ctx)?; for cmd in cmds { let _ = audio_tx .load() .send(crate::engine::AudioCommand::Evaluate { cmd, time: None }); } Ok(()) } pub fn compile_current_step(&mut self, link: &LinkState) { let step_idx = self.editor_ctx.step; let (bank, pattern) = self.current_bank_pattern(); let script = pattern_editor::get_step_script(&self.project_state.project, bank, pattern, step_idx) .unwrap_or_default(); if script.trim().is_empty() { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = None; } return; } let speed = self .project_state .project .pattern_at(bank, pattern) .speed .multiplier(); let ctx = StepContext { step: step_idx, beat: link.beat(), bank, pattern, tempo: link.tempo(), phase: link.phase(), slot: 0, runs: 0, iter: 0, speed, fill: false, nudge_secs: 0.0, cc_memory: None, #[cfg(feature = "desktop")] mouse_x: 0.5, #[cfg(feature = "desktop")] mouse_y: 0.5, #[cfg(feature = "desktop")] mouse_down: 0.0, }; match self.script_engine.evaluate(&script, &ctx) { Ok(cmds) => { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = if cmds.is_empty() { None } else { Some(cmds.join("\n")) }; } self.ui.flash("Script compiled", 150, FlashKind::Info); } Err(e) => { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = None; } self.ui .flash(&format!("Script error: {e}"), 300, FlashKind::Error); } } } pub fn compile_all_steps(&mut self, link: &LinkState) { let pattern_len = self.current_edit_pattern().length; let (bank, pattern) = self.current_bank_pattern(); for step_idx in 0..pattern_len { let script = pattern_editor::get_step_script( &self.project_state.project, bank, pattern, step_idx, ) .unwrap_or_default(); if script.trim().is_empty() { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = None; } continue; } let speed = self .project_state .project .pattern_at(bank, pattern) .speed .multiplier(); let ctx = StepContext { step: step_idx, beat: 0.0, bank, pattern, tempo: link.tempo(), phase: 0.0, slot: 0, runs: 0, iter: 0, speed, fill: false, nudge_secs: 0.0, cc_memory: None, #[cfg(feature = "desktop")] mouse_x: 0.5, #[cfg(feature = "desktop")] mouse_y: 0.5, #[cfg(feature = "desktop")] mouse_down: 0.0, }; if let Ok(cmds) = self.script_engine.evaluate(&script, &ctx) { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = if cmds.is_empty() { None } else { Some(cmds.join("\n")) }; } } } } pub fn stage_pattern_toggle( &mut self, bank: usize, pattern: usize, snapshot: &SequencerSnapshot, ) { let is_playing = snapshot.is_playing(bank, pattern); let pattern_data = self.project_state.project.pattern_at(bank, pattern); let existing = self.playback.staged_changes.iter().position(|c| { c.change.pattern_id().bank == bank && c.change.pattern_id().pattern == pattern }); if let Some(idx) = existing { self.playback.staged_changes.remove(idx); self.ui .set_status(format!("B{:02}:P{:02} unstaged", bank + 1, pattern + 1)); } else if is_playing { self.playback.staged_changes.push(StagedChange { change: PatternChange::Stop { bank, pattern }, quantization: pattern_data.quantization, sync_mode: pattern_data.sync_mode, }); self.ui.set_status(format!( "B{:02}:P{:02} staged to stop", bank + 1, pattern + 1 )); } else { self.playback.staged_changes.push(StagedChange { change: PatternChange::Start { bank, pattern }, quantization: pattern_data.quantization, sync_mode: pattern_data.sync_mode, }); self.ui.set_status(format!( "B{:02}:P{:02} staged to play", bank + 1, pattern + 1 )); } } pub fn commit_staged_changes(&mut self) { if self.playback.staged_changes.is_empty() { self.ui.set_status("No changes to commit".to_string()); return; } let count = self.playback.staged_changes.len(); self.playback .queued_changes .append(&mut self.playback.staged_changes); self.ui.set_status(format!("Committed {count} changes")); } pub fn clear_staged_changes(&mut self) { if self.playback.staged_changes.is_empty() { return; } let count = self.playback.staged_changes.len(); self.playback.staged_changes.clear(); self.ui .set_status(format!("Cleared {count} staged changes")); } pub fn select_edit_pattern(&mut self, pattern: usize) { self.editor_ctx.pattern = pattern; self.editor_ctx.step = 0; self.load_step_to_editor(); } pub fn select_edit_bank(&mut self, bank: usize) { self.editor_ctx.bank = bank; self.editor_ctx.pattern = 0; self.editor_ctx.step = 0; self.load_step_to_editor(); } pub fn save(&mut self, path: PathBuf, link: &LinkState) { self.save_editor_to_step(); self.project_state.project.sample_paths = self.audio.config.sample_paths.clone(); self.project_state.project.tempo = link.tempo(); match model::save(&self.project_state.project, &path) { Ok(final_path) => { self.ui.set_status(format!("Saved: {}", final_path.display())); self.project_state.file_path = Some(final_path); } Err(e) => { self.ui.set_status(format!("Save error: {e}")); } } } pub fn load(&mut self, path: PathBuf, link: &LinkState) { match model::load(&path) { Ok(project) => { let tempo = project.tempo; self.project_state.project = project; self.editor_ctx.step = 0; self.load_step_to_editor(); self.compile_all_steps(link); self.mark_all_patterns_dirty(); link.set_tempo(tempo); self.ui.set_status(format!("Loaded: {}", path.display())); self.project_state.file_path = Some(path); } Err(e) => { self.ui.set_status(format!("Load error: {e}")); } } } pub fn delete_step(&mut self, bank: usize, pattern: usize, step: usize) { let pat = self.project_state.project.pattern_at_mut(bank, pattern); for s in &mut pat.steps { if s.source == Some(step) { s.source = None; s.script.clear(); s.command = None; } } let change = pattern_editor::set_step_script( &mut self.project_state.project, bank, pattern, step, String::new(), ); if let Some(s) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step) { s.command = None; s.source = None; } self.project_state.mark_dirty(change.bank, change.pattern); if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern && self.editor_ctx.step == step { self.load_step_to_editor(); } self.ui.flash("Step deleted", 150, FlashKind::Success); } pub fn delete_steps(&mut self, bank: usize, pattern: usize, steps: &[usize]) { for &step in steps { let pat = self.project_state.project.pattern_at_mut(bank, pattern); for s in &mut pat.steps { if s.source == Some(step) { s.source = None; s.script.clear(); s.command = None; } } let change = pattern_editor::set_step_script( &mut self.project_state.project, bank, pattern, step, String::new(), ); if let Some(s) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step) { s.command = None; s.source = None; } self.project_state.mark_dirty(change.bank, change.pattern); } if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern { self.load_step_to_editor(); } self.editor_ctx.clear_selection(); self.ui.flash( &format!("{} steps deleted", steps.len()), 150, FlashKind::Success, ); } pub fn reset_pattern(&mut self, bank: usize, pattern: usize) { self.project_state.project.banks[bank].patterns[pattern] = Pattern::default(); self.project_state.mark_dirty(bank, pattern); if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern { self.load_step_to_editor(); } self.ui.flash("Pattern reset", 150, FlashKind::Success); } pub fn reset_bank(&mut self, bank: usize) { self.project_state.project.banks[bank] = Bank::default(); for pattern in 0..self.project_state.project.banks[bank].patterns.len() { self.project_state.mark_dirty(bank, pattern); } if self.editor_ctx.bank == bank { self.load_step_to_editor(); } self.ui.flash("Bank reset", 150, FlashKind::Success); } pub fn copy_pattern(&mut self, bank: usize, pattern: usize) { let pat = self.project_state.project.banks[bank].patterns[pattern].clone(); self.copied_pattern = Some(pat); self.ui.flash("Pattern copied", 150, FlashKind::Success); } pub fn paste_pattern(&mut self, bank: usize, pattern: usize) { if let Some(src) = &self.copied_pattern { let mut pat = src.clone(); pat.name = match &src.name { Some(name) if !name.ends_with(" (copy)") => Some(format!("{name} (copy)")), Some(name) => Some(name.clone()), None => Some("(copy)".to_string()), }; self.project_state.project.banks[bank].patterns[pattern] = pat; self.project_state.mark_dirty(bank, pattern); if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern { self.load_step_to_editor(); } self.ui.flash("Pattern pasted", 150, FlashKind::Success); } } pub fn copy_bank(&mut self, bank: usize) { let b = self.project_state.project.banks[bank].clone(); self.copied_bank = Some(b); self.ui.flash("Bank copied", 150, FlashKind::Success); } pub fn paste_bank(&mut self, bank: usize) { if let Some(src) = &self.copied_bank { let mut b = src.clone(); b.name = match &src.name { Some(name) if !name.ends_with(" (copy)") => Some(format!("{name} (copy)")), Some(name) => Some(name.clone()), None => Some("(copy)".to_string()), }; self.project_state.project.banks[bank] = b; for pattern in 0..self.project_state.project.banks[bank].patterns.len() { self.project_state.mark_dirty(bank, pattern); } if self.editor_ctx.bank == bank { self.load_step_to_editor(); } self.ui.flash("Bank pasted", 150, FlashKind::Success); } } pub fn harden_steps(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let indices: Vec = match self.editor_ctx.selection_range() { Some(range) => range.collect(), None => vec![self.editor_ctx.step], }; let pat = self.project_state.project.pattern_at(bank, pattern); let resolutions: Vec<(usize, String)> = indices .iter() .filter_map(|&idx| { let step = pat.step(idx)?; step.source?; let script = pat.resolve_script(idx)?.to_string(); Some((idx, script)) }) .collect(); if resolutions.is_empty() { self.ui.set_status("No linked steps to harden".to_string()); return; } let count = resolutions.len(); for (idx, script) in resolutions { if let Some(s) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(idx) { s.source = None; s.script = script; } } self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); self.editor_ctx.clear_selection(); if count == 1 { self.ui.flash("Step hardened", 150, FlashKind::Success); } else { self.ui.flash(&format!("{count} steps hardened"), 150, FlashKind::Success); } } pub fn copy_steps(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let pat = self.project_state.project.pattern_at(bank, pattern); let indices: Vec = match self.editor_ctx.selection_range() { Some(range) => range.collect(), None => vec![self.editor_ctx.step], }; let mut steps = Vec::new(); let mut scripts = Vec::new(); for &idx in &indices { if let Some(step) = pat.step(idx) { let resolved = pat.resolve_script(idx).unwrap_or("").to_string(); scripts.push(resolved.clone()); steps.push(crate::state::CopiedStepData { script: resolved, active: step.active, source: step.source, original_index: idx, name: step.name.clone(), }); } } let count = steps.len(); self.editor_ctx.copied_steps = Some(crate::state::CopiedSteps { bank, pattern, steps, }); if let Some(clip) = &mut self.clipboard { let _ = clip.set_text(scripts.join("\n")); } self.ui.flash(&format!("Copied {count} steps"), 150, FlashKind::Info); } pub fn paste_steps(&mut self, link: &LinkState) { let Some(copied) = self.editor_ctx.copied_steps.clone() else { self.ui.set_status("Nothing copied".to_string()); return; }; let (bank, pattern) = self.current_bank_pattern(); let pat_len = self.project_state.project.pattern_at(bank, pattern).length; let cursor = self.editor_ctx.step; let same_pattern = copied.bank == bank && copied.pattern == pattern; for (i, data) in copied.steps.iter().enumerate() { let target = cursor + i; if target >= pat_len { break; } if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) { let source = if same_pattern { data.source } else { None }; step.active = data.active; step.source = source; step.name = data.name.clone(); if source.is_some() { step.script.clear(); step.command = None; } else { step.script = data.script.clone(); } } } self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); // Compile affected steps for i in 0..copied.steps.len() { let target = cursor + i; if target >= pat_len { break; } let saved_step = self.editor_ctx.step; self.editor_ctx.step = target; self.compile_current_step(link); self.editor_ctx.step = saved_step; } self.editor_ctx.clear_selection(); self.ui.flash(&format!("Pasted {} steps", copied.steps.len()), 150, FlashKind::Success); } pub fn link_paste_steps(&mut self) { let Some(copied) = self.editor_ctx.copied_steps.clone() else { self.ui.set_status("Nothing copied".to_string()); return; }; let (bank, pattern) = self.current_bank_pattern(); if copied.bank != bank || copied.pattern != pattern { self.ui.set_status("Can only link within same pattern".to_string()); return; } let pat_len = self.project_state.project.pattern_at(bank, pattern).length; let cursor = self.editor_ctx.step; for (i, data) in copied.steps.iter().enumerate() { let target = cursor + i; if target >= pat_len { break; } let source_idx = if data.source.is_some() { // Original was linked, link to same source data.source } else { Some(data.original_index) }; if source_idx == Some(target) { continue; } if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) { step.source = source_idx; step.script.clear(); step.command = None; } } self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); self.editor_ctx.clear_selection(); self.ui.flash(&format!("Linked {} steps", copied.steps.len()), 150, FlashKind::Success); } pub fn duplicate_steps(&mut self, link: &LinkState) { let (bank, pattern) = self.current_bank_pattern(); let pat = self.project_state.project.pattern_at(bank, pattern); let pat_len = pat.length; let indices: Vec = match self.editor_ctx.selection_range() { Some(range) => range.collect(), None => vec![self.editor_ctx.step], }; let count = indices.len(); let paste_at = *indices.last().unwrap() + 1; let dupe_data: Vec<(bool, String, Option)> = indices .iter() .filter_map(|&idx| { let step = pat.step(idx)?; let script = pat.resolve_script(idx).unwrap_or("").to_string(); let source = step.source; Some((step.active, script, source)) }) .collect(); let mut pasted = 0; for (i, (active, script, source)) in dupe_data.into_iter().enumerate() { let target = paste_at + i; if target >= pat_len { break; } if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) { step.active = active; step.source = source; if source.is_some() { step.script.clear(); step.command = None; } else { step.script = script; step.command = None; } } pasted += 1; } self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); for i in 0..pasted { let target = paste_at + i; let saved = self.editor_ctx.step; self.editor_ctx.step = target; self.compile_current_step(link); self.editor_ctx.step = saved; } self.editor_ctx.clear_selection(); self.ui.flash(&format!("Duplicated {count} steps"), 150, FlashKind::Success); } pub fn open_pattern_modal(&mut self, field: PatternField) { let current = match field { PatternField::Length => self.current_edit_pattern().length.to_string(), PatternField::Speed => self.current_edit_pattern().speed.label().to_string(), }; self.ui.modal = Modal::SetPattern { field, input: current, }; } pub fn open_pattern_props_modal(&mut self, bank: usize, pattern: usize) { let pat = self.project_state.project.pattern_at(bank, pattern); self.ui.modal = Modal::PatternProps { bank, pattern, field: PatternPropsField::default(), name: pat.name.clone().unwrap_or_default(), length: pat.length.to_string(), speed: pat.speed, quantization: pat.quantization, sync_mode: pat.sync_mode, }; } pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) { match cmd { // Playback AppCommand::TogglePlaying => self.toggle_playing(), 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(), AppCommand::ToggleFocus => self.toggle_focus(link), AppCommand::SelectEditBank(bank) => self.select_edit_bank(bank), AppCommand::SelectEditPattern(pattern) => self.select_edit_pattern(pattern), // 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::CompileAllSteps => self.compile_all_steps(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::CopyBank { bank } => { self.copy_bank(bank); } AppCommand::PasteBank { bank } => { self.paste_bank(bank); } // 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::StagePatternToggle { bank, pattern } => { self.stage_pattern_toggle(bank, pattern, snapshot); } AppCommand::CommitStagedChanges => { self.commit_staged_changes(); } 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::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), AppCommand::Load(path) => self.load(path, link), // UI AppCommand::SetStatus(msg) => self.ui.set_status(msg), AppCommand::ClearStatus => self.ui.clear_status(), AppCommand::Flash { message, duration_ms, kind, } => self.ui.flash(&message, duration_ms, kind), AppCommand::OpenModal(modal) => { if matches!(modal, Modal::Editor) { // If current step is a shallow copy, navigate to source step 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; } 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::SetPatternProps { bank, pattern, name, length, speed, quantization, sync_mode, } => { let pat = self.project_state.project.pattern_at_mut(bank, pattern); pat.name = name; if let Some(len) = length { pat.set_length(len); } pat.speed = speed; pat.quantization = quantization; pat.sync_mode = sync_mode; self.project_state.mark_dirty(bank, pattern); } // Page navigation AppCommand::PageLeft => self.page.left(), AppCommand::PageRight => self.page.right(), AppCommand::PageUp => self.page.up(), AppCommand::PageDown => self.page.down(), // Help navigation AppCommand::HelpToggleFocus => { use crate::state::HelpFocus; self.ui.help_focus = match self.ui.help_focus { HelpFocus::Topics => HelpFocus::Content, HelpFocus::Content => HelpFocus::Topics, }; } AppCommand::HelpNextTopic(n) => { let count = help_view::topic_count(); self.ui.help_topic = (self.ui.help_topic + n) % count; } AppCommand::HelpPrevTopic(n) => { let count = help_view::topic_count(); self.ui.help_topic = (self.ui.help_topic + count - (n % count)) % count; } AppCommand::HelpScrollDown(n) => { let s = self.ui.help_scroll_mut(); *s = s.saturating_add(n); } AppCommand::HelpScrollUp(n) => { let s = self.ui.help_scroll_mut(); *s = s.saturating_sub(n); } AppCommand::HelpActivateSearch => { self.ui.help_search_active = true; } AppCommand::HelpClearSearch => { self.ui.help_search_query.clear(); self.ui.help_search_active = false; } AppCommand::HelpSearchInput(c) => { self.ui.help_search_query.push(c); if let Some((topic, line)) = help_view::find_match(&self.ui.help_search_query) { self.ui.help_topic = topic; self.ui.help_scrolls[topic] = line; } } AppCommand::HelpSearchBackspace => { self.ui.help_search_query.pop(); if self.ui.help_search_query.is_empty() { return; } if let Some((topic, line)) = help_view::find_match(&self.ui.help_search_query) { self.ui.help_topic = topic; self.ui.help_scrolls[topic] = line; } } AppCommand::HelpSearchConfirm => { self.ui.help_search_active = false; } // Dictionary navigation AppCommand::DictToggleFocus => { self.ui.dict_focus = match self.ui.dict_focus { DictFocus::Categories => DictFocus::Words, DictFocus::Words => DictFocus::Categories, }; } AppCommand::DictNextCategory => { let count = dict_view::category_count(); self.ui.dict_category = (self.ui.dict_category + 1) % count; self.ui.dict_scroll = 0; } AppCommand::DictPrevCategory => { let count = dict_view::category_count(); self.ui.dict_category = (self.ui.dict_category + count - 1) % count; self.ui.dict_scroll = 0; } AppCommand::DictScrollDown(n) => { self.ui.dict_scroll = self.ui.dict_scroll.saturating_add(n); } AppCommand::DictScrollUp(n) => { self.ui.dict_scroll = self.ui.dict_scroll.saturating_sub(n); } AppCommand::DictActivateSearch => { self.ui.dict_search_active = true; self.ui.dict_focus = DictFocus::Words; } AppCommand::DictClearSearch => { self.ui.dict_search_query.clear(); self.ui.dict_search_active = false; self.ui.dict_scroll = 0; } AppCommand::DictSearchInput(c) => { self.ui.dict_search_query.push(c); self.ui.dict_scroll = 0; } AppCommand::DictSearchBackspace => { self.ui.dict_search_query.pop(); self.ui.dict_scroll = 0; } AppCommand::DictSearchConfirm => { self.ui.dict_search_active = false; } // 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(); } AppCommand::PatternsBack => { self.page.down(); } AppCommand::PatternsTogglePlay => { let bank = self.patterns_nav.selected_bank(); let pattern = self.patterns_nav.selected_pattern(); self.stage_pattern_toggle(bank, pattern, snapshot); } } } pub fn flush_queued_changes(&mut self, cmd_tx: &Sender) { for staged in self.playback.queued_changes.drain(..) { match staged.change { PatternChange::Start { bank, pattern } => { let _ = cmd_tx.send(SeqCommand::PatternStart { bank, pattern, quantization: staged.quantization, sync_mode: staged.sync_mode, }); } PatternChange::Stop { bank, pattern } => { let _ = cmd_tx.send(SeqCommand::PatternStop { bank, pattern, quantization: staged.quantization, }); } } } } pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender) { for (bank, pattern) in self.project_state.take_dirty() { let pat = self.project_state.project.pattern_at(bank, pattern); let snapshot = PatternSnapshot { speed: pat.speed, length: pat.length, steps: pat .steps .iter() .take(pat.length) .map(|s| StepSnapshot { active: s.active, script: s.script.clone(), source: s.source, }) .collect(), quantization: pat.quantization, sync_mode: pat.sync_mode, }; let _ = cmd_tx.send(SeqCommand::PatternUpdate { bank, pattern, data: snapshot, }); } } }