diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 4bd25ac..cf25b91 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,3 +1,5 @@ +mod settings; + use crate::{ features::{ self, @@ -6,6 +8,7 @@ use crate::{ force_close::ForceClose, launch::{Launch, Platform}, }, + gui::settings::Settings, util::{ self, consts::{ENHANCED, GTA_WINDOW_TITLE, LEGACY}, @@ -21,7 +24,6 @@ use std::{ time::{Duration, Instant}, }; -const THEME: catppuccin_egui::Theme = catppuccin_egui::MOCHA; const WINDOW_SIZE: [f32; 2] = [240.0, 240.0]; static CONFIG_PATH: LazyLock = LazyLock::new(|| { dirs::config_local_dir() @@ -33,12 +35,14 @@ static CONFIG_PATH: LazyLock = LazyLock::new(|| { #[derive(Serialize, Deserialize)] struct PersistentState { launcher: Platform, + settings: Settings, } #[derive(Debug, Default, PartialEq, Eq)] pub enum Stage { #[default] Main, + Settings, About, } @@ -54,6 +58,7 @@ pub struct Flags { #[derive(Debug)] pub struct App { + settings: Settings, stage: Stage, flags: Flags, pub sysinfo: sysinfo::System, @@ -67,6 +72,7 @@ pub struct App { impl Default for App { fn default() -> Self { Self { + settings: Settings::default(), stage: Stage::default(), flags: Flags::default(), sysinfo: sysinfo::System::new_all(), @@ -82,13 +88,7 @@ impl Default for App { impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { if !self.flags.initialized { - catppuccin_egui::set_theme(ctx, THEME); egui_extras::install_image_loaders(ctx); - if let Ok(config) = fs::read_to_string(CONFIG_PATH.as_path()) { - if let Ok(persistent_state) = serde_json::from_str::(&config) { - self.launch.selected = persistent_state.launcher; - } - } ctx.style_mut(|style| { style.spacing.item_spacing = egui::vec2(4.0, 4.0); style.interaction.selectable_labels = false; @@ -96,12 +96,14 @@ impl eframe::App for App { self.flags.elevated = util::win::is_elevated(); self.flags.initialized = true; } + catppuccin_egui::set_theme(ctx, self.settings.theme.into()); self.run_timers(); egui::TopBottomPanel::bottom("bottom_panel") .exact_height(25.0) .show(ctx, |ui| { ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| { ui.selectable_value(&mut self.stage, Stage::Main, "Main"); + ui.selectable_value(&mut self.stage, Stage::Settings, "Settings"); ui.selectable_value(&mut self.stage, Stage::About, "About"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let button = ui @@ -109,7 +111,9 @@ impl eframe::App for App { .on_hover_text("Relaunch ourselves as administrator.") .on_disabled_hover_text("We are already running elevated."); if button.clicked() { - util::win::elevate(&mut self.flags.closing); + util::win::elevate(util::win::ElevationExitMethod::Gentle( + &mut self.flags.closing, + )); } }); }); @@ -123,6 +127,7 @@ impl eframe::App for App { self.header(ui, "Network"); self.show_network(ctx, ui); } + Stage::Settings => self.show_settings(ctx, ui), Stage::About => self.show_about(ctx, ui), }); if check_debug_keycombo_pressed(ctx) { @@ -183,7 +188,7 @@ impl App { if ui.button("Launch").clicked() { features::launch::launch(&self.launch.selected); }; - egui::ComboBox::from_label("") + egui::ComboBox::from_id_salt("") .selected_text(self.launch.selected.to_string()) .width(120.0) .show_ui(ui, |ui| { @@ -261,7 +266,10 @@ impl App { fn show_network(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) { egui::Frame::new() .inner_margin(egui::vec2(4.0, 4.0)) - .stroke(egui::Stroke::new(1.0, THEME.overlay1)) + .stroke(egui::Stroke::new( + 1.0, + self.settings.theme.to_catppuccin().overlay1, + )) .show(ui, |ui| { let response = ui.add_enabled_ui(self.flags.elevated, |ui| { let label = ui.label("Game's network access"); @@ -312,6 +320,37 @@ impl App { }); } + fn show_settings(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + ui.label("Theme"); + egui::ComboBox::from_id_salt("") + .selected_text(self.settings.theme.to_string()) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.settings.theme, + settings::Theme::CatppuccinLatte, + settings::Theme::CatppuccinLatte.to_string(), + ); + ui.selectable_value( + &mut self.settings.theme, + settings::Theme::CatppuccinFrappe, + settings::Theme::CatppuccinFrappe.to_string(), + ); + ui.selectable_value( + &mut self.settings.theme, + settings::Theme::CatppuccinMacchiato, + settings::Theme::CatppuccinMacchiato.to_string(), + ); + ui.selectable_value( + &mut self.settings.theme, + settings::Theme::CatppuccinMocha, + settings::Theme::CatppuccinMocha.to_string(), + ); + }); + }); + ui.checkbox(&mut self.settings.start_elevated, "Always start elevated"); + } + fn show_debug(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) { ui.collapsing("times", |ui| { ui.label(format!( @@ -355,6 +394,7 @@ impl Drop for App { // save any persistent state to config file let persistent_state = PersistentState { launcher: self.launch.selected, + settings: self.settings.clone(), }; let config_path = CONFIG_PATH.as_path(); let config_path_parent = config_path.parent().unwrap(); @@ -394,6 +434,22 @@ fn load_icon() -> egui::IconData { } } +fn app_creator( + _cc: &eframe::CreationContext<'_>, +) -> Result, Box> { + let mut app = Box::::default(); + if let Ok(config) = fs::read_to_string(CONFIG_PATH.as_path()) { + if let Ok(persistent_state) = serde_json::from_str::(&config) { + app.launch.selected = persistent_state.launcher; + app.settings = persistent_state.settings; + } + } + if app.settings.start_elevated && !util::win::is_elevated() { + util::win::elevate(util::win::ElevationExitMethod::Forced); + } + Ok(app) +} + pub fn run() { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() @@ -404,10 +460,5 @@ pub fn run() { centered: true, ..Default::default() }; - eframe::run_native( - "GTA Tools", - options, - Box::new(|_cc| Ok(Box::::default())), - ) - .unwrap(); + eframe::run_native("GTA Tools", options, Box::new(app_creator)).unwrap(); } diff --git a/src/gui/settings.rs b/src/gui/settings.rs new file mode 100644 index 0000000..baf33c4 --- /dev/null +++ b/src/gui/settings.rs @@ -0,0 +1,54 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Theme { + CatppuccinLatte, + CatppuccinFrappe, + CatppuccinMacchiato, + CatppuccinMocha, +} + +impl Into for Theme { + fn into(self) -> catppuccin_egui::Theme { + match self { + Theme::CatppuccinLatte => catppuccin_egui::LATTE, + Theme::CatppuccinFrappe => catppuccin_egui::FRAPPE, + Theme::CatppuccinMacchiato => catppuccin_egui::MACCHIATO, + Theme::CatppuccinMocha => catppuccin_egui::MOCHA, + } + } +} + +impl Display for Theme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let x = match self { + Theme::CatppuccinLatte => "Catppuccin Latte", + Theme::CatppuccinFrappe => "Catppuccin Frappe", + Theme::CatppuccinMacchiato => "Catppuccin Macchiato", + Theme::CatppuccinMocha => "Catppuccin Mocha", + }; + write!(f, "{x}") + } +} + +impl Theme { + pub fn to_catppuccin(&self) -> catppuccin_egui::Theme { + (*self).into() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Settings { + pub theme: Theme, + pub start_elevated: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + theme: Theme::CatppuccinMocha, + start_elevated: false, + } + } +} diff --git a/src/util/win.rs b/src/util/win.rs index 2861dbd..64d1483 100644 --- a/src/util/win.rs +++ b/src/util/win.rs @@ -6,13 +6,17 @@ use windows::{ UI::{ Input::KeyboardAndMouse::GetAsyncKeyState, Shell::ShellExecuteW, - WindowsAndMessaging::SW_HIDE, - WindowsAndMessaging::{GetForegroundWindow, GetWindowTextW}, + WindowsAndMessaging::{GetForegroundWindow, GetWindowTextW, SW_NORMAL}, }, }, core::{HSTRING, PCWSTR}, }; +pub enum ElevationExitMethod<'a> { + Gentle(&'a mut bool), + Forced, +} + #[allow(clippy::cast_sign_loss)] pub fn is_window_focused(target_title: &str) -> bool { unsafe { @@ -29,7 +33,7 @@ pub fn is_any_key_pressed(keys: &[u8]) -> bool { .any(|&key| unsafe { (i32::from(GetAsyncKeyState(i32::from(key))) & 0x8000) != 0 }) } -pub fn elevate(closing: &mut bool) { +pub fn elevate(closing: ElevationExitMethod) { let exe = std::env::current_exe().unwrap(); unsafe { ShellExecuteW( @@ -38,10 +42,13 @@ pub fn elevate(closing: &mut bool) { &HSTRING::from(exe.as_path()), PCWSTR::null(), PCWSTR::null(), - SW_HIDE, + SW_NORMAL, ); } - *closing = true; + match closing { + ElevationExitMethod::Gentle(closing) => *closing = true, + ElevationExitMethod::Forced => std::process::exit(0), + } } pub fn is_elevated() -> bool {