diff --git a/src/config.rs b/src/config.rs index c3905ec..cdbf539 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,7 +2,7 @@ use crate::{ constants::CMD_NAME, device_prober::PhysicalXRDevice, paths::get_config_dir, - profile::Profile, + profile::{OvrCompatibilityModuleType, Profile, XRServiceType}, profiles::{ lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile, survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile, @@ -49,6 +49,67 @@ fn default_theme_name() -> String { "Follow system".into() } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GlobalPreferences { + pub xrservice_type: XRServiceType, + pub ovr_comp: OvrCompatibilityModuleType, + + /// XRT_COMPOSITOR_SCALE_PERCENTAGE + pub scale_percentage: u16, + /// XRT_COMPOSITOR_COMPUTE (1, 0) + pub use_compositor_gpu_compute: bool, + /// U_PACING_APP_USE_MIN_FRAME_PERIOD (1, 0) + pub use_min_frame_period: bool, + /// XRT_DEBUG_GUI (1, 0) + XRT_CURATED_GUI (1, 0) + pub monado_ui: bool, +} + +impl Default for GlobalPreferences { + fn default() -> Self { + Self { + xrservice_type: XRServiceType::default(), + ovr_comp: OvrCompatibilityModuleType::default(), + + scale_percentage: 140, + use_compositor_gpu_compute: true, + use_min_frame_period: false, + monado_ui: false, + } + } +} + +impl GlobalPreferences { + pub fn get_env_vars(&self) -> HashMap { + HashMap::from([ + ( + "XRT_COMPOSITOR_SCALE_PERCENTAGE".into(), + self.scale_percentage.to_string(), + ), + ( + "XRT_COMPOSITOR_COMPUTE".into(), + (if self.use_compositor_gpu_compute { + 1 + } else { + 0 + }) + .to_string(), + ), + ( + "U_PACING_APP_USE_MIN_FRAME_PERIOD".into(), + (if self.use_min_frame_period { 1 } else { 0 }).to_string(), + ), + ( + "XRT_DEBUG_GUI".into(), + (if self.monado_ui { 1 } else { 0 }).to_string(), + ), + ( + "XRT_CURATED_GUI".into(), + (if self.monado_ui { 1 } else { 0 }).to_string(), + ), + ]) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Config { pub selected_profile_uuid: String, @@ -60,11 +121,13 @@ pub struct Config { pub plugins: HashMap, #[serde(default = "default_theme_name")] pub theme_name: String, + #[serde(default)] + pub global_preferences: GlobalPreferences, } impl Default for Config { fn default() -> Self { - Config { + Self { // TODO: using an empty string here is ugly selected_profile_uuid: "".to_string(), debug_view_enabled: false, @@ -72,6 +135,7 @@ impl Default for Config { win_size: DEFAULT_WIN_SIZE, plugins: HashMap::default(), theme_name: default_theme_name(), + global_preferences: GlobalPreferences::default(), } } } diff --git a/src/profile.rs b/src/profile.rs index aeba3b5..8b0d0d7 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -33,8 +33,9 @@ use std::{ }; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum XRServiceType { + #[default] Monado, Wivrn, } diff --git a/src/ui/app.rs b/src/ui/app.rs index be67b7b..fc7f591 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -46,7 +46,7 @@ use crate::{ set_soldier_runtime_entrypoint_launch_opts_from_profile, }, termcolor::TermColor, - ui::alert::{present_alert, yes_no_dialog}, + ui::{alert::{present_alert, yes_no_dialog}, preferences_window::{PreferencesWindow, PreferencesWindowInit, PreferencesWindowMsg, PreferencesWindowOutMsg}}, util::file_utils::{ rm_rf, setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd, verify_cap_sys_nice_eip, }, @@ -103,6 +103,8 @@ pub struct App { inhibit_fail_notif: Option, pluginstore: Option>, + preferences_win: Option>, + theme_engine: ThemeEngine, build_alerts: VecDeque, @@ -141,6 +143,8 @@ pub enum Msg { OnProberExit(bool), WivrnCheckPairMode, OpenPluginStore, + OpenPreferences, + OnPreferencesWindowClose(Config), UpdateConfigPlugins(HashMap), ShowThemeManager, SaveThemeConfig, @@ -1072,6 +1076,21 @@ impl AsyncComponent for App { pluginstore.sender().emit(PluginStoreMsg::Present); self.pluginstore = Some(pluginstore); } + Msg::OpenPreferences => { + let preferences_win = PreferencesWindow::builder() + .launch(PreferencesWindowInit { + config: self.config.clone(), + root_win: self.app_win.clone().upcast(), + }) + .forward(sender.input_sender(), move |msg| match msg { + PreferencesWindowOutMsg::OnClosed(config) => Msg::OnPreferencesWindowClose(config), + }); + preferences_win.sender().emit(PreferencesWindowMsg::Present); + self.preferences_win = Some(preferences_win); + } + Msg::OnPreferencesWindowClose(config) => { + self.config = config; + } Msg::UpdateConfigPlugins(cp) => { self.config.plugins = cp; self.config.save(); @@ -1211,6 +1230,17 @@ impl AsyncComponent for App { } ) ); + stateless_action!( + actions, + PreferencesAction, + clone!( + #[strong] + sender, + move |_| { + sender.input(Msg::OpenPreferences); + } + ) + ); stateless_action!( actions, ThemeManagerAction, @@ -1318,6 +1348,7 @@ impl AsyncComponent for App { xrservice_ready: false, inhibit_fail_notif: None, pluginstore: None, + preferences_win: None, build_alerts: VecDeque::new(), }; @@ -1357,6 +1388,7 @@ impl AsyncComponent for App { { let app = &model.application; app.set_accelerators_for_action::(&["q"]); + app.set_accelerators_for_action::(&["comma"]); app.set_accelerators_for_action::(&["F5"]); app.set_accelerators_for_action::(&["F5"]); } @@ -1392,6 +1424,7 @@ new_stateless_action!(pub QuitAction, AppActionGroup, "quit"); new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool); new_stateless_action!(pub ConfigureWivrnAction, AppActionGroup, "configurewivrn"); new_stateless_action!(pub PluginStoreAction, AppActionGroup, "store"); +new_stateless_action!(pub PreferencesAction, AppActionGroup, "preferences"); new_stateless_action!(pub ThemeManagerAction, AppActionGroup, "thememanager"); new_stateless_action!(pub DebugOpenDataAction, AppActionGroup, "debugopendata"); diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index e74a262..fc020e5 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -158,6 +158,7 @@ impl AsyncComponent for MainView { "_Debug View" => DebugViewToggleAction, "_Build Profile" => BuildProfileAction, "C_lean Build Profile" => BuildProfileCleanAction, + "_Preferences" => PreferencesAction, "Configure _WiVRn" => ConfigureWivrnAction, }, section! { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f68333c..0af4d4b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -15,6 +15,7 @@ mod main_view; mod openhmd_calibration_box; pub mod plugins; mod preference_rows; +mod preferences_window; mod profile_editor; mod steam_launch_options_box; mod steamvr_calibration_box; diff --git a/src/ui/preference_rows.rs b/src/ui/preference_rows.rs index 3d6e103..a1fc572 100644 --- a/src/ui/preference_rows.rs +++ b/src/ui/preference_rows.rs @@ -5,29 +5,25 @@ use gtk::{ }; use relm4::prelude::*; -pub fn switch_row glib::Propagation + 'static>( +pub fn switch_row( title: &str, description: Option<&str>, value: bool, cb: F, -) -> adw::ActionRow { - let row = adw::ActionRow::builder() +) -> adw::SwitchRow { + let row = adw::SwitchRow::builder() .title(title) .subtitle_lines(0) + .active(value) .build(); if let Some(d) = description { row.set_subtitle(d); } - let switch = gtk::Switch::builder() - .active(value) - .valign(gtk::Align::Center) - .build(); - row.add_suffix(&switch); - row.set_activatable_widget(Some(&switch)); - - switch.connect_state_set(cb); + row.connect_active_notify(move |row| { + cb(row, row.is_active()); + }); row } diff --git a/src/ui/preferences_window.rs b/src/ui/preferences_window.rs new file mode 100644 index 0000000..41942e4 --- /dev/null +++ b/src/ui/preferences_window.rs @@ -0,0 +1,163 @@ +use crate::{ + config::Config, + profile::{OvrCompatibilityModuleType, XRServiceType}, + ui::{ + SENDER_IO_ERR_MSG, + preference_rows::{combo_row, spin_row, switch_row}, + }, +}; +use adw::prelude::*; +use gtk::glib::clone; +use relm4::prelude::*; +use std::{cell::RefCell, rc::Rc}; + +#[tracker::track] +pub struct PreferencesWindow { + config: Rc>, + #[tracker::do_not_track] + win: Option, + #[tracker::do_not_track] + parent: gtk::Window, +} + +#[derive(Debug)] +pub enum PreferencesWindowMsg { + Present, + OnClosed, +} + +#[derive(Debug)] +pub enum PreferencesWindowOutMsg { + OnClosed(Config), +} + +pub struct PreferencesWindowInit { + pub root_win: gtk::Window, + pub config: Config, +} + +#[relm4::component(pub async)] +impl AsyncComponent for PreferencesWindow { + type Init = PreferencesWindowInit; + type Input = PreferencesWindowMsg; + type Output = PreferencesWindowOutMsg; + type CommandOutput = (); + + view! { + #[name(win)] + adw::PreferencesDialog { + set_search_enabled: true, + connect_closed[sender] => move |_| { + sender.input(Self::Input::OnClosed); + }, + add: page = &adw::PreferencesPage { + add: grp = &adw::PreferencesGroup { + add: &combo_row( + "XR Service", + Some("Choose Monado for PCVR and WiVRn for standalone"), + model.config.borrow().global_preferences.xrservice_type.to_string().as_str(), + XRServiceType::iter().map(XRServiceType::to_string).collect(), + clone!(#[strong(rename_to=config)] model.config, move |row| { + config.borrow_mut().global_preferences.xrservice_type = XRServiceType::from(row.selected()); + }) + ), + add: &combo_row( + "OpenVR Compatibility", + None, + model.config.borrow().global_preferences.ovr_comp.to_string().as_str(), + OvrCompatibilityModuleType::iter().map(OvrCompatibilityModuleType::to_string).collect(), + clone!(#[strong(rename_to=config)] model.config, move |row| { + config.borrow_mut().global_preferences.ovr_comp = OvrCompatibilityModuleType::from(row.selected()); + }) + ), + add: &spin_row( + "Render scale percentage", + None, + model.config.borrow().global_preferences.scale_percentage.into(), + 20.0, + 500.0, + 1.0, + clone!(#[strong(rename_to=config)] model.config, move |adj| { + let raw = adj.value().trunc(); + // boring f64 re-clamping for sake of correctness + let val = if raw >= 500.0 { + 500 + } else if raw <= 20.0 { + 20 + } else { + raw as u16 + }; + config.borrow_mut().global_preferences.scale_percentage = val; + }) + ), + add: &switch_row( + "Compositor GPU acceleration", + None, + model.config.borrow().global_preferences.use_compositor_gpu_compute, + clone!(#[strong(rename_to=config)] model.config, move |_, v| { + config.borrow_mut().global_preferences.use_compositor_gpu_compute = v; + }) + ), + add: &switch_row( + "Use minimum frame period", + Some("Disables the refresh rate limit; this can increase performance"), + model.config.borrow().global_preferences.use_min_frame_period, + clone!(#[strong(rename_to=config)] model.config, move |_, v| { + config.borrow_mut().global_preferences.use_min_frame_period = v; + }) + ), + add: &switch_row( + "Monado UI", + None, + model.config.borrow().global_preferences.monado_ui, + clone!(#[strong(rename_to=config)] model.config, move |_, v| { + config.borrow_mut().global_preferences.monado_ui = v; + }) + ), + }, + }, + } + } + + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender, + _root: &Self::Root, + ) { + self.reset(); + + match message { + Self::Input::Present => { + if let Some(win) = self.win.as_ref() { + win.present(Some(&self.parent)); + } + } + Self::Input::OnClosed => { + self.config.borrow().save(); + sender + .output(Self::Output::OnClosed(self.config.borrow().clone())) + .expect(SENDER_IO_ERR_MSG); + } + } + } + + async fn init( + init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let mut model = Self { + win: None, + config: Rc::new(RefCell::new(init.config)), + parent: init.root_win, + tracker: 0, + }; + + let widgets = view_output!(); + + model.win = Some(widgets.win.clone()); + + AsyncComponentParts { model, widgets } + } +} diff --git a/src/ui/profile_editor.rs b/src/ui/profile_editor.rs index 06a8fc2..d2e63e8 100644 --- a/src/ui/profile_editor.rs +++ b/src/ui/profile_editor.rs @@ -118,7 +118,6 @@ impl SimpleComponent for ProfileEditor { model.profile.borrow().pull_on_build, clone!(#[strong] prof, move |_, state| { prof.borrow_mut().pull_on_build = state; - gtk::glib::Propagation::Proceed }) ), add: &path_row( @@ -135,7 +134,6 @@ impl SimpleComponent for ProfileEditor { !model.profile.borrow().skip_dependency_check, clone!(#[strong] prof, move |_, state| { prof.borrow_mut().skip_dependency_check = !state; - gtk::glib::Propagation::Proceed }) ), }, @@ -257,7 +255,6 @@ impl SimpleComponent for ProfileEditor { model.profile.borrow().features.libsurvive.enabled, clone!(#[strong] prof, move |_, state| { prof.borrow_mut().features.libsurvive.enabled = state; - gtk::glib::Propagation::Proceed }) ), add: &path_row( @@ -293,7 +290,6 @@ impl SimpleComponent for ProfileEditor { model.profile.borrow().features.openhmd.enabled, clone!(#[strong] prof, move |_, state| { prof.borrow_mut().features.openhmd.enabled = state; - gtk::glib::Propagation::Proceed }) ), add: &path_row( @@ -329,7 +325,6 @@ impl SimpleComponent for ProfileEditor { model.profile.borrow().features.basalt.enabled, clone!(#[strong] prof, move |_, state| { prof.borrow_mut().features.basalt.enabled = state; - gtk::glib::Propagation::Proceed }) ), add: &path_row( @@ -365,7 +360,6 @@ impl SimpleComponent for ProfileEditor { model.profile.borrow().features.mercury_enabled, clone!(#[strong] prof, move |_, state| { prof.borrow_mut().features.mercury_enabled = state; - gtk::glib::Propagation::Proceed }) ), }, diff --git a/src/ui/wivrn_conf_editor.rs b/src/ui/wivrn_conf_editor.rs index 56bd8db..bc0cc42 100644 --- a/src/ui/wivrn_conf_editor.rs +++ b/src/ui/wivrn_conf_editor.rs @@ -155,7 +155,6 @@ impl SimpleComponent for WivrnConfEditor { model.conf.tcp_only, clone!(#[strong] sender, move |_, val| { sender.input(Self::Input::TcpOnlyChanged(val)); - gtk::glib::Propagation::Proceed }), ), }, @@ -171,7 +170,6 @@ impl SimpleComponent for WivrnConfEditor { model.conf.use_steamvr_lh, clone!(#[strong] sender, move |_, val| { sender.input(Self::Input::UseSteamVrLhChanged(val)); - gtk::glib::Propagation::Proceed }), ), }, @@ -203,7 +201,6 @@ impl SimpleComponent for WivrnConfEditor { model.conf.bit_depth == 10, clone!(#[strong] sender, move |_, val| { sender.input(Self::Input::BitDepthChanged(val)); - gtk::glib::Propagation::Proceed }), ), },