Feat: optimizations
This commit is contained in:
12
CHANGELOG.md
12
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<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
|
||||
- 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
|
||||
|
||||
@@ -45,7 +45,7 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
}
|
||||
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<Token> {
|
||||
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<Token> {
|
||||
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('.')
|
||||
|
||||
@@ -60,11 +60,11 @@ pub enum Op {
|
||||
BranchIfZero(usize, Option<SourceSpan>, Option<SourceSpan>),
|
||||
Branch(usize),
|
||||
NewCmd,
|
||||
SetParam(String),
|
||||
SetParam(&'static str),
|
||||
Emit,
|
||||
Get,
|
||||
Set,
|
||||
GetContext(String),
|
||||
GetContext(&'static str),
|
||||
Rand,
|
||||
ExpRand,
|
||||
LogRand,
|
||||
|
||||
@@ -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<Arc<dyn CcAccess>>,
|
||||
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<String, Value>;
|
||||
pub type Variables = Arc<ArcSwap<VariablesMap>>;
|
||||
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
|
||||
pub type Rng = Arc<Mutex<StdRng>>;
|
||||
pub type Stack = Arc<Mutex<Vec<Value>>>;
|
||||
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(String, Value)]);
|
||||
pub type Stack = Mutex<Vec<Value>>;
|
||||
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<Value>,
|
||||
params: Vec<(String, Value)>,
|
||||
params: Vec<(&'static str, Value)>,
|
||||
deltas: Vec<Value>,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<f64> {
|
||||
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<String>,
|
||||
@@ -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}");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -210,10 +210,8 @@ impl SyncMode {
|
||||
pub struct Step {
|
||||
pub active: bool,
|
||||
pub script: String,
|
||||
#[serde(skip)]
|
||||
pub command: Option<String>,
|
||||
#[serde(default)]
|
||||
pub source: Option<usize>,
|
||||
pub source: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
@@ -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<usize>,
|
||||
source: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
56
src/app.rs
56
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();
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ pub struct PatternSnapshot {
|
||||
pub struct StepSnapshot {
|
||||
pub active: bool,
|
||||
pub script: String,
|
||||
pub source: Option<usize>,
|
||||
pub source: Option<u8>,
|
||||
}
|
||||
|
||||
#[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<PatternId> {
|
||||
})
|
||||
}
|
||||
|
||||
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<TimestampedCommand>,
|
||||
buf_activated: Vec<PatternId>,
|
||||
buf_stopped: Vec<PatternId>,
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<usize>)> = indices
|
||||
let dupe_data: Vec<(bool, String, Option<u8>)> = 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -104,7 +104,7 @@ pub struct CopiedSteps {
|
||||
pub struct CopiedStepData {
|
||||
pub script: String,
|
||||
pub active: bool,
|
||||
pub source: Option<usize>,
|
||||
pub source: Option<u8>,
|
||||
pub original_index: usize,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub enum Modal {
|
||||
bank: usize,
|
||||
selected: bool,
|
||||
},
|
||||
FileBrowser(FileBrowserState),
|
||||
FileBrowser(Box<FileBrowserState>),
|
||||
RenameBank {
|
||||
bank: usize,
|
||||
name: String,
|
||||
@@ -50,7 +50,7 @@ pub enum Modal {
|
||||
input: String,
|
||||
},
|
||||
SetTempo(String),
|
||||
AddSamplePath(FileBrowserState),
|
||||
AddSamplePath(Box<FileBrowserState>),
|
||||
Editor,
|
||||
Preview,
|
||||
PatternProps {
|
||||
|
||||
@@ -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<PathBuf>,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
};
|
||||
|
||||
@@ -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<SourceSpan> {
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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<dyn CcAccess>),
|
||||
cc_access: Some(&cc_memory as &dyn CcAccess),
|
||||
..default_ctx()
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user