Visual
This commit is contained in:
0
seq/cpu.prof
Normal file
0
seq/cpu.prof
Normal file
42841
seq/something
Normal file
42841
seq/something
Normal file
File diff suppressed because it is too large
Load Diff
116
seq/src/app.rs
116
seq/src/app.rs
@@ -11,7 +11,7 @@ use crate::config::MAX_SLOTS;
|
|||||||
use crate::engine::{
|
use crate::engine::{
|
||||||
LinkState, PatternSnapshot, SeqCommand, SequencerSnapshot, SlotChange, StepSnapshot,
|
LinkState, PatternSnapshot, SeqCommand, SequencerSnapshot, SlotChange, StepSnapshot,
|
||||||
};
|
};
|
||||||
use crate::model::{self, Pattern, Rng, ScriptEngine, StepContext, Variables};
|
use crate::model::{self, Bank, Pattern, Rng, ScriptEngine, StepContext, Variables};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::services::pattern_editor;
|
use crate::services::pattern_editor;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
@@ -36,6 +36,8 @@ pub struct App {
|
|||||||
pub variables: Variables,
|
pub variables: Variables,
|
||||||
pub rng: Rng,
|
pub rng: Rng,
|
||||||
pub clipboard: Option<arboard::Clipboard>,
|
pub clipboard: Option<arboard::Clipboard>,
|
||||||
|
pub copied_pattern: Option<Pattern>,
|
||||||
|
pub copied_bank: Option<Bank>,
|
||||||
|
|
||||||
pub audio: AudioSettings,
|
pub audio: AudioSettings,
|
||||||
}
|
}
|
||||||
@@ -62,6 +64,8 @@ impl App {
|
|||||||
rng,
|
rng,
|
||||||
script_engine,
|
script_engine,
|
||||||
clipboard: arboard::Clipboard::new().ok(),
|
clipboard: arboard::Clipboard::new().ok(),
|
||||||
|
copied_pattern: None,
|
||||||
|
copied_bank: None,
|
||||||
|
|
||||||
audio: AudioSettings::default(),
|
audio: AudioSettings::default(),
|
||||||
}
|
}
|
||||||
@@ -351,30 +355,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_pattern_queued(
|
|
||||||
&self,
|
|
||||||
bank: usize,
|
|
||||||
pattern: usize,
|
|
||||||
snapshot: &SequencerSnapshot,
|
|
||||||
) -> Option<bool> {
|
|
||||||
self.playback.queued_changes.iter().find_map(|c| match *c {
|
|
||||||
SlotChange::Add {
|
|
||||||
slot: _,
|
|
||||||
bank: b,
|
|
||||||
pattern: p,
|
|
||||||
} if b == bank && p == pattern => Some(true),
|
|
||||||
SlotChange::Remove { slot } => {
|
|
||||||
let s = snapshot.slot_data[slot];
|
|
||||||
if s.active && s.bank == bank && s.pattern == pattern {
|
|
||||||
Some(false)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_pattern_playback(
|
pub fn toggle_pattern_playback(
|
||||||
&mut self,
|
&mut self,
|
||||||
bank: usize,
|
bank: usize,
|
||||||
@@ -539,6 +519,74 @@ impl App {
|
|||||||
self.ui.flash("Step deleted", 150);
|
self.ui.flash("Step deleted", 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset_pattern(&mut self, bank: usize, pattern: usize) {
|
||||||
|
self.project_state.project.banks[bank].patterns[pattern] = Pattern::default();
|
||||||
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
|
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||||
|
self.load_step_to_editor();
|
||||||
|
}
|
||||||
|
self.ui.flash("Pattern reset", 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_bank(&mut self, bank: usize) {
|
||||||
|
self.project_state.project.banks[bank] = Bank::default();
|
||||||
|
for pattern in 0..self.project_state.project.banks[bank].patterns.len() {
|
||||||
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
|
}
|
||||||
|
if self.editor_ctx.bank == bank {
|
||||||
|
self.load_step_to_editor();
|
||||||
|
}
|
||||||
|
self.ui.flash("Bank reset", 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_pattern(&mut self, bank: usize, pattern: usize) {
|
||||||
|
let pat = self.project_state.project.banks[bank].patterns[pattern].clone();
|
||||||
|
self.copied_pattern = Some(pat);
|
||||||
|
self.ui.flash("Pattern copied", 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste_pattern(&mut self, bank: usize, pattern: usize) {
|
||||||
|
if let Some(src) = &self.copied_pattern {
|
||||||
|
let mut pat = src.clone();
|
||||||
|
pat.name = match &src.name {
|
||||||
|
Some(name) if !name.ends_with(" (copy)") => Some(format!("{} (copy)", name)),
|
||||||
|
Some(name) => Some(name.clone()),
|
||||||
|
None => Some("(copy)".to_string()),
|
||||||
|
};
|
||||||
|
self.project_state.project.banks[bank].patterns[pattern] = pat;
|
||||||
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
|
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||||
|
self.load_step_to_editor();
|
||||||
|
}
|
||||||
|
self.ui.flash("Pattern pasted", 150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_bank(&mut self, bank: usize) {
|
||||||
|
let b = self.project_state.project.banks[bank].clone();
|
||||||
|
self.copied_bank = Some(b);
|
||||||
|
self.ui.flash("Bank copied", 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste_bank(&mut self, bank: usize) {
|
||||||
|
if let Some(src) = &self.copied_bank {
|
||||||
|
let mut b = src.clone();
|
||||||
|
b.name = match &src.name {
|
||||||
|
Some(name) if !name.ends_with(" (copy)") => Some(format!("{} (copy)", name)),
|
||||||
|
Some(name) => Some(name.clone()),
|
||||||
|
None => Some("(copy)".to_string()),
|
||||||
|
};
|
||||||
|
self.project_state.project.banks[bank] = b;
|
||||||
|
for pattern in 0..self.project_state.project.banks[bank].patterns.len() {
|
||||||
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
|
}
|
||||||
|
if self.editor_ctx.bank == bank {
|
||||||
|
self.load_step_to_editor();
|
||||||
|
}
|
||||||
|
self.ui.flash("Bank pasted", 150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn paste_step(&mut self, link: &LinkState) {
|
pub fn paste_step(&mut self, link: &LinkState) {
|
||||||
let text = self
|
let text = self
|
||||||
.clipboard
|
.clipboard
|
||||||
@@ -717,6 +765,24 @@ impl App {
|
|||||||
} => {
|
} => {
|
||||||
self.delete_step(bank, pattern, step);
|
self.delete_step(bank, pattern, step);
|
||||||
}
|
}
|
||||||
|
AppCommand::ResetPattern { bank, pattern } => {
|
||||||
|
self.reset_pattern(bank, pattern);
|
||||||
|
}
|
||||||
|
AppCommand::ResetBank { bank } => {
|
||||||
|
self.reset_bank(bank);
|
||||||
|
}
|
||||||
|
AppCommand::CopyPattern { bank, pattern } => {
|
||||||
|
self.copy_pattern(bank, pattern);
|
||||||
|
}
|
||||||
|
AppCommand::PastePattern { bank, pattern } => {
|
||||||
|
self.paste_pattern(bank, pattern);
|
||||||
|
}
|
||||||
|
AppCommand::CopyBank { bank } => {
|
||||||
|
self.copy_bank(bank);
|
||||||
|
}
|
||||||
|
AppCommand::PasteBank { bank } => {
|
||||||
|
self.paste_bank(bank);
|
||||||
|
}
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
AppCommand::CopyStep => self.copy_step(),
|
AppCommand::CopyStep => self.copy_step(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::engine::SlotChange;
|
|||||||
use crate::model::PatternSpeed;
|
use crate::model::PatternSpeed;
|
||||||
use crate::state::{Modal, PatternField};
|
use crate::state::{Modal, PatternField};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum AppCommand {
|
pub enum AppCommand {
|
||||||
// Playback
|
// Playback
|
||||||
TogglePlaying,
|
TogglePlaying,
|
||||||
@@ -45,6 +46,27 @@ pub enum AppCommand {
|
|||||||
pattern: usize,
|
pattern: usize,
|
||||||
step: usize,
|
step: usize,
|
||||||
},
|
},
|
||||||
|
ResetPattern {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
},
|
||||||
|
ResetBank {
|
||||||
|
bank: usize,
|
||||||
|
},
|
||||||
|
CopyPattern {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
},
|
||||||
|
PastePattern {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
},
|
||||||
|
CopyBank {
|
||||||
|
bank: usize,
|
||||||
|
},
|
||||||
|
PasteBank {
|
||||||
|
bank: usize,
|
||||||
|
},
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
CopyStep,
|
CopyStep,
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ fn sequencer_loop(
|
|||||||
slot_steps: [Arc<AtomicUsize>; MAX_SLOTS],
|
slot_steps: [Arc<AtomicUsize>; MAX_SLOTS],
|
||||||
event_count: Arc<AtomicUsize>,
|
event_count: Arc<AtomicUsize>,
|
||||||
) {
|
) {
|
||||||
let script_engine = ScriptEngine::new(variables, rng);
|
let script_engine = ScriptEngine::new(Arc::clone(&variables), rng);
|
||||||
let mut audio_state = AudioState::new();
|
let mut audio_state = AudioState::new();
|
||||||
let mut pattern_cache = PatternCache::new();
|
let mut pattern_cache = PatternCache::new();
|
||||||
let mut runs_counter = RunsCounter::new();
|
let mut runs_counter = RunsCounter::new();
|
||||||
@@ -413,6 +413,12 @@ fn sequencer_loop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(new_tempo) = {
|
||||||
|
let mut vars = variables.lock().unwrap();
|
||||||
|
vars.remove("__tempo__").and_then(|v| v.as_float().ok())
|
||||||
|
} {
|
||||||
|
link.set_tempo(new_tempo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
seq/src/input.rs
111
seq/src/input.rs
@@ -107,6 +107,70 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Modal::ConfirmResetPattern {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
selected: _,
|
||||||
|
} => {
|
||||||
|
let (bank, pattern) = (*bank, *pattern);
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||||
|
ctx.dispatch(AppCommand::ResetPattern { bank, pattern });
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
KeyCode::Left | KeyCode::Right => {
|
||||||
|
if let Modal::ConfirmResetPattern { selected, .. } = &mut ctx.app.ui.modal {
|
||||||
|
*selected = !*selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
let do_reset =
|
||||||
|
if let Modal::ConfirmResetPattern { selected, .. } = &ctx.app.ui.modal {
|
||||||
|
*selected
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if do_reset {
|
||||||
|
ctx.dispatch(AppCommand::ResetPattern { bank, pattern });
|
||||||
|
}
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Modal::ConfirmResetBank { bank, selected: _ } => {
|
||||||
|
let bank = *bank;
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||||
|
ctx.dispatch(AppCommand::ResetBank { bank });
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
KeyCode::Left | KeyCode::Right => {
|
||||||
|
if let Modal::ConfirmResetBank { selected, .. } = &mut ctx.app.ui.modal {
|
||||||
|
*selected = !*selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
let do_reset =
|
||||||
|
if let Modal::ConfirmResetBank { selected, .. } = &ctx.app.ui.modal {
|
||||||
|
*selected
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if do_reset {
|
||||||
|
ctx.dispatch(AppCommand::ResetBank { bank });
|
||||||
|
}
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
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());
|
||||||
@@ -400,12 +464,14 @@ 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 {
|
||||||
use crate::state::PatternsColumn;
|
use crate::state::PatternsColumn;
|
||||||
|
|
||||||
|
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Left => ctx.dispatch(AppCommand::PatternsCursorLeft),
|
KeyCode::Left => ctx.dispatch(AppCommand::PatternsCursorLeft),
|
||||||
KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight),
|
KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight),
|
||||||
KeyCode::Up => ctx.dispatch(AppCommand::PatternsCursorUp),
|
KeyCode::Up => ctx.dispatch(AppCommand::PatternsCursorUp),
|
||||||
KeyCode::Down => ctx.dispatch(AppCommand::PatternsCursorDown),
|
KeyCode::Down => ctx.dispatch(AppCommand::PatternsCursorDown),
|
||||||
KeyCode::Esc | KeyCode::Backspace => ctx.dispatch(AppCommand::PatternsBack),
|
KeyCode::Esc => ctx.dispatch(AppCommand::PatternsBack),
|
||||||
KeyCode::Enter => ctx.dispatch(AppCommand::PatternsEnter),
|
KeyCode::Enter => ctx.dispatch(AppCommand::PatternsEnter),
|
||||||
KeyCode::Char(' ') => ctx.dispatch(AppCommand::PatternsTogglePlay),
|
KeyCode::Char(' ') => ctx.dispatch(AppCommand::PatternsTogglePlay),
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
@@ -413,6 +479,49 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
selected: false,
|
selected: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('c') if ctrl => {
|
||||||
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
|
match ctx.app.patterns_nav.column {
|
||||||
|
PatternsColumn::Banks => {
|
||||||
|
ctx.dispatch(AppCommand::CopyBank { bank });
|
||||||
|
}
|
||||||
|
PatternsColumn::Patterns => {
|
||||||
|
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||||
|
ctx.dispatch(AppCommand::CopyPattern { bank, pattern });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('v') if ctrl => {
|
||||||
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
|
match ctx.app.patterns_nav.column {
|
||||||
|
PatternsColumn::Banks => {
|
||||||
|
ctx.dispatch(AppCommand::PasteBank { bank });
|
||||||
|
}
|
||||||
|
PatternsColumn::Patterns => {
|
||||||
|
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||||
|
ctx.dispatch(AppCommand::PastePattern { bank, pattern });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Delete | KeyCode::Backspace => {
|
||||||
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
|
match ctx.app.patterns_nav.column {
|
||||||
|
PatternsColumn::Banks => {
|
||||||
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetBank {
|
||||||
|
bank,
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
PatternsColumn::Patterns => {
|
||||||
|
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||||
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmResetPattern {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
KeyCode::Char('r') => {
|
KeyCode::Char('r') => {
|
||||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
match ctx.app.patterns_nav.column {
|
match ctx.app.patterns_nav.column {
|
||||||
|
|||||||
@@ -25,17 +25,37 @@ pub type Variables = Arc<Mutex<HashMap<String, Value>>>;
|
|||||||
pub type Rng = Arc<Mutex<StdRng>>;
|
pub type Rng = Arc<Mutex<StdRng>>;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum Value {
|
pub enum Value {
|
||||||
Int(i64),
|
Int(i64),
|
||||||
Float(f64),
|
Float(f64),
|
||||||
Str(String),
|
Str(String),
|
||||||
Cmd(Vec<(String, String)>),
|
|
||||||
Param(String, String),
|
|
||||||
Marker,
|
Marker,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct CmdRegister {
|
||||||
|
sound: Option<String>,
|
||||||
|
params: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CmdRegister {
|
||||||
|
fn set_sound(&mut self, name: String) {
|
||||||
|
self.sound = Some(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_param(&mut self, key: String, value: String) {
|
||||||
|
self.params.push((key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take(&mut self) -> Option<(String, Vec<(String, String)>)> {
|
||||||
|
let sound = self.sound.take()?;
|
||||||
|
let params = std::mem::take(&mut self.params);
|
||||||
|
Some((sound, params))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Value {
|
impl Value {
|
||||||
fn as_float(&self) -> Result<f64, String> {
|
pub fn as_float(&self) -> Result<f64, String> {
|
||||||
match self {
|
match self {
|
||||||
Value::Float(f) => Ok(*f),
|
Value::Float(f) => Ok(*f),
|
||||||
Value::Int(i) => Ok(*i as f64),
|
Value::Int(i) => Ok(*i as f64),
|
||||||
@@ -63,8 +83,6 @@ impl Value {
|
|||||||
Value::Int(i) => *i != 0,
|
Value::Int(i) => *i != 0,
|
||||||
Value::Float(f) => *f != 0.0,
|
Value::Float(f) => *f != 0.0,
|
||||||
Value::Str(s) => !s.is_empty(),
|
Value::Str(s) => !s.is_empty(),
|
||||||
Value::Cmd(_) => true,
|
|
||||||
Value::Param(_, _) => true,
|
|
||||||
Value::Marker => false,
|
Value::Marker => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,16 +91,12 @@ impl Value {
|
|||||||
matches!(self, Value::Marker)
|
matches!(self, Value::Marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_param(&self) -> bool {
|
|
||||||
matches!(self, Value::Param(_, _))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_param_string(&self) -> String {
|
fn to_param_string(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Value::Int(i) => i.to_string(),
|
Value::Int(i) => i.to_string(),
|
||||||
Value::Float(f) => f.to_string(),
|
Value::Float(f) => f.to_string(),
|
||||||
Value::Str(s) => s.clone(),
|
Value::Str(s) => s.clone(),
|
||||||
Value::Cmd(_) | Value::Param(_, _) | Value::Marker => String::new(),
|
Value::Marker => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,9 +146,21 @@ enum Op {
|
|||||||
Choose,
|
Choose,
|
||||||
Chance,
|
Chance,
|
||||||
Maybe,
|
Maybe,
|
||||||
Wait,
|
|
||||||
ListStart,
|
ListStart,
|
||||||
ListEnd,
|
ListEnd,
|
||||||
|
At,
|
||||||
|
Window,
|
||||||
|
Pop,
|
||||||
|
Subdivide,
|
||||||
|
SetTempo,
|
||||||
|
Each,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct TimeContext {
|
||||||
|
start: f64,
|
||||||
|
duration: f64,
|
||||||
|
subdivisions: Option<Vec<(f64, f64)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -386,7 +412,12 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
|
|||||||
ops.push(Op::PushFloat(0.9));
|
ops.push(Op::PushFloat(0.9));
|
||||||
ops.push(Op::Maybe);
|
ops.push(Op::Maybe);
|
||||||
}
|
}
|
||||||
"wait" => ops.push(Op::Wait),
|
"at" => ops.push(Op::At),
|
||||||
|
"window" => ops.push(Op::Window),
|
||||||
|
"pop" => ops.push(Op::Pop),
|
||||||
|
"div" => ops.push(Op::Subdivide),
|
||||||
|
"each" => ops.push(Op::Each),
|
||||||
|
"tempo!" => ops.push(Op::SetTempo),
|
||||||
"[" => ops.push(Op::ListStart),
|
"[" => ops.push(Op::ListStart),
|
||||||
"]" => ops.push(Op::ListEnd),
|
"]" => ops.push(Op::ListEnd),
|
||||||
"step" => ops.push(Op::GetContext("step".into())),
|
"step" => ops.push(Op::GetContext("step".into())),
|
||||||
@@ -397,7 +428,6 @@ fn compile(tokens: &[Token]) -> Result<Vec<Op>, String> {
|
|||||||
"phase" => ops.push(Op::GetContext("phase".into())),
|
"phase" => ops.push(Op::GetContext("phase".into())),
|
||||||
"slot" => ops.push(Op::GetContext("slot".into())),
|
"slot" => ops.push(Op::GetContext("slot".into())),
|
||||||
"runs" => ops.push(Op::GetContext("runs".into())),
|
"runs" => ops.push(Op::GetContext("runs".into())),
|
||||||
"speed" => ops.push(Op::GetContext("speed".into())),
|
|
||||||
"stepdur" => ops.push(Op::GetContext("stepdur".into())),
|
"stepdur" => ops.push(Op::GetContext("stepdur".into())),
|
||||||
"if" => {
|
"if" => {
|
||||||
let (then_ops, else_ops, consumed) = compile_if(&tokens[i + 1..])?;
|
let (then_ops, else_ops, consumed) = compile_if(&tokens[i + 1..])?;
|
||||||
@@ -487,7 +517,12 @@ impl Forth {
|
|||||||
fn execute(&self, ops: &[Op], ctx: &StepContext) -> Result<Vec<String>, String> {
|
fn execute(&self, ops: &[Op], ctx: &StepContext) -> Result<Vec<String>, String> {
|
||||||
let mut stack: Vec<Value> = Vec::new();
|
let mut stack: Vec<Value> = Vec::new();
|
||||||
let mut outputs: Vec<String> = Vec::new();
|
let mut outputs: Vec<String> = Vec::new();
|
||||||
let mut time_offset: f64 = 0.0;
|
let mut time_stack: Vec<TimeContext> = vec![TimeContext {
|
||||||
|
start: 0.0,
|
||||||
|
duration: ctx.step_duration(),
|
||||||
|
subdivisions: None,
|
||||||
|
}];
|
||||||
|
let mut cmd = CmdRegister::default();
|
||||||
let mut pc = 0;
|
let mut pc = 0;
|
||||||
|
|
||||||
while pc < ops.len() {
|
while pc < ops.len() {
|
||||||
@@ -605,45 +640,31 @@ impl Forth {
|
|||||||
Op::NewCmd => {
|
Op::NewCmd => {
|
||||||
let name = stack.pop().ok_or("stack underflow")?;
|
let name = stack.pop().ok_or("stack underflow")?;
|
||||||
let name = name.as_str()?;
|
let name = name.as_str()?;
|
||||||
stack.push(Value::Cmd(vec![("sound".into(), name.into())]));
|
cmd.set_sound(name.to_string());
|
||||||
}
|
}
|
||||||
Op::SetParam(param) => {
|
Op::SetParam(param) => {
|
||||||
let val = stack.pop().ok_or("stack underflow")?;
|
let val = stack.pop().ok_or("stack underflow")?;
|
||||||
stack.push(Value::Param(param.clone(), val.to_param_string()));
|
cmd.set_param(param.clone(), val.to_param_string());
|
||||||
}
|
}
|
||||||
Op::Emit => {
|
Op::Emit => {
|
||||||
let mut params = Vec::new();
|
let (sound, mut params) = cmd.take().ok_or("no sound set")?;
|
||||||
while let Some(v) = stack.last() {
|
let mut pairs = vec![("sound".into(), sound)];
|
||||||
if v.is_param() {
|
pairs.append(&mut params);
|
||||||
if let Value::Param(k, v) = stack.pop().unwrap() {
|
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
|
||||||
params.push((k, v));
|
if time_ctx.start > 0.0 {
|
||||||
}
|
pairs.push(("delta".into(), time_ctx.start.to_string()));
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
params.reverse();
|
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||||
let cmd = stack.pop().ok_or("stack underflow")?;
|
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||||
if let Value::Cmd(mut pairs) = cmd {
|
}
|
||||||
pairs.extend(params);
|
let stepdur = ctx.step_duration();
|
||||||
if time_offset > 0.0 {
|
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
||||||
pairs.push(("delta".into(), time_offset.to_string()));
|
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
||||||
}
|
pairs[idx].1 = (ratio * stepdur).to_string();
|
||||||
let has_dur = pairs.iter().any(|(k, _)| k == "dur");
|
|
||||||
if !has_dur {
|
|
||||||
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
|
||||||
}
|
|
||||||
let stepdur = ctx.step_duration();
|
|
||||||
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
|
||||||
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
|
||||||
pairs[idx].1 = (ratio * stepdur).to_string();
|
|
||||||
} else {
|
|
||||||
pairs.push(("delaytime".into(), stepdur.to_string()));
|
|
||||||
}
|
|
||||||
outputs.push(format_cmd(&pairs));
|
|
||||||
} else {
|
} else {
|
||||||
return Err("expected command".into());
|
pairs.push(("delaytime".into(), stepdur.to_string()));
|
||||||
}
|
}
|
||||||
|
outputs.push(format_cmd(&pairs));
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Get => {
|
Op::Get => {
|
||||||
@@ -729,20 +750,100 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Op::Maybe => {
|
Op::Maybe => {
|
||||||
let prob = stack.pop().ok_or("stack underflow")?.as_float()?;
|
return Err("? is not yet implemented with the new param model".into());
|
||||||
let param = stack.pop().ok_or("stack underflow")?;
|
}
|
||||||
if !param.is_param() {
|
|
||||||
return Err("? requires a param".into());
|
Op::At => {
|
||||||
|
let pos = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let (sound, mut params) = cmd.take().ok_or("no sound set")?;
|
||||||
|
let mut pairs = vec![("sound".into(), sound)];
|
||||||
|
pairs.append(&mut params);
|
||||||
|
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
|
||||||
|
let absolute_time = time_ctx.start + time_ctx.duration * pos;
|
||||||
|
if absolute_time > 0.0 {
|
||||||
|
pairs.push(("delta".into(), absolute_time.to_string()));
|
||||||
}
|
}
|
||||||
let val: f64 = self.rng.lock().unwrap().gen();
|
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||||
if val < prob {
|
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||||
stack.push(param);
|
}
|
||||||
|
let stepdur = ctx.step_duration();
|
||||||
|
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
||||||
|
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
||||||
|
pairs[idx].1 = (ratio * stepdur).to_string();
|
||||||
|
} else {
|
||||||
|
pairs.push(("delaytime".into(), stepdur.to_string()));
|
||||||
|
}
|
||||||
|
outputs.push(format_cmd(&pairs));
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Window => {
|
||||||
|
let end = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let start_pos = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
|
let parent = time_stack.last().ok_or("time stack underflow")?;
|
||||||
|
let new_start = parent.start + parent.duration * start_pos;
|
||||||
|
let new_duration = parent.duration * (end - start_pos);
|
||||||
|
time_stack.push(TimeContext {
|
||||||
|
start: new_start,
|
||||||
|
duration: new_duration,
|
||||||
|
subdivisions: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Pop => {
|
||||||
|
if time_stack.len() <= 1 {
|
||||||
|
return Err("cannot pop root time context".into());
|
||||||
|
}
|
||||||
|
time_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Subdivide => {
|
||||||
|
let n = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
|
if n == 0 {
|
||||||
|
return Err("subdivide count must be > 0".into());
|
||||||
|
}
|
||||||
|
let time_ctx = time_stack.last_mut().ok_or("time stack underflow")?;
|
||||||
|
let sub_duration = time_ctx.duration / n as f64;
|
||||||
|
let mut subs = Vec::with_capacity(n);
|
||||||
|
for i in 0..n {
|
||||||
|
subs.push((time_ctx.start + sub_duration * i as f64, sub_duration));
|
||||||
|
}
|
||||||
|
time_ctx.subdivisions = Some(subs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::Each => {
|
||||||
|
let (sound, params) = cmd.take().ok_or("no sound set")?;
|
||||||
|
let time_ctx = time_stack.last().ok_or("time stack underflow")?;
|
||||||
|
let subs = time_ctx
|
||||||
|
.subdivisions
|
||||||
|
.as_ref()
|
||||||
|
.ok_or("each requires subdivide first")?;
|
||||||
|
for (sub_start, _sub_dur) in subs {
|
||||||
|
let mut pairs = vec![("sound".into(), sound.clone())];
|
||||||
|
pairs.extend(params.iter().cloned());
|
||||||
|
if *sub_start > 0.0 {
|
||||||
|
pairs.push(("delta".into(), sub_start.to_string()));
|
||||||
|
}
|
||||||
|
if !pairs.iter().any(|(k, _)| k == "dur") {
|
||||||
|
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||||
|
}
|
||||||
|
let stepdur = ctx.step_duration();
|
||||||
|
if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") {
|
||||||
|
let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0);
|
||||||
|
pairs[idx].1 = (ratio * stepdur).to_string();
|
||||||
|
} else {
|
||||||
|
pairs.push(("delaytime".into(), stepdur.to_string()));
|
||||||
|
}
|
||||||
|
outputs.push(format_cmd(&pairs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Wait => {
|
Op::SetTempo => {
|
||||||
let duration = stack.pop().ok_or("stack underflow")?.as_float()?;
|
let tempo = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
time_offset += duration;
|
let clamped = tempo.clamp(20.0, 300.0);
|
||||||
|
self.vars
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert("__tempo__".to_string(), Value::Float(clamped));
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::ListStart => {
|
Op::ListStart => {
|
||||||
@@ -770,7 +871,11 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if outputs.is_empty() {
|
if outputs.is_empty() {
|
||||||
if let Some(Value::Cmd(pairs)) = stack.pop() {
|
if let Some((sound, params)) = cmd.take() {
|
||||||
|
let mut pairs = vec![("sound".into(), sound)];
|
||||||
|
pairs.extend(params);
|
||||||
|
pairs.push(("dur".into(), ctx.step_duration().to_string()));
|
||||||
|
pairs.push(("delaytime".into(), ctx.step_duration().to_string()));
|
||||||
outputs.push(format_cmd(&pairs));
|
outputs.push(format_cmd(&pairs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ pub enum Modal {
|
|||||||
step: usize,
|
step: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
},
|
},
|
||||||
|
ConfirmResetPattern {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
selected: bool,
|
||||||
|
},
|
||||||
|
ConfirmResetBank {
|
||||||
|
bank: usize,
|
||||||
|
selected: bool,
|
||||||
|
},
|
||||||
SaveAs(String),
|
SaveAs(String),
|
||||||
LoadFrom(String),
|
LoadFrom(String),
|
||||||
RenameBank {
|
RenameBank {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
use ratatui::text::{Line, Span};
|
||||||
|
use ratatui::widgets::{Block, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
@@ -8,28 +9,34 @@ use crate::engine::SequencerSnapshot;
|
|||||||
use crate::state::PatternsColumn;
|
use crate::state::PatternsColumn;
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||||
let [banks_area, patterns_area] =
|
let [banks_area, gap, patterns_area] = Layout::horizontal([
|
||||||
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(area);
|
Constraint::Fill(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
])
|
||||||
|
.areas(area);
|
||||||
|
|
||||||
render_banks(frame, app, snapshot, banks_area);
|
render_banks(frame, app, snapshot, banks_area);
|
||||||
|
// gap is just empty space
|
||||||
|
let _ = gap;
|
||||||
render_patterns(frame, app, snapshot, patterns_area);
|
render_patterns(frame, app, snapshot, patterns_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||||
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Banks);
|
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Banks);
|
||||||
let border_color = if is_focused {
|
|
||||||
|
let [title_area, inner] =
|
||||||
|
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
|
||||||
|
|
||||||
|
let title_color = if is_focused {
|
||||||
Color::Rgb(100, 160, 180)
|
Color::Rgb(100, 160, 180)
|
||||||
} else {
|
} else {
|
||||||
Color::Rgb(70, 75, 85)
|
Color::Rgb(70, 75, 85)
|
||||||
};
|
};
|
||||||
|
let title = Paragraph::new("Banks")
|
||||||
let block = Block::default()
|
.style(Style::new().fg(title_color))
|
||||||
.borders(Borders::ALL)
|
.alignment(ratatui::layout::Alignment::Center);
|
||||||
.border_style(Style::new().fg(border_color))
|
frame.render_widget(title, title_area);
|
||||||
.title("Banks");
|
|
||||||
|
|
||||||
let inner = block.inner(area);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let banks_with_playback: Vec<usize> = snapshot
|
let banks_with_playback: Vec<usize> = snapshot
|
||||||
.slot_data
|
.slot_data
|
||||||
@@ -48,14 +55,26 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let rows: Vec<Constraint> = (0..16).map(|_| Constraint::Length(1)).collect();
|
let row_height = (inner.height / 16).max(1);
|
||||||
let row_areas = Layout::vertical(rows).split(inner);
|
let total_needed = row_height * 16;
|
||||||
|
let top_padding = if inner.height > total_needed {
|
||||||
|
(inner.height - total_needed) / 2
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
for idx in 0..16 {
|
for idx in 0..16 {
|
||||||
if idx >= row_areas.len() {
|
let y = inner.y + top_padding + (idx as u16) * row_height;
|
||||||
|
if y >= inner.y + inner.height {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let row_area = row_areas[idx];
|
|
||||||
|
let row_area = Rect {
|
||||||
|
x: inner.x,
|
||||||
|
y,
|
||||||
|
width: inner.width,
|
||||||
|
height: row_height.min(inner.y + inner.height - y),
|
||||||
|
};
|
||||||
|
|
||||||
let is_cursor = is_focused && idx == app.patterns_nav.bank_cursor;
|
let is_cursor = is_focused && idx == app.patterns_nav.bank_cursor;
|
||||||
let is_selected = idx == app.patterns_nav.bank_cursor;
|
let is_selected = idx == app.patterns_nav.bank_cursor;
|
||||||
@@ -89,14 +108,35 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
|
|||||||
style
|
style
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fill the entire row with background color
|
||||||
|
let bg_block = Block::default().style(Style::new().bg(bg));
|
||||||
|
frame.render_widget(bg_block, row_area);
|
||||||
|
|
||||||
|
let text_y = if row_height > 1 {
|
||||||
|
row_area.y + (row_height - 1) / 2
|
||||||
|
} else {
|
||||||
|
row_area.y
|
||||||
|
};
|
||||||
|
let text_area = Rect {
|
||||||
|
x: row_area.x,
|
||||||
|
y: text_y,
|
||||||
|
width: row_area.width,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
let para = Paragraph::new(label).style(style);
|
let para = Paragraph::new(label).style(style);
|
||||||
frame.render_widget(para, row_area);
|
frame.render_widget(para, text_area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||||
|
use crate::model::PatternSpeed;
|
||||||
|
|
||||||
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Patterns);
|
let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Patterns);
|
||||||
let border_color = if is_focused {
|
|
||||||
|
let [title_area, inner] =
|
||||||
|
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area);
|
||||||
|
|
||||||
|
let title_color = if is_focused {
|
||||||
Color::Rgb(100, 160, 180)
|
Color::Rgb(100, 160, 180)
|
||||||
} else {
|
} else {
|
||||||
Color::Rgb(70, 75, 85)
|
Color::Rgb(70, 75, 85)
|
||||||
@@ -104,18 +144,14 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
|
|
||||||
let bank = app.patterns_nav.bank_cursor;
|
let bank = app.patterns_nav.bank_cursor;
|
||||||
let bank_name = app.project_state.project.banks[bank].name.as_deref();
|
let bank_name = app.project_state.project.banks[bank].name.as_deref();
|
||||||
let title = match bank_name {
|
let title_text = match bank_name {
|
||||||
Some(name) => format!("Patterns ({name})"),
|
Some(name) => format!("Patterns ({name})"),
|
||||||
None => format!("Patterns (Bank {:02})", bank + 1),
|
None => format!("Patterns (Bank {:02})", bank + 1),
|
||||||
};
|
};
|
||||||
|
let title = Paragraph::new(title_text)
|
||||||
let block = Block::default()
|
.style(Style::new().fg(title_color))
|
||||||
.borders(Borders::ALL)
|
.alignment(ratatui::layout::Alignment::Center);
|
||||||
.border_style(Style::new().fg(border_color))
|
frame.render_widget(title, title_area);
|
||||||
.title(title);
|
|
||||||
|
|
||||||
let inner = block.inner(area);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let playing_patterns: Vec<usize> = snapshot
|
let playing_patterns: Vec<usize> = snapshot
|
||||||
.slot_data
|
.slot_data
|
||||||
@@ -159,14 +195,26 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let rows: Vec<Constraint> = (0..16).map(|_| Constraint::Length(1)).collect();
|
let row_height = (inner.height / 16).max(1);
|
||||||
let row_areas = Layout::vertical(rows).split(inner);
|
let total_needed = row_height * 16;
|
||||||
|
let top_padding = if inner.height > total_needed {
|
||||||
|
(inner.height - total_needed) / 2
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
for idx in 0..16 {
|
for idx in 0..16 {
|
||||||
if idx >= row_areas.len() {
|
let y = inner.y + top_padding + (idx as u16) * row_height;
|
||||||
|
if y >= inner.y + inner.height {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let row_area = row_areas[idx];
|
|
||||||
|
let row_area = Rect {
|
||||||
|
x: inner.x,
|
||||||
|
y,
|
||||||
|
width: inner.width,
|
||||||
|
height: row_height.min(inner.y + inner.height - y),
|
||||||
|
};
|
||||||
|
|
||||||
let is_cursor = is_focused && idx == app.patterns_nav.pattern_cursor;
|
let is_cursor = is_focused && idx == app.patterns_nav.pattern_cursor;
|
||||||
let is_selected = idx == app.patterns_nav.pattern_cursor;
|
let is_selected = idx == app.patterns_nav.pattern_cursor;
|
||||||
@@ -185,24 +233,70 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
(false, false, false, _) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
(false, false, false, _) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = app.project_state.project.banks[bank].patterns[idx]
|
let pattern = &app.project_state.project.banks[bank].patterns[idx];
|
||||||
.name
|
let name = pattern.name.as_deref().unwrap_or("");
|
||||||
.as_deref()
|
let length = pattern.length;
|
||||||
.unwrap_or("");
|
let speed = pattern.speed;
|
||||||
let label = if name.is_empty() {
|
|
||||||
|
let base_style = Style::new().bg(bg).fg(fg);
|
||||||
|
let bold_style = base_style.add_modifier(Modifier::BOLD);
|
||||||
|
|
||||||
|
// Fill the entire row with background color
|
||||||
|
let bg_block = Block::default().style(Style::new().bg(bg));
|
||||||
|
frame.render_widget(bg_block, row_area);
|
||||||
|
|
||||||
|
let text_y = if row_height > 1 {
|
||||||
|
row_area.y + (row_height - 1) / 2
|
||||||
|
} else {
|
||||||
|
row_area.y
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split row into columns: [index+name] [length] [speed]
|
||||||
|
let speed_width: u16 = 14; // "Speed: 1/4x "
|
||||||
|
let length_width: u16 = 13; // "Length: 16 "
|
||||||
|
let name_width = row_area
|
||||||
|
.width
|
||||||
|
.saturating_sub(speed_width + length_width + 2);
|
||||||
|
|
||||||
|
let [name_area, length_area, speed_area] = Layout::horizontal([
|
||||||
|
Constraint::Length(name_width),
|
||||||
|
Constraint::Length(length_width),
|
||||||
|
Constraint::Length(speed_width),
|
||||||
|
])
|
||||||
|
.areas(Rect {
|
||||||
|
x: row_area.x,
|
||||||
|
y: text_y,
|
||||||
|
width: row_area.width,
|
||||||
|
height: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 1: prefix + index + name (left-aligned)
|
||||||
|
let name_text = if name.is_empty() {
|
||||||
format!("{}{:02}", prefix, idx + 1)
|
format!("{}{:02}", prefix, idx + 1)
|
||||||
} else {
|
} else {
|
||||||
format!("{}{:02} {}", prefix, idx + 1, name)
|
format!("{}{:02} {}", prefix, idx + 1, name)
|
||||||
};
|
};
|
||||||
|
let name_style = if is_playing || is_queued_play {
|
||||||
let style = Style::new().bg(bg).fg(fg);
|
bold_style
|
||||||
let style = if is_playing || is_queued_play {
|
|
||||||
style.add_modifier(Modifier::BOLD)
|
|
||||||
} else {
|
} else {
|
||||||
style
|
base_style
|
||||||
};
|
};
|
||||||
|
frame.render_widget(Paragraph::new(name_text).style(name_style), name_area);
|
||||||
|
|
||||||
let para = Paragraph::new(label).style(style);
|
// Column 2: length
|
||||||
frame.render_widget(para, row_area);
|
let length_line = Line::from(vec![
|
||||||
|
Span::styled("Length: ", bold_style),
|
||||||
|
Span::styled(format!("{}", length), base_style),
|
||||||
|
]);
|
||||||
|
frame.render_widget(Paragraph::new(length_line), length_area);
|
||||||
|
|
||||||
|
// Column 3: speed (only if non-default)
|
||||||
|
if speed != PatternSpeed::Normal {
|
||||||
|
let speed_line = Line::from(vec![
|
||||||
|
Span::styled("Speed: ", bold_style),
|
||||||
|
Span::styled(speed.label(), base_style),
|
||||||
|
]);
|
||||||
|
frame.render_widget(Paragraph::new(speed_line), speed_area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
|
|||||||
height: term.height.saturating_sub(2),
|
height: term.height.saturating_sub(2),
|
||||||
};
|
};
|
||||||
|
|
||||||
let [header_area, body_area, footer_area] = Layout::vertical([
|
let [header_area, _padding, body_area, footer_area] = Layout::vertical([
|
||||||
Constraint::Length(2),
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(1),
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
])
|
])
|
||||||
@@ -49,94 +50,94 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
fn render_header(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||||
let [top_row, bottom_row] =
|
use crate::model::PatternSpeed;
|
||||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
|
|
||||||
|
|
||||||
let pattern = app
|
let bank = &app.project_state.project.banks[app.editor_ctx.bank];
|
||||||
.project_state
|
let pattern = &bank.patterns[app.editor_ctx.pattern];
|
||||||
.project
|
|
||||||
.pattern_at(app.editor_ctx.bank, app.editor_ctx.pattern);
|
|
||||||
|
|
||||||
let play_symbol = if app.playback.playing { "▶" } else { "■" };
|
// Layout: [Transport] [Tempo] [Bank] [Pattern] [Stats]
|
||||||
let play_color = if app.playback.playing {
|
let [transport_area, tempo_area, bank_area, pattern_area, stats_area] = Layout::horizontal([
|
||||||
Color::Green
|
Constraint::Min(12),
|
||||||
} else {
|
Constraint::Min(14),
|
||||||
Color::Red
|
|
||||||
};
|
|
||||||
|
|
||||||
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
|
|
||||||
let cpu_color = if cpu_pct > 80.0 {
|
|
||||||
Color::Red
|
|
||||||
} else if cpu_pct > 50.0 {
|
|
||||||
Color::Yellow
|
|
||||||
} else {
|
|
||||||
Color::Green
|
|
||||||
};
|
|
||||||
|
|
||||||
render_header_row(
|
|
||||||
frame,
|
|
||||||
top_row,
|
|
||||||
Line::from(vec![Span::styled(
|
|
||||||
format!(
|
|
||||||
"B{:02}:P{:02}",
|
|
||||||
app.editor_ctx.bank + 1,
|
|
||||||
app.editor_ctx.pattern + 1
|
|
||||||
),
|
|
||||||
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
|
||||||
)]),
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(play_symbol, Style::new().fg(play_color)),
|
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled(
|
|
||||||
format!("{:.1} BPM", link.tempo()),
|
|
||||||
Style::new().fg(Color::Magenta).add_modifier(Modifier::BOLD),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(format!("CPU {:3.0}%", cpu_pct), Style::new().fg(cpu_color)),
|
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled(
|
|
||||||
format!("V:{}", app.metrics.active_voices),
|
|
||||||
Style::new().fg(Color::Cyan),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
render_header_row(
|
|
||||||
frame,
|
|
||||||
bottom_row,
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(
|
|
||||||
format!("Len {:02}", pattern.length),
|
|
||||||
Style::new().fg(Color::Rgb(180, 140, 90)),
|
|
||||||
),
|
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled(
|
|
||||||
format!("Spd {}", pattern.speed.label()),
|
|
||||||
Style::new().fg(Color::Rgb(180, 140, 90)),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
Line::from(vec![]),
|
|
||||||
Line::from(vec![]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_header_row(frame: &mut Frame, area: Rect, left: Line, center: Line, right: Line) {
|
|
||||||
let [left_area, center_area, right_area] = Layout::horizontal([
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
|
Constraint::Fill(2),
|
||||||
|
Constraint::Min(20),
|
||||||
])
|
])
|
||||||
.areas(area);
|
.areas(area);
|
||||||
|
|
||||||
frame.render_widget(Paragraph::new(left).alignment(Alignment::Left), left_area);
|
// Transport block
|
||||||
|
let (transport_bg, transport_text) = if app.playback.playing {
|
||||||
|
(Color::Rgb(30, 80, 30), " ▶ PLAYING ")
|
||||||
|
} else {
|
||||||
|
(Color::Rgb(80, 30, 30), " ■ STOPPED ")
|
||||||
|
};
|
||||||
|
let transport_style = Style::new().bg(transport_bg).fg(Color::White);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(center).alignment(Alignment::Center),
|
Paragraph::new(transport_text)
|
||||||
center_area,
|
.style(transport_style)
|
||||||
|
.alignment(Alignment::Center),
|
||||||
|
transport_area,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tempo block
|
||||||
|
let tempo_style = Style::new()
|
||||||
|
.bg(Color::Rgb(60, 30, 60))
|
||||||
|
.fg(Color::White)
|
||||||
|
.add_modifier(Modifier::BOLD);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(right).alignment(Alignment::Right),
|
Paragraph::new(format!(" {:.1} BPM ", link.tempo()))
|
||||||
right_area,
|
.style(tempo_style)
|
||||||
|
.alignment(Alignment::Center),
|
||||||
|
tempo_area,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bank block
|
||||||
|
let bank_name = bank
|
||||||
|
.name
|
||||||
|
.as_deref()
|
||||||
|
.map(|n| format!(" {} ", n))
|
||||||
|
.unwrap_or_else(|| format!(" Bank {:02} ", app.editor_ctx.bank + 1));
|
||||||
|
let bank_style = Style::new().bg(Color::Rgb(30, 60, 70)).fg(Color::White);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(bank_name)
|
||||||
|
.style(bank_style)
|
||||||
|
.alignment(Alignment::Center),
|
||||||
|
bank_area,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pattern block (name + length + speed)
|
||||||
|
let default_pattern_name = format!("Pattern {:02}", app.editor_ctx.pattern + 1);
|
||||||
|
let pattern_name = pattern.name.as_deref().unwrap_or(&default_pattern_name);
|
||||||
|
let speed_info = if pattern.speed != PatternSpeed::Normal {
|
||||||
|
format!(" · {}", pattern.speed.label())
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let pattern_text = format!(
|
||||||
|
" {} · {} steps{} ",
|
||||||
|
pattern_name, pattern.length, speed_info
|
||||||
|
);
|
||||||
|
let pattern_style = Style::new().bg(Color::Rgb(30, 50, 50)).fg(Color::White);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(pattern_text)
|
||||||
|
.style(pattern_style)
|
||||||
|
.alignment(Alignment::Center),
|
||||||
|
pattern_area,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stats block
|
||||||
|
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
|
||||||
|
let peers = link.peers();
|
||||||
|
let voices = app.metrics.active_voices;
|
||||||
|
let stats_text = format!(" CPU {:.0}% V:{} L:{} ", cpu_pct, voices, peers);
|
||||||
|
let stats_style = Style::new()
|
||||||
|
.bg(Color::Rgb(35, 35, 40))
|
||||||
|
.fg(Color::Rgb(150, 150, 160));
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(stats_text)
|
||||||
|
.style(stats_style)
|
||||||
|
.alignment(Alignment::Right),
|
||||||
|
stats_area,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +243,20 @@ fn render_modal(frame: &mut Frame, app: &App, term: Rect) {
|
|||||||
ConfirmModal::new("Confirm", &format!("Delete step {}?", step + 1), *selected)
|
ConfirmModal::new("Confirm", &format!("Delete step {}?", step + 1), *selected)
|
||||||
.render_centered(frame, term);
|
.render_centered(frame, term);
|
||||||
}
|
}
|
||||||
|
Modal::ConfirmResetPattern {
|
||||||
|
pattern, selected, ..
|
||||||
|
} => {
|
||||||
|
ConfirmModal::new(
|
||||||
|
"Confirm",
|
||||||
|
&format!("Reset pattern {}?", pattern + 1),
|
||||||
|
*selected,
|
||||||
|
)
|
||||||
|
.render_centered(frame, term);
|
||||||
|
}
|
||||||
|
Modal::ConfirmResetBank { bank, selected } => {
|
||||||
|
ConfirmModal::new("Confirm", &format!("Reset bank {}?", bank + 1), *selected)
|
||||||
|
.render_centered(frame, term);
|
||||||
|
}
|
||||||
Modal::SaveAs(path) => {
|
Modal::SaveAs(path) => {
|
||||||
TextInputModal::new("Save As (Enter to confirm, Esc to cancel)", path)
|
TextInputModal::new("Save As (Enter to confirm, Esc to cancel)", path)
|
||||||
.width(60)
|
.width(60)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use ratatui::layout::Rect;
|
|||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum Orientation {
|
pub enum Orientation {
|
||||||
Horizontal,
|
Horizontal,
|
||||||
Vertical,
|
Vertical,
|
||||||
|
|||||||
Reference in New Issue
Block a user