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

View File

@@ -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('.')

View File

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

View File

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

View File

@@ -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}");

View File

@@ -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);

View File

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

View File

@@ -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();
}

View File

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

View File

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

View File

@@ -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);

View File

@@ -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);

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 {
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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())
};

View File

@@ -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,
);

View File

@@ -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()
};

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)");
// 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");
}