Feat: introduce follow up actions
This commit is contained in:
@@ -94,7 +94,6 @@ pub enum Op {
|
|||||||
Triangle,
|
Triangle,
|
||||||
Range,
|
Range,
|
||||||
Perlin,
|
Perlin,
|
||||||
Chain,
|
|
||||||
Loop,
|
Loop,
|
||||||
Degree(&'static [i64]),
|
Degree(&'static [i64]),
|
||||||
Oct,
|
Oct,
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ pub struct StepContext<'a> {
|
|||||||
pub nudge_secs: f64,
|
pub nudge_secs: f64,
|
||||||
pub cc_access: Option<&'a dyn CcAccess>,
|
pub cc_access: Option<&'a dyn CcAccess>,
|
||||||
pub speed_key: &'a str,
|
pub speed_key: &'a str,
|
||||||
pub chain_key: &'a str,
|
|
||||||
pub mouse_x: f64,
|
pub mouse_x: f64,
|
||||||
pub mouse_y: f64,
|
pub mouse_y: f64,
|
||||||
pub mouse_down: f64,
|
pub mouse_down: f64,
|
||||||
|
|||||||
@@ -992,26 +992,6 @@ impl Forth {
|
|||||||
.insert(ctx.speed_key.to_string(), Value::Float(clamped, None));
|
.insert(ctx.speed_key.to_string(), Value::Float(clamped, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Chain => {
|
|
||||||
let pattern = pop_int(stack)? - 1;
|
|
||||||
let bank = pop_int(stack)? - 1;
|
|
||||||
if bank < 0 || pattern < 0 {
|
|
||||||
return Err("chain: bank and pattern must be >= 1".into());
|
|
||||||
}
|
|
||||||
if bank as usize == ctx.bank && pattern as usize == ctx.pattern {
|
|
||||||
// chaining to self is a no-op
|
|
||||||
} else {
|
|
||||||
use std::fmt::Write;
|
|
||||||
let mut val = String::with_capacity(8);
|
|
||||||
let _ = write!(&mut val, "{bank}:{pattern}");
|
|
||||||
var_writes_cell
|
|
||||||
.borrow_mut()
|
|
||||||
.as_mut()
|
|
||||||
.expect("var_writes taken")
|
|
||||||
.insert(ctx.chain_key.to_string(), Value::Str(Arc::from(val), None));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::Loop => {
|
Op::Loop => {
|
||||||
let beats = pop_float(stack)?;
|
let beats = pop_float(stack)?;
|
||||||
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
|
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"triangle" => Op::Triangle,
|
"triangle" => Op::Triangle,
|
||||||
"range" => Op::Range,
|
"range" => Op::Range,
|
||||||
"perlin" => Op::Perlin,
|
"perlin" => Op::Perlin,
|
||||||
"chain" => Op::Chain,
|
|
||||||
"loop" => Op::Loop,
|
"loop" => Op::Loop,
|
||||||
"oct" => Op::Oct,
|
"oct" => Op::Oct,
|
||||||
"clear" => Op::ClearCmd,
|
"clear" => Op::ClearCmd,
|
||||||
|
|||||||
@@ -254,16 +254,6 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
Word {
|
|
||||||
name: "chain",
|
|
||||||
aliases: &[],
|
|
||||||
category: "Time",
|
|
||||||
stack: "(bank pattern --)",
|
|
||||||
desc: "Chain to bank/pattern (1-indexed) when current pattern ends",
|
|
||||||
example: "1 4 chain",
|
|
||||||
compile: Simple,
|
|
||||||
varargs: false,
|
|
||||||
},
|
|
||||||
Word {
|
Word {
|
||||||
name: "at",
|
name: "at",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ pub const MAX_STEPS: usize = 1024;
|
|||||||
pub const DEFAULT_LENGTH: usize = 16;
|
pub const DEFAULT_LENGTH: usize = 16;
|
||||||
|
|
||||||
pub use file::{load, save, FileError};
|
pub use file::{load, save, FileError};
|
||||||
pub use project::{Bank, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||||
|
|||||||
@@ -206,6 +206,44 @@ impl SyncMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
|
pub enum FollowUp {
|
||||||
|
#[default]
|
||||||
|
Loop,
|
||||||
|
Stop,
|
||||||
|
Chain { bank: usize, pattern: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FollowUp {
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Loop => "Loop",
|
||||||
|
Self::Stop => "Stop",
|
||||||
|
Self::Chain { .. } => "Chain",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_mode(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Loop => Self::Stop,
|
||||||
|
Self::Stop => Self::Chain { bank: 0, pattern: 0 },
|
||||||
|
Self::Chain { .. } => Self::Loop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_mode(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
|
||||||
|
Self::Stop => Self::Loop,
|
||||||
|
Self::Chain { .. } => Self::Stop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_default_follow_up(f: &FollowUp) -> bool {
|
||||||
|
*f == FollowUp::default()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Step {
|
pub struct Step {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
@@ -245,6 +283,7 @@ pub struct Pattern {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub quantization: LaunchQuantization,
|
pub quantization: LaunchQuantization,
|
||||||
pub sync_mode: SyncMode,
|
pub sync_mode: SyncMode,
|
||||||
|
pub follow_up: FollowUp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -280,6 +319,8 @@ struct SparsePattern {
|
|||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
||||||
sync_mode: SyncMode,
|
sync_mode: SyncMode,
|
||||||
|
#[serde(default, skip_serializing_if = "is_default_follow_up")]
|
||||||
|
follow_up: FollowUp,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_default_quantization(q: &LaunchQuantization) -> bool {
|
fn is_default_quantization(q: &LaunchQuantization) -> bool {
|
||||||
@@ -302,6 +343,8 @@ struct LegacyPattern {
|
|||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
sync_mode: SyncMode,
|
sync_mode: SyncMode,
|
||||||
|
#[serde(default)]
|
||||||
|
follow_up: FollowUp,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for Pattern {
|
impl Serialize for Pattern {
|
||||||
@@ -327,6 +370,7 @@ impl Serialize for Pattern {
|
|||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
quantization: self.quantization,
|
quantization: self.quantization,
|
||||||
sync_mode: self.sync_mode,
|
sync_mode: self.sync_mode,
|
||||||
|
follow_up: self.follow_up,
|
||||||
};
|
};
|
||||||
sparse.serialize(serializer)
|
sparse.serialize(serializer)
|
||||||
}
|
}
|
||||||
@@ -361,6 +405,7 @@ impl<'de> Deserialize<'de> for Pattern {
|
|||||||
name: sparse.name,
|
name: sparse.name,
|
||||||
quantization: sparse.quantization,
|
quantization: sparse.quantization,
|
||||||
sync_mode: sparse.sync_mode,
|
sync_mode: sparse.sync_mode,
|
||||||
|
follow_up: sparse.follow_up,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
PatternFormat::Legacy(legacy) => Ok(Pattern {
|
PatternFormat::Legacy(legacy) => Ok(Pattern {
|
||||||
@@ -370,6 +415,7 @@ impl<'de> Deserialize<'de> for Pattern {
|
|||||||
name: legacy.name,
|
name: legacy.name,
|
||||||
quantization: legacy.quantization,
|
quantization: legacy.quantization,
|
||||||
sync_mode: legacy.sync_mode,
|
sync_mode: legacy.sync_mode,
|
||||||
|
follow_up: legacy.follow_up,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,6 +430,7 @@ impl Default for Pattern {
|
|||||||
name: None,
|
name: None,
|
||||||
quantization: LaunchQuantization::default(),
|
quantization: LaunchQuantization::default(),
|
||||||
sync_mode: SyncMode::default(),
|
sync_mode: SyncMode::default(),
|
||||||
|
follow_up: FollowUp::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,18 @@ Each pattern is an independent sequence of steps with its own properties:
|
|||||||
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
|
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
|
||||||
| Quantization | When the pattern launches | `Bar` |
|
| Quantization | When the pattern launches | `Bar` |
|
||||||
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
|
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
|
||||||
|
| Follow Up | What happens when the pattern finishes an iteration | `Loop` |
|
||||||
|
|
||||||
Press `e` in the patterns view to edit these settings.
|
Press `e` in the patterns view to edit these settings.
|
||||||
|
|
||||||
|
### Follow Up
|
||||||
|
|
||||||
|
The follow-up action determines what happens when a pattern reaches the end of its steps:
|
||||||
|
|
||||||
|
- **Loop** — the pattern repeats indefinitely. This is the default behavior.
|
||||||
|
- **Stop** — the pattern plays once and stops.
|
||||||
|
- **Chain** — the pattern plays once, then starts another pattern. Use `Left`/`Right` to set the target bank and pattern in the edit view.
|
||||||
|
|
||||||
## Patterns View
|
## Patterns View
|
||||||
|
|
||||||
Access the patterns view with `F2` (or `Ctrl+Up` from the sequencer). The view shows all banks and patterns in a grid. Indicators show pattern state:
|
Access the patterns view with `F2` (or `Ctrl+Up` from the sequencer). The view shows all banks and patterns in a grid. Indicators show pattern state:
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ impl Plugin for CagirePlugin {
|
|||||||
.collect(),
|
.collect(),
|
||||||
quantization: pat.quantization,
|
quantization: pat.quantization,
|
||||||
sync_mode: pat.sync_mode,
|
sync_mode: pat.sync_mode,
|
||||||
|
follow_up: pat.follow_up,
|
||||||
};
|
};
|
||||||
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
|
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
|
||||||
bank: bank_idx,
|
bank: bank_idx,
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ impl App {
|
|||||||
speed,
|
speed,
|
||||||
quantization,
|
quantization,
|
||||||
sync_mode,
|
sync_mode,
|
||||||
|
follow_up,
|
||||||
} => {
|
} => {
|
||||||
self.playback.staged_prop_changes.insert(
|
self.playback.staged_prop_changes.insert(
|
||||||
(bank, pattern),
|
(bank, pattern),
|
||||||
@@ -189,6 +190,7 @@ impl App {
|
|||||||
speed,
|
speed,
|
||||||
quantization,
|
quantization,
|
||||||
sync_mode,
|
sync_mode,
|
||||||
|
follow_up,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.ui.set_status(format!(
|
self.ui.set_status(format!(
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ impl App {
|
|||||||
speed: pat.speed,
|
speed: pat.speed,
|
||||||
quantization: pat.quantization,
|
quantization: pat.quantization,
|
||||||
sync_mode: pat.sync_mode,
|
sync_mode: pat.sync_mode,
|
||||||
|
follow_up: pat.follow_up,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ impl App {
|
|||||||
nudge_secs: 0.0,
|
nudge_secs: 0.0,
|
||||||
cc_access: None,
|
cc_access: None,
|
||||||
speed_key: "",
|
speed_key: "",
|
||||||
chain_key: "",
|
|
||||||
mouse_x: 0.5,
|
mouse_x: 0.5,
|
||||||
mouse_y: 0.5,
|
mouse_y: 0.5,
|
||||||
mouse_down: 0.0,
|
mouse_down: 0.0,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ impl App {
|
|||||||
.collect(),
|
.collect(),
|
||||||
quantization: pat.quantization,
|
quantization: pat.quantization,
|
||||||
sync_mode: pat.sync_mode,
|
sync_mode: pat.sync_mode,
|
||||||
|
follow_up: pat.follow_up,
|
||||||
};
|
};
|
||||||
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
||||||
bank,
|
bank,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ impl App {
|
|||||||
pat.speed = props.speed;
|
pat.speed = props.speed;
|
||||||
pat.quantization = props.quantization;
|
pat.quantization = props.quantization;
|
||||||
pat.sync_mode = props.sync_mode;
|
pat.sync_mode = props.sync_mode;
|
||||||
|
pat.follow_up = props.follow_up;
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{ColorScheme, DeviceKind, EngineSection, Modal, OptionsFocus, PatternField, SettingKind};
|
use crate::state::{ColorScheme, DeviceKind, EngineSection, Modal, OptionsFocus, PatternField, SettingKind};
|
||||||
|
|
||||||
@@ -144,6 +144,7 @@ pub enum AppCommand {
|
|||||||
speed: PatternSpeed,
|
speed: PatternSpeed,
|
||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
sync_mode: SyncMode,
|
sync_mode: SyncMode,
|
||||||
|
follow_up: FollowUp,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Page navigation
|
// Page navigation
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use super::{substeps_in_window, LinkState, StepTiming, SyncTime};
|
|||||||
use crate::model::{
|
use crate::model::{
|
||||||
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
|
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
|
||||||
};
|
};
|
||||||
use crate::model::{LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
|
use crate::model::{FollowUp, LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
|
||||||
use crate::state::LiveKeyState;
|
use crate::state::LiveKeyState;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||||
@@ -134,6 +134,7 @@ pub struct PatternSnapshot {
|
|||||||
pub steps: Vec<StepSnapshot>,
|
pub steps: Vec<StepSnapshot>,
|
||||||
pub quantization: LaunchQuantization,
|
pub quantization: LaunchQuantization,
|
||||||
pub sync_mode: SyncMode,
|
pub sync_mode: SyncMode,
|
||||||
|
pub follow_up: FollowUp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -523,33 +524,6 @@ struct StepResult {
|
|||||||
any_step_fired: bool,
|
any_step_fired: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VariableReads {
|
|
||||||
new_tempo: Option<f64>,
|
|
||||||
chain_transitions: Vec<(PatternId, PatternId)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_chain_target(s: &str) -> Option<PatternId> {
|
|
||||||
let (bank, pattern) = s.split_once(':')?;
|
|
||||||
Some(PatternId {
|
|
||||||
bank: bank.parse().ok()?,
|
|
||||||
pattern: pattern.parse().ok()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
struct KeyBuf {
|
|
||||||
speed: String,
|
|
||||||
chain: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyBuf {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
speed: String::with_capacity(24),
|
|
||||||
chain: String::with_capacity(24),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
|
fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
buf.clear();
|
buf.clear();
|
||||||
@@ -557,13 +531,6 @@ fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
|
|||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
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 struct SequencerState {
|
pub struct SequencerState {
|
||||||
audio_state: AudioState,
|
audio_state: AudioState,
|
||||||
pattern_cache: PatternCache,
|
pattern_cache: PatternCache,
|
||||||
@@ -575,7 +542,7 @@ pub struct SequencerState {
|
|||||||
variables: Variables,
|
variables: Variables,
|
||||||
dict: Dictionary,
|
dict: Dictionary,
|
||||||
speed_overrides: HashMap<(usize, usize), f64>,
|
speed_overrides: HashMap<(usize, usize), f64>,
|
||||||
key_buf: KeyBuf,
|
speed_key_buf: String,
|
||||||
buf_audio_commands: Vec<TimestampedCommand>,
|
buf_audio_commands: Vec<TimestampedCommand>,
|
||||||
buf_activated: Vec<PatternId>,
|
buf_activated: Vec<PatternId>,
|
||||||
buf_stopped: Vec<PatternId>,
|
buf_stopped: Vec<PatternId>,
|
||||||
@@ -606,7 +573,7 @@ impl SequencerState {
|
|||||||
variables,
|
variables,
|
||||||
dict,
|
dict,
|
||||||
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
|
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
|
||||||
key_buf: KeyBuf::new(),
|
speed_key_buf: String::with_capacity(24),
|
||||||
buf_audio_commands: Vec::with_capacity(32),
|
buf_audio_commands: Vec::with_capacity(32),
|
||||||
buf_activated: Vec::with_capacity(16),
|
buf_activated: Vec::with_capacity(16),
|
||||||
buf_stopped: Vec::with_capacity(16),
|
buf_stopped: Vec::with_capacity(16),
|
||||||
@@ -757,15 +724,15 @@ impl SequencerState {
|
|||||||
input.mouse_down,
|
input.mouse_down,
|
||||||
);
|
);
|
||||||
|
|
||||||
let vars = self.read_variables(&self.buf_completed_iterations, steps.any_step_fired);
|
let new_tempo = self.read_tempo_variable(steps.any_step_fired);
|
||||||
self.apply_chain_transitions(vars.chain_transitions);
|
self.apply_follow_ups();
|
||||||
|
|
||||||
self.audio_state.prev_beat = lookahead_end;
|
self.audio_state.prev_beat = lookahead_end;
|
||||||
|
|
||||||
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
|
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
|
||||||
TickOutput {
|
TickOutput {
|
||||||
audio_commands: std::mem::take(&mut self.buf_audio_commands),
|
audio_commands: std::mem::take(&mut self.buf_audio_commands),
|
||||||
new_tempo: vars.new_tempo,
|
new_tempo,
|
||||||
shared_state: self.build_shared_state(),
|
shared_state: self.build_shared_state(),
|
||||||
flush_midi_notes: flush,
|
flush_midi_notes: flush,
|
||||||
}
|
}
|
||||||
@@ -896,7 +863,7 @@ impl SequencerState {
|
|||||||
{
|
{
|
||||||
let vars = self.variables.load_full();
|
let vars = self.variables.load_full();
|
||||||
for id in self.audio_state.active_patterns.keys() {
|
for id in self.audio_state.active_patterns.keys() {
|
||||||
let key = format_speed_key(&mut self.key_buf.speed, id.bank, id.pattern);
|
let key = format_speed_key(&mut self.speed_key_buf, id.bank, id.pattern);
|
||||||
if let Some(v) = vars.get(key).and_then(|v: &Value| v.as_float().ok()) {
|
if let Some(v) = vars.get(key).and_then(|v: &Value| v.as_float().ok()) {
|
||||||
self.speed_overrides.insert((id.bank, id.pattern), v);
|
self.speed_overrides.insert((id.bank, id.pattern), v);
|
||||||
}
|
}
|
||||||
@@ -947,8 +914,7 @@ impl SequencerState {
|
|||||||
active.pattern,
|
active.pattern,
|
||||||
source_idx,
|
source_idx,
|
||||||
);
|
);
|
||||||
let speed_key = format_speed_key(&mut self.key_buf.speed, active.bank, active.pattern);
|
let speed_key = format_speed_key(&mut self.speed_key_buf, active.bank, active.pattern);
|
||||||
let chain_key = format_chain_key(&mut self.key_buf.chain, active.bank, active.pattern);
|
|
||||||
let ctx = StepContext {
|
let ctx = StepContext {
|
||||||
step: step_idx,
|
step: step_idx,
|
||||||
beat: step_beat,
|
beat: step_beat,
|
||||||
@@ -964,7 +930,6 @@ impl SequencerState {
|
|||||||
nudge_secs,
|
nudge_secs,
|
||||||
cc_access: self.cc_access.as_deref(),
|
cc_access: self.cc_access.as_deref(),
|
||||||
speed_key,
|
speed_key,
|
||||||
chain_key,
|
|
||||||
mouse_x,
|
mouse_x,
|
||||||
mouse_y,
|
mouse_y,
|
||||||
mouse_down,
|
mouse_down,
|
||||||
@@ -1016,14 +981,9 @@ impl SequencerState {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_variables(&self, completed: &[PatternId], any_step_fired: bool) -> VariableReads {
|
fn read_tempo_variable(&self, any_step_fired: bool) -> Option<f64> {
|
||||||
let stopped = &self.buf_stopped;
|
if !any_step_fired {
|
||||||
let needs_access = !completed.is_empty() || !stopped.is_empty() || any_step_fired;
|
return None;
|
||||||
if !needs_access {
|
|
||||||
return VariableReads {
|
|
||||||
new_tempo: None,
|
|
||||||
chain_transitions: Vec::new(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let vars = self.variables.load_full();
|
let vars = self.variables.load_full();
|
||||||
@@ -1031,76 +991,45 @@ impl SequencerState {
|
|||||||
.get("__tempo__")
|
.get("__tempo__")
|
||||||
.and_then(|v: &Value| v.as_float().ok());
|
.and_then(|v: &Value| v.as_float().ok());
|
||||||
|
|
||||||
let mut chain_transitions = Vec::new();
|
if new_tempo.is_some() {
|
||||||
let mut buf = String::with_capacity(24);
|
|
||||||
for id in completed {
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove consumed variables (tempo and chain keys)
|
|
||||||
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();
|
let mut new_vars = (*vars).clone();
|
||||||
new_vars.remove("__tempo__");
|
new_vars.remove("__tempo__");
|
||||||
for id in completed {
|
|
||||||
new_vars.remove(format_chain_key(&mut buf, id.bank, id.pattern));
|
|
||||||
}
|
|
||||||
for id in stopped {
|
|
||||||
new_vars.remove(format_chain_key(&mut buf, id.bank, id.pattern));
|
|
||||||
}
|
|
||||||
self.variables.store(Arc::new(new_vars));
|
self.variables.store(Arc::new(new_vars));
|
||||||
}
|
}
|
||||||
|
|
||||||
VariableReads {
|
new_tempo
|
||||||
new_tempo,
|
|
||||||
chain_transitions,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_chain_transitions(&mut self, transitions: Vec<(PatternId, PatternId)>) {
|
fn apply_follow_ups(&mut self) {
|
||||||
for (source, target) in transitions {
|
for completed_id in &self.buf_completed_iterations {
|
||||||
if !self
|
let Some(pattern) = self.pattern_cache.get(completed_id.bank, completed_id.pattern) else {
|
||||||
.audio_state
|
continue;
|
||||||
.pending_stops
|
};
|
||||||
.iter()
|
|
||||||
.any(|p| p.id == source)
|
match pattern.follow_up {
|
||||||
{
|
FollowUp::Loop => {}
|
||||||
self.audio_state.pending_stops.push(PendingPattern {
|
FollowUp::Stop => {
|
||||||
id: source,
|
self.audio_state.pending_stops.push(PendingPattern {
|
||||||
quantization: LaunchQuantization::Bar,
|
id: *completed_id,
|
||||||
sync_mode: SyncMode::Reset,
|
quantization: LaunchQuantization::Immediate,
|
||||||
});
|
sync_mode: SyncMode::Reset,
|
||||||
}
|
});
|
||||||
if !self
|
}
|
||||||
.audio_state
|
FollowUp::Chain { bank, pattern } => {
|
||||||
.pending_starts
|
self.audio_state.pending_stops.push(PendingPattern {
|
||||||
.iter()
|
id: *completed_id,
|
||||||
.any(|p| p.id == target)
|
quantization: LaunchQuantization::Immediate,
|
||||||
{
|
sync_mode: SyncMode::Reset,
|
||||||
let (quant, sync) = self
|
});
|
||||||
.pattern_cache
|
let target = PatternId { bank, pattern };
|
||||||
.get(target.bank, target.pattern)
|
if !self.audio_state.pending_starts.iter().any(|p| p.id == target) {
|
||||||
.map(|p| (p.quantization, p.sync_mode))
|
self.audio_state.pending_starts.push(PendingPattern {
|
||||||
.unwrap_or((LaunchQuantization::Bar, SyncMode::Reset));
|
id: target,
|
||||||
self.audio_state.pending_starts.push(PendingPattern {
|
quantization: LaunchQuantization::Immediate,
|
||||||
id: target,
|
sync_mode: SyncMode::Reset,
|
||||||
quantization: quant,
|
});
|
||||||
sync_mode: sync,
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1412,6 +1341,7 @@ mod tests {
|
|||||||
.collect(),
|
.collect(),
|
||||||
quantization: LaunchQuantization::Immediate,
|
quantization: LaunchQuantization::Immediate,
|
||||||
sync_mode: SyncMode::Reset,
|
sync_mode: SyncMode::Reset,
|
||||||
|
follow_up: FollowUp::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1528,68 +1458,6 @@ mod tests {
|
|||||||
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_chain_requires_active_source() {
|
|
||||||
let mut state = make_state();
|
|
||||||
|
|
||||||
// Set up: pattern 0 (length 1) chains to pattern 1
|
|
||||||
state.tick(tick_with(
|
|
||||||
vec![
|
|
||||||
SeqCommand::PatternUpdate {
|
|
||||||
bank: 0,
|
|
||||||
pattern: 0,
|
|
||||||
data: simple_pattern(1),
|
|
||||||
},
|
|
||||||
SeqCommand::PatternUpdate {
|
|
||||||
bank: 0,
|
|
||||||
pattern: 1,
|
|
||||||
data: simple_pattern(4),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
0.0,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Start pattern 0
|
|
||||||
state.tick(tick_with(
|
|
||||||
vec![SeqCommand::PatternStart {
|
|
||||||
bank: 0,
|
|
||||||
pattern: 0,
|
|
||||||
quantization: LaunchQuantization::Immediate,
|
|
||||||
sync_mode: SyncMode::Reset,
|
|
||||||
}],
|
|
||||||
0.5,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Set chain variable
|
|
||||||
{
|
|
||||||
let mut vars = (**state.variables.load()).clone();
|
|
||||||
vars.insert(
|
|
||||||
"__chain_0_0__".to_string(),
|
|
||||||
Value::Str(std::sync::Arc::from("0:1"), None),
|
|
||||||
);
|
|
||||||
state.variables.store(Arc::new(vars));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern 0 completes iteration AND gets stopped immediately in the same tick.
|
|
||||||
// The stop removes it from active_patterns before chain evaluation,
|
|
||||||
// so the chain guard (active_patterns.contains_key) blocks the transition.
|
|
||||||
let output = state.tick(tick_with(
|
|
||||||
vec![SeqCommand::PatternStop {
|
|
||||||
bank: 0,
|
|
||||||
pattern: 0,
|
|
||||||
quantization: LaunchQuantization::Immediate,
|
|
||||||
}],
|
|
||||||
1.0,
|
|
||||||
));
|
|
||||||
|
|
||||||
assert!(output.shared_state.active_patterns.is_empty());
|
|
||||||
assert!(!state
|
|
||||||
.audio_state
|
|
||||||
.pending_starts
|
|
||||||
.iter()
|
|
||||||
.any(|p| p.id == pid(0, 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pattern_start_cancels_pending_stop() {
|
fn test_pattern_start_cancels_pending_stop() {
|
||||||
let mut state = make_state();
|
let mut state = make_state();
|
||||||
@@ -1779,67 +1647,6 @@ mod tests {
|
|||||||
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_stop_during_iteration_blocks_chain() {
|
|
||||||
let mut state = make_state();
|
|
||||||
|
|
||||||
state.tick(tick_with(
|
|
||||||
vec![
|
|
||||||
SeqCommand::PatternUpdate {
|
|
||||||
bank: 0,
|
|
||||||
pattern: 0,
|
|
||||||
data: simple_pattern(1),
|
|
||||||
},
|
|
||||||
SeqCommand::PatternUpdate {
|
|
||||||
bank: 0,
|
|
||||||
pattern: 1,
|
|
||||||
data: simple_pattern(4),
|
|
||||||
},
|
|
||||||
SeqCommand::PatternStart {
|
|
||||||
bank: 0,
|
|
||||||
pattern: 0,
|
|
||||||
quantization: LaunchQuantization::Immediate,
|
|
||||||
sync_mode: SyncMode::Reset,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
0.0,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Pattern 0 is now pending (will activate next tick when prev_beat >= 0)
|
|
||||||
// Advance so it activates
|
|
||||||
state.tick(tick_at(0.5, true));
|
|
||||||
assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
|
|
||||||
|
|
||||||
// Set chain: 0:0 -> 0:1
|
|
||||||
{
|
|
||||||
let mut vars = (**state.variables.load()).clone();
|
|
||||||
vars.insert(
|
|
||||||
"__chain_0_0__".to_string(),
|
|
||||||
Value::Str(std::sync::Arc::from("0:1"), None),
|
|
||||||
);
|
|
||||||
state.variables.store(Arc::new(vars));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern 0 (length 1) completes iteration at beat=1.0 AND
|
|
||||||
// an immediate stop removes it from active_patterns first.
|
|
||||||
// Chain guard should block transition to pattern 1.
|
|
||||||
state.tick(tick_with(
|
|
||||||
vec![SeqCommand::PatternStop {
|
|
||||||
bank: 0,
|
|
||||||
pattern: 0,
|
|
||||||
quantization: LaunchQuantization::Immediate,
|
|
||||||
}],
|
|
||||||
1.0,
|
|
||||||
));
|
|
||||||
|
|
||||||
assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 1)));
|
|
||||||
assert!(!state
|
|
||||||
.audio_state
|
|
||||||
.pending_starts
|
|
||||||
.iter()
|
|
||||||
.any(|p| p.id == pid(0, 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_multiple_patterns_independent_quantization() {
|
fn test_multiple_patterns_independent_quantization() {
|
||||||
let mut state = make_state();
|
let mut state = make_state();
|
||||||
@@ -2100,6 +1907,7 @@ mod tests {
|
|||||||
.collect(),
|
.collect(),
|
||||||
quantization: LaunchQuantization::Immediate,
|
quantization: LaunchQuantization::Immediate,
|
||||||
sync_mode: SyncMode::Reset,
|
sync_mode: SyncMode::Reset,
|
||||||
|
follow_up: FollowUp::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
|||||||
use super::{InputContext, InputResult};
|
use super::{InputContext, InputResult};
|
||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
use crate::engine::SeqCommand;
|
use crate::engine::SeqCommand;
|
||||||
use crate::model::PatternSpeed;
|
use crate::model::{FollowUp, PatternSpeed};
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
ConfirmAction, EditorTarget, EuclideanField, Modal, PatternField,
|
ConfirmAction, EditorTarget, EuclideanField, Modal, PatternField,
|
||||||
PatternPropsField, RenameTarget,
|
PatternPropsField, RenameTarget,
|
||||||
@@ -377,21 +377,45 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
speed,
|
speed,
|
||||||
quantization,
|
quantization,
|
||||||
sync_mode,
|
sync_mode,
|
||||||
|
follow_up,
|
||||||
} => {
|
} => {
|
||||||
let (bank, pattern) = (*bank, *pattern);
|
let (bank, pattern) = (*bank, *pattern);
|
||||||
|
let is_chain = matches!(follow_up, FollowUp::Chain { .. });
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up => *field = field.prev(),
|
KeyCode::Up => *field = field.prev(is_chain),
|
||||||
KeyCode::Down | KeyCode::Tab => *field = field.next(),
|
KeyCode::Down | KeyCode::Tab => *field = field.next(is_chain),
|
||||||
KeyCode::Left => match field {
|
KeyCode::Left => match field {
|
||||||
PatternPropsField::Speed => *speed = speed.prev(),
|
PatternPropsField::Speed => *speed = speed.prev(),
|
||||||
PatternPropsField::Quantization => *quantization = quantization.prev(),
|
PatternPropsField::Quantization => *quantization = quantization.prev(),
|
||||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||||
|
PatternPropsField::FollowUp => *follow_up = follow_up.prev_mode(),
|
||||||
|
PatternPropsField::ChainBank => {
|
||||||
|
if let FollowUp::Chain { bank: b, .. } = follow_up {
|
||||||
|
*b = b.saturating_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PatternPropsField::ChainPattern => {
|
||||||
|
if let FollowUp::Chain { pattern: p, .. } = follow_up {
|
||||||
|
*p = p.saturating_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
KeyCode::Right => match field {
|
KeyCode::Right => match field {
|
||||||
PatternPropsField::Speed => *speed = speed.next(),
|
PatternPropsField::Speed => *speed = speed.next(),
|
||||||
PatternPropsField::Quantization => *quantization = quantization.next(),
|
PatternPropsField::Quantization => *quantization = quantization.next(),
|
||||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||||
|
PatternPropsField::FollowUp => *follow_up = follow_up.next_mode(),
|
||||||
|
PatternPropsField::ChainBank => {
|
||||||
|
if let FollowUp::Chain { bank: b, .. } = follow_up {
|
||||||
|
*b = (*b + 1).min(31);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PatternPropsField::ChainPattern => {
|
||||||
|
if let FollowUp::Chain { pattern: p, .. } = follow_up {
|
||||||
|
*p = (*p + 1).min(31);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
KeyCode::Char(c) => match field {
|
KeyCode::Char(c) => match field {
|
||||||
@@ -418,6 +442,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
let speed_val = *speed;
|
let speed_val = *speed;
|
||||||
let quant_val = *quantization;
|
let quant_val = *quantization;
|
||||||
let sync_val = *sync_mode;
|
let sync_val = *sync_mode;
|
||||||
|
let follow_up_val = *follow_up;
|
||||||
ctx.dispatch(AppCommand::StagePatternProps {
|
ctx.dispatch(AppCommand::StagePatternProps {
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
@@ -426,6 +451,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
speed: speed_val,
|
speed: speed_val,
|
||||||
quantization: quant_val,
|
quantization: quant_val,
|
||||||
sync_mode: sync_val,
|
sync_mode: sync_val,
|
||||||
|
follow_up: follow_up_val,
|
||||||
});
|
});
|
||||||
ctx.dispatch(AppCommand::CloseModal);
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub use cagire_forth::{
|
|||||||
Variables, Word, WordCompile, WORDS,
|
Variables, Word, WordCompile, WORDS,
|
||||||
};
|
};
|
||||||
pub use cagire_project::{
|
pub use cagire_project::{
|
||||||
load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS,
|
load, save, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode,
|
||||||
MAX_PATTERNS,
|
MAX_BANKS, MAX_PATTERNS,
|
||||||
};
|
};
|
||||||
pub use script::ScriptEngine;
|
pub use script::ScriptEngine;
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ pub fn update_cache(editor_ctx: &EditorContext) {
|
|||||||
nudge_secs: 0.0,
|
nudge_secs: 0.0,
|
||||||
cc_access: None,
|
cc_access: None,
|
||||||
speed_key: "",
|
speed_key: "",
|
||||||
chain_key: "",
|
|
||||||
mouse_x: 0.5,
|
mouse_x: 0.5,
|
||||||
mouse_y: 0.5,
|
mouse_y: 0.5,
|
||||||
mouse_down: 0.0,
|
mouse_down: 0.0,
|
||||||
|
|||||||
@@ -24,26 +24,37 @@ pub enum PatternPropsField {
|
|||||||
Speed,
|
Speed,
|
||||||
Quantization,
|
Quantization,
|
||||||
SyncMode,
|
SyncMode,
|
||||||
|
FollowUp,
|
||||||
|
ChainBank,
|
||||||
|
ChainPattern,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PatternPropsField {
|
impl PatternPropsField {
|
||||||
pub fn next(&self) -> Self {
|
pub fn next(&self, follow_up_is_chain: bool) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Name => Self::Length,
|
Self::Name => Self::Length,
|
||||||
Self::Length => Self::Speed,
|
Self::Length => Self::Speed,
|
||||||
Self::Speed => Self::Quantization,
|
Self::Speed => Self::Quantization,
|
||||||
Self::Quantization => Self::SyncMode,
|
Self::Quantization => Self::SyncMode,
|
||||||
Self::SyncMode => Self::SyncMode,
|
Self::SyncMode => Self::FollowUp,
|
||||||
|
Self::FollowUp if follow_up_is_chain => Self::ChainBank,
|
||||||
|
Self::FollowUp => Self::FollowUp,
|
||||||
|
Self::ChainBank => Self::ChainPattern,
|
||||||
|
Self::ChainPattern => Self::ChainPattern,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prev(&self) -> Self {
|
pub fn prev(&self, follow_up_is_chain: bool) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Name => Self::Name,
|
Self::Name => Self::Name,
|
||||||
Self::Length => Self::Name,
|
Self::Length => Self::Name,
|
||||||
Self::Speed => Self::Length,
|
Self::Speed => Self::Length,
|
||||||
Self::Quantization => Self::Speed,
|
Self::Quantization => Self::Speed,
|
||||||
Self::SyncMode => Self::Quantization,
|
Self::SyncMode => Self::Quantization,
|
||||||
|
Self::FollowUp => Self::SyncMode,
|
||||||
|
Self::ChainBank => Self::FollowUp,
|
||||||
|
Self::ChainPattern if follow_up_is_chain => Self::ChainBank,
|
||||||
|
Self::ChainPattern => Self::FollowUp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||||
use crate::state::editor::{EuclideanField, PatternField, PatternPropsField};
|
use crate::state::editor::{EuclideanField, PatternField, PatternPropsField};
|
||||||
use crate::state::file_browser::FileBrowserState;
|
use crate::state::file_browser::FileBrowserState;
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ pub enum Modal {
|
|||||||
speed: PatternSpeed,
|
speed: PatternSpeed,
|
||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
sync_mode: SyncMode,
|
sync_mode: SyncMode,
|
||||||
|
follow_up: FollowUp,
|
||||||
},
|
},
|
||||||
KeybindingsHelp {
|
KeybindingsHelp {
|
||||||
scroll: usize,
|
scroll: usize,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::engine::PatternChange;
|
use crate::engine::PatternChange;
|
||||||
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -21,6 +21,7 @@ pub struct StagedPropChange {
|
|||||||
pub speed: PatternSpeed,
|
pub speed: PatternSpeed,
|
||||||
pub quantization: LaunchQuantization,
|
pub quantization: LaunchQuantization,
|
||||||
pub sync_mode: SyncMode,
|
pub sync_mode: SyncMode,
|
||||||
|
pub follow_up: FollowUp,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PlaybackState {
|
pub struct PlaybackState {
|
||||||
|
|||||||
@@ -716,6 +716,8 @@ fn render_properties(
|
|||||||
bank: usize,
|
bank: usize,
|
||||||
pattern_idx: usize,
|
pattern_idx: usize,
|
||||||
) {
|
) {
|
||||||
|
use cagire_project::FollowUp;
|
||||||
|
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let pattern = &app.project_state.project.banks[bank].patterns[pattern_idx];
|
let pattern = &app.project_state.project.banks[bank].patterns[pattern_idx];
|
||||||
|
|
||||||
@@ -729,7 +731,7 @@ fn render_properties(
|
|||||||
let label_style = Style::new().fg(theme.ui.text_muted);
|
let label_style = Style::new().fg(theme.ui.text_muted);
|
||||||
let value_style = Style::new().fg(theme.ui.text_primary);
|
let value_style = Style::new().fg(theme.ui.text_primary);
|
||||||
|
|
||||||
let rows: Vec<Line> = vec![
|
let mut rows: Vec<Line> = vec![
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(" Name ", label_style),
|
Span::styled(" Name ", label_style),
|
||||||
Span::styled(name, value_style),
|
Span::styled(name, value_style),
|
||||||
@@ -752,5 +754,17 @@ fn render_properties(
|
|||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if pattern.follow_up != FollowUp::Loop {
|
||||||
|
let follow_label = match pattern.follow_up {
|
||||||
|
FollowUp::Loop => unreachable!(),
|
||||||
|
FollowUp::Stop => "Stop".to_string(),
|
||||||
|
FollowUp::Chain { bank: b, pattern: p } => format!("Chain B{:02}:P{:02}", b + 1, p + 1),
|
||||||
|
};
|
||||||
|
rows.push(Line::from(vec![
|
||||||
|
Span::styled(" After ", label_style),
|
||||||
|
Span::styled(follow_label, value_style),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
frame.render_widget(Paragraph::new(rows), area);
|
frame.render_widget(Paragraph::new(rows), area);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -609,37 +609,45 @@ fn render_modal(
|
|||||||
speed,
|
speed,
|
||||||
quantization,
|
quantization,
|
||||||
sync_mode,
|
sync_mode,
|
||||||
|
follow_up,
|
||||||
} => {
|
} => {
|
||||||
|
use crate::model::FollowUp;
|
||||||
use crate::state::PatternPropsField;
|
use crate::state::PatternPropsField;
|
||||||
|
|
||||||
|
let is_chain = matches!(follow_up, FollowUp::Chain { .. });
|
||||||
|
let modal_height = if is_chain { 16 } else { 14 };
|
||||||
|
|
||||||
let inner = ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
|
let inner = ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
|
||||||
.width(50)
|
.width(50)
|
||||||
.height(12)
|
.height(modal_height)
|
||||||
.border_color(theme.modal.input)
|
.border_color(theme.modal.input)
|
||||||
.render_centered(frame, term);
|
.render_centered(frame, term);
|
||||||
|
|
||||||
let speed_label = speed.label();
|
let speed_label = speed.label();
|
||||||
let fields: Vec<(&str, &str, bool)> = vec![
|
let follow_up_label = match follow_up {
|
||||||
("Name", name.as_str(), *field == PatternPropsField::Name),
|
FollowUp::Loop => "Loop".to_string(),
|
||||||
(
|
FollowUp::Stop => "Stop".to_string(),
|
||||||
"Length",
|
FollowUp::Chain { bank: b, pattern: p } => {
|
||||||
length.as_str(),
|
format!("Chain B{:02}:P{:02}", b + 1, p + 1)
|
||||||
*field == PatternPropsField::Length,
|
}
|
||||||
),
|
};
|
||||||
("Speed", &speed_label, *field == PatternPropsField::Speed),
|
let mut fields: Vec<(&str, String, bool)> = vec![
|
||||||
(
|
("Name", name.clone(), *field == PatternPropsField::Name),
|
||||||
"Quantization",
|
("Length", length.clone(), *field == PatternPropsField::Length),
|
||||||
quantization.label(),
|
("Speed", speed_label, *field == PatternPropsField::Speed),
|
||||||
*field == PatternPropsField::Quantization,
|
("Quantization", quantization.label().to_string(), *field == PatternPropsField::Quantization),
|
||||||
),
|
("Sync Mode", sync_mode.label().to_string(), *field == PatternPropsField::SyncMode),
|
||||||
(
|
("Follow Up", follow_up_label, *field == PatternPropsField::FollowUp),
|
||||||
"Sync Mode",
|
|
||||||
sync_mode.label(),
|
|
||||||
*field == PatternPropsField::SyncMode,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
if is_chain {
|
||||||
|
if let FollowUp::Chain { bank: b, pattern: p } = follow_up {
|
||||||
|
fields.push((" Bank", format!("{:02}", b + 1), *field == PatternPropsField::ChainBank));
|
||||||
|
fields.push((" Pattern", format!("{:02}", p + 1), *field == PatternPropsField::ChainPattern));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render_props_form(frame, inner, &fields);
|
let fields_ref: Vec<(&str, &str, bool)> = fields.iter().map(|(l, v, s)| (*l, v.as_str(), *s)).collect();
|
||||||
|
render_props_form(frame, inner, &fields_ref);
|
||||||
|
|
||||||
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
|
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
|
||||||
let hints = hint_line(&[
|
let hints = hint_line(&[
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ pub fn default_ctx() -> StepContext<'static> {
|
|||||||
nudge_secs: 0.0,
|
nudge_secs: 0.0,
|
||||||
cc_access: None,
|
cc_access: None,
|
||||||
speed_key: "__speed_0_0__",
|
speed_key: "__speed_0_0__",
|
||||||
chain_key: "__chain_0_0__",
|
|
||||||
mouse_x: 0.5,
|
mouse_x: 0.5,
|
||||||
mouse_y: 0.5,
|
mouse_y: 0.5,
|
||||||
mouse_down: 0.0,
|
mouse_down: 0.0,
|
||||||
|
|||||||
Reference in New Issue
Block a user