Feat: refactoring codebase
This commit is contained in:
287
src/app/clipboard.rs
Normal file
287
src/app/clipboard.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use crate::model;
|
||||
use crate::services::clipboard;
|
||||
use crate::state::FlashKind;
|
||||
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
pub fn copy_pattern(&mut self, bank: usize, pattern: usize) {
|
||||
self.copied_patterns = Some(vec![clipboard::copy_pattern(
|
||||
&self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
)]);
|
||||
self.ui.flash("Pattern copied", 150, FlashKind::Success);
|
||||
}
|
||||
|
||||
pub fn paste_pattern(&mut self, bank: usize, pattern: usize) {
|
||||
if let Some(src) = self.copied_patterns.as_ref().and_then(|v| v.first()) {
|
||||
let src = src.clone();
|
||||
clipboard::paste_pattern(&mut self.project_state.project, bank, pattern, &src);
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.ui.flash("Pattern pasted", 150, FlashKind::Success);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_patterns(&mut self, bank: usize, patterns: &[usize]) {
|
||||
self.copied_patterns = Some(clipboard::copy_patterns(
|
||||
&self.project_state.project,
|
||||
bank,
|
||||
patterns,
|
||||
));
|
||||
let n = patterns.len();
|
||||
self.ui.flash(
|
||||
&format!("{n} pattern{} copied", if n == 1 { "" } else { "s" }),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn paste_patterns(&mut self, bank: usize, start: usize) {
|
||||
if let Some(sources) = self.copied_patterns.clone() {
|
||||
let count = clipboard::paste_patterns(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
start,
|
||||
&sources,
|
||||
);
|
||||
for i in 0..count {
|
||||
self.project_state.mark_dirty(bank, start + i);
|
||||
}
|
||||
if self.editor_ctx.bank == bank {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.ui.flash(
|
||||
&format!("{count} pattern{} pasted", if count == 1 { "" } else { "s" }),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shift_patterns_up(&mut self) {
|
||||
let bank = self.patterns_nav.bank_cursor;
|
||||
let patterns = self.patterns_nav.selected_patterns();
|
||||
let start = *patterns.first().unwrap();
|
||||
let end = *patterns.last().unwrap();
|
||||
if let Some(dirty) = clipboard::shift_patterns_up(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
start..=end,
|
||||
) {
|
||||
for (b, p) in &dirty {
|
||||
self.project_state.mark_dirty(*b, *p);
|
||||
}
|
||||
self.patterns_nav.pattern_cursor -= 1;
|
||||
if let Some(ref mut anchor) = self.patterns_nav.pattern_anchor {
|
||||
*anchor -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shift_patterns_down(&mut self) {
|
||||
let bank = self.patterns_nav.bank_cursor;
|
||||
let patterns = self.patterns_nav.selected_patterns();
|
||||
let start = *patterns.first().unwrap();
|
||||
let end = *patterns.last().unwrap();
|
||||
if let Some(dirty) = clipboard::shift_patterns_down(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
start..=end,
|
||||
) {
|
||||
for (b, p) in &dirty {
|
||||
self.project_state.mark_dirty(*b, *p);
|
||||
}
|
||||
self.patterns_nav.pattern_cursor += 1;
|
||||
if let Some(ref mut anchor) = self.patterns_nav.pattern_anchor {
|
||||
*anchor += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_bank(&mut self, bank: usize) {
|
||||
self.copied_banks = Some(vec![clipboard::copy_bank(
|
||||
&self.project_state.project,
|
||||
bank,
|
||||
)]);
|
||||
self.ui.flash("Bank copied", 150, FlashKind::Success);
|
||||
}
|
||||
|
||||
pub fn paste_bank(&mut self, bank: usize) {
|
||||
if let Some(src) = self.copied_banks.as_ref().and_then(|v| v.first()) {
|
||||
let src = src.clone();
|
||||
let pat_count =
|
||||
clipboard::paste_bank(&mut self.project_state.project, bank, &src);
|
||||
for pattern in 0..pat_count {
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
if self.editor_ctx.bank == bank {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.ui.flash("Bank pasted", 150, FlashKind::Success);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_banks(&mut self, banks: &[usize]) {
|
||||
self.copied_banks = Some(clipboard::copy_banks(
|
||||
&self.project_state.project,
|
||||
banks,
|
||||
));
|
||||
let n = banks.len();
|
||||
self.ui.flash(
|
||||
&format!("{n} bank{} copied", if n == 1 { "" } else { "s" }),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn paste_banks(&mut self, start: usize) {
|
||||
if let Some(sources) = self.copied_banks.clone() {
|
||||
let count = clipboard::paste_banks(
|
||||
&mut self.project_state.project,
|
||||
start,
|
||||
&sources,
|
||||
);
|
||||
for i in 0..count {
|
||||
let bank = start + i;
|
||||
for pattern in 0..model::MAX_PATTERNS {
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
}
|
||||
if (start..start + count).contains(&self.editor_ctx.bank) {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.ui.flash(
|
||||
&format!("{count} bank{} pasted", if count == 1 { "" } else { "s" }),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn harden_steps(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let indices = self.selected_steps();
|
||||
let count = clipboard::harden_steps(&mut self.project_state.project, bank, pattern, &indices);
|
||||
if count == 0 {
|
||||
self.ui.set_status("No linked steps to harden".to_string());
|
||||
return;
|
||||
}
|
||||
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 copy_steps(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let indices = self.selected_steps();
|
||||
let (copied, scripts) = clipboard::copy_steps(
|
||||
&self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
&indices,
|
||||
);
|
||||
let count = copied.steps.len();
|
||||
self.editor_ctx.copied_steps = Some(copied);
|
||||
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: &crate::engine::LinkState) {
|
||||
let Some(copied) = self.editor_ctx.copied_steps.clone() else {
|
||||
self.ui.set_status("Nothing copied".to_string());
|
||||
return;
|
||||
};
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let cursor = self.editor_ctx.step;
|
||||
let result = clipboard::paste_steps(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
cursor,
|
||||
&copied,
|
||||
);
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
self.load_step_to_editor();
|
||||
for &target in &result.compile_targets {
|
||||
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!("Pasted {} steps", result.count),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn link_paste_steps(&mut self) {
|
||||
let Some(copied) = self.editor_ctx.copied_steps.clone() else {
|
||||
self.ui.set_status("Nothing copied".to_string());
|
||||
return;
|
||||
};
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let cursor = self.editor_ctx.step;
|
||||
match clipboard::link_paste_steps(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
cursor,
|
||||
&copied,
|
||||
) {
|
||||
None => {
|
||||
self.ui
|
||||
.set_status("Can only link within same pattern".to_string());
|
||||
}
|
||||
Some(count) => {
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
self.load_step_to_editor();
|
||||
self.editor_ctx.clear_selection();
|
||||
self.ui.flash(
|
||||
&format!("Linked {count} steps"),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duplicate_steps(&mut self, link: &crate::engine::LinkState) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let indices = self.selected_steps();
|
||||
let result = clipboard::duplicate_steps(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
&indices,
|
||||
);
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
self.load_step_to_editor();
|
||||
for &target in &result.compile_targets {
|
||||
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 {} steps", result.count),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
}
|
||||
442
src/app/dispatch.rs
Normal file
442
src/app/dispatch.rs
Normal file
@@ -0,0 +1,442 @@
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::{LinkState, SequencerSnapshot};
|
||||
use crate::services::{dict_nav, euclidean, help_nav, pattern_editor};
|
||||
use crate::state::{undo::UndoEntry, CyclicEnum, FlashKind, Modal, StagedPropChange};
|
||||
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||
match cmd {
|
||||
AppCommand::Undo => {
|
||||
if let Some(entry) = self.undo.pop_undo() {
|
||||
let reverse = self.apply_undo_entry(entry);
|
||||
self.undo.push_redo(reverse);
|
||||
self.ui.flash("Undo", 100, FlashKind::Info);
|
||||
}
|
||||
return;
|
||||
}
|
||||
AppCommand::Redo => {
|
||||
if let Some(entry) = self.undo.pop_redo() {
|
||||
let reverse = self.apply_undo_entry(entry);
|
||||
self.undo.undo_stack.push(reverse);
|
||||
self.ui.flash("Redo", 100, FlashKind::Info);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(scope) = self.undoable_scope(&cmd) {
|
||||
let cursor = (self.editor_ctx.bank, self.editor_ctx.pattern, self.editor_ctx.step);
|
||||
self.undo.push(UndoEntry { scope, cursor });
|
||||
}
|
||||
|
||||
match cmd {
|
||||
AppCommand::Undo | AppCommand::Redo => unreachable!(),
|
||||
|
||||
// Playback
|
||||
AppCommand::TogglePlaying => self.toggle_playing(link),
|
||||
AppCommand::TempoUp => self.tempo_up(link),
|
||||
AppCommand::TempoDown => self.tempo_down(link),
|
||||
|
||||
// Navigation
|
||||
AppCommand::NextStep => self.next_step(),
|
||||
AppCommand::PrevStep => self.prev_step(),
|
||||
AppCommand::StepUp => self.step_up(),
|
||||
AppCommand::StepDown => self.step_down(),
|
||||
|
||||
// Pattern editing
|
||||
AppCommand::ToggleSteps => self.toggle_steps(),
|
||||
AppCommand::LengthIncrease => self.length_increase(),
|
||||
AppCommand::LengthDecrease => self.length_decrease(),
|
||||
AppCommand::SpeedIncrease => self.speed_increase(),
|
||||
AppCommand::SpeedDecrease => self.speed_decrease(),
|
||||
AppCommand::SetLength {
|
||||
bank,
|
||||
pattern,
|
||||
length,
|
||||
} => {
|
||||
let (change, new_len) = pattern_editor::set_length(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
length,
|
||||
);
|
||||
if self.editor_ctx.bank == bank
|
||||
&& self.editor_ctx.pattern == pattern
|
||||
&& self.editor_ctx.step >= new_len
|
||||
{
|
||||
self.editor_ctx.step = new_len - 1;
|
||||
}
|
||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||
}
|
||||
AppCommand::SetSpeed {
|
||||
bank,
|
||||
pattern,
|
||||
speed,
|
||||
} => {
|
||||
let change = pattern_editor::set_speed(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
speed,
|
||||
);
|
||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||
}
|
||||
|
||||
// Script editing
|
||||
AppCommand::SaveEditorToStep => self.save_editor_to_step(),
|
||||
AppCommand::CompileCurrentStep => self.compile_current_step(link),
|
||||
AppCommand::DeleteStep {
|
||||
bank,
|
||||
pattern,
|
||||
step,
|
||||
} => self.delete_step(bank, pattern, step),
|
||||
AppCommand::DeleteSteps {
|
||||
bank,
|
||||
pattern,
|
||||
steps,
|
||||
} => self.delete_steps(bank, pattern, &steps),
|
||||
AppCommand::ResetPattern { bank, pattern } => self.reset_pattern(bank, pattern),
|
||||
AppCommand::ResetBank { bank } => self.reset_bank(bank),
|
||||
AppCommand::CopyPattern { bank, pattern } => self.copy_pattern(bank, pattern),
|
||||
AppCommand::PastePattern { bank, pattern } => self.paste_pattern(bank, pattern),
|
||||
AppCommand::CopyPatterns { bank, patterns } => self.copy_patterns(bank, &patterns),
|
||||
AppCommand::PastePatterns { bank, start } => self.paste_patterns(bank, start),
|
||||
AppCommand::CopyBank { bank } => self.copy_bank(bank),
|
||||
AppCommand::PasteBank { bank } => self.paste_bank(bank),
|
||||
AppCommand::CopyBanks { banks } => self.copy_banks(&banks),
|
||||
AppCommand::PasteBanks { start } => self.paste_banks(start),
|
||||
AppCommand::ResetPatterns { bank, patterns } => self.reset_patterns(bank, &patterns),
|
||||
AppCommand::ResetBanks { banks } => self.reset_banks(&banks),
|
||||
|
||||
// Reorder
|
||||
AppCommand::ShiftPatternsUp => self.shift_patterns_up(),
|
||||
AppCommand::ShiftPatternsDown => self.shift_patterns_down(),
|
||||
|
||||
// Clipboard
|
||||
AppCommand::HardenSteps => self.harden_steps(),
|
||||
AppCommand::CopySteps => self.copy_steps(),
|
||||
AppCommand::PasteSteps => self.paste_steps(link),
|
||||
AppCommand::LinkPasteSteps => self.link_paste_steps(),
|
||||
AppCommand::DuplicateSteps => self.duplicate_steps(link),
|
||||
|
||||
// Pattern playback (staging)
|
||||
AppCommand::ClearStagedChanges => self.clear_staged_changes(),
|
||||
|
||||
// Project
|
||||
AppCommand::RenameBank { bank, name } => {
|
||||
self.project_state.project.banks[bank].name = name;
|
||||
}
|
||||
AppCommand::RenamePattern {
|
||||
bank,
|
||||
pattern,
|
||||
name,
|
||||
} => {
|
||||
self.project_state.project.banks[bank].patterns[pattern].name = name;
|
||||
}
|
||||
AppCommand::RenameStep {
|
||||
bank,
|
||||
pattern,
|
||||
step,
|
||||
name,
|
||||
} => {
|
||||
if let Some(s) =
|
||||
self.project_state.project.banks[bank].patterns[pattern].step_mut(step)
|
||||
{
|
||||
s.name = name;
|
||||
}
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
AppCommand::Save(path) => self.save(path, link, snapshot),
|
||||
AppCommand::Load(path) => self.load(path, link),
|
||||
|
||||
// UI
|
||||
AppCommand::SetStatus(msg) => self.ui.set_status(msg),
|
||||
AppCommand::ClearStatus => self.ui.clear_status(),
|
||||
AppCommand::OpenModal(modal) => {
|
||||
if matches!(modal, Modal::Editor) {
|
||||
let pattern = &self.project_state.project.banks[self.editor_ctx.bank].patterns
|
||||
[self.editor_ctx.pattern];
|
||||
if let Some(source) = pattern.step(self.editor_ctx.step).and_then(|s| s.source)
|
||||
{
|
||||
self.editor_ctx.step = source as usize;
|
||||
}
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.ui.modal = modal;
|
||||
}
|
||||
AppCommand::CloseModal => self.ui.modal = Modal::None,
|
||||
AppCommand::OpenPatternModal(field) => self.open_pattern_modal(field),
|
||||
AppCommand::OpenPatternPropsModal { bank, pattern } => {
|
||||
self.open_pattern_props_modal(bank, pattern);
|
||||
}
|
||||
AppCommand::StagePatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
name,
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
} => {
|
||||
self.playback.staged_prop_changes.insert(
|
||||
(bank, pattern),
|
||||
StagedPropChange {
|
||||
name,
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
},
|
||||
);
|
||||
self.ui.set_status(format!(
|
||||
"B{:02}:P{:02} props staged",
|
||||
bank + 1,
|
||||
pattern + 1
|
||||
));
|
||||
}
|
||||
|
||||
// Page navigation
|
||||
AppCommand::PageLeft => {
|
||||
self.page.left();
|
||||
self.maybe_show_onboarding();
|
||||
}
|
||||
AppCommand::PageRight => {
|
||||
self.page.right();
|
||||
self.maybe_show_onboarding();
|
||||
}
|
||||
AppCommand::PageUp => {
|
||||
self.page.up();
|
||||
self.maybe_show_onboarding();
|
||||
}
|
||||
AppCommand::PageDown => {
|
||||
self.page.down();
|
||||
self.maybe_show_onboarding();
|
||||
}
|
||||
AppCommand::GoToPage(page) => {
|
||||
self.page = page;
|
||||
self.maybe_show_onboarding();
|
||||
}
|
||||
|
||||
// Help navigation
|
||||
AppCommand::HelpToggleFocus => help_nav::toggle_focus(&mut self.ui),
|
||||
AppCommand::HelpNextTopic(n) => help_nav::next_topic(&mut self.ui, n),
|
||||
AppCommand::HelpPrevTopic(n) => help_nav::prev_topic(&mut self.ui, n),
|
||||
AppCommand::HelpScrollDown(n) => help_nav::scroll_down(&mut self.ui, n),
|
||||
AppCommand::HelpScrollUp(n) => help_nav::scroll_up(&mut self.ui, n),
|
||||
AppCommand::HelpActivateSearch => help_nav::activate_search(&mut self.ui),
|
||||
AppCommand::HelpClearSearch => help_nav::clear_search(&mut self.ui),
|
||||
AppCommand::HelpSearchInput(c) => help_nav::search_input(&mut self.ui, c),
|
||||
AppCommand::HelpSearchBackspace => help_nav::search_backspace(&mut self.ui),
|
||||
AppCommand::HelpSearchConfirm => help_nav::search_confirm(&mut self.ui),
|
||||
|
||||
// Dictionary navigation
|
||||
AppCommand::DictToggleFocus => dict_nav::toggle_focus(&mut self.ui),
|
||||
AppCommand::DictNextCategory => dict_nav::next_category(&mut self.ui),
|
||||
AppCommand::DictPrevCategory => dict_nav::prev_category(&mut self.ui),
|
||||
AppCommand::DictScrollDown(n) => dict_nav::scroll_down(&mut self.ui, n),
|
||||
AppCommand::DictScrollUp(n) => dict_nav::scroll_up(&mut self.ui, n),
|
||||
AppCommand::DictActivateSearch => dict_nav::activate_search(&mut self.ui),
|
||||
AppCommand::DictClearSearch => dict_nav::clear_search(&mut self.ui),
|
||||
AppCommand::DictSearchInput(c) => dict_nav::search_input(&mut self.ui, c),
|
||||
AppCommand::DictSearchBackspace => dict_nav::search_backspace(&mut self.ui),
|
||||
AppCommand::DictSearchConfirm => dict_nav::search_confirm(&mut self.ui),
|
||||
|
||||
// Patterns view
|
||||
AppCommand::PatternsCursorLeft => self.patterns_nav.move_left(),
|
||||
AppCommand::PatternsCursorRight => self.patterns_nav.move_right(),
|
||||
AppCommand::PatternsCursorUp => self.patterns_nav.move_up(),
|
||||
AppCommand::PatternsCursorDown => self.patterns_nav.move_down(),
|
||||
AppCommand::PatternsEnter => {
|
||||
let bank = self.patterns_nav.selected_bank();
|
||||
let pattern = self.patterns_nav.selected_pattern();
|
||||
self.select_edit_bank(bank);
|
||||
self.select_edit_pattern(pattern);
|
||||
self.page.down();
|
||||
self.maybe_show_onboarding();
|
||||
}
|
||||
AppCommand::PatternsBack => {
|
||||
self.page.down();
|
||||
self.maybe_show_onboarding();
|
||||
}
|
||||
|
||||
// Mute/Solo (staged)
|
||||
AppCommand::StageMute { bank, pattern } => self.playback.stage_mute(bank, pattern),
|
||||
AppCommand::StageSolo { bank, pattern } => self.playback.stage_solo(bank, pattern),
|
||||
AppCommand::ClearMutes => {
|
||||
self.playback.clear_staged_mutes();
|
||||
self.mute.clear_mute();
|
||||
}
|
||||
AppCommand::ClearSolos => {
|
||||
self.playback.clear_staged_solos();
|
||||
self.mute.clear_solo();
|
||||
}
|
||||
|
||||
// UI state
|
||||
AppCommand::ClearMinimap => self.ui.dismiss_minimap(),
|
||||
AppCommand::HideTitle => {
|
||||
self.ui.show_title = false;
|
||||
self.maybe_show_onboarding();
|
||||
}
|
||||
AppCommand::ToggleEditorStack => {
|
||||
self.editor_ctx.show_stack = !self.editor_ctx.show_stack;
|
||||
if self.editor_ctx.show_stack {
|
||||
crate::services::stack_preview::update_cache(&self.editor_ctx);
|
||||
}
|
||||
}
|
||||
AppCommand::SetColorScheme(scheme) => {
|
||||
self.ui.color_scheme = scheme;
|
||||
let base_theme = scheme.to_theme();
|
||||
let rotated = cagire_ratatui::theme::transform::rotate_theme(base_theme, self.ui.hue_rotation);
|
||||
crate::theme::set(rotated);
|
||||
}
|
||||
AppCommand::SetHueRotation(degrees) => {
|
||||
self.ui.hue_rotation = degrees;
|
||||
let base_theme = self.ui.color_scheme.to_theme();
|
||||
let rotated = cagire_ratatui::theme::transform::rotate_theme(base_theme, degrees);
|
||||
crate::theme::set(rotated);
|
||||
}
|
||||
AppCommand::ToggleRuntimeHighlight => {
|
||||
self.ui.runtime_highlight = !self.ui.runtime_highlight;
|
||||
}
|
||||
AppCommand::ToggleCompletion => {
|
||||
self.ui.show_completion = !self.ui.show_completion;
|
||||
self.editor_ctx
|
||||
.editor
|
||||
.set_completion_enabled(self.ui.show_completion);
|
||||
}
|
||||
AppCommand::ToggleLiveKeysFill => self.live_keys.flip_fill(),
|
||||
|
||||
// Panel
|
||||
AppCommand::ClosePanel => {
|
||||
self.panel.visible = false;
|
||||
self.panel.focus = crate::state::PanelFocus::Main;
|
||||
}
|
||||
|
||||
// Direct navigation (mouse)
|
||||
AppCommand::GoToStep(step) => {
|
||||
let len = self.current_edit_pattern().length;
|
||||
if step < len {
|
||||
self.editor_ctx.step = step;
|
||||
self.editor_ctx.clear_selection();
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
}
|
||||
AppCommand::PatternsSelectBank(bank) => {
|
||||
self.patterns_nav.bank_cursor = bank;
|
||||
self.patterns_nav.clear_selection();
|
||||
}
|
||||
AppCommand::PatternsSelectPattern(pattern) => {
|
||||
self.patterns_nav.pattern_cursor = pattern;
|
||||
self.patterns_nav.clear_selection();
|
||||
}
|
||||
AppCommand::HelpSelectTopic(i) => help_nav::select_topic(&mut self.ui, i),
|
||||
AppCommand::DictSelectCategory(i) => dict_nav::select_category(&mut self.ui, i),
|
||||
|
||||
// Selection
|
||||
AppCommand::SetSelectionAnchor(step) => {
|
||||
self.editor_ctx.selection_anchor = Some(step);
|
||||
}
|
||||
|
||||
// Audio settings (engine page)
|
||||
AppCommand::AudioSetSection(section) => self.audio.section = section,
|
||||
AppCommand::AudioNextSection => self.audio.next_section(),
|
||||
AppCommand::AudioPrevSection => self.audio.prev_section(),
|
||||
AppCommand::AudioOutputListUp => self.audio.output_list.move_up(),
|
||||
AppCommand::AudioOutputListDown(count) => self.audio.output_list.move_down(count),
|
||||
AppCommand::AudioOutputPageUp => self.audio.output_list.page_up(),
|
||||
AppCommand::AudioOutputPageDown(count) => self.audio.output_list.page_down(count),
|
||||
AppCommand::AudioInputListUp => self.audio.input_list.move_up(),
|
||||
AppCommand::AudioInputListDown(count) => self.audio.input_list.move_down(count),
|
||||
AppCommand::AudioInputPageDown(count) => self.audio.input_list.page_down(count),
|
||||
AppCommand::AudioSettingNext => self.audio.setting_kind = self.audio.setting_kind.next(),
|
||||
AppCommand::AudioSettingPrev => self.audio.setting_kind = self.audio.setting_kind.prev(),
|
||||
AppCommand::SetOutputDevice(name) => self.audio.config.output_device = Some(name),
|
||||
AppCommand::SetInputDevice(name) => self.audio.config.input_device = Some(name),
|
||||
AppCommand::SetDeviceKind(kind) => self.audio.device_kind = kind,
|
||||
AppCommand::AdjustAudioSetting { setting, delta } => {
|
||||
use crate::state::SettingKind;
|
||||
match setting {
|
||||
SettingKind::Channels => self.audio.adjust_channels(delta as i16),
|
||||
SettingKind::BufferSize => self.audio.adjust_buffer_size(delta),
|
||||
SettingKind::Polyphony => self.audio.adjust_max_voices(delta),
|
||||
SettingKind::Nudge => {
|
||||
self.metrics.nudge_ms =
|
||||
(self.metrics.nudge_ms + delta as f64).clamp(-50.0, 50.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
AppCommand::AudioTriggerRestart => self.audio.trigger_restart(),
|
||||
AppCommand::RemoveLastSamplePath => self.audio.remove_last_sample_path(),
|
||||
AppCommand::AudioRefreshDevices => self.audio.refresh_devices(),
|
||||
|
||||
// Options page
|
||||
AppCommand::OptionsNextFocus => self.options.next_focus(),
|
||||
AppCommand::OptionsPrevFocus => self.options.prev_focus(),
|
||||
AppCommand::OptionsSetFocus(focus) => self.options.focus = focus,
|
||||
AppCommand::ToggleRefreshRate => self.audio.toggle_refresh_rate(),
|
||||
AppCommand::ToggleScope => self.audio.config.show_scope = !self.audio.config.show_scope,
|
||||
AppCommand::ToggleSpectrum => self.audio.config.show_spectrum = !self.audio.config.show_spectrum,
|
||||
AppCommand::TogglePreview => self.audio.config.show_preview = !self.audio.config.show_preview,
|
||||
|
||||
// Metrics
|
||||
AppCommand::ResetPeakVoices => self.metrics.peak_voices = 0,
|
||||
|
||||
// Euclidean distribution
|
||||
AppCommand::ApplyEuclideanDistribution {
|
||||
bank,
|
||||
pattern,
|
||||
source_step,
|
||||
pulses,
|
||||
steps,
|
||||
rotation,
|
||||
} => {
|
||||
let targets = euclidean::apply_distribution(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
source_step,
|
||||
pulses,
|
||||
steps,
|
||||
rotation,
|
||||
);
|
||||
let created_count = targets.len();
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
for &target in &targets {
|
||||
let saved = self.editor_ctx.step;
|
||||
self.editor_ctx.step = target;
|
||||
self.compile_current_step(link);
|
||||
self.editor_ctx.step = saved;
|
||||
}
|
||||
self.load_step_to_editor();
|
||||
self.ui.flash(
|
||||
&format!("Created {created_count} linked steps (E({pulses},{steps}))"),
|
||||
200,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Onboarding
|
||||
AppCommand::DismissOnboarding => {
|
||||
let name = self.page.name().to_string();
|
||||
if !self.ui.onboarding_dismissed.contains(&name) {
|
||||
self.ui.onboarding_dismissed.push(name);
|
||||
}
|
||||
}
|
||||
AppCommand::ResetOnboarding => self.ui.onboarding_dismissed.clear(),
|
||||
AppCommand::GoToHelpTopic(topic) => {
|
||||
self.ui.modal = Modal::None;
|
||||
self.page = crate::page::Page::Help;
|
||||
self.ui.help_topic = topic;
|
||||
}
|
||||
|
||||
// Prelude
|
||||
AppCommand::OpenPreludeEditor => self.open_prelude_editor(),
|
||||
AppCommand::SavePrelude => self.save_prelude(),
|
||||
AppCommand::EvaluatePrelude => self.evaluate_prelude(link),
|
||||
AppCommand::ClosePreludeEditor => self.close_prelude_editor(),
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/app/editing.rs
Normal file
126
src/app/editing.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use crate::services::pattern_editor;
|
||||
use crate::state::FlashKind;
|
||||
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
pub fn toggle_steps(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
for idx in self.selected_steps() {
|
||||
pattern_editor::toggle_step(&mut self.project_state.project, bank, pattern, idx);
|
||||
}
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
|
||||
pub fn length_increase(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let (change, _) =
|
||||
pattern_editor::increase_length(&mut self.project_state.project, bank, pattern);
|
||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||
}
|
||||
|
||||
pub fn length_decrease(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let (change, new_len) =
|
||||
pattern_editor::decrease_length(&mut self.project_state.project, bank, pattern);
|
||||
if self.editor_ctx.step >= new_len {
|
||||
self.editor_ctx.step = new_len - 1;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||
}
|
||||
|
||||
pub fn speed_increase(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let change = pattern_editor::increase_speed(&mut self.project_state.project, bank, pattern);
|
||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||
}
|
||||
|
||||
pub fn speed_decrease(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let change = pattern_editor::decrease_speed(&mut self.project_state.project, bank, pattern);
|
||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||
}
|
||||
|
||||
pub fn delete_step(&mut self, bank: usize, pattern: usize, step: usize) {
|
||||
let edit = pattern_editor::delete_step(&mut self.project_state.project, bank, pattern, step);
|
||||
self.project_state.mark_dirty(edit.bank, edit.pattern);
|
||||
if self.editor_ctx.bank == bank
|
||||
&& self.editor_ctx.pattern == pattern
|
||||
&& self.editor_ctx.step == step
|
||||
{
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.ui.flash("Step deleted", 150, FlashKind::Success);
|
||||
}
|
||||
|
||||
pub fn delete_steps(&mut self, bank: usize, pattern: usize, steps: &[usize]) {
|
||||
let edit = pattern_editor::delete_steps(&mut self.project_state.project, bank, pattern, steps);
|
||||
self.project_state.mark_dirty(edit.bank, edit.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) {
|
||||
let edit = pattern_editor::reset_pattern(&mut self.project_state.project, bank, pattern);
|
||||
self.project_state.mark_dirty(edit.bank, edit.pattern);
|
||||
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.ui.flash("Pattern reset", 150, FlashKind::Success);
|
||||
}
|
||||
|
||||
pub fn reset_bank(&mut self, bank: usize) {
|
||||
let pat_count = pattern_editor::reset_bank(&mut self.project_state.project, bank);
|
||||
for pattern in 0..pat_count {
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
if self.editor_ctx.bank == bank {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
self.ui.flash("Bank reset", 150, FlashKind::Success);
|
||||
}
|
||||
|
||||
pub fn reset_patterns(&mut self, bank: usize, patterns: &[usize]) {
|
||||
for &pattern in patterns {
|
||||
let edit =
|
||||
pattern_editor::reset_pattern(&mut self.project_state.project, bank, pattern);
|
||||
self.project_state.mark_dirty(edit.bank, edit.pattern);
|
||||
}
|
||||
if self.editor_ctx.bank == bank && patterns.contains(&self.editor_ctx.pattern) {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
let n = patterns.len();
|
||||
self.ui.flash(
|
||||
&format!("{n} pattern{} reset", if n == 1 { "" } else { "s" }),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn reset_banks(&mut self, banks: &[usize]) {
|
||||
for &bank in banks {
|
||||
let pat_count =
|
||||
pattern_editor::reset_bank(&mut self.project_state.project, bank);
|
||||
for pattern in 0..pat_count {
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
}
|
||||
if banks.contains(&self.editor_ctx.bank) {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
let n = banks.len();
|
||||
self.ui.flash(
|
||||
&format!("{n} bank{} reset", if n == 1 { "" } else { "s" }),
|
||||
150,
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
}
|
||||
192
src/app/mod.rs
Normal file
192
src/app/mod.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
mod clipboard;
|
||||
mod dispatch;
|
||||
mod editing;
|
||||
mod navigation;
|
||||
mod persistence;
|
||||
mod scripting;
|
||||
mod sequencer;
|
||||
mod staging;
|
||||
mod undo;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use parking_lot::Mutex;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use cagire_ratatui::CompletionCandidate;
|
||||
|
||||
use crate::engine::LinkState;
|
||||
use crate::midi::MidiState;
|
||||
use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, Variables};
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal, MuteState,
|
||||
OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
|
||||
ProjectState, UiState,
|
||||
};
|
||||
|
||||
const STEPS_PER_PAGE: usize = 32;
|
||||
|
||||
static COMPLETION_CANDIDATES: LazyLock<Vec<CompletionCandidate>> = LazyLock::new(|| {
|
||||
model::WORDS
|
||||
.iter()
|
||||
.map(|w| CompletionCandidate {
|
||||
name: w.name.to_string(),
|
||||
signature: w.stack.to_string(),
|
||||
description: w.desc.to_string(),
|
||||
example: w.example.to_string(),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
pub struct App {
|
||||
pub project_state: ProjectState,
|
||||
pub ui: UiState,
|
||||
pub playback: PlaybackState,
|
||||
pub mute: MuteState,
|
||||
|
||||
pub page: Page,
|
||||
pub editor_ctx: EditorContext,
|
||||
|
||||
pub patterns_nav: PatternsNav,
|
||||
|
||||
pub metrics: Metrics,
|
||||
pub script_engine: ScriptEngine,
|
||||
pub variables: Variables,
|
||||
pub dict: Dictionary,
|
||||
#[allow(dead_code)]
|
||||
pub rng: Rng,
|
||||
pub live_keys: Arc<LiveKeyState>,
|
||||
pub clipboard: Option<arboard::Clipboard>,
|
||||
pub copied_patterns: Option<Vec<Pattern>>,
|
||||
pub copied_banks: Option<Vec<Bank>>,
|
||||
|
||||
pub undo: UndoHistory,
|
||||
|
||||
pub audio: AudioSettings,
|
||||
pub options: OptionsState,
|
||||
pub panel: PanelState,
|
||||
pub midi: MidiState,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
let script_engine =
|
||||
ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), Arc::clone(&rng));
|
||||
let live_keys = Arc::new(LiveKeyState::new());
|
||||
|
||||
Self {
|
||||
project_state: ProjectState::default(),
|
||||
ui: UiState::default(),
|
||||
playback: PlaybackState::default(),
|
||||
mute: MuteState::default(),
|
||||
|
||||
page: Page::default(),
|
||||
editor_ctx: EditorContext::default(),
|
||||
|
||||
patterns_nav: PatternsNav::default(),
|
||||
|
||||
metrics: Metrics::default(),
|
||||
variables,
|
||||
dict,
|
||||
rng,
|
||||
live_keys,
|
||||
script_engine,
|
||||
clipboard: arboard::Clipboard::new().ok(),
|
||||
copied_patterns: None,
|
||||
copied_banks: None,
|
||||
|
||||
undo: UndoHistory::default(),
|
||||
|
||||
audio: AudioSettings::default(),
|
||||
options: OptionsState::default(),
|
||||
panel: PanelState::default(),
|
||||
midi: MidiState::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_bank_pattern(&self) -> (usize, usize) {
|
||||
(self.editor_ctx.bank, self.editor_ctx.pattern)
|
||||
}
|
||||
|
||||
fn selected_steps(&self) -> Vec<usize> {
|
||||
match self.editor_ctx.selection_range() {
|
||||
Some(range) => range.collect(),
|
||||
None => vec![self.editor_ctx.step],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_all_patterns_dirty(&mut self) {
|
||||
self.project_state.mark_all_dirty();
|
||||
}
|
||||
|
||||
pub fn toggle_playing(&mut self, link: &LinkState) {
|
||||
let was_playing = self.playback.playing;
|
||||
self.playback.toggle();
|
||||
if !was_playing && self.playback.playing {
|
||||
self.evaluate_prelude(link);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tempo_up(&self, link: &LinkState) {
|
||||
let current = link.tempo();
|
||||
link.set_tempo((current + 1.0).min(300.0));
|
||||
}
|
||||
|
||||
pub fn tempo_down(&self, link: &LinkState) {
|
||||
let current = link.tempo();
|
||||
link.set_tempo((current - 1.0).max(20.0));
|
||||
}
|
||||
|
||||
pub fn current_edit_pattern(&self) -> &Pattern {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
self.project_state.project.pattern_at(bank, pattern)
|
||||
}
|
||||
|
||||
pub fn open_pattern_modal(&mut self, field: PatternField) {
|
||||
let current = match field {
|
||||
PatternField::Length => self.current_edit_pattern().length.to_string(),
|
||||
PatternField::Speed => self.current_edit_pattern().speed.label().to_string(),
|
||||
};
|
||||
self.ui.modal = Modal::SetPattern {
|
||||
field,
|
||||
input: current,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn open_pattern_props_modal(&mut self, bank: usize, pattern: usize) {
|
||||
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||
self.ui.modal = Modal::PatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
field: PatternPropsField::default(),
|
||||
name: pat.name.clone().unwrap_or_default(),
|
||||
length: pat.length.to_string(),
|
||||
speed: pat.speed,
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn maybe_show_onboarding(&mut self) {
|
||||
if self.ui.modal != Modal::None {
|
||||
return;
|
||||
}
|
||||
let name = self.page.name();
|
||||
if self.ui.onboarding_dismissed.iter().any(|d| d == name) {
|
||||
return;
|
||||
}
|
||||
self.ui.modal = Modal::Onboarding { page: 0 };
|
||||
}
|
||||
}
|
||||
54
src/app/navigation.rs
Normal file
54
src/app/navigation.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use super::{App, STEPS_PER_PAGE};
|
||||
|
||||
impl App {
|
||||
pub fn next_step(&mut self) {
|
||||
let len = self.current_edit_pattern().length;
|
||||
self.editor_ctx.step = (self.editor_ctx.step + 1) % len;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn prev_step(&mut self) {
|
||||
let len = self.current_edit_pattern().length;
|
||||
self.editor_ctx.step = (self.editor_ctx.step + len - 1) % len;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn step_up(&mut self) {
|
||||
let len = self.current_edit_pattern().length;
|
||||
let page_start = (self.editor_ctx.step / STEPS_PER_PAGE) * STEPS_PER_PAGE;
|
||||
let steps_on_page = (page_start + STEPS_PER_PAGE).min(len) - page_start;
|
||||
let num_rows = steps_on_page.div_ceil(8);
|
||||
let steps_per_row = steps_on_page.div_ceil(num_rows);
|
||||
|
||||
if self.editor_ctx.step >= steps_per_row {
|
||||
self.editor_ctx.step -= steps_per_row;
|
||||
} else {
|
||||
self.editor_ctx.step = (self.editor_ctx.step + len - steps_per_row) % len;
|
||||
}
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn step_down(&mut self) {
|
||||
let len = self.current_edit_pattern().length;
|
||||
let page_start = (self.editor_ctx.step / STEPS_PER_PAGE) * STEPS_PER_PAGE;
|
||||
let steps_on_page = (page_start + STEPS_PER_PAGE).min(len) - page_start;
|
||||
let num_rows = steps_on_page.div_ceil(8);
|
||||
let steps_per_row = steps_on_page.div_ceil(num_rows);
|
||||
|
||||
self.editor_ctx.step = (self.editor_ctx.step + steps_per_row) % len;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn select_edit_pattern(&mut self, pattern: usize) {
|
||||
self.editor_ctx.pattern = pattern;
|
||||
self.editor_ctx.step = 0;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn select_edit_bank(&mut self, bank: usize) {
|
||||
self.editor_ctx.bank = bank;
|
||||
self.editor_ctx.pattern = 0;
|
||||
self.editor_ctx.step = 0;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
}
|
||||
125
src/app/persistence.rs
Normal file
125
src/app/persistence.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::engine::{LinkState, PatternChange, SequencerSnapshot};
|
||||
use crate::model;
|
||||
use crate::settings::Settings;
|
||||
use crate::state::StagedChange;
|
||||
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
pub fn save_settings(&self, link: &LinkState) {
|
||||
let settings = Settings {
|
||||
audio: crate::settings::AudioSettings {
|
||||
output_device: self.audio.config.output_device.clone(),
|
||||
input_device: self.audio.config.input_device.clone(),
|
||||
channels: self.audio.config.channels,
|
||||
buffer_size: self.audio.config.buffer_size,
|
||||
max_voices: self.audio.config.max_voices,
|
||||
},
|
||||
display: crate::settings::DisplaySettings {
|
||||
fps: self.audio.config.refresh_rate.to_fps(),
|
||||
runtime_highlight: self.ui.runtime_highlight,
|
||||
show_scope: self.audio.config.show_scope,
|
||||
show_spectrum: self.audio.config.show_spectrum,
|
||||
show_preview: self.audio.config.show_preview,
|
||||
show_completion: self.ui.show_completion,
|
||||
color_scheme: self.ui.color_scheme,
|
||||
layout: self.audio.config.layout,
|
||||
hue_rotation: self.ui.hue_rotation,
|
||||
onboarding_dismissed: self.ui.onboarding_dismissed.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
link: crate::settings::LinkSettings {
|
||||
enabled: link.is_enabled(),
|
||||
tempo: link.tempo(),
|
||||
quantum: link.quantum(),
|
||||
},
|
||||
midi: crate::settings::MidiSettings {
|
||||
output_devices: {
|
||||
let outputs = crate::midi::list_midi_outputs();
|
||||
self.midi
|
||||
.selected_outputs
|
||||
.iter()
|
||||
.map(|opt| {
|
||||
opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone()))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
input_devices: {
|
||||
let inputs = crate::midi::list_midi_inputs();
|
||||
self.midi
|
||||
.selected_inputs
|
||||
.iter()
|
||||
.map(|opt| {
|
||||
opt.and_then(|idx| inputs.get(idx).map(|d| d.name.clone()))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
},
|
||||
};
|
||||
settings.save();
|
||||
}
|
||||
|
||||
pub fn save(&mut self, path: PathBuf, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||
self.save_editor_to_step();
|
||||
self.project_state.project.sample_paths = self.audio.config.sample_paths.clone();
|
||||
self.project_state.project.tempo = link.tempo();
|
||||
self.project_state.project.playing_patterns = snapshot
|
||||
.active_patterns
|
||||
.iter()
|
||||
.map(|p| (p.bank, p.pattern))
|
||||
.collect();
|
||||
match model::save(&self.project_state.project, &path) {
|
||||
Ok(final_path) => {
|
||||
self.ui
|
||||
.set_status(format!("Saved: {}", final_path.display()));
|
||||
self.project_state.file_path = Some(final_path);
|
||||
}
|
||||
Err(e) => {
|
||||
self.ui.set_status(format!("Save error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
|
||||
match model::load(&path) {
|
||||
Ok(project) => {
|
||||
let tempo = project.tempo;
|
||||
let playing = project.playing_patterns.clone();
|
||||
|
||||
self.project_state.project = project;
|
||||
self.editor_ctx.step = 0;
|
||||
self.load_step_to_editor();
|
||||
self.compile_all_steps(link);
|
||||
self.mark_all_patterns_dirty();
|
||||
link.set_tempo(tempo);
|
||||
|
||||
self.playback.clear_queues();
|
||||
self.undo.clear();
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
self.dict.lock().clear();
|
||||
|
||||
self.evaluate_prelude(link);
|
||||
|
||||
for (bank, pattern) in playing {
|
||||
self.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: crate::model::LaunchQuantization::Immediate,
|
||||
sync_mode: crate::model::SyncMode::PhaseLock,
|
||||
});
|
||||
}
|
||||
|
||||
self.ui.set_status(format!("Loaded: {}", path.display()));
|
||||
self.project_state.file_path = Some(path);
|
||||
}
|
||||
Err(e) => {
|
||||
self.ui.set_status(format!("Load error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
src/app/scripting.rs
Normal file
190
src/app/scripting.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use crossbeam_channel::Sender;
|
||||
|
||||
use crate::engine::LinkState;
|
||||
use crate::model::StepContext;
|
||||
use crate::services::pattern_editor;
|
||||
use crate::state::{EditorTarget, FlashKind, Modal, SampleTree};
|
||||
|
||||
use super::{App, COMPLETION_CANDIDATES};
|
||||
|
||||
impl App {
|
||||
pub(super) fn create_step_context(&self, step_idx: usize, link: &LinkState) -> StepContext<'static> {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let speed = self
|
||||
.project_state
|
||||
.project
|
||||
.pattern_at(bank, pattern)
|
||||
.speed
|
||||
.multiplier();
|
||||
StepContext {
|
||||
step: step_idx,
|
||||
beat: link.beat(),
|
||||
bank,
|
||||
pattern,
|
||||
tempo: link.tempo(),
|
||||
phase: link.phase(),
|
||||
slot: 0,
|
||||
runs: 0,
|
||||
iter: 0,
|
||||
speed,
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
cc_access: None,
|
||||
speed_key: "",
|
||||
chain_key: "",
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn load_step_to_editor(&mut self) {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
if let Some(script) = pattern_editor::get_step_script(
|
||||
&self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
self.editor_ctx.step,
|
||||
) {
|
||||
let lines: Vec<String> = if script.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
script.lines().map(String::from).collect()
|
||||
};
|
||||
self.editor_ctx.editor.set_content(lines);
|
||||
self.editor_ctx.editor.set_candidates(COMPLETION_CANDIDATES.clone());
|
||||
self.editor_ctx
|
||||
.editor
|
||||
.set_completion_enabled(self.ui.show_completion);
|
||||
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
|
||||
self.editor_ctx.editor.set_sample_folders(tree.all_folder_names());
|
||||
if self.editor_ctx.show_stack {
|
||||
crate::services::stack_preview::update_cache(&self.editor_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_editor_to_step(&mut self) {
|
||||
let text = self.editor_ctx.editor.content();
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let change = pattern_editor::set_step_script(
|
||||
&mut self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
self.editor_ctx.step,
|
||||
text,
|
||||
);
|
||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||
}
|
||||
|
||||
pub fn open_prelude_editor(&mut self) {
|
||||
let prelude = &self.project_state.project.prelude;
|
||||
let lines: Vec<String> = if prelude.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
prelude.lines().map(String::from).collect()
|
||||
};
|
||||
self.editor_ctx.editor.set_content(lines);
|
||||
self.editor_ctx.editor.set_candidates(COMPLETION_CANDIDATES.clone());
|
||||
self.editor_ctx
|
||||
.editor
|
||||
.set_completion_enabled(self.ui.show_completion);
|
||||
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
|
||||
self.editor_ctx.editor.set_sample_folders(tree.all_folder_names());
|
||||
self.editor_ctx.target = EditorTarget::Prelude;
|
||||
self.ui.modal = Modal::Editor;
|
||||
}
|
||||
|
||||
pub fn save_prelude(&mut self) {
|
||||
let text = self.editor_ctx.editor.content();
|
||||
self.project_state.project.prelude = text;
|
||||
}
|
||||
|
||||
pub fn close_prelude_editor(&mut self) {
|
||||
self.editor_ctx.target = EditorTarget::Step;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn evaluate_prelude(&mut self, link: &LinkState) {
|
||||
let prelude = &self.project_state.project.prelude;
|
||||
if prelude.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
let ctx = self.create_step_context(0, link);
|
||||
match self.script_engine.evaluate(prelude, &ctx) {
|
||||
Ok(_) => {
|
||||
self.ui.flash("Prelude evaluated", 150, FlashKind::Info);
|
||||
}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Prelude error: {e}"), 300, FlashKind::Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_script_oneshot(
|
||||
&self,
|
||||
script: &str,
|
||||
link: &LinkState,
|
||||
audio_tx: &arc_swap::ArcSwap<Sender<crate::engine::AudioCommand>>,
|
||||
) -> Result<(), String> {
|
||||
let ctx = self.create_step_context(self.editor_ctx.step, link);
|
||||
let cmds = self.script_engine.evaluate(script, &ctx)?;
|
||||
for cmd in cmds {
|
||||
let _ = audio_tx
|
||||
.load()
|
||||
.send(crate::engine::AudioCommand::Evaluate { cmd, time: None });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn compile_current_step(&mut self, link: &LinkState) {
|
||||
let step_idx = self.editor_ctx.step;
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
|
||||
let script =
|
||||
pattern_editor::get_step_script(&self.project_state.project, bank, pattern, step_idx)
|
||||
.unwrap_or_default();
|
||||
|
||||
if script.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ctx = self.create_step_context(step_idx, link);
|
||||
|
||||
match self.script_engine.evaluate(&script, &ctx) {
|
||||
Ok(_) => {
|
||||
self.ui.flash("Script compiled", 150, FlashKind::Info);
|
||||
}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Script error: {e}"), 300, FlashKind::Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compile_all_steps(&mut self, link: &LinkState) {
|
||||
let pattern_len = self.current_edit_pattern().length;
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
|
||||
for step_idx in 0..pattern_len {
|
||||
let script = pattern_editor::get_step_script(
|
||||
&self.project_state.project,
|
||||
bank,
|
||||
pattern,
|
||||
step_idx,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
if script.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ctx = self.create_step_context(step_idx, link);
|
||||
let _ = self.script_engine.evaluate(&script, &ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/app/sequencer.rs
Normal file
63
src/app/sequencer.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use crossbeam_channel::Sender;
|
||||
|
||||
use crate::engine::{PatternChange, PatternSnapshot, SeqCommand, StepSnapshot};
|
||||
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
pub fn flush_queued_changes(&mut self, cmd_tx: &Sender<SeqCommand>) {
|
||||
for staged in self.playback.queued_changes.drain(..) {
|
||||
match staged.change {
|
||||
PatternChange::Start { bank, pattern } => {
|
||||
let _ = cmd_tx.send(SeqCommand::PatternStart {
|
||||
bank,
|
||||
pattern,
|
||||
quantization: staged.quantization,
|
||||
sync_mode: staged.sync_mode,
|
||||
});
|
||||
}
|
||||
PatternChange::Stop { bank, pattern } => {
|
||||
let _ = cmd_tx.send(SeqCommand::PatternStop {
|
||||
bank,
|
||||
pattern,
|
||||
quantization: staged.quantization,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_mute_state(&self, cmd_tx: &Sender<SeqCommand>) {
|
||||
let _ = cmd_tx.send(SeqCommand::SetMuteState {
|
||||
muted: self.mute.muted.clone(),
|
||||
soloed: self.mute.soloed.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender<SeqCommand>) {
|
||||
for (bank, pattern) in self.project_state.take_dirty() {
|
||||
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||
let snapshot = PatternSnapshot {
|
||||
speed: pat.speed,
|
||||
length: pat.length,
|
||||
steps: pat
|
||||
.steps
|
||||
.iter()
|
||||
.take(pat.length)
|
||||
.map(|s| StepSnapshot {
|
||||
active: s.active,
|
||||
script: s.script.clone(),
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
};
|
||||
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
bank,
|
||||
pattern,
|
||||
data: snapshot,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/app/staging.rs
Normal file
115
src/app/staging.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use crate::engine::{PatternChange, SequencerSnapshot};
|
||||
use crate::state::StagedChange;
|
||||
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
pub fn stage_pattern_toggle(
|
||||
&mut self,
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
snapshot: &SequencerSnapshot,
|
||||
) {
|
||||
let is_playing = snapshot.is_playing(bank, pattern);
|
||||
let pattern_data = self.project_state.project.pattern_at(bank, pattern);
|
||||
|
||||
let existing = self.playback.staged_changes.iter().position(|c| {
|
||||
c.change.pattern_id().bank == bank && c.change.pattern_id().pattern == pattern
|
||||
});
|
||||
|
||||
if let Some(idx) = existing {
|
||||
self.playback.staged_changes.remove(idx);
|
||||
self.ui
|
||||
.set_status(format!("B{:02}:P{:02} unstaged", bank + 1, pattern + 1));
|
||||
} else if is_playing {
|
||||
self.playback.staged_changes.push(StagedChange {
|
||||
change: PatternChange::Stop { bank, pattern },
|
||||
quantization: pattern_data.quantization,
|
||||
sync_mode: pattern_data.sync_mode,
|
||||
});
|
||||
self.ui.set_status(format!(
|
||||
"B{:02}:P{:02} staged to stop",
|
||||
bank + 1,
|
||||
pattern + 1
|
||||
));
|
||||
} else {
|
||||
self.playback.staged_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
quantization: pattern_data.quantization,
|
||||
sync_mode: pattern_data.sync_mode,
|
||||
});
|
||||
self.ui.set_status(format!(
|
||||
"B{:02}:P{:02} staged to play",
|
||||
bank + 1,
|
||||
pattern + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Commits staged pattern, mute/solo, and prop changes.
|
||||
/// Returns true if mute state changed (caller should send to sequencer).
|
||||
pub fn commit_staged_changes(&mut self) -> bool {
|
||||
let pattern_count = self.playback.staged_changes.len();
|
||||
let mute_count = self.playback.staged_mute_changes.len();
|
||||
let prop_count = self.playback.staged_prop_changes.len();
|
||||
|
||||
if pattern_count == 0 && mute_count == 0 && prop_count == 0 {
|
||||
self.ui.set_status("No changes to commit".to_string());
|
||||
return false;
|
||||
}
|
||||
|
||||
if pattern_count > 0 {
|
||||
self.playback
|
||||
.queued_changes
|
||||
.append(&mut self.playback.staged_changes);
|
||||
}
|
||||
|
||||
let mute_changed = mute_count > 0;
|
||||
for change in self.playback.staged_mute_changes.drain() {
|
||||
match change {
|
||||
crate::state::StagedMuteChange::ToggleMute { bank, pattern } => {
|
||||
self.mute.toggle_mute(bank, pattern);
|
||||
}
|
||||
crate::state::StagedMuteChange::ToggleSolo { bank, pattern } => {
|
||||
self.mute.toggle_solo(bank, pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ((bank, pattern), props) in self.playback.staged_prop_changes.drain() {
|
||||
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
||||
pat.name = props.name;
|
||||
if let Some(len) = props.length {
|
||||
pat.set_length(len);
|
||||
}
|
||||
pat.speed = props.speed;
|
||||
pat.quantization = props.quantization;
|
||||
pat.sync_mode = props.sync_mode;
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
}
|
||||
|
||||
let total = pattern_count + mute_count + prop_count;
|
||||
let status = format!("Committed {total} changes");
|
||||
self.ui.set_status(status);
|
||||
|
||||
mute_changed
|
||||
}
|
||||
|
||||
pub fn clear_staged_changes(&mut self) {
|
||||
let pattern_count = self.playback.staged_changes.len();
|
||||
let mute_count = self.playback.staged_mute_changes.len();
|
||||
let prop_count = self.playback.staged_prop_changes.len();
|
||||
|
||||
if pattern_count == 0 && mute_count == 0 && prop_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.playback.staged_changes.clear();
|
||||
self.playback.staged_mute_changes.clear();
|
||||
self.playback.staged_prop_changes.clear();
|
||||
|
||||
let total = pattern_count + mute_count + prop_count;
|
||||
let status = format!("Cleared {total} staged changes");
|
||||
self.ui.set_status(status);
|
||||
}
|
||||
}
|
||||
88
src/app/undo.rs
Normal file
88
src/app/undo.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use crate::commands::AppCommand;
|
||||
use crate::state::undo::{UndoEntry, UndoScope};
|
||||
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
pub(super) fn undoable_scope(&self, cmd: &AppCommand) -> Option<UndoScope> {
|
||||
match cmd {
|
||||
AppCommand::ToggleSteps
|
||||
| AppCommand::LengthIncrease
|
||||
| AppCommand::LengthDecrease
|
||||
| AppCommand::SpeedIncrease
|
||||
| AppCommand::SpeedDecrease
|
||||
| AppCommand::SaveEditorToStep
|
||||
| AppCommand::CompileCurrentStep
|
||||
| AppCommand::HardenSteps
|
||||
| AppCommand::PasteSteps
|
||||
| AppCommand::LinkPasteSteps
|
||||
| AppCommand::DuplicateSteps
|
||||
| AppCommand::CopySteps => {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let data = self.project_state.project.pattern_at(bank, pattern).clone();
|
||||
Some(UndoScope::Pattern { bank, pattern, data })
|
||||
}
|
||||
AppCommand::SetLength { bank, pattern, .. }
|
||||
| AppCommand::SetSpeed { bank, pattern, .. }
|
||||
| AppCommand::DeleteStep { bank, pattern, .. }
|
||||
| AppCommand::DeleteSteps { bank, pattern, .. }
|
||||
| AppCommand::ResetPattern { bank, pattern }
|
||||
| AppCommand::PastePattern { bank, pattern }
|
||||
| AppCommand::RenamePattern { bank, pattern, .. } => {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
AppCommand::RenameStep { bank, pattern, .. } => {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
AppCommand::ApplyEuclideanDistribution { bank, pattern, .. } => {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
AppCommand::ResetBank { bank } | AppCommand::PasteBank { bank } => {
|
||||
let data = self.project_state.project.banks[*bank].clone();
|
||||
Some(UndoScope::Bank { bank: *bank, data })
|
||||
}
|
||||
AppCommand::ShiftPatternsUp | AppCommand::ShiftPatternsDown => {
|
||||
let bank = self.patterns_nav.bank_cursor;
|
||||
let data = self.project_state.project.banks[bank].clone();
|
||||
Some(UndoScope::Bank { bank, data })
|
||||
}
|
||||
AppCommand::PastePatterns { bank, .. }
|
||||
| AppCommand::ResetPatterns { bank, .. }
|
||||
| AppCommand::RenameBank { bank, .. } => {
|
||||
let data = self.project_state.project.banks[*bank].clone();
|
||||
Some(UndoScope::Bank { bank: *bank, data })
|
||||
}
|
||||
AppCommand::PasteBanks { .. } | AppCommand::ResetBanks { .. } => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn apply_undo_entry(&mut self, entry: UndoEntry) -> UndoEntry {
|
||||
let cursor = (self.editor_ctx.bank, self.editor_ctx.pattern, self.editor_ctx.step);
|
||||
let reverse_scope = match entry.scope {
|
||||
UndoScope::Pattern { bank, pattern, data } => {
|
||||
let current = self.project_state.project.pattern_at(bank, pattern).clone();
|
||||
*self.project_state.project.pattern_at_mut(bank, pattern) = data;
|
||||
self.project_state.mark_dirty(bank, pattern);
|
||||
UndoScope::Pattern { bank, pattern, data: current }
|
||||
}
|
||||
UndoScope::Bank { bank, data } => {
|
||||
let current = self.project_state.project.banks[bank].clone();
|
||||
let pat_count = current.patterns.len();
|
||||
self.project_state.project.banks[bank] = data;
|
||||
for p in 0..pat_count {
|
||||
self.project_state.mark_dirty(bank, p);
|
||||
}
|
||||
UndoScope::Bank { bank, data: current }
|
||||
}
|
||||
};
|
||||
self.editor_ctx.bank = entry.cursor.0;
|
||||
self.editor_ctx.pattern = entry.cursor.1;
|
||||
self.editor_ctx.step = entry.cursor.2;
|
||||
self.load_step_to_editor();
|
||||
UndoEntry { scope: reverse_scope, cursor }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user