controller profiles in rust instead of json

This commit is contained in:
galister 2026-07-02 14:31:00 +09:00
parent 5e316fe0d3
commit 374f85ee81
8 changed files with 598 additions and 335 deletions

View File

@ -53,6 +53,7 @@
"TRACKPAD": "Trackpad", "TRACKPAD": "Trackpad",
"THUMBSTICK": "Thumbstick", "THUMBSTICK": "Thumbstick",
"JOYSTICK": "Joystick", "JOYSTICK": "Joystick",
"MENU": "Menu",
"SYSTEM": "System", "SYSTEM": "System",
"THUMBREST": "Thumbrest", "THUMBREST": "Thumbrest",
"SHOULDER": "Shoulder", "SHOULDER": "Shoulder",

View File

@ -1,6 +1,7 @@
pub mod cached_fetcher; pub mod cached_fetcher;
pub mod networking; pub mod networking;
pub mod openxr_bindings_schema; pub mod openxr_bindings_schema;
pub mod openxr_controller_profiles;
pub mod pactl_wrapper; pub mod pactl_wrapper;
pub mod popup_manager; pub mod popup_manager;
pub mod steam_utils; pub mod steam_utils;

View File

@ -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 serde::{Deserialize, Serialize};
use strum::{AsRefStr, EnumProperty, EnumString}; use strum::{AsRefStr, EnumProperty, EnumString};
use wgui::i18n::Translation; use wgui::i18n::Translation;
static BINDINGS_LZ4: &[u8] = include_bytes!("../../assets/bindings.json.lz4"); pub struct ControllerProfile {
pub display_name: &'static str,
#[derive(Debug, Clone, Serialize, Deserialize)] pub profile_id: &'static str,
pub struct BindingsFile { pub user_paths: &'static [ControllerUserPath],
#[serde(rename = "$schema")]
pub schema: Option<String>,
pub profiles: BTreeMap<String, Rc<Profile>>,
} }
impl BindingsFile { impl ControllerProfile {
pub fn load_embedded() -> Self { pub fn find_userpath(&self, side: Side) -> Option<&ControllerUserPath> {
let mut decoder = lz4_flex::frame::FrameDecoder::new(BINDINGS_LZ4); self.user_paths.iter().find(|x| x.hand == side)
let mut json = Vec::new();
decoder.read_to_end(&mut json).unwrap(); // safe
serde_json::from_slice(&json).unwrap() // safe
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ControllerUserPath {
pub struct Profile { pub hand: Side,
pub title: Rc<str>, pub paths: &'static [Subpath],
#[serde(rename = "type")]
pub kind: ProfileType,
pub steamvr_controllertype: Option<String>,
pub monado_device: Option<String>,
#[serde(default)]
pub extended_by: Vec<String>,
#[serde(default)]
pub subaction_paths: Vec<String>,
#[serde(default)]
pub subpaths: BTreeMap<String, Subpath>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] impl ControllerUserPath {
#[serde(rename_all = "snake_case")] pub fn find_subpath(&self, subpath: SubpathKind) -> Option<&Subpath> {
pub enum ProfileType { self.paths.iter().find(|x| x.kind == subpath)
TrackedController, }
#[serde(other)]
Other,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Subpath { pub struct Subpath {
#[serde(rename = "type")] pub kind: SubpathKind,
pub kind: SubpathType, pub components: &'static [Component],
pub localized_name: String,
#[serde(default)]
pub components: Vec<Component>,
pub side: Option<Side>,
}
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,
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumString, AsRefStr, EnumProperty)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumString, AsRefStr, EnumProperty)]
#[strum(ascii_case_insensitive)] #[strum(ascii_case_insensitive)]
pub enum IdentifierType { pub enum SubpathKind {
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.TRIGGER"))] #[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.TRIGGER"))]
Trigger, Trigger,
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.TRACKPAD"))] #[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.TRACKPAD"))]
@ -112,6 +46,12 @@ pub enum IdentifierType {
Joystick, Joystick,
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.SYSTEM"))] #[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.SYSTEM"))]
System, System,
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.MENU"))]
Menu,
Primary,
Secondary,
A, A,
B, B,
X, X,
@ -126,9 +66,16 @@ pub enum IdentifierType {
Shoulder, Shoulder,
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.SQUEEZE"))] #[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.SQUEEZE"))]
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 { fn translation(&self) -> Translation {
self self
.get_str("Translation") .get_str("Translation")
@ -143,7 +90,7 @@ impl BindingsDropdown for IdentifierType {
} }
fn clear_str(action: &str, side: Side) -> Option<Rc<str>> { fn clear_str(action: &str, side: Side) -> Option<Rc<str>> {
let side = side.as_ref(); 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"))] #[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.VALUE"))]
Value, Value,
/// Not an actual component but monado uses this instead of X/Y
Position,
Pose,
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.PROXIMITY"))] #[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.PROXIMITY"))]
Proximity, Proximity,
Haptic,
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.X_AXIS"))] #[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.X_AXIS"))]
X, X,
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.Y_AXIS"))] #[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.Y_AXIS"))]
Y, Y,
#[serde(other)] // below are hidden
Other, Pose,
} }
impl Component { impl Component {
@ -202,6 +145,7 @@ impl BindingsDropdown for Component {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumString, AsRefStr)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumString, AsRefStr)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[strum(ascii_case_insensitive)]
pub enum Side { pub enum Side {
Left, Left,
Right, Right,
@ -210,16 +154,10 @@ pub enum Side {
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct ParsedOpenXrInputPath { pub struct ParsedOpenXrInputPath {
pub side: Side, pub side: Side,
pub identifier: IdentifierType, pub subpath: SubpathKind,
pub component: Component, 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 { impl<'a> TryFrom<&'a str> for ParsedOpenXrInputPath {
type Error = anyhow::Error; 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 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 { Ok(Self {
side, side,
identifier, subpath: identifier,
component, component,
}) })
} }

View File

@ -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: &[],
},
];

View File

@ -1,6 +1,7 @@
use std::{borrow::Cow, collections::HashMap, rc::Rc}; use std::{collections::HashMap, rc::Rc};
use glam::Vec2; use glam::Vec2;
use strum::EnumProperty;
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::{ components::{
@ -26,7 +27,7 @@ use crate::{
tab::settings::horiz_cell, tab::settings::horiz_cell,
util::{ util::{
openxr_bindings_schema::{ openxr_bindings_schema::{
BindingsDropdown, ClickType, Component, IdentifierType, ParsedOpenXrInputPath, Profile, Side, SubpathType, BindingsDropdown, ClickType, Component, ControllerProfile, ParsedOpenXrInputPath, Side, SubpathKind,
}, },
popup_manager::{MountPopupOnceParams, MountPopupOnceParamsExtra, PopupHolder, PopupPadding}, popup_manager::{MountPopupOnceParams, MountPopupOnceParamsExtra, PopupHolder, PopupPadding},
wgui_simple, wgui_simple,
@ -46,8 +47,7 @@ pub struct Params<'a> {
pub globals: WguiGlobals, pub globals: WguiGlobals,
pub layout: &'a mut Layout, pub layout: &'a mut Layout,
pub parent_id: WidgetID, pub parent_id: WidgetID,
pub profile_id: Rc<str>, pub controller_profile: &'static ControllerProfile,
pub profile: Rc<Profile>,
pub close_callback: Box<dyn FnOnce()>, pub close_callback: Box<dyn FnOnce()>,
} }
@ -59,7 +59,7 @@ pub struct View {
profiles: Vec<OpenXrInputProfile>, profiles: Vec<OpenXrInputProfile>,
cur_profile_idx: usize, cur_profile_idx: usize,
context_menu: context_menu::ContextMenu, context_menu: context_menu::ContextMenu,
schema: Rc<Profile>, controller_profile: &'static ControllerProfile,
close_callback: Option<Box<dyn FnOnce()>>, close_callback: Option<Box<dyn FnOnce()>>,
} }
@ -122,10 +122,10 @@ impl ViewTrait for View {
*side_mut = None; *side_mut = None;
} }
"subpath" => { "subpath" => {
reconstruct_path(&self.schema, side_mut, &side, Some(value.as_str()), None); apply_subpath(side_mut, &side, &value, self.controller_profile);
} }
"comp" => { "comp" => {
reconstruct_path(&self.schema, side_mut, &side, None, Some(value.as_str())); apply_comp(side_mut, &side, &value);
} }
"click" => match value.as_str() { "click" => match value.as_str() {
"triple" => { "triple" => {
@ -163,11 +163,11 @@ impl View {
let cur_profile_idx = profiles let cur_profile_idx = profiles
.iter() .iter()
.position(|i| i.profile.as_str() == &*params.profile_id) .position(|i| i.profile.as_str() == &*params.controller_profile.profile_id)
.unwrap_or_else(|| { .unwrap_or_else(|| {
let idx = profiles.len(); let idx = profiles.len();
profiles.push(OpenXrInputProfile { profiles.push(OpenXrInputProfile {
profile: params.profile_id.to_string(), profile: params.controller_profile.profile_id.to_string(),
..Default::default() ..Default::default()
}); });
idx idx
@ -195,7 +195,7 @@ impl View {
profiles, profiles,
cur_profile_idx, cur_profile_idx,
context_menu: context_menu::ContextMenu::default(), context_menu: context_menu::ContextMenu::default(),
schema: params.profile, controller_profile: params.controller_profile,
close_callback: Some(params.close_callback), close_callback: Some(params.close_callback),
}; };
@ -238,7 +238,7 @@ impl View {
for action in action_names { for action in action_names {
let current = get_action_mut(&mut self.profiles[self.cur_profile_idx], action); 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(()) Ok(())
@ -247,65 +247,33 @@ impl View {
fn ensure_pose_and_haptics(&mut self) { fn ensure_pose_and_haptics(&mut self) {
let cur_profile = &mut self.profiles[self.cur_profile_idx]; let cur_profile = &mut self.profiles[self.cur_profile_idx];
if cur_profile.pose.is_none() { let profile_left = self.controller_profile.find_userpath(Side::Left);
let schema = &self.schema; let profile_right = self.controller_profile.find_userpath(Side::Right);
let mut aim_pose_name: Option<&str> = None;
let mut first_pose_name: Option<&str> = None;
for (key, subpath) in &schema.subpaths { let action = cur_profile.pose.get_or_insert_default();
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);
}
}
// no aim pose → use first one if action.left.is_none() && profile_left.is_some() {
if let Some(name) = aim_pose_name.or(first_pose_name) { let path = "/user/hand/left/input/aim/pose";
let pose_action = cur_profile.pose.get_or_insert_default(); action.left = Some(OneOrMany::One(path.into()));
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 cur_profile.haptic.is_none() { if action.right.is_none() && profile_right.is_some() {
let schema = &self.schema; let path = "/user/hand/right/input/aim/pose";
let mut first_haptic_name: Option<&str> = None; action.right = Some(OneOrMany::One(path.into()));
}
for (key, subpath) in &schema.subpaths { let action = cur_profile.haptic.get_or_insert_default();
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);
}
}
if let Some(name) = first_haptic_name { let has_haptic = profile_left.map(|x| x.find_subpath(SubpathKind::Haptic).is_some()).unwrap_or_default();
let haptic_action = cur_profile.haptic.get_or_insert_with(OpenXrInputAction::default); if action.left.is_none() && has_haptic {
if haptic_action.left.is_none() { let path = "/user/hand/left/output/haptic";
let left_path = format!("/user/hand/left/output/{name}"); action.left = Some(OneOrMany::One(path.into()));
haptic_action.left = Some(OneOrMany::One(left_path)); }
}
if haptic_action.right.is_none() { let has_haptic = profile_right.map(|x| x.find_subpath(SubpathKind::Haptic).is_some()).unwrap_or_default();
let right_path = format!("/user/hand/right/output/{name}"); if action.right.is_none() && has_haptic {
haptic_action.right = Some(OneOrMany::One(right_path)); 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, frontend_tasks: FrontendTasks,
globals: WguiGlobals, globals: WguiGlobals,
popup: PopupHolder<View>, popup: PopupHolder<View>,
profile_id: Rc<str>, controller_profile: &'static ControllerProfile,
profile: Rc<Profile>,
) { ) {
frontend_tasks frontend_tasks
.clone() .clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( .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| { Box::new(move |data| {
let close_callback = popup.get_close_callback(data.layout); let close_callback = popup.get_close_callback(data.layout);
let view = View::new(Params { let view = View::new(Params {
globals: globals.clone(), globals: globals.clone(),
layout: data.layout, layout: data.layout,
parent_id: data.id_content, parent_id: data.id_content,
profile_id, controller_profile,
profile,
close_callback, close_callback,
})?; })?;
@ -373,7 +339,7 @@ fn input_controls_for_action(
mp: &mut MacroParams, mp: &mut MacroParams,
parent: WidgetID, parent: WidgetID,
action: Rc<str>, action: Rc<str>,
profile: &Profile, profile: &ControllerProfile,
current: &mut OpenXrInputAction, current: &mut OpenXrInputAction,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let id = mp.idx.to_string(); let id = mp.idx.to_string();
@ -442,38 +408,24 @@ fn input_controls_for_hand(
side: Side, side: Side,
action: Rc<str>, action: Rc<str>,
click_type: ClickType, click_type: ClickType,
profile: &Profile, profile: &ControllerProfile,
threshold: Option<[f32; 2]>, threshold: Option<[f32; 2]>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let subaction_path = match side { let Some(user_path) = profile.find_userpath(side) else {
Side::Left => "/user/hand/left", return Ok(()); // this hand is not available
Side::Right => "/user/hand/right",
}; };
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 current = current.and_then(|cur| ParsedOpenXrInputPath::try_from(cur).log_warn(cur).ok());
let parent = horiz_cell(mp.layout, parent)?; let parent = horiz_cell(mp.layout, parent)?;
let available_components = current let available_components : Rc<[Component]> = current
.as_ref() .as_ref()
.and_then(|par| profile.subpaths.get(&par.to_subpath())) .and_then(|par| user_path.find_subpath(par.subpath))
.map(|subp| subp.get_effective_components()) .map(|subp| subp.components)
.unwrap_or_default(); .unwrap_or_default().into();
let available_subpaths = profile let available_subpaths : Rc<[SubpathKind]> = user_path.paths.iter().filter(|x| !x.kind.get_bool("Hidden").unwrap_or_default()).map(|x| x.kind).collect();
.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::<Rc<[IdentifierType]>>();
subpath_dropdown( subpath_dropdown(
mp, mp,
@ -481,7 +433,7 @@ fn input_controls_for_hand(
action.clone(), action.clone(),
side, side,
available_subpaths, available_subpaths,
current.as_ref().map(|x| x.identifier), current.as_ref().map(|x| x.subpath),
)?; )?;
if !component_dropdown( if !component_dropdown(
@ -511,8 +463,8 @@ fn subpath_dropdown(
parent: WidgetID, parent: WidgetID,
action: Rc<str>, action: Rc<str>,
side: Side, side: Side,
available: Rc<[IdentifierType]>, available: Rc<[SubpathKind]>,
current: Option<IdentifierType>, current: Option<SubpathKind>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new(); let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.SUBPATH")); params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.SUBPATH"));
@ -674,30 +626,43 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
Ok(()) Ok(())
} }
fn reconstruct_path( fn apply_subpath(side_mut: &mut Option<OneOrMany<String>>, side_str: &str, subpath_str: &str, profile: &ControllerProfile,) {
schema: &Profile, let (Ok(side), Ok(subpath)) = (Side::try_from(side_str), SubpathKind::try_from(subpath_str)) else {
side_mut: &mut Option<OneOrMany<String>>, return;
side: &str, };
subpath: Option<&str>, let Some(subpath_obj) = profile.find_userpath(side).and_then(|p| p.find_subpath(subpath)) else {
comp: Option<&str>,
) {
if side_mut.is_none() {
let Some(subpath) = subpath else {
return; return;
}; };
if let Some(comp) = comp { let comp : Component = if let Some(first) = side_mut.as_ref().map(|x| match x {
*side_mut = Some(OneOrMany::One(format!("/user/hand/{side}/input/{subpath}/{comp}"))); OneOrMany::One(x) => x.as_str(),
} else { OneOrMany::Many(x) => x.first().unwrap().as_str(),
let key = format!("/input/{subpath}"); }) {
let Some(schema_subpath) = schema.subpaths.get(&key) else { let Ok(parsed) = ParsedOpenXrInputPath::try_from(first) else {
return; return;
}; };
let comps = schema_subpath.get_effective_components();
let comp = comps.first().unwrap().as_ref(); // safe let mut parsed_compo = parsed.component;
*side_mut = Some(OneOrMany::One(format!("/user/hand/{side}/input/{subpath}/{comp}"))); 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<OneOrMany<String>>, side: &str, comp: &str) {
if side_mut.is_none() {
return;
}
let first = match side_mut.as_ref().unwrap() { let first = match side_mut.as_ref().unwrap() {
OneOrMany::One(x) => x.as_str(), OneOrMany::One(x) => x.as_str(),
OneOrMany::Many(x) => x.first().unwrap().as_str(), OneOrMany::Many(x) => x.first().unwrap().as_str(),
@ -707,22 +672,9 @@ fn reconstruct_path(
return; return;
}; };
let new_subpath = subpath.map_or_else(|| Cow::Owned(parsed.identifier.as_ref().to_lowercase()), Cow::Borrowed); let subpath = parsed.subpath.as_ref().to_lowercase();
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);
*side_mut = Some(OneOrMany::One(format!( *side_mut = Some(OneOrMany::One(format!(
"/user/hand/{side}/input/{new_subpath}/{new_comp}" "/user/hand/{side}/input/{subpath}/{comp}"
))); )));
}
} }

View File

@ -1,6 +1,5 @@
use std::{collections::HashMap, rc::Rc}; use std::{collections::HashMap, rc::Rc};
use anyhow::Context;
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton, components::button::ComponentButton,
@ -14,15 +13,16 @@ use wgui::{
use crate::{ use crate::{
frontend::{FrontendTask, FrontendTasks}, frontend::{FrontendTask, FrontendTasks},
util::{ util::{
openxr_bindings_schema, openxr_bindings_schema::ControllerProfile,
openxr_controller_profiles::OPENXR_INPUT_PROFILES,
popup_manager::{MountPopupOnceParams, PopupHolder}, popup_manager::{MountPopupOnceParams, PopupHolder},
}, },
views::{self, ViewTrait, ViewUpdateParams, bindings}, views::{self, bindings, ViewTrait, ViewUpdateParams},
}; };
#[derive(Clone)] #[derive(Clone)]
enum Task { enum Task {
SelectProfile(Rc<str>), SelectProfile(&'static ControllerProfile),
} }
pub struct Params<'a> { pub struct Params<'a> {
@ -37,7 +37,6 @@ pub struct View {
frontend_tasks: FrontendTasks, frontend_tasks: FrontendTasks,
globals: WguiGlobals, globals: WguiGlobals,
bindings_popup: PopupHolder<bindings::View>, bindings_popup: PopupHolder<bindings::View>,
bindings_file: openxr_bindings_schema::BindingsFile,
} }
impl ViewTrait for View { impl ViewTrait for View {
@ -46,19 +45,12 @@ impl ViewTrait for View {
for task in self.tasks.drain() { for task in self.tasks.drain() {
match task { match task {
Task::SelectProfile(profile_id) => { Task::SelectProfile(profile) => {
let profile = self
.bindings_file
.profiles
.get(&*profile_id)
.context("Selected non-existing profile. UI bug?")?;
views::bindings::mount_popup( views::bindings::mount_popup(
self.frontend_tasks.clone(), self.frontend_tasks.clone(),
self.globals.clone(), self.globals.clone(),
self.bindings_popup.clone(), self.bindings_popup.clone(),
profile_id.clone(), profile,
profile.clone(),
); );
} }
} }
@ -81,15 +73,12 @@ impl View {
let tasks = Tasks::new(); let tasks = Tasks::new();
let bindings_file = openxr_bindings_schema::BindingsFile::load_embedded(); for (idx, profile) in OPENXR_INPUT_PROFILES.iter().enumerate() {
for (idx, (profile_id, profile)) in bindings_file.profiles.iter().enumerate() {
let id = format!("profile_btn_{idx}"); let id = format!("profile_btn_{idx}");
let profile_name: Rc<str> = profile.title.clone();
let mut cell_params: HashMap<Rc<str>, Rc<str>> = HashMap::new(); let mut cell_params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
cell_params.insert(Rc::from("id"), Rc::from(id.clone())); 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( parser_state.instantiate_template(
doc_params, doc_params,
@ -102,9 +91,8 @@ impl View {
let btn = parser_state.fetch_component_as::<ComponentButton>(&id)?; let btn = parser_state.fetch_component_as::<ComponentButton>(&id)?;
let tasks_clone = tasks.clone(); let tasks_clone = tasks.clone();
btn.on_click(Rc::new({ btn.on_click(Rc::new({
let profile_id: Rc<str> = profile_id.clone().into();
move |_common, _e| { move |_common, _e| {
tasks_clone.push(Task::SelectProfile(profile_id.clone())); tasks_clone.push(Task::SelectProfile(profile));
Ok(()) Ok(())
} }
})); }));
@ -115,7 +103,6 @@ impl View {
frontend_tasks: params.frontend_tasks.clone(), frontend_tasks: params.frontend_tasks.clone(),
globals: params.globals.clone(), globals: params.globals.clone(),
bindings_popup: Default::default(), bindings_popup: Default::default(),
bindings_file,
}) })
} }
} }

View File

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