Files
Cagire/src/app.rs
2026-01-28 18:05:50 +01:00

1271 lines
45 KiB
Rust

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::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<LiveKeyState>,
pub clipboard: Option<arboard::Clipboard>,
pub copied_pattern: Option<Pattern>,
pub copied_bank: Option<Bank>,
pub audio: AudioSettings,
pub options: OptionsState,
pub panel: PanelState,
}
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(),
}
}
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,
},
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,
},
link: crate::settings::LinkSettings {
enabled: link.is_enabled(),
tempo: link.tempo(),
quantum: link.quantum(),
},
};
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<usize> = 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<String> = 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 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,
};
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,
};
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<usize> = 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<usize> = 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,
});
}
}
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;
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<usize> = 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<usize>)> = 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::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::HelpNextTopic => {
self.ui.help_topic = (self.ui.help_topic + 1) % help_view::topic_count();
}
AppCommand::HelpPrevTopic => {
let count = help_view::topic_count();
self.ui.help_topic = (self.ui.help_topic + count - 1) % 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<SeqCommand>) {
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<SeqCommand>) {
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,
});
}
}
}