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::config::MAX_SLOTS; use crate::engine::{ LinkState, PatternSnapshot, SeqCommand, SequencerSnapshot, SlotChange, StepSnapshot, }; use crate::model::{self, Pattern, Rng, ScriptEngine, StepContext, Variables}; use crate::page::Page; use crate::services::pattern_editor; use crate::state::{ AudioSettings, EditorContext, Focus, Metrics, Modal, PatternField, PatternsViewLevel, PlaybackState, ProjectState, UiState, }; use crate::views::doc_view; pub struct App { pub project_state: ProjectState, pub ui: UiState, pub playback: PlaybackState, pub page: Page, pub editor_ctx: EditorContext, pub patterns_view_level: PatternsViewLevel, pub patterns_cursor: usize, pub metrics: Metrics, pub sample_pool_mb: f32, pub script_engine: ScriptEngine, pub variables: Variables, pub rng: Rng, pub clipboard: Option, pub audio: AudioSettings, } impl App { pub fn new() -> Self { let variables = 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(&rng)); Self { project_state: ProjectState::default(), ui: UiState::default(), playback: PlaybackState::default(), page: Page::default(), editor_ctx: EditorContext::default(), patterns_view_level: PatternsViewLevel::default(), patterns_cursor: 0, metrics: Metrics::default(), sample_pool_mb: 0.0, variables, rng, script_engine, clipboard: arboard::Clipboard::new().ok(), audio: AudioSettings::default(), } } 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 num_rows = match len { 0..=8 => 1, 9..=16 => 2, 17..=24 => 3, _ => 4, }; let steps_per_row = len.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 num_rows = match len { 0..=8 => 1, 9..=16 => 2, 17..=24 => 3, _ => 4, }; let steps_per_row = len.div_ceil(num_rows); self.editor_ctx.step = (self.editor_ctx.step + steps_per_row) % len; self.load_step_to_editor(); } pub fn toggle_step(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let change = pattern_editor::toggle_step( &mut self.project_state.project, bank, pattern, self.editor_ctx.step, ); self.project_state.mark_dirty(change.bank, change.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.text = tui_textarea::TextArea::new(lines); } } pub fn save_editor_to_step(&mut self) { let text = self.editor_ctx.text.lines().join("\n"); 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 ctx = StepContext { step: step_idx, beat: link.beat(), bank, pattern, tempo: link.tempo(), phase: link.phase(), slot: 0, }; match self.script_engine.evaluate(&script, &ctx) { Ok(cmd) => { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = Some(cmd); } self.ui.flash("Script compiled", 150); } Err(e) => { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = None; } self.ui.set_status(format!("Script error: {e}")); } } } 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 ctx = StepContext { step: step_idx, beat: 0.0, bank, pattern, tempo: link.tempo(), phase: 0.0, slot: 0, }; if let Ok(cmd) = 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 = Some(cmd); } } } } pub fn is_pattern_queued( &self, bank: usize, pattern: usize, snapshot: &SequencerSnapshot, ) -> Option { self.playback.queued_changes.iter().find_map(|c| match *c { SlotChange::Add { slot: _, bank: b, pattern: p, } if b == bank && p == pattern => Some(true), SlotChange::Remove { slot } => { let s = snapshot.slot_data[slot]; if s.active && s.bank == bank && s.pattern == pattern { Some(false) } else { None } } _ => None, }) } pub fn toggle_pattern_playback( &mut self, bank: usize, pattern: usize, snapshot: &SequencerSnapshot, ) { let playing_slot = snapshot.slot_data.iter().enumerate().find_map(|(i, s)| { if s.active && s.bank == bank && s.pattern == pattern { Some(i) } else { None } }); let pending = self.playback.queued_changes.iter().position(|c| match *c { SlotChange::Add { bank: b, pattern: p, .. } => b == bank && p == pattern, SlotChange::Remove { slot } => { let s = snapshot.slot_data[slot]; s.bank == bank && s.pattern == pattern } }); if let Some(idx) = pending { self.playback.queued_changes.remove(idx); self.ui.set_status(format!( "B{:02}:P{:02} change cancelled", bank + 1, pattern + 1 )); } else if let Some(slot_idx) = playing_slot { self.playback .queued_changes .push(SlotChange::Remove { slot: slot_idx }); self.ui.set_status(format!( "B{:02}:P{:02} queued to stop", bank + 1, pattern + 1 )); } else { let free_slot = (0..MAX_SLOTS).find(|&i| !snapshot.slot_data[i].active); if let Some(slot_idx) = free_slot { self.playback.queued_changes.push(SlotChange::Add { slot: slot_idx, bank, pattern, }); self.ui.set_status(format!( "B{:02}:P{:02} queued to play", bank + 1, pattern + 1 )); } else { self.ui.set_status("All slots occupied".to_string()); } } } 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) { self.save_editor_to_step(); match model::save(&self.project_state.project, &path) { Ok(()) => { self.ui.set_status(format!("Saved: {}", path.display())); self.project_state.file_path = Some(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) => { 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(); 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 copy_step(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let script = pattern_editor::get_step_script( &self.project_state.project, bank, pattern, self.editor_ctx.step, ); if let Some(script) = script { if let Some(clip) = &mut self.clipboard { if clip.set_text(&script).is_ok() { self.ui.set_status("Copied".to_string()); } } } } pub fn paste_step(&mut self, link: &LinkState) { let text = self .clipboard .as_mut() .and_then(|clip| clip.get_text().ok()); if let Some(text) = text { 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); self.load_step_to_editor(); self.compile_current_step(link); } } 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 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::ToggleStep => self.toggle_step(), 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), // Clipboard AppCommand::CopyStep => self.copy_step(), AppCommand::PasteStep => self.paste_step(link), // Pattern playback AppCommand::QueueSlotChange(change) => { self.playback.queued_changes.push(change); } AppCommand::TogglePatternPlayback { bank, pattern } => { self.toggle_pattern_playback(bank, pattern, snapshot); } // 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), 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, } => self.ui.flash(&message, duration_ms), AppCommand::OpenModal(modal) => self.ui.modal = modal, AppCommand::CloseModal => self.ui.modal = Modal::None, AppCommand::OpenPatternModal(field) => self.open_pattern_modal(field), // Page navigation AppCommand::PageLeft => self.page.left(), AppCommand::PageRight => self.page.right(), AppCommand::PageUp => self.page.up(), AppCommand::PageDown => self.page.down(), // Doc navigation AppCommand::DocNextTopic => { self.ui.doc_topic = (self.ui.doc_topic + 1) % doc_view::topic_count(); self.ui.doc_scroll = 0; } AppCommand::DocPrevTopic => { let count = doc_view::topic_count(); self.ui.doc_topic = (self.ui.doc_topic + count - 1) % count; self.ui.doc_scroll = 0; } AppCommand::DocScrollDown(n) => { self.ui.doc_scroll = self.ui.doc_scroll.saturating_add(n); } AppCommand::DocScrollUp(n) => { self.ui.doc_scroll = self.ui.doc_scroll.saturating_sub(n); } // Patterns view AppCommand::PatternsCursorLeft => { self.patterns_cursor = (self.patterns_cursor + 15) % 16; } AppCommand::PatternsCursorRight => { self.patterns_cursor = (self.patterns_cursor + 1) % 16; } AppCommand::PatternsCursorUp => { self.patterns_cursor = (self.patterns_cursor + 12) % 16; } AppCommand::PatternsCursorDown => { self.patterns_cursor = (self.patterns_cursor + 4) % 16; } AppCommand::PatternsEnter => match self.patterns_view_level { PatternsViewLevel::Banks => { let bank = self.patterns_cursor; self.patterns_view_level = PatternsViewLevel::Patterns { bank }; self.patterns_cursor = 0; } PatternsViewLevel::Patterns { bank } => { let pattern = self.patterns_cursor; self.select_edit_bank(bank); self.select_edit_pattern(pattern); self.patterns_view_level = PatternsViewLevel::Banks; self.patterns_cursor = 0; self.page.down(); } }, AppCommand::PatternsBack => match self.patterns_view_level { PatternsViewLevel::Banks => self.page.down(), PatternsViewLevel::Patterns { .. } => { self.patterns_view_level = PatternsViewLevel::Banks; self.patterns_cursor = 0; } }, } } pub fn flush_queued_changes(&mut self, cmd_tx: &Sender) { for change in self.playback.queued_changes.drain(..) { match change { SlotChange::Add { slot, bank, pattern, } => { let _ = cmd_tx.send(SeqCommand::SlotAdd { slot, bank, pattern, }); } SlotChange::Remove { slot } => { let _ = cmd_tx.send(SeqCommand::SlotRemove { slot }); } } } } 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(), }) .collect(), }; let _ = cmd_tx.send(SeqCommand::PatternUpdate { bank, pattern, data: snapshot, }); } } }