399 lines
14 KiB
Rust
399 lines
14 KiB
Rust
//! Preferences dialog UI
|
|
//!
|
|
//! Provides a user interface for configuring application preferences
|
|
|
|
use eframe::egui;
|
|
use crate::config::AppConfig;
|
|
use crate::theme::{Theme, ThemeMode};
|
|
|
|
/// Preferences dialog state
|
|
pub struct PreferencesDialog {
|
|
/// Is the dialog open?
|
|
pub open: bool,
|
|
|
|
/// Working copy of preferences (allows cancel to discard changes)
|
|
working_prefs: PreferencesState,
|
|
|
|
/// Original audio buffer size (to detect changes that need restart)
|
|
original_buffer_size: u32,
|
|
|
|
/// Error message (if validation fails)
|
|
error_message: Option<String>,
|
|
}
|
|
|
|
/// Editable preferences state (working copy)
|
|
#[derive(Debug, Clone)]
|
|
struct PreferencesState {
|
|
bpm: u32,
|
|
framerate: u32,
|
|
file_width: u32,
|
|
file_height: u32,
|
|
scroll_speed: f64,
|
|
audio_buffer_size: u32,
|
|
reopen_last_session: bool,
|
|
restore_layout_from_file: bool,
|
|
debug: bool,
|
|
theme_mode: ThemeMode,
|
|
}
|
|
|
|
impl From<(&AppConfig, &Theme)> for PreferencesState {
|
|
fn from((config, theme): (&AppConfig, &Theme)) -> Self {
|
|
Self {
|
|
bpm: config.bpm,
|
|
framerate: config.framerate,
|
|
file_width: config.file_width,
|
|
file_height: config.file_height,
|
|
scroll_speed: config.scroll_speed,
|
|
audio_buffer_size: config.audio_buffer_size,
|
|
reopen_last_session: config.reopen_last_session,
|
|
restore_layout_from_file: config.restore_layout_from_file,
|
|
debug: config.debug,
|
|
theme_mode: theme.mode(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for PreferencesState {
|
|
fn default() -> Self {
|
|
Self {
|
|
bpm: 120,
|
|
framerate: 24,
|
|
file_width: 800,
|
|
file_height: 600,
|
|
scroll_speed: 1.0,
|
|
audio_buffer_size: 256,
|
|
reopen_last_session: false,
|
|
restore_layout_from_file: true,
|
|
debug: false,
|
|
theme_mode: ThemeMode::System,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result returned when preferences are saved
|
|
#[derive(Debug, Clone)]
|
|
pub struct PreferencesSaveResult {
|
|
/// Whether audio buffer size changed (requires restart)
|
|
pub buffer_size_changed: bool,
|
|
}
|
|
|
|
impl Default for PreferencesDialog {
|
|
fn default() -> Self {
|
|
Self {
|
|
open: false,
|
|
working_prefs: PreferencesState::default(),
|
|
original_buffer_size: 256,
|
|
error_message: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PreferencesDialog {
|
|
/// Open the dialog with current config and theme
|
|
pub fn open(&mut self, config: &AppConfig, theme: &Theme) {
|
|
self.open = true;
|
|
self.working_prefs = PreferencesState::from((config, theme));
|
|
self.original_buffer_size = config.audio_buffer_size;
|
|
self.error_message = None;
|
|
}
|
|
|
|
/// Close the dialog
|
|
pub fn close(&mut self) {
|
|
self.open = false;
|
|
self.error_message = None;
|
|
}
|
|
|
|
/// Render the preferences dialog
|
|
///
|
|
/// Returns Some(PreferencesSaveResult) if user clicked Save, None otherwise.
|
|
pub fn render(
|
|
&mut self,
|
|
ctx: &egui::Context,
|
|
config: &mut AppConfig,
|
|
theme: &mut Theme,
|
|
) -> Option<PreferencesSaveResult> {
|
|
if !self.open {
|
|
return None;
|
|
}
|
|
|
|
let mut should_save = false;
|
|
let mut should_cancel = false;
|
|
let mut open = self.open;
|
|
|
|
egui::Window::new("Preferences")
|
|
.open(&mut open)
|
|
.resizable(false)
|
|
.collapsible(false)
|
|
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
|
|
.show(ctx, |ui| {
|
|
ui.set_width(500.0);
|
|
|
|
// Error message
|
|
if let Some(error) = &self.error_message {
|
|
ui.colored_label(egui::Color32::from_rgb(255, 100, 100), error);
|
|
ui.add_space(8.0);
|
|
}
|
|
|
|
// Scrollable area for preferences sections
|
|
egui::ScrollArea::vertical()
|
|
.max_height(400.0)
|
|
.show(ui, |ui| {
|
|
self.render_general_section(ui);
|
|
ui.add_space(8.0);
|
|
self.render_audio_section(ui);
|
|
ui.add_space(8.0);
|
|
self.render_appearance_section(ui);
|
|
ui.add_space(8.0);
|
|
self.render_startup_section(ui);
|
|
ui.add_space(8.0);
|
|
self.render_advanced_section(ui);
|
|
});
|
|
|
|
ui.add_space(16.0);
|
|
|
|
// Buttons
|
|
ui.horizontal(|ui| {
|
|
if ui.button("Cancel").clicked() {
|
|
should_cancel = true;
|
|
}
|
|
|
|
if ui.button("Reset to Defaults").clicked() {
|
|
self.reset_to_defaults();
|
|
}
|
|
|
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
|
if ui.button("Save").clicked() {
|
|
should_save = true;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Update open state
|
|
self.open = open;
|
|
|
|
if should_cancel {
|
|
self.close();
|
|
return None;
|
|
}
|
|
|
|
if should_save {
|
|
return self.handle_save(config, theme);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn render_general_section(&mut self, ui: &mut egui::Ui) {
|
|
egui::CollapsingHeader::new("General")
|
|
.default_open(true)
|
|
.show(ui, |ui| {
|
|
ui.horizontal(|ui| {
|
|
ui.label("Default BPM:");
|
|
ui.add(
|
|
egui::DragValue::new(&mut self.working_prefs.bpm)
|
|
.clamp_range(20..=300)
|
|
.speed(1.0),
|
|
);
|
|
});
|
|
|
|
ui.horizontal(|ui| {
|
|
ui.label("Default Framerate:");
|
|
ui.add(
|
|
egui::DragValue::new(&mut self.working_prefs.framerate)
|
|
.clamp_range(1..=120)
|
|
.speed(1.0)
|
|
.suffix(" fps"),
|
|
);
|
|
});
|
|
|
|
ui.horizontal(|ui| {
|
|
ui.label("Default File Width:");
|
|
ui.add(
|
|
egui::DragValue::new(&mut self.working_prefs.file_width)
|
|
.clamp_range(100..=10000)
|
|
.speed(10.0)
|
|
.suffix(" px"),
|
|
);
|
|
});
|
|
|
|
ui.horizontal(|ui| {
|
|
ui.label("Default File Height:");
|
|
ui.add(
|
|
egui::DragValue::new(&mut self.working_prefs.file_height)
|
|
.clamp_range(100..=10000)
|
|
.speed(10.0)
|
|
.suffix(" px"),
|
|
);
|
|
});
|
|
|
|
ui.horizontal(|ui| {
|
|
ui.label("Scroll Speed:");
|
|
ui.add(
|
|
egui::DragValue::new(&mut self.working_prefs.scroll_speed)
|
|
.clamp_range(0.1..=10.0)
|
|
.speed(0.1),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
fn render_audio_section(&mut self, ui: &mut egui::Ui) {
|
|
egui::CollapsingHeader::new("Audio")
|
|
.default_open(true)
|
|
.show(ui, |ui| {
|
|
ui.horizontal(|ui| {
|
|
ui.label("Audio Buffer Size:");
|
|
|
|
egui::ComboBox::from_id_source("audio_buffer_size")
|
|
.selected_text(format!("{} samples", self.working_prefs.audio_buffer_size))
|
|
.show_ui(ui, |ui| {
|
|
ui.selectable_value(
|
|
&mut self.working_prefs.audio_buffer_size,
|
|
128,
|
|
"128 samples (~3ms - Low latency)",
|
|
);
|
|
ui.selectable_value(
|
|
&mut self.working_prefs.audio_buffer_size,
|
|
256,
|
|
"256 samples (~6ms - Balanced)",
|
|
);
|
|
ui.selectable_value(
|
|
&mut self.working_prefs.audio_buffer_size,
|
|
512,
|
|
"512 samples (~12ms - Stable)",
|
|
);
|
|
ui.selectable_value(
|
|
&mut self.working_prefs.audio_buffer_size,
|
|
1024,
|
|
"1024 samples (~23ms - Very stable)",
|
|
);
|
|
ui.selectable_value(
|
|
&mut self.working_prefs.audio_buffer_size,
|
|
2048,
|
|
"2048 samples (~46ms - Low-end systems)",
|
|
);
|
|
ui.selectable_value(
|
|
&mut self.working_prefs.audio_buffer_size,
|
|
4096,
|
|
"4096 samples (~93ms - Very low-end systems)",
|
|
);
|
|
});
|
|
});
|
|
|
|
ui.label("⚠ Requires app restart to take effect");
|
|
});
|
|
}
|
|
|
|
fn render_appearance_section(&mut self, ui: &mut egui::Ui) {
|
|
egui::CollapsingHeader::new("Appearance")
|
|
.default_open(true)
|
|
.show(ui, |ui| {
|
|
ui.horizontal(|ui| {
|
|
ui.label("Theme:");
|
|
|
|
egui::ComboBox::from_id_source("theme_mode")
|
|
.selected_text(format!("{:?}", self.working_prefs.theme_mode))
|
|
.show_ui(ui, |ui| {
|
|
ui.selectable_value(
|
|
&mut self.working_prefs.theme_mode,
|
|
ThemeMode::Light,
|
|
"Light",
|
|
);
|
|
ui.selectable_value(
|
|
&mut self.working_prefs.theme_mode,
|
|
ThemeMode::Dark,
|
|
"Dark",
|
|
);
|
|
ui.selectable_value(
|
|
&mut self.working_prefs.theme_mode,
|
|
ThemeMode::System,
|
|
"System",
|
|
);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
fn render_startup_section(&mut self, ui: &mut egui::Ui) {
|
|
egui::CollapsingHeader::new("Startup")
|
|
.default_open(false)
|
|
.show(ui, |ui| {
|
|
ui.checkbox(
|
|
&mut self.working_prefs.reopen_last_session,
|
|
"Reopen last session on startup",
|
|
);
|
|
ui.checkbox(
|
|
&mut self.working_prefs.restore_layout_from_file,
|
|
"Restore layout when opening files",
|
|
);
|
|
});
|
|
}
|
|
|
|
fn render_advanced_section(&mut self, ui: &mut egui::Ui) {
|
|
egui::CollapsingHeader::new("Advanced")
|
|
.default_open(false)
|
|
.show(ui, |ui| {
|
|
ui.checkbox(&mut self.working_prefs.debug, "Enable debug mode");
|
|
});
|
|
}
|
|
|
|
fn reset_to_defaults(&mut self) {
|
|
self.working_prefs = PreferencesState::default();
|
|
self.error_message = None;
|
|
}
|
|
|
|
fn handle_save(
|
|
&mut self,
|
|
config: &mut AppConfig,
|
|
theme: &mut Theme,
|
|
) -> Option<PreferencesSaveResult> {
|
|
// Create temp config for validation
|
|
let mut temp_config = config.clone();
|
|
temp_config.bpm = self.working_prefs.bpm;
|
|
temp_config.framerate = self.working_prefs.framerate;
|
|
temp_config.file_width = self.working_prefs.file_width;
|
|
temp_config.file_height = self.working_prefs.file_height;
|
|
temp_config.scroll_speed = self.working_prefs.scroll_speed;
|
|
temp_config.audio_buffer_size = self.working_prefs.audio_buffer_size;
|
|
temp_config.reopen_last_session = self.working_prefs.reopen_last_session;
|
|
temp_config.restore_layout_from_file = self.working_prefs.restore_layout_from_file;
|
|
temp_config.debug = self.working_prefs.debug;
|
|
temp_config.theme_mode = self.working_prefs.theme_mode.to_string_lower();
|
|
|
|
// Validate
|
|
if let Err(err) = temp_config.validate() {
|
|
self.error_message = Some(err);
|
|
return None;
|
|
}
|
|
|
|
// Check if buffer size changed
|
|
let buffer_size_changed = self.working_prefs.audio_buffer_size != self.original_buffer_size;
|
|
|
|
// Apply changes to config
|
|
config.bpm = self.working_prefs.bpm;
|
|
config.framerate = self.working_prefs.framerate;
|
|
config.file_width = self.working_prefs.file_width;
|
|
config.file_height = self.working_prefs.file_height;
|
|
config.scroll_speed = self.working_prefs.scroll_speed;
|
|
config.audio_buffer_size = self.working_prefs.audio_buffer_size;
|
|
config.reopen_last_session = self.working_prefs.reopen_last_session;
|
|
config.restore_layout_from_file = self.working_prefs.restore_layout_from_file;
|
|
config.debug = self.working_prefs.debug;
|
|
config.theme_mode = self.working_prefs.theme_mode.to_string_lower();
|
|
|
|
// Apply theme immediately
|
|
theme.set_mode(self.working_prefs.theme_mode);
|
|
|
|
// Save to disk
|
|
config.save();
|
|
|
|
// Close dialog
|
|
self.close();
|
|
|
|
Some(PreferencesSaveResult {
|
|
buffer_size_changed,
|
|
})
|
|
}
|
|
}
|