Refactoring

This commit is contained in:
2026-01-20 03:08:37 +01:00
parent a81716cbd5
commit 06ec2ae70f
13 changed files with 678 additions and 263 deletions

View File

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

View File

@@ -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,
}));
} }
_ => {} _ => {}
} }

View File

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

View File

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

View File

@@ -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
View 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
View 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
View 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)
}
}

View File

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

View File

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

View File

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

View File

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