Files
Cagire/src/app/mod.rs
2026-03-07 11:38:49 +01:00

228 lines
6.7 KiB
Rust

//! 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<Arc<[CompletionCandidate]>> = 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<LiveKeyState>,
pub copied_patterns: Option<Vec<Pattern>>,
pub copied_banks: Option<Vec<Bank>>,
pub undo: UndoHistory,
pub audio: AudioSettings,
pub options: OptionsState,
pub sample_browser: Option<SampleBrowserState>,
pub midi: MidiState,
pub plugin_mode: bool,
pub dict_keys: HashSet<String>,
}
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<usize> {
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 };
}
}