Some kind of refactoring
This commit is contained in:
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(" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user