Refactoring
This commit is contained in:
336
seq/src/app.rs
336
seq/src/app.rs
@@ -1,49 +1,43 @@
|
|||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use crate::config::{MAX_BANKS, MAX_PATTERNS, MAX_SLOTS};
|
use crate::commands::AppCommand;
|
||||||
|
use crate::config::MAX_SLOTS;
|
||||||
use crate::file;
|
use crate::file;
|
||||||
use crate::link::LinkState;
|
use crate::link::LinkState;
|
||||||
use crate::model::{Pattern, Project};
|
use crate::model::Pattern;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::script::{Rng, ScriptEngine, StepContext, Variables};
|
use crate::script::{Rng, ScriptEngine, StepContext, Variables};
|
||||||
use crate::sequencer::{SequencerSnapshot, SlotChange};
|
use crate::sequencer::{SequencerSnapshot, SlotChange};
|
||||||
use crate::services::pattern_editor;
|
use crate::services::pattern_editor;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
AudioSettings, EditorContext, Focus, Metrics, Modal, PatternField, PatternsViewLevel,
|
AudioSettings, EditorContext, Focus, Metrics, Modal, PatternField, PatternsViewLevel,
|
||||||
|
PlaybackState, ProjectState, UiState,
|
||||||
};
|
};
|
||||||
|
use crate::views::doc_view;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub playing: bool,
|
pub project_state: ProjectState,
|
||||||
|
pub ui: UiState,
|
||||||
|
pub playback: PlaybackState,
|
||||||
|
|
||||||
pub project: Project,
|
|
||||||
pub page: Page,
|
pub page: Page,
|
||||||
pub editor_ctx: EditorContext,
|
pub editor_ctx: EditorContext,
|
||||||
|
|
||||||
pub patterns_view_level: PatternsViewLevel,
|
pub patterns_view_level: PatternsViewLevel,
|
||||||
pub patterns_cursor: usize,
|
pub patterns_cursor: usize,
|
||||||
|
|
||||||
pub queued_changes: Vec<SlotChange>,
|
|
||||||
|
|
||||||
pub metrics: Metrics,
|
pub metrics: Metrics,
|
||||||
pub sample_pool_mb: f32,
|
pub sample_pool_mb: f32,
|
||||||
pub script_engine: ScriptEngine,
|
pub script_engine: ScriptEngine,
|
||||||
pub variables: Variables,
|
pub variables: Variables,
|
||||||
pub rng: Rng,
|
pub rng: Rng,
|
||||||
pub file_path: Option<PathBuf>,
|
|
||||||
pub status_message: Option<String>,
|
|
||||||
pub flash_until: Option<Instant>,
|
|
||||||
pub modal: Modal,
|
|
||||||
pub clipboard: Option<arboard::Clipboard>,
|
pub clipboard: Option<arboard::Clipboard>,
|
||||||
pub doc_topic: usize,
|
|
||||||
pub doc_scroll: usize,
|
|
||||||
|
|
||||||
pub audio: AudioSettings,
|
pub audio: AudioSettings,
|
||||||
pub dirty_patterns: HashSet<(usize, usize)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -53,32 +47,24 @@ impl App {
|
|||||||
let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&rng));
|
let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&rng));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
playing: true,
|
project_state: ProjectState::default(),
|
||||||
|
ui: UiState::default(),
|
||||||
|
playback: PlaybackState::default(),
|
||||||
|
|
||||||
project: Project::default(),
|
|
||||||
page: Page::default(),
|
page: Page::default(),
|
||||||
editor_ctx: EditorContext::default(),
|
editor_ctx: EditorContext::default(),
|
||||||
|
|
||||||
patterns_view_level: PatternsViewLevel::default(),
|
patterns_view_level: PatternsViewLevel::default(),
|
||||||
patterns_cursor: 0,
|
patterns_cursor: 0,
|
||||||
|
|
||||||
queued_changes: Vec::new(),
|
|
||||||
|
|
||||||
metrics: Metrics::default(),
|
metrics: Metrics::default(),
|
||||||
sample_pool_mb: 0.0,
|
sample_pool_mb: 0.0,
|
||||||
variables,
|
variables,
|
||||||
rng,
|
rng,
|
||||||
script_engine,
|
script_engine,
|
||||||
file_path: None,
|
|
||||||
status_message: None,
|
|
||||||
flash_until: None,
|
|
||||||
modal: Modal::None,
|
|
||||||
clipboard: arboard::Clipboard::new().ok(),
|
clipboard: arboard::Clipboard::new().ok(),
|
||||||
doc_topic: 0,
|
|
||||||
doc_scroll: 0,
|
|
||||||
|
|
||||||
audio: AudioSettings::default(),
|
audio: AudioSettings::default(),
|
||||||
dirty_patterns: HashSet::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,20 +72,12 @@ impl App {
|
|||||||
(self.editor_ctx.bank, self.editor_ctx.pattern)
|
(self.editor_ctx.bank, self.editor_ctx.pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_current_dirty(&mut self) {
|
|
||||||
self.dirty_patterns.insert(self.current_bank_pattern());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mark_all_patterns_dirty(&mut self) {
|
pub fn mark_all_patterns_dirty(&mut self) {
|
||||||
for bank in 0..MAX_BANKS {
|
self.project_state.mark_all_dirty();
|
||||||
for pattern in 0..MAX_PATTERNS {
|
|
||||||
self.dirty_patterns.insert((bank, pattern));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_playing(&mut self) {
|
pub fn toggle_playing(&mut self) {
|
||||||
self.playing = !self.playing;
|
self.playback.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tempo_up(&self, link: &LinkState) {
|
pub fn tempo_up(&self, link: &LinkState) {
|
||||||
@@ -128,7 +106,7 @@ impl App {
|
|||||||
|
|
||||||
pub fn current_edit_pattern(&self) -> &Pattern {
|
pub fn current_edit_pattern(&self) -> &Pattern {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
self.project.pattern_at(bank, pattern)
|
self.project_state.project.pattern_at(bank, pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_step(&mut self) {
|
pub fn next_step(&mut self) {
|
||||||
@@ -177,44 +155,53 @@ impl App {
|
|||||||
|
|
||||||
pub fn toggle_step(&mut self) {
|
pub fn toggle_step(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
pattern_editor::toggle_step(&mut self.project, bank, pattern, self.editor_ctx.step);
|
let change = pattern_editor::toggle_step(
|
||||||
self.mark_current_dirty();
|
&mut self.project_state.project,
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
self.editor_ctx.step,
|
||||||
|
);
|
||||||
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn length_increase(&mut self) {
|
pub fn length_increase(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
pattern_editor::increase_length(&mut self.project, bank, pattern);
|
let (change, _) =
|
||||||
self.mark_current_dirty();
|
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) {
|
pub fn length_decrease(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
pattern_editor::decrease_length(&mut self.project, bank, pattern);
|
let (change, new_len) =
|
||||||
let new_len = pattern_editor::get_length(&self.project, bank, pattern);
|
pattern_editor::decrease_length(&mut self.project_state.project, bank, pattern);
|
||||||
if self.editor_ctx.step >= new_len {
|
if self.editor_ctx.step >= new_len {
|
||||||
self.editor_ctx.step = new_len - 1;
|
self.editor_ctx.step = new_len - 1;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
self.mark_current_dirty();
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn speed_increase(&mut self) {
|
pub fn speed_increase(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
pattern_editor::increase_speed(&mut self.project, bank, pattern);
|
let change = pattern_editor::increase_speed(&mut self.project_state.project, bank, pattern);
|
||||||
self.mark_current_dirty();
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn speed_decrease(&mut self) {
|
pub fn speed_decrease(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
pattern_editor::decrease_speed(&mut self.project, bank, pattern);
|
let change = pattern_editor::decrease_speed(&mut self.project_state.project, bank, pattern);
|
||||||
self.mark_current_dirty();
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_step_to_editor(&mut self) {
|
fn load_step_to_editor(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
if let Some(script) =
|
if let Some(script) = pattern_editor::get_step_script(
|
||||||
pattern_editor::get_step_script(&self.project, bank, pattern, self.editor_ctx.step)
|
&self.project_state.project,
|
||||||
{
|
bank,
|
||||||
|
pattern,
|
||||||
|
self.editor_ctx.step,
|
||||||
|
) {
|
||||||
let lines: Vec<String> = if script.is_empty() {
|
let lines: Vec<String> = if script.is_empty() {
|
||||||
vec![String::new()]
|
vec![String::new()]
|
||||||
} else {
|
} else {
|
||||||
@@ -227,25 +214,27 @@ impl App {
|
|||||||
pub fn save_editor_to_step(&mut self) {
|
pub fn save_editor_to_step(&mut self) {
|
||||||
let text = self.editor_ctx.text.lines().join("\n");
|
let text = self.editor_ctx.text.lines().join("\n");
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
pattern_editor::set_step_script(
|
let change = pattern_editor::set_step_script(
|
||||||
&mut self.project,
|
&mut self.project_state.project,
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
self.editor_ctx.step,
|
self.editor_ctx.step,
|
||||||
text,
|
text,
|
||||||
);
|
);
|
||||||
self.mark_current_dirty();
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile_current_step(&mut self, link: &LinkState) {
|
pub fn compile_current_step(&mut self, link: &LinkState) {
|
||||||
let step_idx = self.editor_ctx.step;
|
let step_idx = self.editor_ctx.step;
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
|
|
||||||
let script = pattern_editor::get_step_script(&self.project, bank, pattern, step_idx)
|
let script =
|
||||||
|
pattern_editor::get_step_script(&self.project_state.project, bank, pattern, step_idx)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if script.trim().is_empty() {
|
if script.trim().is_empty() {
|
||||||
if let Some(step) = self
|
if let Some(step) = self
|
||||||
|
.project_state
|
||||||
.project
|
.project
|
||||||
.pattern_at_mut(bank, pattern)
|
.pattern_at_mut(bank, pattern)
|
||||||
.step_mut(step_idx)
|
.step_mut(step_idx)
|
||||||
@@ -268,24 +257,25 @@ impl App {
|
|||||||
match self.script_engine.evaluate(&script, &ctx) {
|
match self.script_engine.evaluate(&script, &ctx) {
|
||||||
Ok(cmd) => {
|
Ok(cmd) => {
|
||||||
if let Some(step) = self
|
if let Some(step) = self
|
||||||
|
.project_state
|
||||||
.project
|
.project
|
||||||
.pattern_at_mut(bank, pattern)
|
.pattern_at_mut(bank, pattern)
|
||||||
.step_mut(step_idx)
|
.step_mut(step_idx)
|
||||||
{
|
{
|
||||||
step.command = Some(cmd);
|
step.command = Some(cmd);
|
||||||
}
|
}
|
||||||
self.status_message = Some("Script compiled".to_string());
|
self.ui.flash("Script compiled", 150);
|
||||||
self.flash_until = Some(Instant::now() + std::time::Duration::from_millis(150));
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Some(step) = self
|
if let Some(step) = self
|
||||||
|
.project_state
|
||||||
.project
|
.project
|
||||||
.pattern_at_mut(bank, pattern)
|
.pattern_at_mut(bank, pattern)
|
||||||
.step_mut(step_idx)
|
.step_mut(step_idx)
|
||||||
{
|
{
|
||||||
step.command = None;
|
step.command = None;
|
||||||
}
|
}
|
||||||
self.status_message = Some(format!("Script error: {e}"));
|
self.ui.set_status(format!("Script error: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,11 +285,17 @@ impl App {
|
|||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
|
|
||||||
for step_idx in 0..pattern_len {
|
for step_idx in 0..pattern_len {
|
||||||
let script = pattern_editor::get_step_script(&self.project, bank, pattern, step_idx)
|
let script = pattern_editor::get_step_script(
|
||||||
|
&self.project_state.project,
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
step_idx,
|
||||||
|
)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if script.trim().is_empty() {
|
if script.trim().is_empty() {
|
||||||
if let Some(step) = self
|
if let Some(step) = self
|
||||||
|
.project_state
|
||||||
.project
|
.project
|
||||||
.pattern_at_mut(bank, pattern)
|
.pattern_at_mut(bank, pattern)
|
||||||
.step_mut(step_idx)
|
.step_mut(step_idx)
|
||||||
@@ -321,6 +317,7 @@ impl App {
|
|||||||
|
|
||||||
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx) {
|
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx) {
|
||||||
if let Some(step) = self
|
if let Some(step) = self
|
||||||
|
.project_state
|
||||||
.project
|
.project
|
||||||
.pattern_at_mut(bank, pattern)
|
.pattern_at_mut(bank, pattern)
|
||||||
.step_mut(step_idx)
|
.step_mut(step_idx)
|
||||||
@@ -337,7 +334,7 @@ impl App {
|
|||||||
pattern: usize,
|
pattern: usize,
|
||||||
snapshot: &SequencerSnapshot,
|
snapshot: &SequencerSnapshot,
|
||||||
) -> Option<bool> {
|
) -> Option<bool> {
|
||||||
self.queued_changes.iter().find_map(|c| match c {
|
self.playback.queued_changes.iter().find_map(|c| match c {
|
||||||
SlotChange::Add {
|
SlotChange::Add {
|
||||||
slot: _,
|
slot: _,
|
||||||
bank: b,
|
bank: b,
|
||||||
@@ -369,7 +366,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let pending = self.queued_changes.iter().position(|c| match c {
|
let pending = self.playback.queued_changes.iter().position(|c| match c {
|
||||||
SlotChange::Add {
|
SlotChange::Add {
|
||||||
bank: b,
|
bank: b,
|
||||||
pattern: p,
|
pattern: p,
|
||||||
@@ -382,16 +379,17 @@ impl App {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(idx) = pending {
|
if let Some(idx) = pending {
|
||||||
self.queued_changes.remove(idx);
|
self.playback.queued_changes.remove(idx);
|
||||||
self.status_message = Some(format!(
|
self.ui.set_status(format!(
|
||||||
"B{:02}:P{:02} change cancelled",
|
"B{:02}:P{:02} change cancelled",
|
||||||
bank + 1,
|
bank + 1,
|
||||||
pattern + 1
|
pattern + 1
|
||||||
));
|
));
|
||||||
} else if let Some(slot_idx) = playing_slot {
|
} else if let Some(slot_idx) = playing_slot {
|
||||||
self.queued_changes
|
self.playback
|
||||||
|
.queued_changes
|
||||||
.push(SlotChange::Remove { slot: slot_idx });
|
.push(SlotChange::Remove { slot: slot_idx });
|
||||||
self.status_message = Some(format!(
|
self.ui.set_status(format!(
|
||||||
"B{:02}:P{:02} queued to stop",
|
"B{:02}:P{:02} queued to stop",
|
||||||
bank + 1,
|
bank + 1,
|
||||||
pattern + 1
|
pattern + 1
|
||||||
@@ -399,18 +397,18 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
let free_slot = (0..MAX_SLOTS).find(|&i| !snapshot.slot_data[i].active);
|
let free_slot = (0..MAX_SLOTS).find(|&i| !snapshot.slot_data[i].active);
|
||||||
if let Some(slot_idx) = free_slot {
|
if let Some(slot_idx) = free_slot {
|
||||||
self.queued_changes.push(SlotChange::Add {
|
self.playback.queued_changes.push(SlotChange::Add {
|
||||||
slot: slot_idx,
|
slot: slot_idx,
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
});
|
});
|
||||||
self.status_message = Some(format!(
|
self.ui.set_status(format!(
|
||||||
"B{:02}:P{:02} queued to play",
|
"B{:02}:P{:02} queued to play",
|
||||||
bank + 1,
|
bank + 1,
|
||||||
pattern + 1
|
pattern + 1
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
self.status_message = Some("All slots occupied".to_string());
|
self.ui.set_status("All slots occupied".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,13 +428,13 @@ impl App {
|
|||||||
|
|
||||||
pub fn save(&mut self, path: PathBuf) {
|
pub fn save(&mut self, path: PathBuf) {
|
||||||
self.save_editor_to_step();
|
self.save_editor_to_step();
|
||||||
match file::save(&self.project, &path) {
|
match file::save(&self.project_state.project, &path) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.status_message = Some(format!("Saved: {}", path.display()));
|
self.ui.set_status(format!("Saved: {}", path.display()));
|
||||||
self.file_path = Some(path);
|
self.project_state.file_path = Some(path);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.status_message = Some(format!("Save error: {e}"));
|
self.ui.set_status(format!("Save error: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -444,39 +442,33 @@ impl App {
|
|||||||
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
|
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
|
||||||
match file::load(&path) {
|
match file::load(&path) {
|
||||||
Ok(project) => {
|
Ok(project) => {
|
||||||
self.project = project;
|
self.project_state.project = project;
|
||||||
self.editor_ctx.step = 0;
|
self.editor_ctx.step = 0;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
self.compile_all_steps(link);
|
self.compile_all_steps(link);
|
||||||
self.mark_all_patterns_dirty();
|
self.mark_all_patterns_dirty();
|
||||||
self.status_message = Some(format!("Loaded: {}", path.display()));
|
self.ui.set_status(format!("Loaded: {}", path.display()));
|
||||||
self.file_path = Some(path);
|
self.project_state.file_path = Some(path);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.status_message = Some(format!("Load error: {e}"));
|
self.ui.set_status(format!("Load error: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_status(&mut self) {
|
|
||||||
self.status_message = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_flashing(&self) -> bool {
|
|
||||||
self.flash_until
|
|
||||||
.map(|t| Instant::now() < t)
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn copy_step(&mut self) {
|
pub fn copy_step(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
let script =
|
let script = pattern_editor::get_step_script(
|
||||||
pattern_editor::get_step_script(&self.project, bank, pattern, self.editor_ctx.step);
|
&self.project_state.project,
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
self.editor_ctx.step,
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(script) = script {
|
if let Some(script) = script {
|
||||||
if let Some(clip) = &mut self.clipboard {
|
if let Some(clip) = &mut self.clipboard {
|
||||||
if clip.set_text(&script).is_ok() {
|
if clip.set_text(&script).is_ok() {
|
||||||
self.status_message = Some("Copied".to_string());
|
self.ui.set_status("Copied".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -490,14 +482,14 @@ impl App {
|
|||||||
|
|
||||||
if let Some(text) = text {
|
if let Some(text) = text {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
pattern_editor::set_step_script(
|
let change = pattern_editor::set_step_script(
|
||||||
&mut self.project,
|
&mut self.project_state.project,
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
self.editor_ctx.step,
|
self.editor_ctx.step,
|
||||||
text,
|
text,
|
||||||
);
|
);
|
||||||
self.mark_current_dirty();
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
self.compile_current_step(link);
|
self.compile_current_step(link);
|
||||||
}
|
}
|
||||||
@@ -508,9 +500,167 @@ impl App {
|
|||||||
PatternField::Length => self.current_edit_pattern().length.to_string(),
|
PatternField::Length => self.current_edit_pattern().length.to_string(),
|
||||||
PatternField::Speed => self.current_edit_pattern().speed.label().to_string(),
|
PatternField::Speed => self.current_edit_pattern().speed.label().to_string(),
|
||||||
};
|
};
|
||||||
self.modal = Modal::SetPattern {
|
self.ui.modal = Modal::SetPattern {
|
||||||
field,
|
field,
|
||||||
input: current,
|
input: current,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||||
|
match cmd {
|
||||||
|
// Playback
|
||||||
|
AppCommand::TogglePlaying => self.toggle_playing(),
|
||||||
|
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(),
|
||||||
|
AppCommand::ToggleFocus => self.toggle_focus(link),
|
||||||
|
AppCommand::SelectEditBank(bank) => self.select_edit_bank(bank),
|
||||||
|
AppCommand::SelectEditPattern(pattern) => self.select_edit_pattern(pattern),
|
||||||
|
|
||||||
|
// Pattern editing
|
||||||
|
AppCommand::ToggleStep => self.toggle_step(),
|
||||||
|
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::CompileAllSteps => self.compile_all_steps(link),
|
||||||
|
|
||||||
|
// Clipboard
|
||||||
|
AppCommand::CopyStep => self.copy_step(),
|
||||||
|
AppCommand::PasteStep => self.paste_step(link),
|
||||||
|
|
||||||
|
// Pattern playback
|
||||||
|
AppCommand::QueueSlotChange(change) => {
|
||||||
|
self.playback.queued_changes.push(change);
|
||||||
|
}
|
||||||
|
AppCommand::TogglePatternPlayback { bank, pattern } => {
|
||||||
|
self.toggle_pattern_playback(bank, pattern, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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::Save(path) => self.save(path),
|
||||||
|
AppCommand::Load(path) => self.load(path, link),
|
||||||
|
|
||||||
|
// UI
|
||||||
|
AppCommand::SetStatus(msg) => self.ui.set_status(msg),
|
||||||
|
AppCommand::ClearStatus => self.ui.clear_status(),
|
||||||
|
AppCommand::Flash {
|
||||||
|
message,
|
||||||
|
duration_ms,
|
||||||
|
} => self.ui.flash(&message, duration_ms),
|
||||||
|
AppCommand::OpenModal(modal) => self.ui.modal = modal,
|
||||||
|
AppCommand::CloseModal => self.ui.modal = Modal::None,
|
||||||
|
AppCommand::OpenPatternModal(field) => self.open_pattern_modal(field),
|
||||||
|
|
||||||
|
// Page navigation
|
||||||
|
AppCommand::PageLeft => self.page.left(),
|
||||||
|
AppCommand::PageRight => self.page.right(),
|
||||||
|
AppCommand::PageUp => self.page.up(),
|
||||||
|
AppCommand::PageDown => self.page.down(),
|
||||||
|
|
||||||
|
// Doc navigation
|
||||||
|
AppCommand::DocNextTopic => {
|
||||||
|
self.ui.doc_topic = (self.ui.doc_topic + 1) % doc_view::topic_count();
|
||||||
|
self.ui.doc_scroll = 0;
|
||||||
|
}
|
||||||
|
AppCommand::DocPrevTopic => {
|
||||||
|
let count = doc_view::topic_count();
|
||||||
|
self.ui.doc_topic = (self.ui.doc_topic + count - 1) % count;
|
||||||
|
self.ui.doc_scroll = 0;
|
||||||
|
}
|
||||||
|
AppCommand::DocScrollDown(n) => {
|
||||||
|
self.ui.doc_scroll = self.ui.doc_scroll.saturating_add(n);
|
||||||
|
}
|
||||||
|
AppCommand::DocScrollUp(n) => {
|
||||||
|
self.ui.doc_scroll = self.ui.doc_scroll.saturating_sub(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patterns view
|
||||||
|
AppCommand::PatternsCursorLeft => {
|
||||||
|
self.patterns_cursor = (self.patterns_cursor + 15) % 16;
|
||||||
|
}
|
||||||
|
AppCommand::PatternsCursorRight => {
|
||||||
|
self.patterns_cursor = (self.patterns_cursor + 1) % 16;
|
||||||
|
}
|
||||||
|
AppCommand::PatternsCursorUp => {
|
||||||
|
self.patterns_cursor = (self.patterns_cursor + 12) % 16;
|
||||||
|
}
|
||||||
|
AppCommand::PatternsCursorDown => {
|
||||||
|
self.patterns_cursor = (self.patterns_cursor + 4) % 16;
|
||||||
|
}
|
||||||
|
AppCommand::PatternsEnter => match self.patterns_view_level {
|
||||||
|
PatternsViewLevel::Banks => {
|
||||||
|
let bank = self.patterns_cursor;
|
||||||
|
self.patterns_view_level = PatternsViewLevel::Patterns { bank };
|
||||||
|
self.patterns_cursor = 0;
|
||||||
|
}
|
||||||
|
PatternsViewLevel::Patterns { bank } => {
|
||||||
|
let pattern = self.patterns_cursor;
|
||||||
|
self.select_edit_bank(bank);
|
||||||
|
self.select_edit_pattern(pattern);
|
||||||
|
self.patterns_view_level = PatternsViewLevel::Banks;
|
||||||
|
self.patterns_cursor = 0;
|
||||||
|
self.page.down();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AppCommand::PatternsBack => match self.patterns_view_level {
|
||||||
|
PatternsViewLevel::Banks => self.page.down(),
|
||||||
|
PatternsViewLevel::Patterns { .. } => {
|
||||||
|
self.patterns_view_level = PatternsViewLevel::Banks;
|
||||||
|
self.patterns_cursor = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
98
seq/src/commands.rs
Normal file
98
seq/src/commands.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::model::PatternSpeed;
|
||||||
|
use crate::sequencer::SlotChange;
|
||||||
|
use crate::state::{Modal, PatternField};
|
||||||
|
|
||||||
|
pub enum AppCommand {
|
||||||
|
// Playback
|
||||||
|
TogglePlaying,
|
||||||
|
TempoUp,
|
||||||
|
TempoDown,
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
NextStep,
|
||||||
|
PrevStep,
|
||||||
|
StepUp,
|
||||||
|
StepDown,
|
||||||
|
ToggleFocus,
|
||||||
|
SelectEditBank(usize),
|
||||||
|
SelectEditPattern(usize),
|
||||||
|
|
||||||
|
// Pattern editing
|
||||||
|
ToggleStep,
|
||||||
|
LengthIncrease,
|
||||||
|
LengthDecrease,
|
||||||
|
SpeedIncrease,
|
||||||
|
SpeedDecrease,
|
||||||
|
SetLength {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
length: usize,
|
||||||
|
},
|
||||||
|
SetSpeed {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
speed: PatternSpeed,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Script editing
|
||||||
|
SaveEditorToStep,
|
||||||
|
CompileCurrentStep,
|
||||||
|
CompileAllSteps,
|
||||||
|
|
||||||
|
// Clipboard
|
||||||
|
CopyStep,
|
||||||
|
PasteStep,
|
||||||
|
|
||||||
|
// Pattern playback
|
||||||
|
QueueSlotChange(SlotChange),
|
||||||
|
TogglePatternPlayback {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Project
|
||||||
|
RenameBank {
|
||||||
|
bank: usize,
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
|
RenamePattern {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
|
Save(PathBuf),
|
||||||
|
Load(PathBuf),
|
||||||
|
|
||||||
|
// UI
|
||||||
|
SetStatus(String),
|
||||||
|
ClearStatus,
|
||||||
|
Flash {
|
||||||
|
message: String,
|
||||||
|
duration_ms: u64,
|
||||||
|
},
|
||||||
|
OpenModal(Modal),
|
||||||
|
CloseModal,
|
||||||
|
OpenPatternModal(PatternField),
|
||||||
|
|
||||||
|
// Page navigation
|
||||||
|
PageLeft,
|
||||||
|
PageRight,
|
||||||
|
PageUp,
|
||||||
|
PageDown,
|
||||||
|
|
||||||
|
// Doc navigation
|
||||||
|
DocNextTopic,
|
||||||
|
DocPrevTopic,
|
||||||
|
DocScrollDown(usize),
|
||||||
|
DocScrollUp(usize),
|
||||||
|
|
||||||
|
// Patterns view
|
||||||
|
PatternsCursorLeft,
|
||||||
|
PatternsCursorRight,
|
||||||
|
PatternsCursorUp,
|
||||||
|
PatternsCursorDown,
|
||||||
|
PatternsEnter,
|
||||||
|
PatternsBack,
|
||||||
|
}
|
||||||
262
seq/src/input.rs
262
seq/src/input.rs
@@ -5,12 +5,12 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
use crate::commands::AppCommand;
|
||||||
use crate::link::LinkState;
|
use crate::link::LinkState;
|
||||||
use crate::model::PatternSpeed;
|
use crate::model::PatternSpeed;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::sequencer::{AudioCommand, SequencerSnapshot};
|
use crate::sequencer::{AudioCommand, SequencerSnapshot};
|
||||||
use crate::state::{AudioFocus, Focus, Modal, PatternField, PatternsViewLevel};
|
use crate::state::{AudioFocus, Focus, Modal, PatternField, PatternsViewLevel};
|
||||||
use crate::views::doc_view;
|
|
||||||
|
|
||||||
pub enum InputResult {
|
pub enum InputResult {
|
||||||
Continue,
|
Continue,
|
||||||
@@ -25,10 +25,16 @@ pub struct InputContext<'a> {
|
|||||||
pub audio_tx: &'a Sender<AudioCommand>,
|
pub audio_tx: &'a Sender<AudioCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
impl<'a> InputContext<'a> {
|
||||||
ctx.app.clear_status();
|
fn dispatch(&mut self, cmd: AppCommand) {
|
||||||
|
self.app.dispatch(cmd, self.link, self.snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if matches!(ctx.app.modal, Modal::None) {
|
pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
|
ctx.dispatch(AppCommand::ClearStatus);
|
||||||
|
|
||||||
|
if matches!(ctx.app.ui.modal, Modal::None) {
|
||||||
handle_normal_input(ctx, key)
|
handle_normal_input(ctx, key)
|
||||||
} else {
|
} else {
|
||||||
handle_modal_input(ctx, key)
|
handle_modal_input(ctx, key)
|
||||||
@@ -36,11 +42,11 @@ pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
match &mut ctx.app.modal {
|
match &mut ctx.app.ui.modal {
|
||||||
Modal::ConfirmQuit { selected } => match key.code {
|
Modal::ConfirmQuit { selected } => match key.code {
|
||||||
KeyCode::Char('y') | KeyCode::Char('Y') => return InputResult::Quit,
|
KeyCode::Char('y') | KeyCode::Char('Y') => return InputResult::Quit,
|
||||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||||
ctx.app.modal = Modal::None;
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
}
|
}
|
||||||
KeyCode::Left | KeyCode::Right => {
|
KeyCode::Left | KeyCode::Right => {
|
||||||
*selected = !*selected;
|
*selected = !*selected;
|
||||||
@@ -49,7 +55,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
if *selected {
|
if *selected {
|
||||||
return InputResult::Quit;
|
return InputResult::Quit;
|
||||||
} else {
|
} else {
|
||||||
ctx.app.modal = Modal::None;
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -57,10 +63,10 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
Modal::SaveAs(path) => match key.code {
|
Modal::SaveAs(path) => match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let save_path = PathBuf::from(path.as_str());
|
let save_path = PathBuf::from(path.as_str());
|
||||||
ctx.app.modal = Modal::None;
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
ctx.app.save(save_path);
|
ctx.dispatch(AppCommand::Save(save_path));
|
||||||
}
|
}
|
||||||
KeyCode::Esc => ctx.app.modal = Modal::None,
|
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
path.pop();
|
path.pop();
|
||||||
}
|
}
|
||||||
@@ -70,10 +76,10 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
Modal::LoadFrom(path) => match key.code {
|
Modal::LoadFrom(path) => match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let load_path = PathBuf::from(path.as_str());
|
let load_path = PathBuf::from(path.as_str());
|
||||||
ctx.app.modal = Modal::None;
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
ctx.app.load(load_path, ctx.link);
|
ctx.dispatch(AppCommand::Load(load_path));
|
||||||
}
|
}
|
||||||
KeyCode::Esc => ctx.app.modal = Modal::None,
|
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
path.pop();
|
path.pop();
|
||||||
}
|
}
|
||||||
@@ -88,10 +94,13 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
} else {
|
} else {
|
||||||
Some(name.clone())
|
Some(name.clone())
|
||||||
};
|
};
|
||||||
ctx.app.project.banks[bank_idx].name = new_name;
|
ctx.dispatch(AppCommand::RenameBank {
|
||||||
ctx.app.modal = Modal::None;
|
bank: bank_idx,
|
||||||
|
name: new_name,
|
||||||
|
});
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
}
|
}
|
||||||
KeyCode::Esc => ctx.app.modal = Modal::None,
|
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
name.pop();
|
name.pop();
|
||||||
}
|
}
|
||||||
@@ -110,10 +119,14 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
} else {
|
} else {
|
||||||
Some(name.clone())
|
Some(name.clone())
|
||||||
};
|
};
|
||||||
ctx.app.project.banks[bank_idx].patterns[pattern_idx].name = new_name;
|
ctx.dispatch(AppCommand::RenamePattern {
|
||||||
ctx.app.modal = Modal::None;
|
bank: bank_idx,
|
||||||
|
pattern: pattern_idx,
|
||||||
|
name: new_name,
|
||||||
|
});
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
}
|
}
|
||||||
KeyCode::Esc => ctx.app.modal = Modal::None,
|
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
name.pop();
|
name.pop();
|
||||||
}
|
}
|
||||||
@@ -127,36 +140,43 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
match field {
|
match field {
|
||||||
PatternField::Length => {
|
PatternField::Length => {
|
||||||
if let Ok(len) = input.parse::<usize>() {
|
if let Ok(len) = input.parse::<usize>() {
|
||||||
ctx.app
|
ctx.dispatch(AppCommand::SetLength {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
length: len,
|
||||||
|
});
|
||||||
|
let new_len = ctx
|
||||||
|
.app
|
||||||
|
.project_state
|
||||||
.project
|
.project
|
||||||
.pattern_at_mut(bank, pattern)
|
.pattern_at(bank, pattern)
|
||||||
.set_length(len);
|
.length;
|
||||||
let new_len = ctx.app.project.pattern_at(bank, pattern).length;
|
ctx.dispatch(AppCommand::SetStatus(format!("Length set to {new_len}")));
|
||||||
if ctx.app.editor_ctx.step >= new_len {
|
|
||||||
ctx.app.editor_ctx.step = new_len - 1;
|
|
||||||
}
|
|
||||||
ctx.app.dirty_patterns.insert((bank, pattern));
|
|
||||||
ctx.app.status_message = Some(format!("Length set to {new_len}"));
|
|
||||||
} else {
|
} else {
|
||||||
ctx.app.status_message = Some("Invalid length".to_string());
|
ctx.dispatch(AppCommand::SetStatus("Invalid length".to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PatternField::Speed => {
|
PatternField::Speed => {
|
||||||
if let Some(speed) = PatternSpeed::from_label(input) {
|
if let Some(speed) = PatternSpeed::from_label(input) {
|
||||||
ctx.app.project.pattern_at_mut(bank, pattern).speed = speed;
|
ctx.dispatch(AppCommand::SetSpeed {
|
||||||
ctx.app.dirty_patterns.insert((bank, pattern));
|
bank,
|
||||||
ctx.app.status_message =
|
pattern,
|
||||||
Some(format!("Speed set to {}", speed.label()));
|
speed,
|
||||||
|
});
|
||||||
|
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||||
|
"Speed set to {}",
|
||||||
|
speed.label()
|
||||||
|
)));
|
||||||
} else {
|
} else {
|
||||||
ctx.app.status_message = Some(
|
ctx.dispatch(AppCommand::SetStatus(
|
||||||
"Invalid speed (try 1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x)".to_string(),
|
"Invalid speed (try 1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x)".to_string(),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.app.modal = Modal::None;
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
}
|
}
|
||||||
KeyCode::Esc => ctx.app.modal = Modal::None,
|
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
input.pop();
|
input.pop();
|
||||||
}
|
}
|
||||||
@@ -172,13 +192,13 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
let _ = ctx.audio_tx.send(AudioCommand::LoadSamples(index));
|
let _ = ctx.audio_tx.send(AudioCommand::LoadSamples(index));
|
||||||
ctx.app.audio.config.sample_count += count;
|
ctx.app.audio.config.sample_count += count;
|
||||||
ctx.app.audio.add_sample_path(sample_path);
|
ctx.app.audio.add_sample_path(sample_path);
|
||||||
ctx.app.status_message = Some(format!("Added {count} samples"));
|
ctx.dispatch(AppCommand::SetStatus(format!("Added {count} samples")));
|
||||||
} else {
|
} else {
|
||||||
ctx.app.status_message = Some("Path is not a directory".to_string());
|
ctx.dispatch(AppCommand::SetStatus("Path is not a directory".to_string()));
|
||||||
}
|
}
|
||||||
ctx.app.modal = Modal::None;
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
}
|
}
|
||||||
KeyCode::Esc => ctx.app.modal = Modal::None,
|
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||||
KeyCode::Backspace => {
|
KeyCode::Backspace => {
|
||||||
path.pop();
|
path.pop();
|
||||||
}
|
}
|
||||||
@@ -193,23 +213,22 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
|
||||||
// Global navigation with Ctrl+arrows
|
|
||||||
if ctrl {
|
if ctrl {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Left => {
|
KeyCode::Left => {
|
||||||
ctx.app.page.left();
|
ctx.dispatch(AppCommand::PageLeft);
|
||||||
return InputResult::Continue;
|
return InputResult::Continue;
|
||||||
}
|
}
|
||||||
KeyCode::Right => {
|
KeyCode::Right => {
|
||||||
ctx.app.page.right();
|
ctx.dispatch(AppCommand::PageRight);
|
||||||
return InputResult::Continue;
|
return InputResult::Continue;
|
||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
ctx.app.page.up();
|
ctx.dispatch(AppCommand::PageUp);
|
||||||
return InputResult::Continue;
|
return InputResult::Continue;
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
ctx.app.page.down();
|
ctx.dispatch(AppCommand::PageDown);
|
||||||
return InputResult::Continue;
|
return InputResult::Continue;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -228,47 +247,51 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
|||||||
match ctx.app.editor_ctx.focus {
|
match ctx.app.editor_ctx.focus {
|
||||||
Focus::Sequencer => match key.code {
|
Focus::Sequencer => match key.code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
ctx.app.modal = Modal::ConfirmQuit { selected: false };
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') => {
|
||||||
ctx.app.toggle_playing();
|
ctx.dispatch(AppCommand::TogglePlaying);
|
||||||
ctx.playing.store(ctx.app.playing, Ordering::Relaxed);
|
ctx.playing
|
||||||
|
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
KeyCode::Tab => ctx.app.toggle_focus(ctx.link),
|
KeyCode::Tab => ctx.dispatch(AppCommand::ToggleFocus),
|
||||||
KeyCode::Left => ctx.app.prev_step(),
|
KeyCode::Left => ctx.dispatch(AppCommand::PrevStep),
|
||||||
KeyCode::Right => ctx.app.next_step(),
|
KeyCode::Right => ctx.dispatch(AppCommand::NextStep),
|
||||||
KeyCode::Up => ctx.app.step_up(),
|
KeyCode::Up => ctx.dispatch(AppCommand::StepUp),
|
||||||
KeyCode::Down => ctx.app.step_down(),
|
KeyCode::Down => ctx.dispatch(AppCommand::StepDown),
|
||||||
KeyCode::Enter => ctx.app.toggle_step(),
|
KeyCode::Enter => ctx.dispatch(AppCommand::ToggleStep),
|
||||||
KeyCode::Char('s') => {
|
KeyCode::Char('s') => {
|
||||||
let default = ctx
|
let default = ctx
|
||||||
.app
|
.app
|
||||||
|
.project_state
|
||||||
.file_path
|
.file_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| p.display().to_string())
|
.map(|p| p.display().to_string())
|
||||||
.unwrap_or_else(|| "project.buboseq".to_string());
|
.unwrap_or_else(|| "project.buboseq".to_string());
|
||||||
ctx.app.modal = Modal::SaveAs(default);
|
ctx.dispatch(AppCommand::OpenModal(Modal::SaveAs(default)));
|
||||||
}
|
}
|
||||||
KeyCode::Char('l') => {
|
KeyCode::Char('l') => {
|
||||||
ctx.app.modal = Modal::LoadFrom(String::new());
|
ctx.dispatch(AppCommand::OpenModal(Modal::LoadFrom(String::new())));
|
||||||
}
|
}
|
||||||
KeyCode::Char('+') | KeyCode::Char('=') => ctx.app.tempo_up(ctx.link),
|
KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp),
|
||||||
KeyCode::Char('-') => ctx.app.tempo_down(ctx.link),
|
KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown),
|
||||||
KeyCode::Char('<') | KeyCode::Char(',') => ctx.app.length_decrease(),
|
KeyCode::Char('<') | KeyCode::Char(',') => ctx.dispatch(AppCommand::LengthDecrease),
|
||||||
KeyCode::Char('>') | KeyCode::Char('.') => ctx.app.length_increase(),
|
KeyCode::Char('>') | KeyCode::Char('.') => ctx.dispatch(AppCommand::LengthIncrease),
|
||||||
KeyCode::Char('[') => ctx.app.speed_decrease(),
|
KeyCode::Char('[') => ctx.dispatch(AppCommand::SpeedDecrease),
|
||||||
KeyCode::Char(']') => ctx.app.speed_increase(),
|
KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease),
|
||||||
KeyCode::Char('L') => ctx.app.open_pattern_modal(PatternField::Length),
|
KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)),
|
||||||
KeyCode::Char('S') => ctx.app.open_pattern_modal(PatternField::Speed),
|
KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)),
|
||||||
KeyCode::Char('c') if ctrl => ctx.app.copy_step(),
|
KeyCode::Char('c') if ctrl => ctx.dispatch(AppCommand::CopyStep),
|
||||||
KeyCode::Char('v') if ctrl => ctx.app.paste_step(ctx.link),
|
KeyCode::Char('v') if ctrl => ctx.dispatch(AppCommand::PasteStep),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Focus::Editor => match key.code {
|
Focus::Editor => match key.code {
|
||||||
KeyCode::Tab | KeyCode::Esc => ctx.app.toggle_focus(ctx.link),
|
KeyCode::Tab | KeyCode::Esc => ctx.dispatch(AppCommand::ToggleFocus),
|
||||||
KeyCode::Char('e') if ctrl => {
|
KeyCode::Char('e') if ctrl => {
|
||||||
ctx.app.save_editor_to_step();
|
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||||
ctx.app.compile_current_step(ctx.link);
|
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
ctx.app.editor_ctx.text.input(Event::Key(key));
|
ctx.app.editor_ctx.text.input(Event::Key(key));
|
||||||
@@ -280,61 +303,46 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
|||||||
|
|
||||||
fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Left => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 15) % 16,
|
KeyCode::Left => ctx.dispatch(AppCommand::PatternsCursorLeft),
|
||||||
KeyCode::Right => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 1) % 16,
|
KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight),
|
||||||
KeyCode::Up => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 12) % 16,
|
KeyCode::Up => ctx.dispatch(AppCommand::PatternsCursorUp),
|
||||||
KeyCode::Down => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 4) % 16,
|
KeyCode::Down => ctx.dispatch(AppCommand::PatternsCursorDown),
|
||||||
KeyCode::Esc | KeyCode::Backspace => match ctx.app.patterns_view_level {
|
KeyCode::Esc | KeyCode::Backspace => ctx.dispatch(AppCommand::PatternsBack),
|
||||||
PatternsViewLevel::Banks => ctx.app.page.down(),
|
KeyCode::Enter => ctx.dispatch(AppCommand::PatternsEnter),
|
||||||
PatternsViewLevel::Patterns { .. } => {
|
|
||||||
ctx.app.patterns_view_level = PatternsViewLevel::Banks;
|
|
||||||
ctx.app.patterns_cursor = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
KeyCode::Enter => match ctx.app.patterns_view_level {
|
|
||||||
PatternsViewLevel::Banks => {
|
|
||||||
let bank = ctx.app.patterns_cursor;
|
|
||||||
ctx.app.patterns_view_level = PatternsViewLevel::Patterns { bank };
|
|
||||||
ctx.app.patterns_cursor = 0;
|
|
||||||
}
|
|
||||||
PatternsViewLevel::Patterns { bank } => {
|
|
||||||
let pattern = ctx.app.patterns_cursor;
|
|
||||||
ctx.app.select_edit_bank(bank);
|
|
||||||
ctx.app.select_edit_pattern(pattern);
|
|
||||||
ctx.app.patterns_view_level = PatternsViewLevel::Banks;
|
|
||||||
ctx.app.patterns_cursor = 0;
|
|
||||||
ctx.app.page.down();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') => {
|
||||||
if let PatternsViewLevel::Patterns { bank } = ctx.app.patterns_view_level {
|
if let PatternsViewLevel::Patterns { bank } = ctx.app.patterns_view_level {
|
||||||
let pattern = ctx.app.patterns_cursor;
|
let pattern = ctx.app.patterns_cursor;
|
||||||
ctx.app.toggle_pattern_playback(bank, pattern, ctx.snapshot);
|
ctx.dispatch(AppCommand::TogglePatternPlayback { bank, pattern });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
ctx.app.modal = Modal::ConfirmQuit { selected: false };
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
KeyCode::Char('r') => match ctx.app.patterns_view_level {
|
KeyCode::Char('r') => match ctx.app.patterns_view_level {
|
||||||
PatternsViewLevel::Banks => {
|
PatternsViewLevel::Banks => {
|
||||||
let bank = ctx.app.patterns_cursor;
|
let bank = ctx.app.patterns_cursor;
|
||||||
let current_name = ctx.app.project.banks[bank].name.clone().unwrap_or_default();
|
let current_name = ctx.app.project_state.project.banks[bank]
|
||||||
ctx.app.modal = Modal::RenameBank {
|
|
||||||
bank,
|
|
||||||
name: current_name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
PatternsViewLevel::Patterns { bank } => {
|
|
||||||
let pattern = ctx.app.patterns_cursor;
|
|
||||||
let current_name = ctx.app.project.banks[bank].patterns[pattern]
|
|
||||||
.name
|
.name
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
ctx.app.modal = Modal::RenamePattern {
|
ctx.dispatch(AppCommand::OpenModal(Modal::RenameBank {
|
||||||
|
bank,
|
||||||
|
name: current_name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
PatternsViewLevel::Patterns { bank } => {
|
||||||
|
let pattern = ctx.app.patterns_cursor;
|
||||||
|
let current_name = ctx.app.project_state.project.banks[bank].patterns[pattern]
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default();
|
||||||
|
ctx.dispatch(AppCommand::OpenModal(Modal::RenamePattern {
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
name: current_name,
|
name: current_name,
|
||||||
};
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -345,7 +353,9 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
ctx.app.modal = Modal::ConfirmQuit { selected: false };
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
KeyCode::Up | KeyCode::Char('k') => ctx.app.audio.prev_focus(),
|
KeyCode::Up | KeyCode::Char('k') => ctx.app.audio.prev_focus(),
|
||||||
KeyCode::Down | KeyCode::Char('j') => ctx.app.audio.next_focus(),
|
KeyCode::Down | KeyCode::Char('j') => ctx.app.audio.next_focus(),
|
||||||
@@ -364,14 +374,16 @@ fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
AudioFocus::SamplePaths => {}
|
AudioFocus::SamplePaths => {}
|
||||||
},
|
},
|
||||||
KeyCode::Char('R') => ctx.app.audio.trigger_restart(),
|
KeyCode::Char('R') => ctx.app.audio.trigger_restart(),
|
||||||
KeyCode::Char('A') => ctx.app.modal = Modal::AddSamplePath(String::new()),
|
KeyCode::Char('A') => {
|
||||||
|
ctx.dispatch(AppCommand::OpenModal(Modal::AddSamplePath(String::new())));
|
||||||
|
}
|
||||||
KeyCode::Char('D') => {
|
KeyCode::Char('D') => {
|
||||||
ctx.app.audio.refresh_devices();
|
ctx.app.audio.refresh_devices();
|
||||||
let out_count = ctx.app.audio.output_devices.len();
|
let out_count = ctx.app.audio.output_devices.len();
|
||||||
let in_count = ctx.app.audio.input_devices.len();
|
let in_count = ctx.app.audio.input_devices.len();
|
||||||
ctx.app.status_message = Some(format!(
|
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||||
"Found {out_count} output, {in_count} input devices"
|
"Found {out_count} output, {in_count} input devices"
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
KeyCode::Char('h') => {
|
KeyCode::Char('h') => {
|
||||||
let _ = ctx.audio_tx.send(AudioCommand::Hush);
|
let _ = ctx.audio_tx.send(AudioCommand::Hush);
|
||||||
@@ -386,8 +398,9 @@ fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
.send(AudioCommand::Evaluate("sin 440 * 0.3".into()));
|
.send(AudioCommand::Evaluate("sin 440 * 0.3".into()));
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') => {
|
||||||
ctx.app.toggle_playing();
|
ctx.dispatch(AppCommand::TogglePlaying);
|
||||||
ctx.playing.store(ctx.app.playing, Ordering::Relaxed);
|
ctx.playing
|
||||||
|
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -395,20 +408,15 @@ fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_doc_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
fn handle_doc_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
let topic_count = doc_view::topic_count();
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('j') | KeyCode::Down => {
|
KeyCode::Char('j') | KeyCode::Down => ctx.dispatch(AppCommand::DocNextTopic),
|
||||||
ctx.app.doc_topic = (ctx.app.doc_topic + 1) % topic_count;
|
KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::DocPrevTopic),
|
||||||
ctx.app.doc_scroll = 0;
|
KeyCode::PageDown => ctx.dispatch(AppCommand::DocScrollDown(10)),
|
||||||
}
|
KeyCode::PageUp => ctx.dispatch(AppCommand::DocScrollUp(10)),
|
||||||
KeyCode::Char('k') | KeyCode::Up => {
|
|
||||||
ctx.app.doc_topic = (ctx.app.doc_topic + topic_count - 1) % topic_count;
|
|
||||||
ctx.app.doc_scroll = 0;
|
|
||||||
}
|
|
||||||
KeyCode::PageDown => ctx.app.doc_scroll = ctx.app.doc_scroll.saturating_add(10),
|
|
||||||
KeyCode::PageUp => ctx.app.doc_scroll = ctx.app.doc_scroll.saturating_sub(10),
|
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
ctx.app.modal = Modal::ConfirmQuit { selected: false };
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod audio;
|
mod audio;
|
||||||
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod file;
|
mod file;
|
||||||
mod input;
|
mod input;
|
||||||
@@ -148,10 +149,10 @@ fn main() -> io::Result<()> {
|
|||||||
Ok((new_stream, sr)) => {
|
Ok((new_stream, sr)) => {
|
||||||
stream = new_stream;
|
stream = new_stream;
|
||||||
app.audio.config.sample_rate = sr;
|
app.audio.config.sample_rate = sr;
|
||||||
app.status_message = Some("Audio restarted".to_string());
|
app.ui.set_status("Audio restarted".to_string());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_message = Some(format!("Restart failed: {e}"));
|
app.ui.set_status(format!("Restart failed: {e}"));
|
||||||
let mut fallback_samples = Vec::new();
|
let mut fallback_samples = Vec::new();
|
||||||
for path in &app.audio.config.sample_paths {
|
for path in &app.audio.config.sample_paths {
|
||||||
let index = doux::loader::scan_samples_dir(path);
|
let index = doux::loader::scan_samples_dir(path);
|
||||||
@@ -174,7 +175,7 @@ fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.playing = playing.load(Ordering::Relaxed);
|
app.playback.playing = playing.load(Ordering::Relaxed);
|
||||||
|
|
||||||
{
|
{
|
||||||
app.metrics.active_voices = metrics.active_voices.load(Ordering::Relaxed) as usize;
|
app.metrics.active_voices = metrics.active_voices.load(Ordering::Relaxed) as usize;
|
||||||
@@ -187,7 +188,7 @@ fn main() -> io::Result<()> {
|
|||||||
let seq_snapshot = sequencer.snapshot();
|
let seq_snapshot = sequencer.snapshot();
|
||||||
app.metrics.event_count = seq_snapshot.event_count;
|
app.metrics.event_count = seq_snapshot.event_count;
|
||||||
|
|
||||||
for change in app.queued_changes.drain(..) {
|
for change in app.playback.queued_changes.drain(..) {
|
||||||
match change {
|
match change {
|
||||||
SlotChange::Add {
|
SlotChange::Add {
|
||||||
slot,
|
slot,
|
||||||
@@ -206,8 +207,8 @@ fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (bank, pattern) in app.dirty_patterns.drain() {
|
for (bank, pattern) in app.project_state.take_dirty() {
|
||||||
let pat = app.project.pattern_at(bank, pattern);
|
let pat = app.project_state.project.pattern_at(bank, pattern);
|
||||||
let snapshot = PatternSnapshot {
|
let snapshot = PatternSnapshot {
|
||||||
speed: pat.speed,
|
speed: pat.speed,
|
||||||
length: pat.length,
|
length: pat.length,
|
||||||
|
|||||||
@@ -1,37 +1,82 @@
|
|||||||
use crate::model::Project;
|
use crate::model::{PatternSpeed, Project};
|
||||||
|
|
||||||
pub fn toggle_step(project: &mut Project, bank: usize, pattern: usize, step: usize) {
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct PatternChange {
|
||||||
|
pub bank: usize,
|
||||||
|
pub pattern: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PatternChange {
|
||||||
|
pub fn new(bank: usize, pattern: usize) -> Self {
|
||||||
|
Self { bank, pattern }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_step(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
step: usize,
|
||||||
|
) -> PatternChange {
|
||||||
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) {
|
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) {
|
||||||
s.active = !s.active;
|
s.active = !s.active;
|
||||||
}
|
}
|
||||||
|
PatternChange::new(bank, pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_length(project: &mut Project, bank: usize, pattern: usize, length: usize) {
|
pub fn set_length(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
length: usize,
|
||||||
|
) -> (PatternChange, usize) {
|
||||||
project.pattern_at_mut(bank, pattern).set_length(length);
|
project.pattern_at_mut(bank, pattern).set_length(length);
|
||||||
|
let actual = project.pattern_at(bank, pattern).length;
|
||||||
|
(PatternChange::new(bank, pattern), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_length(project: &Project, bank: usize, pattern: usize) -> usize {
|
pub fn get_length(project: &Project, bank: usize, pattern: usize) -> usize {
|
||||||
project.pattern_at(bank, pattern).length
|
project.pattern_at(bank, pattern).length
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn increase_length(project: &mut Project, bank: usize, pattern: usize) {
|
pub fn increase_length(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
) -> (PatternChange, usize) {
|
||||||
let current = get_length(project, bank, pattern);
|
let current = get_length(project, bank, pattern);
|
||||||
set_length(project, bank, pattern, current + 1);
|
set_length(project, bank, pattern, current + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrease_length(project: &mut Project, bank: usize, pattern: usize) {
|
pub fn decrease_length(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
) -> (PatternChange, usize) {
|
||||||
let current = get_length(project, bank, pattern);
|
let current = get_length(project, bank, pattern);
|
||||||
set_length(project, bank, pattern, current.saturating_sub(1));
|
set_length(project, bank, pattern, current.saturating_sub(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn increase_speed(project: &mut Project, bank: usize, pattern: usize) {
|
pub fn set_speed(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
speed: PatternSpeed,
|
||||||
|
) -> PatternChange {
|
||||||
|
project.pattern_at_mut(bank, pattern).speed = speed;
|
||||||
|
PatternChange::new(bank, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increase_speed(project: &mut Project, bank: usize, pattern: usize) -> PatternChange {
|
||||||
let pat = project.pattern_at_mut(bank, pattern);
|
let pat = project.pattern_at_mut(bank, pattern);
|
||||||
pat.speed = pat.speed.next();
|
pat.speed = pat.speed.next();
|
||||||
|
PatternChange::new(bank, pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrease_speed(project: &mut Project, bank: usize, pattern: usize) {
|
pub fn decrease_speed(project: &mut Project, bank: usize, pattern: usize) -> PatternChange {
|
||||||
let pat = project.pattern_at_mut(bank, pattern);
|
let pat = project.pattern_at_mut(bank, pattern);
|
||||||
pat.speed = pat.speed.prev();
|
pat.speed = pat.speed.prev();
|
||||||
|
PatternChange::new(bank, pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_step_script(
|
pub fn set_step_script(
|
||||||
@@ -40,10 +85,11 @@ pub fn set_step_script(
|
|||||||
pattern: usize,
|
pattern: usize,
|
||||||
step: usize,
|
step: usize,
|
||||||
script: String,
|
script: String,
|
||||||
) {
|
) -> PatternChange {
|
||||||
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) {
|
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) {
|
||||||
s.script = script;
|
s.script = script;
|
||||||
}
|
}
|
||||||
|
PatternChange::new(bank, pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_step_script(
|
pub fn get_step_script(
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ pub mod audio;
|
|||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod modal;
|
pub mod modal;
|
||||||
pub mod patterns_nav;
|
pub mod patterns_nav;
|
||||||
|
pub mod playback;
|
||||||
|
pub mod project;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
pub use audio::{AudioFocus, AudioSettings, Metrics};
|
pub use audio::{AudioFocus, AudioSettings, Metrics};
|
||||||
pub use editor::{EditorContext, Focus, PatternField};
|
pub use editor::{EditorContext, Focus, PatternField};
|
||||||
pub use modal::Modal;
|
pub use modal::Modal;
|
||||||
pub use patterns_nav::PatternsViewLevel;
|
pub use patterns_nav::PatternsViewLevel;
|
||||||
|
pub use playback::PlaybackState;
|
||||||
|
pub use project::ProjectState;
|
||||||
|
pub use ui::UiState;
|
||||||
|
|||||||
21
seq/src/state/playback.rs
Normal file
21
seq/src/state/playback.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use crate::sequencer::SlotChange;
|
||||||
|
|
||||||
|
pub struct PlaybackState {
|
||||||
|
pub playing: bool,
|
||||||
|
pub queued_changes: Vec<SlotChange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlaybackState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
playing: true,
|
||||||
|
queued_changes: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaybackState {
|
||||||
|
pub fn toggle(&mut self) {
|
||||||
|
self.playing = !self.playing;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
seq/src/state/project.rs
Normal file
41
seq/src/state/project.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::config::{MAX_BANKS, MAX_PATTERNS};
|
||||||
|
use crate::model::Project;
|
||||||
|
|
||||||
|
pub struct ProjectState {
|
||||||
|
pub project: Project,
|
||||||
|
pub file_path: Option<PathBuf>,
|
||||||
|
pub dirty_patterns: HashSet<(usize, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProjectState {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut state = Self {
|
||||||
|
project: Project::default(),
|
||||||
|
file_path: None,
|
||||||
|
dirty_patterns: HashSet::new(),
|
||||||
|
};
|
||||||
|
state.mark_all_dirty();
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectState {
|
||||||
|
pub fn mark_dirty(&mut self, bank: usize, pattern: usize) {
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_all_dirty(&mut self) {
|
||||||
|
for bank in 0..MAX_BANKS {
|
||||||
|
for pattern in 0..MAX_PATTERNS {
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_dirty(&mut self) -> HashSet<(usize, usize)> {
|
||||||
|
std::mem::take(&mut self.dirty_patterns)
|
||||||
|
}
|
||||||
|
}
|
||||||
44
seq/src/state/ui.rs
Normal file
44
seq/src/state/ui.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::state::Modal;
|
||||||
|
|
||||||
|
pub struct UiState {
|
||||||
|
pub status_message: Option<String>,
|
||||||
|
pub flash_until: Option<Instant>,
|
||||||
|
pub modal: Modal,
|
||||||
|
pub doc_topic: usize,
|
||||||
|
pub doc_scroll: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UiState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
status_message: None,
|
||||||
|
flash_until: None,
|
||||||
|
modal: Modal::None,
|
||||||
|
doc_topic: 0,
|
||||||
|
doc_scroll: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UiState {
|
||||||
|
pub fn flash(&mut self, msg: &str, duration_ms: u64) {
|
||||||
|
self.status_message = Some(msg.to_string());
|
||||||
|
self.flash_until = Some(Instant::now() + Duration::from_millis(duration_ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_status(&mut self, msg: String) {
|
||||||
|
self.status_message = Some(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_status(&mut self) {
|
||||||
|
self.status_message = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_flashing(&self) -> bool {
|
||||||
|
self.flash_until
|
||||||
|
.map(|t| Instant::now() < t)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,8 +36,8 @@ fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
let [left_area, right_area] =
|
let [left_area, right_area] =
|
||||||
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(area);
|
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(area);
|
||||||
|
|
||||||
let play_symbol = if app.playing { "▶" } else { "■" };
|
let play_symbol = if app.playback.playing { "▶" } else { "■" };
|
||||||
let play_color = if app.playing {
|
let play_color = if app.playback.playing {
|
||||||
Color::Green
|
Color::Green
|
||||||
} else {
|
} else {
|
||||||
Color::Red
|
Color::Red
|
||||||
@@ -69,6 +69,7 @@ fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
frame.render_widget(Paragraph::new(Line::from(left_spans)), left_area);
|
frame.render_widget(Paragraph::new(Line::from(left_spans)), left_area);
|
||||||
|
|
||||||
let pattern = app
|
let pattern = app
|
||||||
|
.project_state
|
||||||
.project
|
.project
|
||||||
.pattern_at(app.editor_ctx.bank, app.editor_ctx.pattern);
|
.pattern_at(app.editor_ctx.bank, app.editor_ctx.pattern);
|
||||||
let right_spans = vec![
|
let right_spans = vec![
|
||||||
@@ -109,7 +110,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
Page::Doc => "[DOC] ",
|
Page::Doc => "[DOC] ",
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = if let Some(ref msg) = app.status_message {
|
let content = if let Some(ref msg) = app.ui.status_message {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
page_indicator,
|
page_indicator,
|
||||||
@@ -196,7 +197,7 @@ fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
|
|||||||
|
|
||||||
fn render_modal(frame: &mut Frame, app: &App) {
|
fn render_modal(frame: &mut Frame, app: &App) {
|
||||||
let term = frame.area();
|
let term = frame.area();
|
||||||
match &app.modal {
|
match &app.ui.modal {
|
||||||
Modal::None => {}
|
Modal::None => {}
|
||||||
Modal::ConfirmQuit { selected } => {
|
Modal::ConfirmQuit { selected } => {
|
||||||
let width = 30.min(term.width.saturating_sub(4));
|
let width = 30.min(term.width.saturating_sub(4));
|
||||||
|
|||||||
@@ -26,34 +26,36 @@ fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, (name, _))| {
|
.map(|(i, (name, _))| {
|
||||||
let style = if i == app.doc_topic {
|
let style = if i == app.ui.doc_topic {
|
||||||
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::White)
|
Style::new().fg(Color::White)
|
||||||
};
|
};
|
||||||
let prefix = if i == app.doc_topic { "> " } else { " " };
|
let prefix = if i == app.ui.doc_topic { "> " } else { " " };
|
||||||
ListItem::new(format!("{prefix}{name}")).style(style)
|
ListItem::new(format!("{prefix}{name}")).style(style)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let list = List::new(items)
|
let list = List::new(items).block(Block::default().borders(Borders::ALL).title("Topics"));
|
||||||
.block(Block::default().borders(Borders::ALL).title("Topics"));
|
|
||||||
frame.render_widget(list, area);
|
frame.render_widget(list, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_content(frame: &mut Frame, app: &App, area: Rect) {
|
fn render_content(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
let (title, md) = DOCS[app.doc_topic];
|
let (title, md) = DOCS[app.ui.doc_topic];
|
||||||
let lines = parse_markdown(md);
|
let lines = parse_markdown(md);
|
||||||
|
|
||||||
let visible_height = area.height.saturating_sub(2) as usize;
|
let visible_height = area.height.saturating_sub(2) as usize;
|
||||||
let total_lines = lines.len();
|
let total_lines = lines.len();
|
||||||
let max_scroll = total_lines.saturating_sub(visible_height);
|
let max_scroll = total_lines.saturating_sub(visible_height);
|
||||||
let scroll = app.doc_scroll.min(max_scroll);
|
let scroll = app.ui.doc_scroll.min(max_scroll);
|
||||||
|
|
||||||
let visible: Vec<RLine> = lines.into_iter().skip(scroll).take(visible_height).collect();
|
let visible: Vec<RLine> = lines
|
||||||
|
.into_iter()
|
||||||
|
.skip(scroll)
|
||||||
|
.take(visible_height)
|
||||||
|
.collect();
|
||||||
|
|
||||||
let para = Paragraph::new(visible)
|
let para = Paragraph::new(visible).block(Block::default().borders(Borders::ALL).title(title));
|
||||||
.block(Block::default().borders(Borders::ALL).title(title));
|
|
||||||
frame.render_widget(para, area);
|
frame.render_widget(para, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,12 +82,8 @@ fn composite_to_line(composite: Composite) -> RLine<'static> {
|
|||||||
CompositeStyle::Header(1) => Style::new()
|
CompositeStyle::Header(1) => Style::new()
|
||||||
.fg(Color::Cyan)
|
.fg(Color::Cyan)
|
||||||
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
|
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
|
||||||
CompositeStyle::Header(2) => Style::new()
|
CompositeStyle::Header(2) => Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||||
.fg(Color::Yellow)
|
CompositeStyle::Header(_) => Style::new().fg(Color::Magenta).add_modifier(Modifier::BOLD),
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
CompositeStyle::Header(_) => Style::new()
|
|
||||||
.fg(Color::Magenta)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
CompositeStyle::ListItem(_) => Style::new().fg(Color::White),
|
CompositeStyle::ListItem(_) => Style::new().fg(Color::White),
|
||||||
CompositeStyle::Quote => Style::new().fg(Color::Rgb(150, 150, 150)),
|
CompositeStyle::Quote => Style::new().fg(Color::Rgb(150, 150, 150)),
|
||||||
CompositeStyle::Code => Style::new().fg(Color::Green),
|
CompositeStyle::Code => Style::new().fg(Color::Green),
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ 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_selected = step_idx == app.editor_ctx.step;
|
let is_selected = step_idx == app.editor_ctx.step;
|
||||||
|
|
||||||
let playing_slot = if app.playing {
|
let playing_slot = if app.playback.playing {
|
||||||
(0..8).find(|&i| {
|
(0..8).find(|&i| {
|
||||||
let s = snapshot.slot_data[i];
|
let s = snapshot.slot_data[i];
|
||||||
s.active
|
s.active
|
||||||
@@ -156,7 +156,7 @@ fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
" "
|
" "
|
||||||
};
|
};
|
||||||
|
|
||||||
let border_style = if app.is_flashing() {
|
let border_style = if app.ui.is_flashing() {
|
||||||
Style::new().fg(Color::Green)
|
Style::new().fg(Color::Green)
|
||||||
} else if app.editor_ctx.focus == Focus::Editor {
|
} else if app.editor_ctx.focus == Focus::Editor {
|
||||||
Style::new().fg(Color::Rgb(100, 160, 180))
|
Style::new().fg(Color::Rgb(100, 160, 180))
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let bank_names: Vec<Option<&str>> = app
|
let bank_names: Vec<Option<&str>> = app
|
||||||
|
.project_state
|
||||||
.project
|
.project
|
||||||
.banks
|
.banks
|
||||||
.iter()
|
.iter()
|
||||||
@@ -63,7 +64,7 @@ fn render_patterns(
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
bank: usize,
|
bank: usize,
|
||||||
) {
|
) {
|
||||||
let bank_name = app.project.banks[bank].name.as_deref();
|
let bank_name = app.project_state.project.banks[bank].name.as_deref();
|
||||||
let title_text = match bank_name {
|
let title_text = match bank_name {
|
||||||
Some(name) => format!("{name} › Patterns"),
|
Some(name) => format!("{name} › Patterns"),
|
||||||
None => format!("Bank {:02} › Patterns", bank + 1),
|
None => format!("Bank {:02} › Patterns", bank + 1),
|
||||||
@@ -102,7 +103,7 @@ fn render_patterns(
|
|||||||
usize::MAX
|
usize::MAX
|
||||||
};
|
};
|
||||||
|
|
||||||
let pattern_names: Vec<Option<&str>> = app.project.banks[bank]
|
let pattern_names: Vec<Option<&str>> = app.project_state.project.banks[bank]
|
||||||
.patterns
|
.patterns
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.name.as_deref())
|
.map(|p| p.name.as_deref())
|
||||||
|
|||||||
Reference in New Issue
Block a user