WIP: clap

This commit is contained in:
2026-02-20 22:14:21 +01:00
parent 12752e0167
commit 00d6eb2f1f
76 changed files with 9103 additions and 143 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ pub struct InitArgs {
pub buffer: Option<u32>,
}
#[allow(dead_code)]
pub struct Init {
pub app: App,
pub link: Arc<LinkState>,

View File

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

View File

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

View File

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

View File

@@ -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') => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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![
("F1F6", "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 {

View File

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

View File

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