feat: full body tracking tracker role assignment ui

This commit is contained in:
Gabriele Musco 2023-08-25 20:18:02 +00:00
parent 7580988ed8
commit ac340ae6f2
8 changed files with 400 additions and 6 deletions

View file

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

View file

@ -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<Profile>,
#[tracker::do_not_track]
xr_devices: XRDevices,
#[tracker::do_not_track]
fbt_config_editor: Option<Controller<FbtConfigEditor>>,
}
#[derive(Debug)]
@ -103,6 +106,7 @@ pub enum Msg {
SaveWinSize(i32, i32),
Quit,
ParseLog(Vec<String>),
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::<gtk::Window>(),
})
.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);

View file

@ -1 +1,2 @@
pub mod env_var_row_factory;
pub mod tracker_role_group_factory;

View file

@ -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<TrackerRole>,
}
#[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 = &gtk::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::<Vec<String>>(),
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<Self::ParentInput> {
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<Self>) {
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 {
Self {
index: init.index,
tracker_role: init.tracker_role.unwrap_or_else(|| TrackerRole::default()),
}
}
}

152
src/ui/fbt_config_editor.rs Normal file
View file

@ -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<adw::PreferencesWindow>,
#[tracker::do_not_track]
tracker_role_groups: FactoryVecDeque<TrackerRoleModel>,
}
#[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>) {
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<Self>,
) -> ComponentParts<Self> {
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()),
});
}
}
}

View file

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

View file

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

View file

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