mirror of https://github.com/wayvr-org/wayvr.git
wip: bindings ui
This commit is contained in:
parent
5014cda64a
commit
f3bb2e070b
|
|
@ -1444,6 +1444,7 @@ dependencies = [
|
|||
"hyper",
|
||||
"keyvalues-parser",
|
||||
"log",
|
||||
"lz4_flex",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -3104,6 +3105,15 @@ dependencies = [
|
|||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e"
|
||||
dependencies = [
|
||||
"twox-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
|
|
@ -5969,6 +5979,12 @@ dependencies = [
|
|||
"core_maths",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
|
|
@ -7221,6 +7237,7 @@ dependencies = [
|
|||
"rust-ini",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json5",
|
||||
"smol",
|
||||
"strum",
|
||||
"walkdir",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ regex = "1.12.2"
|
|||
rust-embed = "8.9.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
serde_json5 = "0.2.1"
|
||||
slotmap = "1.1.1"
|
||||
smol = "2.0.2"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ http-body-util = "0.1.3"
|
|||
hyper = { version = "1.8.1", features = ["client", "http1", "http2"] }
|
||||
keyvalues-parser = { git = "https://codeberg.org/CosmicHarper/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
|
||||
log.workspace = true
|
||||
lz4_flex = { version = "0.13.1", features = ["frame"] }
|
||||
rust-embed.workspace = true
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json.workspace = true
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -31,6 +31,13 @@
|
|||
<RadioBox text="${text}" translation="${translation}" value="${value}" tooltip="${tooltip}" />
|
||||
</template>
|
||||
|
||||
<template name="ButtonText">
|
||||
<Button id="${id}" height="32" tooltip="${translation}_HELP" padding="4" gap="8">
|
||||
<sprite src_builtin="${icon}" height="24" width="24" />
|
||||
<label align="left" translation="${translation}" weight="bold" min_width="200" />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template name="DangerButton">
|
||||
<Button id="${id}" color="#AA3333" height="32" tooltip="${translation}_HELP" padding="4" gap="8">
|
||||
<sprite src_builtin="${icon}" height="24" width="24" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<layout>
|
||||
<include src="../t_dropdown_button.xml" />
|
||||
|
||||
<!-- id, translation -->
|
||||
<template name="ActionRow">
|
||||
<div id="${id}" flex_direction="column" gap="4" width="100%" padding="6" round="6" color="#00000044" border="1" border_color="#FFFFFF33">
|
||||
<label translation="${translation}" weight="bold" size="14" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<elements>
|
||||
<div flex_direction="column" gap="8" width="100%" align_items="center">
|
||||
<label id="controller_type" text="OpenXR Bindings" weight="bold" size="18" />
|
||||
<div id="list_parent" flex_direction="column" gap="6" width="100%"></div>
|
||||
<div flex_direction="row" gap="4" align_self="end">
|
||||
<Button id="btn_save" height="32" translation="APP_SETTINGS.SAVE" />
|
||||
<Button id="btn_cancel" height="32" translation="APP_SETTINGS.CANCEL" />
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<layout>
|
||||
<template name="ProfileButton">
|
||||
<Button id="${id}" height="36" padding="4" gap="8" text="${text}" />
|
||||
</template>
|
||||
|
||||
<elements>
|
||||
<div flex_direction="column" gap="8" width="100%" align_items="center">
|
||||
<div id="list_parent" flex_direction="column" gap="4" width="100%"></div>
|
||||
|
||||
<div flex_direction="row" gap="4" align_self="end">
|
||||
<Button id="btn_cancel" height="32" translation="APP_SETTINGS.CANCEL" />
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
@ -38,6 +38,51 @@
|
|||
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Do not block input when watch is hovered",
|
||||
"BLOCK_POSES_ON_KBD_INTERACTION": "Block poses when interacting with keyboard",
|
||||
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blocks the game from receiving poses when the keyboard is hovered and 'Block game input' is enabled",
|
||||
"BINDINGS": {
|
||||
"COMP": {
|
||||
"CLICK": "Click",
|
||||
"FORCE": "Force",
|
||||
"TOUCH": "Touch",
|
||||
"VALUE": "Value",
|
||||
"PROXIMITY": "Proximity",
|
||||
"X_AXIS": "X axis",
|
||||
"Y_AXIS": "Y axis"
|
||||
},
|
||||
"TYPE": {
|
||||
"TRIGGER": "Trigger",
|
||||
"TRACKPAD": "Trackpad",
|
||||
"THUMBSTICK": "Thumbstick",
|
||||
"JOYSTICK": "Joystick",
|
||||
"SYSTEM": "System",
|
||||
"THUMBREST": "Thumbrest",
|
||||
"SHOULDER": "Shoulder",
|
||||
"SQUEEZE": "Grip"
|
||||
},
|
||||
"CLICK": {
|
||||
"ANY": "-",
|
||||
"DOUBLE": "Double-click",
|
||||
"TRIPLE": "Triple-click",
|
||||
"TYPE": "Click count"
|
||||
},
|
||||
"ACTION": {
|
||||
"CLICK": "Click*",
|
||||
"GRAB": "Grab*",
|
||||
"ALT_CLICK": "Custom shell exec (set alt_click in config)",
|
||||
"SHOW_HIDE": "Show, hide, recenter*",
|
||||
"TOGGLE_DASHBOARD": "Toggle dashboard",
|
||||
"SPACE_DRAG": "Playspace drag",
|
||||
"SPACE_ROTATE": "Playspace rotate",
|
||||
"SPACE_RESET": "Playspace reset",
|
||||
"CLICK_MODIFIER_RIGHT": "Right-click modifier",
|
||||
"CLICK_MODIFIER_MIDDLE": "Middle-click modifier",
|
||||
"MOVE_MOUSE": "Move mouse (if off by default)",
|
||||
"SCROLL": "Scroll"
|
||||
},
|
||||
"LEFT": "Left",
|
||||
"RIGHT": "Right",
|
||||
"COMPONENT": "Actuation type",
|
||||
"SUBPATH": "Actuating control"
|
||||
},
|
||||
"BROWSE_ONLINE_CATALOG": "Browse online catalog...",
|
||||
"BROWSE_SKYMAPS": "Browse skymaps",
|
||||
"CAPTURE_METHOD": "Wayland screen capture",
|
||||
|
|
@ -62,6 +107,8 @@
|
|||
"HANDSFREE_POINTER_HELP": "Input to use when motion\ncontrollers are unavailable.\nLeft pinch is grab, right is click.",
|
||||
"HIDE_GRAB_HELP": "Hide grab help",
|
||||
"HIDE_USERNAME": "Hide username",
|
||||
"INPUT_PROFILES": "Change Input Bindings",
|
||||
"INPUT_PROFILES_HELP": "OpenXR controller input bindings",
|
||||
"INVERT_SCROLL_DIRECTION_X": "Invert horizontal scroll direction",
|
||||
"INVERT_SCROLL_DIRECTION_Y": "Invert vertical scroll direction",
|
||||
"KEYBOARD_MIDDLE_CLICK": "Keyboard middle click",
|
||||
|
|
@ -134,7 +181,9 @@
|
|||
"GRID_OPACITY_HELP": "Opacity of the floor grid when the skybox is enabled",
|
||||
"XR_CLICK_SENSITIVITY": "XR trigger sensitivity",
|
||||
"XR_CLICK_SENSITIVITY_HELP": "Press and release values for analog triggers",
|
||||
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default"
|
||||
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default",
|
||||
"SAVE": "Save",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "Application launcher",
|
||||
"APPLICATION_STARTED": "Application started",
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@ impl SettingType {
|
|||
}
|
||||
|
||||
// creates a simple div with horizontal, centered flow
|
||||
fn horiz_cell(layout: &mut Layout, parent: WidgetID) -> anyhow::Result<WidgetID> {
|
||||
pub fn horiz_cell(layout: &mut Layout, parent: WidgetID) -> anyhow::Result<WidgetID> {
|
||||
let (pair, _) = layout.add_child(
|
||||
parent,
|
||||
WidgetDiv::create(),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,82 @@
|
|||
use crate::tab::settings::{
|
||||
SettingType, SettingsMountParams, SettingsTab,
|
||||
macros::{
|
||||
options_category, options_checkbox, options_dropdown, options_range_f32, options_slider_f32, options_slider_i32,
|
||||
},
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
components::button::ComponentButton, globals::WguiGlobals, layout::WidgetID, parser::Fetchable, task::Tasks,
|
||||
};
|
||||
|
||||
pub struct State {}
|
||||
use crate::util::popup_manager::PopupHolder;
|
||||
|
||||
impl SettingsTab for State {}
|
||||
use crate::{
|
||||
frontend::FrontendTasks,
|
||||
tab::settings::{
|
||||
SettingType, SettingsMountParams, SettingsTab,
|
||||
macros::{
|
||||
options_category, options_checkbox, options_dropdown, options_range_f32, options_slider_f32, options_slider_i32,
|
||||
},
|
||||
},
|
||||
views::{ViewUpdateParams, input_profiles},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
OpenInputProfiles,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
popup_input_profiles: PopupHolder<input_profiles::View>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
globals: WguiGlobals,
|
||||
tasks: Tasks<Task>,
|
||||
}
|
||||
|
||||
impl SettingsTab for State {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
self.popup_input_profiles.update(par)?;
|
||||
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::OpenInputProfiles => {
|
||||
input_profiles::mount_popup(
|
||||
self.frontend_tasks.clone(),
|
||||
self.globals.clone(),
|
||||
self.popup_input_profiles.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_input_profiles_button(
|
||||
mp: &mut crate::tab::settings::macros::MacroParams,
|
||||
parent: WidgetID,
|
||||
tasks: Tasks<Task>,
|
||||
_popup: &PopupHolder<input_profiles::View>,
|
||||
) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
|
||||
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
|
||||
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
|
||||
params.insert(Rc::from("translation"), Rc::from("APP_SETTINGS.INPUT_PROFILES"));
|
||||
params.insert(Rc::from("icon"), Rc::from("dashboard/controller.svg"));
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "ButtonText", mp.layout, parent, params)?;
|
||||
|
||||
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
btn.on_click(Rc::new({
|
||||
let tasks = tasks.clone();
|
||||
move |_common, _e| {
|
||||
tasks.push(Task::OpenInputProfiles);
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
|
||||
|
|
@ -41,6 +110,19 @@ impl State {
|
|||
}
|
||||
|
||||
options_slider_i32(par.mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?;
|
||||
Ok(State {})
|
||||
|
||||
let tasks = Tasks::<Task>::new();
|
||||
let popup = PopupHolder::<input_profiles::View>::default();
|
||||
|
||||
if par.feats.openxr {
|
||||
create_input_profiles_button(par.mp, c, tasks.clone(), &popup)?;
|
||||
}
|
||||
|
||||
Ok(State {
|
||||
popup_input_profiles: popup,
|
||||
frontend_tasks: par.frontend_tasks.clone(),
|
||||
globals: par.mp.doc_params.globals.clone(),
|
||||
tasks,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::tab::settings::{
|
||||
SettingType, SettingsMountParams, SettingsTab,
|
||||
macros::{options_category, options_checkbox, options_range_f32, options_slider_f32},
|
||||
macros::{options_category, options_checkbox, options_range_f32},
|
||||
};
|
||||
|
||||
pub struct State {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod cached_fetcher;
|
||||
pub mod networking;
|
||||
pub mod openxr_bindings_schema;
|
||||
pub mod pactl_wrapper;
|
||||
pub mod popup_manager;
|
||||
pub mod steam_utils;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,255 @@
|
|||
use std::{collections::BTreeMap, io::Read, rc::Rc};
|
||||
|
||||
use anyhow::{Context, bail};
|
||||
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<String>,
|
||||
|
||||
pub profiles: BTreeMap<String, Rc<Profile>>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Profile {
|
||||
pub title: Rc<str>,
|
||||
|
||||
#[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)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProfileType {
|
||||
TrackedController,
|
||||
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Subpath {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: SubpathType,
|
||||
|
||||
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)]
|
||||
#[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)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
pub enum IdentifierType {
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.TRIGGER"))]
|
||||
Trigger,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.TRACKPAD"))]
|
||||
Trackpad,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.THUMBSTICK"))]
|
||||
Thumbstick,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.JOYSTICK"))]
|
||||
Joystick,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.SYSTEM"))]
|
||||
System,
|
||||
A,
|
||||
B,
|
||||
X,
|
||||
Y,
|
||||
Start,
|
||||
Home,
|
||||
End,
|
||||
Select,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.THUMBREST"))]
|
||||
Thumbrest,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.SHOULDER"))]
|
||||
Shoulder,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.TYPE.SQUEEZE"))]
|
||||
Squeeze,
|
||||
}
|
||||
|
||||
impl IdentifierType {
|
||||
pub fn translation(&self) -> Translation {
|
||||
self
|
||||
.get_str("Translation")
|
||||
.map(Translation::from_translation_key)
|
||||
.or_else(|| self.get_str("Text").map(Translation::from_raw_text))
|
||||
.unwrap_or_else(|| Translation::from_raw_text(self.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumString, AsRefStr, EnumProperty)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
pub enum Component {
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.CLICK"))]
|
||||
Click,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.FORCE"))]
|
||||
Force,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.TOUCH"))]
|
||||
Touch,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.VALUE"))]
|
||||
Value,
|
||||
Position,
|
||||
Pose,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.PROXIMITY"))]
|
||||
Proximity,
|
||||
Haptic,
|
||||
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.X_AXIS"))]
|
||||
/// Not an actual component, used to turn a 2D Position into a 1D Value
|
||||
X,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.COMP.Y_AXIS"))]
|
||||
/// Not an actual component, used to turn a 2D Position into a 1D Value
|
||||
Y,
|
||||
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Component {
|
||||
pub fn translation(&self) -> Translation {
|
||||
self
|
||||
.get_str("Translation")
|
||||
.map(Translation::from_translation_key)
|
||||
.or_else(|| self.get_str("Text").map(Translation::from_raw_text))
|
||||
.unwrap_or_else(|| Translation::from_raw_text(self.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumString, AsRefStr)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ParsedOpenXrInputPath {
|
||||
pub side: Side,
|
||||
pub identifier: IdentifierType,
|
||||
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;
|
||||
|
||||
fn try_from(path: &str) -> anyhow::Result<Self> {
|
||||
let (side, rest) = if let Some(rest) = path.strip_prefix("/user/hand/left/") {
|
||||
(Side::Left, rest)
|
||||
} else if let Some(rest) = path.strip_prefix("/user/hand/right/") {
|
||||
(Side::Right, rest)
|
||||
} else {
|
||||
bail!("missing hand prefix");
|
||||
};
|
||||
|
||||
let (input, rest) = rest.split_once('/').context("path too short")?;
|
||||
if input != "input" {
|
||||
bail!("missing input prefix");
|
||||
}
|
||||
|
||||
let (identifier, component) = rest.rsplit_once('/').context("missing identifier or component")?;
|
||||
|
||||
if identifier.is_empty() || component.is_empty() {
|
||||
bail!("identifier or component empty");
|
||||
}
|
||||
|
||||
let component = Component::try_from(component).context("bad component")?;
|
||||
|
||||
let identifier = IdentifierType::try_from(identifier).context("bad subpath")?;
|
||||
|
||||
Ok(Self {
|
||||
side,
|
||||
identifier,
|
||||
component,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumString, AsRefStr, EnumProperty)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
pub enum ClickType {
|
||||
#[default]
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.CLICK.ANY"))]
|
||||
Any,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.CLICK.DOUBLE"))]
|
||||
Double,
|
||||
#[strum(props(Translation = "APP_SETTINGS.BINDINGS.CLICK.TRIPLE"))]
|
||||
Triple,
|
||||
}
|
||||
|
||||
impl ClickType {
|
||||
pub fn translation(&self) -> Translation {
|
||||
self
|
||||
.get_str("Translation")
|
||||
.map(Translation::from_translation_key)
|
||||
.or_else(|| self.get_str("Text").map(Translation::from_raw_text))
|
||||
.unwrap_or_else(|| Translation::from_raw_text(self.as_ref()))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,656 @@
|
|||
use std::{borrow::Cow, collections::HashMap, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::{ButtonClickEvent, ComponentButton},
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
log::LogErr,
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
windowing::context_menu::{self, TickResult},
|
||||
};
|
||||
use wlx_common::{
|
||||
config_io,
|
||||
openxr_actions::{OneOrMany, OpenXrInputAction, OpenXrInputProfile, load_xr_input_profiles},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
tab::settings::horiz_cell,
|
||||
util::{
|
||||
openxr_bindings_schema::{ClickType, Component, IdentifierType, ParsedOpenXrInputPath, Profile, Side},
|
||||
popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
},
|
||||
views::{ViewTrait, ViewUpdateParams},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
Save,
|
||||
Cancel,
|
||||
OpenContextMenu(glam::Vec2, Vec<context_menu::Cell>),
|
||||
}
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: WguiGlobals,
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
pub profile_id: Rc<str>,
|
||||
pub profile: Rc<Profile>,
|
||||
pub close_callback: Box<dyn FnOnce()>,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
parser_state: ParserState,
|
||||
tasks: Tasks<Task>,
|
||||
list_parent: WidgetID,
|
||||
globals: WguiGlobals,
|
||||
profiles: Vec<OpenXrInputProfile>,
|
||||
cur_profile_idx: usize,
|
||||
context_menu: context_menu::ContextMenu,
|
||||
schema: Rc<Profile>,
|
||||
close_callback: Option<Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::Save => {
|
||||
let content = serde_json::to_string_pretty(&self.profiles)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to serialize profiles: {e}"))?;
|
||||
config_io::save("openxr_actions.json5", &content)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to save bindings: {e}"))?;
|
||||
|
||||
if let Some(close_callback) = self.close_callback.take() {
|
||||
close_callback();
|
||||
}
|
||||
}
|
||||
Task::Cancel => {
|
||||
if let Some(close_callback) = self.close_callback.take() {
|
||||
close_callback();
|
||||
}
|
||||
}
|
||||
Task::OpenContextMenu(position, cells) => {
|
||||
self.context_menu.open(context_menu::OpenParams {
|
||||
on_custom_attribs: None,
|
||||
position,
|
||||
blueprint: context_menu::Blueprint::Cells(cells),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown handling
|
||||
if let TickResult::Action(name) = self.context_menu.tick(&mut par.layout, &mut self.parser_state)?
|
||||
&& let (Some(action), Some(_), Some(action_name), Some(side), Some(value)) = {
|
||||
let mut s = name.splitn(5, ';');
|
||||
(s.next(), s.next(), s.next(), s.next(), s.next())
|
||||
} {
|
||||
let side = side.to_lowercase();
|
||||
let value = value.to_lowercase();
|
||||
|
||||
log::warn!("{action_name}");
|
||||
|
||||
let mut cur_profile = &mut self.profiles[self.cur_profile_idx];
|
||||
let action_mut = get_action_mut(&mut cur_profile, action_name);
|
||||
let side_mut = if side == "right" {
|
||||
&mut action_mut.right
|
||||
} else {
|
||||
&mut action_mut.left
|
||||
};
|
||||
|
||||
match action {
|
||||
"clear" => {
|
||||
*side_mut = None;
|
||||
}
|
||||
"subpath" => {
|
||||
reconstruct_path(&self.schema, side_mut, &side, Some(value.as_str()), None);
|
||||
}
|
||||
"comp" => {
|
||||
reconstruct_path(&self.schema, side_mut, &side, None, Some(value.as_str()));
|
||||
}
|
||||
"click" => match value.as_str() {
|
||||
"triple" => {
|
||||
action_mut.triple_click = Some(true);
|
||||
action_mut.double_click = None;
|
||||
}
|
||||
"double" => {
|
||||
action_mut.triple_click = None;
|
||||
action_mut.double_click = Some(true);
|
||||
}
|
||||
_ => {
|
||||
action_mut.triple_click = None;
|
||||
action_mut.double_click = None;
|
||||
}
|
||||
},
|
||||
_ => log::warn!("Unknown action {action}"),
|
||||
}
|
||||
|
||||
self.refresh(par.layout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(params: Params) -> anyhow::Result<Self> {
|
||||
let doc_params = &ParseDocumentParams {
|
||||
globals: params.globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/bindings.xml"),
|
||||
extra: Default::default(),
|
||||
};
|
||||
|
||||
let mut profiles = load_xr_input_profiles();
|
||||
|
||||
let cur_profile_idx = profiles
|
||||
.iter()
|
||||
.position(|i| i.profile.as_str() == &*params.profile_id)
|
||||
.unwrap_or_else(|| {
|
||||
let idx = profiles.len();
|
||||
profiles.push(OpenXrInputProfile {
|
||||
profile: params.profile_id.to_string(),
|
||||
..Default::default()
|
||||
});
|
||||
idx
|
||||
});
|
||||
|
||||
let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
|
||||
let list_parent = parser_state.fetch_widget(¶ms.layout.state, "list_parent")?.id;
|
||||
let tasks = Tasks::new();
|
||||
|
||||
{
|
||||
let mut title_label = parser_state.fetch_widget_as::<WidgetLabel>(¶ms.layout.state, "controller_type")?;
|
||||
title_label.set_text_simple(
|
||||
&mut params.globals.get(),
|
||||
Translation::from_raw_text_rc(params.profile.title.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
tasks.handle_button(
|
||||
&parser_state.fetch_component_as::<ComponentButton>("btn_save")?,
|
||||
Task::Save,
|
||||
);
|
||||
|
||||
tasks.handle_button(
|
||||
&parser_state.fetch_component_as::<ComponentButton>("btn_cancel")?,
|
||||
Task::Cancel,
|
||||
);
|
||||
|
||||
let mut me = Self {
|
||||
parser_state,
|
||||
tasks,
|
||||
list_parent,
|
||||
globals: params.globals.clone(),
|
||||
profiles,
|
||||
cur_profile_idx,
|
||||
context_menu: context_menu::ContextMenu::default(),
|
||||
schema: params.profile,
|
||||
close_callback: Some(params.close_callback),
|
||||
};
|
||||
|
||||
me.refresh(params.layout)?;
|
||||
|
||||
Ok(me)
|
||||
}
|
||||
|
||||
fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||
let action_names = [
|
||||
"click",
|
||||
"grab",
|
||||
"alt_click",
|
||||
"show_hide",
|
||||
"toggle_dashboard",
|
||||
"space_drag",
|
||||
"space_rotate",
|
||||
"space_reset",
|
||||
"click_modifier_right",
|
||||
"click_modifier_middle",
|
||||
"move_mouse",
|
||||
"scroll",
|
||||
];
|
||||
|
||||
let mut mp = MacroParams {
|
||||
parser_state: &mut self.parser_state,
|
||||
doc_params: &ParseDocumentParams {
|
||||
globals: self.globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/bindings.xml"),
|
||||
extra: Default::default(),
|
||||
},
|
||||
layout,
|
||||
tasks: self.tasks.clone(),
|
||||
idx: 0,
|
||||
};
|
||||
|
||||
mp.layout.remove_children(self.list_parent);
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_action_mut<'a>(profile: &'a mut OpenXrInputProfile, action_name: &str) -> &'a mut OpenXrInputAction {
|
||||
let action = match action_name {
|
||||
"click" => &mut profile.click,
|
||||
"grab" => &mut profile.grab,
|
||||
"alt_click" => &mut profile.alt_click,
|
||||
"show_hide" => &mut profile.show_hide,
|
||||
"toggle_dashboard" => &mut profile.toggle_dashboard,
|
||||
"space_drag" => &mut profile.space_drag,
|
||||
"space_rotate" => &mut profile.space_rotate,
|
||||
"space_reset" => &mut profile.space_reset,
|
||||
"click_modifier_right" => &mut profile.click_modifier_right,
|
||||
"click_modifier_middle" => &mut profile.click_modifier_middle,
|
||||
"move_mouse" => &mut profile.move_mouse,
|
||||
"scroll" => &mut profile.scroll,
|
||||
_ => panic!("unknown action_name: {action_name}"),
|
||||
};
|
||||
|
||||
action.get_or_insert_with(OpenXrInputAction::default)
|
||||
}
|
||||
|
||||
pub fn mount_popup(
|
||||
frontend_tasks: FrontendTasks,
|
||||
globals: WguiGlobals,
|
||||
popup: PopupHolder<View>,
|
||||
profile_id: Rc<str>,
|
||||
profile: Rc<Profile>,
|
||||
) {
|
||||
frontend_tasks
|
||||
.clone()
|
||||
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
|
||||
Translation::from_translation_key("APP_SETTINGS.INPUT_PROFILES"),
|
||||
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,
|
||||
close_callback,
|
||||
})?;
|
||||
|
||||
popup.set_view(data.handle, view, None);
|
||||
Ok(popup.get_close_callback(data.layout))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
|
||||
struct MacroParams<'a> {
|
||||
pub layout: &'a mut Layout,
|
||||
pub parser_state: &'a mut ParserState,
|
||||
pub doc_params: &'a ParseDocumentParams<'a>,
|
||||
pub tasks: Tasks<Task>,
|
||||
pub idx: usize,
|
||||
}
|
||||
|
||||
fn input_controls_for_action(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
action: Rc<str>,
|
||||
profile: &Profile,
|
||||
current: &mut OpenXrInputAction,
|
||||
) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
|
||||
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
|
||||
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
|
||||
params.insert(
|
||||
Rc::from("translation"),
|
||||
Rc::from(format!(
|
||||
"APP_SETTINGS.BINDINGS.ACTION.{}",
|
||||
action.as_ref().to_uppercase()
|
||||
)),
|
||||
);
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "ActionRow", mp.layout, parent, params)?;
|
||||
|
||||
let parent = mp.parser_state.get_widget_id(&id)?;
|
||||
|
||||
let click_type = if current.triple_click.unwrap_or_default() {
|
||||
ClickType::Triple
|
||||
} else if current.double_click.unwrap_or_default() {
|
||||
ClickType::Double
|
||||
} else {
|
||||
ClickType::Any
|
||||
};
|
||||
|
||||
let current_left = current.left.as_ref().map(|x| match x {
|
||||
OneOrMany::One(s) => s.as_str(),
|
||||
OneOrMany::Many(s) => s.first().unwrap().as_str(), // safe
|
||||
});
|
||||
|
||||
input_controls_for_hand(
|
||||
mp,
|
||||
parent,
|
||||
current_left,
|
||||
Side::Left,
|
||||
action.clone(),
|
||||
click_type,
|
||||
profile,
|
||||
)?;
|
||||
|
||||
let current_right = current.right.as_ref().map(|x| match x {
|
||||
OneOrMany::One(s) => s.as_str(),
|
||||
OneOrMany::Many(s) => s.first().unwrap().as_str(), // safe
|
||||
});
|
||||
|
||||
input_controls_for_hand(mp, parent, current_right, Side::Right, action, click_type, profile)
|
||||
}
|
||||
|
||||
fn input_controls_for_hand(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
current: Option<&str>,
|
||||
side: Side,
|
||||
action: Rc<str>,
|
||||
click_type: ClickType,
|
||||
profile: &Profile,
|
||||
) -> anyhow::Result<()> {
|
||||
let subaction_path = match side {
|
||||
Side::Left => "/user/hand/left",
|
||||
Side::Right => "/user/hand/right",
|
||||
};
|
||||
|
||||
if !profile.subaction_paths.iter().any(|p| p == subaction_path) {
|
||||
return Ok(()); // skip
|
||||
}
|
||||
|
||||
let current = current
|
||||
.map(|cur| ParsedOpenXrInputPath::try_from(cur).log_warn(cur).ok())
|
||||
.flatten();
|
||||
|
||||
let parent = horiz_cell(mp.layout, parent)?;
|
||||
|
||||
let available_components = current
|
||||
.as_ref()
|
||||
.map(|par| profile.subpaths.get(&par.to_subpath()))
|
||||
.flatten()
|
||||
.map(|subp| subp.get_effective_components())
|
||||
.unwrap_or_default();
|
||||
|
||||
let available_subpaths = profile
|
||||
.subpaths
|
||||
.iter()
|
||||
.filter(|(_, path)| path.side.is_none_or(|s| s == side))
|
||||
.filter_map(|(key, _)| {
|
||||
key
|
||||
.strip_prefix("/input/")
|
||||
.map(|ident| IdentifierType::try_from(ident).ok())
|
||||
.flatten()
|
||||
})
|
||||
.collect::<Rc<[IdentifierType]>>();
|
||||
|
||||
subpath_dropdown(
|
||||
mp,
|
||||
parent,
|
||||
action.clone(),
|
||||
side,
|
||||
available_subpaths,
|
||||
current.as_ref().map(|x| x.identifier),
|
||||
)?;
|
||||
|
||||
if !component_dropdown(
|
||||
mp,
|
||||
parent,
|
||||
action.clone(),
|
||||
side,
|
||||
available_components,
|
||||
current.as_ref().map(|x| x.component),
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
clicks_dropdown(mp, parent, action, click_type)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subpath_dropdown(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
action: Rc<str>,
|
||||
side: Side,
|
||||
available: Rc<[IdentifierType]>,
|
||||
current: Option<IdentifierType>,
|
||||
) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
|
||||
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
|
||||
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
|
||||
params.insert(
|
||||
Rc::from("translation"),
|
||||
Rc::from(format!("APP_SETTINGS.BINDINGS.{}", side.as_ref().to_uppercase())),
|
||||
);
|
||||
params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.SUBPATH"));
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
|
||||
|
||||
let title = current
|
||||
.map(|c| c.translation())
|
||||
.unwrap_or_else(|| Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"));
|
||||
|
||||
{
|
||||
let mut label = mp
|
||||
.parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
|
||||
label.set_text_simple(&mut mp.layout.state.globals.get(), title);
|
||||
}
|
||||
|
||||
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
btn.on_click(Rc::new({
|
||||
let available = available.clone();
|
||||
let tasks = mp.tasks.clone();
|
||||
move |_common, e: ButtonClickEvent| {
|
||||
let side = side.as_ref();
|
||||
let mut cells = available
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let value = item.as_ref();
|
||||
let title = item.translation();
|
||||
|
||||
context_menu::Cell {
|
||||
action_name: Some(format!("subpath;{id};{action};{side};{value}").into()),
|
||||
title,
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cells.insert(
|
||||
0,
|
||||
context_menu::Cell {
|
||||
action_name: Some(format!("clear;{id};{action};{side};-").into()),
|
||||
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
},
|
||||
);
|
||||
|
||||
tasks.push(Task::OpenContextMenu(e.mouse_pos_absolute.unwrap_or_default(), cells));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn component_dropdown(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
action: Rc<str>,
|
||||
side: Side,
|
||||
available: Rc<[Component]>,
|
||||
current: Option<Component>,
|
||||
) -> anyhow::Result<bool> {
|
||||
if available.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
|
||||
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
|
||||
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
|
||||
params.insert(Rc::from("text"), Rc::from("・"));
|
||||
params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.COMPONENT"));
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
|
||||
|
||||
let title = current
|
||||
.map(|c| c.translation())
|
||||
.unwrap_or_else(|| Translation::from_raw_text_rc(Default::default()));
|
||||
|
||||
{
|
||||
let mut label = mp
|
||||
.parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
|
||||
label.set_text_simple(&mut mp.layout.state.globals.get(), title);
|
||||
}
|
||||
|
||||
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
btn.on_click(Rc::new({
|
||||
let tasks = mp.tasks.clone();
|
||||
let available = available.clone();
|
||||
move |_common, e: ButtonClickEvent| {
|
||||
tasks.push(Task::OpenContextMenu(
|
||||
e.mouse_pos_absolute.unwrap_or_default(),
|
||||
available
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let value = item.as_ref();
|
||||
let title = item.translation();
|
||||
let side = side.as_ref();
|
||||
|
||||
Some(context_menu::Cell {
|
||||
action_name: Some(format!("comp;{id};{action};{side};{value}").into()),
|
||||
title,
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn clicks_dropdown(mp: &mut MacroParams, parent: WidgetID, action: Rc<str>, current: ClickType) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
|
||||
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
|
||||
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
|
||||
params.insert(Rc::from("text"), Rc::from("・"));
|
||||
params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.CLICK.TYPE"));
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
|
||||
|
||||
let title = current.translation();
|
||||
|
||||
{
|
||||
let mut label = mp
|
||||
.parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
|
||||
label.set_text_simple(&mut mp.layout.state.globals.get(), title);
|
||||
}
|
||||
|
||||
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
btn.on_click(Rc::new({
|
||||
let tasks = mp.tasks.clone();
|
||||
move |_common, e: ButtonClickEvent| {
|
||||
tasks.push(Task::OpenContextMenu(
|
||||
e.mouse_pos_absolute.unwrap_or_default(),
|
||||
[ClickType::Any, ClickType::Double, ClickType::Triple]
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let value = item.as_ref();
|
||||
let title = item.translation();
|
||||
|
||||
Some(context_menu::Cell {
|
||||
action_name: Some(format!("click;{id};{action};-;{value}").into()),
|
||||
title,
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reconstruct_path(
|
||||
schema: &Profile,
|
||||
side_mut: &mut Option<OneOrMany<String>>,
|
||||
side: &str,
|
||||
subpath: Option<&str>,
|
||||
comp: Option<&str>,
|
||||
) {
|
||||
if side_mut.is_none() {
|
||||
let Some(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}")));
|
||||
}
|
||||
} else {
|
||||
let first = match side_mut.as_ref().unwrap() {
|
||||
OneOrMany::One(x) => x.as_str(),
|
||||
OneOrMany::Many(x) => x.first().unwrap().as_str(),
|
||||
};
|
||||
|
||||
let Ok(parsed) = ParsedOpenXrInputPath::try_from(first) else {
|
||||
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);
|
||||
|
||||
*side_mut = Some(OneOrMany::One(format!(
|
||||
"/user/hand/{side}/input/{new_subpath}/{new_comp}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use anyhow::Context;
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::ComponentButton,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams},
|
||||
task::Tasks,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
openxr_bindings_schema,
|
||||
popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
},
|
||||
views::{self, ViewTrait, ViewUpdateParams, bindings},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
SelectProfile(Rc<str>),
|
||||
Cancel,
|
||||
}
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: WguiGlobals,
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
pub frontend_tasks: &'a FrontendTasks,
|
||||
pub close_callback: Box<dyn FnOnce()>,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
tasks: Tasks<Task>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
globals: WguiGlobals,
|
||||
close_callback: Option<Box<dyn FnOnce()>>,
|
||||
bindings_popup: PopupHolder<bindings::View>,
|
||||
bindings_file: openxr_bindings_schema::BindingsFile,
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
self.bindings_popup.update(par)?;
|
||||
|
||||
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?")?;
|
||||
|
||||
views::bindings::mount_popup(
|
||||
self.frontend_tasks.clone(),
|
||||
self.globals.clone(),
|
||||
self.bindings_popup.clone(),
|
||||
profile_id.clone(),
|
||||
profile.clone(),
|
||||
);
|
||||
}
|
||||
Task::Cancel => {
|
||||
if let Some(close_callback) = self.close_callback.take() {
|
||||
close_callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(params: Params) -> anyhow::Result<Self> {
|
||||
let doc_params = &ParseDocumentParams {
|
||||
globals: params.globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/input_profiles.xml"),
|
||||
extra: Default::default(),
|
||||
};
|
||||
|
||||
let mut parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
|
||||
|
||||
let list_parent = parser_state.fetch_widget(¶ms.layout.state, "list_parent")?.id;
|
||||
|
||||
let tasks = Tasks::new();
|
||||
|
||||
tasks.handle_button(
|
||||
&parser_state.fetch_component_as::<ComponentButton>("btn_cancel")?,
|
||||
Task::Cancel,
|
||||
);
|
||||
|
||||
let bindings_file = openxr_bindings_schema::BindingsFile::load_embedded();
|
||||
|
||||
for (idx, (profile_id, profile)) in bindings_file.profiles.iter().enumerate() {
|
||||
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();
|
||||
cell_params.insert(Rc::from("id"), Rc::from(id.clone()));
|
||||
cell_params.insert(Rc::from("text"), Rc::from(profile_name));
|
||||
|
||||
parser_state.instantiate_template(doc_params, "ProfileButton", params.layout, list_parent, cell_params)?;
|
||||
|
||||
let btn = parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
let tasks_clone = tasks.clone();
|
||||
btn.on_click(Rc::new({
|
||||
let profile_id: Rc<str> = profile_id.clone().into();
|
||||
move |_common, _e| {
|
||||
tasks_clone.push(Task::SelectProfile(profile_id.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
tasks,
|
||||
frontend_tasks: params.frontend_tasks.clone(),
|
||||
globals: params.globals.clone(),
|
||||
close_callback: Some(params.close_callback),
|
||||
bindings_popup: Default::default(),
|
||||
bindings_file,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, popup: PopupHolder<View>) {
|
||||
frontend_tasks
|
||||
.clone()
|
||||
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
|
||||
Translation::from_translation_key("APP_SETTINGS.INPUT_PROFILES"),
|
||||
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,
|
||||
frontend_tasks: &frontend_tasks,
|
||||
close_callback,
|
||||
})?;
|
||||
|
||||
popup.set_view(data.handle, view, None);
|
||||
Ok(popup.get_close_callback(data.layout))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
|
|
@ -2,11 +2,13 @@ use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_inte
|
|||
|
||||
pub mod app_launcher;
|
||||
pub mod audio_settings;
|
||||
pub mod bindings;
|
||||
pub mod dialog_box;
|
||||
pub mod download_file;
|
||||
pub mod game_cover;
|
||||
pub mod game_launcher;
|
||||
pub mod game_list;
|
||||
pub mod input_profiles;
|
||||
pub mod remote_skymap_downloader;
|
||||
pub mod remote_skymap_list;
|
||||
pub mod running_games_list;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
#!/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/khr/generic_controller
|
||||
/interaction_profiles/khr/simple_controller
|
||||
/interaction_profiles/ml/ml2_controller
|
||||
/interaction_profiles/microsoft/motion_controller
|
||||
/interaction_profiles/mndx/flipvr
|
||||
/interaction_profiles/mndx/hydra
|
||||
/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"
|
||||
|
|
@ -36,6 +36,7 @@ regex.workspace = true
|
|||
rust-embed.workspace = true
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json.workspace = true
|
||||
serde_json5.workspace = true
|
||||
slotmap.workspace = true
|
||||
strum.workspace = true
|
||||
vulkano.workspace = true
|
||||
|
|
@ -68,7 +69,6 @@ ovr_overlay = { git = "https://github.com/galister/ovr_overlay_oyasumi", rev = "
|
|||
prost = { version = "0.14.3", optional = true }
|
||||
pure-rust-locales = "0.8.2"
|
||||
rosc = { version = "0.11.4", optional = true }
|
||||
serde_json5 = "0.2.1"
|
||||
serde_yaml = "0.9.34"
|
||||
signal-hook = "0.3.18"
|
||||
smallvec = "1.15.1"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ use std::{
|
|||
use glam::{Affine3A, Quat, Vec3, bool};
|
||||
use libmonado::{self as mnd, DeviceLogic};
|
||||
use openxr::{self as xr, Quaternionf, Vector2f, Vector3f};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wlx_common::{config::HandsfreePointer, config_io};
|
||||
use wlx_common::{
|
||||
config::HandsfreePointer,
|
||||
openxr_actions::{OneOrMany, load_xr_input_profiles},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::input::{Haptics, InputState, Pointer, TrackedDevice, TrackedDeviceRole},
|
||||
|
|
@ -647,7 +649,7 @@ macro_rules! add_custom_lr {
|
|||
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 3]) {
|
||||
let profiles = load_action_profiles();
|
||||
let profiles = load_xr_input_profiles();
|
||||
|
||||
for profile in profiles {
|
||||
log::warn!("Loading profile {}", &profile.profile);
|
||||
|
|
@ -723,67 +725,3 @@ fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 3]) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum OneOrMany<T> {
|
||||
One(T),
|
||||
Many(Vec<T>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct OpenXrActionConfAction {
|
||||
left: Option<OneOrMany<String>>,
|
||||
right: Option<OneOrMany<String>>,
|
||||
handsfree: Option<OneOrMany<String>>,
|
||||
threshold: Option<[f32; 2]>,
|
||||
double_click: Option<bool>,
|
||||
triple_click: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct OpenXrActionConfProfile {
|
||||
profile: String,
|
||||
pose: Option<OpenXrActionConfAction>,
|
||||
click: Option<OpenXrActionConfAction>,
|
||||
grab: Option<OpenXrActionConfAction>,
|
||||
alt_click: Option<OpenXrActionConfAction>,
|
||||
show_hide: Option<OpenXrActionConfAction>,
|
||||
toggle_dashboard: Option<OpenXrActionConfAction>,
|
||||
space_drag: Option<OpenXrActionConfAction>,
|
||||
space_rotate: Option<OpenXrActionConfAction>,
|
||||
space_reset: Option<OpenXrActionConfAction>,
|
||||
click_modifier_right: Option<OpenXrActionConfAction>,
|
||||
click_modifier_middle: Option<OpenXrActionConfAction>,
|
||||
move_mouse: Option<OpenXrActionConfAction>,
|
||||
scroll: Option<OpenXrActionConfAction>,
|
||||
haptic: Option<OpenXrActionConfAction>,
|
||||
}
|
||||
|
||||
const DEFAULT_PROFILES: &str = include_str!("openxr_actions.json5");
|
||||
|
||||
fn load_action_profiles() -> Vec<OpenXrActionConfProfile> {
|
||||
let mut profiles: Vec<OpenXrActionConfProfile> =
|
||||
serde_json5::from_str(DEFAULT_PROFILES).unwrap(); // want panic
|
||||
|
||||
let Some(conf) = config_io::load("openxr_actions.json5") else {
|
||||
return profiles;
|
||||
};
|
||||
|
||||
match serde_json5::from_str::<Vec<OpenXrActionConfProfile>>(&conf) {
|
||||
Ok(override_profiles) => {
|
||||
for new in override_profiles {
|
||||
if let Some(i) = profiles.iter().position(|old| old.profile == new.profile) {
|
||||
profiles[i] = new;
|
||||
} else {
|
||||
profiles.push(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load openxr_actions.json5: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
profiles
|
||||
}
|
||||
|
|
|
|||
|
|
@ -453,15 +453,24 @@ impl<T> OverlayWindowManager<T> {
|
|||
for o in self.overlays.values() {
|
||||
if o.config.global {
|
||||
if let Some(state) = &o.config.active_state {
|
||||
app.session.config.global_set.insert(o.config.name.clone(), state.clone());
|
||||
app.session
|
||||
.config
|
||||
.global_set
|
||||
.insert(o.config.name.clone(), state.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (name, state) in &self.global_set.hidden_overlays {
|
||||
app.session.config.global_set.insert(name.clone(), state.clone());
|
||||
app.session
|
||||
.config
|
||||
.global_set
|
||||
.insert(name.clone(), state.clone());
|
||||
}
|
||||
for (name, state) in &self.global_set.inactive_overlays {
|
||||
app.session.config.global_set.insert(name.clone(), state.clone());
|
||||
app.session
|
||||
.config
|
||||
.global_set
|
||||
.insert(name.clone(), state.clone());
|
||||
}
|
||||
|
||||
// BackendAttrib
|
||||
|
|
|
|||
|
|
@ -222,20 +222,20 @@ impl OverlayWindowConfig {
|
|||
.saved_transform
|
||||
.unwrap_or(self.default_state.transform);
|
||||
|
||||
let (parent_transform, align_to_hmd) = match state.positioning {
|
||||
Positioning::Floating | Positioning::FollowHead { .. } => (app.input_state.hmd, false),
|
||||
Positioning::FollowHand {
|
||||
hand, align_to_hmd, ..
|
||||
} => (app.input_state.pointers[hand as usize].pose, align_to_hmd),
|
||||
Positioning::Anchored => (app.anchor, false),
|
||||
Positioning::Static => {
|
||||
if hard_reset {
|
||||
(app.input_state.hmd, false)
|
||||
} else {
|
||||
(Affine3A::IDENTITY, false)
|
||||
let (parent_transform, align_to_hmd) = match state.positioning {
|
||||
Positioning::Floating | Positioning::FollowHead { .. } => (app.input_state.hmd, false),
|
||||
Positioning::FollowHand {
|
||||
hand, align_to_hmd, ..
|
||||
} => (app.input_state.pointers[hand as usize].pose, align_to_hmd),
|
||||
Positioning::Anchored => (app.anchor, false),
|
||||
Positioning::Static => {
|
||||
if hard_reset {
|
||||
(app.input_state.hmd, false)
|
||||
} else {
|
||||
(Affine3A::IDENTITY, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if hard_reset {
|
||||
state.saved_transform = None;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ idmap-derive.workspace = true
|
|||
log.workspace = true
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json.workspace = true
|
||||
serde_json5.workspace = true
|
||||
strum.workspace = true
|
||||
xdg.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -66,3 +66,9 @@ pub fn load(filename: &str) -> Option<String> {
|
|||
|
||||
std::fs::read_to_string(path).ok()
|
||||
}
|
||||
|
||||
pub fn save(filename: &str, content: &str) -> anyhow::Result<()> {
|
||||
let path = get_config_file_path(filename);
|
||||
std::fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub mod dash_interface_emulated;
|
|||
pub mod desktop_finder;
|
||||
mod handle;
|
||||
pub mod locale;
|
||||
pub mod openxr_actions;
|
||||
pub mod overlays;
|
||||
pub mod timestep;
|
||||
pub mod windowing;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config_io;
|
||||
|
||||
const DEFAULT_XR_INPUT_PROFILES: &str = include_str!("../assets/openxr_actions.json5");
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OneOrMany<T> {
|
||||
One(T),
|
||||
Many(Vec<T>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenXrInputAction {
|
||||
pub left: Option<OneOrMany<String>>,
|
||||
pub right: Option<OneOrMany<String>>,
|
||||
pub handsfree: Option<OneOrMany<String>>,
|
||||
pub threshold: Option<[f32; 2]>,
|
||||
pub double_click: Option<bool>,
|
||||
pub triple_click: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenXrInputProfile {
|
||||
pub profile: String,
|
||||
pub pose: Option<OpenXrInputAction>,
|
||||
pub click: Option<OpenXrInputAction>,
|
||||
pub grab: Option<OpenXrInputAction>,
|
||||
pub alt_click: Option<OpenXrInputAction>,
|
||||
pub show_hide: Option<OpenXrInputAction>,
|
||||
pub toggle_dashboard: Option<OpenXrInputAction>,
|
||||
pub space_drag: Option<OpenXrInputAction>,
|
||||
pub space_rotate: Option<OpenXrInputAction>,
|
||||
pub space_reset: Option<OpenXrInputAction>,
|
||||
pub click_modifier_right: Option<OpenXrInputAction>,
|
||||
pub click_modifier_middle: Option<OpenXrInputAction>,
|
||||
pub move_mouse: Option<OpenXrInputAction>,
|
||||
pub scroll: Option<OpenXrInputAction>,
|
||||
pub haptic: Option<OpenXrInputAction>,
|
||||
}
|
||||
|
||||
pub fn load_xr_input_profiles() -> Vec<OpenXrInputProfile> {
|
||||
let mut profiles: Vec<OpenXrInputProfile> = serde_json5::from_str(DEFAULT_XR_INPUT_PROFILES).unwrap(); // want panic
|
||||
|
||||
let Some(conf) = config_io::load("openxr_actions.json5") else {
|
||||
return profiles;
|
||||
};
|
||||
|
||||
match serde_json5::from_str::<Vec<OpenXrInputProfile>>(&conf) {
|
||||
Ok(override_profiles) => {
|
||||
for new in override_profiles {
|
||||
if let Some(i) = profiles.iter().position(|old| old.profile == new.profile) {
|
||||
profiles[i] = new;
|
||||
} else {
|
||||
profiles.push(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load openxr_actions.json5: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
profiles
|
||||
}
|
||||
Loading…
Reference in New Issue