//! Application state: owns the project, editor context, and all UI/playback state. mod clipboard; mod dispatch; mod editing; mod navigation; mod persistence; mod scripting; mod sequencer; mod staging; mod undo; use arc_swap::ArcSwap; use parking_lot::Mutex; use rand::rngs::StdRng; use rand::SeedableRng; use std::collections::{HashMap, HashSet}; use std::sync::{Arc, LazyLock}; use cagire_ratatui::CompletionCandidate; use crate::engine::LinkState; use crate::midi::MidiState; use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, Variables}; use crate::page::Page; use crate::state::{ undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal, OptionsState, PatternField, PatternPropsField, PatternsNav, PlaybackState, ProjectState, SampleBrowserState, ScriptEditorState, UiState, }; static COMPLETION_CANDIDATES: LazyLock> = LazyLock::new(|| { model::WORDS .iter() .map(|w| CompletionCandidate { name: w.name.to_string(), signature: w.stack.to_string(), description: w.desc.to_string(), example: w.example.to_string(), }) .collect() }); pub struct App { pub project_state: ProjectState, pub ui: UiState, pub playback: PlaybackState, pub page: Page, pub editor_ctx: EditorContext, pub script_editor: ScriptEditorState, pub patterns_nav: PatternsNav, pub metrics: Metrics, pub script_engine: ScriptEngine, pub variables: Variables, pub dict: Dictionary, // Held to keep the Arc alive (shared with ScriptEngine). pub _rng: Rng, pub live_keys: Arc, pub copied_patterns: Option>, pub copied_banks: Option>, pub undo: UndoHistory, pub audio: AudioSettings, pub options: OptionsState, pub sample_browser: Option, pub midi: MidiState, pub plugin_mode: bool, pub dict_keys: HashSet, } impl Default for App { fn default() -> Self { Self::new() } } impl App { pub fn new() -> Self { let variables = Arc::new(ArcSwap::from_pointee(HashMap::new())); let dict = Arc::new(Mutex::new(HashMap::new())); let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0))); Self::build(variables, dict, rng, false) } #[allow(dead_code)] // used by plugin crate pub fn new_plugin(variables: Variables, dict: Dictionary, rng: Rng) -> Self { Self::build(variables, dict, rng, true) } fn build(variables: Variables, dict: Dictionary, rng: Rng, plugin_mode: bool) -> Self { 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(), script_editor: ScriptEditorState::default(), patterns_nav: PatternsNav::default(), metrics: Metrics::default(), variables, dict, _rng: rng, live_keys, script_engine, copied_patterns: None, copied_banks: None, undo: UndoHistory::default(), audio: if plugin_mode { AudioSettings::new_plugin() } else { AudioSettings::default() }, options: OptionsState::default(), sample_browser: None, midi: MidiState::new(), plugin_mode, dict_keys: HashSet::new(), } } fn current_bank_pattern(&self) -> (usize, usize) { (self.editor_ctx.bank, self.editor_ctx.pattern) } fn selected_steps(&self) -> Vec { match self.editor_ctx.selection_range() { Some(range) => range.collect(), None => vec![self.editor_ctx.step], } } pub fn mark_all_patterns_dirty(&mut self) { self.project_state.mark_all_dirty(); } pub fn toggle_playing(&mut self, link: &LinkState) { let was_playing = self.playback.playing; self.playback.toggle(); if !was_playing && self.playback.playing { self.evaluate_prelude(link); } } 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 current_edit_pattern(&self) -> &Pattern { let (bank, pattern) = self.current_bank_pattern(); self.project_state.project.pattern_at(bank, pattern) } pub fn open_script_modal(&mut self, field: crate::state::ScriptField) { use crate::state::ScriptField; let current = match field { ScriptField::Speed => self.project_state.project.script_speed.label().to_string(), ScriptField::Length => self.project_state.project.script_length.to_string(), }; self.ui.modal = Modal::SetScript { field, input: current, }; } 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(), description: pat.description.clone().unwrap_or_default(), length: pat.length.to_string(), speed: pat.speed, quantization: pat.quantization, sync_mode: pat.sync_mode, follow_up: pat.follow_up, }; } pub fn maybe_show_onboarding(&mut self) { if self.plugin_mode { return; } if self.ui.modal != Modal::None { return; } if crate::model::onboarding::for_page(self.page).is_empty() { return; } let name = self.page.name(); if self.ui.onboarding_dismissed.iter().any(|d| d == name) { return; } self.ui.modal = Modal::Onboarding { page: 0 }; } }