feat: initial implementation of profile editing

This commit is contained in:
Gabriele Musco 2023-06-25 12:13:20 +02:00
parent 6440a4f8b4
commit 13a1a951a9
No known key found for this signature in database
GPG key ID: 1068D795C80E51DE
12 changed files with 368 additions and 63 deletions

32
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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<Profile>,
}
@ -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>) -> 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<Profile>) {
self.user_profiles = profiles.iter().filter(|p| p.editable).map(Profile::clone).collect();
}
}
#[cfg(test)]

View file

@ -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,

View file

@ -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(),

View file

@ -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,

View file

@ -8,6 +8,7 @@ pub fn wivrn_profile() -> Profile {
let mut environment: HashMap<String, String> = 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,

View file

@ -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<Profile> {
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<Self>,
) -> ComponentParts<Self> {
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::<QuitAction>(&["<Control>q"]);
model
.application
.set_accelerators_for_action::<QuitAction>(&["<Control>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(),
));

View file

@ -8,8 +8,9 @@ use crate::ui::profile_editor::ProfileEditorMsg;
pub struct PathModel {
name: String,
key: String,
value: String,
value: Option<String>,
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<String>),
OpenFileChooser,
}
#[derive(Debug)]
pub enum PathModelOutMsg {
/** key, value */
Changed(String, String),
Changed(String, Option<String>),
}
#[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 = &gtk::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<Self>) {
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::Window>(),
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<Self::ParentInput> {
None
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
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 {
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(),
}
}
}

View file

@ -71,8 +71,10 @@ impl FactoryComponent for SwitchModel {
}
}
fn forward_to_parent(_output: Self::Output) -> Option<Self::ParentInput> {
None
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
Some(match output {
Self::Output::Changed(key, value) => ProfileEditorMsg::SwitchChanged(key, value)
})
}
fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender<Self>) -> Self {

View file

@ -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<String>,
profiles: Vec<Profile>,
selected_profile: Profile,
#[tracker::do_not_track]
profiles_dropdown: Option<gtk::DropDown>,
@ -36,6 +36,8 @@ pub struct MainView {
profile_editor: Controller<ProfileEditor>,
#[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<Profile>),
EnableDebugViewChanged(bool),
UpdateProfileNames(Vec<String>, Config),
UpdateProfiles(Vec<Profile>, 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<Self> {
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!();

View file

@ -35,8 +35,17 @@ pub struct ProfileEditor {
pub enum ProfileEditorMsg {
Present(Profile),
EntryChanged(String, String),
TextChanged(String, String),
PathChanged(String, Option<String>),
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 = &gtk::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());