So much better
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
mod file;
|
mod file;
|
||||||
mod project;
|
mod project;
|
||||||
|
|
||||||
pub const MAX_BANKS: usize = 16;
|
pub const MAX_BANKS: usize = 32;
|
||||||
pub const MAX_PATTERNS: usize = 16;
|
pub const MAX_PATTERNS: usize = 32;
|
||||||
pub const MAX_STEPS: usize = 128;
|
pub const MAX_STEPS: usize = 128;
|
||||||
pub const DEFAULT_LENGTH: usize = 16;
|
pub const DEFAULT_LENGTH: usize = 16;
|
||||||
|
|
||||||
pub use file::{load, save, FileError};
|
pub use file::{load, save, FileError};
|
||||||
pub use project::{Bank, Pattern, PatternSpeed, Project, Step};
|
pub use project::{Bank, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub enum PatternSpeed {
|
pub enum PatternSpeed {
|
||||||
Eighth, // 1/8x
|
Eighth, // 1/8x
|
||||||
Quarter, // 1/4x
|
Quarter, // 1/4x
|
||||||
@@ -79,6 +79,75 @@ impl PatternSpeed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
|
pub enum LaunchQuantization {
|
||||||
|
Immediate,
|
||||||
|
Beat,
|
||||||
|
#[default]
|
||||||
|
Bar,
|
||||||
|
Bars2,
|
||||||
|
Bars4,
|
||||||
|
Bars8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LaunchQuantization {
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Immediate => "Immediate",
|
||||||
|
Self::Beat => "Beat",
|
||||||
|
Self::Bar => "1 Bar",
|
||||||
|
Self::Bars2 => "2 Bars",
|
||||||
|
Self::Bars4 => "4 Bars",
|
||||||
|
Self::Bars8 => "8 Bars",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Immediate => Self::Beat,
|
||||||
|
Self::Beat => Self::Bar,
|
||||||
|
Self::Bar => Self::Bars2,
|
||||||
|
Self::Bars2 => Self::Bars4,
|
||||||
|
Self::Bars4 => Self::Bars8,
|
||||||
|
Self::Bars8 => Self::Bars8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Immediate => Self::Immediate,
|
||||||
|
Self::Beat => Self::Immediate,
|
||||||
|
Self::Bar => Self::Beat,
|
||||||
|
Self::Bars2 => Self::Bar,
|
||||||
|
Self::Bars4 => Self::Bars2,
|
||||||
|
Self::Bars8 => Self::Bars4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
|
pub enum SyncMode {
|
||||||
|
#[default]
|
||||||
|
Reset,
|
||||||
|
PhaseLock,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncMode {
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Reset => "Reset",
|
||||||
|
Self::PhaseLock => "Phase-Lock",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Reset => Self::PhaseLock,
|
||||||
|
Self::PhaseLock => Self::Reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Step {
|
pub struct Step {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
@@ -108,6 +177,10 @@ pub struct Pattern {
|
|||||||
pub speed: PatternSpeed,
|
pub speed: PatternSpeed,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub quantization: LaunchQuantization,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sync_mode: SyncMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Pattern {
|
impl Default for Pattern {
|
||||||
@@ -117,6 +190,8 @@ impl Default for Pattern {
|
|||||||
length: DEFAULT_LENGTH,
|
length: DEFAULT_LENGTH,
|
||||||
speed: PatternSpeed::default(),
|
speed: PatternSpeed::default(),
|
||||||
name: None,
|
name: None,
|
||||||
|
quantization: LaunchQuantization::default(),
|
||||||
|
sync_mode: SyncMode::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,40 @@ pub struct Editor {
|
|||||||
search: SearchState,
|
search: SearchState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Editor {
|
||||||
|
pub fn start_selection(&mut self) {
|
||||||
|
self.text.start_selection();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_selection(&mut self) {
|
||||||
|
self.text.cancel_selection();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_selecting(&self) -> bool {
|
||||||
|
self.text.is_selecting()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy(&mut self) {
|
||||||
|
self.text.copy();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cut(&mut self) -> bool {
|
||||||
|
self.text.cut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste(&mut self) -> bool {
|
||||||
|
self.text.paste()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_all(&mut self) {
|
||||||
|
self.text.select_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
|
||||||
|
self.text.selection_range()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Editor {
|
impl Default for Editor {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
@@ -300,6 +334,9 @@ impl Editor {
|
|||||||
pub fn render(&self, frame: &mut Frame, area: Rect, highlighter: Highlighter) {
|
pub fn render(&self, frame: &mut Frame, area: Rect, highlighter: Highlighter) {
|
||||||
let (cursor_row, cursor_col) = self.text.cursor();
|
let (cursor_row, cursor_col) = self.text.cursor();
|
||||||
let cursor_style = Style::default().bg(Color::White).fg(Color::Black);
|
let cursor_style = Style::default().bg(Color::White).fg(Color::Black);
|
||||||
|
let selection_style = Style::default().bg(Color::Rgb(60, 80, 120));
|
||||||
|
|
||||||
|
let selection = self.text.selection_range();
|
||||||
|
|
||||||
let lines: Vec<Line> = self
|
let lines: Vec<Line> = self
|
||||||
.text
|
.text
|
||||||
@@ -309,39 +346,30 @@ impl Editor {
|
|||||||
.map(|(row, line)| {
|
.map(|(row, line)| {
|
||||||
let tokens = highlighter(row, line);
|
let tokens = highlighter(row, line);
|
||||||
let mut spans: Vec<Span> = Vec::new();
|
let mut spans: Vec<Span> = Vec::new();
|
||||||
|
let mut col = 0;
|
||||||
|
|
||||||
if row == cursor_row {
|
for (base_style, text) in tokens {
|
||||||
let mut col = 0;
|
for ch in text.chars() {
|
||||||
for (style, text) in tokens {
|
let is_cursor = row == cursor_row && col == cursor_col;
|
||||||
let text_len = text.chars().count();
|
let is_selected = is_in_selection(row, col, selection);
|
||||||
if cursor_col >= col && cursor_col < col + text_len {
|
|
||||||
let before = text.chars().take(cursor_col - col).collect::<String>();
|
|
||||||
let cursor_char =
|
|
||||||
text.chars().nth(cursor_col - col).unwrap_or(' ');
|
|
||||||
let after =
|
|
||||||
text.chars().skip(cursor_col - col + 1).collect::<String>();
|
|
||||||
|
|
||||||
if !before.is_empty() {
|
let style = if is_cursor {
|
||||||
spans.push(Span::styled(before, style));
|
cursor_style
|
||||||
}
|
} else if is_selected {
|
||||||
spans.push(Span::styled(cursor_char.to_string(), cursor_style));
|
base_style.bg(selection_style.bg.unwrap())
|
||||||
if !after.is_empty() {
|
|
||||||
spans.push(Span::styled(after, style));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
spans.push(Span::styled(text, style));
|
base_style
|
||||||
}
|
};
|
||||||
col += text_len;
|
|
||||||
}
|
spans.push(Span::styled(ch.to_string(), style));
|
||||||
if cursor_col >= col {
|
col += 1;
|
||||||
spans.push(Span::styled(" ", cursor_style));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (style, text) in tokens {
|
|
||||||
spans.push(Span::styled(text, style));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if row == cursor_row && cursor_col >= col {
|
||||||
|
spans.push(Span::styled(" ", cursor_style));
|
||||||
|
}
|
||||||
|
|
||||||
Line::from(spans)
|
Line::from(spans)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -468,3 +496,23 @@ impl Editor {
|
|||||||
fn is_word_char(c: char) -> bool {
|
fn is_word_char(c: char) -> bool {
|
||||||
c.is_alphanumeric() || matches!(c, '!' | '@' | '?' | '.' | ':' | '_' | '#')
|
c.is_alphanumeric() || matches!(c, '!' | '@' | '?' | '.' | ':' | '_' | '#')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_in_selection(row: usize, col: usize, selection: Option<((usize, usize), (usize, usize))>) -> bool {
|
||||||
|
let Some(((start_row, start_col), (end_row, end_col))) = selection else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if row < start_row || row > end_row {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if row == start_row && row == end_row {
|
||||||
|
col >= start_col && col < end_col
|
||||||
|
} else if row == start_row {
|
||||||
|
col >= start_col
|
||||||
|
} else if row == end_row {
|
||||||
|
col < end_col
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
131
src/app.rs
131
src/app.rs
@@ -16,7 +16,8 @@ use crate::services::pattern_editor;
|
|||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
AudioSettings, DictFocus, EditorContext, FlashKind, Focus, LiveKeyState, Metrics, Modal,
|
AudioSettings, DictFocus, EditorContext, FlashKind, Focus, LiveKeyState, Metrics, Modal,
|
||||||
OptionsState, PanelState, PatternField, PatternsNav, PlaybackState, ProjectState, UiState,
|
OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState,
|
||||||
|
ProjectState, StagedChange, UiState,
|
||||||
};
|
};
|
||||||
use crate::views::{dict_view, help_view};
|
use crate::views::{dict_view, help_view};
|
||||||
|
|
||||||
@@ -399,48 +400,76 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_pattern_playback(
|
pub fn stage_pattern_toggle(
|
||||||
&mut self,
|
&mut self,
|
||||||
bank: usize,
|
bank: usize,
|
||||||
pattern: usize,
|
pattern: usize,
|
||||||
snapshot: &SequencerSnapshot,
|
snapshot: &SequencerSnapshot,
|
||||||
) {
|
) {
|
||||||
let is_playing = snapshot.is_playing(bank, pattern);
|
let is_playing = snapshot.is_playing(bank, pattern);
|
||||||
|
let pattern_data = self.project_state.project.pattern_at(bank, pattern);
|
||||||
|
|
||||||
let pending = self
|
let existing = self
|
||||||
.playback
|
.playback
|
||||||
.queued_changes
|
.staged_changes
|
||||||
.iter()
|
.iter()
|
||||||
.position(|c| c.pattern_id().bank == bank && c.pattern_id().pattern == pattern);
|
.position(|c| {
|
||||||
|
c.change.pattern_id().bank == bank && c.change.pattern_id().pattern == pattern
|
||||||
|
});
|
||||||
|
|
||||||
if let Some(idx) = pending {
|
if let Some(idx) = existing {
|
||||||
self.playback.queued_changes.remove(idx);
|
self.playback.staged_changes.remove(idx);
|
||||||
self.ui.set_status(format!(
|
self.ui.set_status(format!(
|
||||||
"B{:02}:P{:02} change cancelled",
|
"B{:02}:P{:02} unstaged",
|
||||||
bank + 1,
|
bank + 1,
|
||||||
pattern + 1
|
pattern + 1
|
||||||
));
|
));
|
||||||
} else if is_playing {
|
} else if is_playing {
|
||||||
self.playback
|
self.playback.staged_changes.push(StagedChange {
|
||||||
.queued_changes
|
change: PatternChange::Stop { bank, pattern },
|
||||||
.push(PatternChange::Stop { bank, pattern });
|
quantization: pattern_data.quantization,
|
||||||
|
sync_mode: pattern_data.sync_mode,
|
||||||
|
});
|
||||||
self.ui.set_status(format!(
|
self.ui.set_status(format!(
|
||||||
"B{:02}:P{:02} queued to stop",
|
"B{:02}:P{:02} staged to stop",
|
||||||
bank + 1,
|
bank + 1,
|
||||||
pattern + 1
|
pattern + 1
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
self.playback
|
self.playback.staged_changes.push(StagedChange {
|
||||||
.queued_changes
|
change: PatternChange::Start { bank, pattern },
|
||||||
.push(PatternChange::Start { bank, pattern });
|
quantization: pattern_data.quantization,
|
||||||
|
sync_mode: pattern_data.sync_mode,
|
||||||
|
});
|
||||||
self.ui.set_status(format!(
|
self.ui.set_status(format!(
|
||||||
"B{:02}:P{:02} queued to play",
|
"B{:02}:P{:02} staged to play",
|
||||||
bank + 1,
|
bank + 1,
|
||||||
pattern + 1
|
pattern + 1
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn commit_staged_changes(&mut self) {
|
||||||
|
if self.playback.staged_changes.is_empty() {
|
||||||
|
self.ui.set_status("No changes to commit".to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let count = self.playback.staged_changes.len();
|
||||||
|
self.playback
|
||||||
|
.queued_changes
|
||||||
|
.append(&mut self.playback.staged_changes);
|
||||||
|
self.ui.set_status(format!("Committed {count} changes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_staged_changes(&mut self) {
|
||||||
|
if self.playback.staged_changes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let count = self.playback.staged_changes.len();
|
||||||
|
self.playback.staged_changes.clear();
|
||||||
|
self.ui.set_status(format!("Cleared {count} staged changes"));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select_edit_pattern(&mut self, pattern: usize) {
|
pub fn select_edit_pattern(&mut self, pattern: usize) {
|
||||||
self.editor_ctx.pattern = pattern;
|
self.editor_ctx.pattern = pattern;
|
||||||
self.editor_ctx.step = 0;
|
self.editor_ctx.step = 0;
|
||||||
@@ -724,6 +753,20 @@ impl App {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_pattern_props_modal(&mut self, bank: usize, pattern: usize) {
|
||||||
|
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||||
|
self.ui.modal = Modal::PatternProps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
field: PatternPropsField::default(),
|
||||||
|
name: pat.name.clone().unwrap_or_default(),
|
||||||
|
length: pat.length.to_string(),
|
||||||
|
speed: pat.speed,
|
||||||
|
quantization: pat.quantization,
|
||||||
|
sync_mode: pat.sync_mode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||||
match cmd {
|
match cmd {
|
||||||
// Playback
|
// Playback
|
||||||
@@ -815,12 +858,15 @@ impl App {
|
|||||||
AppCommand::LinkPasteStep => self.link_paste_step(),
|
AppCommand::LinkPasteStep => self.link_paste_step(),
|
||||||
AppCommand::HardenStep => self.harden_step(),
|
AppCommand::HardenStep => self.harden_step(),
|
||||||
|
|
||||||
// Pattern playback
|
// Pattern playback (staging)
|
||||||
AppCommand::QueuePatternChange(change) => {
|
AppCommand::StagePatternToggle { bank, pattern } => {
|
||||||
self.playback.queued_changes.push(change);
|
self.stage_pattern_toggle(bank, pattern, snapshot);
|
||||||
}
|
}
|
||||||
AppCommand::TogglePatternPlayback { bank, pattern } => {
|
AppCommand::CommitStagedChanges => {
|
||||||
self.toggle_pattern_playback(bank, pattern, snapshot);
|
self.commit_staged_changes();
|
||||||
|
}
|
||||||
|
AppCommand::ClearStagedChanges => {
|
||||||
|
self.clear_staged_changes();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project
|
// Project
|
||||||
@@ -859,6 +905,28 @@ impl App {
|
|||||||
}
|
}
|
||||||
AppCommand::CloseModal => self.ui.modal = Modal::None,
|
AppCommand::CloseModal => self.ui.modal = Modal::None,
|
||||||
AppCommand::OpenPatternModal(field) => self.open_pattern_modal(field),
|
AppCommand::OpenPatternModal(field) => self.open_pattern_modal(field),
|
||||||
|
AppCommand::OpenPatternPropsModal { bank, pattern } => {
|
||||||
|
self.open_pattern_props_modal(bank, pattern);
|
||||||
|
}
|
||||||
|
AppCommand::SetPatternProps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
name,
|
||||||
|
length,
|
||||||
|
speed,
|
||||||
|
quantization,
|
||||||
|
sync_mode,
|
||||||
|
} => {
|
||||||
|
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
||||||
|
pat.name = name;
|
||||||
|
if let Some(len) = length {
|
||||||
|
pat.set_length(len);
|
||||||
|
}
|
||||||
|
pat.speed = speed;
|
||||||
|
pat.quantization = quantization;
|
||||||
|
pat.sync_mode = sync_mode;
|
||||||
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
|
}
|
||||||
|
|
||||||
// Page navigation
|
// Page navigation
|
||||||
AppCommand::PageLeft => self.page.left(),
|
AppCommand::PageLeft => self.page.left(),
|
||||||
@@ -953,19 +1021,28 @@ impl App {
|
|||||||
AppCommand::PatternsTogglePlay => {
|
AppCommand::PatternsTogglePlay => {
|
||||||
let bank = self.patterns_nav.selected_bank();
|
let bank = self.patterns_nav.selected_bank();
|
||||||
let pattern = self.patterns_nav.selected_pattern();
|
let pattern = self.patterns_nav.selected_pattern();
|
||||||
self.toggle_pattern_playback(bank, pattern, snapshot);
|
self.stage_pattern_toggle(bank, pattern, snapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn flush_queued_changes(&mut self, cmd_tx: &Sender<SeqCommand>) {
|
pub fn flush_queued_changes(&mut self, cmd_tx: &Sender<SeqCommand>) {
|
||||||
for change in self.playback.queued_changes.drain(..) {
|
for staged in self.playback.queued_changes.drain(..) {
|
||||||
match change {
|
match staged.change {
|
||||||
PatternChange::Start { bank, pattern } => {
|
PatternChange::Start { bank, pattern } => {
|
||||||
let _ = cmd_tx.send(SeqCommand::PatternStart { bank, pattern });
|
let _ = cmd_tx.send(SeqCommand::PatternStart {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
quantization: staged.quantization,
|
||||||
|
sync_mode: staged.sync_mode,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
PatternChange::Stop { bank, pattern } => {
|
PatternChange::Stop { bank, pattern } => {
|
||||||
let _ = cmd_tx.send(SeqCommand::PatternStop { bank, pattern });
|
let _ = cmd_tx.send(SeqCommand::PatternStop {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
quantization: staged.quantization,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -987,6 +1064,8 @@ impl App {
|
|||||||
source: s.source,
|
source: s.source,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
quantization: pat.quantization,
|
||||||
|
sync_mode: pat.sync_mode,
|
||||||
};
|
};
|
||||||
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
||||||
bank,
|
bank,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::engine::PatternChange;
|
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
||||||
use crate::model::PatternSpeed;
|
|
||||||
use crate::state::{FlashKind, Modal, PatternField};
|
use crate::state::{FlashKind, Modal, PatternField};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -74,12 +73,13 @@ pub enum AppCommand {
|
|||||||
LinkPasteStep,
|
LinkPasteStep,
|
||||||
HardenStep,
|
HardenStep,
|
||||||
|
|
||||||
// Pattern playback
|
// Pattern playback (staging)
|
||||||
QueuePatternChange(PatternChange),
|
StagePatternToggle {
|
||||||
TogglePatternPlayback {
|
|
||||||
bank: usize,
|
bank: usize,
|
||||||
pattern: usize,
|
pattern: usize,
|
||||||
},
|
},
|
||||||
|
CommitStagedChanges,
|
||||||
|
ClearStagedChanges,
|
||||||
|
|
||||||
// Project
|
// Project
|
||||||
RenameBank {
|
RenameBank {
|
||||||
@@ -105,6 +105,19 @@ pub enum AppCommand {
|
|||||||
OpenModal(Modal),
|
OpenModal(Modal),
|
||||||
CloseModal,
|
CloseModal,
|
||||||
OpenPatternModal(PatternField),
|
OpenPatternModal(PatternField),
|
||||||
|
OpenPatternPropsModal {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
},
|
||||||
|
SetPatternProps {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
name: Option<String>,
|
||||||
|
length: Option<usize>,
|
||||||
|
speed: PatternSpeed,
|
||||||
|
quantization: LaunchQuantization,
|
||||||
|
sync_mode: SyncMode,
|
||||||
|
},
|
||||||
|
|
||||||
// Page navigation
|
// Page navigation
|
||||||
PageLeft,
|
PageLeft,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::time::Duration;
|
|||||||
use thread_priority::{set_current_thread_priority, ThreadPriority};
|
use thread_priority::{set_current_thread_priority, ThreadPriority};
|
||||||
|
|
||||||
use super::LinkState;
|
use super::LinkState;
|
||||||
use crate::model::{MAX_BANKS, MAX_PATTERNS};
|
use crate::model::{LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
|
||||||
use crate::model::{Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables};
|
use crate::model::{Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables};
|
||||||
use crate::state::LiveKeyState;
|
use crate::state::LiveKeyState;
|
||||||
|
|
||||||
@@ -56,10 +56,13 @@ pub enum SeqCommand {
|
|||||||
PatternStart {
|
PatternStart {
|
||||||
bank: usize,
|
bank: usize,
|
||||||
pattern: usize,
|
pattern: usize,
|
||||||
|
quantization: LaunchQuantization,
|
||||||
|
sync_mode: SyncMode,
|
||||||
},
|
},
|
||||||
PatternStop {
|
PatternStop {
|
||||||
bank: usize,
|
bank: usize,
|
||||||
pattern: usize,
|
pattern: usize,
|
||||||
|
quantization: LaunchQuantization,
|
||||||
},
|
},
|
||||||
Shutdown,
|
Shutdown,
|
||||||
}
|
}
|
||||||
@@ -69,6 +72,8 @@ pub struct PatternSnapshot {
|
|||||||
pub speed: crate::model::PatternSpeed,
|
pub speed: crate::model::PatternSpeed,
|
||||||
pub length: usize,
|
pub length: usize,
|
||||||
pub steps: Vec<StepSnapshot>,
|
pub steps: Vec<StepSnapshot>,
|
||||||
|
pub quantization: LaunchQuantization,
|
||||||
|
pub sync_mode: SyncMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -165,11 +170,18 @@ struct ActivePattern {
|
|||||||
iter: usize,
|
iter: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct PendingPattern {
|
||||||
|
id: PatternId,
|
||||||
|
quantization: LaunchQuantization,
|
||||||
|
sync_mode: SyncMode,
|
||||||
|
}
|
||||||
|
|
||||||
struct AudioState {
|
struct AudioState {
|
||||||
prev_beat: f64,
|
prev_beat: f64,
|
||||||
active_patterns: HashMap<PatternId, ActivePattern>,
|
active_patterns: HashMap<PatternId, ActivePattern>,
|
||||||
pending_starts: Vec<PatternId>,
|
pending_starts: Vec<PendingPattern>,
|
||||||
pending_stops: Vec<PatternId>,
|
pending_stops: Vec<PendingPattern>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioState {
|
impl AudioState {
|
||||||
@@ -275,6 +287,41 @@ impl PatternSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_quantization_boundary(
|
||||||
|
quantization: LaunchQuantization,
|
||||||
|
beat: f64,
|
||||||
|
prev_beat: f64,
|
||||||
|
quantum: f64,
|
||||||
|
) -> bool {
|
||||||
|
if prev_beat < 0.0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match quantization {
|
||||||
|
LaunchQuantization::Immediate => true,
|
||||||
|
LaunchQuantization::Beat => beat.floor() as i64 != prev_beat.floor() as i64,
|
||||||
|
LaunchQuantization::Bar => {
|
||||||
|
let bar = (beat / quantum).floor() as i64;
|
||||||
|
let prev_bar = (prev_beat / quantum).floor() as i64;
|
||||||
|
bar != prev_bar
|
||||||
|
}
|
||||||
|
LaunchQuantization::Bars2 => {
|
||||||
|
let bars2 = (beat / (quantum * 2.0)).floor() as i64;
|
||||||
|
let prev_bars2 = (prev_beat / (quantum * 2.0)).floor() as i64;
|
||||||
|
bars2 != prev_bars2
|
||||||
|
}
|
||||||
|
LaunchQuantization::Bars4 => {
|
||||||
|
let bars4 = (beat / (quantum * 4.0)).floor() as i64;
|
||||||
|
let prev_bars4 = (prev_beat / (quantum * 4.0)).floor() as i64;
|
||||||
|
bars4 != prev_bars4
|
||||||
|
}
|
||||||
|
LaunchQuantization::Bars8 => {
|
||||||
|
let bars8 = (beat / (quantum * 8.0)).floor() as i64;
|
||||||
|
let prev_bars8 = (prev_beat / (quantum * 8.0)).floor() as i64;
|
||||||
|
bars8 != prev_bars8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type StepKey = (usize, usize, usize);
|
type StepKey = (usize, usize, usize);
|
||||||
|
|
||||||
struct RunsCounter {
|
struct RunsCounter {
|
||||||
@@ -332,18 +379,35 @@ fn sequencer_loop(
|
|||||||
} => {
|
} => {
|
||||||
pattern_cache.set(bank, pattern, data);
|
pattern_cache.set(bank, pattern, data);
|
||||||
}
|
}
|
||||||
SeqCommand::PatternStart { bank, pattern } => {
|
SeqCommand::PatternStart {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
quantization,
|
||||||
|
sync_mode,
|
||||||
|
} => {
|
||||||
let id = PatternId { bank, pattern };
|
let id = PatternId { bank, pattern };
|
||||||
audio_state.pending_stops.retain(|p| *p != id);
|
audio_state.pending_stops.retain(|p| p.id != id);
|
||||||
if !audio_state.pending_starts.contains(&id) {
|
if !audio_state.pending_starts.iter().any(|p| p.id == id) {
|
||||||
audio_state.pending_starts.push(id);
|
audio_state.pending_starts.push(PendingPattern {
|
||||||
|
id,
|
||||||
|
quantization,
|
||||||
|
sync_mode,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SeqCommand::PatternStop { bank, pattern } => {
|
SeqCommand::PatternStop {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
quantization,
|
||||||
|
} => {
|
||||||
let id = PatternId { bank, pattern };
|
let id = PatternId { bank, pattern };
|
||||||
audio_state.pending_starts.retain(|p| *p != id);
|
audio_state.pending_starts.retain(|p| p.id != id);
|
||||||
if !audio_state.pending_stops.contains(&id) {
|
if !audio_state.pending_stops.iter().any(|p| p.id == id) {
|
||||||
audio_state.pending_stops.push(id);
|
audio_state.pending_stops.push(PendingPattern {
|
||||||
|
id,
|
||||||
|
quantization,
|
||||||
|
sync_mode: SyncMode::Reset,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SeqCommand::Shutdown => {
|
SeqCommand::Shutdown => {
|
||||||
@@ -362,31 +426,67 @@ fn sequencer_loop(
|
|||||||
let beat = state.beat_at_time(time, quantum);
|
let beat = state.beat_at_time(time, quantum);
|
||||||
let tempo = state.tempo();
|
let tempo = state.tempo();
|
||||||
|
|
||||||
let bar = (beat / quantum).floor() as i64;
|
let prev_beat = audio_state.prev_beat;
|
||||||
let prev_bar = (audio_state.prev_beat / quantum).floor() as i64;
|
|
||||||
let mut stopped_chain_keys: Vec<String> = Vec::new();
|
let mut stopped_chain_keys: Vec<String> = Vec::new();
|
||||||
if bar != prev_bar && audio_state.prev_beat >= 0.0 {
|
|
||||||
for id in audio_state.pending_starts.drain(..) {
|
// Process pending starts with per-pattern quantization
|
||||||
|
let mut started_ids: Vec<PatternId> = Vec::new();
|
||||||
|
for pending in &audio_state.pending_starts {
|
||||||
|
let should_start = check_quantization_boundary(
|
||||||
|
pending.quantization,
|
||||||
|
beat,
|
||||||
|
prev_beat,
|
||||||
|
quantum,
|
||||||
|
);
|
||||||
|
if should_start {
|
||||||
|
let start_step = match pending.sync_mode {
|
||||||
|
SyncMode::Reset => 0,
|
||||||
|
SyncMode::PhaseLock => {
|
||||||
|
if let Some(pat) = pattern_cache.get(pending.id.bank, pending.id.pattern) {
|
||||||
|
let speed_mult = pat.speed.multiplier();
|
||||||
|
((beat * 4.0 * speed_mult) as usize) % pat.length
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
audio_state.active_patterns.insert(
|
audio_state.active_patterns.insert(
|
||||||
id,
|
pending.id,
|
||||||
ActivePattern {
|
ActivePattern {
|
||||||
bank: id.bank,
|
bank: pending.id.bank,
|
||||||
pattern: id.pattern,
|
pattern: pending.id.pattern,
|
||||||
step_index: 0,
|
step_index: start_step,
|
||||||
iter: 0,
|
iter: 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
started_ids.push(pending.id);
|
||||||
for id in audio_state.pending_stops.drain(..) {
|
|
||||||
audio_state.active_patterns.remove(&id);
|
|
||||||
step_traces.retain(|&(bank, pattern, _), _| {
|
|
||||||
bank != id.bank || pattern != id.pattern
|
|
||||||
});
|
|
||||||
stopped_chain_keys.push(format!("__chain_{}_{}__", id.bank, id.pattern));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
audio_state.pending_starts.retain(|p| !started_ids.contains(&p.id));
|
||||||
|
|
||||||
|
// Process pending stops with per-pattern quantization
|
||||||
|
let mut stopped_ids: Vec<PatternId> = Vec::new();
|
||||||
|
for pending in &audio_state.pending_stops {
|
||||||
|
let should_stop = check_quantization_boundary(
|
||||||
|
pending.quantization,
|
||||||
|
beat,
|
||||||
|
prev_beat,
|
||||||
|
quantum,
|
||||||
|
);
|
||||||
|
if should_stop {
|
||||||
|
audio_state.active_patterns.remove(&pending.id);
|
||||||
|
step_traces.retain(|&(bank, pattern, _), _| {
|
||||||
|
bank != pending.id.bank || pattern != pending.id.pattern
|
||||||
|
});
|
||||||
|
stopped_chain_keys.push(format!(
|
||||||
|
"__chain_{}_{}__",
|
||||||
|
pending.id.bank, pending.id.pattern
|
||||||
|
));
|
||||||
|
stopped_ids.push(pending.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audio_state.pending_stops.retain(|p| !stopped_ids.contains(&p.id));
|
||||||
|
|
||||||
let prev_beat = audio_state.prev_beat;
|
|
||||||
let mut chain_transitions: Vec<(PatternId, PatternId)> = Vec::new();
|
let mut chain_transitions: Vec<(PatternId, PatternId)> = Vec::new();
|
||||||
let mut chain_keys_to_remove: Vec<String> = Vec::new();
|
let mut chain_keys_to_remove: Vec<String> = Vec::new();
|
||||||
let mut new_tempo: Option<f64> = None;
|
let mut new_tempo: Option<f64> = None;
|
||||||
@@ -515,13 +615,25 @@ fn sequencer_loop(
|
|||||||
link.set_tempo(t);
|
link.set_tempo(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply chain transitions
|
// Apply chain transitions (use Bar quantization for chains)
|
||||||
for (source, target) in chain_transitions {
|
for (source, target) in chain_transitions {
|
||||||
if !audio_state.pending_stops.contains(&source) {
|
if !audio_state.pending_stops.iter().any(|p| p.id == source) {
|
||||||
audio_state.pending_stops.push(source);
|
audio_state.pending_stops.push(PendingPattern {
|
||||||
|
id: source,
|
||||||
|
quantization: LaunchQuantization::Bar,
|
||||||
|
sync_mode: SyncMode::Reset,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if !audio_state.pending_starts.contains(&target) {
|
if !audio_state.pending_starts.iter().any(|p| p.id == target) {
|
||||||
audio_state.pending_starts.push(target);
|
let (quant, sync) = pattern_cache
|
||||||
|
.get(target.bank, target.pattern)
|
||||||
|
.map(|p| (p.quantization, p.sync_mode))
|
||||||
|
.unwrap_or((LaunchQuantization::Bar, SyncMode::Reset));
|
||||||
|
audio_state.pending_starts.push(PendingPattern {
|
||||||
|
id: target,
|
||||||
|
quantization: quant,
|
||||||
|
sync_mode: sync,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
109
src/input.rs
109
src/input.rs
@@ -10,7 +10,10 @@ use crate::commands::AppCommand;
|
|||||||
use crate::engine::{AudioCommand, LinkState, SequencerSnapshot};
|
use crate::engine::{AudioCommand, LinkState, SequencerSnapshot};
|
||||||
use crate::model::PatternSpeed;
|
use crate::model::PatternSpeed;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{DeviceKind, EngineSection, Modal, OptionsFocus, PanelFocus, PatternField, SampleBrowserState, SettingKind, SidePanel};
|
use crate::state::{
|
||||||
|
DeviceKind, EngineSection, Modal, OptionsFocus, PanelFocus, PatternField, PatternPropsField,
|
||||||
|
SampleBrowserState, SettingKind, SidePanel,
|
||||||
|
};
|
||||||
|
|
||||||
pub enum InputResult {
|
pub enum InputResult {
|
||||||
Continue,
|
Continue,
|
||||||
@@ -385,6 +388,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
},
|
},
|
||||||
Modal::Editor => {
|
Modal::Editor => {
|
||||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||||
let editor = &mut ctx.app.editor_ctx.editor;
|
let editor = &mut ctx.app.editor_ctx.editor;
|
||||||
|
|
||||||
if editor.search_active() {
|
if editor.search_active() {
|
||||||
@@ -400,7 +404,9 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
if editor.completion_active() {
|
if editor.is_selecting() {
|
||||||
|
editor.cancel_selection();
|
||||||
|
} else if editor.completion_active() {
|
||||||
editor.dismiss_completion();
|
editor.dismiss_completion();
|
||||||
} else {
|
} else {
|
||||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||||
@@ -421,6 +427,24 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
KeyCode::Char('p') if ctrl => {
|
KeyCode::Char('p') if ctrl => {
|
||||||
editor.search_prev();
|
editor.search_prev();
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('a') if ctrl => {
|
||||||
|
editor.select_all();
|
||||||
|
}
|
||||||
|
KeyCode::Char('c') if ctrl => {
|
||||||
|
editor.copy();
|
||||||
|
}
|
||||||
|
KeyCode::Char('x') if ctrl => {
|
||||||
|
editor.cut();
|
||||||
|
}
|
||||||
|
KeyCode::Char('v') if ctrl => {
|
||||||
|
editor.paste();
|
||||||
|
}
|
||||||
|
KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down if shift => {
|
||||||
|
if !editor.is_selecting() {
|
||||||
|
editor.start_selection();
|
||||||
|
}
|
||||||
|
editor.input(Event::Key(key));
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
editor.input(Event::Key(key));
|
editor.input(Event::Key(key));
|
||||||
}
|
}
|
||||||
@@ -434,6 +458,71 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
KeyCode::Down => ctx.dispatch(AppCommand::StepDown),
|
KeyCode::Down => ctx.dispatch(AppCommand::StepDown),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
Modal::PatternProps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
field,
|
||||||
|
name,
|
||||||
|
length,
|
||||||
|
speed,
|
||||||
|
quantization,
|
||||||
|
sync_mode,
|
||||||
|
} => {
|
||||||
|
let (bank, pattern) = (*bank, *pattern);
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => *field = field.prev(),
|
||||||
|
KeyCode::Down | KeyCode::Tab => *field = field.next(),
|
||||||
|
KeyCode::Left => match field {
|
||||||
|
PatternPropsField::Speed => *speed = speed.prev(),
|
||||||
|
PatternPropsField::Quantization => *quantization = quantization.prev(),
|
||||||
|
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
KeyCode::Right => match field {
|
||||||
|
PatternPropsField::Speed => *speed = speed.next(),
|
||||||
|
PatternPropsField::Quantization => *quantization = quantization.next(),
|
||||||
|
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
KeyCode::Char(c) => match field {
|
||||||
|
PatternPropsField::Name => name.push(c),
|
||||||
|
PatternPropsField::Length if c.is_ascii_digit() => length.push(c),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
KeyCode::Backspace => match field {
|
||||||
|
PatternPropsField::Name => {
|
||||||
|
name.pop();
|
||||||
|
}
|
||||||
|
PatternPropsField::Length => {
|
||||||
|
length.pop();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
KeyCode::Enter => {
|
||||||
|
let name_val = if name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(name.clone())
|
||||||
|
};
|
||||||
|
let length_val = length.parse().ok();
|
||||||
|
let speed_val = *speed;
|
||||||
|
let quant_val = *quantization;
|
||||||
|
let sync_val = *sync_mode;
|
||||||
|
ctx.dispatch(AppCommand::SetPatternProps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
name: name_val,
|
||||||
|
length: length_val,
|
||||||
|
speed: speed_val,
|
||||||
|
quantization: quant_val,
|
||||||
|
sync_mode: sync_val,
|
||||||
|
});
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
|
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Modal::None => unreachable!(),
|
Modal::None => unreachable!(),
|
||||||
}
|
}
|
||||||
InputResult::Continue
|
InputResult::Continue
|
||||||
@@ -660,9 +749,16 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
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 => ctx.dispatch(AppCommand::PatternsBack),
|
KeyCode::Esc => {
|
||||||
|
if !ctx.app.playback.staged_changes.is_empty() {
|
||||||
|
ctx.dispatch(AppCommand::ClearStagedChanges);
|
||||||
|
} else {
|
||||||
|
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('c') if !ctrl => ctx.dispatch(AppCommand::CommitStagedChanges),
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||||
selected: false,
|
selected: false,
|
||||||
@@ -738,6 +834,13 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('e') if !ctrl => {
|
||||||
|
if ctx.app.patterns_nav.column == PatternsColumn::Patterns {
|
||||||
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
|
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||||
|
ctx.dispatch(AppCommand::OpenPatternPropsModal { bank, pattern });
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
InputResult::Continue
|
InputResult::Continue
|
||||||
|
|||||||
10
src/main.rs
10
src/main.rs
@@ -71,9 +71,13 @@ fn main() -> io::Result<()> {
|
|||||||
|
|
||||||
app.playback
|
app.playback
|
||||||
.queued_changes
|
.queued_changes
|
||||||
.push(engine::PatternChange::Start {
|
.push(crate::state::StagedChange {
|
||||||
bank: 0,
|
change: engine::PatternChange::Start {
|
||||||
pattern: 0,
|
bank: 0,
|
||||||
|
pattern: 0,
|
||||||
|
},
|
||||||
|
quantization: crate::model::LaunchQuantization::Immediate,
|
||||||
|
sync_mode: crate::model::SyncMode::Reset,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.audio.config.output_device = args.output.or(settings.audio.output_device);
|
app.audio.config.output_device = args.output.or(settings.audio.output_device);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
mod script;
|
mod script;
|
||||||
|
|
||||||
pub use cagire_forth::{Word, WordCompile, WORDS};
|
pub use cagire_forth::{Word, WordCompile, WORDS};
|
||||||
pub use cagire_project::{load, save, Bank, Pattern, PatternSpeed, Project, MAX_BANKS, MAX_PATTERNS};
|
pub use cagire_project::{
|
||||||
|
load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS,
|
||||||
|
MAX_PATTERNS,
|
||||||
|
};
|
||||||
pub use script::{Dictionary, ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Value, Variables};
|
pub use script::{Dictionary, ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Value, Variables};
|
||||||
|
|||||||
@@ -12,6 +12,38 @@ pub enum PatternField {
|
|||||||
Speed,
|
Speed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum PatternPropsField {
|
||||||
|
#[default]
|
||||||
|
Name,
|
||||||
|
Length,
|
||||||
|
Speed,
|
||||||
|
Quantization,
|
||||||
|
SyncMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PatternPropsField {
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Name => Self::Length,
|
||||||
|
Self::Length => Self::Speed,
|
||||||
|
Self::Speed => Self::Quantization,
|
||||||
|
Self::Quantization => Self::SyncMode,
|
||||||
|
Self::SyncMode => Self::SyncMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Name => Self::Name,
|
||||||
|
Self::Length => Self::Name,
|
||||||
|
Self::Speed => Self::Length,
|
||||||
|
Self::Quantization => Self::Speed,
|
||||||
|
Self::SyncMode => Self::Quantization,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct EditorContext {
|
pub struct EditorContext {
|
||||||
pub bank: usize,
|
pub bank: usize,
|
||||||
pub pattern: usize,
|
pub pattern: usize,
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ pub mod ui;
|
|||||||
|
|
||||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
|
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
|
||||||
pub use options::{OptionsFocus, OptionsState};
|
pub use options::{OptionsFocus, OptionsState};
|
||||||
pub use editor::{CopiedStep, EditorContext, Focus, PatternField};
|
pub use editor::{CopiedStep, EditorContext, Focus, PatternField, PatternPropsField};
|
||||||
pub use live_keys::LiveKeyState;
|
pub use live_keys::LiveKeyState;
|
||||||
pub use modal::Modal;
|
pub use modal::Modal;
|
||||||
pub use panel::{PanelFocus, PanelState, SidePanel};
|
pub use panel::{PanelFocus, PanelState, SidePanel};
|
||||||
pub use patterns_nav::{PatternsColumn, PatternsNav};
|
pub use patterns_nav::{PatternsColumn, PatternsNav};
|
||||||
pub use playback::PlaybackState;
|
pub use playback::{PlaybackState, StagedChange};
|
||||||
pub use project::ProjectState;
|
pub use project::ProjectState;
|
||||||
pub use sample_browser::SampleBrowserState;
|
pub use sample_browser::SampleBrowserState;
|
||||||
pub use ui::{DictFocus, FlashKind, UiState};
|
pub use ui::{DictFocus, FlashKind, UiState};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::state::editor::PatternField;
|
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
||||||
|
use crate::state::editor::{PatternField, PatternPropsField};
|
||||||
use crate::state::file_browser::FileBrowserState;
|
use crate::state::file_browser::FileBrowserState;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
@@ -40,4 +41,14 @@ pub enum Modal {
|
|||||||
AddSamplePath(FileBrowserState),
|
AddSamplePath(FileBrowserState),
|
||||||
Editor,
|
Editor,
|
||||||
Preview,
|
Preview,
|
||||||
|
PatternProps {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
field: PatternPropsField,
|
||||||
|
name: String,
|
||||||
|
length: String,
|
||||||
|
speed: PatternSpeed,
|
||||||
|
quantization: LaunchQuantization,
|
||||||
|
sync_mode: SyncMode,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use crate::model::{MAX_BANKS, MAX_PATTERNS};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub enum PatternsColumn {
|
pub enum PatternsColumn {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -24,10 +26,10 @@ impl PatternsNav {
|
|||||||
pub fn move_up(&mut self) {
|
pub fn move_up(&mut self) {
|
||||||
match self.column {
|
match self.column {
|
||||||
PatternsColumn::Banks => {
|
PatternsColumn::Banks => {
|
||||||
self.bank_cursor = (self.bank_cursor + 15) % 16;
|
self.bank_cursor = (self.bank_cursor + MAX_BANKS - 1) % MAX_BANKS;
|
||||||
}
|
}
|
||||||
PatternsColumn::Patterns => {
|
PatternsColumn::Patterns => {
|
||||||
self.pattern_cursor = (self.pattern_cursor + 15) % 16;
|
self.pattern_cursor = (self.pattern_cursor + MAX_PATTERNS - 1) % MAX_PATTERNS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,10 +37,10 @@ impl PatternsNav {
|
|||||||
pub fn move_down(&mut self) {
|
pub fn move_down(&mut self) {
|
||||||
match self.column {
|
match self.column {
|
||||||
PatternsColumn::Banks => {
|
PatternsColumn::Banks => {
|
||||||
self.bank_cursor = (self.bank_cursor + 1) % 16;
|
self.bank_cursor = (self.bank_cursor + 1) % MAX_BANKS;
|
||||||
}
|
}
|
||||||
PatternsColumn::Patterns => {
|
PatternsColumn::Patterns => {
|
||||||
self.pattern_cursor = (self.pattern_cursor + 1) % 16;
|
self.pattern_cursor = (self.pattern_cursor + 1) % MAX_PATTERNS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
use crate::engine::PatternChange;
|
use crate::engine::PatternChange;
|
||||||
|
use crate::model::{LaunchQuantization, SyncMode};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct StagedChange {
|
||||||
|
pub change: PatternChange,
|
||||||
|
pub quantization: LaunchQuantization,
|
||||||
|
pub sync_mode: SyncMode,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PlaybackState {
|
pub struct PlaybackState {
|
||||||
pub playing: bool,
|
pub playing: bool,
|
||||||
pub queued_changes: Vec<PatternChange>,
|
pub staged_changes: Vec<StagedChange>,
|
||||||
|
pub queued_changes: Vec<StagedChange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PlaybackState {
|
impl Default for PlaybackState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
playing: true,
|
playing: true,
|
||||||
|
staged_changes: Vec::new(),
|
||||||
queued_changes: Vec::new(),
|
queued_changes: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::engine::SequencerSnapshot;
|
use crate::engine::SequencerSnapshot;
|
||||||
|
use crate::model::{MAX_BANKS, MAX_PATTERNS};
|
||||||
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) {
|
||||||
@@ -44,25 +45,25 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
|
|||||||
.map(|p| p.bank)
|
.map(|p| p.bank)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let banks_with_queued: Vec<usize> = app
|
let banks_with_staged: Vec<usize> = app
|
||||||
.playback
|
.playback
|
||||||
.queued_changes
|
.staged_changes
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|c| match c {
|
.filter_map(|c| match &c.change {
|
||||||
crate::engine::PatternChange::Start { bank, .. } => Some(*bank),
|
crate::engine::PatternChange::Start { bank, .. } => Some(*bank),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let row_height = (inner.height / 16).max(1);
|
let row_height = (inner.height / MAX_BANKS as u16).max(1);
|
||||||
let total_needed = row_height * 16;
|
let total_needed = row_height * MAX_BANKS as u16;
|
||||||
let top_padding = if inner.height > total_needed {
|
let top_padding = if inner.height > total_needed {
|
||||||
(inner.height - total_needed) / 2
|
(inner.height - total_needed) / 2
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
for idx in 0..16 {
|
for idx in 0..MAX_BANKS {
|
||||||
let y = inner.y + top_padding + (idx as u16) * row_height;
|
let y = inner.y + top_padding + (idx as u16) * row_height;
|
||||||
if y >= inner.y + inner.height {
|
if y >= inner.y + inner.height {
|
||||||
break;
|
break;
|
||||||
@@ -79,12 +80,12 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
|
|||||||
let is_selected = idx == app.patterns_nav.bank_cursor;
|
let is_selected = idx == app.patterns_nav.bank_cursor;
|
||||||
let is_edit = idx == app.editor_ctx.bank;
|
let is_edit = idx == app.editor_ctx.bank;
|
||||||
let is_playing = banks_with_playback.contains(&idx);
|
let is_playing = banks_with_playback.contains(&idx);
|
||||||
let is_queued = banks_with_queued.contains(&idx);
|
let is_staged = banks_with_staged.contains(&idx);
|
||||||
|
|
||||||
let (bg, fg, prefix) = match (is_cursor, is_playing, is_queued) {
|
let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged) {
|
||||||
(true, _, _) => (Color::Cyan, Color::Black, ""),
|
(true, _, _) => (Color::Cyan, Color::Black, ""),
|
||||||
(false, true, _) => (Color::Rgb(45, 80, 45), Color::Green, "> "),
|
(false, true, _) => (Color::Rgb(45, 80, 45), Color::Green, "> "),
|
||||||
(false, false, true) => (Color::Rgb(80, 80, 45), Color::Yellow, "? "),
|
(false, false, true) => (Color::Rgb(80, 60, 100), Color::Magenta, "+ "),
|
||||||
(false, false, false) if is_selected => (Color::Rgb(60, 65, 75), Color::White, ""),
|
(false, false, false) if is_selected => (Color::Rgb(60, 65, 75), Color::White, ""),
|
||||||
(false, false, false) if is_edit => (Color::Rgb(45, 106, 95), Color::White, ""),
|
(false, false, false) if is_edit => (Color::Rgb(45, 106, 95), Color::White, ""),
|
||||||
(false, false, false) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
(false, false, false) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
||||||
@@ -101,7 +102,7 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area
|
|||||||
};
|
};
|
||||||
|
|
||||||
let style = Style::new().bg(bg).fg(fg);
|
let style = Style::new().bg(bg).fg(fg);
|
||||||
let style = if is_playing || is_queued {
|
let style = if is_playing || is_staged {
|
||||||
style.add_modifier(Modifier::BOLD)
|
style.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
style
|
style
|
||||||
@@ -159,11 +160,11 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
.map(|p| p.pattern)
|
.map(|p| p.pattern)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let queued_to_play: Vec<usize> = app
|
let staged_to_play: Vec<usize> = app
|
||||||
.playback
|
.playback
|
||||||
.queued_changes
|
.staged_changes
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|c| match c {
|
.filter_map(|c| match &c.change {
|
||||||
crate::engine::PatternChange::Start {
|
crate::engine::PatternChange::Start {
|
||||||
bank: b, pattern, ..
|
bank: b, pattern, ..
|
||||||
} if *b == bank => Some(*pattern),
|
} if *b == bank => Some(*pattern),
|
||||||
@@ -171,11 +172,11 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let queued_to_stop: Vec<usize> = app
|
let staged_to_stop: Vec<usize> = app
|
||||||
.playback
|
.playback
|
||||||
.queued_changes
|
.staged_changes
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|c| match c {
|
.filter_map(|c| match &c.change {
|
||||||
crate::engine::PatternChange::Stop {
|
crate::engine::PatternChange::Stop {
|
||||||
bank: b,
|
bank: b,
|
||||||
pattern,
|
pattern,
|
||||||
@@ -190,15 +191,15 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let row_height = (inner.height / 16).max(1);
|
let row_height = (inner.height / MAX_PATTERNS as u16).max(1);
|
||||||
let total_needed = row_height * 16;
|
let total_needed = row_height * MAX_PATTERNS as u16;
|
||||||
let top_padding = if inner.height > total_needed {
|
let top_padding = if inner.height > total_needed {
|
||||||
(inner.height - total_needed) / 2
|
(inner.height - total_needed) / 2
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
for idx in 0..16 {
|
for idx in 0..MAX_PATTERNS {
|
||||||
let y = inner.y + top_padding + (idx as u16) * row_height;
|
let y = inner.y + top_padding + (idx as u16) * row_height;
|
||||||
if y >= inner.y + inner.height {
|
if y >= inner.y + inner.height {
|
||||||
break;
|
break;
|
||||||
@@ -215,14 +216,14 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
let is_selected = idx == app.patterns_nav.pattern_cursor;
|
let is_selected = idx == app.patterns_nav.pattern_cursor;
|
||||||
let is_edit = edit_pattern == Some(idx);
|
let is_edit = edit_pattern == Some(idx);
|
||||||
let is_playing = playing_patterns.contains(&idx);
|
let is_playing = playing_patterns.contains(&idx);
|
||||||
let is_queued_play = queued_to_play.contains(&idx);
|
let is_staged_play = staged_to_play.contains(&idx);
|
||||||
let is_queued_stop = queued_to_stop.contains(&idx);
|
let is_staged_stop = staged_to_stop.contains(&idx);
|
||||||
|
|
||||||
let (bg, fg, prefix) = match (is_cursor, is_playing, is_queued_play, is_queued_stop) {
|
let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged_play, is_staged_stop) {
|
||||||
(true, _, _, _) => (Color::Cyan, Color::Black, ""),
|
(true, _, _, _) => (Color::Cyan, Color::Black, ""),
|
||||||
(false, true, _, true) => (Color::Rgb(120, 90, 30), Color::Yellow, "x "),
|
(false, true, _, true) => (Color::Rgb(120, 60, 80), Color::Magenta, "- "),
|
||||||
(false, true, _, false) => (Color::Rgb(45, 80, 45), Color::Green, "> "),
|
(false, true, _, false) => (Color::Rgb(45, 80, 45), Color::Green, "> "),
|
||||||
(false, false, true, _) => (Color::Rgb(80, 80, 45), Color::Yellow, "? "),
|
(false, false, true, _) => (Color::Rgb(80, 60, 100), Color::Magenta, "+ "),
|
||||||
(false, false, false, _) if is_selected => (Color::Rgb(60, 65, 75), Color::White, ""),
|
(false, false, false, _) if is_selected => (Color::Rgb(60, 65, 75), Color::White, ""),
|
||||||
(false, false, false, _) if is_edit => (Color::Rgb(45, 106, 95), Color::White, ""),
|
(false, false, false, _) if is_edit => (Color::Rgb(45, 106, 95), Color::White, ""),
|
||||||
(false, false, false, _) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
(false, false, false, _) => (Color::Reset, Color::Rgb(120, 125, 135), ""),
|
||||||
@@ -271,7 +272,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
} else {
|
} else {
|
||||||
format!("{}{:02} {}", prefix, idx + 1, name)
|
format!("{}{:02} {}", prefix, idx + 1, name)
|
||||||
};
|
};
|
||||||
let name_style = if is_playing || is_queued_play {
|
let name_style = if is_playing || is_staged_play {
|
||||||
bold_style
|
bold_style
|
||||||
} else {
|
} else {
|
||||||
base_style
|
base_style
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::time::Instant;
|
|||||||
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, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
@@ -626,5 +626,91 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
};
|
};
|
||||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||||
}
|
}
|
||||||
|
Modal::PatternProps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
field,
|
||||||
|
name,
|
||||||
|
length,
|
||||||
|
speed,
|
||||||
|
quantization,
|
||||||
|
sync_mode,
|
||||||
|
} => {
|
||||||
|
use crate::state::PatternPropsField;
|
||||||
|
|
||||||
|
let width = 50u16;
|
||||||
|
let height = 12u16;
|
||||||
|
let x = (term.width.saturating_sub(width)) / 2;
|
||||||
|
let y = (term.height.saturating_sub(height)) / 2;
|
||||||
|
let area = Rect::new(x, y, width, height);
|
||||||
|
|
||||||
|
let block = Block::bordered()
|
||||||
|
.title(format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
|
||||||
|
.border_style(Style::default().fg(Color::Cyan));
|
||||||
|
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
let fields = [
|
||||||
|
("Name", name.as_str(), *field == PatternPropsField::Name),
|
||||||
|
("Length", length.as_str(), *field == PatternPropsField::Length),
|
||||||
|
("Speed", speed.label(), *field == PatternPropsField::Speed),
|
||||||
|
(
|
||||||
|
"Quantization",
|
||||||
|
quantization.label(),
|
||||||
|
*field == PatternPropsField::Quantization,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Sync Mode",
|
||||||
|
sync_mode.label(),
|
||||||
|
*field == PatternPropsField::SyncMode,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, (label, value, selected)) in fields.iter().enumerate() {
|
||||||
|
let y = inner.y + i as u16;
|
||||||
|
if y >= inner.y + inner.height {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (label_style, value_style) = if *selected {
|
||||||
|
(
|
||||||
|
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||||
|
Style::default().fg(Color::White).bg(Color::DarkGray),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Style::default().fg(Color::Gray),
|
||||||
|
Style::default().fg(Color::White),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let label_area = Rect::new(inner.x + 1, y, 14, 1);
|
||||||
|
let value_area = Rect::new(inner.x + 16, y, inner.width.saturating_sub(18), 1);
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(format!("{label}:")).style(label_style),
|
||||||
|
label_area,
|
||||||
|
);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(*value).style(value_style),
|
||||||
|
value_area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
|
||||||
|
let hint = Line::from(vec![
|
||||||
|
Span::styled("↑↓", Style::default().fg(Color::Yellow)),
|
||||||
|
Span::styled(" nav ", Style::default().fg(Color::DarkGray)),
|
||||||
|
Span::styled("←→", Style::default().fg(Color::Yellow)),
|
||||||
|
Span::styled(" change ", Style::default().fg(Color::DarkGray)),
|
||||||
|
Span::styled("Enter", Style::default().fg(Color::Yellow)),
|
||||||
|
Span::styled(" save ", Style::default().fg(Color::DarkGray)),
|
||||||
|
Span::styled("Esc", Style::default().fg(Color::Yellow)),
|
||||||
|
Span::styled(" cancel", Style::default().fg(Color::DarkGray)),
|
||||||
|
]);
|
||||||
|
frame.render_widget(Paragraph::new(hint), hint_area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user