Compare commits

..
Author SHA1 Message Date
Aleksander 4229d5cb61 Add contribution instructions regarding LLM usage policy [skip ci] 2026-10-26 21:26:55 +01:00
Aleksander abafddefd8 add Swedish translation strings 2026-10-24 23:31:49 +02:00
Jonatan Nyberg 9b1fc2774a Add Swedish template to the translation tool (#701) 2026-10-24 22:48:43 +02:00
galister c539ff6a90 XSHM & PipeWire CPU capture fixes (#698) 2026-10-23 21:13:08 +09:00
galister f94c0c4ace desktop_finder rework (#697)
* desktop_finder rework

* fix args for wayvrctl process-launch
2026-10-23 19:29:25 +09:00
Aleksander 5f4c97794c wayvr: prevent alpha from going NaN again 2026-10-22 18:04:40 +02:00
Aleksander d9288a877c dash-frontend: refactoring: deduplicate dropdown code 2026-10-21 19:57:14 +02:00
galister dff4b0067a thumbstick axes fix 2026-10-21 14:29:48 +09:00
oo8dev adf852971f dash-frontend: whisper GPU device selector (Closes #680) (#692) 2026-10-18 17:19:10 +02:00
Aleksander 074d2de7aa wgui: bugfix: layout is recomputed every frame (tick/render loop) (Closes #690) 2026-10-18 12:37:49 +02:00
diffy e8d9a35714 openxr: pivot unlocked space rotate on the hand (#689)
* openxr: pivot unlocked space rotate on the hand

* openxr: don't start space drag during space rotate
2026-10-18 16:53:05 +09:00
diffy 36f9fab5c6 apply ui_gradient_intensity to theme (#688) 2026-10-17 13:16:03 +09:00
diffy 058792fc8e wlx-capture: destroy dmabuf params after create_immed (#687) 2026-10-17 13:15:48 +09:00
Dandrid ef934e0b02 chore: Swap away from using deprecated associated constants (#686) 2026-10-17 13:05:56 +09:00
galister c42e992a62 support both systemctl and loginctl 2026-10-15 13:20:31 +09:00
galister 41d7a0b3d2 try fix appimage with multi-desktop files 2026-10-14 13:22:25 +09:00
galister 471e4be946 show toast when starting hidden 2026-10-14 13:02:22 +09:00
galister 26afa57fe3 include headless desktop entry 2026-10-14 12:51:33 +09:00
galister d120617abe openvr: hide set & dash when showing bindings 2026-10-14 12:39:49 +09:00
galister 351b24533c openvr: button to open bindings 2026-10-14 12:09:52 +09:00
galister 09ed96542f openvr: fix space drag interrupting itself 2026-10-14 11:44:36 +09:00
oo8devandgalister 9b89622ff9 Power options, context menu icons (#681)
* dash-frontend: Power options (Closes #640), Context menu: refactor: positioning

* dash-frontend: Power options

* systemctl → loginctl

Co-authored-by: galister <22305755+galister@users.noreply.github.com>

---------

Co-authored-by: galister <22305755+galister@users.noreply.github.com>
2026-10-13 14:06:47 +09:00
galister 346c6391ef translations 2026-10-11 22:14:17 +09:00
galister e850c2c8b6 toast expiry tweaks 2026-10-11 22:14:17 +09:00
galister 5360697828 frame controller trigger thresholds 2026-10-11 17:51:59 +09:00
galister 3a869e3c35 openvr: hide at startup until steamvr dashboard is closed 2026-10-11 17:51:59 +09:00
galister b3ac55474f show toast if steamvr dashboard shown at startup 2026-10-11 17:51:59 +09:00
galister a4986a3d9f toast: extend_display_time 2026-10-11 17:51:59 +09:00
galister f3f7d97ed6 toast queue, priority toasts 2026-10-11 17:51:59 +09:00
Aleksander 10857739a4 dash-frontend: make WiVRn microphone show up, add volume control support for microphones with mono channel 2026-10-10 18:19:57 +02:00
Aleksander 42e9deb54c wgui: Layout: Fix content_size regression (Closes #679) 2026-10-09 09:29:20 +02:00
Aleksander d6515b3c2a dash-frontend: Tweak home screen buttons, minor logic fix 2026-10-08 22:44:23 +02:00
Aleksander a101c79892 wgui: Fix context menus leaving renderable content bounadry (Closes #668) 2026-10-08 22:25:33 +02:00
galister 82565e975f prevent alpha from going NaN 2026-10-05 14:15:00 +09:00
Aleksander 959f66b271 graphics: change dmabuf log level 2026-10-04 18:45:18 +02:00
oo8dev 745728d90d dash-frontend: diagnostics: list gpus and capabilities (vram info) (#674) 2026-10-03 13:14:58 +02:00
hypevhs 68944184de Embed update information within AppImages (#672)
Let linuxdeploy embed Update Information into each AppImage, allowing
for external apps (Gear Lever, Shelly, AppImageUpdate) to easily
download WayVR updates.

This also requires that a corresponding .zsync file be attached to each
release, for efficient transfer of updated AppImages.

See this section in the AppImage spec:

https://github.com/AppImage/AppImageSpec/blob/master/draft.md#update-information
2026-10-03 11:45:41 +09:00
galister 4590371a4c openvr: add generic_hmd to actions 2026-09-30 12:49:13 +09:00
galister d1255e5f71 openvr: fix eye tracking 2026-09-30 12:47:46 +09:00
galister 537f2f5397 openvr: fix missing toggle dash bind 2026-09-30 12:27:12 +09:00
galister 77730432b1 openvr: frame bindings 2026-09-30 12:22:43 +09:00
galister fd3e9069e4 openvr eye tracking 2026-09-30 06:19:07 +09:00
galister 2226d95834 update ovr_overlay 2026-09-30 05:35:09 +09:00
galister a0d36d7257 openvr: handsfree pointer 2026-09-30 05:13:24 +09:00
galister 38382275ec Revert "headless appimages for frame"
This reverts commit 0a158c102c.
2026-09-30 04:34:42 +09:00
galister 5517c3a809 focus picker to override controller input 2026-09-30 04:09:02 +09:00
galister f0d96802ba wvr_server: fix resize on windows using wl_viewporter 2026-09-30 00:18:19 +09:00
galister 3f43003a0b client-side decorations 2026-09-29 23:24:48 +09:00
galister 28842d6d69 wvr_server: drag and drop support (#669) 2026-09-29 18:58:51 +09:00
Dimo 573bd5da53 Ignore invisible overlays during raycast (#665) 2026-09-29 18:42:13 +09:00
galister da3b19a495 sets_on_watch by default 2026-09-29 13:34:50 +09:00
galister ad0d1aa8a2 fmt 2026-09-29 11:58:54 +09:00
galister b85976bdf0 toasts stay while you look! 2026-09-29 11:58:26 +09:00
galister 2ea97e88bb bump key repeat delay 2026-09-29 03:07:19 +09:00
galister b7b93131c9 [skip-ci] Update readme 2026-09-29 02:56:09 +09:00
galister e7c07c7789 update readme 2026-09-29 02:05:13 +09:00
galister 0a158c102c headless appimages for frame 2026-09-29 02:02:53 +09:00
galister 11a5c8305b rework WGfx to work with ash instead of vulkano 2026-09-29 01:50:42 +09:00
galister d5f0306aab Revert "check vkGetMemoryFdPropertiesKHR on dmabuf import, allow only SAMPLED drm formats"
This reverts commit 6d6e7033ac.
2026-09-29 01:50:42 +09:00
galister 6d6e7033ac check vkGetMemoryFdPropertiesKHR on dmabuf import, allow only SAMPLED drm formats 2026-09-27 21:55:14 +09:00
galister 16bc4383b5 openxr: binding chords support, update frame controller as per 2026-09-27 19:58:19 +09:00
galister 0e6526836e add option for --no-audio 2026-09-27 16:37:19 +09:00
galister 2e8d1589f5 oxr bindings: scroll is actually Vec2; tweak frame_controller 2026-09-26 22:29:24 +09:00
galister 54713b95f4 lax precision on IPD logs 2026-09-26 14:37:32 +09:00
galister 6f761a99dc log available extensions 2026-09-26 14:37:21 +09:00
galister c87bcee358 build aarch64 appimage 2026-09-25 14:59:50 +09:00
oo8dev 0eab390702 dash-frontend: Diagnostics tab (#661)
* dash-frontend: diagnostics

* wgui: support booleans, Button: `adjust_children` tag

* dash-frontend: diagnostics: add tooltips
2026-09-20 11:32:17 +02:00
Aleksander 9dcdf852c7 dash-frontend: audio settings: bugfix: panic after trying to open a non-existent asset path in the card profile selector 2026-09-19 18:51:43 +02:00
galister b3261dbe4f fmt 2026-09-19 12:10:42 +09:00
galister 329a89ca08 setting: Middle-click modifier grabs individual overlays 2026-09-19 11:54:07 +09:00
galister c284162068 fmt 2026-09-18 01:37:22 +09:00
galister f2beabd39a XR_MNDX_composition_layer_alpha_blend_system_ui 2026-09-18 01:37:12 +09:00
galister 126120e52e fmt 2026-09-12 16:55:10 +09:00
galister e986c3591e debug logs for input_capture 2026-09-12 16:55:10 +09:00
galister a0f096b9dc wayvrctl: add window-attrib 2026-09-12 16:55:10 +09:00
galister 7b95f01ff4 wayvrctl: rework window-set-visible 2026-09-12 16:55:10 +09:00
galister d9cc73be8b wayvrctl: rework window-list 2026-09-12 16:55:10 +09:00
galister 548ea36a84 wayvrctl windows-state: add get/set for global 2026-09-12 16:55:10 +09:00
galister d4a5d4c81f wayvrctl: window-state subcommand 2026-09-12 16:55:10 +09:00
galister 443bc01361 missing translations 2026-09-12 16:54:52 +09:00
galister 9b9d1b9f8a avoid using vulkano's approximative gamma correction 2026-09-12 13:35:09 +09:00
oo8dev 40811f219e Add various animations, UI fixes (#651)
* wgui: Animation system refactoring (seconds instead of ticks), dash-frontend: pop-up slide effects

* dash-frontend: audio_settings UI fixes

* wgui: do not swipe in non-scrollable axis, dash-frontend: skymap list xml fixes

* wayvr: notification animations, wgui: animation system refactoring
2026-09-06 16:50:39 +02:00
200 changed files with 13797 additions and 4463 deletions

No files matched your search

+17 -4
View File
@@ -1,6 +1,7 @@
name: Build AppImage
on:
workflow_dispatch:
push:
branches:
- 'main'
@@ -12,10 +13,17 @@ env:
CARGO_TERM_COLOR: always
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
jobs:
build_appimage:
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- runner: ubuntu-24.04
arch: x86_64
- runner: ubuntu-24.04-arm
arch: aarch64
runs-on: ${{ matrix.runner }}
defaults:
run:
working-directory: ./wayvr
@@ -38,5 +46,10 @@ jobs:
- name: Upload AppImage
uses: actions/upload-artifact@v4
with:
name: WayVR-${{ github.ref_name }}-x86_64.AppImage
path: ./wayvr/WayVR-x86_64.AppImage
name: WayVR-${{ github.ref_name }}-${{ matrix.arch }}.AppImage
path: ./wayvr/WayVR-${{ matrix.arch }}.AppImage
- name: Upload AppImage zsync
uses: actions/upload-artifact@v4
with:
name: WayVR-${{ github.ref_name }}-${{ matrix.arch }}.AppImage.zsync
path: ./wayvr/WayVR-${{ matrix.arch }}.AppImage.zsync
+10
View File
@@ -83,6 +83,16 @@ jobs:
asset_name: WayVR-${{ github.ref_name }}-x86_64.AppImage
asset_content_type: application/octet-stream
- name: Upload AppImage zsync
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_KEY }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./wayvr/WayVR-x86_64.AppImage.zsync
asset_name: WayVR-${{ github.ref_name }}-x86_64.AppImage.zsync
asset_content_type: application/octet-stream
- name: Upload vendor tarball
uses: actions/upload-release-asset@v1
env:
@@ -1,6 +1,7 @@
#!/bin/sh
set -eu
sudo apt install \
sudo apt-get install -y \
xwayland \
libxcb1-dev \
libxcb-cursor-dev \
@@ -11,7 +12,7 @@ git clone https://github.com/Supreeeme/xwayland-satellite.git
cd xwayland-satellite
cargo build --release
chmod +x ../target/release/xwayland-satellite
chmod +x target/release/xwayland-satellite
cd ..
cp xwayland-satellite/target/release/xwayland-satellite ${APPDIR}/usr/bin
cp xwayland-satellite/target/release/xwayland-satellite "${APPDIR}/usr/bin"
@@ -1,4 +1,6 @@
#!/bin/sh
set -eu
cargo build --release
chmod +x ../target/release/wayvr
cp ../target/release/wayvr ${APPDIR}/usr/bin
cp ../target/release/wayvr "${APPDIR}/usr/bin"
+34 -3
View File
@@ -1,4 +1,35 @@
#!/bin/sh
export VERSION=$GITHUB_REF_NAME
./linuxdeploy-x86_64.AppImage -dwayvr.desktop -iwayvr.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
mv WayVR-$VERSION-x86_64.AppImage WayVR-x86_64.AppImage
set -eu
case "$(uname -m)" in
x86_64|amd64)
APPIMAGE_ARCH=x86_64
;;
aarch64|arm64)
APPIMAGE_ARCH=aarch64
;;
*)
echo "Unsupported architecture: $(uname -m)" >&2
exit 1
;;
esac
LINUXDEPLOY="linuxdeploy-${APPIMAGE_ARCH}.AppImage"
VERSION="${GITHUB_REF_NAME:-dev}"
export VERSION
export LDAI_UPDATE_INFORMATION="gh-releases-zsync|wayvr-org|wayvr|latest|WayVR-*-${APPIMAGE_ARCH}.AppImage.zsync"
echo "Packaging AppImage for ${APPIMAGE_ARCH}"
# linuxdeploy doesn't like multiple -d's being passed
install -Dm 0644 wayvr-headless.desktop "${APPDIR}/usr/share/applications/wayvr-headless.desktop"
"./${LINUXDEPLOY}" \
-dwayvr.desktop \
-iwayvr.png \
--appdir="${APPDIR}" \
--output appimage \
--exclude-library '*libpipewire*'
mv "WayVR-${VERSION}-${APPIMAGE_ARCH}.AppImage" "WayVR-${APPIMAGE_ARCH}.AppImage"
mv "WayVR-${VERSION}-${APPIMAGE_ARCH}.AppImage.zsync" "WayVR-${APPIMAGE_ARCH}.AppImage.zsync"
@@ -1,17 +1,39 @@
#!/bin/sh
set -eu
case "$(uname -m)" in
x86_64|amd64)
APPIMAGE_ARCH=x86_64
;;
aarch64|arm64)
APPIMAGE_ARCH=aarch64
;;
*)
echo "Unsupported architecture: $(uname -m)" >&2
exit 1
;;
esac
LINUXDEPLOY="linuxdeploy-${APPIMAGE_ARCH}.AppImage"
echo "Preparing AppImage environment for ${APPIMAGE_ARCH}"
sudo apt-get update
sudo apt-get install -y fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-dev libxkbcommon-x11-0 libxkbcommon-x11-dev \
libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 \
libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1 libwayland-dev libegl-dev libxcb-glx0 libxcb-glx0-dev \
libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1t64 libwayland-dev libegl-dev libxcb-glx0 libxcb-glx0-dev \
dav1d libdav1d-dev libvulkan-dev glslc vulkan-tools libudev-dev libudev1 libinput-dev libinput10
rustup update
if [ "$APPDIR" != "" ]; then
test -f linuxdeploy-x86_64.AppImage || wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
chmod +x linuxdeploy-x86_64.AppImage
if [ -n "${APPDIR:-}" ]; then
if [ ! -f "$LINUXDEPLOY" ]; then
wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/${LINUXDEPLOY}"
fi
chmod +x "$LINUXDEPLOY"
test -d ${APPDIR} && rm -rf ${APPDIR}
mkdir -p ${APPDIR}/usr/bin
if [ -d "$APPDIR" ]; then
rm -rf "$APPDIR"
fi
mkdir -p "$APPDIR/usr/bin"
fi
+46
View File
@@ -0,0 +1,46 @@
# Contributing
Contributions are warmly welcome. Before you start, please read the entire document carefully.
## AI Policy
Do not use LLMs to generate substantial pieces of code. You need to fully understand every line of code you change. No exceptions.
Commits, issues and pull requests must be made by humans. Co-authoring by AI is not allowed.
Looking to create an issue on the project, such as a feature request or bug report? Focus on writing what you know for sure in simple terms. We'll figure it out from there.
Usage of Gen-AI for visuals and audio is prohibited.
The only exception is LLM-translated strings generated by the scripts/translator tool.
## Basics
Before committing, make sure to format your Rust code by running:
```
cargo fmt
```
You should also check for any compiler warnings too:
```
cargo check
```
or even better:
```
cargo clippy
```
## Large contributions
If you want to submit a relatively big pull requests, it's highly advisable to create a separate issue request for it. That way we can discuss the approach and sort out the details first.
## Adding translations
Look at `scripts/translator/run.sh` for more information.
## Licensing
WayVR is developed and published under [GPL-3 License](https://www.gnu.org/licenses/gpl-3.0.en.html).
Generated
+135 -144
View File
@@ -689,6 +689,12 @@ dependencies = [
"wyz",
]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -729,6 +735,17 @@ dependencies = [
"piper",
]
[[package]]
name = "bstr"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
dependencies = [
"memchr",
"regex-automata",
"serde_core",
]
[[package]]
name = "built"
version = "0.8.1"
@@ -1250,15 +1267,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -1410,6 +1418,7 @@ dependencies = [
"rust-embed",
"serde",
"serde_json",
"shell-words",
"smol",
"strum",
"ureq",
@@ -1735,7 +1744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -1930,6 +1939,21 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "freedesktop-desktop-entry"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a69ad6111a2b572cb41e0f3542d546386e841013cee7c45d705b61a15a87dd"
dependencies = [
"bstr",
"gettext-rs",
"log",
"memchr",
"thiserror 2.0.18",
"unicase",
"xdg 3.0.0",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
@@ -2109,6 +2133,26 @@ dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "gettext-rs"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df5e3fa8c077b15fdfa8ce130e2bc73d663b8aaa6d51b71421b8659980b1d80"
dependencies = [
"gettext-sys",
"locale_config",
]
[[package]]
name = "gettext-sys"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4caa7e4460b196107cc1bb2629488e8b6dbb7f34f6683ea6753c4ac22f590785"
dependencies = [
"cc",
"temp-dir",
]
[[package]]
name = "gif"
version = "0.14.2"
@@ -2878,6 +2922,19 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "locale_config"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934"
dependencies = [
"lazy_static",
"objc",
"objc-foundation",
"regex",
"winapi",
]
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -2939,6 +2996,15 @@ dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "matchers"
version = "0.2.0"
@@ -3149,7 +3215,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -3230,6 +3296,26 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -3268,7 +3354,7 @@ dependencies = [
"objc2-core-data",
"objc2-core-image",
"objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2",
"objc2-quartz-core",
]
[[package]]
@@ -3377,7 +3463,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal 0.2.2",
"objc2-metal",
]
[[package]]
@@ -3458,17 +3544,6 @@ dependencies = [
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-metal"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
dependencies = [
"bitflags 2.13.0",
"objc2 0.6.4",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-open-directory"
version = "0.3.2"
@@ -3490,20 +3565,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal 0.2.2",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [
"bitflags 2.13.0",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"objc2-metal 0.3.2",
"objc2-metal",
]
[[package]]
@@ -3531,7 +3593,7 @@ dependencies = [
"objc2-core-location",
"objc2-foundation 0.2.2",
"objc2-link-presentation",
"objc2-quartz-core 0.2.2",
"objc2-quartz-core",
"objc2-symbols",
"objc2-uniform-type-identifiers",
"objc2-user-notifications",
@@ -3561,6 +3623,15 @@ dependencies = [
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.37.3"
@@ -3651,7 +3722,7 @@ dependencies = [
[[package]]
name = "ovr_overlay"
version = "0.0.0"
source = "git+https://github.com/galister/ovr_overlay_oyasumi?rev=e477bd2a9e04293ea68c1e7529ef2cb131f32acc#e477bd2a9e04293ea68c1e7529ef2cb131f32acc"
source = "git+https://github.com/galister/ovr_overlay_oyasumi?rev=3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd#3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd"
dependencies = [
"byteorder",
"derive_more",
@@ -3666,7 +3737,7 @@ dependencies = [
[[package]]
name = "ovr_overlay_sys"
version = "0.0.0"
source = "git+https://github.com/galister/ovr_overlay_oyasumi?rev=e477bd2a9e04293ea68c1e7529ef2cb131f32acc#e477bd2a9e04293ea68c1e7529ef2cb131f32acc"
source = "git+https://github.com/galister/ovr_overlay_oyasumi?rev=3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd#3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd"
dependencies = [
"autocxx",
"autocxx-build",
@@ -4054,7 +4125,7 @@ version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
dependencies = [
"heck 0.5.0",
"heck 0.4.1",
"itertools 0.14.0",
"log",
"multimap",
@@ -4357,18 +4428,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "raw-window-metal"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135"
dependencies = [
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.2",
]
[[package]]
name = "rayon"
version = "1.12.0"
@@ -4667,7 +4726,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -4714,8 +4773,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rxscreen"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbab13f83503a8272b7150d487494992cf6c59c299dcaf5d8ffac75bad9fc96"
source = "git+https://github.com/galister/rxscreen.git?rev=7197e1de6622b65859e6f2060b5750185e0dad2a#7197e1de6622b65859e6f2060b5750185e0dad2a"
dependencies = [
"libc",
]
@@ -5026,12 +5084,6 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "slabbin"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9db491c0d4152a069911a0fbdaca959691bf0b9d7110d98a7ed1c8e59b79ab30"
[[package]]
name = "slice-of-array"
version = "0.3.2"
@@ -5394,6 +5446,12 @@ version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "temp-dir"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964"
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -5401,10 +5459,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -5739,13 +5797,17 @@ dependencies = [
"rust-embed",
"tracing-subscriber",
"ureq",
"vulkano",
"vulkano-shaders",
"wgui",
"winit",
"wlx-common",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
@@ -5905,70 +5967,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vk-parse"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3859da4d7b98bec73e68fb65815d47a263819c415c90eed42b80440a02cbce8c"
dependencies = [
"xml-rs",
]
[[package]]
name = "vulkano"
version = "0.35.2"
source = "git+https://github.com/galister/vulkano.git?rev=cf7f92867928a56ce16b376037c1120f2b167678#cf7f92867928a56ce16b376037c1120f2b167678"
dependencies = [
"ash",
"bytemuck",
"crossbeam-queue",
"foldhash 0.1.5",
"half",
"heck 0.4.1",
"indexmap 2.14.0",
"libloading 0.8.9",
"nom 7.1.3",
"once_cell",
"parking_lot",
"proc-macro2",
"quote",
"raw-window-handle",
"raw-window-metal",
"serde",
"serde_json",
"slabbin",
"smallvec",
"thread_local",
"vk-parse",
"vulkano-macros",
]
[[package]]
name = "vulkano-macros"
version = "0.35.0"
source = "git+https://github.com/galister/vulkano.git?rev=cf7f92867928a56ce16b376037c1120f2b167678#cf7f92867928a56ce16b376037c1120f2b167678"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "vulkano-shaders"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf501461be7cef2893c0e62c50945add9763cc482051d29053f6157089d5ea9"
dependencies = [
"foldhash 0.1.5",
"heck 0.4.1",
"proc-macro2",
"quote",
"shaderc",
"syn 2.0.118",
"vulkano",
]
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -6211,7 +6209,6 @@ name = "wayvr"
version = "26.8.0"
dependencies = [
"anyhow",
"ash",
"bytes",
"calloop 0.14.4",
"calloop-wayland-source 0.4.1",
@@ -6249,6 +6246,7 @@ dependencies = [
"serde_json",
"serde_json5",
"serde_yaml",
"shaderc",
"signal-hook",
"slotmap",
"smallvec",
@@ -6261,8 +6259,6 @@ dependencies = [
"tracing",
"tracing-subscriber",
"uuid",
"vulkano",
"vulkano-shaders",
"wayland-client",
"wayland-protocols",
"wayland-protocols-misc",
@@ -6349,6 +6345,7 @@ name = "wgui"
version = "0.1.0"
dependencies = [
"anyhow",
"ash",
"bytes",
"cosmic-text",
"dav1d-sys",
@@ -6360,18 +6357,18 @@ dependencies = [
"lru",
"ouroboros",
"parking_lot",
"raw-window-handle",
"regex",
"resvg",
"roxmltree 0.21.1",
"rust-embed",
"rustc-hash",
"serde_json",
"shaderc",
"slotmap",
"smallvec",
"strum",
"taffy",
"vulkano",
"vulkano-shaders",
]
[[package]]
@@ -6425,7 +6422,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]
@@ -6857,6 +6854,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"freedesktop-desktop-entry",
"glam",
"glob",
"identicons-svg",
@@ -6864,7 +6862,6 @@ dependencies = [
"idmap-derive",
"log",
"rodio",
"rust-ini",
"serde",
"serde_json",
"serde_json5",
@@ -6982,12 +6979,6 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "xml-rs"
version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
[[package]]
name = "xmlparser"
version = "0.13.6"
+1 -14
View File
@@ -32,32 +32,19 @@ rust-embed = "8.9.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.145"
serde_json5 = "0.2.1"
shell-words = "1.1.1"
slotmap = "1.1.1"
smallvec = "1.15.2"
smol = "2.0.2"
strum = { version = "0.27.2", features = ["derive"] }
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
uuid = { version = "1.19.0", features = ["fast-rng", "serde", "v4"] }
vulkano = {
version = "0.35.2",
default-features = false,
features = [
"macros",
]
}
vulkano-shaders = "0.35.0"
wayland-client = { version = "0.31" }
wayland-protocols = { version = "0.32" }
wayland-protocols-misc = { version = "0.3.12" }
winit = "0.30.12"
xdg = "3.0.0"
[patch.crates-io]
vulkano = {
git = "https://github.com/galister/vulkano.git",
rev = "cf7f92867928a56ce16b376037c1120f2b167678"
}
[profile.dev]
opt-level = 1
debug = true
+15 -1
View File
@@ -37,7 +37,7 @@ There are multiple ways to install WayVR:
**Note:** If you are using Monado or WiVRn, no additional setup steps are required for Flatpak Steam compatibility—most people use WayVR seamlessly with Monado/WiVRn.
### SteamVR via Steam Flatpak
### SteamVR (PC) via Steam Flatpak
For users specifically running **SteamVR via Steam Flatpak**, follow these steps:
@@ -50,6 +50,20 @@ For users specifically running **SteamVR via Steam Flatpak**, follow these steps
1. Start SteamVR.
1. `flatpak run --command='/path/to/squashfs-root/AppRun' com.valvesoftware.Steam`
### Installing on Steam Frame
Steam Frame support is experimental. Please report issues.
It's possible to install and run WayVR on the Steam Frame in headless mode. You will not have access to your KDE desktop, but you will be able to run applications in 3D space around you.
Steps to install on Steam Frame:
1. Grab the latest aarch64 AppImage
- For nightly AppImages go [here](https://github.com/wayvr-org/wayvr/actions/workflows/build-appimage.yml) and select the topmost list item a ✅️
- The downloads will be on the bottom of the page (GitHub login required).
1. Install the AppImage using GearLever
1. (Recommended) In Frame's SteamVR settings, set overlay quality to High.
1. Start WayVR from the taskbar (same place where you'd start Destkop)
## First Start
**When the screen share pop-up appears, check your notifications or the terminal and select the screens in the order it requests.**
+1
View File
@@ -19,6 +19,7 @@ log.workspace = true
rust-embed.workspace = true
serde = { workspace = true, features = ["rc"] }
serde_json.workspace = true
shell-words.workspace = true
smol = { workspace = true }
strum.workspace = true
uuid.workspace = true
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 13.4l-4.9 4.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7l4.9-4.9l-4.9-4.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.9 4.9l4.9-4.9q.275-.275.7-.275t.7.275t.275.7t-.275.7L13.4 12l4.9 4.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275z"/></svg>

After

Width:  |  Height:  |  Size: 468 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M9 22q-.825 0-1.412-.587T7 20v-3H4q-.825 0-1.412-.587T2 15v-2h6.45l1.7 2.55q.125.2.35.325t.475.125q.325 0 .6-.2t.375-.5l1.35-4.05l.85 1.3q.15.2.375.325T15 13h7v2q0 .825-.588 1.413T20 17h-3v3q0 .825-.587 1.413T15 22zm1.7-9.25l-.875-1.3q-.125-.2-.35-.325T9 11H2V9q0-.825.588-1.412T4 7h3V4q0-.825.588-1.412T9 2h6q.825 0 1.413.588T17 4v3h3q.825 0 1.413.588T22 9v2h-6.475l-1.7-2.55q-.125-.2-.35-.325T13 8q-.325 0-.587.2t-.363.5z"/></svg>

After

Width:  |  Height:  |  Size: 660 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs1" />
<path fill="currentColor" d="m 10,12 c 0,-1.104569 0.895431,-2 2,-2 1.104569,0 2,0.895431 2,2 0,1.104569 -0.895431,2 -2,2 -1.104569,0 -2,-0.895431 -2,-2" id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 422 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M2 17h18a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H2m0 16V3m5 14v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-3"/><circle cx="16" cy="11" r="2"/><circle cx="8" cy="11" r="2"/></g></svg>

After

Width:  |  Height:  |  Size: 462 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M17 10q-.825 0-1.412-.575T15 8V4q0-.85.588-1.425T17 2q.85 0 1.425.575T19 4v4q0 .85-.575 1.425T17 10M5 22q-.825 0-1.412-.587T3 20V4q0-.825.588-1.412T5 2h8v2H5v16h11v-2h2v2q0 .825-.587 1.413T16 22zm2-4v-2h7v2zm0-3v-2h5v2zm11 1h-2v-2.6q-1.925-.35-3.213-1.862T11.5 8h2q0 1.45 1.025 2.475T17 11.5q1.475 0 2.488-1.025T20.5 8h2q0 2.025-1.275 3.538T18 13.4z"/></svg>

After

Width:  |  Height:  |  Size: 585 B

+13
View File
@@ -6,6 +6,16 @@
<var key="side_button_size" value="48" />
</vars>
<blueprint name="context_menu_power_options">
<context_menu>
<cell translation="APP_SETTINGS.RESTART_WAYVR" action="restart" sprite_src_builtin="@/dashboard/refresh.svg"/>
<cell translation="APP_SETTINGS.CLOSE_WAYVR" action="close" sprite_src_builtin="@/dashboard/close.svg"/>
<separator/>
<cell translation="POWER_OPTIONS.SUSPEND" action="suspend" sprite_src_builtin="@/dashboard/sleep.svg"/>
<cell translation="POWER_OPTIONS.SHUTDOWN" action="shutdown" sprite_src_builtin="@/dashboard/power.svg"/>
</context_menu>
</blueprint>
<template name="SideButton">
<Button
id="${id}"
@@ -136,6 +146,9 @@
<!-- Left bottom side -->
<div margin_left="8">
<Button id="btn_power" color="background(transparent)" border_color="background(transparent)" tooltip="POWER_OPTIONS.TITLE" tooltip_side="top">
<sprite src_builtin="@/dashboard/power.svg" width="24" height="24" margin="8" />
</Button>
<Button id="btn_audio" color="background(transparent)" border_color="background(transparent)" tooltip="AUDIO.VOLUME" tooltip_side="top">
<sprite src_builtin="@/dashboard/volume.svg" width="24" height="24" margin="8" />
</Button>
+18 -11
View File
@@ -1,6 +1,21 @@
<layout version="2">
<include src="@/gui/t_menu_button.xml" />
<template name="MiscButton">
<Button
id="${id}" macro="home_button"
min_width="64" height="36"
padding_left="8" padding_right="8">
<div gap="8"
align_items="center"
justify_content="center"
flex_direction="row">
<sprite src_builtin="${icon}" width="24" height="24" />
<label weight="bold" size="18" text="${text}" translation="${translation}" />
</div>
</Button>
</template>
<elements>
<div
flex_direction="column"
@@ -33,17 +48,9 @@
</div>
<div gap="8">
<Button id="btn_welcome_screen" macro="home_button" tooltip="APP_SETTINGS.SHOW_WELCOME_SCREEN">
<sprite src_builtin="@/dashboard/welcome.svg" width="32" height="32" margin="16" />
</Button>
<Button id="btn_website" macro="home_button" tooltip_str="wayvr.org">
<sprite src_builtin="@/dashboard/globe.svg" width="32" height="32" margin="16" />
</Button>
<Button id="btn_donate" macro="home_button" tooltip="DONATE.SUPPORT_US">
<sprite src_builtin="@/dashboard/opencollective.svg" width="32" height="32" margin="16" />
</Button>
<MiscButton id="btn_welcome_screen" icon="@/dashboard/welcome.svg" translation="APP_SETTINGS.TUTORIAL"/>
<MiscButton id="btn_website" icon="@/dashboard/globe.svg" text="wayvr.org"/>
<MiscButton id="btn_donate" icon="@/dashboard/opencollective.svg" translation="DONATE.SUPPORT_US"/>
</div>
</div>
</elements>
+22
View File
@@ -31,6 +31,27 @@
<RadioBox text="${text}" translation="${translation}" value="${value}" tooltip="${tooltip}" />
</template>
<!-- title, sprite, color, tooltip_str -->
<template name="DiagnosticsRow">
<div align_items="center">
<label text="${title}" weight="bold" min_width="140"/>
<Button tooltip_str="${tooltip_str}" color="background(transparent)">
<sprite width="24" height="24" color="${color}" src_builtin="${sprite}" parent_color="ignore"/>
</Button>
</div>
</template>
<!-- name, info -->
<template name="DiagnosticsDrmCard">
<GroupBox>
<div gap="8">
<sprite width="24" height="24" src_builtin="@/dashboard/gpu.svg"/>
<label text="${name}"/>
</div>
<label text="${info}"/>
</GroupBox>
</template>
<template name="StatRow">
<div flex_direction="row" justify_content="space_between" align_items="center" gap="12" width="100%">
<label weight="bold" translation="${translation}" />
@@ -77,6 +98,7 @@
<Tab macro="tab_style" name="misc" translation="APP_SETTINGS.MISC" sprite_src_builtin="@/dashboard/blocks.svg" />
<Tab macro="tab_style" name="autostart_apps" translation="APP_SETTINGS.AUTOSTART_APPS" sprite_src_builtin="@/dashboard/apps.svg" />
<Tab macro="tab_style" name="statistics" translation="APP_SETTINGS.STATISTICS" sprite_src_builtin="@/dashboard/barchart.svg" />
<Tab macro="tab_style" name="diagnostics" translation="APP_SETTINGS.DIAGNOSTICS.TITLE" sprite_src_builtin="@/dashboard/diagnostics.svg" />
<Tab macro="tab_style" name="troubleshooting" translation="APP_SETTINGS.TROUBLESHOOTING" sprite_src_builtin="@/dashboard/cpu.svg" />
</Tabs>
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" width="100%">
@@ -5,13 +5,13 @@
<template name="DeviceSlider">
<GroupBox id="rect">
<div width="100%" align_items="center" justify_content="center" gap="8">
<sprite src_builtin="${device_icon}" width="16" height="16" color="on_background" />
<sprite src_builtin="${device_icon}" width="24" height="24" color="on_background" />
<label text="${device_name}" margin_right="8" size="12" weight="bold" color="on_background" />
</div>
<div width="100%" align_items="center">
<CheckBox id="checkbox" />
<Button sprite_src_builtin="${volume_icon}" id="btn_mute" width="32" />
<Slider id="slider" flex_grow="1" height="16" min_value="0" max_value="150" margin_left="8" />
<CheckBox id="checkbox" tooltip="AUDIO.SWITCH_TO_THIS_DEVICE" />
<Slider id="slider" flex_grow="1" height="16" min_value="0" max_value="150" margin_left="8" margin_right="8" />
<Button sprite_src_builtin="${volume_icon}" id="btn_mute" width="32" height="32" />
</div>
</GroupBox>
</template>
+4 -1
View File
@@ -1,6 +1,9 @@
<layout version="2">
<template name="DialogBoxButton">
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"/>
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"
text_size="16"
padding_left="16" padding_right="16"
padding_top="8" padding_bottom="8"/>
</template>
<elements>
@@ -37,7 +37,7 @@
-->
<template name="ResolutionPip">
<rectangle color="${color}" padding_left="4" padding_right="4" padding_top="2" padding_bottom="2" round="3" align_self="start">
<label text="${text}" weight="bold" size="12" color="#FFFFFF" shadow="#000000" shadow_x="2" shadow_y="2"/>
<label text="${text}" weight="bold" size="12" color="#FFFFFF" shadow="#000000" shadow_x="2" shadow_y="2" parent_color="ignore"/>
</rectangle>
</template>
</layout>
+38 -7
View File
@@ -97,7 +97,10 @@
"RIGHT": "Rechts",
"COMPONENT": "Aktivierungsart",
"SUBPATH": "Bedienelement",
"THRESHOLD": "Aktivieren bei Überschreiten des oberen Wertes\nDeaktivieren bei Unterschreiten des unteren Wertes"
"THRESHOLD": "Aktivieren bei Überschreiten des oberen Wertes\nDeaktivieren bei Unterschreiten des unteren Wertes",
"CHORD": "Chord",
"CHORD_ADD_INPUT": "Chord-Input hinzufügen",
"CHORD_INPUT_HAND": "Eingabehand"
},
"BROWSE_ONLINE_CATALOG": "Online-Katalog durchsuchen...",
"BROWSE_SKYMAPS": "Skymaps durchsuchen",
@@ -194,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Nutze aktuellen Playspace für die Playspace-Reset-Belegung.",
"RESET_PLAYSPACE_CENTER": "Playspace-Zentrum zurücksetzen",
"RESET_PLAYSPACE_CENTER_HELP": "Vorheriges Playspace-Zentrum zurücksetzen.",
"RESTART_SOFTWARE": "Software neu starten",
"RESTART_SOFTWARE_HELP": "Einstellungen übernehmen, die einen Neustart erfordern",
"ROUND_MULTIPLIER": "Abrundung der UI-Kanten",
"SCREEN_RENDER_DOWN": "Bildschirm mit niedrigerer Auflösung rendern",
"SCREEN_RENDER_DOWN_HELP": "Hilft gegen Treppeneffekte bei hochauflösenden Bildschirmen",
@@ -205,7 +206,6 @@
"SNAP_ANGLE_DEG_HELP": "Gegriffene Fenster rasten auf der Nick- und Rollachse in diesem Winkel ein.",
"ENABLE_WATCH": "Handgelenk-Uhr aktivieren",
"SETS_ON_WATCH": "Sets auf Handgelenk-Uhr",
"SHOW_WELCOME_SCREEN": "Willkommensbildschirm anzeigen",
"SKYBOX": "Skymap",
"SKYMAP_ALREADY_DOWNLOADED": "Diese Skymap ist bereits heruntergeladen. Wähle eine Aktion aus.",
"SPACE_DRAG": "Playspace",
@@ -247,7 +247,25 @@
"STATISTICS": "Statistiken",
"ROTATIONS": "HMD-Rotationen",
"SESSION_TIME": "Sitzungszeit",
"LENS_SEPARATION_MM": "Linsenabstand (mm)"
"LENS_SEPARATION_MM": "Linsenabstand (mm)",
"MIDDLE_GRABS_SINGLE": "Mittelklick-Modifier greift einzeln",
"MIDDLE_GRABS_SINGLE_HELP": "Greifen mit dem Mittelklick-Modifier (lila Laser) \ngreift einzelne Overlays statt des gesamten Sets",
"DIAGNOSTICS_REASON": {
"PACTL": "Erforderlich für Lautsprecher-/Mikrofonsteuerung und das Wechseln der Soundkarte",
"CAGE": "Empfohlen bei Inkompatibilitäten mit einigen X11-Anwendungen (Kompatibilitätsmodus)",
"XWAYLAND_SATELLITE": "Empfohlen, wenn X11 (Xwayland) Anwendungen im nativen Modus ausgeführt werden sollen",
"XDG_OPEN": "Erforderlich zum Öffnen von URLs oder Steam-Spielen"
},
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "CPU-zugänglicher VRAM: {NUM} MiB",
"TITLE": "Diagnose",
"ENABLE_REBAR": "Aktiviere resizable bar in den BIOS-Einstellungen für optimale VR-Performance"
},
"TUTORIAL": "Tutorial",
"RESTART_WAYVR": "WayVR neu starten",
"CLOSE_WAYVR": "WayVR schließen",
"SPEECH_TO_TEXT": "Sprache zu Text",
"WHISPER_GPU_SELECT": "GPU auswählen"
},
"APPLICATION_LAUNCHER": "Programmstarter",
"APPLICATION_STARTED": "Anwendung gestartet",
@@ -264,7 +282,8 @@
"SETTINGS": "Audio-Einstellungen",
"SPEAKERS": "Lautsprecher",
"SPEAKERS_SET_SUCCESSFULLY": "Lautsprecher erfolgreich eingestellt",
"VOLUME": "Lautstärke"
"VOLUME": "Lautstärke",
"SWITCH_TO_THIS_DEVICE": "Zu diesem Gerät wechseln"
},
"CLOSE_WINDOW": "Fenster schließen",
"CREATION_DATE": "Erstellungsdatum",
@@ -349,5 +368,17 @@
"ADD_TO_FAVOURITES": "Zu angehefteten Anwendungen hinzufügen",
"ADD_TO_FAVOURITES_HINT": "Voreinstellung speichern und an die App-Schublade anheften",
"REMOVE_FROM_FAVOURITES": "Aus angehefteten Anwendungen entfernen",
"EMPTY": "Leer"
"EMPTY": "Leer",
"INSTALLED": "Installiert",
"NOT_INSTALLED": "Nicht installiert",
"POWER_OPTIONS": {
"TITLE": "Energieoptionen",
"SHUTDOWN": "Herunterfahren",
"SUSPEND": "Ruhezustand",
"SHUTDOWN_ARE_YOU_SURE": "Bist du sicher, dass du den Computer ausschalten willst?",
"NOT_SUPPORTED": "Energieoptionen erfordern systemd oder elogind.",
"SUSPEND_ARE_YOU_SURE": "Bist du sicher, dass du den Computer in den Ruhezustand versetzen willst?"
},
"YES": "Ja",
"NO": "Nein"
}
+29 -3
View File
@@ -74,6 +74,9 @@ APP_SETTINGS.BINDINGS.ACTION.MOVE_MOUSE,Move mouse (if off by default),Label for
APP_SETTINGS.BINDINGS.ACTION.SCROLL,Scroll*,Label for the scroll action row. Maps controller input to scrolling. Leave the '*' at the end.
APP_SETTINGS.BINDINGS.LEFT,Left,Label in the bindings editor. Refers to the left controller/hand configuration.
APP_SETTINGS.BINDINGS.RIGHT,Right,Label in the bindings editor. Refers to the right controller/hand configuration.
APP_SETTINGS.BINDINGS.CHORD,Chord,Label for a group of controller inputs that must all be active together. As in Input Chord
APP_SETTINGS.BINDINGS.CHORD_ADD_INPUT,Add chord input,"Button in the controller bindings editor that adds another physical input to a chord. ""Chord"" was previously translated as ""${APP_SETTINGS.BINDINGS.CHORD}""."
APP_SETTINGS.BINDINGS.CHORD_INPUT_HAND,Input hand,"Selector for the hand to which we will be binding the input action to."
APP_SETTINGS.BINDINGS.COMPONENT,Actuation type,"Tooltip for the component dropdown in the bindings editor. Asks user to select what type of actuation (click, touch, axis, etc.)."
APP_SETTINGS.BINDINGS.SUBPATH,Actuating control,Tooltip for the subpath dropdown in the bindings editor. Asks user to select which controller button/stick/trackpad to bind.
APP_SETTINGS.BINDINGS.THRESHOLD,"Activate when above upper value
@@ -119,6 +122,13 @@ controllers are unavailable.
Left pinch is grab, right is click.",Tooltip for the handsfree mode dropdown.
APP_SETTINGS.HANDSFREE_ALT_TAB,App switcher aim,Dropdown/setting in Settings > Features tab. Controls how the alt-tab app switcher is aimed.
APP_SETTINGS.HANDSFREE_ALT_TAB_HELP,"Hold Super+X, aim at the desired window, then release to switch focus.",Tooltip explaining the handsfree alt-tab behavior.
APP_SETTINGS.DIAGNOSTICS.CPU_ACCESSIBLE_VRAM,"CPU-accessible VRAM: {NUM} MiB","Amount of VRAM accessible by the CPU. For example: 256/4096"
APP_SETTINGS.DIAGNOSTICS.TITLE,"Diagnostics","Tab name for the hardware/software check panel"
APP_SETTINGS.DIAGNOSTICS.ENABLE_REBAR,"Enable Resizable BAR in the BIOS settings for optimal VR performance","Info shown if resizable bar is disabled in the BIOS. Leave [resizable bar] translation as-is."
APP_SETTINGS.DIAGNOSTICS_REASON.PACTL,"Required for speaker/microphone control and audio card switching","Reason for why the given software is recommended"
APP_SETTINGS.DIAGNOSTICS_REASON.CAGE,"Recommended in case if there's an incompatibility with some X11 applications (Compatibility mode)","Reason for why the given software is required or recommended"
APP_SETTINGS.DIAGNOSTICS_REASON.XWAYLAND_SATELLITE,"Recommended in case if you want to run X11 (Xwayland) applications in native mode","Reason for why the given software is required or recommended"
APP_SETTINGS.DIAGNOSTICS_REASON.XDG_OPEN,"Required for opening URLs or Steam games","Reason for why the given software is required or recommended"
APP_SETTINGS.HIDE_GRAB_HELP,Hide grab help,Checkbox label in Settings > Look & Feel tab. Hides the grab help overlay/text.
APP_SETTINGS.HIDE_USERNAME,Hide username,Checkbox label in Settings > Look & Feel tab. Hides the user's username from the UI.
APP_SETTINGS.INPUT_EMULATION_METHOD,Input emulation method,"Dropdown label in Settings > Miscellaneous tab. Selects how WayVR emulates mouse/keyboard input (uinput, Wayland virtual, none)."
@@ -143,6 +153,9 @@ APP_SETTINGS.LEFT_HANDED_MOUSE_HELP,Use this if mouse buttons are swapped,Toolti
APP_SETTINGS.LONG_PRESS_DURATION,Long press duration,Setting in Settings > Features tab. Controls how long a press must be held to register as a long press.
APP_SETTINGS.LOOK_AND_FEEL,Look & Feel,"Category header in Settings. Groups visual/customization settings like language, opacity, animation speed, color palettes."
APP_SETTINGS.MISC,Miscellaneous,"Category header in Settings. Groups troubleshooting and system-level settings like capture method, input emulation, xwayland."
APP_SETTINGS.MIDDLE_GRABS_SINGLE,"Middle-click modifier grabs individual","Checkbox label in Settings > Controls tab. When enabled, grabbing with the middle modifier will always grab individual objects, never the entire set of objects. ""Middle-click modifier"" was previously translated as ""${APP_SETTINGS.BINDINGS.ACTION.CLICK_MODIFIER_MIDDLE}"""
APP_SETTINGS.MIDDLE_GRABS_SINGLE_HELP,"Grabbing with the middle-click modifier (purple laser)
will grab individual overlays, not the entire set","Tooltip for the middle modifier grabs single overlays checkbox. ""Middle-click modifier"" was previously translated as ""${APP_SETTINGS.BINDINGS.ACTION.CLICK_MODIFIER_MIDDLE}"""
APP_SETTINGS.NOTIFICATIONS_ENABLED,Enable notifications,Checkbox label in Settings > Features tab. Enables or disables system notifications within WayVR.
APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED,Notification sounds,Checkbox label in Settings > Features tab. Enables or disables sound effects for notifications.
APP_SETTINGS.NOT_SUPPORTED,Not supported,Label shown in the space drag/physics settings when a feature (like gravity) is not supported by the current backend.
@@ -195,8 +208,8 @@ APP_SETTINGS.SAVE_PLAYSPACE_CENTER,Save playspace center,"Button in Settings > S
APP_SETTINGS.SAVE_PLAYSPACE_CENTER_HELP,Use current playspace for reset playspace binding.,"Tooltip for the Save playspace center button. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
APP_SETTINGS.RESET_PLAYSPACE_CENTER,Reset playspace center,"Button in Settings > Space Drag tab. Clears the previously saved playspace center. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
APP_SETTINGS.RESET_PLAYSPACE_CENTER_HELP,Clear previously set playspace center.,"Tooltip for the Reset playspace center button. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
APP_SETTINGS.RESTART_SOFTWARE,Restart software,Button in Settings > Troubleshooting tab. Restarts the WayVR software to apply settings that require a restart.
APP_SETTINGS.RESTART_SOFTWARE_HELP,Apply settings that require a restart,Tooltip for the Restart software button.
APP_SETTINGS.RESTART_WAYVR,Restart WayVR,Button in Settings which restarts WayVR
APP_SETTINGS.CLOSE_WAYVR,Close WayVR,Button in Settings which closes WayVR
APP_SETTINGS.ROUND_MULTIPLIER,UI Edge roundness,Slider in Settings > Look & Feel tab. Controls how rounded the corners of UI elements appear (range 0.1-5.0).
APP_SETTINGS.SCREEN_RENDER_DOWN,Render screen at lower resolution,Checkbox in Settings > Miscellaneous tab. Renders the captured desktop screen at a lower resolution to improve performance or reduce aliasing.
APP_SETTINGS.SCREEN_RENDER_DOWN_HELP,Helps with aliasing on high-res screens,Tooltip for the lower-resolution screen render setting.
@@ -206,7 +219,6 @@ APP_SETTINGS.SNAP_ANGLE_DEG,Window pitch & roll snap angle,Slider in Settings >
APP_SETTINGS.SNAP_ANGLE_DEG_HELP,Grabbed windows snap to this angle on pitch and roll axis.,Tooltip for the snap angle setting.
APP_SETTINGS.ENABLE_WATCH,Enable watch,"Checkbox in Settings > Look & Feel tab. Enables the wrist watch UI element. ""Watch"" was previously translated as ""${DEFINITIONS.WATCH}""."
APP_SETTINGS.SETS_ON_WATCH,Sets on watch,"Checkbox in Settings > Look & Feel tab. Shows overlay sets on the wrist watch display. ""Watch"" was previously translated as ""${DEFINITIONS.WATCH}"". ""Set"" was previously translated as ""${DEFINITIONS.SET}""."
APP_SETTINGS.SHOW_WELCOME_SCREEN,Show welcome screen,Checkbox in Settings > Features tab. Controls whether the welcome/tutorial screen is shown on first launch.
APP_SETTINGS.SKYBOX,Skymap,"Category header in Settings > Skymap tab. Groups skymap-related settings. ""Skymap"" was previously translated as ""${DEFINITIONS.SKYMAP}""."
APP_SETTINGS.SKYMAP_ALREADY_DOWNLOADED,This skymap is already downloaded. Select desired action.,"Message shown in a dialog when attempting to download a skymap that is already present locally. ""Skymap"" was previously translated as ""${DEFINITIONS.SKYMAP}""."
APP_SETTINGS.SPACE_DRAG,Playspace,"Category header in Settings. Groups playspace drag/physics settings including reset buttons and gravity. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
@@ -247,6 +259,9 @@ APP_SETTINGS.WHISPER_MODEL,Speech-to-text model,Dropdown label in Settings > Fea
APP_SETTINGS.WHISPER_MODEL_HELP,"Whisper model to use.
Larger models produce more accurate
results, but consume more VRAM.",Tooltip for the speech-to-text model dropdown.
APP_SETTINGS.SPEECH_TO_TEXT,"Speech to text","Category header in Settings"
APP_SETTINGS.WHISPER_GPU_SELECT,"Select GPU","A list of GPUs the user can select from"
APP_SETTINGS.TUTORIAL,Tutorial,Tooltip for tutorial button in the main screen.
APP_SETTINGS.GRID_OPACITY,Floor grid opacity,Slider in Settings > Skybox tab. Controls the opacity of the floor grid when skybox is enabled.
APP_SETTINGS.GRID_OPACITY_HELP,Opacity of the floor grid when the skymap is enabled,"Tooltip for the floor grid opacity slider. ""Skymap"" was previously translated as ""${DEFINITIONS.SKYMAP}""."
APP_SETTINGS.XWAYLAND_BY_DEFAULT,Run apps in Compatibility mode by default,Checkbox in Settings > Miscellaneous tab. Launches apps in Cage/compatibility mode by default.
@@ -255,6 +270,7 @@ APP_SETTINGS.CANCEL,Cancel,Button label in the controller bindings editor popup
APPLICATION_LAUNCHER,Application launcher,App launcher configuration popup title - appears as the label for the app launch/options dialog.
APPLICATION_STARTED,Application started,Toast notification shown after a successful app launch.
APPLICATIONS,Applications,Tab label in the dashboard sidebar for the Apps tab.
AUDIO.SWITCH_TO_THIS_DEVICE,Switch to this device,Button in the audio settings dialog
AUDIO.AUTO_SWITCH_TO_VR_AUDIO,Auto-switch to VR audio,Tooltip on the auto-switch audio button in the audio settings popup.
AUDIO.CARDS,Cards,Button label in the audio settings popup for selecting audio card profiles.
AUDIO.FAILED_TO_SWITCH_MICROPHONE,Failed to switch microphone,Error toast shown when switching to a VR microphone fails.
@@ -322,6 +338,12 @@ MODIFICATION_DATE,Modification date,Column label in the remote skymap downloader
MONADO_RUNTIME,Monado runtime,Tab label in the dashboard sidebar for the Monado runtime tab.
LOADING,Loading...,Label in the loading spinner template used across the UI.
POPUP_ADD_DISPLAY.RESOLUTION,Resolution,Label for the resolution field in the add display popup.
POWER_OPTIONS.TITLE,"Power options","A button which allows the user to perform a specific power action"
POWER_OPTIONS.SHUTDOWN,"Shut down","One of the power options: shuts down the computer"
POWER_OPTIONS.SUSPEND,"Suspend","One of the power options: suspends the computer"
POWER_OPTIONS.NOT_SUPPORTED,"Power options require systemd or elogind.","Notification to the user that power options (shutdown and suspend) are not available. 'systemd' and 'elogind' are package names and should not be translated."
POWER_OPTIONS.SHUTDOWN_ARE_YOU_SURE,"Are you sure you want to turn off your computer?","Question dialog box before shutting down the computer"
POWER_OPTIONS.SUSPEND_ARE_YOU_SURE,"Are you sure you want to put your computer to sleep?","Question dialog box before putting the computer in sleep mode"
PROCESS.FORCE_KILL,Force-kill,Tooltip on the kill button in the running games list.
PROCESS.STOP,Stop,Tooltip on the stop button in the running games list.
PROCESS_LIST,Process list,Tab name and group box title in the Monado runtime process list section.
@@ -341,3 +363,7 @@ ADD_TO_FAVOURITES,Add to pinned apps,"Label on the pin button in the app launche
ADD_TO_FAVOURITES_HINT,Save preset and pin it to the app drawer,"Tooltip on the add to favourites button in the app launcher popup. ""Pinned apps"" was previously translated as ""${PINNED_APPS}""."
REMOVE_FROM_FAVOURITES,Remove from pinned apps,"Label on the unpin section in the app launcher popup. ""Pinned apps"" was previously translated as ""${PINNED_APPS}""."
EMPTY,Empty,Label shown in the pinned apps section when there are no pinned apps.
INSTALLED,"Installed","Installation flag (installed/not installed)"
NOT_INSTALLED,"Not installed","Installation flag (installed/not installed)"
YES,"Yes","Just a simple 'Yes' message, button in the dialog box"
NO,"No","Just a simple 'No' message, button in the dialog box"
1 DEFINITIONS.ANCHORED Anchored A movement option that makes this window move together with the rest of the workspace. Emphasize the "moving together" part as opposed to "not moving at all".
74 APP_SETTINGS.BINDINGS.ACTION.SCROLL Scroll* Label for the scroll action row. Maps controller input to scrolling. Leave the '*' at the end.
75 APP_SETTINGS.BINDINGS.LEFT Left Label in the bindings editor. Refers to the left controller/hand configuration.
76 APP_SETTINGS.BINDINGS.RIGHT Right Label in the bindings editor. Refers to the right controller/hand configuration.
77 APP_SETTINGS.BINDINGS.CHORD Chord Label for a group of controller inputs that must all be active together. As in Input Chord
78 APP_SETTINGS.BINDINGS.CHORD_ADD_INPUT Add chord input Button in the controller bindings editor that adds another physical input to a chord. "Chord" was previously translated as "${APP_SETTINGS.BINDINGS.CHORD}".
79 APP_SETTINGS.BINDINGS.CHORD_INPUT_HAND Input hand Selector for the hand to which we will be binding the input action to.
80 APP_SETTINGS.BINDINGS.COMPONENT Actuation type Tooltip for the component dropdown in the bindings editor. Asks user to select what type of actuation (click, touch, axis, etc.).
81 APP_SETTINGS.BINDINGS.SUBPATH Actuating control Tooltip for the subpath dropdown in the bindings editor. Asks user to select which controller button/stick/trackpad to bind.
82 APP_SETTINGS.BINDINGS.THRESHOLD Activate when above upper value Deactivate when below lower value Tooltip for the threshold slider in the bindings editor. Explains the two-value threshold range for analog inputs.
122 APP_SETTINGS.INPUT_EMULATION_METHOD APP_SETTINGS.DIAGNOSTICS.ENABLE_REBAR Input emulation method Enable Resizable BAR in the BIOS settings for optimal VR performance Dropdown label in Settings > Miscellaneous tab. Selects how WayVR emulates mouse/keyboard input (uinput, Wayland virtual, none). Info shown if resizable bar is disabled in the BIOS. Leave [resizable bar] translation as-is.
123 APP_SETTINGS.INPUT_EMULATION_METHOD_HELP APP_SETTINGS.DIAGNOSTICS_REASON.PACTL Try change this in case mouse or keyboard input does not register. Required for speaker/microphone control and audio card switching Tooltip for the input emulation method dropdown. Reason for why the given software is recommended
124 APP_SETTINGS.INPUT_CAPTURE_METHOD APP_SETTINGS.DIAGNOSTICS_REASON.CAGE Input capture method Recommended in case if there's an incompatibility with some X11 applications (Compatibility mode) Dropdown label in Settings > Miscellaneous tab. Selects how physical input devices are captured for use inside WayVR. Reason for why the given software is required or recommended
125 APP_SETTINGS.DIAGNOSTICS_REASON.XWAYLAND_SATELLITE Recommended in case if you want to run X11 (Xwayland) applications in native mode Reason for why the given software is required or recommended
126 APP_SETTINGS.DIAGNOSTICS_REASON.XDG_OPEN Required for opening URLs or Steam games Reason for why the given software is required or recommended
127 APP_SETTINGS.HIDE_GRAB_HELP Hide grab help Checkbox label in Settings > Look & Feel tab. Hides the grab help overlay/text.
128 APP_SETTINGS.HIDE_USERNAME Hide username Checkbox label in Settings > Look & Feel tab. Hides the user's username from the UI.
129 APP_SETTINGS.INPUT_EMULATION_METHOD Input emulation method Dropdown label in Settings > Miscellaneous tab. Selects how WayVR emulates mouse/keyboard input (uinput, Wayland virtual, none).
130 APP_SETTINGS.INPUT_EMULATION_METHOD_HELP Try change this in case mouse or keyboard input does not register. Tooltip for the input emulation method dropdown.
131 APP_SETTINGS.INPUT_CAPTURE_METHOD Input capture method Dropdown label in Settings > Miscellaneous tab. Selects how physical input devices are captured for use inside WayVR.
132 APP_SETTINGS.INPUT_CAPTURE_METHOD_HELP Allows using physical peripherial inside WayVR. Super+Z: Switch input between WayVR and real desktop Super+X: Switch between WayVR apps Tooltip explaining the input capture method setting.
133 APP_SETTINGS.INPUT_PROFILES Change Input Bindings Button label in Settings > Controls tab. Opens the controller bindings editor popup.
134 APP_SETTINGS.INPUT_PROFILES_HELP OpenXR controller input bindings Tooltip for the Change Input Bindings button.
153 APP_SETTINGS.OPTION.FLOATING_HELP APP_SETTINGS.OPAQUE_BACKGROUND Moves independently, recenters. Easy to misplace; not recommended. Opaque background Tooltip for the Floating positioning option. Checkbox label in Settings > Look & Feel tab. Makes overlay backgrounds fully opaque (no transparency).
154 APP_SETTINGS.OPTION.STATIC_HELP APP_SETTINGS.OPTION.ANCHORED_HELP Stays in place relative to world. Moves with center marker. Recommended. Tooltip for the Static positioning option. Tooltip for the Anchored positioning option.
155 APP_SETTINGS.OPTION.AUTO APP_SETTINGS.OPTION.FLOATING_HELP Automatic Moves independently, recenters. Easy to misplace; not recommended. Dropdown option for screen capture method. Uses ScreenCopy GPU if supported, otherwise PipeWire GPU. Tooltip for the Floating positioning option.
156 APP_SETTINGS.OPTION.STATIC_HELP Stays in place relative to world. Tooltip for the Static positioning option.
157 APP_SETTINGS.OPTION.AUTO Automatic Dropdown option for screen capture method. Uses ScreenCopy GPU if supported, otherwise PipeWire GPU.
158 APP_SETTINGS.OPTION.AUTO_HELP ScreenCopy GPU if supported, otherwise PipeWire GPU. Tooltip for the Automatic capture method option.
159 APP_SETTINGS.OPTION.AUTO_HELP APP_SETTINGS.OPTION.EYE_ONLY ScreenCopy GPU if supported, otherwise PipeWire GPU. Eye laser Tooltip for the Automatic capture method option. Dropdown option for handsfree pointer mode. Uses only eye-tracking laser (no pinch).
160 APP_SETTINGS.OPTION.EYE_ONLY APP_SETTINGS.OPTION.EYE_PINCH Eye laser Eye + pinch Dropdown option for handsfree pointer mode. Uses only eye-tracking laser (no pinch). Dropdown option for handsfree pointer mode. Uses eye-tracking for aiming and pinch for click.
161 APP_SETTINGS.OPTION.EYE_PINCH APP_SETTINGS.OPTION.HMD_ONLY Eye + pinch HMD laser Dropdown option for handsfree pointer mode. Uses eye-tracking for aiming and pinch for click. Dropdown option for handsfree pointer/alt-tab mode. Uses only the HMD forward laser.
208 APP_SETTINGS.SPACE_GRAVITY_FLING_STRENGTH_HELP APP_SETTINGS.SPACE_GRAVITY_GRAVITY_HELP Intensity multiplier of gravitational launch force after space-drag. 0.0 - no movement at all, 2.0 - double intensity Amount of downwards force. 0.0 - no gravity Tooltip for the fling strength setting. "Space" in this context refers to "Playspace" which was translated as "${DEFINITIONS.PLAYSPACE}". Tooltip for the gravity setting.
209 APP_SETTINGS.SPACE_GRAVITY_GRAVITY APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION Gravity Ground friction Setting in the space gravity physics panel. Amount of downward force (0.0 = no gravity). Setting in the space gravity physics panel. Friction when touching the ground (0.0 = ice-like, 1.0 = rough).
210 APP_SETTINGS.SPACE_GRAVITY_GRAVITY_HELP APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION_HELP Amount of downwards force. 0.0 - no gravity Amount of friction slowing you down if you're touching the ground. 0.0 - no friction (just like on ice), 1.0 - rough surface Tooltip for the gravity setting. Tooltip for the ground friction setting.
211 APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT Ground friction Floor height Setting in the space gravity physics panel. Friction when touching the ground (0.0 = ice-like, 1.0 = rough). Setting in the space gravity physics panel. The Y position where the virtual floor is.
212 APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION_HELP APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT_HELP Amount of friction slowing you down if you're touching the ground. 0.0 - no friction (just like on ice), 1.0 - rough surface The Y position where the floor is. Gravity stops when you reach this height. Tooltip for the ground friction setting. Tooltip for the floor height setting.
213 APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT APP_SETTINGS.SPACE_ROTATE_UNLOCKED Floor height Allow space rotate on all axes Setting in the space gravity physics panel. The Y position where the virtual floor is. Checkbox in Settings > Space Drag tab. Unlocks space rotate to work on all axes. "Space" in this context refers to "Playspace" which was translated as "${DEFINITIONS.PLAYSPACE}".
214 APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT_HELP APP_SETTINGS.ROTATIONS The Y position where the floor is. Gravity stops when you reach this height. HMD rotations Tooltip for the floor height setting. Number of full 360-degree rotations of the HMD since startup. Shown in Settings > Statistics.
215 APP_SETTINGS.SPACE_ROTATE_UNLOCKED APP_SETTINGS.SESSION_TIME Allow space rotate on all axes Session time Checkbox in Settings > Space Drag tab. Unlocks space rotate to work on all axes. "Space" in this context refers to "Playspace" which was translated as "${DEFINITIONS.PLAYSPACE}". Time since WayVR started. Shown in Settings > Statistics.
219 APP_SETTINGS.TROUBLESHOOTING APP_SETTINGS.UPRIGHT_SCREEN_FIX Troubleshooting Upright screen fix Category header in Settings. Groups diagnostic/reset actions like clearing tokens, wiping config, and restarting. Checkbox in Settings > Miscellaneous tab. Fixes upright screen orientation on some desktop environments.
220 APP_SETTINGS.UI_GRADIENT_INTENSITY APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP UI Gradient intensity Fixes upright screens on some desktops Slider in Settings > Look & Feel tab. Controls the intensity of gradient effects on the UI (range 0.0-1.0). Tooltip for the upright screen fix checkbox.
221 APP_SETTINGS.UPRIGHT_SCREEN_FIX APP_SETTINGS.USE_PASSTHROUGH Upright screen fix Enable passthrough Checkbox in Settings > Miscellaneous tab. Fixes upright screen orientation on some desktop environments. Checkbox in Settings > Skybox tab. Allows camera passthrough mode if the XR runtime supports it.
APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP Fixes upright screens on some desktops Tooltip for the upright screen fix checkbox.
222 APP_SETTINGS.USE_PASSTHROUGH APP_SETTINGS.USE_PASSTHROUGH_HELP Enable passthrough Allow passthrough if the XR runtime supports it Checkbox in Settings > Skybox tab. Allows camera passthrough mode if the XR runtime supports it. Tooltip for the passthrough checkbox.
223 APP_SETTINGS.USE_PASSTHROUGH_HELP APP_SETTINGS.USE_SKYBOX Allow passthrough if the XR runtime supports it Show skymap Tooltip for the passthrough checkbox. Checkbox in Settings > Skybox tab. Shows a skybox when there's no scene app or passthrough active. "Skymap" was previously translated as "${DEFINITIONS.SKYMAP}".
224 APP_SETTINGS.USE_SKYBOX APP_SETTINGS.USE_SKYBOX_HELP Show skymap Show a skymap if there's no scene app or passthrough Checkbox in Settings > Skybox tab. Shows a skybox when there's no scene app or passthrough active. "Skymap" was previously translated as "${DEFINITIONS.SKYMAP}". Tooltip for the enable skybox checkbox. "Skymap" was previously translated as "${DEFINITIONS.SKYMAP}".
259 DONATE.BUTTON_TOOLTIP DONATE.SUPPORT_US Donate via Open Collective Support us Tooltip on the donate button on the donate tab. Button tooltip on the home screen donate button.
260 DONATE.CURRENT_SUPPORTERS DONATE.DESCRIPTION Current Supporters We're a small team of 2 developers building WayVR in our free time. If you enjoy using this software, please consider supporting us - every donation helps us keep going. Section header on the donate tab listing supporters. Description text on the donate tab explaining the project.
261 DONATE.BROUGHT_TO_YOU_VARIANTS_OFTEN DONATE.THANK_YOU_WAYVR Supported by {USER}|Special thanks to {USER}|Sponsored by {USER}|Backed by {USER}|Driven by community members like {USER}|Made possible by {USER}|Sustained by {USER}|Contributed by {USER}|Funded by {USER} Thank you from the WayVR Team! This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it. Thank you message displayed on the donate tab.
262 DONATE.BUTTON_TOOLTIP Donate via Open Collective Tooltip on the donate button on the donate tab.
263 DONATE.CURRENT_SUPPORTERS Current Supporters Section header on the donate tab listing supporters.
264 DONATE.BROUGHT_TO_YOU_VARIANTS_OFTEN Supported by {USER}|Special thanks to {USER}|Sponsored by {USER}|Backed by {USER}|Driven by community members like {USER}|Made possible by {USER}|Sustained by {USER}|Contributed by {USER}|Funded by {USER} This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it.
265 DONATE.BROUGHT_TO_YOU_VARIANTS_RARE Coffee provided by {USER}|Fuelled by {USER}|Shoutout to {USER}!|Keeping us going: {USER}|Helping us grow: {USER}|Keeping the servers running: {USER}|With gratitude to {USER}|Powered by {USER}|Supported by friends like {USER} This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it (rare versions).
266 DONATE.BROUGHT_TO_YOU_VARIANTS_VERY_RARE Supported by legends like {USER}|Caffeine supplied by {USER}|A true legend: {USER}|Our hero of the day: {USER}|An absolute superstar: {USER}|Paying for the electricity bill: {USER}|The savior of our sanity: {USER}|Leading the way: {USER}|Caffeine-to-code converter provided by {USER}|Keeping the production stable: {USER}|The one and only: {USER}|Passing dmabuf buffers: {USER} This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it (rare versions).
267 DOWNLOAD Download Button in the Whisper model download dialog to confirm downloading a model.
270 WELCOME.BASICS These basics will allow you to start using WayVR with ease. Description text on the final welcome page under the You are all set heading.
271 WELCOME.CENTER_MARKER Center marker is what will appear in front of you when recentering. Place overlays around it! Description of the center marker on the moving your set tutorial page. "Overlay" was previously translated as "${DEFINITIONS.OVERLAY}".
272 WELCOME.DOUBLE_PRESS_TO_TOGGLE Double-press B (on Index) or Y (on Meta) to show, recenter or hide your set. Try it now. Instruction text on the working set tutorial page explaining the double-press gesture. "Set" was previously translated as "${DEFINITIONS.SET}".
273 WELCOME.FEATURES_DOCUMENTATION There are many other features worth customizing. For more information, check the documentation at Text on the final welcome page linking to documentation.
274 WELCOME.FEATURES_DOCUMENTATION WELCOME.FINISH_TUTORIAL There are many other features worth customizing. For more information, check the documentation at Finish tutorial Text on the final welcome page linking to documentation. Button text on the final welcome page to complete the tutorial.
275 WELCOME.FINISH_TUTORIAL WELCOME.GRAB_BOTH_HANDS Finish tutorial Grab it with both hands if you want to move a specific overlay in your set. Button text on the final welcome page to complete the tutorial. Instruction text on the moving your set tutorial page for two-hand grab.
276 WELCOME.GRAB_BOTH_HANDS WELCOME.GRAB_ONE_HAND Grab it with both hands if you want to move a specific overlay in your set. Grab it with one hand to move your set. Instruction text on the moving your set tutorial page for two-hand grab. Instruction text on the moving your set tutorial page for one-hand grab.
338 NO No Just a simple 'No' message, button in the dialog box
339
340
341
342
343
344
345
346
347
348
349
363
364
365
366
367
368
369
+35 -4
View File
@@ -95,6 +95,9 @@
},
"LEFT": "Left",
"RIGHT": "Right",
"CHORD": "Chord",
"CHORD_ADD_INPUT": "Add chord input",
"CHORD_INPUT_HAND": "Input hand",
"COMPONENT": "Actuation type",
"SUBPATH": "Actuating control",
"THRESHOLD": "Activate when above upper value\nDeactivate when below lower value"
@@ -136,6 +139,17 @@
"HANDSFREE_POINTER_HELP": "Input to use when motion\ncontrollers are unavailable.\nLeft pinch is grab, right is click.",
"HANDSFREE_ALT_TAB": "App switcher aim",
"HANDSFREE_ALT_TAB_HELP": "Hold Super+X, aim at the desired window, then release to switch focus.",
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "CPU-accessible VRAM: {NUM} MiB",
"TITLE": "Diagnostics",
"ENABLE_REBAR": "Enable Resizable BAR in the BIOS settings for optimal VR performance"
},
"DIAGNOSTICS_REASON": {
"PACTL": "Required for speaker/microphone control and audio card switching",
"CAGE": "Recommended in case if there's an incompatibility with some X11 applications (Compatibility mode)",
"XWAYLAND_SATELLITE": "Recommended in case if you want to run X11 (Xwayland) applications in native mode",
"XDG_OPEN": "Required for opening URLs or Steam games"
},
"HIDE_GRAB_HELP": "Hide grab help",
"HIDE_USERNAME": "Hide username",
"INPUT_EMULATION_METHOD": "Input emulation method",
@@ -156,6 +170,8 @@
"LONG_PRESS_DURATION": "Long press duration",
"LOOK_AND_FEEL": "Look & Feel",
"MISC": "Miscellaneous",
"MIDDLE_GRABS_SINGLE": "Middle-click modifier grabs individual",
"MIDDLE_GRABS_SINGLE_HELP": "Grabbing with the middle-click modifier (purple laser)\nwill grab individual overlays, not the entire set",
"NOTIFICATIONS_ENABLED": "Enable notifications",
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
"NOT_SUPPORTED": "Not supported",
@@ -195,8 +211,8 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Use current playspace for reset playspace binding.",
"RESET_PLAYSPACE_CENTER": "Reset playspace center",
"RESET_PLAYSPACE_CENTER_HELP": "Clear previously set playspace center.",
"RESTART_SOFTWARE": "Restart software",
"RESTART_SOFTWARE_HELP": "Apply settings that require a restart",
"RESTART_WAYVR": "Restart WayVR",
"CLOSE_WAYVR": "Close WayVR",
"ROUND_MULTIPLIER": "UI Edge roundness",
"SCREEN_RENDER_DOWN": "Render screen at lower resolution",
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
@@ -206,7 +222,6 @@
"SNAP_ANGLE_DEG_HELP": "Grabbed windows snap to this angle on pitch and roll axis.",
"ENABLE_WATCH": "Enable watch",
"SETS_ON_WATCH": "Sets on watch",
"SHOW_WELCOME_SCREEN": "Show welcome screen",
"SKYBOX": "Skymap",
"SKYMAP_ALREADY_DOWNLOADED": "This skymap is already downloaded. Select desired action.",
"SPACE_DRAG": "Playspace",
@@ -243,6 +258,9 @@
},
"WHISPER_MODEL": "Speech-to-text model",
"WHISPER_MODEL_HELP": "Whisper model to use.\nLarger models produce more accurate\nresults, but consume more VRAM.",
"SPEECH_TO_TEXT": "Speech to text",
"WHISPER_GPU_SELECT": "Select GPU",
"TUTORIAL": "Tutorial",
"GRID_OPACITY": "Floor grid opacity",
"GRID_OPACITY_HELP": "Opacity of the floor grid when the skymap is enabled",
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default",
@@ -253,6 +271,7 @@
"APPLICATION_STARTED": "Application started",
"APPLICATIONS": "Applications",
"AUDIO": {
"SWITCH_TO_THIS_DEVICE": "Switch to this device",
"AUTO_SWITCH_TO_VR_AUDIO": "Auto-switch to VR audio",
"CARDS": "Cards",
"FAILED_TO_SWITCH_MICROPHONE": "Failed to switch microphone",
@@ -329,6 +348,14 @@
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"
},
"POWER_OPTIONS": {
"TITLE": "Power options",
"SHUTDOWN": "Shut down",
"SUSPEND": "Suspend",
"NOT_SUPPORTED": "Power options require systemd or elogind.",
"SHUTDOWN_ARE_YOU_SURE": "Are you sure you want to turn off your computer?",
"SUSPEND_ARE_YOU_SURE": "Are you sure you want to put your computer to sleep?"
},
"PROCESS": {
"FORCE_KILL": "Force-kill",
"STOP": "Stop"
@@ -349,5 +376,9 @@
"ADD_TO_FAVOURITES": "Add to pinned apps",
"ADD_TO_FAVOURITES_HINT": "Save preset and pin it to the app drawer",
"REMOVE_FROM_FAVOURITES": "Remove from pinned apps",
"EMPTY": "Empty"
"EMPTY": "Empty",
"INSTALLED": "Installed",
"NOT_INSTALLED": "Not installed",
"YES": "Yes",
"NO": "No"
}
+38 -7
View File
@@ -97,7 +97,10 @@
"RIGHT": "Derecha",
"COMPONENT": "Tipo de accionamiento",
"SUBPATH": "Control a accionar",
"THRESHOLD": "Activar al superar el valor superior\nDesactivar al caer bajo el valor inferior"
"THRESHOLD": "Activar al superar el valor superior\nDesactivar al caer bajo el valor inferior",
"CHORD": "Acorde",
"CHORD_ADD_INPUT": "Añadir entrada al acorde",
"CHORD_INPUT_HAND": "Mano de entrada"
},
"BROWSE_ONLINE_CATALOG": "Explorar catálogo en línea...",
"BROWSE_SKYMAPS": "Explorar skymaps",
@@ -194,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Usa el espacio de juego actual para el comando de reiniciar espacio de juego.",
"RESET_PLAYSPACE_CENTER": "Reiniciar centro del espacio de juego",
"RESET_PLAYSPACE_CENTER_HELP": "Restablece el centro del espacio de juego definido previamente.",
"RESTART_SOFTWARE": "Reiniciar software",
"RESTART_SOFTWARE_HELP": "Aplicar ajustes que requieren reiniciar",
"ROUND_MULTIPLIER": "Redondeo de bordes de la UI",
"SCREEN_RENDER_DOWN": "Renderizar pantalla a resolución menor",
"SCREEN_RENDER_DOWN_HELP": "Ayuda con el aliasing en pantallas de alta resolución",
@@ -205,7 +206,6 @@
"SNAP_ANGLE_DEG_HELP": "Las ventanas agarradas se ajustan a este ángulo en los ejes de cabeceo y balanceo.",
"ENABLE_WATCH": "Habilitar reloj",
"SETS_ON_WATCH": "Sets en el reloj",
"SHOW_WELCOME_SCREEN": "Mostrar pantalla de bienvenida",
"SKYBOX": "Skymap",
"SKYMAP_ALREADY_DOWNLOADED": "Este skymap ya está descargado. Selecciona la acción deseada.",
"SPACE_DRAG": "Espacio de juego",
@@ -247,7 +247,25 @@
"STATISTICS": "Estadísticas",
"ROTATIONS": "Rotaciones del HMD",
"SESSION_TIME": "Tiempo de sesión",
"LENS_SEPARATION_MM": "Separación de lentes (mm)"
"LENS_SEPARATION_MM": "Separación de lentes (mm)",
"MIDDLE_GRABS_SINGLE": "El modificador de clic central agarra objetos individuales",
"MIDDLE_GRABS_SINGLE_HELP": "Al agarrar con el modificador de clic central (láser púrpura),\nse agarrarán overlays individuales y no el set completo",
"DIAGNOSTICS_REASON": {
"PACTL": "Necesario para el control de altavoces/micrófono y el cambio de tarjeta de audio",
"CAGE": "Recomendado en caso de incompatibilidad con algunas aplicaciones X11 (Modo de compatibilidad)",
"XWAYLAND_SATELLITE": "Recomendado si quieres ejecutar aplicaciones X11 (Xwayland) en modo nativo",
"XDG_OPEN": "Necesario para abrir URLs o juegos de Steam"
},
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "VRAM accesible por CPU: {NUM} MiB",
"TITLE": "Diagnósticos",
"ENABLE_REBAR": "Activa [resizable bar] en la configuración de la BIOS para un rendimiento óptimo de VR"
},
"TUTORIAL": "Tutorial",
"RESTART_WAYVR": "Reiniciar WayVR",
"CLOSE_WAYVR": "Cerrar WayVR",
"SPEECH_TO_TEXT": "Dictado por voz",
"WHISPER_GPU_SELECT": "Seleccionar GPU"
},
"APPLICATION_LAUNCHER": "Lanzador de aplicaciones",
"APPLICATION_STARTED": "Aplicación iniciada",
@@ -264,7 +282,8 @@
"SETTINGS": "Ajustes de audio",
"SPEAKERS": "Altavoces",
"SPEAKERS_SET_SUCCESSFULLY": "Altavoces configurados correctamente",
"VOLUME": "Volumen"
"VOLUME": "Volumen",
"SWITCH_TO_THIS_DEVICE": "Cambiar a este dispositivo"
},
"CLOSE_WINDOW": "Cerrar ventana",
"CREATION_DATE": "Fecha de creación",
@@ -349,5 +368,17 @@
"ADD_TO_FAVOURITES": "Añadir a aplicaciones fijadas",
"ADD_TO_FAVOURITES_HINT": "Guardar ajuste y fijarlo al cajón de aplicaciones",
"REMOVE_FROM_FAVOURITES": "Quitar de aplicaciones fijadas",
"EMPTY": "Vacío"
"EMPTY": "Vacío",
"INSTALLED": "Instalado",
"NOT_INSTALLED": "No instalado",
"POWER_OPTIONS": {
"TITLE": "Opciones de energía",
"SHUTDOWN": "Apagar",
"SUSPEND": "Suspender",
"SHUTDOWN_ARE_YOU_SURE": "¿Seguro que quieres apagar el ordenador?",
"NOT_SUPPORTED": "Las opciones de energía requieren systemd o elogind.",
"SUSPEND_ARE_YOU_SURE": "¿Estás seguro de que quieres suspender el equipo?"
},
"YES": "Sí",
"NO": "No"
}
+38 -7
View File
@@ -97,7 +97,10 @@
"RIGHT": "Destro",
"COMPONENT": "Tipo di attuazione",
"SUBPATH": "Comando di attivazione",
"THRESHOLD": "Attiva sopra il valore superiore\nDisattiva sotto il valore inferiore"
"THRESHOLD": "Attiva sopra il valore superiore\nDisattiva sotto il valore inferiore",
"CHORD": "Combinazione",
"CHORD_ADD_INPUT": "Aggiungi input combinazione",
"CHORD_INPUT_HAND": "Mano di input"
},
"BROWSE_ONLINE_CATALOG": "Sfoglia catalogo online...",
"BROWSE_SKYMAPS": "Sfoglia skymap",
@@ -194,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Usa il playspace corrente per il comando di reset del playspace.",
"RESET_PLAYSPACE_CENTER": "Resetta centro playspace",
"RESET_PLAYSPACE_CENTER_HELP": "Rimuovi il centro del playspace impostato precedentemente.",
"RESTART_SOFTWARE": "Riavvia software",
"RESTART_SOFTWARE_HELP": "Applica le impostazioni che richiedono il riavvio",
"ROUND_MULTIPLIER": "Arrotondamento bordi UI",
"SCREEN_RENDER_DOWN": "Rendering schermo a risoluzione inferiore",
"SCREEN_RENDER_DOWN_HELP": "Riduce l'effetto aliasing sugli schermi ad alta risoluzione",
@@ -205,7 +206,6 @@
"SNAP_ANGLE_DEG_HELP": "Le finestre afferrate si allineano a questo angolo sugli assi di beccheggio e rollio.",
"ENABLE_WATCH": "Attiva orologio",
"SETS_ON_WATCH": "Set sull'orologio",
"SHOW_WELCOME_SCREEN": "Mostra schermata di benvenuto",
"SKYBOX": "Skymap",
"SKYMAP_ALREADY_DOWNLOADED": "Questo skymap è già stato scaricato. Seleziona l'azione desiderata.",
"SPACE_DRAG": "Playspace",
@@ -247,7 +247,25 @@
"STATISTICS": "Statistiche",
"ROTATIONS": "Rotazioni HMD",
"SESSION_TIME": "Tempo sessione",
"LENS_SEPARATION_MM": "Distanza lenti (mm)"
"LENS_SEPARATION_MM": "Distanza lenti (mm)",
"MIDDLE_GRABS_SINGLE": "Il modificatore click centrale afferra singoli oggetti",
"MIDDLE_GRABS_SINGLE_HELP": "Afferrando con il modificatore click centrale (laser viola),\nsi afferrano i singoli overlay e non l'intero set",
"DIAGNOSTICS_REASON": {
"PACTL": "Necessario per il controllo di altoparlanti/microfono e il cambio della scheda audio",
"CAGE": "Consigliato in caso di incompatibilità con alcune applicazioni X11 (Modalità compatibilità)",
"XWAYLAND_SATELLITE": "Consigliato per eseguire applicazioni X11 (Xwayland) in modalità nativa",
"XDG_OPEN": "Necessario per aprire URL o giochi Steam"
},
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "VRAM accessibile dalla CPU: {NUM} MiB",
"TITLE": "Diagnostica",
"ENABLE_REBAR": "Abilita Resizable BAR nelle impostazioni del BIOS per prestazioni VR ottimali"
},
"TUTORIAL": "Tutorial",
"RESTART_WAYVR": "Riavvia WayVR",
"CLOSE_WAYVR": "Chiudi WayVR",
"SPEECH_TO_TEXT": "Speech-to-text",
"WHISPER_GPU_SELECT": "Seleziona GPU"
},
"APPLICATION_LAUNCHER": "Avvio applicazioni",
"APPLICATION_STARTED": "Applicazione avviata",
@@ -264,7 +282,8 @@
"SETTINGS": "Impostazioni audio",
"SPEAKERS": "Altoparlanti",
"SPEAKERS_SET_SUCCESSFULLY": "Altoparlanti impostati correttamente",
"VOLUME": "Volume"
"VOLUME": "Volume",
"SWITCH_TO_THIS_DEVICE": "Passa a questo dispositivo"
},
"CLOSE_WINDOW": "Chiudi finestra",
"CREATION_DATE": "Data di creazione",
@@ -349,5 +368,17 @@
"ADD_TO_FAVOURITES": "Aggiungi alle applicazioni fissate",
"ADD_TO_FAVOURITES_HINT": "Salva il preset e fissalo al cassetto delle app",
"REMOVE_FROM_FAVOURITES": "Rimuovi dalle app fissate",
"EMPTY": "Vuoto"
"EMPTY": "Vuoto",
"INSTALLED": "Installato",
"NOT_INSTALLED": "Non installato",
"POWER_OPTIONS": {
"TITLE": "Opzioni di alimentazione",
"SHUTDOWN": "Spegni",
"SUSPEND": "Sospendi",
"SHUTDOWN_ARE_YOU_SURE": "Sei sicuro di voler spegnere il computer?",
"NOT_SUPPORTED": "Le opzioni di alimentazione richiedono systemd o elogind.",
"SUSPEND_ARE_YOU_SURE": "Sei sicuro di voler mettere il computer in sospensione?"
},
"YES": "Sì",
"NO": "No"
}
+39 -8
View File
@@ -94,10 +94,13 @@
"SCROLL": "スクロール*"
},
"LEFT": "左手",
"RIGHT": "右",
"RIGHT": "右手",
"COMPONENT": "入力形式",
"SUBPATH": "操作ボタン",
"THRESHOLD": "上の値を超えると有効\n下の値を下回ると無効"
"THRESHOLD": "上の値を超えると有効\n下の値を下回ると無効",
"CHORD": "コード",
"CHORD_ADD_INPUT": "コード入力の追加",
"CHORD_INPUT_HAND": "入力側の手"
},
"BROWSE_ONLINE_CATALOG": "オンラインカタログを見る...",
"BROWSE_SKYMAPS": "スカイマップを選択",
@@ -194,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "現在のプレイスペースを、プレイスペースリセットのバインディングに使用する。",
"RESET_PLAYSPACE_CENTER": "プレイスペースの中心をリセット",
"RESET_PLAYSPACE_CENTER_HELP": "設定済みのプレイスペースの中心点をリセットする。",
"RESTART_SOFTWARE": "ソフトウェアを再起動",
"RESTART_SOFTWARE_HELP": "再起動が必要な設定を適用する",
"ROUND_MULTIPLIER": "UIの角の丸み",
"SCREEN_RENDER_DOWN": "低解像度で画面をレンダリング",
"SCREEN_RENDER_DOWN_HELP": "高解像度ディスプレイでのジャギーを軽減する",
@@ -205,7 +206,6 @@
"SNAP_ANGLE_DEG_HELP": "掴んでいるウィンドウが、ピッチ軸とロール軸でこの角度にスナップします。",
"ENABLE_WATCH": "ウォッチを有効にする",
"SETS_ON_WATCH": "ウォッチにセットを表示",
"SHOW_WELCOME_SCREEN": "ウェルカムスクリーンを表示",
"SKYBOX": "スカイマップ",
"SKYMAP_ALREADY_DOWNLOADED": "このスカイマップはすでにダウンロード済みです。操作を選択してください。",
"SPACE_DRAG": "プレイスペース",
@@ -247,7 +247,25 @@
"STATISTICS": "統計",
"ROTATIONS": "HMD回転数",
"SESSION_TIME": "セッション時間",
"LENS_SEPARATION_MM": "レンズ間隔 (mm)"
"LENS_SEPARATION_MM": "レンズ間隔 (mm)",
"MIDDLE_GRABS_SINGLE": "中クリック修飾キーで個別に掴む",
"MIDDLE_GRABS_SINGLE_HELP": "中クリック修飾キー(紫色のレーザー)で掴むと、\nセット全体ではなく個別のオーバーレイを掴むことができる",
"DIAGNOSTICS_REASON": {
"PACTL": "スピーカー・マイクの制御およびオーディオカードの切り替えに必要",
"CAGE": "一部のX11アプリで互換性問題が発生する場合に推奨(互換モード)",
"XWAYLAND_SATELLITE": "X11 (Xwayland) アプリケーションをネイティブモードで動作させたい場合に推奨",
"XDG_OPEN": "URLやSteamゲームの起動に必要"
},
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "CPUアクセス可能VRAM: {NUM} MiB",
"TITLE": "診断",
"ENABLE_REBAR": "VRパフォーマンスを最適化するため、BIOS設定でresizable BARを有効にして下さい"
},
"TUTORIAL": "チュートリアル",
"RESTART_WAYVR": "WayVRを再起動",
"CLOSE_WAYVR": "WayVRを閉じる",
"SPEECH_TO_TEXT": "音声入力",
"WHISPER_GPU_SELECT": "GPUを選択"
},
"APPLICATION_LAUNCHER": "アプリランチャー",
"APPLICATION_STARTED": "アプリが起動しました",
@@ -264,7 +282,8 @@
"SETTINGS": "オーディオ設定",
"SPEAKERS": "スピーカー",
"SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定完了",
"VOLUME": "音量"
"VOLUME": "音量",
"SWITCH_TO_THIS_DEVICE": "このデバイスに切り替え"
},
"CLOSE_WINDOW": "ウィンドウを閉じる",
"CREATION_DATE": "作成日",
@@ -349,5 +368,17 @@
"ADD_TO_FAVOURITES": "固定済みアプリに追加",
"ADD_TO_FAVOURITES_HINT": "プリセットを保存し、アプリドロワーに固定する",
"REMOVE_FROM_FAVOURITES": "固定済みアプリから外す",
"EMPTY": "なし"
"EMPTY": "なし",
"INSTALLED": "インストール済み",
"NOT_INSTALLED": "未インストール",
"POWER_OPTIONS": {
"TITLE": "電源オプション",
"SHUTDOWN": "シャットダウン",
"SUSPEND": "サスペンド",
"SHUTDOWN_ARE_YOU_SURE": "コンピューターをシャットダウンしてもよろしいですか?",
"NOT_SUPPORTED": "電源オプションには systemd または elogind が必要です。",
"SUSPEND_ARE_YOU_SURE": "コンピューターをスリープ状態にしますか?"
},
"YES": "はい",
"NO": "いいえ"
}
+38 -7
View File
@@ -97,7 +97,10 @@
"RIGHT": "Prawy",
"COMPONENT": "Typ aktywacji",
"SUBPATH": "Aktywujący sterownik",
"THRESHOLD": "Aktywuj powyżej górnej wartości\nDeaktywuj poniżej dolnej wartości"
"THRESHOLD": "Aktywuj powyżej górnej wartości\nDeaktywuj poniżej dolnej wartości",
"CHORD": "Akord",
"CHORD_ADD_INPUT": "Dodaj wejście do akordu",
"CHORD_INPUT_HAND": "Ręka sterująca"
},
"BROWSE_ONLINE_CATALOG": "Przeglądaj katalog online...",
"BROWSE_SKYMAPS": "Przeglądaj skymapy",
@@ -194,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Użyj aktualnej przestrzeni gry dla przypisania resetowania przestrzeni.",
"RESET_PLAYSPACE_CENTER": "Zresetuj centrum przestrzeni gry",
"RESET_PLAYSPACE_CENTER_HELP": "Resetuj aktualny środek przestrzeni gry.",
"RESTART_SOFTWARE": "Uruchom ponownie program",
"RESTART_SOFTWARE_HELP": "Zastosuj ustawienia wymagające restartu",
"ROUND_MULTIPLIER": "Zaokrąglenie krawędzi UI",
"SCREEN_RENDER_DOWN": "Renderuj ekran w niższej rozdzielczości",
"SCREEN_RENDER_DOWN_HELP": "Pomaga zredukować aliasing na ekranach o wysokiej rozdzielczości",
@@ -205,7 +206,6 @@
"SNAP_ANGLE_DEG_HELP": "Chwycone okna przyciągają się do tego kąta na osi pochylenia i obrotu.",
"ENABLE_WATCH": "Włącz zegarek",
"SETS_ON_WATCH": "Zestawy na zegarku",
"SHOW_WELCOME_SCREEN": "Pokaż ekran powitalny",
"SKYBOX": "Skymap",
"SKYMAP_ALREADY_DOWNLOADED": "Ta skymapa jest już pobrana. Wybierz żądaną akcję.",
"SPACE_DRAG": "Przestrzeń gry",
@@ -247,7 +247,25 @@
"STATISTICS": "Statystyki",
"ROTATIONS": "Obroty HMD",
"SESSION_TIME": "Czas sesji",
"LENS_SEPARATION_MM": "Rozstaw soczewek (mm)"
"LENS_SEPARATION_MM": "Rozstaw soczewek (mm)",
"MIDDLE_GRABS_SINGLE": "Modyfikator środkowego przycisku chwyta pojedyncze obiekty",
"MIDDLE_GRABS_SINGLE_HELP": "Chwytanie z modyfikatorem środkowego przycisku myszy (fioletowy laser)\npochwyci pojedyncze nakładki, a nie cały zestaw",
"DIAGNOSTICS_REASON": {
"PACTL": "Wymagane do sterowania głośnikami/mikrofonem i przełączania kart dźwiękowych",
"CAGE": "Zalecane w przypadku niekompatybilności z niektórymi aplikacjami X11 (tryb kompatybilności)",
"XWAYLAND_SATELLITE": "Zalecane, jeśli chcesz uruchamiać aplikacje X11 (Xwayland) w trybie natywnym",
"XDG_OPEN": "Wymagane do otwierania linków URL lub gier Steam"
},
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "VRAM dostępny dla CPU: {NUM} MiB",
"TITLE": "Diagnostyka",
"ENABLE_REBAR": "Włącz resizable BAR w ustawieniach BIOS, aby uzyskać optymalną wydajność VR"
},
"TUTORIAL": "Samouczek",
"RESTART_WAYVR": "Zrestartuj WayVR",
"CLOSE_WAYVR": "Zamknij WayVR",
"SPEECH_TO_TEXT": "Zamiana mowy na tekst",
"WHISPER_GPU_SELECT": "Wybierz GPU"
},
"APPLICATION_LAUNCHER": "Launcher aplikacji",
"APPLICATION_STARTED": "Uruchomiono aplikację",
@@ -264,7 +282,8 @@
"SETTINGS": "Ustawienia dźwięku",
"SPEAKERS": "Głośniki",
"SPEAKERS_SET_SUCCESSFULLY": "Głośniki ustawione pomyślnie",
"VOLUME": "Głośność"
"VOLUME": "Głośność",
"SWITCH_TO_THIS_DEVICE": "Przełącz na to urządzenie"
},
"CLOSE_WINDOW": "Zamknij okno",
"CREATION_DATE": "Data utworzenia",
@@ -349,5 +368,17 @@
"ADD_TO_FAVOURITES": "Przypnij aplikację",
"ADD_TO_FAVOURITES_HINT": "Zapisz preset i przypnij go do szuflady aplikacji",
"REMOVE_FROM_FAVOURITES": "Usuń z przypiętych aplikacji",
"EMPTY": "Puste"
"EMPTY": "Puste",
"INSTALLED": "Zainstalowano",
"NOT_INSTALLED": "Nie zainstalowano",
"POWER_OPTIONS": {
"TITLE": "Opcje zasilania",
"SHUTDOWN": "Wyłącz",
"SUSPEND": "Uśpij",
"SHUTDOWN_ARE_YOU_SURE": "Czy na pewno chcesz wyłączyć komputer?",
"NOT_SUPPORTED": "Opcje zasilania wymagają systemd lub elogind.",
"SUSPEND_ARE_YOU_SURE": "Czy na pewno chcesz uśpić komputer?"
},
"YES": "Tak",
"NO": "Nie"
}
+384
View File
@@ -0,0 +1,384 @@
{
"DEFINITIONS": {
"ANCHORED": "Förankrad",
"FLOATING": "Flytande",
"STATIC": "Stationär",
"SET": "Set",
"WATCH": "Klocka",
"DASHBOARD": "Kontrollpanel",
"EDIT_MODE": "Redigeringsläge",
"SETTINGS": "Inställningar",
"KEYBOARD": "Tangentbord",
"TASKBAR": "Uppgiftsfält",
"FEATURES": "Funktioner",
"OVERLAY": "Overlay",
"SKYMAP": "Skymap",
"PLAYSPACE": "Playspace",
"D_PAD": "D-pad"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Centrera om playspace"
},
"APP_LAUNCHER": {
"ASPECT": {
"SEMI_TALL": "Halvhög",
"SEMI_WIDE": "Halvbredd",
"SQUARE": "Kvadratisk",
"TALL": "Hög",
"WIDE": "Bredbild"
},
"ASPECT_TITLE": "Bildförhållande",
"AUTOSTART": "Kör automatiskt vid start",
"LAUNCH": "Starta",
"MODE": {
"CAGE": "Kompatibilitetsläge (Cage)",
"NATIVE": "Nativt läge"
},
"RES_TITLE": "Upplösning"
},
"APP_SETTINGS": {
"ALLOW_SLIDING": "Stick-interaktion vid grepp",
"ANIMATION_SPEED": "Hastighet för UI-animationer",
"AUTOSTART_APPS": "Appar som ska köras vid start",
"BLOCK_GAME_INPUT": "Blockera spelindata",
"BLOCK_GAME_INPUT_HELP": "Blockerar all input när en overlay hovras över",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignorera klockan vid blockering av input",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Blockera inte inmatning när klockan är markerad",
"BLOCK_POSES_ON_KBD_INTERACTION": "Blockera poser vid interaktion med tangentbord",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blockerar spelets mottagning av poser när tangentbordet hovras över och 'Blockera spel-input' är aktiverat",
"BINDINGS": {
"COMP": {
"CLICK": "Klick",
"FORCE": "Tryckkraft",
"TOUCH": "Beröring",
"VALUE": "Värde",
"PROXIMITY": "Närhet",
"X_AXIS": "X-axel",
"Y_AXIS": "Y-axel"
},
"TYPE": {
"BUMPER": "Bumper",
"DPAD_UP": "D-pad upp",
"DPAD_DOWN": "D-pad ned",
"DPAD_LEFT": "D-pad vänster",
"DPAD_RIGHT": "D-pad höger",
"JOYSTICK": "Joystick",
"MENU": "Meny",
"SHOULDER": "Axel",
"SQUEEZE": "Grepp",
"SYSTEM": "System",
"THUMBREST": "Tumstöd",
"THUMBSTICK": "Thumbstick",
"TRACKPAD": "Trackpad",
"TRIGGER": "Trigger",
"VIEW": "Visa"
},
"CLICK": {
"ANY": "Vilken som helst",
"DOUBLE": "Dubbelklicka",
"TRIPLE": "Trippelklick",
"TYPE": "Antal tryck"
},
"ACTION": {
"CLICK": "Klicka*",
"GRAB": "Greppa*",
"ALT_CLICK": "Anpassat shell-kommando (ställ in alt_click i config)",
"SHOW_HIDE": "Visa, dölj, centrera om*",
"TOGGLE_DASHBOARD": "Växla instrumentpanel",
"SPACE_DRAG": "Dra playspace",
"SPACE_ROTATE": "Rotera playspace",
"SPACE_RESET": "Återställ Playspace",
"CLICK_MODIFIER_RIGHT": "Högerklick-modifierare",
"CLICK_MODIFIER_MIDDLE": "Modifierare för mittklick",
"MOVE_MOUSE": "Flytta mus (om avstängd som standard)",
"SCROLL": "Scrolla*"
},
"LEFT": "Vänster",
"RIGHT": "Höger",
"CHORD": "Kombination",
"CHORD_ADD_INPUT": "Lägg till inmatning för kombination",
"CHORD_INPUT_HAND": "Inmatningshand",
"COMPONENT": "Aktiveringstyp",
"SUBPATH": "Aktiverande kontroll",
"THRESHOLD": "Aktivera vid övre värdet\nAvaktivera vid nedre värdet"
},
"BROWSE_ONLINE_CATALOG": "Bläddra i onlinekatalogen...",
"BROWSE_SKYMAPS": "Bläddra bland skymaps",
"CAPTURE_METHOD": "Metod för skärmavbildning",
"CAPTURE_METHOD_HELP": "Prova att ändra detta om du\nupplever svarta eller glitchiga skärmar",
"COLOR_KEYING": "Färgnyckelning",
"COLOR_PALETTE": "Färgpalett",
"COLOR_PALETTE_HELP": "Klicka för att bläddra bland färgpaletter.\nSe webbplatsen för att skapa egna!",
"COLOR_PALETTES": "Färgpaletter",
"COLOR_PALETTE_CURRENT": "Nuvarande aktivt tema.",
"COLOR_PALETTE_CUSTOM_URL": "Lägg till ett eget anpassat tema!",
"COLOR_PALETTE_CUSTOM_URL2": "Klicka för att öppna webbmanualen.",
"COLOR_PALETTE_ACTIVATE": "Klicka för att använda detta tema.",
"CLEAR_PIPEWIRE_TOKENS": "Rensa PipeWire-tokens",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Fråga om skärmval vid nästa start",
"CLEAR_SAVED_STATE": "Återställ sparade inställningar",
"CLEAR_SAVED_STATE_HELP": "Återställ sets och overlay-positioner",
"CLICK_FREEZE_TIME_MS": "Klickfrysningstid (ms)",
"CLICK_FREEZE_TIME_MS_HELP": "Hjälper till med precision vid dubbelklick",
"CLOCK_12H": "12-timmarsklocka",
"CONTROLS": "Kontroller",
"DELETE_ALL_CONFIGS": "Rensa konfiguration",
"DELETE_ALL_CONFIGS_HELP": "Ta bort alla konfigurationsfiler från conf.d",
"DOUBLE_CURSOR_FIX": "Fix för dubbla markörer",
"DOUBLE_CURSOR_FIX_HELP": "Aktivera om du ser två markörer",
"DEFAULT_OVERLAY_SCALE": "Standardstorlek för overlay",
"DEFAULT_OVERLAY_SCALE_HELP": "Skapa nya overlays i den här skalan.",
"DEFAULT_CURVATURE": "Standardkurvatur för overlay",
"DEFAULT_OPACITY": "Standardopacitet för overlay",
"DEFAULT_POSITIONING": "Standardposition för overlay",
"OVERLAY_DEFAULTS_HELP": "Gäller endast nya overlays",
"ENABLED": "Aktiverad",
"FEATURES": "Funktioner",
"FOCUS_FOLLOWS_MOUSE_MODE": "Musrörelse vid trigger-tryck",
"HANDSFREE_POINTER": "Handsfree-läge",
"HANDSFREE_POINTER_HELP": "Inmatning som används när rörelsekontroller saknas.\nVänster nyp är greppa, höger är klicka.",
"HANDSFREE_ALT_TAB": "Sikte för app-växlare",
"HANDSFREE_ALT_TAB_HELP": "Håll in Super+X, sikta på önskat fönster och släpp sedan för att byta fokus.",
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "CPU-tillgängligt VRAM: {NUM} MiB",
"TITLE": "Diagnostik",
"ENABLE_REBAR": "Aktivera [resizable bar] i BIOS-inställningarna för optimal VR-prestanda"
},
"DIAGNOSTICS_REASON": {
"PACTL": "Krävs för kontroll av högtalare/mikrofon och byte av ljudkort",
"CAGE": "Rekommenderas vid inkompatibilitet med vissa X11-applikationer (kompatibilitetsläge)",
"XWAYLAND_SATELLITE": "Rekommenderas om du vill köra X11 (Xwayland)-applikationer i nativt läge",
"XDG_OPEN": "Krävs för att öppna URL:er eller Steam-spel"
},
"HIDE_GRAB_HELP": "Dölj grepphjälp",
"HIDE_USERNAME": "Dölj användarnamn",
"INPUT_EMULATION_METHOD": "Metod för inmatningssimulering",
"INPUT_EMULATION_METHOD_HELP": "Prova att ändra detta om mus- eller\ntangentbordindata inte registreras.",
"INPUT_CAPTURE_METHOD": "Metod för inmatningsfångst",
"INPUT_CAPTURE_METHOD_HELP": "Möjliggör användning av fysisk kringutrustning i WayVR.\n Super+Z: Växla input mellan WayVR och skrivbordet\n Super+X: Växla mellan WayVR-appar",
"INPUT_PROFILES": "Ändra styrinställningar",
"INPUT_PROFILES_HELP": "OpenXR-kontrollbindningar",
"INVERT_SCROLL_DIRECTION_X": "Invertera horisontell scrollriktning",
"INVERT_SCROLL_DIRECTION_Y": "Invertera vertikal rullningsriktning",
"LENS_SEPARATION_MM": "Linsavstånd (mm)",
"KEYBOARD_MIDDLE_CLICK": "Tangentbordets mittklick",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier som används vid skrivning\nmed lila laser",
"KEYBOARD_SOUND_ENABLED": "Tangentbordsljud",
"LANGUAGE": "Språk",
"LEFT_HANDED_MOUSE": "Vänstermus",
"LEFT_HANDED_MOUSE_HELP": "Används om musknapparna är ombytta",
"LONG_PRESS_DURATION": "Långtryckstid",
"LOOK_AND_FEEL": "Utseende och känsla",
"MISC": "Övrigt",
"MIDDLE_GRABS_SINGLE": "Modifierare för mittklick greppar enskilda objekt",
"MIDDLE_GRABS_SINGLE_HELP": "Att greppa med modifieraren för mittklick (lila laser)\ngreppar enskilda overlays istället för hela setet",
"NOTIFICATIONS_ENABLED": "Aktivera aviseringar",
"NOTIFICATIONS_SOUND_ENABLED": "Ljud för aviseringar",
"NOT_SUPPORTED": "Ej stöds",
"NO_SKYMAPS_FOUND": "Inga skymaps hittades",
"OPAQUE_BACKGROUND": "Opak bakgrund",
"OPTION": {
"ANCHORED_HELP": "Följer centermarkören. Rekommenderas.",
"FLOATING_HELP": "Flyttas oberoende, centreras om.\nLätt att tappa bort; rekommenderas ej.",
"STATIC_HELP": "Håller sig på plats i förhållande till världen.",
"AUTO": "Automatisk",
"AUTO_HELP": "ScreenCopy GPU om det stöds,\nannars PipeWire GPU.",
"EYE_ONLY": "Ögonlaser",
"EYE_PINCH": "Öga + pinch",
"HMD_ONLY": "HMD-laser",
"HMD_PINCH": "HMD + pinch",
"NONE": "Ingen",
"NONE_INPUT_HELP": "Emulera inte mus och tangentbord.\nInteraktion med skärmar blir inte möjlig.",
"NONE_INPUT_CAPTURE_HELP": "Fånga inte mus och tangentbord.\nDu kommer inte att kunna använda fysisk mus\neller tangentbord i appar som startas i WayVR.",
"PIPEWIRE_HELP": "Snabb GPU-avbildning,\nstandard för alla skrivbord.",
"PW_FALLBACK_HELP": "Långsam metod med hög CPU-belastning.\nProva om PipeWire GPU inte fungerar",
"SCREENCOPY_GPU_HELP": "Snabb, inga popup-fönster för skärmdelning.\nFungerar på: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Långsam, inga popup-fönster för skärmdelning.\nFungerar på: Hyprland, Niri, River, Sway",
"UINPUT": "Enhetsemulering",
"UINPUT_HELP": "Använder modulen uinput för att emulera en enhet.\nFungerar generellt bra på de flesta skrivbord.",
"EVDEV_HELP": "Ta kontroll över inmatningsenheter för exklusiv användning.\nAnvändaren måste tillhöra gruppen 'input'.",
"WL_VIRTUAL": "Wayland virtuell inmatning",
"WL_VIRTUAL_HELP": "Skickar händelser till Wayland-kompositorn via\nzwlr_virtual_pointer och zwp_virtual_keyboard.\nFungerar endast på vissa skrivbord.\nFungerar INTE på KDE, GNOME eller COSMIC."
},
"POINTER_ACCELERATION": "Musacceleration",
"POINTER_ACCELERATION_HELP": "På: Adaptiv musacceleration\nAv: Konstant hastighet",
"POINTER_LERP_FACTOR": "Utjämning av pekare",
"POINTER_SPEED": "Mushastighet",
"REQUIRES_RESTART": "Kräver omstart",
"RESET_PLAYSPACE": "Återställ playspace",
"RESET_PLAYSPACE_HELP": "Nollställ offset för playspace.",
"SAVE_PLAYSPACE_CENTER": "Spara playspace-center",
"SAVE_PLAYSPACE_CENTER_HELP": "Använd nuvarande playspace för bindningen \"återställ playspace\".",
"RESET_PLAYSPACE_CENTER": "Återställ playspace-center",
"RESET_PLAYSPACE_CENTER_HELP": "Rensa tidigare inställd mittpunkt för playspace.",
"RESTART_WAYVR": "Starta om WayVR",
"CLOSE_WAYVR": "Stäng WayVR",
"ROUND_MULTIPLIER": "UI-kanternas rundning",
"SCREEN_RENDER_DOWN": "Rendera skärm i lägre upplösning",
"SCREEN_RENDER_DOWN_HELP": "Hjälper mot aliasing på skärmar med hög upplösning",
"SCROLL_SPEED": "Scrollningshastighet",
"SELECT_VARIANT": "Välj variant",
"SNAP_ANGLE_DEG": "Snapvinkel för fönster (pitch & roll)",
"SNAP_ANGLE_DEG_HELP": "Greppade fönster snäpps till denna vinkel på pitch- och rollaxeln.",
"ENABLE_WATCH": "Aktivera klockan",
"SETS_ON_WATCH": "Set på klockan",
"SKYBOX": "Skymap",
"SKYMAP_ALREADY_DOWNLOADED": "Denna skymap är redan nedladdad. Välj åtgärd.",
"SPACE_DRAG": "Playspace",
"SPACE_DRAG_MULTIPLIER": "Multiplikator för space-drag",
"SPACE_DRAG_AFFECTS_WORLD": "Lämna overlays kvar vid flytt av playspace",
"SPACE_DRAG_UNLOCKED": "Tillåt space-drag på alla axlar",
"SPACE_GRAVITY_DAMPING": "Dämpning",
"SPACE_GRAVITY_DAMPING_HELP": "Artificiell dämpning för att sakta ner rörelser. 0,1 – hög dämpning, 1,0 – ingen dämpning",
"SPACE_GRAVITY_FLING_STRENGTH": "Kaststyrka",
"SPACE_GRAVITY_FLING_STRENGTH_HELP": "Intensitetsmultiplikator för gravitationskraften efter space-drag.\n0.0 - ingen rörelse alls, 2.0 - dubbel intensitet",
"SPACE_GRAVITY_GRAVITY": "Gravitation",
"SPACE_GRAVITY_GRAVITY_HELP": "Mängd nedåtgående kraft. 0.0 - ingen gravitation",
"SPACE_GRAVITY_GROUND_FRICTION": "Markfriktion",
"SPACE_GRAVITY_GROUND_FRICTION_HELP": "Mängden friktion som saktar ner dig när du rör vid marken.\n0.0 - ingen friktion (som på is), 1.0 - sträv yta",
"SPACE_GRAVITY_FLOOR_HEIGHT": "Golvhöjd",
"SPACE_GRAVITY_FLOOR_HEIGHT_HELP": "Y-positionen för golvet. Gravitationen upphör när denna höjd nås.",
"SPACE_ROTATE_UNLOCKED": "Tillåt rotation av playspace på alla axlar",
"ROTATIONS": "HMD-rotationer",
"SESSION_TIME": "Sessionstid",
"STATISTICS": "Statistik",
"TROUBLESHOOTING": "Felsökning",
"UI_GRADIENT_INTENSITY": "Intensitet för UI-gradient",
"UPRIGHT_SCREEN_FIX": "Fix för upprätt skärm",
"UPRIGHT_SCREEN_FIX_HELP": "Fixar upprätt ställda skärmar på vissa skrivbord",
"USE_PASSTHROUGH": "Aktivera passthrough",
"USE_PASSTHROUGH_HELP": "Tillåt passthrough om XR-runtime stöder det",
"USE_SKYBOX": "Visa skymap",
"USE_SKYBOX_HELP": "Visa en skymap om ingen scenapp eller passthrough används",
"WATCH_VIEW_ANGLE": "Klockans synvinklar",
"WATCH_VIEW_ANGLE_HELP": "Styr hur klockan tonas ut",
"WHISPER": {
"NEED_TO_DOWNLOAD_MODEL": "Den valda modellen måste laddas ner.\nVill du ladda ner den nu?",
"REMOVE_UNUSED_MODELS": "Du stänger av tal-till-text-funktionen.\nVill du ta bort nedladdade Whisper-modeller för att frigöra utrymme?"
},
"WHISPER_MODEL": "Tal-till-text-modell",
"WHISPER_MODEL_HELP": "Whisper-modell som ska användas.\nStörre modeller ger mer precisa\nresultat, men förbrukar mer VRAM.",
"SPEECH_TO_TEXT": "Tal till text",
"WHISPER_GPU_SELECT": "Välj GPU",
"TUTORIAL": "Handbok",
"GRID_OPACITY": "Golvgridens opacitet",
"GRID_OPACITY_HELP": "Opacitet för golvgridet när skymap är aktiverat",
"XWAYLAND_BY_DEFAULT": "Kör appar i kompatibilitetsläge som standard",
"SAVE": "Spara",
"CANCEL": "Avbryt"
},
"APPLICATION_LAUNCHER": "App-startare",
"APPLICATION_STARTED": "Applikationen startades",
"APPLICATIONS": "Applikationer",
"AUDIO": {
"SWITCH_TO_THIS_DEVICE": "Byt till den här enheten",
"AUTO_SWITCH_TO_VR_AUDIO": "Byt automatiskt till VR-ljud",
"CARDS": "Kort",
"FAILED_TO_SWITCH_MICROPHONE": "Kunde inte byta mikrofon",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofonen har ändrats",
"MICROPHONES": "Mikrofoner",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Ingen VR-mikrofon hittades. Byt manuellt.",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Inga VR-högtalare hittades. Byt manuellt.",
"SELECT_AUDIO_CARD_PROFILE": "Välj ljudkortsprofil",
"SETTINGS": "Ljudinställningar",
"SPEAKERS": "Högtalare",
"SPEAKERS_SET_SUCCESSFULLY": "Högtalarna har ändrats",
"VOLUME": "Volym"
},
"CLOSE_WINDOW": "Stäng fönster",
"CREATION_DATE": "Skapad datum",
"DEBUG_INFO": "Debuginfo",
"DISPLAY_BRIGHTNESS": "Skärmljusstyrka",
"DONATE": {
"SUPPORT_US": "Stöd oss",
"DESCRIPTION": "Vi är ett litet team på två utvecklare som bygger WayVR på vår fritid. Om du gillar att använda programvaran, överväg att stötta oss – varje donation hjälper oss att fortsätta.",
"THANK_YOU_WAYVR": "Tack från WayVR-teamet!",
"BUTTON_TOOLTIP": "Donera via Open Collective",
"CURRENT_SUPPORTERS": "Nuvarande supportrar",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "Stöds av {USER}|Särskilt tack till {USER}|Sponsras av {USER}|Backas av {USER}|Drivs av community-medlemmar som {USER}|Möjliggjorts av {USER}|Upprätthålls av {USER}|Bidrag från {USER}|Finansierat av {USER}",
"BROUGHT_TO_YOU_VARIANTS_RARE": "Kaffe sponsrat av {USER}|Drivs av {USER}|Hälsningar till {USER}!|Håller oss igång: {USER}|Hjälper oss att växa: {USER}|Håller servrarna igång: {USER}|Med tacksamhet till {USER}|Drivs av {USER}|Stöds av vänner som {USER}",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "Stöttas av legender som {USER}|Koffein sponsrat av {USER}|En sann legend: {USER}|Dagens hjälte: {USER}|En total superstjärna: {USER}|Betalar elräkningen: {USER}|Räddaren av vårt förstånd: {USER}|Visar vägen: {USER}|Koffein-till-kod-omvandlare tillhandahållen av {USER}|Håller produktionen stabil: {USER}|Den ende och ende: {USER}|Skickar dmabuf-buffertar: {USER}"
},
"DOWNLOAD": "Ladda ner",
"DOWNLOADER": "Nedladdare",
"WELCOME": {
"ALL_SET": "Nu är du redo!",
"BASICS": "Dessa grunder gör att du enkelt kan börja använda WayVR.",
"CENTER_MARKER": "Centreringsmarkören visas framför dig vid omcentrering. Placera overlays runt den!",
"DOUBLE_PRESS_TO_TOGGLE": "Dubbeltryck B (på Index) eller Y (på Meta) för att visa, centrera om eller dölja ditt set. Prova nu.",
"FEATURES_DOCUMENTATION": "Det finns många andra funktioner värda att anpassa. För mer information, se dokumentationen på",
"FINISH_TUTORIAL": "Avsluta handledningen",
"GRAB_BOTH_HANDS": "Greppa med båda händerna om du vill flytta ett specifikt overlay i ditt set.",
"GRAB_ONE_HAND": "Greppa med en hand för att flytta ditt set.",
"GREETING": "Hej!",
"GUIDE_INTRO": "Låt oss guida dig genom dina första steg.",
"LASER_COLORS": "Laserfärger",
"LASER_FUNCTIONALITY": "Mycket av funktionaliteten i WayVR beror på vilken laserfärg som används för att interagera med ett UI-element. Du behöver inte trycka på knapparna, det räcker att röra vid dem!",
"LAST_PAGE_INFO": "Denna handledning slutar visas när du når den sista sidan.",
"LOOK_AT_LEFT_WRIST": "Titta på din vänstra handled. Det är din klocka.",
"MIDDLE_CLICK_MODE_PURPLE_LASER": "Mittenklick-läge: Lila laser",
"MOVING_YOUR_SET": "Flytta ditt set",
"NEXT_BUTTON": "Tryck på nästa-knappen nedan för att komma igång.",
"REGULAR_MODE_BLUE_LASER": "Vanligt läge: Blå laser",
"REPOSITION_THIS_WINDOW_BY_GRABBING": "Flytta det här fönstret med kontrollens grip-funktion.",
"RIGHT_CLICK_MODE_ORANGE_LASER": "Högerklicksläge: Orange laser",
"THANKS_FOR_INSTALLING": "Tack för att du installerade WayVR!",
"WATCH": "Klocka",
"WORKING_SET": "Aktivt Set"
},
"DOWNLOAD_AGAIN": "Ladda ner igen",
"DOWNLOADING_FILE": "Laddar ner fil...",
"FAILED_TO_LAUNCH_APPLICATION": "Kunde inte starta applikationen:",
"GAME_LAUNCHED": "Spelet har startats",
"GAME_LIST": {
"NO_GAMES_FOUND": "Inga spel hittades",
"RUNNING_GAMES_LIST": "Lista över körande spel"
},
"GAMES": "Spel",
"GENERAL_SETTINGS": "Allmänna inställningar",
"GETTING_STARTED": "Kom igång",
"HEIGHT": "Höjd",
"HELLO": "Hej!",
"HELLO_USER": "Hej, {USER}!",
"HIDE": "Dölj",
"HOME_SCREEN": "Hem",
"MODIFICATION_DATE": "Ändringsdatum",
"MONADO_RUNTIME": "Monado runtime",
"LOADING": "Laddar...",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Upplösning"
},
"POWER_OPTIONS": {
"TITLE": "Strömalternativ",
"SHUTDOWN": "Stäng av",
"SUSPEND": "Viloläge",
"NOT_SUPPORTED": "Strömalternativ kräver systemd eller elogind.",
"SHUTDOWN_ARE_YOU_SURE": "Är du säker på att du vill stänga av datorn?",
"SUSPEND_ARE_YOU_SURE": "Är du säker på att du vill försätta datorn i viloläge?"
},
"PROCESS": {
"FORCE_KILL": "Tvinga avslut",
"STOP": "Stoppa"
},
"PROCESS_LIST": "Processlista",
"REFRESH": "Uppdatera",
"REMOVE": "Ta bort",
"APPLY": "Verkställ",
"RELOAD_FROM_DISK": "Ladda om från disk",
"SETTINGS": "Inställningar",
"SHOW": "Visa",
"TARGET_PATH": "Mål sökväg",
"TERMINATE_PROCESS": "Avsluta process",
"VERSION": "Version",
"WIDTH": "Bredd",
"PINNED_APPS": "Fästa applikationer",
"SAVED_TO_FAVOURITES": "Sparad i fästa applikationer",
"ADD_TO_FAVOURITES": "Lägg till i fästa applikationer",
"ADD_TO_FAVOURITES_HINT": "Spara förinställning och fäst den i app-menyn",
"REMOVE_FROM_FAVOURITES": "Ta bort från fästa applikationer",
"EMPTY": "Tomt",
"INSTALLED": "Installerad",
"NOT_INSTALLED": "Ej installerad",
"YES": "Ja",
"NO": "Nej"
}
+38 -7
View File
@@ -97,7 +97,10 @@
"RIGHT": "右侧",
"COMPONENT": "触发类型",
"SUBPATH": "激活控件",
"THRESHOLD": "高于上限值时激活\n低于下限值时取消激活"
"THRESHOLD": "高于上限值时激活\n低于下限值时取消激活",
"CHORD": "组合键",
"CHORD_ADD_INPUT": "添加组合键输入",
"CHORD_INPUT_HAND": "输入手"
},
"BROWSE_ONLINE_CATALOG": "浏览在线目录...",
"BROWSE_SKYMAPS": "浏览天空盒",
@@ -194,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "将当前的活动空间设置为重置活动空间的绑定点。",
"RESET_PLAYSPACE_CENTER": "重置活动空间中心",
"RESET_PLAYSPACE_CENTER_HELP": "重置之前设置的活动空间中心。",
"RESTART_SOFTWARE": "重启软件",
"RESTART_SOFTWARE_HELP": "应用需要重启才能生效的设置",
"ROUND_MULTIPLIER": "UI 边缘圆角",
"SCREEN_RENDER_DOWN": "低分辨率渲染屏幕",
"SCREEN_RENDER_DOWN_HELP": "有助于减轻高分辨率屏幕上的锯齿现象",
@@ -205,7 +206,6 @@
"SNAP_ANGLE_DEG_HELP": "抓取窗口在俯仰轴和翻滚轴上按此角度对齐。",
"ENABLE_WATCH": "启用手表",
"SETS_ON_WATCH": "手表显示套装",
"SHOW_WELCOME_SCREEN": "显示欢迎界面",
"SKYBOX": "天空图",
"SKYMAP_ALREADY_DOWNLOADED": "此天空盒已下载。请选择操作。",
"SPACE_DRAG": "活动空间",
@@ -247,7 +247,25 @@
"STATISTICS": "统计数据",
"ROTATIONS": "HMD 旋转次数",
"SESSION_TIME": "会话时间",
"LENS_SEPARATION_MM": "瞳距 (mm)"
"LENS_SEPARATION_MM": "瞳距 (mm)",
"MIDDLE_GRABS_SINGLE": "中键点击修饰键抓取单个对象",
"MIDDLE_GRABS_SINGLE_HELP": "使用中键点击修饰键(紫色激光)抓取时\n将抓取单个叠加层,而非整个集合",
"DIAGNOSTICS_REASON": {
"PACTL": "用于扬声器/麦克风控制及声卡切换",
"CAGE": "若部分 X11 应用程序不兼容,建议开启(兼容模式)",
"XWAYLAND_SATELLITE": "如果你想以原生模式运行 X11 (Xwayland) 应用程序,建议开启此项",
"XDG_OPEN": "打开 URL 或 Steam 游戏所需"
},
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "CPU 可访问 VRAM: {NUM} MiB",
"TITLE": "诊断",
"ENABLE_REBAR": "请在 BIOS 设置中启用 Resizable BAR 以获得最佳 VR 性能"
},
"TUTORIAL": "教程",
"RESTART_WAYVR": "重启 WayVR",
"CLOSE_WAYVR": "关闭 WayVR",
"SPEECH_TO_TEXT": "语音转文本",
"WHISPER_GPU_SELECT": "选择 GPU"
},
"APPLICATION_LAUNCHER": "应用启动器",
"APPLICATION_STARTED": "应用程序已启动",
@@ -264,7 +282,8 @@
"SETTINGS": "音频设置",
"SPEAKERS": "扬声器",
"SPEAKERS_SET_SUCCESSFULLY": "扬声器设置成功",
"VOLUME": "音量"
"VOLUME": "音量",
"SWITCH_TO_THIS_DEVICE": "切换至此设备"
},
"CLOSE_WINDOW": "关闭窗口",
"CREATION_DATE": "创建日期",
@@ -349,5 +368,17 @@
"ADD_TO_FAVOURITES": "添加到固定应用",
"ADD_TO_FAVOURITES_HINT": "保存预设并将其固定到应用抽屉",
"REMOVE_FROM_FAVOURITES": "从固定应用中移除",
"EMPTY": "暂无"
"EMPTY": "暂无",
"INSTALLED": "已安装",
"NOT_INSTALLED": "未安装",
"POWER_OPTIONS": {
"TITLE": "电源选项",
"SHUTDOWN": "关机",
"SUSPEND": "暂停",
"SHUTDOWN_ARE_YOU_SURE": "确定要关机吗?",
"NOT_SUPPORTED": "电源选项需要 systemd 或 elogind。",
"SUSPEND_ARE_YOU_SURE": "确定要让计算机进入睡眠状态吗?"
},
"YES": "确定",
"NO": "否"
}
+128 -12
View File
@@ -1,11 +1,11 @@
use std::{path::PathBuf, rc::Rc};
use std::{path::PathBuf, process::Command, rc::Rc};
use chrono::Timelike;
use glam::Vec2;
use strum::EnumCount;
use wgui::{
assets::{AssetPathRef, AssetProvider},
components::button::ComponentButton,
components::{ComponentTrait, button::ComponentButton},
event::{CallbackDataCommon, StyleSetRequest},
font_config::WguiFontConfig,
globals::WguiGlobals,
@@ -17,12 +17,15 @@ use wgui::{
task::Tasks,
theme::WguiTheme,
widget::{label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite},
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
windowing::{
context_menu::{self, ContextMenu},
window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
},
};
use wlx_common::{
async_executor::AsyncExecutor,
audio,
dash_interface::{BoxDashInterface, ConfigChangeKind, DashPlayspaceTask},
dash_interface::{BoxDashInterface, ConfigChangeKind, DashBackendTask, DashPlayspaceTask},
locale::WayVRLangProvider,
palette::load_palette,
timestep::{self, Timestep},
@@ -35,10 +38,11 @@ use crate::{
settings::TabSettings, welcome::TabWelcome,
},
util::{
popup_manager::{MountPopupOnceParams, PopupManager, PopupManagerParams},
is_exec_installed,
popup_manager::{MountPopupOnceParams, PopupHolder, PopupManager, PopupManagerParams},
toast_manager::ToastManager,
},
views,
views::{self, ViewUpdateParams},
};
pub struct FrontendWidgets {
@@ -74,9 +78,11 @@ pub struct Frontend<T> {
toast_manager: ToastManager,
timestep: Timestep,
sounds_to_play: Vec<SoundType>,
context_menu_power: ContextMenu,
window_audio_settings: WguiWindow,
view_audio_settings: Option<views::audio_settings::View>,
popup_dialog_box: PopupHolder<views::dialog_box::View>,
}
pub struct FrontendUpdateParams<'a, T> {
@@ -116,13 +122,16 @@ pub enum FrontendTask {
MountPopupOnce(MountPopupOnceParams),
RefreshPopupManager,
ShowAudioSettings,
ShowPowerSettings(WidgetID /* source widget */),
UpdateAudioSettingsView,
RecenterPlayspace,
PushToast(Translation),
PlaySound(SoundType),
OpenURL(Rc<str>),
RunCommand(String /* program */, Vec<String> /* args */),
HideDashboard,
MarkTutorialGraduated,
BackendTask(DashBackendTask),
}
impl<T: 'static> Frontend<T> {
@@ -202,9 +211,11 @@ impl<T: 'static> Frontend<T> {
popup_manager,
toast_manager,
window_audio_settings: WguiWindow::default(),
context_menu_power: Default::default(),
view_audio_settings: None,
executor: params.executor,
sounds_to_play: Vec::new(),
popup_dialog_box: Default::default(),
};
// init some things first
@@ -220,7 +231,7 @@ impl<T: 'static> Frontend<T> {
self.sounds_to_play.push(sound_type);
}
fn play_sound(&mut self, audio_system: &mut audio::AudioSystem, sound_type: SoundType) -> anyhow::Result<()> {
fn play_sound(&mut self, audio: &mut Box<dyn audio::AudioProvider>, sound_type: SoundType) -> anyhow::Result<()> {
let mut assets = self.globals.assets_builtin();
let path = match sound_type {
@@ -236,7 +247,7 @@ impl<T: 'static> Frontend<T> {
};
let sample = audio::AudioSample::from_mp3(&sound_bytes)?;
audio_system.play_sample(&sample);
audio.play_raw_sample(&sample);
Ok(())
}
@@ -264,24 +275,79 @@ impl<T: 'static> Frontend<T> {
pub fn process_update(
&mut self,
res: FrontendUpdateResult,
audio_system: &mut audio::AudioSystem,
audio_sample_player: &mut audio::SamplePlayer,
audio: &mut Box<dyn audio::AudioProvider>,
) -> anyhow::Result<()> {
for sound_type in res.sounds_to_play {
self.play_sound(audio_system, sound_type)?;
self.play_sound(audio, sound_type)?;
}
audio_sample_player.play_wgui_samples(audio_system, res.layout_result.sounds_to_play);
audio.play_wgui_samples(res.layout_result.sounds_to_play);
Ok(())
}
fn power_action_confirm(&mut self, power_action: PowerAction) {
let translation_key = match power_action {
PowerAction::Suspend => "POWER_OPTIONS.SUSPEND_ARE_YOU_SURE",
PowerAction::Poweroff => "POWER_OPTIONS.SHUTDOWN_ARE_YOU_SURE",
};
let tasks = self.tasks.clone();
views::dialog_box::mount_popup(
self.popup_dialog_box.clone(),
self.tasks.clone(),
views::dialog_box::Params {
globals: self.globals.clone(),
message: Translation::from_translation_key(translation_key),
entries: views::dialog_box::get_entries_yes_no(),
on_action_click: Box::new(move |action| {
if action == "yes" {
tasks.push(task_for_power_action(power_action));
}
}),
},
);
}
fn process_power_action(&mut self, action: &str, data: &mut T) {
match action {
"restart" => {
self.interface.restart(data);
}
"close" => {
self.interface.close(data);
}
"suspend" => {
self.power_action_confirm(PowerAction::Suspend);
}
"shutdown" => {
self.power_action_confirm(PowerAction::Poweroff);
}
_ => {
unreachable!()
}
}
}
fn tick(&mut self, params: FrontendUpdateParams<T>) -> anyhow::Result<FrontendUpdateResult> {
// fixme: timer events instead of this thing
if self.ticks.is_multiple_of(1000) {
self.update_time(params.data)?;
}
// TODO: refactor: simplify update function (no configs or T are required in most cases)
let mut config_change_kind = None;
self.popup_dialog_box.update(&mut ViewUpdateParams {
layout: &mut self.layout,
executor: &self.executor,
general_config: self.interface.general_config(params.data),
config_change_kind: &mut config_change_kind,
})?;
if let context_menu::TickResult::Action(action) = self.context_menu_power.tick(&mut self.layout, &mut self.state)? {
self.process_power_action(&action, params.data);
}
{
// always 30 times per second
while self.timestep.on_tick() {
@@ -372,13 +438,16 @@ impl<T: 'static> Frontend<T> {
FrontendTask::MountPopupOnce(popup_params) => self.mount_popup_once(popup_params, params.data)?,
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
FrontendTask::ShowPowerSettings(pos) => self.action_show_power_settings(pos)?,
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
FrontendTask::RecenterPlayspace => self.action_recenter_playspace(params.data)?,
FrontendTask::PushToast(content) => self.toast_manager.push(content),
FrontendTask::PlaySound(sound_type) => self.queue_play_sound(sound_type),
FrontendTask::HideDashboard => self.action_hide_dashboard(params.data),
FrontendTask::OpenURL(url) => self.action_open_url(url),
FrontendTask::RunCommand(program, args) => self.action_run_command(program, args),
FrontendTask::MarkTutorialGraduated => self.action_tutorial_graduated(params.data),
FrontendTask::BackendTask(task) => self.interface.backend_task(params.data, task)?,
};
Ok(())
}
@@ -504,6 +573,11 @@ impl<T: 'static> Frontend<T> {
FrontendTask::ShowAudioSettings,
);
let btn_power = self.state.fetch_component_as::<ComponentButton>("btn_power")?;
self
.tasks
.handle_button(&btn_power, FrontendTask::ShowPowerSettings(btn_power.base().get_id()));
// "Recenter playspace" bottom bar button
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_recenter")?,
@@ -513,6 +587,18 @@ impl<T: 'static> Frontend<T> {
Ok(())
}
fn action_show_power_settings(&mut self, source_id: WidgetID) -> anyhow::Result<()> {
self.context_menu_power.open(context_menu::OpenParams {
on_custom_attribs: None,
position: source_id.into(),
blueprint: context_menu::Blueprint::Template {
template_name: "context_menu_power_options".into(),
template_params: Default::default(),
},
});
Ok(())
}
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
self.window_audio_settings.open(&mut WguiWindowParams {
position: Vec2::new(64.0, 64.0),
@@ -577,4 +663,34 @@ impl<T: 'static> Frontend<T> {
url
))));
}
fn action_run_command(&mut self, command: String, args: Vec<String>) {
if let Err(e) = Command::new(command).args(args).spawn() {
self
.tasks
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"Failed to launch command: {e}"
))))
};
}
}
#[derive(Debug, Clone, Copy)]
pub enum PowerAction {
Suspend,
Poweroff,
}
pub fn task_for_power_action(action: PowerAction) -> FrontendTask {
for cmd in ["systemctl", "loginctl"] {
if is_exec_installed(cmd) {
let (command, args) = match action {
PowerAction::Suspend => (cmd.to_string(), vec!["suspend".to_string()]),
PowerAction::Poweroff => (cmd.to_string(), vec!["poweroff".to_string()]),
};
log::debug!("Command line for {action:?}: {command} {}", args.join(" "));
return FrontendTask::RunCommand(command, args);
}
}
FrontendTask::PushToast(Translation::from_translation_key("POWER_OPTIONS.NOT_SUPPORTED"))
}
+14 -11
View File
@@ -61,7 +61,7 @@ impl<T> Tab<T> for TabApps<T> {
}
}
fn find_entry_from_app_name<'a>(app_id: &str, entries: &'a [DesktopEntry]) -> Option<&'a DesktopEntry> {
fn find_entry_from_app_name<'a>(app_id: &str, entries: &'a [Rc<DesktopEntry>]) -> Option<&'a Rc<DesktopEntry>> {
entries.iter().find(|&entry| *entry.app_id == *app_id)
}
@@ -72,7 +72,7 @@ impl<T> TabApps<T> {
let mut stale_entries = Vec::<Rc<str>>::new();
let mut pinned_desktop_entries = Vec::<(PinnedApp, &DesktopEntry)>::new();
let mut pinned_desktop_entries = Vec::<(PinnedApp, Rc<DesktopEntry>)>::new();
// collect pinned desktop entries
{
@@ -83,7 +83,7 @@ impl<T> TabApps<T> {
continue;
};
pinned_desktop_entries.push((pinned_app.clone(), desktop_entry));
pinned_desktop_entries.push((pinned_app.clone(), desktop_entry.clone()));
}
// cleanup:
// remove non-existent app ids from pinned apps
@@ -119,7 +119,7 @@ impl<T> TabApps<T> {
&mut state.parser_state,
&doc_params(frontend.globals.clone()),
self.pinned_apps_parent,
desktop_entry,
desktop_entry.as_ref(),
Some(tooltip_string),
)?;
@@ -139,8 +139,8 @@ impl<T> TabApps<T> {
struct AppList {
//data: Vec<ParserData>,
all_entries: Vec<DesktopEntry>,
entries_to_mount: VecDeque<DesktopEntry>,
all_entries: Vec<Rc<DesktopEntry>>,
entries_to_mount: VecDeque<Rc<DesktopEntry>>,
list_parent: WidgetPair,
prev_category_name: String,
}
@@ -150,7 +150,7 @@ fn on_app_click(
frontend_tasks: FrontendTasks,
tasks: Tasks<Task>,
globals: WguiGlobals,
entry: DesktopEntry,
entry: Rc<DesktopEntry>,
state: Rc<RefCell<State>>,
pinned_app: Option<PinnedApp>,
) -> ButtonClickCallback {
@@ -307,14 +307,17 @@ fn get_category_name_score(name: &str) -> u8 {
Scores::Unknown as u8
}
fn get_best_category_name(categories: &[Rc<str>]) -> Option<&Rc<str>> {
fn get_best_category_name<S>(categories: &[S]) -> Option<&str>
where
S: AsRef<str>,
{
let mut best_score: u8 = 0;
let mut best_category: Option<&Rc<str>> = None;
let mut best_category: Option<&str> = None;
for cat in categories {
let score = get_category_name_score(cat);
let score = get_category_name_score(cat.as_ref());
if score > best_score {
best_category = Some(cat);
best_category = Some(cat.as_ref());
best_score = score;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
use std::{marker::PhantomData, rc::Rc};
use wgui::{
assets::{AssetPathRef},
assets::AssetPathRef,
components::button::ComponentButton,
globals::WguiGlobals,
layout::{Layout, WidgetID},
+1 -1
View File
@@ -1,7 +1,7 @@
use std::{collections::HashMap, marker::PhantomData, rc::Rc};
use wgui::{
assets::{AssetPathRef},
assets::AssetPathRef,
components::{
bar_graph::{ComponentBarGraph, ValueCell},
button::ComponentButton,
+100 -66
View File
@@ -1,16 +1,14 @@
use std::rc::Rc;
use crate::tab::settings::{self, SettingType, Task, horiz_cell, mount_requires_restart};
use crate::{
tab::settings::{self, SettingType, Task, horiz_cell, mount_requires_restart},
util::wgui_simple,
};
use wgui::{
components::{
button::{ButtonClickEvent, ComponentButton},
checkbox::ComponentCheckbox,
slider::ComponentSlider,
},
components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider},
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu,
};
use wlx_common::{DesktopBackend, XrBackend, config::GeneralConfig, dash_interface::InterfaceFeats};
@@ -49,6 +47,49 @@ pub fn options_stat_row(mp: &mut MacroParams, parent: WidgetID, translation: &st
mp.parser_state.get_widget_id(&id)
}
pub fn options_diagnostics_row(
mp: &mut MacroParams,
parent: WidgetID,
title: &str,
tooltip_str: &str,
checked: bool,
) -> anyhow::Result<()> {
let mut params = TemplateParams::new();
params.insert("title", title);
params.insert("tooltip_str", tooltip_str);
params.insert("color", if checked { "success" } else { "danger" });
params.insert(
"sprite",
if checked {
"@/dashboard/check.svg"
} else {
"@/dashboard/cross.svg"
},
);
mp.parser_state
.instantiate_template(mp.doc_params, "DiagnosticsRow", mp.layout, parent, params)?;
Ok(())
}
pub fn options_diagnostics_drm_card(
mp: &mut MacroParams,
parent: WidgetID,
name: &str,
info: &str,
) -> anyhow::Result<()> {
let mut params = TemplateParams::new();
params.insert("name", name);
params.insert("info", info);
mp.parser_state
.instantiate_template(mp.doc_params, "DiagnosticsDrmCard", mp.layout, parent, params)?;
Ok(())
}
pub fn options_checkbox(mp: &mut MacroParams, parent: WidgetID, setting: SettingType) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
@@ -254,7 +295,6 @@ where
mp.idx += 1;
let mut params = TemplateParams::new();
params.insert("id", &id);
match setting.get_translation() {
Ok(translation) => params.insert("translation", translation),
@@ -267,69 +307,63 @@ where
let id_cell = horiz_cell(mp.layout, parent)?;
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, id_cell, params)?;
let setting_str = setting.as_ref();
let title = setting.get_enum_title(mp.config);
let feats = mp.feats;
let cells = EnumType::VARIANTS
.iter()
.filter_map(|item| {
if item.get_bool("Hidden").unwrap_or(false) {
return None;
}
if item
.get_str("Backend")
.is_some_and(|x| XrBackend::try_from(x).unwrap() != feats.xr_backend)
{
return None;
}
if item
.get_str("Desktop")
.is_some_and(|x| DesktopBackend::try_from(x).unwrap() != feats.desktop_backend)
{
return None;
}
let value = item.as_ref();
let title = SettingType::get_enum_title_inner(*item);
let tooltip = SettingType::get_enum_tooltip_inner(*item);
let text = &title.text;
let translated = if title.translated { "1" } else { "0" };
let action = format!("{setting_str};{id};{value};{text};{translated}");
Some(context_menu::Cell::Button(context_menu::Button {
action_name: Some(action.into()),
title,
tooltip,
..Default::default()
}))
})
.collect();
let tasks = mp.tasks.clone();
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
layout: mp.layout,
parser_state: mp.parser_state,
doc_params: mp.doc_params,
parent: id_cell,
id,
params,
current_text: title,
cells,
on_open: Box::new(move |pos, cells| tasks.push(Task::OpenContextMenu(pos, cells))),
})?;
if setting.requires_restart() {
mount_requires_restart(mp.layout, id_cell)?;
}
let setting_str = setting.as_ref();
let title = setting.get_enum_title(mp.config);
{
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 feats = mp.feats;
let tasks = mp.tasks.clone();
move |_common, e: ButtonClickEvent| {
tasks.push(Task::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
EnumType::VARIANTS
.iter()
.filter_map(|item| {
if item.get_bool("Hidden").unwrap_or(false) {
return None;
}
if item
.get_str("Backend")
.is_some_and(|x| XrBackend::try_from(x).unwrap() != feats.xr_backend)
{
return None;
}
if item
.get_str("Desktop")
.is_some_and(|x| DesktopBackend::try_from(x).unwrap() != feats.desktop_backend)
{
return None;
}
let value = item.as_ref();
let title = SettingType::get_enum_title_inner(*item);
let tooltip = SettingType::get_enum_tooltip_inner(*item);
let text = &title.text;
let translated = if title.translated { "1" } else { "0" };
Some(context_menu::Cell {
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
title,
tooltip,
attribs: vec![],
})
})
.collect(),
));
Ok(())
}
}));
Ok(())
}
+28 -13
View File
@@ -1,8 +1,7 @@
use glam::Vec2;
use std::{marker::PhantomData, rc::Rc, str::FromStr};
use strum::{AsRefStr, EnumProperty, EnumString};
use wgui::{
assets::{AssetPathRef},
assets::AssetPathRef,
color::WguiColorName,
components::tabs::ComponentTabs,
event::StyleSetRequest,
@@ -35,6 +34,7 @@ use crate::{
mod macros;
mod tab_autostart_apps;
mod tab_controls;
mod tab_diagnostics;
mod tab_features;
mod tab_look_and_feel;
mod tab_misc;
@@ -53,6 +53,7 @@ pub(crate) enum TabNameEnum {
Skybox,
SpaceDrag,
Statistics,
Diagnostics,
Troubleshooting,
}
@@ -67,6 +68,7 @@ impl TabNameEnum {
"skybox" => Some(TabNameEnum::Skybox),
"space_drag" => Some(TabNameEnum::SpaceDrag),
"statistics" => Some(TabNameEnum::Statistics),
"diagnostics" => Some(TabNameEnum::Diagnostics),
"troubleshooting" => Some(TabNameEnum::Troubleshooting),
_ => None,
}
@@ -78,11 +80,10 @@ pub(crate) enum Task {
ClearPipewireTokens,
ClearSavedState,
DeleteAllConfigs,
OpenContextMenu(Vec2, Vec<context_menu::Cell>),
OpenContextMenu(context_menu::Position, Vec<context_menu::Cell>),
RemoveAutostartApp(Rc<str>),
ResetPlayspace,
ResetPlayspaceCenter,
RestartSoftware,
SavePlayspaceCenter,
SetTab(TabNameEnum),
SettingUpdated(SettingType),
@@ -140,6 +141,7 @@ pub struct TabSettings<T> {
tasks: Tasks<Task>,
marker: PhantomData<T>,
frontend_tasks: FrontendTasks,
feats: InterfaceFeats,
}
impl<T> Tab<T> for TabSettings<T> {
@@ -228,10 +230,6 @@ impl<T> Tab<T> for TabSettings<T> {
frontend.interface.playspace_task(data, DashPlayspaceTask::Reset)?;
return Ok(());
}
Task::RestartSoftware => {
frontend.interface.restart(data);
return Ok(());
}
Task::OpenContextMenu(position, cells) => {
self.context_menu.open(context_menu::OpenParams {
on_custom_attribs: None,
@@ -347,6 +345,7 @@ pub(crate) enum SettingType {
NotificationsEnabled,
NotificationsSoundEnabled,
OpaqueBackground,
MiddleGrabsSingle,
MouseAcceleration,
PointerLerpFactor,
MouseSpeed,
@@ -403,6 +402,7 @@ impl SettingType {
Self::NotificationsEnabled => &mut config.notifications_enabled,
Self::NotificationsSoundEnabled => &mut config.notifications_sound_enabled,
Self::OpaqueBackground => &mut config.opaque_background,
Self::MiddleGrabsSingle => &mut config.middle_grabs_single,
Self::MouseAcceleration => &mut config.wvr_mouse_acceleration,
Self::ScreenRenderDown => &mut config.screen_render_down,
Self::SetsOnWatch => &mut config.sets_on_watch,
@@ -553,6 +553,7 @@ impl SettingType {
Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"),
Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"),
Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"),
Self::MiddleGrabsSingle => Ok("APP_SETTINGS.MIDDLE_GRABS_SINGLE"),
Self::MouseSpeed => Ok("APP_SETTINGS.POINTER_SPEED"),
Self::MouseAcceleration => Ok("APP_SETTINGS.POINTER_ACCELERATION"),
Self::PointerLerpFactor => Ok("APP_SETTINGS.POINTER_LERP_FACTOR"),
@@ -602,6 +603,7 @@ impl SettingType {
Self::InputEmulationMethod => Some("APP_SETTINGS.INPUT_EMULATION_METHOD_HELP"),
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
Self::MiddleGrabsSingle => Some("APP_SETTINGS.MIDDLE_GRABS_SINGLE_HELP"),
Self::MouseAcceleration => Some("APP_SETTINGS.POINTER_ACCELERATION_HELP"),
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
Self::SnapAngleDeg => Some("APP_SETTINGS.SNAP_ANGLE_DEG_HELP"),
@@ -696,10 +698,14 @@ impl<T> TabSettings<T> {
fn set_tab(&mut self, frontend: &mut Frontend<T>, data: &mut T, name: TabNameEnum) -> anyhow::Result<()> {
let root = self.state.get_widget_id("settings_root")?;
frontend.layout.remove_children(root);
let globals = frontend.layout.state.globals.clone();
self.current_tab = None;
let globals = frontend.layout.state.globals.clone();
let feats = frontend.interface.get_feats(data);
let vulkan_devices = if matches!(name, TabNameEnum::Features) {
Some(frontend.interface.enumerate_vulkan_devices(data)?)
} else {
None
};
let mut mp = MacroParams {
layout: &mut frontend.layout,
@@ -708,14 +714,14 @@ impl<T> TabSettings<T> {
config: frontend.interface.general_config(data),
tasks: self.tasks.clone(),
idx: 9001,
feats,
feats: self.feats,
};
let settings_mount_params = SettingsMountParams {
mp: &mut mp,
id_parent: root,
frontend_tasks: &self.frontend_tasks,
feats,
feats: self.feats,
};
match name {
@@ -723,7 +729,10 @@ impl<T> TabSettings<T> {
self.current_tab = Some(Box::new(tab_look_and_feel::State::mount(settings_mount_params)?));
}
TabNameEnum::Features => {
self.current_tab = Some(Box::new(tab_features::State::mount(settings_mount_params)?));
self.current_tab = Some(Box::new(tab_features::State::mount(
settings_mount_params,
vulkan_devices.unwrap(), /* safe */
)?));
}
TabNameEnum::SpaceDrag => {
self.current_tab = Some(Box::new(tab_space_drag::State::mount(settings_mount_params)?));
@@ -749,6 +758,9 @@ impl<T> TabSettings<T> {
TabNameEnum::Statistics => {
self.current_tab = Some(Box::new(tab_statistics::State::mount(settings_mount_params)?));
}
TabNameEnum::Diagnostics => {
self.current_tab = Some(Box::new(tab_diagnostics::State::mount(settings_mount_params)?));
}
}
Ok(())
@@ -786,6 +798,8 @@ impl<T> TabSettings<T> {
tasks.push(Task::SetTab(TabNameEnum::LookAndFeel));
let feats = frontend.interface.get_feats(data);
Ok(Self {
app_button_ids: Vec::new(),
tasks,
@@ -794,6 +808,7 @@ impl<T> TabSettings<T> {
context_menu: ContextMenu::default(),
current_tab: None,
frontend_tasks: frontend.tasks.clone(),
feats,
})
}
}
+24 -6
View File
@@ -3,7 +3,10 @@ use wgui::parser::TemplateParams;
use wgui::{
components::button::ComponentButton, globals::WguiGlobals, layout::WidgetID, parser::Fetchable, task::Tasks,
};
use wlx_common::XrBackend;
use wlx_common::dash_interface::DashBackendTask;
use crate::frontend::FrontendTask;
use crate::tab::settings::{UpdateExtra, horiz_cell, mount_requires_restart};
use crate::util::popup_manager::PopupHolder;
@@ -18,7 +21,8 @@ use crate::{
#[derive(Clone)]
enum Task {
OpenInputProfiles,
OpenInputProfilesDash,
OpenInputProfilesBackend,
}
pub struct State {
@@ -34,13 +38,23 @@ impl SettingsTab for State {
for task in self.tasks.drain() {
match task {
Task::OpenInputProfiles => {
Task::OpenInputProfilesDash => {
input_profiles::mount_popup(
self.frontend_tasks.clone(),
self.globals.clone(),
self.popup_input_profiles.clone(),
);
}
Task::OpenInputProfilesBackend => {
self
.frontend_tasks
.push(FrontendTask::BackendTask(DashBackendTask::ShowBindingsUI));
// overlay sort order doesn't work on steamvr,
// so the dashboard would be drawn over the bindings UI
// while the bindings UI has input focus
self.frontend_tasks.push(FrontendTask::HideDashboard);
}
}
}
@@ -53,6 +67,7 @@ fn create_input_profiles_button(
parent: WidgetID,
tasks: Tasks<Task>,
_popup: &PopupHolder<input_profiles::View>,
xr_backend: XrBackend,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
@@ -71,7 +86,11 @@ fn create_input_profiles_button(
btn.on_click(Rc::new({
let tasks = tasks.clone();
move |_common, _e| {
tasks.push(Task::OpenInputProfiles);
if xr_backend.is_open_vr() {
tasks.push(Task::OpenInputProfilesBackend);
} else {
tasks.push(Task::OpenInputProfilesDash);
}
Ok(())
}
}));
@@ -92,14 +111,13 @@ impl State {
"APP_SETTINGS.CONTROLS",
"@/dashboard/controller.svg",
)?;
if par.feats.xr_backend.is_open_xr() {
create_input_profiles_button(par.mp, c, tasks.clone(), &popup)?;
}
create_input_profiles_button(par.mp, c, tasks.clone(), &popup, par.feats.xr_backend)?;
options_dropdown::<wlx_common::config::AltModifier>(par.mp, c, &SettingType::KeyboardMiddleClick)?;
options_dropdown::<wlx_common::config::HandsfreePointer>(par.mp, c, &SettingType::HandsfreePointer)?;
options_dropdown::<wlx_common::config::HandsfreeAltTab>(par.mp, c, &SettingType::HandsfreeAltTab)?;
options_checkbox(par.mp, c, SettingType::FocusFollowsMouseMode)?;
options_checkbox(par.mp, c, SettingType::LeftHandedMouse)?;
options_checkbox(par.mp, c, SettingType::MiddleGrabsSingle)?;
options_checkbox(par.mp, c, SettingType::AllowSliding)?;
options_checkbox(par.mp, c, SettingType::InvertScrollDirectionX)?;
options_checkbox(par.mp, c, SettingType::InvertScrollDirectionY)?;
@@ -0,0 +1,178 @@
use wgui::layout::WidgetID;
use crate::{
tab::settings::{
SettingsMountParams, SettingsTab,
macros::{MacroParams, options_category, options_diagnostics_drm_card, options_diagnostics_row},
},
util::is_exec_installed,
};
pub struct State {}
impl SettingsTab for State {}
#[derive(Default)]
struct LspciInfo {
// slot: String,
// class: String,
vendor: String,
device: String,
// s_vendor: String,
}
fn lspci_info(vendor: &str /* hex */, device: &str /* hex */) -> Option<LspciInfo> {
// example output for "lspci -vmmd 1002:747e":
//
// Slot: 0b:00.0
// Class: VGA compatible controller
// Vendor: Advanced Micro Devices, Inc. [AMD/ATI]
// Device: Navi 32 [Radeon RX 7700 XT / 7800 XT]
// SVendor: Sapphire Technology Limited
// SDevice: Device 475a
// Rev: c8
// ProgIf: 00
let mut cmd = std::process::Command::new("lspci");
cmd.arg("-vmmd");
cmd.arg(format!("{}:{}", vendor, device));
let res = cmd.output().ok()?;
if !res.status.success() {
return None;
}
let mut info = LspciInfo::default();
let stdout = str::from_utf8(&res.stdout).ok()?;
for line in stdout.lines() {
let Some((key, value)) = line.split_once(":") else {
continue;
};
if key == "Device" {
info.device = String::from(value.trim());
}
}
Some(info)
}
fn mount_installed_path(
mp: &mut MacroParams,
parent: WidgetID,
exec: &str,
reason_translation: Option<&str>,
) -> anyhow::Result<()> {
let title = if is_exec_installed(exec) {
"INSTALLED"
} else {
"NOT_INSTALLED"
};
let title_translated = mp.layout.state.globals.i18n().translate(title);
let tooltip = if let Some(reason) = reason_translation {
format!(
"{}\n{}",
title_translated,
mp.layout.state.globals.i18n().translate(reason)
)
} else {
title_translated.to_string()
};
options_diagnostics_row(mp, parent, exec, &tooltip, is_exec_installed(exec))?;
Ok(())
}
fn read_sysfs_param(device_path: &str, param: &str) -> anyhow::Result<String> {
let string = std::fs::read_to_string(format!("{}/{}", device_path, param))?;
Ok(string.trim().into())
}
fn read_sysfs_param_int(device_path: &str, param: &str) -> anyhow::Result<u64> {
Ok(read_sysfs_param(device_path, param)?.parse::<u64>()?)
}
fn mount_drm_card(mp: &mut MacroParams, parent: WidgetID, device_path: &str) -> anyhow::Result<()> {
let vendor = read_sysfs_param(device_path, "vendor")?;
let device = read_sysfs_param(device_path, "device")?;
let vram_total = read_sysfs_param_int(device_path, "mem_info_vram_total").ok();
let vis_vram_total = read_sysfs_param_int(device_path, "mem_info_vis_vram_total").ok();
let name = match lspci_info(&vendor, &device) {
Some(info) => {
format!("{} {}", info.vendor, info.device)
}
None => format!("{}:{}", vendor, device),
};
let info = if let Some(vram_total) = vram_total
&& let Some(vis_vram_total) = vis_vram_total
{
let mut s = mp.layout.state.globals.i18n().translate_and_replace(
"APP_SETTINGS.DIAGNOSTICS.CPU_ACCESSIBLE_VRAM",
(
"{NUM}",
&format!("{}/{}", vis_vram_total / 1024 / 1024, vram_total / 1024 / 1024),
),
);
if vis_vram_total != vram_total {
s += " [ALERT!]\nEnable Resizable BAR in the BIOS settings for optimal VR performance";
}
s
} else {
String::from("No info")
};
options_diagnostics_drm_card(mp, parent, &name, &info)?;
Ok(())
}
fn mount_drm_cards(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
for file in std::fs::read_dir("/sys/class/drm/")?.flatten() {
let file_name = file.file_name();
let Some(file_name) = file_name.to_str() else {
continue;
};
if !file_name.starts_with("renderD") {
continue;
}
if let Err(e) = mount_drm_card(mp, parent, &format!("/sys/class/drm/{}/device", file_name)) {
log::error!("failed to mount drm card: {}", e);
}
}
Ok(())
}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
let c = options_category(
par.mp,
par.id_parent,
"APP_SETTINGS.DIAGNOSTICS.TITLE",
"@/dashboard/diagnostics.svg",
)?;
mount_installed_path(par.mp, c, "pactl", Some("APP_SETTINGS.DIAGNOSTICS_REASON.PACTL"))?;
mount_installed_path(par.mp, c, "cage", Some("APP_SETTINGS.DIAGNOSTICS_REASON.CAGE"))?;
mount_installed_path(
par.mp,
c,
"xwayland-satellite",
Some("APP_SETTINGS.DIAGNOSTICS_REASON.XWAYLAND_SATELLITE"),
)?;
mount_installed_path(par.mp, c, "xdg-open", Some("APP_SETTINGS.DIAGNOSTICS_REASON.XDG_OPEN"))?;
mount_installed_path(par.mp, c, "steam", None)?;
mount_drm_cards(par.mp, c)?;
Ok(State {})
}
}
+128 -72
View File
@@ -1,7 +1,6 @@
use std::rc::Rc;
use wgui::{
components::button::{ButtonClickEvent, ComponentButton},
globals::WguiGlobals,
i18n::Translation,
layout::WidgetID,
@@ -9,7 +8,7 @@ use wgui::{
parser::{Fetchable, TemplateParams},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu::{self},
windowing::context_menu,
};
use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_interface::ConfigChangeKind};
@@ -21,6 +20,7 @@ use crate::{
},
util::{
popup_manager::PopupHolder,
wgui_simple,
whisper::{
WHISPER_MODELS, WhisperModel, whisper_any_models_downloaded, whisper_delete_all_models, whisper_model_from_name,
whisper_model_path,
@@ -104,41 +104,60 @@ impl SettingsTab for State {
layout: &mut wgui::layout::Layout,
state: &mut wgui::parser::ParserState,
) -> anyhow::Result<()> {
if let (Some(_), Some(id), Some(model_name)) = {
let mut s = action.splitn(3, ';');
(s.next(), s.next(), s.next())
} {
if !model_name.is_empty()
&& let Some(model) = whisper_model_from_name(model_name)
{
let mut common = layout.common();
let mut label = state.fetch_widget_as::<WidgetLabel>(common.state, &format!("{id}_value"))?;
label.set_text(&mut common, Translation::from_raw_text(model.display_name));
let (Some(_), Some(_type), Some(id), Some(value)) = ({
let mut s = action.splitn(4, ';');
(s.next(), s.next(), s.next(), s.next())
}) else {
debug_assert!(false);
return Ok(());
};
config.whisper_model = model.file_name.into();
if !whisper_model_path(model.file_name).exists() {
self.show_whisper_model_dialog_box_download(model)?;
match _type {
"whisper_model" => {
let model_name = value;
if !model_name.is_empty()
&& let Some(model) = whisper_model_from_name(model_name)
{
let mut common = layout.common();
let mut label = state.fetch_widget_as::<WidgetLabel>(common.state, &format!("{id}_value"))?;
label.set_text(&mut common, Translation::from_raw_text(model.display_name));
config.whisper_model = model.file_name.into();
if !whisper_model_path(model.file_name).exists() {
self.show_whisper_model_dialog_box_download(model)?;
} else {
change_kind.replace(ConfigChangeKind::Other);
}
} else {
config.whisper_model = "".into();
change_kind.replace(ConfigChangeKind::Other);
}
} else {
config.whisper_model = "".into();
change_kind.replace(ConfigChangeKind::Other);
// re-init the whole tab
if whisper_any_models_downloaded().unwrap_or_default() {
self.show_whisper_model_dialog_box_cleanup()?;
} else {
self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features));
// re-init the whole tab
if whisper_any_models_downloaded().unwrap_or_default() {
self.show_whisper_model_dialog_box_cleanup()?;
} else {
self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features));
}
}
}
}
"whisper_gpu" => {
let mut common = layout.common();
let mut label = state.fetch_widget_as::<WidgetLabel>(common.state, &format!("{id}_value"))?;
config.whisper_gpu = value.parse::<u32>().unwrap();
label.set_text(
&mut common,
Translation::from_raw_text_string(config.whisper_gpu.to_string()),
);
change_kind.replace(ConfigChangeKind::Other);
}
_ => unreachable!(),
};
Ok(())
}
}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
pub fn mount(par: SettingsMountParams, vulkan_devices: Vec<String>) -> anyhow::Result<State> {
let tasks = Tasks::<Task>::new();
let popup_download = PopupHolder::<views::download_file::View>::default();
let popup_dialog = PopupHolder::<views::dialog_box::View>::default();
@@ -151,7 +170,9 @@ impl State {
)?;
if par.feats.whisper {
let c = options_category(par.mp, c, "APP_SETTINGS.SPEECH_TO_TEXT", "@/dashboard/stt.svg")?;
whisper_models_dropdown(par.mp, c)?;
whisper_gpu_select_dropdown(par.mp, c, vulkan_devices)?;
}
options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?;
@@ -287,6 +308,48 @@ impl State {
}
}
fn whisper_gpu_select_dropdown(
mp: &mut MacroParams,
parent: WidgetID,
vulkan_devices: Vec<String>,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
let parent = horiz_cell(mp.layout, parent)?;
let mut params = TemplateParams::new();
params.insert("translation", "APP_SETTINGS.WHISPER_GPU_SELECT");
let current_text = Translation::from_raw_text_string(mp.config.whisper_gpu.to_string());
let cells = vulkan_devices
.iter()
.enumerate()
.filter(|(_, name)| !name.contains("llvmpipe"))
.map(|(idx, name)| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_gpu;{};{}", id, idx).into()),
title: Translation::from_raw_text_string(name.to_string()),
..Default::default()
})
})
.collect();
let tasks = mp.tasks.clone();
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
layout: mp.layout,
parser_state: mp.parser_state,
doc_params: mp.doc_params,
parent,
id,
params,
current_text,
cells,
on_open: Box::new(move |pos, cells| tasks.push(ParentTask::OpenContextMenu(pos, cells))),
})
}
fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
@@ -307,56 +370,49 @@ fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Re
});
let mut params = TemplateParams::new();
params.insert("id", &id);
params.insert("translation", "APP_SETTINGS.WHISPER_MODEL");
params.insert("tooltip", "APP_SETTINGS.WHISPER_MODEL_HELP");
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
{
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(), current_translation);
}
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Rc::new({
let parent_tasks = mp.tasks.clone();
move |_common, e: ButtonClickEvent| {
let mut cells = WHISPER_MODELS
.iter()
.enumerate()
.map(|(idx, item)| context_menu::Cell {
action_name: Some(format!(";{id};{}", item.file_name).into()),
title: Translation::from_raw_text_string(format!(
"{}{}",
item.display_name,
if downloaded_models.contains(&idx) { " ✅" } else { "" }
)),
tooltip: None,
attribs: vec![],
let mut cells = WHISPER_MODELS
.iter()
.enumerate()
.map(|(idx, item)| {
if downloaded_models.contains(&idx) {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_model;{id};{}", item.file_name).into()),
title: Translation::from_raw_text(item.display_name),
sprite_src_builtin: Some("@/dashboard/check.svg".into()),
..Default::default()
})
.collect::<Vec<_>>();
} else {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_model;{id};{}", item.file_name).into()),
title: Translation::from_raw_text(item.display_name),
..Default::default()
})
}
})
.collect::<Vec<_>>();
cells.insert(
0,
context_menu::Cell {
action_name: Some(format!(";{id};").into()),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
tooltip: None,
attribs: vec![],
},
);
cells.insert(
0,
context_menu::Cell::Button(context_menu::Button {
action_name: Some(format!(";whisper_model;{id};").into()),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
..Default::default()
}),
);
parent_tasks.push(ParentTask::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
cells,
));
Ok(())
}
}));
Ok(())
let tasks = mp.tasks.clone();
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
layout: mp.layout,
parser_state: mp.parser_state,
doc_params: mp.doc_params,
parent,
id,
params,
current_text: current_translation,
cells,
on_open: Box::new(move |pos, cells| tasks.push(ParentTask::OpenContextMenu(pos, cells))),
})
}
@@ -36,13 +36,6 @@ impl State {
"@/dashboard/circle.svg",
Task::DeleteAllConfigs,
)?;
options_danger_button(
par.mp,
c,
"APP_SETTINGS.RESTART_SOFTWARE",
"@/dashboard/refresh.svg",
Task::RestartSoftware,
)?;
Ok(State {})
}
}
+13
View File
@@ -7,3 +7,16 @@ pub mod steam_utils;
pub mod toast_manager;
pub mod wgui_simple;
pub mod whisper;
// works good enough
pub fn is_exec_installed(exec: &str) -> bool {
let mut cmd = std::process::Command::new("sh");
cmd.arg("-c");
cmd.arg(format!("command -v {}", exec));
let Ok(res) = cmd.output() else {
return false;
};
res.status.success()
}
+10 -5
View File
@@ -81,7 +81,7 @@ impl BindingsDropdown for XrInputComponent {
pub struct ParsedOpenXrInputPath {
pub side: XrInputSide,
pub subpath: XrInputSubpathKind,
pub component: XrInputComponent,
pub component: Option<XrInputComponent>,
}
impl TryFrom<&str> for ParsedOpenXrInputPath {
@@ -101,13 +101,18 @@ impl TryFrom<&str> for ParsedOpenXrInputPath {
bail!("missing input prefix");
}
let (identifier, component) = rest.rsplit_once('/').context("missing identifier or component")?;
let (identifier, component) = match rest.rsplit_once('/') {
Some((ident, comp)) if !comp.is_empty() => {
let comp = XrInputComponent::try_from(comp).context("bad component")?;
(ident, Some(comp))
}
_ => (rest, None),
};
if identifier.is_empty() || component.is_empty() {
bail!("identifier or component empty");
if identifier.is_empty() {
bail!("identifier empty");
}
let component = XrInputComponent::try_from(component).context("bad component")?;
let subpath = XrInputSubpathKind::try_from(identifier).context("bad subpath")?;
Ok(Self {
+6 -1
View File
@@ -21,6 +21,9 @@ pub struct Volume {
#[serde(rename = "front-right")]
pub front_right: Option<VolumeChannel>,
#[serde(rename = "mono")]
pub mono: Option<VolumeChannel>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
@@ -207,7 +210,7 @@ pub fn list_sources() -> anyhow::Result<Vec<Source>> {
// exclude all monitor sources
sources.retain(|source| match &source.properties.device_class {
Some(c) => c != "monitor",
None => false,
None => true, /* unknown, pass it */
});
Ok(sources)
@@ -242,6 +245,8 @@ pub fn get_source_volume(source: &Source) -> anyhow::Result<f32> {
front_left
} else if let Some(aux0) = &source.volume.aux0 {
aux0
} else if let Some(mono) = &source.volume.mono {
mono
} else {
return Ok(0.0); // fail silently
}
+43 -2
View File
@@ -1,12 +1,16 @@
use glam::{Mat4, Vec2};
use std::rc::Rc;
use wgui::{
animation::{Animation, AnimationDuration, AnimationEasing},
assets::AssetPathRef,
color::WguiColorName,
components::{self, button::ButtonClickCallback},
components::{
self,
button::{ButtonClickCallback, ButtonClickEvent, ComponentButton},
},
i18n::Translation,
layout::{Layout, LayoutTask, WidgetID},
parser::{Fetchable, ParseDocumentParams},
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
renderer_vk::{
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
util::centered_matrix,
@@ -17,6 +21,7 @@ use wgui::{
label::{WidgetLabel, WidgetLabelParams},
sprite::{WidgetSprite, WidgetSpriteParams},
},
windowing::context_menu,
};
pub struct CreateButtonParams<'a> {
@@ -166,3 +171,39 @@ pub fn create_loading(par: CreateLoadingParams) -> anyhow::Result<WidgetID> {
Ok(id_root)
}
pub struct MountDropdownParams<'a> {
pub layout: &'a mut Layout,
pub parser_state: &'a mut ParserState,
pub doc_params: &'a ParseDocumentParams<'a>,
pub parent: WidgetID,
pub id: String,
pub params: TemplateParams,
pub current_text: Translation,
pub cells: Vec<context_menu::Cell>,
pub on_open: Box<dyn Fn(context_menu::Position, Vec<context_menu::Cell>)>,
}
pub fn mount_dropdown(par: MountDropdownParams) -> anyhow::Result<()> {
let mut params = par.params;
params.insert("id", &par.id);
par
.parser_state
.instantiate_template(par.doc_params, "DropdownButton", par.layout, par.parent, params)?;
{
let mut label = par
.parser_state
.fetch_widget_as::<WidgetLabel>(&par.layout.state, &format!("{}_value", par.id))?;
label.set_text_simple(&mut par.layout.state.globals.get(), par.current_text);
}
let btn = par.parser_state.fetch_component_as::<ComponentButton>(&par.id)?;
btn.on_click(Rc::new(move |_common, e: ButtonClickEvent| {
(par.on_open)(e.mouse_pos_absolute.into(), par.cells.clone());
Ok(())
}));
Ok(())
}
+21 -19
View File
@@ -53,7 +53,7 @@ struct LaunchParams<'a, T> {
pub struct View {
#[allow(dead_code)]
state: ParserState,
entry: DesktopEntry,
entry: Rc<DesktopEntry>,
tasks: Tasks<Task>,
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
@@ -80,7 +80,7 @@ pub struct View {
pub struct Params<'a> {
pub globals: &'a WguiGlobals,
pub entry: DesktopEntry,
pub entry: Rc<DesktopEntry>,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub config: &'a GeneralConfig,
@@ -119,10 +119,8 @@ impl View {
{
let mut label_exec = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_exec")?;
label_exec.set_text_simple(
&mut params.globals.get(),
Translation::from_raw_text_string(format!("{} {}", params.entry.exec_path, params.entry.exec_args)),
);
let command = shell_words::join(params.entry.exec_argv.iter().map(|x| x.as_ref()));
label_exec.set_text_simple(&mut params.globals.get(), Translation::from_raw_text_string(command));
}
let tasks = Tasks::new();
@@ -406,14 +404,19 @@ impl View {
env.push("ELECTRON_OZONE_PLATFORM_HINT=wayland".into());
}
let args = match params.compositor_mode {
AppCompositorMode::Cage => format!("-- {} {}", params.application.exec_path, params.application.exec_args),
AppCompositorMode::Native => params.application.exec_args.to_string(),
};
let exec = match params.compositor_mode {
AppCompositorMode::Cage => "cage".to_string(),
AppCompositorMode::Native => params.application.exec_path.to_string(),
let argv = match params.compositor_mode {
AppCompositorMode::Cage => ["cage", "--"]
.iter()
.copied()
.chain(params.application.exec_argv.iter().map(|x| x.as_ref()))
.map(|x| x.to_string())
.collect::<Vec<_>>(),
AppCompositorMode::Native => params
.application
.exec_argv
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>(),
};
let pos_mode = match params.pos_mode {
@@ -423,7 +426,7 @@ impl View {
};
let mut userdata = HashMap::new();
userdata.insert("desktop-entry".to_string(), serde_json::to_string(params.application)?);
userdata.insert("desktop-entry".into(), serde_json::to_string(params.application)?);
let resolution = Self::calculate_resolution(params.res_mode, params.orientation_mode);
@@ -432,12 +435,11 @@ impl View {
params.auto_start,
WvrProcessLaunchParams {
env,
exec,
argv,
name: params.application.app_name.to_string(),
args,
resolution,
pos_mode,
icon: params.application.icon_path.as_ref().map(|x| x.as_ref().to_string()),
icon: params.application.icon_path.as_deref().map(String::from),
userdata,
},
)?;
@@ -486,7 +488,7 @@ impl View {
pub fn mount_popup(
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
entry: DesktopEntry,
entry: Rc<DesktopEntry>,
popup: PopupHolder<View>,
on_app_pins_changed: Box<dyn Fn()>,
pinned_app: Option<PinnedApp>,
+12 -12
View File
@@ -1,7 +1,7 @@
use std::rc::Rc;
use wgui::{
assets::AssetPathRef,
assets::{AssetPathRef, AssetPathSource},
color::{WguiColor, WguiColorName},
components::{
self,
@@ -12,7 +12,7 @@ use wgui::{
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams, strip_path_as},
task::Tasks,
widget::{ConstructEssentials, rectangle::WidgetRectangle},
};
@@ -100,7 +100,7 @@ struct ProfileDisplayName {
struct SelectorCell {
key: String,
display_text: String,
icon_path: &'static str,
icon_path_builtin: &'static str,
}
struct MultiSelectorParams<'a> {
@@ -124,7 +124,7 @@ fn mount_multi_selector(params: MultiSelectorParams) -> anyhow::Result<()> {
params.ess,
components::button::Params {
text: Some(Translation::from_raw_text(&cell.display_text)),
sprite_src: Some(AssetPathRef::BuiltIn(cell.icon_path)),
sprite_src: Some(strip_path_as(AssetPathSource::BuiltIn, cell.icon_path_builtin).unwrap()),
color,
..Default::default()
},
@@ -402,7 +402,7 @@ struct MountDeviceSliderParams<'a> {
checked: bool,
muted: bool,
disp: Option<ProfileDisplayName>,
alt_desc: String,
device_desc: String,
}
fn push_popup_speakers_set_successfully(globals: &WguiGlobals, frontend_tasks: &FrontendTasks, name: &str) {
@@ -775,13 +775,13 @@ impl View {
par.insert("device_name", &disp.name);
par.insert("device_icon", disp.icon_path);
} else {
let icon_path = if params.alt_desc.contains("WiVRn") {
let icon_path = if params.device_desc.contains("WiVRn") {
"@/dashboard/wivrn_head_symbolic.svg"
} else {
"@/dashboard/binary.svg"
};
par.insert("device_name", &params.alt_desc);
par.insert("device_name", &params.device_desc);
par.insert("device_icon", icon_path);
}
@@ -865,7 +865,7 @@ impl View {
false
};
let alt_desc = sink.description.clone();
let device_desc = sink.description.clone();
let muted = sink.mute;
let control = Rc::new(ControlSink::new(self.tasks.clone(), self.on_update.clone(), sink));
@@ -877,7 +877,7 @@ impl View {
self.mount_device_slider(MountDeviceSliderParams {
checked,
disp,
alt_desc,
device_desc,
layout,
control,
muted,
@@ -905,7 +905,7 @@ impl View {
false
};
let alt_desc = source.description.clone();
let device_desc = source.description.clone();
let muted = source.mute;
let control = Rc::new(ControlSource::new(self.tasks.clone(), self.on_update.clone(), source));
@@ -917,7 +917,7 @@ impl View {
self.mount_device_slider(MountDeviceSliderParams {
checked,
disp,
alt_desc,
device_desc,
layout,
control,
muted,
@@ -975,7 +975,7 @@ impl View {
cells.push(SelectorCell {
key: profile_name.clone(),
display_text: disp_name.name,
icon_path: disp_name.icon_path,
icon_path_builtin: disp_name.icon_path,
});
}
+680 -107
View File
@@ -3,22 +3,18 @@ use std::rc::Rc;
use strum::EnumProperty;
use wgui::{
assets::AssetPathRef,
components::{
button::{ButtonClickEvent, ComponentButton},
slider::ComponentSlider,
},
components::{button::ComponentButton, slider::ComponentSlider},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
log::LogErr,
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu::{self, TickResult},
};
use wlx_common::{
config_io,
openxr_actions::{OneOrMany, OpenXrInputAction, OpenXrInputProfile, load_xr_input_profiles},
openxr_actions::{OneOrMany, OpenXrInputAction, OpenXrInputChordMember, OpenXrInputProfile, load_xr_input_profiles},
openxr_bindings_schema::{
DEFAULT_BUTTON_THRESHOLDS, XrControllerProfile, XrInputComponent, XrInputSide, XrInputSubpathKind,
},
@@ -39,8 +35,11 @@ use crate::{
enum Task {
Save,
Cancel,
OpenContextMenu(glam::Vec2, Vec<context_menu::Cell>),
OpenContextMenu(context_menu::Position, Vec<context_menu::Cell>),
UpdateThreshold(Rc<str>, XrInputSide, usize, f32),
AddChordMember(Rc<str>, XrInputSide),
RemoveChordMember(Rc<str>, XrInputSide, usize),
UpdateChordThreshold(Rc<str>, XrInputSide, usize, usize, f32),
}
pub struct Params<'a> {
@@ -100,53 +99,114 @@ impl ViewTrait for View {
};
threshold[i] = val;
log::warn!("UpdateThreshold {action_name} {side:?} {threshold:?}");
}
Task::AddChordMember(action_name, output_side) => {
if let Some(member) = default_chord_member(self.controller_profile, output_side) {
let cur_profile = &mut self.profiles[self.cur_profile_idx];
let action_mut = get_action_mut(cur_profile, &action_name);
action_chord_mut(action_mut, output_side)
.get_or_insert_default()
.push(member);
self.refresh(par.layout)?;
}
}
Task::RemoveChordMember(action_name, output_side, member_idx) => {
let cur_profile = &mut self.profiles[self.cur_profile_idx];
let action_mut = get_action_mut(cur_profile, &action_name);
let chord = action_chord_mut(action_mut, output_side);
let chord_empty = if let Some(members) = chord.as_mut()
&& member_idx < members.len()
{
members.remove(member_idx);
members.is_empty()
} else {
false
};
if chord_empty {
*chord = None;
}
self.refresh(par.layout)?;
}
Task::UpdateChordThreshold(action_name, output_side, member_idx, i, val) => {
let cur_profile = &mut self.profiles[self.cur_profile_idx];
let action_mut = get_action_mut(cur_profile, &action_name);
if let Some(member) = action_chord_mut(action_mut, output_side)
.as_mut()
.and_then(|members| members.get_mut(member_idx))
{
let mut threshold = member.threshold().unwrap_or(DEFAULT_BUTTON_THRESHOLDS);
threshold[i] = val;
set_chord_member_threshold(member, threshold);
}
}
}
}
// Dropdown handling
if let TickResult::Action(name) = self.context_menu.tick(par.layout, &mut self.parser_state)?
&& let (Some(action), Some(action_name), Some(side), Some(value)) = {
let mut s = name.splitn(4, ';');
(s.next(), s.next(), s.next(), s.next())
} {
if let TickResult::Action(name) = self.context_menu.tick(par.layout, &mut self.parser_state)? {
let parts = name.split(';').collect::<Vec<_>>();
let cur_profile = &mut self.profiles[self.cur_profile_idx];
let action_mut = get_action_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" => {
apply_subpath(side_mut, side, value, self.controller_profile);
}
"comp" => {
apply_comp(side_mut, side, value);
}
"click" => match value {
"triple" => {
action_mut.triple_click = Some(true);
action_mut.double_click = None;
match parts.as_slice() {
[action, action_name, side, value] => {
let action_mut = get_action_mut(cur_profile, action_name);
match *action {
"clear" => {
*action_binding_mut(action_mut, side) = None;
}
"subpath" => {
apply_subpath(
action_binding_mut(action_mut, side),
action_name,
side,
value,
self.controller_profile,
);
}
"comp" => {
apply_comp(action_binding_mut(action_mut, side), side, value);
}
"click" => apply_click_count(action_mut, value),
_ => log::warn!("Unknown action {action}"),
}
"double" => {
action_mut.triple_click = None;
action_mut.double_click = Some(true);
self.refresh(par.layout)?;
}
[kind, action_name, output_side, member_idx, value] => {
let (Ok(output_side), Ok(member_idx)) = (XrInputSide::try_from(*output_side), member_idx.parse::<usize>())
else {
return Ok(());
};
let action_mut = get_action_mut(cur_profile, action_name);
let Some(member) = action_chord_mut(action_mut, output_side)
.as_mut()
.and_then(|members| members.get_mut(member_idx))
else {
return Ok(());
};
match *kind {
"chord_hand" => {
if let Ok(side) = XrInputSide::try_from(*value) {
apply_chord_hand(member, side, self.controller_profile);
}
}
"chord_subpath" => {
if let Ok(subpath) = XrInputSubpathKind::try_from(*value) {
apply_chord_subpath(member, subpath, self.controller_profile);
}
}
"chord_comp" => {
if let Ok(component) = XrInputComponent::try_from(*value) {
apply_chord_component(member, component);
}
}
_ => log::warn!("Unknown chord action {kind}"),
}
_ => {
action_mut.triple_click = None;
action_mut.double_click = None;
}
},
_ => log::warn!("Unknown action {action}"),
self.refresh(par.layout)?;
}
_ => log::warn!("Malformed bindings context action: {name}"),
}
self.refresh(par.layout)?;
}
Ok(())
@@ -413,6 +473,172 @@ fn input_controls_for_action(
},
)?;
if &*action != "scroll" {
if is_side_agnostic_action(action.as_ref()) {
chord_controls_for_output(
mp,
parent,
&action,
XrInputSide::Left,
current.left_chord.as_deref(),
click_type,
profile,
false,
)?;
} else {
for output_side in [XrInputSide::Left, XrInputSide::Right] {
if profile.find_userpath(output_side).is_none() {
continue;
}
let chord = if matches!(output_side, XrInputSide::Right) {
current.right_chord.as_deref()
} else {
current.left_chord.as_deref()
};
chord_controls_for_output(mp, parent, &action, output_side, chord, click_type, profile, true)?;
}
}
}
Ok(())
}
fn is_side_agnostic_action(action: &str) -> bool {
matches!(action, "show_hide" | "toggle_dashboard")
}
fn chord_controls_for_output(
mp: &mut MacroParams,
parent: WidgetID,
action: &Rc<str>,
output_side: XrInputSide,
members: Option<&[OpenXrInputChordMember]>,
click_type: ClickType,
profile: &XrControllerProfile,
show_output_side: bool,
) -> anyhow::Result<()> {
let header = horiz_cell(mp.layout, parent)?;
if show_output_side {
wgui_simple::create_icon(
mp.layout,
header,
Vec2::new(24.0, 24.0),
AssetPathRef::BuiltIn(&format!("dashboard/hand_{}.svg", output_side.as_ref())),
)?;
wgui_simple::create_label(mp.layout, header, side_translation(output_side))?;
}
wgui_simple::create_label(
mp.layout,
header,
Translation::from_translation_key("APP_SETTINGS.BINDINGS.CHORD"),
)?;
clicks_dropdown(mp, header, action.clone(), click_type)?;
let tasks = mp.tasks.clone();
let action_for_add = action.clone();
wgui_simple::create_button(wgui_simple::CreateButtonParams {
id_parent: header,
layout: mp.layout,
content: Translation::from_translation_key("APP_SETTINGS.BINDINGS.CHORD_ADD_INPUT"),
icon_builtin: AssetPathRef::BuiltIn("dashboard/add.svg"),
on_click: Rc::new(move |_common, _event| {
tasks.push(Task::AddChordMember(action_for_add.clone(), output_side));
Ok(())
}),
})?;
for (member_idx, member) in members.unwrap_or_default().iter().enumerate() {
chord_member_controls(mp, parent, action, output_side, member_idx, member, profile)?;
}
Ok(())
}
fn chord_member_controls(
mp: &mut MacroParams,
parent: WidgetID,
action: &Rc<str>,
output_side: XrInputSide,
member_idx: usize,
member: &OpenXrInputChordMember,
profile: &XrControllerProfile,
) -> anyhow::Result<()> {
let row = horiz_cell(mp.layout, parent)?;
let parsed = ParsedOpenXrInputPath::try_from(member.path())
.log_warn(member.path())
.ok();
let physical_side = parsed.as_ref().map(|x| x.side).unwrap_or(output_side);
chord_hand_dropdown(mp, row, action.clone(), output_side, member_idx, profile, physical_side)?;
let available_subpaths: Rc<[XrInputSubpathKind]> = profile
.find_userpath(physical_side)
.map(|user_path| {
user_path
.paths
.iter()
.filter(|x| !x.kind.get_bool("Hidden").unwrap_or_default())
.map(|x| x.kind)
.collect::<Vec<_>>()
.into()
})
.unwrap_or_default();
chord_subpath_dropdown(
mp,
row,
action.clone(),
output_side,
member_idx,
available_subpaths,
parsed.as_ref().map(|x| x.subpath),
)?;
let available_components: Rc<[XrInputComponent]> = parsed
.as_ref()
.and_then(|parsed| {
profile
.find_userpath(parsed.side)
.and_then(|user_path| user_path.find_subpath(parsed.subpath))
})
.map(|subpath| subpath.components)
.unwrap_or_default()
.into();
chord_component_dropdown(
mp,
row,
action.clone(),
output_side,
member_idx,
available_components,
parsed.and_then(|x| x.component),
)?;
let tasks = mp.tasks.clone();
let action_for_remove = action.clone();
wgui_simple::create_button(wgui_simple::CreateButtonParams {
id_parent: row,
layout: mp.layout,
content: Translation::from_raw_text(""),
icon_builtin: AssetPathRef::BuiltIn("dashboard/trash.svg"),
on_click: Rc::new(move |_common, _event| {
tasks.push(Task::RemoveChordMember(
action_for_remove.clone(),
output_side,
member_idx,
));
Ok(())
}),
})?;
// put threshold slider on its own row
if parsed.and_then(|x| x.component).is_some_and(|x| x.is_analog()) {
chord_threshold_slider(mp, parent, action.clone(), output_side, member_idx, member.threshold())?;
}
Ok(())
}
@@ -437,13 +663,6 @@ fn input_controls_for_hand(mp: &mut MacroParams, par: InputControlsForHandParams
let parent = horiz_cell(mp.layout, par.parent)?;
let available_components: Rc<[XrInputComponent]> = current
.as_ref()
.and_then(|par| user_path.find_subpath(par.subpath))
.map(|subp| subp.components)
.unwrap_or_default()
.into();
let available_subpaths: Rc<[XrInputSubpathKind]> = user_path
.paths
.iter()
@@ -460,23 +679,33 @@ fn input_controls_for_hand(mp: &mut MacroParams, par: InputControlsForHandParams
current.as_ref().map(|x| x.subpath),
)?;
if &**par.action == "scroll" {
clicks_dropdown(mp, parent, par.action.clone(), par.click_type)?;
return Ok(());
}
let available_components: Rc<[XrInputComponent]> = current
.as_ref()
.and_then(|par| user_path.find_subpath(par.subpath))
.map(|subp| subp.components)
.unwrap_or_default()
.into();
if !component_dropdown(
mp,
parent,
par.action.clone(),
par.side,
available_components,
current.as_ref().map(|x| x.component),
current.and_then(|x| x.component),
)? {
return Ok(());
}
clicks_dropdown(mp, parent, par.action.clone(), par.click_type)?;
if let Some(component) = current.as_ref().map(|x| x.component)
if let Some(component) = current.and_then(|x| x.component)
&& component.is_analog()
&& &**par.action != "scroll"
// hax
{
threshold_slider(mp, parent, par.action.clone(), par.side, par.threshold)?;
}
@@ -552,6 +781,181 @@ fn clicks_dropdown(mp: &mut MacroParams, parent: WidgetID, action: Rc<str>, curr
Ok(())
}
fn side_translation(side: XrInputSide) -> Translation {
Translation::from_translation_key(match side {
XrInputSide::Left => "APP_SETTINGS.BINDINGS.LEFT",
XrInputSide::Right => "APP_SETTINGS.BINDINGS.RIGHT",
})
}
fn chord_hand_dropdown(
mp: &mut MacroParams,
parent: WidgetID,
action: Rc<str>,
output_side: XrInputSide,
member_idx: usize,
profile: &XrControllerProfile,
current: XrInputSide,
) -> anyhow::Result<()> {
let mut params = TemplateParams::new();
params.insert("tooltip", "APP_SETTINGS.BINDINGS.CHORD_INPUT_HAND");
params.insert("min_width", "100");
let cells = [XrInputSide::Left, XrInputSide::Right]
.into_iter()
.filter(|side| profile.find_userpath(*side).is_some())
.map(|side| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(
format!(
"chord_hand;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
side.as_ref()
)
.into(),
),
title: side_translation(side),
..Default::default()
})
})
.collect();
create_dropdown_with_cells(mp, parent, params, side_translation(current), cells)
}
fn chord_subpath_dropdown(
mp: &mut MacroParams,
parent: WidgetID,
action: Rc<str>,
output_side: XrInputSide,
member_idx: usize,
available: Rc<[XrInputSubpathKind]>,
current: Option<XrInputSubpathKind>,
) -> anyhow::Result<()> {
let mut params = TemplateParams::new();
params.insert("tooltip", "APP_SETTINGS.BINDINGS.SUBPATH");
params.insert("min_width", "100");
let current_text = current
.map(|subpath| subpath.translation())
.unwrap_or_else(|| Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"));
let cells = available
.iter()
.map(|subpath| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(
format!(
"chord_subpath;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
subpath.as_ref()
)
.into(),
),
title: subpath.translation(),
..Default::default()
})
})
.collect();
create_dropdown_with_cells(mp, parent, params, current_text, cells)
}
fn chord_component_dropdown(
mp: &mut MacroParams,
parent: WidgetID,
action: Rc<str>,
output_side: XrInputSide,
member_idx: usize,
available: Rc<[XrInputComponent]>,
current: Option<XrInputComponent>,
) -> anyhow::Result<bool> {
if available.is_empty() {
return Ok(false);
}
let mut params = TemplateParams::new();
params.insert("text", "・");
params.insert("tooltip", "APP_SETTINGS.BINDINGS.COMPONENT");
params.insert("min_width", "100");
let current_text = current
.map(|component| component.translation())
.unwrap_or_else(|| Translation::from_raw_text_rc(Default::default()));
let cells = available
.iter()
.map(|component| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(
format!(
"chord_comp;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
component.as_ref()
)
.into(),
),
title: component.translation(),
..Default::default()
})
})
.collect();
create_dropdown_with_cells(mp, parent, params, current_text, cells)?;
Ok(true)
}
fn chord_threshold_slider(
mp: &mut MacroParams,
parent: WidgetID,
action: Rc<str>,
output_side: XrInputSide,
member_idx: usize,
current: Option<[f32; 2]>,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
let current = current.unwrap_or(DEFAULT_BUTTON_THRESHOLDS);
let mut params = TemplateParams::new();
params.insert("id", &id);
params.insert("tooltip", "APP_SETTINGS.BINDINGS.THRESHOLD");
params.insert_str("value", format!("{:.2}", current[0]));
params.insert_str("value2", format!("{:.2}", current[1]));
params.insert("min", "0.0");
params.insert("max", "1.0");
params.insert("step", "0.1");
mp.parser_state
.instantiate_template(mp.doc_params, "ThresholdSlider", mp.layout, parent, params)?;
let slider = mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
let tasks = mp.tasks.clone();
move |_common, e| {
let threshold_idx = if matches!(e.index, wgui::components::slider::ValueIndex::Primary) {
0
} else {
1
};
tasks.push(Task::UpdateChordThreshold(
action.clone(),
output_side,
member_idx,
threshold_idx,
e.value,
));
}
}));
Ok(())
}
fn threshold_slider(
mp: &mut MacroParams,
parent: WidgetID,
@@ -594,67 +998,63 @@ fn threshold_slider(
fn create_dropdown<B: 'static + BindingsDropdown>(
mp: &mut MacroParams,
parent: WidgetID,
mut params: TemplateParams,
params: TemplateParams,
action: Rc<str>,
side: XrInputSide,
current_text: Translation,
available: Rc<[B]>,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
params.insert("id", &id);
let mut cells = available
.iter()
.map(|item| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(item.action_str(&action, side).into()),
title: item.translation(),
..Default::default()
})
})
.collect::<Vec<_>>();
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
{
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(), current_text);
if let Some(action_str) = B::clear_str(&action, side) {
cells.insert(
0,
context_menu::Cell::Button(context_menu::Button {
action_name: Some(action_str.into()),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
..Default::default()
}),
);
}
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| {
let mut cells = available
.iter()
.map(|item| {
let title = item.translation();
create_dropdown_with_cells(mp, parent, params, current_text, cells)
}
context_menu::Cell {
action_name: Some(item.action_str(&action, side)),
title,
tooltip: None,
attribs: vec![],
}
})
.collect::<Vec<_>>();
if let Some(action_str) = B::clear_str(&action, side) {
cells.insert(
0,
context_menu::Cell {
action_name: Some(action_str),
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 create_dropdown_with_cells(
mp: &mut MacroParams,
parent: WidgetID,
params: TemplateParams,
current_text: Translation,
cells: Vec<context_menu::Cell>,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
let tasks = mp.tasks.clone();
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
layout: mp.layout,
parser_state: mp.parser_state,
doc_params: mp.doc_params,
parent,
id,
params,
current_text,
cells,
on_open: Box::new(move |pos, cells| tasks.push(Task::OpenContextMenu(pos, cells))),
})
}
fn apply_subpath(
side_mut: &mut Option<OneOrMany<String>>,
action_name: &str,
side_str: &str,
subpath_str: &str,
profile: &XrControllerProfile,
@@ -665,6 +1065,12 @@ fn apply_subpath(
) else {
return;
};
if action_name == "scroll" {
*side_mut = Some(OneOrMany::One(format!("/user/hand/{side_str}/input/{subpath_str}")));
return;
}
let Some(subpath_obj) = profile.find_userpath(side).and_then(|p| p.find_subpath(subpath)) else {
return;
};
@@ -677,11 +1083,12 @@ fn apply_subpath(
return;
};
let mut parsed_compo = parsed.component;
if !subpath_obj.components.contains(&parsed_compo) {
parsed_compo = *subpath_obj.components.first().unwrap();
let parsed_compo = parsed.component.unwrap_or(*subpath_obj.components.first().unwrap());
if subpath_obj.components.contains(&parsed_compo) {
parsed_compo
} else {
*subpath_obj.components.first().unwrap()
}
parsed_compo
} else {
*subpath_obj.components.first().unwrap()
};
@@ -710,3 +1117,169 @@ fn apply_comp(side_mut: &mut Option<OneOrMany<String>>, side: &str, comp: &str)
*side_mut = Some(OneOrMany::One(format!("/user/hand/{side}/input/{subpath}/{comp}")));
}
fn action_chord_mut(
action: &mut OpenXrInputAction,
output_side: XrInputSide,
) -> &mut Option<Vec<OpenXrInputChordMember>> {
match output_side {
XrInputSide::Left => &mut action.left_chord,
XrInputSide::Right => &mut action.right_chord,
}
}
fn action_binding_mut<'a>(action: &'a mut OpenXrInputAction, side: &str) -> &'a mut Option<OneOrMany<String>> {
if side == "right" {
&mut action.right
} else {
&mut action.left
}
}
fn apply_click_count(action: &mut OpenXrInputAction, value: &str) {
match value {
"triple" => {
action.triple_click = Some(true);
action.double_click = None;
}
"double" => {
action.triple_click = None;
action.double_click = Some(true);
}
_ => {
action.triple_click = None;
action.double_click = None;
}
}
}
fn default_chord_member(profile: &XrControllerProfile, preferred_side: XrInputSide) -> Option<OpenXrInputChordMember> {
let other_side = match preferred_side {
XrInputSide::Left => XrInputSide::Right,
XrInputSide::Right => XrInputSide::Left,
};
for side in [preferred_side, other_side] {
let Some(user_path) = profile.find_userpath(side) else {
continue;
};
let Some(subpath) = user_path
.paths
.iter()
.find(|subpath| !subpath.kind.get_bool("Hidden").unwrap_or_default() && !subpath.components.is_empty())
else {
continue;
};
let component = *subpath.components.first()?;
return Some(OpenXrInputChordMember::Path(format!(
"/user/hand/{}/input/{}/{}",
side.as_ref(),
subpath.kind.as_ref(),
component.as_ref()
)));
}
None
}
fn set_chord_member_path(member: &mut OpenXrInputChordMember, path: String, component: XrInputComponent) {
let threshold = if component.is_analog() {
member.threshold()
} else {
None
};
*member = if threshold.is_some() {
OpenXrInputChordMember::Detailed { path, threshold }
} else {
OpenXrInputChordMember::Path(path)
};
}
fn set_chord_member_threshold(member: &mut OpenXrInputChordMember, threshold: [f32; 2]) {
*member = OpenXrInputChordMember::Detailed {
path: member.path().to_owned(),
threshold: Some(threshold),
};
}
fn apply_chord_hand(member: &mut OpenXrInputChordMember, side: XrInputSide, profile: &XrControllerProfile) {
let parsed = ParsedOpenXrInputPath::try_from(member.path()).ok();
let Some(user_path) = profile.find_userpath(side) else {
return;
};
let subpath = parsed
.and_then(|parsed| user_path.find_subpath(parsed.subpath))
.filter(|subpath| !subpath.kind.get_bool("Hidden").unwrap_or_default() && !subpath.components.is_empty())
.or_else(|| {
user_path
.paths
.iter()
.find(|subpath| !subpath.kind.get_bool("Hidden").unwrap_or_default() && !subpath.components.is_empty())
});
let Some(subpath) = subpath else {
return;
};
let component = parsed
.and_then(|parsed| parsed.component)
.filter(|component| subpath.components.contains(component))
.unwrap_or(subpath.components[0]);
let path = format!(
"/user/hand/{}/input/{}/{}",
side.as_ref(),
subpath.kind.as_ref(),
component.as_ref()
);
set_chord_member_path(member, path, component);
}
fn apply_chord_subpath(
member: &mut OpenXrInputChordMember,
subpath: XrInputSubpathKind,
profile: &XrControllerProfile,
) {
let Ok(parsed) = ParsedOpenXrInputPath::try_from(member.path()) else {
return;
};
let Some(subpath_obj) = profile
.find_userpath(parsed.side)
.and_then(|user_path| user_path.find_subpath(subpath))
else {
return;
};
let Some(first_component) = subpath_obj.components.first().copied() else {
return;
};
let component = if let Some(comp) = parsed.component {
if subpath_obj.components.contains(&comp) {
comp
} else {
first_component
}
} else {
first_component
};
let path = format!(
"/user/hand/{}/input/{}/{}",
parsed.side.as_ref(),
subpath.as_ref(),
component.as_ref()
);
set_chord_member_path(member, path, component);
}
fn apply_chord_component(member: &mut OpenXrInputChordMember, component: XrInputComponent) {
let Ok(parsed) = ParsedOpenXrInputPath::try_from(member.path()) else {
return;
};
let path = format!(
"/user/hand/{}/input/{}/{}",
parsed.side.as_ref(),
parsed.subpath.as_ref(),
component.as_ref()
);
set_chord_member_path(member, path, component);
}
+22 -3
View File
@@ -32,6 +32,21 @@ enum Task {
ActionClicked(&'static str),
}
pub fn get_entries_yes_no() -> Vec<ButtonEntry> {
vec![
ButtonEntry {
content: Translation::from_translation_key("YES"),
icon: "@/dashboard/check.svg",
action: "yes",
},
ButtonEntry {
content: Translation::from_translation_key("NO"),
icon: "@/dashboard/cancel.svg",
action: "no",
},
]
}
pub struct View {
tasks: Tasks<Task>,
@@ -52,6 +67,13 @@ fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
impl ViewTrait for View {
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
self.tick();
Ok(())
}
}
impl View {
pub fn tick(&mut self) {
for task in self.tasks.drain() {
match task {
Task::ActionClicked(action) => {
@@ -65,11 +87,8 @@ impl ViewTrait for View {
}
}
}
Ok(())
}
}
impl View {
pub fn new(
layout: &mut Layout,
id_parent: WidgetID,
+2 -2
View File
@@ -92,7 +92,7 @@ impl ViewTrait for View {
par.layout,
self.id_loading_parent,
Vec2::splat(32.0),
AssetPathRef::BuiltIn("@/dashboard/check.svg"),
AssetPathRef::BuiltIn("dashboard/check.svg"),
)?;
// "Close window" button
@@ -114,7 +114,7 @@ impl ViewTrait for View {
par.layout,
self.id_loading_parent,
Vec2::splat(32.0),
AssetPathRef::BuiltIn("@/dashboard/error.svg"),
AssetPathRef::BuiltIn("dashboard/error.svg"),
)?;
}
Task::Close => {
+34 -4
View File
@@ -1,6 +1,32 @@
#!/bin/bash
# Do not run this script if you don't have at least 32 GiB of system memory.
# This is the translation tool intended for machine-translating strings.
#
# ###########################
# # Adding new translations #
# ###########################
#
# If you want to add a new language, follow these steps:
#
# 1. Include the language in `wlx-common/src/locale.rs`:
# - add your entry in `enum Language`
# - add your entry in Language::code() function
# - add your language code in Language::add_codes() function
#
# 2. Create a new template file
# - Add your language code in `scripts/translator/templates/<lang code>.json`
# (you can copy one of nearby files and adapt them accordingly)
#
# 3. Modify this very file
# - Go to the bottom and add your language code entry here too
#
# 4. Run the script (it will hint you which LANG_PATHs are supported)
#
# ############################################
# # Running machine translations (local LLM) #
# ############################################
#
# Do not run this script if you don't have at least ~16 GiB of free system memory.
# Submitting PRs that lack translated strings is okay if you don't meet the
# system requirements to run this script, or if you simply prefer not to; we are
# regularly updating the missing translation strings anyway.
@@ -8,15 +34,18 @@
# Base language: English (en.json)
#
# This tool expects llama.cpp server being present (llama-server) at port 8080.
#
# Currently we're using Gemma 4 for machine translations.
# Model URL:
# https://huggingface.co/unsloth/gemma-4-31B-it-GGUF
# Recommended quantization: Q4_K_M
#
set -e
cd "$(dirname "$0")"
bun install
# model URL:
# https://huggingface.co/unsloth/gemma-4-31B-it-GGUF
# you can specify your own if you want to.
if [ -z "${MODEL}" ]; then
export MODEL="gemma-4-31B-it-Q4_K_M"
fi
@@ -31,4 +60,5 @@ TEMPLATE="de" bun main.ts
TEMPLATE="ja" bun main.ts
TEMPLATE="es" bun main.ts
TEMPLATE="it" bun main.ts
TEMPLATE="sv" bun main.ts
TEMPLATE="zh_CN" bun main.ts
+20
View File
@@ -0,0 +1,20 @@
{
"full_name": "Swedish",
"examples": [
{
"key": "BAR.OPACITY",
"en": "Opacity",
"translated": "Opacitet"
},
{
"key": "PANEL.CLOSE",
"en": "Close panel",
"translated": "Stäng panelen"
},
{
"key": "SETTINGS.LEFT_HANDED_MODE",
"en": "Left-handed mode",
"translated": "Vänsterhänt läge"
}
]
}
-2
View File
@@ -17,7 +17,5 @@ glam.workspace = true
log.workspace = true
rust-embed.workspace = true
tracing-subscriber.workspace = true
vulkano.workspace = true
vulkano-shaders.workspace = true
winit.workspace = true
ureq = "3.3.0"
+48 -144
View File
@@ -6,20 +6,9 @@ use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use vulkan::init_window;
use vulkano::{
Validated, VulkanError,
command_buffer::CommandBufferUsage,
format::Format,
image::{ImageUsage, view::ImageView},
swapchain::{
ColorSpace, CompositeAlpha, PresentMode, Surface, SurfaceInfo, Swapchain, SwapchainCreateInfo,
SwapchainPresentInfo, acquire_next_image,
},
sync::GpuFuture,
};
use wgui::{
event::{MouseButtonEvent, MouseButtonIndex, MouseMotionEvent, MouseWheelEvent},
gfx::{WGfx, cmd::WGfxClearMode},
gfx::{CommandBufferUsage, PresentStatus, SwapchainError, WGfx, WGfxClearMode},
renderer_vk::{self},
};
use winit::{
@@ -59,13 +48,16 @@ fn init_logging() {
.init();
}
fn load_testbed(audio_sample_player: &mut audio::SamplePlayer) -> anyhow::Result<Box<dyn Testbed>> {
fn load_testbed(
gfx: &Arc<WGfx>,
audio: &mut Box<dyn audio::AudioProvider>,
) -> anyhow::Result<Box<dyn Testbed>> {
let mut assets = Box::new(assets::Asset {});
audio_sample_player.register_wgui_samples(assets.as_mut())?;
audio.register_wgui_samples(assets.as_mut())?;
let name = std::env::var("TESTBED").unwrap_or_default();
Ok(match name.as_str() {
"dashboard" => Box::new(TestbedDashboard::new()?),
"dashboard" => Box::new(TestbedDashboard::new(gfx.clone())?),
"" => Box::new(TestbedGeneric::new(assets)?),
_ => Box::new(TestbedAny::new(assets, &name)?),
})
@@ -74,41 +66,19 @@ fn load_testbed(audio_sample_player: &mut audio::SamplePlayer) -> anyhow::Result
fn main() -> Result<(), Box<dyn std::error::Error>> {
init_logging();
let (gfx, event_loop, window, surface, color_space) =
let (gfx, event_loop, window) =
init_window("[-/=]: gui scale, F10: debug draw, F11: print tree")?;
let inner_size = window.inner_size();
let mut swapchain_size = [inner_size.width, inner_size.height];
let mut swapchain_create_info = swapchain_create_info(
&gfx,
gfx.surface_format,
color_space,
surface.clone(),
swapchain_size,
);
let (mut swapchain, mut images) = {
let (swapchain, images) = Swapchain::new(
gfx.device.clone(),
surface.clone(),
swapchain_create_info.clone(),
)?;
let image_views = images
.into_iter()
.map(|image| ImageView::new_default(image).unwrap())
.collect::<Vec<_>>();
(swapchain, image_views)
};
let mut swapchain = gfx.create_swapchain(swapchain_size)?;
swapchain_size = swapchain.extent();
let mut recreate = false;
let mut scale = window.scale_factor() as f32;
let mut audio_system = audio::AudioSystem::new();
let mut audio_sample_player = audio::SamplePlayer::new();
let mut testbed = load_testbed(&mut audio_sample_player)?;
let mut audio: Box<dyn audio::AudioProvider> = Box::new(audio::RealAudioProvider::new());
let mut testbed = load_testbed(&gfx, &mut audio)?;
let mut mouse = Vec2::ZERO;
@@ -281,20 +251,21 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} => {
if recreate {
let inner_size = window.inner_size();
swapchain_size = [inner_size.width, inner_size.height];
if inner_size.width == 0 || inner_size.height == 0 {
return;
}
swapchain_create_info.image_extent = swapchain_size;
(swapchain, images) = {
let (swapchain, images) = swapchain.recreate(swapchain_create_info.clone()).unwrap();
let image_views = images
.into_iter()
.map(|image| ImageView::new_default(image).unwrap())
.collect::<Vec<_>>();
(swapchain, image_views)
};
let requested_size = [inner_size.width, inner_size.height];
match swapchain.recreate(requested_size) {
Ok(new_swapchain) => {
swapchain = new_swapchain;
swapchain_size = swapchain.extent();
}
Err(e) => {
log::error!("failed to recreate swapchain: {e:#}");
return;
}
}
render_context
.update_viewport(&mut shared_context, swapchain_size, scale)
@@ -314,8 +285,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
width: (swapchain_size[0] as f32 / scale) as _,
height: (swapchain_size[1] as f32 / scale) as _,
timestep_alpha: timestep.alpha,
audio_system: &mut audio_system,
audio_sample_player: &mut audio_sample_player,
audio: &mut audio,
})
.unwrap();
@@ -332,20 +302,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
profiler.start();
{
let (image_index, _, acquire_future) =
match acquire_next_image(swapchain.clone(), None).map_err(Validated::unwrap) {
Ok(r) => r,
Err(VulkanError::OutOfDate) => {
recreate = true;
return;
}
Err(e) => {
log::error!("failed to acquire next image: {e}");
return;
}
};
let frame = match swapchain.acquire() {
Ok(frame) => frame,
Err(SwapchainError::OutOfDate) => {
recreate = true;
return;
}
Err(e) => {
log::error!("failed to acquire next image: {e}");
return;
}
};
let tgt = images[image_index as usize].clone();
let tgt = frame.image_view().clone();
let mut cmd_buf = gfx
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)
@@ -388,17 +357,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let cmd_buf = cmd_buf.build().unwrap();
acquire_future
.then_execute(gfx.queue_gfx.clone(), cmd_buf)
.unwrap()
.then_swapchain_present(
gfx.queue_gfx.clone(),
SwapchainPresentInfo::swapchain_image_index(swapchain.clone(), image_index),
)
.then_signal_fence_and_flush()
.unwrap()
.wait(None)
.unwrap();
match frame.present(&cmd_buf) {
Ok(PresentStatus::Optimal) => {}
Ok(PresentStatus::Suboptimal) | Err(SwapchainError::OutOfDate) => {
recreate = true;
}
Err(e) => {
log::error!("failed to present swapchain image: {e}");
}
}
}
profiler.end();
@@ -414,66 +381,3 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn swapchain_create_info(
graphics: &WGfx,
format: Format,
color_space: ColorSpace,
surface: Arc<Surface>,
extent: [u32; 2],
) -> SwapchainCreateInfo {
let surface_capabilities = graphics
.device
.physical_device()
.surface_capabilities(&surface, SurfaceInfo::default())
.unwrap(); // want panic
let mut composite_alpha = None;
for c in [
CompositeAlpha::PreMultiplied,
CompositeAlpha::PostMultiplied,
CompositeAlpha::Opaque,
] {
if surface_capabilities
.supported_composite_alpha
.contains_enum(c)
{
composite_alpha = Some(c);
break;
}
log::warn!(
"GPU driver doesn't support {c:?} compositeAlpha! Desktop window will be blended using a fallback method and may look different than in VR."
);
}
let composite_alpha = composite_alpha
.expect("GPU driver issue: VkSurfaceCapabilitiesKHR has empty supportedCompositeAlpha.");
let present_modes = graphics
.device
.physical_device()
.surface_present_modes(&surface, SurfaceInfo::default())
.expect("Could not get GPU present modes for VKSurface.");
let mut present_mode = None;
for pm in [PresentMode::Mailbox, PresentMode::Fifo] {
if present_modes.contains(&pm) {
present_mode = Some(pm);
break;
}
log::warn!(
"GPU driver doesn't support {pm:?} presentMode! Desktop window may have a higher latency and/or glitch during grab/resize."
);
}
let present_mode = present_mode.expect("GPU driver issue: VkPresentModeKHR FIFO is not supported even though it's required by Vulkan spec.");
SwapchainCreateInfo {
min_image_count: surface_capabilities.min_image_count.max(2),
present_mode,
image_format: format,
image_extent: extent,
image_usage: ImageUsage::COLOR_ATTACHMENT,
image_color_space: color_space,
composite_alpha,
..Default::default()
}
}
+2 -5
View File
@@ -9,15 +9,12 @@ pub struct TestbedUpdateParams<'a> {
pub width: f32,
pub height: f32,
pub timestep_alpha: f32,
pub audio_system: &'a mut audio::AudioSystem,
pub audio_sample_player: &'a mut audio::SamplePlayer,
pub audio: &'a mut Box<dyn audio::AudioProvider>,
}
impl<'a> TestbedUpdateParams<'a> {
pub fn process_layout_result(&mut self, res: LayoutUpdateResult) {
self
.audio_sample_player
.play_wgui_samples(self.audio_system, res.sounds_to_play);
self.audio.play_wgui_samples(res.sounds_to_play);
}
}
+6 -7
View File
@@ -1,8 +1,8 @@
use std::rc::Rc;
use std::{rc::Rc, sync::Arc};
use crate::testbed::{Testbed, TestbedUpdateParams};
use dash_frontend::frontend::{self, FrontendUpdateParams};
use wgui::{layout::Layout, theme::WguiTheme};
use wgui::{gfx::WGfx, i18n::Locale, layout::Layout, theme::WguiTheme};
use wlx_common::{
async_executor::AsyncExecutor, dash_interface_emulated::DashInterfaceEmulated,
locale::WayVRLangProvider,
@@ -14,9 +14,10 @@ pub struct TestbedDashboard {
}
impl TestbedDashboard {
pub fn new() -> anyhow::Result<Self> {
let interface = DashInterfaceEmulated::new();
pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> {
let lang_provider = WayVRLangProvider::default();
let locale = Locale::from_env(&lang_provider);
let interface = DashInterfaceEmulated::new(gfx, &locale);
let palette_name = std::env::var("PALETTE").unwrap_or_else(|_| "Default".to_string());
let executor = wlx_common::async_executor::create_local();
@@ -41,9 +42,7 @@ impl Testbed for TestbedDashboard {
height: params.height,
timestep_alpha: params.timestep_alpha,
})?;
self
.frontend
.process_update(res, params.audio_system, params.audio_sample_player)?;
self.frontend.process_update(res, params.audio)?;
while self.executor.try_tick() {}
Ok(())
+6 -4
View File
@@ -33,7 +33,7 @@ use wlx_common::locale::WayVRLangProvider;
#[derive(Clone)]
pub enum TestbedTask {
ShowPopup,
ShowContextMenu(Vec2),
ShowContextMenu(context_menu::Position),
}
struct Data {
@@ -196,7 +196,7 @@ impl TestbedGeneric {
button_context_menu.on_click({
let tasks = testbed.tasks.clone();
Rc::new(move |_common, m| {
tasks.push(TestbedTask::ShowContextMenu(m.boundary.bottom_left()));
tasks.push(TestbedTask::ShowContextMenu(m.boundary.into()));
Ok(())
})
});
@@ -212,7 +212,9 @@ impl TestbedGeneric {
) -> anyhow::Result<()> {
match task {
TestbedTask::ShowPopup => self.show_popup(params, data)?,
TestbedTask::ShowContextMenu(position) => self.show_context_menu(params, data, *position),
TestbedTask::ShowContextMenu(position) => {
self.show_context_menu(params, data, position.clone())
}
}
Ok(())
@@ -251,7 +253,7 @@ impl TestbedGeneric {
&mut self,
_params: &mut TestbedUpdateParams,
data: &mut Data,
position: Vec2,
position: context_menu::Position,
) {
data.context_menu.open(context_menu::OpenParams {
on_custom_attribs: Some(Rc::new(move |custom_attribs| {
+13 -197
View File
@@ -1,46 +1,11 @@
use anyhow::Context;
use std::sync::{Arc, OnceLock};
use vulkano::{
device::{
Device, DeviceCreateInfo, DeviceExtensions, DeviceFeatures, Queue, QueueCreateInfo, QueueFlags,
physical::{PhysicalDevice, PhysicalDeviceType},
},
instance::{Instance, InstanceCreateInfo},
swapchain::ColorSpace,
};
use wgui::gfx::WGfx;
use std::sync::Arc;
static VULKAN_LIBRARY: OnceLock<Arc<vulkano::VulkanLibrary>> = OnceLock::new();
fn get_vulkan_library() -> &'static Arc<vulkano::VulkanLibrary> {
VULKAN_LIBRARY.get_or_init(|| vulkano::VulkanLibrary::new().unwrap()) // want panic
}
use wgui::gfx::{WGfx, WGfxCreateInfo};
use winit::{event_loop::EventLoop, window::Window};
#[allow(clippy::type_complexity)]
pub fn init_window(
title: &str,
) -> anyhow::Result<(
Arc<WGfx>,
winit::event_loop::EventLoop<()>,
Arc<winit::window::Window>,
Arc<vulkano::swapchain::Surface>,
ColorSpace,
)> {
use vulkano::{instance::InstanceCreateFlags, swapchain::Surface};
use winit::{event_loop::EventLoop, window::Window};
pub fn init_window(title: &str) -> anyhow::Result<(Arc<WGfx>, EventLoop<()>, Arc<Window>)> {
let event_loop = EventLoop::new().unwrap(); // want panic
let mut vk_instance_extensions = Surface::required_extensions(&event_loop).unwrap();
vk_instance_extensions.khr_get_physical_device_properties2 = true;
log::debug!("Instance exts for runtime: {:?}", vk_instance_extensions);
let instance = Instance::new(
get_vulkan_library().clone(),
InstanceCreateInfo {
flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
enabled_extensions: vk_instance_extensions,
..Default::default()
},
)?;
#[allow(deprecated)]
let window = Arc::new(
@@ -52,169 +17,20 @@ pub fn init_window(
)
.unwrap(), // want panic
);
let surface = Surface::from_window(instance.clone(), window.clone())?;
let mut device_extensions = DeviceExtensions::empty();
device_extensions.khr_swapchain = true;
log::debug!("Device exts for app: {:?}", device_extensions);
let (physical_device, mut my_extensions, queue_families) = instance
.enumerate_physical_devices()?
.filter_map(|p| {
if p.supported_extensions().contains(&device_extensions) {
Some((p, device_extensions))
} else {
log::debug!(
"Not using {} because it does not implement the following device extensions:",
p.properties().device_name,
);
for (ext, missing) in p.supported_extensions().difference(&device_extensions) {
if missing {
log::debug!(" {ext}");
}
}
None
}
})
.filter_map(|(p, my_extensions)| {
try_all_queue_families(p.as_ref()).map(|families| (p, my_extensions, families))
})
.min_by_key(|(p, _, _)| prio_from_device_type(p))
.expect("no suitable physical device found");
log::info!(
"Using vkPhysicalDevice: {}",
physical_device.properties().device_name,
);
if physical_device.supported_extensions().img_filter_cubic {
my_extensions.img_filter_cubic = true;
log::info!("img_filter_cubic!");
}
let (format, color_space) = physical_device
.surface_formats(&surface, vulkano::swapchain::SurfaceInfo::default())?
.into_iter()
.find(|(format, _)| !format.components().iter().any(|b| *b > 8))
.context("Could not read or no suitable surface formats for PhysicalDevice")?;
let (device, queues) = Device::new(
physical_device,
DeviceCreateInfo {
enabled_extensions: my_extensions,
enabled_features: DeviceFeatures {
dynamic_rendering: true,
descriptor_binding_sampled_image_update_after_bind: true,
..DeviceFeatures::empty()
},
queue_create_infos: queue_families
.iter()
.map(|fam| QueueCreateInfo {
queue_family_index: fam.queue_family_index,
queues: fam.priorities.clone(),
..Default::default()
})
.collect::<Vec<_>>(),
let gfx = WGfx::new_for_window(
window.as_ref(),
&WGfxCreateInfo {
application_name: "uidev".to_owned(),
engine_name: "wgui".to_owned(),
..Default::default()
},
)?;
let (queue_gfx, queue_xfer, _) = unwrap_queues(queues.collect());
let me = WGfx::new_from_raw(instance, device, queue_gfx, queue_xfer, format);
Ok((me, event_loop, window, surface, color_space))
}
#[derive(Debug)]
struct QueueFamilyLayout {
queue_family_index: u32,
priorities: Vec<f32>,
}
fn prio_from_device_type(physical_device: &PhysicalDevice) -> u32 {
match physical_device.properties().device_type {
PhysicalDeviceType::DiscreteGpu => 0,
PhysicalDeviceType::IntegratedGpu => 1,
PhysicalDeviceType::VirtualGpu => 2,
PhysicalDeviceType::Cpu => 3,
_ => 4,
}
}
fn unwrap_queues(queues: Vec<Arc<Queue>>) -> (Arc<Queue>, Arc<Queue>, Option<Arc<Queue>>) {
match queues[..] {
[ref g, ref t, ref c] => (g.clone(), t.clone(), Some(c.clone())),
[ref gt, ref c] => (gt.clone(), gt.clone(), Some(c.clone())),
[ref gt] => (gt.clone(), gt.clone(), None),
_ => unreachable!(),
}
}
fn try_all_queue_families(physical_device: &PhysicalDevice) -> Option<Vec<QueueFamilyLayout>> {
queue_families_priorities(
physical_device,
vec![
// main-thread graphics + uploads
QueueFlags::GRAPHICS | QueueFlags::TRANSFER,
// capture-thread uploads
QueueFlags::TRANSFER,
],
)
.or_else(|| {
queue_families_priorities(
physical_device,
vec![
// main thread graphics
QueueFlags::GRAPHICS,
// main thread uploads
QueueFlags::TRANSFER,
// capture thread uploads
QueueFlags::TRANSFER,
],
)
})
.or_else(|| {
queue_families_priorities(
physical_device,
// main thread-only. software capture not supported.
vec![QueueFlags::GRAPHICS | QueueFlags::TRANSFER],
)
})
}
fn queue_families_priorities(
physical_device: &PhysicalDevice,
mut requested_queues: Vec<QueueFlags>,
) -> Option<Vec<QueueFamilyLayout>> {
let mut result = Vec::with_capacity(3);
for (idx, props) in physical_device.queue_family_properties().iter().enumerate() {
let mut remaining = props.queue_count;
let mut want = 0usize;
requested_queues.retain(|requested| {
if props.queue_flags.intersects(*requested) && remaining > 0 {
remaining -= 1;
want += 1;
false
} else {
true
}
});
if want > 0 {
result.push(QueueFamilyLayout {
queue_family_index: idx as u32,
priorities: std::iter::repeat_n(1.0, want).collect(),
});
}
log::info!("Using vkPhysicalDevice: {}", gfx.device_info().name);
if gfx.capabilities().filter_cubic {
log::info!("cubic filtering available");
}
if requested_queues.is_empty() {
log::debug!("Selected GPU queue families: {result:?}");
Some(result)
} else {
None
}
Ok((gfx, event_loop, window))
}
+65 -5
View File
@@ -321,21 +321,34 @@ impl WayVRClient {
}
}
pub async fn fn_wvr_window_list(
pub async fn fn_wlx_overlay_list(
client: WayVRClientMutex,
serial: Serial,
) -> anyhow::Result<Option<Vec<packet_server::WvrWindow>>> {
params: packet_client::WlxOverlayListParams,
) -> anyhow::Result<Vec<String>> {
Ok(
send_and_wait!(
client,
serial,
&PacketClient::WvrWindowList(serial),
WvrWindowListResponse
&PacketClient::WlxOverlayList(serial, params),
WlxOverlayListResponse
)
.map(|res| res.list),
.list,
)
}
pub async fn fn_wlx_overlay_set_visible(
client: WayVRClientMutex,
overlay: String,
visible: bool,
) -> anyhow::Result<()> {
send_only!(
client,
&PacketClient::WlxOverlaySetVisible(overlay, visible)
);
Ok(())
}
pub async fn fn_wvr_window_set_visible(
client: WayVRClientMutex,
handle: packet_server::WvrWindowHandle,
@@ -449,6 +462,53 @@ impl WayVRClient {
send_only!(client, &PacketClient::WlxModifyPanel(params));
Ok(())
}
pub async fn fn_wlx_window_state_get(
client: WayVRClientMutex,
serial: Serial,
params: packet_client::WlxWindowStateGetParams,
) -> anyhow::Result<Result<packet_client::WlxWindowStateValue, String>> {
Ok(send_and_wait!(
client,
serial,
&PacketClient::WlxWindowStateGet(serial, params),
WlxWindowStateGetResponse
))
}
pub async fn fn_wlx_window_state_set(
client: WayVRClientMutex,
params: packet_client::WlxWindowStateSetParams,
) -> anyhow::Result<()> {
send_only!(client, &PacketClient::WlxWindowStateSet(params));
Ok(())
}
pub async fn fn_wlx_window_attrib_get(
client: WayVRClientMutex,
serial: Serial,
params: packet_client::WlxWindowAttribGetParams,
) -> anyhow::Result<Result<packet_client::WlxWindowAttribValue, String>> {
Ok(send_and_wait!(
client,
serial,
&PacketClient::WlxWindowAttribGet(serial, params),
WlxWindowAttribGetResponse
))
}
pub async fn fn_wlx_window_attrib_set(
client: WayVRClientMutex,
serial: Serial,
params: packet_client::WlxWindowAttribSetParams,
) -> anyhow::Result<Result<(), String>> {
Ok(send_and_wait!(
client,
serial,
&PacketClient::WlxWindowAttribSet(serial, params),
WlxWindowAttribSetResponse
))
}
}
impl Drop for WayVRClient {
+1 -1
View File
@@ -22,7 +22,7 @@ impl SerialGenerator {
}
}
pub const PROTOCOL_VERSION: u32 = 4;
pub const PROTOCOL_VERSION: u32 = 5;
pub const CONNECTION_MAGIC: &str = "wayvr_ipc";
pub fn data_encode<T>(data: &T) -> Vec<u8>
+127 -3
View File
@@ -49,9 +49,8 @@ pub enum HandsfreeParams {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WvrProcessLaunchParams {
pub name: String,
pub exec: String,
pub argv: Vec<String>,
pub env: Vec<String>,
pub args: String,
pub icon: Option<String>,
pub resolution: [u32; 2],
pub pos_mode: PositionMode,
@@ -81,10 +80,130 @@ pub struct WlxModifyPanelParams {
pub command: WlxModifyPanelCommand,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WlxHand {
Left,
Right,
}
// see wlx_common::windowing::Positioning
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum WlxPositioning {
Floating,
Anchored,
Static,
FollowHead {
#[serde(default)]
lerp: f32,
},
FollowHand {
hand: WlxHand,
#[serde(default)]
lerp: f32,
},
}
// see wlx_common::windowing::OverlayWindowState
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WlxWindowStateField {
Alpha,
Grabbable,
Interactable,
Positioning,
Curvature,
Additive,
BlockInput,
AlignToHmd,
/// see wlx_common::windowing::OverlayWindowConfig::global
Global,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum WlxWindowStateValue {
Bool(bool),
Float(f32),
Positioning(WlxPositioning),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WlxWindowStateGetParams {
pub overlay: String,
pub field: WlxWindowStateField,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WlxWindowStateSetParams {
pub overlay: String,
pub field: WlxWindowStateField,
pub value: WlxWindowStateValue,
}
// see wlx_common::overlays::BackendAttrib
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WlxWindowAttrib {
Stereo,
StereoFullFrame,
StereoAdjustMouse,
MouseTransform,
WindowSize,
}
// see wlx_common::overlays::StereoMode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WlxStereoMode {
None,
LeftRight,
RightLeft,
TopBottom,
BottomTop,
}
// see wlx_common::overlays::MouseTransform
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WlxMouseTransform {
Default,
Normal,
Rotated90,
Rotated180,
Rotated270,
Flipped,
Flipped90,
Flipped180,
Flipped270,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum WlxWindowAttribValue {
Stereo(WlxStereoMode),
StereoFullFrame(bool),
StereoAdjustMouse(bool),
MouseTransform(WlxMouseTransform),
WindowSize([u32; 2]),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WlxWindowAttribGetParams {
pub overlay: String,
pub attrib: WlxWindowAttrib,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WlxWindowAttribSetParams {
pub overlay: String,
pub attrib: WlxWindowAttrib,
pub value: WlxWindowAttribValue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct WlxOverlayListParams {
pub visible: bool,
pub hidden: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum PacketClient {
Handshake(Handshake),
WvrWindowList(Serial),
WlxOverlayList(Serial, WlxOverlayListParams),
WvrWindowSetVisible(packet_server::WvrWindowHandle, bool),
WvrProcessGet(Serial, packet_server::WvrProcessHandle),
WvrProcessLaunch(Serial, WvrProcessLaunchParams),
@@ -97,4 +216,9 @@ pub enum PacketClient {
WlxShowHide,
WlxSwitchSet(Option<usize>),
WlxHandsfree(HandsfreeParams),
WlxWindowStateGet(Serial, WlxWindowStateGetParams),
WlxWindowStateSet(WlxWindowStateSetParams),
WlxWindowAttribGet(Serial, WlxWindowAttribGetParams),
WlxWindowAttribSet(Serial, WlxWindowAttribSetParams),
WlxOverlaySetVisible(String, bool),
}
+11 -5
View File
@@ -4,7 +4,7 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::ipc::Serial;
use super::{ipc::Serial, packet_client};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {}
@@ -39,8 +39,8 @@ pub struct WvrWindow {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WvrWindowList {
pub list: Vec<WvrWindow>,
pub struct WlxOverlayList {
pub list: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -98,10 +98,13 @@ pub enum PacketServer {
Disconnect(Disconnect),
HandshakeSuccess(HandshakeSuccess),
WlxInputStateResponse(Serial, WlxInputState),
WvrWindowListResponse(Serial, Option<WvrWindowList>),
WlxOverlayListResponse(Serial, WlxOverlayList),
WvrProcessGetResponse(Serial, Option<WvrProcess>),
WvrProcessLaunchResponse(Serial, Result<WvrProcessHandle, String>),
WvrProcessListResponse(Serial, WvrProcessList),
WlxWindowStateGetResponse(Serial, Result<packet_client::WlxWindowStateValue, String>),
WlxWindowAttribGetResponse(Serial, Result<packet_client::WlxWindowAttribValue, String>),
WlxWindowAttribSetResponse(Serial, Result<(), String>),
WvrStateChanged(WvrStateChanged),
}
@@ -111,10 +114,13 @@ impl PacketServer {
PacketServer::Disconnect(_) => None,
PacketServer::HandshakeSuccess(_) => None,
PacketServer::WlxInputStateResponse(serial, _) => Some(serial),
PacketServer::WvrWindowListResponse(serial, _) => Some(serial),
PacketServer::WlxOverlayListResponse(serial, _) => Some(serial),
PacketServer::WvrProcessGetResponse(serial, _) => Some(serial),
PacketServer::WvrProcessLaunchResponse(serial, _) => Some(serial),
PacketServer::WvrProcessListResponse(serial, _) => Some(serial),
PacketServer::WlxWindowStateGetResponse(serial, _) => Some(serial),
PacketServer::WlxWindowAttribGetResponse(serial, _) => Some(serial),
PacketServer::WlxWindowAttribSetResponse(serial, _) => Some(serial),
PacketServer::WvrStateChanged(_) => None,
}
}
+2 -4
View File
@@ -53,15 +53,12 @@ smol = { workspace = true }
strum.workspace = true
tracing-subscriber.workspace = true
uuid = { workspace = true }
vulkano.workspace = true
vulkano-shaders.workspace = true
wayland-client = { workspace = true }
wayland-protocols = { workspace = true }
wayland-protocols-misc = { workspace = true }
winit = { workspace = true, optional = true }
xdg.workspace = true
ash = "^0.38.0" # must match vulkano
bytes = { workspace = true }
chrono-tz = "0.10.4"
config = "0.15.25"
@@ -86,7 +83,7 @@ mint = "0.5.9"
openxr = { version = "0.21.1", features = ["linked", "mint"], optional = true }
ovr_overlay = {
git = "https://github.com/galister/ovr_overlay_oyasumi",
rev = "e477bd2a9e04293ea68c1e7529ef2cb131f32acc",
rev = "3b7d24cee24aa2b1ad7dffc5d5c8568508049bdd",
features = [
"ovr_input",
"ovr_system",
@@ -130,6 +127,7 @@ evdev = "0.13.2"
[build-dependencies]
regex.workspace = true
shaderc = "0.8"
[features]
default = ["openvr", "openxr", "osc", "wayland", "whisper", "x11"]
+38 -2
View File
@@ -1,7 +1,21 @@
use regex::Regex;
use std::process::Command;
use shaderc::{Compiler, EnvVersion, ShaderKind, TargetEnv};
use std::{path::PathBuf, process::Command};
const SHADERS: &[(&str, ShaderKind)] = &[
("quad.vert", ShaderKind::Vertex),
("color.frag", ShaderKind::Fragment),
("grid.frag", ShaderKind::Fragment),
("screen.frag", ShaderKind::Fragment),
("simple.frag", ShaderKind::Fragment),
("wayland.frag", ShaderKind::Fragment),
("srgb.frag", ShaderKind::Fragment),
("sky.frag", ShaderKind::Fragment),
];
fn main() {
compile_shaders();
let mut wlx_build = get_version().unwrap_or(format!("{}-unknown", env!("CARGO_PKG_VERSION")));
match std::env::var("GITHUB_JOB").as_deref() {
@@ -13,7 +27,29 @@ fn main() {
}
_ => {}
}
println!("cargo:rustc-env=WLX_BUILD={}", wlx_build);
println!("cargo:rustc-env=WLX_BUILD={wlx_build}");
}
fn compile_shaders() {
let shader_dir = PathBuf::from("src/shaders");
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo"));
let compiler = Compiler::new().expect("shaderc compiler is available");
let mut options = shaderc::CompileOptions::new().expect("shaderc options are available");
options.set_target_env(TargetEnv::Vulkan, EnvVersion::Vulkan1_3 as u32);
for (name, kind) in SHADERS {
let path = shader_dir.join(name);
println!("cargo:rerun-if-changed={}", path.display());
let source = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
let artifact = compiler
.compile_into_spirv(&source, *kind, name, "main", Some(&options))
.unwrap_or_else(|e| panic!("failed to compile {}: {e}", path.display()));
std::fs::write(out_dir.join(format!("{name}.spv")), artifact.as_binary_u8())
.unwrap_or_else(|e| panic!("failed to write compiled shader {name}: {e}"));
}
}
fn get_version() -> Result<String, Box<dyn std::error::Error>> {
-39
View File
@@ -5984,45 +5984,6 @@
"dest": "cargo/vendor/vk-parse-0.15.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/vulkano/vulkano-0.35.1.crate",
"sha256": "08840c2b51759a6f88f26f5ea378bc8b5c199a5b4760ddda292304be087249c4",
"dest": "cargo/vendor/vulkano-0.35.1"
},
{
"type": "inline",
"contents": "{\"package\": \"08840c2b51759a6f88f26f5ea378bc8b5c199a5b4760ddda292304be087249c4\", \"files\": {}}",
"dest": "cargo/vendor/vulkano-0.35.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/vulkano-macros/vulkano-macros-0.35.0.crate",
"sha256": "1dc929c42c9336fd082079ac3ea30126e4a0dfe36fd2e2b3581303f7d140d20f",
"dest": "cargo/vendor/vulkano-macros-0.35.0"
},
{
"type": "inline",
"contents": "{\"package\": \"1dc929c42c9336fd082079ac3ea30126e4a0dfe36fd2e2b3581303f7d140d20f\", \"files\": {}}",
"dest": "cargo/vendor/vulkano-macros-0.35.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/vulkano-shaders/vulkano-shaders-0.35.0.crate",
"sha256": "3bf501461be7cef2893c0e62c50945add9763cc482051d29053f6157089d5ea9",
"dest": "cargo/vendor/vulkano-shaders-0.35.0"
},
{
"type": "inline",
"contents": "{\"package\": \"3bf501461be7cef2893c0e62c50945add9763cc482051d29053f6157089d5ea9\", \"files\": {}}",
"dest": "cargo/vendor/vulkano-shaders-0.35.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
+1 -2
View File
@@ -3,6 +3,5 @@ use wgui::layout::LayoutUpdateResult;
use crate::state::AppState;
pub fn process_layout_result(app: &mut AppState, res: LayoutUpdateResult) {
app.audio_sample_player
.play_wgui_samples(&mut app.audio_system, res.sounds_to_play);
app.audio.play_wgui_samples(res.sounds_to_play);
}
+2 -2
View File
@@ -180,10 +180,10 @@
<blueprint name="menu_handsfree">
<context_menu >
<cell translation="BAR.HANDSFREE.NONE" _press="::HandsfreeMode None" _press2="::ContextMenuClose" />
<cell translation="BAR.HANDSFREE.HMD" _press="::HandsfreeMode Hmd" _press2="::ContextMenuClose" />
<cell translation="BAR.HANDSFREE.HMD" _press="::HandsfreeMode Hmd" _press2="::ContextMenuClose" skip="${openvr}" />
<cell translation="BAR.HANDSFREE.HMD_ONLY" _press="::HandsfreeMode HmdOnly" _press2="::ContextMenuClose" />
<cell translation="BAR.HANDSFREE.EYE_TRACKING" _press="::HandsfreeMode EyeTracking" _press2="::ContextMenuClose" skip="${openvr}" />
<cell translation="BAR.HANDSFREE.EYE_ONLY" _press="::HandsfreeMode EyeTrackingOnly" _press2="::ContextMenuClose" skip="${openvr}" />
<cell translation="BAR.HANDSFREE.EYE_ONLY" _press="::HandsfreeMode EyeTrackingOnly" _press2="::ContextMenuClose" />
</context_menu>
</blueprint>
+3 -2
View File
@@ -1,6 +1,7 @@
<layout version="2">
<elements>
<div interactable="0">
<!-- Provides a larger hitbox for eye gaze -->
<div padding="60">
<rectangle
max_width="500"
padding="16"
@@ -25,4 +26,4 @@
</rectangle>
</div>
</elements>
</layout>
</layout>
+7 -2
View File
@@ -108,7 +108,8 @@
"TOGGLE_CAPTURE": "Kontrolle an Desktop zurückgeben",
"FOCUS_MODE": "Fokus-Modus (Super halten)",
"CLOSE": "Fokussierte App schließen",
"MOUSE": "Echte Mausklicks & Scrollen wirken ebenfalls als VR-Pointer"
"MOUSE": "Echte Mausklicks & Scrollen wirken ebenfalls als VR-Pointer",
"KEYBOARD": "Tastatur umschalten"
},
"GRAB": {
"ADJUST_DISTANCE": "Entfernung anpassen",
@@ -142,7 +143,11 @@
"NO_SET_SELECTED": "Kein Set ausgewählt.",
"ONE_CONTROLLER_ON_FLOOR": "Einen Controller auf den Boden legen!",
"NO_KEYMAPS_CONFIGURED": "Keine Tastaturbelegungen konfiguriert",
"NO_KEYMAPS_CONFIGURED_HELP": "Besuch wayvr.org, um zu erfahren, wie du deine Tastaturlayouts einrichtest!"
"NO_KEYMAPS_CONFIGURED_HELP": "Besuch wayvr.org, um zu erfahren, wie du deine Tastaturlayouts einrichtest!",
"STEAMVR_DASH_TITLE": "SteamVR Dashboard geöffnet",
"STEAMVR_DASH_TEXT": "SteamVR-Dashboard schließen, um WayVR zu nutzen.",
"WAYVR_RUNNING_TITLE": "WayVR läuft!",
"WAYVR_RUNNING_TEXT": "Schau auf dein Handgelenk für die Watch oder nutze die Ein/Aus-Belegung, um zu starten!"
},
"WATCH": {
"ADD_NEW_SET": "Neues Set hinzufügen",
+5
View File
@@ -88,6 +88,7 @@ ALTTAB.GRABALL,Move entire set,Key binding action to grab and move the entire se
ALTTAB.GRAB,Move individual overlay,Key binding action to grab and move a single overlay. “Overlay” was previously translated as “{$DEFINITIONS.OVERLAY}”
ALTTAB.DASH,Toggle dashboard,Key binding action that toggles the dashboard. “Dashboard” was previously translated as “${DEFINITIONS.DASHBOARD}”
ALTTAB.EDIT,Toggle edit mode,Key binding action that toggles edit mode. “Edit mode” was previously translated as “${DEFINITIONS.EDIT_MODE}”
ALTTAB.KEYBOARD,Toggle keyboard,Key binding action that toggles the keyboard. “Keyboard” was previously translated as “${DEFINITIONS.KEYBOARD}”
ALTTAB.SET,Switch set,Key binding action to switch between sets. “Set” was previously translated as “${DEFINITIONS.SET}”
ALTTAB.RECENTER,Show/hide/recenter,"Key binding action to show, hide, or recenter the current set. “Set” was previously translated as “${DEFINITIONS.SET}”"
ALTTAB.GENERAL_BINDS,General bindings,Title for key bindings that don’t need any specific criteria
@@ -113,6 +114,10 @@ SCREENCAST.FAIL,Could not initialize screen cast source.,Error message on a wind
TOAST.TITLE_SCREENCAST_FAIL,Could not start screen cast source!,Error message on a window that normally displays screen casts.
TOAST.CANNOT_ADD_SET,Cannot add set!,Error message telling the user the set could not be added. “Set” was previously translated as “${DEFINITIONS.SET}”
TOAST.CANNOT_REMOVE_SET,Cannot remove set!,Error message telling the user the set could not be removed. “Set” was previously translated as “${DEFINITIONS.SET}”
TOAST.STEAMVR_DASH_TITLE,SteamVR Dashboard Open,Title of toast shown when SteamVR dashboard is visible
TOAST.STEAMVR_DASH_TEXT,Close SteamVR dashboard to start using WayVR.,Body of toast shown when SteamVR dashboard is visible
TOAST.WAYVR_RUNNING_TITLE,WayVR is running!,Title of toast shown when WayVR is started but its windows are hidden by default
TOAST.WAYVR_RUNNING_TEXT,"Check your wrist for the watch, or use show-hide binding to get started!",Body of toast shown when WayVR is started but its windows are hidden by default
TOAST.DEFAULT_TITLE,Notification,Default title of notifications
TOAST.EMPTY_SET,Empty set!,Title of a pop-up when the user switches to an empty set. “Set” was previously translated as “${DEFINITIONS.SET}”
TOAST.ERROR,Error,Default title for error pop-ups
1 DEFINITIONS.ANCHORED Anchored A movement option that makes this window move together with the rest of the workspace. Emphasize the “moving together” part as opposed to “not moving at all”.
88 ALTTAB.EDIT Toggle edit mode Key binding action that toggles edit mode. “Edit mode” was previously translated as “${DEFINITIONS.EDIT_MODE}”
89 ALTTAB.SET ALTTAB.KEYBOARD Switch set Toggle keyboard Key binding action to switch between sets. “Set” was previously translated as “${DEFINITIONS.SET}” Key binding action that toggles the keyboard. “Keyboard” was previously translated as “${DEFINITIONS.KEYBOARD}”
90 ALTTAB.RECENTER ALTTAB.SET Show/hide/recenter Switch set Key binding action to show, hide, or recenter the current set. “Set” was previously translated as “${DEFINITIONS.SET}” Key binding action to switch between sets. “Set” was previously translated as “${DEFINITIONS.SET}”
91 ALTTAB.RECENTER Show/hide/recenter Key binding action to show, hide, or recenter the current set. “Set” was previously translated as “${DEFINITIONS.SET}”
92 ALTTAB.GENERAL_BINDS General bindings Title for key bindings that don’t need any specific criteria
93 ALTTAB.TOGGLE_CAPTURE Give control back to desktop Key binding action that releases exclusive input capture
94 ALTTAB.FOCUS_MODE Focus mode (hold Super) Key binding action to enter into a task-switcher kind of mode, like Alt-Tab.
114 TOAST.DEFAULT_TITLE TOAST.STEAMVR_DASH_TITLE Notification SteamVR Dashboard Open Default title of notifications Title of toast shown when SteamVR dashboard is visible
115 TOAST.EMPTY_SET TOAST.STEAMVR_DASH_TEXT Empty set! Close SteamVR dashboard to start using WayVR. Title of a pop-up when the user switches to an empty set. “Set” was previously translated as “${DEFINITIONS.SET}” Body of toast shown when SteamVR dashboard is visible
116 TOAST.ERROR TOAST.WAYVR_RUNNING_TITLE Error WayVR is running! Default title for error pop-ups Title of toast shown when WayVR is started but its windows are hidden by default
117 TOAST.WAYVR_RUNNING_TEXT Check your wrist for the watch, or use show-hide binding to get started! Body of toast shown when WayVR is started but its windows are hidden by default
118 TOAST.DEFAULT_TITLE Notification Default title of notifications
119 TOAST.EMPTY_SET Empty set! Title of a pop-up when the user switches to an empty set. “Set” was previously translated as “${DEFINITIONS.SET}”
120 TOAST.ERROR Error Default title for error pop-ups
121 TOAST.FIXING_FLOOR_IN_X_SECS Fixing floor in {SECONDS} second(s)... A countdown pop-up for automatically adjusting floor height. Keep the {SECONDS} part intact as it’s a parameter.
122 TOAST.LAST_EXISTING_SET This is the last existing set. Notification body explaining that the set cannot be removed because this is the last existing set. “Set” was previously translated as “${DEFINITIONS.SET}”
123 TOAST.LETS_ADD_OVERLAYS Let's add some overlays from the taskbar! Instructions prompting the user to add overlays. “Taskbar” was previously translated as “${DEFINITIONS.TASKBAR}”, “Overlay” was previously translated as “${DEFINITIONS.OVERLAY}”
+5
View File
@@ -102,6 +102,7 @@
"GRAB": "Move individual overlay",
"DASH": "Toggle dashboard",
"EDIT": "Toggle edit mode",
"KEYBOARD": "Toggle keyboard",
"SET": "Switch set",
"RECENTER": "Show/hide/recenter",
"GENERAL_BINDS": "General bindings",
@@ -132,6 +133,10 @@
"TITLE_SCREENCAST_FAIL": "Could not start screen cast source!",
"CANNOT_ADD_SET": "Cannot add set!",
"CANNOT_REMOVE_SET": "Cannot remove set!",
"STEAMVR_DASH_TITLE": "SteamVR Dashboard Open",
"STEAMVR_DASH_TEXT": "Close SteamVR dashboard to start using WayVR.",
"WAYVR_RUNNING_TITLE": "WayVR is running!",
"WAYVR_RUNNING_TEXT": "Check your wrist for the watch, or use show-hide binding to get started!",
"DEFAULT_TITLE": "Notification",
"EMPTY_SET": "Empty set!",
"ERROR": "Error",
+7 -2
View File
@@ -108,7 +108,8 @@
"TOGGLE_CAPTURE": "Devolver el control al escritorio",
"FOCUS_MODE": "Modo enfoque (mantén Super)",
"CLOSE": "Cerrar app enfocada",
"MOUSE": "Los clics y el scroll del ratón real también actúan como el puntero de VR"
"MOUSE": "Los clics y el scroll del ratón real también actúan como el puntero de VR",
"KEYBOARD": "Alternar teclado"
},
"GRAB": {
"ADJUST_DISTANCE": "Ajustar distancia",
@@ -142,7 +143,11 @@
"NO_SET_SELECTED": "No hay ningún set seleccionado.",
"ONE_CONTROLLER_ON_FLOOR": "¡Coloca un controlador en el suelo!",
"NO_KEYMAPS_CONFIGURED": "Sin distribuciones de teclado configuradas",
"NO_KEYMAPS_CONFIGURED_HELP": "¡Consulta wayvr.org para configurar tus diseños de teclado!"
"NO_KEYMAPS_CONFIGURED_HELP": "¡Consulta wayvr.org para configurar tus diseños de teclado!",
"STEAMVR_DASH_TITLE": "Panel de SteamVR abierto",
"STEAMVR_DASH_TEXT": "Cierra el tablero de SteamVR para empezar a usar WayVR.",
"WAYVR_RUNNING_TITLE": "¡WayVR está ejecutándose!",
"WAYVR_RUNNING_TEXT": "¡Mira tu muñeca para ver el reloj o usa el mando de mostrar/ocultar para empezar!"
},
"WATCH": {
"ADD_NEW_SET": "Añadir un set nuevo",
+7 -2
View File
@@ -108,7 +108,8 @@
"TOGGLE_CAPTURE": "Rendi il controllo al desktop",
"FOCUS_MODE": "Modalità focus (tieni premuto Super)",
"CLOSE": "Chiudi app selezionata",
"MOUSE": "I clic e lo scorrimento del mouse reale influenzano anche il puntatore VR"
"MOUSE": "I clic e lo scorrimento del mouse reale influenzano anche il puntatore VR",
"KEYBOARD": "Attiva/disattiva tastiera"
},
"GRAB": {
"ADJUST_DISTANCE": "Regola distanza",
@@ -142,7 +143,11 @@
"NO_SET_SELECTED": "Nessun set selezionato.",
"ONE_CONTROLLER_ON_FLOOR": "Appoggia un controller sul pavimento!",
"NO_KEYMAPS_CONFIGURED": "Nessuna disposizione tastiera configurata",
"NO_KEYMAPS_CONFIGURED_HELP": "Consulta wayvr.org per configurare i layout della tastiera!"
"NO_KEYMAPS_CONFIGURED_HELP": "Consulta wayvr.org per configurare i layout della tastiera!",
"STEAMVR_DASH_TITLE": "Dashboard SteamVR Aperta",
"STEAMVR_DASH_TEXT": "Chiudi il pannello di SteamVR per iniziare a usare WayVR.",
"WAYVR_RUNNING_TITLE": "WayVR è attivo!",
"WAYVR_RUNNING_TEXT": "Controlla il polso per l'orologio, o usa il comando show-hide per iniziare!"
},
"WATCH": {
"ADD_NEW_SET": "Aggiungi un nuovo set",
+8 -3
View File
@@ -108,7 +108,8 @@
"TOGGLE_CAPTURE": "デスクトップに操作を戻す",
"FOCUS_MODE": "フォーカスモード (Superキーを長押し)",
"CLOSE": "フォーカス中のアプリを閉じる",
"MOUSE": "実マウスのクリックとスクロールもVRポインターに反映される"
"MOUSE": "実マウスのクリックとスクロールもVRポインターに反映される",
"KEYBOARD": "キーボードの切り替え"
},
"GRAB": {
"ADJUST_DISTANCE": "距離を調整",
@@ -142,7 +143,11 @@
"NO_SET_SELECTED": "セットが選択されていません。",
"ONE_CONTROLLER_ON_FLOOR": "コントローラーを片方、床に置いてください!",
"NO_KEYMAPS_CONFIGURED": "キーボードレイアウトが設定されていない",
"NO_KEYMAPS_CONFIGURED_HELP": "キーボードレイアウトの設定方法は wayvr.org を確認してね!"
"NO_KEYMAPS_CONFIGURED_HELP": "キーボードレイアウトの設定方法は wayvr.org を確認してね!",
"STEAMVR_DASH_TITLE": "SteamVRダッシュボード展開中",
"STEAMVR_DASH_TEXT": "WayVRを使用するには、SteamVRダッシュボードを閉じてください。",
"WAYVR_RUNNING_TITLE": "WayVR 実行中!",
"WAYVR_RUNNING_TEXT": "手首のウォッチを確認するか、表示・非表示のバインドを使用して開始してください!"
},
"WATCH": {
"ADD_NEW_SET": "新しいセットを追加",
@@ -177,4 +182,4 @@
"CANNOT_CAPTURE_KBD_MOUSE": "キーボード/マウスのキャプチャを初期化できませんでした!\nユーザーが 'input' グループに所属しているか確認してください。",
"WE_ARE_GRABBING": "WayVR がキーボードとマウスを制御しています。\n解除するには、Super+Zを押してください。"
}
}
}
+7 -2
View File
@@ -108,7 +108,8 @@
"TOGGLE_CAPTURE": "Oddaj kontrolę do pulpitu",
"FOCUS_MODE": "Tryb skupienia (przytrzymaj Super)",
"CLOSE": "Zamknij aktywną aplikację",
"MOUSE": "Kliknięcia i przewijanie myszką przenoszą się na wskaźnik VR"
"MOUSE": "Kliknięcia i przewijanie myszką przenoszą się na wskaźnik VR",
"KEYBOARD": "Przełącz klawiaturę"
},
"GRAB": {
"ADJUST_DISTANCE": "Dostosuj odległość",
@@ -142,7 +143,11 @@
"NO_SET_SELECTED": "Żaden zestaw nie jest wybrany.",
"ONE_CONTROLLER_ON_FLOOR": "Połóż jeden kontroler na podłodze!",
"NO_KEYMAPS_CONFIGURED": "Brak skonfigurowanych układów klawiatury",
"NO_KEYMAPS_CONFIGURED_HELP": "Odwiedź stronę wayvr.org, aby dowiedzieć się, jak skonfigurować układy klawiatury!"
"NO_KEYMAPS_CONFIGURED_HELP": "Odwiedź stronę wayvr.org, aby dowiedzieć się, jak skonfigurować układy klawiatury!",
"STEAMVR_DASH_TITLE": "Panel SteamVR otwarty",
"STEAMVR_DASH_TEXT": "Zamknij pulpit SteamVR, aby zacząć korzystać z WayVR.",
"WAYVR_RUNNING_TITLE": "WayVR działa!",
"WAYVR_RUNNING_TEXT": "Spójrz na nadgarstek, by zobaczyć zegarek, lub użyj przycisku pokaż/ukryj, aby zacząć!"
},
"WATCH": {
"ADD_NEW_SET": "Dodaj nowy zestaw",
+185
View File
@@ -0,0 +1,185 @@
{
"DEFINITIONS": {
"ANCHORED": "Förankrad",
"FLOATING": "Flytande",
"STATIC": "Statisk",
"SET": "Set",
"WATCH": "Klocka",
"DASHBOARD": "Kontrollpanel",
"EDIT_MODE": "Redigeringsläge",
"MIRROR": "Spegling",
"SETTINGS": "Inställningar",
"KEYBOARD": "Tangentbord",
"TASKBAR": "Uppgiftspanel",
"FEATURES": "Funktioner",
"OVERLAY": "Overlay"
},
"ANCHOR": {
"CENTER": "Center"
},
"BAR": {
"ADD_MIRROR": "Lägg till ny spegling-overlay",
"ADD_NEW_SET": "Lägg till ny set",
"ADD_PASSTHRU": "Lägg till ny passthrough-utskärning",
"CLOSE_APP": "Stäng appen",
"CLOSE_OVERLAY": "Stäng {$DEFINITIONS.OVERLAY}",
"DELETE_CURRENT_SET": "Ta bort nuvarande set",
"EDIT_MODE_TOGGLE": "Växla redigeringsläge",
"FORCE_CLOSE_APP": "Tvinga stäng app",
"HANDSFREE": {
"EYE_ONLY": "Ögonsiktet",
"EYE_TRACKING": "Öga + pinch",
"HMD": "HMD + pinch",
"HMD_ONLY": "HMD-laser",
"NONE": "Av",
"TITLE": "Händsfree-läge"
},
"RELOAD_FROM_DISK": "Ladda om XML från disk",
"RESET_POSITION": "Återställ position",
"TOGGLE_VISIBILITY": "Växla synlighet"
},
"DEFAULT": "Standard",
"DISABLED": "Inaktiverat",
"DONE": "Klar",
"EDIT_MODE": {
"ADJUST_CURVATURE": "Justera krökning",
"ALIGN_TO_HMD": "Rikta mot HMD",
"ALPHA_BLEND_MODE": "Alfablendingsläge",
"ANGLE_FADE": "Vinkeltoning",
"ANGLE_FADE_HELP": "Tona ut när den inte är vänd mot HMD",
"BLENDING_ADDITIVE": "Additiv blandning",
"BLOCK_INPUT": "Blockera spel-input",
"CURVATURE": "Krökning",
"DELETE": "Håll inne för att ta bort från nuvarande set",
"DISABLE_GRAB": "Inaktivera grepp",
"GLOBAL": "Alltid synlig",
"HINT_POINT_WINDOW": "Peka på ett fönster för att ändra dess parametrar.\nNär du är klar, lämna redigeringsläget via knappen till höger.",
"INTERPOLATION": "Interpolation",
"KEYBOARD": "Tangentbord",
"LEAVE": "Lämna redigeringsläge",
"LOCK_INTERACTION": "Lås interaktion",
"MOUSE": {
"FLIP180": "Speglad 180°",
"FLIP270": "Speglad 270°",
"FLIP90": "Speglad 90°",
"FLIPPED": "Speglad",
"NORMAL": "Normal",
"ROTATE180": "Roterad 180°",
"ROTATE270": "Roterad 270°",
"ROTATE90": "Roterad 90°",
"TITLE": "Musfixar",
"WRONG_SCREEN_SELECTION_HELP": "Om markören rör sig på en helt annan skärm\nhar skärmarna troligtvis valts fel. Se readme."
},
"MOVE_PRESS_AND_DRAG": "Flytta (tryck & dra)",
"OPACITY": "Opacitet",
"POS_ANCHORED": "Förankrad: Flyttas tillsammans med set. Standard.",
"POS_FLOATING": "Flytande: Flyttas oberoende, centreras om vid visning.",
"POS_HAND_L": "Följ vänster hand.",
"POS_HAND_R": "Följ höger hand.",
"POS_HMD": "Följ HMD.",
"POS_STATIC": "Statisk: Inte en del av något set, ingen återcentrering.",
"POSITIONING": "Positionering",
"RESIZE_PRESS_AND_DRAG": "Ändra storlek (tryck & dra)",
"RESIZE_CORNER": "Dra för att ändra storlek",
"STEREO_3D_MODE": {
"ADJUST_MOUSE": "Justera mus",
"FULL_FRAME": "Full 3D",
"FULL_FRAME_BAT": "Full-BAT",
"FULL_FRAME_SBS": "Full-SBS",
"FULL_FRAME_TAB": "Full-TAB",
"SPLIT_BOTTOM_TOP": "NEDRE→ÖVRE",
"SPLIT_LEFT_RIGHT": "VÄNSTRER→HÖGER",
"SPLIT_RIGHT_LEFT": "HÖGER→VÄNSTER",
"SPLIT_TOP_BOTTOM": "TOPP→BOTTEN",
"TITLE": "3D-stereoläge"
}
},
"ALTTAB": {
"RELEASE_TO_FOCUS": "Sikta på en app och släpp Super för att fokusera på den.",
"WHILE_AIMING": "Vid sikte",
"CLICK": "VR-pekarklick",
"GRABALL": "Flytta hela setet",
"GRAB": "Flytta enskild overlay",
"DASH": "Växla WayVR Dashboard",
"EDIT": "Växla redigeringsläge",
"KEYBOARD": "Växla tangentbord",
"SET": "Byt set",
"RECENTER": "Visa/dölj/centrera",
"GENERAL_BINDS": "Allmänna bindningar",
"TOGGLE_CAPTURE": "Återlämna kontrollen till skrivbordet",
"FOCUS_MODE": "Fokusläge (håll in Super)",
"CLOSE": "Stäng fokuserad app",
"MOUSE": "Klick och skroll med fysisk mus översätts även till VR-pekaren"
},
"GRAB": {
"ADJUST_DISTANCE": "Justera avstånd",
"ADJUST_SIZE": "Justera storlek",
"GRABBING_ANCHORED": "Förankrade överlägg rör sig med setet. Separera ett enskilt fönster genom att greppa det med den andra handen samtidigt som du håller i setet.",
"GRABBING_ANCHORED_EDIT": "Det här överlägget kommer att förbli förankrat vid centreringsmarkören.",
"GRABBING_FLOATING": "Det här överlägget är flytande och kommer att stanna på plats, om det inte centreras om.",
"GRABBING_FOLLOW": "Detta overlay följer den enhet den är kopplad till.",
"GRABBING_STATIC": "Det här överlägget är statiskt och kommer att stanna kvar på platsen, oavsett om du centrerar om.",
"GRABBING_WATCH": "För att byta hand, flytta klockan framför dig och greppa den med den andra handen.",
"UNRESTRICTED_MOVEMENT": "Obegränsad rörelse"
},
"SCREENCAST": {
"PENDING": "Vänligen vänta...",
"QUEUED": "Väntar på att en annan overlays skärmdelningsförfrågan ska godkännas.",
"WAITING_FOR_USER_DESC": "Kontrollera skrivbordet efter ett popup-fönster för skärmdelning.\nVälj följande:",
"WAITING_FOR_USER": "Kontrollera din skrivbordsdisplay för ett popup-fönster om skärmdelning.",
"FAIL": "Kunde inte initiera källan för skärmöverföring."
},
"TOAST": {
"TITLE_SCREENCAST_FAIL": "Kunde inte starta källan för skärmdelning!",
"CANNOT_ADD_SET": "Kunde inte lägga till set!",
"CANNOT_REMOVE_SET": "Kan inte ta bort set!",
"STEAMVR_DASH_TITLE": "SteamVR-kontrollpanelen är öppen",
"STEAMVR_DASH_TEXT": "Stäng SteamVR-panelen för att börja använda WayVR.",
"WAYVR_RUNNING_TITLE": "WayVR körs!",
"WAYVR_RUNNING_TEXT": "Kolla handleden för klockan, eller använd bindningen för visa/dölj för att komma igång!",
"DEFAULT_TITLE": "Avisering",
"EMPTY_SET": "Tomt set!",
"ERROR": "Fel",
"FIXING_FLOOR_IN_X_SECS": "Justerar golvet om {SECONDS} sekunder...",
"LAST_EXISTING_SET": "Detta är det sista befintliga setet.",
"LETS_ADD_OVERLAYS": "Låt oss lägga till några overlays från uppgiftspanelen!",
"MAXIMUM_SETS_REACHED": "Maximalt antal set har uppnåtts.",
"NO_KEYMAPS_CONFIGURED": "Inga tangentbordslayouts konfigurerade",
"NO_KEYMAPS_CONFIGURED_HELP": "Kolla wayvr.org för att se hur du ställer in dina tangentbordslayouter!",
"NO_SET_SELECTED": "Inget Set är valt.",
"ONE_CONTROLLER_ON_FLOOR": "Placera en kontroller på golvet!"
},
"WATCH": {
"ADD_NEW_SET": "Lägg till ett nytt set",
"CLEANUP_MIRRORS": "Ta bort speglingar som\ninte är synliga just nu",
"DASHBOARD": "Kontrollpanel",
"EDIT_MODE": "Redigeringsläge",
"FIX_FLOOR": "Justera golvnivå",
"LONG_PRESS_TO_DELETE_SET": "Håll för att ta bort",
"RECENTER": "Centrera playspace",
"SWITCH_TO_SET": "Byt till set",
"TOGGLE_FOR_CURRENT_SET": "Växla för nuvarande set"
},
"WHISPER": {
"TRANSCRIBE": "Håll ned för att transkribera text",
"PASTE": "Kopiera transkribering till urklipp",
"PASTE_AND_GO": "Kopiera transkribering och skicka Ctrl-V till fokuserad app",
"SEND_OSC": "Skicka transkription till OSC-chatt",
"UNLOAD_AND_CLOSE": "Avlasta modell och stäng",
"MODEL_NOT_DOWNLOADED": "Tal-till-text är inte konfigurerat!",
"DOWNLOAD_GUIDANCE": "Konfigurera tal-till-text under inställningar, funktioner!",
"INITIAL_TEXT": "Håll inne mikroknappen för att börja transkribera...",
"INIT_ERROR": "Fel vid start av Whisper-motorn",
"ENTER_KEY": "Enter-tangent"
},
"NOTIFICATION": {
"COUD_NOT_CONNECT_TO_VR_RUNTIME": "Kunde inte ansluta till VR-runtime.",
"NOT_RUNNING_XR_VR": "Se till att Monado, WiVRn eller SteamVR är igång.",
"NOT_RUNNING_VR": "Se till att Monado, WiVRn eller SteamVR är igång.",
"NOT_RUNNING_XR": "Se till att Monado eller WiVRn är igång.",
"INVALID_ARGS": "Kontrollera dina startargument.",
"SELECT_SCREEN_CAST_FOR": "Välj skärmdelning för display {DISPLAY}",
"CANNOT_CAPTURE_KBD_MOUSE": "Kunde inte initiera fångst av tangentbord/mus!\nKontrollera att din användare tillhör gruppen 'input'.",
"WE_ARE_GRABBING": "WayVR styr nu ditt tangentbord och din mus!\nTryck Super+Z för att släppa."
}
}
+7 -2
View File
@@ -108,7 +108,8 @@
"TOGGLE_CAPTURE": "将控制权还给桌面",
"FOCUS_MODE": "焦点模式 (按住 Super 键)",
"CLOSE": "关闭当前应用",
"MOUSE": "实体鼠标点击与滚动同样可转换为 VR 指针"
"MOUSE": "实体鼠标点击与滚动同样可转换为 VR 指针",
"KEYBOARD": "切换键盘"
},
"GRAB": {
"ADJUST_DISTANCE": "调整距离",
@@ -142,7 +143,11 @@
"NO_SET_SELECTED": "未选择套装。",
"ONE_CONTROLLER_ON_FLOOR": "将一只控制器放在地面上!",
"NO_KEYMAPS_CONFIGURED": "尚未配置键盘布局",
"NO_KEYMAPS_CONFIGURED_HELP": "请访问 wayvr.org 了解如何设置键盘布局!"
"NO_KEYMAPS_CONFIGURED_HELP": "请访问 wayvr.org 了解如何设置键盘布局!",
"STEAMVR_DASH_TITLE": "SteamVR 控制面板已开启",
"STEAMVR_DASH_TEXT": "关闭 SteamVR 仪表板以开始使用 WayVR。",
"WAYVR_RUNNING_TITLE": "WayVR 正在运行!",
"WAYVR_RUNNING_TEXT": "查看手腕上的手表,或使用显示/隐藏快捷键开始!"
},
"WATCH": {
"ADD_NEW_SET": "添加新套装",
+29 -105
View File
@@ -1,9 +1,8 @@
use std::f32::consts::PI;
use std::process::{Child, Command};
use std::sync::Arc;
use std::time::{Duration, Instant};
use glam::{Affine3A, Quat, Vec2, Vec3A, Vec3Swizzles};
use glam::{Affine3A, Quat, Vec2, Vec3A};
use idmap_derive::IntegerId;
use smallvec::{SmallVec, smallvec};
@@ -21,7 +20,7 @@ use crate::subsystem::input::InputFocus;
use crate::windowing::backend::OverlayEventData;
use crate::windowing::manager::OverlayWindowManager;
use crate::windowing::window::{self, OverlayCategory, OverlayWindowData, realign, scalar_scale};
use crate::windowing::{OverlayID, OverlaySelector};
use crate::windowing::{OverlayID, OverlaySelector, raycast_overlay};
use super::task::TaskType;
@@ -69,6 +68,7 @@ pub enum FocusPickState {
pub struct InputState {
pub hmd: Affine3A,
pub ipd: f32,
pub eye_gaze: Option<Affine3A>,
pub pointers: [Pointer; 2],
pub devices: Vec<TrackedDevice>,
pub handsfree_state: PointerState,
@@ -84,6 +84,7 @@ impl InputState {
Self {
hmd: Affine3A::IDENTITY,
ipd: 0.0,
eye_gaze: None,
pointers: [Pointer::new(0), Pointer::new(1)],
devices: Vec::new(),
processes: Vec::new(),
@@ -348,6 +349,8 @@ pub struct Pointer {
pub(super) interaction: InteractionState,
pub tracked: bool,
pub handsfree: bool,
/// distance of the most recent ray hit on a wayland client window, used for drag n drop
pub last_wvr_hit_distance: Option<f32>,
}
impl Pointer {
@@ -364,6 +367,7 @@ impl Pointer {
interaction: InteractionState::default(),
tracked: false,
handsfree: false,
last_wvr_hit_distance: None,
}
}
@@ -426,7 +430,7 @@ pub struct GrabData {
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, Default)]
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum PointerMode {
#[default]
Left,
@@ -514,6 +518,8 @@ where
);
}
crate::overlays::drag_item::update_position(overlays, app);
[hits[0].1, hits[1].1]
}
@@ -548,10 +554,24 @@ where
let hovered_id = pointer.interaction.hovered_id.take();
let (Some((mut hit, raw_hit)), haptics) = get_nearest_hit(idx, overlays, app) else {
if let Some(wvr_server) = app.wvr_server.as_mut() {
wvr_server.clear_dnd_focus(idx);
}
handle_no_hit(idx, hovered_id, overlays, app);
return (None, pending_haptics); // no hit
};
let hit_is_wayvr = overlays
.iter()
.find(|(id, _)| *id == hit.overlay)
.is_some_and(|(_, overlay)| overlay.config.category == OverlayCategory::WayVR);
if hit_is_wayvr {
app.input_state.pointers[idx].last_wvr_hit_distance = Some(raw_hit.dist);
} else if let Some(wvr_server) = app.wvr_server.as_mut() {
wvr_server.clear_dnd_focus(idx);
}
// focus change
if let Some(hovered_id) = hovered_id
&& hovered_id != hit.overlay
@@ -782,7 +802,7 @@ where
let Some(overlay_state) = overlay.config.active_state.as_ref() else {
continue;
};
if !overlay_state.interactable && !edit_mode {
if (!overlay_state.interactable || overlay_state.alpha < 0.025) && !edit_mode {
continue;
}
@@ -857,7 +877,9 @@ fn start_grab(par: StartGrabParams) {
let grab_anchor = !par.edit_mode
&& !par.grab_float
&& !app.anchor_grabbed
&& matches!(state.positioning, Positioning::Anchored);
&& matches!(state.positioning, Positioning::Anchored)
&& !(app.session.config.middle_grabs_single
&& pointer.interaction.mode == PointerMode::Middle);
let relative_grab_transform = if grab_anchor {
app.anchor
@@ -1099,17 +1121,7 @@ fn ray_test(
overlay_pose: &Affine3A,
curvature: Option<&f32>,
) -> Option<RayHit> {
let (dist, local_pos) = curvature.map_or_else(
|| {
Some(raycast_plane(
ray_origin,
Vec3A::NEG_Z,
overlay_pose,
Vec3A::NEG_Z,
))
},
|curvature| raycast_cylinder(ray_origin, Vec3A::NEG_Z, overlay_pose, *curvature),
)?;
let (dist, local_pos) = raycast_overlay(ray_origin, overlay_pose, curvature.copied())?;
if dist < 0.0 {
// hit is behind us
@@ -1123,91 +1135,3 @@ fn ray_test(
dist,
})
}
fn raycast_plane(
source: &Affine3A,
source_fwd: Vec3A,
plane: &Affine3A,
plane_norm: Vec3A,
) -> (f32, Vec2) {
let plane_normal = plane.transform_vector3a(plane_norm);
let ray_dir = source.transform_vector3a(source_fwd);
let d = plane.translation.dot(-plane_normal);
let mut dist = -(d + source.translation.dot(plane_normal)) / ray_dir.dot(plane_normal);
let hit_local = plane
.inverse()
.transform_point3a(source.translation + ray_dir * dist)
.xy();
// hitting the backside of the plane, make the hit invalid
if ray_dir.dot(plane_normal) < 0.0 && dist.is_sign_positive() {
dist = -dist;
}
(dist, hit_local)
}
fn raycast_cylinder(
source: &Affine3A,
source_fwd: Vec3A,
plane: &Affine3A,
curvature: f32,
) -> Option<(f32, Vec2)> {
// this is solved locally; (0,0) is the center of the cylinder, and the cylinder is aligned with the Y axis
let size = plane.x_axis.length();
let to_local = Affine3A {
matrix3: plane.matrix3.mul_scalar(1.0 / size),
translation: plane.translation,
}
.inverse();
let radius = size / (2.0 * PI * curvature);
let ray_dir = to_local.transform_vector3a(source.transform_vector3a(source_fwd));
let ray_origin = to_local.transform_point3a(source.translation) + Vec3A::NEG_Z * radius;
let v_dir = ray_dir.xz();
let v_pos = ray_origin.xz();
let l_dir = v_dir.dot(v_dir);
let l_pos = v_dir.dot(v_pos);
let c = radius.mul_add(-radius, v_pos.dot(v_pos));
let d = l_pos.mul_add(l_pos, -(l_dir * c));
if d < f32::EPSILON {
return None;
}
let sqrt_d = d.sqrt();
let t1 = (-l_pos - sqrt_d) / l_dir;
let t2 = (-l_pos + sqrt_d) / l_dir;
let mut t = t1.max(t2);
if t < f32::EPSILON {
return None;
}
let mut hit_local = ray_origin + ray_dir * t;
if hit_local.z > 0.0 {
// hitting the opposite half of the cylinder
return None;
}
let normal = Vec3A::new(hit_local.x, 0.0, hit_local.z).normalize();
// If hitting from the outside, flip t
if ray_dir.dot(normal) < 0.0 && t.is_sign_positive() {
t = -t;
}
let max_angle = 2.0 * (size / (2.0 * radius));
let x_angle = (hit_local.x / radius).asin();
hit_local.x = x_angle / max_angle;
hit_local.y /= size;
Some((t, hit_local.xy()))
}
+1
View File
@@ -37,4 +37,5 @@ pub enum BackendError {
pub struct RunParams {
pub wgui_globals: WguiGlobals,
pub config: GeneralConfig,
pub no_audio: bool,
}
+244 -78
View File
@@ -1,6 +1,7 @@
use std::{array, fs::File, io::Write, time::Duration};
use anyhow::bail;
use glam::{Affine3A, FloatExt, Quat, Vec3};
use ovr_overlay::{
TrackedDeviceIndex,
input::{ActionHandle, ActionSetHandle, ActiveActionSet, InputManager, InputValueHandle},
@@ -11,10 +12,10 @@ use ovr_overlay::{
},
system::SystemManager,
};
use wlx_common::config_io;
use wlx_common::{config::HandsfreePointer, config_io};
use crate::{
backend::input::{Haptics, TrackedDevice, TrackedDeviceRole},
backend::input::{Haptics, Pointer, PointerState, TrackedDevice, TrackedDeviceRole},
state::AppState,
};
@@ -22,6 +23,7 @@ use super::helpers::{Affine3AConvert, OVRError};
const SET_DEFAULT: &str = "/actions/default";
const INPUT_SOURCES: [&str; 2] = ["/user/hand/left", "/user/hand/right"];
const INPUT_HEAD: &str = "/user/head";
const PATH_POSES: [&str; 2] = [
"/actions/default/in/LeftHand",
"/actions/default/in/RightHand",
@@ -32,6 +34,7 @@ const PATH_HAPTICS: [&str; 2] = [
];
const PATH_ALT_CLICK: &str = "/actions/default/in/AltClick";
const PATH_EYE_TRACKING: &str = "/actions/default/in/EyeTracking";
const PATH_CLICK_MODIFIER_MIDDLE: &str = "/actions/default/in/ClickModifierMiddle";
const PATH_CLICK_MODIFIER_RIGHT: &str = "/actions/default/in/ClickModifierRight";
const PATH_CLICK: &str = "/actions/default/in/Click";
@@ -48,7 +51,9 @@ const INPUT_ANY: InputValueHandle = InputValueHandle(ovr_overlay::sys::k_ulInval
pub(super) struct OpenVrInputSource {
hands: [OpenVrHandSource; 2],
head_hnd: InputValueHandle,
set_hnd: ActionSetHandle,
eye_tracking_hnd: ActionHandle,
click_hnd: ActionHandle,
grab_hnd: ActionHandle,
scroll_hnd: ActionHandle,
@@ -74,7 +79,9 @@ pub(super) struct OpenVrHandSource {
impl OpenVrInputSource {
pub fn new(input: &mut InputManager) -> Result<Self, OVRError> {
let set_hnd = input.get_action_set_handle(SET_DEFAULT)?;
let head_hnd = input.get_input_source_handle(INPUT_HEAD)?;
let eye_tracking_hnd = input.get_action_handle(PATH_EYE_TRACKING)?;
let click_hnd = input.get_action_handle(PATH_CLICK)?;
let grab_hnd = input.get_action_handle(PATH_GRAB)?;
let scroll_hnd = input.get_action_handle(PATH_SCROLL)?;
@@ -113,7 +120,9 @@ impl OpenVrInputSource {
Ok(Self {
hands,
head_hnd,
set_hnd,
eye_tracking_hnd,
click_hnd,
grab_hnd,
scroll_hnd,
@@ -129,6 +138,10 @@ impl OpenVrInputSource {
})
}
pub fn open_bindings(&mut self, input: &mut InputManager) {
let _ = input.open_binding_ui(None, None, self.hands[0].input_hnd, false);
}
pub fn haptics(&mut self, input: &mut InputManager, hand: usize, haptics: &Haptics) {
let action_handle = self.hands[hand].haptics_hnd;
let _ = input.trigger_haptic_vibration_action(
@@ -182,90 +195,188 @@ impl OpenVrInputSource {
},
});
let _ = input.update_actions(&mut [aas_left, aas_right]);
let aas_head = ActiveActionSet(ovr_overlay::sys::VRActiveActionSet_t {
ulActionSet: self.set_hnd.0,
ulRestrictedToDevice: self.head_hnd.0,
ulSecondaryActionSet: 0,
unPadding: 0,
nPriority: 0x0,
});
let _ = input.update_actions(&mut [aas_left, aas_right, aas_head]);
let eye_gaze = input
.get_eye_tracking_data_relative_to_now(self.eye_tracking_hnd, universe.clone(), 0.005)
.ok()
.and_then(|data| eye_tracking_pose(&data.0));
app.input_state.eye_gaze = eye_gaze;
let devices = system.get_device_to_absolute_tracking_pose(universe.clone(), 0.005);
if devices[0].bPoseIsValid {
app.input_state.hmd = devices[0].mDeviceToAbsoluteTracking.to_affine();
let hmd = devices[0].mDeviceToAbsoluteTracking.to_affine();
let hmd_tracked = devices[0].bPoseIsValid;
if hmd_tracked {
app.input_state.hmd = hmd;
}
for i in 0..2 {
let hand = &mut self.hands[i];
let app_hand = &mut app.input_state.pointers[i];
let picking_focus = app.input_state.picking_focus;
let mut any_tracked = false;
if let Some(device) = hand.device.filter(|_| !overlay.is_dashboard_visible()) {
app_hand.raw_pose = devices[device.0 as usize]
.mDeviceToAbsoluteTracking
.to_affine();
app_hand.tracked = devices[device.0 as usize].bPoseIsValid;
} else {
app_hand.tracked = false;
if picking_focus.is_none() {
for i in 0..2 {
let hand = &mut self.hands[i];
let app_hand = &mut app.input_state.pointers[i];
app_hand.handsfree = false;
if let Some(device) = hand.device.filter(|_| !overlay.is_dashboard_visible()) {
app_hand.raw_pose = devices[device.0 as usize]
.mDeviceToAbsoluteTracking
.to_affine();
app_hand.tracked = devices[device.0 as usize].bPoseIsValid;
} else {
app_hand.tracked = false;
}
any_tracked |= app_hand.tracked;
hand.has_pose = false;
let _ = input
.get_pose_action_data_relative_to_now(
hand.pose_hnd,
universe.clone(),
0.005,
INPUT_ANY,
)
.map(|pose| {
app_hand.pose = pose.0.pose.mDeviceToAbsoluteTracking.to_affine();
hand.has_pose = true;
});
app_hand.now.click = input
.get_digital_action_data(self.click_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.grab = input
.get_digital_action_data(self.grab_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.alt_click = input
.get_digital_action_data(self.alt_click_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.show_hide = input
.get_digital_action_data(self.show_hide_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.toggle_dashboard = input
.get_digital_action_data(self.toggle_dashboard_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.space_drag = input
.get_digital_action_data(self.space_drag_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.space_rotate = input
.get_digital_action_data(self.space_rotate_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.space_reset = input
.get_digital_action_data(self.space_reset_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.click_modifier_right = input
.get_digital_action_data(self.click_modifier_right_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.click_modifier_middle = input
.get_digital_action_data(self.click_modifier_middle_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.move_mouse = input
.get_digital_action_data(self.move_mouse_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
let scroll = input
.get_analog_action_data(self.scroll_hnd, hand.input_hnd)
.map_or((0.0, 0.0), |x| (x.0.x, x.0.y));
app_hand.now.scroll_x = scroll.0;
app_hand.now.scroll_y = scroll.1;
}
} else {
app.input_state.handsfree_state.scroll_x =
app.input_state.handsfree_state.scroll_x.lerp(0.0, 0.7);
app.input_state.handsfree_state.scroll_y =
app.input_state.handsfree_state.scroll_y.lerp(0.0, 0.7);
hand.has_pose = false;
let _ = input
.get_pose_action_data_relative_to_now(
hand.pose_hnd,
universe.clone(),
0.005,
INPUT_ANY,
)
.map(|pose| {
app_hand.pose = pose.0.pose.mDeviceToAbsoluteTracking.to_affine();
hand.has_pose = true;
});
app_hand.now.click = input
.get_digital_action_data(self.click_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.grab = input
.get_digital_action_data(self.grab_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.alt_click = input
.get_digital_action_data(self.alt_click_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.show_hide = input
.get_digital_action_data(self.show_hide_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.toggle_dashboard = input
.get_digital_action_data(self.toggle_dashboard_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.space_drag = input
.get_digital_action_data(self.space_drag_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.space_rotate = input
.get_digital_action_data(self.space_rotate_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.space_reset = input
.get_digital_action_data(self.space_reset_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.click_modifier_right = input
.get_digital_action_data(self.click_modifier_right_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.click_modifier_middle = input
.get_digital_action_data(self.click_modifier_middle_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
app_hand.now.move_mouse = input
.get_digital_action_data(self.move_mouse_hnd, hand.input_hnd)
.is_ok_and(|x| x.0.bState);
let scroll = input
.get_analog_action_data(self.scroll_hnd, hand.input_hnd)
.map_or((0.0, 0.0), |x| (x.0.x, x.0.y));
app_hand.now.scroll_x = scroll.0;
app_hand.now.scroll_y = scroll.1;
for pointer in &mut app.input_state.pointers {
pointer.before = pointer.now;
pointer.now = PointerState::default();
pointer.tracked = false;
pointer.handsfree = false;
}
}
if !picking_focus.is_none() || !any_tracked {
let mode: HandsfreePointer = if picking_focus.is_none() {
app.session.config.handsfree_pointer
} else {
app.session.config.handsfree_alt_tab.into()
};
let handsfree_state = app.input_state.handsfree_state;
let delta_time = app.delta_time;
let pointer_lerp_factor = app.session.config.pointer_lerp_factor;
Self::update_handsfree(
&mut app.input_state.pointers[0],
mode,
hmd,
hmd_tracked,
eye_gaze,
handsfree_state,
delta_time,
pointer_lerp_factor,
);
}
}
fn update_handsfree(
pointer: &mut Pointer,
mode: HandsfreePointer,
hmd: Affine3A,
hmd_tracked: bool,
eye_gaze: Option<Affine3A>,
handsfree_state: PointerState,
delta_time: f32,
pointer_lerp_factor: f32,
) {
let (raw_pose, tracked, lerp_scale) = match mode {
HandsfreePointer::None => return,
HandsfreePointer::Hmd | HandsfreePointer::HmdOnly => (hmd, hmd_tracked, 1.0),
HandsfreePointer::EyeTracking | HandsfreePointer::EyeTrackingOnly => {
let Some(gaze) = eye_gaze else {
pointer.tracked = false;
pointer.handsfree = false;
pointer.now = handsfree_pointer_state(handsfree_state);
return;
};
// Match the OpenXR backend's stronger smoothing for eye gaze.
(gaze, true, 0.5)
}
};
let cur_quat = Quat::from_affine3(&pointer.pose);
let cur_pos = Vec3::from(pointer.pose.translation);
let new_quat = Quat::from_affine3(&raw_pose);
let new_pos = Vec3::from(raw_pose.translation);
let lerp_factor = (delta_time * 100.0 * pointer_lerp_factor * lerp_scale).clamp(0.1, 1.0);
pointer.raw_pose = raw_pose;
pointer.pose = Affine3A::from_rotation_translation(
cur_quat.lerp(new_quat, lerp_factor),
cur_pos.lerp(new_pos, lerp_factor),
);
pointer.tracked = tracked;
pointer.handsfree = tracked;
pointer.now = handsfree_pointer_state(handsfree_state);
}
pub fn update_devices(&mut self, system: &mut SystemManager, app: &mut AppState) -> bool {
@@ -317,6 +428,49 @@ impl OpenVrInputSource {
}
}
fn handsfree_pointer_state(state: PointerState) -> PointerState {
PointerState {
click: state.click,
grab: state.grab,
grab_float: state.grab_float,
click_modifier_right: state.click_modifier_right,
click_modifier_middle: state.click_modifier_middle,
scroll_x: state.scroll_x,
scroll_y: state.scroll_y,
..PointerState::default()
}
}
fn eye_tracking_pose(data: &ovr_overlay::sys::VREyeTrackingData_t) -> Option<Affine3A> {
if !(data.bActive && data.bValid && data.bTracked) {
return None;
}
let origin = Vec3::new(
data.vGazeOrigin.v[0],
data.vGazeOrigin.v[1],
data.vGazeOrigin.v[2],
);
let target = Vec3::new(
data.vGazeTarget.v[0],
data.vGazeTarget.v[1],
data.vGazeTarget.v[2],
);
if !origin.is_finite() || !target.is_finite() {
return None;
}
let direction = target - origin;
if !direction.is_finite() || direction.length_squared() <= f32::EPSILON {
return None;
}
Some(Affine3A::from_rotation_translation(
Quat::from_rotation_arc(Vec3::NEG_Z, direction.normalize()),
origin,
))
}
fn get_tracked_device(
system: &mut SystemManager,
index: TrackedDeviceIndex,
@@ -395,6 +549,18 @@ pub fn set_action_manifest(input: &mut InputManager) -> anyhow::Result<()> {
.write_all(include_bytes!("../../res/actions_binding_oculus.json"))?;
}
let binding_path = config_io::get_config_root().join("actions_binding_frame.json");
if !binding_path.is_file() {
File::create(&binding_path)?
.write_all(include_bytes!("../../res/actions_binding_frame.json"))?;
}
let binding_path = config_io::get_config_root().join("actions_binding_generic_hmd.json");
if !binding_path.is_file() {
File::create(&binding_path)?
.write_all(include_bytes!("../../res/actions_binding_generic_hmd.json"))?;
}
if let Err(e) = input.set_action_manifest(action_path.as_path()) {
bail!("Failed to set action manifest: {e}");
}
+3 -78
View File
@@ -2,29 +2,14 @@ use std::f32::consts::PI;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use ash::vk::SubmitInfo;
use glam::{Affine3A, Quat, Vec3, Vec3A, Vec4};
use idmap::IdMap;
use ovr_overlay::overlay::OverlayManager;
use ovr_overlay::sys::ETrackingUniverseOrigin;
use vulkano::{
VulkanObject,
command_buffer::{
CommandBufferBeginInfo, CommandBufferLevel, CommandBufferUsage, RecordingCommandBuffer,
},
format::Format,
image::view::ImageView,
image::{Image, ImageLayout},
sync::{
AccessFlags, DependencyInfo, ImageMemoryBarrier, PipelineStages,
fence::{Fence, FenceCreateInfo},
},
};
use wgui::gfx::WGfx;
use wgui::gfx::{CommandBufferUsage, Format, ImageView, WGfx};
use wlx_common::overlays::{BackendAttrib, BackendAttribValue};
use crate::backend::input::{HoverResult, PointerHit};
use crate::graphics::ExtentExt;
use crate::state::AppState;
use crate::subsystem::hid::WheelDelta;
use crate::windowing::Z_ORDER_LINES;
@@ -53,15 +38,7 @@ impl LinePool {
let texture = command_buffer.upload_image(2, 2, Format::R8G8B8A8_UNORM, &buf)?;
command_buffer.build_and_execute_now()?;
transition_layout(
&graphics,
texture.clone(),
ImageLayout::ShaderReadOnlyOptimal,
ImageLayout::TransferSrcOptimal,
)?
.wait(None)?;
let view = ImageView::new_default(texture)?;
let view = graphics.create_image_view(texture)?;
Ok(Self {
lines: IdMap::new(),
@@ -222,7 +199,7 @@ impl OverlayBackend for LineBackend {
}
fn frame_meta(&mut self) -> Option<FrameMeta> {
Some(FrameMeta {
extent: self.view.extent_u32arr(),
extent: self.view.extent_2d(),
..Default::default()
})
}
@@ -247,55 +224,3 @@ impl OverlayBackend for LineBackend {
false
}
}
pub fn transition_layout(
gfx: &WGfx,
image: Arc<Image>,
old_layout: ImageLayout,
new_layout: ImageLayout,
) -> anyhow::Result<Fence> {
let barrier = ImageMemoryBarrier {
src_stages: PipelineStages::ALL_TRANSFER,
src_access: AccessFlags::TRANSFER_WRITE,
dst_stages: PipelineStages::ALL_TRANSFER,
dst_access: AccessFlags::TRANSFER_READ,
old_layout,
new_layout,
subresource_range: image.subresource_range(),
..ImageMemoryBarrier::image(image)
};
let command_buffer = unsafe {
let mut builder = RecordingCommandBuffer::new(
gfx.command_buffer_allocator.clone(),
gfx.queue_gfx.queue_family_index(),
CommandBufferLevel::Primary,
CommandBufferBeginInfo {
usage: CommandBufferUsage::OneTimeSubmit,
inheritance_info: None,
..Default::default()
},
)?;
builder.pipeline_barrier(&DependencyInfo {
image_memory_barriers: smallvec::smallvec![barrier],
..Default::default()
})?;
builder.end()?
};
let fence = Fence::new(gfx.device.clone(), FenceCreateInfo::default())?;
let fns = gfx.device.fns();
unsafe {
(fns.v1_0.queue_submit)(
gfx.queue_gfx.handle(),
1,
[SubmitInfo::default().command_buffers(&[command_buffer.handle()])].as_ptr(),
fence.handle(),
)
}
.result()?;
Ok(fence)
}
+61 -14
View File
@@ -11,7 +11,6 @@ use ovr_overlay::{
sys::{ETrackedDeviceProperty, EVRApplicationType, EVREventType},
};
use smallvec::smallvec;
use vulkano::{Handle, VulkanObject, device::physical::PhysicalDevice};
use wgui::i18n::Translation;
use wlx_common::{dash_interface::InterfaceFeats, overlays::ToastTopic};
@@ -77,16 +76,12 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
let mut chaperone_mgr = context.chaperone_setup_mngr();
let mut compositor_mgr = context.compositor_mngr();
let device_extensions_fn = |device: &PhysicalDevice| {
let names = compositor_mgr.get_vulkan_device_extensions_required(device.handle().as_raw());
names.iter().map(std::string::String::as_str).collect()
let device_extensions_fn = |physical_device_handle: u64| {
compositor_mgr.get_vulkan_device_extensions_required(physical_device_handle)
};
let mut compositor_mgr = context.compositor_mngr();
let instance_extensions = {
let names = compositor_mgr.get_vulkan_instance_extensions_required();
names.iter().map(std::string::String::as_str).collect()
};
let instance_extensions = compositor_mgr.get_vulkan_instance_extensions_required();
let feats = InterfaceFeats::default_for_backend(XrBackend::OpenVR);
@@ -121,6 +116,10 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
let mut overlays = OverlayWindowManager::<OpenVrOverlayData>::new(&mut app, args.headless)?;
let mut dashboard_hidden = false;
let mut dash_toast_shown = false;
let mut dash_toast_id = None;
let mut playspace = playspace::PlayspaceMover::new();
playspace.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
@@ -209,6 +208,39 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
}
}
if !dashboard_hidden {
if overlay_mgr.is_dashboard_visible() {
if !dash_toast_shown {
let id = Toast::new(
ToastTopic::Priority,
Some(Translation::from_translation_key(
"TOAST.STEAMVR_DASH_TITLE",
)),
Translation::from_translation_key("TOAST.STEAMVR_DASH_TEXT"),
)
.with_timeout(300.0) // we'll zero this if dash gets hidden
.submit(&mut app);
dash_toast_id = Some(id);
dash_toast_shown = true;
}
} else {
dashboard_hidden = true;
if let Some(id) = dash_toast_id {
Toast::expire_now(&mut app, id);
} else if !args.show {
Toast::new(
ToastTopic::Priority,
Some(Translation::from_translation_key(
"TOAST.WAYVR_RUNNING_TITLE",
)),
Translation::from_translation_key("TOAST.WAYVR_RUNNING_TEXT"),
)
.with_timeout(5.0)
.submit(&mut app);
}
}
}
if next_device_update <= Instant::now() {
let changed = input_source.update_devices(&mut system_mgr, &mut app);
if changed {
@@ -238,6 +270,13 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
OpenVrTask::ColorGain(channel, value) => {
let _ = adjust_gain(&mut settings_mgr, channel, value);
}
OpenVrTask::ShowBindingsUI => {
// hide current set because overlay sort order is broken
// and will cover the bindings UI while not taking input
overlays.switch_to_set(&mut app, None, false);
input_source.open_bindings(&mut input_mgr);
}
},
#[cfg(feature = "openxr")]
TaskType::OpenXR(_) => {}
@@ -280,7 +319,7 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
.enqueue(TaskType::Overlay(OverlayTask::ToggleDashboard));
}
overlays.values_mut().for_each(|o| o.config.tick(&mut app));
overlays.tick(&mut app);
playspace.update(&mut chaperone_mgr, &mut overlays, &mut app);
@@ -288,7 +327,9 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
let haptics = interact(&mut overlays, &mut app, &mut current_lines);
for (idx, haptics) in haptics.iter().enumerate() {
if let Some(haptics) = haptics {
if let Some(haptics) = haptics
&& !app.input_state.pointers[idx].handsfree
{
input_source.haptics(&mut input_mgr, idx, haptics);
}
}
@@ -326,6 +367,9 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
for o in overlays.values_mut() {
if o.data.visible {
if !(dashboard_hidden || o.data.always_visible) {
continue;
}
let ShouldRender::Should = o.should_render(&mut app)? else {
continue;
};
@@ -336,16 +380,19 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
let mut rdr = RenderResources::new(app.gfx.clone(), tgt, &meta)?;
o.render(&mut app, &mut rdr)?;
o.data.image_dirty = true;
futures.execute_results(rdr.end()?)?;
futures.execute_results(rdr.end()?);
}
}
log::trace!("Rendering overlays");
futures.wait()?;
overlays
.values_mut()
.for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &app.gfx));
overlays.values_mut().for_each(|o| {
if !(dashboard_hidden || o.data.always_visible) {
return;
}
o.after_render(universe.clone(), &mut overlay_mgr, &app.gfx);
});
lines.reset();
+23 -20
View File
@@ -1,4 +1,3 @@
use core::f32;
use std::sync::Arc;
use glam::Vec4;
@@ -7,13 +6,9 @@ use ovr_overlay::{
pose::Matrix3x4,
sys::{ETrackingUniverseOrigin, VRVulkanTextureData_t},
};
use vulkano::{
Handle, VulkanObject,
image::{ImageUsage, view::ImageView},
};
use wgui::gfx::WGfx;
use wgui::gfx::{ImageLayout, ImageUsage, ImageView, WGfx};
use crate::{graphics::ExtentExt, state::AppState, windowing::window::OverlayWindowData};
use crate::{overlays::toast::TOAST_NAME, state::AppState, windowing::window::OverlayWindowData};
use super::helpers::Affine3AConvert;
@@ -21,6 +16,7 @@ use super::helpers::Affine3AConvert;
pub(super) struct OpenVrOverlayData {
pub(super) handle: Option<OverlayHandle>,
pub(super) visible: bool,
pub(super) always_visible: bool,
pub(super) color: Vec4,
pub(crate) width: f32,
pub(super) override_width: bool,
@@ -44,6 +40,7 @@ impl OverlayWindowData<OpenVrOverlayData> {
};
log::debug!("{}: initialize", self.config.name);
self.data.always_visible = self.config.name.as_ref() == TOAST_NAME.as_ref();
self.data.handle = Some(handle);
self.data.color = Vec4::ONE;
@@ -68,7 +65,7 @@ impl OverlayWindowData<OpenVrOverlayData> {
extent: [u32; 2],
) -> anyhow::Result<Arc<ImageView>> {
if let Some(image_view) = self.data.image_view.as_ref()
&& image_view.extent_u32arr() == extent
&& image_view.extent_2d() == extent
{
return Ok(image_view.clone());
}
@@ -83,10 +80,10 @@ impl OverlayWindowData<OpenVrOverlayData> {
let image = app.gfx.new_image(
extent[0],
extent[1],
app.gfx.surface_format,
app.gfx.surface_format(),
ImageUsage::TRANSFER_SRC | ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED,
)?;
let image_view = ImageView::new_default(image)?;
let image_view = app.gfx.create_image_view(image)?;
self.data.image_view = Some(image_view.clone());
Ok(image_view)
}
@@ -293,20 +290,26 @@ impl OverlayWindowData<OpenVrOverlayData> {
}
}
let raw_image = image.handle().as_raw();
let format = image.format();
if let Err(e) = graphics.transition_image_now(&image, ImageLayout::TransferSrc) {
log::error!(
"{}: Failed to prepare overlay image: {e:#}",
self.config.name
);
return;
}
let format = image.format();
let mut texture = VRVulkanTextureData_t {
m_nImage: raw_image,
m_nFormat: format as _,
m_nImage: image.raw_handle(),
m_nFormat: graphics.raw_format(format) as _,
m_nWidth: dimensions[0],
m_nHeight: dimensions[1],
m_nSampleCount: image.samples() as u32,
m_pDevice: graphics.device.handle().as_raw() as *mut _,
m_pPhysicalDevice: graphics.device.physical_device().handle().as_raw() as *mut _,
m_pInstance: graphics.instance.handle().as_raw() as *mut _,
m_pQueue: graphics.queue_gfx.handle().as_raw() as *mut _,
m_nQueueFamilyIndex: graphics.queue_gfx.queue_family_index(),
m_nSampleCount: 1,
m_pDevice: graphics.raw_device_handle() as *mut _,
m_pPhysicalDevice: graphics.raw_physical_device_handle() as *mut _,
m_pInstance: graphics.raw_instance_handle() as *mut _,
m_pQueue: graphics.raw_graphics_queue_handle() as *mut _,
m_nQueueFamilyIndex: graphics.graphics_queue_family_index(),
};
log::trace!(
"{}: UploadTex {:?}, {}x{}, {:?}",
-9
View File
@@ -339,15 +339,6 @@ impl PlayspaceMover {
);
self.universe = new_universe;
}
if self.drag.is_some() {
log::info!("Space drag interrupted by external change");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by external change");
self.rotate = None;
}
}
pub fn get_universe(&self) -> ETrackingUniverseOrigin {
+35 -8
View File
@@ -19,8 +19,15 @@ pub(crate) use next_chain_insert;
use crate::state::AppState;
pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
#[derive(Default)]
pub(super) struct ExtraExts {
pub mndx_composition_layer_alpha_blend_system_ui: bool,
pub ext_eye_gaze_interaction: bool,
}
pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId, ExtraExts), anyhow::Error> {
let entry = xr::Entry::linked();
let mut extra_exts = ExtraExts::default();
let Ok(available_extensions) = entry.enumerate_extensions() else {
bail!("Failed to enumerate OpenXR extensions.");
@@ -43,11 +50,6 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
} else {
log::warn!("Missing EXT_dpad_binding extension.");
}
if available_extensions.fb_composition_layer_alpha_blend {
enabled_extensions.fb_composition_layer_alpha_blend = true;
} else {
log::warn!("Missing XR_FB_composition_layer_alpha_blend extension.");
}
if available_extensions.ext_samsung_odyssey_controller {
enabled_extensions.ext_samsung_odyssey_controller = true;
@@ -66,6 +68,7 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
}
if available_extensions.ext_eye_gaze_interaction {
enabled_extensions.ext_eye_gaze_interaction = true;
extra_exts.ext_eye_gaze_interaction = true;
} else {
log::warn!("Missing EXT_eye_gaze_interaction extension.");
}
@@ -85,6 +88,14 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
log::warn!("Missing XR_KHR_composition_layer_color_scale_bias extension.");
}
let xr_extension = b"XR_MNDX_composition_layer_alpha_blend_system_ui\0".to_vec();
if available_extensions.other.contains(&xr_extension) {
enabled_extensions.other.push(xr_extension);
extra_exts.mndx_composition_layer_alpha_blend_system_ui = true;
} else {
log::warn!("Missing XR_MNDX_composition_layer_alpha_blend_system_ui extension.");
}
let xr_extension = b"XR_MNDX_system_buttons\0".to_vec();
if available_extensions.other.contains(&xr_extension) {
enabled_extensions.other.push(xr_extension);
@@ -131,6 +142,22 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
instance_props.runtime_version
);
let available_extensions = available_extensions
.names()
.into_iter()
.filter_map(|name| {
Some(
CStr::from_bytes_with_nul(name)
.ok()?
.to_string_lossy()
.into_owned(),
)
})
.collect::<Vec<_>>()
.join("\n");
log::debug!("Available OpenXR extensions:\n{available_extensions}");
let enabled_extensions = enabled_extensions
.names()
.into_iter()
@@ -145,7 +172,7 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
.collect::<Vec<_>>()
.join("\n");
log::info!("OpenXR extensions:\n{enabled_extensions}");
log::debug!("Enabled OpenXR extensions:\n{enabled_extensions}");
let Ok(system) = xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY) else {
bail!("Failed to access OpenXR HMD system.");
@@ -167,7 +194,7 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
);
}
Ok((xr_instance, system))
Ok((xr_instance, system, extra_exts))
}
pub(super) unsafe fn create_overlay_session(
File diff suppressed because it is too large. Load diff
+6 -13
View File
@@ -11,20 +11,13 @@ use std::{
};
use wgui::gfx::{
Buffer, BufferUsage, CommandBufferUsage, Scissor, Vert2Uv,
cmd::WGfxClearMode,
pass::WGfxPass,
pipeline::{WGfxPipeline, WPipelineCreateInfo},
};
use crate::{
backend::openxr::helpers,
graphics::{GpuFutures, Vert2Uv},
state::AppState,
};
use vulkano::{
buffer::{BufferUsage, Subbuffer},
command_buffer::CommandBufferUsage,
};
use crate::{backend::openxr::helpers, graphics::GpuFutures, state::AppState};
use super::{
CompositionLayer, XrState,
@@ -55,7 +48,7 @@ impl LinePool {
let pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
app.gfx_extras.shaders.get("frag_color").unwrap(), // want panic
WPipelineCreateInfo::new(app.gfx.surface_format),
WPipelineCreateInfo::new(app.gfx.surface_format()),
)?;
Ok(Self {
@@ -80,7 +73,7 @@ impl LinePool {
0..4,
0..1,
vec![set0],
&Default::default(),
Scissor::from_viewport([1.0, 1.0], [0.0, 0.0]),
)?;
let srd = create_swapchain(xr, app.gfx.clone(), [1, 1], 1, SwapchainOpts::new())?;
@@ -182,7 +175,7 @@ impl LinePool {
cmd_buffer.run_ref(&line.pass)?;
cmd_buffer.end_rendering()?;
futures.execute(cmd_buffer.queue.clone(), cmd_buffer.build()?)?;
futures.execute(cmd_buffer.build()?);
}
}
@@ -225,7 +218,7 @@ pub(super) struct Line {
struct LineContainer {
swapchain: WlxSwapchain,
buf_color: Subbuffer<[f32]>,
buf_color: Arc<Buffer<f32>>,
pass: WGfxPass<Vert2Uv>,
maybe_line: Option<Line>,
}
+78 -26
View File
@@ -9,7 +9,6 @@ use glam::{Affine3A, Vec3};
use input::OpenXrInputSource;
use openxr as xr;
use skybox::{Skybox, create_skybox};
use vulkano::{Handle, VulkanObject};
use wgui::i18n::Translation;
use wlx_common::{
dash_interface::InterfaceFeats,
@@ -21,7 +20,12 @@ use crate::{
backend::{
BackendError, RunParams,
input::interact,
openxr::{helpers::try_apply_chroma_key, lines::LinePool, overlay::OpenXrOverlayData},
openxr::{
helpers::{ExtraExts, try_apply_chroma_key},
lines::LinePool,
overlay::OpenXrOverlayData,
swapchain::SwapchainRetirementQueue,
},
task::{OpenXrTask, OverlayTask, TaskType},
},
config::{save_settings, save_state},
@@ -53,12 +57,25 @@ struct XrState {
fps: f32,
stage: Arc<xr::Space>,
view: Arc<xr::Space>,
extra_exts: ExtraExts,
}
/// XR runtime using our vk device/queue can have submissions in flight after last xrEndFrame/xrReleaseSwapchainImage
/// This guard is dropped first, draining shared device before swapchain/session teardown begins
struct OpenXrGpuIdleGuard(Arc<wgui::gfx::WGfx>);
impl Drop for OpenXrGpuIdleGuard {
fn drop(&mut self) {
if let Err(e) = self.0.wait_idle() {
log::error!("Failed waiting for Vulkan device idle during OpenXR shutdown: {e:#}");
}
}
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
let (xr_instance, system) = match helpers::init_xr() {
Ok((xr_instance, system)) => (xr_instance, system),
let (xr_instance, system, extra_exts) = match helpers::init_xr() {
Ok((xr_instance, system, extra_exts)) => (xr_instance, system, extra_exts),
Err(e) => {
if !args.wait {
log::warn!("Will not use OpenXR: {e}");
@@ -68,10 +85,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
};
let feats = InterfaceFeats {
passthru: xr_instance
.exts()
.fb_composition_layer_alpha_blend
.is_some(),
passthru: extra_exts.mndx_composition_layer_alpha_blend_system_ui,
..InterfaceFeats::default_for_backend(XrBackend::OpenXR)
};
@@ -83,6 +97,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
app.session.no_autostart = args.no_autostart;
let modes = xr_instance.enumerate_environment_blend_modes(system, VIEW_TYPE)?;
log::debug!("Env blend modes: {modes:?}");
if args.show {
app.session.config.tutorial_graduated = true;
@@ -112,11 +127,11 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
&xr_instance,
system,
&xr::vulkan::SessionCreateInfo {
instance: app.gfx.instance.handle().as_raw() as _,
physical_device: app.gfx.device.physical_device().handle().as_raw() as _,
device: app.gfx.device.handle().as_raw() as _,
queue_family_index: app.gfx.queue_gfx.queue_family_index(),
queue_index: 0,
instance: app.gfx.raw_instance_handle() as _,
physical_device: app.gfx.raw_physical_device_handle() as _,
device: app.gfx.raw_device_handle() as _,
queue_family_index: app.gfx.graphics_queue_family_index(),
queue_index: app.gfx.graphics_queue_index(),
},
)?;
xr::Session::from_raw(xr_instance.clone(), raw_session, Box::new(()))
@@ -134,6 +149,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
fps: 30.0,
stage: Arc::new(stage),
view: Arc::new(view),
extra_exts,
};
let mut skybox: Option<Skybox> = None;
@@ -158,6 +174,22 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
let mut main_session_visible = false;
let mut environment_blend_mode = modes[0];
let mut last_frame_time = Instant::now();
let mut retired_swapchains = SwapchainRetirementQueue::default();
// declared last so rust drops this before all long-lived XR objects
let _gpu_idle_before_xr_drop = OpenXrGpuIdleGuard(app.gfx.clone());
if !args.show {
Toast::new(
ToastTopic::Priority,
Some(Translation::from_translation_key(
"TOAST.WAYVR_RUNNING_TITLE",
)),
Translation::from_translation_key("TOAST.WAYVR_RUNNING_TEXT"),
)
.with_timeout(5.0)
.submit(&mut app);
}
'main_loop: loop {
let now = Instant::now();
@@ -192,6 +224,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
&xr_state,
&modes,
&mut skybox,
&mut retired_swapchains,
&mut environment_blend_mode,
main_session_visible,
);
@@ -219,6 +252,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
&xr_state,
&modes,
&mut skybox,
&mut retired_swapchains,
&mut environment_blend_mode,
main_session_visible,
);
@@ -241,6 +275,9 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
continue 'main_loop;
}
// poll only, never wait for GPU
retired_swapchains.collect(&app.gfx);
log::trace!("xrWaitFrame");
let xr_frame_state = frame_wait.wait()?;
log::trace!("xrBeginFrame");
@@ -320,7 +357,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
)?;
let ipd = helpers::ipd_from_views(&views);
if (app.input_state.ipd - ipd).abs() > 0.05 {
if (app.input_state.ipd - ipd).abs() > 0.5 {
log::info!("IPD changed: {} -> {}", app.input_state.ipd, ipd);
app.input_state.ipd = ipd;
Toast::new(
@@ -331,7 +368,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
.submit(&mut app);
}
overlays.values_mut().for_each(|o| o.config.tick(&mut app));
overlays.tick(&mut app);
current_lines.clear();
@@ -406,11 +443,17 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
log::trace!("{}: render new frame", o.config.name);
let meta = o.config.backend.frame_meta().unwrap(); // want panic
let stereo = !matches!(meta.stereo, StereoMode::None);
let wsi = o.ensure_swapchain_acquire(&app, &xr_state, meta.extent, stereo)?;
let wsi = o.ensure_swapchain_acquire(
&app,
&xr_state,
&mut retired_swapchains,
meta.extent,
stereo,
)?;
let tgt = RenderTarget { views: wsi.views };
let mut rdr = RenderResources::new(app.gfx.clone(), tgt, &meta)?;
o.render(&mut app, &mut rdr)?;
futures.execute_results(rdr.end()?)?;
futures.execute_results(rdr.end()?);
} else if o.data.swapchain.is_none() {
log::trace!("{}: not showing due to missing swapchain", o.config.name);
continue;
@@ -502,6 +545,7 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
&xr_state,
&modes,
&mut skybox,
&mut retired_swapchains,
&mut environment_blend_mode,
main_session_visible,
);
@@ -512,10 +556,14 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
}
}
while let Some(o) = overlays.pop_dropped() {
while let Some(mut o) = overlays.pop_dropped() {
if let Some(swapchain) = o.data.swapchain.take() {
retired_swapchains.retire(swapchain, &app.gfx);
}
delete_queue.push((o, cur_frame + 5));
}
retired_swapchains.collect(&app.gfx);
delete_queue.retain(|(_, frame)| *frame > cur_frame);
//FIXME: Temporary workaround for Monado bug
@@ -559,6 +607,7 @@ fn reconfigure_environment_blend(
xr_state: &XrState,
modes: &[xr::EnvironmentBlendMode],
skybox: &mut Option<Skybox>,
retired_swapchains: &mut SwapchainRetirementQueue,
environment_blend_mode: &mut xr::EnvironmentBlendMode,
main_session_visible: bool,
) {
@@ -579,18 +628,21 @@ fn reconfigure_environment_blend(
&& !main_session_visible;
if want_skybox {
if let Some(curr_skybox) = skybox.as_ref() {
if curr_skybox.needs_recreate(app) {
*skybox = None;
log::debug!("Allocating skybox.");
*skybox = create_skybox(xr_state, app);
if skybox
.as_ref()
.is_some_and(|curr_skybox| curr_skybox.needs_recreate(app))
{
if let Some(old_skybox) = skybox.take() {
old_skybox.retire(retired_swapchains, app);
}
} else {
log::debug!("Allocating skybox.");
*skybox = create_skybox(xr_state, app);
} else if skybox.is_none() {
log::debug!("Allocating skybox.");
*skybox = create_skybox(xr_state, app);
}
} else {
} else if let Some(old_skybox) = skybox.take() {
log::debug!("Destroying skybox.");
*skybox = None;
old_skybox.retire(retired_swapchains, app);
}
}
+6 -7
View File
@@ -8,7 +8,7 @@ use super::{CompositionLayer, XrState, helpers, swapchain::WlxSwapchain};
use crate::{
backend::openxr::{
helpers::next_chain_insert,
swapchain::{SwapchainOpts, WlxSwapchainImage, create_swapchain},
swapchain::{SwapchainOpts, SwapchainRetirementQueue, WlxSwapchainImage, create_swapchain},
},
state::AppState,
windowing::window::{OverlayCategory, OverlayWindowData},
@@ -29,6 +29,7 @@ impl OverlayWindowData<OpenXrOverlayData> {
&'a mut self,
app: &AppState,
xr: &'a XrState,
retired_swapchains: &mut SwapchainRetirementQueue,
extent: [u32; 2],
stereo: bool,
) -> anyhow::Result<WlxSwapchainImage> {
@@ -56,7 +57,9 @@ impl OverlayWindowData<OpenXrOverlayData> {
SwapchainOpts::new(),
)?;
let tgt = swapchain.acquire_wait_image()?;
self.data.swapchain = Some(swapchain);
if let Some(old_swapchain) = self.data.swapchain.replace(swapchain) {
retired_swapchains.retire(old_swapchain, &app.gfx);
}
Ok(tgt)
}
@@ -111,11 +114,7 @@ impl OverlayWindowData<OpenXrOverlayData> {
flags |= CompositionLayerFlags::UNPREMULTIPLIED_ALPHA;
}
if xr
.instance
.exts()
.fb_composition_layer_alpha_blend
.is_some()
if xr.extra_exts.mndx_composition_layer_alpha_blend_system_ui
&& self.config.category == OverlayCategory::Passthru
&& self.data.layer_alpha_blend_fb.is_none()
&& !self.config.editing
+26 -19
View File
@@ -21,7 +21,7 @@ struct MoverData<T> {
pub(super) struct PlayspaceMover {
drag: Option<MoverData<Vec3A>>,
rotate: Option<MoverData<Quat>>,
rotate: Option<MoverData<Affine3A>>,
gravity: SpaceGravity,
playspace_state: PlayspaceState,
}
@@ -93,32 +93,37 @@ impl PlayspaceMover {
return;
}
let pose_quat = Quat::from_affine3(&data.pose).normalize();
let new_hand = (pose_quat
* Quat::from_affine3(&app.input_state.pointers[data.hand].raw_pose))
.normalize();
let new_hand = data.pose * app.input_state.pointers[data.hand].raw_pose;
let before_pose = data.pose;
let dq = (pose_quat.conjugate() * (new_hand * data.hand_pose.conjugate()) * pose_quat)
.normalize();
let mut space_transform = if app.session.config.space_rotate_unlocked {
Affine3A::from_quat(dq)
if app.session.config.space_rotate_unlocked {
// pivot on the hand so the body orbits it
let pose = new_hand * data.hand_pose.inverse() * data.pose;
data.pose = Affine3A::from_rotation_translation(
Quat::from_affine3(&pose).normalize(),
pose.translation.into(),
);
} else {
let pose_quat = Quat::from_affine3(&data.pose).normalize();
let dq = (pose_quat.conjugate()
* Quat::from_affine3(&(new_hand * data.hand_pose.inverse()))
* pose_quat)
.normalize();
let rel_y = f32::atan2(
2.0 * dq.y.mul_add(dq.w, dq.x * dq.z),
2.0f32.mul_add(dq.w.mul_add(dq.w, dq.x * dq.x), -1.0),
);
Affine3A::from_rotation_y(rel_y)
};
let offset = (space_transform.transform_vector3a(app.input_state.hmd.translation)
- app.input_state.hmd.translation)
* -1.0;
let mut space_transform = Affine3A::from_rotation_y(rel_y);
space_transform.translation = (space_transform
.transform_vector3a(app.input_state.hmd.translation)
- app.input_state.hmd.translation)
* -1.0;
space_transform.translation = offset;
data.pose *= space_transform;
data.pose.matrix3 = Mat3A::from_quat(Quat::from_affine3(&data.pose).normalize());
}
let before_pose = data.pose;
data.pose *= space_transform;
data.pose.matrix3 = Mat3A::from_quat(Quat::from_affine3(&data.pose).normalize());
if !app.session.config.space_drag_affects_world {
playspace_common::shift_world(overlays, &mut app.anchor, &before_pose, &data.pose);
}
@@ -126,6 +131,8 @@ impl PlayspaceMover {
apply_offset(data.pose, &mut monado.ipc);
self.rotate = Some(data);
// drag and rotate can share a button
return;
} else {
// start space rotate
for (i, pointer) in app.input_state.pointers.iter().enumerate() {
@@ -144,7 +151,7 @@ impl PlayspaceMover {
return;
};
let hand_pose = Quat::from_affine3(&(pose * pointer.raw_pose)).normalize();
let hand_pose = pose * pointer.raw_pose;
self.rotate = Some(MoverData {
pose,
hand: i,
+30 -18
View File
@@ -2,19 +2,18 @@ use std::{f32::consts::PI, fs::File, sync::Arc};
use glam::{Quat, Vec3A};
use openxr as xr;
use vulkano::{
command_buffer::CommandBufferUsage, image::view::ImageView,
pipeline::graphics::color_blend::AttachmentBlend,
use wgui::gfx::{
BLEND_ALPHA, CommandBufferUsage, ImageView, Scissor, cmd::WGfxClearMode,
pipeline::WPipelineCreateInfo,
};
use wgui::gfx::{cmd::WGfxClearMode, pipeline::WPipelineCreateInfo};
use wlx_common::config_io;
use crate::{
backend::openxr::{
helpers::{next_chain_insert, translation_rotation_to_posef},
swapchain::SwapchainOpts,
swapchain::{SwapchainOpts, SwapchainRetirementQueue},
},
graphics::{ExtentExt, GpuFutures, dds::WlxCommandBufferDds},
graphics::{GpuFutures, dds::WlxCommandBufferDds},
state::AppState,
};
@@ -76,7 +75,7 @@ impl Skybox {
let view = if let Some(image) = maybe_image {
command_buffer.build_and_execute_now()?;
Some(ImageView::new_default(image)?)
Some(app.gfx.create_image_view(image)?)
} else {
None
};
@@ -113,6 +112,19 @@ impl Skybox {
*self.current_skybox != *app.session.config.skybox_texture
}
pub(super) fn retire(
mut self,
retired_swapchains: &mut SwapchainRetirementQueue,
app: &AppState,
) {
if let Some(swapchain) = self.sky.take() {
retired_swapchains.retire(swapchain, &app.gfx);
}
if let Some(swapchain) = self.grid.take() {
retired_swapchains.retire(swapchain, &app.gfx);
}
}
fn prepare_sky<'a>(
&'a mut self,
xr: &'a XrState,
@@ -127,7 +139,7 @@ impl Skybox {
let extent = self
.view
.as_ref()
.map_or([4096, 4096], ExtentExt::extent_u32arr);
.map_or([4096, 4096], |view| view.extent_2d());
let mut swapchain = create_swapchain(xr, app.gfx.clone(), extent, 1, opts)?;
let tgt = swapchain
@@ -141,11 +153,11 @@ impl Skybox {
let pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
app.gfx_extras.shaders.get("frag_srgb").unwrap(), // want panic
WPipelineCreateInfo::new(app.gfx.surface_format),
WPipelineCreateInfo::new(app.gfx.surface_format()),
)?;
let set0 = pipeline.uniform_sampler(0, view.clone(), app.gfx.texture_filter)?;
let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?;
let set0 = pipeline.uniform_sampler(0, view.clone(), app.gfx.texture_filter())?;
let set1 = pipeline.uniform_buffer_upload(1, &[1f32])?;
pipeline.create_pass(
tgt.extent_f32(),
[0.0, 0.0],
@@ -153,13 +165,13 @@ impl Skybox {
0..4,
0..1,
vec![set0, set1],
&Default::default(),
Scissor::new([0, 0], tgt.extent_2d()),
)?
} else {
let pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
app.gfx_extras.shaders.get("frag_sky").unwrap(), // want panic
WPipelineCreateInfo::new(app.gfx.surface_format),
WPipelineCreateInfo::new(app.gfx.surface_format()),
)?;
pipeline.create_pass(
@@ -169,7 +181,7 @@ impl Skybox {
0..4,
0..1,
vec![],
&Default::default(),
Scissor::new([0, 0], tgt.extent_2d()),
)?
};
@@ -180,7 +192,7 @@ impl Skybox {
cmd_buffer.run_ref(&pass)?;
cmd_buffer.end_rendering()?;
futures.execute(cmd_buffer.queue.clone(), cmd_buffer.build()?)?;
futures.execute(cmd_buffer.build()?);
self.sky = Some(swapchain);
Ok(())
@@ -207,7 +219,7 @@ impl Skybox {
let pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
app.gfx_extras.shaders.get("frag_grid").unwrap(), // want panic
WPipelineCreateInfo::new(app.gfx.surface_format).use_blend(AttachmentBlend::alpha()),
WPipelineCreateInfo::new(app.gfx.surface_format()).use_blend(BLEND_ALPHA),
)?;
let tgt = swapchain
@@ -223,7 +235,7 @@ impl Skybox {
0..4,
0..1,
vec![],
&Default::default(),
Scissor::new([0, 0], tgt.extent_2d()),
)?;
let mut cmd_buffer = app
@@ -233,7 +245,7 @@ impl Skybox {
cmd_buffer.run_ref(&pass)?;
cmd_buffer.end_rendering()?;
futures.execute(cmd_buffer.queue.clone(), cmd_buffer.build()?)?;
futures.execute(cmd_buffer.build()?);
self.grid = Some(swapchain);
Ok(())
+97 -26
View File
@@ -1,18 +1,12 @@
use std::sync::Arc;
use ash::vk;
use openxr as xr;
use smallvec::SmallVec;
use vulkano::{
Handle,
image::{
ImageCreateInfo, ImageUsage,
sys::RawImage,
view::{ImageView, ImageViewCreateInfo},
},
use wgui::gfx::{
GpuCompletionMarker, ImageCreateInfo, ImageLayout, ImageUsage, ImageView, ImageViewCreateInfo,
WGfx,
};
use wgui::gfx::WGfx;
use super::XrState;
@@ -48,7 +42,7 @@ pub(super) fn create_swapchain(
let swapchain = xr.session.create_swapchain(&xr::SwapchainCreateInfo {
create_flags,
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT | xr::SwapchainUsageFlags::SAMPLED,
format: gfx.surface_format as _,
format: gfx.raw_format(gfx.surface_format()) as _,
sample_count: 1,
width: extent[0],
height: extent[1],
@@ -61,28 +55,35 @@ pub(super) fn create_swapchain(
.enumerate_images()?
.into_iter()
.map(|handle| {
let vk_image = vk::Image::from_raw(handle);
// thanks @yshui
let raw_image = unsafe {
RawImage::from_handle_borrowed(
gfx.device.clone(),
vk_image,
// SAFETY: XR runtime owns the swapchain image & keeps it alive until explicitly destroyed
let image = unsafe {
gfx.wrap_external_image_with_layout(
handle,
ImageCreateInfo {
format: gfx.surface_format as _,
format: gfx.surface_format(),
extent: [extent[0], extent[1], 1],
array_layers: array_size,
usage: ImageUsage::COLOR_ATTACHMENT,
..Default::default()
usage: ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED,
..ImageCreateInfo::new_2d(
extent[0],
extent[1],
gfx.surface_format(),
ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED,
)
},
ImageLayout::ColorAttachment,
)?
};
// SAFETY: OpenXR guarantees that the image is a swapchain image, thus has memory backing it.
let image = Arc::new(unsafe { raw_image.assume_bound() });
let mut wsi = WlxSwapchainImage::default();
for d in 0..array_size {
let mut create_info = ImageViewCreateInfo::from_image(&image);
create_info.subresource_range.array_layers = d..d + 1;
wsi.views.push(ImageView::new(image.clone(), create_info)?);
wsi.views.push(gfx.create_image_view_with_info(
image.clone(),
ImageViewCreateInfo {
base_array_layer: d,
array_layer_count: 1,
..Default::default()
},
)?);
}
Ok(wsi)
})
@@ -91,8 +92,8 @@ pub(super) fn create_swapchain(
Ok(WlxSwapchain {
acquired: false,
ever_acquired: false,
swapchain,
images,
swapchain,
extent,
array_size,
})
@@ -106,10 +107,11 @@ pub(super) struct WlxSwapchainImage {
pub(super) struct WlxSwapchain {
acquired: bool,
pub(super) ever_acquired: bool,
// drop image views before destroying parent swapchain
pub(super) images: SmallVec<[WlxSwapchainImage; 4]>,
pub(super) swapchain: xr::Swapchain<xr::Vulkan>,
pub(super) extent: [u32; 2],
pub(super) array_size: u32,
pub(super) images: SmallVec<[WlxSwapchainImage; 4]>,
}
impl WlxSwapchain {
@@ -143,3 +145,72 @@ impl WlxSwapchain {
.image_array_index(array_index)
}
}
struct RetiredSwapchain {
// keep alive until completion is signaled
_swapchain: WlxSwapchain,
completion: Option<GpuCompletionMarker>,
}
/// Keeps destroyed xrswapchains alive until work submitted by runtime from has finished
#[derive(Default)]
pub(super) struct SwapchainRetirementQueue {
retired: Vec<RetiredSwapchain>,
}
impl SwapchainRetirementQueue {
/// queue the swapchain for non-blocking retirement
pub(super) fn retire(&mut self, swapchain: WlxSwapchain, gfx: &Arc<WGfx>) {
self.retired.push(RetiredSwapchain {
_swapchain: swapchain,
completion: None,
});
let idx = self.retired.len() - 1;
if let Err(e) = Self::arm(&mut self.retired[idx], gfx) {
log::warn!("Failed arming retired OpenXR swapchain: {e:#}");
}
}
fn arm(retired: &mut RetiredSwapchain, gfx: &Arc<WGfx>) -> anyhow::Result<()> {
if retired.completion.is_some() {
return Ok(());
}
retired._swapchain.ensure_image_released()?;
retired.completion = Some(gfx.submit_graphics_completion_marker()?);
Ok(())
}
pub(super) fn collect(&mut self, gfx: &Arc<WGfx>) {
let mut idx = 0;
while idx < self.retired.len() {
if self.retired[idx].completion.is_none() {
if let Err(e) = Self::arm(&mut self.retired[idx], gfx) {
log::warn!("Failed re-arming retired OpenXR swapchain: {e:#}");
idx += 1;
continue;
}
}
let complete = match self.retired[idx]
.completion
.as_ref()
.expect("retired swapchain completion marker missing after arm")
.is_complete()
{
Ok(complete) => complete,
Err(e) => {
log::warn!("Failed polling retired OpenXR swapchain: {e:#}");
false
}
};
if complete {
self.retired.swap_remove(idx);
} else {
idx += 1;
}
}
}
}
+4 -1
View File
@@ -12,7 +12,7 @@ use crate::{
backend::input,
state::AppState,
windowing::{
OverlaySelector,
OverlayID, OverlaySelector,
window::{OverlayCategory, OverlayWindowConfig},
},
};
@@ -56,6 +56,7 @@ pub enum InputTask {
#[allow(dead_code)]
pub enum OpenVrTask {
ColorGain(ColorChannel, f32),
ShowBindingsUI,
}
#[cfg(feature = "openxr")]
@@ -135,6 +136,8 @@ pub enum OverlayTask {
Spawn(OverlaySelector, SpawnPos, Box<CreateOverlayTask>),
ModifyPanel(ModifyPanelTask),
Drop(OverlaySelector),
DropImmediate(OverlaySelector),
DropExpired(OverlayID),
}
pub type GlobalTask = dyn FnOnce(&mut AppState) + Send;
+111 -25
View File
@@ -1,4 +1,5 @@
use anyhow::Context;
use smithay::backend::allocator::Buffer;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::{BufferType, buffer_type};
use smithay::desktop::{
@@ -14,7 +15,9 @@ use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration;
use smithay::reexports::wayland_server::Resource;
use smithay::reexports::wayland_server::backend::ObjectId;
use smithay::reexports::wayland_server::protocol::{wl_buffer, wl_callback, wl_output, wl_seat};
use smithay::reexports::wayland_server::protocol::{
wl_buffer, wl_callback, wl_data_source, wl_output, wl_seat,
};
use smithay::reexports::wayland_server::{self, DisplayHandle};
use smithay::utils::{Logical, Rectangle, Serial, Size};
use smithay::wayland::buffer::BufferHandler;
@@ -40,7 +43,7 @@ use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorati
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
};
use smithay::wayland::shm::{ShmHandler, ShmState, with_buffer_contents};
use smithay::wayland::shm::{ShmHandler, ShmState, shm_format_to_fourcc, with_buffer_contents};
use smithay::wayland::single_pixel_buffer::get_single_pixel_buffer;
use smithay::wayland::viewporter::ViewporterState;
use smithay::{
@@ -57,10 +60,10 @@ use std::sync::{Arc, Mutex};
use wayland_server::Client;
use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
use wayland_server::protocol::wl_surface::WlSurface;
use wlx_capture::DrmFourcc;
use crate::backend::wayvr::image_importer::ImageImporter;
use crate::backend::wayvr::{SurfaceBufWithImage, WAYVR_SCREEN_RES, time};
use crate::graphics::ExtentExt;
use crate::ipc::event_queue::SyncEventQueue;
use super::WayVRTask;
@@ -89,8 +92,15 @@ pub struct Application {
pub viewporter: ViewporterState,
pub display_handle: DisplayHandle,
pub redraw_requests: HashSet<ObjectId>,
pub(super) no_server_decorations: HashSet<ObjectId>,
pub pending_frame_callbacks: HashMap<ObjectId, Vec<wl_callback::WlCallback>>,
pub cursor_image: CursorImageStatus,
/// pointer that last drove the wvr_server mouse. used for DND
pub vr_pointer: usize,
/// pointer that produced the button press that correlates to DND serial
pub pointer_button_owner: Option<usize>,
/// pointer currently owning a DND grab
pub dnd_pointer: Option<usize>,
}
impl Application {
@@ -172,6 +182,23 @@ impl Application {
self.redraw_requests.remove(surface_id)
}
pub fn has_server_side_decorations(&self, surface: &WlSurface) -> bool {
!self.no_server_decorations.contains(&surface.id())
}
fn set_server_side_decorations(&mut self, surface: &WlSurface, enabled: bool) {
let id = surface.id();
let changed = if enabled {
self.no_server_decorations.remove(&id)
} else {
self.no_server_decorations.insert(id.clone())
};
if changed {
self.redraw_requests.insert(id);
}
}
pub fn output_logical_size(&self) -> Size<i32, Logical> {
self.output.current_mode().map_or_else(
|| Size::new(WAYVR_SCREEN_RES[0], WAYVR_SCREEN_RES[1]),
@@ -181,12 +208,7 @@ impl Application {
fn surface_logical_size(surface: &WlSurface) -> Option<Size<i32, Logical>> {
smithay::wayland::compositor::with_states(surface, |states| {
SurfaceBufWithImage::get_from_surface(states).map(|buf| {
let extent = buf.image.extent_u32arr();
let scale = buf.scale.max(1) as u32;
Size::new((extent[0] / scale) as i32, (extent[1] / scale) as i32)
})
SurfaceBufWithImage::get_from_surface(states).map(|buf| buf.logical_size(states))
})
}
@@ -213,6 +235,13 @@ impl Application {
}
}
fn drm_format_has_alpha(format: DrmFourcc) -> bool {
matches!(
format,
DrmFourcc::Abgr8888 | DrmFourcc::Argb8888 | DrmFourcc::Abgr2101010
)
}
impl compositor::CompositorHandler for Application {
fn compositor_state(&mut self) -> &mut compositor::CompositorState {
&mut self.compositor
@@ -239,8 +268,12 @@ impl compositor::CompositorHandler for Application {
Some(BufferType::Dma) => {
let dmabuf = get_dmabuf(&buffer).unwrap(); // always Ok due to buffer_type
if let Ok(image) =
self.image_importer.get_or_import_dmabuf(dmabuf.clone())
if let Ok(image) = self
.image_importer
.get_or_import_dmabuf(dmabuf.clone())
.inspect_err(|e| {
log::warn!("wayland_server failed to import DMA-buf: {e:?}");
})
{
let sbwi = SurfaceBufWithImage {
image,
@@ -249,6 +282,7 @@ impl compositor::CompositorHandler for Application {
),
scale: attrs.buffer_scale,
dmabuf: true,
has_alpha: drm_format_has_alpha(dmabuf.format().code),
};
if let Some(old_buffer) =
@@ -276,6 +310,8 @@ impl compositor::CompositorHandler for Application {
),
scale: attrs.buffer_scale,
dmabuf: false,
has_alpha: shm_format_to_fourcc(buf.format)
.is_some_and(drm_format_has_alpha),
};
sbwi.apply_to_surface(states, None);
}
@@ -297,6 +333,7 @@ impl compositor::CompositorHandler for Application {
),
scale: attrs.buffer_scale,
dmabuf: false,
has_alpha: true,
};
sbwi.apply_to_surface(states, None);
}
@@ -318,6 +355,19 @@ impl compositor::CompositorHandler for Application {
}
});
// validate wp_viewport against currently attached backing buffer
smithay::wayland::compositor::with_states(surface, |states| {
if let Some(buf) = SurfaceBufWithImage::get_from_surface(states) {
let extent = buf.image.extent_2d();
let scale = buf.scale.max(1) as u32;
let buffer_size = Size::new(
(extent[0] / scale).max(1) as i32,
(extent[1] / scale).max(1) as i32,
);
smithay::wayland::viewporter::ensure_viewport_valid(states, buffer_size);
}
});
self.redraw_requests.insert(surface.id());
}
}
@@ -347,7 +397,25 @@ impl BufferHandler for Application {
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
}
impl ClientDndGrabHandler for Application {}
impl ClientDndGrabHandler for Application {
fn started(
&mut self,
_source: Option<wl_data_source::WlDataSource>,
icon: Option<WlSurface>,
_seat: Seat<Self>,
) {
let pointer = self.pointer_button_owner.unwrap_or(self.vr_pointer);
self.dnd_pointer = Some(pointer);
self.wayvr_tasks
.send(WayVRTask::DndStarted { icon, pointer });
}
fn dropped(&mut self, _target: Option<WlSurface>, _validated: bool, _seat: Seat<Self>) {
self.dnd_pointer = None;
self.pointer_button_owner = None;
self.wayvr_tasks.send(WayVRTask::DndDropped);
}
}
impl ServerDndGrabHandler for Application {
fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat<Self>) {}
@@ -430,6 +498,8 @@ impl XdgShellHandler for Application {
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
self.output.leave(surface.wl_surface());
self.no_server_decorations
.remove(&surface.wl_surface().id());
if let Some(client) = surface.wl_surface().client() {
self.wayvr_tasks
@@ -610,10 +680,16 @@ impl DmabufHandler for Application {
dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
if self.image_importer.get_or_import_dmabuf(dmabuf).is_ok() {
let _ = notifier.successful::<Self>();
} else {
notifier.failed();
match self.image_importer.get_or_import_dmabuf(dmabuf) {
Ok(_) => {
log::info!("wayland_server DMA-buf import complete; notifying client");
let _ = notifier.successful::<Self>();
log::info!("wayland_server DMA-buf success notification returned");
}
Err(e) => {
log::warn!("wayland_server rejected DMA-buf import: {e:?}");
notifier.failed();
}
}
}
}
@@ -637,24 +713,26 @@ impl selection_ext::DataControlHandler for Application {
}
impl XdgDecorationHandler for Application {
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
self.set_server_side_decorations(toplevel.wl_surface(), true);
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
toplevel.send_configure();
}
fn request_mode(
&mut self,
toplevel: ToplevelSurface,
_mode: zxdg_toplevel_decoration_v1::Mode,
) {
fn request_mode(&mut self, toplevel: ToplevelSurface, mode: zxdg_toplevel_decoration_v1::Mode) {
self.set_server_side_decorations(
toplevel.wl_surface(),
matches!(mode, zxdg_toplevel_decoration_v1::Mode::ServerSide),
);
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
state.decoration_mode = Some(mode);
});
toplevel.send_configure();
}
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
self.set_server_side_decorations(toplevel.wl_surface(), true);
toplevel.with_pending_state(|state| {
state.decoration_mode = Some(zxdg_toplevel_decoration_v1::Mode::ServerSide);
});
@@ -669,11 +747,19 @@ impl KdeDecorationHandler for Application {
fn request_mode(
&mut self,
_surface: &WlSurface,
surface: &WlSurface,
decoration: &org_kde_kwin_server_decoration::OrgKdeKwinServerDecoration,
_mode: wayland_server::WEnum<org_kde_kwin_server_decoration::Mode>,
mode: wayland_server::WEnum<org_kde_kwin_server_decoration::Mode>,
) {
decoration.mode(org_kde_kwin_server_decoration::Mode::Server);
let wayland_server::WEnum::Value(mode) = mode else {
return;
};
self.set_server_side_decorations(
surface,
matches!(mode, org_kde_kwin_server_decoration::Mode::Server),
);
decoration.mode(mode);
}
}
+29 -14
View File
@@ -12,9 +12,7 @@ use smithay::{
},
};
use std::{ops::RangeInclusive, rc::Rc, sync::Arc};
use vulkano::image::view::ImageView;
use crate::graphics::ExtentExt;
use wgui::gfx::ImageView;
use crate::backend::input::PointerHit;
@@ -36,6 +34,9 @@ pub struct RenderedSurface {
pub image: Arc<ImageView>,
pub pos: Vec2,
pub size: Vec2,
pub has_alpha: bool,
/// normalized source rect in backing wl_buffer
pub source_uv: [f32; 4],
}
#[derive(Clone)]
@@ -208,15 +209,17 @@ pub fn collect_rendered_surface_tree_at(
let pos = *parent_pos + surface_location(states);
if let Some(surf) = SurfaceBufWithImage::get_from_surface(states) {
let extent = surf.image.extent_f32();
let scale = surf.scale.max(1) as f32;
let logical_size = surf.logical_size(states);
let source_uv = surf.source_uv(states);
out.push(RenderedSurface {
surface: surface.clone(),
surface_id: surface.id(),
image: surf.image,
pos: Vec2::new(pos.x as f32, pos.y as f32),
size: Vec2::new(extent[0] / scale, extent[1] / scale),
size: Vec2::new(logical_size.w as f32, logical_size.h as f32),
has_alpha: surf.has_alpha,
source_uv,
});
}
},
@@ -274,10 +277,8 @@ fn surface_accepts_input_states(
// fallback for normal rendered surfaces
if let Some(surf) = SurfaceBufWithImage::get_from_surface(states) {
let extent = surf.image.extent_f32();
let scale = surf.scale.max(1) as f32;
return local.x < extent[0] / scale && local.y < extent[1] / scale;
let size = surf.logical_size(states);
return local.x < size.w as f32 && local.y < size.h as f32;
}
false
@@ -328,11 +329,20 @@ pub fn rendered_surfaces_dirty(old: &[RenderedSurface], new: &[RenderedSurface])
a.surface_id != b.surface_id
|| a.pos != b.pos
|| a.size != b.size
|| *a.image.image() != *b.image.image()
|| a.has_alpha != b.has_alpha
|| a.source_uv != b.source_uv
|| !Arc::ptr_eq(a.image.image(), b.image.image())
})
}
pub fn compute_transforms(inner_extent: [u32; 2]) -> (Affine2, RangeInclusive<f32>) {
pub fn compute_transforms(
inner_extent: [u32; 2],
server_side_decorations: bool,
) -> (Affine2, RangeInclusive<f32>) {
if !server_side_decorations {
return (Affine2::IDENTITY, 0.0..=1.0);
}
let ix = inner_extent[0].max(1) as f32;
let iy = inner_extent[1].max(1) as f32;
@@ -359,9 +369,14 @@ pub fn build_hit_context(
toplevel: &WlSurface,
_popup_manager: &PopupManager,
inner_extent: [u32; 2],
server_side_decorations: bool,
) -> WvrHitContext {
let (mouse_transform, uv_range) = compute_transforms(inner_extent);
let panel_height = BORDER_SIZE * 2 + BAR_SIZE;
let (mouse_transform, uv_range) = compute_transforms(inner_extent, server_side_decorations);
let panel_height = if server_side_decorations {
BORDER_SIZE * 2 + BAR_SIZE
} else {
0
};
let surfaces = collect_rendered_surface_tree(toplevel);
+13 -12
View File
@@ -11,8 +11,7 @@ use smithay::{
single_pixel_buffer::SinglePixelBufferUserData,
},
};
use vulkano::{format::Format, image::view::ImageView};
use wgui::gfx::WGfx;
use wgui::gfx::{CommandBufferUsage, Format, ImageView, WGfx};
use wlx_capture::frame::{DmabufFrame, FrameFormat, Transform};
use crate::graphics::dmabuf::{WGfxDmabuf, fourcc_to_vk};
@@ -34,17 +33,16 @@ impl ImageImporter {
&mut self,
spb: &SinglePixelBufferUserData,
) -> anyhow::Result<Arc<ImageView>> {
let mut cmd_buf = self.gfx.create_xfer_command_buffer(
vulkano::command_buffer::CommandBufferUsage::OneTimeSubmit,
)?;
let mut cmd_buf = self
.gfx
.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
let rgba = spb.rgba8888();
let image = cmd_buf.upload_image(1, 1, Format::R8G8B8A8_UNORM, &rgba)?;
cmd_buf.build_and_execute_now()?; //TODO: async
let image_view = ImageView::new_default(image)?;
Ok(image_view)
self.gfx.create_image_view(image)
}
pub fn import_shm(
@@ -53,9 +51,9 @@ impl ImageImporter {
size: usize,
bd: BufferData,
) -> anyhow::Result<Arc<ImageView>> {
let mut cmd_buf = self.gfx.create_xfer_command_buffer(
vulkano::command_buffer::CommandBufferUsage::OneTimeSubmit,
)?;
let mut cmd_buf = self
.gfx
.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
let fourcc = shm_format_to_fourcc(bd.format)
.with_context(|| format!("Could not convert {:?} to fourcc", bd.format))?;
@@ -96,7 +94,7 @@ impl ImageImporter {
let image = cmd_buf.upload_image(bd.width as _, bd.height as _, format, &packed)?;
cmd_buf.build_and_execute_now()?;
Ok(ImageView::new_default(image)?)
self.gfx.create_image_view(image)
}
pub fn get_or_import_dmabuf(&mut self, dmabuf: Dmabuf) -> anyhow::Result<Arc<ImageView>> {
@@ -132,8 +130,11 @@ impl ImageImporter {
}
let image = self.gfx.dmabuf_texture(frame)?;
let image_view = ImageView::new_default(image)?;
log::info!("DMA-buf import: creating image view");
let image_view = self.gfx.create_image_view(image)?;
log::info!("DMA-buf import: image view created");
self.dmabufs.insert(key, image_view.clone());
log::info!("DMA-buf import: cached ImageView");
Ok(image_view)
}
+55 -10
View File
@@ -182,11 +182,16 @@ impl RestrictedState {
if entry.device.is_grabbed() {
Ok(false)
} else {
entry
.device
.grab()
.map(|()| true)
.map_err(|error| with_device_context("grab", &entry.path, error))
match entry.device.grab() {
Ok(()) => {
log::debug!(
"exclusively grabbed input device: {}",
evdev_device_desc(&entry.path, &entry.device)
);
Ok(true)
}
Err(error) => Err(with_device_context("grab", &entry.path, error)),
}
}
};
@@ -218,10 +223,18 @@ impl RestrictedState {
continue;
}
if let Err(error) = entry.device.ungrab()
&& first_error.is_none()
{
first_error = Some(with_device_context("ungrab", &entry.path, error));
match entry.device.ungrab() {
Ok(()) => {
log::debug!(
"ungrabbed input device: {}",
evdev_device_desc(&entry.path, &entry.device)
);
}
Err(error) => {
if first_error.is_none() {
first_error = Some(with_device_context("ungrab", &entry.path, error));
}
}
}
}
@@ -261,6 +274,15 @@ impl LibinputInterface for RestrictedInterface {
if state.desired_grabbed {
device.grab().map_err(io_errno)?;
log::debug!(
"exclusively grabbed input device on open: {}",
evdev_device_desc(path, &device)
);
} else {
log::debug!(
"opened input device (non-exclusive): {}",
evdev_device_desc(path, &device)
);
}
state.devices.insert(
@@ -275,7 +297,13 @@ impl LibinputInterface for RestrictedInterface {
}
fn close_restricted(&mut self, fd: OwnedFd) {
self.state.borrow_mut().devices.remove(&fd.as_raw_fd());
let removed = self.state.borrow_mut().devices.remove(&fd.as_raw_fd());
if let Some(removed) = removed {
log::debug!(
"closed input device (releasing grab): {}",
evdev_device_desc(&removed.path, &removed.device)
);
}
drop(fd);
}
}
@@ -316,6 +344,8 @@ fn worker_main(
return;
}
log::debug!("input capture worker started");
let mut grabbed = false;
let mut pending_grab = false;
let mut watchdog = Instant::now() + WATCHDOG_TIMEOUT;
@@ -563,9 +593,11 @@ fn process_libinput_event(par: ProcessLibinputEventParams) -> ProcessResult {
if !par.grabbed {
// while ungrabbed, arm an automatic grab that triggers after release
if par.allow_deferred_grab
&& !*par.pending_grab
&& key_combo_is_pressed(KeyCombo::GrabRelease, &state.pressed_keys)
{
*par.pending_grab = true;
log::debug!("key combo triggered: GrabRelease (grab deferred until release)");
}
return ProcessResult::Continue;
@@ -591,6 +623,8 @@ fn process_libinput_event(par: ProcessLibinputEventParams) -> ProcessResult {
continue;
}
log::debug!("key combo triggered: {combo:?} pressed={combo_pressed}");
if combo_pressed {
state.active_combos.insert(combo);
} else {
@@ -908,6 +942,17 @@ fn io_errno(error: io::Error) -> i32 {
error.raw_os_error().unwrap_or(libc::EIO)
}
fn evdev_device_desc(path: &Path, device: &EvdevDevice) -> String {
let id = device.input_id();
format!(
"{} (vendor {:04x}, product {:04x}) {}",
device.name().unwrap_or("<unknown>"),
id.vendor(),
id.product(),
path.display()
)
}
fn with_device_context(action: &str, path: &Path, error: io::Error) -> io::Error {
io::Error::new(
error.kind(),
+134 -12
View File
@@ -51,13 +51,16 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use vulkano::image::view::ImageView;
use wayland_protocols::xdg::shell::server::xdg_toplevel;
use wayvr_ipc::packet_client::PositionMode;
use wgui::{gfx::WGfx, globals::WguiGlobals, log::LogErr};
use wgui::{
gfx::{ImageView, WGfx},
globals::WguiGlobals,
log::LogErr,
};
use wlx_capture::frame::Transform;
use wlx_common::{
audio::{AudioSystem, SamplePlayer},
audio,
config::{DefaultPositioning, GeneralConfig, InputCaptureMethod},
desktop_finder::DesktopFinder,
};
@@ -74,10 +77,11 @@ use crate::{
window::CreateWindowParams,
},
},
graphics::{ExtentExt, WGfxExtras},
graphics::WGfxExtras,
ipc::{event_queue::SyncEventQueue, ipc_server},
overlays::{
anchor::ALTTAB_HELP_NAME,
drag_item::{DRAG_ITEM_NAME, create_drag_item},
keyboard::KEYBOARD_NAME,
wayvr::{WvrCommand, create_wl_window_overlay},
},
@@ -132,6 +136,11 @@ pub enum WayVRTask {
NewExternalProcess(ExternalProcessRequest),
ProcessTerminationRequest(process::ProcessHandle, KillSignal),
CloseWindowRequest(window::WindowHandle),
DndStarted {
icon: Option<WlSurface>,
pointer: usize,
},
DndDropped,
}
pub struct WvrServerState {
@@ -168,7 +177,7 @@ pub enum TickTask {
NewExternalProcess(ExternalProcessRequest), // Call WayVRCompositor::add_client after receiving this message
}
const KEY_REPEAT_DELAY: i32 = 200;
const KEY_REPEAT_DELAY: i32 = 500;
const KEY_REPEAT_RATE: i32 = 50;
const WAYVR_SCREEN_RES: [i32; 2] = [2560, 1440];
@@ -288,8 +297,12 @@ impl WvrServerState {
popup_manager: PopupManager::default(),
viewporter,
redraw_requests: HashSet::new(),
no_server_decorations: HashSet::new(),
pending_frame_callbacks: HashMap::new(),
cursor_image: CursorImageStatus::default_named(),
vr_pointer: 0,
pointer_button_owner: None,
dnd_pointer: None,
};
Ok(Self {
@@ -638,6 +651,37 @@ impl WvrServerState {
);
}
}
WayVRTask::DndStarted { icon, pointer } => {
// don't freeze on drag start
wvr_server.mouse_freeze = Instant::now();
let selector = OverlaySelector::Name(DRAG_ITEM_NAME.clone());
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::DropImmediate(
selector.clone(),
)));
if let Some(icon) = icon {
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Spawn(
selector,
SpawnPos::FixedNoRealign,
Box::new(move |app| {
create_drag_item(app, icon, pointer)
.inspect_err(|e| {
log::warn!("Could not create drag item overlay: {e:?}")
})
.ok()
}),
)));
}
}
WayVRTask::DndDropped => {
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::DropImmediate(
OverlaySelector::Name(DRAG_ITEM_NAME.clone()),
)));
}
}
}
@@ -652,8 +696,7 @@ impl WvrServerState {
&mut app.input_state,
&mut app.tasks,
&mut app.hid_provider,
&mut app.audio_sample_player,
&mut app.audio_system,
&mut app.audio,
&app.wgui_globals,
&app.session.config,
);
@@ -774,8 +817,7 @@ impl WvrServerState {
input_state: &mut InputState,
tasks: &mut TaskContainer,
hid_wrapper: &mut HidWrapper,
audio_sample_player: &mut SamplePlayer,
audio_system: &mut AudioSystem,
audio: &mut Box<dyn audio::AudioProvider>,
globals: &WguiGlobals,
config: &GeneralConfig,
) {
@@ -924,7 +966,7 @@ impl WvrServerState {
}
}
input_capture::CapturedEvent::Grabbed => {
audio_sample_player.play_sample(audio_system, "input_grab");
audio.play_sample("input_grab");
if !self.grab_toast_sent {
self.grab_toast_sent = true;
let text = &globals.i18n().translate("NOTIFICATION.WE_ARE_GRABBING");
@@ -937,7 +979,7 @@ impl WvrServerState {
self.manager.release_all_keys();
self.has_input_focus = false;
self.wm.keyboard_focus = None;
audio_sample_player.play_sample(audio_system, "input_ungrab");
audio.play_sample("input_ungrab");
}
input_capture::CapturedEvent::KeyCombo { combo, pressed } => match combo {
input_capture::KeyCombo::CloseApp if pressed => {
@@ -992,13 +1034,14 @@ impl WvrServerState {
let toplevel = window.toplevel.wl_surface().clone();
let inner_extent = with_states(&toplevel, |states| {
SurfaceBufWithImage::get_from_surface(states)
.map_or([1, 1], |s| s.image.extent_u32arr())
.map_or([1, 1], |s| s.image.extent_2d())
});
let hit_ctx = build_hit_context(
&toplevel,
&self.manager.state.popup_manager,
inner_extent,
self.manager.state.has_server_side_decorations(&toplevel),
);
let new_x = (hover.pos.x + mouse_delta.x).clamp(0., inner_extent[0] as f64);
@@ -1183,6 +1226,47 @@ impl WvrServerState {
self.manager.send_pointer_button(index, pressed);
}
pub fn set_vr_pointer(&mut self, pointer: usize) {
self.manager.state.vr_pointer = pointer;
}
pub fn set_vr_pointer_button(&mut self, pointer: usize, pressed: bool) {
self.manager.state.vr_pointer = pointer;
if pressed {
self.manager.state.pointer_button_owner = Some(pointer);
} else if self.manager.state.dnd_pointer.is_none()
&& self.manager.state.pointer_button_owner == Some(pointer)
{
self.manager.state.pointer_button_owner = None;
}
}
pub fn dnd_active(&self) -> bool {
self.manager.state.dnd_pointer.is_some()
}
pub fn dnd_active_for_pointer(&self, pointer: usize) -> bool {
self.manager.state.dnd_pointer == Some(pointer)
}
pub fn clear_dnd_focus(&mut self, pointer: usize) {
if !self.dnd_active_for_pointer(pointer) {
return;
}
let pos = self
.wm
.mouse
.as_ref()
.map_or(DVec2::ZERO, |mouse| mouse.pos);
self.manager
.send_mouse_move(None, pos, DVec2::ZERO, DVec2::ZERO);
}
pub fn send_dnd_release(&mut self, index: MouseIndex) {
self.manager.send_pointer_button(index, false);
}
pub fn send_mouse_scroll(
&mut self,
hover_window: window::WindowHandle,
@@ -1354,9 +1438,47 @@ pub struct SurfaceBufWithImage {
pub transform: Transform,
pub scale: i32,
pub dmabuf: bool,
pub has_alpha: bool,
}
impl SurfaceBufWithImage {
/// Logical size of wl_surface after buffer scaling and wp_viewport
pub fn logical_size(&self, surface_data: &SurfaceData) -> Size<i32, Logical> {
use smithay::wayland::viewporter::ViewportCachedState;
let extent = self.image.extent_2d();
let scale = self.scale.max(1) as u32;
let buffer_size = Size::new(
(extent[0] / scale).max(1) as i32,
(extent[1] / scale).max(1) as i32,
);
let mut guard = surface_data.cached_state.get::<ViewportCachedState>();
guard.current().size().unwrap_or(buffer_size)
}
/// Normalized source rectangle in backing image cropped by wp_viewport
pub fn source_uv(&self, surface_data: &SurfaceData) -> [f32; 4] {
use smithay::wayland::viewporter::ViewportCachedState;
let extent = self.image.extent_2d();
let scale = self.scale.max(1) as f64;
let buffer_w = (extent[0] as f64 / scale).max(1.0);
let buffer_h = (extent[1] as f64 / scale).max(1.0);
let mut guard = surface_data.cached_state.get::<ViewportCachedState>();
let viewport = *guard.current();
let Some(src) = viewport.src else {
return [0.0, 0.0, 1.0, 1.0];
};
let u0 = (src.loc.x / buffer_w).clamp(0.0, 1.0) as f32;
let v0 = (src.loc.y / buffer_h).clamp(0.0, 1.0) as f32;
let u1 = ((src.loc.x + src.size.w) / buffer_w).clamp(0.0, 1.0) as f32;
let v1 = ((src.loc.y + src.size.h) / buffer_h).clamp(0.0, 1.0) as f32;
[u0, v0, u1, v1]
}
fn apply_to_surface(
self,
surface_data: &SurfaceData,
+30 -9
View File
@@ -2,12 +2,18 @@ use std::rc::Rc;
use glam::DVec2;
use slotmap::{DenseSlotMap, new_key_type};
use smithay::utils::{Logical, Size};
use smithay::utils::{Logical, Serial, Size};
use smithay::wayland::shell::xdg::ToplevelSurface;
use wayvr_ipc::packet_server;
use crate::backend::wayvr::process;
#[derive(Debug, Clone, Copy)]
pub struct PendingConfigure {
pub size: Size<i32, Logical>,
pub serial: Serial,
}
#[derive(Debug)]
pub struct Window {
pub min_size: Size<i32, Logical>,
@@ -19,7 +25,7 @@ pub struct Window {
pub toplevel: Rc<ToplevelSurface>,
pub process: process::ProcessHandle,
pub pending_configure_size: Option<Size<i32, Logical>>,
pub pending_configure: Option<PendingConfigure>,
}
impl Window {
@@ -39,7 +45,7 @@ impl Window {
visible: true,
toplevel,
process,
pending_configure_size: None,
pending_configure: None,
}
}
@@ -68,7 +74,10 @@ impl Window {
fn send_size_configure(&mut self, size: Size<i32, Logical>, bounds: Size<i32, Logical>) {
let clamped_size = self.clamp_configure_size(size, bounds);
if self.pending_configure_size == Some(clamped_size) {
if self
.pending_configure
.is_some_and(|pending| pending.size == clamped_size)
{
return;
}
@@ -77,10 +86,13 @@ impl Window {
state.size = Some(clamped_size);
});
self.toplevel.send_configure();
let serial = self.toplevel.send_configure();
self.bounds = bounds;
self.pending_configure_size = Some(clamped_size);
self.pending_configure = Some(PendingConfigure {
size: clamped_size,
serial,
});
}
pub fn checked_configure_size(&mut self, size: Size<i32, Logical>) {
@@ -91,7 +103,11 @@ impl Window {
self.send_size_configure(size, bounds);
}
pub fn remember_committed_size(&mut self, size: Size<i32, Logical>) -> bool {
pub fn remember_committed_size(
&mut self,
size: Size<i32, Logical>,
configure_serial: Option<Serial>,
) -> bool {
let size_x = size.w.max(1) as u32;
let size_y = size.h.max(1) as u32;
@@ -100,7 +116,12 @@ impl Window {
self.size_x = size_x;
self.size_y = size_y;
self.pending_configure_size = None;
if self
.pending_configure
.is_some_and(|pending| Some(pending.serial) == configure_serial)
{
self.pending_configure = None;
}
changed
}
@@ -155,7 +176,7 @@ impl WindowManager {
par.min_size,
par.max_size,
);
window.remember_committed_size(Size::new(par.size_x as i32, par.size_y as i32));
window.remember_committed_size(Size::new(par.size_x as i32, par.size_y as i32), None);
self.windows.insert(window)
}
+4
View File
@@ -189,6 +189,7 @@ pub struct AutoSettings {
pub input_emulation_method: InputEmulationMethod,
pub tutorial_graduated: bool,
pub whisper_model: Arc<str>,
pub whisper_gpu: u32,
pub default_overlay_scale: f32,
pub color_palette: Arc<str>,
pub snap_angle_deg: f32,
@@ -198,6 +199,7 @@ pub struct AutoSettings {
pub default_curvature: f32,
pub default_opacity: f32,
pub default_positioning: DefaultPositioning,
pub middle_grabs_single: bool,
}
fn get_settings_path() -> PathBuf {
@@ -264,6 +266,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
input_emulation_method: config.input_emulation_method,
tutorial_graduated: config.tutorial_graduated,
whisper_model: config.whisper_model.clone(),
whisper_gpu: config.whisper_gpu,
default_overlay_scale: config.default_overlay_scale,
color_palette: config.color_palette.clone(),
snap_angle_deg: config.snap_angle_deg,
@@ -273,6 +276,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
default_curvature: config.default_curvature,
default_opacity: config.default_opacity,
default_positioning: config.default_positioning,
middle_grabs_single: config.middle_grabs_single,
};
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff. Show more