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
-
-
+
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]