From 717b0d2cd65d2761525dcd2144f9ec44436e5320 Mon Sep 17 00:00:00 2001 From: PancakeTAS Date: Thu, 24 Jul 2025 02:39:58 +0200 Subject: [PATCH] ui: bugfixes and cleanup --- ui/rsc/pane/main.ui | 10 +- ui/src/config.rs | 13 ++- ui/src/config/structs.rs | 19 ++++ ui/src/main.rs | 197 +---------------------------------- ui/src/ui.rs | 33 ++++++ ui/src/ui/entry_handler.rs | 56 ++++++++++ ui/src/ui/main_handler.rs | 58 +++++++++++ ui/src/ui/sidebar_handler.rs | 67 ++++++++++++ ui/src/wrapper/pane/main.rs | 10 +- 9 files changed, 254 insertions(+), 209 deletions(-) create mode 100644 ui/src/ui.rs create mode 100644 ui/src/ui/entry_handler.rs create mode 100644 ui/src/ui/main_handler.rs create mode 100644 ui/src/ui/sidebar_handler.rs diff --git a/ui/rsc/pane/main.ui b/ui/rsc/pane/main.ui index 0fea93d..94e6b0e 100644 --- a/ui/rsc/pane/main.ui +++ b/ui/rsc/pane/main.ui @@ -31,19 +31,19 @@ Frame Generation - + Multiplier - + Flow Scale - + Performance Mode false @@ -56,14 +56,14 @@ Misc - + HDR Mode false - + Experimental Present Mode 0 diff --git a/ui/src/config.rs b/ui/src/config.rs index 39f6d88..896c1d3 100644 --- a/ui/src/config.rs +++ b/ui/src/config.rs @@ -33,13 +33,18 @@ pub fn load_config() -> Result<(), anyhow::Error> { let path = find_config_file(); let data = std::fs::read(path) .context("Failed to read conf.toml")?; - let config: TomlConfig = toml::from_slice(&data) + let mut config: TomlConfig = toml::from_slice(&data) .context("Failed to parse conf.toml")?; - CONFIG.set(Arc::new(RwLock::new(config))) - .ok().context("Failed to set configuration state")?; + + // remove duplicate entries + config.game.sort_by_key(|e| e.exe.clone()); + config.game.dedup_by_key(|e| e.exe.clone()); + config.game.retain(|e| !e.exe.is_empty()); // create the configuration writer thread let (tx, rx) = std::sync::mpsc::channel::<()>(); + CONFIG.set(Arc::new(RwLock::new(config))) + .ok().context("Failed to set configuration state")?; CONFIG_WRITER.set(tx) .ok().context("Failed to set configuration writer")?; @@ -61,8 +66,6 @@ pub fn load_config() -> Result<(), anyhow::Error> { if let Ok(config) = config.try_read() { if let Err(e) = save_config(&config) { eprintln!("Failed to save configuration: {}", e); - } else { - eprintln!("Configuration saved successfully"); } } else { eprintln!("Failed to read configuration state"); diff --git a/ui/src/config/structs.rs b/ui/src/config/structs.rs index 24dde38..d191dc6 100644 --- a/ui/src/config/structs.rs +++ b/ui/src/config/structs.rs @@ -39,6 +39,25 @@ pub enum PresentMode { impl Default for PresentMode { fn default() -> Self { PresentMode::Vsync } } +impl From for PresentMode { + fn from(value: i64) -> Self { + match value { + 0 => PresentMode::Vsync, + 1 => PresentMode::Mailbox, + 2 => PresentMode::Immediate, + _ => PresentMode::Vsync, + } + } +} +impl Into for PresentMode { + fn into(self) -> u32 { + match self { + PresentMode::Vsync => 0, + PresentMode::Mailbox => 1, + PresentMode::Immediate => 2, + } + } +} /// Global configuration for the application #[derive(Debug, Clone, Default, Deserialize, Serialize)] diff --git a/ui/src/main.rs b/ui/src/main.rs index deb5d30..5dd0682 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -1,10 +1,9 @@ use std::sync::{Arc, OnceLock, RwLock}; -use adw::{self, subclass::prelude::ObjectSubclassIsExt}; use gtk::{gio, prelude::*}; +use adw; -use crate::config::*; - +mod ui; mod wrapper; mod config; @@ -33,196 +32,6 @@ fn main() { let app = adw::Application::builder() .application_id(APP_ID) .build(); - app.connect_activate(build_ui); + app.connect_activate(ui::build); app.run(); } - -fn helper_add_deletion_signal( - entry: wrapper::entry::Entry, - profiles: gtk::ListBox) { - let entry_clone = entry.clone(); - entry.imp().delete.connect_clicked(move |btn| { - let dialog = gtk::AlertDialog::builder() - .message("Delete Profile") - .detail("Are you sure you want to delete this profile?") - .buttons(vec!["Cancel".to_string(), "Delete".to_string()]) - .cancel_button(0) - .default_button(1) - .modal(true) - .build(); - let window = btn.root() - .and_downcast::() - .expect("Button root is not a Window"); - - let profiles = profiles.clone(); - let entry_clone = entry_clone.clone(); - dialog.choose(Some(&window), None::<&gio::Cancellable>, move |result| { - match result { - Ok(idx) if idx == 1 => { - let _ = config::edit_config(|config| { - config.game.retain(|g| g.exe != entry_clone.exe()); - }); - profiles.remove(&entry_clone); - }, - _ => return, - }; - }); - }); -} - -fn build_ui(app: &adw::Application) { - // create the main window - let window = wrapper::Window::new(app); - window.set_application(Some(app)); - - // load profiles from configuration - let sidebar = window.imp().sidebar.imp(); - - let config = config::get_config() - .expect("Failed to get configuration"); - for game in config.game.iter() { - let entry = wrapper::entry::Entry::new(); - entry.set_exe(game.exe.clone()); - - helper_add_deletion_signal(entry.clone(), sidebar.profiles.clone()); - sidebar.profiles.append(&entry); - } - - // register side pane signals - let profiles = sidebar.profiles.clone(); - let main = window.imp().main.clone(); - sidebar.create.connect_clicked(move |_| { - let mut conf_entry = config::TomlGame::default(); - conf_entry.exe = "new profile".to_string(); - let _ = config::edit_config(|config| { - config.game.push(conf_entry.clone()); - }); - - let entry = wrapper::entry::Entry::new(); - entry.set_exe(conf_entry.exe); - - helper_add_deletion_signal(entry.clone(), profiles.clone()); - profiles.append(&entry); - entry.activate(); - }); - - let state = STATE.get().unwrap().clone(); - sidebar.profiles.connect_row_activated(move |_, entry| { - // find config entry - let index = entry.index() as usize; - let config = config::get_config() - .expect("Failed to get configuration"); - let conf = config.game[index].clone(); - - // update state - { - let mut state = state.write() - .expect("Failed to acquire write lock on state"); - state.selected_game = Some(index); - } - - // update main pane - let main = main.imp(); - let pref_multiplier = main.pref_multiplier.imp(); - pref_multiplier.number.set_value(conf.multiplier.into()); - let pref_flow_scale = main.pref_flow_scale.imp(); - pref_flow_scale.slider.set_value(Into::::into(conf.flow_scale) * 100.0); - let pref_performance_mode = main.pref_performance_mode.imp(); - pref_performance_mode.switch.set_state(conf.performance_mode); - let pref_hdr_mode = main.pref_hdr_mode.imp(); - pref_hdr_mode.switch.set_state(conf.hdr_mode); - let pref_experimental_present_mode = main.pref_experimental_present_mode.imp(); - let mode = match conf.experimental_present_mode { - PresentMode::Vsync => 0, - PresentMode::Mailbox => 1, - PresentMode::Immediate => 2, - }; - pref_experimental_present_mode.dropdown.set_selected(mode); - }); - - // register main pane signals - let main = window.imp().main.imp(); - - let pref_multiplier = main.pref_multiplier.imp(); - pref_multiplier.number.connect_value_changed(|dropdown| { - if let Ok(state) = STATE.get().unwrap().try_read() { - if state.selected_game.is_none() { - return; - } - - let multiplier = (dropdown.value() as i64).into(); - let _ = config::edit_config(|config| { - config.game[state.selected_game.unwrap()] - .multiplier = multiplier; - }); - } - }); - - let pref_flow_scale = main.pref_flow_scale.imp(); - pref_flow_scale.slider.connect_value_changed(|slider| { - if let Ok(state) = STATE.get().unwrap().try_read() { - if state.selected_game.is_none() { - return; - } - - let flow_scale = (slider.value() / 100.0).into(); - let _ = config::edit_config(|config| { - config.game[state.selected_game.unwrap()] - .flow_scale = flow_scale; - }); - } - }); - - let pref_performance_mode = main.pref_performance_mode.imp(); - pref_performance_mode.switch.connect_state_notify(|switch| { - if let Ok(state) = STATE.get().unwrap().try_read() { - if state.selected_game.is_none() { - return; - } - - let performance_mode = switch.state(); - let _ = config::edit_config(|config| { - config.game[state.selected_game.unwrap()] - .performance_mode = performance_mode; - }); - } - }); - - let pref_hdr_mode = main.pref_hdr_mode.imp(); - pref_hdr_mode.switch.connect_state_notify(|switch| { - if let Ok(state) = STATE.get().unwrap().try_read() { - if state.selected_game.is_none() { - return; - } - - let hdr_mode = switch.state(); - let _ = config::edit_config(|config| { - config.game[state.selected_game.unwrap()] - .hdr_mode = hdr_mode; - }); - } - }); - - let pref_experimental_present_mode = main.pref_experimental_present_mode.imp(); - pref_experimental_present_mode.dropdown.connect_selected_notify(|dropdown| { - if let Ok(state) = STATE.get().unwrap().try_read() { - if state.selected_game.is_none() { - return; - } - - let selected = match dropdown.selected() { - 0 => PresentMode::Vsync, - 1 => PresentMode::Mailbox, - 2 => PresentMode::Immediate, - _ => PresentMode::Vsync, - }; - config::edit_config(|config| { - config.game[state.selected_game.unwrap()] - .experimental_present_mode = selected; - }).unwrap(); - } - }); - - // present the window - window.present(); -} diff --git a/ui/src/ui.rs b/ui/src/ui.rs new file mode 100644 index 0000000..db7dee3 --- /dev/null +++ b/ui/src/ui.rs @@ -0,0 +1,33 @@ +use adw::{self, subclass::prelude::ObjectSubclassIsExt}; +use gtk::prelude::*; + +use crate::config; +use crate::wrapper; + +pub mod entry_handler; +pub mod main_handler; +pub mod sidebar_handler; + +pub fn build(app: &adw::Application) { + // create the main window + let window = wrapper::Window::new(app); + window.set_application(Some(app)); + let imp = window.imp(); + + // load profiles from configuration + let config = config::get_config().unwrap(); + for game in config.game.iter() { + let entry = wrapper::entry::Entry::new(); + entry.set_exe(game.exe.clone()); + entry_handler::add_entry(entry, imp.sidebar.imp().profiles.clone()); + } + + // register handlers on sidebar pane. + sidebar_handler::register_signals(&imp.sidebar, imp.main.clone()); + + // register handlers on main pane. + main_handler::register_signals(&imp.main); + + // present the window + window.present(); +} diff --git a/ui/src/ui/entry_handler.rs b/ui/src/ui/entry_handler.rs new file mode 100644 index 0000000..ebddab9 --- /dev/null +++ b/ui/src/ui/entry_handler.rs @@ -0,0 +1,56 @@ +use adw::subclass::prelude::ObjectSubclassIsExt; +use gtk::{gio, glib::object::CastNone, prelude::{ButtonExt, ListBoxRowExt, WidgetExt}}; + +use crate::{config, wrapper::entry, STATE}; + +/// +/// Register signals for removing presets when adding a new entry. +/// +pub fn add_entry(entry_: entry::Entry, profiles_: gtk::ListBox) { + let entry = entry_.clone(); + let profiles = profiles_.clone(); + entry_.imp().delete.connect_clicked(move |btn| { + // prompt for confirmation + let dialog = gtk::AlertDialog::builder() + .message("Delete Profile") + .detail("Are you sure you want to delete this profile?") + .buttons(vec!["Cancel".to_string(), "Delete".to_string()]) + .cancel_button(0) + .default_button(1) + .modal(true) + .build(); + let window = btn.root() + .and_downcast::() + .expect("Button root is not a Window"); + + let profiles = profiles.clone(); + let entry = entry.clone(); + dialog.choose(Some(&window), None::<&gio::Cancellable>, move |result| { + if result.is_err() || result.unwrap() != 1 { + return; + } + + // remove config entry + let _ = config::edit_config(|config| { + config.game.remove(entry.index() as usize); + }); + + // remove ui entry + profiles.remove(&entry); + + // select next entry + let idx = entry.index() as usize; + if idx == 0 { + let state = STATE.get().unwrap().clone(); + if let Ok(mut state) = state.write() { + state.selected_game = None; + } + return; + } + + profiles.row_at_index(0).unwrap().activate(); + }); + }); + + profiles_.append(&entry_); +} diff --git a/ui/src/ui/main_handler.rs b/ui/src/ui/main_handler.rs new file mode 100644 index 0000000..3e683dd --- /dev/null +++ b/ui/src/ui/main_handler.rs @@ -0,0 +1,58 @@ +use adw::subclass::prelude::ObjectSubclassIsExt; +use gtk::prelude::RangeExt; + +use crate::{config, wrapper::pane, STATE}; + +// update the currently selected game configuration +fn update_game(update: F) { + if let Ok(state) = STATE.get().unwrap().try_read() { + if let Some(selected_game) = state.selected_game { + let _ = config::edit_config(|config| { + update(&mut config.game[selected_game]) + }); + } + } +} + +/// +/// Register signals for preset preferences. +/// +pub fn register_signals(main: &pane::PaneMain) { + let main = main.imp(); + let multiplier = main.multiplier.imp(); + let flow_scale = main.flow_scale.imp(); + let performance_mode = main.performance_mode.imp(); + let hdr_mode = main.hdr_mode.imp(); + let experimental_present_mode = main.experimental_present_mode.imp(); + + multiplier.number.connect_value_changed(|dropdown| { + update_game(|conf| { + conf.multiplier = (dropdown.value() as i64).into(); + }) + }); + flow_scale.slider.connect_value_changed(|slider| { + update_game(|conf| { + conf.flow_scale = (slider.value() / 100.0).into(); + }); + }); + performance_mode.switch.connect_state_notify(|switch| { + update_game(|conf| { + conf.performance_mode = switch.state(); + }); + }); + hdr_mode.switch.connect_state_notify(|switch| { + update_game(|conf| { + conf.hdr_mode = switch.state(); + }); + }); + experimental_present_mode.dropdown.connect_selected_notify(|dropdown| { + update_game(|conf| { + conf.experimental_present_mode = match dropdown.selected() { + 0 => config::PresentMode::Vsync, + 1 => config::PresentMode::Mailbox, + 2 => config::PresentMode::Immediate, + _ => config::PresentMode::Vsync, + }; + }); + }); +} diff --git a/ui/src/ui/sidebar_handler.rs b/ui/src/ui/sidebar_handler.rs new file mode 100644 index 0000000..1daecb9 --- /dev/null +++ b/ui/src/ui/sidebar_handler.rs @@ -0,0 +1,67 @@ +use adw::subclass::prelude::ObjectSubclassIsExt; +use gtk::prelude::{ButtonExt, ListBoxRowExt, RangeExt, WidgetExt}; + +use crate::{config, ui::entry_handler, wrapper::{pane, entry}, STATE}; + +/// +/// Register signals for adding and selecting presets. +/// +pub fn register_signals(sidebar_: &pane::PaneSidebar, main: pane::PaneMain) { + // activate signal + let state = STATE.get().unwrap().clone(); + sidebar_.imp().profiles.connect_row_activated(move |_, entry| { + // find config entry by index + let index = entry.index() as usize; + let config = config::get_config(); + if config.is_err() { + return; + } + let config = config.unwrap(); + let conf = config.game[index].clone(); + + // update main pane + let main = main.imp(); + let multiplier = main.multiplier.imp(); + let flow_scale = main.flow_scale.imp(); + let performance_mode = main.performance_mode.imp(); + let hdr_mode = main.hdr_mode.imp(); + let experimental_present_mode = main.experimental_present_mode.imp(); + + // (lock state early, so the ui update doesn't override the config) + if let Ok(mut state) = state.write() { + multiplier.number.set_value(conf.multiplier.into()); + flow_scale.slider.set_value(Into::::into(conf.flow_scale) * 100.0); + performance_mode.switch.set_state(conf.performance_mode); + hdr_mode.switch.set_state(conf.hdr_mode); + experimental_present_mode.dropdown.set_selected(conf.experimental_present_mode.into()); + + // update state + state.selected_game = Some(index); + } + }); + + // create signal + let sidebar = sidebar_.clone(); + sidebar_.imp().create.connect_clicked(move |_| { + // ensure no config entry with the same name exist + let config = config::get_config().unwrap(); + if config.game.iter().any(|e| e.exe == "new profile") { + return; + } + + // create config entry + let mut conf_entry = config::TomlGame::default(); + conf_entry.exe = "new profile".to_string(); + let _ = config::edit_config(|config| { + config.game.push(conf_entry.clone()); + }); + + // add entry to sidebar + let entry = entry::Entry::new(); + entry.set_exe(conf_entry.exe); + entry_handler::add_entry(entry.clone(), sidebar.imp().profiles.clone()); + + // select the new entry + entry.activate(); + }); +} diff --git a/ui/src/wrapper/pane/main.rs b/ui/src/wrapper/pane/main.rs index 853ea59..3cc8b67 100644 --- a/ui/src/wrapper/pane/main.rs +++ b/ui/src/wrapper/pane/main.rs @@ -7,15 +7,15 @@ use crate::wrapper::pref::*; #[template(resource = "/gay/pancake/lsfg-vk/pane/main.ui")] pub struct PaneMain { #[template_child] - pub pref_multiplier: TemplateChild, + pub multiplier: TemplateChild, #[template_child] - pub pref_flow_scale: TemplateChild, + pub flow_scale: TemplateChild, #[template_child] - pub pref_performance_mode: TemplateChild, + pub performance_mode: TemplateChild, #[template_child] - pub pref_hdr_mode: TemplateChild, + pub hdr_mode: TemplateChild, #[template_child] - pub pref_experimental_present_mode: TemplateChild + pub experimental_present_mode: TemplateChild } #[glib::object_subclass]