diff --git a/src/file_builders/monado_config_v0.rs b/src/file_builders/monado_config_v0.rs index 52c988e..96441b7 100644 --- a/src/file_builders/monado_config_v0.rs +++ b/src/file_builders/monado_config_v0.rs @@ -1,3 +1,5 @@ +use std::slice::Iter; + use serde::{Deserialize, Serialize}; use crate::{ @@ -41,6 +43,101 @@ pub enum XrtTrackerRole { ViveTrackerHtcxKeyboard, } +impl XrtTrackerRole { + pub fn to_picker_string(&self) -> &str { + match self { + Self::ViveTrackerHtcxWaist => "Waist", + Self::ViveTrackerHtcxLeftFoot => "Left foot", + Self::ViveTrackerHtcxRightFoot => "Right foot", + Self::ViveTrackerHtcxHandheldObject => "Handheld object", + Self::ViveTrackerHtcxLeftShoulder => "Left shoulder", + Self::ViveTrackerHtcxRightShoulder => "Right shoulder", + Self::ViveTrackerHtcxLeftElbow => "Left elbow", + Self::ViveTrackerHtcxRightElbow => "Right elbow", + Self::ViveTrackerHtcxLeftKnee => "Left knee", + Self::ViveTrackerHtcxRightKnee => "Right knee", + Self::ViveTrackerHtcxChest => "Chest", + Self::ViveTrackerHtcxCamera => "Camera", + Self::ViveTrackerHtcxKeyboard => "Keyboard", + } + } + + pub fn from_picker_string(s: &str) -> Self { + match s.to_lowercase().trim() { + "waist" => Self::ViveTrackerHtcxWaist, + "left foot" => Self::ViveTrackerHtcxLeftFoot, + "right foot" => Self::ViveTrackerHtcxRightFoot, + "handheld object" => Self::ViveTrackerHtcxHandheldObject, + "left shoulder" => Self::ViveTrackerHtcxLeftShoulder, + "right shoulder" => Self::ViveTrackerHtcxRightShoulder, + "left elbow" => Self::ViveTrackerHtcxLeftElbow, + "right elbow" => Self::ViveTrackerHtcxRightElbow, + "left knee" => Self::ViveTrackerHtcxLeftKnee, + "right knee" => Self::ViveTrackerHtcxRightKnee, + "chest" => Self::ViveTrackerHtcxChest, + "camera" => Self::ViveTrackerHtcxCamera, + "keyboard" => Self::ViveTrackerHtcxKeyboard, + _ => Self::ViveTrackerHtcxWaist, + } + } + + pub fn iter() -> Iter<'static, Self> { + [ + Self::ViveTrackerHtcxWaist, + Self::ViveTrackerHtcxLeftFoot, + Self::ViveTrackerHtcxRightFoot, + Self::ViveTrackerHtcxHandheldObject, + Self::ViveTrackerHtcxLeftShoulder, + Self::ViveTrackerHtcxRightShoulder, + Self::ViveTrackerHtcxLeftElbow, + Self::ViveTrackerHtcxRightElbow, + Self::ViveTrackerHtcxLeftKnee, + Self::ViveTrackerHtcxRightKnee, + Self::ViveTrackerHtcxChest, + Self::ViveTrackerHtcxCamera, + Self::ViveTrackerHtcxKeyboard, + ] + .iter() + } + + pub fn to_number(&self) -> u32 { + match self { + Self::ViveTrackerHtcxWaist => 0, + Self::ViveTrackerHtcxLeftFoot => 1, + Self::ViveTrackerHtcxRightFoot => 2, + Self::ViveTrackerHtcxHandheldObject => 3, + Self::ViveTrackerHtcxLeftShoulder => 4, + Self::ViveTrackerHtcxRightShoulder => 5, + Self::ViveTrackerHtcxLeftElbow => 6, + Self::ViveTrackerHtcxRightElbow => 7, + Self::ViveTrackerHtcxLeftKnee => 8, + Self::ViveTrackerHtcxRightKnee => 9, + Self::ViveTrackerHtcxChest => 10, + Self::ViveTrackerHtcxCamera => 11, + Self::ViveTrackerHtcxKeyboard => 12, + } + } + + pub fn from_number(n: &u32) -> Self { + match n { + 0 => Self::ViveTrackerHtcxWaist, + 1 => Self::ViveTrackerHtcxLeftFoot, + 2 => Self::ViveTrackerHtcxRightFoot, + 3 => Self::ViveTrackerHtcxHandheldObject, + 4 => Self::ViveTrackerHtcxLeftShoulder, + 5 => Self::ViveTrackerHtcxRightShoulder, + 6 => Self::ViveTrackerHtcxLeftElbow, + 7 => Self::ViveTrackerHtcxRightElbow, + 8 => Self::ViveTrackerHtcxLeftKnee, + 9 => Self::ViveTrackerHtcxRightKnee, + 10 => Self::ViveTrackerHtcxChest, + 11 => Self::ViveTrackerHtcxCamera, + 12 => Self::ViveTrackerHtcxKeyboard, + _ => Self::ViveTrackerHtcxWaist, + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TrackerRole { pub device_serial: String, @@ -48,6 +145,16 @@ pub struct TrackerRole { pub xrt_input_name: XrtInputName, } +impl Default for TrackerRole { + fn default() -> Self { + Self { + device_serial: "".into(), + role: XrtTrackerRole::ViveTrackerHtcxWaist, + xrt_input_name: XrtInputName::XrtInputGenericTrackerPose, + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub struct MonadoConfigV0 { #[serde(skip_serializing_if = "Option::is_none", rename = "$schema")] diff --git a/src/ui/app.rs b/src/ui/app.rs index 8371b4a..2d19e5d 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -2,6 +2,7 @@ use super::about_dialog::AboutDialog; use super::alert::alert; use super::build_window::{BuildStatus, BuildWindow}; use super::debug_view::{DebugView, DebugViewMsg}; +use super::fbt_config_editor::{FbtConfigEditor, FbtConfigEditorInit, FbtConfigEditorMsg}; use super::libsurvive_setup_window::LibsurviveSetupWindow; use super::main_view::MainViewMsg; use crate::builders::build_basalt::get_build_basalt_runners; @@ -86,6 +87,8 @@ pub struct App { profiles: Vec, #[tracker::do_not_track] xr_devices: XRDevices, + #[tracker::do_not_track] + fbt_config_editor: Option>, } #[derive(Debug)] @@ -103,6 +106,7 @@ pub enum Msg { SaveWinSize(i32, i32), Quit, ParseLog(Vec), + ConfigFbt, } impl App { @@ -528,6 +532,19 @@ impl SimpleComponent for App { )) .expect("Failed to present Libsurvive Setup Window"); } + Msg::ConfigFbt => { + self.fbt_config_editor = Some( + FbtConfigEditor::builder() + .launch(FbtConfigEditorInit { + root_win: self.app_win.clone().upcast::(), + }) + .detach(), + ); + self.fbt_config_editor + .as_ref() + .unwrap() + .emit(FbtConfigEditorMsg::Present); + } Msg::SaveWinSize(w, h) => { self.config.win_size = [w, h]; self.config.save(); @@ -621,6 +638,7 @@ impl SimpleComponent for App { xrservice_runner: None, build_pipeline: None, xr_devices: XRDevices::default(), + fbt_config_editor: None, }; let widgets = view_output!(); @@ -638,6 +656,12 @@ impl SimpleComponent for App { sender.input_sender().emit(Msg::BuildProfile(true)); }); } + { + withclones![sender]; + stateless_action!(actions, ConfigFbtAction, { + sender.input_sender().emit(Msg::ConfigFbt); + }); + } { let abd_sender = model.about_dialog.sender().clone(); stateless_action!(actions, AboutAction, { @@ -692,5 +716,6 @@ new_action_group!(pub AppActionGroup, "win"); new_stateless_action!(pub AboutAction, AppActionGroup, "about"); new_stateless_action!(pub BuildProfileAction, AppActionGroup, "buildprofile"); new_stateless_action!(pub BuildProfileCleanAction, AppActionGroup, "buildprofileclean"); +new_stateless_action!(pub ConfigFbtAction, AppActionGroup, "configfbt"); new_stateless_action!(pub QuitAction, AppActionGroup, "quit"); new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool); diff --git a/src/ui/factories/mod.rs b/src/ui/factories/mod.rs index ebb6df3..41fe8c9 100644 --- a/src/ui/factories/mod.rs +++ b/src/ui/factories/mod.rs @@ -1 +1,2 @@ pub mod env_var_row_factory; +pub mod tracker_role_group_factory; diff --git a/src/ui/factories/tracker_role_group_factory.rs b/src/ui/factories/tracker_role_group_factory.rs new file mode 100644 index 0000000..3eed25c --- /dev/null +++ b/src/ui/factories/tracker_role_group_factory.rs @@ -0,0 +1,109 @@ +use crate::{ + file_builders::monado_config_v0::{TrackerRole, XrtTrackerRole}, + ui::fbt_config_editor::FbtConfigEditorMsg, + ui::preference_rows::{combo_row, entry_row}, + withclones, +}; +use adw::prelude::*; +use relm4::prelude::*; + +#[derive(Debug)] +pub struct TrackerRoleModel { + index: usize, + tracker_role: TrackerRole, +} + +pub struct TrackerRoleModelInit { + pub index: usize, + pub tracker_role: Option, +} + +#[derive(Debug)] +pub enum TrackerRoleModelMsg { + Changed(TrackerRole), + Delete, +} + +#[derive(Debug)] +pub enum TrackerRoleModelOutMsg { + Changed(usize, TrackerRole), + Delete(usize), +} + +#[relm4::factory(pub)] +impl FactoryComponent for TrackerRoleModel { + type Init = TrackerRoleModelInit; + type Input = TrackerRoleModelMsg; + type Output = TrackerRoleModelOutMsg; + type CommandOutput = (); + type Widgets = TrackerRoleModelWidgets; + type ParentInput = FbtConfigEditorMsg; + type ParentWidget = adw::PreferencesPage; + + view! { + root = adw::PreferencesGroup { + set_title: "Tracker", + #[wrap(Some)] + set_header_suffix: del_btn = >k::Button { + set_icon_name: "edit-delete-symbolic", + set_tooltip_text: Some("Delete Tracker"), + set_valign: gtk::Align::Center, + add_css_class: "flat", + add_css_class: "circular", + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Delete); + } + }, + add: { + withclones![sender]; + let tr = self.tracker_role.clone(); + &entry_row("Device serial", self.tracker_role.device_serial.as_str(), move |row| { + let mut ntr = tr.clone(); + ntr.device_serial = row.text().to_string(); + sender.input(Self::Input::Changed(ntr)); + }) + }, + add: { + withclones![sender]; + let tr = self.tracker_role.clone(); + &combo_row("Tracker role", None, &tr.role.clone().to_picker_string(), + XrtTrackerRole::iter() + .map(XrtTrackerRole::to_picker_string) + .map(String::from) + .collect::>(), + move |row| { + let mut ntr = tr.clone(); + ntr.role = XrtTrackerRole::from_number(&row.selected()); + sender.input(Self::Input::Changed(ntr)); + } + ) + }, + } + } + + fn forward_to_parent(output: Self::Output) -> Option { + Some(match output { + Self::Output::Changed(index, tracker_role) => { + Self::ParentInput::TrackerRoleChanged(index, tracker_role.clone()) + } + Self::Output::Delete(index) => Self::ParentInput::TrackerRoleDeleted(index), + }) + } + + fn update(&mut self, message: Self::Input, sender: FactorySender) { + match message { + Self::Input::Changed(r) => { + self.tracker_role = r; + sender.output(Self::Output::Changed(self.index, self.tracker_role.clone())); + } + Self::Input::Delete => sender.output(Self::Output::Delete(self.index)), + } + } + + fn init_model(init: Self::Init, _index: &Self::Index, _sender: FactorySender) -> Self { + Self { + index: init.index, + tracker_role: init.tracker_role.unwrap_or_else(|| TrackerRole::default()), + } + } +} diff --git a/src/ui/fbt_config_editor.rs b/src/ui/fbt_config_editor.rs new file mode 100644 index 0000000..9d495c1 --- /dev/null +++ b/src/ui/fbt_config_editor.rs @@ -0,0 +1,152 @@ +use super::factories::tracker_role_group_factory::TrackerRoleModel; +use crate::{ + file_builders::monado_config_v0::{ + dump_monado_config_v0, get_monado_config_v0, MonadoConfigV0, TrackerRole, + }, + ui::factories::tracker_role_group_factory::TrackerRoleModelInit, + withclones, +}; +use adw::prelude::*; +use relm4::{factory::FactoryVecDeque, prelude::*}; + +#[tracker::track] +pub struct FbtConfigEditor { + monado_config_v0: MonadoConfigV0, + + #[tracker::do_not_track] + win: Option, + + #[tracker::do_not_track] + tracker_role_groups: FactoryVecDeque, +} + +#[derive(Debug)] +pub enum FbtConfigEditorMsg { + Present, + Save, + TrackerRoleNew, + TrackerRoleChanged(usize, TrackerRole), + TrackerRoleDeleted(usize), + Repopulate, +} + +pub struct FbtConfigEditorInit { + pub root_win: gtk::Window, +} +#[relm4::component(pub)] +impl SimpleComponent for FbtConfigEditor { + type Init = FbtConfigEditorInit; + type Input = FbtConfigEditorMsg; + type Output = (); + + view! { + #[name(win)] + adw::PreferencesWindow { + set_title: Some("Full Body Trackers"), + set_modal: true, + set_transient_for: Some(&init.root_win), + add: model.tracker_role_groups.widget(), + } + } + + fn update(&mut self, message: Self::Input, sender: ComponentSender) { + self.reset(); + + match message { + Self::Input::Present => { + self.win.as_ref().unwrap().present(); + } + Self::Input::Save => { + dump_monado_config_v0(&self.monado_config_v0); + self.win.as_ref().unwrap().close(); + } + Self::Input::TrackerRoleChanged(index, n_role) => { + let role = self.monado_config_v0.tracker_roles.get_mut(index).unwrap(); + role.device_serial = n_role.device_serial; + role.role = n_role.role; + role.xrt_input_name = n_role.xrt_input_name; + } + Self::Input::TrackerRoleDeleted(index) => { + self.monado_config_v0.tracker_roles.remove(index); + sender.input(Self::Input::Repopulate); + } + Self::Input::TrackerRoleNew => { + self.monado_config_v0 + .tracker_roles + .push(TrackerRole::default()); + self.tracker_role_groups + .guard() + .push_back(TrackerRoleModelInit { + index: self.monado_config_v0.tracker_roles.len() - 1, + tracker_role: None, + }); + } + Self::Input::Repopulate => { + self.populate_tracker_roles(); + } + } + } + + fn init( + init: Self::Init, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let page = adw::PreferencesPage::builder().build(); + let grp = adw::PreferencesGroup::builder().build(); + let add_btn = gtk::Button::builder() + .label("Add Tracker") + .margin_bottom(12) + .css_classes(["pill"]) + .halign(gtk::Align::Center) + .build(); + let save_btn = gtk::Button::builder() + .label("Save") + .margin_bottom(12) + .halign(gtk::Align::Center) + .css_classes(["suggested-action", "pill"]) + .build(); + { + withclones![sender]; + add_btn.connect_clicked(move |_| { + sender.input(Self::Input::TrackerRoleNew); + }); + }; + { + withclones![sender]; + save_btn.connect_clicked(move |_| { + sender.input(Self::Input::Save); + }); + }; + grp.add(&save_btn); + grp.add(&add_btn); + page.add(&grp); + + let mut model = Self { + win: None, + tracker: 0, + monado_config_v0: get_monado_config_v0(), + tracker_role_groups: FactoryVecDeque::new(page, sender.input_sender()), + }; + + model.populate_tracker_roles(); + + let widgets = view_output!(); + model.win = Some(widgets.win.clone()); + + ComponentParts { model, widgets } + } +} + +impl FbtConfigEditor { + fn populate_tracker_roles(&mut self) { + let mut guard = self.tracker_role_groups.guard(); + guard.clear(); + for (i, v) in self.monado_config_v0.tracker_roles.iter().enumerate() { + guard.push_back(TrackerRoleModelInit { + index: i, + tracker_role: Some(v.clone()), + }); + } + } +} diff --git a/src/ui/main_view.rs b/src/ui/main_view.rs index 6e8f768..175cc0f 100644 --- a/src/ui/main_view.rs +++ b/src/ui/main_view.rs @@ -10,7 +10,8 @@ use crate::gpu_profile::{get_gpu_power_profile, get_set_vr_pow_prof_cmd, GpuPowe use crate::profile::{LighthouseDriver, Profile}; use crate::steamvr_utils::chaperone_info_exists; use crate::ui::app::{ - AboutAction, BuildProfileAction, BuildProfileCleanAction, DebugViewToggleAction, + AboutAction, BuildProfileAction, BuildProfileCleanAction, ConfigFbtAction, + DebugViewToggleAction, }; use crate::ui::profile_editor::ProfileEditorInit; use crate::ui::util::limit_dropdown_width; @@ -109,6 +110,7 @@ impl SimpleComponent for MainView { "_Debug View" => DebugViewToggleAction, "_Build Profile" => BuildProfileAction, "C_lean Build Profile" => BuildProfileCleanAction, + "Configure Full Body _Tracking" => ConfigFbtAction, }, section! { "_About" => AboutAction, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 81adc39..9b78670 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,6 +5,7 @@ pub mod build_window; pub mod debug_view; pub mod devices_box; pub mod factories; +pub mod fbt_config_editor; pub mod install_wivrn_box; pub mod libsurvive_setup_window; pub mod macros; diff --git a/src/ui/profile_editor.rs b/src/ui/profile_editor.rs index 266ecba..288ea21 100644 --- a/src/ui/profile_editor.rs +++ b/src/ui/profile_editor.rs @@ -1,11 +1,8 @@ -use super::{ - factories::env_var_row_factory::{EnvVarModel, EnvVarModelInit}, - preference_rows::combo_row, -}; +use super::factories::env_var_row_factory::{EnvVarModel, EnvVarModelInit}; use crate::{ env_var_descriptions::env_var_descriptions_as_paragraph, profile::{LighthouseDriver, Profile, XRServiceType}, - ui::preference_rows::{entry_row, path_row, switch_row}, + ui::preference_rows::{combo_row, entry_row, path_row, switch_row}, withclones, }; use adw::prelude::*;