Some kind of refactoring

This commit is contained in:
2026-02-04 19:35:30 +01:00
parent 6cf9d2eec1
commit 82b0668bcf
18 changed files with 688 additions and 772 deletions

View File

@@ -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
}