diff --git a/seq/src/app.rs b/seq/src/app.rs index a28d5b4..c1bfe4d 100644 --- a/seq/src/app.rs +++ b/seq/src/app.rs @@ -1,49 +1,43 @@ use rand::rngs::StdRng; use rand::SeedableRng; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::path::PathBuf; 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::link::LinkState; -use crate::model::{Pattern, Project}; +use crate::model::Pattern; use crate::page::Page; use crate::script::{Rng, ScriptEngine, StepContext, Variables}; use crate::sequencer::{SequencerSnapshot, SlotChange}; use crate::services::pattern_editor; use crate::state::{ AudioSettings, EditorContext, Focus, Metrics, Modal, PatternField, PatternsViewLevel, + PlaybackState, ProjectState, UiState, }; +use crate::views::doc_view; pub struct App { - pub playing: bool, + pub project_state: ProjectState, + pub ui: UiState, + pub playback: PlaybackState, - pub project: Project, pub page: Page, pub editor_ctx: EditorContext, pub patterns_view_level: PatternsViewLevel, pub patterns_cursor: usize, - pub queued_changes: Vec, - pub metrics: Metrics, pub sample_pool_mb: f32, pub script_engine: ScriptEngine, pub variables: Variables, pub rng: Rng, - pub file_path: Option, - pub status_message: Option, - pub flash_until: Option, - pub modal: Modal, pub clipboard: Option, - pub doc_topic: usize, - pub doc_scroll: usize, pub audio: AudioSettings, - pub dirty_patterns: HashSet<(usize, usize)>, } impl App { @@ -53,32 +47,24 @@ impl App { let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&rng)); Self { - playing: true, + project_state: ProjectState::default(), + ui: UiState::default(), + playback: PlaybackState::default(), - project: Project::default(), page: Page::default(), editor_ctx: EditorContext::default(), patterns_view_level: PatternsViewLevel::default(), patterns_cursor: 0, - queued_changes: Vec::new(), - metrics: Metrics::default(), sample_pool_mb: 0.0, variables, rng, script_engine, - file_path: None, - status_message: None, - flash_until: None, - modal: Modal::None, clipboard: arboard::Clipboard::new().ok(), - doc_topic: 0, - doc_scroll: 0, audio: AudioSettings::default(), - dirty_patterns: HashSet::new(), } } @@ -86,20 +72,12 @@ impl App { (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) { - for bank in 0..MAX_BANKS { - for pattern in 0..MAX_PATTERNS { - self.dirty_patterns.insert((bank, pattern)); - } - } + self.project_state.mark_all_dirty(); } pub fn toggle_playing(&mut self) { - self.playing = !self.playing; + self.playback.toggle(); } pub fn tempo_up(&self, link: &LinkState) { @@ -128,7 +106,7 @@ impl App { pub fn current_edit_pattern(&self) -> &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) { @@ -177,44 +155,53 @@ impl App { pub fn toggle_step(&mut self) { let (bank, pattern) = self.current_bank_pattern(); - pattern_editor::toggle_step(&mut self.project, bank, pattern, self.editor_ctx.step); - self.mark_current_dirty(); + let change = pattern_editor::toggle_step( + &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) { let (bank, pattern) = self.current_bank_pattern(); - pattern_editor::increase_length(&mut self.project, bank, pattern); - self.mark_current_dirty(); + let (change, _) = + pattern_editor::increase_length(&mut self.project_state.project, bank, pattern); + self.project_state.mark_dirty(change.bank, change.pattern); } pub fn length_decrease(&mut self) { let (bank, pattern) = self.current_bank_pattern(); - pattern_editor::decrease_length(&mut self.project, bank, pattern); - let new_len = pattern_editor::get_length(&self.project, bank, pattern); + let (change, new_len) = + pattern_editor::decrease_length(&mut self.project_state.project, bank, pattern); if self.editor_ctx.step >= new_len { self.editor_ctx.step = new_len - 1; self.load_step_to_editor(); } - self.mark_current_dirty(); + self.project_state.mark_dirty(change.bank, change.pattern); } pub fn speed_increase(&mut self) { let (bank, pattern) = self.current_bank_pattern(); - pattern_editor::increase_speed(&mut self.project, bank, pattern); - self.mark_current_dirty(); + let change = pattern_editor::increase_speed(&mut self.project_state.project, bank, pattern); + self.project_state.mark_dirty(change.bank, change.pattern); } pub fn speed_decrease(&mut self) { let (bank, pattern) = self.current_bank_pattern(); - pattern_editor::decrease_speed(&mut self.project, bank, pattern); - self.mark_current_dirty(); + let change = pattern_editor::decrease_speed(&mut self.project_state.project, bank, pattern); + self.project_state.mark_dirty(change.bank, change.pattern); } fn load_step_to_editor(&mut self) { let (bank, pattern) = self.current_bank_pattern(); - if let Some(script) = - pattern_editor::get_step_script(&self.project, bank, pattern, self.editor_ctx.step) - { + if let Some(script) = pattern_editor::get_step_script( + &self.project_state.project, + bank, + pattern, + self.editor_ctx.step, + ) { let lines: Vec = if script.is_empty() { vec![String::new()] } else { @@ -227,25 +214,27 @@ impl App { pub fn save_editor_to_step(&mut self) { let text = self.editor_ctx.text.lines().join("\n"); let (bank, pattern) = self.current_bank_pattern(); - pattern_editor::set_step_script( - &mut self.project, + let change = pattern_editor::set_step_script( + &mut self.project_state.project, bank, pattern, self.editor_ctx.step, text, ); - self.mark_current_dirty(); + self.project_state.mark_dirty(change.bank, change.pattern); } pub fn compile_current_step(&mut self, link: &LinkState) { let step_idx = self.editor_ctx.step; let (bank, pattern) = self.current_bank_pattern(); - let script = pattern_editor::get_step_script(&self.project, bank, pattern, step_idx) - .unwrap_or_default(); + let script = + pattern_editor::get_step_script(&self.project_state.project, bank, pattern, step_idx) + .unwrap_or_default(); if script.trim().is_empty() { if let Some(step) = self + .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) @@ -268,24 +257,25 @@ impl App { match self.script_engine.evaluate(&script, &ctx) { Ok(cmd) => { if let Some(step) = self + .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = Some(cmd); } - self.status_message = Some("Script compiled".to_string()); - self.flash_until = Some(Instant::now() + std::time::Duration::from_millis(150)); + self.ui.flash("Script compiled", 150); } Err(e) => { if let Some(step) = self + .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { 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(); for step_idx in 0..pattern_len { - let script = pattern_editor::get_step_script(&self.project, bank, pattern, step_idx) - .unwrap_or_default(); + let script = pattern_editor::get_step_script( + &self.project_state.project, + bank, + pattern, + step_idx, + ) + .unwrap_or_default(); if script.trim().is_empty() { if let Some(step) = self + .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) @@ -321,6 +317,7 @@ impl App { if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx) { if let Some(step) = self + .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) @@ -337,7 +334,7 @@ impl App { pattern: usize, snapshot: &SequencerSnapshot, ) -> Option { - self.queued_changes.iter().find_map(|c| match c { + self.playback.queued_changes.iter().find_map(|c| match c { SlotChange::Add { slot: _, 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 { bank: b, pattern: p, @@ -382,16 +379,17 @@ impl App { }); if let Some(idx) = pending { - self.queued_changes.remove(idx); - self.status_message = Some(format!( + self.playback.queued_changes.remove(idx); + self.ui.set_status(format!( "B{:02}:P{:02} change cancelled", bank + 1, pattern + 1 )); } else if let Some(slot_idx) = playing_slot { - self.queued_changes + self.playback + .queued_changes .push(SlotChange::Remove { slot: slot_idx }); - self.status_message = Some(format!( + self.ui.set_status(format!( "B{:02}:P{:02} queued to stop", bank + 1, pattern + 1 @@ -399,18 +397,18 @@ impl App { } else { let free_slot = (0..MAX_SLOTS).find(|&i| !snapshot.slot_data[i].active); if let Some(slot_idx) = free_slot { - self.queued_changes.push(SlotChange::Add { + self.playback.queued_changes.push(SlotChange::Add { slot: slot_idx, bank, pattern, }); - self.status_message = Some(format!( + self.ui.set_status(format!( "B{:02}:P{:02} queued to play", bank + 1, pattern + 1 )); } 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) { self.save_editor_to_step(); - match file::save(&self.project, &path) { + match file::save(&self.project_state.project, &path) { Ok(()) => { - self.status_message = Some(format!("Saved: {}", path.display())); - self.file_path = Some(path); + self.ui.set_status(format!("Saved: {}", path.display())); + self.project_state.file_path = Some(path); } 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) { match file::load(&path) { Ok(project) => { - self.project = project; + self.project_state.project = project; self.editor_ctx.step = 0; self.load_step_to_editor(); self.compile_all_steps(link); self.mark_all_patterns_dirty(); - self.status_message = Some(format!("Loaded: {}", path.display())); - self.file_path = Some(path); + self.ui.set_status(format!("Loaded: {}", path.display())); + self.project_state.file_path = Some(path); } 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) { let (bank, pattern) = self.current_bank_pattern(); - let script = - pattern_editor::get_step_script(&self.project, bank, pattern, self.editor_ctx.step); + let script = pattern_editor::get_step_script( + &self.project_state.project, + bank, + pattern, + self.editor_ctx.step, + ); if let Some(script) = script { if let Some(clip) = &mut self.clipboard { 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 { let (bank, pattern) = self.current_bank_pattern(); - pattern_editor::set_step_script( - &mut self.project, + let change = pattern_editor::set_step_script( + &mut self.project_state.project, bank, pattern, self.editor_ctx.step, text, ); - self.mark_current_dirty(); + self.project_state.mark_dirty(change.bank, change.pattern); self.load_step_to_editor(); self.compile_current_step(link); } @@ -508,9 +500,167 @@ impl App { PatternField::Length => self.current_edit_pattern().length.to_string(), PatternField::Speed => self.current_edit_pattern().speed.label().to_string(), }; - self.modal = Modal::SetPattern { + self.ui.modal = Modal::SetPattern { field, 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; + } + }, + } + } } diff --git a/seq/src/commands.rs b/seq/src/commands.rs new file mode 100644 index 0000000..230e299 --- /dev/null +++ b/seq/src/commands.rs @@ -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, + }, + RenamePattern { + bank: usize, + pattern: usize, + name: Option, + }, + 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, +} diff --git a/seq/src/input.rs b/seq/src/input.rs index 1451ab7..7bc1921 100644 --- a/seq/src/input.rs +++ b/seq/src/input.rs @@ -5,12 +5,12 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use crate::app::App; +use crate::commands::AppCommand; use crate::link::LinkState; use crate::model::PatternSpeed; use crate::page::Page; use crate::sequencer::{AudioCommand, SequencerSnapshot}; use crate::state::{AudioFocus, Focus, Modal, PatternField, PatternsViewLevel}; -use crate::views::doc_view; pub enum InputResult { Continue, @@ -25,10 +25,16 @@ pub struct InputContext<'a> { pub audio_tx: &'a Sender, } -pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult { - ctx.app.clear_status(); +impl<'a> InputContext<'a> { + 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) } else { 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 { - match &mut ctx.app.modal { + match &mut ctx.app.ui.modal { Modal::ConfirmQuit { selected } => match key.code { KeyCode::Char('y') | KeyCode::Char('Y') => return InputResult::Quit, KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { - ctx.app.modal = Modal::None; + ctx.dispatch(AppCommand::CloseModal); } KeyCode::Left | KeyCode::Right => { *selected = !*selected; @@ -49,7 +55,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { if *selected { return InputResult::Quit; } 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 { KeyCode::Enter => { let save_path = PathBuf::from(path.as_str()); - ctx.app.modal = Modal::None; - ctx.app.save(save_path); + ctx.dispatch(AppCommand::CloseModal); + ctx.dispatch(AppCommand::Save(save_path)); } - KeyCode::Esc => ctx.app.modal = Modal::None, + KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal), KeyCode::Backspace => { path.pop(); } @@ -70,10 +76,10 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { Modal::LoadFrom(path) => match key.code { KeyCode::Enter => { let load_path = PathBuf::from(path.as_str()); - ctx.app.modal = Modal::None; - ctx.app.load(load_path, ctx.link); + ctx.dispatch(AppCommand::CloseModal); + ctx.dispatch(AppCommand::Load(load_path)); } - KeyCode::Esc => ctx.app.modal = Modal::None, + KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal), KeyCode::Backspace => { path.pop(); } @@ -88,10 +94,13 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { } else { Some(name.clone()) }; - ctx.app.project.banks[bank_idx].name = new_name; - ctx.app.modal = Modal::None; + ctx.dispatch(AppCommand::RenameBank { + 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 => { name.pop(); } @@ -110,10 +119,14 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { } else { Some(name.clone()) }; - ctx.app.project.banks[bank_idx].patterns[pattern_idx].name = new_name; - ctx.app.modal = Modal::None; + ctx.dispatch(AppCommand::RenamePattern { + 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 => { name.pop(); } @@ -127,36 +140,43 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { match field { PatternField::Length => { if let Ok(len) = input.parse::() { - ctx.app + ctx.dispatch(AppCommand::SetLength { + bank, + pattern, + length: len, + }); + let new_len = ctx + .app + .project_state .project - .pattern_at_mut(bank, pattern) - .set_length(len); - let new_len = ctx.app.project.pattern_at(bank, pattern).length; - 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}")); + .pattern_at(bank, pattern) + .length; + ctx.dispatch(AppCommand::SetStatus(format!("Length set to {new_len}"))); } else { - ctx.app.status_message = Some("Invalid length".to_string()); + ctx.dispatch(AppCommand::SetStatus("Invalid length".to_string())); } } PatternField::Speed => { if let Some(speed) = PatternSpeed::from_label(input) { - ctx.app.project.pattern_at_mut(bank, pattern).speed = speed; - ctx.app.dirty_patterns.insert((bank, pattern)); - ctx.app.status_message = - Some(format!("Speed set to {}", speed.label())); + ctx.dispatch(AppCommand::SetSpeed { + bank, + pattern, + speed, + }); + ctx.dispatch(AppCommand::SetStatus(format!( + "Speed set to {}", + speed.label() + ))); } 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(), - ); + )); } } } - ctx.app.modal = Modal::None; + ctx.dispatch(AppCommand::CloseModal); } - KeyCode::Esc => ctx.app.modal = Modal::None, + KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal), KeyCode::Backspace => { input.pop(); } @@ -172,13 +192,13 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { let _ = ctx.audio_tx.send(AudioCommand::LoadSamples(index)); ctx.app.audio.config.sample_count += count; 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 { - 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 => { 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 { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); - // Global navigation with Ctrl+arrows if ctrl { match key.code { KeyCode::Left => { - ctx.app.page.left(); + ctx.dispatch(AppCommand::PageLeft); return InputResult::Continue; } KeyCode::Right => { - ctx.app.page.right(); + ctx.dispatch(AppCommand::PageRight); return InputResult::Continue; } KeyCode::Up => { - ctx.app.page.up(); + ctx.dispatch(AppCommand::PageUp); return InputResult::Continue; } KeyCode::Down => { - ctx.app.page.down(); + ctx.dispatch(AppCommand::PageDown); 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 { Focus::Sequencer => match key.code { KeyCode::Char('q') => { - ctx.app.modal = Modal::ConfirmQuit { selected: false }; + ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit { + selected: false, + })); } KeyCode::Char(' ') => { - ctx.app.toggle_playing(); - ctx.playing.store(ctx.app.playing, Ordering::Relaxed); + ctx.dispatch(AppCommand::TogglePlaying); + ctx.playing + .store(ctx.app.playback.playing, Ordering::Relaxed); } - KeyCode::Tab => ctx.app.toggle_focus(ctx.link), - KeyCode::Left => ctx.app.prev_step(), - KeyCode::Right => ctx.app.next_step(), - KeyCode::Up => ctx.app.step_up(), - KeyCode::Down => ctx.app.step_down(), - KeyCode::Enter => ctx.app.toggle_step(), + KeyCode::Tab => ctx.dispatch(AppCommand::ToggleFocus), + KeyCode::Left => ctx.dispatch(AppCommand::PrevStep), + KeyCode::Right => ctx.dispatch(AppCommand::NextStep), + KeyCode::Up => ctx.dispatch(AppCommand::StepUp), + KeyCode::Down => ctx.dispatch(AppCommand::StepDown), + KeyCode::Enter => ctx.dispatch(AppCommand::ToggleStep), KeyCode::Char('s') => { let default = ctx .app + .project_state .file_path .as_ref() .map(|p| p.display().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') => { - 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('-') => ctx.app.tempo_down(ctx.link), - KeyCode::Char('<') | KeyCode::Char(',') => ctx.app.length_decrease(), - KeyCode::Char('>') | KeyCode::Char('.') => ctx.app.length_increase(), - KeyCode::Char('[') => ctx.app.speed_decrease(), - KeyCode::Char(']') => ctx.app.speed_increase(), - KeyCode::Char('L') => ctx.app.open_pattern_modal(PatternField::Length), - KeyCode::Char('S') => ctx.app.open_pattern_modal(PatternField::Speed), - KeyCode::Char('c') if ctrl => ctx.app.copy_step(), - KeyCode::Char('v') if ctrl => ctx.app.paste_step(ctx.link), + KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp), + KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown), + KeyCode::Char('<') | KeyCode::Char(',') => ctx.dispatch(AppCommand::LengthDecrease), + KeyCode::Char('>') | KeyCode::Char('.') => ctx.dispatch(AppCommand::LengthIncrease), + KeyCode::Char('[') => ctx.dispatch(AppCommand::SpeedDecrease), + KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease), + KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)), + KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)), + KeyCode::Char('c') if ctrl => ctx.dispatch(AppCommand::CopyStep), + KeyCode::Char('v') if ctrl => ctx.dispatch(AppCommand::PasteStep), _ => {} }, 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 => { - ctx.app.save_editor_to_step(); - ctx.app.compile_current_step(ctx.link); + ctx.dispatch(AppCommand::SaveEditorToStep); + ctx.dispatch(AppCommand::CompileCurrentStep); } _ => { 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 { match key.code { - KeyCode::Left => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 15) % 16, - KeyCode::Right => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 1) % 16, - KeyCode::Up => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 12) % 16, - KeyCode::Down => ctx.app.patterns_cursor = (ctx.app.patterns_cursor + 4) % 16, - KeyCode::Esc | KeyCode::Backspace => match ctx.app.patterns_view_level { - PatternsViewLevel::Banks => ctx.app.page.down(), - 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::Left => ctx.dispatch(AppCommand::PatternsCursorLeft), + KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight), + KeyCode::Up => ctx.dispatch(AppCommand::PatternsCursorUp), + KeyCode::Down => ctx.dispatch(AppCommand::PatternsCursorDown), + KeyCode::Esc | KeyCode::Backspace => ctx.dispatch(AppCommand::PatternsBack), + KeyCode::Enter => ctx.dispatch(AppCommand::PatternsEnter), KeyCode::Char(' ') => { if let PatternsViewLevel::Patterns { bank } = ctx.app.patterns_view_level { let pattern = ctx.app.patterns_cursor; - ctx.app.toggle_pattern_playback(bank, pattern, ctx.snapshot); + ctx.dispatch(AppCommand::TogglePatternPlayback { bank, pattern }); } } 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 { PatternsViewLevel::Banks => { let bank = ctx.app.patterns_cursor; - let current_name = ctx.app.project.banks[bank].name.clone().unwrap_or_default(); - 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] + let current_name = ctx.app.project_state.project.banks[bank] .name .clone() .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, pattern, 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 { match key.code { 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::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 => {} }, 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') => { ctx.app.audio.refresh_devices(); let out_count = ctx.app.audio.output_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" - )); + ))); } KeyCode::Char('h') => { 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())); } KeyCode::Char(' ') => { - ctx.app.toggle_playing(); - ctx.playing.store(ctx.app.playing, Ordering::Relaxed); + ctx.dispatch(AppCommand::TogglePlaying); + 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 { - let topic_count = doc_view::topic_count(); match key.code { - KeyCode::Char('j') | KeyCode::Down => { - ctx.app.doc_topic = (ctx.app.doc_topic + 1) % topic_count; - ctx.app.doc_scroll = 0; - } - 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('j') | KeyCode::Down => ctx.dispatch(AppCommand::DocNextTopic), + KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::DocPrevTopic), + KeyCode::PageDown => ctx.dispatch(AppCommand::DocScrollDown(10)), + KeyCode::PageUp => ctx.dispatch(AppCommand::DocScrollUp(10)), KeyCode::Char('q') => { - ctx.app.modal = Modal::ConfirmQuit { selected: false }; + ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit { + selected: false, + })); } _ => {} } diff --git a/seq/src/main.rs b/seq/src/main.rs index c0d04dc..2fcf2f3 100644 --- a/seq/src/main.rs +++ b/seq/src/main.rs @@ -1,5 +1,6 @@ mod app; mod audio; +mod commands; mod config; mod file; mod input; @@ -148,10 +149,10 @@ fn main() -> io::Result<()> { Ok((new_stream, sr)) => { stream = new_stream; app.audio.config.sample_rate = sr; - app.status_message = Some("Audio restarted".to_string()); + app.ui.set_status("Audio restarted".to_string()); } Err(e) => { - app.status_message = Some(format!("Restart failed: {e}")); + app.ui.set_status(format!("Restart failed: {e}")); let mut fallback_samples = Vec::new(); for path in &app.audio.config.sample_paths { 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; @@ -187,7 +188,7 @@ fn main() -> io::Result<()> { let seq_snapshot = sequencer.snapshot(); 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 { SlotChange::Add { slot, @@ -206,8 +207,8 @@ fn main() -> io::Result<()> { } } - for (bank, pattern) in app.dirty_patterns.drain() { - let pat = app.project.pattern_at(bank, pattern); + for (bank, pattern) in app.project_state.take_dirty() { + let pat = app.project_state.project.pattern_at(bank, pattern); let snapshot = PatternSnapshot { speed: pat.speed, length: pat.length, diff --git a/seq/src/services/pattern_editor.rs b/seq/src/services/pattern_editor.rs index d111ee1..6143b5a 100644 --- a/seq/src/services/pattern_editor.rs +++ b/seq/src/services/pattern_editor.rs @@ -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) { - if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) { - s.active = !s.active; +#[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 set_length(project: &mut Project, bank: usize, pattern: usize, length: usize) { +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) { + s.active = !s.active; + } + PatternChange::new(bank, pattern) +} + +pub fn set_length( + project: &mut Project, + bank: usize, + pattern: usize, + length: usize, +) -> (PatternChange, usize) { 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 { 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); - 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); - 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); 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); pat.speed = pat.speed.prev(); + PatternChange::new(bank, pattern) } pub fn set_step_script( @@ -40,10 +85,11 @@ pub fn set_step_script( pattern: usize, step: usize, script: String, -) { +) -> PatternChange { if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) { s.script = script; } + PatternChange::new(bank, pattern) } pub fn get_step_script( diff --git a/seq/src/state/mod.rs b/seq/src/state/mod.rs index 3a70039..b63c0f4 100644 --- a/seq/src/state/mod.rs +++ b/seq/src/state/mod.rs @@ -2,8 +2,14 @@ pub mod audio; pub mod editor; pub mod modal; pub mod patterns_nav; +pub mod playback; +pub mod project; +pub mod ui; pub use audio::{AudioFocus, AudioSettings, Metrics}; pub use editor::{EditorContext, Focus, PatternField}; pub use modal::Modal; pub use patterns_nav::PatternsViewLevel; +pub use playback::PlaybackState; +pub use project::ProjectState; +pub use ui::UiState; diff --git a/seq/src/state/playback.rs b/seq/src/state/playback.rs new file mode 100644 index 0000000..92f3768 --- /dev/null +++ b/seq/src/state/playback.rs @@ -0,0 +1,21 @@ +use crate::sequencer::SlotChange; + +pub struct PlaybackState { + pub playing: bool, + pub queued_changes: Vec, +} + +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; + } +} diff --git a/seq/src/state/project.rs b/seq/src/state/project.rs new file mode 100644 index 0000000..087f1e8 --- /dev/null +++ b/seq/src/state/project.rs @@ -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, + 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) + } +} diff --git a/seq/src/state/ui.rs b/seq/src/state/ui.rs new file mode 100644 index 0000000..7c3ab3c --- /dev/null +++ b/seq/src/state/ui.rs @@ -0,0 +1,44 @@ +use std::time::{Duration, Instant}; + +use crate::state::Modal; + +pub struct UiState { + pub status_message: Option, + pub flash_until: Option, + 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) + } +} diff --git a/seq/src/ui.rs b/seq/src/ui.rs index 733ff78..7be01b1 100644 --- a/seq/src/ui.rs +++ b/seq/src/ui.rs @@ -36,8 +36,8 @@ fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { let [left_area, right_area] = Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(area); - let play_symbol = if app.playing { "▶" } else { "■" }; - let play_color = if app.playing { + let play_symbol = if app.playback.playing { "▶" } else { "■" }; + let play_color = if app.playback.playing { Color::Green } else { 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); let pattern = app + .project_state .project .pattern_at(app.editor_ctx.bank, app.editor_ctx.pattern); let right_spans = vec![ @@ -109,7 +110,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { 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![ Span::styled( page_indicator, @@ -196,7 +197,7 @@ fn centered_rect(width: u16, height: u16, area: Rect) -> Rect { fn render_modal(frame: &mut Frame, app: &App) { let term = frame.area(); - match &app.modal { + match &app.ui.modal { Modal::None => {} Modal::ConfirmQuit { selected } => { let width = 30.min(term.width.saturating_sub(4)); diff --git a/seq/src/views/doc_view.rs b/seq/src/views/doc_view.rs index eb8d485..9856076 100644 --- a/seq/src/views/doc_view.rs +++ b/seq/src/views/doc_view.rs @@ -26,34 +26,36 @@ fn render_topics(frame: &mut Frame, app: &App, area: Rect) { .iter() .enumerate() .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) } else { 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) }) .collect(); - let list = List::new(items) - .block(Block::default().borders(Borders::ALL).title("Topics")); + let list = List::new(items).block(Block::default().borders(Borders::ALL).title("Topics")); frame.render_widget(list, area); } 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 visible_height = area.height.saturating_sub(2) as usize; let total_lines = lines.len(); 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 = lines.into_iter().skip(scroll).take(visible_height).collect(); + let visible: Vec = lines + .into_iter() + .skip(scroll) + .take(visible_height) + .collect(); - let para = Paragraph::new(visible) - .block(Block::default().borders(Borders::ALL).title(title)); + let para = Paragraph::new(visible).block(Block::default().borders(Borders::ALL).title(title)); frame.render_widget(para, area); } @@ -80,12 +82,8 @@ fn composite_to_line(composite: Composite) -> RLine<'static> { CompositeStyle::Header(1) => Style::new() .fg(Color::Cyan) .add_modifier(Modifier::BOLD | Modifier::UNDERLINED), - CompositeStyle::Header(2) => Style::new() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - CompositeStyle::Header(_) => Style::new() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), + CompositeStyle::Header(2) => Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD), + CompositeStyle::Header(_) => Style::new().fg(Color::Magenta).add_modifier(Modifier::BOLD), CompositeStyle::ListItem(_) => Style::new().fg(Color::White), CompositeStyle::Quote => Style::new().fg(Color::Rgb(150, 150, 150)), CompositeStyle::Code => Style::new().fg(Color::Green), diff --git a/seq/src/views/main_view.rs b/seq/src/views/main_view.rs index e0de4a2..717fe99 100644 --- a/seq/src/views/main_view.rs +++ b/seq/src/views/main_view.rs @@ -113,7 +113,7 @@ fn render_tile( let is_active = step.map(|s| s.active).unwrap_or(false); 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| { let s = snapshot.slot_data[i]; 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) } else if app.editor_ctx.focus == Focus::Editor { Style::new().fg(Color::Rgb(100, 160, 180)) diff --git a/seq/src/views/patterns_view.rs b/seq/src/views/patterns_view.rs index bd3f95f..c0fb724 100644 --- a/seq/src/views/patterns_view.rs +++ b/seq/src/views/patterns_view.rs @@ -40,6 +40,7 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area .collect(); let bank_names: Vec> = app + .project_state .project .banks .iter() @@ -63,7 +64,7 @@ fn render_patterns( area: Rect, 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 { Some(name) => format!("{name} › Patterns"), None => format!("Bank {:02} › Patterns", bank + 1), @@ -102,7 +103,7 @@ fn render_patterns( usize::MAX }; - let pattern_names: Vec> = app.project.banks[bank] + let pattern_names: Vec> = app.project_state.project.banks[bank] .patterns .iter() .map(|p| p.name.as_deref())