Feat: introduce follow up actions
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s

This commit is contained in:
2026-02-22 03:59:09 +01:00
parent 003ee0518e
commit 8ba98e8f3b
25 changed files with 203 additions and 307 deletions

View File

@@ -94,7 +94,6 @@ pub enum Op {
Triangle,
Range,
Perlin,
Chain,
Loop,
Degree(&'static [i64]),
Oct,

View File

@@ -59,7 +59,6 @@ pub struct StepContext<'a> {
pub nudge_secs: f64,
pub cc_access: Option<&'a dyn CcAccess>,
pub speed_key: &'a str,
pub chain_key: &'a str,
pub mouse_x: f64,
pub mouse_y: f64,
pub mouse_down: f64,

View File

@@ -992,26 +992,6 @@ impl Forth {
.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 => {
let beats = pop_float(stack)?;
if ctx.tempo == 0.0 || ctx.speed == 0.0 {

View File

@@ -89,7 +89,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"triangle" => Op::Triangle,
"range" => Op::Range,
"perlin" => Op::Perlin,
"chain" => Op::Chain,
"loop" => Op::Loop,
"oct" => Op::Oct,
"clear" => Op::ClearCmd,

View File

@@ -254,16 +254,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
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 {
name: "at",
aliases: &[],

View File

@@ -7,4 +7,4 @@ pub const MAX_STEPS: usize = 1024;
pub const DEFAULT_LENGTH: usize = 16;
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};

View File

@@ -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)]
pub struct Step {
pub active: bool,
@@ -245,6 +283,7 @@ pub struct Pattern {
pub name: Option<String>,
pub quantization: LaunchQuantization,
pub sync_mode: SyncMode,
pub follow_up: FollowUp,
}
#[derive(Serialize, Deserialize)]
@@ -280,6 +319,8 @@ struct SparsePattern {
quantization: LaunchQuantization,
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
sync_mode: SyncMode,
#[serde(default, skip_serializing_if = "is_default_follow_up")]
follow_up: FollowUp,
}
fn is_default_quantization(q: &LaunchQuantization) -> bool {
@@ -302,6 +343,8 @@ struct LegacyPattern {
quantization: LaunchQuantization,
#[serde(default)]
sync_mode: SyncMode,
#[serde(default)]
follow_up: FollowUp,
}
impl Serialize for Pattern {
@@ -327,6 +370,7 @@ impl Serialize for Pattern {
name: self.name.clone(),
quantization: self.quantization,
sync_mode: self.sync_mode,
follow_up: self.follow_up,
};
sparse.serialize(serializer)
}
@@ -361,6 +405,7 @@ impl<'de> Deserialize<'de> for Pattern {
name: sparse.name,
quantization: sparse.quantization,
sync_mode: sparse.sync_mode,
follow_up: sparse.follow_up,
})
}
PatternFormat::Legacy(legacy) => Ok(Pattern {
@@ -370,6 +415,7 @@ impl<'de> Deserialize<'de> for Pattern {
name: legacy.name,
quantization: legacy.quantization,
sync_mode: legacy.sync_mode,
follow_up: legacy.follow_up,
}),
}
}
@@ -384,6 +430,7 @@ impl Default for Pattern {
name: None,
quantization: LaunchQuantization::default(),
sync_mode: SyncMode::default(),
follow_up: FollowUp::default(),
}
}
}

View File

@@ -27,9 +27,18 @@ Each pattern is an independent sequence of steps with its own properties:
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
| Quantization | When the pattern launches | `Bar` |
| 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.
### 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
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:

View File

@@ -217,6 +217,7 @@ impl Plugin for CagirePlugin {
.collect(),
quantization: pat.quantization,
sync_mode: pat.sync_mode,
follow_up: pat.follow_up,
};
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
bank: bank_idx,

View File

@@ -180,6 +180,7 @@ impl App {
speed,
quantization,
sync_mode,
follow_up,
} => {
self.playback.staged_prop_changes.insert(
(bank, pattern),
@@ -189,6 +190,7 @@ impl App {
speed,
quantization,
sync_mode,
follow_up,
},
);
self.ui.set_status(format!(

View File

@@ -189,6 +189,7 @@ impl App {
speed: pat.speed,
quantization: pat.quantization,
sync_mode: pat.sync_mode,
follow_up: pat.follow_up,
};
}

View File

@@ -31,7 +31,6 @@ impl App {
nudge_secs: 0.0,
cc_access: None,
speed_key: "",
chain_key: "",
mouse_x: 0.5,
mouse_y: 0.5,
mouse_down: 0.0,

View File

@@ -54,6 +54,7 @@ impl App {
.collect(),
quantization: pat.quantization,
sync_mode: pat.sync_mode,
follow_up: pat.follow_up,
};
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
bank,

View File

@@ -85,6 +85,7 @@ impl App {
pat.speed = props.speed;
pat.quantization = props.quantization;
pat.sync_mode = props.sync_mode;
pat.follow_up = props.follow_up;
self.project_state.mark_dirty(bank, pattern);
}

View File

@@ -1,6 +1,6 @@
use std::path::PathBuf;
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
use crate::page::Page;
use crate::state::{ColorScheme, DeviceKind, EngineSection, Modal, OptionsFocus, PatternField, SettingKind};
@@ -144,6 +144,7 @@ pub enum AppCommand {
speed: PatternSpeed,
quantization: LaunchQuantization,
sync_mode: SyncMode,
follow_up: FollowUp,
},
// Page navigation

View File

@@ -16,7 +16,7 @@ use super::{substeps_in_window, LinkState, StepTiming, SyncTime};
use crate::model::{
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;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
@@ -134,6 +134,7 @@ pub struct PatternSnapshot {
pub steps: Vec<StepSnapshot>,
pub quantization: LaunchQuantization,
pub sync_mode: SyncMode,
pub follow_up: FollowUp,
}
#[derive(Clone)]
@@ -523,33 +524,6 @@ struct StepResult {
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 {
use std::fmt::Write;
buf.clear();
@@ -557,13 +531,6 @@ fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
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 {
audio_state: AudioState,
pattern_cache: PatternCache,
@@ -575,7 +542,7 @@ pub struct SequencerState {
variables: Variables,
dict: Dictionary,
speed_overrides: HashMap<(usize, usize), f64>,
key_buf: KeyBuf,
speed_key_buf: String,
buf_audio_commands: Vec<TimestampedCommand>,
buf_activated: Vec<PatternId>,
buf_stopped: Vec<PatternId>,
@@ -606,7 +573,7 @@ impl SequencerState {
variables,
dict,
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_activated: Vec::with_capacity(16),
buf_stopped: Vec::with_capacity(16),
@@ -757,15 +724,15 @@ impl SequencerState {
input.mouse_down,
);
let vars = self.read_variables(&self.buf_completed_iterations, steps.any_step_fired);
self.apply_chain_transitions(vars.chain_transitions);
let new_tempo = self.read_tempo_variable(steps.any_step_fired);
self.apply_follow_ups();
self.audio_state.prev_beat = lookahead_end;
let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
TickOutput {
audio_commands: std::mem::take(&mut self.buf_audio_commands),
new_tempo: vars.new_tempo,
new_tempo,
shared_state: self.build_shared_state(),
flush_midi_notes: flush,
}
@@ -896,7 +863,7 @@ impl SequencerState {
{
let vars = self.variables.load_full();
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()) {
self.speed_overrides.insert((id.bank, id.pattern), v);
}
@@ -947,8 +914,7 @@ 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 speed_key = format_speed_key(&mut self.speed_key_buf, active.bank, active.pattern);
let ctx = StepContext {
step: step_idx,
beat: step_beat,
@@ -964,7 +930,6 @@ impl SequencerState {
nudge_secs,
cc_access: self.cc_access.as_deref(),
speed_key,
chain_key,
mouse_x,
mouse_y,
mouse_down,
@@ -1016,14 +981,9 @@ impl SequencerState {
result
}
fn read_variables(&self, completed: &[PatternId], any_step_fired: bool) -> VariableReads {
let stopped = &self.buf_stopped;
let needs_access = !completed.is_empty() || !stopped.is_empty() || any_step_fired;
if !needs_access {
return VariableReads {
new_tempo: None,
chain_transitions: Vec::new(),
};
fn read_tempo_variable(&self, any_step_fired: bool) -> Option<f64> {
if !any_step_fired {
return None;
}
let vars = self.variables.load_full();
@@ -1031,79 +991,48 @@ impl SequencerState {
.get("__tempo__")
.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 = 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 {
if new_tempo.is_some() {
let mut new_vars = (*vars).clone();
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));
}
VariableReads {
new_tempo,
chain_transitions,
}
new_tempo
}
fn apply_chain_transitions(&mut self, transitions: Vec<(PatternId, PatternId)>) {
for (source, target) in transitions {
if !self
.audio_state
.pending_stops
.iter()
.any(|p| p.id == source)
{
fn apply_follow_ups(&mut self) {
for completed_id in &self.buf_completed_iterations {
let Some(pattern) = self.pattern_cache.get(completed_id.bank, completed_id.pattern) else {
continue;
};
match pattern.follow_up {
FollowUp::Loop => {}
FollowUp::Stop => {
self.audio_state.pending_stops.push(PendingPattern {
id: source,
quantization: LaunchQuantization::Bar,
id: *completed_id,
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
});
}
if !self
.audio_state
.pending_starts
.iter()
.any(|p| p.id == target)
{
let (quant, sync) = self
.pattern_cache
.get(target.bank, target.pattern)
.map(|p| (p.quantization, p.sync_mode))
.unwrap_or((LaunchQuantization::Bar, SyncMode::Reset));
FollowUp::Chain { bank, pattern } => {
self.audio_state.pending_stops.push(PendingPattern {
id: *completed_id,
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
});
let target = PatternId { bank, pattern };
if !self.audio_state.pending_starts.iter().any(|p| p.id == target) {
self.audio_state.pending_starts.push(PendingPattern {
id: target,
quantization: quant,
sync_mode: sync,
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
});
}
}
}
}
}
fn build_shared_state(&self) -> SharedSequencerState {
SharedSequencerState {
@@ -1412,6 +1341,7 @@ mod tests {
.collect(),
quantization: LaunchQuantization::Immediate,
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)));
}
#[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]
fn test_pattern_start_cancels_pending_stop() {
let mut state = make_state();
@@ -1779,67 +1647,6 @@ mod tests {
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]
fn test_multiple_patterns_independent_quantization() {
let mut state = make_state();
@@ -2100,6 +1907,7 @@ mod tests {
.collect(),
quantization: LaunchQuantization::Immediate,
sync_mode: SyncMode::Reset,
follow_up: FollowUp::default(),
}
}

View File

@@ -3,7 +3,7 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use super::{InputContext, InputResult};
use crate::commands::AppCommand;
use crate::engine::SeqCommand;
use crate::model::PatternSpeed;
use crate::model::{FollowUp, PatternSpeed};
use crate::state::{
ConfirmAction, EditorTarget, EuclideanField, Modal, PatternField,
PatternPropsField, RenameTarget,
@@ -377,21 +377,45 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
speed,
quantization,
sync_mode,
follow_up,
} => {
let (bank, pattern) = (*bank, *pattern);
let is_chain = matches!(follow_up, FollowUp::Chain { .. });
match key.code {
KeyCode::Up => *field = field.prev(),
KeyCode::Down | KeyCode::Tab => *field = field.next(),
KeyCode::Up => *field = field.prev(is_chain),
KeyCode::Down | KeyCode::Tab => *field = field.next(is_chain),
KeyCode::Left => match field {
PatternPropsField::Speed => *speed = speed.prev(),
PatternPropsField::Quantization => *quantization = quantization.prev(),
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 {
PatternPropsField::Speed => *speed = speed.next(),
PatternPropsField::Quantization => *quantization = quantization.next(),
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 {
@@ -418,6 +442,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
let speed_val = *speed;
let quant_val = *quantization;
let sync_val = *sync_mode;
let follow_up_val = *follow_up;
ctx.dispatch(AppCommand::StagePatternProps {
bank,
pattern,
@@ -426,6 +451,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
speed: speed_val,
quantization: quant_val,
sync_mode: sync_val,
follow_up: follow_up_val,
});
ctx.dispatch(AppCommand::CloseModal);
}

View File

@@ -8,7 +8,7 @@ pub use cagire_forth::{
Variables, Word, WordCompile, WORDS,
};
pub use cagire_project::{
load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS,
MAX_PATTERNS,
load, save, Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode,
MAX_BANKS, MAX_PATTERNS,
};
pub use script::ScriptEngine;

View File

@@ -60,7 +60,6 @@ pub fn update_cache(editor_ctx: &EditorContext) {
nudge_secs: 0.0,
cc_access: None,
speed_key: "",
chain_key: "",
mouse_x: 0.5,
mouse_y: 0.5,
mouse_down: 0.0,

View File

@@ -24,26 +24,37 @@ pub enum PatternPropsField {
Speed,
Quantization,
SyncMode,
FollowUp,
ChainBank,
ChainPattern,
}
impl PatternPropsField {
pub fn next(&self) -> Self {
pub fn next(&self, follow_up_is_chain: bool) -> Self {
match self {
Self::Name => Self::Length,
Self::Length => Self::Speed,
Self::Speed => Self::Quantization,
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 {
Self::Name => Self::Name,
Self::Length => Self::Name,
Self::Speed => Self::Length,
Self::Quantization => Self::Speed,
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,
}
}
}

View File

@@ -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::file_browser::FileBrowserState;
@@ -77,6 +77,7 @@ pub enum Modal {
speed: PatternSpeed,
quantization: LaunchQuantization,
sync_mode: SyncMode,
follow_up: FollowUp,
},
KeybindingsHelp {
scroll: usize,

View File

@@ -1,5 +1,5 @@
use crate::engine::PatternChange;
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode};
use std::collections::{HashMap, HashSet};
#[derive(Clone)]
@@ -21,6 +21,7 @@ pub struct StagedPropChange {
pub speed: PatternSpeed,
pub quantization: LaunchQuantization,
pub sync_mode: SyncMode,
pub follow_up: FollowUp,
}
pub struct PlaybackState {

View File

@@ -716,6 +716,8 @@ fn render_properties(
bank: usize,
pattern_idx: usize,
) {
use cagire_project::FollowUp;
let theme = theme::get();
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 value_style = Style::new().fg(theme.ui.text_primary);
let rows: Vec<Line> = vec![
let mut rows: Vec<Line> = vec![
Line::from(vec![
Span::styled(" Name ", label_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);
}

View File

@@ -609,37 +609,45 @@ fn render_modal(
speed,
quantization,
sync_mode,
follow_up,
} => {
use crate::model::FollowUp;
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))
.width(50)
.height(12)
.height(modal_height)
.border_color(theme.modal.input)
.render_centered(frame, term);
let speed_label = speed.label();
let fields: Vec<(&str, &str, bool)> = vec![
("Name", name.as_str(), *field == PatternPropsField::Name),
(
"Length",
length.as_str(),
*field == PatternPropsField::Length,
),
("Speed", &speed_label, *field == PatternPropsField::Speed),
(
"Quantization",
quantization.label(),
*field == PatternPropsField::Quantization,
),
(
"Sync Mode",
sync_mode.label(),
*field == PatternPropsField::SyncMode,
),
let follow_up_label = match follow_up {
FollowUp::Loop => "Loop".to_string(),
FollowUp::Stop => "Stop".to_string(),
FollowUp::Chain { bank: b, pattern: p } => {
format!("Chain B{:02}:P{:02}", b + 1, p + 1)
}
};
let mut fields: Vec<(&str, String, bool)> = vec![
("Name", name.clone(), *field == PatternPropsField::Name),
("Length", length.clone(), *field == PatternPropsField::Length),
("Speed", speed_label, *field == PatternPropsField::Speed),
("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),
];
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 hints = hint_line(&[

View File

@@ -22,7 +22,6 @@ pub fn default_ctx() -> StepContext<'static> {
nudge_secs: 0.0,
cc_access: None,
speed_key: "__speed_0_0__",
chain_key: "__chain_0_0__",
mouse_x: 0.5,
mouse_y: 0.5,
mouse_down: 0.0,