From ea6094daf5d9546e6f6dabc36de16bddacc36974 Mon Sep 17 00:00:00 2001 From: Observer KRypt0n_ Date: Wed, 3 Aug 2022 21:38:01 +0200 Subject: [PATCH] 0.7.0 - added gamescope support - now gamemode option will not be clickabke if gamemode is not installed - reworked enhancements page's events - changed winesync env variables - added `lib::is_available` function to check packages availability --- Cargo.lock | 2 +- Cargo.toml | 2 +- assets/resources.xml | 1 + assets/ui/preferences/enhancements.blp | 17 +- assets/ui/preferences/gamescope.blp | 111 ++++++++++++ src/lib/config/mod.rs | 120 ++++++++++++- src/lib/config/wine_sync.rs | 22 +-- src/lib/game.rs | 25 +-- src/lib/mod.rs | 21 +++ src/ui/first_run/mod.rs | 33 ++-- src/ui/main.rs | 2 +- src/ui/preferences/enhancements.rs | 174 ++++++++++-------- src/ui/preferences/gamescope.rs | 235 +++++++++++++++++++++++++ src/ui/preferences/mod.rs | 6 +- 14 files changed, 631 insertions(+), 140 deletions(-) create mode 100644 assets/ui/preferences/gamescope.blp create mode 100644 src/ui/preferences/gamescope.rs diff --git a/Cargo.lock b/Cargo.lock index f94fb07..8f9d397 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,7 +51,7 @@ dependencies = [ [[package]] name = "anime-game-launcher" -version = "0.6.3" +version = "0.7.0" dependencies = [ "anime-game-core", "dirs", diff --git a/Cargo.toml b/Cargo.toml index ceb2a27..1cc7dde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "anime-game-launcher" -version = "0.6.3" +version = "0.7.0" description = "Anime Game launcher" authors = ["Nikita Podvirnyy "] license = "GPL-3.0" diff --git a/assets/resources.xml b/assets/resources.xml index 2997889..856b712 100644 --- a/assets/resources.xml +++ b/assets/resources.xml @@ -19,6 +19,7 @@ ui/.dist/preferences/general.ui ui/.dist/preferences/enhancements.ui + ui/.dist/preferences/gamescope.ui ui/.dist/preferences/environment.ui diff --git a/assets/ui/preferences/enhancements.blp b/assets/ui/preferences/enhancements.blp index 0ac9c96..6f2b821 100644 --- a/assets/ui/preferences/enhancements.blp +++ b/assets/ui/preferences/enhancements.blp @@ -73,7 +73,7 @@ Adw.PreferencesPage page { } } - Adw.ActionRow { + Adw.ActionRow gamemode_row { title: "Gamemode"; subtitle: "This prioritizes the game over the rest of the processes"; @@ -81,5 +81,20 @@ Adw.PreferencesPage page { valign: center; } } + + Adw.ActionRow gamescope_row { + title: "Gamescope"; + + Gtk.Button gamescope_settings { + icon-name: "emblem-system-symbolic"; + valign: center; + + styles ["flat"] + } + + Gtk.Switch gamescope_switcher { + valign: center; + } + } } } diff --git a/assets/ui/preferences/gamescope.blp b/assets/ui/preferences/gamescope.blp new file mode 100644 index 0000000..e25e107 --- /dev/null +++ b/assets/ui/preferences/gamescope.blp @@ -0,0 +1,111 @@ +using Gtk 4.0; +using Adw 1; + +Adw.PreferencesWindow window { + title: "Gamescope"; + + modal: true; + hide-on-close: true; + + Adw.PreferencesPage { + Adw.PreferencesGroup { + title: "Game resolution"; + + Adw.ActionRow { + title: "Width"; + + Gtk.Entry game_width { + placeholder-text: "0"; + valign: center; + input-purpose: digits; + } + } + + Adw.ActionRow { + title: "Height"; + + Gtk.Entry game_height { + placeholder-text: "0"; + valign: center; + input-purpose: digits; + } + } + } + + Adw.PreferencesGroup { + title: "Gamescope resolution"; + + Adw.ActionRow { + title: "Width"; + + Gtk.Entry gamescope_width { + placeholder-text: "0"; + valign: center; + input-purpose: digits; + } + } + + Adw.ActionRow { + title: "Height"; + + Gtk.Entry gamescope_height { + placeholder-text: "0"; + valign: center; + input-purpose: digits; + } + } + } + + Adw.PreferencesGroup { + title: "Other settings"; + + Adw.ActionRow { + title: "Framerate limit"; + + Gtk.Entry framerate_limit { + placeholder-text: "0"; + valign: center; + input-purpose: digits; + } + } + + Adw.ActionRow { + title: "Unfocused framerate limit"; + + Gtk.Entry framerate_unfocused_limit { + placeholder-text: "0"; + valign: center; + input-purpose: digits; + } + } + + Adw.ActionRow { + title: "Use integer scaling"; + + Gtk.Switch integer_scaling { + valign: center; + } + } + + Adw.ActionRow { + title: "Window type"; + + Gtk.Box { + orientation: horizontal; + + Gtk.ToggleButton borderless { + label: "Borderless"; + valign: center; + } + + Gtk.ToggleButton fullscreen { + label: "Fullscreen"; + valign: center; + } + + styles ["linked"] + } + } + } + } +} diff --git a/src/lib/config/mod.rs b/src/lib/config/mod.rs index d45afcf..a8ee1ef 100644 --- a/src/lib/config/mod.rs +++ b/src/lib/config/mod.rs @@ -3,10 +3,10 @@ use std::fs::File; use std::io::Read; use std::path::Path; use std::io::{Error, ErrorKind, Write}; -use std::process::{Command, Stdio}; use serde::{Serialize, Deserialize}; +use crate::lib; use super::consts::*; use super::wine::{ Version as WineVersion, @@ -148,9 +148,12 @@ impl Config { // ???? None }, - None => match Command::new("wine").stdout(Stdio::null()).stderr(Stdio::null()).output() { - Ok(output) => if output.status.success() { Some(String::from("wine")) } else { None }, - Err(_) => None + None => { + if lib::is_available("wine") { + Some(String::from("wine")) + } else { + None + } } } } @@ -176,6 +179,65 @@ impl Config { None => None } } + + pub fn get_gamescope_command(&self) -> Option { + // https://github.com/bottlesdevs/Bottles/blob/b908311348ed1184ead23dd76f9d8af41ff24082/src/backend/wine/winecommand.py#L478 + if self.game.enhancements.gamescope.enabled { + let mut gamescope = String::from("gamescope"); + + // Set window type + match self.game.enhancements.gamescope.window_type { + WindowType::Borderless => gamescope += " -b", + WindowType::Fullscreen => gamescope += " -f" + } + + // Set game width + if self.game.enhancements.gamescope.game.width > 0 { + gamescope += &format!(" -w {}", self.game.enhancements.gamescope.game.width); + } + + // Set game height + if self.game.enhancements.gamescope.game.height > 0 { + gamescope += &format!(" -h {}", self.game.enhancements.gamescope.game.height); + } + + // Set gamescope width + if self.game.enhancements.gamescope.gamescope.width > 0 { + gamescope += &format!(" -W {}", self.game.enhancements.gamescope.gamescope.width); + } + + // Set gamescope height + if self.game.enhancements.gamescope.gamescope.height > 0 { + gamescope += &format!(" -H {}", self.game.enhancements.gamescope.gamescope.height); + } + + // Set focused framerate limit + if self.game.enhancements.gamescope.framerate.focused > 0 { + gamescope += &format!(" -r {}", self.game.enhancements.gamescope.framerate.focused); + } + + // Set unfocused framerate limit + if self.game.enhancements.gamescope.framerate.unfocused > 0 { + gamescope += &format!(" -o {}", self.game.enhancements.gamescope.framerate.unfocused); + } + + // Set integer scaling + if self.game.enhancements.gamescope.integer_scaling { + gamescope += " -n"; + } + + // Set FSR support + if self.game.enhancements.fsr.enabled { + gamescope += " -U"; + } + + Some(gamescope) + } + + else { + None + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -314,7 +376,8 @@ impl Default for Dxvk { pub struct Enhancements { pub fsr: Fsr, pub gamemode: bool, - pub hud: HUD + pub hud: HUD, + pub gamescope: Gamescope } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] @@ -347,3 +410,50 @@ impl Fsr { } } } + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Gamescope { + pub enabled: bool, + pub game: Size, + pub gamescope: Size, + pub framerate: Framerate, + pub integer_scaling: bool, + pub window_type: WindowType +} + +impl Default for Gamescope { + fn default() -> Self { + Self { + enabled: false, + game: Size::default(), + gamescope: Size::default(), + framerate: Framerate::default(), + integer_scaling: true, + window_type: WindowType::default() + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +pub struct Size { + pub width: u16, + pub height: u16 +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +pub struct Framerate { + pub focused: u16, + pub unfocused: u16 +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum WindowType { + Borderless, + Fullscreen +} + +impl Default for WindowType { + fn default() -> Self { + Self::Borderless + } +} diff --git a/src/lib/config/wine_sync.rs b/src/lib/config/wine_sync.rs index bb8e1ca..83c92cc 100644 --- a/src/lib/config/wine_sync.rs +++ b/src/lib/config/wine_sync.rs @@ -44,20 +44,12 @@ impl Into for WineSync { impl WineSync { /// Get environment variables corresponding to used wine sync pub fn get_env_vars(&self) -> HashMap<&str, &str> { - match self { - Self::None => HashMap::new(), - Self::ESync => HashMap::from([ - ("WINEESYNC", "1") - ]), - Self::FSync => HashMap::from([ - ("WINEESYNC", "1"), - ("WINEFSYNC", "1") - ]), - Self::Futex2 => HashMap::from([ - ("WINEESYNC", "1"), - ("WINEFSYNC", "1"), - ("WINEFSYNC_FUTEX2", "1") - ]) - } + HashMap::from([(match self { + Self::None => return HashMap::new(), + + Self::ESync => "WINEESYNC", + Self::FSync => "WINEFSYNC", + Self::Futex2 => "WINEFSYNC_FUTEX2" + }, "1")]) } } diff --git a/src/lib/game.rs b/src/lib/game.rs index 7365535..2645dd7 100644 --- a/src/lib/game.rs +++ b/src/lib/game.rs @@ -7,7 +7,7 @@ use anime_game_core::telemetry; use super::consts; use super::config; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/*#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Terminal { GnomeTerminal, Konsole, @@ -59,7 +59,7 @@ pub fn try_get_terminal() -> Option { } None -} +}*/ /// Try to run the game /// @@ -90,26 +90,17 @@ pub fn run(debug: bool) -> std::io::Result<()> { bash_chain += "gamemoderun "; } - bash_chain += &format!("'{}' ", wine_executable); + bash_chain += &format!("'{wine_executable}' "); if debug { - // Is not supported now because new spawned terminal needs - // to have cwd and env variables specified directly - // which is kinda difficult todo!(); - - /*match try_get_terminal() { - Some(terminal) => { - command = Command::new(terminal.get_command()); - - command.args(terminal.get_args("launcher.bat")); - }, - None => return Err(Error::new(ErrorKind::Other, "Couldn't find terminal application")) - }*/ + } else { + bash_chain += "launcher.bat"; } - else { - bash_chain += "launcher.bat"; + // gamescope -- + if let Some(gamescope) = config.get_gamescope_command() { + bash_chain = format!("{gamescope} -- {bash_chain}"); } let bash_chain = match config.game.command { diff --git a/src/lib/mod.rs b/src/lib/mod.rs index c07ab63..3965581 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -6,3 +6,24 @@ pub mod wine; pub mod wine_prefix; pub mod launcher; pub mod prettify_bytes; + +use std::process::{Command, Stdio}; + +/// Check if specified binary is available +/// +/// ``` +/// use crate::lib; +/// +/// assert!(lib::is_available("bash")); +/// ``` +#[allow(unused_must_use)] +pub fn is_available(binary: &str) -> bool { + match Command::new(binary).stdout(Stdio::null()).stderr(Stdio::null()).spawn() { + Ok(mut child) => { + child.kill(); + + true + }, + Err(_) => false + } +} diff --git a/src/ui/first_run/mod.rs b/src/ui/first_run/mod.rs index e84ee2e..936d3dc 100644 --- a/src/ui/first_run/mod.rs +++ b/src/ui/first_run/mod.rs @@ -6,7 +6,7 @@ use gtk::glib::clone; use std::rc::Rc; use std::cell::Cell; -use std::process::{Command, Stdio}; +use std::process::Command; use anime_game_core::prelude::*; @@ -20,6 +20,7 @@ mod page_6; use crate::ui::*; use crate::ui::components::progress_bar::*; +use crate::lib; use crate::lib::wine_prefix::WinePrefix; use crate::lib::config; @@ -100,14 +101,6 @@ impl Actions { } } -/// This enum is used to store some of this application data -/// -/// In this example we store a counter here to know what should we increment or decrement -/// -/// This must implement `Default` trait -#[derive(Debug, Default)] -pub struct Values; - /// The main application structure /// /// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets @@ -122,7 +115,6 @@ pub struct Values; #[derive(Clone)] pub struct App { widgets: AppWidgets, - // values: Rc>, actions: Rc>>> } @@ -132,7 +124,6 @@ impl App { // Get default widgets from ui file and add events to them let result = Self { widgets: AppWidgets::try_get()?, - // values: Default::default(), actions: Default::default() }.init_events().init_actions(); @@ -176,20 +167,18 @@ impl App { match action { Actions::FirstPageContinue => { - match Command::new("git").stdout(Stdio::null()).spawn() { - Ok(_) => match Command::new("xdelta3").stdout(Stdio::null()).spawn() { - Ok(_) => this.widgets.carousel.scroll_to(&this.widgets.page_3.page, true), - Err(_) => this.widgets.carousel.scroll_to(&this.widgets.page_2.page, true) - }, - Err(_) => this.widgets.carousel.scroll_to(&this.widgets.page_2.page, true) - } + this.widgets.carousel.scroll_to({ + if lib::is_available("git") && lib::is_available("xdelta3") { + &this.widgets.page_3.page + } else { + &this.widgets.page_2.page + } + }, true); } Actions::SecondPageCheck => { - if let Ok(_) = Command::new("git").stdout(Stdio::null()).spawn() { - if let Ok(_) = Command::new("xdelta3").stdout(Stdio::null()).spawn() { - this.widgets.carousel.scroll_to(&this.widgets.page_3.page, true); - } + if lib::is_available("git") && lib::is_available("xdelta3") { + this.widgets.carousel.scroll_to(&this.widgets.page_3.page, true); } } diff --git a/src/ui/main.rs b/src/ui/main.rs index c72c73e..7aa95aa 100644 --- a/src/ui/main.rs +++ b/src/ui/main.rs @@ -80,7 +80,7 @@ impl AppWidgets { get_object(&builder, "progress_bar_group")? ), - preferences_stack: PreferencesStack::new()? + preferences_stack: PreferencesStack::new(&window)? }; // Set devel style to ApplicationWindow if it's debug mode diff --git a/src/ui/preferences/enhancements.rs b/src/ui/preferences/enhancements.rs index bf117a0..68474e0 100644 --- a/src/ui/preferences/enhancements.rs +++ b/src/ui/preferences/enhancements.rs @@ -1,16 +1,16 @@ use gtk4 as gtk; use libadwaita::{self as adw, prelude::*}; -use gtk4::glib; -use gtk4::glib::clone; +use gtk::glib; +use gtk::glib::clone; -use std::rc::Rc; -use std::cell::Cell; -use std::io::Error; - -use crate::ui::get_object; +use crate::lib; use crate::lib::config; +use crate::ui::*; + +use super::gamescope::App as GamescopeApp; + /// This structure is used to describe widgets used in application /// /// `AppWidgets::try_get` function loads UI file from `.assets/ui/.dist` folder and returns structure with references to its widgets @@ -26,11 +26,19 @@ pub struct AppWidgets { pub hud_combo: adw::ComboRow, pub fsr_combo: adw::ComboRow, pub fsr_switcher: gtk::Switch, - pub gamemode_switcher: gtk::Switch + + pub gamemode_row: adw::ActionRow, + pub gamemode_switcher: gtk::Switch, + + pub gamescope_row: adw::ActionRow, + pub gamescope_settings: gtk::Button, + pub gamescope_switcher: gtk::Switch, + + pub gamescope_app: GamescopeApp } impl AppWidgets { - fn try_get() -> Result { + fn try_get(window: &adw::ApplicationWindow) -> Result { let builder = gtk::Builder::from_resource("/org/app/ui/preferences/enhancements.ui"); let result = Self { @@ -42,29 +50,33 @@ impl AppWidgets { hud_combo: get_object(&builder, "hud_combo")?, fsr_combo: get_object(&builder, "fsr_combo")?, fsr_switcher: get_object(&builder, "fsr_switcher")?, - gamemode_switcher: get_object(&builder, "gamemode_switcher")? + + gamemode_row: get_object(&builder, "gamemode_row")?, + gamemode_switcher: get_object(&builder, "gamemode_switcher")?, + + gamescope_row: get_object(&builder, "gamescope_row")?, + gamescope_settings: get_object(&builder, "gamescope_settings")?, + gamescope_switcher: get_object(&builder, "gamescope_switcher")?, + + gamescope_app: GamescopeApp::new(window)? }; + // Disable gamemode row if it's not available + if !lib::is_available("gamemoderun") { + result.gamemode_row.set_sensitive(false); + result.gamemode_row.set_tooltip_text(Some("Gamemode is not installed")); + } + + // Disable gamescope row if it's not available + if !lib::is_available("gamescope") { + result.gamescope_row.set_sensitive(false); + result.gamescope_row.set_tooltip_text(Some("Gamescope is not installed")); + } + Ok(result) } } -/// This enum is used to describe an action inside of this application -/// -/// It may be helpful if you want to add the same event for several widgets, or call an action inside of another action -#[derive(Debug)] -pub enum Actions { - OptionSelection(fn(crate::lib::config::Config, T) -> crate::lib::config::Config, T) -} - -/// This enum is used to store some of this application data -/// -/// In this example we store a counter here to know what should we increment or decrement -/// -/// This must implement `Default` trait -#[derive(Debug, Default, glib::Downgrade)] -pub struct Values; - /// The main application structure /// /// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets @@ -78,16 +90,14 @@ pub struct Values; /// That's what we need and what we use in `App::update` method #[derive(Clone, glib::Downgrade)] pub struct App { - widgets: AppWidgets, - values: Rc> + widgets: AppWidgets } impl App { /// Create new application - pub fn new() -> Result { + pub fn new(window: &adw::ApplicationWindow) -> Result { let result = Self { - widgets: AppWidgets::try_get()?, - values: Default::default() + widgets: AppWidgets::try_get(window)? }.init_events(); Ok(result) @@ -96,25 +106,31 @@ impl App { /// Add default events and values to the widgets fn init_events(self) -> Self { // Wine sync selection - self.widgets.sync_combo.connect_selected_notify(clone!(@weak self as this => move |hud| { - this.update(Actions::OptionSelection(|mut config, value| { - config.game.wine.sync = value; config - }, config::WineSync::try_from(hud.selected()).unwrap())); - })); + self.widgets.sync_combo.connect_selected_notify(move |row| { + if let Ok(mut config) = config::get() { + config.game.wine.sync = config::WineSync::try_from(row.selected()).unwrap(); + + config::update(config); + } + }); // Wine language selection - self.widgets.wine_lang.connect_selected_notify(clone!(@weak self as this => move |hud| { - this.update(Actions::OptionSelection(|mut config, value| { - config.game.wine.language = value; config - }, config::WineLang::try_from(hud.selected()).unwrap())); - })); + self.widgets.wine_lang.connect_selected_notify(move |row| { + if let Ok(mut config) = config::get() { + config.game.wine.language = config::WineLang::try_from(row.selected()).unwrap(); + + config::update(config); + } + }); // HUD selection - self.widgets.hud_combo.connect_selected_notify(clone!(@weak self as this => move |hud| { - this.update(Actions::OptionSelection(|mut config, value| { - config.game.enhancements.hud = value; config - }, config::HUD::try_from(hud.selected()).unwrap())); - })); + self.widgets.hud_combo.connect_selected_notify(move |row| { + if let Ok(mut config) = config::get() { + config.game.enhancements.hud = config::HUD::try_from(row.selected()).unwrap(); + + config::update(config); + } + }); // FSR strength selection // @@ -124,42 +140,47 @@ impl App { // Performance = 2 // // Source: Bottles (https://github.com/bottlesdevs/Bottles/blob/22fa3573a13f4e9b9c429e4cdfe4ca29787a2832/src/ui/details-preferences.ui#L88) - self.widgets.fsr_combo.connect_selected_notify(clone!(@weak self as this => move |hud| { - this.update(Actions::OptionSelection(|mut config, value| { - config.game.enhancements.fsr.strength = value; config - }, 5 - hud.selected())); - })); + self.widgets.fsr_combo.connect_selected_notify(move |row| { + if let Ok(mut config) = config::get() { + config.game.enhancements.fsr.strength = 5 - row.selected(); + + config::update(config); + } + }); // FSR switching - self.widgets.fsr_switcher.connect_state_notify(clone!(@weak self as this => move |switcher| { - this.update(Actions::OptionSelection(|mut config, value| { - config.game.enhancements.fsr.enabled = value; config - }, switcher.state())); - })); + self.widgets.fsr_switcher.connect_state_notify(move |switch| { + if let Ok(mut config) = config::get() { + config.game.enhancements.fsr.enabled = switch.state(); + + config::update(config); + } + }); // Gamemode switching - self.widgets.gamemode_switcher.connect_state_notify(clone!(@weak self as this => move |switcher| { - this.update(Actions::OptionSelection(|mut config, value| { - config.game.enhancements.gamemode = value; config - }, switcher.state())); + self.widgets.gamemode_switcher.connect_state_notify(move |switch| { + if let Ok(mut config) = config::get() { + config.game.enhancements.gamemode = switch.state(); + + config::update(config); + } + }); + + // Gamescope settings app + self.widgets.gamescope_settings.connect_clicked(clone!(@weak self as this => move |_| { + this.widgets.gamescope_app.show(); })); - self - } + // Gamescope swithing + self.widgets.gamescope_switcher.connect_state_notify(move |switch| { + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.enabled = switch.state(); - /// Update widgets state by calling some action - pub fn update(&self, action: Actions) { - let values = self.values.take(); - - match action { - Actions::OptionSelection(update, value) => { - if let Ok(config) = config::get() { - config::update((update)(config, value)); - } + config::update(config); } - } + }); - self.values.set(values); + self } pub fn title() -> String { @@ -171,10 +192,10 @@ impl App { } /// This method is being called by the `PreferencesStack::update` - pub fn prepare(&self, status_page: &adw::StatusPage) -> Result<(), Error> { + pub fn prepare(&self, status_page: &adw::StatusPage) -> std::io::Result<()> { let config = config::get()?; - status_page.set_description(Some("Loading preferences...")); + status_page.set_description(Some("Loading enhancements...")); // Update Wine sync self.widgets.sync_combo.set_selected(config.game.wine.sync.into()); @@ -194,6 +215,9 @@ impl App { // Gamemode switching self.widgets.gamemode_switcher.set_state(config.game.enhancements.gamemode); + // Prepare gamescope settings app + self.widgets.gamescope_app.prepare(status_page)?; + Ok(()) } } diff --git a/src/ui/preferences/gamescope.rs b/src/ui/preferences/gamescope.rs new file mode 100644 index 0000000..d7e3a42 --- /dev/null +++ b/src/ui/preferences/gamescope.rs @@ -0,0 +1,235 @@ +use gtk4 as gtk; +use libadwaita::{self as adw, prelude::*}; + +use gtk::glib; + +use crate::ui::get_object; +use crate::lib::config; + +/// This structure is used to describe widgets used in application +/// +/// `AppWidgets::try_get` function loads UI file from `.assets/ui/.dist` folder and returns structure with references to its widgets +/// +/// This function does not implement events +#[derive(Clone, glib::Downgrade)] +pub struct AppWidgets { + pub window: adw::PreferencesWindow, + + pub game_width: gtk::Entry, + pub game_height: gtk::Entry, + + pub gamescope_width: gtk::Entry, + pub gamescope_height: gtk::Entry, + + pub framerate_limit: gtk::Entry, + pub framerate_unfocused_limit: gtk::Entry, + pub integer_scaling: gtk::Switch, + + pub borderless: gtk::ToggleButton, + pub fullscreen: gtk::ToggleButton +} + +impl AppWidgets { + fn try_get(window: &adw::ApplicationWindow) -> Result { + let builder = gtk::Builder::from_resource("/org/app/ui/preferences/gamescope.ui"); + + let result = Self { + window: get_object(&builder, "window")?, + + game_width: get_object(&builder, "game_width")?, + game_height: get_object(&builder, "game_height")?, + + gamescope_width: get_object(&builder, "gamescope_width")?, + gamescope_height: get_object(&builder, "gamescope_height")?, + + framerate_limit: get_object(&builder, "framerate_limit")?, + framerate_unfocused_limit: get_object(&builder, "framerate_unfocused_limit")?, + integer_scaling: get_object(&builder, "integer_scaling")?, + + borderless: get_object(&builder, "borderless")?, + fullscreen: get_object(&builder, "fullscreen")? + }; + + result.window.set_transient_for(Some(window)); + + Ok(result) + } +} + +/// The main application structure +/// +/// `Default` macro automatically calls `AppWidgets::default`, i.e. loads UI file and reference its widgets +/// +/// `Rc>` means this: +/// - `Rc` addeds ability to reference the same value from various clones of the structure. +/// This will guarantee us that inner `Cell` is the same for all the `App::clone()` values +/// - `Cell` addeds inner mutability to its value, so we can mutate it even without mutable reference. +/// +/// So we have a shared reference to some value that can be changed without mutable reference. +/// That's what we need and what we use in `App::update` method +#[derive(Clone, glib::Downgrade)] +pub struct App { + widgets: AppWidgets +} + +impl App { + /// Create new application + pub fn new(window: &adw::ApplicationWindow) -> Result { + let result = Self { + widgets: AppWidgets::try_get(window)? + }.init_events(); + + Ok(result) + } + + /// Add default events and values to the widgets + fn init_events(self) -> Self { + // Game width + self.widgets.game_width.connect_changed(move |entry| { + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.game.width = entry.text().parse().unwrap_or(0); + + config::update(config); + } + }); + + // Game height + self.widgets.game_height.connect_changed(move |entry| { + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.game.height = entry.text().parse().unwrap_or(0); + + config::update(config); + } + }); + + // Gamescope width + self.widgets.gamescope_width.connect_changed(move |entry| { + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.gamescope.width = entry.text().parse().unwrap_or(0); + + config::update(config); + } + }); + + // Gamescope height + self.widgets.gamescope_height.connect_changed(move |entry| { + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.gamescope.height = entry.text().parse().unwrap_or(0); + + config::update(config); + } + }); + + // Framerate focused + self.widgets.framerate_limit.connect_changed(move |entry| { + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.framerate.focused = entry.text().parse().unwrap_or(0); + + config::update(config); + } + }); + + // Framerate unfocused + self.widgets.framerate_unfocused_limit.connect_changed(move |entry| { + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.framerate.unfocused = entry.text().parse().unwrap_or(0); + + config::update(config); + } + }); + + // Use integer scaling + self.widgets.integer_scaling.connect_state_notify(move |switch| { + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.integer_scaling = switch.state(); + + config::update(config); + } + }); + + // Window type + + let borderless = self.widgets.borderless.clone(); + let fullscreen = self.widgets.fullscreen.clone(); + + // Window type (Borderless) + self.widgets.borderless.connect_clicked(move |button| { + if !button.is_active() { + button.activate(); + } + + else { + fullscreen.set_active(false); + + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.window_type = if button.is_active() { + config::WindowType::Borderless + } else { + config::WindowType::Fullscreen + }; + + config::update(config); + } + } + }); + + // Window type (Fullscreen) + self.widgets.fullscreen.connect_clicked(move |button| { + if !button.is_active() { + button.activate(); + } + + else { + borderless.set_active(false); + + if let Ok(mut config) = config::get() { + config.game.enhancements.gamescope.window_type = if button.is_active() { + config::WindowType::Fullscreen + } else { + config::WindowType::Borderless + }; + + config::update(config); + } + } + }); + + self + } + + /// This method is being called by the `EnhancementsPage::prepare` + pub fn prepare(&self, status_page: &adw::StatusPage) -> std::io::Result<()> { + let config = config::get()?; + + status_page.set_description(Some("Loading gamescope...")); + + fn set_text(widget: >k::Entry, value: u16) { + widget.set_text(&if value == 0 { String::new() } else { value.to_string() }); + } + + set_text(&self.widgets.game_width, config.game.enhancements.gamescope.game.width); + set_text(&self.widgets.game_height, config.game.enhancements.gamescope.game.height); + + set_text(&self.widgets.gamescope_width, config.game.enhancements.gamescope.gamescope.width); + set_text(&self.widgets.gamescope_height, config.game.enhancements.gamescope.gamescope.height); + + set_text(&self.widgets.framerate_limit, config.game.enhancements.gamescope.framerate.focused); + set_text(&self.widgets.framerate_unfocused_limit, config.game.enhancements.gamescope.framerate.unfocused); + + self.widgets.integer_scaling.set_state(config.game.enhancements.gamescope.integer_scaling); + + match config.game.enhancements.gamescope.window_type { + config::WindowType::Borderless => self.widgets.borderless.set_active(true), + config::WindowType::Fullscreen => self.widgets.fullscreen.set_active(true) + }; + + Ok(()) + } + + pub fn show(&self) { + self.widgets.window.show(); + } +} + +unsafe impl Send for App {} +unsafe impl Sync for App {} diff --git a/src/ui/preferences/mod.rs b/src/ui/preferences/mod.rs index 1867030..7f9c038 100644 --- a/src/ui/preferences/mod.rs +++ b/src/ui/preferences/mod.rs @@ -14,6 +14,8 @@ mod general; mod enhancements; mod environment; +pub mod gamescope; + pub mod pages { pub use super::general::App as GeneralPage; pub use super::enhancements::App as EnhancementsPage; @@ -38,7 +40,7 @@ pub struct PreferencesStack { } impl PreferencesStack { - pub fn new() -> Result { + pub fn new(window: &adw::ApplicationWindow) -> Result { let builder = gtk::Builder::from_resource("/org/app/ui/preferences.ui"); let result = Self { @@ -53,7 +55,7 @@ impl PreferencesStack { stack: get_object(&builder, "stack")?, general_page: pages::GeneralPage::new()?, - enhancements_page: pages::EnhancementsPage::new()?, + enhancements_page: pages::EnhancementsPage::new(window)?, environment_page: pages::EnvironmentPage::new()? };