ok
This commit is contained in:
@@ -74,7 +74,7 @@ impl<'a> ListSelect<'a> {
|
||||
normal_style
|
||||
};
|
||||
|
||||
let prefix = if is_selected { "● " } else { " " };
|
||||
let prefix = if is_selected { "x " } else { " " };
|
||||
let mut spans = vec![
|
||||
Span::styled(prefix.to_string(), style),
|
||||
Span::styled(name.clone(), style),
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::services::pattern_editor;
|
||||
use crate::settings::Settings;
|
||||
use crate::state::{
|
||||
AudioSettings, DictFocus, EditorContext, FlashKind, Focus, LiveKeyState, Metrics, Modal,
|
||||
PanelState, PatternField, PatternsNav, PlaybackState, ProjectState, UiState,
|
||||
OptionsState, PanelState, PatternField, PatternsNav, PlaybackState, ProjectState, UiState,
|
||||
};
|
||||
use crate::views::{dict_view, help_view};
|
||||
|
||||
@@ -43,6 +43,7 @@ pub struct App {
|
||||
pub copied_bank: Option<Bank>,
|
||||
|
||||
pub audio: AudioSettings,
|
||||
pub options: OptionsState,
|
||||
pub panel: PanelState,
|
||||
}
|
||||
|
||||
@@ -75,6 +76,7 @@ impl App {
|
||||
copied_bank: None,
|
||||
|
||||
audio: AudioSettings::default(),
|
||||
options: OptionsState::default(),
|
||||
panel: PanelState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
264
src/input.rs
264
src/input.rs
@@ -9,7 +9,7 @@ use crate::commands::AppCommand;
|
||||
use crate::engine::{AudioCommand, LinkState, SequencerSnapshot};
|
||||
use crate::model::PatternSpeed;
|
||||
use crate::page::Page;
|
||||
use crate::state::{AudioFocus, Modal, PanelFocus, PatternField, SampleBrowserState, SidePanel};
|
||||
use crate::state::{DeviceKind, EngineSection, Modal, OptionsFocus, PanelFocus, PatternField, SampleBrowserState, SettingKind, SidePanel};
|
||||
|
||||
pub enum InputResult {
|
||||
Continue,
|
||||
@@ -453,7 +453,8 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match ctx.app.page {
|
||||
Page::Main => handle_main_page(ctx, key, ctrl),
|
||||
Page::Patterns => handle_patterns_page(ctx, key),
|
||||
Page::Audio => handle_audio_page(ctx, key),
|
||||
Page::Engine => handle_engine_page(ctx, key),
|
||||
Page::Options => handle_options_page(ctx, key),
|
||||
Page::Help => handle_help_page(ctx, key),
|
||||
Page::Dict => handle_dict_page(ctx, key),
|
||||
}
|
||||
@@ -719,132 +720,113 @@ fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Tab => ctx.app.audio.next_focus(),
|
||||
KeyCode::BackTab => ctx.app.audio.prev_focus(),
|
||||
KeyCode::Up => match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice => ctx.app.audio.output_list.move_up(),
|
||||
AudioFocus::InputDevice => ctx.app.audio.input_list.move_up(),
|
||||
_ => {}
|
||||
KeyCode::Tab => ctx.app.audio.next_section(),
|
||||
KeyCode::BackTab => ctx.app.audio.prev_section(),
|
||||
KeyCode::Up => match ctx.app.audio.section {
|
||||
EngineSection::Devices => match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => ctx.app.audio.output_list.move_up(),
|
||||
DeviceKind::Input => ctx.app.audio.input_list.move_up(),
|
||||
},
|
||||
EngineSection::Settings => {
|
||||
ctx.app.audio.setting_kind = ctx.app.audio.setting_kind.prev();
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
},
|
||||
KeyCode::Down => match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
ctx.app.audio.output_list.move_down(count);
|
||||
KeyCode::Down => match ctx.app.audio.section {
|
||||
EngineSection::Devices => match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
ctx.app.audio.output_list.move_down(count);
|
||||
}
|
||||
DeviceKind::Input => {
|
||||
let count = ctx.app.audio.input_devices.len();
|
||||
ctx.app.audio.input_list.move_down(count);
|
||||
}
|
||||
},
|
||||
EngineSection::Settings => {
|
||||
ctx.app.audio.setting_kind = ctx.app.audio.setting_kind.next();
|
||||
}
|
||||
AudioFocus::InputDevice => {
|
||||
let count = ctx.app.audio.input_devices.len();
|
||||
ctx.app.audio.input_list.move_down(count);
|
||||
}
|
||||
_ => {}
|
||||
EngineSection::Samples => {}
|
||||
},
|
||||
KeyCode::PageUp => match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice => ctx.app.audio.output_list.page_up(),
|
||||
AudioFocus::InputDevice => ctx.app.audio.input_list.page_up(),
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::PageDown => match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
ctx.app.audio.output_list.page_down(count);
|
||||
}
|
||||
AudioFocus::InputDevice => {
|
||||
let count = ctx.app.audio.input_devices.len();
|
||||
ctx.app.audio.input_list.page_down(count);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Enter => match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice => {
|
||||
let cursor = ctx.app.audio.output_list.cursor;
|
||||
if cursor < ctx.app.audio.output_devices.len() {
|
||||
ctx.app.audio.config.output_device =
|
||||
Some(ctx.app.audio.output_devices[cursor].name.clone());
|
||||
ctx.app.save_settings(ctx.link);
|
||||
KeyCode::PageUp => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => ctx.app.audio.output_list.page_up(),
|
||||
DeviceKind::Input => ctx.app.audio.input_list.page_up(),
|
||||
}
|
||||
}
|
||||
AudioFocus::InputDevice => {
|
||||
let cursor = ctx.app.audio.input_list.cursor;
|
||||
if cursor < ctx.app.audio.input_devices.len() {
|
||||
ctx.app.audio.config.input_device =
|
||||
Some(ctx.app.audio.input_devices[cursor].name.clone());
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Left => {
|
||||
match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice | AudioFocus::InputDevice => {}
|
||||
AudioFocus::Channels => ctx.app.audio.adjust_channels(-1),
|
||||
AudioFocus::BufferSize => ctx.app.audio.adjust_buffer_size(-64),
|
||||
AudioFocus::Polyphony => ctx.app.audio.adjust_max_voices(-1),
|
||||
AudioFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(),
|
||||
AudioFocus::RuntimeHighlight => {
|
||||
ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight
|
||||
}
|
||||
AudioFocus::ShowScope => {
|
||||
ctx.app.audio.config.show_scope = !ctx.app.audio.config.show_scope;
|
||||
}
|
||||
AudioFocus::ShowSpectrum => {
|
||||
ctx.app.audio.config.show_spectrum = !ctx.app.audio.config.show_spectrum;
|
||||
}
|
||||
AudioFocus::ShowCompletion => {
|
||||
ctx.app.ui.show_completion = !ctx.app.ui.show_completion;
|
||||
}
|
||||
AudioFocus::SamplePaths => ctx.app.audio.remove_last_sample_path(),
|
||||
AudioFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
AudioFocus::StartStopSync => ctx
|
||||
.link
|
||||
.set_start_stop_sync_enabled(!ctx.link.is_start_stop_sync_enabled()),
|
||||
AudioFocus::Quantum => ctx.link.set_quantum(ctx.link.quantum() - 1.0),
|
||||
}
|
||||
if !matches!(
|
||||
ctx.app.audio.focus,
|
||||
AudioFocus::SamplePaths | AudioFocus::OutputDevice | AudioFocus::InputDevice
|
||||
) {
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
match ctx.app.audio.focus {
|
||||
AudioFocus::OutputDevice | AudioFocus::InputDevice => {}
|
||||
AudioFocus::Channels => ctx.app.audio.adjust_channels(1),
|
||||
AudioFocus::BufferSize => ctx.app.audio.adjust_buffer_size(64),
|
||||
AudioFocus::Polyphony => ctx.app.audio.adjust_max_voices(1),
|
||||
AudioFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(),
|
||||
AudioFocus::RuntimeHighlight => {
|
||||
ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight
|
||||
KeyCode::PageDown => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
ctx.app.audio.output_list.page_down(count);
|
||||
}
|
||||
DeviceKind::Input => {
|
||||
let count = ctx.app.audio.input_devices.len();
|
||||
ctx.app.audio.input_list.page_down(count);
|
||||
}
|
||||
}
|
||||
AudioFocus::ShowScope => {
|
||||
ctx.app.audio.config.show_scope = !ctx.app.audio.config.show_scope;
|
||||
}
|
||||
AudioFocus::ShowSpectrum => {
|
||||
ctx.app.audio.config.show_spectrum = !ctx.app.audio.config.show_spectrum;
|
||||
}
|
||||
AudioFocus::ShowCompletion => {
|
||||
ctx.app.ui.show_completion = !ctx.app.ui.show_completion;
|
||||
}
|
||||
AudioFocus::SamplePaths => {}
|
||||
AudioFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
AudioFocus::StartStopSync => ctx
|
||||
.link
|
||||
.set_start_stop_sync_enabled(!ctx.link.is_start_stop_sync_enabled()),
|
||||
AudioFocus::Quantum => ctx.link.set_quantum(ctx.link.quantum() + 1.0),
|
||||
}
|
||||
if !matches!(
|
||||
ctx.app.audio.focus,
|
||||
AudioFocus::SamplePaths | AudioFocus::OutputDevice | AudioFocus::InputDevice
|
||||
) {
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let cursor = ctx.app.audio.output_list.cursor;
|
||||
if cursor < ctx.app.audio.output_devices.len() {
|
||||
ctx.app.audio.config.output_device =
|
||||
Some(ctx.app.audio.output_devices[cursor].name.clone());
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
}
|
||||
DeviceKind::Input => {
|
||||
let cursor = ctx.app.audio.input_list.cursor;
|
||||
if cursor < ctx.app.audio.input_devices.len() {
|
||||
ctx.app.audio.config.input_device =
|
||||
Some(ctx.app.audio.input_devices[cursor].name.clone());
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Left => match ctx.app.audio.section {
|
||||
EngineSection::Devices => {
|
||||
ctx.app.audio.device_kind = DeviceKind::Output;
|
||||
}
|
||||
EngineSection::Settings => {
|
||||
match ctx.app.audio.setting_kind {
|
||||
SettingKind::Channels => ctx.app.audio.adjust_channels(-1),
|
||||
SettingKind::BufferSize => ctx.app.audio.adjust_buffer_size(-64),
|
||||
SettingKind::Polyphony => ctx.app.audio.adjust_max_voices(-1),
|
||||
}
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
},
|
||||
KeyCode::Right => match ctx.app.audio.section {
|
||||
EngineSection::Devices => {
|
||||
ctx.app.audio.device_kind = DeviceKind::Input;
|
||||
}
|
||||
EngineSection::Settings => {
|
||||
match ctx.app.audio.setting_kind {
|
||||
SettingKind::Channels => ctx.app.audio.adjust_channels(1),
|
||||
SettingKind::BufferSize => ctx.app.audio.adjust_buffer_size(64),
|
||||
SettingKind::Polyphony => ctx.app.audio.adjust_max_voices(1),
|
||||
}
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
},
|
||||
KeyCode::Char('R') => ctx.app.audio.trigger_restart(),
|
||||
KeyCode::Char('A') => {
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
@@ -852,12 +834,16 @@ fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::AddSamplePath(state)));
|
||||
}
|
||||
KeyCode::Char('D') => {
|
||||
ctx.app.audio.refresh_devices();
|
||||
let out_count = ctx.app.audio.output_devices.len();
|
||||
let in_count = ctx.app.audio.input_devices.len();
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"Found {out_count} output, {in_count} input devices"
|
||||
)));
|
||||
if ctx.app.audio.section == EngineSection::Samples {
|
||||
ctx.app.audio.remove_last_sample_path();
|
||||
} else {
|
||||
ctx.app.audio.refresh_devices();
|
||||
let out_count = ctx.app.audio.output_devices.len();
|
||||
let in_count = ctx.app.audio.input_devices.len();
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"Found {out_count} output, {in_count} input devices"
|
||||
)));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
let _ = ctx.audio_tx.send(AudioCommand::Hush);
|
||||
@@ -871,6 +857,46 @@ fn handle_audio_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
.audio_tx
|
||||
.send(AudioCommand::Evaluate("/sound/sine/dur/0.5/decay/0.2".into()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::ConfirmQuit {
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Down | KeyCode::Tab => ctx.app.options.next_focus(),
|
||||
KeyCode::Up | KeyCode::BackTab => ctx.app.options.prev_focus(),
|
||||
KeyCode::Left | KeyCode::Right => {
|
||||
match ctx.app.options.focus {
|
||||
OptionsFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(),
|
||||
OptionsFocus::RuntimeHighlight => {
|
||||
ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight
|
||||
}
|
||||
OptionsFocus::ShowScope => {
|
||||
ctx.app.audio.config.show_scope = !ctx.app.audio.config.show_scope
|
||||
}
|
||||
OptionsFocus::ShowSpectrum => {
|
||||
ctx.app.audio.config.show_spectrum = !ctx.app.audio.config.show_spectrum
|
||||
}
|
||||
OptionsFocus::ShowCompletion => {
|
||||
ctx.app.ui.show_completion = !ctx.app.ui.show_completion
|
||||
}
|
||||
OptionsFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
OptionsFocus::StartStopSync => ctx
|
||||
.link
|
||||
.set_start_stop_sync_enabled(!ctx.link.is_start_stop_sync_enabled()),
|
||||
OptionsFocus::Quantum => {
|
||||
let delta = if key.code == KeyCode::Left { -1.0 } else { 1.0 };
|
||||
ctx.link.set_quantum(ctx.link.quantum() + delta);
|
||||
}
|
||||
}
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
|
||||
16
src/page.rs
16
src/page.rs
@@ -3,9 +3,10 @@ pub enum Page {
|
||||
#[default]
|
||||
Main,
|
||||
Patterns,
|
||||
Audio,
|
||||
Engine,
|
||||
Help,
|
||||
Dict,
|
||||
Options,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
@@ -13,9 +14,10 @@ impl Page {
|
||||
pub const ALL: &'static [Page] = &[
|
||||
Page::Main,
|
||||
Page::Patterns,
|
||||
Page::Audio,
|
||||
Page::Engine,
|
||||
Page::Help,
|
||||
Page::Dict,
|
||||
Page::Options,
|
||||
];
|
||||
|
||||
/// Grid dimensions (cols, rows)
|
||||
@@ -24,14 +26,15 @@ impl Page {
|
||||
/// Grid position (col, row) for each page
|
||||
/// Layout:
|
||||
/// col 0 col 1 col 2
|
||||
/// row 0 Patterns Help
|
||||
/// row 1 Dict Sequencer Audio
|
||||
/// row 0 Options Patterns Help
|
||||
/// row 1 Dict Sequencer Engine
|
||||
pub const fn grid_pos(self) -> (i8, i8) {
|
||||
match self {
|
||||
Page::Options => (0, 0),
|
||||
Page::Dict => (0, 1),
|
||||
Page::Main => (1, 1),
|
||||
Page::Patterns => (1, 0),
|
||||
Page::Audio => (2, 1),
|
||||
Page::Engine => (2, 1),
|
||||
Page::Help => (2, 0),
|
||||
}
|
||||
}
|
||||
@@ -46,9 +49,10 @@ impl Page {
|
||||
match self {
|
||||
Page::Main => "Sequencer",
|
||||
Page::Patterns => "Patterns",
|
||||
Page::Audio => "Audio",
|
||||
Page::Engine => "Engine",
|
||||
Page::Help => "Help",
|
||||
Page::Dict => "Dict",
|
||||
Page::Options => "Options",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,22 +119,44 @@ impl ListSelectState {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum AudioFocus {
|
||||
pub enum EngineSection {
|
||||
#[default]
|
||||
Devices,
|
||||
Settings,
|
||||
Samples,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum DeviceKind {
|
||||
#[default]
|
||||
Output,
|
||||
Input,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum SettingKind {
|
||||
#[default]
|
||||
OutputDevice,
|
||||
InputDevice,
|
||||
Channels,
|
||||
BufferSize,
|
||||
Polyphony,
|
||||
RefreshRate,
|
||||
RuntimeHighlight,
|
||||
ShowScope,
|
||||
ShowSpectrum,
|
||||
ShowCompletion,
|
||||
SamplePaths,
|
||||
LinkEnabled,
|
||||
StartStopSync,
|
||||
Quantum,
|
||||
}
|
||||
|
||||
impl SettingKind {
|
||||
pub fn next(self) -> Self {
|
||||
match self {
|
||||
Self::Channels => Self::BufferSize,
|
||||
Self::BufferSize => Self::Polyphony,
|
||||
Self::Polyphony => Self::Channels,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev(self) -> Self {
|
||||
match self {
|
||||
Self::Channels => Self::Polyphony,
|
||||
Self::BufferSize => Self::Channels,
|
||||
Self::Polyphony => Self::BufferSize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Metrics {
|
||||
@@ -169,7 +191,9 @@ impl Default for Metrics {
|
||||
|
||||
pub struct AudioSettings {
|
||||
pub config: AudioConfig,
|
||||
pub focus: AudioFocus,
|
||||
pub section: EngineSection,
|
||||
pub device_kind: DeviceKind,
|
||||
pub setting_kind: SettingKind,
|
||||
pub output_devices: Vec<AudioDeviceInfo>,
|
||||
pub input_devices: Vec<AudioDeviceInfo>,
|
||||
pub output_list: ListSelectState,
|
||||
@@ -182,7 +206,9 @@ impl Default for AudioSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: AudioConfig::default(),
|
||||
focus: AudioFocus::default(),
|
||||
section: EngineSection::default(),
|
||||
device_kind: DeviceKind::default(),
|
||||
setting_kind: SettingKind::default(),
|
||||
output_devices: doux::audio::list_output_devices(),
|
||||
input_devices: doux::audio::list_input_devices(),
|
||||
output_list: ListSelectState { cursor: 0, scroll_offset: 0 },
|
||||
@@ -199,41 +225,19 @@ impl AudioSettings {
|
||||
self.input_devices = doux::audio::list_input_devices();
|
||||
}
|
||||
|
||||
pub fn next_focus(&mut self) {
|
||||
self.focus = match self.focus {
|
||||
AudioFocus::OutputDevice => AudioFocus::InputDevice,
|
||||
AudioFocus::InputDevice => AudioFocus::Channels,
|
||||
AudioFocus::Channels => AudioFocus::BufferSize,
|
||||
AudioFocus::BufferSize => AudioFocus::Polyphony,
|
||||
AudioFocus::Polyphony => AudioFocus::RefreshRate,
|
||||
AudioFocus::RefreshRate => AudioFocus::RuntimeHighlight,
|
||||
AudioFocus::RuntimeHighlight => AudioFocus::ShowScope,
|
||||
AudioFocus::ShowScope => AudioFocus::ShowSpectrum,
|
||||
AudioFocus::ShowSpectrum => AudioFocus::ShowCompletion,
|
||||
AudioFocus::ShowCompletion => AudioFocus::SamplePaths,
|
||||
AudioFocus::SamplePaths => AudioFocus::LinkEnabled,
|
||||
AudioFocus::LinkEnabled => AudioFocus::StartStopSync,
|
||||
AudioFocus::StartStopSync => AudioFocus::Quantum,
|
||||
AudioFocus::Quantum => AudioFocus::OutputDevice,
|
||||
pub fn next_section(&mut self) {
|
||||
self.section = match self.section {
|
||||
EngineSection::Devices => EngineSection::Settings,
|
||||
EngineSection::Settings => EngineSection::Samples,
|
||||
EngineSection::Samples => EngineSection::Devices,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn prev_focus(&mut self) {
|
||||
self.focus = match self.focus {
|
||||
AudioFocus::OutputDevice => AudioFocus::Quantum,
|
||||
AudioFocus::InputDevice => AudioFocus::OutputDevice,
|
||||
AudioFocus::Channels => AudioFocus::InputDevice,
|
||||
AudioFocus::BufferSize => AudioFocus::Channels,
|
||||
AudioFocus::Polyphony => AudioFocus::BufferSize,
|
||||
AudioFocus::RefreshRate => AudioFocus::Polyphony,
|
||||
AudioFocus::RuntimeHighlight => AudioFocus::RefreshRate,
|
||||
AudioFocus::ShowScope => AudioFocus::RuntimeHighlight,
|
||||
AudioFocus::ShowSpectrum => AudioFocus::ShowScope,
|
||||
AudioFocus::ShowCompletion => AudioFocus::ShowSpectrum,
|
||||
AudioFocus::SamplePaths => AudioFocus::ShowCompletion,
|
||||
AudioFocus::LinkEnabled => AudioFocus::SamplePaths,
|
||||
AudioFocus::StartStopSync => AudioFocus::LinkEnabled,
|
||||
AudioFocus::Quantum => AudioFocus::StartStopSync,
|
||||
pub fn prev_section(&mut self) {
|
||||
self.section = match self.section {
|
||||
EngineSection::Devices => EngineSection::Samples,
|
||||
EngineSection::Settings => EngineSection::Devices,
|
||||
EngineSection::Samples => EngineSection::Settings,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod editor;
|
||||
pub mod file_browser;
|
||||
pub mod live_keys;
|
||||
pub mod modal;
|
||||
pub mod options;
|
||||
pub mod panel;
|
||||
pub mod patterns_nav;
|
||||
pub mod playback;
|
||||
@@ -10,7 +11,8 @@ pub mod project;
|
||||
pub mod sample_browser;
|
||||
pub mod ui;
|
||||
|
||||
pub use audio::{AudioFocus, AudioSettings, Metrics};
|
||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind};
|
||||
pub use options::{OptionsFocus, OptionsState};
|
||||
pub use editor::{CopiedStep, EditorContext, Focus, PatternField};
|
||||
pub use live_keys::LiveKeyState;
|
||||
pub use modal::Modal;
|
||||
|
||||
45
src/state/options.rs
Normal file
45
src/state/options.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum OptionsFocus {
|
||||
#[default]
|
||||
RefreshRate,
|
||||
RuntimeHighlight,
|
||||
ShowScope,
|
||||
ShowSpectrum,
|
||||
ShowCompletion,
|
||||
LinkEnabled,
|
||||
StartStopSync,
|
||||
Quantum,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct OptionsState {
|
||||
pub focus: OptionsFocus,
|
||||
}
|
||||
|
||||
impl OptionsState {
|
||||
pub fn next_focus(&mut self) {
|
||||
self.focus = match self.focus {
|
||||
OptionsFocus::RefreshRate => OptionsFocus::RuntimeHighlight,
|
||||
OptionsFocus::RuntimeHighlight => OptionsFocus::ShowScope,
|
||||
OptionsFocus::ShowScope => OptionsFocus::ShowSpectrum,
|
||||
OptionsFocus::ShowSpectrum => OptionsFocus::ShowCompletion,
|
||||
OptionsFocus::ShowCompletion => OptionsFocus::LinkEnabled,
|
||||
OptionsFocus::LinkEnabled => OptionsFocus::StartStopSync,
|
||||
OptionsFocus::StartStopSync => OptionsFocus::Quantum,
|
||||
OptionsFocus::Quantum => OptionsFocus::RefreshRate,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn prev_focus(&mut self) {
|
||||
self.focus = match self.focus {
|
||||
OptionsFocus::RefreshRate => OptionsFocus::Quantum,
|
||||
OptionsFocus::RuntimeHighlight => OptionsFocus::RefreshRate,
|
||||
OptionsFocus::ShowScope => OptionsFocus::RuntimeHighlight,
|
||||
OptionsFocus::ShowSpectrum => OptionsFocus::ShowScope,
|
||||
OptionsFocus::ShowCompletion => OptionsFocus::ShowSpectrum,
|
||||
OptionsFocus::LinkEnabled => OptionsFocus::ShowCompletion,
|
||||
OptionsFocus::StartStopSync => OptionsFocus::LinkEnabled,
|
||||
OptionsFocus::Quantum => OptionsFocus::StartStopSync,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,430 +0,0 @@
|
||||
use cagire_ratatui::ListSelect;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Row, Table};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::LinkState;
|
||||
use crate::state::AudioFocus;
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let [left_col, _, right_col] = Layout::horizontal([
|
||||
Constraint::Percentage(52),
|
||||
Constraint::Length(2),
|
||||
Constraint::Percentage(48),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
render_audio_section(frame, app, left_col);
|
||||
render_link_section(frame, app, link, right_col);
|
||||
}
|
||||
|
||||
fn truncate_name(name: &str, max_len: usize) -> String {
|
||||
if name.len() > max_len {
|
||||
format!("{}...", &name[..max_len.saturating_sub(3)])
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_audio_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Audio ")
|
||||
.border_style(Style::new().fg(Color::Magenta));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let padded = Rect {
|
||||
x: inner.x + 1,
|
||||
y: inner.y + 1,
|
||||
width: inner.width.saturating_sub(2),
|
||||
height: inner.height.saturating_sub(1),
|
||||
};
|
||||
|
||||
let devices_height = devices_section_height(app);
|
||||
|
||||
let [devices_area, _, settings_area, _, samples_area] = Layout::vertical([
|
||||
Constraint::Length(devices_height),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(3),
|
||||
])
|
||||
.areas(padded);
|
||||
|
||||
render_devices(frame, app, devices_area);
|
||||
render_settings(frame, app, settings_area);
|
||||
render_samples(frame, app, samples_area);
|
||||
}
|
||||
|
||||
fn list_height(item_count: usize) -> u16 {
|
||||
let visible = item_count.min(5) as u16;
|
||||
if item_count > 5 { visible + 1 } else { visible }
|
||||
}
|
||||
|
||||
fn devices_section_height(app: &App) -> u16 {
|
||||
// header(1) + "Output" label(1) + output list + "Input" label(1) + input list
|
||||
1 + 1 + list_height(app.audio.output_devices.len())
|
||||
+ 1 + list_height(app.audio.input_devices.len())
|
||||
}
|
||||
|
||||
fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
|
||||
let output_h = list_height(app.audio.output_devices.len());
|
||||
let input_h = list_height(app.audio.input_devices.len());
|
||||
|
||||
let [header_area, output_label_area, output_list_area, input_label_area, input_list_area] =
|
||||
Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(output_h),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(input_h),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Devices").style(header_style), header_area);
|
||||
frame.render_widget(Paragraph::new(Span::styled("Output", label_style)), output_label_area);
|
||||
frame.render_widget(Paragraph::new(Span::styled("Input", label_style)), input_label_area);
|
||||
|
||||
let output_items: Vec<String> = app.audio.output_devices.iter()
|
||||
.map(|d| truncate_name(&d.name, 35))
|
||||
.collect();
|
||||
let output_selected = app.audio.current_output_device_index();
|
||||
ListSelect::new(&output_items, output_selected, app.audio.output_list.cursor)
|
||||
.focused(app.audio.focus == AudioFocus::OutputDevice)
|
||||
.scroll_offset(app.audio.output_list.scroll_offset)
|
||||
.render(frame, output_list_area);
|
||||
|
||||
let input_items: Vec<String> = app.audio.input_devices.iter()
|
||||
.map(|d| truncate_name(&d.name, 35))
|
||||
.collect();
|
||||
let input_selected = app.audio.current_input_device_index();
|
||||
ListSelect::new(&input_items, input_selected, app.audio.input_list.cursor)
|
||||
.focused(app.audio.focus == AudioFocus::InputDevice)
|
||||
.scroll_offset(app.audio.input_list.scroll_offset)
|
||||
.render(frame, input_list_area);
|
||||
}
|
||||
|
||||
fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Settings").style(header_style), header_area);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let normal = Style::new().fg(Color::White);
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
let value_style = Style::new().fg(Color::Rgb(180, 180, 190));
|
||||
|
||||
let channels_focused = app.audio.focus == AudioFocus::Channels;
|
||||
let buffer_focused = app.audio.focus == AudioFocus::BufferSize;
|
||||
let polyphony_focused = app.audio.focus == AudioFocus::Polyphony;
|
||||
let fps_focused = app.audio.focus == AudioFocus::RefreshRate;
|
||||
let highlight_focused = app.audio.focus == AudioFocus::RuntimeHighlight;
|
||||
let scope_focused = app.audio.focus == AudioFocus::ShowScope;
|
||||
let spectrum_focused = app.audio.focus == AudioFocus::ShowSpectrum;
|
||||
let completion_focused = app.audio.focus == AudioFocus::ShowCompletion;
|
||||
|
||||
let highlight_text = if app.ui.runtime_highlight { "On" } else { "Off" };
|
||||
let scope_text = if app.audio.config.show_scope { "On" } else { "Off" };
|
||||
let spectrum_text = if app.audio.config.show_spectrum { "On" } else { "Off" };
|
||||
let completion_text = if app.ui.show_completion { "On" } else { "Off" };
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
Span::styled("Output channels", label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.channels),
|
||||
channels_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Buffer size", label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.buffer_size),
|
||||
buffer_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Max voices", label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.max_voices),
|
||||
polyphony_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Refresh rate", label_style),
|
||||
render_selector(
|
||||
app.audio.config.refresh_rate.label(),
|
||||
fps_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Show highlight", label_style),
|
||||
render_selector(highlight_text, highlight_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Show scope", label_style),
|
||||
render_selector(scope_text, scope_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Show spectrum", label_style),
|
||||
render_selector(spectrum_text, spectrum_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Completion", label_style),
|
||||
render_selector(completion_text, completion_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Sample rate", label_style),
|
||||
Span::styled(
|
||||
format!("{:.0} Hz", app.audio.config.sample_rate),
|
||||
value_style,
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(16), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
}
|
||||
|
||||
fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let samples_focused = app.audio.focus == AudioFocus::SamplePaths;
|
||||
|
||||
let header_text = format!(
|
||||
"Samples {} paths · {} indexed",
|
||||
app.audio.config.sample_paths.len(),
|
||||
app.audio.config.sample_count
|
||||
);
|
||||
|
||||
let header_line = if samples_focused {
|
||||
Line::from(vec![
|
||||
Span::styled("Samples ", header_style),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{} paths · {} indexed",
|
||||
app.audio.config.sample_paths.len(),
|
||||
app.audio.config.sample_count
|
||||
),
|
||||
highlight,
|
||||
),
|
||||
])
|
||||
} else {
|
||||
Line::from(Span::styled(header_text, header_style))
|
||||
};
|
||||
frame.render_widget(Paragraph::new(header_line), header_area);
|
||||
|
||||
let dim = Style::new().fg(Color::Rgb(80, 85, 95));
|
||||
let path_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
for (i, path) in app.audio.config.sample_paths.iter().take(4).enumerate() {
|
||||
let path_str = path.to_string_lossy();
|
||||
let display = truncate_name(&path_str, 45);
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {} ", i + 1), dim),
|
||||
Span::styled(display, path_style),
|
||||
]));
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
lines.push(Line::from(Span::styled(
|
||||
" No sample paths configured",
|
||||
dim,
|
||||
)));
|
||||
}
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), content_area);
|
||||
}
|
||||
|
||||
fn render_link_section(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Ableton Link ")
|
||||
.border_style(Style::new().fg(Color::Cyan));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let padded = Rect {
|
||||
x: inner.x + 1,
|
||||
y: inner.y + 1,
|
||||
width: inner.width.saturating_sub(2),
|
||||
height: inner.height.saturating_sub(1),
|
||||
};
|
||||
|
||||
let [status_area, _, config_area, _, info_area] = Layout::vertical([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.areas(padded);
|
||||
|
||||
render_link_status(frame, link, status_area);
|
||||
render_link_config(frame, app, link, config_area);
|
||||
render_link_info(frame, link, info_area);
|
||||
}
|
||||
|
||||
fn render_link_status(frame: &mut Frame, link: &LinkState, area: Rect) {
|
||||
let enabled = link.is_enabled();
|
||||
let peers = link.peers();
|
||||
|
||||
let (status_text, status_color) = if !enabled {
|
||||
("DISABLED", Color::Rgb(120, 60, 60))
|
||||
} else if peers > 0 {
|
||||
("CONNECTED", Color::Rgb(60, 120, 60))
|
||||
} else {
|
||||
("LISTENING", Color::Rgb(120, 120, 60))
|
||||
};
|
||||
|
||||
let status_style = Style::new().fg(status_color).add_modifier(Modifier::BOLD);
|
||||
|
||||
let peer_text = if enabled {
|
||||
if peers == 0 {
|
||||
"No peers".to_string()
|
||||
} else if peers == 1 {
|
||||
"1 peer".to_string()
|
||||
} else {
|
||||
format!("{peers} peers")
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
Line::from(Span::styled(status_text, status_style)),
|
||||
Line::from(Span::styled(
|
||||
peer_text,
|
||||
Style::new().fg(Color::Rgb(120, 125, 135)),
|
||||
)),
|
||||
];
|
||||
|
||||
frame.render_widget(Paragraph::new(lines).alignment(Alignment::Center), area);
|
||||
}
|
||||
|
||||
fn render_link_config(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new("Configuration").style(header_style),
|
||||
header_area,
|
||||
);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let normal = Style::new().fg(Color::White);
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
|
||||
let enabled_focused = app.audio.focus == AudioFocus::LinkEnabled;
|
||||
let startstop_focused = app.audio.focus == AudioFocus::StartStopSync;
|
||||
let quantum_focused = app.audio.focus == AudioFocus::Quantum;
|
||||
|
||||
let enabled_text = if link.is_enabled() { "On" } else { "Off" };
|
||||
let startstop_text = if link.is_start_stop_sync_enabled() {
|
||||
"On"
|
||||
} else {
|
||||
"Off"
|
||||
};
|
||||
let quantum_text = format!("{:.0}", link.quantum());
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
Span::styled("Enabled", label_style),
|
||||
render_selector(enabled_text, enabled_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Start/Stop", label_style),
|
||||
render_selector(startstop_text, startstop_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Quantum", label_style),
|
||||
render_selector(&quantum_text, quantum_focused, highlight, normal),
|
||||
]),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(10), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
}
|
||||
|
||||
fn render_link_info(frame: &mut Frame, link: &LinkState, area: Rect) {
|
||||
let header_style = Style::new()
|
||||
.fg(Color::Rgb(100, 160, 180))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Session").style(header_style), header_area);
|
||||
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
let value_style = Style::new().fg(Color::Rgb(180, 180, 190));
|
||||
let tempo_style = Style::new()
|
||||
.fg(Color::Rgb(220, 180, 100))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let tempo = link.tempo();
|
||||
let beat = link.beat();
|
||||
let phase = link.phase();
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
Span::styled("Tempo", label_style),
|
||||
Span::styled(format!("{tempo:.1} BPM"), tempo_style),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Beat", label_style),
|
||||
Span::styled(format!("{beat:.2}"), value_style),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled("Phase", label_style),
|
||||
Span::styled(format!("{phase:.2}"), value_style),
|
||||
]),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(10), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
}
|
||||
|
||||
fn render_selector(value: &str, focused: bool, highlight: Style, normal: Style) -> Span<'static> {
|
||||
let style = if focused { highlight } else { normal };
|
||||
if focused {
|
||||
Span::styled(format!("< {value} >"), style)
|
||||
} else {
|
||||
Span::styled(format!(" {value} "), style)
|
||||
}
|
||||
}
|
||||
343
src/views/engine_view.rs
Normal file
343
src/views/engine_view.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
use cagire_ratatui::ListSelect;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Row, Table};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::state::{DeviceKind, EngineSection, SettingKind};
|
||||
use crate::widgets::{Orientation, Scope, Spectrum};
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [left_col, _, right_col] = Layout::horizontal([
|
||||
Constraint::Percentage(55),
|
||||
Constraint::Length(2),
|
||||
Constraint::Percentage(45),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
render_settings_section(frame, app, left_col);
|
||||
render_visualizers(frame, app, right_col);
|
||||
}
|
||||
|
||||
fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Engine ")
|
||||
.border_style(Style::new().fg(Color::Magenta));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let padded = Rect {
|
||||
x: inner.x + 1,
|
||||
y: inner.y + 1,
|
||||
width: inner.width.saturating_sub(2),
|
||||
height: inner.height.saturating_sub(1),
|
||||
};
|
||||
|
||||
let devices_height = devices_section_height(app);
|
||||
|
||||
let [devices_area, _, settings_area, _, samples_area] = Layout::vertical([
|
||||
Constraint::Length(devices_height),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(5),
|
||||
])
|
||||
.areas(padded);
|
||||
|
||||
render_devices(frame, app, devices_area);
|
||||
render_settings(frame, app, settings_area);
|
||||
render_samples(frame, app, samples_area);
|
||||
}
|
||||
|
||||
fn render_visualizers(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [scope_area, _, spectrum_area] = Layout::vertical([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Length(1),
|
||||
Constraint::Percentage(50),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
render_scope(frame, app, scope_area);
|
||||
render_spectrum(frame, app, spectrum_area);
|
||||
}
|
||||
|
||||
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Scope ")
|
||||
.border_style(Style::new().fg(Color::Green));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let scope = Scope::new(&app.metrics.scope)
|
||||
.orientation(Orientation::Horizontal)
|
||||
.color(Color::Green);
|
||||
frame.render_widget(scope, inner);
|
||||
}
|
||||
|
||||
fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Spectrum ")
|
||||
.border_style(Style::new().fg(Color::Cyan));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let spectrum = Spectrum::new(&app.metrics.spectrum);
|
||||
frame.render_widget(spectrum, inner);
|
||||
}
|
||||
|
||||
fn truncate_name(name: &str, max_len: usize) -> String {
|
||||
if name.len() > max_len {
|
||||
format!("{}...", &name[..max_len.saturating_sub(3)])
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn list_height(item_count: usize) -> u16 {
|
||||
let visible = item_count.min(5) as u16;
|
||||
if item_count > 5 { visible + 1 } else { visible }
|
||||
}
|
||||
|
||||
fn devices_section_height(app: &App) -> u16 {
|
||||
let output_h = list_height(app.audio.output_devices.len());
|
||||
let input_h = list_height(app.audio.input_devices.len());
|
||||
2 + output_h.max(input_h)
|
||||
}
|
||||
|
||||
fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let section_focused = app.audio.section == EngineSection::Devices;
|
||||
let header_style = if section_focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 160, 180)).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
let [header_area, content_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Devices").style(header_style), header_area);
|
||||
|
||||
let [output_col, separator, input_col] = Layout::horizontal([
|
||||
Constraint::Percentage(48),
|
||||
Constraint::Length(3),
|
||||
Constraint::Percentage(48),
|
||||
]).areas(content_area);
|
||||
|
||||
let output_focused = section_focused && app.audio.device_kind == DeviceKind::Output;
|
||||
let input_focused = section_focused && app.audio.device_kind == DeviceKind::Input;
|
||||
|
||||
render_device_column(
|
||||
frame, app, output_col,
|
||||
"Output", &app.audio.output_devices,
|
||||
app.audio.current_output_device_index(),
|
||||
app.audio.output_list.cursor,
|
||||
app.audio.output_list.scroll_offset,
|
||||
output_focused,
|
||||
section_focused,
|
||||
);
|
||||
|
||||
let sep_style = Style::new().fg(Color::Rgb(60, 65, 75));
|
||||
let sep_lines: Vec<Line> = (0..separator.height)
|
||||
.map(|_| Line::from(Span::styled("│", sep_style)))
|
||||
.collect();
|
||||
frame.render_widget(Paragraph::new(sep_lines), separator);
|
||||
|
||||
render_device_column(
|
||||
frame, app, input_col,
|
||||
"Input", &app.audio.input_devices,
|
||||
app.audio.current_input_device_index(),
|
||||
app.audio.input_list.cursor,
|
||||
app.audio.input_list.scroll_offset,
|
||||
input_focused,
|
||||
section_focused,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_device_column(
|
||||
frame: &mut Frame,
|
||||
_app: &App,
|
||||
area: Rect,
|
||||
label: &str,
|
||||
devices: &[doux::audio::AudioDeviceInfo],
|
||||
selected_idx: usize,
|
||||
cursor: usize,
|
||||
scroll_offset: usize,
|
||||
focused: bool,
|
||||
section_focused: bool,
|
||||
) {
|
||||
let [label_area, list_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
]).areas(area);
|
||||
|
||||
let label_style = if focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else if section_focused {
|
||||
Style::new().fg(Color::Rgb(150, 155, 165))
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 105, 115))
|
||||
};
|
||||
|
||||
let arrow = if focused { "> " } else { " " };
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!("{arrow}{label}")).style(label_style),
|
||||
label_area,
|
||||
);
|
||||
|
||||
let items: Vec<String> = devices.iter()
|
||||
.map(|d| truncate_name(&d.name, 25))
|
||||
.collect();
|
||||
|
||||
ListSelect::new(&items, selected_idx, cursor)
|
||||
.focused(focused)
|
||||
.scroll_offset(scroll_offset)
|
||||
.render(frame, list_area);
|
||||
}
|
||||
|
||||
fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let section_focused = app.audio.section == EngineSection::Settings;
|
||||
let header_style = if section_focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 160, 180)).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Settings").style(header_style), header_area);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let normal = Style::new().fg(Color::White);
|
||||
let label_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
let value_style = Style::new().fg(Color::Rgb(180, 180, 190));
|
||||
|
||||
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 rows = vec![
|
||||
Row::new(vec![
|
||||
Span::styled(if channels_focused { "> Channels" } else { " Channels" }, label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.channels),
|
||||
channels_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(if buffer_focused { "> Buffer" } else { " Buffer" }, label_style),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.buffer_size),
|
||||
buffer_focused,
|
||||
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(" Sample rate", label_style),
|
||||
Span::styled(
|
||||
format!("{:.0} Hz", app.audio.config.sample_rate),
|
||||
value_style,
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(14), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
}
|
||||
|
||||
fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let section_focused = app.audio.section == EngineSection::Samples;
|
||||
let header_style = if section_focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 160, 180)).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
let [header_area, content_area, _, hint_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let path_count = app.audio.config.sample_paths.len();
|
||||
let sample_count = app.audio.config.sample_count;
|
||||
let header_text = format!("Samples {path_count} paths · {sample_count} indexed");
|
||||
frame.render_widget(Paragraph::new(header_text).style(header_style), header_area);
|
||||
|
||||
let dim = Style::new().fg(Color::Rgb(80, 85, 95));
|
||||
let path_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
if app.audio.config.sample_paths.is_empty() {
|
||||
lines.push(Line::from(Span::styled(
|
||||
" No sample paths configured",
|
||||
dim,
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Add folders containing .wav files",
|
||||
dim,
|
||||
)));
|
||||
} else {
|
||||
for (i, path) in app.audio.config.sample_paths.iter().take(4).enumerate() {
|
||||
let path_str = path.to_string_lossy();
|
||||
let display = truncate_name(&path_str, 40);
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {} ", i + 1), dim),
|
||||
Span::styled(display, path_style),
|
||||
]));
|
||||
}
|
||||
if path_count > 4 {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" ... and {} more", path_count - 4),
|
||||
dim,
|
||||
)));
|
||||
}
|
||||
}
|
||||
frame.render_widget(Paragraph::new(lines), content_area);
|
||||
|
||||
let hint_style = if section_focused {
|
||||
Style::new().fg(Color::Rgb(180, 180, 100))
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(60, 60, 70))
|
||||
};
|
||||
let hint = Line::from(vec![
|
||||
Span::styled("A", hint_style),
|
||||
Span::styled(":add ", Style::new().fg(Color::Rgb(80, 85, 95))),
|
||||
Span::styled("D", hint_style),
|
||||
Span::styled(":remove", Style::new().fg(Color::Rgb(80, 85, 95))),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint), hint_area);
|
||||
}
|
||||
|
||||
fn render_selector(value: &str, focused: bool, highlight: Style, normal: Style) -> Span<'static> {
|
||||
let style = if focused { highlight } else { normal };
|
||||
if focused {
|
||||
Span::styled(format!("< {value} >"), style)
|
||||
} else {
|
||||
Span::styled(format!(" {value} "), style)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
pub mod audio_view;
|
||||
pub mod dict_view;
|
||||
pub mod engine_view;
|
||||
pub mod help_view;
|
||||
pub mod highlight;
|
||||
pub mod main_view;
|
||||
pub mod options_view;
|
||||
pub mod patterns_view;
|
||||
mod render;
|
||||
pub mod title_view;
|
||||
|
||||
246
src/views/options_view.rs
Normal file
246
src/views/options_view.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::LinkState;
|
||||
use crate::state::OptionsFocus;
|
||||
|
||||
const LABEL_COLOR: Color = Color::Rgb(120, 125, 135);
|
||||
const HEADER_COLOR: Color = Color::Rgb(100, 160, 180);
|
||||
const DIVIDER_COLOR: Color = Color::Rgb(60, 65, 70);
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Options ")
|
||||
.border_style(Style::new().fg(Color::Cyan));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
let padded = Rect {
|
||||
x: inner.x + 2,
|
||||
y: inner.y + 1,
|
||||
width: inner.width.saturating_sub(4),
|
||||
height: inner.height.saturating_sub(2),
|
||||
};
|
||||
|
||||
let [display_area, _, link_area, _, session_area] = Layout::vertical([
|
||||
Constraint::Length(7),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(5),
|
||||
])
|
||||
.areas(padded);
|
||||
|
||||
render_display_section(frame, app, display_area);
|
||||
render_link_section(frame, app, link, link_area);
|
||||
render_session_section(frame, link, session_area);
|
||||
}
|
||||
|
||||
fn render_display_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [header_area, divider_area, content_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let header = Line::from(Span::styled(
|
||||
"DISPLAY",
|
||||
Style::new().fg(HEADER_COLOR).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
frame.render_widget(Paragraph::new(header), header_area);
|
||||
|
||||
let divider = "─".repeat(area.width as usize);
|
||||
frame.render_widget(
|
||||
Paragraph::new(divider).style(Style::new().fg(DIVIDER_COLOR)),
|
||||
divider_area,
|
||||
);
|
||||
|
||||
let focus = app.options.focus;
|
||||
let lines = vec![
|
||||
render_option_line(
|
||||
"Refresh rate",
|
||||
app.audio.config.refresh_rate.label(),
|
||||
focus == OptionsFocus::RefreshRate,
|
||||
),
|
||||
render_option_line(
|
||||
"Runtime highlight",
|
||||
if app.ui.runtime_highlight { "On" } else { "Off" },
|
||||
focus == OptionsFocus::RuntimeHighlight,
|
||||
),
|
||||
render_option_line(
|
||||
"Show scope",
|
||||
if app.audio.config.show_scope { "On" } else { "Off" },
|
||||
focus == OptionsFocus::ShowScope,
|
||||
),
|
||||
render_option_line(
|
||||
"Show spectrum",
|
||||
if app.audio.config.show_spectrum {
|
||||
"On"
|
||||
} else {
|
||||
"Off"
|
||||
},
|
||||
focus == OptionsFocus::ShowSpectrum,
|
||||
),
|
||||
render_option_line(
|
||||
"Completion",
|
||||
if app.ui.show_completion { "On" } else { "Off" },
|
||||
focus == OptionsFocus::ShowCompletion,
|
||||
),
|
||||
];
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), content_area);
|
||||
}
|
||||
|
||||
fn render_link_section(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
||||
let [header_area, divider_area, content_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let enabled = link.is_enabled();
|
||||
let peers = link.peers();
|
||||
|
||||
let (status_text, status_color) = if !enabled {
|
||||
("DISABLED", Color::Rgb(120, 60, 60))
|
||||
} else if peers > 0 {
|
||||
("CONNECTED", Color::Rgb(60, 120, 60))
|
||||
} else {
|
||||
("LISTENING", Color::Rgb(120, 120, 60))
|
||||
};
|
||||
|
||||
let peer_text = if enabled && peers > 0 {
|
||||
if peers == 1 {
|
||||
" · 1 peer".to_string()
|
||||
} else {
|
||||
format!(" · {peers} peers")
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let header = Line::from(vec![
|
||||
Span::styled(
|
||||
"ABLETON LINK",
|
||||
Style::new().fg(HEADER_COLOR).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
status_text,
|
||||
Style::new().fg(status_color).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(peer_text, Style::new().fg(LABEL_COLOR)),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(header), header_area);
|
||||
|
||||
let divider = "─".repeat(area.width as usize);
|
||||
frame.render_widget(
|
||||
Paragraph::new(divider).style(Style::new().fg(DIVIDER_COLOR)),
|
||||
divider_area,
|
||||
);
|
||||
|
||||
let focus = app.options.focus;
|
||||
let quantum_str = format!("{:.0}", link.quantum());
|
||||
let lines = vec![
|
||||
render_option_line(
|
||||
"Enabled",
|
||||
if link.is_enabled() { "On" } else { "Off" },
|
||||
focus == OptionsFocus::LinkEnabled,
|
||||
),
|
||||
render_option_line(
|
||||
"Start/Stop sync",
|
||||
if link.is_start_stop_sync_enabled() {
|
||||
"On"
|
||||
} else {
|
||||
"Off"
|
||||
},
|
||||
focus == OptionsFocus::StartStopSync,
|
||||
),
|
||||
render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum),
|
||||
];
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), content_area);
|
||||
}
|
||||
|
||||
fn render_session_section(frame: &mut Frame, link: &LinkState, area: Rect) {
|
||||
let [header_area, divider_area, content_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let header = Line::from(Span::styled(
|
||||
"SESSION",
|
||||
Style::new().fg(HEADER_COLOR).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
frame.render_widget(Paragraph::new(header), header_area);
|
||||
|
||||
let divider = "─".repeat(area.width as usize);
|
||||
frame.render_widget(
|
||||
Paragraph::new(divider).style(Style::new().fg(DIVIDER_COLOR)),
|
||||
divider_area,
|
||||
);
|
||||
|
||||
let tempo_style = Style::new()
|
||||
.fg(Color::Rgb(220, 180, 100))
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let value_style = Style::new().fg(Color::Rgb(140, 145, 155));
|
||||
|
||||
let tempo_str = format!("{:.1} BPM", link.tempo());
|
||||
let beat_str = format!("{:.2}", link.beat());
|
||||
let phase_str = format!("{:.2}", link.phase());
|
||||
|
||||
let lines = vec![
|
||||
render_readonly_line("Tempo", &tempo_str, tempo_style),
|
||||
render_readonly_line("Beat", &beat_str, value_style),
|
||||
render_readonly_line("Phase", &phase_str, value_style),
|
||||
];
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), content_area);
|
||||
}
|
||||
|
||||
fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool) -> Line<'a> {
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let normal = Style::new().fg(Color::White);
|
||||
let label_style = Style::new().fg(LABEL_COLOR);
|
||||
|
||||
let prefix = if focused { "> " } else { " " };
|
||||
let prefix_style = if focused { highlight } else { normal };
|
||||
|
||||
let value_display = if focused {
|
||||
format!("< {value} >")
|
||||
} else {
|
||||
format!(" {value} ")
|
||||
};
|
||||
let value_style = if focused { highlight } else { normal };
|
||||
|
||||
let label_width = 20;
|
||||
let padded_label = format!("{label:<label_width$}");
|
||||
|
||||
Line::from(vec![
|
||||
Span::styled(prefix, prefix_style),
|
||||
Span::styled(padded_label, label_style),
|
||||
Span::styled(value_display, value_style),
|
||||
])
|
||||
}
|
||||
|
||||
fn render_readonly_line<'a>(label: &'a str, value: &'a str, value_style: Style) -> Line<'a> {
|
||||
let label_style = Style::new().fg(LABEL_COLOR);
|
||||
let label_width = 20;
|
||||
let padded_label = format!("{label:<label_width$}");
|
||||
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(padded_label, label_style),
|
||||
Span::styled(format!(" {value}"), value_style),
|
||||
])
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel};
|
||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||
use crate::widgets::{ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal};
|
||||
|
||||
use super::{audio_view, dict_view, help_view, main_view, patterns_view, title_view};
|
||||
use super::{dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view};
|
||||
|
||||
fn adjust_spans_for_line(spans: &[SourceSpan], line_start: usize, line_len: usize) -> Vec<SourceSpan> {
|
||||
spans.iter().filter_map(|s| {
|
||||
@@ -82,7 +82,8 @@ pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &Seq
|
||||
match app.page {
|
||||
Page::Main => main_view::render(frame, app, snapshot, page_area),
|
||||
Page::Patterns => patterns_view::render(frame, app, snapshot, page_area),
|
||||
Page::Audio => audio_view::render(frame, app, link, page_area),
|
||||
Page::Engine => engine_view::render(frame, app, page_area),
|
||||
Page::Options => options_view::render(frame, app, link, page_area),
|
||||
Page::Help => help_view::render(frame, app, page_area),
|
||||
Page::Dict => dict_view::render(frame, app, page_area),
|
||||
}
|
||||
@@ -261,7 +262,8 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let page_indicator = match app.page {
|
||||
Page::Main => "[MAIN]",
|
||||
Page::Patterns => "[PATTERNS]",
|
||||
Page::Audio => "[AUDIO]",
|
||||
Page::Engine => "[ENGINE]",
|
||||
Page::Options => "[OPTIONS]",
|
||||
Page::Help => "[HELP]",
|
||||
Page::Dict => "[DICT]",
|
||||
};
|
||||
@@ -294,13 +296,16 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
("r", "Rename"),
|
||||
("Del", "Reset"),
|
||||
],
|
||||
Page::Audio => vec![
|
||||
Page::Engine => vec![
|
||||
("Tab", "Section"),
|
||||
("←→", "Switch/Adjust"),
|
||||
("↑↓", "Navigate"),
|
||||
("←→", "Adjust"),
|
||||
("h", "Hush"),
|
||||
("p", "Panic"),
|
||||
("r", "Reset"),
|
||||
("t", "Test"),
|
||||
("Enter", "Select"),
|
||||
("A", "Add path"),
|
||||
],
|
||||
Page::Options => vec![
|
||||
("Tab", "Next"),
|
||||
("←→", "Toggle"),
|
||||
("Space", "Play"),
|
||||
],
|
||||
Page::Help => vec![
|
||||
|
||||
Reference in New Issue
Block a user