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",
|
"hyper",
|
||||||
"keyvalues-parser",
|
"keyvalues-parser",
|
||||||
"log",
|
"log",
|
||||||
|
"lz4_flex",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -3104,6 +3105,15 @@ dependencies = [
|
||||||
"hashbrown 0.16.1",
|
"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]]
|
[[package]]
|
||||||
name = "mach2"
|
name = "mach2"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
@ -5969,6 +5979,12 @@ dependencies = [
|
||||||
"core_maths",
|
"core_maths",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "twox-hash"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
|
@ -7221,6 +7237,7 @@ dependencies = [
|
||||||
"rust-ini",
|
"rust-ini",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_json5",
|
||||||
"smol",
|
"smol",
|
||||||
"strum",
|
"strum",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ regex = "1.12.2"
|
||||||
rust-embed = "8.9.0"
|
rust-embed = "8.9.0"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
|
serde_json5 = "0.2.1"
|
||||||
slotmap = "1.1.1"
|
slotmap = "1.1.1"
|
||||||
smol = "2.0.2"
|
smol = "2.0.2"
|
||||||
strum = { version = "0.27.2", features = ["derive"] }
|
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"] }
|
hyper = { version = "1.8.1", features = ["client", "http1", "http2"] }
|
||||||
keyvalues-parser = { git = "https://codeberg.org/CosmicHarper/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
|
keyvalues-parser = { git = "https://codeberg.org/CosmicHarper/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
lz4_flex = { version = "0.13.1", features = ["frame"] }
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
serde = { workspace = true, features = ["rc"] }
|
serde = { workspace = true, features = ["rc"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -31,6 +31,13 @@
|
||||||
<RadioBox text="${text}" translation="${translation}" value="${value}" tooltip="${tooltip}" />
|
<RadioBox text="${text}" translation="${translation}" value="${value}" tooltip="${tooltip}" />
|
||||||
</template>
|
</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">
|
<template name="DangerButton">
|
||||||
<Button id="${id}" color="#AA3333" height="32" tooltip="${translation}_HELP" padding="4" gap="8">
|
<Button id="${id}" color="#AA3333" height="32" tooltip="${translation}_HELP" padding="4" gap="8">
|
||||||
<sprite src_builtin="${icon}" height="24" width="24" />
|
<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_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": "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",
|
"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_ONLINE_CATALOG": "Browse online catalog...",
|
||||||
"BROWSE_SKYMAPS": "Browse skymaps",
|
"BROWSE_SKYMAPS": "Browse skymaps",
|
||||||
"CAPTURE_METHOD": "Wayland screen capture",
|
"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.",
|
"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_GRAB_HELP": "Hide grab help",
|
||||||
"HIDE_USERNAME": "Hide username",
|
"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_X": "Invert horizontal scroll direction",
|
||||||
"INVERT_SCROLL_DIRECTION_Y": "Invert vertical scroll direction",
|
"INVERT_SCROLL_DIRECTION_Y": "Invert vertical scroll direction",
|
||||||
"KEYBOARD_MIDDLE_CLICK": "Keyboard middle click",
|
"KEYBOARD_MIDDLE_CLICK": "Keyboard middle click",
|
||||||
|
|
@ -134,7 +181,9 @@
|
||||||
"GRID_OPACITY_HELP": "Opacity of the floor grid when the skybox is enabled",
|
"GRID_OPACITY_HELP": "Opacity of the floor grid when the skybox is enabled",
|
||||||
"XR_CLICK_SENSITIVITY": "XR trigger sensitivity",
|
"XR_CLICK_SENSITIVITY": "XR trigger sensitivity",
|
||||||
"XR_CLICK_SENSITIVITY_HELP": "Press and release values for analog triggers",
|
"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_LAUNCHER": "Application launcher",
|
||||||
"APPLICATION_STARTED": "Application started",
|
"APPLICATION_STARTED": "Application started",
|
||||||
|
|
|
||||||
|
|
@ -548,7 +548,7 @@ impl SettingType {
|
||||||
}
|
}
|
||||||
|
|
||||||
// creates a simple div with horizontal, centered flow
|
// 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(
|
let (pair, _) = layout.add_child(
|
||||||
parent,
|
parent,
|
||||||
WidgetDiv::create(),
|
WidgetDiv::create(),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,82 @@
|
||||||
use crate::tab::settings::{
|
use std::{collections::HashMap, rc::Rc};
|
||||||
SettingType, SettingsMountParams, SettingsTab,
|
|
||||||
macros::{
|
use wgui::{
|
||||||
options_category, options_checkbox, options_dropdown, options_range_f32, options_slider_f32, options_slider_i32,
|
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 {
|
impl State {
|
||||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<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)?;
|
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::{
|
use crate::tab::settings::{
|
||||||
SettingType, SettingsMountParams, SettingsTab,
|
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 {}
|
pub struct State {}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod cached_fetcher;
|
pub mod cached_fetcher;
|
||||||
pub mod networking;
|
pub mod networking;
|
||||||
|
pub mod openxr_bindings_schema;
|
||||||
pub mod pactl_wrapper;
|
pub mod pactl_wrapper;
|
||||||
pub mod popup_manager;
|
pub mod popup_manager;
|
||||||
pub mod steam_utils;
|
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 app_launcher;
|
||||||
pub mod audio_settings;
|
pub mod audio_settings;
|
||||||
|
pub mod bindings;
|
||||||
pub mod dialog_box;
|
pub mod dialog_box;
|
||||||
pub mod download_file;
|
pub mod download_file;
|
||||||
pub mod game_cover;
|
pub mod game_cover;
|
||||||
pub mod game_launcher;
|
pub mod game_launcher;
|
||||||
pub mod game_list;
|
pub mod game_list;
|
||||||
|
pub mod input_profiles;
|
||||||
pub mod remote_skymap_downloader;
|
pub mod remote_skymap_downloader;
|
||||||
pub mod remote_skymap_list;
|
pub mod remote_skymap_list;
|
||||||
pub mod running_games_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
|
rust-embed.workspace = true
|
||||||
serde = { workspace = true, features = ["rc"] }
|
serde = { workspace = true, features = ["rc"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
serde_json5.workspace = true
|
||||||
slotmap.workspace = true
|
slotmap.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
vulkano.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 }
|
prost = { version = "0.14.3", optional = true }
|
||||||
pure-rust-locales = "0.8.2"
|
pure-rust-locales = "0.8.2"
|
||||||
rosc = { version = "0.11.4", optional = true }
|
rosc = { version = "0.11.4", optional = true }
|
||||||
serde_json5 = "0.2.1"
|
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
signal-hook = "0.3.18"
|
signal-hook = "0.3.18"
|
||||||
smallvec = "1.15.1"
|
smallvec = "1.15.1"
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ use std::{
|
||||||
use glam::{Affine3A, Quat, Vec3, bool};
|
use glam::{Affine3A, Quat, Vec3, bool};
|
||||||
use libmonado::{self as mnd, DeviceLogic};
|
use libmonado::{self as mnd, DeviceLogic};
|
||||||
use openxr::{self as xr, Quaternionf, Vector2f, Vector3f};
|
use openxr::{self as xr, Quaternionf, Vector2f, Vector3f};
|
||||||
use serde::{Deserialize, Serialize};
|
use wlx_common::{
|
||||||
use wlx_common::{config::HandsfreePointer, config_io};
|
config::HandsfreePointer,
|
||||||
|
openxr_actions::{OneOrMany, load_xr_input_profiles},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::input::{Haptics, InputState, Pointer, TrackedDevice, TrackedDeviceRole},
|
backend::input::{Haptics, InputState, Pointer, TrackedDevice, TrackedDeviceRole},
|
||||||
|
|
@ -647,7 +649,7 @@ macro_rules! add_custom_lr {
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||||
fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 3]) {
|
fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 3]) {
|
||||||
let profiles = load_action_profiles();
|
let profiles = load_xr_input_profiles();
|
||||||
|
|
||||||
for profile in profiles {
|
for profile in profiles {
|
||||||
log::warn!("Loading profile {}", &profile.profile);
|
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() {
|
for o in self.overlays.values() {
|
||||||
if o.config.global {
|
if o.config.global {
|
||||||
if let Some(state) = &o.config.active_state {
|
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 {
|
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 {
|
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
|
// BackendAttrib
|
||||||
|
|
|
||||||
|
|
@ -222,20 +222,20 @@ impl OverlayWindowConfig {
|
||||||
.saved_transform
|
.saved_transform
|
||||||
.unwrap_or(self.default_state.transform);
|
.unwrap_or(self.default_state.transform);
|
||||||
|
|
||||||
let (parent_transform, align_to_hmd) = match state.positioning {
|
let (parent_transform, align_to_hmd) = match state.positioning {
|
||||||
Positioning::Floating | Positioning::FollowHead { .. } => (app.input_state.hmd, false),
|
Positioning::Floating | Positioning::FollowHead { .. } => (app.input_state.hmd, false),
|
||||||
Positioning::FollowHand {
|
Positioning::FollowHand {
|
||||||
hand, align_to_hmd, ..
|
hand, align_to_hmd, ..
|
||||||
} => (app.input_state.pointers[hand as usize].pose, align_to_hmd),
|
} => (app.input_state.pointers[hand as usize].pose, align_to_hmd),
|
||||||
Positioning::Anchored => (app.anchor, false),
|
Positioning::Anchored => (app.anchor, false),
|
||||||
Positioning::Static => {
|
Positioning::Static => {
|
||||||
if hard_reset {
|
if hard_reset {
|
||||||
(app.input_state.hmd, false)
|
(app.input_state.hmd, false)
|
||||||
} else {
|
} else {
|
||||||
(Affine3A::IDENTITY, false)
|
(Affine3A::IDENTITY, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
if hard_reset {
|
if hard_reset {
|
||||||
state.saved_transform = None;
|
state.saved_transform = None;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ idmap-derive.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde = { workspace = true, features = ["rc"] }
|
serde = { workspace = true, features = ["rc"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
serde_json5.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
xdg.workspace = true
|
xdg.workspace = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,3 +66,9 @@ pub fn load(filename: &str) -> Option<String> {
|
||||||
|
|
||||||
std::fs::read_to_string(path).ok()
|
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;
|
pub mod desktop_finder;
|
||||||
mod handle;
|
mod handle;
|
||||||
pub mod locale;
|
pub mod locale;
|
||||||
|
pub mod openxr_actions;
|
||||||
pub mod overlays;
|
pub mod overlays;
|
||||||
pub mod timestep;
|
pub mod timestep;
|
||||||
pub mod windowing;
|
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