Some kind of refactoring
This commit is contained in:
507
src/app.rs
507
src/app.rs
@@ -15,14 +15,13 @@ use crate::engine::{
|
||||
use crate::midi::MidiState;
|
||||
use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, StepContext, Variables};
|
||||
use crate::page::Page;
|
||||
use crate::services::pattern_editor;
|
||||
use crate::services::{clipboard, dict_nav, euclidean, help_nav, pattern_editor};
|
||||
use crate::settings::Settings;
|
||||
use crate::state::{
|
||||
AudioSettings, CyclicEnum, DictFocus, EditorContext, FlashKind, LiveKeyState, Metrics, Modal,
|
||||
AudioSettings, CyclicEnum, EditorContext, FlashKind, LiveKeyState, Metrics, Modal,
|
||||
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
||||
};
|
||||
use crate::model::{categories, docs};
|
||||
|
||||
const STEPS_PER_PAGE: usize = 32;
|
||||
|
||||
@@ -41,6 +40,7 @@ pub struct App {
|
||||
pub script_engine: ScriptEngine,
|
||||
pub variables: Variables,
|
||||
pub dict: Dictionary,
|
||||
#[allow(dead_code)] // kept alive for ScriptEngine's Arc clone
|
||||
pub rng: Rng,
|
||||
pub live_keys: Arc<LiveKeyState>,
|
||||
pub clipboard: Option<arboard::Clipboard>,
|
||||
@@ -161,14 +161,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn annotate_copy_name(name: &Option<String>) -> Option<String> {
|
||||
match name {
|
||||
Some(n) if !n.ends_with(" (copy)") => Some(format!("{n} (copy)")),
|
||||
Some(n) => Some(n.clone()),
|
||||
None => Some("(copy)".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_all_patterns_dirty(&mut self) {
|
||||
self.project_state.mark_all_dirty();
|
||||
}
|
||||
@@ -328,6 +320,9 @@ impl App {
|
||||
self.editor_ctx
|
||||
.editor
|
||||
.set_completion_enabled(self.ui.show_completion);
|
||||
if self.editor_ctx.show_stack {
|
||||
crate::services::stack_preview::update_cache(&self.editor_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,32 +633,8 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
let edit = pattern_editor::delete_step(&mut self.project_state.project, bank, pattern, step);
|
||||
self.project_state.mark_dirty(edit.bank, edit.pattern);
|
||||
if self.editor_ctx.bank == bank
|
||||
&& self.editor_ctx.pattern == pattern
|
||||
&& self.editor_ctx.step == step
|
||||
@@ -674,34 +645,8 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
let edit = pattern_editor::delete_steps(&mut self.project_state.project, bank, pattern, steps);
|
||||
self.project_state.mark_dirty(edit.bank, edit.pattern);
|
||||
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
@@ -714,8 +659,8 @@ impl App {
|
||||
}
|
||||
|
||||
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);
|
||||
let edit = pattern_editor::reset_pattern(&mut self.project_state.project, bank, pattern);
|
||||
self.project_state.mark_dirty(edit.bank, edit.pattern);
|
||||
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
@@ -723,8 +668,8 @@ impl App {
|
||||
}
|
||||
|
||||
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() {
|
||||
let pat_count = pattern_editor::reset_bank(&mut self.project_state.project, bank);
|
||||
for pattern in 0..pat_count {
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
if self.editor_ctx.bank == bank {
|
||||
@@ -734,16 +679,13 @@ impl App {
|
||||
}
|
||||
|
||||
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.copied_pattern = Some(clipboard::copy_pattern(&self.project_state.project, bank, pattern));
|
||||
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 = Self::annotate_copy_name(&src.name);
|
||||
self.project_state.project.banks[bank].patterns[pattern] = pat;
|
||||
if let Some(src) = self.copied_pattern.clone() {
|
||||
clipboard::paste_pattern(&mut self.project_state.project, bank, pattern, &src);
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||
self.load_step_to_editor();
|
||||
@@ -753,17 +695,14 @@ impl App {
|
||||
}
|
||||
|
||||
pub fn copy_bank(&mut self, bank: usize) {
|
||||
let b = self.project_state.project.banks[bank].clone();
|
||||
self.copied_bank = Some(b);
|
||||
self.copied_bank = Some(clipboard::copy_bank(&self.project_state.project, bank));
|
||||
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 = Self::annotate_copy_name(&src.name);
|
||||
self.project_state.project.banks[bank] = b;
|
||||
for pattern in 0..self.project_state.project.banks[bank].patterns.len() {
|
||||
if let Some(src) = self.copied_bank.clone() {
|
||||
let pat_count = clipboard::paste_bank(&mut self.project_state.project, bank, &src);
|
||||
for pattern in 0..pat_count {
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
if self.editor_ctx.bank == bank {
|
||||
@@ -776,36 +715,11 @@ impl App {
|
||||
pub fn harden_steps(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let indices = self.selected_steps();
|
||||
|
||||
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() {
|
||||
let count = clipboard::harden_steps(&mut self.project_state.project, bank, pattern, &indices);
|
||||
if count == 0 {
|
||||
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();
|
||||
@@ -819,36 +733,18 @@ impl App {
|
||||
|
||||
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 = self.selected_steps();
|
||||
|
||||
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,
|
||||
name: step.name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let count = steps.len();
|
||||
self.editor_ctx.copied_steps = Some(crate::state::CopiedSteps {
|
||||
let (copied, scripts) = clipboard::copy_steps(
|
||||
&self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
steps,
|
||||
});
|
||||
|
||||
&indices,
|
||||
);
|
||||
let count = copied.steps.len();
|
||||
self.editor_ctx.copied_steps = Some(copied);
|
||||
if let Some(clip) = &mut self.clipboard {
|
||||
let _ = clip.set_text(scripts.join("\n"));
|
||||
}
|
||||
|
||||
self.ui
|
||||
.flash(&format!("Copied {count} steps"), 150, FlashKind::Info);
|
||||
}
|
||||
@@ -858,54 +754,26 @@ impl App {
|
||||
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;
|
||||
step.name = data.name.clone();
|
||||
if source.is_some() {
|
||||
step.script.clear();
|
||||
step.command = None;
|
||||
} else {
|
||||
step.script = data.script.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = clipboard::paste_steps(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
cursor,
|
||||
&copied,
|
||||
);
|
||||
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;
|
||||
for &target in &result.compile_targets {
|
||||
let saved = self.editor_ctx.step;
|
||||
self.editor_ctx.step = target;
|
||||
self.compile_current_step(link);
|
||||
self.editor_ctx.step = saved_step;
|
||||
self.editor_ctx.step = saved;
|
||||
}
|
||||
|
||||
self.editor_ctx.clear_selection();
|
||||
self.ui.flash(
|
||||
&format!("Pasted {} steps", copied.steps.len()),
|
||||
&format!("Pasted {} steps", result.count),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
@@ -916,111 +784,52 @@ impl App {
|
||||
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;
|
||||
match clipboard::link_paste_steps(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
cursor,
|
||||
&copied,
|
||||
) {
|
||||
None => {
|
||||
self.ui
|
||||
.set_status("Can only link within same pattern".to_string());
|
||||
}
|
||||
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;
|
||||
Some(count) => {
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
self.load_step_to_editor();
|
||||
self.editor_ctx.clear_selection();
|
||||
self.ui.flash(
|
||||
&format!("Linked {count} steps"),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 = self.selected_steps();
|
||||
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;
|
||||
}
|
||||
|
||||
let result = clipboard::duplicate_steps(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
&indices,
|
||||
);
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
self.load_step_to_editor();
|
||||
|
||||
for i in 0..pasted {
|
||||
let target = paste_at + i;
|
||||
for &target in &result.compile_targets {
|
||||
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"),
|
||||
&format!("Duplicated {} steps", result.count),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
@@ -1232,100 +1041,28 @@ impl App {
|
||||
AppCommand::PageDown => self.page.down(),
|
||||
|
||||
// Help navigation
|
||||
AppCommand::HelpToggleFocus => {
|
||||
use crate::state::HelpFocus;
|
||||
self.ui.help_focus = match self.ui.help_focus {
|
||||
HelpFocus::Topics => HelpFocus::Content,
|
||||
HelpFocus::Content => HelpFocus::Topics,
|
||||
};
|
||||
}
|
||||
AppCommand::HelpNextTopic(n) => {
|
||||
let count = docs::topic_count();
|
||||
self.ui.help_topic = (self.ui.help_topic + n) % count;
|
||||
}
|
||||
AppCommand::HelpPrevTopic(n) => {
|
||||
let count = docs::topic_count();
|
||||
self.ui.help_topic = (self.ui.help_topic + count - (n % count)) % 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)) = docs::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)) = docs::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;
|
||||
}
|
||||
AppCommand::HelpToggleFocus => help_nav::toggle_focus(&mut self.ui),
|
||||
AppCommand::HelpNextTopic(n) => help_nav::next_topic(&mut self.ui, n),
|
||||
AppCommand::HelpPrevTopic(n) => help_nav::prev_topic(&mut self.ui, n),
|
||||
AppCommand::HelpScrollDown(n) => help_nav::scroll_down(&mut self.ui, n),
|
||||
AppCommand::HelpScrollUp(n) => help_nav::scroll_up(&mut self.ui, n),
|
||||
AppCommand::HelpActivateSearch => help_nav::activate_search(&mut self.ui),
|
||||
AppCommand::HelpClearSearch => help_nav::clear_search(&mut self.ui),
|
||||
AppCommand::HelpSearchInput(c) => help_nav::search_input(&mut self.ui, c),
|
||||
AppCommand::HelpSearchBackspace => help_nav::search_backspace(&mut self.ui),
|
||||
AppCommand::HelpSearchConfirm => help_nav::search_confirm(&mut self.ui),
|
||||
|
||||
// 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 = categories::category_count();
|
||||
self.ui.dict_category = (self.ui.dict_category + 1) % count;
|
||||
}
|
||||
AppCommand::DictPrevCategory => {
|
||||
let count = categories::category_count();
|
||||
self.ui.dict_category = (self.ui.dict_category + count - 1) % count;
|
||||
}
|
||||
AppCommand::DictScrollDown(n) => {
|
||||
let s = self.ui.dict_scroll_mut();
|
||||
*s = s.saturating_add(n);
|
||||
}
|
||||
AppCommand::DictScrollUp(n) => {
|
||||
let s = self.ui.dict_scroll_mut();
|
||||
*s = s.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_mut() = 0;
|
||||
}
|
||||
AppCommand::DictSearchInput(c) => {
|
||||
self.ui.dict_search_query.push(c);
|
||||
*self.ui.dict_scroll_mut() = 0;
|
||||
}
|
||||
AppCommand::DictSearchBackspace => {
|
||||
self.ui.dict_search_query.pop();
|
||||
*self.ui.dict_scroll_mut() = 0;
|
||||
}
|
||||
AppCommand::DictSearchConfirm => {
|
||||
self.ui.dict_search_active = false;
|
||||
}
|
||||
AppCommand::DictToggleFocus => dict_nav::toggle_focus(&mut self.ui),
|
||||
AppCommand::DictNextCategory => dict_nav::next_category(&mut self.ui),
|
||||
AppCommand::DictPrevCategory => dict_nav::prev_category(&mut self.ui),
|
||||
AppCommand::DictScrollDown(n) => dict_nav::scroll_down(&mut self.ui, n),
|
||||
AppCommand::DictScrollUp(n) => dict_nav::scroll_up(&mut self.ui, n),
|
||||
AppCommand::DictActivateSearch => dict_nav::activate_search(&mut self.ui),
|
||||
AppCommand::DictClearSearch => dict_nav::clear_search(&mut self.ui),
|
||||
AppCommand::DictSearchInput(c) => dict_nav::search_input(&mut self.ui, c),
|
||||
AppCommand::DictSearchBackspace => dict_nav::search_backspace(&mut self.ui),
|
||||
AppCommand::DictSearchConfirm => dict_nav::search_confirm(&mut self.ui),
|
||||
|
||||
// Patterns view
|
||||
AppCommand::PatternsCursorLeft => {
|
||||
@@ -1381,6 +1118,9 @@ impl App {
|
||||
}
|
||||
AppCommand::ToggleEditorStack => {
|
||||
self.editor_ctx.show_stack = !self.editor_ctx.show_stack;
|
||||
if self.editor_ctx.show_stack {
|
||||
crate::services::stack_preview::update_cache(&self.editor_ctx);
|
||||
}
|
||||
}
|
||||
AppCommand::SetColorScheme(scheme) => {
|
||||
self.ui.color_scheme = scheme;
|
||||
@@ -1516,51 +1256,26 @@ impl App {
|
||||
steps,
|
||||
rotation,
|
||||
} => {
|
||||
let pat_len = self.project_state.project.pattern_at(bank, pattern).length;
|
||||
let rhythm = euclidean_rhythm(pulses, steps, rotation);
|
||||
|
||||
let mut created_count = 0;
|
||||
for (i, &is_hit) in rhythm.iter().enumerate() {
|
||||
if !is_hit {
|
||||
continue;
|
||||
}
|
||||
|
||||
let target = (source_step + i) % pat_len;
|
||||
|
||||
if target == source_step {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(step) = self
|
||||
.project_state
|
||||
.project
|
||||
.pattern_at_mut(bank, pattern)
|
||||
.step_mut(target)
|
||||
{
|
||||
step.source = Some(source_step);
|
||||
step.script.clear();
|
||||
step.command = None;
|
||||
step.active = true;
|
||||
}
|
||||
created_count += 1;
|
||||
}
|
||||
|
||||
let targets = euclidean::apply_distribution(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
source_step,
|
||||
pulses,
|
||||
steps,
|
||||
rotation,
|
||||
);
|
||||
let created_count = targets.len();
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
|
||||
for (i, &is_hit) in rhythm.iter().enumerate() {
|
||||
if !is_hit || i == 0 {
|
||||
continue;
|
||||
}
|
||||
let target = (source_step + i) % pat_len;
|
||||
for &target in &targets {
|
||||
let saved = self.editor_ctx.step;
|
||||
self.editor_ctx.step = target;
|
||||
self.compile_current_step(link);
|
||||
self.editor_ctx.step = saved;
|
||||
}
|
||||
|
||||
self.load_step_to_editor();
|
||||
self.ui.flash(
|
||||
&format!("Created {} linked steps (E({pulses},{steps}))", created_count),
|
||||
&format!("Created {created_count} linked steps (E({pulses},{steps}))"),
|
||||
200,
|
||||
FlashKind::Success,
|
||||
);
|
||||
@@ -1624,21 +1339,3 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn euclidean_rhythm(pulses: usize, steps: usize, rotation: usize) -> Vec<bool> {
|
||||
if pulses == 0 || steps == 0 || pulses > steps {
|
||||
return vec![false; steps];
|
||||
}
|
||||
|
||||
let mut pattern = vec![false; steps];
|
||||
for i in 0..pulses {
|
||||
let pos = (i * steps) / pulses;
|
||||
pattern[pos] = true;
|
||||
}
|
||||
|
||||
if rotation > 0 {
|
||||
pattern.rotate_left(rotation % steps);
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
|
||||
use parking_lot::Mutex;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "desktop")]
|
||||
use std::sync::atomic::AtomicU32;
|
||||
@@ -120,6 +123,7 @@ pub enum SeqCommand {
|
||||
soloed: std::collections::HashSet<(usize, usize)>,
|
||||
},
|
||||
StopAll,
|
||||
ResetScriptState,
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
@@ -274,13 +278,9 @@ pub struct SequencerConfig {
|
||||
pub mouse_down: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spawn_sequencer(
|
||||
link: Arc<LinkState>,
|
||||
playing: Arc<std::sync::atomic::AtomicBool>,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
rng: Rng,
|
||||
quantum: f64,
|
||||
live_keys: Arc<LiveKeyState>,
|
||||
nudge_us: Arc<AtomicI64>,
|
||||
@@ -329,9 +329,6 @@ pub fn spawn_sequencer(
|
||||
sequencer_audio_tx,
|
||||
link,
|
||||
playing,
|
||||
variables,
|
||||
dict,
|
||||
rng,
|
||||
quantum,
|
||||
shared_state_clone,
|
||||
live_keys,
|
||||
@@ -667,6 +664,15 @@ impl SequencerState {
|
||||
self.runs_counter.counts.clear();
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
}
|
||||
SeqCommand::ResetScriptState => {
|
||||
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
self.script_engine =
|
||||
ScriptEngine::new(Arc::clone(&variables), dict, rng);
|
||||
self.variables = variables;
|
||||
self.speed_overrides.clear();
|
||||
}
|
||||
SeqCommand::Shutdown => {}
|
||||
}
|
||||
}
|
||||
@@ -1063,9 +1069,6 @@ fn sequencer_loop(
|
||||
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
link: Arc<LinkState>,
|
||||
playing: Arc<std::sync::atomic::AtomicBool>,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
rng: Rng,
|
||||
quantum: f64,
|
||||
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||
live_keys: Arc<LiveKeyState>,
|
||||
@@ -1093,6 +1096,9 @@ fn sequencer_loop(
|
||||
eprintln!("[cagire] Then log out and back in.");
|
||||
}
|
||||
|
||||
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
|
||||
|
||||
loop {
|
||||
|
||||
@@ -149,9 +149,6 @@ pub fn init(args: InitArgs) -> Init {
|
||||
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
|
||||
Arc::clone(&link),
|
||||
Arc::clone(&playing),
|
||||
Arc::clone(&app.variables),
|
||||
Arc::clone(&app.dict),
|
||||
Arc::clone(&app.rng),
|
||||
settings.link.quantum,
|
||||
Arc::clone(&app.live_keys),
|
||||
Arc::clone(&nudge_us),
|
||||
|
||||
@@ -259,6 +259,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
FileBrowserMode::Save => ctx.dispatch(AppCommand::Save(path)),
|
||||
FileBrowserMode::Load => {
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::ResetScriptState);
|
||||
ctx.dispatch(AppCommand::Load(path));
|
||||
load_project_samples(ctx);
|
||||
}
|
||||
@@ -556,6 +557,10 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
editor.input(Event::Key(key));
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.app.editor_ctx.show_stack {
|
||||
crate::services::stack_preview::update_cache(&ctx.app.editor_ctx);
|
||||
}
|
||||
}
|
||||
Modal::Preview => match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('p') => ctx.dispatch(AppCommand::CloseModal),
|
||||
|
||||
@@ -242,6 +242,9 @@ fn main() -> io::Result<()> {
|
||||
Event::Paste(text) => {
|
||||
if matches!(app.ui.modal, state::Modal::Editor) {
|
||||
app.editor_ctx.editor.insert_str(&text);
|
||||
if app.editor_ctx.show_stack {
|
||||
services::stack_preview::update_cache(&app.editor_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
|
||||
use midir::{MidiInput, MidiOutput};
|
||||
|
||||
use cagire_forth::CcAccess;
|
||||
use crate::model::CcAccess;
|
||||
|
||||
pub const MAX_MIDI_OUTPUTS: usize = 4;
|
||||
pub const MAX_MIDI_INPUTS: usize = 4;
|
||||
|
||||
@@ -2,12 +2,12 @@ pub mod categories;
|
||||
pub mod docs;
|
||||
mod script;
|
||||
|
||||
pub use cagire_forth::{lookup_word, Word, WordCompile, WORDS};
|
||||
pub use cagire_forth::{
|
||||
lookup_word, CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value,
|
||||
Variables, Word, WordCompile, WORDS,
|
||||
};
|
||||
pub use cagire_project::{
|
||||
load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS,
|
||||
MAX_PATTERNS,
|
||||
};
|
||||
pub use script::{
|
||||
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Value,
|
||||
Variables,
|
||||
};
|
||||
pub use script::ScriptEngine;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use cagire_forth::Forth;
|
||||
|
||||
pub use cagire_forth::{
|
||||
CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables,
|
||||
};
|
||||
use cagire_forth::{Dictionary, ExecutionTrace, Forth, Rng, StepContext, Value, Variables};
|
||||
|
||||
pub struct ScriptEngine {
|
||||
forth: Forth,
|
||||
@@ -27,4 +23,8 @@ impl ScriptEngine {
|
||||
) -> Result<Vec<String>, String> {
|
||||
self.forth.evaluate_with_trace(script, ctx, trace)
|
||||
}
|
||||
|
||||
pub fn stack(&self) -> Vec<Value> {
|
||||
self.forth.stack()
|
||||
}
|
||||
}
|
||||
|
||||
220
src/services/clipboard.rs
Normal file
220
src/services/clipboard.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use crate::model::{Bank, Pattern, Project};
|
||||
use crate::state::{CopiedStepData, CopiedSteps};
|
||||
|
||||
fn annotate_copy_name(name: &Option<String>) -> Option<String> {
|
||||
match name {
|
||||
Some(n) if !n.ends_with(" (copy)") => Some(format!("{n} (copy)")),
|
||||
Some(n) => Some(n.clone()),
|
||||
None => Some("(copy)".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_pattern(project: &Project, bank: usize, pattern: usize) -> Pattern {
|
||||
project.banks[bank].patterns[pattern].clone()
|
||||
}
|
||||
|
||||
pub fn paste_pattern(
|
||||
project: &mut Project,
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
source: &Pattern,
|
||||
) {
|
||||
let mut pat = source.clone();
|
||||
pat.name = annotate_copy_name(&source.name);
|
||||
project.banks[bank].patterns[pattern] = pat;
|
||||
}
|
||||
|
||||
pub fn copy_bank(project: &Project, bank: usize) -> Bank {
|
||||
project.banks[bank].clone()
|
||||
}
|
||||
|
||||
pub fn paste_bank(project: &mut Project, bank: usize, source: &Bank) -> usize {
|
||||
let mut b = source.clone();
|
||||
b.name = annotate_copy_name(&source.name);
|
||||
project.banks[bank] = b;
|
||||
project.banks[bank].patterns.len()
|
||||
}
|
||||
|
||||
pub fn copy_steps(
|
||||
project: &Project,
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
indices: &[usize],
|
||||
) -> (CopiedSteps, Vec<String>) {
|
||||
let pat = project.pattern_at(bank, pattern);
|
||||
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(CopiedStepData {
|
||||
script: resolved,
|
||||
active: step.active,
|
||||
source: step.source,
|
||||
original_index: idx,
|
||||
name: step.name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let copied = CopiedSteps {
|
||||
bank,
|
||||
pattern,
|
||||
steps,
|
||||
};
|
||||
(copied, scripts)
|
||||
}
|
||||
|
||||
pub struct PasteResult {
|
||||
pub count: usize,
|
||||
pub compile_targets: Vec<usize>,
|
||||
}
|
||||
|
||||
pub fn paste_steps(
|
||||
project: &mut Project,
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
cursor: usize,
|
||||
copied: &CopiedSteps,
|
||||
) -> PasteResult {
|
||||
let pat_len = project.pattern_at(bank, pattern).length;
|
||||
let same_pattern = copied.bank == bank && copied.pattern == pattern;
|
||||
let mut compile_targets = Vec::new();
|
||||
|
||||
for (i, data) in copied.steps.iter().enumerate() {
|
||||
let target = cursor + i;
|
||||
if target >= pat_len {
|
||||
break;
|
||||
}
|
||||
if let Some(step) = 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;
|
||||
step.name = data.name.clone();
|
||||
if source.is_some() {
|
||||
step.script.clear();
|
||||
step.command = None;
|
||||
} else {
|
||||
step.script = data.script.clone();
|
||||
}
|
||||
}
|
||||
compile_targets.push(target);
|
||||
}
|
||||
|
||||
PasteResult {
|
||||
count: copied.steps.len(),
|
||||
compile_targets,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn link_paste_steps(
|
||||
project: &mut Project,
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
cursor: usize,
|
||||
copied: &CopiedSteps,
|
||||
) -> Option<usize> {
|
||||
if copied.bank != bank || copied.pattern != pattern {
|
||||
return None;
|
||||
}
|
||||
|
||||
let pat_len = project.pattern_at(bank, pattern).length;
|
||||
|
||||
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() {
|
||||
data.source
|
||||
} else {
|
||||
Some(data.original_index)
|
||||
};
|
||||
if source_idx == Some(target) {
|
||||
continue;
|
||||
}
|
||||
if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) {
|
||||
step.source = source_idx;
|
||||
step.script.clear();
|
||||
step.command = None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(copied.steps.len())
|
||||
}
|
||||
|
||||
pub fn harden_steps(
|
||||
project: &mut Project,
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
indices: &[usize],
|
||||
) -> usize {
|
||||
let pat = 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();
|
||||
|
||||
let count = resolutions.len();
|
||||
for (idx, script) in resolutions {
|
||||
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(idx) {
|
||||
s.source = None;
|
||||
s.script = script;
|
||||
}
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
pub fn duplicate_steps(
|
||||
project: &mut Project,
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
indices: &[usize],
|
||||
) -> PasteResult {
|
||||
let pat = project.pattern_at(bank, pattern);
|
||||
let pat_len = pat.length;
|
||||
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 compile_targets = Vec::new();
|
||||
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) = 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;
|
||||
}
|
||||
}
|
||||
compile_targets.push(target);
|
||||
}
|
||||
|
||||
PasteResult {
|
||||
count: indices.len(),
|
||||
compile_targets,
|
||||
}
|
||||
}
|
||||
54
src/services/dict_nav.rs
Normal file
54
src/services/dict_nav.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::model::categories;
|
||||
use crate::state::{DictFocus, UiState};
|
||||
|
||||
pub fn toggle_focus(ui: &mut UiState) {
|
||||
ui.dict_focus = match ui.dict_focus {
|
||||
DictFocus::Categories => DictFocus::Words,
|
||||
DictFocus::Words => DictFocus::Categories,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn next_category(ui: &mut UiState) {
|
||||
let count = categories::category_count();
|
||||
ui.dict_category = (ui.dict_category + 1) % count;
|
||||
}
|
||||
|
||||
pub fn prev_category(ui: &mut UiState) {
|
||||
let count = categories::category_count();
|
||||
ui.dict_category = (ui.dict_category + count - 1) % count;
|
||||
}
|
||||
|
||||
pub fn scroll_down(ui: &mut UiState, n: usize) {
|
||||
let s = ui.dict_scroll_mut();
|
||||
*s = s.saturating_add(n);
|
||||
}
|
||||
|
||||
pub fn scroll_up(ui: &mut UiState, n: usize) {
|
||||
let s = ui.dict_scroll_mut();
|
||||
*s = s.saturating_sub(n);
|
||||
}
|
||||
|
||||
pub fn activate_search(ui: &mut UiState) {
|
||||
ui.dict_search_active = true;
|
||||
ui.dict_focus = DictFocus::Words;
|
||||
}
|
||||
|
||||
pub fn clear_search(ui: &mut UiState) {
|
||||
ui.dict_search_query.clear();
|
||||
ui.dict_search_active = false;
|
||||
*ui.dict_scroll_mut() = 0;
|
||||
}
|
||||
|
||||
pub fn search_input(ui: &mut UiState, c: char) {
|
||||
ui.dict_search_query.push(c);
|
||||
*ui.dict_scroll_mut() = 0;
|
||||
}
|
||||
|
||||
pub fn search_backspace(ui: &mut UiState) {
|
||||
ui.dict_search_query.pop();
|
||||
*ui.dict_scroll_mut() = 0;
|
||||
}
|
||||
|
||||
pub fn search_confirm(ui: &mut UiState) {
|
||||
ui.dict_search_active = false;
|
||||
}
|
||||
56
src/services/euclidean.rs
Normal file
56
src/services/euclidean.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::model::Project;
|
||||
|
||||
pub fn euclidean_rhythm(pulses: usize, steps: usize, rotation: usize) -> Vec<bool> {
|
||||
if pulses == 0 || steps == 0 || pulses > steps {
|
||||
return vec![false; steps];
|
||||
}
|
||||
|
||||
let mut pattern = vec![false; steps];
|
||||
for i in 0..pulses {
|
||||
let pos = (i * steps) / pulses;
|
||||
pattern[pos] = true;
|
||||
}
|
||||
|
||||
if rotation > 0 {
|
||||
pattern.rotate_left(rotation % steps);
|
||||
}
|
||||
|
||||
pattern
|
||||
}
|
||||
|
||||
/// Applies euclidean distribution as linked steps from a source step.
|
||||
/// Returns the indices of steps that were created (for compilation).
|
||||
pub fn apply_distribution(
|
||||
project: &mut Project,
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
source_step: usize,
|
||||
pulses: usize,
|
||||
steps: usize,
|
||||
rotation: usize,
|
||||
) -> Vec<usize> {
|
||||
let pat_len = project.pattern_at(bank, pattern).length;
|
||||
let rhythm = euclidean_rhythm(pulses, steps, rotation);
|
||||
|
||||
let mut targets = Vec::new();
|
||||
for (i, &is_hit) in rhythm.iter().enumerate() {
|
||||
if !is_hit || i == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let target = (source_step + i) % pat_len;
|
||||
if target == source_step {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) {
|
||||
step.source = Some(source_step);
|
||||
step.script.clear();
|
||||
step.command = None;
|
||||
step.active = true;
|
||||
}
|
||||
targets.push(target);
|
||||
}
|
||||
|
||||
targets
|
||||
}
|
||||
61
src/services/help_nav.rs
Normal file
61
src/services/help_nav.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use crate::model::docs;
|
||||
use crate::state::{HelpFocus, UiState};
|
||||
|
||||
pub fn toggle_focus(ui: &mut UiState) {
|
||||
ui.help_focus = match ui.help_focus {
|
||||
HelpFocus::Topics => HelpFocus::Content,
|
||||
HelpFocus::Content => HelpFocus::Topics,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn next_topic(ui: &mut UiState, n: usize) {
|
||||
let count = docs::topic_count();
|
||||
ui.help_topic = (ui.help_topic + n) % count;
|
||||
}
|
||||
|
||||
pub fn prev_topic(ui: &mut UiState, n: usize) {
|
||||
let count = docs::topic_count();
|
||||
ui.help_topic = (ui.help_topic + count - (n % count)) % count;
|
||||
}
|
||||
|
||||
pub fn scroll_down(ui: &mut UiState, n: usize) {
|
||||
let s = ui.help_scroll_mut();
|
||||
*s = s.saturating_add(n);
|
||||
}
|
||||
|
||||
pub fn scroll_up(ui: &mut UiState, n: usize) {
|
||||
let s = ui.help_scroll_mut();
|
||||
*s = s.saturating_sub(n);
|
||||
}
|
||||
|
||||
pub fn activate_search(ui: &mut UiState) {
|
||||
ui.help_search_active = true;
|
||||
}
|
||||
|
||||
pub fn clear_search(ui: &mut UiState) {
|
||||
ui.help_search_query.clear();
|
||||
ui.help_search_active = false;
|
||||
}
|
||||
|
||||
pub fn search_input(ui: &mut UiState, c: char) {
|
||||
ui.help_search_query.push(c);
|
||||
if let Some((topic, line)) = docs::find_match(&ui.help_search_query) {
|
||||
ui.help_topic = topic;
|
||||
ui.help_scrolls[topic] = line;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_backspace(ui: &mut UiState) {
|
||||
ui.help_search_query.pop();
|
||||
if ui.help_search_query.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some((topic, line)) = docs::find_match(&ui.help_search_query) {
|
||||
ui.help_topic = topic;
|
||||
ui.help_scrolls[topic] = line;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_confirm(ui: &mut UiState) {
|
||||
ui.help_search_active = false;
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
pub mod clipboard;
|
||||
pub mod dict_nav;
|
||||
pub mod euclidean;
|
||||
pub mod help_nav;
|
||||
pub mod pattern_editor;
|
||||
pub mod stack_preview;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::model::{PatternSpeed, Project};
|
||||
use crate::model::{Bank, Pattern, PatternSpeed, Project};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PatternEdit {
|
||||
@@ -90,3 +90,38 @@ pub fn get_step_script(
|
||||
.step(step)
|
||||
.map(|s| s.script.clone())
|
||||
}
|
||||
|
||||
pub fn delete_step(project: &mut Project, bank: usize, pattern: usize, step: usize) -> PatternEdit {
|
||||
let pat = 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;
|
||||
}
|
||||
}
|
||||
|
||||
set_step_script(project, bank, pattern, step, String::new());
|
||||
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) {
|
||||
s.command = None;
|
||||
s.source = None;
|
||||
}
|
||||
PatternEdit::new(bank, pattern)
|
||||
}
|
||||
|
||||
pub fn delete_steps(project: &mut Project, bank: usize, pattern: usize, steps: &[usize]) -> PatternEdit {
|
||||
for &step in steps {
|
||||
delete_step(project, bank, pattern, step);
|
||||
}
|
||||
PatternEdit::new(bank, pattern)
|
||||
}
|
||||
|
||||
pub fn reset_pattern(project: &mut Project, bank: usize, pattern: usize) -> PatternEdit {
|
||||
project.banks[bank].patterns[pattern] = Pattern::default();
|
||||
PatternEdit::new(bank, pattern)
|
||||
}
|
||||
|
||||
pub fn reset_bank(project: &mut Project, bank: usize) -> usize {
|
||||
project.banks[bank] = Bank::default();
|
||||
project.banks[bank].patterns.len()
|
||||
}
|
||||
|
||||
106
src/services/stack_preview.rs
Normal file
106
src/services/stack_preview.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
|
||||
use crate::model::{ScriptEngine, StepContext, Value};
|
||||
use crate::state::{EditorContext, StackCache};
|
||||
|
||||
pub fn update_cache(editor_ctx: &EditorContext) {
|
||||
let lines = editor_ctx.editor.lines();
|
||||
let cursor_line = editor_ctx.editor.cursor().0;
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if i > cursor_line {
|
||||
break;
|
||||
}
|
||||
line.hash(&mut hasher);
|
||||
}
|
||||
let lines_hash = hasher.finish();
|
||||
|
||||
if let Some(ref c) = *editor_ctx.stack_cache.borrow() {
|
||||
if c.cursor_line == cursor_line && c.lines_hash == lines_hash {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let partial: Vec<&str> = lines
|
||||
.iter()
|
||||
.take(cursor_line + 1)
|
||||
.map(|s| s.as_str())
|
||||
.collect();
|
||||
let script = partial.join("\n");
|
||||
|
||||
let result = if script.trim().is_empty() {
|
||||
"Stack: []".to_string()
|
||||
} else {
|
||||
let vars = 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(42)));
|
||||
let engine = ScriptEngine::new(vars, dict, rng);
|
||||
|
||||
let ctx = StepContext {
|
||||
step: 0,
|
||||
beat: 0.0,
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
tempo: 120.0,
|
||||
phase: 0.0,
|
||||
slot: 0,
|
||||
runs: 0,
|
||||
iter: 0,
|
||||
speed: 1.0,
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
cc_access: None,
|
||||
speed_key: "",
|
||||
chain_key: "",
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
};
|
||||
|
||||
match engine.evaluate(&script, &ctx) {
|
||||
Ok(_) => {
|
||||
let stack = engine.stack();
|
||||
let formatted: Vec<String> = stack.iter().map(format_value).collect();
|
||||
format!("Stack: [{}]", formatted.join(" "))
|
||||
}
|
||||
Err(e) => format!("Error: {e}"),
|
||||
}
|
||||
};
|
||||
|
||||
*editor_ctx.stack_cache.borrow_mut() = Some(StackCache {
|
||||
cursor_line,
|
||||
lines_hash,
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
fn format_value(v: &Value) -> String {
|
||||
match v {
|
||||
Value::Int(n, _) => n.to_string(),
|
||||
Value::Float(f, _) => {
|
||||
if f.fract() == 0.0 && f.abs() < 1_000_000.0 {
|
||||
format!("{f:.1}")
|
||||
} else {
|
||||
format!("{f:.4}")
|
||||
}
|
||||
}
|
||||
Value::Str(s, _) => format!("\"{s}\""),
|
||||
Value::Quotation(..) => "[...]".to_string(),
|
||||
Value::CycleList(items) => {
|
||||
let inner: Vec<String> = items.iter().map(format_value).collect();
|
||||
format!("({})", inner.join(" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
@@ -16,121 +8,21 @@ use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::{LinkState, SequencerSnapshot};
|
||||
use crate::model::{SourceSpan, StepContext, Value};
|
||||
use crate::model::SourceSpan;
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache,
|
||||
EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
||||
};
|
||||
use crate::theme;
|
||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||
use crate::widgets::{
|
||||
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
|
||||
};
|
||||
use cagire_forth::Forth;
|
||||
|
||||
use super::{
|
||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
||||
};
|
||||
|
||||
fn compute_stack_display(
|
||||
lines: &[String],
|
||||
editor: &cagire_ratatui::Editor,
|
||||
cache: &std::cell::RefCell<Option<StackCache>>,
|
||||
) -> String {
|
||||
let cursor_line = editor.cursor().0;
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
if i > cursor_line {
|
||||
break;
|
||||
}
|
||||
line.hash(&mut hasher);
|
||||
}
|
||||
let lines_hash = hasher.finish();
|
||||
|
||||
if let Some(ref c) = *cache.borrow() {
|
||||
if c.cursor_line == cursor_line && c.lines_hash == lines_hash {
|
||||
return c.result.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let partial: Vec<&str> = lines
|
||||
.iter()
|
||||
.take(cursor_line + 1)
|
||||
.map(|s| s.as_str())
|
||||
.collect();
|
||||
let script = partial.join("\n");
|
||||
|
||||
let result = if script.trim().is_empty() {
|
||||
"Stack: []".to_string()
|
||||
} else {
|
||||
let vars = 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(42)));
|
||||
let forth = Forth::new(vars, dict, rng);
|
||||
|
||||
let ctx = StepContext {
|
||||
step: 0,
|
||||
beat: 0.0,
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
tempo: 120.0,
|
||||
phase: 0.0,
|
||||
slot: 0,
|
||||
runs: 0,
|
||||
iter: 0,
|
||||
speed: 1.0,
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
cc_access: None,
|
||||
speed_key: "",
|
||||
chain_key: "",
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
};
|
||||
|
||||
match forth.evaluate(&script, &ctx) {
|
||||
Ok(_) => {
|
||||
let stack = forth.stack();
|
||||
let formatted: Vec<String> = stack.iter().map(format_value).collect();
|
||||
format!("Stack: [{}]", formatted.join(" "))
|
||||
}
|
||||
Err(e) => format!("Error: {e}"),
|
||||
}
|
||||
};
|
||||
|
||||
*cache.borrow_mut() = Some(StackCache {
|
||||
cursor_line,
|
||||
lines_hash,
|
||||
result: result.clone(),
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn format_value(v: &Value) -> String {
|
||||
match v {
|
||||
Value::Int(n, _) => n.to_string(),
|
||||
Value::Float(f, _) => {
|
||||
if f.fract() == 0.0 && f.abs() < 1_000_000.0 {
|
||||
format!("{f:.1}")
|
||||
} else {
|
||||
format!("{f:.4}")
|
||||
}
|
||||
}
|
||||
Value::Str(s, _) => format!("\"{s}\""),
|
||||
Value::Quotation(..) => "[...]".to_string(),
|
||||
Value::CycleList(items) => {
|
||||
let inner: Vec<String> = items.iter().map(format_value).collect();
|
||||
format!("({})", inner.join(" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn adjust_spans_for_line(
|
||||
spans: &[SourceSpan],
|
||||
line_start: usize,
|
||||
@@ -850,11 +742,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||
} else if app.editor_ctx.show_stack {
|
||||
let stack_text = compute_stack_display(
|
||||
text_lines,
|
||||
&app.editor_ctx.editor,
|
||||
&app.editor_ctx.stack_cache,
|
||||
);
|
||||
let stack_text = app
|
||||
.editor_ctx
|
||||
.stack_cache
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|c| c.result.clone())
|
||||
.unwrap_or_else(|| "Stack: []".to_string());
|
||||
let hint = Line::from(vec![
|
||||
Span::styled("Esc", key),
|
||||
Span::styled(" save ", dim),
|
||||
|
||||
Reference in New Issue
Block a user