feat: initial ui for profile editing

This commit is contained in:
Gabriele Musco 2023-06-22 18:53:29 +02:00
commit 50a89399e2
No known key found for this signature in database
GPG key ID: 1068D795C80E51DE
13 changed files with 554 additions and 19 deletions

64
Cargo.lock generated
View file

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

View file

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

View file

@ -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::<Vec<String>>().join("\n\n")
}

View file

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

View file

@ -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<Self>) {
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<Self::ParentInput> {
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 {
Self {
name: init.name,
value: init.value,
}
}
}

3
src/ui/factories/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod entry_row_factory;
pub mod switch_row_factory;
pub mod path_row_factory;

View file

@ -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<String>,
}
#[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<Self>) {
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<Self::ParentInput> {
None
}
fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender<Self>) -> 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(),
}
}
}

View file

@ -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<String>,
key: String,
value: bool,
}
pub struct SwitchModelInit {
pub name: String,
pub description: Option<String>,
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 = &gtk::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<Self>) {
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<Self::ParentInput> {
None
}
fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender<Self>) -> Self {
Self {
name: init.name,
description: init.description,
key: init.key,
value: init.value,
}
}
}

View file

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

View file

@ -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<String>,
selected_profile: Profile,
#[tracker::do_not_track]
profiles_dropdown: Option<gtk::DropDown>,
#[tracker::do_not_track]
@ -28,6 +30,8 @@ pub struct MainView {
steam_launch_options_box: Controller<SteamLaunchOptionsBox>,
#[tracker::do_not_track]
runtime_switcher_box: Controller<RuntimeSwitcherBox>,
#[tracker::do_not_track]
profile_editor: Controller<ProfileEditor>,
}
#[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 = &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()));
pack_start = &gtk::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<Self> {
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!();

View file

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

184
src/ui/profile_editor.rs Normal file
View file

@ -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<Profile>,
win: Option<adw::PreferencesWindow>,
name_row: adw::EntryRow,
env_rows: FactoryVecDeque<EntryModel>,
switch_rows: FactoryVecDeque<SwitchModel>,
path_rows: FactoryVecDeque<PathModel>,
}
#[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<Self>) {
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<Self>,
) -> ComponentParts<Self> {
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 }
}
}

View file

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