vastly improved selection system

This commit is contained in:
2026-01-28 02:29:17 +01:00
parent 322885b908
commit c7a9f7bc5a
13 changed files with 1507 additions and 437 deletions

View File

@@ -89,4 +89,5 @@ pub enum Op {
StackStart, StackStart,
EmitN, EmitN,
ClearCmd, ClearCmd,
SetSpeed,
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -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') => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, ..
} => { } => {