WIP: clap
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::{LinkState, SequencerSnapshot};
|
||||
use crate::services::{dict_nav, euclidean, help_nav, pattern_editor};
|
||||
use crate::state::{undo::UndoEntry, CyclicEnum, FlashKind, Modal, StagedPropChange};
|
||||
use crate::state::{undo::UndoEntry, FlashKind, Modal, StagedPropChange};
|
||||
|
||||
use super::App;
|
||||
|
||||
@@ -344,8 +344,8 @@ impl App {
|
||||
|
||||
// Audio settings (engine page)
|
||||
AppCommand::AudioSetSection(section) => self.audio.section = section,
|
||||
AppCommand::AudioNextSection => self.audio.next_section(),
|
||||
AppCommand::AudioPrevSection => self.audio.prev_section(),
|
||||
AppCommand::AudioNextSection => self.audio.next_section(self.plugin_mode),
|
||||
AppCommand::AudioPrevSection => self.audio.prev_section(self.plugin_mode),
|
||||
AppCommand::AudioOutputListUp => self.audio.output_list.move_up(),
|
||||
AppCommand::AudioOutputListDown(count) => self.audio.output_list.move_down(count),
|
||||
AppCommand::AudioOutputPageUp => self.audio.output_list.page_up(),
|
||||
@@ -353,8 +353,8 @@ impl App {
|
||||
AppCommand::AudioInputListUp => self.audio.input_list.move_up(),
|
||||
AppCommand::AudioInputListDown(count) => self.audio.input_list.move_down(count),
|
||||
AppCommand::AudioInputPageDown(count) => self.audio.input_list.page_down(count),
|
||||
AppCommand::AudioSettingNext => self.audio.setting_kind = self.audio.setting_kind.next(),
|
||||
AppCommand::AudioSettingPrev => self.audio.setting_kind = self.audio.setting_kind.prev(),
|
||||
AppCommand::AudioSettingNext => self.audio.next_setting(self.plugin_mode),
|
||||
AppCommand::AudioSettingPrev => self.audio.prev_setting(self.plugin_mode),
|
||||
AppCommand::SetOutputDevice(name) => self.audio.config.output_device = Some(name),
|
||||
AppCommand::SetInputDevice(name) => self.audio.config.input_device = Some(name),
|
||||
AppCommand::SetDeviceKind(kind) => self.audio.device_kind = kind,
|
||||
|
||||
@@ -69,6 +69,7 @@ pub struct App {
|
||||
pub options: OptionsState,
|
||||
pub panel: PanelState,
|
||||
pub midi: MidiState,
|
||||
pub plugin_mode: bool,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
@@ -82,6 +83,15 @@ impl App {
|
||||
let variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
Self::build(variables, dict, rng)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_shared(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
|
||||
Self::build(variables, dict, rng)
|
||||
}
|
||||
|
||||
fn build(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
|
||||
let script_engine =
|
||||
ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), Arc::clone(&rng));
|
||||
let live_keys = Arc::new(LiveKeyState::new());
|
||||
@@ -113,6 +123,7 @@ impl App {
|
||||
options: OptionsState::default(),
|
||||
panel: PanelState::default(),
|
||||
midi: MidiState::new(),
|
||||
plugin_mode: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,8 @@ impl App {
|
||||
cc_access: None,
|
||||
speed_key: "",
|
||||
chain_key: "",
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@ impl App {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender<SeqCommand>) {
|
||||
for (bank, pattern) in self.project_state.take_dirty() {
|
||||
pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender<SeqCommand>) -> bool {
|
||||
let dirty = self.project_state.take_dirty();
|
||||
let had_dirty = !dirty.is_empty();
|
||||
for (bank, pattern) in dirty {
|
||||
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||
let snapshot = PatternSnapshot {
|
||||
speed: pat.speed,
|
||||
@@ -59,5 +61,6 @@ impl App {
|
||||
data: snapshot,
|
||||
});
|
||||
}
|
||||
had_dirty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@ pub use timing::{substeps_in_window, StepTiming, SyncTime};
|
||||
|
||||
// AnalysisHandle and SequencerHandle are used by src/bin/desktop.rs
|
||||
#[allow(unused_imports)]
|
||||
pub use audio::{build_stream, AnalysisHandle, AudioStreamConfig, ScopeBuffer, SpectrumBuffer};
|
||||
pub use audio::{
|
||||
build_stream, spawn_analysis_thread, AnalysisHandle, AudioStreamConfig, ScopeBuffer,
|
||||
SpectrumBuffer,
|
||||
};
|
||||
pub use link::LinkState;
|
||||
#[allow(unused_imports)]
|
||||
pub use sequencer::{
|
||||
spawn_sequencer, AudioCommand, MidiCommand, PatternChange, PatternSnapshot, SeqCommand,
|
||||
SequencerConfig, SequencerHandle, SequencerSnapshot, StepSnapshot,
|
||||
parse_midi_command, spawn_sequencer, AudioCommand, MidiCommand, PatternChange,
|
||||
PatternSnapshot, SeqCommand, SequencerConfig, SequencerHandle, SequencerSnapshot,
|
||||
SequencerState, SharedSequencerState, StepSnapshot, TickInput, TickOutput, TimestampedCommand,
|
||||
};
|
||||
|
||||
@@ -166,7 +166,26 @@ pub struct SequencerSnapshot {
|
||||
pub event_count: usize,
|
||||
}
|
||||
|
||||
impl From<&SharedSequencerState> for SequencerSnapshot {
|
||||
fn from(s: &SharedSequencerState) -> Self {
|
||||
Self {
|
||||
active_patterns: s.active_patterns.clone(),
|
||||
step_traces: Arc::clone(&s.step_traces),
|
||||
event_count: s.event_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SequencerSnapshot {
|
||||
#[allow(dead_code)]
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
active_patterns: Vec::new(),
|
||||
step_traces: Arc::new(HashMap::new()),
|
||||
event_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_playing(&self, bank: usize, pattern: usize) -> bool {
|
||||
self.active_patterns
|
||||
.iter()
|
||||
@@ -452,7 +471,7 @@ impl RunsCounter {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TickInput {
|
||||
pub struct TickInput {
|
||||
pub commands: Vec<SeqCommand>,
|
||||
pub playing: bool,
|
||||
pub beat: f64,
|
||||
@@ -463,11 +482,8 @@ pub(crate) struct TickInput {
|
||||
pub nudge_secs: f64,
|
||||
pub current_time_us: SyncTime,
|
||||
pub engine_time: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_x: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_y: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_down: f64,
|
||||
}
|
||||
|
||||
@@ -476,7 +492,7 @@ pub struct TimestampedCommand {
|
||||
pub time: Option<f64>,
|
||||
}
|
||||
|
||||
pub(crate) struct TickOutput {
|
||||
pub struct TickOutput {
|
||||
pub audio_commands: Vec<TimestampedCommand>,
|
||||
pub new_tempo: Option<f64>,
|
||||
pub shared_state: SharedSequencerState,
|
||||
@@ -528,7 +544,7 @@ fn format_chain_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
|
||||
buf
|
||||
}
|
||||
|
||||
pub(crate) struct SequencerState {
|
||||
pub struct SequencerState {
|
||||
audio_state: AudioState,
|
||||
pattern_cache: PatternCache,
|
||||
pending_updates: HashMap<(usize, usize), PatternSnapshot>,
|
||||
@@ -710,11 +726,8 @@ impl SequencerState {
|
||||
input.nudge_secs,
|
||||
input.current_time_us,
|
||||
input.engine_time,
|
||||
#[cfg(feature = "desktop")]
|
||||
input.mouse_x,
|
||||
#[cfg(feature = "desktop")]
|
||||
input.mouse_y,
|
||||
#[cfg(feature = "desktop")]
|
||||
input.mouse_down,
|
||||
);
|
||||
|
||||
@@ -842,9 +855,9 @@ impl SequencerState {
|
||||
nudge_secs: f64,
|
||||
_current_time_us: SyncTime,
|
||||
engine_time: f64,
|
||||
#[cfg(feature = "desktop")] mouse_x: f64,
|
||||
#[cfg(feature = "desktop")] mouse_y: f64,
|
||||
#[cfg(feature = "desktop")] mouse_down: f64,
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
mouse_down: f64,
|
||||
) -> StepResult {
|
||||
self.buf_audio_commands.clear();
|
||||
self.buf_completed_iterations.clear();
|
||||
@@ -924,11 +937,8 @@ impl SequencerState {
|
||||
cc_access: self.cc_access.as_deref(),
|
||||
speed_key,
|
||||
chain_key,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down,
|
||||
};
|
||||
if let Some(script) = resolved_script {
|
||||
@@ -1170,10 +1180,16 @@ fn sequencer_loop(
|
||||
engine_time,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: f32::from_bits(mouse_x.load(Ordering::Relaxed)) as f64,
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: f32::from_bits(mouse_y.load(Ordering::Relaxed)) as f64,
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: f32::from_bits(mouse_down.load(Ordering::Relaxed)) as f64,
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
mouse_down: 0.0,
|
||||
};
|
||||
|
||||
let output = seq_state.tick(input);
|
||||
@@ -1231,7 +1247,7 @@ fn sequencer_loop(
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>, f64)> {
|
||||
pub fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>, f64)> {
|
||||
if !cmd.starts_with("/midi/") {
|
||||
return None;
|
||||
}
|
||||
@@ -1384,11 +1400,8 @@ mod tests {
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
engine_time: 0.0,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
}
|
||||
}
|
||||
@@ -1405,11 +1418,8 @@ mod tests {
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
engine_time: 0.0,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct InitArgs {
|
||||
pub buffer: Option<u32>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Init {
|
||||
pub app: App,
|
||||
pub link: Arc<LinkState>,
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(crate) fn cycle_engine_setting(ctx: &mut InputContext, right: bool) {
|
||||
|
||||
pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
@@ -42,17 +42,17 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::AudioNextSection),
|
||||
KeyCode::BackTab => ctx.dispatch(AppCommand::AudioPrevSection),
|
||||
KeyCode::Up => match ctx.app.audio.section {
|
||||
EngineSection::Devices => match ctx.app.audio.device_kind {
|
||||
EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputListUp),
|
||||
DeviceKind::Input => ctx.dispatch(AppCommand::AudioInputListUp),
|
||||
},
|
||||
EngineSection::Settings => {
|
||||
ctx.dispatch(AppCommand::AudioSettingPrev);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Down => match ctx.app.audio.section {
|
||||
EngineSection::Devices => match ctx.app.audio.device_kind {
|
||||
EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
ctx.dispatch(AppCommand::AudioOutputListDown(count));
|
||||
@@ -65,10 +65,10 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
EngineSection::Settings => {
|
||||
ctx.dispatch(AppCommand::AudioSettingNext);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::PageUp => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputPageUp),
|
||||
DeviceKind::Input => ctx.app.audio.input_list.page_up(),
|
||||
@@ -76,7 +76,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
@@ -90,7 +90,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let cursor = ctx.app.audio.output_list.cursor;
|
||||
@@ -112,20 +112,22 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
KeyCode::Left => match ctx.app.audio.section {
|
||||
EngineSection::Devices => {
|
||||
EngineSection::Devices if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Output));
|
||||
}
|
||||
EngineSection::Settings => cycle_engine_setting(ctx, false),
|
||||
EngineSection::Samples => {}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Right => match ctx.app.audio.section {
|
||||
EngineSection::Devices => {
|
||||
EngineSection::Devices if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Input));
|
||||
}
|
||||
EngineSection::Settings => cycle_engine_setting(ctx, true),
|
||||
EngineSection::Samples => {}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Char('R') => ctx.dispatch(AppCommand::AudioTriggerRestart),
|
||||
KeyCode::Char('R') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::AudioTriggerRestart);
|
||||
}
|
||||
KeyCode::Char('A') => {
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
let state = FileBrowserState::new_load(String::new());
|
||||
@@ -134,7 +136,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Char('D') => {
|
||||
if ctx.app.audio.section == EngineSection::Samples {
|
||||
ctx.dispatch(AppCommand::RemoveLastSamplePath);
|
||||
} else {
|
||||
} else if !ctx.app.plugin_mode {
|
||||
ctx.dispatch(AppCommand::AudioRefreshDevices);
|
||||
let out_count = ctx.app.audio.output_devices.len();
|
||||
let in_count = ctx.app.audio.input_devices.len();
|
||||
@@ -144,15 +146,19 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Hush);
|
||||
if !ctx.app.plugin_mode {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Hush);
|
||||
}
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
|
||||
if !ctx.app.plugin_mode {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
|
||||
}
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||
}
|
||||
KeyCode::Char('r') => ctx.dispatch(AppCommand::ResetPeakVoices),
|
||||
KeyCode::Char('t') => {
|
||||
KeyCode::Char('t') if !ctx.app.plugin_mode => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate {
|
||||
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
|
||||
time: None,
|
||||
|
||||
@@ -66,7 +66,7 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
|
||||
},
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::HelpScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::HelpScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
@@ -236,7 +236,7 @@ pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
|
||||
},
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::DictScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::DictScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
|
||||
@@ -23,7 +23,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
ctx.app.panel.focus = PanelFocus::Side;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
|
||||
@@ -434,7 +434,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => {
|
||||
let bindings_count = crate::views::keybindings::bindings_for(ctx.app.page).len();
|
||||
let bindings_count = crate::views::keybindings::bindings_for(ctx.app.page, ctx.app.plugin_mode).len();
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('?') => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
|
||||
@@ -149,7 +149,7 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
|
||||
|
||||
pub(super) fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
|
||||
@@ -95,7 +95,7 @@ pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> Inp
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
|
||||
@@ -61,11 +61,8 @@ pub fn update_cache(editor_ctx: &EditorContext) {
|
||||
cc_access: None,
|
||||
speed_key: "",
|
||||
chain_key: "",
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
};
|
||||
|
||||
|
||||
@@ -258,12 +258,52 @@ impl AudioSettings {
|
||||
self.input_devices = doux::audio::list_input_devices();
|
||||
}
|
||||
|
||||
pub fn next_section(&mut self) {
|
||||
self.section = self.section.next();
|
||||
pub fn next_section(&mut self, plugin_mode: bool) {
|
||||
self.section = if plugin_mode {
|
||||
match self.section {
|
||||
EngineSection::Settings => EngineSection::Samples,
|
||||
EngineSection::Samples => EngineSection::Settings,
|
||||
EngineSection::Devices => EngineSection::Settings,
|
||||
}
|
||||
} else {
|
||||
self.section.next()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn prev_section(&mut self) {
|
||||
self.section = self.section.prev();
|
||||
pub fn prev_section(&mut self, plugin_mode: bool) {
|
||||
self.section = if plugin_mode {
|
||||
match self.section {
|
||||
EngineSection::Settings => EngineSection::Samples,
|
||||
EngineSection::Samples => EngineSection::Settings,
|
||||
EngineSection::Devices => EngineSection::Settings,
|
||||
}
|
||||
} else {
|
||||
self.section.prev()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn next_setting(&mut self, plugin_mode: bool) {
|
||||
self.setting_kind = if plugin_mode {
|
||||
match self.setting_kind {
|
||||
SettingKind::Polyphony => SettingKind::Nudge,
|
||||
SettingKind::Nudge => SettingKind::Polyphony,
|
||||
_ => SettingKind::Polyphony,
|
||||
}
|
||||
} else {
|
||||
self.setting_kind.next()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn prev_setting(&mut self, plugin_mode: bool) {
|
||||
self.setting_kind = if plugin_mode {
|
||||
match self.setting_kind {
|
||||
SettingKind::Polyphony => SettingKind::Nudge,
|
||||
SettingKind::Nudge => SettingKind::Polyphony,
|
||||
_ => SettingKind::Polyphony,
|
||||
}
|
||||
} else {
|
||||
self.setting_kind.prev()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn current_output_device_index(&self) -> usize {
|
||||
|
||||
@@ -46,24 +46,31 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
};
|
||||
|
||||
// Calculate section heights
|
||||
let devices_lines = devices_section_height(app) as usize;
|
||||
let settings_lines: usize = 8; // header(1) + divider(1) + 6 rows
|
||||
let plugin_mode = app.plugin_mode;
|
||||
let devices_lines = if plugin_mode {
|
||||
0
|
||||
} else {
|
||||
devices_section_height(app) as usize
|
||||
};
|
||||
let settings_lines: usize = if plugin_mode { 5 } else { 8 }; // plugin: header(1) + divider(1) + 3 rows
|
||||
let samples_lines: usize = 6; // header(1) + divider(1) + content(3) + hint(1)
|
||||
let total_lines = devices_lines + 1 + settings_lines + 1 + samples_lines;
|
||||
|
||||
let sections_gap = if plugin_mode { 1 } else { 2 }; // 1 gap without devices, 2 gaps with
|
||||
let total_lines = devices_lines + settings_lines + samples_lines + sections_gap;
|
||||
|
||||
let max_visible = padded.height as usize;
|
||||
|
||||
// Calculate scroll offset based on focused section
|
||||
let settings_start = if plugin_mode { 0 } else { devices_lines + 1 };
|
||||
let (focus_start, focus_height) = match app.audio.section {
|
||||
EngineSection::Devices => (0, devices_lines),
|
||||
EngineSection::Settings => (devices_lines + 1, settings_lines),
|
||||
EngineSection::Samples => (devices_lines + 1 + settings_lines + 1, samples_lines),
|
||||
EngineSection::Settings => (settings_start, settings_lines),
|
||||
EngineSection::Samples => (settings_start + settings_lines + 1, samples_lines),
|
||||
};
|
||||
|
||||
let scroll_offset = if total_lines <= max_visible {
|
||||
0
|
||||
} else {
|
||||
// Keep focused section in view (top-aligned when possible)
|
||||
let focus_end = focus_start + focus_height;
|
||||
if focus_end <= max_visible {
|
||||
0
|
||||
@@ -75,25 +82,26 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let viewport_top = padded.y as i32;
|
||||
let viewport_bottom = (padded.y + padded.height) as i32;
|
||||
|
||||
// Render each section at adjusted position
|
||||
let mut y = viewport_top - scroll_offset as i32;
|
||||
|
||||
// Devices section
|
||||
let devices_top = y;
|
||||
let devices_bottom = y + devices_lines as i32;
|
||||
if devices_bottom > viewport_top && devices_top < viewport_bottom {
|
||||
let clipped_y = devices_top.max(viewport_top) as u16;
|
||||
let clipped_height =
|
||||
(devices_bottom.min(viewport_bottom) - devices_top.max(viewport_top)) as u16;
|
||||
let devices_area = Rect {
|
||||
x: padded.x,
|
||||
y: clipped_y,
|
||||
width: padded.width,
|
||||
height: clipped_height,
|
||||
};
|
||||
render_devices(frame, app, devices_area);
|
||||
// Devices section (skip in plugin mode)
|
||||
if !plugin_mode {
|
||||
let devices_top = y;
|
||||
let devices_bottom = y + devices_lines as i32;
|
||||
if devices_bottom > viewport_top && devices_top < viewport_bottom {
|
||||
let clipped_y = devices_top.max(viewport_top) as u16;
|
||||
let clipped_height =
|
||||
(devices_bottom.min(viewport_bottom) - devices_top.max(viewport_top)) as u16;
|
||||
let devices_area = Rect {
|
||||
x: padded.x,
|
||||
y: clipped_y,
|
||||
width: padded.width,
|
||||
height: clipped_height,
|
||||
};
|
||||
render_devices(frame, app, devices_area);
|
||||
}
|
||||
y += devices_lines as i32 + 1;
|
||||
}
|
||||
y += devices_lines as i32 + 1; // +1 for blank line
|
||||
|
||||
// Settings section
|
||||
let settings_top = y;
|
||||
@@ -310,8 +318,6 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let label_style = Style::new().fg(theme.engine.label);
|
||||
let value_style = Style::new().fg(theme.engine.value);
|
||||
|
||||
let channels_focused = section_focused && app.audio.setting_kind == SettingKind::Channels;
|
||||
let buffer_focused = section_focused && app.audio.setting_kind == SettingKind::BufferSize;
|
||||
let polyphony_focused = section_focused && app.audio.setting_kind == SettingKind::Polyphony;
|
||||
let nudge_focused = section_focused && app.audio.setting_kind == SettingKind::Nudge;
|
||||
let nudge_ms = app.metrics.nudge_ms;
|
||||
@@ -321,8 +327,15 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
format!("{nudge_ms:+.1} ms")
|
||||
};
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
let mut rows = Vec::new();
|
||||
|
||||
if !app.plugin_mode {
|
||||
let channels_focused =
|
||||
section_focused && app.audio.setting_kind == SettingKind::Channels;
|
||||
let buffer_focused =
|
||||
section_focused && app.audio.setting_kind == SettingKind::BufferSize;
|
||||
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(
|
||||
if channels_focused {
|
||||
"> Channels"
|
||||
@@ -337,8 +350,8 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
]));
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(
|
||||
if buffer_focused {
|
||||
"> Buffer"
|
||||
@@ -357,38 +370,42 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(
|
||||
if polyphony_focused {
|
||||
"> Voices"
|
||||
} else {
|
||||
" Voices"
|
||||
},
|
||||
label_style,
|
||||
),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.max_voices),
|
||||
polyphony_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(
|
||||
if nudge_focused { "> Nudge" } else { " Nudge" },
|
||||
label_style,
|
||||
),
|
||||
render_selector(&nudge_label, nudge_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(" Sample rate", label_style),
|
||||
Span::styled(
|
||||
format!("{:.0} Hz", app.audio.config.sample_rate),
|
||||
value_style,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
]));
|
||||
}
|
||||
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(
|
||||
if polyphony_focused {
|
||||
"> Voices"
|
||||
} else {
|
||||
" Voices"
|
||||
},
|
||||
label_style,
|
||||
),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.max_voices),
|
||||
polyphony_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]));
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(
|
||||
if nudge_focused { "> Nudge" } else { " Nudge" },
|
||||
label_style,
|
||||
),
|
||||
render_selector(&nudge_label, nudge_focused, highlight, normal),
|
||||
]));
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(" Sample rate", label_style),
|
||||
Span::styled(
|
||||
format!("{:.0} Hz", app.audio.config.sample_rate),
|
||||
value_style,
|
||||
),
|
||||
]));
|
||||
|
||||
if !app.plugin_mode {
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(" Audio host", label_style),
|
||||
Span::styled(
|
||||
if app.audio.config.host_name.is_empty() {
|
||||
@@ -398,8 +415,8 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
},
|
||||
value_style,
|
||||
),
|
||||
]),
|
||||
];
|
||||
]));
|
||||
}
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(14), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use crate::page::Page;
|
||||
|
||||
pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
let mut bindings = vec![
|
||||
("F1–F6", "Go to view", "Dict/Patterns/Options/Help/Sequencer/Engine"),
|
||||
("Ctrl+←→↑↓", "Navigate", "Switch between adjacent views"),
|
||||
("q", "Quit", "Quit application"),
|
||||
];
|
||||
if !plugin_mode {
|
||||
bindings.push(("q", "Quit", "Quit application"));
|
||||
}
|
||||
bindings.extend([
|
||||
("s", "Save", "Save project"),
|
||||
("l", "Load", "Load project"),
|
||||
("?", "Keybindings", "Show this help"),
|
||||
];
|
||||
]);
|
||||
|
||||
// Page-specific bindings
|
||||
match page {
|
||||
|
||||
@@ -78,7 +78,7 @@ pub fn render(
|
||||
frame.render_widget(Block::new().style(Style::default().bg(bg_color)), term);
|
||||
|
||||
if app.ui.show_title {
|
||||
title_view::render(frame, term, &app.ui);
|
||||
title_view::render(frame, term, &app.ui, app.plugin_mode);
|
||||
|
||||
let mut fx = app.ui.title_fx.borrow_mut();
|
||||
if let Some(effect) = fx.as_mut() {
|
||||
@@ -1036,7 +1036,7 @@ fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: R
|
||||
.border_color(theme.modal.editor)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let bindings = super::keybindings::bindings_for(app.page);
|
||||
let bindings = super::keybindings::bindings_for(app.page, app.plugin_mode);
|
||||
let visible_rows = inner.height.saturating_sub(2) as usize;
|
||||
|
||||
let rows: Vec<Row> = bindings
|
||||
|
||||
@@ -8,7 +8,7 @@ use tui_big_text::{BigText, PixelSize};
|
||||
use crate::state::ui::UiState;
|
||||
use crate::theme;
|
||||
|
||||
pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
|
||||
pub fn render(frame: &mut Frame, area: Rect, ui: &UiState, plugin_mode: bool) {
|
||||
let theme = theme::get();
|
||||
frame.render_widget(&ui.sparkles, area);
|
||||
|
||||
@@ -39,15 +39,17 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
|
||||
Line::from(Span::styled("AGPL-3.0", license_style)),
|
||||
];
|
||||
|
||||
let keybindings = [
|
||||
let mut keybindings = vec![
|
||||
("Ctrl+Arrows", "Navigate Views"),
|
||||
("Enter", "Edit Step"),
|
||||
("Space", "Play/Stop"),
|
||||
("s", "Save"),
|
||||
("l", "Load"),
|
||||
("q", "Quit"),
|
||||
("?", "Keybindings"),
|
||||
];
|
||||
if !plugin_mode {
|
||||
keybindings.push(("q", "Quit"));
|
||||
}
|
||||
keybindings.push(("?", "Keybindings"));
|
||||
|
||||
let key_style = Style::new().fg(theme.modal.confirm);
|
||||
let desc_style = Style::new().fg(theme.ui.text_primary);
|
||||
|
||||
Reference in New Issue
Block a user