diff --git a/src/ui/main.rs b/src/ui/main.rs index e0a8829..e4ed01a 100644 --- a/src/ui/main.rs +++ b/src/ui/main.rs @@ -1,6 +1,9 @@ use gtk4::{self as gtk, prelude::*}; use libadwaita::{self as adw, prelude::*}; +use gtk4::glib; +use gtk4::glib::clone; + use std::rc::Rc; use std::cell::Cell; @@ -16,7 +19,7 @@ use crate::lib::game; /// `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)] +#[derive(Clone, glib::Downgrade)] pub struct AppWidgets { pub window: adw::ApplicationWindow, pub toast_overlay: adw::ToastOverlay, @@ -80,7 +83,7 @@ pub enum Actions { /// In this example we store a counter here to know what should we increment or decrement /// /// This must implement `Default` trait -#[derive(Debug, Default)] +#[derive(Debug, Default, glib::Downgrade)] pub struct Values; /// The main application structure @@ -94,7 +97,7 @@ pub struct Values; /// /// 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)] +#[derive(Clone, glib::Downgrade)] pub struct App { widgets: AppWidgets, values: Rc> @@ -117,25 +120,19 @@ impl App { /// Add default events and values to the widgets fn init_events(self) -> Self { // Open preferences page - let self_copy = self.clone(); - - self.widgets.open_preferences.connect_clicked(move |_| { - self_copy.update(Actions::OpenPreferencesPage); - }); + self.widgets.open_preferences.connect_clicked(clone!(@strong self as this => move |_| { + this.update(Actions::OpenPreferencesPage); + })); // Go back button for preferences page - let self_copy = self.clone(); - - self.widgets.preferences_stack.preferences_go_back.connect_clicked(move |_| { - self_copy.update(Actions::PreferencesGoBack); - }); + self.widgets.preferences_stack.preferences_go_back.connect_clicked(clone!(@strong self as this => move |_| { + this.update(Actions::PreferencesGoBack); + })); // Launch game - let self_copy = self.clone(); - - self.widgets.launch_game.connect_clicked(move |_| { - self_copy.update(Actions::LaunchGame); - }); + self.widgets.launch_game.connect_clicked(clone!(@strong self as this => move |_| { + this.update(Actions::LaunchGame); + })); self } diff --git a/src/ui/preferences/enhanced_page.rs b/src/ui/preferences/enhanced_page.rs index 67b6645..1ea2528 100644 --- a/src/ui/preferences/enhanced_page.rs +++ b/src/ui/preferences/enhanced_page.rs @@ -1,13 +1,23 @@ use gtk4::{self as gtk, prelude::*}; use libadwaita::{self as adw, prelude::*}; +use gtk4::glib; +use gtk4::glib::clone; + +use std::rc::Rc; +use std::cell::Cell; use std::io::Error; use crate::ui::get_object; use crate::lib::config; -#[derive(Clone)] -pub struct Page { +/// 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 page: adw::PreferencesPage, pub sync_combo: adw::ComboRow, @@ -19,8 +29,8 @@ pub struct Page { pub gamemode_switcher: gtk::Switch } -impl Page { - pub fn new() -> Result { +impl AppWidgets { + fn try_get() -> Result { let builder = gtk::Builder::from_string(include_str!("../../../assets/ui/.dist/preferences_enhanced.ui")); let result = Self { @@ -35,103 +45,155 @@ impl Page { gamemode_switcher: get_object(&builder, "gamemode_switcher")? }; - // Wine sync selection - result.sync_combo.connect_selected_notify(|hud| { - if let Ok(mut config) = config::get() { - // TODO: show toast - config.game.wine.sync = config::WineSync::try_from(hud.selected()).unwrap(); + Ok(result) + } +} - config::update(config).unwrap(); - } - }); +/// 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) +} - // Wine language selection - result.wine_lang.connect_selected_notify(|hud| { - if let Ok(mut config) = config::get() { - // TODO: show toast - config.game.wine.language = config::WineLang::try_from(hud.selected()).unwrap(); +/// 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; - config::update(config).unwrap(); - } - }); +/// 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, + values: Rc> +} - // HUD selection - result.hud_combo.connect_selected_notify(|hud| { - if let Ok(mut config) = config::get() { - // TODO: show toast - config.game.enhancements.hud = config::HUD::try_from(hud.selected()).unwrap(); - - config::update(config).unwrap(); - } - }); - - // FSR strength selection - result.fsr_combo.connect_selected_notify(|hud| { - if let Ok(mut config) = config::get() { - // TODO: show toast - - // Ultra Quality = 5 - // Quality = 4 - // Balanced = 3 - // Performance = 2 - // - // Source: Bottles (https://github.com/bottlesdevs/Bottles/blob/22fa3573a13f4e9b9c429e4cdfe4ca29787a2832/src/ui/details-preferences.ui#L88) - config.game.enhancements.fsr.strength = 5 - hud.selected(); - - config::update(config).unwrap(); - } - }); - - // FSR switching - result.fsr_switcher.connect_state_notify(|switcher| { - if let Ok(mut config) = config::get() { - // TODO: show toast - config.game.enhancements.fsr.enabled = switcher.state(); - - config::update(config).unwrap(); - } - }); - - // Gamemode switching - result.gamemode_switcher.connect_state_notify(|switcher| { - if let Ok(mut config) = config::get() { - // TODO: show toast - config.game.enhancements.gamemode = switcher.state(); - - config::update(config).unwrap(); - } - }); +impl App { + /// Create new application + pub fn new() -> Result { + let result = Self { + widgets: AppWidgets::try_get()?, + values: Default::default() + }.init_events(); Ok(result) } + /// 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())); + })); + + // 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())); + })); + + // 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())); + })); + + // FSR strength selection + // + // Ultra Quality = 5 + // Quality = 4 + // Balanced = 3 + // 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())); + })); + + // 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())); + })); + + // 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 + } + + /// 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() { + // TODO: show toast + config::update((update)(config, value)).unwrap(); + } + } + } + + self.values.set(values); + } + pub fn title() -> String { String::from("Enhanced") } + pub fn get_page(&self) -> adw::PreferencesPage { + self.widgets.page.clone() + } + /// This method is being called by the `PreferencesStack::update` - pub fn update(&self, status_page: &adw::StatusPage) -> Result<(), Error> { + pub fn prepare(&self, status_page: &adw::StatusPage) -> Result<(), Error> { let config = config::get()?; status_page.set_description(Some("Loading preferences...")); // Update Wine sync - self.sync_combo.set_selected(config.game.wine.sync.into()); + self.widgets.sync_combo.set_selected(config.game.wine.sync.into()); // Update wine language - self.wine_lang.set_selected(config.game.wine.language.into()); + self.widgets.wine_lang.set_selected(config.game.wine.language.into()); // Update HUD - self.hud_combo.set_selected(config.game.enhancements.hud.into()); + self.widgets.hud_combo.set_selected(config.game.enhancements.hud.into()); // FSR strength selection - self.fsr_combo.set_selected(5 - config.game.enhancements.fsr.strength); + self.widgets.fsr_combo.set_selected(5 - config.game.enhancements.fsr.strength); // FSR switching - self.fsr_switcher.set_state(config.game.enhancements.fsr.enabled); + self.widgets.fsr_switcher.set_state(config.game.enhancements.fsr.enabled); // Gamemode switching - self.fsr_switcher.set_state(config.game.enhancements.gamemode); + self.widgets.fsr_switcher.set_state(config.game.enhancements.gamemode); Ok(()) } diff --git a/src/ui/preferences/general_page.rs b/src/ui/preferences/general_page.rs index 3f6e445..7d43632 100644 --- a/src/ui/preferences/general_page.rs +++ b/src/ui/preferences/general_page.rs @@ -1,6 +1,11 @@ use gtk4::{self as gtk, prelude::*}; use libadwaita::{self as adw, prelude::*}; +use gtk4::glib; +use gtk4::glib::clone; + +use std::rc::Rc; +use std::cell::Cell; use std::io::Error; use anime_game_core::prelude::*; @@ -9,8 +14,13 @@ use crate::ui::get_object; use crate::lib::config; use crate::lib::dxvk; -#[derive(Clone)] -pub struct Page { +/// 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 page: adw::PreferencesPage, pub game_version: gtk::Label, @@ -18,14 +28,16 @@ pub struct Page { pub dxvk_recommended_only: gtk::Switch, pub dxvk_vanilla: adw::ExpanderRow, - pub dxvk_async: adw::ExpanderRow + pub dxvk_async: adw::ExpanderRow, + + pub dxvk_components: Rc> } -impl Page { - pub fn new() -> Result { +impl AppWidgets { + fn try_get() -> Result { let builder = gtk::Builder::from_string(include_str!("../../../assets/ui/.dist/preferences_general.ui")); - let result = Self { + let mut result = Self { page: get_object(&builder, "general_page")?, game_version: get_object(&builder, "game_version")?, @@ -33,7 +45,9 @@ impl Page { dxvk_recommended_only: get_object(&builder, "dxvk_recommended_only")?, dxvk_vanilla: get_object(&builder, "dxvk_vanilla")?, - dxvk_async: get_object(&builder, "dxvk_async")? + dxvk_async: get_object(&builder, "dxvk_async")?, + + dxvk_components: Default::default() }; // Update DXVK list @@ -77,55 +91,122 @@ impl Page { result.dxvk_async.add_row(&row); components.push((row, version)); } - + + result.dxvk_components = Rc::new(components); + + 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 { + +} + +/// 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 +/// +/// `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, + values: Rc> +} + +impl App { + /// Create new application + pub fn new() -> Result { + let result = Self { + widgets: AppWidgets::try_get()?, + values: Default::default() + }.init_events(); + + Ok(result) + } + + /// Add default events and values to the widgets + fn init_events(self) -> Self { // Set DXVK recommended only switcher event - result.dxvk_recommended_only.connect_state_notify(move |switcher| { - for (component, version) in &components { + self.widgets.dxvk_recommended_only.connect_state_notify(clone!(@weak self as this => move |switcher| { + for (component, version) in &*this.widgets.dxvk_components { component.set_visible(if switcher.state() { version.recommended } else { true }); } - }); + })); - Ok(result) + self + } + + /// Update widgets state by calling some action + pub fn update(&self, action: Actions) { + /*let values = self.values.take(); + + match action { + + } + + self.values.set(values);*/ } pub fn title() -> String { String::from("General") } + pub fn get_page(&self) -> adw::PreferencesPage { + self.widgets.page.clone() + } + /// This method is being called by the `PreferencesStack::update` - pub fn update(&self, status_page: &adw::StatusPage) -> Result<(), Error> { + pub fn prepare(&self, status_page: &adw::StatusPage) -> Result<(), Error> { let config = config::get()?; let game = Game::new(config.game.path); - self.game_version.set_tooltip_text(None); - self.patch_version.set_tooltip_text(None); + self.widgets.game_version.set_tooltip_text(None); + self.widgets.patch_version.set_tooltip_text(None); // Update game version status_page.set_description(Some("Updating game info...")); match game.try_get_diff()? { VersionDiff::Latest(version) => { - self.game_version.set_label(&version.to_string()); + self.widgets.game_version.set_label(&version.to_string()); }, VersionDiff::Diff { current, latest, .. } => { - self.game_version.set_label(¤t.to_string()); - self.game_version.set_css_classes(&["warning"]); + self.widgets.game_version.set_label(¤t.to_string()); + self.widgets.game_version.set_css_classes(&["warning"]); - self.game_version.set_tooltip_text(Some(&format!("Game update available: {} -> {}", current, latest))); + self.widgets.game_version.set_tooltip_text(Some(&format!("Game update available: {} -> {}", current, latest))); }, VersionDiff::Outdated { current, latest } => { - self.game_version.set_label(¤t.to_string()); - self.game_version.set_css_classes(&["error"]); + self.widgets.game_version.set_label(¤t.to_string()); + self.widgets.game_version.set_css_classes(&["error"]); - self.game_version.set_tooltip_text(Some(&format!("Game is too outdated and can't be updated. Latest version: {}", latest))); + self.widgets.game_version.set_tooltip_text(Some(&format!("Game is too outdated and can't be updated. Latest version: {}", latest))); }, VersionDiff::NotInstalled { .. } => { - self.game_version.set_label("not installed"); - self.game_version.set_css_classes(&[]); + self.widgets.game_version.set_label("not installed"); + self.widgets.game_version.set_css_classes(&[]); } } @@ -134,32 +215,32 @@ impl Page { match Patch::try_fetch(config.patch.servers)? { Patch::NotAvailable => { - self.patch_version.set_label("not available"); - self.patch_version.set_css_classes(&["error"]); + self.widgets.patch_version.set_label("not available"); + self.widgets.patch_version.set_css_classes(&["error"]); - self.patch_version.set_tooltip_text(Some("Patch is not available")); + self.widgets.patch_version.set_tooltip_text(Some("Patch is not available")); }, Patch::Outdated { current, latest, .. } => { - self.patch_version.set_label("outdated"); - self.patch_version.set_css_classes(&["warning"]); + self.widgets.patch_version.set_label("outdated"); + self.widgets.patch_version.set_css_classes(&["warning"]); - self.patch_version.set_tooltip_text(Some(&format!("Patch is outdated ({} -> {})", current, latest))); + self.widgets.patch_version.set_tooltip_text(Some(&format!("Patch is outdated ({} -> {})", current, latest))); }, Patch::Preparation { .. } => { - self.patch_version.set_label("preparation"); - self.patch_version.set_css_classes(&["warning"]); + self.widgets.patch_version.set_label("preparation"); + self.widgets.patch_version.set_css_classes(&["warning"]); - self.patch_version.set_tooltip_text(Some("Patch is in preparation state and will be available later")); + self.widgets.patch_version.set_tooltip_text(Some("Patch is in preparation state and will be available later")); }, Patch::Testing { version, .. } => { - self.patch_version.set_label(&version.to_string()); - self.patch_version.set_css_classes(&["warning"]); + self.widgets.patch_version.set_label(&version.to_string()); + self.widgets.patch_version.set_css_classes(&["warning"]); - self.patch_version.set_tooltip_text(Some("Patch is in testing phase")); + self.widgets.patch_version.set_tooltip_text(Some("Patch is in testing phase")); }, Patch::Available { version, .. } => { - self.patch_version.set_label(&version.to_string()); - self.patch_version.set_css_classes(&["success"]); + self.widgets.patch_version.set_label(&version.to_string()); + self.widgets.patch_version.set_css_classes(&["success"]); } } diff --git a/src/ui/preferences/mod.rs b/src/ui/preferences/mod.rs index 7ea62cb..8c400cc 100644 --- a/src/ui/preferences/mod.rs +++ b/src/ui/preferences/mod.rs @@ -1,6 +1,8 @@ use gtk4::{self as gtk, prelude::*}; use libadwaita::{self as adw, prelude::*}; +use gtk4::glib; + use std::io::Error; use crate::ui::get_object; @@ -10,11 +12,11 @@ mod general_page; mod enhanced_page; pub mod pages { - pub use super::general_page::Page as GeneralPage; - pub use super::enhanced_page::Page as EnhancedPage; + pub use super::general_page::App as GeneralPage; + pub use super::enhanced_page::App as EnhancedPage; } -#[derive(Clone)] +#[derive(Clone, glib::Downgrade)] pub struct PreferencesStack { pub window: adw::ApplicationWindow, pub toast_overlay: adw::ToastOverlay, @@ -51,8 +53,8 @@ impl PreferencesStack { enhanced_page: pages::EnhancedPage::new()? }; - result.stack.add_titled(&result.general_page.page, None, &pages::GeneralPage::title()); - result.stack.add_titled(&result.enhanced_page.page, None, &pages::EnhancedPage::title()); + result.stack.add_titled(&result.general_page.get_page(), None, &pages::GeneralPage::title()); + result.stack.add_titled(&result.enhanced_page.get_page(), None, &pages::EnhancedPage::title()); Ok(result) } @@ -67,8 +69,8 @@ impl PreferencesStack { self.status_page.set_description(None); self.flap.set_visible(false); - self.general_page.update(&self.status_page)?; - self.enhanced_page.update(&self.status_page)?; + self.general_page.prepare(&self.status_page)?; + self.enhanced_page.prepare(&self.status_page)?; self.status_page.set_visible(false); self.flap.set_visible(true);