diff --git a/Cargo.lock b/Cargo.lock index b2b77f8..9f397d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1272,6 +1272,47 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.0" @@ -1372,6 +1413,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "raw-window-handle" version = "0.4.3" @@ -1523,6 +1579,8 @@ dependencies = [ "gettext-rs", "gtk4", "nix", + "phf", + "phf_macros", "relm4", "relm4-components", "relm4-icons", @@ -1672,6 +1730,12 @@ dependencies = [ "serde", ] +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + [[package]] name = "slab" version = "0.4.8" diff --git a/Cargo.toml b/Cargo.toml index b3b52a7..857568a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,14 @@ gtk4 = { version = "0.6.6", features = [ "v4_10" ] } nix = "0.26.2" +phf = "0.11.1" +phf_macros = "0.11.1" relm4 = { version = "0.6.0", features = [ "libadwaita" ] } relm4-components = "0.6.0" relm4-icons = { version = "0.6.0", features = [ - "menu", "loupe", "copy" + "menu", "loupe", "copy", "edit", "plus" ] } reqwest = { version = "0.11.18", features = [ "blocking" diff --git a/src/env_var_descriptions.rs b/src/env_var_descriptions.rs new file mode 100644 index 0000000..e159fa7 --- /dev/null +++ b/src/env_var_descriptions.rs @@ -0,0 +1,19 @@ +use phf::Map; +use phf_macros::phf_map; + +pub static ENV_VAR_DESCRIPTIONS: Map<&str, &str> = phf_map! { + "XRT_COMPOSITOR_SCALE_PECENTAGE" => + "Render resolution percentage. A percentage higher than the native resolution (>100) will help with antialiasing and image clarity.", + // "XRT_COMPOSITOR_COMPUTE" => "", + "SURVIVE_GLOBALSCENESOLVER" => + "Continuously recalibrate lighthouse tracking during use. In the current state it's recommended to disable this feature by setting this value to 0.", + // "SURVIVE_TIMECODE_OFFSET_MS" => "", + "LD_LIBRARY_PATH" => + "Colon-separated list of directories where the dynamic linker will search for shared object libraries.", +}; + +pub fn env_var_descriptions_as_paragraph() -> String { + ENV_VAR_DESCRIPTIONS.into_iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>().join("\n\n") +} diff --git a/src/main.rs b/src/main.rs index 006a31b..38f04c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ pub mod runner_pipeline; pub mod ui; pub mod adb; pub mod downloader; +pub mod env_var_descriptions; fn main() -> Result<()> { // Prepare i18n diff --git a/src/ui/factories/entry_row_factory.rs b/src/ui/factories/entry_row_factory.rs new file mode 100644 index 0000000..c41d5c3 --- /dev/null +++ b/src/ui/factories/entry_row_factory.rs @@ -0,0 +1,70 @@ +use crate::ui::profile_editor::ProfileEditorMsg; +use adw::prelude::*; +use gtk::prelude::*; +use relm4::prelude::*; + +#[derive(Debug)] +pub struct EntryModel { + name: String, + value: String, +} + +pub struct EntryModelInit { + pub name: String, + pub value: String, +} + +#[derive(Debug)] +pub enum EntryModelMsg { + Changed(String), +} + +#[derive(Debug)] +pub enum EntryModelOutMsg { + Changed(String, String), +} + +#[relm4::factory(pub)] +impl FactoryComponent for EntryModel { + type Init = EntryModelInit; + type Input = EntryModelMsg; + type Output = EntryModelOutMsg; + type CommandOutput = (); + type Widgets = EntryModelWidgets; + type ParentInput = ProfileEditorMsg; + type ParentWidget = adw::PreferencesGroup; + + view! { + root = adw::EntryRow { + set_title: &self.name, + set_text: &self.value, + connect_changed[sender] => move |entry| { + sender.input_sender().emit(Self::Input::Changed(entry.text().to_string())); + }, + } + } + + fn update(&mut self, message: Self::Input, sender: FactorySender) { + match message { + Self::Input::Changed(val) => { + self.value = val.clone(); + sender + .output_sender() + .emit(Self::Output::Changed(self.name.clone(), val)); + } + } + } + + fn forward_to_parent(output: Self::Output) -> Option { + Some(match output { + Self::Output::Changed(name, value) => ProfileEditorMsg::EntryChanged(name, value), + }) + } + + fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender) -> Self { + Self { + name: init.name, + value: init.value, + } + } +} diff --git a/src/ui/factories/mod.rs b/src/ui/factories/mod.rs new file mode 100644 index 0000000..5fc67a9 --- /dev/null +++ b/src/ui/factories/mod.rs @@ -0,0 +1,3 @@ +pub mod entry_row_factory; +pub mod switch_row_factory; +pub mod path_row_factory; diff --git a/src/ui/factories/path_row_factory.rs b/src/ui/factories/path_row_factory.rs new file mode 100644 index 0000000..e824ffd --- /dev/null +++ b/src/ui/factories/path_row_factory.rs @@ -0,0 +1,87 @@ +use adw::prelude::*; +use gtk::prelude::*; +use relm4::prelude::*; + +use crate::ui::profile_editor::ProfileEditorMsg; + +#[derive(Debug)] +pub struct PathModel { + name: String, + key: String, + value: String, + path_label: gtk::Label, +} + +pub struct PathModelInit { + pub name: String, + pub key: String, + pub value: Option, +} + +#[derive(Debug)] +pub enum PathModelMsg { + Changed(String), + OpenFileChooser, +} + +#[derive(Debug)] +pub enum PathModelOutMsg { + /** key, value */ + Changed(String, String), +} + +#[relm4::factory(pub)] +impl FactoryComponent for PathModel { + type Init = PathModelInit; + type Input = PathModelMsg; + type Output = PathModelOutMsg; + type CommandOutput = (); + type Widgets = PathModelWidgets; + type ParentInput = ProfileEditorMsg; + type ParentWidget = adw::PreferencesGroup; + + view! { + root = adw::ActionRow { + set_title: &self.name, + set_subtitle_lines: 0, + set_icon_name: Some("folder-open-symbolic"), + add_suffix: &self.path_label, + set_activatable: true, + connect_activated[sender] => move |_| { + sender.input(Self::Input::OpenFileChooser) + } + } + } + + fn update(&mut self, message: Self::Input, sender: FactorySender) { + match message { + Self::Input::Changed(val) => { + self.value = val.clone(); + self.path_label.set_label(val.as_str()); + sender + .output_sender() + .emit(Self::Output::Changed(self.key.clone(), val)) + } + Self::Input::OpenFileChooser => { + println!("file chooser"); + } + } + } + + fn forward_to_parent(_output: Self::Output) -> Option { + None + } + + fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender) -> Self { + Self { + name: init.name, + key: init.key, + value: match init.value.as_ref() { + Some(val) => val.clone(), None => "".into() + }, + path_label: gtk::Label::builder().label(match init.value { + Some(val) => val, None => "(None)".into() + }).ellipsize(gtk::pango::EllipsizeMode::Start).build(), + } + } +} diff --git a/src/ui/factories/switch_row_factory.rs b/src/ui/factories/switch_row_factory.rs new file mode 100644 index 0000000..6d9dbfa --- /dev/null +++ b/src/ui/factories/switch_row_factory.rs @@ -0,0 +1,86 @@ +use adw::prelude::*; +use gtk::prelude::*; +use relm4::prelude::*; + +use crate::ui::profile_editor::ProfileEditorMsg; + +#[derive(Debug)] +pub struct SwitchModel { + name: String, + description: Option, + key: String, + value: bool, +} + +pub struct SwitchModelInit { + pub name: String, + pub description: Option, + pub key: String, + pub value: bool, +} + +#[derive(Debug)] +pub enum SwitchModelMsg { + Changed(bool), +} + +#[derive(Debug)] +pub enum SwitchModelOutMsg { + /** key, value */ + Changed(String, bool), +} + +#[relm4::factory(pub)] +impl FactoryComponent for SwitchModel { + type Init = SwitchModelInit; + type Input = SwitchModelMsg; + type Output = SwitchModelOutMsg; + type CommandOutput = (); + type Widgets = SwitchModelWidgets; + type ParentInput = ProfileEditorMsg; + type ParentWidget = adw::PreferencesGroup; + + view! { + root = adw::ActionRow { + set_title: &self.name, + set_subtitle_lines: 0, + set_subtitle: match &self.description { + Some(s) => s, + None => "".into(), + }, + add_suffix: switch = >k::Switch { + set_valign: gtk::Align::Center, + set_active: self.value, + connect_state_set[sender] => move |_, state| { + sender.input(Self::Input::Changed(state)); + gtk::Inhibit(false) + } + }, + set_activatable_widget: Some(&switch), + } + } + + fn update(&mut self, message: Self::Input, sender: FactorySender) { + match message { + Self::Input::Changed(val) => { + self.value = val.clone(); + sender + .output_sender() + .emit(Self::Output::Changed(self.key.clone(), val)) + } + } + } + + fn forward_to_parent(_output: Self::Output) -> Option { + None + } + + fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender) -> Self { + Self { + name: init.name, + description: init.description, + key: init.key, + value: init.value, + } + } +} diff --git a/src/ui/install_wivrn_box.rs b/src/ui/install_wivrn_box.rs index 31e64b3..117c200 100644 --- a/src/ui/install_wivrn_box.rs +++ b/src/ui/install_wivrn_box.rs @@ -136,11 +136,7 @@ impl SimpleComponent for InstallWivrnBox { match message { Self::Input::ClockTicking => { if self.download_thread.is_some() { - let finished = self - .download_thread - .as_ref() - .unwrap() - .is_finished(); + let finished = self.download_thread.as_ref().unwrap().is_finished(); if finished { let joinh = self.download_thread.take().unwrap(); match joinh.join().unwrap() { diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 5b95163..9915d5e 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -1,4 +1,5 @@ use super::install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg}; +use super::profile_editor::{ProfileEditor, ProfileEditorMsg}; use super::runtime_switcher_box::{ RuntimeSwitcherBox, RuntimeSwitcherBoxInit, RuntimeSwitcherBoxMsg, }; @@ -9,6 +10,7 @@ use crate::profile::{Profile, XRServiceType}; use crate::ui::app::{ AboutAction, BuildProfileAction, DebugViewToggleAction, LibsurviveSetupAction, }; +use crate::ui::profile_editor::ProfileEditorInit; use gtk::prelude::*; use relm4::prelude::*; use relm4::{ComponentParts, ComponentSender, SimpleComponent}; @@ -17,9 +19,9 @@ use relm4_icons::icon_name; #[tracker::track] pub struct MainView { xrservice_active: bool, - xrservice_type: XRServiceType, enable_debug_view: bool, profile_names: Vec, + selected_profile: Profile, #[tracker::do_not_track] profiles_dropdown: Option, #[tracker::do_not_track] @@ -28,6 +30,8 @@ pub struct MainView { steam_launch_options_box: Controller, #[tracker::do_not_track] runtime_switcher_box: Controller, + #[tracker::do_not_track] + profile_editor: Controller, } #[derive(Debug)] @@ -40,6 +44,7 @@ pub enum MainViewMsg { SetSelectedProfile(u32), ProfileSelected(u32), UpdateSelectedProfile(Profile), + EditProfile, } #[derive(Debug)] @@ -94,16 +99,29 @@ impl SimpleComponent for MainView { set_icon_name: icon_name::MENU, set_menu_model: Some(&app_menu), }, - pack_start: profiles_dropdown = >k::DropDown { - set_tooltip_text: Some("Profiles"), - #[track = "model.changed(Self::profile_names())"] - set_model: Some(&{ - let names: Vec<_> = model.profile_names.iter().map(String::as_str).collect(); - gtk::StringList::new(&names) - }), - connect_selected_item_notify[sender] => move |this| { - sender.input(MainViewMsg::ProfileSelected(this.selected())); + pack_start = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + add_css_class: "linked", + #[name(profiles_dropdown)] + gtk::DropDown { + set_tooltip_text: Some("Profiles"), + #[track = "model.changed(Self::profile_names())"] + set_model: Some(&{ + let names: Vec<_> = model.profile_names.iter().map(String::as_str).collect(); + gtk::StringList::new(&names) + }), + connect_selected_item_notify[sender] => move |this| { + sender.input(MainViewMsg::ProfileSelected(this.selected())); + }, }, + gtk::Button { + set_icon_name: icon_name::EDIT, + add_css_class: "suggested-action", + set_tooltip_text: Some("Edit Profile"), + connect_clicked[sender] => move |_| { + sender.input(Self::Input::EditProfile); + } + } }, #[track = "model.changed(Self::enable_debug_view())"] set_show_end_title_buttons: !model.enable_debug_view, @@ -173,7 +191,7 @@ impl SimpleComponent for MainView { self.set_enable_debug_view(val); } Self::Input::UpdateSelectedProfile(prof) => { - self.set_xrservice_type(prof.xrservice_type.clone()); + self.set_selected_profile(prof.clone()); self.install_wivrn_box .sender() .emit(InstallWivrnBoxMsg::UpdateSelectedProfile(prof.clone())); @@ -212,6 +230,9 @@ impl SimpleComponent for MainView { self.profile_names.get(position as usize).unwrap().clone(), )); } + Self::Input::EditProfile => { + self.profile_editor.emit(ProfileEditorMsg::Present(self.selected_profile.clone())); + } } } @@ -222,7 +243,6 @@ impl SimpleComponent for MainView { ) -> ComponentParts { let mut model = Self { xrservice_active: false, - xrservice_type: XRServiceType::Monado, enable_debug_view: init.config.debug_view_enabled, profiles_dropdown: None, profile_names: vec![], @@ -237,6 +257,8 @@ impl SimpleComponent for MainView { selected_profile: init.selected_profile.clone(), }) .detach(), + profile_editor: ProfileEditor::builder().launch(ProfileEditorInit {}).detach(), + selected_profile: init.selected_profile.clone(), tracker: 0, }; let widgets = view_output!(); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6bdd3bb..5daa97f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -7,3 +7,5 @@ pub mod libsurvive_setup_window; pub mod install_wivrn_box; pub mod steam_launch_options_box; pub mod runtime_switcher_box; +pub mod profile_editor; +pub mod factories; diff --git a/src/ui/profile_editor.rs b/src/ui/profile_editor.rs new file mode 100644 index 0000000..04c55b1 --- /dev/null +++ b/src/ui/profile_editor.rs @@ -0,0 +1,184 @@ +use super::factories::{entry_row_factory::{EntryModel, EntryModelInit}, switch_row_factory::{SwitchModel, SwitchModelInit}, path_row_factory::{PathModel, PathModelInit}}; +use crate::{env_var_descriptions::env_var_descriptions_as_paragraph, profile::Profile}; +use adw::prelude::*; +use gtk::prelude::*; +use relm4::{factory::FactoryVecDeque, prelude::*}; +use relm4_icons::icon_name; + +pub struct ProfileEditor { + profile: Option, + win: Option, + + name_row: adw::EntryRow, + env_rows: FactoryVecDeque, + switch_rows: FactoryVecDeque, + path_rows: FactoryVecDeque, +} + +#[derive(Debug)] +pub enum ProfileEditorMsg { + Present(Profile), + EntryChanged(String, String), + AddEnvVar, + SaveProfile, // ? +} + +pub struct ProfileEditorInit {} + +#[relm4::component(pub)] +impl SimpleComponent for ProfileEditor { + type Init = ProfileEditorInit; + type Input = ProfileEditorMsg; + type Output = (); + + view! { + #[name(win)] + adw::PreferencesWindow { + set_hide_on_close: true, + set_modal: true, + add: mainpage = &adw::PreferencesPage { + add: maingrp = &adw::PreferencesGroup { + set_title: "Profile Info", + model.name_row.clone(), + }, + add: model.env_rows.widget(), + add: model.switch_rows.widget(), + add: model.path_rows.widget(), + } + } + } + + fn update(&mut self, message: Self::Input, sender: ComponentSender) { + match message { + Self::Input::Present(prof) => { + let win = self.win.as_ref().unwrap(); + let p = prof.clone(); + win.set_title(Some(p.name.as_str())); + + self.name_row.set_text(p.name.as_str()); + self.env_rows.guard().clear(); + for (k, v) in p.environment.iter() { + self.env_rows.guard().push_back(EntryModelInit { + name: k.clone(), + value: v.clone(), + }); + } + + self.switch_rows.guard().clear(); + self.switch_rows.guard().push_back(SwitchModelInit { + name: "Libsurvive".into(), + description: Some("Lighthouse based spacial tracking".into()), + key: "libsurvive_enabled".into(), + value: p.libsurvive_enabled, + }); + self.switch_rows.guard().push_back(SwitchModelInit { + name: "Basalt".into(), + description: Some("Camera based SLAM tracking".into()), + key: "basalt_enabled".into(), + value: p.basalt_enabled, + }); + self.switch_rows.guard().push_back(SwitchModelInit { + name: "Mercury".into(), + description: Some("Camera based hand tracking".into()), + key: "mercury_enabled".into(), + value: p.mercury_enabled, + }); + + self.path_rows.guard().clear(); + self.path_rows.guard().push_back(PathModelInit { + name: "XR Service Path".into(), + key: "xrservice_path".into(), + value: Some(p.xrservice_path) + }); + self.path_rows.guard().push_back(PathModelInit { + name: "OpenComposite Path".into(), + key: "opencomposite_path".into(), + value: Some(p.opencomposite_path) + }); + self.path_rows.guard().push_back(PathModelInit { + name: "Libsurvive Path".into(), + key: "libsurvive_path".into(), + value: p.libsurvive_path + }); + self.path_rows.guard().push_back(PathModelInit { + name: "Basalt Path".into(), + key: "basalt_path".into(), + value: p.basalt_path + }); + self.path_rows.guard().push_back(PathModelInit { + name: "Mercury Path".into(), + key: "mercury_path".into(), + value: p.mercury_path + }); + + self.profile = Some(prof); + win.present(); + } + Self::Input::SaveProfile => {} + Self::Input::EntryChanged(name, value) => { + println!("{}: {}", name, value); + self.profile + .as_mut() + .unwrap() + .environment + .insert(name, value); + } + Self::Input::AddEnvVar => { + println!("Add env var"); + } + } + } + + fn init( + init: Self::Init, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let add_env_var_btn = gtk::Button::builder() + .icon_name(icon_name::PLUS) + .tooltip_text("Add Environment Variable") + .css_classes(["flat"]) + .valign(gtk::Align::Start) + .halign(gtk::Align::End) + .build(); + { + let btn_sender = sender.clone(); + add_env_var_btn.connect_clicked(move |_| { + btn_sender.input(Self::Input::AddEnvVar); + }); + } + + let mut model = Self { + profile: None, + win: None, + name_row: adw::EntryRow::builder().title("Name").build(), + env_rows: FactoryVecDeque::new( + adw::PreferencesGroup::builder() + .title("Environment Variables") + .description(env_var_descriptions_as_paragraph()) + .header_suffix(&add_env_var_btn) + .build(), + sender.input_sender(), + ), + switch_rows: FactoryVecDeque::new( + adw::PreferencesGroup::builder() + .title("Components") + .description("Enable or disable features") + .build(), + sender.input_sender(), + ), + path_rows: FactoryVecDeque::new( + adw::PreferencesGroup::builder() + .title("Paths") + // .description("") + .build(), + sender.input_sender(), + ) + }; + + let widgets = view_output!(); + model.win = Some(widgets.win.clone()); + + ComponentParts { model, widgets } + } +} diff --git a/src/ui/runtime_switcher_box.rs b/src/ui/runtime_switcher_box.rs index 5df2117..1eb2789 100644 --- a/src/ui/runtime_switcher_box.rs +++ b/src/ui/runtime_switcher_box.rs @@ -1,6 +1,5 @@ use relm4::prelude::*; use gtk::prelude::*; - use crate::{constants::APP_NAME, file_builders::{active_runtime_json::{get_current_active_runtime, self, set_current_active_runtime_to_profile, set_current_active_runtime_to_steam}, openvrpaths_vrpath::{set_current_openvrpaths_to_profile, set_current_openvrpaths_to_steam}}, profile::Profile}; pub struct RuntimeSwitcherBox {