From 374f85ee81566edb10e9b49a5a41952259561eae Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:31:00 +0900 Subject: [PATCH] controller profiles in rust instead of json --- dash-frontend/assets/bindings.json.lz4 | Bin 4797 -> 0 bytes dash-frontend/assets/lang/en.json | 1 + dash-frontend/src/util/mod.rs | 1 + .../src/util/openxr_bindings_schema.rs | 142 ++---- .../src/util/openxr_controller_profiles.rs | 465 ++++++++++++++++++ dash-frontend/src/views/bindings.rs | 212 +++----- dash-frontend/src/views/input_profiles.rs | 31 +- dash-frontend/update-bindings-json.sh | 81 --- 8 files changed, 598 insertions(+), 335 deletions(-) delete mode 100644 dash-frontend/assets/bindings.json.lz4 create mode 100644 dash-frontend/src/util/openxr_controller_profiles.rs delete mode 100755 dash-frontend/update-bindings-json.sh diff --git a/dash-frontend/assets/bindings.json.lz4 b/dash-frontend/assets/bindings.json.lz4 deleted file mode 100644 index 0a6a4a95b3412d46a9e6828809ea05d057140e6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4797 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, - - 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"