228 lines
6.7 KiB
Rust
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 };
|
|
}
|
|
}
|