audio-oxide-old/src/gui/settings.rs

349 lines
11 KiB
Rust

use crate::config::{AudioOxideConfig, RecordingFormat};
use iced::widget::{
button, column, container, horizontal_rule, pick_list, row, text, text_input, toggler, Space,
};
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingsTab {
General,
Audio,
Display,
Advanced,
}
impl SettingsTab {
pub const ALL: [SettingsTab; 4] = [
SettingsTab::General,
SettingsTab::Audio,
SettingsTab::Display,
SettingsTab::Advanced,
];
fn label(&self) -> &'static str {
match self {
SettingsTab::General => "General",
SettingsTab::Audio => "Audio",
SettingsTab::Display => "Display",
SettingsTab::Advanced => "Advanced",
}
}
}
#[derive(Debug, Clone)]
pub struct State {
pub config: AudioOxideConfig,
pub active_tab: SettingsTab,
}
impl State {
pub fn new(config: &AudioOxideConfig) -> Self {
Self {
config: config.clone(),
active_tab: SettingsTab::General,
}
}
}
#[derive(Debug, Clone)]
pub enum Message {
TabSelected(SettingsTab),
// General
ProjectDirChanged(String),
AutoSaveToggled(bool),
AutoSaveIntervalChanged(String),
AskToSaveToggled(bool),
// Audio
DefaultSampleRateSelected(u32),
DefaultBufferSizeSelected(u32),
DefaultAudioDeviceChanged(String),
RecordingFormatSelected(RecordingFormat),
RecordingBitDepthSelected(u16),
// Display
DefaultTrackHeightChanged(String),
ShowToolbarOnOpenToggled(bool),
ShowInspectorOnOpenToggled(bool),
// Actions
Save,
Cancel,
}
fn section_header(label: &str) -> Element<'_, Message> {
column![
text(label)
.size(13)
.color(Color::from_rgb8(0x88, 0x88, 0x88)),
horizontal_rule(1),
]
.spacing(4)
.into()
}
fn setting_row<'a>(
label: &'a str,
control: Element<'a, Message>,
) -> Element<'a, Message> {
row![
text(label).size(13).width(200),
control,
]
.spacing(12)
.align_y(Alignment::Center)
.into()
}
fn tab_button<'a>(tab: SettingsTab, active: SettingsTab) -> Element<'a, Message> {
let is_active = tab == active;
let btn = button(text(tab.label()).size(13))
.on_press(Message::TabSelected(tab))
.padding([6, 16]);
if is_active {
btn.style(|_theme: &Theme, _status| button::Style {
background: Some(Background::Color(Color::from_rgb8(0x00, 0x7A, 0xFF))),
text_color: Color::WHITE,
border: Border {
radius: 4.0.into(),
..Border::default()
},
..button::Style::default()
})
.into()
} else {
btn.style(|_theme: &Theme, _status| button::Style {
background: Some(Background::Color(Color::from_rgb8(0x30, 0x32, 0x34))),
text_color: Color::from_rgb8(0xAA, 0xAA, 0xAA),
border: Border {
radius: 4.0.into(),
..Border::default()
},
..button::Style::default()
})
.into()
}
}
fn view_general(config: &AudioOxideConfig) -> Element<'_, Message> {
let project_dir_input = text_input(
"Project directory",
config.project_dir.to_str().unwrap_or(""),
)
.on_input(Message::ProjectDirChanged)
.width(300);
let auto_save_toggle = toggler(config.auto_save)
.on_toggle(Message::AutoSaveToggled)
.width(Length::Shrink);
let auto_save_interval = text_input(
"seconds",
&config.auto_save_interval_secs.to_string(),
)
.on_input(Message::AutoSaveIntervalChanged)
.width(80);
let ask_save_toggle = toggler(config.ask_to_save_on_close)
.on_toggle(Message::AskToSaveToggled)
.width(Length::Shrink);
column![
section_header("Project Handling"),
setting_row("Default project directory", project_dir_input.into()),
setting_row("Auto-save", auto_save_toggle.into()),
setting_row("Auto-save interval (sec)", auto_save_interval.into()),
setting_row("Ask to save on close", ask_save_toggle.into()),
]
.spacing(10)
.padding(16)
.into()
}
fn view_audio(config: &AudioOxideConfig) -> Element<'_, Message> {
let sample_rates: Vec<u32> = vec![22050, 44100, 48000, 88200, 96000, 176400, 192000];
let sr_picker = pick_list(
sample_rates,
Some(config.default_sample_rate),
Message::DefaultSampleRateSelected,
)
.width(120);
let buffer_sizes: Vec<u32> = vec![32, 64, 128, 256, 512, 1024, 2048, 4096];
let bs_picker = pick_list(
buffer_sizes,
Some(config.default_buffer_size),
Message::DefaultBufferSizeSelected,
)
.width(120);
let device_input = text_input("Audio device", &config.default_audio_device)
.on_input(Message::DefaultAudioDeviceChanged)
.width(200);
let format_picker = pick_list(
&RecordingFormat::ALL[..],
Some(config.recording_format),
Message::RecordingFormatSelected,
)
.width(120);
let bit_depths: Vec<u16> = vec![16, 24, 32];
let bd_picker = pick_list(
bit_depths,
Some(config.recording_bit_depth),
Message::RecordingBitDepthSelected,
)
.width(120);
column![
section_header("Devices"),
setting_row("Default audio device", device_input.into()),
setting_row("Default sample rate", sr_picker.into()),
setting_row("Default buffer size", bs_picker.into()),
section_header("Recording"),
setting_row("File format", format_picker.into()),
setting_row("Bit depth", bd_picker.into()),
]
.spacing(10)
.padding(16)
.into()
}
fn view_display(config: &AudioOxideConfig) -> Element<'_, Message> {
let track_height_input = text_input("pixels", &format!("{:.0}", config.default_track_height))
.on_input(Message::DefaultTrackHeightChanged)
.width(80);
let toolbar_toggle = toggler(config.show_toolbar_on_open)
.on_toggle(Message::ShowToolbarOnOpenToggled)
.width(Length::Shrink);
let inspector_toggle = toggler(config.show_inspector_on_open)
.on_toggle(Message::ShowInspectorOnOpenToggled)
.width(Length::Shrink);
column![
section_header("Layout"),
setting_row("Default track height (px)", track_height_input.into()),
setting_row("Show toolbar on open", toolbar_toggle.into()),
setting_row("Show inspector on open", inspector_toggle.into()),
]
.spacing(10)
.padding(16)
.into()
}
fn view_advanced(_config: &AudioOxideConfig) -> Element<'_, Message> {
column![
section_header("Engine"),
text("Engine configuration options will appear here.")
.size(12)
.color(Color::from_rgb8(0x66, 0x66, 0x66)),
section_header("Plugins"),
text("Module/plugin scan paths and loading options will appear here.")
.size(12)
.color(Color::from_rgb8(0x66, 0x66, 0x66)),
]
.spacing(10)
.padding(16)
.into()
}
pub fn view(state: &State) -> Element<'_, Message> {
let title = text("Settings").size(20);
// Tab bar
let tabs = SettingsTab::ALL.iter().fold(
row![].spacing(4),
|r, tab| r.push(tab_button(*tab, state.active_tab)),
);
// Tab content
let content: Element<_> = match state.active_tab {
SettingsTab::General => view_general(&state.config),
SettingsTab::Audio => view_audio(&state.config),
SettingsTab::Display => view_display(&state.config),
SettingsTab::Advanced => view_advanced(&state.config),
};
let content_area = container(content)
.width(Length::Fill)
.height(Length::Fill)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb8(0x22, 0x24, 0x26))),
border: Border {
radius: 4.0.into(),
color: Color::from_rgb8(0x35, 0x37, 0x39),
width: 1.0,
},
..container::Style::default()
});
// Action buttons
let cancel_btn = button(text("Cancel").size(13)).on_press(Message::Cancel);
let save_btn = button(text("Save").size(13))
.on_press(Message::Save)
.style(|_theme: &Theme, _status| button::Style {
background: Some(Background::Color(Color::from_rgb8(0x00, 0x7A, 0xFF))),
text_color: Color::WHITE,
border: Border {
radius: 4.0.into(),
..Border::default()
},
..button::Style::default()
});
let actions = row![Space::new(Length::Fill, 0), cancel_btn, save_btn]
.spacing(8)
.align_y(Alignment::Center);
let dialog = column![title, tabs, content_area, actions]
.spacing(12)
.padding(20);
container(dialog)
.max_width(600)
.max_height(500)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.extended_palette().background.weak.color,
)),
border: Border {
radius: 8.0.into(),
color: Color::from_rgb8(0x40, 0x42, 0x44),
width: 1.0,
},
..container::Style::default()
})
.into()
}
pub fn handle_message(state: &mut State, message: Message) -> bool {
match message {
Message::TabSelected(tab) => state.active_tab = tab,
Message::ProjectDirChanged(p) => state.config.project_dir = p.into(),
Message::AutoSaveToggled(v) => state.config.auto_save = v,
Message::AutoSaveIntervalChanged(s) => {
if let Ok(v) = s.parse::<u32>() {
state.config.auto_save_interval_secs = v;
}
}
Message::AskToSaveToggled(v) => state.config.ask_to_save_on_close = v,
Message::DefaultSampleRateSelected(sr) => state.config.default_sample_rate = sr,
Message::DefaultBufferSizeSelected(bs) => state.config.default_buffer_size = bs,
Message::DefaultAudioDeviceChanged(d) => state.config.default_audio_device = d,
Message::RecordingFormatSelected(f) => state.config.recording_format = f,
Message::RecordingBitDepthSelected(bd) => state.config.recording_bit_depth = bd,
Message::DefaultTrackHeightChanged(s) => {
if let Ok(v) = s.parse::<f32>() {
if v > 0.0 {
state.config.default_track_height = v;
}
}
}
Message::ShowToolbarOnOpenToggled(v) => state.config.show_toolbar_on_open = v,
Message::ShowInspectorOnOpenToggled(v) => state.config.show_inspector_on_open = v,
Message::Save => return true,
Message::Cancel => return true,
}
false
}