vastly improved selection system
This commit is contained in:
@@ -89,4 +89,5 @@ pub enum Op {
|
|||||||
StackStart,
|
StackStart,
|
||||||
EmitN,
|
EmitN,
|
||||||
ClearCmd,
|
ClearCmd,
|
||||||
|
SetSpeed,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -625,6 +625,16 @@ impl Forth {
|
|||||||
.insert("__tempo__".to_string(), Value::Float(clamped, None));
|
.insert("__tempo__".to_string(), Value::Float(clamped, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Op::SetSpeed => {
|
||||||
|
let speed = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let clamped = speed.clamp(0.125, 8.0);
|
||||||
|
let key = format!("__speed_{}_{}__", ctx.bank, ctx.pattern);
|
||||||
|
self.vars
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(key, Value::Float(clamped, None));
|
||||||
|
}
|
||||||
|
|
||||||
Op::Chain => {
|
Op::Chain => {
|
||||||
let pattern = stack.pop().ok_or("stack underflow")?.as_int()? - 1;
|
let pattern = stack.pop().ok_or("stack underflow")?.as_int()? - 1;
|
||||||
let bank = stack.pop().ok_or("stack underflow")?.as_int()? - 1;
|
let bank = stack.pop().ok_or("stack underflow")?.as_int()? - 1;
|
||||||
|
|||||||
@@ -755,6 +755,14 @@ pub const WORDS: &[Word] = &[
|
|||||||
example: "140 tempo!",
|
example: "140 tempo!",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "speed!",
|
||||||
|
category: "Time",
|
||||||
|
stack: "(multiplier --)",
|
||||||
|
desc: "Set pattern speed multiplier",
|
||||||
|
example: "2.0 speed!",
|
||||||
|
compile: Simple,
|
||||||
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "chain",
|
name: "chain",
|
||||||
category: "Time",
|
category: "Time",
|
||||||
@@ -1909,6 +1917,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"_" => Op::Silence,
|
"_" => Op::Silence,
|
||||||
"scale!" => Op::Scale,
|
"scale!" => Op::Scale,
|
||||||
"tempo!" => Op::SetTempo,
|
"tempo!" => Op::SetTempo,
|
||||||
|
"speed!" => Op::SetSpeed,
|
||||||
"[" => Op::ListStart,
|
"[" => Op::ListStart,
|
||||||
"]" => Op::ListEnd,
|
"]" => Op::ListEnd,
|
||||||
">" => Op::ListEndCycle,
|
">" => Op::ListEndCycle,
|
||||||
|
|||||||
387
src/app.rs
387
src/app.rs
@@ -187,15 +187,21 @@ impl App {
|
|||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_step(&mut self) {
|
pub fn toggle_steps(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
let change = pattern_editor::toggle_step(
|
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
|
||||||
&mut self.project_state.project,
|
Some(range) => range.collect(),
|
||||||
bank,
|
None => vec![self.editor_ctx.step],
|
||||||
pattern,
|
};
|
||||||
self.editor_ctx.step,
|
for idx in indices {
|
||||||
);
|
pattern_editor::toggle_step(
|
||||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
&mut self.project_state.project,
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
idx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn length_increase(&mut self) {
|
pub fn length_increase(&mut self) {
|
||||||
@@ -517,26 +523,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_step(&mut self) {
|
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
|
||||||
let step = self.editor_ctx.step;
|
|
||||||
let script =
|
|
||||||
pattern_editor::get_step_script(&self.project_state.project, bank, pattern, step);
|
|
||||||
|
|
||||||
if let Some(script) = script {
|
|
||||||
if let Some(clip) = &mut self.clipboard {
|
|
||||||
if clip.set_text(&script).is_ok() {
|
|
||||||
self.editor_ctx.copied_step = Some(crate::state::CopiedStep {
|
|
||||||
bank,
|
|
||||||
pattern,
|
|
||||||
step,
|
|
||||||
});
|
|
||||||
self.ui.set_status("Copied".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_step(&mut self, bank: usize, pattern: usize, step: usize) {
|
pub fn delete_step(&mut self, bank: usize, pattern: usize, step: usize) {
|
||||||
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
||||||
for s in &mut pat.steps {
|
for s in &mut pat.steps {
|
||||||
@@ -573,6 +559,46 @@ impl App {
|
|||||||
self.ui.flash("Step deleted", 150, FlashKind::Success);
|
self.ui.flash("Step deleted", 150, FlashKind::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete_steps(&mut self, bank: usize, pattern: usize, steps: &[usize]) {
|
||||||
|
for &step in steps {
|
||||||
|
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
||||||
|
for s in &mut pat.steps {
|
||||||
|
if s.source == Some(step) {
|
||||||
|
s.source = None;
|
||||||
|
s.script.clear();
|
||||||
|
s.command = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let change = pattern_editor::set_step_script(
|
||||||
|
&mut self.project_state.project,
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
step,
|
||||||
|
String::new(),
|
||||||
|
);
|
||||||
|
if let Some(s) = self
|
||||||
|
.project_state
|
||||||
|
.project
|
||||||
|
.pattern_at_mut(bank, pattern)
|
||||||
|
.step_mut(step)
|
||||||
|
{
|
||||||
|
s.command = None;
|
||||||
|
s.source = None;
|
||||||
|
}
|
||||||
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||||
|
}
|
||||||
|
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||||
|
self.load_step_to_editor();
|
||||||
|
}
|
||||||
|
self.editor_ctx.clear_selection();
|
||||||
|
self.ui.flash(
|
||||||
|
&format!("{} steps deleted", steps.len()),
|
||||||
|
150,
|
||||||
|
FlashKind::Success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reset_pattern(&mut self, bank: usize, pattern: usize) {
|
pub fn reset_pattern(&mut self, bank: usize, pattern: usize) {
|
||||||
self.project_state.project.banks[bank].patterns[pattern] = Pattern::default();
|
self.project_state.project.banks[bank].patterns[pattern] = Pattern::default();
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
@@ -641,108 +667,235 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paste_step(&mut self, link: &LinkState) {
|
pub fn harden_steps(&mut self) {
|
||||||
let text = self
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
.clipboard
|
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
|
||||||
.as_mut()
|
Some(range) => range.collect(),
|
||||||
.and_then(|clip| clip.get_text().ok());
|
None => vec![self.editor_ctx.step],
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(text) = text {
|
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let resolutions: Vec<(usize, String)> = indices
|
||||||
let change = pattern_editor::set_step_script(
|
.iter()
|
||||||
&mut self.project_state.project,
|
.filter_map(|&idx| {
|
||||||
bank,
|
let step = pat.step(idx)?;
|
||||||
pattern,
|
step.source?;
|
||||||
self.editor_ctx.step,
|
let script = pat.resolve_script(idx)?.to_string();
|
||||||
text,
|
Some((idx, script))
|
||||||
);
|
})
|
||||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
.collect();
|
||||||
self.load_step_to_editor();
|
|
||||||
self.compile_current_step(link);
|
if resolutions.is_empty() {
|
||||||
|
self.ui.set_status("No linked steps to harden".to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = resolutions.len();
|
||||||
|
for (idx, script) in resolutions {
|
||||||
|
if let Some(s) = self
|
||||||
|
.project_state
|
||||||
|
.project
|
||||||
|
.pattern_at_mut(bank, pattern)
|
||||||
|
.step_mut(idx)
|
||||||
|
{
|
||||||
|
s.source = None;
|
||||||
|
s.script = script;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
|
self.load_step_to_editor();
|
||||||
|
self.editor_ctx.clear_selection();
|
||||||
|
if count == 1 {
|
||||||
|
self.ui.flash("Step hardened", 150, FlashKind::Success);
|
||||||
|
} else {
|
||||||
|
self.ui.flash(&format!("{count} steps hardened"), 150, FlashKind::Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn link_paste_step(&mut self) {
|
pub fn copy_steps(&mut self) {
|
||||||
let Some(copied) = self.editor_ctx.copied_step else {
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
|
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||||
|
|
||||||
|
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
|
||||||
|
Some(range) => range.collect(),
|
||||||
|
None => vec![self.editor_ctx.step],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut steps = Vec::new();
|
||||||
|
let mut scripts = Vec::new();
|
||||||
|
for &idx in &indices {
|
||||||
|
if let Some(step) = pat.step(idx) {
|
||||||
|
let resolved = pat.resolve_script(idx).unwrap_or("").to_string();
|
||||||
|
scripts.push(resolved.clone());
|
||||||
|
steps.push(crate::state::CopiedStepData {
|
||||||
|
script: resolved,
|
||||||
|
active: step.active,
|
||||||
|
source: step.source,
|
||||||
|
original_index: idx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = steps.len();
|
||||||
|
self.editor_ctx.copied_steps = Some(crate::state::CopiedSteps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
steps,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(clip) = &mut self.clipboard {
|
||||||
|
let _ = clip.set_text(scripts.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ui.flash(&format!("Copied {count} steps"), 150, FlashKind::Info);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste_steps(&mut self, link: &LinkState) {
|
||||||
|
let Some(copied) = self.editor_ctx.copied_steps.clone() else {
|
||||||
self.ui.set_status("Nothing copied".to_string());
|
self.ui.set_status("Nothing copied".to_string());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
let step = self.editor_ctx.step;
|
let pat_len = self.project_state.project.pattern_at(bank, pattern).length;
|
||||||
|
let cursor = self.editor_ctx.step;
|
||||||
|
|
||||||
if copied.bank != bank || copied.pattern != pattern {
|
let same_pattern = copied.bank == bank && copied.pattern == pattern;
|
||||||
self.ui
|
for (i, data) in copied.steps.iter().enumerate() {
|
||||||
.set_status("Can only link within same pattern".to_string());
|
let target = cursor + i;
|
||||||
return;
|
if target >= pat_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) {
|
||||||
|
let source = if same_pattern { data.source } else { None };
|
||||||
|
step.active = data.active;
|
||||||
|
step.source = source;
|
||||||
|
if source.is_some() {
|
||||||
|
step.script.clear();
|
||||||
|
step.command = None;
|
||||||
|
} else {
|
||||||
|
step.script = data.script.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if copied.step == step {
|
|
||||||
self.ui.set_status("Cannot link step to itself".to_string());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let source_step = self
|
|
||||||
.project_state
|
|
||||||
.project
|
|
||||||
.pattern_at(bank, pattern)
|
|
||||||
.step(copied.step);
|
|
||||||
if source_step.map(|s| s.source.is_some()).unwrap_or(false) {
|
|
||||||
self.ui
|
|
||||||
.set_status("Cannot link to a linked step".to_string());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(s) = self
|
|
||||||
.project_state
|
|
||||||
.project
|
|
||||||
.pattern_at_mut(bank, pattern)
|
|
||||||
.step_mut(step)
|
|
||||||
{
|
|
||||||
s.source = Some(copied.step);
|
|
||||||
s.script.clear();
|
|
||||||
s.command = None;
|
|
||||||
}
|
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
self.ui.flash(
|
|
||||||
&format!("Linked to step {:02}", copied.step + 1),
|
// Compile affected steps
|
||||||
150,
|
for i in 0..copied.steps.len() {
|
||||||
FlashKind::Success,
|
let target = cursor + i;
|
||||||
);
|
if target >= pat_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let saved_step = self.editor_ctx.step;
|
||||||
|
self.editor_ctx.step = target;
|
||||||
|
self.compile_current_step(link);
|
||||||
|
self.editor_ctx.step = saved_step;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.editor_ctx.clear_selection();
|
||||||
|
self.ui.flash(&format!("Pasted {} steps", copied.steps.len()), 150, FlashKind::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn harden_step(&mut self) {
|
pub fn link_paste_steps(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let Some(copied) = self.editor_ctx.copied_steps.clone() else {
|
||||||
let step = self.editor_ctx.step;
|
self.ui.set_status("Nothing copied".to_string());
|
||||||
|
|
||||||
let resolved_script = self
|
|
||||||
.project_state
|
|
||||||
.project
|
|
||||||
.pattern_at(bank, pattern)
|
|
||||||
.resolve_script(step)
|
|
||||||
.map(|s| s.to_string());
|
|
||||||
|
|
||||||
let Some(script) = resolved_script else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(s) = self
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
.project_state
|
|
||||||
.project
|
if copied.bank != bank || copied.pattern != pattern {
|
||||||
.pattern_at_mut(bank, pattern)
|
self.ui.set_status("Can only link within same pattern".to_string());
|
||||||
.step_mut(step)
|
return;
|
||||||
{
|
|
||||||
if s.source.is_none() {
|
|
||||||
self.ui.set_status("Step is not linked".to_string());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
s.source = None;
|
|
||||||
s.script = script;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pat_len = self.project_state.project.pattern_at(bank, pattern).length;
|
||||||
|
let cursor = self.editor_ctx.step;
|
||||||
|
|
||||||
|
for (i, data) in copied.steps.iter().enumerate() {
|
||||||
|
let target = cursor + i;
|
||||||
|
if target >= pat_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let source_idx = if data.source.is_some() {
|
||||||
|
// Original was linked, link to same source
|
||||||
|
data.source
|
||||||
|
} else {
|
||||||
|
Some(data.original_index)
|
||||||
|
};
|
||||||
|
if source_idx == Some(target) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) {
|
||||||
|
step.source = source_idx;
|
||||||
|
step.script.clear();
|
||||||
|
step.command = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
self.ui.flash("Step hardened", 150, FlashKind::Success);
|
self.editor_ctx.clear_selection();
|
||||||
|
self.ui.flash(&format!("Linked {} steps", copied.steps.len()), 150, FlashKind::Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn duplicate_steps(&mut self, link: &LinkState) {
|
||||||
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
|
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||||
|
let pat_len = pat.length;
|
||||||
|
let indices: Vec<usize> = match self.editor_ctx.selection_range() {
|
||||||
|
Some(range) => range.collect(),
|
||||||
|
None => vec![self.editor_ctx.step],
|
||||||
|
};
|
||||||
|
let count = indices.len();
|
||||||
|
let paste_at = *indices.last().unwrap() + 1;
|
||||||
|
|
||||||
|
let dupe_data: Vec<(bool, String, Option<usize>)> = indices
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&idx| {
|
||||||
|
let step = pat.step(idx)?;
|
||||||
|
let script = pat.resolve_script(idx).unwrap_or("").to_string();
|
||||||
|
let source = step.source;
|
||||||
|
Some((step.active, script, source))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut pasted = 0;
|
||||||
|
for (i, (active, script, source)) in dupe_data.into_iter().enumerate() {
|
||||||
|
let target = paste_at + i;
|
||||||
|
if target >= pat_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(step) = self.project_state.project.pattern_at_mut(bank, pattern).step_mut(target) {
|
||||||
|
step.active = active;
|
||||||
|
step.source = source;
|
||||||
|
if source.is_some() {
|
||||||
|
step.script.clear();
|
||||||
|
step.command = None;
|
||||||
|
} else {
|
||||||
|
step.script = script;
|
||||||
|
step.command = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pasted += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
|
self.load_step_to_editor();
|
||||||
|
|
||||||
|
for i in 0..pasted {
|
||||||
|
let target = paste_at + i;
|
||||||
|
let saved = self.editor_ctx.step;
|
||||||
|
self.editor_ctx.step = target;
|
||||||
|
self.compile_current_step(link);
|
||||||
|
self.editor_ctx.step = saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.editor_ctx.clear_selection();
|
||||||
|
self.ui.flash(&format!("Duplicated {count} steps"), 150, FlashKind::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_pattern_modal(&mut self, field: PatternField) {
|
pub fn open_pattern_modal(&mut self, field: PatternField) {
|
||||||
@@ -787,7 +940,7 @@ impl App {
|
|||||||
AppCommand::SelectEditPattern(pattern) => self.select_edit_pattern(pattern),
|
AppCommand::SelectEditPattern(pattern) => self.select_edit_pattern(pattern),
|
||||||
|
|
||||||
// Pattern editing
|
// Pattern editing
|
||||||
AppCommand::ToggleStep => self.toggle_step(),
|
AppCommand::ToggleSteps => self.toggle_steps(),
|
||||||
AppCommand::LengthIncrease => self.length_increase(),
|
AppCommand::LengthIncrease => self.length_increase(),
|
||||||
AppCommand::LengthDecrease => self.length_decrease(),
|
AppCommand::LengthDecrease => self.length_decrease(),
|
||||||
AppCommand::SpeedIncrease => self.speed_increase(),
|
AppCommand::SpeedIncrease => self.speed_increase(),
|
||||||
@@ -836,6 +989,13 @@ impl App {
|
|||||||
} => {
|
} => {
|
||||||
self.delete_step(bank, pattern, step);
|
self.delete_step(bank, pattern, step);
|
||||||
}
|
}
|
||||||
|
AppCommand::DeleteSteps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
steps,
|
||||||
|
} => {
|
||||||
|
self.delete_steps(bank, pattern, &steps);
|
||||||
|
}
|
||||||
AppCommand::ResetPattern { bank, pattern } => {
|
AppCommand::ResetPattern { bank, pattern } => {
|
||||||
self.reset_pattern(bank, pattern);
|
self.reset_pattern(bank, pattern);
|
||||||
}
|
}
|
||||||
@@ -856,10 +1016,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
AppCommand::CopyStep => self.copy_step(),
|
AppCommand::HardenSteps => self.harden_steps(),
|
||||||
AppCommand::PasteStep => self.paste_step(link),
|
AppCommand::CopySteps => self.copy_steps(),
|
||||||
AppCommand::LinkPasteStep => self.link_paste_step(),
|
AppCommand::PasteSteps => self.paste_steps(link),
|
||||||
AppCommand::HardenStep => self.harden_step(),
|
AppCommand::LinkPasteSteps => self.link_paste_steps(),
|
||||||
|
AppCommand::DuplicateSteps => self.duplicate_steps(link),
|
||||||
|
|
||||||
// Pattern playback (staging)
|
// Pattern playback (staging)
|
||||||
AppCommand::StagePatternToggle { bank, pattern } => {
|
AppCommand::StagePatternToggle { bank, pattern } => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub enum AppCommand {
|
|||||||
SelectEditPattern(usize),
|
SelectEditPattern(usize),
|
||||||
|
|
||||||
// Pattern editing
|
// Pattern editing
|
||||||
ToggleStep,
|
ToggleSteps,
|
||||||
LengthIncrease,
|
LengthIncrease,
|
||||||
LengthDecrease,
|
LengthDecrease,
|
||||||
SpeedIncrease,
|
SpeedIncrease,
|
||||||
@@ -45,6 +45,11 @@ pub enum AppCommand {
|
|||||||
pattern: usize,
|
pattern: usize,
|
||||||
step: usize,
|
step: usize,
|
||||||
},
|
},
|
||||||
|
DeleteSteps {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
steps: Vec<usize>,
|
||||||
|
},
|
||||||
ResetPattern {
|
ResetPattern {
|
||||||
bank: usize,
|
bank: usize,
|
||||||
pattern: usize,
|
pattern: usize,
|
||||||
@@ -68,10 +73,11 @@ pub enum AppCommand {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
CopyStep,
|
HardenSteps,
|
||||||
PasteStep,
|
CopySteps,
|
||||||
LinkPasteStep,
|
PasteSteps,
|
||||||
HardenStep,
|
LinkPasteSteps,
|
||||||
|
DuplicateSteps,
|
||||||
|
|
||||||
// Pattern playback (staging)
|
// Pattern playback (staging)
|
||||||
StagePatternToggle {
|
StagePatternToggle {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
145
src/input.rs
145
src/input.rs
@@ -7,7 +7,7 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
use crate::engine::{AudioCommand, LinkState, SequencerSnapshot};
|
use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
|
||||||
use crate::model::PatternSpeed;
|
use crate::model::PatternSpeed;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
@@ -26,6 +26,7 @@ pub struct InputContext<'a> {
|
|||||||
pub snapshot: &'a SequencerSnapshot,
|
pub snapshot: &'a SequencerSnapshot,
|
||||||
pub playing: &'a Arc<AtomicBool>,
|
pub playing: &'a Arc<AtomicBool>,
|
||||||
pub audio_tx: &'a ArcSwap<Sender<AudioCommand>>,
|
pub audio_tx: &'a ArcSwap<Sender<AudioCommand>>,
|
||||||
|
pub seq_cmd_tx: &'a Sender<SeqCommand>,
|
||||||
pub nudge_us: &'a Arc<AtomicI64>,
|
pub nudge_us: &'a Arc<AtomicI64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +141,49 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Modal::ConfirmDeleteSteps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
steps,
|
||||||
|
selected: _,
|
||||||
|
} => {
|
||||||
|
let (bank, pattern, steps) = (*bank, *pattern, steps.clone());
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||||
|
ctx.dispatch(AppCommand::DeleteSteps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
steps,
|
||||||
|
});
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
KeyCode::Left | KeyCode::Right => {
|
||||||
|
if let Modal::ConfirmDeleteSteps { selected, .. } = &mut ctx.app.ui.modal {
|
||||||
|
*selected = !*selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
let do_delete =
|
||||||
|
if let Modal::ConfirmDeleteSteps { selected, .. } = &ctx.app.ui.modal {
|
||||||
|
*selected
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if do_delete {
|
||||||
|
ctx.dispatch(AppCommand::DeleteSteps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
steps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Modal::ConfirmResetPattern {
|
Modal::ConfirmResetPattern {
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
@@ -650,6 +694,8 @@ fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult {
|
fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult {
|
||||||
|
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
if ctx.app.panel.visible {
|
if ctx.app.panel.visible {
|
||||||
@@ -674,12 +720,54 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
|||||||
ctx.playing
|
ctx.playing
|
||||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
KeyCode::Left => ctx.dispatch(AppCommand::PrevStep),
|
KeyCode::Left if shift && !ctrl => {
|
||||||
KeyCode::Right => ctx.dispatch(AppCommand::NextStep),
|
if ctx.app.editor_ctx.selection_anchor.is_none() {
|
||||||
KeyCode::Up => ctx.dispatch(AppCommand::StepUp),
|
ctx.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
|
||||||
KeyCode::Down => ctx.dispatch(AppCommand::StepDown),
|
}
|
||||||
KeyCode::Enter => ctx.dispatch(AppCommand::OpenModal(Modal::Editor)),
|
ctx.dispatch(AppCommand::PrevStep);
|
||||||
KeyCode::Char('t') => ctx.dispatch(AppCommand::ToggleStep),
|
}
|
||||||
|
KeyCode::Right if shift && !ctrl => {
|
||||||
|
if ctx.app.editor_ctx.selection_anchor.is_none() {
|
||||||
|
ctx.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
|
||||||
|
}
|
||||||
|
ctx.dispatch(AppCommand::NextStep);
|
||||||
|
}
|
||||||
|
KeyCode::Up if shift && !ctrl => {
|
||||||
|
if ctx.app.editor_ctx.selection_anchor.is_none() {
|
||||||
|
ctx.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
|
||||||
|
}
|
||||||
|
ctx.dispatch(AppCommand::StepUp);
|
||||||
|
}
|
||||||
|
KeyCode::Down if shift && !ctrl => {
|
||||||
|
if ctx.app.editor_ctx.selection_anchor.is_none() {
|
||||||
|
ctx.app.editor_ctx.selection_anchor = Some(ctx.app.editor_ctx.step);
|
||||||
|
}
|
||||||
|
ctx.dispatch(AppCommand::StepDown);
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
ctx.app.editor_ctx.clear_selection();
|
||||||
|
ctx.dispatch(AppCommand::PrevStep);
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
ctx.app.editor_ctx.clear_selection();
|
||||||
|
ctx.dispatch(AppCommand::NextStep);
|
||||||
|
}
|
||||||
|
KeyCode::Up => {
|
||||||
|
ctx.app.editor_ctx.clear_selection();
|
||||||
|
ctx.dispatch(AppCommand::StepUp);
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
ctx.app.editor_ctx.clear_selection();
|
||||||
|
ctx.dispatch(AppCommand::StepDown);
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
ctx.app.editor_ctx.clear_selection();
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
ctx.app.editor_ctx.clear_selection();
|
||||||
|
ctx.dispatch(AppCommand::OpenModal(Modal::Editor));
|
||||||
|
}
|
||||||
|
KeyCode::Char('t') => ctx.dispatch(AppCommand::ToggleSteps),
|
||||||
KeyCode::Char('s') => {
|
KeyCode::Char('s') => {
|
||||||
use crate::state::file_browser::FileBrowserState;
|
use crate::state::file_browser::FileBrowserState;
|
||||||
let initial = ctx
|
let initial = ctx
|
||||||
@@ -692,10 +780,19 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
|||||||
let state = FileBrowserState::new_save(initial);
|
let state = FileBrowserState::new_save(initial);
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(state)));
|
ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(state)));
|
||||||
}
|
}
|
||||||
KeyCode::Char('c') if ctrl => ctx.dispatch(AppCommand::CopyStep),
|
KeyCode::Char('c') if ctrl => {
|
||||||
KeyCode::Char('v') if ctrl => ctx.dispatch(AppCommand::PasteStep),
|
ctx.dispatch(AppCommand::CopySteps);
|
||||||
KeyCode::Char('b') if ctrl => ctx.dispatch(AppCommand::LinkPasteStep),
|
}
|
||||||
KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenStep),
|
KeyCode::Char('v') if ctrl => {
|
||||||
|
ctx.dispatch(AppCommand::PasteSteps);
|
||||||
|
}
|
||||||
|
KeyCode::Char('b') if ctrl => {
|
||||||
|
ctx.dispatch(AppCommand::LinkPasteSteps);
|
||||||
|
}
|
||||||
|
KeyCode::Char('d') if ctrl => {
|
||||||
|
ctx.dispatch(AppCommand::DuplicateSteps);
|
||||||
|
}
|
||||||
|
KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenSteps),
|
||||||
KeyCode::Char('l') => {
|
KeyCode::Char('l') => {
|
||||||
use crate::state::file_browser::FileBrowserState;
|
use crate::state::file_browser::FileBrowserState;
|
||||||
let default_dir = ctx
|
let default_dir = ctx
|
||||||
@@ -730,13 +827,23 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
|||||||
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenModal(Modal::Preview)),
|
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenModal(Modal::Preview)),
|
||||||
KeyCode::Delete | KeyCode::Backspace => {
|
KeyCode::Delete | KeyCode::Backspace => {
|
||||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||||
let step = ctx.app.editor_ctx.step;
|
if let Some(range) = ctx.app.editor_ctx.selection_range() {
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmDeleteStep {
|
let steps: Vec<usize> = range.collect();
|
||||||
bank,
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmDeleteSteps {
|
||||||
pattern,
|
bank,
|
||||||
step,
|
pattern,
|
||||||
selected: false,
|
steps,
|
||||||
}));
|
selected: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
let step = ctx.app.editor_ctx.step;
|
||||||
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmDeleteStep {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
step,
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -987,9 +1094,11 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
}
|
}
|
||||||
KeyCode::Char('h') => {
|
KeyCode::Char('h') => {
|
||||||
let _ = ctx.audio_tx.load().send(AudioCommand::Hush);
|
let _ = ctx.audio_tx.load().send(AudioCommand::Hush);
|
||||||
|
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||||
}
|
}
|
||||||
KeyCode::Char('p') => {
|
KeyCode::Char('p') => {
|
||||||
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
|
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
|
||||||
|
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||||
}
|
}
|
||||||
KeyCode::Char('r') => ctx.app.metrics.peak_voices = 0,
|
KeyCode::Char('r') => ctx.app.metrics.peak_voices = 0,
|
||||||
KeyCode::Char('t') => {
|
KeyCode::Char('t') => {
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ fn main() -> io::Result<()> {
|
|||||||
snapshot: &seq_snapshot,
|
snapshot: &seq_snapshot,
|
||||||
playing: &playing,
|
playing: &playing,
|
||||||
audio_tx: &sequencer.audio_tx,
|
audio_tx: &sequencer.audio_tx,
|
||||||
|
seq_cmd_tx: &sequencer.cmd_tx,
|
||||||
nudge_us: &nudge_us,
|
nudge_us: &nudge_us,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use cagire_ratatui::Editor;
|
use cagire_ratatui::Editor;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -50,14 +52,36 @@ pub struct EditorContext {
|
|||||||
pub step: usize,
|
pub step: usize,
|
||||||
pub focus: Focus,
|
pub focus: Focus,
|
||||||
pub editor: Editor,
|
pub editor: Editor,
|
||||||
pub copied_step: Option<CopiedStep>,
|
pub selection_anchor: Option<usize>,
|
||||||
|
pub copied_steps: Option<CopiedSteps>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone)]
|
||||||
pub struct CopiedStep {
|
pub struct CopiedSteps {
|
||||||
pub bank: usize,
|
pub bank: usize,
|
||||||
pub pattern: usize,
|
pub pattern: usize,
|
||||||
pub step: usize,
|
pub steps: Vec<CopiedStepData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CopiedStepData {
|
||||||
|
pub script: String,
|
||||||
|
pub active: bool,
|
||||||
|
pub source: Option<usize>,
|
||||||
|
pub original_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditorContext {
|
||||||
|
pub fn selection_range(&self) -> Option<RangeInclusive<usize>> {
|
||||||
|
let anchor = self.selection_anchor?;
|
||||||
|
let a = anchor.min(self.step);
|
||||||
|
let b = anchor.max(self.step);
|
||||||
|
Some(a..=b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_selection(&mut self) {
|
||||||
|
self.selection_anchor = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EditorContext {
|
impl Default for EditorContext {
|
||||||
@@ -68,7 +92,8 @@ impl Default for EditorContext {
|
|||||||
step: 0,
|
step: 0,
|
||||||
focus: Focus::Sequencer,
|
focus: Focus::Sequencer,
|
||||||
editor: Editor::new(),
|
editor: Editor::new(),
|
||||||
copied_step: None,
|
selection_anchor: None,
|
||||||
|
copied_steps: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub mod ui;
|
|||||||
|
|
||||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
|
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
|
||||||
pub use options::{OptionsFocus, OptionsState};
|
pub use options::{OptionsFocus, OptionsState};
|
||||||
pub use editor::{CopiedStep, EditorContext, Focus, PatternField, PatternPropsField};
|
pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField};
|
||||||
pub use live_keys::LiveKeyState;
|
pub use live_keys::LiveKeyState;
|
||||||
pub use modal::Modal;
|
pub use modal::Modal;
|
||||||
pub use panel::{PanelFocus, PanelState, SidePanel};
|
pub use panel::{PanelFocus, PanelState, SidePanel};
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ pub enum Modal {
|
|||||||
step: usize,
|
step: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
},
|
},
|
||||||
|
ConfirmDeleteSteps {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
steps: Vec<usize>,
|
||||||
|
selected: bool,
|
||||||
|
},
|
||||||
ConfirmResetPattern {
|
ConfirmResetPattern {
|
||||||
bank: usize,
|
bank: usize,
|
||||||
pattern: usize,
|
pattern: usize,
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ fn render_tile(
|
|||||||
let is_active = step.map(|s| s.active).unwrap_or(false);
|
let is_active = step.map(|s| s.active).unwrap_or(false);
|
||||||
let is_linked = step.map(|s| s.source.is_some()).unwrap_or(false);
|
let is_linked = step.map(|s| s.source.is_some()).unwrap_or(false);
|
||||||
let is_selected = step_idx == app.editor_ctx.step;
|
let is_selected = step_idx == app.editor_ctx.step;
|
||||||
|
let in_selection = app.editor_ctx.selection_range()
|
||||||
|
.map(|r| r.contains(&step_idx))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let is_playing = if app.playback.playing {
|
let is_playing = if app.playback.playing {
|
||||||
snapshot.get_step(app.editor_ctx.bank, app.editor_ctx.pattern) == Some(step_idx)
|
snapshot.get_step(app.editor_ctx.bank, app.editor_ctx.pattern) == Some(step_idx)
|
||||||
@@ -145,21 +148,23 @@ fn render_tile(
|
|||||||
(BRIGHT[i], DIM[i])
|
(BRIGHT[i], DIM[i])
|
||||||
});
|
});
|
||||||
|
|
||||||
let (bg, fg) = match (is_playing, is_active, is_selected, is_linked) {
|
let (bg, fg) = match (is_playing, is_active, is_selected, is_linked, in_selection) {
|
||||||
(true, true, _, _) => (Color::Rgb(195, 85, 65), Color::White),
|
(true, true, _, _, _) => (Color::Rgb(195, 85, 65), Color::White),
|
||||||
(true, false, _, _) => (Color::Rgb(180, 120, 45), Color::Black),
|
(true, false, _, _, _) => (Color::Rgb(180, 120, 45), Color::Black),
|
||||||
(false, true, true, true) => {
|
(false, true, true, true, _) => {
|
||||||
let (r, g, b) = link_color.unwrap().0;
|
let (r, g, b) = link_color.unwrap().0;
|
||||||
(Color::Rgb(r, g, b), Color::Black)
|
(Color::Rgb(r, g, b), Color::Black)
|
||||||
}
|
}
|
||||||
(false, true, true, false) => (Color::Rgb(0, 220, 180), Color::Black),
|
(false, true, true, false, _) => (Color::Rgb(0, 220, 180), Color::Black),
|
||||||
(false, true, false, true) => {
|
(false, true, _, _, true) => (Color::Rgb(0, 170, 140), Color::Black),
|
||||||
|
(false, true, false, true, _) => {
|
||||||
let (r, g, b) = link_color.unwrap().1;
|
let (r, g, b) = link_color.unwrap().1;
|
||||||
(Color::Rgb(r, g, b), Color::White)
|
(Color::Rgb(r, g, b), Color::White)
|
||||||
}
|
}
|
||||||
(false, true, false, false) => (Color::Rgb(45, 106, 95), Color::White),
|
(false, true, false, false, _) => (Color::Rgb(45, 106, 95), Color::White),
|
||||||
(false, false, true, _) => (Color::Rgb(80, 180, 255), Color::Black),
|
(false, false, true, _, _) => (Color::Rgb(80, 180, 255), Color::Black),
|
||||||
(false, false, false, _) => (Color::Rgb(45, 48, 55), Color::Rgb(120, 125, 135)),
|
(false, false, _, _, true) => (Color::Rgb(60, 140, 200), Color::Black),
|
||||||
|
(false, false, false, _, _) => (Color::Rgb(45, 48, 55), Color::Rgb(120, 125, 135)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let symbol = if is_playing {
|
let symbol = if is_playing {
|
||||||
|
|||||||
@@ -295,13 +295,20 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
} else {
|
} else {
|
||||||
let bindings: Vec<(&str, &str)> = match app.page {
|
let bindings: Vec<(&str, &str)> = match app.page {
|
||||||
Page::Main => vec![
|
Page::Main => vec![
|
||||||
("←→↑↓", "Navigate"),
|
("←→↑↓", "Nav"),
|
||||||
|
("Shift+↑↓", "Select"),
|
||||||
("t", "Toggle"),
|
("t", "Toggle"),
|
||||||
("Enter", "Edit"),
|
("Enter", "Edit"),
|
||||||
("p", "Preview"),
|
|
||||||
("Space", "Play"),
|
("Space", "Play"),
|
||||||
("<>", "Length"),
|
("^C", "Copy"),
|
||||||
("[]", "Speed"),
|
("^V", "Paste"),
|
||||||
|
("^B", "Link"),
|
||||||
|
("^D", "Dup"),
|
||||||
|
("^H", "Harden"),
|
||||||
|
("Del", "Delete"),
|
||||||
|
("<>", "Len"),
|
||||||
|
("[]", "Spd"),
|
||||||
|
("+-", "Tempo"),
|
||||||
],
|
],
|
||||||
Page::Patterns => vec![
|
Page::Patterns => vec![
|
||||||
("←→↑↓", "Navigate"),
|
("←→↑↓", "Navigate"),
|
||||||
@@ -382,6 +389,12 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
ConfirmModal::new("Confirm", &format!("Delete step {}?", step + 1), *selected)
|
ConfirmModal::new("Confirm", &format!("Delete step {}?", step + 1), *selected)
|
||||||
.render_centered(frame, term);
|
.render_centered(frame, term);
|
||||||
}
|
}
|
||||||
|
Modal::ConfirmDeleteSteps { steps, selected, .. } => {
|
||||||
|
let nums: Vec<String> = steps.iter().map(|s| format!("{:02}", s + 1)).collect();
|
||||||
|
let label = format!("Delete steps {}?", nums.join(", "));
|
||||||
|
ConfirmModal::new("Confirm", &label, *selected)
|
||||||
|
.render_centered(frame, term);
|
||||||
|
}
|
||||||
Modal::ConfirmResetPattern {
|
Modal::ConfirmResetPattern {
|
||||||
pattern, selected, ..
|
pattern, selected, ..
|
||||||
} => {
|
} => {
|
||||||
|
|||||||
Reference in New Issue
Block a user