From 53167e35b6f2b9f435a515e3482efca9e18f6219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Thu, 5 Feb 2026 23:15:46 +0100 Subject: [PATCH] Feat: optimizations --- CHANGELOG.md | 12 +++++ crates/forth/src/compiler.rs | 8 ++-- crates/forth/src/ops.rs | 4 +- crates/forth/src/types.rs | 16 +++---- crates/forth/src/vm.rs | 42 ++++++++--------- crates/forth/src/words/compile.rs | 4 +- crates/project/src/project.rs | 10 ++-- src/app.rs | 56 ++-------------------- src/engine/sequencer.rs | 77 ++++++++++++++++--------------- src/input.rs | 6 +-- src/services/clipboard.rs | 10 ++-- src/services/euclidean.rs | 3 +- src/services/pattern_editor.rs | 4 +- src/state/editor.rs | 2 +- src/state/modal.rs | 4 +- src/state/project.rs | 26 ++++++----- src/views/help_view.rs | 19 ++++++++ src/views/highlight.rs | 4 +- src/views/main_view.rs | 4 +- src/views/render.rs | 69 ++++++++++----------------- tests/forth/midi.rs | 4 +- tests/forth/temporal.rs | 6 +-- 22 files changed, 175 insertions(+), 215 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f341608..a51fcc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,20 @@ All notable changes to this project will be documented in this file. ### 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. +- 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` to `Option` (~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` + - Removed unnecessary `Arc` wrapper from `Stack` type + - Variable key cache computes on-demand with reusable buffers instead of pre-allocating 2048 Strings ### 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`. ## [0.0.7] - 2026-05-02 diff --git a/crates/forth/src/compiler.rs b/crates/forth/src/compiler.rs index 9d8ae5c..0a21f4c 100644 --- a/crates/forth/src/compiler.rs +++ b/crates/forth/src/compiler.rs @@ -45,7 +45,7 @@ fn tokenize(input: &str) -> Vec { } 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; } @@ -66,8 +66,8 @@ fn tokenize(input: &str) -> Vec { tokens.push(Token::Word( ";".to_string(), SourceSpan { - start: pos, - end: pos + 1, + start: pos as u32, + end: (pos + 1) as u32, }, )); continue; @@ -85,7 +85,7 @@ fn tokenize(input: &str) -> Vec { 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 let word_to_parse = if word.starts_with('.') diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index 3c8eb7c..1643c57 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -60,11 +60,11 @@ pub enum Op { BranchIfZero(usize, Option, Option), Branch(usize), NewCmd, - SetParam(String), + SetParam(&'static str), Emit, Get, Set, - GetContext(String), + GetContext(&'static str), Rand, ExpRand, LogRand, diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index 131beb7..e24d189 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -14,8 +14,8 @@ pub trait CcAccess: Send + Sync { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct SourceSpan { - pub start: usize, - pub end: usize, + pub start: u32, + pub end: u32, } #[derive(Clone, Debug, Default)] @@ -37,7 +37,7 @@ pub struct StepContext<'a> { pub speed: f64, pub fill: bool, pub nudge_secs: f64, - pub cc_access: Option>, + pub cc_access: Option<&'a dyn CcAccess>, pub speed_key: &'a str, pub chain_key: &'a str, #[cfg(feature = "desktop")] @@ -58,8 +58,8 @@ pub type VariablesMap = HashMap; pub type Variables = Arc>; pub type Dictionary = Arc>>>; pub type Rng = Arc>; -pub type Stack = Arc>>; -pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(String, Value)]); +pub type Stack = Mutex>; +pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]); #[derive(Clone, Debug)] pub enum Value { @@ -138,7 +138,7 @@ impl Value { #[derive(Clone, Debug, Default)] pub(super) struct CmdRegister { sound: Option, - params: Vec<(String, Value)>, + params: Vec<(&'static str, Value)>, deltas: Vec, } @@ -155,7 +155,7 @@ impl CmdRegister { 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)); } @@ -171,7 +171,7 @@ impl CmdRegister { self.sound.as_ref() } - pub(super) fn params(&self) -> &[(String, Value)] { + pub(super) fn params(&self) -> &[(&'static str, Value)] { &self.params } diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 8a9ed94..f343999 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -22,7 +22,7 @@ pub struct Forth { impl Forth { pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self { Self { - stack: Arc::new(Mutex::new(Vec::new())), + stack: Mutex::new(Vec::new()), vars, dict, rng, @@ -227,7 +227,7 @@ impl Forth { Some(v) => Some(v.as_str()?.to_string()), None => None, }; - let resolved_params: Vec<(String, String)> = params + let resolved_params: Vec<(&str, String)> = params .iter() .map(|(k, v)| { 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(); emit_output( @@ -555,7 +555,7 @@ impl Forth { } else { Value::CycleList(Arc::from(values)) }; - cmd.set_param(param.clone(), val); + cmd.set_param(param, val); } Op::Emit => { @@ -613,7 +613,7 @@ impl Forth { } Op::GetContext(name) => { - let val = match name.as_str() { + let val = match *name { "step" => Value::Int(ctx.step as i64, None), "beat" => Value::Float(ctx.beat, 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()); } let dur = beats * 60.0 / ctx.tempo / ctx.speed; - cmd.set_param("fit".into(), Value::Float(dur, None)); - cmd.set_param("dur".into(), Value::Float(dur, None)); + cmd.set_param("fit", Value::Float(dur, None)); + cmd.set_param("dur", Value::Float(dur, None)); } Op::At => { @@ -896,18 +896,18 @@ impl Forth { let s = stack.pop().ok_or("stack underflow")?; let d = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; - cmd.set_param("attack".into(), a); - cmd.set_param("decay".into(), d); - cmd.set_param("sustain".into(), s); - cmd.set_param("release".into(), r); + cmd.set_param("attack", a); + cmd.set_param("decay", d); + cmd.set_param("sustain", s); + cmd.set_param("release", r); } Op::Ad => { let d = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; - cmd.set_param("attack".into(), a); - cmd.set_param("decay".into(), d); - cmd.set_param("sustain".into(), Value::Int(0, None)); + cmd.set_param("attack", a); + cmd.set_param("decay", d); + cmd.set_param("sustain", Value::Int(0, None)); } Op::Apply => { @@ -1055,14 +1055,14 @@ impl Forth { params .iter() .rev() - .find(|(k, _)| k == name) + .find(|(k, _)| *k == name) .and_then(|(_, v)| v.as_int().ok()) }; let get_float = |name: &str| -> Option { params .iter() .rev() - .find(|(k, _)| k == name) + .find(|(k, _)| *k == name) .and_then(|(_, v)| v.as_float().ok()) }; 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 .iter() .rev() - .find(|(k, _)| k == "dev") + .find(|(k, _)| *k == "dev") .and_then(|(_, v)| v.as_int().ok()) .map(|d| d.clamp(0, 3) as u8) .unwrap_or(0) @@ -1181,7 +1181,7 @@ fn is_tempo_scaled_param(name: &str) -> bool { fn emit_output( sound: Option<&str>, - params: &[(String, String)], + params: &[(&str, String)], step_duration: f64, nudge_secs: f64, outputs: &mut Vec, @@ -1190,8 +1190,8 @@ fn emit_output( let mut out = String::with_capacity(128); out.push('/'); - let has_dur = params.iter().any(|(k, _)| k == "dur"); - let delaytime_idx = params.iter().position(|(k, _)| k == "delaytime"); + let has_dur = params.iter().any(|(k, _)| *k == "dur"); + let delaytime_idx = params.iter().position(|(k, _)| *k == "delaytime"); if let Some(s) = sound { let _ = write!(&mut out, "sound/{s}"); diff --git a/crates/forth/src/words/compile.rs b/crates/forth/src/words/compile.rs index b25f7d0..5030871 100644 --- a/crates/forth/src/words/compile.rs +++ b/crates/forth/src/words/compile.rs @@ -216,8 +216,8 @@ pub(crate) fn compile_word( ops.push(op); } } - Context(ctx) => ops.push(Op::GetContext((*ctx).into())), - Param => ops.push(Op::SetParam(word.name.into())), + Context(ctx) => ops.push(Op::GetContext(ctx)), + Param => ops.push(Op::SetParam(word.name)), Probability(p) => { ops.push(Op::PushFloat(*p, None)); ops.push(Op::ChanceExec); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7a3d48f..cf47924 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -210,10 +210,8 @@ impl SyncMode { pub struct Step { pub active: bool, pub script: String, - #[serde(skip)] - pub command: Option, #[serde(default)] - pub source: Option, + pub source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, } @@ -229,7 +227,6 @@ impl Default for Step { Self { active: true, script: String::new(), - command: None, source: None, name: None, } @@ -254,7 +251,7 @@ struct SparseStep { #[serde(default, skip_serializing_if = "String::is_empty")] script: String, #[serde(default, skip_serializing_if = "Option::is_none")] - source: Option, + source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] name: Option, } @@ -348,7 +345,6 @@ impl<'de> Deserialize<'de> for Pattern { steps[ss.i] = Step { active: ss.active, script: ss.script, - command: None, source: ss.source, name: ss.name, }; @@ -410,7 +406,7 @@ impl Pattern { for _ in 0..self.steps.len() { if let Some(step) = self.steps.get(current) { if let Some(source) = step.source { - current = source; + current = source as usize; } else { return current; } diff --git a/src/app.rs b/src/app.rs index 7c5b212..583c41b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -419,44 +419,16 @@ impl App { .unwrap_or_default(); 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; } let ctx = self.create_step_context(step_idx, link); match self.script_engine.evaluate(&script, &ctx) { - Ok(cmds) => { - 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")) - }; - } + Ok(_) => { self.ui.flash("Script compiled", 150, FlashKind::Info); } Err(e) => { - if let Some(step) = self - .project_state - .project - .pattern_at_mut(bank, pattern) - .step_mut(step_idx) - { - step.command = None; - } self.ui .flash(&format!("Script error: {e}"), 300, FlashKind::Error); } @@ -477,33 +449,11 @@ impl App { .unwrap_or_default(); 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; } let ctx = self.create_step_context(step_idx, link); - - 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")) - }; - } - } + let _ = self.script_engine.evaluate(&script, &ctx); } } @@ -1054,7 +1004,7 @@ impl App { [self.editor_ctx.pattern]; 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(); } diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 50a91f0..f954351 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -140,7 +140,7 @@ pub struct PatternSnapshot { pub struct StepSnapshot { pub active: bool, pub script: String, - pub source: Option, + pub source: Option, } #[derive(Clone, Copy, Default, Debug)] @@ -392,7 +392,7 @@ impl PatternSnapshot { for _ in 0..self.steps.len() { if let Some(step) = self.steps.get(current) { if let Some(source) = step.source { - current = source; + current = source as usize; } else { return current; } @@ -500,30 +500,32 @@ fn parse_chain_target(s: &str) -> Option { }) } -struct KeyCache { - speed_keys: [[String; MAX_PATTERNS]; MAX_BANKS], - chain_keys: [[String; MAX_PATTERNS]; MAX_BANKS], +struct KeyBuf { + speed: String, + chain: String, } -impl KeyCache { +impl KeyBuf { fn new() -> Self { Self { - speed_keys: std::array::from_fn(|bank| { - std::array::from_fn(|pattern| format!("__speed_{bank}_{pattern}__")) - }), - chain_keys: std::array::from_fn(|bank| { - std::array::from_fn(|pattern| format!("__chain_{bank}_{pattern}__")) - }), + speed: String::with_capacity(24), + chain: String::with_capacity(24), } } +} - fn speed_key(&self, bank: usize, pattern: usize) -> &str { - &self.speed_keys[bank][pattern] - } +fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str { + use std::fmt::Write; + buf.clear(); + write!(buf, "__speed_{bank}_{pattern}__").unwrap(); + buf +} - fn chain_key(&self, bank: usize, pattern: usize) -> &str { - &self.chain_keys[bank][pattern] - } +fn format_chain_key(buf: &mut String, bank: usize, pattern: usize) -> &str { + use std::fmt::Write; + buf.clear(); + write!(buf, "__chain_{bank}_{pattern}__").unwrap(); + buf } pub(crate) struct SequencerState { @@ -537,7 +539,7 @@ pub(crate) struct SequencerState { variables: Variables, dict: Dictionary, speed_overrides: HashMap<(usize, usize), f64>, - key_cache: KeyCache, + key_buf: KeyBuf, buf_audio_commands: Vec, buf_activated: Vec, buf_stopped: Vec, @@ -566,7 +568,7 @@ impl SequencerState { variables, dict, speed_overrides: HashMap::with_capacity(MAX_PATTERNS), - key_cache: KeyCache::new(), + key_buf: KeyBuf::new(), buf_audio_commands: Vec::with_capacity(32), buf_activated: Vec::with_capacity(16), buf_stopped: Vec::with_capacity(16), @@ -834,7 +836,7 @@ impl SequencerState { { let vars = self.variables.load(); 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()) { self.speed_overrides.insert((id.bank, id.pattern), v); } @@ -884,6 +886,8 @@ impl SequencerState { active.pattern, 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 { step: step_idx, beat: step_beat, @@ -897,9 +901,9 @@ impl SequencerState { speed: speed_mult, fill, nudge_secs, - cc_access: self.cc_access.clone(), - speed_key: self.key_cache.speed_key(active.bank, active.pattern), - chain_key: self.key_cache.chain_key(active.bank, active.pattern), + cc_access: self.cc_access.as_deref(), + speed_key, + chain_key, #[cfg(feature = "desktop")] mouse_x, #[cfg(feature = "desktop")] @@ -970,8 +974,9 @@ impl SequencerState { .and_then(|v: &Value| v.as_float().ok()); let mut chain_transitions = Vec::new(); + let mut buf = String::with_capacity(24); 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(target) = parse_chain_target(s) { chain_transitions.push((*id, target)); @@ -980,24 +985,24 @@ impl SequencerState { } // Remove consumed variables (tempo and chain keys) - let needs_removal = new_tempo.is_some() - || completed.iter().any(|id| { - let chain_key = self.key_cache.chain_key(id.bank, id.pattern); - vars.contains_key(chain_key) - }) - || stopped.iter().any(|id| { - let chain_key = self.key_cache.chain_key(id.bank, id.pattern); - vars.contains_key(chain_key) - }); + let mut needs_removal = new_tempo.is_some(); + if !needs_removal { + for id in completed.iter().chain(stopped.iter()) { + if vars.contains_key(format_chain_key(&mut buf, id.bank, id.pattern)) { + needs_removal = true; + break; + } + } + } if needs_removal { let mut new_vars = (**vars).clone(); new_vars.remove("__tempo__"); 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 { - 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)); } diff --git a/src/input.rs b/src/input.rs index 7256038..af25e59 100644 --- a/src/input.rs +++ b/src/input.rs @@ -979,7 +979,7 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR .map(|p| p.display().to_string()) .unwrap_or_default(); 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 => { ctx.dispatch(AppCommand::CopySteps); @@ -1011,7 +1011,7 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR }) .unwrap_or_default(); 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('-') => ctx.dispatch(AppCommand::TempoDown), @@ -1422,7 +1422,7 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { KeyCode::Char('A') => { use crate::state::file_browser::FileBrowserState; 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') => { if ctx.app.audio.section == EngineSection::Samples { diff --git a/src/services/clipboard.rs b/src/services/clipboard.rs index 6847014..7b2818b 100644 --- a/src/services/clipboard.rs +++ b/src/services/clipboard.rs @@ -95,7 +95,6 @@ pub fn paste_steps( step.name = data.name.clone(); if source.is_some() { step.script.clear(); - step.command = None; } else { step.script = data.script.clone(); } @@ -130,15 +129,14 @@ pub fn link_paste_steps( let source_idx = if data.source.is_some() { data.source } else { - Some(data.original_index) + Some(data.original_index as u8) }; - if source_idx == Some(target) { + if source_idx == Some(target as u8) { continue; } if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) { step.source = source_idx; step.script.clear(); - step.command = None; } } @@ -183,7 +181,7 @@ pub fn duplicate_steps( let pat_len = pat.length; let paste_at = *indices.last().unwrap() + 1; - let dupe_data: Vec<(bool, String, Option)> = indices + let dupe_data: Vec<(bool, String, Option)> = indices .iter() .filter_map(|&idx| { let step = pat.step(idx)?; @@ -204,10 +202,8 @@ pub fn duplicate_steps( step.source = source; if source.is_some() { step.script.clear(); - step.command = None; } else { step.script = script; - step.command = None; } } compile_targets.push(target); diff --git a/src/services/euclidean.rs b/src/services/euclidean.rs index 0806680..bbd256b 100644 --- a/src/services/euclidean.rs +++ b/src/services/euclidean.rs @@ -44,9 +44,8 @@ pub fn apply_distribution( } 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.command = None; step.active = true; } targets.push(target); diff --git a/src/services/pattern_editor.rs b/src/services/pattern_editor.rs index fef7014..877ca19 100644 --- a/src/services/pattern_editor.rs +++ b/src/services/pattern_editor.rs @@ -94,16 +94,14 @@ pub fn get_step_script( 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) { + if s.source == Some(step as u8) { 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) diff --git a/src/state/editor.rs b/src/state/editor.rs index dda07b8..89fdf6b 100644 --- a/src/state/editor.rs +++ b/src/state/editor.rs @@ -104,7 +104,7 @@ pub struct CopiedSteps { pub struct CopiedStepData { pub script: String, pub active: bool, - pub source: Option, + pub source: Option, pub original_index: usize, pub name: Option, } diff --git a/src/state/modal.rs b/src/state/modal.rs index e438262..17dfa93 100644 --- a/src/state/modal.rs +++ b/src/state/modal.rs @@ -29,7 +29,7 @@ pub enum Modal { bank: usize, selected: bool, }, - FileBrowser(FileBrowserState), + FileBrowser(Box), RenameBank { bank: usize, name: String, @@ -50,7 +50,7 @@ pub enum Modal { input: String, }, SetTempo(String), - AddSamplePath(FileBrowserState), + AddSamplePath(Box), Editor, Preview, PatternProps { diff --git a/src/state/project.rs b/src/state/project.rs index 9601647..aaa3b66 100644 --- a/src/state/project.rs +++ b/src/state/project.rs @@ -1,4 +1,3 @@ -use std::collections::HashSet; use std::path::PathBuf; use crate::model::{MAX_BANKS, MAX_PATTERNS}; @@ -7,7 +6,7 @@ use crate::model::Project; pub struct ProjectState { pub project: Project, pub file_path: Option, - pub dirty_patterns: HashSet<(usize, usize)>, + dirty_patterns: [[bool; MAX_PATTERNS]; MAX_BANKS], } impl Default for ProjectState { @@ -15,7 +14,7 @@ impl Default for ProjectState { let mut state = Self { project: Project::default(), file_path: None, - dirty_patterns: HashSet::new(), + dirty_patterns: [[false; MAX_PATTERNS]; MAX_BANKS], }; state.mark_all_dirty(); state @@ -24,18 +23,23 @@ impl Default for ProjectState { impl ProjectState { 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) { - for bank in 0..MAX_BANKS { - for pattern in 0..MAX_PATTERNS { - self.dirty_patterns.insert((bank, pattern)); - } - } + self.dirty_patterns = [[true; MAX_PATTERNS]; MAX_BANKS]; } - pub fn take_dirty(&mut self) -> HashSet<(usize, usize)> { - std::mem::take(&mut self.dirty_patterns) + pub fn take_dirty(&mut self) -> Vec<(usize, usize)> { + 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 } } diff --git a/src/views/help_view.rs b/src/views/help_view.rs index 76deb7a..c9a3901 100644 --- a/src/views/help_view.rs +++ b/src/views/help_view.rs @@ -4,6 +4,7 @@ use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line as RLine, Span}; use ratatui::widgets::{Block, Borders, List, ListItem, Padding, Paragraph, Wrap}; use ratatui::Frame; +#[cfg(not(feature = "desktop"))] use tui_big_text::{BigText, PixelSize}; use crate::app::App; @@ -173,7 +174,10 @@ fn render_topics(frame: &mut Frame, app: &App, area: Rect) { } const WELCOME_TOPIC: usize = 0; +#[cfg(not(feature = "desktop"))] 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) { let theme = theme::get(); @@ -186,19 +190,34 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) { let [title_area, rest] = Layout::vertical([Constraint::Length(BIG_TITLE_HEIGHT), Constraint::Fill(1)]) .areas(area); + + #[cfg(not(feature = "desktop"))] let big_title = BigText::builder() .pixel_size(PixelSize::Quadrant) .style(Style::new().fg(theme.markdown.h1).bold()) .lines(vec!["CAGIRE".into()]) .centered() .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( "A Forth Sequencer", Style::new().fg(theme.ui.text_primary), ))) .alignment(ratatui::layout::Alignment::Center); + + #[cfg(not(feature = "desktop"))] let [big_area, subtitle_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(subtitle, subtitle_area); rest diff --git a/src/views/highlight.rs b/src/views/highlight.rs index 76d22eb..28d91dd 100644 --- a/src/views/highlight.rs +++ b/src/views/highlight.rs @@ -212,10 +212,10 @@ pub fn highlight_line_with_runtime( let is_selected = selected_spans .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 .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(); if token.varargs { diff --git a/src/views/main_view.rs b/src/views/main_view.rs index 614c131..c218d5b 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -196,7 +196,7 @@ fn render_tile( }; 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]) }); @@ -236,7 +236,7 @@ fn render_tile( // For linked steps, get the name from the source step 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 { step.and_then(|s| s.name.as_ref()) }; diff --git a/src/views/render.rs b/src/views/render.rs index 346d831..cf68e33 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -3,7 +3,7 @@ use std::time::{Duration, Instant}; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Modifier, Style}; 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 crate::app::App; @@ -28,15 +28,17 @@ fn adjust_spans_for_line( line_start: usize, line_len: usize, ) -> Vec { + let ls = line_start as u32; + let ll = line_len as u32; spans .iter() .filter_map(|s| { - if s.end <= line_start || s.start >= line_start + line_len { + if s.end <= ls || s.start >= ls + ll { return None; } Some(SourceSpan { - start: s.start.max(line_start) - line_start, - end: (s.end.min(line_start + line_len)) - line_start, + start: s.start.max(ls) - ls, + end: (s.end.min(ls + ll)) - ls, }) }) .collect() @@ -158,12 +160,8 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc } } -fn header_height(width: u16) -> u16 { - if width >= 80 { - 1 - } else { - 2 - } +fn header_height(_width: u16) -> u16 { + 3 } 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 pattern = &bank.patterns[app.editor_ctx.pattern]; - let (transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area) = - 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 pad = Padding::vertical(1); - let [t, l, tp, s] = Layout::horizontal([ - Constraint::Min(12), - Constraint::Length(9), - Constraint::Fill(1), - Constraint::Min(20), - ]) - .areas(line1); - - let [b, p] = - Layout::horizontal([Constraint::Fill(1), Constraint::Fill(2)]).areas(line2); - - (t, l, tp, b, p, s) - }; + let [transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area] = + Layout::horizontal([ + Constraint::Min(12), + Constraint::Length(9), + Constraint::Min(14), + Constraint::Fill(1), + Constraint::Fill(2), + Constraint::Min(20), + ]) + .areas(area); // Transport block 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); frame.render_widget( Paragraph::new(transport_text) - .style(transport_style) + .block(Block::default().padding(pad).style(transport_style)) .alignment(Alignment::Center), transport_area, ); @@ -285,7 +266,7 @@ fn render_header( let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg); frame.render_widget( Paragraph::new(if fill { "F" } else { "ยท" }) - .style(fill_style) + .block(Block::default().padding(pad).style(fill_style)) .alignment(Alignment::Center), live_area, ); @@ -297,7 +278,7 @@ fn render_header( .add_modifier(Modifier::BOLD); frame.render_widget( Paragraph::new(format!(" {:.1} BPM ", link.tempo())) - .style(tempo_style) + .block(Block::default().padding(pad).style(tempo_style)) .alignment(Alignment::Center), tempo_area, ); @@ -313,7 +294,7 @@ fn render_header( .fg(theme.ui.text_primary); frame.render_widget( Paragraph::new(bank_name) - .style(bank_style) + .block(Block::default().padding(pad).style(bank_style)) .alignment(Alignment::Center), bank_area, ); @@ -346,7 +327,7 @@ fn render_header( .fg(theme.ui.text_primary); frame.render_widget( Paragraph::new(pattern_text) - .style(pattern_style) + .block(Block::default().padding(pad).style(pattern_style)) .alignment(Alignment::Center), pattern_area, ); @@ -361,7 +342,7 @@ fn render_header( .fg(theme.header.stats_fg); frame.render_widget( Paragraph::new(stats_text) - .style(stats_style) + .block(Block::default().padding(pad).style(stats_style)) .alignment(Alignment::Right), stats_area, ); diff --git a/tests/forth/midi.rs b/tests/forth/midi.rs index f17b09f..22bd62c 100644 --- a/tests/forth/midi.rs +++ b/tests/forth/midi.rs @@ -1,7 +1,7 @@ use crate::harness::{default_ctx, expect_outputs, forth}; use cagire::forth::{CcAccess, StepContext}; use cagire::midi::CcMemory; -use std::sync::Arc; + #[allow(unused_imports)] use cagire::forth::Value; @@ -52,7 +52,7 @@ fn test_ccval_reads_from_cc_memory() { let f = forth(); let ctx = StepContext { - cc_access: Some(Arc::new(cc_memory.clone()) as Arc), + cc_access: Some(&cc_memory as &dyn CcAccess), ..default_ctx() }; diff --git a/tests/forth/temporal.rs b/tests/forth/temporal.rs index 276d4c0..9728d4c 100644 --- a/tests/forth/temporal.rs +++ b/tests/forth/temporal.rs @@ -203,7 +203,7 @@ fn at_records_selected_spans() { assert_eq!(trace.selected_spans.len(), 6, "expected 6 selected spans (3 at + 3 sound)"); // 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[2].start..trace.selected_spans[2].end], "0.5"); - assert_eq!(&script[trace.selected_spans[4].start..trace.selected_spans[4].end], "0.75"); + 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 as usize..trace.selected_spans[2].end as usize], "0.5"); + assert_eq!(&script[trace.selected_spans[4].start as usize..trace.selected_spans[4].end as usize], "0.75"); }