diff --git a/dash-frontend/assets/bindings.json.lz4 b/dash-frontend/assets/bindings.json.lz4 deleted file mode 100644 index 0a6a4a95..00000000 Binary files a/dash-frontend/assets/bindings.json.lz4 and /dev/null differ diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json index a03f26da..18e51146 100644 --- a/dash-frontend/assets/lang/en.json +++ b/dash-frontend/assets/lang/en.json @@ -53,6 +53,7 @@ "TRACKPAD": "Trackpad", "THUMBSTICK": "Thumbstick", "JOYSTICK": "Joystick", + "MENU": "Menu", "SYSTEM": "System", "THUMBREST": "Thumbrest", "SHOULDER": "Shoulder", diff --git a/dash-frontend/src/util/mod.rs b/dash-frontend/src/util/mod.rs index ece73c3a..ac473862 100644 --- a/dash-frontend/src/util/mod.rs +++ b/dash-frontend/src/util/mod.rs @@ -1,6 +1,7 @@ pub mod cached_fetcher; pub mod networking; pub mod openxr_bindings_schema; +pub mod openxr_controller_profiles; pub mod pactl_wrapper; pub mod popup_manager; pub mod steam_utils; diff --git a/dash-frontend/src/util/openxr_bindings_schema.rs b/dash-frontend/src/util/openxr_bindings_schema.rs index 04c3f0e9..bd0b27c9 100644 --- a/dash-frontend/src/util/openxr_bindings_schema.rs +++ b/dash-frontend/src/util/openxr_bindings_schema.rs @@ -1,107 +1,41 @@ -use std::{collections::BTreeMap, io::Read, rc::Rc}; +use std::rc::Rc; -use anyhow::{Context, bail}; +use anyhow::{bail, Context}; use serde::{Deserialize, Serialize}; use strum::{AsRefStr, EnumProperty, EnumString}; use wgui::i18n::Translation; -static BINDINGS_LZ4: &[u8] = include_bytes!("../../assets/bindings.json.lz4"); - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BindingsFile { - #[serde(rename = "$schema")] - pub schema: Option, - - pub profiles: BTreeMap>, +pub struct ControllerProfile { + pub display_name: &'static str, + pub profile_id: &'static str, + pub user_paths: &'static [ControllerUserPath], } -impl BindingsFile { - pub fn load_embedded() -> Self { - let mut decoder = lz4_flex::frame::FrameDecoder::new(BINDINGS_LZ4); - let mut json = Vec::new(); - decoder.read_to_end(&mut json).unwrap(); // safe - - serde_json::from_slice(&json).unwrap() // safe +impl ControllerProfile { + pub fn find_userpath(&self, side: Side) -> Option<&ControllerUserPath> { + self.user_paths.iter().find(|x| x.hand == side) } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Profile { - pub title: Rc, - - #[serde(rename = "type")] - pub kind: ProfileType, - - pub steamvr_controllertype: Option, - pub monado_device: Option, - - #[serde(default)] - pub extended_by: Vec, - - #[serde(default)] - pub subaction_paths: Vec, - - #[serde(default)] - pub subpaths: BTreeMap, +pub struct ControllerUserPath { + pub hand: Side, + pub paths: &'static [Subpath], } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ProfileType { - TrackedController, - - #[serde(other)] - Other, +impl ControllerUserPath { + pub fn find_subpath(&self, subpath: SubpathKind) -> Option<&Subpath> { + self.paths.iter().find(|x| x.kind == subpath) + } } -#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Subpath { - #[serde(rename = "type")] - pub kind: SubpathType, - - pub localized_name: String, - - #[serde(default)] - pub components: Vec, - - pub side: Option, -} - -impl Subpath { - pub fn get_effective_components(&self) -> Rc<[Component]> { - let mut v = vec![]; - for c in self.components.iter() { - match c { - // position is not an openxr component, it's just a monado thing - Component::Position => { - v.push(Component::X); - v.push(Component::Y); - } - Component::Other => {} - other => v.push(*other), - } - } - v.into() - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum SubpathType { - Button, - Trigger, - Joystick, - Pose, - Trackpad, - Vibration, - - #[serde(other)] - Other, + pub kind: SubpathKind, + pub components: &'static [Component], } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumString, AsRefStr, EnumProperty)] #[strum(ascii_case_insensitive)] -pub enum IdentifierType { +pub enum SubpathKind { #[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.TRIGGER"))] Trigger, #[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.TRACKPAD"))] @@ -112,6 +46,12 @@ pub enum IdentifierType { Joystick, #[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.SYSTEM"))] System, + #[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.MENU"))] + Menu, + + Primary, + Secondary, + A, B, X, @@ -126,9 +66,16 @@ pub enum IdentifierType { Shoulder, #[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.SQUEEZE"))] Squeeze, + + #[strum(props(Hidden = true))] + Grip, + #[strum(props(Hidden = true))] + Aim, + #[strum(props(Hidden = true))] + Haptic, } -impl BindingsDropdown for IdentifierType { +impl BindingsDropdown for SubpathKind { fn translation(&self) -> Translation { self .get_str("Translation") @@ -143,7 +90,7 @@ impl BindingsDropdown for IdentifierType { } fn clear_str(action: &str, side: Side) -> Option> { let side = side.as_ref(); - Some(format!("subpath;{action};{side};-").into()) + Some(format!("clear;{action};{side};-").into()) } } @@ -160,20 +107,16 @@ pub enum Component { #[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.VALUE"))] Value, - /// Not an actual component but monado uses this instead of X/Y - Position, - Pose, #[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.PROXIMITY"))] Proximity, - Haptic, #[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.X_AXIS"))] X, #[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.Y_AXIS"))] Y, - #[serde(other)] - Other, + // below are hidden + Pose, } impl Component { @@ -202,6 +145,7 @@ impl BindingsDropdown for Component { #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumString, AsRefStr)] #[serde(rename_all = "snake_case")] +#[strum(ascii_case_insensitive)] pub enum Side { Left, Right, @@ -210,16 +154,10 @@ pub enum Side { #[derive(Debug, Clone, Copy, PartialEq)] pub struct ParsedOpenXrInputPath { pub side: Side, - pub identifier: IdentifierType, + pub subpath: SubpathKind, pub component: Component, } -impl ParsedOpenXrInputPath { - pub fn to_subpath(&self) -> String { - format!("/input/{}", self.identifier.as_ref().to_lowercase()) - } -} - impl<'a> TryFrom<&'a str> for ParsedOpenXrInputPath { type Error = anyhow::Error; @@ -245,11 +183,11 @@ impl<'a> TryFrom<&'a str> for ParsedOpenXrInputPath { let component = Component::try_from(component).context("bad component")?; - let identifier = IdentifierType::try_from(identifier).context("bad subpath")?; + let identifier = SubpathKind::try_from(identifier).context("bad subpath")?; Ok(Self { side, - identifier, + subpath: identifier, component, }) } diff --git a/dash-frontend/src/util/openxr_controller_profiles.rs b/dash-frontend/src/util/openxr_controller_profiles.rs new file mode 100644 index 00000000..d4bbe2fe --- /dev/null +++ b/dash-frontend/src/util/openxr_controller_profiles.rs @@ -0,0 +1,465 @@ +use crate::util::openxr_bindings_schema::{ + Component, ControllerProfile, ControllerUserPath, Side, Subpath, SubpathKind, +}; + +pub const OPENXR_INPUT_PROFILES: &[&ControllerProfile] = &[ + &VALVE_INDEX_CONTROLLER_PROFILE, + &OCULUS_TOUCH_CONTROLLER_PROFILE, + &HTC_VIVE_CONTROLLER_PROFILE, + &HP_MIXED_REALITY_CONTROLLER_PROFILE, + &MICROSOFT_MOTION_CONTROLLER_PROFILE, + &SAMSUNG_ODYSSEY_CONTROLLER_PROFILE, + &KHR_GENERIC_CONTROLLER_PROFILE, +]; + +pub const VALVE_INDEX_CONTROLLER_PROFILE: ControllerProfile = ControllerProfile { + display_name: "Valve Index Controller", + profile_id: "/interaction_profiles/valve/index_controller", + user_paths: &[ + ControllerUserPath { + hand: Side::Left, + paths: VALVE_INDEX_USER_PATHS, + }, + ControllerUserPath { + hand: Side::Right, + paths: VALVE_INDEX_USER_PATHS, + }, + ], +}; + +const VALVE_INDEX_USER_PATHS: &[Subpath] = &[ + Subpath { + kind: SubpathKind::System, + components: &[Component::Click, Component::Touch], + }, + Subpath { + kind: SubpathKind::A, + components: &[Component::Click, Component::Touch], + }, + Subpath { + kind: SubpathKind::B, + components: &[Component::Click, Component::Touch], + }, + Subpath { + kind: SubpathKind::Squeeze, + components: &[Component::Value, Component::Force], + }, + Subpath { + kind: SubpathKind::Trigger, + components: &[Component::Click, Component::Value, Component::Touch], + }, + Subpath { + kind: SubpathKind::Thumbstick, + components: &[Component::Click, Component::Touch, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Trackpad, + components: &[Component::Force, Component::Touch, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Grip, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Aim, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Haptic, + components: &[], + }, +]; + +pub const OCULUS_TOUCH_CONTROLLER_PROFILE: ControllerProfile = ControllerProfile { + display_name: "Touch Controller", + profile_id: "/interaction_profiles/oculus/touch_controller", + user_paths: &[ + ControllerUserPath { + hand: Side::Left, + paths: OCULUS_TOUCH_LEFT_USER_PATHS, + }, + ControllerUserPath { + hand: Side::Right, + paths: OCULUS_TOUCH_RIGHT_USER_PATHS, + }, + ], +}; + +const OCULUS_TOUCH_LEFT_USER_PATHS: &[Subpath] = &[ + Subpath { + kind: SubpathKind::X, + components: &[Component::Click, Component::Touch], + }, + Subpath { + kind: SubpathKind::Y, + components: &[Component::Click, Component::Touch], + }, + Subpath { + kind: SubpathKind::Menu, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Squeeze, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Trigger, + components: &[Component::Value, Component::Touch], + }, + Subpath { + kind: SubpathKind::Thumbstick, + components: &[Component::Click, Component::Touch, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Thumbrest, + components: &[Component::Touch], + }, + Subpath { + kind: SubpathKind::Grip, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Aim, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Haptic, + components: &[], + }, +]; + +const OCULUS_TOUCH_RIGHT_USER_PATHS: &[Subpath] = &[ + Subpath { + kind: SubpathKind::A, + components: &[Component::Click, Component::Touch], + }, + Subpath { + kind: SubpathKind::B, + components: &[Component::Click, Component::Touch], + }, + Subpath { + kind: SubpathKind::System, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Squeeze, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Trigger, + components: &[Component::Value, Component::Touch], + }, + Subpath { + kind: SubpathKind::Thumbstick, + components: &[Component::Click, Component::Touch, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Thumbrest, + components: &[Component::Touch], + }, + Subpath { + kind: SubpathKind::Grip, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Aim, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Haptic, + components: &[], + }, +]; + +pub const HP_MIXED_REALITY_CONTROLLER_PROFILE: ControllerProfile = ControllerProfile { + display_name: "HP Reverb G2 Controller", + profile_id: "/interaction_profiles/hp/mixed_reality_controller", + user_paths: &[ + ControllerUserPath { + hand: Side::Left, + paths: HP_MIXED_REALITY_LEFT_USER_PATHS, + }, + ControllerUserPath { + hand: Side::Right, + paths: HP_MIXED_REALITY_RIGHT_USER_PATHS, + }, + ], +}; + +const HP_MIXED_REALITY_LEFT_USER_PATHS: &[Subpath] = &[ + Subpath { + kind: SubpathKind::X, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Y, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Menu, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Squeeze, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Trigger, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Thumbstick, + components: &[Component::Click, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Grip, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Aim, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Haptic, + components: &[], + }, +]; + +const HP_MIXED_REALITY_RIGHT_USER_PATHS: &[Subpath] = &[ + Subpath { + kind: SubpathKind::A, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::B, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Menu, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Squeeze, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Trigger, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Thumbstick, + components: &[Component::Click, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Grip, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Aim, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Haptic, + components: &[], + }, +]; + +pub const SAMSUNG_ODYSSEY_CONTROLLER_PROFILE: ControllerProfile = ControllerProfile { + display_name: "Samsung Odyssey Controller", + profile_id: "/interaction_profiles/samsung/odyssey_controller", + user_paths: &[ + ControllerUserPath { + hand: Side::Left, + paths: SAMSUNG_ODYSSEY_USER_PATHS, + }, + ControllerUserPath { + hand: Side::Right, + paths: SAMSUNG_ODYSSEY_USER_PATHS, + }, + ], +}; + +const SAMSUNG_ODYSSEY_USER_PATHS: &[Subpath] = &[ + Subpath { + kind: SubpathKind::Menu, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Squeeze, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Trigger, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Thumbstick, + components: &[Component::Click, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Trackpad, + components: &[Component::Click, Component::Touch, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Grip, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Aim, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Haptic, + components: &[], + }, +]; + +pub const HTC_VIVE_CONTROLLER_PROFILE: ControllerProfile = ControllerProfile { + display_name: "HTC Vive Controller", + profile_id: "/interaction_profiles/htc/vive_controller", + user_paths: &[ + ControllerUserPath { + hand: Side::Left, + paths: HTC_VIVE_USER_PATHS, + }, + ControllerUserPath { + hand: Side::Right, + paths: HTC_VIVE_USER_PATHS, + }, + ], +}; + +const HTC_VIVE_USER_PATHS: &[Subpath] = &[ + Subpath { + kind: SubpathKind::System, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Squeeze, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Menu, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Trigger, + components: &[Component::Click, Component::Value], + }, + Subpath { + kind: SubpathKind::Trackpad, + components: &[Component::Click, Component::Touch, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Grip, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Aim, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Haptic, + components: &[], + }, +]; + +pub const MICROSOFT_MOTION_CONTROLLER_PROFILE: ControllerProfile = ControllerProfile { + display_name: "Microsoft WMR Controller", + profile_id: "/interaction_profiles/microsoft/motion_controller", + user_paths: &[ + ControllerUserPath { + hand: Side::Left, + paths: MICROSOFT_MOTION_CONTROLLER_USER_PATHS, + }, + ControllerUserPath { + hand: Side::Right, + paths: MICROSOFT_MOTION_CONTROLLER_USER_PATHS, + }, + ], +}; + +const MICROSOFT_MOTION_CONTROLLER_USER_PATHS: &[Subpath] = &[ + Subpath { + kind: SubpathKind::Menu, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Squeeze, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Trigger, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Thumbstick, + components: &[Component::Click, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Trackpad, + components: &[Component::Click, Component::Touch, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Grip, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Aim, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Haptic, + components: &[], + }, +]; + +pub const KHR_GENERIC_CONTROLLER_PROFILE: ControllerProfile = ControllerProfile { + display_name: "Khronos Generic Controller", + profile_id: "/interaction_profiles/khr/generic_controller", + user_paths: &[ + ControllerUserPath { + hand: Side::Left, + paths: KHR_GENERIC_CONTROLLER_USER_PATHS, + }, + ControllerUserPath { + hand: Side::Right, + paths: KHR_GENERIC_CONTROLLER_USER_PATHS, + }, + ], +}; + +const KHR_GENERIC_CONTROLLER_USER_PATHS: &[Subpath] = &[ + Subpath { + kind: SubpathKind::Primary, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Secondary, + components: &[Component::Click], + }, + Subpath { + kind: SubpathKind::Thumbstick, + components: &[Component::Click, Component::X, Component::Y], + }, + Subpath { + kind: SubpathKind::Squeeze, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Trigger, + components: &[Component::Value], + }, + Subpath { + kind: SubpathKind::Grip, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Aim, + components: &[Component::Pose], + }, + Subpath { + kind: SubpathKind::Haptic, + components: &[], + }, +]; diff --git a/dash-frontend/src/views/bindings.rs b/dash-frontend/src/views/bindings.rs index 93dc6f8b..d2f7844f 100644 --- a/dash-frontend/src/views/bindings.rs +++ b/dash-frontend/src/views/bindings.rs @@ -1,6 +1,7 @@ -use std::{borrow::Cow, collections::HashMap, rc::Rc}; +use std::{collections::HashMap, rc::Rc}; use glam::Vec2; +use strum::EnumProperty; use wgui::{ assets::AssetPath, components::{ @@ -26,7 +27,7 @@ use crate::{ tab::settings::horiz_cell, util::{ openxr_bindings_schema::{ - BindingsDropdown, ClickType, Component, IdentifierType, ParsedOpenXrInputPath, Profile, Side, SubpathType, + BindingsDropdown, ClickType, Component, ControllerProfile, ParsedOpenXrInputPath, Side, SubpathKind, }, popup_manager::{MountPopupOnceParams, MountPopupOnceParamsExtra, PopupHolder, PopupPadding}, wgui_simple, @@ -46,8 +47,7 @@ pub struct Params<'a> { pub globals: WguiGlobals, pub layout: &'a mut Layout, pub parent_id: WidgetID, - pub profile_id: Rc, - pub profile: Rc, + pub controller_profile: &'static ControllerProfile, pub close_callback: Box, } @@ -59,7 +59,7 @@ pub struct View { profiles: Vec, cur_profile_idx: usize, context_menu: context_menu::ContextMenu, - schema: Rc, + controller_profile: &'static ControllerProfile, close_callback: Option>, } @@ -122,10 +122,10 @@ impl ViewTrait for View { *side_mut = None; } "subpath" => { - reconstruct_path(&self.schema, side_mut, &side, Some(value.as_str()), None); + apply_subpath(side_mut, &side, &value, self.controller_profile); } "comp" => { - reconstruct_path(&self.schema, side_mut, &side, None, Some(value.as_str())); + apply_comp(side_mut, &side, &value); } "click" => match value.as_str() { "triple" => { @@ -163,11 +163,11 @@ impl View { let cur_profile_idx = profiles .iter() - .position(|i| i.profile.as_str() == &*params.profile_id) + .position(|i| i.profile.as_str() == &*params.controller_profile.profile_id) .unwrap_or_else(|| { let idx = profiles.len(); profiles.push(OpenXrInputProfile { - profile: params.profile_id.to_string(), + profile: params.controller_profile.profile_id.to_string(), ..Default::default() }); idx @@ -195,7 +195,7 @@ impl View { profiles, cur_profile_idx, context_menu: context_menu::ContextMenu::default(), - schema: params.profile, + controller_profile: params.controller_profile, close_callback: Some(params.close_callback), }; @@ -238,7 +238,7 @@ impl View { for action in action_names { let current = get_action_mut(&mut self.profiles[self.cur_profile_idx], action); - input_controls_for_action(&mut mp, self.list_parent, action.into(), &self.schema, current)?; + input_controls_for_action(&mut mp, self.list_parent, action.into(), &self.controller_profile, current)?; } Ok(()) @@ -247,65 +247,33 @@ impl View { fn ensure_pose_and_haptics(&mut self) { let cur_profile = &mut self.profiles[self.cur_profile_idx]; - if cur_profile.pose.is_none() { - let schema = &self.schema; - let mut aim_pose_name: Option<&str> = None; - let mut first_pose_name: Option<&str> = None; + let profile_left = self.controller_profile.find_userpath(Side::Left); + let profile_right = self.controller_profile.find_userpath(Side::Right); - for (key, subpath) in &schema.subpaths { - if subpath.kind != SubpathType::Pose { - continue; - } - let name = key.strip_prefix("/input/"); - let Some(name) = name else { continue }; - if first_pose_name.is_none() { - first_pose_name = Some(name); - } - if name == "aim" { - aim_pose_name = Some(name); - } - } + let action = cur_profile.pose.get_or_insert_default(); - // no aim pose → use first one - if let Some(name) = aim_pose_name.or(first_pose_name) { - let pose_action = cur_profile.pose.get_or_insert_default(); - if pose_action.left.is_none() { - let left_path = format!("/user/hand/left/input/{name}/pose"); - pose_action.left = Some(OneOrMany::One(left_path)); - } - if pose_action.right.is_none() { - let right_path = format!("/user/hand/right/input/{name}/pose"); - pose_action.right = Some(OneOrMany::One(right_path)); - } - } + if action.left.is_none() && profile_left.is_some() { + let path = "/user/hand/left/input/aim/pose"; + action.left = Some(OneOrMany::One(path.into())); } - if cur_profile.haptic.is_none() { - let schema = &self.schema; - let mut first_haptic_name: Option<&str> = None; + if action.right.is_none() && profile_right.is_some() { + let path = "/user/hand/right/input/aim/pose"; + action.right = Some(OneOrMany::One(path.into())); + } - for (key, subpath) in &schema.subpaths { - if subpath.kind != SubpathType::Vibration { - continue; - } - let name = key.strip_prefix("/output/"); - let Some(name) = name else { continue }; - if first_haptic_name.is_none() { - first_haptic_name = Some(name); - } - } + let action = cur_profile.haptic.get_or_insert_default(); - if let Some(name) = first_haptic_name { - let haptic_action = cur_profile.haptic.get_or_insert_with(OpenXrInputAction::default); - if haptic_action.left.is_none() { - let left_path = format!("/user/hand/left/output/{name}"); - haptic_action.left = Some(OneOrMany::One(left_path)); - } - if haptic_action.right.is_none() { - let right_path = format!("/user/hand/right/output/{name}"); - haptic_action.right = Some(OneOrMany::One(right_path)); - } - } + let has_haptic = profile_left.map(|x| x.find_subpath(SubpathKind::Haptic).is_some()).unwrap_or_default(); + if action.left.is_none() && has_haptic { + let path = "/user/hand/left/output/haptic"; + action.left = Some(OneOrMany::One(path.into())); + } + + let has_haptic = profile_right.map(|x| x.find_subpath(SubpathKind::Haptic).is_some()).unwrap_or_default(); + if action.right.is_none() && has_haptic { + let path = "/user/hand/right/output/haptic"; + action.right = Some(OneOrMany::One(path.into())); } } } @@ -334,21 +302,19 @@ pub fn mount_popup( frontend_tasks: FrontendTasks, globals: WguiGlobals, popup: PopupHolder, - profile_id: Rc, - profile: Rc, + controller_profile: &'static ControllerProfile, ) { frontend_tasks .clone() .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( - Translation::from_raw_text_rc(profile.title.clone()), + Translation::from_raw_text(controller_profile.display_name), Box::new(move |data| { let close_callback = popup.get_close_callback(data.layout); let view = View::new(Params { globals: globals.clone(), layout: data.layout, parent_id: data.id_content, - profile_id, - profile, + controller_profile, close_callback, })?; @@ -373,7 +339,7 @@ fn input_controls_for_action( mp: &mut MacroParams, parent: WidgetID, action: Rc, - profile: &Profile, + profile: &ControllerProfile, current: &mut OpenXrInputAction, ) -> anyhow::Result<()> { let id = mp.idx.to_string(); @@ -442,38 +408,24 @@ fn input_controls_for_hand( side: Side, action: Rc, click_type: ClickType, - profile: &Profile, + profile: &ControllerProfile, threshold: Option<[f32; 2]>, ) -> anyhow::Result<()> { - let subaction_path = match side { - Side::Left => "/user/hand/left", - Side::Right => "/user/hand/right", + let Some(user_path) = profile.find_userpath(side) else { + return Ok(()); // this hand is not available }; - if !profile.subaction_paths.iter().any(|p| p == subaction_path) { - return Ok(()); // skip - } - let current = current.and_then(|cur| ParsedOpenXrInputPath::try_from(cur).log_warn(cur).ok()); let parent = horiz_cell(mp.layout, parent)?; - let available_components = current + let available_components : Rc<[Component]> = current .as_ref() - .and_then(|par| profile.subpaths.get(&par.to_subpath())) - .map(|subp| subp.get_effective_components()) - .unwrap_or_default(); + .and_then(|par| user_path.find_subpath(par.subpath)) + .map(|subp| subp.components) + .unwrap_or_default().into(); - let available_subpaths = profile - .subpaths - .iter() - .filter(|(_, path)| path.side.is_none_or(|s| s == side)) - .filter_map(|(key, _)| { - key - .strip_prefix("/input/") - .and_then(|ident| IdentifierType::try_from(ident).ok()) - }) - .collect::>(); + let available_subpaths : Rc<[SubpathKind]> = user_path.paths.iter().filter(|x| !x.kind.get_bool("Hidden").unwrap_or_default()).map(|x| x.kind).collect(); subpath_dropdown( mp, @@ -481,7 +433,7 @@ fn input_controls_for_hand( action.clone(), side, available_subpaths, - current.as_ref().map(|x| x.identifier), + current.as_ref().map(|x| x.subpath), )?; if !component_dropdown( @@ -511,8 +463,8 @@ fn subpath_dropdown( parent: WidgetID, action: Rc, side: Side, - available: Rc<[IdentifierType]>, - current: Option, + available: Rc<[SubpathKind]>, + current: Option, ) -> anyhow::Result<()> { let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.SUBPATH")); @@ -674,30 +626,43 @@ fn create_dropdown( Ok(()) } -fn reconstruct_path( - schema: &Profile, - side_mut: &mut Option>, - side: &str, - subpath: Option<&str>, - comp: Option<&str>, -) { - if side_mut.is_none() { - let Some(subpath) = subpath else { +fn apply_subpath(side_mut: &mut Option>, side_str: &str, subpath_str: &str, profile: &ControllerProfile,) { + let (Ok(side), Ok(subpath)) = (Side::try_from(side_str), SubpathKind::try_from(subpath_str)) else { + return; + }; + let Some(subpath_obj) = profile.find_userpath(side).and_then(|p| p.find_subpath(subpath)) else { return; }; - if let Some(comp) = comp { - *side_mut = Some(OneOrMany::One(format!("/user/hand/{side}/input/{subpath}/{comp}"))); - } else { - let key = format!("/input/{subpath}"); - let Some(schema_subpath) = schema.subpaths.get(&key) else { - return; - }; - let comps = schema_subpath.get_effective_components(); - let comp = comps.first().unwrap().as_ref(); // safe - *side_mut = Some(OneOrMany::One(format!("/user/hand/{side}/input/{subpath}/{comp}"))); + let comp : Component = if let Some(first) = side_mut.as_ref().map(|x| match x { + OneOrMany::One(x) => x.as_str(), + OneOrMany::Many(x) => x.first().unwrap().as_str(), + }) { + let Ok(parsed) = ParsedOpenXrInputPath::try_from(first) else { + return; + }; + + let mut parsed_compo = parsed.component; + if !subpath_obj.components.contains(&parsed_compo) { + parsed_compo = *subpath_obj.components.first().unwrap(); } - } else { + parsed_compo + } else { + *subpath_obj.components.first().unwrap() + }; + + let comp_str = comp.as_ref().to_lowercase(); + + *side_mut = Some(OneOrMany::One(format!( + "/user/hand/{side_str}/input/{subpath_str}/{comp_str}" + ))); +} + +fn apply_comp(side_mut: &mut Option>, side: &str, comp: &str) { + if side_mut.is_none() { + return; + } + let first = match side_mut.as_ref().unwrap() { OneOrMany::One(x) => x.as_str(), OneOrMany::Many(x) => x.first().unwrap().as_str(), @@ -707,22 +672,9 @@ fn reconstruct_path( return; }; - let new_subpath = subpath.map_or_else(|| Cow::Owned(parsed.identifier.as_ref().to_lowercase()), Cow::Borrowed); - - let mut parsed_compo = parsed.component; - let key = format!("/input/{new_subpath}"); - let Some(schema_subpath) = schema.subpaths.get(&key) else { - return; - }; - let effective_compo = schema_subpath.get_effective_components(); - if !effective_compo.contains(&parsed_compo) { - parsed_compo = *effective_compo.first().unwrap(); - } - - let new_comp = comp.map_or_else(|| Cow::Owned(parsed_compo.as_ref().to_lowercase()), Cow::Borrowed); + let subpath = parsed.subpath.as_ref().to_lowercase(); *side_mut = Some(OneOrMany::One(format!( - "/user/hand/{side}/input/{new_subpath}/{new_comp}" + "/user/hand/{side}/input/{subpath}/{comp}" ))); - } } diff --git a/dash-frontend/src/views/input_profiles.rs b/dash-frontend/src/views/input_profiles.rs index 8881f870..5729072e 100644 --- a/dash-frontend/src/views/input_profiles.rs +++ b/dash-frontend/src/views/input_profiles.rs @@ -1,6 +1,5 @@ use std::{collections::HashMap, rc::Rc}; -use anyhow::Context; use wgui::{ assets::AssetPath, components::button::ComponentButton, @@ -14,15 +13,16 @@ use wgui::{ use crate::{ frontend::{FrontendTask, FrontendTasks}, util::{ - openxr_bindings_schema, + openxr_bindings_schema::ControllerProfile, + openxr_controller_profiles::OPENXR_INPUT_PROFILES, popup_manager::{MountPopupOnceParams, PopupHolder}, }, - views::{self, ViewTrait, ViewUpdateParams, bindings}, + views::{self, bindings, ViewTrait, ViewUpdateParams}, }; #[derive(Clone)] enum Task { - SelectProfile(Rc), + SelectProfile(&'static ControllerProfile), } pub struct Params<'a> { @@ -37,7 +37,6 @@ pub struct View { frontend_tasks: FrontendTasks, globals: WguiGlobals, bindings_popup: PopupHolder, - bindings_file: openxr_bindings_schema::BindingsFile, } impl ViewTrait for View { @@ -46,19 +45,12 @@ impl ViewTrait for View { for task in self.tasks.drain() { match task { - Task::SelectProfile(profile_id) => { - let profile = self - .bindings_file - .profiles - .get(&*profile_id) - .context("Selected non-existing profile. UI bug?")?; - + Task::SelectProfile(profile) => { views::bindings::mount_popup( self.frontend_tasks.clone(), self.globals.clone(), self.bindings_popup.clone(), - profile_id.clone(), - profile.clone(), + profile, ); } } @@ -81,15 +73,12 @@ impl View { let tasks = Tasks::new(); - let bindings_file = openxr_bindings_schema::BindingsFile::load_embedded(); - - for (idx, (profile_id, profile)) in bindings_file.profiles.iter().enumerate() { + for (idx, profile) in OPENXR_INPUT_PROFILES.iter().enumerate() { let id = format!("profile_btn_{idx}"); - let profile_name: Rc = profile.title.clone(); let mut cell_params: HashMap, Rc> = HashMap::new(); cell_params.insert(Rc::from("id"), Rc::from(id.clone())); - cell_params.insert(Rc::from("text"), profile_name); + cell_params.insert(Rc::from("text"), Rc::from(profile.display_name)); parser_state.instantiate_template( doc_params, @@ -102,9 +91,8 @@ impl View { let btn = parser_state.fetch_component_as::(&id)?; let tasks_clone = tasks.clone(); btn.on_click(Rc::new({ - let profile_id: Rc = profile_id.clone().into(); move |_common, _e| { - tasks_clone.push(Task::SelectProfile(profile_id.clone())); + tasks_clone.push(Task::SelectProfile(profile)); Ok(()) } })); @@ -115,7 +103,6 @@ impl View { frontend_tasks: params.frontend_tasks.clone(), globals: params.globals.clone(), bindings_popup: Default::default(), - bindings_file, }) } } diff --git a/dash-frontend/update-bindings-json.sh b/dash-frontend/update-bindings-json.sh deleted file mode 100755 index 69d03564..00000000 --- a/dash-frontend/update-bindings-json.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -readonly ALLOWED_PROFILES="$( - cat <<'EOF' -/interaction_profiles/hp/mixed_reality_controller -/interaction_profiles/htc/vive_controller -/interaction_profiles/htc/vive_cosmos_controller -/interaction_profiles/htc/vive_focus3_controller -/interaction_profiles/ml/ml2_controller -/interaction_profiles/microsoft/motion_controller -/interaction_profiles/mndx/flipvr -/interaction_profiles/mndx/pssense_controller_mndx -/interaction_profiles/oculus/touch_controller -/interaction_profiles/oppo/mr_controller_oppo -/interaction_profiles/samsung/odyssey_controller -/interaction_profiles/valve/index_controller -EOF -)" - -script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -output_dir="${script_dir}/assets" -output_json="${output_dir}/bindings.json" -output_lz4="${output_json}.lz4" - -repo_url="https://gitlab.freedesktop.org/monado/monado.git" -repo_branch="main" -bindings_path="src/xrt/auxiliary/bindings/bindings.json" - -tmpdir="$(mktemp -d)" -tmp_output="" - -cleanup() { - rm -rf "$tmpdir" - - if [[ -n "$tmp_output" && -f "$tmp_output" ]]; then - rm -f "$tmp_output" - fi -} -trap cleanup EXIT - -command -v git >/dev/null || { echo "git is required" >&2; exit 1; } -command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; } -command -v lz4 >/dev/null || { echo "lz4 is required" >&2; exit 1; } - -git clone \ - --depth 1 \ - --branch "$repo_branch" \ - --filter=blob:none \ - --sparse \ - "$repo_url" \ - "$tmpdir/monado" - -git -C "$tmpdir/monado" sparse-checkout set --no-cone "$bindings_path" - -input_json="$tmpdir/monado/$bindings_path" - -mkdir -p "$output_dir" -tmp_output="$(mktemp "${output_json}.tmp.XXXXXX")" - -jq_filter=' - ($allowed_lines - | split("\n") - | map(sub("\r$"; "")) - | map(select(length > 0)) - | reduce .[] as $key ({}; .[$key] = true) - ) as $allowed - | .profiles |= with_entries(select($allowed[.key] == true)) -' - -jq -c \ - --arg allowed_lines "$ALLOWED_PROFILES" \ - "$jq_filter" \ - "$input_json" > "$tmp_output" - -mv "$tmp_output" "$output_json" -tmp_output="" - -lz4 -f --rm "$output_json" "$output_lz4" - -echo "Wrote compressed filtered bindings to: $output_lz4"