This commit is contained in:
@@ -22,7 +22,7 @@ pub struct ExecutionTrace {
|
|||||||
pub selected_spans: Vec<SourceSpan>,
|
pub selected_spans: Vec<SourceSpan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StepContext {
|
pub struct StepContext<'a> {
|
||||||
pub step: usize,
|
pub step: usize,
|
||||||
pub beat: f64,
|
pub beat: f64,
|
||||||
pub bank: usize,
|
pub bank: usize,
|
||||||
@@ -36,6 +36,8 @@ pub struct StepContext {
|
|||||||
pub fill: bool,
|
pub fill: bool,
|
||||||
pub nudge_secs: f64,
|
pub nudge_secs: f64,
|
||||||
pub cc_access: Option<Arc<dyn CcAccess>>,
|
pub cc_access: Option<Arc<dyn CcAccess>>,
|
||||||
|
pub speed_key: &'a str,
|
||||||
|
pub chain_key: &'a str,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
pub mouse_x: f64,
|
pub mouse_x: f64,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
@@ -44,7 +46,7 @@ pub struct StepContext {
|
|||||||
pub mouse_down: f64,
|
pub mouse_down: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StepContext {
|
impl StepContext<'_> {
|
||||||
pub fn step_duration(&self) -> f64 {
|
pub fn step_duration(&self) -> f64 {
|
||||||
60.0 / self.tempo / 4.0 / self.speed
|
60.0 / self.tempo / 4.0 / self.speed
|
||||||
}
|
}
|
||||||
@@ -138,6 +140,14 @@ pub(super) struct CmdRegister {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CmdRegister {
|
impl CmdRegister {
|
||||||
|
pub(super) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
sound: None,
|
||||||
|
params: Vec::with_capacity(16),
|
||||||
|
deltas: Vec::with_capacity(4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn set_sound(&mut self, val: Value) {
|
pub(super) fn set_sound(&mut self, val: Value) {
|
||||||
self.sound = Some(val);
|
self.sound = Some(val);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ impl Forth {
|
|||||||
trace: Option<&mut ExecutionTrace>,
|
trace: Option<&mut ExecutionTrace>,
|
||||||
) -> Result<Vec<String>, String> {
|
) -> Result<Vec<String>, String> {
|
||||||
let mut stack = self.stack.lock().unwrap();
|
let mut stack = self.stack.lock().unwrap();
|
||||||
let mut outputs: Vec<String> = Vec::new();
|
let mut outputs: Vec<String> = Vec::with_capacity(8);
|
||||||
let mut cmd = CmdRegister::default();
|
let mut cmd = CmdRegister::new();
|
||||||
|
|
||||||
self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?;
|
self.execute_ops(ops, ctx, &mut stack, &mut outputs, &mut cmd, trace)?;
|
||||||
|
|
||||||
@@ -148,8 +148,8 @@ impl Forth {
|
|||||||
return Err("stack underflow".into());
|
return Err("stack underflow".into());
|
||||||
}
|
}
|
||||||
let start = stack.len() - count;
|
let start = stack.len() - count;
|
||||||
let values: Vec<Value> = stack.drain(start..).collect();
|
let selected = stack[start + idx].clone();
|
||||||
let selected = values[idx].clone();
|
stack.truncate(start);
|
||||||
select_and_run(selected, stack, outputs, cmd)
|
select_and_run(selected, stack, outputs, cmd)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -718,11 +718,10 @@ impl Forth {
|
|||||||
Op::SetSpeed => {
|
Op::SetSpeed => {
|
||||||
let speed = stack.pop().ok_or("stack underflow")?.as_float()?;
|
let speed = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
let clamped = speed.clamp(0.125, 8.0);
|
let clamped = speed.clamp(0.125, 8.0);
|
||||||
let key = format!("__speed_{}_{}__", ctx.bank, ctx.pattern);
|
|
||||||
self.vars
|
self.vars
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(key, Value::Float(clamped, None));
|
.insert(ctx.speed_key.to_string(), Value::Float(clamped, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Chain => {
|
Op::Chain => {
|
||||||
@@ -734,9 +733,10 @@ impl Forth {
|
|||||||
if bank as usize == ctx.bank && pattern as usize == ctx.pattern {
|
if bank as usize == ctx.bank && pattern as usize == ctx.pattern {
|
||||||
// chaining to self is a no-op
|
// chaining to self is a no-op
|
||||||
} else {
|
} else {
|
||||||
let key = format!("__chain_{}_{}__", ctx.bank, ctx.pattern);
|
use std::fmt::Write;
|
||||||
let val = format!("{bank}:{pattern}");
|
let mut val = String::with_capacity(8);
|
||||||
self.vars.lock().unwrap().insert(key, Value::Str(Arc::from(val), None));
|
let _ = write!(&mut val, "{bank}:{pattern}");
|
||||||
|
self.vars.lock().unwrap().insert(ctx.chain_key.to_string(), Value::Str(Arc::from(val), None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,36 +1010,57 @@ fn emit_output(
|
|||||||
nudge_secs: f64,
|
nudge_secs: f64,
|
||||||
outputs: &mut Vec<String>,
|
outputs: &mut Vec<String>,
|
||||||
) {
|
) {
|
||||||
let mut pairs: Vec<(String, String)> = if let Some(s) = sound {
|
use std::fmt::Write;
|
||||||
vec![("sound".into(), s.to_string())]
|
let mut out = String::with_capacity(128);
|
||||||
} else {
|
out.push('/');
|
||||||
vec![]
|
|
||||||
};
|
let has_dur = params.iter().any(|(k, _)| k == "dur");
|
||||||
pairs.extend(params.iter().cloned());
|
let delaytime_idx = params.iter().position(|(k, _)| k == "delaytime");
|
||||||
if nudge_secs > 0.0 {
|
|
||||||
pairs.push(("delta".into(), nudge_secs.to_string()));
|
if let Some(s) = sound {
|
||||||
|
let _ = write!(&mut out, "sound/{s}");
|
||||||
}
|
}
|
||||||
// Only add default dur if there's a sound (new voice)
|
|
||||||
if sound.is_some() && !pairs.iter().any(|(k, _)| k == "dur") {
|
for (i, (k, v)) in params.iter().enumerate() {
|
||||||
pairs.push(("dur".into(), step_duration.to_string()));
|
if !out.ends_with('/') {
|
||||||
}
|
out.push('/');
|
||||||
// Only add default delaytime if there's a sound (new voice)
|
|
||||||
if sound.is_some() {
|
|
||||||
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
|
||||||
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
|
||||||
pairs[idx].1 = (ratio * step_duration).to_string();
|
|
||||||
} else {
|
|
||||||
pairs.push(("delaytime".into(), step_duration.to_string()));
|
|
||||||
}
|
}
|
||||||
}
|
if is_tempo_scaled_param(k) {
|
||||||
for pair in &mut pairs {
|
if let Ok(val) = v.parse::<f64>() {
|
||||||
if is_tempo_scaled_param(&pair.0) {
|
let _ = write!(&mut out, "{k}/{}", val * step_duration);
|
||||||
if let Ok(val) = pair.1.parse::<f64>() {
|
continue;
|
||||||
pair.1 = (val * step_duration).to_string();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if Some(i) == delaytime_idx && sound.is_some() {
|
||||||
|
let ratio: f64 = v.parse().unwrap_or(1.0);
|
||||||
|
let _ = write!(&mut out, "{k}/{}", ratio * step_duration);
|
||||||
|
} else {
|
||||||
|
let _ = write!(&mut out, "{k}/{v}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
outputs.push(format_cmd(&pairs));
|
|
||||||
|
if nudge_secs > 0.0 {
|
||||||
|
if !out.ends_with('/') {
|
||||||
|
out.push('/');
|
||||||
|
}
|
||||||
|
let _ = write!(&mut out, "delta/{nudge_secs}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if sound.is_some() && !has_dur {
|
||||||
|
if !out.ends_with('/') {
|
||||||
|
out.push('/');
|
||||||
|
}
|
||||||
|
let _ = write!(&mut out, "dur/{step_duration}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if sound.is_some() && delaytime_idx.is_none() {
|
||||||
|
if !out.ends_with('/') {
|
||||||
|
out.push('/');
|
||||||
|
}
|
||||||
|
let _ = write!(&mut out, "delaytime/{step_duration}");
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.push(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perlin_grad(hash_input: i64) -> f64 {
|
fn perlin_grad(hash_input: i64) -> f64 {
|
||||||
@@ -1116,11 +1137,6 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_cmd(pairs: &[(String, String)]) -> String {
|
|
||||||
let parts: Vec<String> = pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
|
|
||||||
format!("/{}", parts.join("/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> {
|
fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> {
|
||||||
match val {
|
match val {
|
||||||
Value::CycleList(items) if !items.is_empty() => {
|
Value::CycleList(items) if !items.is_empty() => {
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ impl App {
|
|||||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_step_context(&self, step_idx: usize, link: &LinkState) -> StepContext {
|
fn create_step_context(&self, step_idx: usize, link: &LinkState) -> StepContext<'static> {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
let speed = self
|
let speed = self
|
||||||
.project_state
|
.project_state
|
||||||
@@ -288,6 +288,8 @@ impl App {
|
|||||||
fill: false,
|
fill: false,
|
||||||
nudge_secs: 0.0,
|
nudge_secs: 0.0,
|
||||||
cc_access: None,
|
cc_access: None,
|
||||||
|
speed_key: "",
|
||||||
|
chain_key: "",
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mouse_x: 0.5,
|
mouse_x: 0.5,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ pub fn build_stream(
|
|||||||
|
|
||||||
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
|
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
|
||||||
|
|
||||||
|
let mut cmd_buffer = String::with_capacity(256);
|
||||||
|
|
||||||
let stream = device
|
let stream = device
|
||||||
.build_output_stream(
|
.build_output_stream(
|
||||||
&stream_config,
|
&stream_config,
|
||||||
@@ -291,11 +293,16 @@ pub fn build_stream(
|
|||||||
while let Ok(cmd) = audio_rx.try_recv() {
|
while let Ok(cmd) = audio_rx.try_recv() {
|
||||||
match cmd {
|
match cmd {
|
||||||
AudioCommand::Evaluate { cmd, time } => {
|
AudioCommand::Evaluate { cmd, time } => {
|
||||||
let cmd_with_time = match time {
|
let cmd_ref = match time {
|
||||||
Some(t) => format!("{cmd}/time/{t:.6}"),
|
Some(t) => {
|
||||||
None => cmd,
|
cmd_buffer.clear();
|
||||||
|
use std::fmt::Write;
|
||||||
|
let _ = write!(&mut cmd_buffer, "{cmd}/time/{t:.6}");
|
||||||
|
cmd_buffer.as_str()
|
||||||
|
}
|
||||||
|
None => &cmd,
|
||||||
};
|
};
|
||||||
engine.evaluate(&cmd_with_time);
|
engine.evaluate(cmd_ref);
|
||||||
}
|
}
|
||||||
AudioCommand::Hush => {
|
AudioCommand::Hush => {
|
||||||
engine.hush();
|
engine.hush();
|
||||||
|
|||||||
@@ -555,6 +555,8 @@ pub(crate) struct SequencerState {
|
|||||||
speed_overrides: HashMap<(usize, usize), f64>,
|
speed_overrides: HashMap<(usize, usize), f64>,
|
||||||
key_cache: KeyCache,
|
key_cache: KeyCache,
|
||||||
buf_audio_commands: Vec<TimestampedCommand>,
|
buf_audio_commands: Vec<TimestampedCommand>,
|
||||||
|
buf_activated: Vec<PatternId>,
|
||||||
|
buf_stopped: Vec<PatternId>,
|
||||||
cc_access: Option<Arc<dyn CcAccess>>,
|
cc_access: Option<Arc<dyn CcAccess>>,
|
||||||
active_notes: HashMap<(u8, u8, u8), ActiveNote>,
|
active_notes: HashMap<(u8, u8, u8), ActiveNote>,
|
||||||
muted: std::collections::HashSet<(usize, usize)>,
|
muted: std::collections::HashSet<(usize, usize)>,
|
||||||
@@ -580,7 +582,9 @@ impl SequencerState {
|
|||||||
variables,
|
variables,
|
||||||
speed_overrides: HashMap::new(),
|
speed_overrides: HashMap::new(),
|
||||||
key_cache: KeyCache::new(),
|
key_cache: KeyCache::new(),
|
||||||
buf_audio_commands: Vec::new(),
|
buf_audio_commands: Vec::with_capacity(32),
|
||||||
|
buf_activated: Vec::with_capacity(16),
|
||||||
|
buf_stopped: Vec::with_capacity(16),
|
||||||
cc_access,
|
cc_access,
|
||||||
active_notes: HashMap::new(),
|
active_notes: HashMap::new(),
|
||||||
muted: std::collections::HashSet::new(),
|
muted: std::collections::HashSet::new(),
|
||||||
@@ -685,15 +689,8 @@ impl SequencerState {
|
|||||||
let beat = input.beat;
|
let beat = input.beat;
|
||||||
let prev_beat = self.audio_state.prev_beat;
|
let prev_beat = self.audio_state.prev_beat;
|
||||||
|
|
||||||
let activated = self.activate_pending(beat, prev_beat, input.quantum);
|
self.activate_pending(beat, prev_beat, input.quantum);
|
||||||
self.audio_state
|
self.deactivate_pending(beat, prev_beat, input.quantum);
|
||||||
.pending_starts
|
|
||||||
.retain(|p| !activated.contains(&p.id));
|
|
||||||
|
|
||||||
let stopped = self.deactivate_pending(beat, prev_beat, input.quantum);
|
|
||||||
self.audio_state
|
|
||||||
.pending_stops
|
|
||||||
.retain(|p| !stopped.contains(&p.id));
|
|
||||||
|
|
||||||
let steps = self.execute_steps(
|
let steps = self.execute_steps(
|
||||||
beat,
|
beat,
|
||||||
@@ -713,7 +710,7 @@ impl SequencerState {
|
|||||||
input.mouse_down,
|
input.mouse_down,
|
||||||
);
|
);
|
||||||
|
|
||||||
let vars = self.read_variables(&steps.completed_iterations, &stopped, steps.any_step_fired);
|
let vars = self.read_variables(&steps.completed_iterations, steps.any_step_fired);
|
||||||
self.apply_chain_transitions(vars.chain_transitions);
|
self.apply_chain_transitions(vars.chain_transitions);
|
||||||
|
|
||||||
self.audio_state.prev_beat = beat;
|
self.audio_state.prev_beat = beat;
|
||||||
@@ -746,8 +743,8 @@ impl SequencerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) -> Vec<PatternId> {
|
fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||||
let mut activated = Vec::new();
|
self.buf_activated.clear();
|
||||||
for pending in &self.audio_state.pending_starts {
|
for pending in &self.audio_state.pending_starts {
|
||||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||||
let start_step = match pending.sync_mode {
|
let start_step = match pending.sync_mode {
|
||||||
@@ -773,24 +770,30 @@ impl SequencerState {
|
|||||||
iter: 0,
|
iter: 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
activated.push(pending.id);
|
self.buf_activated.push(pending.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activated
|
let activated = &self.buf_activated;
|
||||||
|
self.audio_state
|
||||||
|
.pending_starts
|
||||||
|
.retain(|p| !activated.contains(&p.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) -> Vec<PatternId> {
|
fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
|
||||||
let mut stopped = Vec::new();
|
self.buf_stopped.clear();
|
||||||
for pending in &self.audio_state.pending_stops {
|
for pending in &self.audio_state.pending_stops {
|
||||||
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
|
||||||
self.audio_state.active_patterns.remove(&pending.id);
|
self.audio_state.active_patterns.remove(&pending.id);
|
||||||
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
|
||||||
bank != pending.id.bank || pattern != pending.id.pattern
|
bank != pending.id.bank || pattern != pending.id.pattern
|
||||||
});
|
});
|
||||||
stopped.push(pending.id);
|
self.buf_stopped.push(pending.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stopped
|
let stopped = &self.buf_stopped;
|
||||||
|
self.audio_state
|
||||||
|
.pending_stops
|
||||||
|
.retain(|p| !stopped.contains(&p.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -876,6 +879,8 @@ impl SequencerState {
|
|||||||
fill,
|
fill,
|
||||||
nudge_secs,
|
nudge_secs,
|
||||||
cc_access: self.cc_access.clone(),
|
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),
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mouse_x,
|
mouse_x,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
@@ -931,9 +936,9 @@ impl SequencerState {
|
|||||||
fn read_variables(
|
fn read_variables(
|
||||||
&self,
|
&self,
|
||||||
completed: &[PatternId],
|
completed: &[PatternId],
|
||||||
stopped: &[PatternId],
|
|
||||||
any_step_fired: bool,
|
any_step_fired: bool,
|
||||||
) -> VariableReads {
|
) -> VariableReads {
|
||||||
|
let stopped = &self.buf_stopped;
|
||||||
let needs_access = !completed.is_empty() || !stopped.is_empty() || any_step_fired;
|
let needs_access = !completed.is_empty() || !stopped.is_empty() || any_step_fired;
|
||||||
if !needs_access {
|
if !needs_access {
|
||||||
return VariableReads {
|
return VariableReads {
|
||||||
@@ -1208,10 +1213,18 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
|
|||||||
if !cmd.starts_with("/midi/") {
|
if !cmd.starts_with("/midi/") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let parts: Vec<&str> = cmd.split('/').filter(|s| !s.is_empty()).collect();
|
let mut parts: [&str; 16] = [""; 16];
|
||||||
if parts.len() < 2 {
|
let mut count = 0;
|
||||||
|
for part in cmd.split('/').filter(|s| !s.is_empty()) {
|
||||||
|
if count < 16 {
|
||||||
|
parts[count] = part;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count < 2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let parts = &parts[..count];
|
||||||
|
|
||||||
let find_param = |key: &str| -> Option<&str> {
|
let find_param = |key: &str| -> Option<&str> {
|
||||||
parts
|
parts
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ fn compute_stack_display(
|
|||||||
fill: false,
|
fill: false,
|
||||||
nudge_secs: 0.0,
|
nudge_secs: 0.0,
|
||||||
cc_access: None,
|
cc_access: None,
|
||||||
|
speed_key: "",
|
||||||
|
chain_key: "",
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mouse_x: 0.5,
|
mouse_x: 0.5,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use rand::SeedableRng;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
pub fn default_ctx() -> StepContext {
|
pub fn default_ctx() -> StepContext<'static> {
|
||||||
StepContext {
|
StepContext {
|
||||||
step: 0,
|
step: 0,
|
||||||
beat: 0.0,
|
beat: 0.0,
|
||||||
@@ -19,10 +19,12 @@ pub fn default_ctx() -> StepContext {
|
|||||||
fill: false,
|
fill: false,
|
||||||
nudge_secs: 0.0,
|
nudge_secs: 0.0,
|
||||||
cc_access: None,
|
cc_access: None,
|
||||||
|
speed_key: "__speed_0_0__",
|
||||||
|
chain_key: "__chain_0_0__",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ctx_with(f: impl FnOnce(&mut StepContext)) -> StepContext {
|
pub fn ctx_with(f: impl FnOnce(&mut StepContext<'static>)) -> StepContext<'static> {
|
||||||
let mut ctx = default_ctx();
|
let mut ctx = default_ctx();
|
||||||
f(&mut ctx);
|
f(&mut ctx);
|
||||||
ctx
|
ctx
|
||||||
|
|||||||
Reference in New Issue
Block a user