Feat: optimizations

This commit is contained in:
2026-02-05 23:15:46 +01:00
parent 2c98a915fa
commit 51f52be4ce
22 changed files with 175 additions and 215 deletions

View File

@@ -6,8 +6,20 @@ All notable changes to this project will be documented in this file.
### Improved ### Improved
- Sample library browser: search now shows folder names only (no files) while typing, sorted by fuzzy match score. After confirming search with Enter, folders can be expanded and collapsed normally. Esc clears the search filter before closing the panel. Left arrow on a file collapses the parent folder. Cursor and scroll position stay valid after expand/collapse operations. - Sample library browser: search now shows folder names only (no files) while typing, sorted by fuzzy match score. After confirming search with Enter, folders can be expanded and collapsed normally. Esc clears the search filter before closing the panel. Left arrow on a file collapses the parent folder. Cursor and scroll position stay valid after expand/collapse operations.
- RAM optimizations saving ~5 MB at startup plus smaller enums and fewer hot-path allocations:
- Removed dead `Step::command` field (~3.1 MB)
- Narrowed `Step::source` from `Option<usize>` to `Option<u8>` (~1.8 MB)
- `Op::SetParam` and `Op::GetContext` now use `&'static str` instead of `String`
- `SourceSpan` fields narrowed from `usize` to `u32`
- Dirty pattern tracking uses fixed `[[bool; 32]; 32]` array instead of `HashSet`
- Boxed `FileBrowserState` in `Modal` enum to shrink all variants
- `StepContext::cc_access` borrows instead of cloning `Arc<dyn CcAccess>`
- Removed unnecessary `Arc` wrapper from `Stack` type
- Variable key cache computes on-demand with reusable buffers instead of pre-allocating 2048 Strings
### Changed ### Changed
- Header bar is now always 3 lines tall with vertically centered content and full-height background colors, replacing the previous 1-or-2-line width-dependent layout.
- Help view Welcome page: BigText title is now gated behind `cfg(not(feature = "desktop"))`, falling back to a plain text title in the desktop build (same strategy as the splash screen).
- Space now toggles play/pause on all views, including the Patterns page where it previously toggled pattern play. Pattern play on the Patterns page is now bound to `p`. - Space now toggles play/pause on all views, including the Patterns page where it previously toggled pattern play. Pattern play on the Patterns page is now bound to `p`.
## [0.0.7] - 2026-05-02 ## [0.0.7] - 2026-05-02

View File

@@ -45,7 +45,7 @@ fn tokenize(input: &str) -> Vec<Token> {
} }
s.push(ch); s.push(ch);
} }
tokens.push(Token::Str(s, SourceSpan { start, end })); tokens.push(Token::Str(s, SourceSpan { start: start as u32, end: end as u32 }));
continue; continue;
} }
@@ -66,8 +66,8 @@ fn tokenize(input: &str) -> Vec<Token> {
tokens.push(Token::Word( tokens.push(Token::Word(
";".to_string(), ";".to_string(),
SourceSpan { SourceSpan {
start: pos, start: pos as u32,
end: pos + 1, end: (pos + 1) as u32,
}, },
)); ));
continue; continue;
@@ -85,7 +85,7 @@ fn tokenize(input: &str) -> Vec<Token> {
chars.next(); chars.next();
} }
let span = SourceSpan { start, end }; let span = SourceSpan { start: start as u32, end: end as u32 };
// Normalize shorthand float syntax: .25 -> 0.25, -.5 -> -0.5 // Normalize shorthand float syntax: .25 -> 0.25, -.5 -> -0.5
let word_to_parse = if word.starts_with('.') let word_to_parse = if word.starts_with('.')

View File

@@ -60,11 +60,11 @@ pub enum Op {
BranchIfZero(usize, Option<SourceSpan>, Option<SourceSpan>), BranchIfZero(usize, Option<SourceSpan>, Option<SourceSpan>),
Branch(usize), Branch(usize),
NewCmd, NewCmd,
SetParam(String), SetParam(&'static str),
Emit, Emit,
Get, Get,
Set, Set,
GetContext(String), GetContext(&'static str),
Rand, Rand,
ExpRand, ExpRand,
LogRand, LogRand,

View File

@@ -14,8 +14,8 @@ pub trait CcAccess: Send + Sync {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SourceSpan { pub struct SourceSpan {
pub start: usize, pub start: u32,
pub end: usize, pub end: u32,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@@ -37,7 +37,7 @@ pub struct StepContext<'a> {
pub speed: f64, pub speed: f64,
pub fill: bool, pub fill: bool,
pub nudge_secs: f64, pub nudge_secs: f64,
pub cc_access: Option<Arc<dyn CcAccess>>, pub cc_access: Option<&'a dyn CcAccess>,
pub speed_key: &'a str, pub speed_key: &'a str,
pub chain_key: &'a str, pub chain_key: &'a str,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
@@ -58,8 +58,8 @@ pub type VariablesMap = HashMap<String, Value>;
pub type Variables = Arc<ArcSwap<VariablesMap>>; pub type Variables = Arc<ArcSwap<VariablesMap>>;
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>; pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
pub type Rng = Arc<Mutex<StdRng>>; pub type Rng = Arc<Mutex<StdRng>>;
pub type Stack = Arc<Mutex<Vec<Value>>>; pub type Stack = Mutex<Vec<Value>>;
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(String, Value)]); pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Value { pub enum Value {
@@ -138,7 +138,7 @@ impl Value {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub(super) struct CmdRegister { pub(super) struct CmdRegister {
sound: Option<Value>, sound: Option<Value>,
params: Vec<(String, Value)>, params: Vec<(&'static str, Value)>,
deltas: Vec<Value>, deltas: Vec<Value>,
} }
@@ -155,7 +155,7 @@ impl CmdRegister {
self.sound = Some(val); self.sound = Some(val);
} }
pub(super) fn set_param(&mut self, key: String, val: Value) { pub(super) fn set_param(&mut self, key: &'static str, val: Value) {
self.params.push((key, val)); self.params.push((key, val));
} }
@@ -171,7 +171,7 @@ impl CmdRegister {
self.sound.as_ref() self.sound.as_ref()
} }
pub(super) fn params(&self) -> &[(String, Value)] { pub(super) fn params(&self) -> &[(&'static str, Value)] {
&self.params &self.params
} }

View File

@@ -22,7 +22,7 @@ pub struct Forth {
impl Forth { impl Forth {
pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self { pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self {
Self { Self {
stack: Arc::new(Mutex::new(Vec::new())), stack: Mutex::new(Vec::new()),
vars, vars,
dict, dict,
rng, rng,
@@ -227,7 +227,7 @@ impl Forth {
Some(v) => Some(v.as_str()?.to_string()), Some(v) => Some(v.as_str()?.to_string()),
None => None, None => None,
}; };
let resolved_params: Vec<(String, String)> = params let resolved_params: Vec<(&str, String)> = params
.iter() .iter()
.map(|(k, v)| { .map(|(k, v)| {
let resolved = resolve_cycling(v, emit_idx); let resolved = resolve_cycling(v, emit_idx);
@@ -238,7 +238,7 @@ impl Forth {
} }
} }
} }
(k.clone(), resolved.to_param_string()) (*k, resolved.to_param_string())
}) })
.collect(); .collect();
emit_output( emit_output(
@@ -555,7 +555,7 @@ impl Forth {
} else { } else {
Value::CycleList(Arc::from(values)) Value::CycleList(Arc::from(values))
}; };
cmd.set_param(param.clone(), val); cmd.set_param(param, val);
} }
Op::Emit => { Op::Emit => {
@@ -613,7 +613,7 @@ impl Forth {
} }
Op::GetContext(name) => { Op::GetContext(name) => {
let val = match name.as_str() { let val = match *name {
"step" => Value::Int(ctx.step as i64, None), "step" => Value::Int(ctx.step as i64, None),
"beat" => Value::Float(ctx.beat, None), "beat" => Value::Float(ctx.beat, None),
"bank" => Value::Int(ctx.bank as i64, None), "bank" => Value::Int(ctx.bank as i64, None),
@@ -879,8 +879,8 @@ impl Forth {
return Err("tempo and speed must be non-zero".into()); return Err("tempo and speed must be non-zero".into());
} }
let dur = beats * 60.0 / ctx.tempo / ctx.speed; let dur = beats * 60.0 / ctx.tempo / ctx.speed;
cmd.set_param("fit".into(), Value::Float(dur, None)); cmd.set_param("fit", Value::Float(dur, None));
cmd.set_param("dur".into(), Value::Float(dur, None)); cmd.set_param("dur", Value::Float(dur, None));
} }
Op::At => { Op::At => {
@@ -896,18 +896,18 @@ impl Forth {
let s = stack.pop().ok_or("stack underflow")?; let s = stack.pop().ok_or("stack underflow")?;
let d = stack.pop().ok_or("stack underflow")?; let d = stack.pop().ok_or("stack underflow")?;
let a = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?;
cmd.set_param("attack".into(), a); cmd.set_param("attack", a);
cmd.set_param("decay".into(), d); cmd.set_param("decay", d);
cmd.set_param("sustain".into(), s); cmd.set_param("sustain", s);
cmd.set_param("release".into(), r); cmd.set_param("release", r);
} }
Op::Ad => { Op::Ad => {
let d = stack.pop().ok_or("stack underflow")?; let d = stack.pop().ok_or("stack underflow")?;
let a = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?;
cmd.set_param("attack".into(), a); cmd.set_param("attack", a);
cmd.set_param("decay".into(), d); cmd.set_param("decay", d);
cmd.set_param("sustain".into(), Value::Int(0, None)); cmd.set_param("sustain", Value::Int(0, None));
} }
Op::Apply => { Op::Apply => {
@@ -1055,14 +1055,14 @@ impl Forth {
params params
.iter() .iter()
.rev() .rev()
.find(|(k, _)| k == name) .find(|(k, _)| *k == name)
.and_then(|(_, v)| v.as_int().ok()) .and_then(|(_, v)| v.as_int().ok())
}; };
let get_float = |name: &str| -> Option<f64> { let get_float = |name: &str| -> Option<f64> {
params params
.iter() .iter()
.rev() .rev()
.find(|(k, _)| k == name) .find(|(k, _)| *k == name)
.and_then(|(_, v)| v.as_float().ok()) .and_then(|(_, v)| v.as_float().ok())
}; };
let chan = get_int("chan") let chan = get_int("chan")
@@ -1140,11 +1140,11 @@ impl Forth {
} }
} }
fn extract_dev_param(params: &[(String, Value)]) -> u8 { fn extract_dev_param(params: &[(&str, Value)]) -> u8 {
params params
.iter() .iter()
.rev() .rev()
.find(|(k, _)| k == "dev") .find(|(k, _)| *k == "dev")
.and_then(|(_, v)| v.as_int().ok()) .and_then(|(_, v)| v.as_int().ok())
.map(|d| d.clamp(0, 3) as u8) .map(|d| d.clamp(0, 3) as u8)
.unwrap_or(0) .unwrap_or(0)
@@ -1181,7 +1181,7 @@ fn is_tempo_scaled_param(name: &str) -> bool {
fn emit_output( fn emit_output(
sound: Option<&str>, sound: Option<&str>,
params: &[(String, String)], params: &[(&str, String)],
step_duration: f64, step_duration: f64,
nudge_secs: f64, nudge_secs: f64,
outputs: &mut Vec<String>, outputs: &mut Vec<String>,
@@ -1190,8 +1190,8 @@ fn emit_output(
let mut out = String::with_capacity(128); let mut out = String::with_capacity(128);
out.push('/'); out.push('/');
let has_dur = params.iter().any(|(k, _)| k == "dur"); let has_dur = params.iter().any(|(k, _)| *k == "dur");
let delaytime_idx = params.iter().position(|(k, _)| k == "delaytime"); let delaytime_idx = params.iter().position(|(k, _)| *k == "delaytime");
if let Some(s) = sound { if let Some(s) = sound {
let _ = write!(&mut out, "sound/{s}"); let _ = write!(&mut out, "sound/{s}");

View File

@@ -216,8 +216,8 @@ pub(crate) fn compile_word(
ops.push(op); ops.push(op);
} }
} }
Context(ctx) => ops.push(Op::GetContext((*ctx).into())), Context(ctx) => ops.push(Op::GetContext(ctx)),
Param => ops.push(Op::SetParam(word.name.into())), Param => ops.push(Op::SetParam(word.name)),
Probability(p) => { Probability(p) => {
ops.push(Op::PushFloat(*p, None)); ops.push(Op::PushFloat(*p, None));
ops.push(Op::ChanceExec); ops.push(Op::ChanceExec);

View File

@@ -210,10 +210,8 @@ impl SyncMode {
pub struct Step { pub struct Step {
pub active: bool, pub active: bool,
pub script: String, pub script: String,
#[serde(skip)]
pub command: Option<String>,
#[serde(default)] #[serde(default)]
pub source: Option<usize>, pub source: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
} }
@@ -229,7 +227,6 @@ impl Default for Step {
Self { Self {
active: true, active: true,
script: String::new(), script: String::new(),
command: None,
source: None, source: None,
name: None, name: None,
} }
@@ -254,7 +251,7 @@ struct SparseStep {
#[serde(default, skip_serializing_if = "String::is_empty")] #[serde(default, skip_serializing_if = "String::is_empty")]
script: String, script: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
source: Option<usize>, source: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>, name: Option<String>,
} }
@@ -348,7 +345,6 @@ impl<'de> Deserialize<'de> for Pattern {
steps[ss.i] = Step { steps[ss.i] = Step {
active: ss.active, active: ss.active,
script: ss.script, script: ss.script,
command: None,
source: ss.source, source: ss.source,
name: ss.name, name: ss.name,
}; };
@@ -410,7 +406,7 @@ impl Pattern {
for _ in 0..self.steps.len() { for _ in 0..self.steps.len() {
if let Some(step) = self.steps.get(current) { if let Some(step) = self.steps.get(current) {
if let Some(source) = step.source { if let Some(source) = step.source {
current = source; current = source as usize;
} else { } else {
return current; return current;
} }

View File

@@ -419,44 +419,16 @@ impl App {
.unwrap_or_default(); .unwrap_or_default();
if script.trim().is_empty() { if script.trim().is_empty() {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = None;
}
return; return;
} }
let ctx = self.create_step_context(step_idx, link); let ctx = self.create_step_context(step_idx, link);
match self.script_engine.evaluate(&script, &ctx) { match self.script_engine.evaluate(&script, &ctx) {
Ok(cmds) => { Ok(_) => {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = if cmds.is_empty() {
None
} else {
Some(cmds.join("\n"))
};
}
self.ui.flash("Script compiled", 150, FlashKind::Info); self.ui.flash("Script compiled", 150, FlashKind::Info);
} }
Err(e) => { Err(e) => {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = None;
}
self.ui self.ui
.flash(&format!("Script error: {e}"), 300, FlashKind::Error); .flash(&format!("Script error: {e}"), 300, FlashKind::Error);
} }
@@ -477,33 +449,11 @@ impl App {
.unwrap_or_default(); .unwrap_or_default();
if script.trim().is_empty() { if script.trim().is_empty() {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = None;
}
continue; continue;
} }
let ctx = self.create_step_context(step_idx, link); let ctx = self.create_step_context(step_idx, link);
let _ = self.script_engine.evaluate(&script, &ctx);
if let Ok(cmds) = self.script_engine.evaluate(&script, &ctx) {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = if cmds.is_empty() {
None
} else {
Some(cmds.join("\n"))
};
}
}
} }
} }
@@ -1054,7 +1004,7 @@ impl App {
[self.editor_ctx.pattern]; [self.editor_ctx.pattern];
if let Some(source) = pattern.step(self.editor_ctx.step).and_then(|s| s.source) if let Some(source) = pattern.step(self.editor_ctx.step).and_then(|s| s.source)
{ {
self.editor_ctx.step = source; self.editor_ctx.step = source as usize;
} }
self.load_step_to_editor(); self.load_step_to_editor();
} }

View File

@@ -140,7 +140,7 @@ pub struct PatternSnapshot {
pub struct StepSnapshot { pub struct StepSnapshot {
pub active: bool, pub active: bool,
pub script: String, pub script: String,
pub source: Option<usize>, pub source: Option<u8>,
} }
#[derive(Clone, Copy, Default, Debug)] #[derive(Clone, Copy, Default, Debug)]
@@ -392,7 +392,7 @@ impl PatternSnapshot {
for _ in 0..self.steps.len() { for _ in 0..self.steps.len() {
if let Some(step) = self.steps.get(current) { if let Some(step) = self.steps.get(current) {
if let Some(source) = step.source { if let Some(source) = step.source {
current = source; current = source as usize;
} else { } else {
return current; return current;
} }
@@ -500,30 +500,32 @@ fn parse_chain_target(s: &str) -> Option<PatternId> {
}) })
} }
struct KeyCache { struct KeyBuf {
speed_keys: [[String; MAX_PATTERNS]; MAX_BANKS], speed: String,
chain_keys: [[String; MAX_PATTERNS]; MAX_BANKS], chain: String,
} }
impl KeyCache { impl KeyBuf {
fn new() -> Self { fn new() -> Self {
Self { Self {
speed_keys: std::array::from_fn(|bank| { speed: String::with_capacity(24),
std::array::from_fn(|pattern| format!("__speed_{bank}_{pattern}__")) chain: String::with_capacity(24),
}),
chain_keys: std::array::from_fn(|bank| {
std::array::from_fn(|pattern| format!("__chain_{bank}_{pattern}__"))
}),
} }
} }
}
fn speed_key(&self, bank: usize, pattern: usize) -> &str { fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
&self.speed_keys[bank][pattern] use std::fmt::Write;
} buf.clear();
write!(buf, "__speed_{bank}_{pattern}__").unwrap();
buf
}
fn chain_key(&self, bank: usize, pattern: usize) -> &str { fn format_chain_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
&self.chain_keys[bank][pattern] use std::fmt::Write;
} buf.clear();
write!(buf, "__chain_{bank}_{pattern}__").unwrap();
buf
} }
pub(crate) struct SequencerState { pub(crate) struct SequencerState {
@@ -537,7 +539,7 @@ pub(crate) struct SequencerState {
variables: Variables, variables: Variables,
dict: Dictionary, dict: Dictionary,
speed_overrides: HashMap<(usize, usize), f64>, speed_overrides: HashMap<(usize, usize), f64>,
key_cache: KeyCache, key_buf: KeyBuf,
buf_audio_commands: Vec<TimestampedCommand>, buf_audio_commands: Vec<TimestampedCommand>,
buf_activated: Vec<PatternId>, buf_activated: Vec<PatternId>,
buf_stopped: Vec<PatternId>, buf_stopped: Vec<PatternId>,
@@ -566,7 +568,7 @@ impl SequencerState {
variables, variables,
dict, dict,
speed_overrides: HashMap::with_capacity(MAX_PATTERNS), speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
key_cache: KeyCache::new(), key_buf: KeyBuf::new(),
buf_audio_commands: Vec::with_capacity(32), buf_audio_commands: Vec::with_capacity(32),
buf_activated: Vec::with_capacity(16), buf_activated: Vec::with_capacity(16),
buf_stopped: Vec::with_capacity(16), buf_stopped: Vec::with_capacity(16),
@@ -834,7 +836,7 @@ impl SequencerState {
{ {
let vars = self.variables.load(); let vars = self.variables.load();
for id in self.audio_state.active_patterns.keys() { for id in self.audio_state.active_patterns.keys() {
let key = self.key_cache.speed_key(id.bank, id.pattern); let key = format_speed_key(&mut self.key_buf.speed, id.bank, id.pattern);
if let Some(v) = vars.get(key).and_then(|v: &Value| v.as_float().ok()) { if let Some(v) = vars.get(key).and_then(|v: &Value| v.as_float().ok()) {
self.speed_overrides.insert((id.bank, id.pattern), v); self.speed_overrides.insert((id.bank, id.pattern), v);
} }
@@ -884,6 +886,8 @@ impl SequencerState {
active.pattern, active.pattern,
source_idx, source_idx,
); );
let speed_key = format_speed_key(&mut self.key_buf.speed, active.bank, active.pattern);
let chain_key = format_chain_key(&mut self.key_buf.chain, active.bank, active.pattern);
let ctx = StepContext { let ctx = StepContext {
step: step_idx, step: step_idx,
beat: step_beat, beat: step_beat,
@@ -897,9 +901,9 @@ impl SequencerState {
speed: speed_mult, speed: speed_mult,
fill, fill,
nudge_secs, nudge_secs,
cc_access: self.cc_access.clone(), cc_access: self.cc_access.as_deref(),
speed_key: self.key_cache.speed_key(active.bank, active.pattern), speed_key,
chain_key: self.key_cache.chain_key(active.bank, active.pattern), chain_key,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
mouse_x, mouse_x,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
@@ -970,8 +974,9 @@ impl SequencerState {
.and_then(|v: &Value| v.as_float().ok()); .and_then(|v: &Value| v.as_float().ok());
let mut chain_transitions = Vec::new(); let mut chain_transitions = Vec::new();
let mut buf = String::with_capacity(24);
for id in completed { for id in completed {
let chain_key = self.key_cache.chain_key(id.bank, id.pattern); let chain_key = format_chain_key(&mut buf, id.bank, id.pattern);
if let Some(Value::Str(s, _)) = vars.get(chain_key) { if let Some(Value::Str(s, _)) = vars.get(chain_key) {
if let Some(target) = parse_chain_target(s) { if let Some(target) = parse_chain_target(s) {
chain_transitions.push((*id, target)); chain_transitions.push((*id, target));
@@ -980,24 +985,24 @@ impl SequencerState {
} }
// Remove consumed variables (tempo and chain keys) // Remove consumed variables (tempo and chain keys)
let needs_removal = new_tempo.is_some() let mut needs_removal = new_tempo.is_some();
|| completed.iter().any(|id| { if !needs_removal {
let chain_key = self.key_cache.chain_key(id.bank, id.pattern); for id in completed.iter().chain(stopped.iter()) {
vars.contains_key(chain_key) if vars.contains_key(format_chain_key(&mut buf, id.bank, id.pattern)) {
}) needs_removal = true;
|| stopped.iter().any(|id| { break;
let chain_key = self.key_cache.chain_key(id.bank, id.pattern); }
vars.contains_key(chain_key) }
}); }
if needs_removal { if needs_removal {
let mut new_vars = (**vars).clone(); let mut new_vars = (**vars).clone();
new_vars.remove("__tempo__"); new_vars.remove("__tempo__");
for id in completed { for id in completed {
new_vars.remove(self.key_cache.chain_key(id.bank, id.pattern)); new_vars.remove(format_chain_key(&mut buf, id.bank, id.pattern));
} }
for id in stopped { for id in stopped {
new_vars.remove(self.key_cache.chain_key(id.bank, id.pattern)); new_vars.remove(format_chain_key(&mut buf, id.bank, id.pattern));
} }
self.variables.store(Arc::new(new_vars)); self.variables.store(Arc::new(new_vars));
} }

View File

@@ -979,7 +979,7 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
.map(|p| p.display().to_string()) .map(|p| p.display().to_string())
.unwrap_or_default(); .unwrap_or_default();
let state = FileBrowserState::new_save(initial); let state = FileBrowserState::new_save(initial);
ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(state))); ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state))));
} }
KeyCode::Char('c') if ctrl => { KeyCode::Char('c') if ctrl => {
ctx.dispatch(AppCommand::CopySteps); ctx.dispatch(AppCommand::CopySteps);
@@ -1011,7 +1011,7 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
}) })
.unwrap_or_default(); .unwrap_or_default();
let state = FileBrowserState::new_load(default_dir); let state = FileBrowserState::new_load(default_dir);
ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(state))); ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state))));
} }
KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp), KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp),
KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown), KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown),
@@ -1422,7 +1422,7 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::Char('A') => { KeyCode::Char('A') => {
use crate::state::file_browser::FileBrowserState; use crate::state::file_browser::FileBrowserState;
let state = FileBrowserState::new_load(String::new()); let state = FileBrowserState::new_load(String::new());
ctx.dispatch(AppCommand::OpenModal(Modal::AddSamplePath(state))); ctx.dispatch(AppCommand::OpenModal(Modal::AddSamplePath(Box::new(state))));
} }
KeyCode::Char('D') => { KeyCode::Char('D') => {
if ctx.app.audio.section == EngineSection::Samples { if ctx.app.audio.section == EngineSection::Samples {

View File

@@ -95,7 +95,6 @@ pub fn paste_steps(
step.name = data.name.clone(); step.name = data.name.clone();
if source.is_some() { if source.is_some() {
step.script.clear(); step.script.clear();
step.command = None;
} else { } else {
step.script = data.script.clone(); step.script = data.script.clone();
} }
@@ -130,15 +129,14 @@ pub fn link_paste_steps(
let source_idx = if data.source.is_some() { let source_idx = if data.source.is_some() {
data.source data.source
} else { } else {
Some(data.original_index) Some(data.original_index as u8)
}; };
if source_idx == Some(target) { if source_idx == Some(target as u8) {
continue; continue;
} }
if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) { if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) {
step.source = source_idx; step.source = source_idx;
step.script.clear(); step.script.clear();
step.command = None;
} }
} }
@@ -183,7 +181,7 @@ pub fn duplicate_steps(
let pat_len = pat.length; let pat_len = pat.length;
let paste_at = *indices.last().unwrap() + 1; let paste_at = *indices.last().unwrap() + 1;
let dupe_data: Vec<(bool, String, Option<usize>)> = indices let dupe_data: Vec<(bool, String, Option<u8>)> = indices
.iter() .iter()
.filter_map(|&idx| { .filter_map(|&idx| {
let step = pat.step(idx)?; let step = pat.step(idx)?;
@@ -204,10 +202,8 @@ pub fn duplicate_steps(
step.source = source; step.source = source;
if source.is_some() { if source.is_some() {
step.script.clear(); step.script.clear();
step.command = None;
} else { } else {
step.script = script; step.script = script;
step.command = None;
} }
} }
compile_targets.push(target); compile_targets.push(target);

View File

@@ -44,9 +44,8 @@ pub fn apply_distribution(
} }
if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) { if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) {
step.source = Some(source_step); step.source = Some(source_step as u8);
step.script.clear(); step.script.clear();
step.command = None;
step.active = true; step.active = true;
} }
targets.push(target); targets.push(target);

View File

@@ -94,16 +94,14 @@ pub fn get_step_script(
pub fn delete_step(project: &mut Project, bank: usize, pattern: usize, step: usize) -> PatternEdit { pub fn delete_step(project: &mut Project, bank: usize, pattern: usize, step: usize) -> PatternEdit {
let pat = project.pattern_at_mut(bank, pattern); let pat = project.pattern_at_mut(bank, pattern);
for s in &mut pat.steps { for s in &mut pat.steps {
if s.source == Some(step) { if s.source == Some(step as u8) {
s.source = None; s.source = None;
s.script.clear(); s.script.clear();
s.command = None;
} }
} }
set_step_script(project, bank, pattern, step, String::new()); set_step_script(project, bank, pattern, step, String::new());
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) { if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) {
s.command = None;
s.source = None; s.source = None;
} }
PatternEdit::new(bank, pattern) PatternEdit::new(bank, pattern)

View File

@@ -104,7 +104,7 @@ pub struct CopiedSteps {
pub struct CopiedStepData { pub struct CopiedStepData {
pub script: String, pub script: String,
pub active: bool, pub active: bool,
pub source: Option<usize>, pub source: Option<u8>,
pub original_index: usize, pub original_index: usize,
pub name: Option<String>, pub name: Option<String>,
} }

View File

@@ -29,7 +29,7 @@ pub enum Modal {
bank: usize, bank: usize,
selected: bool, selected: bool,
}, },
FileBrowser(FileBrowserState), FileBrowser(Box<FileBrowserState>),
RenameBank { RenameBank {
bank: usize, bank: usize,
name: String, name: String,
@@ -50,7 +50,7 @@ pub enum Modal {
input: String, input: String,
}, },
SetTempo(String), SetTempo(String),
AddSamplePath(FileBrowserState), AddSamplePath(Box<FileBrowserState>),
Editor, Editor,
Preview, Preview,
PatternProps { PatternProps {

View File

@@ -1,4 +1,3 @@
use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
use crate::model::{MAX_BANKS, MAX_PATTERNS}; use crate::model::{MAX_BANKS, MAX_PATTERNS};
@@ -7,7 +6,7 @@ use crate::model::Project;
pub struct ProjectState { pub struct ProjectState {
pub project: Project, pub project: Project,
pub file_path: Option<PathBuf>, pub file_path: Option<PathBuf>,
pub dirty_patterns: HashSet<(usize, usize)>, dirty_patterns: [[bool; MAX_PATTERNS]; MAX_BANKS],
} }
impl Default for ProjectState { impl Default for ProjectState {
@@ -15,7 +14,7 @@ impl Default for ProjectState {
let mut state = Self { let mut state = Self {
project: Project::default(), project: Project::default(),
file_path: None, file_path: None,
dirty_patterns: HashSet::new(), dirty_patterns: [[false; MAX_PATTERNS]; MAX_BANKS],
}; };
state.mark_all_dirty(); state.mark_all_dirty();
state state
@@ -24,18 +23,23 @@ impl Default for ProjectState {
impl ProjectState { impl ProjectState {
pub fn mark_dirty(&mut self, bank: usize, pattern: usize) { pub fn mark_dirty(&mut self, bank: usize, pattern: usize) {
self.dirty_patterns.insert((bank, pattern)); self.dirty_patterns[bank][pattern] = true;
} }
pub fn mark_all_dirty(&mut self) { pub fn mark_all_dirty(&mut self) {
for bank in 0..MAX_BANKS { self.dirty_patterns = [[true; MAX_PATTERNS]; MAX_BANKS];
for pattern in 0..MAX_PATTERNS {
self.dirty_patterns.insert((bank, pattern));
}
}
} }
pub fn take_dirty(&mut self) -> HashSet<(usize, usize)> { pub fn take_dirty(&mut self) -> Vec<(usize, usize)> {
std::mem::take(&mut self.dirty_patterns) let mut result = Vec::new();
for (bank, patterns) in self.dirty_patterns.iter_mut().enumerate() {
for (pattern, dirty) in patterns.iter_mut().enumerate() {
if *dirty {
*dirty = false;
result.push((bank, pattern));
}
}
}
result
} }
} }

View File

@@ -4,6 +4,7 @@ use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line as RLine, Span}; use ratatui::text::{Line as RLine, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Wrap}; use ratatui::widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Wrap};
use ratatui::Frame; use ratatui::Frame;
#[cfg(not(feature = "desktop"))]
use tui_big_text::{BigText, PixelSize}; use tui_big_text::{BigText, PixelSize};
use crate::app::App; use crate::app::App;
@@ -173,7 +174,10 @@ fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
} }
const WELCOME_TOPIC: usize = 0; const WELCOME_TOPIC: usize = 0;
#[cfg(not(feature = "desktop"))]
const BIG_TITLE_HEIGHT: u16 = 6; const BIG_TITLE_HEIGHT: u16 = 6;
#[cfg(feature = "desktop")]
const BIG_TITLE_HEIGHT: u16 = 3;
fn render_content(frame: &mut Frame, app: &App, area: Rect) { fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get(); let theme = theme::get();
@@ -186,19 +190,34 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let [title_area, rest] = let [title_area, rest] =
Layout::vertical([Constraint::Length(BIG_TITLE_HEIGHT), Constraint::Fill(1)]) Layout::vertical([Constraint::Length(BIG_TITLE_HEIGHT), Constraint::Fill(1)])
.areas(area); .areas(area);
#[cfg(not(feature = "desktop"))]
let big_title = BigText::builder() let big_title = BigText::builder()
.pixel_size(PixelSize::Quadrant) .pixel_size(PixelSize::Quadrant)
.style(Style::new().fg(theme.markdown.h1).bold()) .style(Style::new().fg(theme.markdown.h1).bold())
.lines(vec!["CAGIRE".into()]) .lines(vec!["CAGIRE".into()])
.centered() .centered()
.build(); .build();
#[cfg(feature = "desktop")]
let big_title = Paragraph::new(RLine::from(Span::styled(
"CAGIRE",
Style::new().fg(theme.markdown.h1).bold(),
)))
.alignment(ratatui::layout::Alignment::Center);
let subtitle = Paragraph::new(RLine::from(Span::styled( let subtitle = Paragraph::new(RLine::from(Span::styled(
"A Forth Sequencer", "A Forth Sequencer",
Style::new().fg(theme.ui.text_primary), Style::new().fg(theme.ui.text_primary),
))) )))
.alignment(ratatui::layout::Alignment::Center); .alignment(ratatui::layout::Alignment::Center);
#[cfg(not(feature = "desktop"))]
let [big_area, subtitle_area] = let [big_area, subtitle_area] =
Layout::vertical([Constraint::Length(4), Constraint::Length(2)]).areas(title_area); Layout::vertical([Constraint::Length(4), Constraint::Length(2)]).areas(title_area);
#[cfg(feature = "desktop")]
let [big_area, subtitle_area] =
Layout::vertical([Constraint::Length(1), Constraint::Length(2)]).areas(title_area);
frame.render_widget(big_title, big_area); frame.render_widget(big_title, big_area);
frame.render_widget(subtitle, subtitle_area); frame.render_widget(subtitle, subtitle_area);
rest rest

View File

@@ -212,10 +212,10 @@ pub fn highlight_line_with_runtime(
let is_selected = selected_spans let is_selected = selected_spans
.iter() .iter()
.any(|span| overlaps(token.start, token.end, span.start, span.end)); .any(|span| overlaps(token.start, token.end, span.start as usize, span.end as usize));
let is_executed = executed_spans let is_executed = executed_spans
.iter() .iter()
.any(|span| overlaps(token.start, token.end, span.start, span.end)); .any(|span| overlaps(token.start, token.end, span.start as usize, span.end as usize));
let mut style = token.kind.style(); let mut style = token.kind.style();
if token.varargs { if token.varargs {

View File

@@ -196,7 +196,7 @@ fn render_tile(
}; };
let link_color = step.and_then(|s| s.source).map(|src| { let link_color = step.and_then(|s| s.source).map(|src| {
let i = src % 5; let i = src as usize % 5;
(theme.tile.link_bright[i], theme.tile.link_dim[i]) (theme.tile.link_bright[i], theme.tile.link_dim[i])
}); });
@@ -236,7 +236,7 @@ fn render_tile(
// For linked steps, get the name from the source step // For linked steps, get the name from the source step
let step_name = if let Some(src) = source_idx { let step_name = if let Some(src) = source_idx {
pattern.step(src).and_then(|s| s.name.as_ref()) pattern.step(src as usize).and_then(|s| s.name.as_ref())
} else { } else {
step.and_then(|s| s.name.as_ref()) step.and_then(|s| s.name.as_ref())
}; };

View File

@@ -3,7 +3,7 @@ use std::time::{Duration, Instant};
use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style}; use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table}; use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table};
use ratatui::Frame; use ratatui::Frame;
use crate::app::App; use crate::app::App;
@@ -28,15 +28,17 @@ fn adjust_spans_for_line(
line_start: usize, line_start: usize,
line_len: usize, line_len: usize,
) -> Vec<SourceSpan> { ) -> Vec<SourceSpan> {
let ls = line_start as u32;
let ll = line_len as u32;
spans spans
.iter() .iter()
.filter_map(|s| { .filter_map(|s| {
if s.end <= line_start || s.start >= line_start + line_len { if s.end <= ls || s.start >= ls + ll {
return None; return None;
} }
Some(SourceSpan { Some(SourceSpan {
start: s.start.max(line_start) - line_start, start: s.start.max(ls) - ls,
end: (s.end.min(line_start + line_len)) - line_start, end: (s.end.min(ls + ll)) - ls,
}) })
}) })
.collect() .collect()
@@ -158,12 +160,8 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc
} }
} }
fn header_height(width: u16) -> u16 { fn header_height(_width: u16) -> u16 {
if width >= 80 { 3
1
} else {
2
}
} }
fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) { fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
@@ -231,35 +229,18 @@ fn render_header(
let bank = &app.project_state.project.banks[app.editor_ctx.bank]; let bank = &app.project_state.project.banks[app.editor_ctx.bank];
let pattern = &bank.patterns[app.editor_ctx.pattern]; let pattern = &bank.patterns[app.editor_ctx.pattern];
let (transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area) = let pad = Padding::vertical(1);
if area.height == 1 {
let [t, l, tp, b, p, s] = Layout::horizontal([
Constraint::Min(12),
Constraint::Length(9),
Constraint::Min(14),
Constraint::Fill(1),
Constraint::Fill(2),
Constraint::Min(20),
])
.areas(area);
(t, l, tp, b, p, s)
} else {
let [line1, line2] =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
let [t, l, tp, s] = Layout::horizontal([ let [transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area] =
Constraint::Min(12), Layout::horizontal([
Constraint::Length(9), Constraint::Min(12),
Constraint::Fill(1), Constraint::Length(9),
Constraint::Min(20), Constraint::Min(14),
]) Constraint::Fill(1),
.areas(line1); Constraint::Fill(2),
Constraint::Min(20),
let [b, p] = ])
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(2)]).areas(line2); .areas(area);
(t, l, tp, b, p, s)
};
// Transport block // Transport block
let (transport_bg, transport_text) = if app.playback.playing { let (transport_bg, transport_text) = if app.playback.playing {
@@ -270,7 +251,7 @@ fn render_header(
let transport_style = Style::new().bg(transport_bg).fg(theme.ui.text_primary); let transport_style = Style::new().bg(transport_bg).fg(theme.ui.text_primary);
frame.render_widget( frame.render_widget(
Paragraph::new(transport_text) Paragraph::new(transport_text)
.style(transport_style) .block(Block::default().padding(pad).style(transport_style))
.alignment(Alignment::Center), .alignment(Alignment::Center),
transport_area, transport_area,
); );
@@ -285,7 +266,7 @@ fn render_header(
let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg); let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg);
frame.render_widget( frame.render_widget(
Paragraph::new(if fill { "F" } else { "·" }) Paragraph::new(if fill { "F" } else { "·" })
.style(fill_style) .block(Block::default().padding(pad).style(fill_style))
.alignment(Alignment::Center), .alignment(Alignment::Center),
live_area, live_area,
); );
@@ -297,7 +278,7 @@ fn render_header(
.add_modifier(Modifier::BOLD); .add_modifier(Modifier::BOLD);
frame.render_widget( frame.render_widget(
Paragraph::new(format!(" {:.1} BPM ", link.tempo())) Paragraph::new(format!(" {:.1} BPM ", link.tempo()))
.style(tempo_style) .block(Block::default().padding(pad).style(tempo_style))
.alignment(Alignment::Center), .alignment(Alignment::Center),
tempo_area, tempo_area,
); );
@@ -313,7 +294,7 @@ fn render_header(
.fg(theme.ui.text_primary); .fg(theme.ui.text_primary);
frame.render_widget( frame.render_widget(
Paragraph::new(bank_name) Paragraph::new(bank_name)
.style(bank_style) .block(Block::default().padding(pad).style(bank_style))
.alignment(Alignment::Center), .alignment(Alignment::Center),
bank_area, bank_area,
); );
@@ -346,7 +327,7 @@ fn render_header(
.fg(theme.ui.text_primary); .fg(theme.ui.text_primary);
frame.render_widget( frame.render_widget(
Paragraph::new(pattern_text) Paragraph::new(pattern_text)
.style(pattern_style) .block(Block::default().padding(pad).style(pattern_style))
.alignment(Alignment::Center), .alignment(Alignment::Center),
pattern_area, pattern_area,
); );
@@ -361,7 +342,7 @@ fn render_header(
.fg(theme.header.stats_fg); .fg(theme.header.stats_fg);
frame.render_widget( frame.render_widget(
Paragraph::new(stats_text) Paragraph::new(stats_text)
.style(stats_style) .block(Block::default().padding(pad).style(stats_style))
.alignment(Alignment::Right), .alignment(Alignment::Right),
stats_area, stats_area,
); );

View File

@@ -1,7 +1,7 @@
use crate::harness::{default_ctx, expect_outputs, forth}; use crate::harness::{default_ctx, expect_outputs, forth};
use cagire::forth::{CcAccess, StepContext}; use cagire::forth::{CcAccess, StepContext};
use cagire::midi::CcMemory; use cagire::midi::CcMemory;
use std::sync::Arc;
#[allow(unused_imports)] #[allow(unused_imports)]
use cagire::forth::Value; use cagire::forth::Value;
@@ -52,7 +52,7 @@ fn test_ccval_reads_from_cc_memory() {
let f = forth(); let f = forth();
let ctx = StepContext { let ctx = StepContext {
cc_access: Some(Arc::new(cc_memory.clone()) as Arc<dyn CcAccess>), cc_access: Some(&cc_memory as &dyn CcAccess),
..default_ctx() ..default_ctx()
}; };

View File

@@ -203,7 +203,7 @@ fn at_records_selected_spans() {
assert_eq!(trace.selected_spans.len(), 6, "expected 6 selected spans (3 at + 3 sound)"); assert_eq!(trace.selected_spans.len(), 6, "expected 6 selected spans (3 at + 3 sound)");
// Verify at delta spans (even indices: 0, 2, 4) // Verify at delta spans (even indices: 0, 2, 4)
assert_eq!(&script[trace.selected_spans[0].start..trace.selected_spans[0].end], "0"); assert_eq!(&script[trace.selected_spans[0].start as usize..trace.selected_spans[0].end as usize], "0");
assert_eq!(&script[trace.selected_spans[2].start..trace.selected_spans[2].end], "0.5"); assert_eq!(&script[trace.selected_spans[2].start as usize..trace.selected_spans[2].end as usize], "0.5");
assert_eq!(&script[trace.selected_spans[4].start..trace.selected_spans[4].end], "0.75"); assert_eq!(&script[trace.selected_spans[4].start as usize..trace.selected_spans[4].end as usize], "0.75");
} }