mirror of
https://github.com/wayvr-org/wayvr.git
synced 2026-10-28 16:02:47 +01:00
Compare commits
84
Commits
animations
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a65b33fed2 | ||
|
|
786903efd5 | ||
|
|
466960d32a | ||
|
|
abafddefd8 | ||
|
|
9b1fc2774a | ||
|
|
c539ff6a90 | ||
|
|
f94c0c4ace | ||
|
|
5f4c97794c | ||
|
|
d9288a877c | ||
|
|
dff4b0067a | ||
|
|
adf852971f | ||
|
|
074d2de7aa | ||
|
|
e8d9a35714 | ||
|
|
36f9fab5c6 | ||
|
|
058792fc8e | ||
|
|
ef934e0b02 | ||
|
|
c42e992a62 | ||
|
|
41d7a0b3d2 | ||
|
|
471e4be946 | ||
|
|
26afa57fe3 | ||
|
|
d120617abe | ||
|
|
351b24533c | ||
|
|
09ed96542f | ||
|
|
9b89622ff9 | ||
|
|
346c6391ef | ||
|
|
e850c2c8b6 | ||
|
|
5360697828 | ||
|
|
3a869e3c35 | ||
|
|
b3ac55474f | ||
|
|
a4986a3d9f | ||
|
|
f3f7d97ed6 | ||
|
|
10857739a4 | ||
|
|
42e9deb54c | ||
|
|
d6515b3c2a | ||
|
|
a101c79892 | ||
|
|
82565e975f | ||
|
|
959f66b271 | ||
|
|
745728d90d | ||
|
|
68944184de | ||
|
|
4590371a4c | ||
|
|
d1255e5f71 | ||
|
|
537f2f5397 | ||
|
|
77730432b1 | ||
|
|
fd3e9069e4 | ||
|
|
2226d95834 | ||
|
|
a0d36d7257 | ||
|
|
38382275ec | ||
|
|
5517c3a809 | ||
|
|
f0d96802ba | ||
|
|
3f43003a0b | ||
|
|
28842d6d69 | ||
|
|
573bd5da53 | ||
|
|
da3b19a495 | ||
|
|
ad0d1aa8a2 | ||
|
|
b85976bdf0 | ||
|
|
2ea97e88bb | ||
|
|
b7b93131c9 | ||
|
|
e7c07c7789 | ||
|
|
0a158c102c | ||
|
|
11a5c8305b | ||
|
|
d5f0306aab | ||
|
|
6d6e7033ac | ||
|
|
16bc4383b5 | ||
|
|
0e6526836e | ||
|
|
2e8d1589f5 | ||
|
|
54713b95f4 | ||
|
|
6f761a99dc | ||
|
|
c87bcee358 | ||
|
|
0eab390702 | ||
|
|
9dcdf852c7 | ||
|
|
b3261dbe4f | ||
|
|
329a89ca08 | ||
|
|
c284162068 | ||
|
|
f2beabd39a | ||
|
|
126120e52e | ||
|
|
e986c3591e | ||
|
|
a0f096b9dc | ||
|
|
7b95f01ff4 | ||
|
|
d9cc73be8b | ||
|
|
548ea36a84 | ||
|
|
d4a5d4c81f | ||
|
|
443bc01361 | ||
|
|
9b9d1b9f8a | ||
|
|
40811f219e |
No files matched your search
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
|
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "いいえ"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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"))
|
||||
}
|
||||
@@ -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,7 +1,7 @@
|
||||
use std::{marker::PhantomData, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
assets::{AssetPathRef},
|
||||
assets::AssetPathRef,
|
||||
components::button::ComponentButton,
|
||||
globals::WguiGlobals,
|
||||
layout::{Layout, WidgetID},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {})
|
||||
}
|
||||
}
|
||||
@@ -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 {})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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>(¶ms.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>,
|
||||
|
||||
@@ -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", ¶ms.alt_desc);
|
||||
par.insert("device_name", ¶ms.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
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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を押してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()))
|
||||
}
|
||||
@@ -37,4 +37,5 @@ pub enum BackendError {
|
||||
pub struct RunParams {
|
||||
pub wgui_globals: WguiGlobals,
|
||||
pub config: GeneralConfig,
|
||||
pub no_audio: bool,
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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{}, {:?}",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
+593
-307
File diff suppressed because it is too large.
Load diff
@@ -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>,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -21,7 +21,7 @@ use smithay::{
|
||||
use wgui::log::LogErr;
|
||||
use xkbcommon::xkb;
|
||||
|
||||
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
|
||||
use crate::backend::wayvr::WayVRTask;
|
||||
|
||||
use super::{
|
||||
ProcessWayVREnv,
|
||||
@@ -32,6 +32,7 @@ use super::{
|
||||
pub struct WayVRClient {
|
||||
pub client: wayland_server::Client,
|
||||
pub pid: u32,
|
||||
pub process: process::ProcessHandle,
|
||||
}
|
||||
|
||||
pub struct WayVRCompositor {
|
||||
@@ -160,16 +161,17 @@ impl WayVRCompositor {
|
||||
let process_env = get_wayvr_env_from_pid(credentials.pid)?;
|
||||
|
||||
// Find suitable auth key from the process list
|
||||
for (_, process) in processes {
|
||||
if let process::Process::Managed(process) = &process
|
||||
for (handle, process) in processes.iter() {
|
||||
if let process::Process::Managed(process) = process
|
||||
&& let Some(auth_key) = &process_env.display_auth
|
||||
{
|
||||
// Find process with matching auth key
|
||||
if process.auth_key.as_str() == auth_key {
|
||||
// Add client
|
||||
// Associate client now, instead of trying to match env later
|
||||
self.add_client(WayVRClient {
|
||||
client,
|
||||
pid: credentials.pid as u32,
|
||||
process: handle,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
@@ -178,18 +180,15 @@ impl WayVRCompositor {
|
||||
|
||||
// This is a new process which we didn't met before.
|
||||
// Treat external processes exclusively (spawned by the user or external program)
|
||||
log::warn!(
|
||||
"External process ID {} connected to this Wayland server",
|
||||
credentials.pid
|
||||
);
|
||||
|
||||
self.state
|
||||
.wayvr_tasks
|
||||
.send(WayVRTask::NewExternalProcess(ExternalProcessRequest {
|
||||
env: process_env,
|
||||
client,
|
||||
pid: credentials.pid as u32,
|
||||
}));
|
||||
let pid = credentials.pid as u32;
|
||||
log::warn!("External process ID {pid} connected to this Wayland server");
|
||||
let process =
|
||||
processes.insert(process::Process::External(process::ExternalProcess { pid }));
|
||||
self.add_client(WayVRClient {
|
||||
client,
|
||||
pid,
|
||||
process,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+111
-25
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+150
-47
@@ -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},
|
||||
},
|
||||
@@ -114,14 +118,6 @@ pub struct ProcessWayVREnv {
|
||||
pub display_name: Option<String>, // Externally spawned process by a user script
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExternalProcessRequest {
|
||||
#[allow(dead_code)]
|
||||
pub env: ProcessWayVREnv,
|
||||
pub client: wayland_server::Client,
|
||||
pub pid: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum WayVRTask {
|
||||
NewToplevel(ClientId, ToplevelSurface),
|
||||
@@ -129,9 +125,13 @@ pub enum WayVRTask {
|
||||
MinimizeRequest(ClientId, ToplevelSurface),
|
||||
TitleChange(ClientId, ToplevelSurface),
|
||||
VisibilityChange(window::WindowHandle, bool),
|
||||
NewExternalProcess(ExternalProcessRequest),
|
||||
ProcessTerminationRequest(process::ProcessHandle, KillSignal),
|
||||
CloseWindowRequest(window::WindowHandle),
|
||||
DndStarted {
|
||||
icon: Option<WlSurface>,
|
||||
pointer: usize,
|
||||
},
|
||||
DndDropped,
|
||||
}
|
||||
|
||||
pub struct WvrServerState {
|
||||
@@ -164,11 +164,7 @@ pub enum PointerFocusTarget {
|
||||
None,
|
||||
}
|
||||
|
||||
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 +284,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 {
|
||||
@@ -312,26 +312,29 @@ impl WvrServerState {
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::significant_drop_tightening)]
|
||||
pub fn tick_events(app: &mut AppState) -> anyhow::Result<Vec<TickTask>> {
|
||||
let mut tasks: Vec<TickTask> = Vec::new();
|
||||
|
||||
pub fn tick_events(app: &mut AppState) -> anyhow::Result<()> {
|
||||
let Some(wvr_server) = app.wvr_server.as_mut() else {
|
||||
return Ok(tasks);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
app.ipc_server.tick(&mut ipc_server::TickParams {
|
||||
wvr_server,
|
||||
input_state: &app.input_state,
|
||||
tasks: &mut tasks,
|
||||
signals: &app.wayvr_signals,
|
||||
hid_wrapper: &mut app.hid_provider,
|
||||
});
|
||||
|
||||
// Tick all child processes
|
||||
// Keep launch records while wl client is attached
|
||||
let attached_processes: HashSet<_> = wvr_server
|
||||
.manager
|
||||
.clients
|
||||
.iter()
|
||||
.map(|client| client.process)
|
||||
.collect();
|
||||
let mut to_remove: SmallVec<[process::ProcessHandle; 2]> = SmallVec::new();
|
||||
|
||||
for (handle, process) in &mut wvr_server.processes {
|
||||
if !process.is_running() {
|
||||
if !process.is_running() && !attached_processes.contains(&handle) {
|
||||
to_remove.push(handle);
|
||||
}
|
||||
}
|
||||
@@ -343,9 +346,6 @@ impl WvrServerState {
|
||||
|
||||
while let Some(task) = wvr_server.tasks.read() {
|
||||
match task {
|
||||
WayVRTask::NewExternalProcess(req) => {
|
||||
tasks.push(TickTask::NewExternalProcess(req));
|
||||
}
|
||||
WayVRTask::NewToplevel(client_id, toplevel) => {
|
||||
let toplevel = Rc::new(toplevel);
|
||||
|
||||
@@ -355,15 +355,14 @@ impl WvrServerState {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(process_handle) =
|
||||
process::find_by_pid(&wvr_server.processes, client.pid)
|
||||
else {
|
||||
let process_handle = client.process;
|
||||
if !wvr_server.processes.contains_key(process_handle) {
|
||||
log::error!(
|
||||
"WayVR window creation failed: Unexpected process ID {}. It wasn't registered before.",
|
||||
"WayVR window creation failed: Process for PID {} was removed after the client connected.",
|
||||
client.pid
|
||||
);
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
let output_bounds = wvr_server.manager.state.output_logical_size();
|
||||
|
||||
@@ -588,7 +587,7 @@ impl WvrServerState {
|
||||
}
|
||||
let Some(window_handle) = wvr_server.wm.find_window_handle(&toplevel)
|
||||
else {
|
||||
log::warn!("MinimizeRequest: Couldn't find matching window handle");
|
||||
log::warn!("TitleChange: Couldn't find matching window handle");
|
||||
continue;
|
||||
};
|
||||
if let Some(oid) = wvr_server.window_to_overlay.get(&window_handle) {
|
||||
@@ -638,6 +637,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,15 +682,14 @@ 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,
|
||||
);
|
||||
|
||||
wvr_server.ticks += 1;
|
||||
|
||||
Ok(tasks)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn config_changed(&mut self, config: &GeneralConfig) {
|
||||
@@ -774,8 +803,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 +952,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 +965,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 +1020,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 +1212,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,
|
||||
@@ -1223,11 +1293,6 @@ impl WvrServerState {
|
||||
self.manager.state.set_clipboard_text(content);
|
||||
}
|
||||
|
||||
pub fn add_external_process(&mut self, pid: u32) -> process::ProcessHandle {
|
||||
self.processes
|
||||
.insert(process::Process::External(process::ExternalProcess { pid }))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spawn_process(
|
||||
&mut self,
|
||||
@@ -1354,9 +1419,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,
|
||||
|
||||
@@ -87,12 +87,14 @@ impl Process {
|
||||
|
||||
impl Drop for WayVRProcess {
|
||||
fn drop(&mut self) {
|
||||
log::info!(
|
||||
"Sending SIGTERM (graceful exit) to process {} ({})",
|
||||
self.child.id(),
|
||||
self.exec_path.as_str()
|
||||
);
|
||||
self.kill(libc::SIGTERM);
|
||||
if self.is_running() {
|
||||
log::info!(
|
||||
"Sending SIGTERM (graceful exit) to process {} ({})",
|
||||
self.child.id(),
|
||||
self.exec_path.as_str()
|
||||
);
|
||||
self.kill(libc::SIGTERM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,9 +129,11 @@ impl WayVRProcess {
|
||||
}
|
||||
|
||||
fn kill(&mut self, signal: i32) {
|
||||
unsafe {
|
||||
// Gracefully stop process
|
||||
libc::kill(self.child.id() as i32, signal);
|
||||
if self.is_running() {
|
||||
unsafe {
|
||||
// Gracefully stop process
|
||||
libc::kill(self.child.id() as i32, signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 204 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user