diff --git a/Cargo.lock b/Cargo.lock index 9f397d0..91c191b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1351,6 +1351,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1419,6 +1425,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] @@ -1427,6 +1445,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "raw-window-handle" @@ -1588,6 +1609,7 @@ dependencies = [ "serde", "serde_json", "tracker", + "uuid", ] [[package]] @@ -2042,6 +2064,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" +dependencies = [ + "getrandom", + "rand", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 857568a..c604b11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,4 @@ serde = { version = "1.0.163", features = [ ] } serde_json = "1.0.96" tracker = "0.2.1" +uuid = { version = "1.3.4", features = ["v4", "fast-rng"] } diff --git a/src/config.rs b/src/config.rs index ce41047..8ffe405 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,7 +8,7 @@ use std::{fs::File, io::BufReader}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Config { - pub selected_profile_name: String, + pub selected_profile_uuid: String, pub debug_view_enabled: bool, pub user_profiles: Vec, } @@ -17,7 +17,7 @@ impl Default for Config { fn default() -> Self { Config { // TODO: handle first start with no profile selected - selected_profile_name: "".to_string(), + selected_profile_uuid: "".to_string(), debug_view_enabled: false, user_profiles: vec![], } @@ -26,7 +26,7 @@ impl Default for Config { impl Config { pub fn get_selected_profile(&self, profiles: &Vec) -> Profile { - match profiles.iter().find(|p| {p.name == self.selected_profile_name}) { + match profiles.iter().find(|p| {p.uuid == self.selected_profile_uuid}) { Some(p) => p.clone(), None => profiles.get(0).expect_dialog("No profiles found").clone(), } @@ -65,6 +65,10 @@ impl Config { pub fn get_config() -> Self { Self::from_path(Self::config_file_path()) } + + pub fn set_profiles(&mut self, profiles: &Vec) { + self.user_profiles = profiles.iter().filter(|p| p.editable).map(Profile::clone).collect(); + } } #[cfg(test)] diff --git a/src/profile.rs b/src/profile.rs index e97da9c..cf08115 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -5,6 +5,7 @@ use crate::{ use expect_dialog::ExpectDialog; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fmt::Display, fs::File, io::BufReader, slice::Iter}; +use uuid::Uuid; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum XRServiceType { @@ -27,7 +28,8 @@ impl XRServiceType { pub fn as_number(&self) -> u32 { match self { - Self::Monado => 0, Self::Wivrn => 1, + Self::Monado => 0, + Self::Wivrn => 1, } } } @@ -43,6 +45,7 @@ impl Display for XRServiceType { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Profile { + pub uuid: String, pub name: String, pub xrservice_path: String, pub xrservice_type: XRServiceType, @@ -69,6 +72,7 @@ impl Display for Profile { impl Default for Profile { fn default() -> Self { Self { + uuid: Uuid::new_v4().to_string(), name: "Default profile name".into(), xrservice_path: data_monado_path(), xrservice_type: XRServiceType::Monado, @@ -125,6 +129,26 @@ impl Profile { let writer = get_writer(path_s); serde_json::to_writer_pretty(writer, self).expect_dialog("Could not write profile") } + + pub fn create_duplicate(&self) -> Self { + let mut dup = self.clone(); + dup.uuid = Uuid::new_v4().to_string(); + dup.editable = true; + dup.name = format!("Duplicate of {}", dup.name); + dup + } + + pub fn validate(&self) -> bool { + !self.name.is_empty() + && self.editable + && !self.uuid.is_empty() + && !self.xrservice_path.is_empty() + && !self.prefix.is_empty() + && (!self.libsurvive_enabled + || self.libsurvive_path.as_ref().is_some_and(|p| !p.is_empty())) + && (!self.basalt_enabled || self.basalt_path.as_ref().is_some_and(|p| !p.is_empty())) + && (!self.mercury_enabled || self.mercury_path.as_ref().is_some_and(|p| !p.is_empty())) + } } #[cfg(test)] @@ -166,6 +190,7 @@ mod tests { env.insert("XRT_COMPOSITOR_SCALE_PERCENTAGE".into(), "140".into()); env.insert("XRT_COMPOSITOR_COMPUTE".into(), "1".into()); let p = Profile { + uuid: "demo".into(), name: "Demo profile".into(), xrservice_path: String::from("/home/user/monado"), xrservice_type: XRServiceType::Monado, diff --git a/src/profiles/system_valve_index.rs b/src/profiles/system_valve_index.rs index 3928717..6b71bbf 100644 --- a/src/profiles/system_valve_index.rs +++ b/src/profiles/system_valve_index.rs @@ -8,6 +8,7 @@ pub fn system_valve_index_profile() -> Profile { environment.insert("SURVIVE_GLOBALSCENESOLVER".into(), "0".into()); environment.insert("SURVIVE_TIMECODE_OFFSET_MS".into(), "-6.94".into()); Profile { + uuid: "system-valve-index-default".into(), name: format!("Valve Index (System) - {name} Default", name = APP_NAME), opencomposite_path: data_opencomposite_path(), xrservice_path: "".into(), diff --git a/src/profiles/valve_index.rs b/src/profiles/valve_index.rs index 1736495..e276d04 100644 --- a/src/profiles/valve_index.rs +++ b/src/profiles/valve_index.rs @@ -11,6 +11,7 @@ pub fn valve_index_profile() -> Profile { environment.insert("SURVIVE_TIMECODE_OFFSET_MS".into(), "-6.94".into()); environment.insert("LD_LIBRARY_PATH".into(), format!("{pfx}/lib", pfx = prefix)); Profile { + uuid: "valve-index-default".into(), name: format!("Valve Index - {name} Default", name = APP_NAME), xrservice_path: data_monado_path(), xrservice_type: XRServiceType::Monado, diff --git a/src/profiles/wivrn.rs b/src/profiles/wivrn.rs index 5ffd1f4..96bacb9 100644 --- a/src/profiles/wivrn.rs +++ b/src/profiles/wivrn.rs @@ -8,6 +8,7 @@ pub fn wivrn_profile() -> Profile { let mut environment: HashMap = HashMap::new(); environment.insert("LD_LIBRARY_PATH".into(), format!("{pfx}/lib", pfx = prefix)); Profile { + uuid: "wivrn-default".into(), name: format!("WiVRn - {name} Default", name = APP_NAME), xrservice_path: data_wivrn_path(), xrservice_type: XRServiceType::Wivrn, diff --git a/src/ui/app.rs b/src/ui/app.rs index 3c316b8..e517258 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -29,7 +29,7 @@ use crate::ui::libsurvive_setup_window::LibsurviveSetupMsg; use crate::ui::main_view::{MainView, MainViewInit, MainViewOutMsg}; use expect_dialog::ExpectDialog; use gtk::prelude::*; -use relm4::actions::{ActionGroupName, RelmAction, RelmActionGroup, AccelsPlus}; +use relm4::actions::{AccelsPlus, ActionGroupName, RelmAction, RelmActionGroup}; use relm4::adw::traits::MessageDialogExt; use relm4::adw::ResponseAppearance; use relm4::gtk::glib; @@ -77,7 +77,9 @@ pub enum Msg { BuildProfile, EnableDebugViewChanged(bool), DoStartStopXRService, - ProfileSelected(String), + ProfileSelected(Profile), + DeleteProfile, + SaveProfile(Profile), RunSetCap, OpenLibsurviveSetup, Quit, @@ -103,6 +105,17 @@ impl App { runner.start(); self.xrservice_runner = Some(runner); } + + pub fn profiles_list(config: &Config) -> Vec { + let mut profiles = vec![ + valve_index_profile(), + system_valve_index_profile(), + wivrn_profile(), + ]; + profiles.extend(config.user_profiles.clone()); + profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name)); + profiles + } } #[derive(Debug)] @@ -196,7 +209,9 @@ impl SimpleComponent for App { ); // apparently setcap on wivrn causes issues, so in case // it's not monado, we're just skipping this - if self.get_selected_profile().xrservice_type == XRServiceType::Monado { + if self.get_selected_profile().xrservice_type + == XRServiceType::Monado + { self.setcap_confirm_dialog.present(); } self.build_window @@ -320,6 +335,47 @@ impl SimpleComponent for App { .emit(BuildWindowMsg::UpdateCanClose(false)); self.build_pipeline = Some(pipeline); } + Msg::DeleteProfile => { + let todel = self.get_selected_profile(); + if todel.editable { + self.config.user_profiles = self + .config + .user_profiles + .iter() + .filter(|p| p.uuid != todel.uuid) + .map(|p| p.clone()) + .collect(); + self.config.save(); + self.profiles = Self::profiles_list(&self.config); + self.main_view.sender().emit(MainViewMsg::UpdateSelectedProfile( + self.get_selected_profile() + )); + self.main_view + .sender() + .emit(MainViewMsg::UpdateProfiles( + self.profiles.clone(), + self.config.clone(), + )) + } + } + Msg::SaveProfile(prof) => { + match self.profiles.iter().position(|p| p.uuid == prof.uuid) { + None => {}, + Some(index) => { + self.profiles.remove(index); + } + } + self.profiles.push(prof); + self.profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name)); + self.config.set_profiles(&self.profiles); + self.config.save(); + self.main_view + .sender() + .emit(MainViewMsg::UpdateProfiles( + self.profiles.clone(), + self.config.clone(), + )) + } Msg::RunSetCap => { if !check_dependency(pkexec_dep()) { println!("pkexec not found, skipping setcap"); @@ -331,11 +387,11 @@ impl SimpleComponent for App { )); } } - Msg::ProfileSelected(prof_name) => { - if prof_name == self.config.selected_profile_name { + Msg::ProfileSelected(prof) => { + if prof.uuid == self.config.selected_profile_uuid { return; } - self.config.selected_profile_name = prof_name; + self.config.selected_profile_uuid = prof.uuid; self.config.save(); let profile = self.get_selected_profile(); self.main_view @@ -349,7 +405,7 @@ impl SimpleComponent for App { self.get_selected_profile().clone(), )) .expect_dialog("Failed to present Libsurvive Setup Window"); - }, + } Msg::Quit => { self.application.quit(); } @@ -362,12 +418,7 @@ impl SimpleComponent for App { sender: ComponentSender, ) -> ComponentParts { let config = Config::get_config(); - let mut profiles = vec![ - valve_index_profile(), - system_valve_index_profile(), - wivrn_profile(), - ]; - profiles.extend(config.user_profiles.clone()); + let profiles = Self::profiles_list(&config); let dependencies_dialog = adw::MessageDialog::builder() .modal(true) .transient_for(root) @@ -410,7 +461,9 @@ impl SimpleComponent for App { .forward(sender.input_sender(), |message| match message { MainViewOutMsg::EnableDebugViewChanged(val) => Msg::EnableDebugViewChanged(val), MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService, - MainViewOutMsg::ProfileSelected(name) => Msg::ProfileSelected(name), + MainViewOutMsg::ProfileSelected(uuid) => Msg::ProfileSelected(uuid), + MainViewOutMsg::DeleteProfile => Msg::DeleteProfile, + MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p), }), debug_view: DebugView::builder() .launch(DebugViewInit { @@ -492,13 +545,15 @@ impl SimpleComponent for App { actions.add_action(libsurvive_setup_action); root.insert_action_group(AppActionGroup::NAME, Some(&actions.into_action_group())); - model.application.set_accelerators_for_action::(&["q"]); + model + .application + .set_accelerators_for_action::(&["q"]); model .main_view .sender() - .emit(MainViewMsg::UpdateProfileNames( - model.profiles.iter().map(|p| p.clone().name).collect(), + .emit(MainViewMsg::UpdateProfiles( + model.profiles.clone(), model.config.clone(), )); diff --git a/src/ui/factories/path_row_factory.rs b/src/ui/factories/path_row_factory.rs index e824ffd..6908b33 100644 --- a/src/ui/factories/path_row_factory.rs +++ b/src/ui/factories/path_row_factory.rs @@ -8,8 +8,9 @@ use crate::ui::profile_editor::ProfileEditorMsg; pub struct PathModel { name: String, key: String, - value: String, + value: Option, path_label: gtk::Label, + filedialog: gtk::FileDialog, } pub struct PathModelInit { @@ -20,14 +21,14 @@ pub struct PathModelInit { #[derive(Debug)] pub enum PathModelMsg { - Changed(String), + Changed(Option), OpenFileChooser, } #[derive(Debug)] pub enum PathModelOutMsg { /** key, value */ - Changed(String, String), + Changed(String, Option), } #[relm4::factory(pub)] @@ -47,41 +48,77 @@ impl FactoryComponent for PathModel { set_icon_name: Some("folder-open-symbolic"), add_suffix: &self.path_label, set_activatable: true, + add_suffix: unset_btn = >k::Button { + set_valign: gtk::Align::Center, + add_css_class: "flat", + add_css_class: "circular", + set_icon_name: "edit-clear", + set_tooltip_text: Some("Clear Path"), + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Changed(None)); + } + }, 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()); + self.value = val; + self.path_label.set_label(match self.value.as_ref() { + Some(val) => val.as_str(), + None => "(None)".into(), + }); sender .output_sender() - .emit(Self::Output::Changed(self.key.clone(), val)) + .emit(Self::Output::Changed(self.key.clone(), self.value.clone())) } Self::Input::OpenFileChooser => { - println!("file chooser"); + let fd_sender = sender.clone(); + self.filedialog.select_folder( + self.init_root().root().and_downcast_ref::(), + gtk::gio::Cancellable::NONE, + move |res| match res { + Ok(file) => { + let path = file.path(); + if path.is_some() { + fd_sender.input(Self::Input::Changed( + Some(path.unwrap().to_str().unwrap().to_string()) + )); + } + } + _ => {} + }, + ); } } } - fn forward_to_parent(_output: Self::Output) -> Option { - None + fn forward_to_parent(output: Self::Output) -> Option { + Some(match output { + Self::Output::Changed(key, value) => ProfileEditorMsg::PathChanged(key, value) + }) } fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender) -> Self { Self { - name: init.name, + name: init.name.clone(), 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(), + value: init.value.clone(), + path_label: gtk::Label::builder() + .label(match init.value { + Some(val) => val, + None => "(None)".into(), + }) + .ellipsize(gtk::pango::EllipsizeMode::Start) + .build(), + filedialog: gtk::FileDialog::builder() + .modal(true) + .title(format!("Select Path for {}", init.name.clone())) + .build(), } } } diff --git a/src/ui/factories/switch_row_factory.rs b/src/ui/factories/switch_row_factory.rs index 6d9dbfa..df7472f 100644 --- a/src/ui/factories/switch_row_factory.rs +++ b/src/ui/factories/switch_row_factory.rs @@ -71,8 +71,10 @@ impl FactoryComponent for SwitchModel { } } - fn forward_to_parent(_output: Self::Output) -> Option { - None + fn forward_to_parent(output: Self::Output) -> Option { + Some(match output { + Self::Output::Changed(key, value) => ProfileEditorMsg::SwitchChanged(key, value) + }) } fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender) -> Self { diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 457bd4d..584b04c 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -1,5 +1,5 @@ use super::install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg}; -use super::profile_editor::{ProfileEditor, ProfileEditorMsg}; +use super::profile_editor::{ProfileEditor, ProfileEditorMsg, ProfileEditorOutMsg}; use super::runtime_switcher_box::{ RuntimeSwitcherBox, RuntimeSwitcherBoxInit, RuntimeSwitcherBoxMsg, }; @@ -22,7 +22,7 @@ use relm4_icons::icon_name; pub struct MainView { xrservice_active: bool, enable_debug_view: bool, - profile_names: Vec, + profiles: Vec, selected_profile: Profile, #[tracker::do_not_track] profiles_dropdown: Option, @@ -36,6 +36,8 @@ pub struct MainView { profile_editor: Controller, #[tracker::do_not_track] profile_not_editable_dialog: adw::MessageDialog, + #[tracker::do_not_track] + profile_delete_confirm_dialog: adw::MessageDialog, } #[derive(Debug)] @@ -44,20 +46,24 @@ pub enum MainViewMsg { StartStopClicked, XRServiceActiveChanged(bool, Option), EnableDebugViewChanged(bool), - UpdateProfileNames(Vec, Config), + UpdateProfiles(Vec, Config), SetSelectedProfile(u32), ProfileSelected(u32), UpdateSelectedProfile(Profile), EditProfile, CreateProfile, + DeleteProfile, DuplicateProfile, + SaveProfile(Profile), } #[derive(Debug)] pub enum MainViewOutMsg { EnableDebugViewChanged(bool), DoStartStopXRService, - ProfileSelected(String), + ProfileSelected(Profile), + DeleteProfile, + SaveProfile(Profile), } pub struct MainViewInit { @@ -156,9 +162,9 @@ impl SimpleComponent for MainView { gtk::DropDown { set_hexpand: true, set_tooltip_text: Some("Profiles"), - #[track = "model.changed(Self::profile_names())"] + #[track = "model.changed(Self::profiles())"] set_model: Some(&{ - let names: Vec<_> = model.profile_names.iter().map(String::as_str).collect(); + let names: Vec<_> = model.profiles.iter().map(|p| p.name.as_str()).collect(); gtk::StringList::new(&names) }), connect_selected_item_notify[sender] => move |this| { @@ -178,7 +184,17 @@ impl SimpleComponent for MainView { connect_clicked[sender] => move |_| { sender.input(Self::Input::CreateProfile); } - } + }, + gtk::Button { + set_icon_name: "edit-delete-symbolic", + add_css_class: "destructive-action", + set_tooltip_text: Some("Delete Profile"), + #[track = "model.changed(Self::selected_profile())"] + set_sensitive: model.selected_profile.editable, + connect_clicked[sender] => move |_| { + sender.input(Self::Input::DeleteProfile); + } + }, }, } } @@ -222,17 +238,17 @@ impl SimpleComponent for MainView { .sender() .emit(RuntimeSwitcherBoxMsg::UpdateSelectedProfile(prof.clone())); } - Self::Input::UpdateProfileNames(names, config) => { - self.set_profile_names(names); + Self::Input::UpdateProfiles(profiles, config) => { + self.set_profiles(profiles); // why send another message to set the dropdown selection? // set_* from tracker likely updates the view obj in the next // draw, so selecting here will result in nothing cause the // dropdown is effectively empty sender.input(MainViewMsg::SetSelectedProfile({ let pos = self - .profile_names + .profiles .iter() - .position(|p| p.clone() == config.selected_profile_name); + .position(|p| p.uuid == config.selected_profile_uuid); match pos { Some(idx) => idx as u32, None => 0, @@ -247,10 +263,8 @@ impl SimpleComponent for MainView { .set_selected(index); } Self::Input::ProfileSelected(position) => { - // self.install_wivrn_box.sender.emit(InstallWivrnBoxMsg::Upda); - // TODO: send profile to install_wivrn_box sender.output(MainViewOutMsg::ProfileSelected( - self.profile_names.get(position as usize).unwrap().clone(), + self.profiles.get(position as usize).unwrap().clone(), )); } Self::Input::EditProfile => { @@ -262,10 +276,20 @@ impl SimpleComponent for MainView { } } Self::Input::CreateProfile => { - println!("create"); + self.profile_editor + .sender() + .emit(ProfileEditorMsg::Present(Profile::default())); + } + Self::Input::DeleteProfile => { + self.profile_delete_confirm_dialog.present(); + } + Self::Input::SaveProfile(prof) => { + sender.output(Self::Output::SaveProfile(prof)); } Self::Input::DuplicateProfile => { - println!("dup"); + self.profile_editor.sender().emit(ProfileEditorMsg::Present( + self.selected_profile.create_duplicate(), + )); } } } @@ -277,12 +301,12 @@ impl SimpleComponent for MainView { ) -> ComponentParts { let profile_not_editable_dialog = adw::MessageDialog::builder() .modal(true) + .hide_on_close(true) .heading("This profile is not editable") .body(concat!( "You can duplicate it and edit the new copy. ", "Do you want to duplicate the current profile?" )) - .hide_on_close(true) .build(); profile_not_editable_dialog.add_response("no", "_No"); profile_not_editable_dialog.add_response("yes", "_Yes"); @@ -298,11 +322,31 @@ impl SimpleComponent for MainView { }); } + let profile_delete_confirm_dialog = adw::MessageDialog::builder() + .modal(true) + .hide_on_close(true) + .heading("Are you sure you want to delete this profile?") + .build(); + profile_delete_confirm_dialog.add_response("no", "_No"); + profile_delete_confirm_dialog.add_response("yes", "_Yes"); + profile_delete_confirm_dialog + .set_response_appearance("no", ResponseAppearance::Destructive); + profile_delete_confirm_dialog.set_response_appearance("yes", ResponseAppearance::Suggested); + + { + let pdc_sender = sender.clone(); + profile_delete_confirm_dialog.connect_response(None, move |_, res| { + if res == "yes" { + pdc_sender.output(Self::Output::DeleteProfile); + } + }); + } + let mut model = Self { xrservice_active: false, enable_debug_view: init.config.debug_view_enabled, profiles_dropdown: None, - profile_names: vec![], + profiles: vec![], steam_launch_options_box: SteamLaunchOptionsBox::builder().launch(()).detach(), install_wivrn_box: InstallWivrnBox::builder() .launch(InstallWivrnBoxInit { @@ -316,9 +360,12 @@ impl SimpleComponent for MainView { .detach(), profile_editor: ProfileEditor::builder() .launch(ProfileEditorInit {}) - .detach(), + .forward(sender.input_sender(), |message| match message { + ProfileEditorOutMsg::SaveProfile(p) => Self::Input::SaveProfile(p), + }), selected_profile: init.selected_profile.clone(), profile_not_editable_dialog, + profile_delete_confirm_dialog, tracker: 0, }; let widgets = view_output!(); diff --git a/src/ui/profile_editor.rs b/src/ui/profile_editor.rs index 2c9a0d2..a6c477a 100644 --- a/src/ui/profile_editor.rs +++ b/src/ui/profile_editor.rs @@ -35,8 +35,17 @@ pub struct ProfileEditor { pub enum ProfileEditorMsg { Present(Profile), EntryChanged(String, String), + TextChanged(String, String), + PathChanged(String, Option), + SwitchChanged(String, bool), + ComboChanged(String, String), AddEnvVar, - SaveProfile, // ? + SaveProfile, +} + +#[derive(Debug)] +pub enum ProfileEditorOutMsg { + SaveProfile(Profile), } pub struct ProfileEditorInit {} @@ -45,7 +54,7 @@ pub struct ProfileEditorInit {} impl SimpleComponent for ProfileEditor { type Init = ProfileEditorInit; type Input = ProfileEditorMsg; - type Output = (); + type Output = ProfileEditorOutMsg; view! { #[name(win)] @@ -66,6 +75,21 @@ impl SimpleComponent for ProfileEditor { add: model.env_rows.widget(), add: model.switch_rows.widget(), add: model.path_rows.widget(), + add: save_grp = &adw::PreferencesGroup { + add: save_box = >k::Box { + set_orientation: gtk::Orientation::Vertical, + set_hexpand: true, + gtk::Button { + set_halign: gtk::Align::Center, + set_label: "Save", + add_css_class: "pill", + add_css_class: "suggested-action", + connect_clicked[sender] => move |_| { + sender.input(Self::Input::SaveProfile); + }, + }, + } + }, } } } @@ -135,20 +159,71 @@ impl SimpleComponent for ProfileEditor { key: "mercury_path".into(), value: p.mercury_path, }); + self.path_rows.guard().push_back(PathModelInit { + name: "Install Prefix".into(), + key: "prefix".into(), + value: Some(p.prefix), + }); self.set_profile(Some(prof.clone())); self.win.as_ref().unwrap().present(); } - Self::Input::SaveProfile => {} + Self::Input::SaveProfile => { + let prof = self.profile.as_ref().unwrap(); + if prof.validate() { + sender.output(ProfileEditorOutMsg::SaveProfile(prof.clone())); + self.win.as_ref().unwrap().close(); + } else { + self.win.as_ref().unwrap().add_toast(adw::Toast::builder().title("Profile failed validation").build()); + } + } + // TODO: rename to reflect this is only for env + // + make entryfactory take a signal to send to parent Self::Input::EntryChanged(name, value) => { - println!("{}: {}", name, value); self.profile .as_mut() .unwrap() .environment .insert(name, value); } + Self::Input::PathChanged(key, value) => { + let prof = self.profile.as_mut().unwrap(); + match key.as_str() { + "xrservice_path" => prof.xrservice_path = value.unwrap_or("".to_string()), + "opencomposite_path" => { + prof.opencomposite_path = value.unwrap_or("".to_string()) + } + "libsurvive_path" => prof.libsurvive_path = value, + "basalt_path" => prof.basalt_path = value, + "mercury_path" => prof.mercury_path = value, + "prefix" => prof.prefix = value.unwrap_or("".to_string()), + _ => panic!("Unknown profile path key"), + } + } + Self::Input::SwitchChanged(key, value) => { + let prof = self.profile.as_mut().unwrap(); + match key.as_str() { + "libsurvive_enabled" => prof.libsurvive_enabled = value, + "basalt_enabled" => prof.basalt_enabled = value, + "mercury_enabled" => prof.mercury_enabled = value, + _ => panic!("Unknown profile switch key"), + } + } + Self::Input::TextChanged(key, value) => { + let prof = self.profile.as_mut().unwrap(); + match key.as_str() { + "name" => prof.name = value, + _ => panic!("Unknown profile text key"), + } + } + Self::Input::ComboChanged(key, value) => { + let prof = self.profile.as_mut().unwrap(); + match key.as_str() { + "xrservice_type" => prof.xrservice_type = XRServiceType::from_string(value), + _ => panic!("Unknown profile text key"), + } + } Self::Input::AddEnvVar => { println!("Add env var"); } @@ -218,6 +293,30 @@ impl SimpleComponent for ProfileEditor { tracker: 0, }; + { + let name_sender = sender.clone(); + model.name_row.connect_changed(move |nr| { + name_sender.input(Self::Input::TextChanged( + "name".to_string(), + nr.text().to_string(), + )); + }); + } + + { + let type_sender = sender.clone(); + model.type_row.connect_selected_item_notify(move |tr| { + type_sender.input(Self::Input::ComboChanged( + "xrservice_type".to_string(), + match tr.selected() { + 0 => "monado".to_string(), + 1 => "wivrn".to_string(), + _ => panic!("XRServiceType combo row cannot have more than 2 choices"), + }, + )); + }); + } + let widgets = view_output!(); model.win = Some(widgets.win.clone());