ui: bugfixes and cleanup

This commit is contained in:
PancakeTAS 2025-07-24 02:39:58 +02:00 committed by Pancake
parent f4f18a6e6d
commit 717b0d2cd6
9 changed files with 254 additions and 209 deletions

View file

@ -31,19 +31,19 @@
<property name="title">Frame Generation</property>
<!-- Frame Generation: Multiplier -->
<child>
<object class="LSPrefNumber" id="pref_multiplier">
<object class="LSPrefNumber" id="multiplier">
<property name="opt-name">Multiplier</property>
</object>
</child>
<!-- Frame Generation: Flow Scale -->
<child>
<object class="LSPrefSlider" id="pref_flow_scale">
<object class="LSPrefSlider" id="flow_scale">
<property name="opt-name">Flow Scale</property>
</object>
</child>
<!-- Frame Generation: Performance Mode -->
<child>
<object class="LSPrefSwitch" id="pref_performance_mode">
<object class="LSPrefSwitch" id="performance_mode">
<property name="opt-name">Performance Mode</property>
<property name="default-state">false</property>
</object>
@ -56,14 +56,14 @@
<property name="title">Misc</property>
<!-- Misc: HDR Mode -->
<child>
<object class="LSPrefSwitch" id="pref_hdr_mode">
<object class="LSPrefSwitch" id="hdr_mode">
<property name="opt-name">HDR Mode</property>
<property name="default-state">false</property>
</object>
</child>
<!-- Misc: Experimental Present Mode -->
<child>
<object class="LSPrefDropdown" id="pref_experimental_present_mode">
<object class="LSPrefDropdown" id="experimental_present_mode">
<property name="opt-name">Experimental Present Mode</property>
<property name="default-selection">0</property>
<property name="options">

View file

@ -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");

View file

@ -39,6 +39,25 @@ pub enum PresentMode {
impl Default for PresentMode {
fn default() -> Self { PresentMode::Vsync }
}
impl From<i64> for PresentMode {
fn from(value: i64) -> Self {
match value {
0 => PresentMode::Vsync,
1 => PresentMode::Mailbox,
2 => PresentMode::Immediate,
_ => PresentMode::Vsync,
}
}
}
impl Into<u32> 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)]

View file

@ -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::<gtk::Window>()
.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::<f64>::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();
}

33
ui/src/ui.rs Normal file
View file

@ -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();
}

View file

@ -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::<gtk::Window>()
.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_);
}

58
ui/src/ui/main_handler.rs Normal file
View file

@ -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<F: FnOnce(&mut config::TomlGame)>(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,
};
});
});
}

View file

@ -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::<f64>::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();
});
}

View file

@ -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<PrefNumber>,
pub multiplier: TemplateChild<PrefNumber>,
#[template_child]
pub pref_flow_scale: TemplateChild<PrefSlider>,
pub flow_scale: TemplateChild<PrefSlider>,
#[template_child]
pub pref_performance_mode: TemplateChild<PrefSwitch>,
pub performance_mode: TemplateChild<PrefSwitch>,
#[template_child]
pub pref_hdr_mode: TemplateChild<PrefSwitch>,
pub hdr_mode: TemplateChild<PrefSwitch>,
#[template_child]
pub pref_experimental_present_mode: TemplateChild<PrefDropdown>
pub experimental_present_mode: TemplateChild<PrefDropdown>
}
#[glib::object_subclass]