mirror of
https://github.com/wayvr-org/wayvr.git
synced 2026-10-28 21:02:27 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ca634b552 | ||
|
|
409b89585e | ||
|
|
92c59850b6 | ||
|
|
bffb4c12cf | ||
|
|
feac111949 | ||
|
|
f13a473048 | ||
|
|
b2a7d3c4cb | ||
|
|
4cb12a2a01 | ||
|
|
b830c826a0 | ||
|
|
45ab38c310 | ||
|
|
bc6c510af1 | ||
|
|
3dd31ec468 | ||
|
|
60850d8d70 | ||
|
|
7ac3072506 | ||
|
|
2952a3cea1 | ||
|
|
bb2f8380fd | ||
|
|
1deae19f92 | ||
|
|
3365aefc13 | ||
|
|
cf29682e66 | ||
|
|
55867e803f | ||
|
|
60bc8b986e | ||
|
|
5f93bc9cac | ||
|
|
659f1492fb | ||
|
|
a55d0d74bb | ||
|
|
6d39380ebc | ||
|
|
a1cc41f541 | ||
|
|
bca6376b50 | ||
|
|
049f91eed3 | ||
|
|
05b3baa5d9 | ||
|
|
ed804820e9 | ||
|
|
91af7c99b1 | ||
|
|
0b7cd61d05 | ||
|
|
59b489680a | ||
|
|
b0883e81bf | ||
|
|
36074307b7 | ||
|
|
fa3af63e89 | ||
|
|
5cf6eeedb0 | ||
|
|
89680bf8cc | ||
|
|
009862e2a2 | ||
|
|
075e55a8e8 | ||
|
|
e66a8d27c9 | ||
|
|
507ff7353e | ||
|
|
c5a82c2879 | ||
|
|
965174c781 | ||
|
|
bf3f3dafbe | ||
|
|
4701d2b734 | ||
|
|
e44872f831 | ||
|
|
3c67abcebb | ||
|
|
da129b62b3 | ||
|
|
52298ccfdf | ||
|
|
8766999e28 | ||
|
|
5d9f80ea40 | ||
|
|
ba5228c643 | ||
|
|
8ba0b2ca04 | ||
|
|
b22af57000 | ||
|
|
2bf8984ba1 | ||
|
|
3c792608e7 | ||
|
|
b209f79188 | ||
|
|
80d9b02ac7 | ||
|
|
244f8cfa80 | ||
|
|
6c95607d44 | ||
|
|
bbed686a5e | ||
|
|
83026deaab | ||
|
|
aa2e74f6b6 | ||
|
|
d8180bb4ed | ||
|
|
ff150100b4 | ||
|
|
de6c5b8ad9 | ||
|
|
eb3087f230 | ||
|
|
ab6b5c0b46 | ||
|
|
9b2df6be08 | ||
|
|
1d4f4337d0 | ||
|
|
c6ff314e93 | ||
|
|
1903271d05 | ||
|
|
4de6116985 | ||
|
|
65432ebde4 | ||
|
|
a71f4de0ff | ||
|
|
18b2e96079 | ||
|
|
c09da86659 | ||
|
|
e818259b5b | ||
|
|
c96ead7cf0 | ||
|
|
546b38204c | ||
|
|
12218828d4 | ||
|
|
26a872e685 | ||
|
|
add121bd52 | ||
|
|
3b3138cfe9 | ||
|
|
8d6ff1d3a7 | ||
|
|
51174b64d3 | ||
|
|
54737b02d3 | ||
|
|
2c31d57afe | ||
|
|
30736575be | ||
|
|
689fbbe5e4 | ||
|
|
bdf02ea206 | ||
|
|
5ef2e40b5f | ||
|
|
e23444aa89 | ||
|
|
a3f9f7cc46 | ||
|
|
02faf693dc | ||
|
|
502cf5d372 | ||
|
|
d212098a98 | ||
|
|
ced753235e | ||
|
|
9da1c509e3 | ||
|
|
058baebd0c | ||
|
|
5a060c28f2 | ||
|
|
96db24d02e | ||
|
|
24096b7b5a | ||
|
|
f941f76339 | ||
|
|
50b30565da | ||
|
|
7e09c010db | ||
|
|
66fb2fc48c | ||
|
|
9195a338fd | ||
|
|
cf5eabdfdf | ||
|
|
7cb8e4a9d2 | ||
|
|
0b1a7e8bc8 | ||
|
|
8d82993704 | ||
|
|
cf03beef07 | ||
|
|
83bf0fff5d | ||
|
|
07d7afa96f | ||
|
|
79d91e3d02 | ||
|
|
d9dddbad11 | ||
|
|
edfa77e07c | ||
|
|
f84d57dc42 | ||
|
|
bc9bac8c40 | ||
|
|
9dbd1f994e | ||
|
|
022ae53aea | ||
|
|
64b3263309 | ||
|
|
85aa0fb1bc | ||
|
|
7f72abc62a | ||
|
|
83ae798215 | ||
|
|
bdbdbea20b | ||
|
|
a077f2dda4 | ||
|
|
8d35b45c2b | ||
|
|
6af5623fa7 | ||
|
|
62ef7a1210 | ||
|
|
3f425820d7 | ||
|
|
6a2d8b9f90 | ||
|
|
583a5f7fe0 | ||
|
|
03f2eaf97f | ||
|
|
21331b5c42 | ||
|
|
72a82530ae | ||
|
|
13d9284bb8 | ||
|
|
b0a33a1181 | ||
|
|
4f38d1ad44 | ||
|
|
c6bc880bc3 | ||
|
|
0a2be76150 | ||
|
|
1a87823847 | ||
|
|
e763e15bdc | ||
|
|
6a3b60f298 | ||
|
|
397b3eedd1 | ||
|
|
9d18628354 | ||
|
|
dc985f6da6 | ||
|
|
850601b3b9 | ||
|
|
8a4c7a400f | ||
|
|
d9287f3250 | ||
|
|
378ea120c8 | ||
|
|
f4cc49ca69 | ||
|
|
57ff3aa1dd | ||
|
|
0808383e70 | ||
|
|
c71911998e | ||
|
|
50c3f49989 | ||
|
|
b6a07c43a5 | ||
|
|
ee3fbff44c | ||
|
|
c29bbbb142 | ||
|
|
aca177635e | ||
|
|
c3c8694e1a | ||
|
|
1de9cea397 | ||
|
|
939a537d20 | ||
|
|
26124be6ef | ||
|
|
3d0611052f | ||
|
|
dcc2cd0575 | ||
|
|
57ff3ec3bc | ||
|
|
e14b35bece | ||
|
|
88da3c832a | ||
|
|
7b4576bd52 | ||
|
|
a98b7a210f | ||
|
|
32e0e7656a | ||
|
|
f27c320231 | ||
|
|
a56ed68b44 | ||
|
|
7a6040bfee | ||
|
|
17addcde78 | ||
|
|
5a45d4dd2f | ||
|
|
9a928b1c0c | ||
|
|
3764873209 | ||
|
|
106028be67 | ||
|
|
d58268b8ec | ||
|
|
787a3851b3 | ||
|
|
2bfa78e9db | ||
|
|
71068a0215 | ||
|
|
5575e4f49f | ||
|
|
a991c6b84a | ||
|
|
bdc500973c | ||
|
|
80bd65235d | ||
|
|
39595faaad | ||
|
|
1fc6cb4a36 | ||
|
|
10db0f690a | ||
|
|
072540fbb5 | ||
|
|
d225250b77 | ||
|
|
c7aa88647c | ||
|
|
abc132c553 | ||
|
|
371d3c2757 | ||
|
|
c06ec6d974 | ||
|
|
b3ae583cb4 | ||
|
|
8fb9ef4a05 |
No files matched your search
@@ -0,0 +1,28 @@
|
||||
name: Check All Features
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "!main" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.3
|
||||
- name: Prepare Environment
|
||||
run: |
|
||||
sudo add-apt-repository -syn universe
|
||||
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
|
||||
sudo apt-get update -y
|
||||
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
|
||||
- name: Build
|
||||
run: cargo build --verbose --all-features
|
||||
- name: Run tests
|
||||
run: cargo test --verbose --all-features
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Build AppImage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'staging'
|
||||
|
||||
env:
|
||||
APPDIR: WlxOverlay-S.AppDir
|
||||
CARGO_TERM_COLOR: always
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
|
||||
jobs:
|
||||
build_appimage:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.3
|
||||
- name: Prepare Environment
|
||||
run: |
|
||||
sudo add-apt-repository -syn universe
|
||||
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
|
||||
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
|
||||
rustup update
|
||||
|
||||
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
|
||||
|
||||
test -d ${APPDIR} && rm -rf ${APPDIR}
|
||||
mkdir -p ${APPDIR}/usr/bin
|
||||
|
||||
- name: Cargo Build
|
||||
run: |
|
||||
cargo build --release
|
||||
mv target/release/wlx-overlay-s ${APPDIR}/usr/bin
|
||||
chmod +x ${APPDIR}/usr/bin/wlx-overlay-s
|
||||
|
||||
- name: Package AppImage
|
||||
run: |
|
||||
export VERSION=$GITHUB_REF_NAME
|
||||
./linuxdeploy-x86_64.AppImage -dwlx-overlay-s.desktop -iwlx-overlay-s.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
|
||||
mv WlxOverlay-S-$VERSION-x86_64.AppImage WlxOverlay-S-x86_64.AppImage
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: WlxOverlay-S-${{ github.ref_name }}-x86_64.AppImage
|
||||
path: ./WlxOverlay-S-x86_64.AppImage
|
||||
@@ -1,8 +1,8 @@
|
||||
name: Check Full
|
||||
name: Check Default
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
#branches: [ "main" ]
|
||||
branches: [ "!main" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -21,8 +21,12 @@ jobs:
|
||||
sudo add-apt-repository -syn universe
|
||||
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
|
||||
sudo apt-get update -y
|
||||
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
|
||||
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
|
||||
- name: Run cargo fmt
|
||||
run: cargo fmt --check
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run clippy
|
||||
run: cargo clippy --no-deps
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Check Wayland+OpenXR+OpenVR+WayVR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
#branches: [ "main" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.3
|
||||
- name: Prepare Environment
|
||||
run: |
|
||||
sudo add-apt-repository -syn universe
|
||||
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
|
||||
sudo apt-get update -y
|
||||
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
|
||||
- name: Build
|
||||
run: cargo build --verbose --no-default-features --features=wayland,openxr,openvr,wayvr
|
||||
- name: Run tests
|
||||
run: cargo test --verbose --no-default-features --features=wayland,openxr,openvr,wayvr
|
||||
@@ -17,13 +17,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.3
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- name: Prepare Environment
|
||||
run: |
|
||||
sudo add-apt-repository -syn universe
|
||||
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
|
||||
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
|
||||
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
|
||||
rustup update
|
||||
|
||||
test -f linuxdeploy-x86_64.AppImage || wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Cargo Build
|
||||
run: |
|
||||
cargo build --release
|
||||
mv target/release/wlx-overlay-s ${APPDIR}/usr/bin
|
||||
cp target/release/wlx-overlay-s ${APPDIR}/usr/bin
|
||||
chmod +x ${APPDIR}/usr/bin/wlx-overlay-s
|
||||
|
||||
- name: Package AppImage
|
||||
@@ -44,6 +44,13 @@ jobs:
|
||||
./linuxdeploy-x86_64.AppImage -dwlx-overlay-s.desktop -iwlx-overlay-s.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
|
||||
mv WlxOverlay-S-$VERSION-x86_64.AppImage WlxOverlay-S-x86_64.AppImage
|
||||
|
||||
- name: Make tarball
|
||||
run: |
|
||||
pip install portage pycargoebuild
|
||||
wget https://github.com/gentoo/gentoo/raw/refs/heads/master/metadata/license-mapping.conf
|
||||
mkdir dist
|
||||
pycargoebuild --distdir dist --license-mapping license-mapping.conf --crate-tarball --crate-tarball-path wlx-overlay-s-crates.tar.xz
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
@@ -55,6 +62,16 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
- name: Upload ELF
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_KEY }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: target/release/wlx-overlay-s
|
||||
asset_name: wlx-overlay-s
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
@@ -64,3 +81,13 @@ jobs:
|
||||
asset_path: ./WlxOverlay-S-x86_64.AppImage
|
||||
asset_name: WlxOverlay-S-${{ github.ref_name }}-x86_64.AppImage
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload crates tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_KEY }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./wlx-overlay-s-crates.tar.xz
|
||||
asset_name: WlxOverlay-S-${{ github.ref_name }}-crates.tar.xz
|
||||
asset_content_type: application/x-gtar
|
||||
Generated
+2111
-992
File diff suppressed because it is too large.
Load diff
+79
-36
@@ -4,74 +4,117 @@ debug = true
|
||||
|
||||
[package]
|
||||
name = "wlx-overlay-s"
|
||||
version = "0.4.1"
|
||||
version = "25.4.1"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-only"
|
||||
authors = ["galister"]
|
||||
description = "Access your Wayland/X11 desktop from Monado/WiVRn/SteamVR. Now with Vulkan!"
|
||||
repository = "https://github.com/galister/wlx-overlay-s"
|
||||
keywords = ["linux", "openvr", "openxr", "x11", "wayland", "openvr-overlay", "openxr-overlay"]
|
||||
categories = ["games"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.79"
|
||||
ash = "^0.37.2"
|
||||
chrono = "0.4.29"
|
||||
chrono-tz = "0.9.0"
|
||||
clap = { version = "4.5.1", features = ["derive"] }
|
||||
config = "0.14.0"
|
||||
cstr = "0.2.12"
|
||||
ctrlc = { version = "3.4.2", features = ["termination"] }
|
||||
anyhow = "1.0.89"
|
||||
ash = "^0.38.0" # must match vulkano
|
||||
chrono = "0.4.38"
|
||||
chrono-tz = "0.10.0"
|
||||
clap = { version = "4.5.6", features = ["derive"] }
|
||||
config = "0.15.11"
|
||||
ctrlc = { version = "3.4.4", features = ["termination"] }
|
||||
dbus = { version = "0.9.7" }
|
||||
flexi_logger = "0.28.0"
|
||||
fontconfig-rs = "0.1.1"
|
||||
freetype-rs = "0.36.0"
|
||||
futures = "0.3.29"
|
||||
glam = { version = "0.27.0", features = ["approx", "serde"] }
|
||||
freetype-rs = "0.36.0" # latest version supported on ubuntu 22.04
|
||||
futures = "0.3.30"
|
||||
glam = { version = "0.30.1", features = ["approx", "mint", "serde"] }
|
||||
idmap = { version = "0.2.21", features = ["serde"] }
|
||||
idmap-derive = "0.1.2"
|
||||
input-linux = "0.6.0"
|
||||
input-linux = "0.7.0"
|
||||
json = { version = "0.12.4", optional = true }
|
||||
json5 = "0.4.1"
|
||||
libc = "0.2.153"
|
||||
libloading = "0.8.3"
|
||||
libc = "0.2.155"
|
||||
log = "0.4.21"
|
||||
once_cell = "1.19.0"
|
||||
openxr = { version = "0.17.1", features = ["linked"], optional = true }
|
||||
openxr = { git = "https://github.com/galister/openxrs", rev = "af4a55d", features = [
|
||||
"linked",
|
||||
"mint",
|
||||
], optional = true }
|
||||
ovr_overlay = { features = [
|
||||
"ovr_input",
|
||||
"ovr_system",
|
||||
], git = "https://github.com/galister/ovr_overlay_oyasumi", optional = true }
|
||||
regex = "1.9.5"
|
||||
rodio = { version = "0.17.1", default-features = false, features = [
|
||||
regex = "1.11.1"
|
||||
rodio = { version = "0.20.1", default-features = false, features = [
|
||||
"wav",
|
||||
"hound",
|
||||
] }
|
||||
rosc = { version = "0.10.1", optional = true }
|
||||
serde = { version = "1.0.188", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.113"
|
||||
rosc = { version = "0.11.4", optional = true }
|
||||
serde = { version = "1.0.203", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.117"
|
||||
serde_yaml = "0.9.34"
|
||||
smallvec = "1.11.0"
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
sysinfo = { version = "0.30.0" }
|
||||
thiserror = "1.0.56"
|
||||
vulkano = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
|
||||
vulkano-shaders = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
|
||||
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.3.11", default-features = false }
|
||||
winit = { version = "0.29.15", optional = true }
|
||||
smallvec = "1.13.2"
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
sysinfo = { version = "0.34.2" }
|
||||
thiserror = "2.0.3"
|
||||
vulkano = { version = "0.35.1" }
|
||||
vulkano-shaders = { version = "0.35.0" }
|
||||
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.5.0", default-features = false }
|
||||
libmonado = { git = "https://github.com/technobaboo/libmonado-rs", rev = "256895b18c8f9368174fad8a6232ff07764eeacb", optional = true }
|
||||
winit = { version = "0.30.0", optional = true }
|
||||
xdg = "2.5.2"
|
||||
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
|
||||
serde_json5 = "0.1.0"
|
||||
xkbcommon = { version = "0.7.0" }
|
||||
serde_json5 = "0.2.1"
|
||||
xkbcommon = { version = "0.8.0" }
|
||||
xcb = { version = "1.4.0", optional = true, features = [
|
||||
"as-raw-xcb-connection",
|
||||
] }
|
||||
image_dds = { version = "0.7.2", default-features = false, features = [
|
||||
"ddsfile",
|
||||
] }
|
||||
mint = "0.5.9"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracing = "0.1.41"
|
||||
|
||||
################################
|
||||
#WayVR-only deps
|
||||
################################
|
||||
khronos-egl = { version = "6.0.0", features = ["static"], optional = true }
|
||||
smithay = { version = "0.5.1", default-features = false, features = [
|
||||
"renderer_gl",
|
||||
"backend_egl",
|
||||
"backend_drm",
|
||||
"xwayland",
|
||||
"wayland_frontend",
|
||||
], optional = true }
|
||||
uuid = { version = "1.10.0", features = ["v4", "fast-rng"], optional = true }
|
||||
wayland-client = { version = "0.31.6", optional = true }
|
||||
wayland-egl = { version = "0.32.4", optional = true }
|
||||
interprocess = { version = "2.2.2", optional = true }
|
||||
bytes = { version = "1.9.0", optional = true }
|
||||
wayvr_ipc = { git = "https://github.com/olekolek1000/wayvr-ipc.git", rev = "a72587d23f3bb8624d9aeb1f13c0a21e65350f51", default-features = false, optional = true }
|
||||
################################
|
||||
|
||||
[build-dependencies]
|
||||
regex = { version = "1.11.1" }
|
||||
|
||||
[features]
|
||||
default = ["openvr", "openxr", "osc", "x11", "wayland"]
|
||||
default = ["openvr", "openxr", "osc", "x11", "wayland", "wayvr"]
|
||||
openvr = ["dep:ovr_overlay", "dep:json"]
|
||||
openxr = ["dep:openxr"]
|
||||
openxr = ["dep:openxr", "dep:libmonado"]
|
||||
osc = ["dep:rosc"]
|
||||
x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"]
|
||||
wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"]
|
||||
pipewire = ["wlx-capture/pipewire"]
|
||||
uidev = ["dep:winit"]
|
||||
no-dmabuf = []
|
||||
xcb = ["dep:xcb"]
|
||||
wayvr = [
|
||||
"dep:khronos-egl",
|
||||
"dep:smithay",
|
||||
"dep:uuid",
|
||||
"dep:wayland-client",
|
||||
"dep:wayland-egl",
|
||||
"dep:interprocess",
|
||||
"dep:bytes",
|
||||
"dep:wayvr_ipc",
|
||||
]
|
||||
as-raw-xcb-connection = []
|
||||
@@ -2,60 +2,81 @@
|
||||
|
||||
A lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops, inspired by XSOverlay.
|
||||
|
||||
WlxOverlay-S allows you to access your desktop screens while in VR.
|
||||
WlxOverlay-S lets you to access your desktop screens while in VR.
|
||||
|
||||
Compared to similar software, WlxOverlay-S aims to run alongside your other VR games or experiences and have as little performance impact as possible. The UI looks and rendering methods are kept simple and efficient as much as possible.
|
||||
In comparison to similar overlays, WlxOverlay-S aims to run alongside VR games and experiences while having as little performance impact as possible. The UI appearance and rendering techniques are kept as simple and efficient as possible, while still allowing a high degree of customizability.
|
||||
|
||||
This is the coming-together of two of my previous projects:
|
||||
- [WlxOverlay](https://github.com/galister/WlxOverlay) (SteamVR overlay written in C#)
|
||||
- [WlxOverlay-X](https://github.com/galister/wlx-overlay-x) (OpenXR overlay using StereoKit, written in Rust)
|
||||

|
||||
|
||||
# Join the Linux VR Community
|
||||
## Join the Linux VR Community
|
||||
|
||||
We are available on either:
|
||||
- Discord: https://discord.gg/gHwJ2vwSWV
|
||||
|
||||
- Discord: <https://discord.gg/gHwJ2vwSWV>
|
||||
- Matrix Space: `#linux-vr-adventures:matrix.org`
|
||||
|
||||
Questions/issues specific to WlxOverlay-S will be handled in the `wlxoverlay` chat room.
|
||||
|
||||
# Setup
|
||||
## Setup
|
||||
|
||||
### General Setup
|
||||
|
||||
1. Grab the latest AppImage from [Releases](https://github.com/galister/wlx-overlay-s/releases).
|
||||
1. `chmod +x WlxOverlay-S-*.AppImage`
|
||||
1. Start Monado, WiVRn or SteamVR.
|
||||
1. Run the overlay
|
||||
|
||||
**Note:** If you are using Monado or WiVRn, no additional setup steps are required for Flatpak Steam compatibility—most people use WlxOverlay-S seamlessly with Monado/WiVRn.
|
||||
|
||||
### SteamVR via Steam Flatpak
|
||||
|
||||
For users specifically running **SteamVR via Steam Flatpak**, follow these steps:
|
||||
|
||||
1. Grab the latest AppImage from [Releases](https://github.com/galister/wlx-overlay-s/releases).
|
||||
1. `WlxOverlay-S-*.AppImage --appimage-extract`
|
||||
1. `chmod +x squashfs-root/AppRun`
|
||||
1. Move the newly created `squashfs-root` folder to a location accessible by the Steam Flatpak.
|
||||
1. `flatpak override com.valvesoftware.Steam --user --filesystem=xdg-run/pipewire-0/:rw`
|
||||
1. Restart Steam.
|
||||
1. Start SteamVR.
|
||||
1. `flatpak run --command='/path/to/squashfs-root/AppRun' com.valvesoftware.Steam`
|
||||
|
||||
AUR package is [wlx-overlay-s-git](https://aur.archlinux.org/packages/wlx-overlay-s-git).
|
||||
|
||||
You may also want to [build from source](https://github.com/galister/wlx-overlay-s/wiki/Building-from-Source).
|
||||
|
||||
# First Start
|
||||
## First Start
|
||||
|
||||
**If you get a screen share pop-up, check the terminal and select the screens in the order it tells you to.**
|
||||
**When the screen share pop-up appears, check the terminal and select the screens in the order it requests.**
|
||||
|
||||
If you selected the screens in the wrong order:
|
||||
- `rm ~/.config/wlxoverlay/conf.d/pw_tokens.yaml` then restart
|
||||
In case screens were selected in the wrong order:
|
||||
|
||||
SteamVR users: WlxOverlay-S will register itself for auto-start, so you will not need to start it every time.
|
||||
- `rm ~/.config/wlxoverlay/conf.d/pw_tokens.yaml` then restart
|
||||
|
||||
**SteamVR users**: WlxOverlay-S will register itself for auto-start, so there is no need to start it every time.
|
||||
|
||||
**Envision users**: Set `wlx-overlay-s --openxr --show` as the _Autostart Command_ on your Envision profile! This will show a home environment with headset passthrough by default or a [customizable background](https://github.com/galister/wlx-overlay-s/wiki/OpenXR-Skybox)! If you are using the appimage instead, set the _Autostart Command_ to the location of tha appimage binary, e.g `/full/path/to/wlx-overlay-s.appimage --openxr --show`.
|
||||
|
||||
**Please continue reading the guide below.**
|
||||
|
||||
# Getting Started
|
||||
## Getting Started
|
||||
|
||||
### Working Set
|
||||
|
||||
Your working set consists of your currently selected overlays; screens, mirrors, keyboard, etc.
|
||||
The working set consists of all currently selected overlays; screens, mirrors, keyboard, etc.
|
||||
|
||||
The working set appears in front of you when shown, and can be re-centered by hiding and showing again.
|
||||
The working set appears in front of the headset when shown, and can be re-centered by hiding and showing again.
|
||||
|
||||
Show and hide your working set using:
|
||||
- Non-vive controller: double-tap B or Y on your left controller.
|
||||
- Vive controller: double-tap the menu button on your left controller (OpenXR, for SteamVR, you might need to bind `showhide` yourself.)
|
||||
Show and hide the working set using:
|
||||
|
||||
- Non-vive controller: double-tap B or Y on the left controller.
|
||||
- Vive controller: double-tap the menu button on the left controller (for SteamVR, the `showhide` binding must be bound)
|
||||
|
||||
### Pointer Modes AKA Laser Colors
|
||||
|
||||
Much of the functionality in WlxOverlay-S depends on what color of laser you are using to interact with a UI element. \
|
||||
Much of the functionality in WlxOverlay-S depends on what color of laser is used to interact with a UI element. \
|
||||
Using the default settings, there are 3 modes:
|
||||
|
||||
- Regular Mode: Blue laser
|
||||
- Right-click Mode: Orange laser
|
||||
- Middle-click Mode: Purple laser
|
||||
@@ -66,7 +87,7 @@ The guide here uses the colors for ease of getting started.
|
||||
|
||||
### The Watch
|
||||
|
||||
Check your left wrist for the watch. The watch is your primary tool for controlling the app.
|
||||
Check your left wrist for the watch. The watch is the primary tool for controlling the app.
|
||||
|
||||

|
||||
|
||||
@@ -75,26 +96,28 @@ Check your left wrist for the watch. The watch is your primary tool for controll
|
||||
Hovering a pointer over a screen will move the mouse. If there are more than one pointers hovering a screen, the pointer that was last used to click will take precedence.
|
||||
|
||||
The click depends on the laser color:
|
||||
|
||||
- Blue laser: Left click
|
||||
- Orange laser: Right click
|
||||
- Purple laser: Middle click
|
||||
- Stick up/down: Scroll wheel
|
||||
|
||||
To **curve screens**, grab them with one hand, then with the other hand, hover the laser over the overlay and use the scroll action.
|
||||
To **curve a screen**, grab it with one hand. Then, using the other hand, hover the laser over the screen and use the scroll action.
|
||||
|
||||
See the bindings section on how to grab, move and resize screens.
|
||||
See the [bindings](#default-bindings) section on how to grab, move and resize screens.
|
||||
|
||||
### The keyboard
|
||||
|
||||
The keyboard is fully customizable via the [keyboard.yaml](https://raw.githubusercontent.com/galister/wlx-overlay-s/main/src/res/keyboard.yaml) file. \
|
||||
Download it into your `~/.config/wlxoverlay/` folder and edit it to your liking.
|
||||
Download it into the `~/.config/wlxoverlay/` folder and edit it to your liking.
|
||||
|
||||
Typing
|
||||
|
||||
- Use the BLUE laser when typing regularly.
|
||||
- While using ORANGE laser, all keystrokes will have SHIFT applied.
|
||||
- Purple laser has no effect as of now.
|
||||
|
||||
**Modifier Keys** are sticky. They will remain pressed until you press a non-modifier key, or toggle them off.
|
||||
**Modifier Keys** are sticky. They will remain pressed until a non-modifier key is pressed, the modifier gets toggled off, or the keyboard gets hidden.
|
||||
|
||||
### Default Bindings
|
||||
|
||||
@@ -102,29 +125,54 @@ Typing
|
||||
|
||||

|
||||
|
||||
To customize bindings on OpenXR, refer to the [OpenXR Bindings wiki page](https://github.com/galister/wlx-overlay-s/wiki/OpenXR-Bindings).
|
||||
|
||||
If your bindings are not supported, please reach out. \
|
||||
We would like to work with you and include additional bindings.
|
||||
|
||||
# Troubleshooting
|
||||
## Troubleshooting
|
||||
|
||||
When an error is detected, we often print tips for fixing into the log file.
|
||||
|
||||
Logs will be at `/tmp/wlx.log` for most distros.
|
||||
|
||||
Check [here](https://github.com/galister/wlx-overlay-s/wiki/Troubleshooting) for tips.
|
||||
|
||||
# Known Issues
|
||||
## Known Issues
|
||||
|
||||
## Auto-Start not working on SteamVR 2.4.4
|
||||
### Mouse is not where it should be
|
||||
|
||||
This seems to only affect SteamVR 2.4.4 (stable as of writing). Try switching to `temp_v1.27.5` or `beta` using the SteamVR beta selection if this bothers you. (The `linux-v1.14` branch will not work.)
|
||||
Hyprland users: Hyprland v0.41.0 changed their absolute input implementation to one that does not respect existing absolute input standards. Make your voice heard: [Hyprland#6023](https://github.com/hyprwm/Hyprland/issues/6023)・[Hyprland#6889](https://github.com/hyprwm/Hyprland/issues/6889)
|
||||
|
||||
## Modifiers get stuck in weird ways
|
||||
Niri users: use on Niri 0.1.7 or later.
|
||||
|
||||
This is a rare issue that can make your desktop not react to click or keys due to a modifier being stuck somewhere. Restarting the overlay fixes this.
|
||||
X11 users might be dealing with a [Phantom Monitor](https://wiki.archlinux.org/title/Xrandr#Disabling_phantom_monitor).
|
||||
|
||||
## Scroll wheel doesn't work
|
||||
Other desktops: The screens may have been selected in the wrong order, see [First Start](#first-start).
|
||||
|
||||
This seems to be an issue specific to Electron apps (Discord, Element, Slack, Spotify) on Wayland. Scrolling will work when using these in your web browser.
|
||||
### Crashes, blank screens
|
||||
|
||||
## X11 limitations
|
||||
There are some driver-desktop combinations that don't play nice with DMA-buf capture.
|
||||
|
||||
Disabling DMA-buf capture is a good first step to try when encountering an app crash or gpu driver reset.
|
||||
|
||||
```bash
|
||||
echo 'capture_method: pw_fallback' > ~/.config/wlxoverlay/conf.d/pw_fallback.yaml
|
||||
```
|
||||
|
||||
Without DMA-buf capture, capturing screens takes CPU power, so let's try and not show too many screens at the same time.
|
||||
|
||||
### Space-drag crashes SteamVR
|
||||
|
||||
This has been idenfitied as an issue with SteamVR versions 2.5.5 and above (latest tested 2.7.2). One way to avoid the crash is by switching to the `temp-v1.27.5` branch of SteamVR (via beta selection) and selecting [Steam-Play-None](https://github.com/Scrumplex/Steam-Play-None) under the compatibility tab.
|
||||
|
||||
### Modifiers get stuck in weird ways
|
||||
|
||||
This is a rare issue that can make KDE Plasma not react to click or keys due to what seems to be a race condition with modifiers. Restarting the overlay fixes this.
|
||||
|
||||
### X11 limitations
|
||||
|
||||
- X11 capture can generally seem slow. This is because zero-copy GPU capture is not supported on the general X11 desktop. Consider trying Wayland or Picom.
|
||||
- DPI scaling is not supported and may cause the mouse to not follow the laser properly.
|
||||
- Upright screens are not supported and can cause the mouse to act weirdly.
|
||||
- Screen changes (connecting / disconnecting a display, resolution changes, etc) are not handled at runtime. Restart the overlay for these to take effect.
|
||||
@@ -0,0 +1,40 @@
|
||||
use regex::Regex;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let mut wlx_build = get_version().unwrap_or(format!("{}-unknown", env!("CARGO_PKG_VERSION")));
|
||||
|
||||
match std::env::var("GITHUB_JOB").as_deref() {
|
||||
Ok("make_release") => {
|
||||
wlx_build = format!("{} (Release)", &wlx_build);
|
||||
}
|
||||
Ok("build_appimage") => {
|
||||
wlx_build = format!("{} (AppImage)", &wlx_build);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
println!("cargo:rustc-env=WLX_BUILD={}", &wlx_build);
|
||||
}
|
||||
|
||||
fn get_version() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let re = Regex::new(r"v([0-9.]+)-([0-9]+)-g([a-f0-9]+)").unwrap(); // safe
|
||||
let output = Command::new("git")
|
||||
.args(["describe", "--tags", "--abbrev=7", "--dirty"])
|
||||
.output()?;
|
||||
|
||||
let mut output_str = String::from_utf8(output.stdout)?;
|
||||
|
||||
if output_str.is_empty() {
|
||||
let output = Command::new("git")
|
||||
.args(["describe", "--tags", "--abbrev=7", "--dirty", "--always"])
|
||||
.output()?;
|
||||
|
||||
output_str = format!(
|
||||
"{}-{}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
String::from_utf8(output.stdout)?
|
||||
);
|
||||
}
|
||||
|
||||
Ok(re.replace_all(&output_str, "${1}.r${2}.${3}").into_owned())
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/galister/wlx-overlay-s/refs/heads/guide/wayvr/logo.svg" height="120"/>
|
||||
</p>
|
||||
|
||||
**WayVR acts as a bridge between Wayland applications and wlx-overlay-s panels, allowing you to display your applications within a VR environment. Internally, WayVR utilizes Smithay to run a Wayland compositor.**
|
||||
|
||||
# >> Quick setup <<
|
||||
|
||||
#### Configure your applications list
|
||||
|
||||
Go to `src/res/wayvr.yaml` to configure your desired application list. This configuration file represents all currently available WayVR options. Feel free to adjust it to your liking.
|
||||
|
||||
#### Add WayVR Launcher to your watch
|
||||
|
||||
Copy `watch_wayvr_example.yaml` to `~/.config/wlxoverlay/watch.yaml`. This file contains pre-configured **WayVRLauncher** and **WayVRDisplayList** widget types. By default, the _default_catalog_ is used.
|
||||
|
||||
That's it; you're all set!
|
||||
|
||||
###### _Make sure you have `wayvr` feature enabled in Cargo.toml (enabled by default)_
|
||||
|
||||

|
||||
|
||||
# Overview
|
||||
|
||||
### Features
|
||||
|
||||
- Display Wayland applications without GPU overhead (zero-copy via dma-buf)
|
||||
- Mouse and keyboard input, with precision scrolling support
|
||||
- Tested on AMD and Nvidia
|
||||
|
||||
### Supported software
|
||||
|
||||
- Basically all Qt and GTK applications (they work out of the box)
|
||||
- Most XWayland applications via `cage`
|
||||
|
||||
### XWayland
|
||||
|
||||
WayVR does not have native XWayland support. You can run X11 applications (or these who require DISPLAY set) by wrapping them in a `cage` program, like so:
|
||||
|
||||
```yaml
|
||||
- name: "Xeyes"
|
||||
target_display: "Disp1"
|
||||
exec: "cage"
|
||||
args: "xeyes -- -fg blue"
|
||||
```
|
||||
|
||||
instead of:
|
||||
|
||||
```yaml
|
||||
- name: "Xeyes"
|
||||
target_display: "Disp1"
|
||||
exec: "xeyes"
|
||||
args: "-fg blue"
|
||||
```
|
||||
|
||||
in `wayvr.yaml` configuration file, in your desired catalog.
|
||||
|
||||
### Launching external apps inside WayVR
|
||||
|
||||
To launch your app externally:
|
||||
|
||||
```sh
|
||||
DISPLAY= WAYLAND_DISPLAY=wayland-$(cat $XDG_RUNTIME_DIR/wayvr.disp) yourapp
|
||||
```
|
||||
|
||||
or (in the most cases):
|
||||
|
||||
```
|
||||
DISPLAY= WAYLAND_DISPLAY=wayland-20 yourapp
|
||||
```
|
||||
|
||||
Setting `DISPLAY` to an empty string forces various apps to use Wayland instead of X11.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
### My application doesn't launch but others do!
|
||||
|
||||
Even though some applications support Wayland, some still check for the `DISPLAY` environment variable and an available X11 server, throwing an error. This can also be fixed by running `cage` on top of them.
|
||||
|
||||
### Image corruption
|
||||
|
||||
dma-buf textures may display various graphical glitches due to unsupported dma-buf tiling modifiers between GLES<->Vulkan on Radeon RDNA3 graphics cards. Current situation: https://gitlab.freedesktop.org/mesa/mesa/-/issues/11629). Nvidia should work out of the box, without any isues. Alternatively, you can run wlx-overlay-s with `LIBGL_ALWAYS_SOFTWARE=1` to mitigate that (only the Smithay compositor will run in software renderer mode, wlx will still be accelerated).
|
||||
|
||||
### Floating windows
|
||||
|
||||
Context menus are not functional in most cases yet, including drag & drop support.
|
||||
|
||||
### Forced window shadows in GTK
|
||||
|
||||
GNOME still insists on rendering client-side decorations instead of server-side ones. This results in all GTK applications looking odd due to additional window shadows. [Fix here, "Client-side decorations"](https://wiki.archlinux.org/title/GTK)
|
||||
@@ -0,0 +1,207 @@
|
||||
# looking to make changes?
|
||||
# drop me in ~/.config/wlxoverlay/watch.yaml
|
||||
#
|
||||
|
||||
width: 0.115
|
||||
|
||||
size: [400, 272]
|
||||
|
||||
elements:
|
||||
# background panel
|
||||
- type: Panel
|
||||
rect: [0, 30, 400, 130]
|
||||
corner_radius: 20
|
||||
bg_color: "#24273a"
|
||||
|
||||
- type: Button
|
||||
rect: [2, 162, 26, 36]
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
bg_color: "#c6a0f6"
|
||||
fg_color: "#24273a"
|
||||
text: "C"
|
||||
click_up: # destroy if exists, otherwise create
|
||||
- type: Window
|
||||
target: settings
|
||||
action: ShowUi # only triggers if not exists
|
||||
- type: Window
|
||||
target: settings
|
||||
action: Destroy # only triggers if exists since before current frame
|
||||
|
||||
# Dashboard toggle button
|
||||
- type: Button
|
||||
rect: [32, 162, 48, 36]
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
bg_color: "#2288FF"
|
||||
fg_color: "#24273a"
|
||||
text: "Dash"
|
||||
click_up:
|
||||
- type: WayVR
|
||||
action: ToggleDashboard
|
||||
|
||||
# Keyboard button
|
||||
- type: Button
|
||||
rect: [84, 162, 48, 36]
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#a6da95"
|
||||
text: Kbd
|
||||
click_up:
|
||||
- type: Overlay
|
||||
target: "kbd"
|
||||
action: ToggleVisible
|
||||
long_click_up:
|
||||
- type: Overlay
|
||||
target: "kbd"
|
||||
action: Reset
|
||||
right_up:
|
||||
- type: Overlay
|
||||
target: "kbd"
|
||||
action: ToggleImmovable
|
||||
middle_up:
|
||||
- type: Overlay
|
||||
target: "kbd"
|
||||
action: ToggleInteraction
|
||||
scroll_up:
|
||||
- type: Overlay
|
||||
target: "kbd"
|
||||
action:
|
||||
Opacity: { delta: 0.025 }
|
||||
scroll_down:
|
||||
- type: Overlay
|
||||
target: "kbd"
|
||||
action:
|
||||
Opacity: { delta: -0.025 }
|
||||
|
||||
# bottom row, of keyboard + overlays
|
||||
- type: OverlayList
|
||||
rect: [134, 160, 266, 40]
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#1e2030"
|
||||
layout: Horizontal
|
||||
click_up: ToggleVisible
|
||||
long_click_up: Reset
|
||||
right_up: ToggleImmovable
|
||||
middle_up: ToggleInteraction
|
||||
scroll_up:
|
||||
Opacity: { delta: 0.025 }
|
||||
scroll_down:
|
||||
Opacity: { delta: -0.025 }
|
||||
|
||||
- type: WayVRLauncher
|
||||
rect: [0, 200, 400, 36]
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#e590c4"
|
||||
catalog_name: "default_catalog"
|
||||
|
||||
- type: WayVRDisplayList
|
||||
rect: [0, 236, 400, 36]
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#ca68a4"
|
||||
|
||||
# local clock
|
||||
- type: Label
|
||||
rect: [19, 90, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 46 # Use 32 for 12-hour time
|
||||
fg_color: "#cad3f5"
|
||||
source: Clock
|
||||
format: "%H:%M" # 23:59
|
||||
#format: "%I:%M %p" # 11:59 PM
|
||||
|
||||
# local date
|
||||
- type: Label
|
||||
rect: [20, 117, 200, 20]
|
||||
corner_radius: 4
|
||||
font_size: 14
|
||||
fg_color: "#cad3f5"
|
||||
source: Clock
|
||||
format: "%x" # local date representation
|
||||
|
||||
# local day-of-week
|
||||
- type: Label
|
||||
rect: [20, 137, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 14
|
||||
fg_color: "#cad3f5"
|
||||
source: Clock
|
||||
format: "%A" # Tuesday
|
||||
#format: "%a" # Tue
|
||||
|
||||
# alt clock 1
|
||||
- type: Label
|
||||
rect: [210, 90, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 24 # Use 18 for 12-hour time
|
||||
fg_color: "#8bd5ca"
|
||||
source: Clock
|
||||
timezone: 0 # change TZ1 here
|
||||
format: "%H:%M" # 23:59
|
||||
#format: "%I:%M %p" # 11:59 PM
|
||||
- type: Label
|
||||
rect: [210, 60, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 14
|
||||
fg_color: "#8bd5ca"
|
||||
source: Timezone
|
||||
timezone: 0 # change TZ1 label here
|
||||
|
||||
# alt clock 2
|
||||
- type: Label
|
||||
rect: [210, 150, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 24 # Use 18 for 12-hour time
|
||||
fg_color: "#b7bdf8"
|
||||
source: Clock
|
||||
timezone: 1 # change TZ2 here
|
||||
format: "%H:%M" # 23:59
|
||||
#format: "%I:%M %p" # 11:59 PM
|
||||
- type: Label
|
||||
rect: [210, 120, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 14
|
||||
fg_color: "#b7bdf8"
|
||||
source: Timezone
|
||||
timezone: 1 # change TZ2 label here
|
||||
|
||||
# batteries
|
||||
- type: BatteryList
|
||||
rect: [0, 5, 400, 30]
|
||||
corner_radius: 4
|
||||
font_size: 16
|
||||
fg_color: "#8bd5ca"
|
||||
fg_color_low: "#B06060"
|
||||
fg_color_charging: "#6080A0"
|
||||
num_devices: 9
|
||||
layout: Horizontal
|
||||
low_threshold: 33
|
||||
|
||||
# volume buttons
|
||||
- type: Button
|
||||
rect: [315, 52, 70, 32]
|
||||
corner_radius: 4
|
||||
font_size: 13
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#5b6078"
|
||||
text: "Vol +"
|
||||
click_down:
|
||||
- type: Exec
|
||||
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%"]
|
||||
- type: Button
|
||||
rect: [315, 116, 70, 32]
|
||||
corner_radius: 4
|
||||
font_size: 13
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#5b6078"
|
||||
text: "Vol -"
|
||||
click_down:
|
||||
- type: Exec
|
||||
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%"]
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=wlx-overlay-s - Lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops
|
||||
After=monado.service
|
||||
BindsTo=monado.service
|
||||
Requires=monado.socket
|
||||
Requires=graphical-session.target
|
||||
|
||||
[Service]
|
||||
ExecStart=@prefix@/bin/wlx-overlay-s
|
||||
|
||||
[Install]
|
||||
WantedBy=monado.service
|
||||
+30
-30
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
#[cfg(feature = "openxr")]
|
||||
use openxr as xr;
|
||||
|
||||
@@ -14,14 +13,14 @@ use crate::{
|
||||
hid::{get_keymap_wl, get_keymap_x11},
|
||||
overlays::{
|
||||
anchor::create_anchor,
|
||||
keyboard::create_keyboard,
|
||||
keyboard::{create_keyboard, KEYBOARD_NAME},
|
||||
screen::WlxClientAlias,
|
||||
watch::{create_watch, WATCH_NAME},
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::overlay::OverlayData;
|
||||
use super::overlay::{OverlayData, OverlayID};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BackendError {
|
||||
@@ -69,17 +68,17 @@ where
|
||||
app.screens.clear();
|
||||
let data = if let Some(wl) = wl.as_mut() {
|
||||
keymap = get_keymap_wl()
|
||||
.map_err(|f| log::warn!("Could not load keyboard layout: {}", f))
|
||||
.map_err(|f| log::warn!("Could not load keyboard layout: {f}"))
|
||||
.ok();
|
||||
crate::overlays::screen::create_screens_wayland(wl, app)?
|
||||
crate::overlays::screen::create_screens_wayland(wl, app)
|
||||
} else {
|
||||
keymap = get_keymap_x11()
|
||||
.map_err(|f| log::warn!("Could not load keyboard layout: {}", f))
|
||||
.map_err(|f| log::warn!("Could not load keyboard layout: {f}"))
|
||||
.ok();
|
||||
match crate::overlays::screen::create_screens_x11pw(app) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
log::info!("Will not use PipeWire capture: {:?}", e);
|
||||
log::info!("Will not use PipeWire capture: {e:?}");
|
||||
crate::overlays::screen::create_screens_xshm(app)?
|
||||
}
|
||||
}
|
||||
@@ -95,10 +94,9 @@ where
|
||||
for (meta, mut state, backend) in data.screens {
|
||||
if show_screens.arc_get(state.name.as_ref()) {
|
||||
state.show_hide = true;
|
||||
state.want_visible = false;
|
||||
}
|
||||
overlays.insert(
|
||||
state.id,
|
||||
state.id.0,
|
||||
OverlayData::<T> {
|
||||
state,
|
||||
backend,
|
||||
@@ -109,16 +107,16 @@ where
|
||||
}
|
||||
|
||||
let anchor = create_anchor(app)?;
|
||||
overlays.insert(anchor.state.id, anchor);
|
||||
overlays.insert(anchor.state.id.0, anchor);
|
||||
|
||||
let mut watch = create_watch::<T>(app)?;
|
||||
watch.state.want_visible = true;
|
||||
overlays.insert(watch.state.id, watch);
|
||||
overlays.insert(watch.state.id.0, watch);
|
||||
|
||||
let mut keyboard = create_keyboard(app, keymap)?;
|
||||
keyboard.state.show_hide = true;
|
||||
keyboard.state.show_hide = show_screens.arc_get(KEYBOARD_NAME);
|
||||
keyboard.state.want_visible = false;
|
||||
overlays.insert(keyboard.state.id, keyboard);
|
||||
overlays.insert(keyboard.state.id.0, keyboard);
|
||||
|
||||
Ok(Self { overlays, wl })
|
||||
}
|
||||
@@ -128,6 +126,8 @@ where
|
||||
Ok(vec![])
|
||||
}
|
||||
#[cfg(feature = "wayland")]
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
|
||||
pub fn update(&mut self, app: &mut AppState) -> anyhow::Result<Vec<OverlayData<T>>> {
|
||||
use crate::overlays::{
|
||||
screen::{create_screen_interaction, create_screen_renderer_wl, load_pw_token_config},
|
||||
@@ -155,11 +155,11 @@ where
|
||||
if create_ran {
|
||||
continue;
|
||||
}
|
||||
let data = crate::overlays::screen::create_screens_wayland(wl, app)?;
|
||||
let data = crate::overlays::screen::create_screens_wayland(wl, app);
|
||||
create_ran = true;
|
||||
for (meta, state, backend) in data.screens {
|
||||
self.overlays.insert(
|
||||
state.id,
|
||||
state.id.0,
|
||||
OverlayData::<T> {
|
||||
state,
|
||||
backend,
|
||||
@@ -176,7 +176,7 @@ where
|
||||
};
|
||||
|
||||
let meta = &app.screens[idx];
|
||||
let removed = self.overlays.remove(meta.id).unwrap();
|
||||
let removed = self.overlays.remove(meta.id.0).unwrap();
|
||||
removed_overlays.push(removed);
|
||||
log::info!("{}: Destroyed", meta.name);
|
||||
app.screens.remove(idx);
|
||||
@@ -188,7 +188,7 @@ where
|
||||
continue;
|
||||
};
|
||||
let output = wl.outputs.get(id).unwrap();
|
||||
let Some(overlay) = self.overlays.get_mut(meta.id) else {
|
||||
let Some(overlay) = self.overlays.get_mut(meta.id.0) else {
|
||||
continue;
|
||||
};
|
||||
let logical_pos =
|
||||
@@ -210,7 +210,7 @@ where
|
||||
continue;
|
||||
};
|
||||
let output = wl.outputs.get(id).unwrap();
|
||||
let Some(overlay) = self.overlays.get_mut(meta.id) else {
|
||||
let Some(overlay) = self.overlays.get_mut(meta.id.0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -229,7 +229,7 @@ where
|
||||
has_wlr_dmabuf,
|
||||
has_wlr_screencopy,
|
||||
pw_token_store,
|
||||
&app.session,
|
||||
&app,
|
||||
) {
|
||||
overlay.backend.set_renderer(Box::new(renderer));
|
||||
}
|
||||
@@ -271,7 +271,7 @@ where
|
||||
|
||||
pub fn remove_by_selector(&mut self, selector: &OverlaySelector) -> Option<OverlayData<T>> {
|
||||
match selector {
|
||||
OverlaySelector::Id(id) => self.overlays.remove(id),
|
||||
OverlaySelector::Id(id) => self.overlays.remove(id.0),
|
||||
OverlaySelector::Name(name) => {
|
||||
let id = self
|
||||
.overlays
|
||||
@@ -283,12 +283,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_by_id(&mut self, id: usize) -> Option<&OverlayData<T>> {
|
||||
self.overlays.get(id)
|
||||
pub fn get_by_id(&mut self, id: OverlayID) -> Option<&OverlayData<T>> {
|
||||
self.overlays.get(id.0)
|
||||
}
|
||||
|
||||
pub fn mut_by_id(&mut self, id: usize) -> Option<&mut OverlayData<T>> {
|
||||
self.overlays.get_mut(id)
|
||||
pub fn mut_by_id(&mut self, id: OverlayID) -> Option<&mut OverlayData<T>> {
|
||||
self.overlays.get_mut(id.0)
|
||||
}
|
||||
|
||||
pub fn get_by_name<'a>(&'a mut self, name: &str) -> Option<&'a OverlayData<T>> {
|
||||
@@ -308,7 +308,7 @@ where
|
||||
}
|
||||
|
||||
pub fn add(&mut self, overlay: OverlayData<T>) {
|
||||
self.overlays.insert(overlay.state.id, overlay);
|
||||
self.overlays.insert(overlay.state.id.0, overlay);
|
||||
}
|
||||
|
||||
pub fn show_hide(&mut self, app: &mut AppState) {
|
||||
@@ -318,8 +318,8 @@ where
|
||||
.any(|o| o.state.show_hide && o.state.want_visible);
|
||||
|
||||
if !any_shown {
|
||||
static ANCHOR_LOCAL: Lazy<Affine3A> =
|
||||
Lazy::new(|| Affine3A::from_translation(Vec3::NEG_Z));
|
||||
static ANCHOR_LOCAL: LazyLock<Affine3A> =
|
||||
LazyLock::new(|| Affine3A::from_translation(Vec3::NEG_Z));
|
||||
let hmd = snap_upright(app.input_state.hmd, Vec3A::Y);
|
||||
app.anchor = hmd * *ANCHOR_LOCAL;
|
||||
}
|
||||
@@ -338,14 +338,14 @@ where
|
||||
if !any_shown && *o.state.name == *WATCH_NAME {
|
||||
o.state.reset(app, true);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum OverlaySelector {
|
||||
Id(usize),
|
||||
Id(OverlayID),
|
||||
Name(Arc<str>),
|
||||
}
|
||||
|
||||
|
||||
+167
-100
@@ -1,4 +1,5 @@
|
||||
use std::f32::consts::PI;
|
||||
use std::process::{Child, Command};
|
||||
use std::{collections::VecDeque, time::Instant};
|
||||
|
||||
use glam::{Affine3A, Vec2, Vec3, Vec3A, Vec3Swizzles};
|
||||
@@ -8,8 +9,9 @@ use smallvec::{smallvec, SmallVec};
|
||||
use crate::backend::common::{snap_upright, OverlaySelector};
|
||||
use crate::config::{AStrMapExt, GeneralConfig};
|
||||
use crate::overlays::anchor::ANCHOR_NAME;
|
||||
use crate::state::AppState;
|
||||
use crate::state::{AppSession, AppState, KeyboardFocus};
|
||||
|
||||
use super::overlay::{OverlayID, OverlayState};
|
||||
use super::task::{TaskContainer, TaskType};
|
||||
use super::{common::OverlayContainer, overlay::OverlayData};
|
||||
|
||||
@@ -34,6 +36,7 @@ pub struct InputState {
|
||||
pub ipd: f32,
|
||||
pub pointers: [Pointer; 2],
|
||||
pub devices: Vec<TrackedDevice>,
|
||||
processes: Vec<Child>,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
@@ -43,51 +46,19 @@ impl InputState {
|
||||
ipd: 0.0,
|
||||
pointers: [Pointer::new(0), Pointer::new(1)],
|
||||
devices: Vec::new(),
|
||||
processes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pre_update(&mut self) {
|
||||
pub const fn pre_update(&mut self) {
|
||||
self.pointers[0].before = self.pointers[0].now;
|
||||
self.pointers[1].before = self.pointers[1].now;
|
||||
}
|
||||
|
||||
pub fn post_update(&mut self) {
|
||||
pub fn post_update(&mut self, session: &AppSession) {
|
||||
for hand in &mut self.pointers {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if hand.now.click != hand.before.click {
|
||||
log::debug!("Hand {}: click {}", hand.idx, hand.now.click);
|
||||
}
|
||||
if hand.now.grab != hand.before.grab {
|
||||
log::debug!("Hand {}: grab {}", hand.idx, hand.now.grab);
|
||||
}
|
||||
if hand.now.alt_click != hand.before.alt_click {
|
||||
log::debug!("Hand {}: alt_click {}", hand.idx, hand.now.alt_click);
|
||||
}
|
||||
if hand.now.show_hide != hand.before.show_hide {
|
||||
log::debug!("Hand {}: show_hide {}", hand.idx, hand.now.show_hide);
|
||||
}
|
||||
if hand.now.space_drag != hand.before.space_drag {
|
||||
log::debug!("Hand {}: space_drag {}", hand.idx, hand.now.space_drag);
|
||||
}
|
||||
if hand.now.space_rotate != hand.before.space_rotate {
|
||||
log::debug!("Hand {}: space_rotate {}", hand.idx, hand.now.space_rotate);
|
||||
}
|
||||
if hand.now.click_modifier_right != hand.before.click_modifier_right {
|
||||
log::debug!(
|
||||
"Hand {}: click_modifier_right {}",
|
||||
hand.idx,
|
||||
hand.now.click_modifier_right
|
||||
);
|
||||
}
|
||||
if hand.now.click_modifier_middle != hand.before.click_modifier_middle {
|
||||
log::debug!(
|
||||
"Hand {}: click_modifier_middle {}",
|
||||
hand.idx,
|
||||
hand.now.click_modifier_middle
|
||||
);
|
||||
}
|
||||
}
|
||||
debug_print_hand(hand);
|
||||
|
||||
if hand.now.click {
|
||||
hand.last_click = Instant::now();
|
||||
@@ -104,8 +75,8 @@ impl InputState {
|
||||
}
|
||||
|
||||
let hmd_up = self.hmd.transform_vector3a(Vec3A::Y);
|
||||
let dot =
|
||||
hmd_up.dot(hand.pose.transform_vector3a(Vec3A::X)) * (1.0 - 2.0 * hand.idx as f32);
|
||||
let dot = hmd_up.dot(hand.pose.transform_vector3a(Vec3A::X))
|
||||
* 2.0f32.mul_add(-(hand.idx as f32), 1.0);
|
||||
|
||||
hand.interaction.mode = if dot < -0.85 {
|
||||
PointerMode::Right
|
||||
@@ -129,7 +100,73 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
if hand.now.alt_click != hand.before.alt_click {
|
||||
// Reap previous processes
|
||||
self.processes
|
||||
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
|
||||
|
||||
let mut args = if hand.now.alt_click {
|
||||
session.config.alt_click_down.iter()
|
||||
} else {
|
||||
session.config.alt_click_up.iter()
|
||||
};
|
||||
|
||||
if let Some(program) = args.next() {
|
||||
if let Ok(child) = Command::new(program).args(args).spawn() {
|
||||
self.processes.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug_print_hand(hand: &Pointer) {
|
||||
{
|
||||
if hand.now.click != hand.before.click {
|
||||
log::debug!("Hand {}: click {}", hand.idx, hand.now.click);
|
||||
}
|
||||
if hand.now.grab != hand.before.grab {
|
||||
log::debug!("Hand {}: grab {}", hand.idx, hand.now.grab);
|
||||
}
|
||||
if hand.now.alt_click != hand.before.alt_click {
|
||||
log::debug!("Hand {}: alt_click {}", hand.idx, hand.now.alt_click);
|
||||
}
|
||||
if hand.now.show_hide != hand.before.show_hide {
|
||||
log::debug!("Hand {}: show_hide {}", hand.idx, hand.now.show_hide);
|
||||
}
|
||||
if hand.now.toggle_dashboard != hand.before.toggle_dashboard {
|
||||
log::debug!(
|
||||
"Hand {}: toggle_dashboard {}",
|
||||
hand.idx,
|
||||
hand.now.toggle_dashboard
|
||||
);
|
||||
}
|
||||
if hand.now.space_drag != hand.before.space_drag {
|
||||
log::debug!("Hand {}: space_drag {}", hand.idx, hand.now.space_drag);
|
||||
}
|
||||
if hand.now.space_rotate != hand.before.space_rotate {
|
||||
log::debug!("Hand {}: space_rotate {}", hand.idx, hand.now.space_rotate);
|
||||
}
|
||||
if hand.now.space_reset != hand.before.space_reset {
|
||||
log::debug!("Hand {}: space_reset {}", hand.idx, hand.now.space_reset);
|
||||
}
|
||||
if hand.now.click_modifier_right != hand.before.click_modifier_right {
|
||||
log::debug!(
|
||||
"Hand {}: click_modifier_right {}",
|
||||
hand.idx,
|
||||
hand.now.click_modifier_right
|
||||
);
|
||||
}
|
||||
if hand.now.click_modifier_middle != hand.before.click_modifier_middle {
|
||||
log::debug!(
|
||||
"Hand {}: click_modifier_middle {}",
|
||||
hand.idx,
|
||||
hand.now.click_modifier_middle
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,8 +174,8 @@ impl InputState {
|
||||
pub struct InteractionState {
|
||||
pub mode: PointerMode,
|
||||
pub grabbed: Option<GrabData>,
|
||||
pub clicked_id: Option<usize>,
|
||||
pub hovered_id: Option<usize>,
|
||||
pub clicked_id: Option<OverlayID>,
|
||||
pub hovered_id: Option<OverlayID>,
|
||||
pub release_actions: VecDeque<Box<dyn Fn()>>,
|
||||
pub next_push: Instant,
|
||||
pub haptics: Option<f32>,
|
||||
@@ -175,23 +212,26 @@ impl Pointer {
|
||||
idx,
|
||||
pose: Affine3A::IDENTITY,
|
||||
raw_pose: Affine3A::IDENTITY,
|
||||
now: Default::default(),
|
||||
before: Default::default(),
|
||||
now: PointerState::default(),
|
||||
before: PointerState::default(),
|
||||
last_click: Instant::now(),
|
||||
interaction: Default::default(),
|
||||
interaction: InteractionState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct PointerState {
|
||||
pub scroll: f32,
|
||||
pub scroll_x: f32,
|
||||
pub scroll_y: f32,
|
||||
pub click: bool,
|
||||
pub grab: bool,
|
||||
pub alt_click: bool,
|
||||
pub show_hide: bool,
|
||||
pub toggle_dashboard: bool,
|
||||
pub space_drag: bool,
|
||||
pub space_rotate: bool,
|
||||
pub space_reset: bool,
|
||||
pub click_modifier_right: bool,
|
||||
pub click_modifier_middle: bool,
|
||||
pub move_mouse: bool,
|
||||
@@ -200,13 +240,14 @@ pub struct PointerState {
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct PointerHit {
|
||||
pub pointer: usize,
|
||||
pub overlay: usize,
|
||||
pub overlay: OverlayID,
|
||||
pub mode: PointerMode,
|
||||
pub primary: bool,
|
||||
pub uv: Vec2,
|
||||
pub dist: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Haptics {
|
||||
pub intensity: f32,
|
||||
pub duration: f32,
|
||||
@@ -217,7 +258,7 @@ pub trait InteractionHandler {
|
||||
fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> Option<Haptics>;
|
||||
fn on_left(&mut self, app: &mut AppState, pointer: usize);
|
||||
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool);
|
||||
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta: f32);
|
||||
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta_y: f32, delta_x: f32);
|
||||
}
|
||||
|
||||
pub struct DummyInteractionHandler;
|
||||
@@ -228,12 +269,12 @@ impl InteractionHandler for DummyInteractionHandler {
|
||||
None
|
||||
}
|
||||
fn on_pointer(&mut self, _app: &mut AppState, _hit: &PointerHit, _pressed: bool) {}
|
||||
fn on_scroll(&mut self, _app: &mut AppState, _hit: &PointerHit, _delta: f32) {}
|
||||
fn on_scroll(&mut self, _app: &mut AppState, _hit: &PointerHit, _delta_y: f32, _delta_x: f32) {}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct RayHit {
|
||||
overlay: usize,
|
||||
overlay: OverlayID,
|
||||
global_pos: Vec3A,
|
||||
local_pos: Vec2,
|
||||
dist: f32,
|
||||
@@ -242,7 +283,7 @@ struct RayHit {
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct GrabData {
|
||||
pub offset: Vec3A,
|
||||
pub grabbed_id: usize,
|
||||
pub grabbed_id: OverlayID,
|
||||
pub old_curvature: Option<f32>,
|
||||
pub grab_all: bool,
|
||||
}
|
||||
@@ -257,6 +298,15 @@ pub enum PointerMode {
|
||||
Special,
|
||||
}
|
||||
|
||||
fn update_focus(focus: &mut KeyboardFocus, state: &OverlayState) {
|
||||
if let Some(f) = &state.keyboard_focus {
|
||||
if *focus != *f {
|
||||
log::info!("Setting keyboard focus to {:?}", *f);
|
||||
*focus = *f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interact<O>(
|
||||
overlays: &mut OverlayContainer<O>,
|
||||
app: &mut AppState,
|
||||
@@ -275,6 +325,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
fn interact_hand<O>(
|
||||
idx: usize,
|
||||
overlays: &mut OverlayContainer<O>,
|
||||
@@ -295,7 +346,7 @@ where
|
||||
&mut app.session.config,
|
||||
);
|
||||
} else {
|
||||
log::warn!("Grabbed overlay {} does not exist", grab_data.grabbed_id);
|
||||
log::warn!("Grabbed overlay {} does not exist", grab_data.grabbed_id.0);
|
||||
pointer.interaction.grabbed = None;
|
||||
}
|
||||
return (0.1, None);
|
||||
@@ -309,15 +360,17 @@ where
|
||||
pointer = &mut app.input_state.pointers[idx];
|
||||
pointer.interaction.hovered_id = None;
|
||||
}
|
||||
if let Some(clicked_id) = pointer.interaction.clicked_id.take() {
|
||||
if let Some(clicked) = overlays.mut_by_id(clicked_id) {
|
||||
let hit = PointerHit {
|
||||
pointer: pointer.idx,
|
||||
overlay: clicked_id,
|
||||
mode: pointer.interaction.mode,
|
||||
..Default::default()
|
||||
};
|
||||
clicked.backend.on_pointer(app, &hit, false);
|
||||
if !pointer.now.click && pointer.before.click {
|
||||
if let Some(clicked_id) = pointer.interaction.clicked_id.take() {
|
||||
if let Some(clicked) = overlays.mut_by_id(clicked_id) {
|
||||
let hit = PointerHit {
|
||||
pointer: pointer.idx,
|
||||
overlay: clicked_id,
|
||||
mode: pointer.interaction.mode,
|
||||
..Default::default()
|
||||
};
|
||||
clicked.backend.on_pointer(app, &hit, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (0.0, None); // no hit
|
||||
@@ -335,7 +388,7 @@ where
|
||||
}
|
||||
}
|
||||
let Some(hovered) = overlays.mut_by_id(hit.overlay) else {
|
||||
log::warn!("Hit overlay {} does not exist", hit.overlay);
|
||||
log::warn!("Hit overlay {} does not exist", hit.overlay.0);
|
||||
return (0.0, None); // no hit
|
||||
};
|
||||
|
||||
@@ -355,6 +408,7 @@ where
|
||||
log::trace!("Hit: {} {:?}", hovered.state.name, hit);
|
||||
|
||||
if pointer.now.grab && !pointer.before.grab && hovered.state.grabbable {
|
||||
update_focus(&mut app.keyboard_focus, &hovered.state);
|
||||
pointer.start_grab(hovered, &mut app.tasks);
|
||||
return (
|
||||
hit.dist,
|
||||
@@ -366,38 +420,44 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
// Pass mouse motion events only if not scrolling
|
||||
// (allows scrolling on all Chromium-based applications)
|
||||
let haptics = hovered.backend.on_hover(app, &hit);
|
||||
|
||||
pointer = &mut app.input_state.pointers[idx];
|
||||
|
||||
if pointer.now.scroll.abs() > 0.1 {
|
||||
let scroll = pointer.now.scroll;
|
||||
if pointer.now.scroll_x.abs() > 0.1 || pointer.now.scroll_y.abs() > 0.1 {
|
||||
let scroll_x = pointer.now.scroll_x;
|
||||
let scroll_y = pointer.now.scroll_y;
|
||||
if app.input_state.pointers[1 - idx]
|
||||
.interaction
|
||||
.grabbed
|
||||
.is_some_and(|x| x.grabbed_id == hit.overlay)
|
||||
{
|
||||
let is_portrait = hovered.view().is_some_and(|v| {
|
||||
let extent = v.image().extent();
|
||||
extent[0] >= extent[1]
|
||||
});
|
||||
let can_curve = hovered
|
||||
.frame_meta()
|
||||
.is_some_and(|e| e.extent[0] >= e.extent[1]);
|
||||
|
||||
if is_portrait {
|
||||
if can_curve {
|
||||
let cur = hovered.state.curvature.unwrap_or(0.0);
|
||||
let new = (cur - scroll * 0.01).min(0.35);
|
||||
let new = scroll_y.mul_add(-0.01, cur).min(0.5);
|
||||
if new <= f32::EPSILON {
|
||||
hovered.state.curvature = None;
|
||||
} else {
|
||||
hovered.state.curvature = Some(new);
|
||||
}
|
||||
} else {
|
||||
hovered.state.curvature = None;
|
||||
}
|
||||
} else {
|
||||
hovered.backend.on_scroll(app, &hit, scroll);
|
||||
hovered.backend.on_scroll(app, &hit, scroll_y, scroll_x);
|
||||
}
|
||||
pointer = &mut app.input_state.pointers[idx];
|
||||
}
|
||||
|
||||
if pointer.now.click && !pointer.before.click {
|
||||
pointer.interaction.clicked_id = Some(hit.overlay);
|
||||
update_focus(&mut app.keyboard_focus, &hovered.state);
|
||||
hovered.backend.on_pointer(app, &hit, true);
|
||||
} else if !pointer.now.click && pointer.before.click {
|
||||
if let Some(clicked_id) = pointer.interaction.clicked_id.take() {
|
||||
@@ -426,7 +486,7 @@ impl Pointer {
|
||||
if let Some(hit) = self.ray_test(
|
||||
overlay.state.id,
|
||||
&overlay.state.transform,
|
||||
&overlay.state.curvature,
|
||||
overlay.state.curvature.as_ref(),
|
||||
) {
|
||||
if hit.dist.is_infinite() || hit.dist.is_nan() {
|
||||
continue;
|
||||
@@ -437,7 +497,7 @@ impl Pointer {
|
||||
|
||||
hits.sort_by(|a, b| a.dist.total_cmp(&b.dist));
|
||||
|
||||
for hit in hits.iter() {
|
||||
for hit in &hits {
|
||||
let overlay = overlays.get_by_id(hit.overlay).unwrap(); // safe because we just got the id from the overlay
|
||||
|
||||
let uv = overlay
|
||||
@@ -508,10 +568,10 @@ impl Pointer {
|
||||
if self.now.click {
|
||||
self.interaction.mode = PointerMode::Special;
|
||||
let cur_scale = overlay.state.transform.x_axis.length();
|
||||
if cur_scale < 0.1 && self.now.scroll > 0.0 {
|
||||
if cur_scale < 0.1 && self.now.scroll_y > 0.0 {
|
||||
return;
|
||||
}
|
||||
if cur_scale > 20. && self.now.scroll < 0.0 {
|
||||
if cur_scale > 20. && self.now.scroll_y < 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -519,15 +579,15 @@ impl Pointer {
|
||||
.state
|
||||
.transform
|
||||
.matrix3
|
||||
.mul_scalar(1.0 - 0.025 * self.now.scroll);
|
||||
} else if config.allow_sliding && self.now.scroll.is_finite() {
|
||||
grab_data.offset.z -= self.now.scroll * 0.05;
|
||||
.mul_scalar(0.025f32.mul_add(-self.now.scroll_y, 1.0));
|
||||
} else if config.allow_sliding && self.now.scroll_y.is_finite() {
|
||||
grab_data.offset.z -= self.now.scroll_y * 0.05;
|
||||
}
|
||||
overlay.state.transform.translation = self.pose.transform_point3a(grab_data.offset);
|
||||
overlay.state.realign(hmd);
|
||||
overlay.state.dirty = true;
|
||||
} else {
|
||||
log::error!("Grabbed overlay {} does not exist", overlay.state.id);
|
||||
log::error!("Grabbed overlay {} does not exist", overlay.state.id.0);
|
||||
self.interaction.grabbed = None;
|
||||
}
|
||||
} else {
|
||||
@@ -547,7 +607,7 @@ impl Pointer {
|
||||
}
|
||||
config.transform_values.arc_set(
|
||||
overlay.state.name.clone(),
|
||||
overlay.state.saved_transform.unwrap(),
|
||||
overlay.state.saved_transform.unwrap(), // safe
|
||||
);
|
||||
}
|
||||
|
||||
@@ -564,14 +624,21 @@ impl Pointer {
|
||||
|
||||
fn ray_test(
|
||||
&self,
|
||||
overlay: usize,
|
||||
overlay: OverlayID,
|
||||
transform: &Affine3A,
|
||||
curvature: &Option<f32>,
|
||||
curvature: Option<&f32>,
|
||||
) -> Option<RayHit> {
|
||||
let (dist, local_pos) = match curvature {
|
||||
Some(curvature) => raycast_cylinder(&self.pose, Vec3A::NEG_Z, transform, *curvature),
|
||||
_ => raycast_plane(&self.pose, Vec3A::NEG_Z, transform, Vec3A::NEG_Z),
|
||||
}?;
|
||||
let (dist, local_pos) = curvature.map_or_else(
|
||||
|| {
|
||||
Some(raycast_plane(
|
||||
&self.pose,
|
||||
Vec3A::NEG_Z,
|
||||
transform,
|
||||
Vec3A::NEG_Z,
|
||||
))
|
||||
},
|
||||
|curvature| raycast_cylinder(&self.pose, Vec3A::NEG_Z, transform, *curvature),
|
||||
)?;
|
||||
|
||||
if dist < 0.0 {
|
||||
// hit is behind us
|
||||
@@ -592,7 +659,7 @@ fn raycast_plane(
|
||||
source_fwd: Vec3A,
|
||||
plane: &Affine3A,
|
||||
plane_norm: Vec3A,
|
||||
) -> Option<(f32, Vec2)> {
|
||||
) -> (f32, Vec2) {
|
||||
let plane_normal = plane.transform_vector3a(plane_norm);
|
||||
let ray_dir = source.transform_vector3a(source_fwd);
|
||||
|
||||
@@ -604,7 +671,7 @@ fn raycast_plane(
|
||||
.transform_point3a(source.translation + ray_dir * dist)
|
||||
.xy();
|
||||
|
||||
Some((dist, hit_local))
|
||||
(dist, hit_local)
|
||||
}
|
||||
|
||||
fn raycast_cylinder(
|
||||
@@ -621,27 +688,27 @@ fn raycast_cylinder(
|
||||
}
|
||||
.inverse();
|
||||
|
||||
let r = size / (2.0 * PI * curvature);
|
||||
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 * r;
|
||||
let ray_origin = to_local.transform_point3a(source.translation) + Vec3A::NEG_Z * radius;
|
||||
|
||||
let d = ray_dir.xz();
|
||||
let s = ray_origin.xz();
|
||||
let v_dir = ray_dir.xz();
|
||||
let v_pos = ray_origin.xz();
|
||||
|
||||
let a = d.dot(d);
|
||||
let b = d.dot(s);
|
||||
let c = s.dot(s) - r * r;
|
||||
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 = (b * b) - (a * c);
|
||||
let d = l_pos.mul_add(l_pos, -(l_dir * c));
|
||||
if d < f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sqrt_d = d.sqrt();
|
||||
|
||||
let t1 = (-b - sqrt_d) / a;
|
||||
let t2 = (-b + sqrt_d) / a;
|
||||
let t1 = (-l_pos - sqrt_d) / l_dir;
|
||||
let t2 = (-l_pos + sqrt_d) / l_dir;
|
||||
|
||||
let t = t1.max(t2);
|
||||
|
||||
@@ -655,8 +722,8 @@ fn raycast_cylinder(
|
||||
return None;
|
||||
}
|
||||
|
||||
let max_angle = 2.0 * (size / (2.0 * r));
|
||||
let x_angle = (hit_local.x / r).asin();
|
||||
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;
|
||||
|
||||
@@ -2,6 +2,9 @@ pub mod common;
|
||||
pub mod input;
|
||||
pub mod notifications;
|
||||
|
||||
#[allow(clippy::all)]
|
||||
mod notifications_dbus;
|
||||
|
||||
#[cfg(feature = "openvr")]
|
||||
pub mod openvr;
|
||||
|
||||
@@ -14,6 +17,9 @@ pub mod uidev;
|
||||
#[cfg(feature = "osc")]
|
||||
pub mod osc;
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
pub mod wayvr;
|
||||
|
||||
pub mod overlay;
|
||||
|
||||
pub mod task;
|
||||
+105
-50
@@ -1,4 +1,9 @@
|
||||
use dbus::{blocking::Connection, channel::MatchingReceiver, message::MatchRule};
|
||||
use dbus::{
|
||||
arg::{PropMap, Variant},
|
||||
blocking::Connection,
|
||||
channel::MatchingReceiver,
|
||||
message::MatchRule,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
sync::{
|
||||
@@ -9,6 +14,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::notifications_dbus::OrgFreedesktopNotifications,
|
||||
overlays::toast::{Toast, ToastTopic},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -51,8 +57,7 @@ impl NotificationManager {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to connect to dbus. Desktop notifications will not work. Cause: {:?}",
|
||||
e
|
||||
"Failed to connect to dbus. Desktop notifications will not work. Cause: {e:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -75,55 +80,50 @@ impl NotificationManager {
|
||||
(vec![rule.match_str()], 0u32),
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let sender = self.tx_toast.clone();
|
||||
c.start_receive(
|
||||
rule,
|
||||
Box::new(move |msg, _| {
|
||||
if let Ok(toast) = parse_dbus(&msg) {
|
||||
match sender.try_send(toast) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to send notification: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}),
|
||||
);
|
||||
log::info!("Listening to DBus notifications via BecomeMonitor.");
|
||||
}
|
||||
Err(_) => {
|
||||
let rule_with_eavesdrop = {
|
||||
let mut rule = rule.clone();
|
||||
rule.eavesdrop = true;
|
||||
rule
|
||||
};
|
||||
|
||||
let sender2 = self.tx_toast.clone();
|
||||
let result = c.add_match(rule_with_eavesdrop, move |_: (), _, msg| {
|
||||
if let Ok(toast) = parse_dbus(msg) {
|
||||
match sender2.try_send(toast) {
|
||||
Ok(_) => {}
|
||||
if matches!(result, Ok(())) {
|
||||
let sender = self.tx_toast.clone();
|
||||
c.start_receive(
|
||||
rule,
|
||||
Box::new(move |msg, _| {
|
||||
if let Ok(toast) = parse_dbus(&msg) {
|
||||
match sender.try_send(toast) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to send notification: {:?}", e);
|
||||
log::error!("Failed to send notification: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
}),
|
||||
);
|
||||
log::info!("Listening to DBus notifications via BecomeMonitor.");
|
||||
} else {
|
||||
let rule_with_eavesdrop = {
|
||||
let mut rule = rule.clone();
|
||||
rule.eavesdrop = true;
|
||||
rule
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
log::info!("Listening to DBus notifications via eavesdrop.");
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!(
|
||||
"Failed to add DBus match. Desktop notifications will not work.",
|
||||
);
|
||||
let sender2 = self.tx_toast.clone();
|
||||
let result = c.add_match(rule_with_eavesdrop, move |(): (), _, msg| {
|
||||
if let Ok(toast) = parse_dbus(msg) {
|
||||
match sender2.try_send(toast) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to send notification: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
log::info!("Listening to DBus notifications via eavesdrop.");
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Failed to add DBus match. Desktop notifications will not work.",);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,12 +138,12 @@ impl NotificationManager {
|
||||
let socket = match std::net::UdpSocket::bind(addr) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Failed to bind notification socket @ {}: {:?}", addr, e);
|
||||
log::error!("Failed to bind notification socket @ {addr}: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(err) = socket.set_read_timeout(Some(Duration::from_millis(200))) {
|
||||
log::error!("Failed to set read timeout: {:?}", err);
|
||||
log::error!("Failed to set read timeout: {err:?}");
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 1024 * 16]; // vrcx embeds icons as b64
|
||||
@@ -153,14 +153,14 @@ impl NotificationManager {
|
||||
let json_str = match std::str::from_utf8(&buf[..num_bytes]) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Failed to receive notification message: {:?}", e);
|
||||
log::error!("Failed to receive notification message: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let msg = match serde_json::from_str::<XsoMessage>(json_str) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse notification message: {:?}", e);
|
||||
log::error!("Failed to parse notification message: {e:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -178,9 +178,9 @@ impl NotificationManager {
|
||||
.with_sound(msg.volume.unwrap_or(-1.) >= 0.); // XSOverlay still plays at 0,
|
||||
|
||||
match sender.try_send(toast) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::error!("Failed to send notification: {:?}", e);
|
||||
log::error!("Failed to send notification: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,60 @@ impl Drop for NotificationManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DbusNotificationSender {
|
||||
connection: Connection,
|
||||
}
|
||||
|
||||
impl DbusNotificationSender {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
connection: Connection::new_session()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn notify_send(
|
||||
&self,
|
||||
summary: &str,
|
||||
body: &str,
|
||||
urgency: u8,
|
||||
timeout: i32,
|
||||
replaces_id: u32,
|
||||
transient: bool,
|
||||
) -> anyhow::Result<u32> {
|
||||
let proxy = self.connection.with_proxy(
|
||||
"org.freedesktop.Notifications",
|
||||
"/org/freedesktop/Notifications",
|
||||
Duration::from_millis(1000),
|
||||
);
|
||||
|
||||
let mut hints = PropMap::new();
|
||||
hints.insert("urgency".to_string(), Variant(Box::new(urgency)));
|
||||
hints.insert("transient".to_string(), Variant(Box::new(transient)));
|
||||
|
||||
Ok(proxy.notify(
|
||||
"WlxOverlay-S",
|
||||
replaces_id,
|
||||
"",
|
||||
summary,
|
||||
body,
|
||||
vec![],
|
||||
hints,
|
||||
timeout,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn notify_close(&self, id: u32) -> anyhow::Result<()> {
|
||||
let proxy = self.connection.with_proxy(
|
||||
"org.freedesktop.Notifications",
|
||||
"/org/freedesktop/Notifications",
|
||||
Duration::from_millis(1000),
|
||||
);
|
||||
|
||||
proxy.close_notification(id)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
|
||||
let mut args = msg.iter_init();
|
||||
let app_name: String = args.read()?;
|
||||
@@ -218,6 +272,7 @@ fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
|
||||
// leave the audio part to the desktop env
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct XsoMessage {
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
// This code was autogenerated with `dbus-codegen-rust -g -m None -d org.freedesktop.Notifications -p /org/freedesktop/Notifications`, see https://github.com/diwic/dbus-rs
|
||||
use dbus;
|
||||
#[allow(unused_imports)]
|
||||
use dbus::arg;
|
||||
use dbus::blocking;
|
||||
|
||||
pub trait OrgFreedesktopDBusProperties {
|
||||
fn get<R0: for<'b> arg::Get<'b> + 'static>(
|
||||
&self,
|
||||
interface_name: &str,
|
||||
property_name: &str,
|
||||
) -> Result<R0, dbus::Error>;
|
||||
fn get_all(&self, interface_name: &str) -> Result<arg::PropMap, dbus::Error>;
|
||||
fn set<I2: arg::Arg + arg::Append>(
|
||||
&self,
|
||||
interface_name: &str,
|
||||
property_name: &str,
|
||||
value: I2,
|
||||
) -> Result<(), dbus::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopDBusPropertiesPropertiesChanged {
|
||||
pub interface_name: String,
|
||||
pub changed_properties: arg::PropMap,
|
||||
pub invalidated_properties: Vec<String>,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopDBusPropertiesPropertiesChanged {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.interface_name, i);
|
||||
arg::RefArg::append(&self.changed_properties, i);
|
||||
arg::RefArg::append(&self.invalidated_properties, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopDBusPropertiesPropertiesChanged {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self {
|
||||
interface_name: i.read()?,
|
||||
changed_properties: i.read()?,
|
||||
invalidated_properties: i.read()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopDBusPropertiesPropertiesChanged {
|
||||
const NAME: &'static str = "PropertiesChanged";
|
||||
const INTERFACE: &'static str = "org.freedesktop.DBus.Properties";
|
||||
}
|
||||
|
||||
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopDBusProperties
|
||||
for blocking::Proxy<'a, C>
|
||||
{
|
||||
fn get<R0: for<'b> arg::Get<'b> + 'static>(
|
||||
&self,
|
||||
interface_name: &str,
|
||||
property_name: &str,
|
||||
) -> Result<R0, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"Get",
|
||||
(interface_name, property_name),
|
||||
)
|
||||
.and_then(|r: (arg::Variant<R0>,)| Ok((r.0).0))
|
||||
}
|
||||
|
||||
fn get_all(&self, interface_name: &str) -> Result<arg::PropMap, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"GetAll",
|
||||
(interface_name,),
|
||||
)
|
||||
.and_then(|r: (arg::PropMap,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn set<I2: arg::Arg + arg::Append>(
|
||||
&self,
|
||||
interface_name: &str,
|
||||
property_name: &str,
|
||||
value: I2,
|
||||
) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"Set",
|
||||
(interface_name, property_name, arg::Variant(value)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OrgFreedesktopDBusIntrospectable {
|
||||
fn introspect(&self) -> Result<String, dbus::Error>;
|
||||
}
|
||||
|
||||
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>>
|
||||
OrgFreedesktopDBusIntrospectable for blocking::Proxy<'a, C>
|
||||
{
|
||||
fn introspect(&self) -> Result<String, dbus::Error> {
|
||||
self.method_call("org.freedesktop.DBus.Introspectable", "Introspect", ())
|
||||
.and_then(|r: (String,)| Ok(r.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OrgFreedesktopDBusPeer {
|
||||
fn ping(&self) -> Result<(), dbus::Error>;
|
||||
fn get_machine_id(&self) -> Result<String, dbus::Error>;
|
||||
}
|
||||
|
||||
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopDBusPeer
|
||||
for blocking::Proxy<'a, C>
|
||||
{
|
||||
fn ping(&self) -> Result<(), dbus::Error> {
|
||||
self.method_call("org.freedesktop.DBus.Peer", "Ping", ())
|
||||
}
|
||||
|
||||
fn get_machine_id(&self) -> Result<String, dbus::Error> {
|
||||
self.method_call("org.freedesktop.DBus.Peer", "GetMachineId", ())
|
||||
.and_then(|r: (String,)| Ok(r.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OrgFreedesktopNotifications {
|
||||
fn set_noti_window_visibility(&self, value: bool) -> Result<(), dbus::Error>;
|
||||
fn toggle_dnd(&self) -> Result<bool, dbus::Error>;
|
||||
fn set_dnd(&self, state: bool) -> Result<(), dbus::Error>;
|
||||
fn get_dnd(&self) -> Result<bool, dbus::Error>;
|
||||
fn manually_close_notification(&self, id: u32, timeout: bool) -> Result<(), dbus::Error>;
|
||||
fn close_all_notifications(&self) -> Result<(), dbus::Error>;
|
||||
fn hide_latest_notification(&self, close: bool) -> Result<(), dbus::Error>;
|
||||
fn get_capabilities(&self) -> Result<Vec<String>, dbus::Error>;
|
||||
fn notify(
|
||||
&self,
|
||||
app_name: &str,
|
||||
replaces_id: u32,
|
||||
app_icon: &str,
|
||||
summary: &str,
|
||||
body: &str,
|
||||
actions: Vec<&str>,
|
||||
hints: arg::PropMap,
|
||||
expire_timeout: i32,
|
||||
) -> Result<u32, dbus::Error>;
|
||||
fn close_notification(&self, id: u32) -> Result<(), dbus::Error>;
|
||||
fn get_server_information(&self) -> Result<(String, String, String, String), dbus::Error>;
|
||||
fn dnd(&self) -> Result<bool, dbus::Error>;
|
||||
fn set_dnd_(&self, value: bool) -> Result<(), dbus::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopNotificationsOnDndToggle {
|
||||
pub dnd: bool,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopNotificationsOnDndToggle {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.dnd, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopNotificationsOnDndToggle {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self { dnd: i.read()? })
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsOnDndToggle {
|
||||
const NAME: &'static str = "OnDndToggle";
|
||||
const INTERFACE: &'static str = "org.freedesktop.Notifications";
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopNotificationsNotificationClosed {
|
||||
pub id: u32,
|
||||
pub reason: u32,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopNotificationsNotificationClosed {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.id, i);
|
||||
arg::RefArg::append(&self.reason, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopNotificationsNotificationClosed {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self {
|
||||
id: i.read()?,
|
||||
reason: i.read()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsNotificationClosed {
|
||||
const NAME: &'static str = "NotificationClosed";
|
||||
const INTERFACE: &'static str = "org.freedesktop.Notifications";
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopNotificationsActionInvoked {
|
||||
pub id: u32,
|
||||
pub action_key: String,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopNotificationsActionInvoked {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.id, i);
|
||||
arg::RefArg::append(&self.action_key, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopNotificationsActionInvoked {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self {
|
||||
id: i.read()?,
|
||||
action_key: i.read()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsActionInvoked {
|
||||
const NAME: &'static str = "ActionInvoked";
|
||||
const INTERFACE: &'static str = "org.freedesktop.Notifications";
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OrgFreedesktopNotificationsNotificationReplied {
|
||||
pub id: u32,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl arg::AppendAll for OrgFreedesktopNotificationsNotificationReplied {
|
||||
fn append(&self, i: &mut arg::IterAppend) {
|
||||
arg::RefArg::append(&self.id, i);
|
||||
arg::RefArg::append(&self.text, i);
|
||||
}
|
||||
}
|
||||
|
||||
impl arg::ReadAll for OrgFreedesktopNotificationsNotificationReplied {
|
||||
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
|
||||
Ok(Self {
|
||||
id: i.read()?,
|
||||
text: i.read()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsNotificationReplied {
|
||||
const NAME: &'static str = "NotificationReplied";
|
||||
const INTERFACE: &'static str = "org.freedesktop.Notifications";
|
||||
}
|
||||
|
||||
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopNotifications
|
||||
for blocking::Proxy<'a, C>
|
||||
{
|
||||
fn set_noti_window_visibility(&self, value: bool) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.Notifications",
|
||||
"SetNotiWindowVisibility",
|
||||
(value,),
|
||||
)
|
||||
}
|
||||
|
||||
fn toggle_dnd(&self) -> Result<bool, dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "ToggleDnd", ())
|
||||
.and_then(|r: (bool,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn set_dnd(&self, state: bool) -> Result<(), dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "SetDnd", (state,))
|
||||
}
|
||||
|
||||
fn get_dnd(&self) -> Result<bool, dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "GetDnd", ())
|
||||
.and_then(|r: (bool,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn manually_close_notification(&self, id: u32, timeout: bool) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.Notifications",
|
||||
"ManuallyCloseNotification",
|
||||
(id, timeout),
|
||||
)
|
||||
}
|
||||
|
||||
fn close_all_notifications(&self) -> Result<(), dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "CloseAllNotifications", ())
|
||||
}
|
||||
|
||||
fn hide_latest_notification(&self, close: bool) -> Result<(), dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.Notifications",
|
||||
"HideLatestNotification",
|
||||
(close,),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_capabilities(&self) -> Result<Vec<String>, dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "GetCapabilities", ())
|
||||
.and_then(|r: (Vec<String>,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn notify(
|
||||
&self,
|
||||
app_name: &str,
|
||||
replaces_id: u32,
|
||||
app_icon: &str,
|
||||
summary: &str,
|
||||
body: &str,
|
||||
actions: Vec<&str>,
|
||||
hints: arg::PropMap,
|
||||
expire_timeout: i32,
|
||||
) -> Result<u32, dbus::Error> {
|
||||
self.method_call(
|
||||
"org.freedesktop.Notifications",
|
||||
"Notify",
|
||||
(
|
||||
app_name,
|
||||
replaces_id,
|
||||
app_icon,
|
||||
summary,
|
||||
body,
|
||||
actions,
|
||||
hints,
|
||||
expire_timeout,
|
||||
),
|
||||
)
|
||||
.and_then(|r: (u32,)| Ok(r.0))
|
||||
}
|
||||
|
||||
fn close_notification(&self, id: u32) -> Result<(), dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "CloseNotification", (id,))
|
||||
}
|
||||
|
||||
fn get_server_information(&self) -> Result<(String, String, String, String), dbus::Error> {
|
||||
self.method_call("org.freedesktop.Notifications", "GetServerInformation", ())
|
||||
}
|
||||
|
||||
fn dnd(&self) -> Result<bool, dbus::Error> {
|
||||
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(
|
||||
self,
|
||||
"org.freedesktop.Notifications",
|
||||
"Dnd",
|
||||
)
|
||||
}
|
||||
|
||||
fn set_dnd_(&self, value: bool) -> Result<(), dbus::Error> {
|
||||
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::set(
|
||||
self,
|
||||
"org.freedesktop.Notifications",
|
||||
"Dnd",
|
||||
value,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ pub trait Affine3AConvert {
|
||||
|
||||
impl Affine3AConvert for Matrix3x4 {
|
||||
fn from_affine(affine: &Affine3A) -> Self {
|
||||
Matrix3x4([
|
||||
Self([
|
||||
[
|
||||
affine.matrix3.x_axis.x,
|
||||
affine.matrix3.y_axis.x,
|
||||
@@ -47,7 +47,7 @@ impl Affine3AConvert for Matrix3x4 {
|
||||
|
||||
impl Affine3AConvert for HmdMatrix34_t {
|
||||
fn from_affine(affine: &Affine3A) -> Self {
|
||||
HmdMatrix34_t {
|
||||
Self {
|
||||
m: [
|
||||
[
|
||||
affine.matrix3.x_axis.x,
|
||||
@@ -89,22 +89,21 @@ pub(super) enum OVRError {
|
||||
|
||||
impl From<ovr_overlay::errors::EVRInputError> for OVRError {
|
||||
fn from(e: ovr_overlay::errors::EVRInputError) -> Self {
|
||||
OVRError::InputError(e.description())
|
||||
Self::InputError(e.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OVRError> for BackendError {
|
||||
fn from(e: OVRError) -> Self {
|
||||
BackendError::Fatal(anyhow::Error::new(e))
|
||||
Self::Fatal(anyhow::Error::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
use cstr::cstr;
|
||||
const STEAMVR_SECTION: &CStr = cstr!("steamvr");
|
||||
const STEAMVR_SECTION: &CStr = c"steamvr";
|
||||
const COLOR_GAIN_CSTR: [&CStr; 3] = [
|
||||
cstr!("hmdDisplayColorGainR"),
|
||||
cstr!("hmdDisplayColorGainG"),
|
||||
cstr!("hmdDisplayColorGainB"),
|
||||
c"hmdDisplayColorGainR",
|
||||
c"hmdDisplayColorGainG",
|
||||
c"hmdDisplayColorGainB",
|
||||
];
|
||||
|
||||
pub(super) fn adjust_gain(
|
||||
|
||||
+30
-19
@@ -13,7 +13,7 @@ use ovr_overlay::{
|
||||
|
||||
use crate::{
|
||||
backend::input::{Haptics, TrackedDevice, TrackedDeviceRole},
|
||||
config_io::CONFIG_ROOT_PATH,
|
||||
config_io,
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
@@ -30,16 +30,17 @@ const PATH_HAPTICS: [&str; 2] = [
|
||||
"/actions/default/out/HapticsRight",
|
||||
];
|
||||
|
||||
const PATH_ALT_CLICK: &str = "/actions/default/in/AltClick";
|
||||
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";
|
||||
const PATH_GRAB: &str = "/actions/default/in/Grab";
|
||||
const PATH_MOVE_MOUSE: &str = "/actions/default/in/MoveMouse";
|
||||
const PATH_SCROLL: &str = "/actions/default/in/Scroll";
|
||||
const PATH_ALT_CLICK: &str = "/actions/default/in/AltClick";
|
||||
const PATH_SHOW_HIDE: &str = "/actions/default/in/ShowHide";
|
||||
const PATH_SPACE_DRAG: &str = "/actions/default/in/SpaceDrag";
|
||||
const PATH_SPACE_ROTATE: &str = "/actions/default/in/SpaceRotate";
|
||||
const PATH_CLICK_MODIFIER_RIGHT: &str = "/actions/default/in/ClickModifierRight";
|
||||
const PATH_CLICK_MODIFIER_MIDDLE: &str = "/actions/default/in/ClickModifierMiddle";
|
||||
const PATH_MOVE_MOUSE: &str = "/actions/default/in/MoveMouse";
|
||||
const PATH_TOGGLE_DASHBOARD: &str = "/actions/default/in/ToggleDashboard";
|
||||
|
||||
const INPUT_ANY: InputValueHandle = InputValueHandle(ovr_overlay::sys::k_ulInvalidInputValueHandle);
|
||||
|
||||
@@ -51,6 +52,7 @@ pub(super) struct OpenVrInputSource {
|
||||
scroll_hnd: ActionHandle,
|
||||
alt_click_hnd: ActionHandle,
|
||||
show_hide_hnd: ActionHandle,
|
||||
toggle_dashboard_hnd: ActionHandle,
|
||||
space_drag_hnd: ActionHandle,
|
||||
space_rotate_hnd: ActionHandle,
|
||||
click_modifier_right_hnd: ActionHandle,
|
||||
@@ -75,6 +77,7 @@ impl OpenVrInputSource {
|
||||
let scroll_hnd = input.get_action_handle(PATH_SCROLL)?;
|
||||
let alt_click_hnd = input.get_action_handle(PATH_ALT_CLICK)?;
|
||||
let show_hide_hnd = input.get_action_handle(PATH_SHOW_HIDE)?;
|
||||
let toggle_dashboard_hnd = input.get_action_handle(PATH_TOGGLE_DASHBOARD)?;
|
||||
let space_drag_hnd = input.get_action_handle(PATH_SPACE_DRAG)?;
|
||||
let space_rotate_hnd = input.get_action_handle(PATH_SPACE_ROTATE)?;
|
||||
let click_modifier_right_hnd = input.get_action_handle(PATH_CLICK_MODIFIER_RIGHT)?;
|
||||
@@ -104,26 +107,27 @@ impl OpenVrInputSource {
|
||||
haptics_hnd: haptics_hnd[i],
|
||||
});
|
||||
|
||||
Ok(OpenVrInputSource {
|
||||
Ok(Self {
|
||||
hands,
|
||||
set_hnd,
|
||||
click_hnd,
|
||||
grab_hnd,
|
||||
scroll_hnd,
|
||||
alt_click_hnd,
|
||||
show_hide_hnd,
|
||||
toggle_dashboard_hnd,
|
||||
space_drag_hnd,
|
||||
space_rotate_hnd,
|
||||
click_modifier_right_hnd,
|
||||
click_modifier_middle_hnd,
|
||||
move_mouse_hnd,
|
||||
hands,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn haptics(&mut self, input: &mut InputManager, hand: usize, haptics: &Haptics) {
|
||||
let hnd = self.hands[hand].haptics_hnd;
|
||||
let action_handle = self.hands[hand].haptics_hnd;
|
||||
let _ = input.trigger_haptic_vibration_action(
|
||||
hnd,
|
||||
action_handle,
|
||||
0.0,
|
||||
Duration::from_secs_f32(haptics.duration),
|
||||
haptics.frequency,
|
||||
@@ -196,6 +200,11 @@ impl OpenVrInputSource {
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
|
||||
app_hand.now.toggle_dashboard = input
|
||||
.get_digital_action_data(self.toggle_dashboard_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
|
||||
app_hand.now.space_drag = input
|
||||
.get_digital_action_data(self.space_drag_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
@@ -221,10 +230,12 @@ impl OpenVrInputSource {
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
|
||||
app_hand.now.scroll = input
|
||||
let scroll = input
|
||||
.get_analog_action_data(self.scroll_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.y)
|
||||
.unwrap_or(0.0);
|
||||
.map(|x| (x.0.x, x.0.y))
|
||||
.unwrap_or((0.0, 0.0));
|
||||
app_hand.now.scroll_x = scroll.0;
|
||||
app_hand.now.scroll_y = scroll.1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +277,8 @@ impl OpenVrInputSource {
|
||||
}
|
||||
|
||||
app.input_state.devices.sort_by(|a, b| {
|
||||
(a.soc.is_none() as u8)
|
||||
.cmp(&(b.soc.is_none() as u8))
|
||||
u8::from(a.soc.is_none())
|
||||
.cmp(&u8::from(b.soc.is_none()))
|
||||
.then((a.role as u8).cmp(&(b.role as u8)))
|
||||
.then(a.soc.unwrap_or(999.).total_cmp(&b.soc.unwrap_or(999.)))
|
||||
});
|
||||
@@ -316,27 +327,27 @@ fn get_tracked_device(
|
||||
}
|
||||
|
||||
pub fn set_action_manifest(input: &mut InputManager) -> anyhow::Result<()> {
|
||||
let action_path = CONFIG_ROOT_PATH.join("actions.json");
|
||||
let action_path = config_io::get_config_root().join("actions.json");
|
||||
|
||||
if let Err(e) = File::create(&action_path)
|
||||
.and_then(|mut f| f.write_all(include_bytes!("../../res/actions.json")))
|
||||
{
|
||||
log::warn!("Could not write action manifest: {}", e);
|
||||
log::warn!("Could not write action manifest: {e}");
|
||||
}
|
||||
|
||||
let binding_path = CONFIG_ROOT_PATH.join("actions_binding_knuckles.json");
|
||||
let binding_path = config_io::get_config_root().join("actions_binding_knuckles.json");
|
||||
if !binding_path.is_file() {
|
||||
File::create(&binding_path)?
|
||||
.write_all(include_bytes!("../../res/actions_binding_knuckles.json"))?;
|
||||
}
|
||||
|
||||
let binding_path = CONFIG_ROOT_PATH.join("actions_binding_vive.json");
|
||||
let binding_path = config_io::get_config_root().join("actions_binding_vive.json");
|
||||
if !binding_path.is_file() {
|
||||
File::create(&binding_path)?
|
||||
.write_all(include_bytes!("../../res/actions_binding_vive.json"))?;
|
||||
}
|
||||
|
||||
let binding_path = CONFIG_ROOT_PATH.join("actions_binding_oculus.json");
|
||||
let binding_path = config_io::get_config_root().join("actions_binding_oculus.json");
|
||||
if !binding_path.is_file() {
|
||||
File::create(&binding_path)?
|
||||
.write_all(include_bytes!("../../res/actions_binding_oculus.json"))?;
|
||||
|
||||
+32
-23
@@ -11,13 +11,16 @@ use vulkano::format::Format;
|
||||
use vulkano::image::view::ImageView;
|
||||
use vulkano::image::ImageLayout;
|
||||
|
||||
use crate::backend::overlay::{OverlayData, OverlayRenderer, OverlayState, SplitOverlayBackend};
|
||||
use crate::graphics::WlxGraphics;
|
||||
use crate::backend::overlay::{
|
||||
FrameMeta, OverlayData, OverlayRenderer, OverlayState, ShouldRender, SplitOverlayBackend,
|
||||
Z_ORDER_LINES,
|
||||
};
|
||||
use crate::graphics::{CommandBuffers, WlxGraphics};
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::overlay::OpenVrOverlayData;
|
||||
|
||||
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
|
||||
static LINE_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
|
||||
|
||||
pub(super) struct LinePool {
|
||||
lines: IdMap<usize, OverlayData<OpenVrOverlayData>>,
|
||||
@@ -27,12 +30,14 @@ pub(super) struct LinePool {
|
||||
|
||||
impl LinePool {
|
||||
pub fn new(graphics: Arc<WlxGraphics>) -> anyhow::Result<Self> {
|
||||
let mut command_buffer =
|
||||
graphics.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
let mut command_buffer = graphics.create_uploads_command_buffer(
|
||||
graphics.transfer_queue.clone(),
|
||||
CommandBufferUsage::OneTimeSubmit,
|
||||
)?;
|
||||
|
||||
let buf = vec![255; 16];
|
||||
|
||||
let texture = command_buffer.texture2d(2, 2, Format::R8G8B8A8_UNORM, &buf)?;
|
||||
let texture = command_buffer.texture2d_raw(2, 2, Format::R8G8B8A8_UNORM, &buf)?;
|
||||
command_buffer.build_and_execute_now()?;
|
||||
|
||||
graphics
|
||||
@@ -45,7 +50,7 @@ impl LinePool {
|
||||
|
||||
let view = ImageView::new_default(texture)?;
|
||||
|
||||
Ok(LinePool {
|
||||
Ok(Self {
|
||||
lines: IdMap::new(),
|
||||
view,
|
||||
colors: [
|
||||
@@ -59,11 +64,11 @@ impl LinePool {
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self) -> usize {
|
||||
let id = AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
|
||||
let id = LINE_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let mut data = OverlayData::<OpenVrOverlayData> {
|
||||
state: OverlayState {
|
||||
name: Arc::from(format!("wlx-line{}", id)),
|
||||
name: Arc::from(format!("wlx-line{id}")),
|
||||
show_hide: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -80,7 +85,7 @@ impl LinePool {
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
data.state.z_order = 69;
|
||||
data.state.z_order = Z_ORDER_LINES;
|
||||
data.state.dirty = true;
|
||||
|
||||
self.lines.insert(id, data);
|
||||
@@ -129,15 +134,7 @@ impl LinePool {
|
||||
data.state.transform = transform;
|
||||
data.data.color = color;
|
||||
} else {
|
||||
log::warn!("Line {} does not exist", id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide(&mut self, id: usize) {
|
||||
if let Some(data) = self.lines.get_mut(id) {
|
||||
data.state.want_visible = false;
|
||||
} else {
|
||||
log::warn!("Line {} does not exist", id);
|
||||
log::warn!("Line {id} does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,10 +180,22 @@ impl OverlayRenderer for StaticRenderer {
|
||||
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn render(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
fn should_render(&mut self, _app: &mut AppState) -> anyhow::Result<ShouldRender> {
|
||||
Ok(ShouldRender::Unable)
|
||||
}
|
||||
fn view(&mut self) -> Option<Arc<ImageView>> {
|
||||
Some(self.view.clone())
|
||||
fn render(
|
||||
&mut self,
|
||||
_app: &mut AppState,
|
||||
_tgt: Arc<ImageView>,
|
||||
_buf: &mut CommandBuffers,
|
||||
_alpha: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
Some(FrameMeta {
|
||||
extent: self.view.image().extent(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ use anyhow::bail;
|
||||
use json::{array, object};
|
||||
use ovr_overlay::applications::ApplicationsManager;
|
||||
|
||||
use crate::config_io::CONFIG_ROOT_PATH;
|
||||
use crate::config_io;
|
||||
|
||||
const APP_KEY: &str = "galister.wlxoverlay-s";
|
||||
|
||||
pub(super) fn install_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Result<()> {
|
||||
let manifest_path = CONFIG_ROOT_PATH.join("wlx-overlay-s.vrmanifest");
|
||||
let manifest_path = config_io::get_config_root().join("wlx-overlay-s.vrmanifest");
|
||||
|
||||
let appimage_path = std::env::var("APPIMAGE");
|
||||
let executable_pathbuf = std::env::current_exe()?;
|
||||
@@ -21,7 +21,7 @@ pub(super) fn install_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Res
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid executable path"))?,
|
||||
};
|
||||
|
||||
if let Ok(true) = app_mgr.is_application_installed(APP_KEY) {
|
||||
if app_mgr.is_application_installed(APP_KEY) == Ok(true) {
|
||||
if let Ok(mut file) = File::open(&manifest_path) {
|
||||
let mut buf = String::new();
|
||||
if file.read_to_string(&mut buf).is_ok() {
|
||||
@@ -62,26 +62,26 @@ pub(super) fn install_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Res
|
||||
manifest_path,
|
||||
e
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(e) = app_mgr.add_application_manifest(&manifest_path, false) {
|
||||
bail!("Failed to add manifest to OpenVR: {}", e.description());
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(e) = app_mgr.set_application_auto_launch(APP_KEY, true) {
|
||||
bail!("Failed to set auto launch: {}", e.description());
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn uninstall_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Result<()> {
|
||||
let manifest_path = CONFIG_ROOT_PATH.join("wlx-overlay-s.vrmanifest");
|
||||
let manifest_path = config_io::get_config_root().join("wlx-overlay-s.vrmanifest");
|
||||
|
||||
if let Ok(true) = app_mgr.is_application_installed(APP_KEY) {
|
||||
if app_mgr.is_application_installed(APP_KEY) == Ok(true) {
|
||||
if let Err(e) = app_mgr.remove_application_manifest(&manifest_path) {
|
||||
bail!("Failed to remove manifest from OpenVR: {}", e.description());
|
||||
};
|
||||
}
|
||||
log::info!("Uninstalled manifest");
|
||||
}
|
||||
Ok(())
|
||||
|
||||
+78
-28
@@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
ops::Add,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
@@ -12,11 +13,7 @@ use ovr_overlay::{
|
||||
sys::{ETrackedDeviceProperty, EVRApplicationType, EVREventType},
|
||||
TrackedDeviceIndex,
|
||||
};
|
||||
use vulkano::{
|
||||
device::{physical::PhysicalDevice, DeviceExtensions},
|
||||
instance::InstanceExtensions,
|
||||
Handle, VulkanObject,
|
||||
};
|
||||
use vulkano::{device::physical::PhysicalDevice, Handle, VulkanObject};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
@@ -30,10 +27,10 @@ use crate::{
|
||||
manifest::{install_manifest, uninstall_manifest},
|
||||
overlay::OpenVrOverlayData,
|
||||
},
|
||||
overlay::OverlayData,
|
||||
overlay::{OverlayData, ShouldRender},
|
||||
task::{SystemTask, TaskType},
|
||||
},
|
||||
graphics::WlxGraphics,
|
||||
graphics::{CommandBuffers, WlxGraphics},
|
||||
overlays::{
|
||||
toast::{Toast, ToastTopic},
|
||||
watch::{watch_fade, WATCH_NAME},
|
||||
@@ -41,6 +38,9 @@ use crate::{
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
use crate::{gui::modular::button::WayVRAction, overlays::wayvr::wayvr_action};
|
||||
|
||||
pub mod helpers;
|
||||
pub mod input;
|
||||
pub mod lines;
|
||||
@@ -61,7 +61,8 @@ pub fn openvr_uninstall() {
|
||||
let _ = uninstall_manifest(&mut app_mgr);
|
||||
}
|
||||
|
||||
pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
pub fn openvr_run(running: Arc<AtomicBool>, show_by_default: bool) -> Result<(), BackendError> {
|
||||
let app_type = EVRApplicationType::VRApplication_Overlay;
|
||||
let Ok(context) = ovr_overlay::Context::init(app_type) else {
|
||||
log::warn!("Will not use OpenVR: Context init failed");
|
||||
@@ -80,14 +81,13 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
|
||||
let device_extensions_fn = |device: &PhysicalDevice| {
|
||||
let names = compositor_mgr.get_vulkan_device_extensions_required(device.handle().as_raw());
|
||||
let ext = DeviceExtensions::from_iter(names.iter().map(|s| s.as_str()));
|
||||
ext
|
||||
names.iter().map(std::string::String::as_str).collect()
|
||||
};
|
||||
|
||||
let mut compositor_mngr = context.compositor_mngr();
|
||||
let mut compositor_mgr = context.compositor_mngr();
|
||||
let instance_extensions = {
|
||||
let names = compositor_mngr.get_vulkan_instance_extensions_required();
|
||||
InstanceExtensions::from_iter(names.iter().map(|s| s.as_str()))
|
||||
let names = compositor_mgr.get_vulkan_instance_extensions_required();
|
||||
names.iter().map(std::string::String::as_str).collect()
|
||||
};
|
||||
|
||||
let mut state = {
|
||||
@@ -95,6 +95,13 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
AppState::from_graphics(graphics)?
|
||||
};
|
||||
|
||||
if show_by_default {
|
||||
state.tasks.enqueue_at(
|
||||
TaskType::System(SystemTask::ShowHide),
|
||||
Instant::now().add(Duration::from_secs(1)),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(ipd) = system_mgr.get_tracked_device_property::<f32>(
|
||||
TrackedDeviceIndex::HMD,
|
||||
ETrackedDeviceProperty::Prop_UserIpdMeters_Float,
|
||||
@@ -113,10 +120,6 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
let mut playspace = playspace::PlayspaceMover::new();
|
||||
playspace.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
let mut osc_sender =
|
||||
crate::backend::osc::OscSender::new(state.session.config.osc_out_port).ok();
|
||||
|
||||
set_action_manifest(&mut input_mgr)?;
|
||||
|
||||
let mut input_source = OpenVrInputSource::new(&mut input_mgr)?;
|
||||
@@ -130,7 +133,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
)));
|
||||
};
|
||||
|
||||
log::info!("HMD running @ {} Hz", refresh_rate);
|
||||
log::info!("HMD running @ {refresh_rate} Hz");
|
||||
|
||||
let watch_id = overlays.get_by_name(WATCH_NAME).unwrap().state.id; // want panic
|
||||
|
||||
@@ -181,7 +184,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
Toast::new(
|
||||
ToastTopic::IpdChange,
|
||||
"IPD".into(),
|
||||
format!("{:.1} mm", ipd).into(),
|
||||
format!("{ipd:.1} mm").into(),
|
||||
)
|
||||
.submit(&mut state);
|
||||
}
|
||||
@@ -202,18 +205,17 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
state.tasks.retrieve_due(&mut due_tasks);
|
||||
|
||||
let mut removed_overlays = overlays.update(&mut state)?;
|
||||
for o in removed_overlays.iter_mut() {
|
||||
for o in &mut removed_overlays {
|
||||
o.destroy(&mut overlay_mgr);
|
||||
}
|
||||
|
||||
while let Some(task) = due_tasks.pop_front() {
|
||||
match task {
|
||||
TaskType::Global(f) => f(&mut state),
|
||||
TaskType::Overlay(sel, f) => {
|
||||
if let Some(o) = overlays.mut_by_selector(&sel) {
|
||||
f(&mut state, &mut o.state);
|
||||
} else {
|
||||
log::warn!("Overlay not found for task: {:?}", sel);
|
||||
log::warn!("Overlay not found for task: {sel:?}");
|
||||
}
|
||||
}
|
||||
TaskType::CreateOverlay(sel, f) => {
|
||||
@@ -250,7 +252,14 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
SystemTask::ResetPlayspace => {
|
||||
playspace.reset_offset(&mut chaperone_mgr, &state.input_state);
|
||||
}
|
||||
SystemTask::ShowHide => {
|
||||
overlays.show_hide(&mut state);
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "wayvr")]
|
||||
TaskType::WayVR(action) => {
|
||||
wayvr_action(&mut state, &mut overlays, &action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +272,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
&mut system_mgr,
|
||||
&mut state,
|
||||
);
|
||||
state.input_state.post_update();
|
||||
state.input_state.post_update(&state.session);
|
||||
|
||||
if state
|
||||
.input_state
|
||||
@@ -275,6 +284,16 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
overlays.show_hide(&mut state);
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
if state
|
||||
.input_state
|
||||
.pointers
|
||||
.iter()
|
||||
.any(|p| p.now.toggle_dashboard && !p.before.toggle_dashboard)
|
||||
{
|
||||
wayvr_action(&mut state, &mut overlays, &WayVRAction::ToggleDashboard);
|
||||
}
|
||||
|
||||
overlays
|
||||
.iter_mut()
|
||||
.for_each(|o| o.state.auto_movement(&mut state));
|
||||
@@ -292,11 +311,12 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
&state.input_state.hmd,
|
||||
);
|
||||
if let Some(haptics) = haptics {
|
||||
input_source.haptics(&mut input_mgr, idx, haptics)
|
||||
input_source.haptics(&mut input_mgr, idx, haptics);
|
||||
}
|
||||
}
|
||||
|
||||
state.hid_provider.commit();
|
||||
let mut buffers = CommandBuffers::default();
|
||||
|
||||
lines.update(universe.clone(), &mut overlay_mgr, &mut state)?;
|
||||
|
||||
@@ -305,24 +325,54 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
if let Some(ref mut sender) = osc_sender {
|
||||
let _ = sender.send_params(&overlays);
|
||||
};
|
||||
if let Some(ref mut sender) = state.osc_sender {
|
||||
let _ = sender.send_params(&overlays, &state.input_state.devices);
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
if let Err(e) =
|
||||
crate::overlays::wayvr::tick_events::<OpenVrOverlayData>(&mut state, &mut overlays)
|
||||
{
|
||||
log::error!("WayVR tick_events failed: {e:?}");
|
||||
}
|
||||
|
||||
log::trace!("Rendering frame");
|
||||
|
||||
for o in overlays.iter_mut() {
|
||||
if o.state.want_visible {
|
||||
o.render(&mut state)?;
|
||||
let ShouldRender::Should = o.should_render(&mut state)? else {
|
||||
continue;
|
||||
};
|
||||
if !o.ensure_image_allocated(&mut state)? {
|
||||
continue;
|
||||
}
|
||||
o.data.image_dirty = o.render(
|
||||
&mut state,
|
||||
o.data.image_view.as_ref().unwrap().clone(),
|
||||
&mut buffers,
|
||||
1.0, // alpha is instead set using OVR API
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("Rendering overlays");
|
||||
|
||||
if let Some(mut future) = buffers.execute_now(state.graphics.graphics_queue.clone())? {
|
||||
if let Err(e) = future.flush() {
|
||||
return Err(BackendError::Fatal(e.into()));
|
||||
}
|
||||
future.cleanup_finished();
|
||||
}
|
||||
|
||||
overlays
|
||||
.iter_mut()
|
||||
.for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &state.graphics));
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
if let Some(wayvr) = &state.wayvr {
|
||||
wayvr.borrow_mut().data.tick_finish()?;
|
||||
}
|
||||
|
||||
// chaperone
|
||||
|
||||
// close font handles?
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use core::f32;
|
||||
use std::sync::Arc;
|
||||
|
||||
use glam::Vec4;
|
||||
use ovr_overlay::{
|
||||
@@ -6,25 +7,21 @@ use ovr_overlay::{
|
||||
pose::Matrix3x4,
|
||||
sys::{ETrackingUniverseOrigin, VRVulkanTextureData_t},
|
||||
};
|
||||
use vulkano::{Handle, VulkanObject};
|
||||
use vulkano::{image::view::ImageView, Handle, VulkanObject};
|
||||
|
||||
use crate::{
|
||||
backend::overlay::{OverlayData, RelativeTo},
|
||||
graphics::WlxGraphics,
|
||||
state::AppState,
|
||||
};
|
||||
use crate::{backend::overlay::OverlayData, graphics::WlxGraphics, state::AppState};
|
||||
|
||||
use super::helpers::Affine3AConvert;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct OpenVrOverlayData {
|
||||
pub(super) handle: Option<OverlayHandle>,
|
||||
pub(super) last_image: Option<u64>,
|
||||
pub(super) visible: bool,
|
||||
pub(super) color: Vec4,
|
||||
pub(crate) width: f32,
|
||||
pub(super) override_width: bool,
|
||||
pub(super) relative_to: RelativeTo,
|
||||
pub(super) image_view: Option<Arc<ImageView>>,
|
||||
pub(super) image_dirty: bool,
|
||||
}
|
||||
|
||||
impl OverlayData<OpenVrOverlayData> {
|
||||
@@ -38,7 +35,7 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
let handle = match overlay.create_overlay(&key, &key) {
|
||||
Ok(handle) => handle,
|
||||
Err(e) => {
|
||||
panic!("Failed to create overlay: {}", e);
|
||||
panic!("Failed to create overlay: {e}");
|
||||
}
|
||||
};
|
||||
log::debug!("{}: initialize", self.state.name);
|
||||
@@ -61,6 +58,20 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
pub(super) fn ensure_image_allocated(&mut self, app: &mut AppState) -> anyhow::Result<bool> {
|
||||
if self.data.image_view.is_some() {
|
||||
return Ok(true);
|
||||
}
|
||||
let Some(meta) = self.backend.frame_meta() else {
|
||||
return Ok(false);
|
||||
};
|
||||
let image = app
|
||||
.graphics
|
||||
.render_texture(meta.extent[0], meta.extent[1], meta.format)?;
|
||||
self.data.image_view = Some(ImageView::new_default(image)?);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(super) fn after_input(
|
||||
&mut self,
|
||||
overlay: &mut OverlayManager,
|
||||
@@ -188,7 +199,7 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
}
|
||||
|
||||
pub(super) fn upload_transform(
|
||||
&self,
|
||||
&mut self,
|
||||
universe: ETrackingUniverseOrigin,
|
||||
overlay: &mut OverlayManager,
|
||||
) {
|
||||
@@ -197,7 +208,14 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
return;
|
||||
};
|
||||
|
||||
let transform = Matrix3x4::from_affine(&self.state.transform);
|
||||
let effective = self.state.transform
|
||||
* self
|
||||
.backend
|
||||
.frame_meta()
|
||||
.map(|f| f.transform)
|
||||
.unwrap_or_default();
|
||||
|
||||
let transform = Matrix3x4::from_affine(&effective);
|
||||
|
||||
if let Err(e) = overlay.set_transform_absolute(handle, universe, &transform) {
|
||||
log::error!(
|
||||
@@ -214,21 +232,17 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(view) = self.backend.view() else {
|
||||
let Some(view) = self.data.image_view.as_ref() else {
|
||||
log::debug!("{}: Not rendered", self.state.name);
|
||||
return;
|
||||
};
|
||||
|
||||
let image = view.image().clone();
|
||||
|
||||
let raw_image = image.handle().as_raw();
|
||||
|
||||
if let Some(last_image) = self.data.last_image {
|
||||
if last_image == raw_image {
|
||||
return;
|
||||
}
|
||||
if !self.data.image_dirty {
|
||||
return;
|
||||
}
|
||||
self.data.image_dirty = false;
|
||||
|
||||
let image = view.image().clone();
|
||||
let dimensions = image.extent();
|
||||
if !self.data.override_width {
|
||||
let new_width = ((dimensions[0] as f32) / (dimensions[1] as f32)).min(1.0);
|
||||
@@ -239,6 +253,7 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
}
|
||||
}
|
||||
|
||||
let raw_image = image.handle().as_raw();
|
||||
let format = image.format();
|
||||
|
||||
let mut texture = VRVulkanTextureData_t {
|
||||
@@ -250,8 +265,8 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
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.handle().as_raw() as *mut _,
|
||||
m_nQueueFamilyIndex: graphics.queue.queue_family_index(),
|
||||
m_pQueue: graphics.graphics_queue.handle().as_raw() as *mut _,
|
||||
m_nQueueFamilyIndex: graphics.graphics_queue.queue_family_index(),
|
||||
};
|
||||
log::trace!(
|
||||
"{}: UploadTex {:?}, {}x{}, {:?}",
|
||||
|
||||
@@ -25,7 +25,7 @@ pub(super) struct PlayspaceMover {
|
||||
}
|
||||
|
||||
impl PlayspaceMover {
|
||||
pub fn new() -> Self {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
universe: ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated,
|
||||
drag: None,
|
||||
@@ -33,6 +33,7 @@ impl PlayspaceMover {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
pub fn update(
|
||||
&mut self,
|
||||
chaperone_mgr: &mut ChaperoneSetupManager,
|
||||
@@ -54,8 +55,8 @@ impl PlayspaceMover {
|
||||
|
||||
let dq = new_hand * data.hand_pose.conjugate();
|
||||
let rel_y = f32::atan2(
|
||||
2.0 * (dq.y * dq.w + dq.x * dq.z),
|
||||
(2.0 * (dq.w * dq.w + dq.x * dq.x)) - 1.0,
|
||||
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),
|
||||
);
|
||||
|
||||
let mut space_transform = Affine3A::from_rotation_y(rel_y);
|
||||
@@ -114,7 +115,8 @@ impl PlayspaceMover {
|
||||
let new_hand = data
|
||||
.pose
|
||||
.transform_point3a(state.input_state.pointers[data.hand].raw_pose.translation);
|
||||
let relative_pos = new_hand - data.hand_pose;
|
||||
let relative_pos =
|
||||
(new_hand - data.hand_pose) * state.session.config.space_drag_multiplier;
|
||||
|
||||
if relative_pos.length_squared() > 1000.0 {
|
||||
log::warn!("Space drag too fast, ignoring");
|
||||
@@ -199,6 +201,15 @@ impl PlayspaceMover {
|
||||
|
||||
set_working_copy(&self.universe, chaperone_mgr, &mat);
|
||||
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
|
||||
|
||||
if self.drag.is_some() {
|
||||
log::info!("Space drag interrupted by fix floor");
|
||||
self.drag = None;
|
||||
}
|
||||
if self.rotate.is_some() {
|
||||
log::info!("Space rotate interrupted by fix floor");
|
||||
self.rotate = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn playspace_changed(
|
||||
@@ -231,7 +242,7 @@ impl PlayspaceMover {
|
||||
}
|
||||
}
|
||||
|
||||
fn universe_str(universe: &ETrackingUniverseOrigin) -> &'static str {
|
||||
const fn universe_str(universe: &ETrackingUniverseOrigin) -> &'static str {
|
||||
match universe {
|
||||
ETrackingUniverseOrigin::TrackingUniverseSeated => "Seated",
|
||||
ETrackingUniverseOrigin::TrackingUniverseStanding => "Standing",
|
||||
@@ -261,31 +272,31 @@ fn set_working_copy(
|
||||
let mat = HmdMatrix34_t::from_affine(mat);
|
||||
match universe {
|
||||
ETrackingUniverseOrigin::TrackingUniverseStanding => {
|
||||
chaperone_mgr.set_working_standing_zero_pose_to_raw_tracking_pose(&mat)
|
||||
chaperone_mgr.set_working_standing_zero_pose_to_raw_tracking_pose(&mat);
|
||||
}
|
||||
_ => chaperone_mgr.set_working_seated_zero_pose_to_raw_tracking_pose(&mat),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_chaperone_offset(offset: Vec3A, chaperone_mgr: &mut ChaperoneSetupManager) {
|
||||
let mut quads = chaperone_mgr.get_live_collision_bounds_info();
|
||||
quads.iter_mut().for_each(|quad| {
|
||||
for quad in &mut quads {
|
||||
quad.vCorners.iter_mut().for_each(|corner| {
|
||||
corner.v[0] += offset.x;
|
||||
corner.v[2] += offset.z;
|
||||
});
|
||||
});
|
||||
}
|
||||
chaperone_mgr.set_working_collision_bounds_info(quads.as_mut_slice());
|
||||
}
|
||||
|
||||
fn apply_chaperone_transform(transform: Affine3A, chaperone_mgr: &mut ChaperoneSetupManager) {
|
||||
let mut quads = chaperone_mgr.get_live_collision_bounds_info();
|
||||
quads.iter_mut().for_each(|quad| {
|
||||
for quad in &mut quads {
|
||||
quad.vCorners.iter_mut().for_each(|corner| {
|
||||
let coord = transform.transform_point3a(Vec3A::from_slice(&corner.v));
|
||||
corner.v[0] = coord.x;
|
||||
corner.v[2] = coord.z;
|
||||
});
|
||||
});
|
||||
}
|
||||
chaperone_mgr.set_working_collision_bounds_info(quads.as_mut_slice());
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use glam::{Affine3A, Quat, Vec3, Vec3A};
|
||||
use openxr as xr;
|
||||
use sysinfo::Process;
|
||||
use openxr::{self as xr, SessionCreateFlags};
|
||||
use xr::OverlaySessionCreateFlagsEXTX;
|
||||
|
||||
pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
@@ -35,6 +32,17 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
} else {
|
||||
log::warn!("Missing EXT_hp_mixed_reality_controller extension.");
|
||||
}
|
||||
if available_extensions.khr_composition_layer_cylinder {
|
||||
enabled_extensions.khr_composition_layer_cylinder = true;
|
||||
} else {
|
||||
log::warn!("Missing EXT_composition_layer_cylinder extension.");
|
||||
}
|
||||
|
||||
if available_extensions.khr_composition_layer_equirect2 {
|
||||
enabled_extensions.khr_composition_layer_equirect2 = true;
|
||||
} else {
|
||||
log::warn!("Missing EXT_composition_layer_equirect2 extension.");
|
||||
}
|
||||
|
||||
//#[cfg(not(debug_assertions))]
|
||||
let layers = [];
|
||||
@@ -102,7 +110,7 @@ pub(super) unsafe fn create_overlay_session(
|
||||
};
|
||||
let binding = xr::sys::GraphicsBindingVulkanKHR {
|
||||
ty: xr::sys::GraphicsBindingVulkanKHR::TYPE,
|
||||
next: &overlay as *const _ as *const _,
|
||||
next: (&raw const overlay).cast(),
|
||||
instance: info.instance,
|
||||
physical_device: info.physical_device,
|
||||
device: info.device,
|
||||
@@ -111,8 +119,8 @@ pub(super) unsafe fn create_overlay_session(
|
||||
};
|
||||
let info = xr::sys::SessionCreateInfo {
|
||||
ty: xr::sys::SessionCreateInfo::TYPE,
|
||||
next: &binding as *const _ as *const _,
|
||||
create_flags: Default::default(),
|
||||
next: (&raw const binding).cast(),
|
||||
create_flags: SessionCreateFlags::default(),
|
||||
system_id: system,
|
||||
};
|
||||
let mut out = xr::sys::Session::NULL;
|
||||
@@ -124,21 +132,14 @@ pub(super) unsafe fn create_overlay_session(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn hmd_pose_from_views(views: &[xr::View]) -> (Affine3A, f32) {
|
||||
let ipd;
|
||||
let pos = {
|
||||
let pos0: Vec3 = unsafe { std::mem::transmute(views[0].pose.position) };
|
||||
let pos1: Vec3 = unsafe { std::mem::transmute(views[1].pose.position) };
|
||||
ipd = (pos0.distance(pos1) * 1000.0).round() * 0.1;
|
||||
(pos0 + pos1) * 0.5
|
||||
};
|
||||
let rot = {
|
||||
let rot0: Quat = unsafe { std::mem::transmute(views[0].pose.orientation) };
|
||||
let rot1: Quat = unsafe { std::mem::transmute(views[1].pose.orientation) };
|
||||
rot0.lerp(rot1, 0.5)
|
||||
};
|
||||
type Vec3M = mint::Vector3<f32>;
|
||||
type QuatM = mint::Quaternion<f32>;
|
||||
|
||||
(Affine3A::from_rotation_translation(rot, pos), ipd)
|
||||
pub(super) fn ipd_from_views(views: &[xr::View]) -> f32 {
|
||||
let p0: Vec3 = Vec3M::from(views[0].pose.position).into();
|
||||
let p1: Vec3 = Vec3M::from(views[1].pose.position).into();
|
||||
|
||||
(p0.distance(p1) * 10000.0).round() * 0.1
|
||||
}
|
||||
|
||||
pub(super) fn transform_to_norm_quat(transform: &Affine3A) -> Quat {
|
||||
@@ -174,48 +175,8 @@ pub(super) fn transform_to_posef(transform: &Affine3A) -> xr::Posef {
|
||||
translation_rotation_to_posef(translation, rotation)
|
||||
}
|
||||
|
||||
pub(super) fn find_libmonado() -> anyhow::Result<libloading::Library> {
|
||||
//check env var first
|
||||
if let Ok(path) = std::env::var("LIBMONADO_PATH") {
|
||||
let path = PathBuf::from(path);
|
||||
if path.exists() {
|
||||
return Ok(unsafe { libloading::Library::new(path)? });
|
||||
} else {
|
||||
bail!("LIBMONADO_PATH points to a non-existing file.");
|
||||
}
|
||||
}
|
||||
|
||||
const PROC_NAMES: [&str; 2] = ["monado-service", "wivrn-server"];
|
||||
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_processes();
|
||||
for p in system.processes().values() {
|
||||
for proc_name in PROC_NAMES.iter() {
|
||||
if p.name().contains(proc_name) {
|
||||
if let Some(lib) = proc_load_libmonado(p) {
|
||||
return Ok(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("Could not find libmonado.");
|
||||
}
|
||||
|
||||
fn proc_load_libmonado(proc: &Process) -> Option<libloading::Library> {
|
||||
// relative to folder containing binary
|
||||
const SEARCH_PATHS: [&str; 3] = [
|
||||
"../lib/libmonado.so",
|
||||
"../libmonado/libmonado.so",
|
||||
"../_deps/monado-build/src/xrt/targets/libmonado/libmonado.so",
|
||||
];
|
||||
|
||||
for &p in SEARCH_PATHS.iter() {
|
||||
let path = proc.exe()?.parent()?.join(p);
|
||||
|
||||
if path.exists() {
|
||||
return Some(unsafe { libloading::Library::new(path).ok()? });
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
pub(super) fn posef_to_transform(pose: &xr::Posef) -> Affine3A {
|
||||
let rotation = QuatM::from(pose.orientation).into();
|
||||
let translation = Vec3M::from(pose.position).into();
|
||||
Affine3A::from_rotation_translation(rotation, translation)
|
||||
}
|
||||
+381
-231
@@ -1,21 +1,27 @@
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
array::from_fn,
|
||||
mem::transmute,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use glam::{bool, Affine3A, Quat, Vec3};
|
||||
use openxr as xr;
|
||||
use libmonado as mnd;
|
||||
use openxr::{self as xr, Quaternionf, Vector2f, Vector3f};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
backend::input::{Haptics, Pointer},
|
||||
backend::input::{Haptics, Pointer, TrackedDevice, TrackedDeviceRole},
|
||||
config_io,
|
||||
state::{AppSession, AppState},
|
||||
};
|
||||
|
||||
use super::XrState;
|
||||
use super::{helpers::posef_to_transform, XrState};
|
||||
|
||||
type XrSession = xr::Session<xr::Vulkan>;
|
||||
static DOUBLE_CLICK_TIME: Duration = Duration::from_millis(500);
|
||||
|
||||
pub(super) struct OpenXrAction {}
|
||||
static CLICK_TIMES: [Duration; 3] = [
|
||||
Duration::ZERO,
|
||||
Duration::from_millis(500),
|
||||
Duration::from_millis(750),
|
||||
];
|
||||
|
||||
pub(super) struct OpenXrInputSource {
|
||||
action_set: xr::ActionSet,
|
||||
@@ -27,47 +33,97 @@ pub(super) struct OpenXrHand {
|
||||
space: xr::Space,
|
||||
}
|
||||
|
||||
pub struct CustomClickAction {
|
||||
pub struct MultiClickHandler<const COUNT: usize> {
|
||||
name: String,
|
||||
action_f32: xr::Action<f32>,
|
||||
action_bool: xr::Action<bool>,
|
||||
action_f32_double: xr::Action<f32>,
|
||||
action_bool_double: xr::Action<bool>,
|
||||
last_click: Option<Instant>,
|
||||
held: bool,
|
||||
previous: [Instant; COUNT],
|
||||
held_active: bool,
|
||||
held_inactive: bool,
|
||||
}
|
||||
|
||||
impl<const COUNT: usize> MultiClickHandler<COUNT> {
|
||||
fn new(action_set: &xr::ActionSet, action_name: &str, side: &str) -> anyhow::Result<Self> {
|
||||
let name = format!("{side}_{COUNT}-{action_name}");
|
||||
let name_f32 = format!("{}_value", &name);
|
||||
|
||||
let action_bool = action_set.create_action::<bool>(&name, &name, &[])?;
|
||||
let action_f32 = action_set.create_action::<f32>(&name_f32, &name_f32, &[])?;
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
action_f32,
|
||||
action_bool,
|
||||
previous: from_fn(|_| Instant::now()),
|
||||
held_active: false,
|
||||
held_inactive: false,
|
||||
})
|
||||
}
|
||||
fn check<G>(&mut self, session: &xr::Session<G>, threshold: f32) -> anyhow::Result<bool> {
|
||||
let res = self.action_bool.state(session, xr::Path::NULL)?;
|
||||
let mut state = res.is_active && res.current_state;
|
||||
|
||||
if !state {
|
||||
let res = self.action_f32.state(session, xr::Path::NULL)?;
|
||||
state = res.is_active && res.current_state > threshold;
|
||||
}
|
||||
|
||||
if !state {
|
||||
self.held_active = false;
|
||||
self.held_inactive = false;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if self.held_active {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if self.held_inactive {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let passed = self
|
||||
.previous
|
||||
.iter()
|
||||
.all(|instant| instant.elapsed() < CLICK_TIMES[COUNT]);
|
||||
|
||||
if passed {
|
||||
log::trace!("{}: passed", self.name);
|
||||
self.held_active = true;
|
||||
self.held_inactive = false;
|
||||
|
||||
// reset to no prior clicks
|
||||
let long_ago = Instant::now().checked_sub(Duration::from_secs(10)).unwrap();
|
||||
self.previous
|
||||
.iter_mut()
|
||||
.for_each(|instant| *instant = long_ago);
|
||||
} else if COUNT > 0 {
|
||||
log::trace!("{}: rotate", self.name);
|
||||
self.previous.rotate_right(1);
|
||||
self.previous[0] = Instant::now();
|
||||
self.held_inactive = true;
|
||||
}
|
||||
|
||||
Ok(passed)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CustomClickAction {
|
||||
single: MultiClickHandler<0>,
|
||||
double: MultiClickHandler<1>,
|
||||
triple: MultiClickHandler<2>,
|
||||
}
|
||||
|
||||
impl CustomClickAction {
|
||||
pub fn new(action_set: &xr::ActionSet, name: &str, side: &str) -> anyhow::Result<Self> {
|
||||
let action_f32 = action_set.create_action::<f32>(
|
||||
&format!("{}_{}_value", side, name),
|
||||
&format!("{} hand {} value", side, name),
|
||||
&[],
|
||||
)?;
|
||||
let action_f32_double = action_set.create_action::<f32>(
|
||||
&format!("{}_{}_value_double", side, name),
|
||||
&format!("{} hand {} value double", side, name),
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let action_bool = action_set.create_action::<bool>(
|
||||
&format!("{}_{}", side, name),
|
||||
&format!("{} hand {}", side, name),
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let action_bool_double = action_set.create_action::<bool>(
|
||||
&format!("{}_{}_double", side, name),
|
||||
&format!("{} hand {} double", side, name),
|
||||
&[],
|
||||
)?;
|
||||
let single = MultiClickHandler::new(action_set, name, side)?;
|
||||
let double = MultiClickHandler::new(action_set, name, side)?;
|
||||
let triple = MultiClickHandler::new(action_set, name, side)?;
|
||||
|
||||
Ok(Self {
|
||||
action_f32,
|
||||
action_f32_double,
|
||||
action_bool,
|
||||
action_bool_double,
|
||||
last_click: None,
|
||||
held: false,
|
||||
single,
|
||||
double,
|
||||
triple,
|
||||
})
|
||||
}
|
||||
pub fn state(
|
||||
@@ -76,76 +132,33 @@ impl CustomClickAction {
|
||||
state: &XrState,
|
||||
session: &AppSession,
|
||||
) -> anyhow::Result<bool> {
|
||||
let res = self.action_bool.state(&state.session, xr::Path::NULL)?;
|
||||
if res.is_active && res.current_state {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let res = self
|
||||
.action_bool_double
|
||||
.state(&state.session, xr::Path::NULL)?;
|
||||
if res.is_active
|
||||
&& ((before && res.current_state) || self.check_double_click(res.current_state))
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let threshold = if before {
|
||||
session.config.xr_click_sensitivity_release
|
||||
} else {
|
||||
session.config.xr_click_sensitivity
|
||||
};
|
||||
|
||||
let res = self.action_f32.state(&state.session, xr::Path::NULL)?;
|
||||
if res.is_active && res.current_state > threshold {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let res = self.action_f32.state(&state.session, xr::Path::NULL)?;
|
||||
if res.is_active
|
||||
&& ((before && res.current_state > threshold)
|
||||
|| self.check_double_click(res.current_state > threshold))
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// submit a click. returns true if it should count as a double click
|
||||
fn check_double_click(&mut self, state: bool) -> bool {
|
||||
if !state {
|
||||
self.held = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.held {
|
||||
return false;
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let double_click = match self.last_click {
|
||||
Some(last_click) => now - last_click < DOUBLE_CLICK_TIME,
|
||||
None => false,
|
||||
};
|
||||
self.last_click = if double_click { None } else { Some(now) };
|
||||
self.held = true;
|
||||
double_click
|
||||
Ok(self.single.check(&state.session, threshold)?
|
||||
|| self.double.check(&state.session, threshold)?
|
||||
|| self.triple.check(&state.session, threshold)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct OpenXrHandSource {
|
||||
action_pose: xr::Action<xr::Posef>,
|
||||
action_click: CustomClickAction,
|
||||
action_grab: CustomClickAction,
|
||||
action_alt_click: CustomClickAction,
|
||||
action_show_hide: CustomClickAction,
|
||||
action_space_drag: CustomClickAction,
|
||||
action_modifier_right: CustomClickAction,
|
||||
action_modifier_middle: CustomClickAction,
|
||||
action_move_mouse: CustomClickAction,
|
||||
action_scroll: xr::Action<f32>,
|
||||
action_haptics: xr::Action<xr::Haptic>,
|
||||
pose: xr::Action<xr::Posef>,
|
||||
click: CustomClickAction,
|
||||
grab: CustomClickAction,
|
||||
alt_click: CustomClickAction,
|
||||
show_hide: CustomClickAction,
|
||||
toggle_dashboard: CustomClickAction,
|
||||
space_drag: CustomClickAction,
|
||||
space_rotate: CustomClickAction,
|
||||
space_reset: CustomClickAction,
|
||||
modifier_right: CustomClickAction,
|
||||
modifier_middle: CustomClickAction,
|
||||
move_mouse: CustomClickAction,
|
||||
scroll: xr::Action<Vector2f>,
|
||||
haptics: xr::Action<xr::Haptic>,
|
||||
}
|
||||
|
||||
impl OpenXrInputSource {
|
||||
@@ -158,7 +171,7 @@ impl OpenXrInputSource {
|
||||
let left_source = OpenXrHandSource::new(&mut action_set, "left")?;
|
||||
let right_source = OpenXrHandSource::new(&mut action_set, "right")?;
|
||||
|
||||
suggest_bindings(&xr.instance, &[&left_source, &right_source])?;
|
||||
suggest_bindings(&xr.instance, &[&left_source, &right_source]);
|
||||
|
||||
xr.session.attach_action_sets(&[&action_set])?;
|
||||
|
||||
@@ -172,9 +185,9 @@ impl OpenXrInputSource {
|
||||
}
|
||||
|
||||
pub fn haptics(&self, xr: &XrState, hand: usize, haptics: &Haptics) {
|
||||
let action = &self.hands[hand].source.action_haptics;
|
||||
let action = &self.hands[hand].source.haptics;
|
||||
|
||||
let duration_nanos = (haptics.duration as f64) * 1_000_000_000.0;
|
||||
let duration_nanos = f64::from(haptics.duration) * 1_000_000_000.0;
|
||||
|
||||
let _ = action.apply_feedback(
|
||||
&xr.session,
|
||||
@@ -189,20 +202,107 @@ impl OpenXrInputSource {
|
||||
pub fn update(&mut self, xr: &XrState, state: &mut AppState) -> anyhow::Result<()> {
|
||||
xr.session.sync_actions(&[(&self.action_set).into()])?;
|
||||
|
||||
let loc = xr.view.locate(&xr.stage, xr.predicted_display_time)?;
|
||||
let hmd = posef_to_transform(&loc.pose);
|
||||
if loc
|
||||
.location_flags
|
||||
.contains(xr::SpaceLocationFlags::ORIENTATION_VALID)
|
||||
{
|
||||
state.input_state.hmd.matrix3 = hmd.matrix3;
|
||||
}
|
||||
|
||||
if loc
|
||||
.location_flags
|
||||
.contains(xr::SpaceLocationFlags::POSITION_VALID)
|
||||
{
|
||||
state.input_state.hmd.translation = hmd.translation;
|
||||
}
|
||||
|
||||
for i in 0..2 {
|
||||
self.hands[i].update(&mut state.input_state.pointers[i], xr, &state.session)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_device_battery_status(
|
||||
device: &mut mnd::Device,
|
||||
role: TrackedDeviceRole,
|
||||
app: &mut AppState,
|
||||
) {
|
||||
if let Ok(status) = device.battery_status() {
|
||||
if status.present {
|
||||
app.input_state.devices.push(TrackedDevice {
|
||||
soc: Some(status.charge),
|
||||
charging: status.charging,
|
||||
role,
|
||||
});
|
||||
log::debug!(
|
||||
"Device {} role {:#?}: {:.0}% (charging {})",
|
||||
device.index,
|
||||
role,
|
||||
status.charge * 100.0f32,
|
||||
status.charging
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_devices(app: &mut AppState, monado: &mut mnd::Monado) {
|
||||
app.input_state.devices.clear();
|
||||
|
||||
let roles = [
|
||||
(mnd::DeviceRole::Head, TrackedDeviceRole::Hmd),
|
||||
(mnd::DeviceRole::Eyes, TrackedDeviceRole::None),
|
||||
(mnd::DeviceRole::Left, TrackedDeviceRole::LeftHand),
|
||||
(mnd::DeviceRole::Right, TrackedDeviceRole::RightHand),
|
||||
(mnd::DeviceRole::Gamepad, TrackedDeviceRole::None),
|
||||
(
|
||||
mnd::DeviceRole::HandTrackingLeft,
|
||||
TrackedDeviceRole::LeftHand,
|
||||
),
|
||||
(
|
||||
mnd::DeviceRole::HandTrackingRight,
|
||||
TrackedDeviceRole::RightHand,
|
||||
),
|
||||
];
|
||||
let mut seen = Vec::<u32>::with_capacity(32);
|
||||
for (mnd_role, wlx_role) in roles {
|
||||
let device = monado.device_from_role(mnd_role);
|
||||
if let Ok(mut device) = device {
|
||||
if !seen.contains(&device.index) {
|
||||
seen.push(device.index);
|
||||
Self::update_device_battery_status(&mut device, wlx_role, app);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(devices) = monado.devices() {
|
||||
for mut device in devices {
|
||||
if !seen.contains(&device.index) {
|
||||
let role = if device.name_id >= 4 && device.name_id <= 8 {
|
||||
TrackedDeviceRole::Tracker
|
||||
} else {
|
||||
TrackedDeviceRole::None
|
||||
};
|
||||
Self::update_device_battery_status(&mut device, role, app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.input_state.devices.sort_by(|a, b| {
|
||||
u8::from(a.soc.is_none())
|
||||
.cmp(&u8::from(b.soc.is_none()))
|
||||
.then((a.role as u8).cmp(&(b.role as u8)))
|
||||
.then(a.soc.unwrap_or(999.).total_cmp(&b.soc.unwrap_or(999.)))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenXrHand {
|
||||
pub(super) fn new(xr: &XrState, source: OpenXrHandSource) -> Result<Self, xr::sys::Result> {
|
||||
let space = source.action_pose.create_space(
|
||||
xr.session.clone(),
|
||||
xr::Path::NULL,
|
||||
xr::Posef::IDENTITY,
|
||||
)?;
|
||||
let space =
|
||||
source
|
||||
.pose
|
||||
.create_space(xr.session.clone(), xr::Path::NULL, xr::Posef::IDENTITY)?;
|
||||
|
||||
Ok(Self { source, space })
|
||||
}
|
||||
@@ -218,59 +318,81 @@ impl OpenXrHand {
|
||||
.location_flags
|
||||
.contains(xr::SpaceLocationFlags::ORIENTATION_VALID)
|
||||
{
|
||||
let quat = unsafe { std::mem::transmute::<_, Quat>(location.pose.orientation) };
|
||||
let pos = unsafe { std::mem::transmute::<_, Vec3>(location.pose.position) };
|
||||
pointer.pose = Affine3A::from_rotation_translation(quat, pos);
|
||||
let (cur_quat, cur_pos) = (Quat::from_affine3(&pointer.pose), pointer.pose.translation);
|
||||
|
||||
let (new_quat, new_pos) = unsafe {
|
||||
(
|
||||
transmute::<Quaternionf, Quat>(location.pose.orientation),
|
||||
transmute::<Vector3f, Vec3>(location.pose.position),
|
||||
)
|
||||
};
|
||||
let lerp_factor =
|
||||
(1.0 / (xr.fps / 100.0) * session.config.pointer_lerp_factor).clamp(0.1, 1.0);
|
||||
pointer.raw_pose = Affine3A::from_rotation_translation(new_quat, new_pos);
|
||||
pointer.pose = Affine3A::from_rotation_translation(
|
||||
cur_quat.lerp(new_quat, lerp_factor),
|
||||
cur_pos.lerp(new_pos.into(), lerp_factor).into(),
|
||||
);
|
||||
}
|
||||
|
||||
pointer.now.click = self
|
||||
.source
|
||||
.action_click
|
||||
.state(pointer.before.click, xr, session)?;
|
||||
pointer.now.click = self.source.click.state(pointer.before.click, xr, session)?;
|
||||
|
||||
pointer.now.grab = self
|
||||
.source
|
||||
.action_grab
|
||||
.state(pointer.before.grab, xr, session)?;
|
||||
pointer.now.grab = self.source.grab.state(pointer.before.grab, xr, session)?;
|
||||
|
||||
pointer.now.scroll = self
|
||||
let scroll = self
|
||||
.source
|
||||
.action_scroll
|
||||
.scroll
|
||||
.state(&xr.session, xr::Path::NULL)?
|
||||
.current_state;
|
||||
|
||||
pointer.now.scroll_x = scroll.x;
|
||||
pointer.now.scroll_y = scroll.y;
|
||||
|
||||
pointer.now.alt_click =
|
||||
self.source
|
||||
.action_alt_click
|
||||
.alt_click
|
||||
.state(pointer.before.alt_click, xr, session)?;
|
||||
|
||||
pointer.now.show_hide =
|
||||
self.source
|
||||
.action_show_hide
|
||||
.show_hide
|
||||
.state(pointer.before.show_hide, xr, session)?;
|
||||
|
||||
pointer.now.click_modifier_right = self.source.action_modifier_right.state(
|
||||
pointer.before.click_modifier_right,
|
||||
xr,
|
||||
session,
|
||||
)?;
|
||||
pointer.now.click_modifier_right =
|
||||
self.source
|
||||
.modifier_right
|
||||
.state(pointer.before.click_modifier_right, xr, session)?;
|
||||
|
||||
pointer.now.click_modifier_middle = self.source.action_modifier_middle.state(
|
||||
pointer.before.click_modifier_middle,
|
||||
xr,
|
||||
session,
|
||||
)?;
|
||||
pointer.now.toggle_dashboard =
|
||||
self.source
|
||||
.toggle_dashboard
|
||||
.state(pointer.before.toggle_dashboard, xr, session)?;
|
||||
|
||||
pointer.now.click_modifier_middle =
|
||||
self.source
|
||||
.modifier_middle
|
||||
.state(pointer.before.click_modifier_middle, xr, session)?;
|
||||
|
||||
pointer.now.move_mouse =
|
||||
self.source
|
||||
.action_move_mouse
|
||||
.move_mouse
|
||||
.state(pointer.before.move_mouse, xr, session)?;
|
||||
|
||||
pointer.now.space_drag =
|
||||
self.source
|
||||
.action_space_drag
|
||||
.space_drag
|
||||
.state(pointer.before.space_drag, xr, session)?;
|
||||
|
||||
pointer.now.space_rotate =
|
||||
self.source
|
||||
.space_rotate
|
||||
.state(pointer.before.space_rotate, xr, session)?;
|
||||
|
||||
pointer.now.space_reset =
|
||||
self.source
|
||||
.space_reset
|
||||
.state(pointer.before.space_reset, xr, session)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -279,98 +401,99 @@ impl OpenXrHand {
|
||||
impl OpenXrHandSource {
|
||||
pub(super) fn new(action_set: &mut xr::ActionSet, side: &str) -> anyhow::Result<Self> {
|
||||
let action_pose = action_set.create_action::<xr::Posef>(
|
||||
&format!("{}_hand", side),
|
||||
&format!("{} hand pose", side),
|
||||
&format!("{side}_hand"),
|
||||
&format!("{side} hand pose"),
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let action_scroll = action_set.create_action::<f32>(
|
||||
&format!("{}_scroll", side),
|
||||
&format!("{} hand scroll", side),
|
||||
let action_scroll = action_set.create_action::<Vector2f>(
|
||||
&format!("{side}_scroll"),
|
||||
&format!("{side} hand scroll"),
|
||||
&[],
|
||||
)?;
|
||||
let action_haptics = action_set.create_action::<xr::Haptic>(
|
||||
&format!("{}_haptics", side),
|
||||
&format!("{} hand haptics", side),
|
||||
&format!("{side}_haptics"),
|
||||
&format!("{side} hand haptics"),
|
||||
&[],
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
action_pose,
|
||||
action_click: CustomClickAction::new(action_set, "click", side)?,
|
||||
action_grab: CustomClickAction::new(action_set, "grab", side)?,
|
||||
action_scroll,
|
||||
action_alt_click: CustomClickAction::new(action_set, "alt_click", side)?,
|
||||
action_show_hide: CustomClickAction::new(action_set, "show_hide", side)?,
|
||||
action_space_drag: CustomClickAction::new(action_set, "space_drag", side)?,
|
||||
action_modifier_right: CustomClickAction::new(
|
||||
action_set,
|
||||
"click_modifier_right",
|
||||
side,
|
||||
)?,
|
||||
action_modifier_middle: CustomClickAction::new(
|
||||
action_set,
|
||||
"click_modifier_middle",
|
||||
side,
|
||||
)?,
|
||||
action_move_mouse: CustomClickAction::new(action_set, "move_mouse", side)?,
|
||||
action_haptics,
|
||||
pose: action_pose,
|
||||
click: CustomClickAction::new(action_set, "click", side)?,
|
||||
grab: CustomClickAction::new(action_set, "grab", side)?,
|
||||
scroll: action_scroll,
|
||||
alt_click: CustomClickAction::new(action_set, "alt_click", side)?,
|
||||
show_hide: CustomClickAction::new(action_set, "show_hide", side)?,
|
||||
toggle_dashboard: CustomClickAction::new(action_set, "toggle_dashboard", side)?,
|
||||
space_drag: CustomClickAction::new(action_set, "space_drag", side)?,
|
||||
space_rotate: CustomClickAction::new(action_set, "space_rotate", side)?,
|
||||
space_reset: CustomClickAction::new(action_set, "space_reset", side)?,
|
||||
modifier_right: CustomClickAction::new(action_set, "click_modifier_right", side)?,
|
||||
modifier_middle: CustomClickAction::new(action_set, "click_modifier_middle", side)?,
|
||||
move_mouse: CustomClickAction::new(action_set, "move_mouse", side)?,
|
||||
haptics: action_haptics,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn to_path(maybe_path_str: &Option<String>, instance: &xr::Instance) -> Option<xr::Path> {
|
||||
maybe_path_str
|
||||
.as_ref()
|
||||
.and_then(|s| match instance.string_to_path(s) {
|
||||
Ok(path) => Some(path),
|
||||
Err(_) => {
|
||||
log::warn!("Invalid binding path: {}", s);
|
||||
None
|
||||
}
|
||||
})
|
||||
fn to_path(maybe_path_str: Option<&String>, instance: &xr::Instance) -> Option<xr::Path> {
|
||||
maybe_path_str.as_ref().and_then(|s| {
|
||||
instance
|
||||
.string_to_path(s)
|
||||
.inspect_err(|_| {
|
||||
log::warn!("Invalid binding path: {s}");
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn is_bool(maybe_type_str: &Option<String>) -> bool {
|
||||
fn is_bool(maybe_type_str: Option<&String>) -> bool {
|
||||
maybe_type_str
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.unwrap() // want panic
|
||||
.split('/')
|
||||
.last()
|
||||
.map(|last| matches!(last, "click" | "touch"))
|
||||
.unwrap_or(false)
|
||||
.next_back()
|
||||
.is_some_and(|last| matches!(last, "click" | "touch"))
|
||||
}
|
||||
|
||||
macro_rules! add_custom {
|
||||
($action:expr, $left:expr, $right:expr, $bindings:expr, $instance:expr) => {
|
||||
if let Some(action) = $action.as_ref() {
|
||||
if let Some(p) = to_path(&action.left, $instance) {
|
||||
if is_bool(&action.left) {
|
||||
if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$left.action_bool_double, p));
|
||||
if let Some(p) = to_path(action.left.as_ref(), $instance) {
|
||||
if is_bool(action.left.as_ref()) {
|
||||
if action.triple_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$left.triple.action_bool, p));
|
||||
} else if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$left.double.action_bool, p));
|
||||
} else {
|
||||
$bindings.push(xr::Binding::new(&$left.action_bool, p));
|
||||
$bindings.push(xr::Binding::new(&$left.single.action_bool, p));
|
||||
}
|
||||
} else {
|
||||
if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$left.action_f32_double, p));
|
||||
if action.triple_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$left.triple.action_f32, p));
|
||||
} else if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$left.double.action_f32, p));
|
||||
} else {
|
||||
$bindings.push(xr::Binding::new(&$left.action_f32, p));
|
||||
$bindings.push(xr::Binding::new(&$left.single.action_f32, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(p) = to_path(&action.right, $instance) {
|
||||
if is_bool(&action.right) {
|
||||
if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$right.action_bool_double, p));
|
||||
if let Some(p) = to_path(action.right.as_ref(), $instance) {
|
||||
if is_bool(action.right.as_ref()) {
|
||||
if action.triple_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$right.triple.action_bool, p));
|
||||
} else if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$right.double.action_bool, p));
|
||||
} else {
|
||||
$bindings.push(xr::Binding::new(&$right.action_bool, p));
|
||||
$bindings.push(xr::Binding::new(&$right.single.action_bool, p));
|
||||
}
|
||||
} else {
|
||||
if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$right.action_f32_double, p));
|
||||
if action.triple_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$right.triple.action_f32, p));
|
||||
} else if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$right.double.action_f32, p));
|
||||
} else {
|
||||
$bindings.push(xr::Binding::new(&$right.action_f32, p));
|
||||
$bindings.push(xr::Binding::new(&$right.single.action_f32, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,8 +501,9 @@ macro_rules! add_custom {
|
||||
};
|
||||
}
|
||||
|
||||
fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) -> anyhow::Result<()> {
|
||||
let profiles = load_action_profiles()?;
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) {
|
||||
let profiles = load_action_profiles();
|
||||
|
||||
for profile in profiles {
|
||||
let Ok(profile_path) = instance.string_to_path(&profile.profile) else {
|
||||
@@ -390,92 +514,116 @@ fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) ->
|
||||
let mut bindings: Vec<xr::Binding> = vec![];
|
||||
|
||||
if let Some(action) = profile.pose {
|
||||
if let Some(p) = to_path(&action.left, instance) {
|
||||
bindings.push(xr::Binding::new(&hands[0].action_pose, p));
|
||||
if let Some(p) = to_path(action.left.as_ref(), instance) {
|
||||
bindings.push(xr::Binding::new(&hands[0].pose, p));
|
||||
}
|
||||
if let Some(p) = to_path(&action.right, instance) {
|
||||
bindings.push(xr::Binding::new(&hands[1].action_pose, p));
|
||||
if let Some(p) = to_path(action.right.as_ref(), instance) {
|
||||
bindings.push(xr::Binding::new(&hands[1].pose, p));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(action) = profile.haptic {
|
||||
if let Some(p) = to_path(&action.left, instance) {
|
||||
bindings.push(xr::Binding::new(&hands[0].action_haptics, p));
|
||||
if let Some(p) = to_path(action.left.as_ref(), instance) {
|
||||
bindings.push(xr::Binding::new(&hands[0].haptics, p));
|
||||
}
|
||||
if let Some(p) = to_path(&action.right, instance) {
|
||||
bindings.push(xr::Binding::new(&hands[1].action_haptics, p));
|
||||
if let Some(p) = to_path(action.right.as_ref(), instance) {
|
||||
bindings.push(xr::Binding::new(&hands[1].haptics, p));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(action) = profile.scroll {
|
||||
if let Some(p) = to_path(&action.left, instance) {
|
||||
bindings.push(xr::Binding::new(&hands[0].action_scroll, p));
|
||||
if let Some(p) = to_path(action.left.as_ref(), instance) {
|
||||
bindings.push(xr::Binding::new(&hands[0].scroll, p));
|
||||
}
|
||||
if let Some(p) = to_path(&action.right, instance) {
|
||||
bindings.push(xr::Binding::new(&hands[1].action_scroll, p));
|
||||
if let Some(p) = to_path(action.right.as_ref(), instance) {
|
||||
bindings.push(xr::Binding::new(&hands[1].scroll, p));
|
||||
}
|
||||
}
|
||||
|
||||
add_custom!(
|
||||
profile.click,
|
||||
hands[0].action_click,
|
||||
hands[1].action_click,
|
||||
hands[0].click,
|
||||
hands[1].click,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.alt_click,
|
||||
&hands[0].action_alt_click,
|
||||
&hands[1].action_alt_click,
|
||||
&hands[0].alt_click,
|
||||
&hands[1].alt_click,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.grab,
|
||||
&hands[0].action_grab,
|
||||
&hands[1].action_grab,
|
||||
&hands[0].grab,
|
||||
&hands[1].grab,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.show_hide,
|
||||
&hands[0].action_show_hide,
|
||||
&hands[1].action_show_hide,
|
||||
&hands[0].show_hide,
|
||||
&hands[1].show_hide,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.toggle_dashboard,
|
||||
&hands[0].toggle_dashboard,
|
||||
&hands[1].toggle_dashboard,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.space_drag,
|
||||
&hands[0].action_space_drag,
|
||||
&hands[1].action_space_drag,
|
||||
&hands[0].space_drag,
|
||||
&hands[1].space_drag,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.space_rotate,
|
||||
&hands[0].space_rotate,
|
||||
&hands[1].space_rotate,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.space_reset,
|
||||
&hands[0].space_reset,
|
||||
&hands[1].space_reset,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.click_modifier_right,
|
||||
&hands[0].action_modifier_right,
|
||||
&hands[1].action_modifier_right,
|
||||
&hands[0].modifier_right,
|
||||
&hands[1].modifier_right,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.click_modifier_middle,
|
||||
&hands[0].action_modifier_middle,
|
||||
&hands[1].action_modifier_middle,
|
||||
&hands[0].modifier_middle,
|
||||
&hands[1].modifier_middle,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.move_mouse,
|
||||
&hands[0].action_move_mouse,
|
||||
&hands[1].action_move_mouse,
|
||||
&hands[0].move_mouse,
|
||||
&hands[1].move_mouse,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
@@ -488,8 +636,6 @@ fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) ->
|
||||
log::error!("Verify config: ~/.config/wlxoverlay/openxr_actions.json5");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -498,6 +644,7 @@ struct OpenXrActionConfAction {
|
||||
right: Option<String>,
|
||||
threshold: Option<[f32; 2]>,
|
||||
double_click: Option<bool>,
|
||||
triple_click: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -508,7 +655,10 @@ struct OpenXrActionConfProfile {
|
||||
grab: Option<OpenXrActionConfAction>,
|
||||
alt_click: Option<OpenXrActionConfAction>,
|
||||
show_hide: Option<OpenXrActionConfAction>,
|
||||
toggle_dashboard: Option<OpenXrActionConfAction>,
|
||||
space_drag: Option<OpenXrActionConfAction>,
|
||||
space_rotate: Option<OpenXrActionConfAction>,
|
||||
space_reset: Option<OpenXrActionConfAction>,
|
||||
click_modifier_right: Option<OpenXrActionConfAction>,
|
||||
click_modifier_middle: Option<OpenXrActionConfAction>,
|
||||
move_mouse: Option<OpenXrActionConfAction>,
|
||||
@@ -518,26 +668,26 @@ struct OpenXrActionConfProfile {
|
||||
|
||||
const DEFAULT_PROFILES: &str = include_str!("openxr_actions.json5");
|
||||
|
||||
fn load_action_profiles() -> anyhow::Result<Vec<OpenXrActionConfProfile>> {
|
||||
fn load_action_profiles() -> Vec<OpenXrActionConfProfile> {
|
||||
let mut profiles: Vec<OpenXrActionConfProfile> =
|
||||
serde_json5::from_str(DEFAULT_PROFILES).unwrap(); // want panic
|
||||
|
||||
let Some(conf) = config_io::load("openxr_actions.json5") else {
|
||||
return Ok(profiles);
|
||||
return profiles;
|
||||
};
|
||||
|
||||
match serde_json5::from_str::<Vec<OpenXrActionConfProfile>>(&conf) {
|
||||
Ok(override_profiles) => {
|
||||
override_profiles.into_iter().for_each(|new| {
|
||||
for new in override_profiles {
|
||||
if let Some(i) = profiles.iter().position(|old| old.profile == new.profile) {
|
||||
profiles[i] = new;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load openxr_actions.json5: {}", e);
|
||||
log::error!("Failed to load openxr_actions.json5: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(profiles)
|
||||
profiles
|
||||
}
|
||||
+69
-53
@@ -7,58 +7,55 @@ use std::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
usize,
|
||||
};
|
||||
|
||||
use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::view::ImageView};
|
||||
use vulkano::command_buffer::CommandBufferUsage;
|
||||
|
||||
use crate::{
|
||||
backend::openxr::helpers,
|
||||
graphics::{WlxCommandBuffer, WlxGraphics},
|
||||
graphics::{CommandBuffers, WlxGraphics, WlxPipeline, SWAPCHAIN_FORMAT},
|
||||
};
|
||||
|
||||
use super::{
|
||||
swapchain::{create_swapchain_render_data, SwapchainRenderData},
|
||||
swapchain::{create_swapchain, SwapchainOpts, WlxSwapchain},
|
||||
CompositionLayer, XrState,
|
||||
};
|
||||
|
||||
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
|
||||
static LINE_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
|
||||
pub(super) const LINE_WIDTH: f32 = 0.002;
|
||||
|
||||
// TODO customizable colors
|
||||
static COLORS: [[f32; 6]; 5] = {
|
||||
[
|
||||
[1., 1., 1., 1., 0., 0.],
|
||||
[0., 0.375, 0.5, 1., 0., 0.],
|
||||
[0.69, 0.188, 0., 1., 0., 0.],
|
||||
[0.375, 0., 0.5, 1., 0., 0.],
|
||||
[1., 0., 0., 1., 0., 0.],
|
||||
]
|
||||
};
|
||||
|
||||
pub(super) struct LinePool {
|
||||
lines: IdMap<usize, LineContainer>,
|
||||
colors: Vec<Arc<ImageView>>,
|
||||
pipeline: Arc<WlxPipeline>,
|
||||
}
|
||||
|
||||
impl LinePool {
|
||||
pub(super) fn new(graphics: Arc<WlxGraphics>) -> anyhow::Result<Self> {
|
||||
let mut command_buffer =
|
||||
graphics.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
let Ok(shaders) = graphics.shared_shaders.read() else {
|
||||
anyhow::bail!("Failed to lock shared shaders for reading");
|
||||
};
|
||||
|
||||
// TODO customizable colors
|
||||
let colors = [
|
||||
[0xff, 0xff, 0xff, 0xff],
|
||||
[0x00, 0x60, 0x80, 0xff],
|
||||
[0xB0, 0x30, 0x00, 0xff],
|
||||
[0x60, 0x00, 0x80, 0xff],
|
||||
[0xff, 0x00, 0x00, 0xff],
|
||||
];
|
||||
let pipeline = graphics.create_pipeline(
|
||||
shaders.get("vert_common").unwrap().clone(), // want panic
|
||||
shaders.get("frag_color").unwrap().clone(), // want panic
|
||||
SWAPCHAIN_FORMAT,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let views: anyhow::Result<Vec<Arc<ImageView>>> = colors
|
||||
.into_iter()
|
||||
.map(
|
||||
|color| match command_buffer.texture2d(1, 1, Format::R8G8B8A8_UNORM, &color) {
|
||||
Ok(tex) => ImageView::new_default(tex).map_err(|e| anyhow::anyhow!(e)),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
command_buffer.build_and_execute_now()?;
|
||||
|
||||
Ok(LinePool {
|
||||
Ok(Self {
|
||||
lines: IdMap::new(),
|
||||
colors: views?,
|
||||
pipeline,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,9 +64,9 @@ impl LinePool {
|
||||
xr: &XrState,
|
||||
graphics: Arc<WlxGraphics>,
|
||||
) -> anyhow::Result<usize> {
|
||||
let id = AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
|
||||
let id = LINE_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let srd = create_swapchain_render_data(xr, graphics, [1, 1, 1])?;
|
||||
let srd = create_swapchain(xr, graphics, [1, 1, 1], SwapchainOpts::new())?;
|
||||
self.lines.insert(
|
||||
id,
|
||||
LineContainer {
|
||||
@@ -92,10 +89,10 @@ impl LinePool {
|
||||
return;
|
||||
}
|
||||
|
||||
debug_assert!(color < self.colors.len());
|
||||
debug_assert!(color < COLORS.len());
|
||||
|
||||
let Some(line) = self.lines.get_mut(id) else {
|
||||
log::warn!("Line {} not found", id);
|
||||
log::warn!("Line {id} not found");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -125,28 +122,55 @@ impl LinePool {
|
||||
let posef = helpers::transform_to_posef(&transform);
|
||||
|
||||
line.maybe_line = Some(Line {
|
||||
view: self.colors[color].clone(),
|
||||
color,
|
||||
pose: posef,
|
||||
length: len,
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn present_xr<'a>(
|
||||
pub(super) fn render(
|
||||
&mut self,
|
||||
graphics: Arc<WlxGraphics>,
|
||||
buf: &mut CommandBuffers,
|
||||
) -> anyhow::Result<()> {
|
||||
for line in self.lines.values_mut() {
|
||||
if let Some(inner) = line.maybe_line.as_mut() {
|
||||
let tgt = line.swapchain.acquire_wait_image()?;
|
||||
|
||||
let set0 = self
|
||||
.pipeline
|
||||
.uniform_buffer(0, COLORS[inner.color].to_vec())?;
|
||||
|
||||
let pass = self
|
||||
.pipeline
|
||||
.create_pass_for_target(tgt.clone(), vec![set0])?;
|
||||
|
||||
let mut cmd_buffer =
|
||||
graphics.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
cmd_buffer.begin_rendering(tgt)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
cmd_buffer.end_rendering()?;
|
||||
|
||||
buf.push(cmd_buffer.build()?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn present<'a>(
|
||||
&'a mut self,
|
||||
xr: &'a XrState,
|
||||
command_buffer: &mut WlxCommandBuffer,
|
||||
) -> anyhow::Result<Vec<CompositionLayer>> {
|
||||
) -> anyhow::Result<Vec<CompositionLayer<'a>>> {
|
||||
let mut quads = Vec::new();
|
||||
|
||||
for line in self.lines.values_mut() {
|
||||
line.swapchain.ensure_image_released()?;
|
||||
|
||||
if let Some(inner) = line.maybe_line.take() {
|
||||
let quad = xr::CompositionLayerQuad::new()
|
||||
.pose(inner.pose)
|
||||
.sub_image(line.swapchain.acquire_present_release(
|
||||
command_buffer,
|
||||
inner.view,
|
||||
1.0,
|
||||
)?)
|
||||
.sub_image(line.swapchain.get_subimage())
|
||||
.eye_visibility(xr::EyeVisibility::BOTH)
|
||||
.space(&xr.stage)
|
||||
.size(xr::Extent2Df {
|
||||
@@ -160,23 +184,15 @@ impl LinePool {
|
||||
|
||||
Ok(quads)
|
||||
}
|
||||
|
||||
/// the number of lines that are waiting to be drawn
|
||||
pub(super) fn num_pending(&self) -> usize {
|
||||
self.lines
|
||||
.values()
|
||||
.filter(|l| l.maybe_line.is_some())
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Line {
|
||||
pub(super) view: Arc<ImageView>,
|
||||
pub(super) color: usize,
|
||||
pub(super) pose: xr::Posef,
|
||||
pub(super) length: f32,
|
||||
}
|
||||
|
||||
struct LineContainer {
|
||||
swapchain: SwapchainRenderData,
|
||||
swapchain: WlxSwapchain,
|
||||
maybe_line: Option<Line>,
|
||||
}
|
||||
+249
-92
@@ -1,15 +1,19 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
ops::Add,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use glam::{Affine3A, Vec3};
|
||||
use input::OpenXrInputSource;
|
||||
use libmonado::Monado;
|
||||
use openxr as xr;
|
||||
use vulkano::{command_buffer::CommandBufferUsage, Handle, VulkanObject};
|
||||
use skybox::create_skybox;
|
||||
use vulkano::{Handle, VulkanObject};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
@@ -17,10 +21,10 @@ use crate::{
|
||||
input::interact,
|
||||
notifications::NotificationManager,
|
||||
openxr::{lines::LinePool, overlay::OpenXrOverlayData},
|
||||
overlay::OverlayData,
|
||||
overlay::{OverlayData, ShouldRender},
|
||||
task::{SystemTask, TaskType},
|
||||
},
|
||||
graphics::WlxGraphics,
|
||||
graphics::{CommandBuffers, WlxGraphics},
|
||||
overlays::{
|
||||
toast::{Toast, ToastTopic},
|
||||
watch::{watch_fade, WATCH_NAME},
|
||||
@@ -28,74 +32,91 @@ use crate::{
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
use crate::{gui::modular::button::WayVRAction, overlays::wayvr::wayvr_action};
|
||||
|
||||
mod helpers;
|
||||
mod input;
|
||||
mod lines;
|
||||
mod overlay;
|
||||
mod playspace;
|
||||
mod skybox;
|
||||
mod swapchain;
|
||||
|
||||
const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO;
|
||||
const VIEW_COUNT: u32 = 2;
|
||||
static FRAME_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
struct XrState {
|
||||
instance: xr::Instance,
|
||||
system: xr::SystemId,
|
||||
session: xr::Session<xr::Vulkan>,
|
||||
predicted_display_time: xr::Time,
|
||||
fps: f32,
|
||||
stage: Arc<xr::Space>,
|
||||
stage_offset: Affine3A,
|
||||
view: Arc<xr::Space>,
|
||||
}
|
||||
|
||||
pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
pub fn openxr_run(running: Arc<AtomicBool>, show_by_default: bool) -> Result<(), BackendError> {
|
||||
let (xr_instance, system) = match helpers::init_xr() {
|
||||
Ok((xr_instance, system)) => (xr_instance, system),
|
||||
Err(e) => {
|
||||
log::warn!("Will not use OpenXR: {}", e);
|
||||
log::warn!("Will not use OpenXR: {e}");
|
||||
return Err(BackendError::NotSupported);
|
||||
}
|
||||
};
|
||||
|
||||
let environment_blend_mode =
|
||||
xr_instance.enumerate_environment_blend_modes(system, VIEW_TYPE)?[0];
|
||||
log::info!("Using environment blend mode: {:?}", environment_blend_mode);
|
||||
|
||||
let mut app_state = {
|
||||
let mut app = {
|
||||
let graphics = WlxGraphics::new_openxr(xr_instance.clone(), system)?;
|
||||
AppState::from_graphics(graphics)?
|
||||
};
|
||||
|
||||
let mut overlays = OverlayContainer::<OpenXrOverlayData>::new(&mut app_state)?;
|
||||
let mut lines = LinePool::new(app_state.graphics.clone())?;
|
||||
let environment_blend_mode = {
|
||||
let modes = xr_instance.enumerate_environment_blend_modes(system, VIEW_TYPE)?;
|
||||
if modes.contains(&xr::EnvironmentBlendMode::ALPHA_BLEND)
|
||||
&& app.session.config.use_passthrough
|
||||
{
|
||||
xr::EnvironmentBlendMode::ALPHA_BLEND
|
||||
} else {
|
||||
modes[0]
|
||||
}
|
||||
};
|
||||
log::info!("Using environment blend mode: {environment_blend_mode:?}");
|
||||
|
||||
if show_by_default {
|
||||
app.tasks.enqueue_at(
|
||||
TaskType::System(SystemTask::ShowHide),
|
||||
Instant::now().add(Duration::from_secs(1)),
|
||||
);
|
||||
}
|
||||
|
||||
let mut overlays = OverlayContainer::<OpenXrOverlayData>::new(&mut app)?;
|
||||
let mut lines = LinePool::new(app.graphics.clone())?;
|
||||
|
||||
let mut notifications = NotificationManager::new();
|
||||
notifications.run_dbus();
|
||||
notifications.run_udp();
|
||||
|
||||
let mut delete_queue = vec![];
|
||||
let mut playspace = playspace::PlayspaceMover::try_new()
|
||||
.map_err(|e| log::warn!("Will not use Monado playspace mover: {}", e))
|
||||
|
||||
let mut monado = Monado::auto_connect()
|
||||
.map_err(|e| log::warn!("Will not use libmonado: {e}"))
|
||||
.ok();
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
let mut osc_sender =
|
||||
crate::backend::osc::OscSender::new(app_state.session.config.osc_out_port).ok();
|
||||
let mut playspace = monado.as_mut().and_then(|m| {
|
||||
playspace::PlayspaceMover::new(m)
|
||||
.map_err(|e| log::warn!("Will not use Monado playspace mover: {e}"))
|
||||
.ok()
|
||||
});
|
||||
|
||||
let (session, mut frame_wait, mut frame_stream) = unsafe {
|
||||
let raw_session = helpers::create_overlay_session(
|
||||
&xr_instance,
|
||||
system,
|
||||
&xr::vulkan::SessionCreateInfo {
|
||||
instance: app_state.graphics.instance.handle().as_raw() as _,
|
||||
physical_device: app_state
|
||||
.graphics
|
||||
.device
|
||||
.physical_device()
|
||||
.handle()
|
||||
.as_raw() as _,
|
||||
device: app_state.graphics.device.handle().as_raw() as _,
|
||||
queue_family_index: app_state.graphics.queue.queue_family_index(),
|
||||
instance: app.graphics.instance.handle().as_raw() as _,
|
||||
physical_device: app.graphics.device.physical_device().handle().as_raw() as _,
|
||||
device: app.graphics.device.handle().as_raw() as _,
|
||||
queue_family_index: app.graphics.graphics_queue.queue_family_index(),
|
||||
queue_index: 0,
|
||||
},
|
||||
)?;
|
||||
@@ -105,18 +126,26 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
let stage =
|
||||
session.create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)?;
|
||||
|
||||
let view = session.create_reference_space(xr::ReferenceSpaceType::VIEW, xr::Posef::IDENTITY)?;
|
||||
|
||||
let mut xr_state = XrState {
|
||||
instance: xr_instance,
|
||||
system,
|
||||
session,
|
||||
predicted_display_time: xr::Time::from_nanos(0),
|
||||
fps: 30.0,
|
||||
stage: Arc::new(stage),
|
||||
stage_offset: Affine3A::IDENTITY,
|
||||
view: Arc::new(view),
|
||||
};
|
||||
|
||||
let mut skybox = if environment_blend_mode == xr::EnvironmentBlendMode::OPAQUE {
|
||||
create_skybox(&xr_state, &app)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let pointer_lines = [
|
||||
lines.allocate(&xr_state, app_state.graphics.clone())?,
|
||||
lines.allocate(&xr_state, app_state.graphics.clone())?,
|
||||
lines.allocate(&xr_state, app.graphics.clone())?,
|
||||
lines.allocate(&xr_state, app.graphics.clone())?,
|
||||
];
|
||||
|
||||
let watch_id = overlays.get_by_name(WATCH_NAME).unwrap().state.id; // want panic
|
||||
@@ -126,27 +155,31 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
let mut session_running = false;
|
||||
let mut event_storage = xr::EventDataBuffer::new();
|
||||
|
||||
let mut next_device_update = Instant::now();
|
||||
let mut due_tasks = VecDeque::with_capacity(4);
|
||||
|
||||
let mut fps_counter: VecDeque<Instant> = VecDeque::new();
|
||||
|
||||
let mut main_session_visible = false;
|
||||
|
||||
'main_loop: loop {
|
||||
let cur_frame = FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
if !running.load(Ordering::Relaxed) {
|
||||
log::warn!("Received shutdown signal.");
|
||||
match xr_state.session.request_exit() {
|
||||
Ok(_) => log::info!("OpenXR session exit requested."),
|
||||
Ok(()) => log::info!("OpenXR session exit requested."),
|
||||
Err(xr::sys::Result::ERROR_SESSION_NOT_RUNNING) => break 'main_loop,
|
||||
Err(e) => {
|
||||
log::error!("Failed to request OpenXR session exit: {}", e);
|
||||
log::error!("Failed to request OpenXR session exit: {e}");
|
||||
break 'main_loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(event) = xr_state.instance.poll_event(&mut event_storage)? {
|
||||
use xr::Event::*;
|
||||
match event {
|
||||
SessionStateChanged(e) => {
|
||||
xr::Event::SessionStateChanged(e) => {
|
||||
// Session state change is where we can begin and end sessions, as well as
|
||||
// find quit messages!
|
||||
log::info!("entered state {:?}", e.state());
|
||||
@@ -165,16 +198,36 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
InstanceLossPending(_) => {
|
||||
xr::Event::InstanceLossPending(_) => {
|
||||
break 'main_loop;
|
||||
}
|
||||
EventsLost(e) => {
|
||||
xr::Event::EventsLost(e) => {
|
||||
log::warn!("lost {} events", e.lost_event_count());
|
||||
}
|
||||
xr::Event::MainSessionVisibilityChangedEXTX(e) => {
|
||||
if main_session_visible != e.visible() {
|
||||
main_session_visible = e.visible();
|
||||
log::info!("Main session visible: {main_session_visible}");
|
||||
if main_session_visible {
|
||||
log::debug!("Destroying skybox.");
|
||||
skybox = None;
|
||||
} else if environment_blend_mode == xr::EnvironmentBlendMode::OPAQUE {
|
||||
log::debug!("Allocating skybox.");
|
||||
skybox = create_skybox(&xr_state, &app);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if next_device_update <= Instant::now() {
|
||||
if let Some(monado) = &mut monado {
|
||||
OpenXrInputSource::update_devices(&mut app, monado);
|
||||
next_device_update = Instant::now() + Duration::from_secs(30);
|
||||
}
|
||||
}
|
||||
|
||||
if !session_running {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
continue 'main_loop;
|
||||
@@ -184,6 +237,23 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
frame_stream.begin()?;
|
||||
|
||||
xr_state.predicted_display_time = xr_frame_state.predicted_display_time;
|
||||
xr_state.fps = {
|
||||
fps_counter.push_back(Instant::now());
|
||||
|
||||
while let Some(time) = fps_counter.front() {
|
||||
if time.elapsed().as_secs_f32() > 1. {
|
||||
fps_counter.pop_front();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let total_elapsed = fps_counter
|
||||
.front()
|
||||
.map_or(0f32, |time| time.elapsed().as_secs_f32());
|
||||
|
||||
fps_counter.len() as f32 / total_elapsed
|
||||
};
|
||||
|
||||
if !xr_frame_state.should_render {
|
||||
frame_stream.end(
|
||||
@@ -194,32 +264,46 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
continue 'main_loop;
|
||||
}
|
||||
|
||||
app_state.input_state.pre_update();
|
||||
input_source.update(&xr_state, &mut app_state)?;
|
||||
app_state.input_state.post_update();
|
||||
app.input_state.pre_update();
|
||||
input_source.update(&xr_state, &mut app)?;
|
||||
app.input_state.post_update(&app.session);
|
||||
|
||||
if app_state
|
||||
if app
|
||||
.input_state
|
||||
.pointers
|
||||
.iter()
|
||||
.any(|p| p.now.show_hide && !p.before.show_hide)
|
||||
{
|
||||
overlays.show_hide(&mut app_state);
|
||||
overlays.show_hide(&mut app);
|
||||
}
|
||||
|
||||
watch_fade(&mut app_state, overlays.mut_by_id(watch_id).unwrap()); // want panic
|
||||
#[cfg(feature = "wayvr")]
|
||||
if app
|
||||
.input_state
|
||||
.pointers
|
||||
.iter()
|
||||
.any(|p| p.now.toggle_dashboard && !p.before.toggle_dashboard)
|
||||
{
|
||||
wayvr_action(&mut app, &mut overlays, &WayVRAction::ToggleDashboard);
|
||||
}
|
||||
|
||||
watch_fade(&mut app, overlays.mut_by_id(watch_id).unwrap()); // want panic
|
||||
if let Some(ref mut space_mover) = playspace {
|
||||
space_mover.update(&mut overlays, &app_state);
|
||||
space_mover.update(
|
||||
&mut overlays,
|
||||
&app,
|
||||
monado.as_mut().unwrap(), // safe
|
||||
);
|
||||
}
|
||||
|
||||
for o in overlays.iter_mut() {
|
||||
o.after_input(&mut app_state)?;
|
||||
o.after_input(&mut app)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
if let Some(ref mut sender) = osc_sender {
|
||||
let _ = sender.send_params(&overlays);
|
||||
};
|
||||
if let Some(ref mut sender) = app.osc_sender {
|
||||
let _ = sender.send_params(&overlays, &app.input_state.devices);
|
||||
}
|
||||
|
||||
let (_, views) = xr_state.session.locate_views(
|
||||
VIEW_TYPE,
|
||||
@@ -227,38 +311,37 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
&xr_state.stage,
|
||||
)?;
|
||||
|
||||
let (hmd, ipd) = helpers::hmd_pose_from_views(&views);
|
||||
app_state.input_state.hmd = hmd;
|
||||
if (app_state.input_state.ipd - ipd).abs() > 0.01 {
|
||||
log::info!("IPD changed: {} -> {}", app_state.input_state.ipd, ipd);
|
||||
app_state.input_state.ipd = ipd;
|
||||
let ipd = helpers::ipd_from_views(&views);
|
||||
if (app.input_state.ipd - ipd).abs() > 0.01 {
|
||||
log::info!("IPD changed: {} -> {}", app.input_state.ipd, ipd);
|
||||
app.input_state.ipd = ipd;
|
||||
Toast::new(
|
||||
ToastTopic::IpdChange,
|
||||
"IPD".into(),
|
||||
format!("{:.1} mm", ipd).into(),
|
||||
format!("{ipd:.1} mm").into(),
|
||||
)
|
||||
.submit(&mut app_state);
|
||||
.submit(&mut app);
|
||||
}
|
||||
|
||||
overlays
|
||||
.iter_mut()
|
||||
.for_each(|o| o.state.auto_movement(&mut app_state));
|
||||
.for_each(|o| o.state.auto_movement(&mut app));
|
||||
|
||||
let lengths_haptics = interact(&mut overlays, &mut app_state);
|
||||
let lengths_haptics = interact(&mut overlays, &mut app);
|
||||
for (idx, (len, haptics)) in lengths_haptics.iter().enumerate() {
|
||||
lines.draw_from(
|
||||
pointer_lines[idx],
|
||||
app_state.input_state.pointers[idx].pose,
|
||||
app.input_state.pointers[idx].pose,
|
||||
*len,
|
||||
app_state.input_state.pointers[idx].interaction.mode as usize + 1,
|
||||
&app_state.input_state.hmd,
|
||||
app.input_state.pointers[idx].interaction.mode as usize + 1,
|
||||
&app.input_state.hmd,
|
||||
);
|
||||
if let Some(haptics) = haptics {
|
||||
input_source.haptics(&xr_state, idx, haptics);
|
||||
}
|
||||
}
|
||||
|
||||
app_state.hid_provider.commit();
|
||||
app.hid_provider.commit();
|
||||
|
||||
let watch = overlays.mut_by_id(watch_id).unwrap(); // want panic
|
||||
let watch_transform = watch.state.transform;
|
||||
@@ -271,47 +354,108 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
});
|
||||
}
|
||||
|
||||
let mut layers = vec![];
|
||||
let mut command_buffer = app_state
|
||||
.graphics
|
||||
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
#[cfg(feature = "wayvr")]
|
||||
if let Err(e) =
|
||||
crate::overlays::wayvr::tick_events::<OpenXrOverlayData>(&mut app, &mut overlays)
|
||||
{
|
||||
log::error!("WayVR tick_events failed: {e:?}");
|
||||
}
|
||||
|
||||
// Begin rendering
|
||||
let mut buffers = CommandBuffers::default();
|
||||
|
||||
if !main_session_visible {
|
||||
if let Some(skybox) = skybox.as_mut() {
|
||||
skybox.render(&xr_state, &app, &mut buffers)?;
|
||||
}
|
||||
}
|
||||
|
||||
for o in overlays.iter_mut() {
|
||||
o.data.cur_visible = false;
|
||||
if !o.state.want_visible {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !o.data.init {
|
||||
o.init(&mut app_state)?;
|
||||
o.init(&mut app)?;
|
||||
o.data.init = true;
|
||||
}
|
||||
|
||||
o.render(&mut app_state)?;
|
||||
let should_render = match o.should_render(&mut app)? {
|
||||
ShouldRender::Should => true,
|
||||
ShouldRender::Can => (o.data.last_alpha - o.state.alpha).abs() > f32::EPSILON,
|
||||
ShouldRender::Unable => false, //try show old image if exists
|
||||
};
|
||||
|
||||
let dist_sq = (app_state.input_state.hmd.translation - o.state.transform.translation)
|
||||
.length_squared()
|
||||
+ (100f32 - o.state.z_order as f32);
|
||||
|
||||
if !dist_sq.is_normal() {
|
||||
if should_render {
|
||||
if !o.ensure_swapchain(&app, &xr_state)? {
|
||||
continue;
|
||||
}
|
||||
let tgt = o.data.swapchain.as_mut().unwrap().acquire_wait_image()?; // want
|
||||
if !o.render(&mut app, tgt, &mut buffers, o.state.alpha)? {
|
||||
o.data.swapchain.as_mut().unwrap().ensure_image_released()?; // want
|
||||
continue;
|
||||
}
|
||||
o.data.last_alpha = o.state.alpha;
|
||||
} else if o.data.swapchain.is_none() {
|
||||
continue;
|
||||
}
|
||||
o.data.cur_visible = true;
|
||||
}
|
||||
|
||||
let maybe_layer = o.present_xr(&xr_state, &mut command_buffer)?;
|
||||
if let CompositionLayer::None = maybe_layer {
|
||||
lines.render(app.graphics.clone(), &mut buffers)?;
|
||||
|
||||
let future = buffers.execute_now(app.graphics.graphics_queue.clone())?;
|
||||
if let Some(mut future) = future {
|
||||
if let Err(e) = future.flush() {
|
||||
return Err(BackendError::Fatal(e.into()));
|
||||
}
|
||||
future.cleanup_finished();
|
||||
}
|
||||
// End rendering
|
||||
|
||||
// Layer composition
|
||||
let mut layers = vec![];
|
||||
if !main_session_visible {
|
||||
if let Some(skybox) = skybox.as_mut() {
|
||||
for (idx, layer) in skybox.present(&xr_state, &app)?.into_iter().enumerate() {
|
||||
layers.push(((idx as f32).mul_add(-50.0, 200.0), layer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for o in overlays.iter_mut() {
|
||||
if !o.data.cur_visible {
|
||||
continue;
|
||||
}
|
||||
let dist_sq = (app.input_state.hmd.translation - o.state.transform.translation)
|
||||
.length_squared()
|
||||
+ (100f32 - o.state.z_order as f32);
|
||||
if !dist_sq.is_normal() {
|
||||
o.data.swapchain.as_mut().unwrap().ensure_image_released()?;
|
||||
continue;
|
||||
}
|
||||
let maybe_layer = o.present(&xr_state)?;
|
||||
if matches!(maybe_layer, CompositionLayer::None) {
|
||||
continue;
|
||||
}
|
||||
layers.push((dist_sq, maybe_layer));
|
||||
}
|
||||
|
||||
for maybe_layer in lines.present_xr(&xr_state, &mut command_buffer)? {
|
||||
if let CompositionLayer::None = maybe_layer {
|
||||
for maybe_layer in lines.present(&xr_state)? {
|
||||
if matches!(maybe_layer, CompositionLayer::None) {
|
||||
continue;
|
||||
}
|
||||
layers.push((0.0, maybe_layer));
|
||||
}
|
||||
// End layer composition
|
||||
|
||||
command_buffer.build_and_execute_now()?;
|
||||
#[cfg(feature = "wayvr")]
|
||||
if let Some(wayvr) = &app.wayvr {
|
||||
wayvr.borrow_mut().data.tick_finish()?;
|
||||
}
|
||||
|
||||
// Begin layer submit
|
||||
layers.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||
|
||||
let frame_ref = layers
|
||||
@@ -319,6 +463,7 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
.map(|f| match f.1 {
|
||||
CompositionLayer::Quad(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
|
||||
CompositionLayer::Cylinder(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
|
||||
CompositionLayer::Equirect2(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
|
||||
CompositionLayer::None => unreachable!(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -328,23 +473,23 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
environment_blend_mode,
|
||||
&frame_ref,
|
||||
)?;
|
||||
// End layer submit
|
||||
|
||||
let removed_overlays = overlays.update(&mut app_state)?;
|
||||
let removed_overlays = overlays.update(&mut app)?;
|
||||
for o in removed_overlays {
|
||||
delete_queue.push((o, cur_frame + 5));
|
||||
}
|
||||
|
||||
notifications.submit_pending(&mut app_state);
|
||||
notifications.submit_pending(&mut app);
|
||||
|
||||
app_state.tasks.retrieve_due(&mut due_tasks);
|
||||
app.tasks.retrieve_due(&mut due_tasks);
|
||||
while let Some(task) = due_tasks.pop_front() {
|
||||
match task {
|
||||
TaskType::Global(f) => f(&mut app_state),
|
||||
TaskType::Overlay(sel, f) => {
|
||||
if let Some(o) = overlays.mut_by_selector(&sel) {
|
||||
f(&mut app_state, &mut o.state);
|
||||
f(&mut app, &mut o.state);
|
||||
} else {
|
||||
log::warn!("Overlay not found for task: {:?}", sel);
|
||||
log::warn!("Overlay not found for task: {sel:?}");
|
||||
}
|
||||
}
|
||||
TaskType::CreateOverlay(sel, f) => {
|
||||
@@ -352,20 +497,21 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some((mut state, backend)) = f(&mut app_state) else {
|
||||
let Some((mut overlay_state, overlay_backend)) = f(&mut app) else {
|
||||
continue;
|
||||
};
|
||||
state.birthframe = cur_frame;
|
||||
overlay_state.birthframe = cur_frame;
|
||||
|
||||
overlays.add(OverlayData {
|
||||
state,
|
||||
backend,
|
||||
state: overlay_state,
|
||||
backend: overlay_backend,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
TaskType::DropOverlay(sel) => {
|
||||
if let Some(o) = overlays.mut_by_selector(&sel) {
|
||||
if o.state.birthframe < cur_frame {
|
||||
log::debug!("{}: destroy", o.state.name);
|
||||
if let Some(o) = overlays.remove_by_selector(&sel) {
|
||||
// set for deletion after all images are done showing
|
||||
delete_queue.push((o, cur_frame + 5));
|
||||
@@ -376,16 +522,26 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
TaskType::System(task) => match task {
|
||||
SystemTask::FixFloor => {
|
||||
if let Some(ref mut playspace) = playspace {
|
||||
playspace.fix_floor(&app_state.input_state);
|
||||
playspace.fix_floor(
|
||||
&app.input_state,
|
||||
monado.as_mut().unwrap(), // safe
|
||||
);
|
||||
}
|
||||
}
|
||||
SystemTask::ResetPlayspace => {
|
||||
if let Some(ref mut playspace) = playspace {
|
||||
playspace.reset_offset();
|
||||
playspace.reset_offset(monado.as_mut().unwrap()); // safe
|
||||
}
|
||||
}
|
||||
SystemTask::ShowHide => {
|
||||
overlays.show_hide(&mut app);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
#[cfg(feature = "wayvr")]
|
||||
TaskType::WayVR(action) => {
|
||||
wayvr_action(&mut app, &mut overlays, &action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,4 +558,5 @@ pub(super) enum CompositionLayer<'a> {
|
||||
None,
|
||||
Quad(xr::CompositionLayerQuad<'a, xr::Vulkan>),
|
||||
Cylinder(xr::CompositionLayerCylinderKHR<'a, xr::Vulkan>),
|
||||
Equirect2(xr::CompositionLayerEquirect2KHR<'a, xr::Vulkan>),
|
||||
}
|
||||
@@ -12,9 +12,15 @@
|
||||
// -- space_drag --
|
||||
// move your stage (playspace drag)
|
||||
//
|
||||
// -- toggle_dashboard --
|
||||
// run or toggle visibility of a previously configured WayVR-compatible dashboard
|
||||
//
|
||||
// -- space_rotate --
|
||||
// rotate your stage (playspace rotate, WIP)
|
||||
//
|
||||
// -- space_reset --
|
||||
// reset your stage (reset the offset from playspace drag)
|
||||
//
|
||||
// -- click_modifier_right --
|
||||
// while this is held, your pointer will turn ORANGE and your mouse clicks will be RIGHT clicks
|
||||
//
|
||||
@@ -75,6 +81,10 @@
|
||||
left: "/user/hand/left/input/thumbstick/y",
|
||||
right: "/user/hand/right/input/thumbstick/y"
|
||||
},
|
||||
scroll_horizontal: {
|
||||
left: "/user/hand/left/input/thumbstick/x",
|
||||
right: "/user/hand/right/input/thumbstick/x"
|
||||
},
|
||||
show_hide: {
|
||||
double_click: true,
|
||||
left: "/user/hand/left/input/y/click",
|
||||
@@ -124,6 +134,14 @@
|
||||
left: "/user/hand/left/input/thumbstick/y",
|
||||
right: "/user/hand/right/input/thumbstick/y"
|
||||
},
|
||||
scroll_horizontal: {
|
||||
left: "/user/hand/left/input/thumbstick/x",
|
||||
right: "/user/hand/right/input/thumbstick/x"
|
||||
},
|
||||
toggle_dashboard: {
|
||||
double_click: false,
|
||||
right: "/user/hand/right/input/system/click",
|
||||
},
|
||||
show_hide: {
|
||||
double_click: true,
|
||||
left: "/user/hand/left/input/b/click",
|
||||
@@ -132,6 +150,10 @@
|
||||
left: "/user/hand/left/input/trackpad/force",
|
||||
// right trackpad is alt_click
|
||||
},
|
||||
space_reset: {
|
||||
double_click: true,
|
||||
left: "/user/hand/left/input/trackpad/force",
|
||||
},
|
||||
click_modifier_right: {
|
||||
left: "/user/hand/left/input/b/touch",
|
||||
right: "/user/hand/right/input/b/touch"
|
||||
@@ -166,6 +188,10 @@
|
||||
left: "/user/hand/left/input/trackpad/y",
|
||||
right: "/user/hand/right/input/trackpad/y"
|
||||
},
|
||||
scroll_horizontal: {
|
||||
left: "/user/hand/left/input/trackpad/x",
|
||||
right: "/user/hand/right/input/trackpad/x"
|
||||
},
|
||||
show_hide: {
|
||||
left: "/user/hand/left/input/menu/click",
|
||||
},
|
||||
@@ -201,6 +227,10 @@
|
||||
left: "/user/hand/left/input/thumbstick/y",
|
||||
right: "/user/hand/right/input/thumbstick/y"
|
||||
},
|
||||
scroll_horizontal: {
|
||||
left: "/user/hand/left/input/thumbstick/x",
|
||||
right: "/user/hand/right/input/thumbstick/x"
|
||||
},
|
||||
show_hide: {
|
||||
left: "/user/hand/left/input/menu/click",
|
||||
},
|
||||
@@ -232,6 +262,10 @@
|
||||
left: "/user/hand/left/input/thumbstick/y",
|
||||
right: "/user/hand/right/input/thumbstick/y"
|
||||
},
|
||||
scroll_horizontal: {
|
||||
left: "/user/hand/left/input/thumbstick/x",
|
||||
right: "/user/hand/right/input/thumbstick/x"
|
||||
},
|
||||
show_hide: {
|
||||
left: "/user/hand/left/input/menu/click",
|
||||
},
|
||||
|
||||
@@ -1,80 +1,90 @@
|
||||
use glam::Vec3A;
|
||||
use openxr as xr;
|
||||
use std::{f32::consts::PI, sync::Arc};
|
||||
use openxr::{self as xr, CompositionLayerFlags};
|
||||
use std::f32::consts::PI;
|
||||
use xr::EyeVisibility;
|
||||
|
||||
use super::{helpers, swapchain::SwapchainRenderData, CompositionLayer, XrState};
|
||||
use super::{helpers, swapchain::WlxSwapchain, CompositionLayer, XrState};
|
||||
use crate::{
|
||||
backend::{openxr::swapchain::create_swapchain_render_data, overlay::OverlayData},
|
||||
graphics::WlxCommandBuffer,
|
||||
backend::{
|
||||
openxr::swapchain::{create_swapchain, SwapchainOpts},
|
||||
overlay::OverlayData,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
use vulkano::image::view::ImageView;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct OpenXrOverlayData {
|
||||
last_view: Option<Arc<ImageView>>,
|
||||
last_visible: bool,
|
||||
pub(super) swapchain: Option<SwapchainRenderData>,
|
||||
pub(super) swapchain: Option<WlxSwapchain>,
|
||||
pub(super) init: bool,
|
||||
pub(super) cur_visible: bool,
|
||||
pub(super) last_alpha: f32,
|
||||
}
|
||||
|
||||
impl OverlayData<OpenXrOverlayData> {
|
||||
pub(super) fn present_xr<'a>(
|
||||
pub(super) fn ensure_swapchain<'a>(
|
||||
&'a mut self,
|
||||
app: &AppState,
|
||||
xr: &'a XrState,
|
||||
command_buffer: &mut WlxCommandBuffer,
|
||||
) -> anyhow::Result<CompositionLayer> {
|
||||
if let Some(new_view) = self.view() {
|
||||
self.data.last_view = Some(new_view);
|
||||
) -> anyhow::Result<bool> {
|
||||
if self.data.swapchain.is_some() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let my_view = if let Some(view) = self.data.last_view.as_ref() {
|
||||
view.clone()
|
||||
} else {
|
||||
log::warn!("{}: Will not show - image not ready", self.state.name);
|
||||
let Some(meta) = self.frame_meta() else {
|
||||
log::warn!(
|
||||
"{}: swapchain cannot be created due to missing metadata",
|
||||
self.state.name
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let extent = meta.extent;
|
||||
self.data.swapchain = Some(create_swapchain(
|
||||
xr,
|
||||
app.graphics.clone(),
|
||||
extent,
|
||||
SwapchainOpts::new(),
|
||||
)?);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(super) fn present<'a>(
|
||||
&'a mut self,
|
||||
xr: &'a XrState,
|
||||
) -> anyhow::Result<CompositionLayer<'a>> {
|
||||
let Some(swapchain) = self.data.swapchain.as_mut() else {
|
||||
log::warn!("{}: swapchain not ready", self.state.name);
|
||||
return Ok(CompositionLayer::None);
|
||||
};
|
||||
let extent = my_view.image().extent();
|
||||
if !swapchain.ever_acquired {
|
||||
log::warn!("{}: swapchain not rendered", self.state.name);
|
||||
return Ok(CompositionLayer::None);
|
||||
}
|
||||
swapchain.ensure_image_released()?;
|
||||
|
||||
let data = match self.data.swapchain {
|
||||
Some(ref mut data) => data,
|
||||
None => {
|
||||
let srd =
|
||||
create_swapchain_render_data(xr, command_buffer.graphics.clone(), extent)?;
|
||||
log::debug!(
|
||||
"{}: Created swapchain {}x{}, {} images, {} MB",
|
||||
self.state.name,
|
||||
extent[0],
|
||||
extent[1],
|
||||
srd.images.len(),
|
||||
extent[0] * extent[1] * 4 * srd.images.len() as u32 / 1024 / 1024
|
||||
);
|
||||
self.data.swapchain = Some(srd);
|
||||
self.data.swapchain.as_mut().unwrap() //safe
|
||||
}
|
||||
};
|
||||
let sub_image = swapchain.get_subimage();
|
||||
let transform = self.state.transform * self.backend.frame_meta().unwrap().transform; // contract
|
||||
|
||||
let sub_image = data.acquire_present_release(command_buffer, my_view, self.state.alpha)?;
|
||||
|
||||
let aspect_ratio = extent[1] as f32 / extent[0] as f32;
|
||||
let aspect_ratio = swapchain.extent[1] as f32 / swapchain.extent[0] as f32;
|
||||
let (scale_x, scale_y) = if aspect_ratio < 1.0 {
|
||||
let major = self.state.transform.matrix3.col(0).length();
|
||||
let major = transform.matrix3.col(0).length();
|
||||
(major, major * aspect_ratio)
|
||||
} else {
|
||||
let major = self.state.transform.matrix3.col(1).length();
|
||||
let major = transform.matrix3.col(1).length();
|
||||
(major / aspect_ratio, major)
|
||||
};
|
||||
|
||||
if let Some(curvature) = self.state.curvature {
|
||||
let radius = scale_x / (2.0 * PI * curvature);
|
||||
let quat = helpers::transform_to_norm_quat(&self.state.transform);
|
||||
let center_point = self.state.transform.translation + quat.mul_vec3a(Vec3A::Z * radius);
|
||||
let quat = helpers::transform_to_norm_quat(&transform);
|
||||
let center_point = transform.translation + quat.mul_vec3a(Vec3A::Z * radius);
|
||||
|
||||
let posef = helpers::translation_rotation_to_posef(center_point, quat);
|
||||
let angle = 2.0 * (scale_x / (2.0 * radius));
|
||||
|
||||
let cylinder = xr::CompositionLayerCylinderKHR::new()
|
||||
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
|
||||
.pose(posef)
|
||||
.sub_image(sub_image)
|
||||
.eye_visibility(EyeVisibility::BOTH)
|
||||
@@ -84,8 +94,9 @@ impl OverlayData<OpenXrOverlayData> {
|
||||
.aspect_ratio(aspect_ratio);
|
||||
Ok(CompositionLayer::Cylinder(cylinder))
|
||||
} else {
|
||||
let posef = helpers::transform_to_posef(&self.state.transform);
|
||||
let posef = helpers::transform_to_posef(&transform);
|
||||
let quad = xr::CompositionLayerQuad::new()
|
||||
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
|
||||
.pose(posef)
|
||||
.sub_image(sub_image)
|
||||
.eye_visibility(EyeVisibility::BOTH)
|
||||
|
||||
+113
-94
@@ -1,20 +1,12 @@
|
||||
use std::ffi::c_void;
|
||||
|
||||
use glam::{Affine3A, Quat, Vec3A};
|
||||
use libloading::{Library, Symbol};
|
||||
use libmonado::{Monado, Pose, ReferenceSpaceType};
|
||||
|
||||
use crate::{
|
||||
backend::{common::OverlayContainer, input::InputState},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::{helpers, overlay::OpenXrOverlayData};
|
||||
|
||||
#[repr(C)]
|
||||
struct XrtPose {
|
||||
orientation: [f32; 4],
|
||||
position: [f32; 3],
|
||||
}
|
||||
use super::overlay::OpenXrOverlayData;
|
||||
|
||||
struct MoverData<T> {
|
||||
pose: Affine3A,
|
||||
@@ -22,68 +14,99 @@ struct MoverData<T> {
|
||||
hand_pose: T,
|
||||
}
|
||||
|
||||
// Legacy implementation
|
||||
type PlaySpaceMove = extern "C" fn(*mut c_void, f32, f32, f32) -> i32;
|
||||
|
||||
// New implementation
|
||||
type ApplyStageOffset = extern "C" fn(*mut c_void, *const XrtPose) -> i32;
|
||||
|
||||
enum ApiImpl {
|
||||
None,
|
||||
PlaySpaceMove(PlaySpaceMove),
|
||||
ApplyStageOffset(ApplyStageOffset),
|
||||
}
|
||||
|
||||
pub(super) struct PlayspaceMover {
|
||||
last_transform: Affine3A,
|
||||
drag: Option<MoverData<Vec3A>>,
|
||||
|
||||
libmonado: Library,
|
||||
mnd_root: *mut c_void,
|
||||
api_impl: ApiImpl,
|
||||
rotate: Option<MoverData<Quat>>,
|
||||
}
|
||||
|
||||
impl PlayspaceMover {
|
||||
pub fn try_new() -> anyhow::Result<Self> {
|
||||
unsafe {
|
||||
let libmonado = helpers::find_libmonado()?;
|
||||
pub fn new(monado: &mut Monado) -> anyhow::Result<Self> {
|
||||
log::info!("Monado: using space offset API");
|
||||
|
||||
let root_create: Symbol<extern "C" fn(*mut *mut c_void) -> i32> =
|
||||
libmonado.get(b"mnd_root_create\0")?;
|
||||
let Ok(stage) = monado.get_reference_space_offset(ReferenceSpaceType::Stage) else {
|
||||
anyhow::bail!("Space offsets not supported.");
|
||||
};
|
||||
|
||||
let mut api_impl = ApiImpl::None;
|
||||
if let Ok(playspace_move) = libmonado.get(b"mnd_root_playspace_move\0") {
|
||||
log::info!("Monado: using playspace_move");
|
||||
api_impl = ApiImpl::PlaySpaceMove(*playspace_move);
|
||||
} else if let Ok(apply_stage_offset) = libmonado.get(b"mnd_root_apply_stage_offset\0") {
|
||||
log::info!("Monado: using apply_stage_offset");
|
||||
api_impl = ApiImpl::ApplyStageOffset(*apply_stage_offset);
|
||||
}
|
||||
log::debug!("STAGE is at {:?}, {:?}", stage.position, stage.orientation);
|
||||
|
||||
if let ApiImpl::None = api_impl {
|
||||
anyhow::bail!("Monado does not support playspace mover.");
|
||||
}
|
||||
// initial offset
|
||||
let last_transform =
|
||||
Affine3A::from_rotation_translation(stage.orientation.into(), stage.position.into());
|
||||
|
||||
let mut root: *mut c_void = std::ptr::null_mut();
|
||||
Ok(Self {
|
||||
last_transform,
|
||||
|
||||
let ret = root_create(&mut root);
|
||||
|
||||
if ret != 0 {
|
||||
anyhow::bail!("Failed to create root, code: {}", ret);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
last_transform: Affine3A::IDENTITY,
|
||||
drag: None,
|
||||
|
||||
libmonado,
|
||||
mnd_root: root,
|
||||
api_impl,
|
||||
})
|
||||
}
|
||||
drag: None,
|
||||
rotate: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, overlays: &mut OverlayContainer<OpenXrOverlayData>, state: &AppState) {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
overlays: &mut OverlayContainer<OpenXrOverlayData>,
|
||||
state: &AppState,
|
||||
monado: &mut Monado,
|
||||
) {
|
||||
for pointer in &state.input_state.pointers {
|
||||
if pointer.now.space_reset {
|
||||
if !pointer.before.space_reset {
|
||||
log::info!("Space reset");
|
||||
self.reset_offset(monado);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut data) = self.rotate.take() {
|
||||
let pointer = &state.input_state.pointers[data.hand];
|
||||
if !pointer.now.space_rotate {
|
||||
self.last_transform = data.pose;
|
||||
log::info!("End space rotate");
|
||||
return;
|
||||
}
|
||||
|
||||
let new_hand =
|
||||
Quat::from_affine3(&(data.pose * state.input_state.pointers[data.hand].raw_pose));
|
||||
|
||||
let dq = new_hand * data.hand_pose.conjugate();
|
||||
let mut space_transform = if state.session.config.space_rotate_unlocked {
|
||||
Affine3A::from_quat(dq)
|
||||
} else {
|
||||
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(state.input_state.hmd.translation)
|
||||
- state.input_state.hmd.translation)
|
||||
* -1.0;
|
||||
|
||||
space_transform.translation = offset;
|
||||
|
||||
data.pose *= space_transform;
|
||||
data.hand_pose = new_hand;
|
||||
|
||||
apply_offset(data.pose, monado);
|
||||
self.rotate = Some(data);
|
||||
} else {
|
||||
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
|
||||
if pointer.now.space_rotate {
|
||||
let hand_pose = Quat::from_affine3(&(self.last_transform * pointer.raw_pose));
|
||||
self.rotate = Some(MoverData {
|
||||
pose: self.last_transform,
|
||||
hand: i,
|
||||
hand_pose,
|
||||
});
|
||||
self.drag = None;
|
||||
log::info!("Start space rotate");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut data) = self.drag.take() {
|
||||
let pointer = &state.input_state.pointers[data.hand];
|
||||
if !pointer.now.space_drag {
|
||||
@@ -94,8 +117,9 @@ impl PlayspaceMover {
|
||||
|
||||
let new_hand = data
|
||||
.pose
|
||||
.transform_point3a(state.input_state.pointers[data.hand].pose.translation);
|
||||
let relative_pos = new_hand - data.hand_pose;
|
||||
.transform_point3a(state.input_state.pointers[data.hand].raw_pose.translation);
|
||||
let relative_pos =
|
||||
(new_hand - data.hand_pose) * state.session.config.space_drag_multiplier;
|
||||
|
||||
if relative_pos.length_squared() > 1000.0 {
|
||||
log::warn!("Space drag too fast, ignoring");
|
||||
@@ -114,14 +138,14 @@ impl PlayspaceMover {
|
||||
data.pose.translation += relative_pos;
|
||||
data.hand_pose = new_hand;
|
||||
|
||||
self.apply_offset(data.pose);
|
||||
apply_offset(data.pose, monado);
|
||||
self.drag = Some(data);
|
||||
} else {
|
||||
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
|
||||
if pointer.now.space_drag {
|
||||
let hand_pos = self
|
||||
.last_transform
|
||||
.transform_point3a(pointer.pose.translation);
|
||||
.transform_point3a(pointer.raw_pose.translation);
|
||||
self.drag = Some(MoverData {
|
||||
pose: self.last_transform,
|
||||
hand: i,
|
||||
@@ -134,47 +158,42 @@ impl PlayspaceMover {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_offset(&mut self) {
|
||||
pub fn reset_offset(&mut self, monado: &mut Monado) {
|
||||
if self.drag.is_some() {
|
||||
log::info!("Cannot reset offset while dragging.");
|
||||
return;
|
||||
log::info!("Space drag interrupted by manual reset");
|
||||
self.drag = None;
|
||||
}
|
||||
if self.rotate.is_some() {
|
||||
log::info!("Space rotate interrupted by manual reset");
|
||||
self.rotate = None;
|
||||
}
|
||||
|
||||
self.last_transform = Affine3A::IDENTITY;
|
||||
self.apply_offset(self.last_transform);
|
||||
apply_offset(self.last_transform, monado);
|
||||
}
|
||||
|
||||
pub fn fix_floor(&mut self, input: &InputState) {
|
||||
pub fn fix_floor(&mut self, input: &InputState, monado: &mut Monado) {
|
||||
if self.drag.is_some() {
|
||||
log::info!("Cannot fix floor while dragging.");
|
||||
return;
|
||||
log::info!("Space drag interrupted by fix floor");
|
||||
self.drag = None;
|
||||
}
|
||||
if self.rotate.is_some() {
|
||||
log::info!("Space rotate interrupted by fix floor");
|
||||
self.rotate = None;
|
||||
}
|
||||
|
||||
let y1 = input.pointers[0].pose.translation.y;
|
||||
let y2 = input.pointers[1].pose.translation.y;
|
||||
let y1 = input.pointers[0].raw_pose.translation.y;
|
||||
let y2 = input.pointers[1].raw_pose.translation.y;
|
||||
let delta = y1.min(y2) - 0.03;
|
||||
self.last_transform.translation.y += delta;
|
||||
self.apply_offset(self.last_transform);
|
||||
}
|
||||
|
||||
fn apply_offset(&self, transform: Affine3A) {
|
||||
match self.api_impl {
|
||||
ApiImpl::PlaySpaceMove(playspace_move) => {
|
||||
(playspace_move)(
|
||||
self.mnd_root,
|
||||
transform.translation.x,
|
||||
transform.translation.y,
|
||||
transform.translation.z,
|
||||
);
|
||||
}
|
||||
ApiImpl::ApplyStageOffset(apply_stage_offset) => {
|
||||
let xrt_pose = XrtPose {
|
||||
orientation: Quat::from_affine3(&transform).into(),
|
||||
position: transform.translation.into(),
|
||||
};
|
||||
(apply_stage_offset)(self.mnd_root, &xrt_pose);
|
||||
}
|
||||
ApiImpl::None => {}
|
||||
}
|
||||
apply_offset(self.last_transform, monado);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_offset(transform: Affine3A, monado: &mut Monado) {
|
||||
let pose = Pose {
|
||||
position: transform.translation.into(),
|
||||
orientation: Quat::from_affine3(&transform).into(),
|
||||
};
|
||||
let _ = monado.set_reference_space_offset(ReferenceSpaceType::Stage, pose);
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
use std::{
|
||||
f32::consts::PI,
|
||||
fs::File,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use glam::{Quat, Vec3A};
|
||||
use openxr::{self as xr, CompositionLayerFlags};
|
||||
use vulkano::{
|
||||
command_buffer::CommandBufferUsage, image::view::ImageView,
|
||||
pipeline::graphics::color_blend::AttachmentBlend,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::openxr::{helpers::translation_rotation_to_posef, swapchain::SwapchainOpts},
|
||||
config_io,
|
||||
graphics::{dds::WlxCommandBufferDds, CommandBuffers, SWAPCHAIN_FORMAT},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::{
|
||||
swapchain::{create_swapchain, WlxSwapchain},
|
||||
CompositionLayer, XrState,
|
||||
};
|
||||
|
||||
pub(super) struct Skybox {
|
||||
view: Arc<ImageView>,
|
||||
sky: Option<WlxSwapchain>,
|
||||
grid: Option<WlxSwapchain>,
|
||||
}
|
||||
|
||||
impl Skybox {
|
||||
pub fn new(app: &AppState) -> anyhow::Result<Self> {
|
||||
let mut command_buffer = app.graphics.create_uploads_command_buffer(
|
||||
app.graphics.transfer_queue.clone(),
|
||||
CommandBufferUsage::OneTimeSubmit,
|
||||
)?;
|
||||
|
||||
let mut maybe_image = None;
|
||||
|
||||
'custom_tex: {
|
||||
if app.session.config.skybox_texture.is_empty() {
|
||||
break 'custom_tex;
|
||||
}
|
||||
|
||||
let real_path = config_io::get_config_root().join(&*app.session.config.skybox_texture);
|
||||
let Ok(f) = File::open(real_path) else {
|
||||
log::warn!(
|
||||
"Could not open custom skybox texture at: {}",
|
||||
app.session.config.skybox_texture
|
||||
);
|
||||
break 'custom_tex;
|
||||
};
|
||||
match command_buffer.texture2d_dds(f) {
|
||||
Ok(image) => {
|
||||
maybe_image = Some(image);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Could not use custom skybox texture at: {}",
|
||||
app.session.config.skybox_texture
|
||||
);
|
||||
log::warn!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maybe_image.is_none() {
|
||||
let p = include_bytes!("../../res/table_mountain_2.dds");
|
||||
maybe_image = Some(command_buffer.texture2d_dds(p.as_slice())?);
|
||||
}
|
||||
|
||||
command_buffer.build_and_execute_now()?;
|
||||
|
||||
let view = ImageView::new_default(maybe_image.unwrap())?; // safe unwrap
|
||||
|
||||
Ok(Self {
|
||||
view,
|
||||
sky: None,
|
||||
grid: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_sky<'a>(
|
||||
&'a mut self,
|
||||
xr: &'a XrState,
|
||||
app: &AppState,
|
||||
buf: &mut CommandBuffers,
|
||||
) -> anyhow::Result<()> {
|
||||
if self.sky.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let opts = SwapchainOpts::new().immutable();
|
||||
|
||||
let Ok(shaders) = app.graphics.shared_shaders.read() else {
|
||||
anyhow::bail!("Failed to lock shared shaders for reading");
|
||||
};
|
||||
|
||||
let extent = self.view.image().extent();
|
||||
let mut swapchain = create_swapchain(xr, app.graphics.clone(), extent, opts)?;
|
||||
let tgt = swapchain.acquire_wait_image()?;
|
||||
let pipeline = app.graphics.create_pipeline(
|
||||
shaders.get("vert_common").unwrap().clone(), // want panic
|
||||
shaders.get("frag_srgb").unwrap().clone(), // want panic
|
||||
SWAPCHAIN_FORMAT,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let set0 =
|
||||
pipeline.uniform_sampler(0, self.view.clone(), app.graphics.texture_filtering)?;
|
||||
|
||||
let set1 = pipeline.uniform_buffer(1, vec![1f32])?;
|
||||
|
||||
let pass = pipeline.create_pass_for_target(tgt.clone(), vec![set0, set1])?;
|
||||
|
||||
let mut cmd_buffer = app
|
||||
.graphics
|
||||
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
cmd_buffer.begin_rendering(tgt)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
cmd_buffer.end_rendering()?;
|
||||
|
||||
buf.push(cmd_buffer.build()?);
|
||||
|
||||
self.sky = Some(swapchain);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_grid<'a>(
|
||||
&'a mut self,
|
||||
xr: &'a XrState,
|
||||
app: &AppState,
|
||||
buf: &mut CommandBuffers,
|
||||
) -> anyhow::Result<()> {
|
||||
if self.grid.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let Ok(shaders) = app.graphics.shared_shaders.read() else {
|
||||
anyhow::bail!("Failed to lock shared shaders for reading");
|
||||
};
|
||||
|
||||
let extent = [1024, 1024, 1];
|
||||
let mut swapchain = create_swapchain(
|
||||
xr,
|
||||
app.graphics.clone(),
|
||||
extent,
|
||||
SwapchainOpts::new().immutable(),
|
||||
)?;
|
||||
let pipeline = app.graphics.create_pipeline(
|
||||
shaders.get("vert_common").unwrap().clone(), // want panic
|
||||
shaders.get("frag_grid").unwrap().clone(), // want panic
|
||||
SWAPCHAIN_FORMAT,
|
||||
Some(AttachmentBlend::alpha()),
|
||||
)?;
|
||||
|
||||
let tgt = swapchain.acquire_wait_image()?;
|
||||
let pass = pipeline.create_pass_for_target(tgt.clone(), vec![])?;
|
||||
|
||||
let mut cmd_buffer = app
|
||||
.graphics
|
||||
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
cmd_buffer.begin_rendering(tgt)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
cmd_buffer.end_rendering()?;
|
||||
|
||||
buf.push(cmd_buffer.build()?);
|
||||
|
||||
self.grid = Some(swapchain);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn render(
|
||||
&mut self,
|
||||
xr: &XrState,
|
||||
app: &AppState,
|
||||
buf: &mut CommandBuffers,
|
||||
) -> anyhow::Result<()> {
|
||||
self.prepare_sky(xr, app, buf)?;
|
||||
self.prepare_grid(xr, app, buf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn present<'a>(
|
||||
&'a mut self,
|
||||
xr: &'a XrState,
|
||||
app: &AppState,
|
||||
) -> anyhow::Result<Vec<CompositionLayer<'a>>> {
|
||||
// cover the entire sphere
|
||||
const HORIZ_ANGLE: f32 = 2.0 * PI;
|
||||
const HI_VERT_ANGLE: f32 = 0.5 * PI;
|
||||
const LO_VERT_ANGLE: f32 = -0.5 * PI;
|
||||
|
||||
static GRID_POSE: LazyLock<xr::Posef> = LazyLock::new(|| {
|
||||
translation_rotation_to_posef(Vec3A::ZERO, Quat::from_rotation_x(PI * -0.5))
|
||||
});
|
||||
|
||||
let pose = xr::Posef {
|
||||
orientation: xr::Quaternionf::IDENTITY,
|
||||
position: xr::Vector3f {
|
||||
x: app.input_state.hmd.translation.x,
|
||||
y: app.input_state.hmd.translation.y,
|
||||
z: app.input_state.hmd.translation.z,
|
||||
},
|
||||
};
|
||||
|
||||
self.sky.as_mut().unwrap().ensure_image_released()?;
|
||||
|
||||
let sky = xr::CompositionLayerEquirect2KHR::new()
|
||||
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
|
||||
.pose(pose)
|
||||
.radius(10.0)
|
||||
.sub_image(self.sky.as_ref().unwrap().get_subimage())
|
||||
.eye_visibility(xr::EyeVisibility::BOTH)
|
||||
.space(&xr.stage)
|
||||
.central_horizontal_angle(HORIZ_ANGLE)
|
||||
.upper_vertical_angle(HI_VERT_ANGLE)
|
||||
.lower_vertical_angle(LO_VERT_ANGLE);
|
||||
|
||||
self.grid.as_mut().unwrap().ensure_image_released()?;
|
||||
let grid = xr::CompositionLayerQuad::new()
|
||||
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
|
||||
.pose(*GRID_POSE)
|
||||
.size(xr::Extent2Df {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
})
|
||||
.sub_image(self.grid.as_ref().unwrap().get_subimage())
|
||||
.eye_visibility(xr::EyeVisibility::BOTH)
|
||||
.space(&xr.stage);
|
||||
|
||||
Ok(vec![
|
||||
CompositionLayer::Equirect2(sky),
|
||||
CompositionLayer::Quad(grid),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn create_skybox(xr: &XrState, app: &AppState) -> Option<Skybox> {
|
||||
if !app.session.config.use_skybox {
|
||||
return None;
|
||||
}
|
||||
xr.instance
|
||||
.exts()
|
||||
.khr_composition_layer_equirect2
|
||||
.and_then(|_| Skybox::new(app).ok())
|
||||
}
|
||||
@@ -1,29 +1,49 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use ash::vk;
|
||||
use openxr as xr;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use vulkano::{
|
||||
image::{sampler::Filter, sys::RawImage, view::ImageView, ImageCreateInfo, ImageUsage},
|
||||
pipeline::graphics::color_blend::AttachmentBlend,
|
||||
image::{sys::RawImage, view::ImageView, ImageCreateInfo, ImageUsage},
|
||||
Handle,
|
||||
};
|
||||
|
||||
use crate::graphics::{WlxCommandBuffer, WlxGraphics, WlxPipeline, WlxPipelineDynamic};
|
||||
use crate::graphics::{WlxGraphics, SWAPCHAIN_FORMAT};
|
||||
|
||||
use super::XrState;
|
||||
|
||||
pub(super) fn create_swapchain_render_data(
|
||||
#[derive(Default)]
|
||||
pub(super) struct SwapchainOpts {
|
||||
pub immutable: bool,
|
||||
}
|
||||
|
||||
impl SwapchainOpts {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub const fn immutable(mut self) -> Self {
|
||||
self.immutable = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn create_swapchain(
|
||||
xr: &XrState,
|
||||
graphics: Arc<WlxGraphics>,
|
||||
extent: [u32; 3],
|
||||
) -> anyhow::Result<SwapchainRenderData> {
|
||||
opts: SwapchainOpts,
|
||||
) -> anyhow::Result<WlxSwapchain> {
|
||||
let create_flags = if opts.immutable {
|
||||
xr::SwapchainCreateFlags::STATIC_IMAGE
|
||||
} else {
|
||||
xr::SwapchainCreateFlags::EMPTY
|
||||
};
|
||||
|
||||
let swapchain = xr.session.create_swapchain(&xr::SwapchainCreateInfo {
|
||||
create_flags: xr::SwapchainCreateFlags::EMPTY,
|
||||
create_flags,
|
||||
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT | xr::SwapchainUsageFlags::SAMPLED,
|
||||
format: graphics.native_format as _,
|
||||
format: SWAPCHAIN_FORMAT as _,
|
||||
sample_count: 1,
|
||||
width: extent[0],
|
||||
height: extent[1],
|
||||
@@ -32,16 +52,6 @@ pub(super) fn create_swapchain_render_data(
|
||||
mip_count: 1,
|
||||
})?;
|
||||
|
||||
let Ok(shaders) = graphics.shared_shaders.read() else {
|
||||
bail!("Failed to lock shared shaders for reading");
|
||||
};
|
||||
let pipeline = graphics.create_pipeline_dynamic(
|
||||
shaders.get("vert_common").unwrap().clone(), // want panic
|
||||
shaders.get("frag_srgb").unwrap().clone(), // want panic
|
||||
graphics.native_format,
|
||||
Some(AttachmentBlend::alpha()),
|
||||
)?;
|
||||
|
||||
let images = swapchain
|
||||
.enumerate_images()?
|
||||
.into_iter()
|
||||
@@ -53,7 +63,7 @@ pub(super) fn create_swapchain_render_data(
|
||||
graphics.device.clone(),
|
||||
vk_image,
|
||||
ImageCreateInfo {
|
||||
format: graphics.native_format,
|
||||
format: SWAPCHAIN_FORMAT as _,
|
||||
extent,
|
||||
usage: ImageUsage::COLOR_ATTACHMENT,
|
||||
..Default::default()
|
||||
@@ -66,62 +76,51 @@ pub(super) fn create_swapchain_render_data(
|
||||
})
|
||||
.collect::<anyhow::Result<SmallVec<[Arc<ImageView>; 4]>>>()?;
|
||||
|
||||
Ok(SwapchainRenderData {
|
||||
Ok(WlxSwapchain {
|
||||
acquired: false,
|
||||
ever_acquired: false,
|
||||
swapchain,
|
||||
pipeline,
|
||||
images,
|
||||
extent,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) struct SwapchainRenderData {
|
||||
pub(super) struct WlxSwapchain {
|
||||
acquired: bool,
|
||||
pub(super) ever_acquired: bool,
|
||||
pub(super) swapchain: xr::Swapchain<xr::Vulkan>,
|
||||
pub(super) pipeline: Arc<WlxPipeline<WlxPipelineDynamic>>,
|
||||
pub(super) extent: [u32; 3],
|
||||
pub(super) images: SmallVec<[Arc<ImageView>; 4]>,
|
||||
}
|
||||
|
||||
impl SwapchainRenderData {
|
||||
pub(super) fn acquire_present_release(
|
||||
&mut self,
|
||||
command_buffer: &mut WlxCommandBuffer,
|
||||
view: Arc<ImageView>,
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<xr::SwapchainSubImage<xr::Vulkan>> {
|
||||
impl WlxSwapchain {
|
||||
pub(super) fn acquire_wait_image(&mut self) -> anyhow::Result<Arc<ImageView>> {
|
||||
let idx = self.swapchain.acquire_image()? as usize;
|
||||
self.swapchain.wait_image(xr::Duration::INFINITE)?;
|
||||
self.ever_acquired = true;
|
||||
self.acquired = true;
|
||||
Ok(self.images[idx].clone())
|
||||
}
|
||||
|
||||
let render_target = &mut self.images[idx];
|
||||
command_buffer.begin_rendering(render_target.clone())?;
|
||||
pub(super) fn ensure_image_released(&mut self) -> anyhow::Result<()> {
|
||||
if self.acquired {
|
||||
self.swapchain.release_image()?;
|
||||
self.acquired = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let target_extent = render_target.image().extent();
|
||||
|
||||
let set0 = self
|
||||
.pipeline
|
||||
.uniform_sampler(0, view.clone(), Filter::Linear)?;
|
||||
|
||||
let set1 = self.pipeline.uniform_buffer(1, vec![alpha])?;
|
||||
|
||||
let pass = self.pipeline.create_pass(
|
||||
[target_extent[0] as _, target_extent[1] as _],
|
||||
command_buffer.graphics.quad_verts.clone(),
|
||||
command_buffer.graphics.quad_indices.clone(),
|
||||
vec![set0, set1],
|
||||
)?;
|
||||
command_buffer.run_ref(&pass)?;
|
||||
command_buffer.end_rendering()?;
|
||||
|
||||
self.swapchain.release_image()?;
|
||||
|
||||
Ok(xr::SwapchainSubImage::new()
|
||||
pub(super) fn get_subimage(&self) -> xr::SwapchainSubImage<xr::Vulkan> {
|
||||
debug_assert!(self.ever_acquired, "swapchain was never acquired!");
|
||||
xr::SwapchainSubImage::new()
|
||||
.swapchain(&self.swapchain)
|
||||
.image_rect(xr::Rect2Di {
|
||||
offset: xr::Offset2Di { x: 0, y: 0 },
|
||||
extent: xr::Extent2Di {
|
||||
width: target_extent[0] as _,
|
||||
height: target_extent[1] as _,
|
||||
width: self.extent[0] as _,
|
||||
height: self.extent[1] as _,
|
||||
},
|
||||
})
|
||||
.image_array_index(0))
|
||||
.image_array_index(0)
|
||||
}
|
||||
}
|
||||
+125
-37
@@ -8,10 +8,13 @@ use rosc::{OscMessage, OscPacket, OscType};
|
||||
|
||||
use crate::overlays::{keyboard::KEYBOARD_NAME, watch::WATCH_NAME};
|
||||
|
||||
use super::common::OverlayContainer;
|
||||
use crate::backend::input::TrackedDeviceRole;
|
||||
|
||||
use super::{common::OverlayContainer, input::TrackedDevice};
|
||||
|
||||
pub struct OscSender {
|
||||
last_sent: Instant,
|
||||
last_sent_overlay: Instant,
|
||||
last_sent_battery: Instant,
|
||||
upstream: UdpSocket,
|
||||
}
|
||||
|
||||
@@ -23,13 +26,14 @@ impl OscSender {
|
||||
bail!("Failed to bind UDP socket - OSC will not function.");
|
||||
};
|
||||
|
||||
let Ok(_) = upstream.connect(SocketAddr::new(ip, send_port)) else {
|
||||
let Ok(()) = upstream.connect(SocketAddr::new(ip, send_port)) else {
|
||||
bail!("Failed to connect UDP socket - OSC will not function.");
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
upstream,
|
||||
last_sent: Instant::now(),
|
||||
last_sent_overlay: Instant::now(),
|
||||
last_sent_battery: Instant::now(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,49 +50,133 @@ impl OscSender {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_params<D>(&mut self, overlays: &OverlayContainer<D>) -> anyhow::Result<()>
|
||||
pub fn send_params<D>(
|
||||
&mut self,
|
||||
overlays: &OverlayContainer<D>,
|
||||
devices: &Vec<TrackedDevice>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
D: Default,
|
||||
{
|
||||
if self.last_sent.elapsed().as_millis() < 100 {
|
||||
return Ok(());
|
||||
}
|
||||
// send overlay data every 0.1 seconds
|
||||
if self.last_sent_overlay.elapsed().as_millis() >= 100 {
|
||||
self.last_sent_overlay = Instant::now();
|
||||
|
||||
let mut num_overlays = 0;
|
||||
let mut has_keyboard = false;
|
||||
let mut has_wrist = false;
|
||||
let mut num_overlays = 0;
|
||||
let mut has_keyboard = false;
|
||||
let mut has_wrist = false;
|
||||
|
||||
for o in overlays.iter() {
|
||||
if !o.state.want_visible {
|
||||
continue;
|
||||
}
|
||||
match o.state.name.as_ref() {
|
||||
WATCH_NAME => has_wrist = true,
|
||||
KEYBOARD_NAME => has_keyboard = true,
|
||||
_ => {
|
||||
if o.state.interactable {
|
||||
num_overlays += 1
|
||||
for o in overlays.iter() {
|
||||
if !o.state.want_visible {
|
||||
continue;
|
||||
}
|
||||
match o.state.name.as_ref() {
|
||||
WATCH_NAME => has_wrist = true,
|
||||
KEYBOARD_NAME => has_keyboard = true,
|
||||
_ => {
|
||||
if o.state.interactable {
|
||||
num_overlays += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.send_message(
|
||||
"/avatar/parameters/isOverlayOpen".into(),
|
||||
vec![OscType::Bool(num_overlays > 0)],
|
||||
)?;
|
||||
self.send_message(
|
||||
"/avatar/parameters/isKeyboardOpen".into(),
|
||||
vec![OscType::Bool(has_keyboard)],
|
||||
)?;
|
||||
self.send_message(
|
||||
"/avatar/parameters/isWristVisible".into(),
|
||||
vec![OscType::Bool(has_wrist)],
|
||||
)?;
|
||||
self.send_message(
|
||||
"/avatar/parameters/openOverlayCount".into(),
|
||||
vec![OscType::Int(num_overlays)],
|
||||
)?;
|
||||
}
|
||||
|
||||
self.send_message(
|
||||
"/avatar/parameters/isOverlayOpen".into(),
|
||||
vec![OscType::Bool(num_overlays > 0)],
|
||||
)?;
|
||||
self.send_message(
|
||||
"/avatar/parameters/isKeyboardOpen".into(),
|
||||
vec![OscType::Bool(has_keyboard)],
|
||||
)?;
|
||||
self.send_message(
|
||||
"/avatar/parameters/isWristVisible".into(),
|
||||
vec![OscType::Bool(has_wrist)],
|
||||
)?;
|
||||
self.send_message(
|
||||
"/avatar/parameters/openOverlayCount".into(),
|
||||
vec![OscType::Int(num_overlays)],
|
||||
)?;
|
||||
// send battery levels every 10 seconds
|
||||
if self.last_sent_battery.elapsed().as_millis() >= 10000 {
|
||||
self.last_sent_battery = Instant::now();
|
||||
|
||||
let mut tracker_count: i8 = 0;
|
||||
let mut controller_count: i8 = 0;
|
||||
let mut tracker_total_bat = 0.0;
|
||||
let mut controller_total_bat = 0.0;
|
||||
|
||||
for device in devices {
|
||||
let tracker_param;
|
||||
|
||||
// soc is the battery level (set to device status.charge)
|
||||
let level = device.soc.unwrap_or(-1.0);
|
||||
let parameter = match device.role {
|
||||
TrackedDeviceRole::None => continue,
|
||||
TrackedDeviceRole::Hmd => {
|
||||
// legacy OVR Toolkit style (int)
|
||||
// as of 20 Nov 2024 OVR Toolkit uses int 0-100, but this may change in a future update.
|
||||
//TODO: update this once their implementation matches their docs
|
||||
self.send_message(
|
||||
"/avatar/parameters/hmdBattery".into(),
|
||||
vec![OscType::Int((level * 100.0f32).round() as i32)],
|
||||
)?;
|
||||
|
||||
"headset"
|
||||
}
|
||||
TrackedDeviceRole::LeftHand => {
|
||||
controller_count += 1;
|
||||
controller_total_bat += level;
|
||||
"leftController"
|
||||
}
|
||||
TrackedDeviceRole::RightHand => {
|
||||
controller_count += 1;
|
||||
controller_total_bat += level;
|
||||
"rightController"
|
||||
}
|
||||
TrackedDeviceRole::Tracker => {
|
||||
tracker_count += 1;
|
||||
tracker_total_bat += level;
|
||||
tracker_param = format!("tracker{tracker_count}");
|
||||
tracker_param.as_str()
|
||||
}
|
||||
};
|
||||
|
||||
// send device battery parameters
|
||||
self.send_message(
|
||||
format!("/avatar/parameters/{parameter}Battery"),
|
||||
vec![OscType::Float(level)],
|
||||
)?;
|
||||
self.send_message(
|
||||
format!("/avatar/parameters/{parameter}Charging"),
|
||||
vec![OscType::Bool(device.charging)],
|
||||
)?;
|
||||
}
|
||||
|
||||
// send average controller and tracker battery parameters
|
||||
self.send_message(
|
||||
String::from("/avatar/parameters/averageControllerBattery"),
|
||||
vec![OscType::Float(
|
||||
controller_total_bat / f32::from(controller_count),
|
||||
)],
|
||||
)?;
|
||||
self.send_message(
|
||||
String::from("/avatar/parameters/averageTrackerBattery"),
|
||||
vec![OscType::Float(tracker_total_bat / f32::from(tracker_count))],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_single_param(
|
||||
&mut self,
|
||||
parameter: String,
|
||||
values: Vec<OscType>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.send_message(parameter, values)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+111
-48
@@ -8,21 +8,36 @@ use std::{
|
||||
|
||||
use anyhow::Ok;
|
||||
use glam::{Affine2, Affine3A, Mat3A, Quat, Vec2, Vec3, Vec3A};
|
||||
use vulkano::image::view::ImageView;
|
||||
use serde::Deserialize;
|
||||
use vulkano::{format::Format, image::view::ImageView};
|
||||
|
||||
use crate::{config::AStrMapExt, state::AppState};
|
||||
use crate::{
|
||||
config::AStrMapExt,
|
||||
graphics::CommandBuffers,
|
||||
state::{AppState, KeyboardFocus},
|
||||
};
|
||||
|
||||
use super::input::{DummyInteractionHandler, Haptics, InteractionHandler, PointerHit};
|
||||
|
||||
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
|
||||
static OVERLAY_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub trait OverlayBackend: OverlayRenderer + InteractionHandler {
|
||||
fn set_renderer(&mut self, renderer: Box<dyn OverlayRenderer>);
|
||||
fn set_interaction(&mut self, interaction: Box<dyn InteractionHandler>);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
|
||||
pub struct OverlayID(pub usize);
|
||||
|
||||
pub const Z_ORDER_TOAST: u32 = 70;
|
||||
pub const Z_ORDER_LINES: u32 = 69;
|
||||
pub const Z_ORDER_WATCH: u32 = 68;
|
||||
pub const Z_ORDER_ANCHOR: u32 = 67;
|
||||
pub const Z_ORDER_DEFAULT: u32 = 0;
|
||||
pub const Z_ORDER_DASHBOARD: u32 = Z_ORDER_DEFAULT;
|
||||
|
||||
pub struct OverlayState {
|
||||
pub id: usize,
|
||||
pub id: OverlayID,
|
||||
pub name: Arc<str>,
|
||||
pub want_visible: bool,
|
||||
pub show_hide: bool,
|
||||
@@ -30,6 +45,7 @@ pub struct OverlayState {
|
||||
pub interactable: bool,
|
||||
pub recenter: bool,
|
||||
pub anchored: bool,
|
||||
pub keyboard_focus: Option<KeyboardFocus>,
|
||||
pub dirty: bool,
|
||||
pub alpha: f32,
|
||||
pub z_order: u32,
|
||||
@@ -40,15 +56,14 @@ pub struct OverlayState {
|
||||
pub saved_transform: Option<Affine3A>,
|
||||
pub relative_to: RelativeTo,
|
||||
pub curvature: Option<f32>,
|
||||
pub primary_pointer: Option<usize>,
|
||||
pub interaction_transform: Affine2,
|
||||
pub birthframe: usize,
|
||||
}
|
||||
|
||||
impl Default for OverlayState {
|
||||
fn default() -> Self {
|
||||
OverlayState {
|
||||
id: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed),
|
||||
Self {
|
||||
id: OverlayID(OVERLAY_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed)),
|
||||
name: Arc::from(""),
|
||||
want_visible: false,
|
||||
show_hide: false,
|
||||
@@ -56,9 +71,10 @@ impl Default for OverlayState {
|
||||
recenter: false,
|
||||
interactable: false,
|
||||
anchored: false,
|
||||
keyboard_focus: None,
|
||||
dirty: true,
|
||||
alpha: 1.0,
|
||||
z_order: 0,
|
||||
z_order: Z_ORDER_DEFAULT,
|
||||
relative_to: RelativeTo::None,
|
||||
curvature: None,
|
||||
spawn_scale: 1.0,
|
||||
@@ -66,7 +82,6 @@ impl Default for OverlayState {
|
||||
spawn_rotation: Quat::IDENTITY,
|
||||
saved_transform: None,
|
||||
transform: Affine3A::IDENTITY,
|
||||
primary_pointer: None,
|
||||
interaction_transform: Affine2::IDENTITY,
|
||||
birthframe: 0,
|
||||
}
|
||||
@@ -88,8 +103,8 @@ where
|
||||
T: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
OverlayData {
|
||||
state: Default::default(),
|
||||
Self {
|
||||
state: OverlayState::default(),
|
||||
backend: Box::<SplitOverlayBackend>::default(),
|
||||
primary_pointer: None,
|
||||
data: Default::default(),
|
||||
@@ -98,7 +113,7 @@ where
|
||||
}
|
||||
|
||||
impl OverlayState {
|
||||
pub fn parent_transform(&self, app: &AppState) -> Option<Affine3A> {
|
||||
pub const fn parent_transform(&self, app: &AppState) -> Option<Affine3A> {
|
||||
match self.relative_to {
|
||||
RelativeTo::Head => Some(app.input_state.hmd),
|
||||
RelativeTo::Hand(idx) => Some(app.input_state.pointers[idx].pose),
|
||||
@@ -106,7 +121,7 @@ impl OverlayState {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_anchor(&self, app: &AppState) -> Affine3A {
|
||||
const fn get_anchor(&self, app: &AppState) -> Affine3A {
|
||||
if self.anchored {
|
||||
app.anchor
|
||||
} else {
|
||||
@@ -194,12 +209,14 @@ where
|
||||
T: Default,
|
||||
{
|
||||
pub fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.state.curvature = app
|
||||
.session
|
||||
.config
|
||||
.curve_values
|
||||
.arc_get(self.state.name.as_ref())
|
||||
.copied();
|
||||
if self.state.curvature.is_none() {
|
||||
self.state.curvature = app
|
||||
.session
|
||||
.config
|
||||
.curve_values
|
||||
.arc_get(self.state.name.as_ref())
|
||||
.copied();
|
||||
}
|
||||
|
||||
if matches!(self.state.relative_to, RelativeTo::None) {
|
||||
let hard_reset;
|
||||
@@ -218,32 +235,59 @@ where
|
||||
}
|
||||
self.backend.init(app)
|
||||
}
|
||||
pub fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.backend.render(app)
|
||||
pub fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
|
||||
self.backend.should_render(app)
|
||||
}
|
||||
pub fn view(&mut self) -> Option<Arc<ImageView>> {
|
||||
self.backend.view()
|
||||
pub fn render(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
tgt: Arc<ImageView>,
|
||||
buf: &mut CommandBuffers,
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
self.backend.render(app, tgt, buf, alpha)
|
||||
}
|
||||
pub fn set_visible(&mut self, app: &mut AppState, visible: bool) -> anyhow::Result<()> {
|
||||
let old_visible = self.state.want_visible;
|
||||
self.state.want_visible = visible;
|
||||
if visible != old_visible {
|
||||
if visible {
|
||||
self.backend.resume(app)?;
|
||||
} else {
|
||||
self.backend.pause(app)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
pub fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
self.backend.frame_meta()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub struct FrameMeta {
|
||||
pub extent: [u32; 3],
|
||||
pub transform: Affine3A,
|
||||
pub format: Format,
|
||||
}
|
||||
|
||||
pub enum ShouldRender {
|
||||
Should,
|
||||
Can,
|
||||
Unable,
|
||||
}
|
||||
|
||||
pub trait OverlayRenderer {
|
||||
/// Called once, before the first frame is rendered
|
||||
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()>;
|
||||
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()>;
|
||||
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()>;
|
||||
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()>;
|
||||
fn view(&mut self) -> Option<Arc<ImageView>>;
|
||||
|
||||
/// Called when the presentation layer is ready to present a new frame
|
||||
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender>;
|
||||
|
||||
/// Called when the contents need to be rendered to the swapchain
|
||||
fn render(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
tgt: Arc<ImageView>,
|
||||
buf: &mut CommandBuffers,
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<bool>;
|
||||
|
||||
/// Called to retrieve the effective extent of the image
|
||||
/// Used for creating swapchains.
|
||||
///
|
||||
/// Must be true if should_render was also true on the same frame.
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta>;
|
||||
}
|
||||
|
||||
pub struct FallbackRenderer;
|
||||
@@ -258,10 +302,19 @@ impl OverlayRenderer for FallbackRenderer {
|
||||
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn render(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
fn should_render(&mut self, _app: &mut AppState) -> anyhow::Result<ShouldRender> {
|
||||
Ok(ShouldRender::Unable)
|
||||
}
|
||||
fn view(&mut self) -> Option<Arc<ImageView>> {
|
||||
fn render(
|
||||
&mut self,
|
||||
_app: &mut AppState,
|
||||
_tgt: Arc<ImageView>,
|
||||
_buf: &mut CommandBuffers,
|
||||
_alpha: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -286,8 +339,8 @@ pub struct SplitOverlayBackend {
|
||||
}
|
||||
|
||||
impl Default for SplitOverlayBackend {
|
||||
fn default() -> SplitOverlayBackend {
|
||||
SplitOverlayBackend {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
renderer: Box::new(FallbackRenderer),
|
||||
interaction: Box::new(DummyInteractionHandler),
|
||||
}
|
||||
@@ -312,13 +365,23 @@ impl OverlayRenderer for SplitOverlayBackend {
|
||||
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.renderer.resume(app)
|
||||
}
|
||||
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.renderer.render(app)
|
||||
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
|
||||
self.renderer.should_render(app)
|
||||
}
|
||||
fn view(&mut self) -> Option<Arc<ImageView>> {
|
||||
self.renderer.view()
|
||||
fn render(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
tgt: Arc<ImageView>,
|
||||
buf: &mut CommandBuffers,
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
self.renderer.render(app, tgt, buf, alpha)
|
||||
}
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
self.renderer.frame_meta()
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractionHandler for SplitOverlayBackend {
|
||||
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
|
||||
self.interaction.on_left(app, pointer);
|
||||
@@ -326,15 +389,15 @@ impl InteractionHandler for SplitOverlayBackend {
|
||||
fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
|
||||
self.interaction.on_hover(app, hit)
|
||||
}
|
||||
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta: f32) {
|
||||
self.interaction.on_scroll(app, hit, delta);
|
||||
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta_y: f32, delta_x: f32) {
|
||||
self.interaction.on_scroll(app, hit, delta_y, delta_x);
|
||||
}
|
||||
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
|
||||
self.interaction.on_pointer(app, hit, pressed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui_transform(extent: &[u32; 2]) -> Affine2 {
|
||||
pub fn ui_transform(extent: [u32; 2]) -> Affine2 {
|
||||
let aspect = extent[0] as f32 / extent[1] as f32;
|
||||
let scale = if aspect < 1.0 {
|
||||
Vec2 {
|
||||
|
||||
+9
-4
@@ -9,6 +9,9 @@ use serde::Deserialize;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
use crate::gui::modular::button::WayVRAction;
|
||||
|
||||
use super::{
|
||||
common::OverlaySelector,
|
||||
overlay::{OverlayBackend, OverlayState},
|
||||
@@ -22,12 +25,12 @@ struct AppTask {
|
||||
pub task: TaskType,
|
||||
}
|
||||
|
||||
impl PartialEq<AppTask> for AppTask {
|
||||
impl PartialEq<Self> for AppTask {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other) == cmp::Ordering::Equal
|
||||
}
|
||||
}
|
||||
impl PartialOrd<AppTask> for AppTask {
|
||||
impl PartialOrd<Self> for AppTask {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
@@ -46,6 +49,7 @@ pub enum SystemTask {
|
||||
ColorGain(ColorChannel, f32),
|
||||
ResetPlayspace,
|
||||
FixFloor,
|
||||
ShowHide,
|
||||
}
|
||||
|
||||
pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send;
|
||||
@@ -53,11 +57,12 @@ pub type CreateOverlayTask =
|
||||
dyn FnOnce(&mut AppState) -> Option<(OverlayState, Box<dyn OverlayBackend>)> + Send;
|
||||
|
||||
pub enum TaskType {
|
||||
Global(Box<dyn FnOnce(&mut AppState) + Send>),
|
||||
Overlay(OverlaySelector, Box<OverlayTask>),
|
||||
CreateOverlay(OverlaySelector, Box<CreateOverlayTask>),
|
||||
DropOverlay(OverlaySelector),
|
||||
System(SystemTask),
|
||||
#[cfg(feature = "wayvr")]
|
||||
WayVR(WayVRAction),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
@@ -73,7 +78,7 @@ pub struct TaskContainer {
|
||||
}
|
||||
|
||||
impl TaskContainer {
|
||||
pub fn new() -> Self {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
tasks: BinaryHeap::new(),
|
||||
}
|
||||
|
||||
+35
-62
@@ -1,10 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use vulkano::{
|
||||
command_buffer::CommandBufferUsage,
|
||||
image::{sampler::Filter, view::ImageView, ImageUsage},
|
||||
image::{view::ImageView, ImageUsage},
|
||||
swapchain::{
|
||||
acquire_next_image, Surface, Swapchain, SwapchainCreateInfo, SwapchainPresentInfo,
|
||||
acquire_next_image, Surface, SurfaceInfo, Swapchain, SwapchainCreateInfo,
|
||||
SwapchainPresentInfo,
|
||||
},
|
||||
sync::GpuFuture,
|
||||
Validated, VulkanError,
|
||||
@@ -19,10 +19,10 @@ use winit::{
|
||||
use crate::{
|
||||
config::load_custom_ui,
|
||||
config_io,
|
||||
graphics::{DynamicPass, DynamicPipeline, WlxGraphics, BLEND_ALPHA},
|
||||
graphics::{CommandBuffers, WlxGraphics},
|
||||
gui::{
|
||||
canvas::Canvas,
|
||||
modular::{modular_canvas, ModularData},
|
||||
Canvas,
|
||||
},
|
||||
hid::USE_UINPUT,
|
||||
state::{AppState, ScreenMeta},
|
||||
@@ -30,15 +30,13 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
input::{TrackedDevice, TrackedDeviceRole},
|
||||
overlay::OverlayRenderer,
|
||||
overlay::{OverlayID, OverlayRenderer},
|
||||
};
|
||||
|
||||
static LAST_SIZE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
struct PreviewState {
|
||||
canvas: Canvas<(), ModularData>,
|
||||
pipeline: Arc<DynamicPipeline>,
|
||||
pass: DynamicPass,
|
||||
swapchain: Arc<Swapchain>,
|
||||
images: Vec<Arc<ImageView>>,
|
||||
}
|
||||
@@ -63,52 +61,27 @@ impl PreviewState {
|
||||
window.set_min_inner_size(Some(logical_size));
|
||||
window.set_max_inner_size(Some(logical_size));
|
||||
LAST_SIZE.store(
|
||||
(config.size[1] as u64) << 32 | config.size[0] as u64,
|
||||
((config.size[1] as u64) << 32) | config.size[0] as u64,
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
|
||||
let inner_size = window.inner_size();
|
||||
let swapchain_size = [inner_size.width, inner_size.height];
|
||||
let (swapchain, images) =
|
||||
create_swapchain(&state.graphics, surface.clone(), swapchain_size)?;
|
||||
let (swapchain, images) = create_swapchain(&state.graphics, surface, swapchain_size)?;
|
||||
|
||||
let mut canvas = modular_canvas(&config.size, &config.elements, state)?;
|
||||
let mut canvas = modular_canvas(config.size, &config.elements, state)?;
|
||||
canvas.init(state)?;
|
||||
let view = canvas.view().unwrap();
|
||||
|
||||
let pipeline = {
|
||||
let shaders = state.graphics.shared_shaders.read().unwrap();
|
||||
state.graphics.create_pipeline_dynamic(
|
||||
shaders.get("vert_common").unwrap().clone(), // want panic
|
||||
shaders.get("frag_sprite").unwrap().clone(), // want panic
|
||||
state.graphics.native_format,
|
||||
Some(BLEND_ALPHA),
|
||||
)
|
||||
}?;
|
||||
let set0 = pipeline
|
||||
.uniform_sampler(0, view.clone(), Filter::Linear)
|
||||
.unwrap();
|
||||
|
||||
let pass = pipeline
|
||||
.create_pass(
|
||||
[swapchain_size[0] as f32, swapchain_size[1] as f32],
|
||||
state.graphics.quad_verts.clone(),
|
||||
state.graphics.quad_indices.clone(),
|
||||
vec![set0],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(PreviewState {
|
||||
Ok(Self {
|
||||
canvas,
|
||||
pipeline,
|
||||
pass,
|
||||
swapchain,
|
||||
images,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn uidev_run(panel_name: &str) -> anyhow::Result<()> {
|
||||
let (graphics, event_loop, window, surface) = WlxGraphics::new_window()?;
|
||||
window.set_resizable(false);
|
||||
@@ -127,11 +100,12 @@ pub fn uidev_run(panel_name: &str) -> anyhow::Result<()> {
|
||||
panel_name,
|
||||
)?);
|
||||
|
||||
let watch_path = config_io::CONFIG_ROOT_PATH.join(format!("{}.yaml", panel_name));
|
||||
let watch_path = config_io::get_config_root().join(format!("{panel_name}.yaml"));
|
||||
let mut path_last_modified = watch_path.metadata()?.modified()?;
|
||||
let mut recreate = false;
|
||||
let mut last_draw = std::time::Instant::now();
|
||||
|
||||
#[allow(deprecated)]
|
||||
event_loop.run(move |event, elwt| {
|
||||
elwt.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
@@ -182,29 +156,27 @@ pub fn uidev_run(panel_name: &str) -> anyhow::Result<()> {
|
||||
Err(e) => panic!("failed to acquire next image: {e}"),
|
||||
};
|
||||
|
||||
if let Err(e) = preview.canvas.render(&mut state) {
|
||||
let mut canvas_cmd_buf = CommandBuffers::default();
|
||||
let tgt = preview.images[image_index as usize].clone();
|
||||
|
||||
if let Err(e) = preview
|
||||
.canvas
|
||||
.render(&mut state, tgt, &mut canvas_cmd_buf, 1.0)
|
||||
{
|
||||
log::error!("failed to render canvas: {e}");
|
||||
window.request_redraw();
|
||||
};
|
||||
}
|
||||
|
||||
let target = preview.images[image_index as usize].clone();
|
||||
|
||||
let mut cmd_buf = state
|
||||
.graphics
|
||||
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)
|
||||
.unwrap();
|
||||
cmd_buf.begin_rendering(target).unwrap();
|
||||
cmd_buf.run_ref(&preview.pass).unwrap();
|
||||
cmd_buf.end_rendering().unwrap();
|
||||
last_draw = std::time::Instant::now();
|
||||
|
||||
let command_buffer = cmd_buf.build().unwrap();
|
||||
vulkano::sync::now(graphics.device.clone())
|
||||
.join(acquire_future)
|
||||
.then_execute(graphics.queue.clone(), command_buffer)
|
||||
canvas_cmd_buf
|
||||
.execute_after(
|
||||
state.graphics.graphics_queue.clone(),
|
||||
Box::new(acquire_future),
|
||||
)
|
||||
.unwrap()
|
||||
.then_swapchain_present(
|
||||
graphics.queue.clone(),
|
||||
graphics.graphics_queue.clone(),
|
||||
SwapchainPresentInfo::swapchain_image_index(
|
||||
preview.swapchain.clone(),
|
||||
image_index,
|
||||
@@ -236,12 +208,12 @@ fn create_swapchain(
|
||||
let surface_capabilities = graphics
|
||||
.device
|
||||
.physical_device()
|
||||
.surface_capabilities(&surface, Default::default())
|
||||
.unwrap();
|
||||
.surface_capabilities(&surface, SurfaceInfo::default())
|
||||
.unwrap(); // want panic
|
||||
|
||||
let (swapchain, images) = Swapchain::new(
|
||||
graphics.device.clone(),
|
||||
surface.clone(),
|
||||
surface,
|
||||
SwapchainCreateInfo {
|
||||
min_image_count: surface_capabilities.min_image_count.max(2),
|
||||
image_format: graphics.native_format,
|
||||
@@ -251,13 +223,14 @@ fn create_swapchain(
|
||||
.supported_composite_alpha
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap(),
|
||||
.unwrap(), // want panic
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let image_views = images
|
||||
.into_iter()
|
||||
// want panic
|
||||
.map(|image| ImageView::new_default(image).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -300,17 +273,17 @@ fn add_dummy_devices(app: &mut AppState) {
|
||||
fn add_dummy_screens(app: &mut AppState) {
|
||||
app.screens.push(ScreenMeta {
|
||||
name: "HDMI-A-1".into(),
|
||||
id: 0,
|
||||
id: OverlayID(0),
|
||||
native_handle: 0,
|
||||
});
|
||||
app.screens.push(ScreenMeta {
|
||||
name: "DP-2".into(),
|
||||
id: 0,
|
||||
id: OverlayID(0),
|
||||
native_handle: 0,
|
||||
});
|
||||
app.screens.push(ScreenMeta {
|
||||
name: "DP-3".into(),
|
||||
id: 0,
|
||||
id: OverlayID(0),
|
||||
native_handle: 0,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
use std::{io::Read, os::unix::net::UnixStream, path::PathBuf, sync::Arc};
|
||||
|
||||
use smithay::{
|
||||
backend::input::Keycode,
|
||||
input::{keyboard::KeyboardHandle, pointer::PointerHandle},
|
||||
reexports::wayland_server,
|
||||
utils::SerialCounter,
|
||||
};
|
||||
|
||||
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
|
||||
|
||||
use super::{
|
||||
comp::{self, ClientState},
|
||||
display, process, ProcessWayVREnv,
|
||||
};
|
||||
|
||||
pub struct WayVRClient {
|
||||
pub client: wayland_server::Client,
|
||||
pub display_handle: display::DisplayHandle,
|
||||
pub pid: u32,
|
||||
}
|
||||
|
||||
pub struct WayVRCompositor {
|
||||
pub state: comp::Application,
|
||||
pub seat_keyboard: KeyboardHandle<comp::Application>,
|
||||
pub seat_pointer: PointerHandle<comp::Application>,
|
||||
pub serial_counter: SerialCounter,
|
||||
pub wayland_env: super::WaylandEnv,
|
||||
|
||||
display: wayland_server::Display<comp::Application>,
|
||||
listener: wayland_server::ListeningSocket,
|
||||
|
||||
toplevel_surf_count: u32, // for logging purposes
|
||||
|
||||
pub clients: Vec<WayVRClient>,
|
||||
}
|
||||
|
||||
fn get_wayvr_env_from_pid(pid: i32) -> anyhow::Result<ProcessWayVREnv> {
|
||||
let path = format!("/proc/{pid}/environ");
|
||||
let mut env_data = String::new();
|
||||
std::fs::File::open(path)?.read_to_string(&mut env_data)?;
|
||||
|
||||
let lines: Vec<&str> = env_data.split('\0').filter(|s| !s.is_empty()).collect();
|
||||
|
||||
let mut env = ProcessWayVREnv {
|
||||
display_auth: None,
|
||||
display_name: None,
|
||||
};
|
||||
|
||||
for line in lines {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
if key == "WAYVR_DISPLAY_AUTH" {
|
||||
env.display_auth = Some(String::from(value));
|
||||
} else if key == "WAYVR_DISPLAY_NAME" {
|
||||
env.display_name = Some(String::from(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
impl WayVRCompositor {
|
||||
pub fn new(
|
||||
state: comp::Application,
|
||||
display: wayland_server::Display<comp::Application>,
|
||||
seat_keyboard: KeyboardHandle<comp::Application>,
|
||||
seat_pointer: PointerHandle<comp::Application>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let (wayland_env, listener) = create_wayland_listener()?;
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
display,
|
||||
seat_keyboard,
|
||||
seat_pointer,
|
||||
listener,
|
||||
wayland_env,
|
||||
serial_counter: SerialCounter::new(),
|
||||
clients: Vec::new(),
|
||||
toplevel_surf_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_client(&mut self, client: WayVRClient) {
|
||||
self.clients.push(client);
|
||||
}
|
||||
|
||||
pub fn cleanup_clients(&mut self) {
|
||||
self.clients.retain(|client| {
|
||||
let Some(data) = client.client.get_data::<ClientState>() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if *data.disconnected.lock().unwrap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
fn accept_connection(
|
||||
&mut self,
|
||||
stream: UnixStream,
|
||||
displays: &mut display::DisplayVec,
|
||||
processes: &mut process::ProcessVec,
|
||||
) -> anyhow::Result<()> {
|
||||
let client = self
|
||||
.display
|
||||
.handle()
|
||||
.insert_client(stream, Arc::new(comp::ClientState::default()))
|
||||
.unwrap();
|
||||
|
||||
let creds = client.get_credentials(&self.display.handle())?;
|
||||
|
||||
let process_env = get_wayvr_env_from_pid(creds.pid)?;
|
||||
|
||||
// Find suitable auth key from the process list
|
||||
for p in processes.vec.iter().flatten() {
|
||||
if let process::Process::Managed(process) = &p.obj {
|
||||
if let Some(auth_key) = &process_env.display_auth {
|
||||
// Find process with matching auth key
|
||||
if process.auth_key.as_str() == auth_key {
|
||||
// Check if display handle is valid
|
||||
if displays.get(&process.display_handle).is_some() {
|
||||
// Add client
|
||||
self.add_client(WayVRClient {
|
||||
client,
|
||||
display_handle: process.display_handle,
|
||||
pid: creds.pid as u32,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
creds.pid
|
||||
);
|
||||
|
||||
self.state
|
||||
.wayvr_tasks
|
||||
.send(WayVRTask::NewExternalProcess(ExternalProcessRequest {
|
||||
env: process_env,
|
||||
client,
|
||||
pid: creds.pid as u32,
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn accept_connections(
|
||||
&mut self,
|
||||
displays: &mut display::DisplayVec,
|
||||
processes: &mut process::ProcessVec,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(stream) = self.listener.accept()? {
|
||||
if let Err(e) = self.accept_connection(stream, displays, processes) {
|
||||
log::error!("Failed to accept connection: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tick_wayland(
|
||||
&mut self,
|
||||
displays: &mut display::DisplayVec,
|
||||
processes: &mut process::ProcessVec,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Err(e) = self.accept_connections(displays, processes) {
|
||||
log::error!("accept_connections failed: {e}");
|
||||
}
|
||||
|
||||
self.display.dispatch_clients(&mut self.state)?;
|
||||
self.display.flush_clients()?;
|
||||
|
||||
let surf_count = self.state.xdg_shell.toplevel_surfaces().len() as u32;
|
||||
if surf_count != self.toplevel_surf_count {
|
||||
self.toplevel_surf_count = surf_count;
|
||||
log::info!("Toplevel surface count changed: {surf_count}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_key(&mut self, virtual_key: u32, down: bool) {
|
||||
let state = if down {
|
||||
smithay::backend::input::KeyState::Pressed
|
||||
} else {
|
||||
smithay::backend::input::KeyState::Released
|
||||
};
|
||||
|
||||
self.seat_keyboard.input::<(), _>(
|
||||
&mut self.state,
|
||||
Keycode::new(virtual_key),
|
||||
state,
|
||||
self.serial_counter.next_serial(),
|
||||
0,
|
||||
|_, _, _| smithay::input::keyboard::FilterResult::Forward,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const STARTING_WAYLAND_ADDR_IDX: u32 = 20;
|
||||
|
||||
fn export_display_number(display_num: u32) -> anyhow::Result<()> {
|
||||
let mut path = std::env::var("XDG_RUNTIME_DIR").map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from);
|
||||
path.push("wayvr.disp");
|
||||
std::fs::write(path, format!("{display_num}\n"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_wayland_listener() -> anyhow::Result<(super::WaylandEnv, wayland_server::ListeningSocket)>
|
||||
{
|
||||
let mut env = super::WaylandEnv {
|
||||
display_num: STARTING_WAYLAND_ADDR_IDX,
|
||||
};
|
||||
|
||||
let listener = loop {
|
||||
let display_str = env.display_num_string();
|
||||
log::debug!("Trying to open socket \"{display_str}\"");
|
||||
match wayland_server::ListeningSocket::bind(display_str.as_str()) {
|
||||
Ok(listener) => {
|
||||
log::debug!("Listening to {display_str}");
|
||||
break listener;
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!(
|
||||
"Failed to open socket \"{display_str}\" (reason: {e}), trying next..."
|
||||
);
|
||||
|
||||
env.display_num += 1;
|
||||
if env.display_num > STARTING_WAYLAND_ADDR_IDX + 20 {
|
||||
// Highly unlikely for the user to have 20 Wayland displays enabled at once. Return error instead.
|
||||
anyhow::bail!("Failed to create wayland-server socket")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let _ = export_display_number(env.display_num);
|
||||
|
||||
Ok((env, listener))
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::utils::on_commit_buffer_handler;
|
||||
use smithay::backend::renderer::ImportDma;
|
||||
use smithay::input::{Seat, SeatHandler, SeatState};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_server;
|
||||
use smithay::reexports::wayland_server::protocol::{wl_buffer, wl_seat, wl_surface};
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
use smithay::wayland::dmabuf::{
|
||||
DmabufFeedback, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier,
|
||||
};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
use smithay::{
|
||||
delegate_compositor, delegate_data_device, delegate_dmabuf, delegate_output, delegate_seat,
|
||||
delegate_shm, delegate_xdg_shell,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use smithay::utils::Serial;
|
||||
use smithay::wayland::compositor::{
|
||||
self, with_surface_tree_downward, SurfaceAttributes, TraversalAction,
|
||||
};
|
||||
|
||||
use smithay::wayland::selection::data_device::{
|
||||
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
|
||||
};
|
||||
use smithay::wayland::selection::SelectionHandler;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
|
||||
};
|
||||
use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
|
||||
use wayland_server::protocol::wl_surface::WlSurface;
|
||||
use wayland_server::Client;
|
||||
|
||||
use super::event_queue::SyncEventQueue;
|
||||
use super::WayVRTask;
|
||||
|
||||
pub struct Application {
|
||||
pub gles_renderer: GlesRenderer,
|
||||
pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
|
||||
pub compositor: compositor::CompositorState,
|
||||
pub xdg_shell: XdgShellState,
|
||||
pub seat_state: SeatState<Application>,
|
||||
pub shm: ShmState,
|
||||
pub data_device: DataDeviceState,
|
||||
pub wayvr_tasks: SyncEventQueue<WayVRTask>,
|
||||
pub redraw_requests: HashSet<wayland_server::backend::ObjectId>,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
pub fn check_redraw(&mut self, surface: &WlSurface) -> bool {
|
||||
self.redraw_requests.remove(&surface.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl compositor::CompositorHandler for Application {
|
||||
fn compositor_state(&mut self) -> &mut compositor::CompositorState {
|
||||
&mut self.compositor
|
||||
}
|
||||
|
||||
fn client_compositor_state<'a>(
|
||||
&self,
|
||||
client: &'a Client,
|
||||
) -> &'a compositor::CompositorClientState {
|
||||
&client.get_data::<ClientState>().unwrap().compositor_state
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
self.redraw_requests.insert(surface.id());
|
||||
}
|
||||
}
|
||||
|
||||
impl SeatHandler for Application {
|
||||
type KeyboardFocus = WlSurface;
|
||||
type PointerFocus = WlSurface;
|
||||
type TouchFocus = WlSurface;
|
||||
|
||||
fn seat_state(&mut self) -> &mut SeatState<Self> {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
|
||||
fn cursor_image(
|
||||
&mut self,
|
||||
_seat: &Seat<Self>,
|
||||
_image: smithay::input::pointer::CursorImageStatus,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferHandler for Application {
|
||||
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
|
||||
}
|
||||
|
||||
impl ClientDndGrabHandler for Application {}
|
||||
|
||||
impl ServerDndGrabHandler for Application {
|
||||
fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat<Self>) {}
|
||||
}
|
||||
|
||||
impl DataDeviceHandler for Application {
|
||||
fn data_device_state(&self) -> &DataDeviceState {
|
||||
&self.data_device
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionHandler for Application {
|
||||
type SelectionUserData = ();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClientState {
|
||||
compositor_state: compositor::CompositorClientState,
|
||||
pub disconnected: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl ClientData for ClientState {
|
||||
fn initialized(&self, client_id: ClientId) {
|
||||
log::debug!("Client ID {client_id:?} connected");
|
||||
}
|
||||
|
||||
fn disconnected(&self, client_id: ClientId, reason: DisconnectReason) {
|
||||
*self.disconnected.lock().unwrap() = true;
|
||||
log::debug!(
|
||||
"Client ID {client_id:?} disconnected. Reason: {reason:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<compositor::CompositorState> for Application {
|
||||
fn as_mut(&mut self) -> &mut compositor::CompositorState {
|
||||
&mut self.compositor
|
||||
}
|
||||
}
|
||||
|
||||
impl XdgShellHandler for Application {
|
||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||
&mut self.xdg_shell
|
||||
}
|
||||
|
||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||
if let Some(client) = surface.wl_surface().client() {
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::NewToplevel(client.id(), surface.clone()));
|
||||
}
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Activated);
|
||||
});
|
||||
surface.send_configure();
|
||||
}
|
||||
|
||||
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
||||
if let Some(client) = surface.wl_surface().client() {
|
||||
self.wayvr_tasks
|
||||
.send(WayVRTask::DropToplevel(client.id(), surface.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) {
|
||||
// Handle popup creation here
|
||||
}
|
||||
|
||||
fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {
|
||||
// Handle popup grab here
|
||||
}
|
||||
|
||||
fn reposition_request(
|
||||
&mut self,
|
||||
_surface: PopupSurface,
|
||||
_positioner: PositionerState,
|
||||
_token: u32,
|
||||
) {
|
||||
// Handle popup reposition here
|
||||
}
|
||||
}
|
||||
|
||||
impl ShmHandler for Application {
|
||||
fn shm_state(&self) -> &ShmState {
|
||||
&self.shm
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputHandler for Application {}
|
||||
|
||||
impl DmabufHandler for Application {
|
||||
fn dmabuf_state(&mut self) -> &mut DmabufState {
|
||||
&mut self.dmabuf_state.0
|
||||
}
|
||||
|
||||
fn dmabuf_imported(
|
||||
&mut self,
|
||||
_global: &DmabufGlobal,
|
||||
dmabuf: Dmabuf,
|
||||
notifier: ImportNotifier,
|
||||
) {
|
||||
if self.gles_renderer.import_dmabuf(&dmabuf, None).is_ok() {
|
||||
let _ = notifier.successful::<Self>();
|
||||
} else {
|
||||
notifier.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_dmabuf!(Application);
|
||||
delegate_xdg_shell!(Application);
|
||||
delegate_compositor!(Application);
|
||||
delegate_shm!(Application);
|
||||
delegate_seat!(Application);
|
||||
delegate_data_device!(Application);
|
||||
delegate_output!(Application);
|
||||
|
||||
pub fn send_frames_surface_tree(surface: &wl_surface::WlSurface, time: u32) {
|
||||
with_surface_tree_downward(
|
||||
surface,
|
||||
(),
|
||||
|_, _, &()| TraversalAction::DoChildren(()),
|
||||
|_surf, states, &()| {
|
||||
// the surface may not have any user_data if it is a subsurface and has not
|
||||
// yet been commited
|
||||
for callback in states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.frame_callbacks
|
||||
.drain(..)
|
||||
{
|
||||
callback.done(time);
|
||||
}
|
||||
},
|
||||
|_, _, &()| true,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
|
||||
use smithay::{
|
||||
backend::renderer::{
|
||||
element::{
|
||||
surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement},
|
||||
Kind,
|
||||
},
|
||||
gles::{ffi, GlesRenderer, GlesTexture},
|
||||
utils::draw_render_elements,
|
||||
Bind, Color32F, Frame, Renderer,
|
||||
},
|
||||
input,
|
||||
utils::{Logical, Point, Rectangle, Size, Transform},
|
||||
wayland::shell::xdg::ToplevelSurface,
|
||||
};
|
||||
use wayvr_ipc::packet_server;
|
||||
|
||||
use crate::{
|
||||
backend::{overlay::OverlayID, wayvr::time::get_millis},
|
||||
gen_id,
|
||||
};
|
||||
|
||||
use super::{
|
||||
client::WayVRCompositor, comp::send_frames_surface_tree, egl_data, event_queue::SyncEventQueue,
|
||||
process, smithay_wrapper, time, window, BlitMethod, WayVRSignal,
|
||||
};
|
||||
|
||||
fn generate_auth_key() -> String {
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
uuid.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DisplayWindow {
|
||||
pub window_handle: window::WindowHandle,
|
||||
pub toplevel: ToplevelSurface,
|
||||
pub process_handle: process::ProcessHandle,
|
||||
}
|
||||
|
||||
pub struct SpawnProcessResult {
|
||||
pub auth_key: String,
|
||||
pub child: std::process::Child,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DisplayTask {
|
||||
ProcessCleanup(process::ProcessHandle),
|
||||
}
|
||||
|
||||
const MAX_DISPLAY_SIZE: u16 = 8192;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Display {
|
||||
// Display info stuff
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub name: String,
|
||||
pub visible: bool,
|
||||
pub layout: packet_server::WvrDisplayWindowLayout,
|
||||
pub overlay_id: Option<OverlayID>,
|
||||
pub wants_redraw: bool,
|
||||
pub primary: bool,
|
||||
pub wm: Rc<RefCell<window::WindowManager>>,
|
||||
pub displayed_windows: Vec<DisplayWindow>,
|
||||
wayland_env: super::WaylandEnv,
|
||||
last_pressed_time_ms: u64,
|
||||
pub no_windows_since: Option<u64>,
|
||||
|
||||
// Render data stuff
|
||||
gles_texture: GlesTexture, // TODO: drop texture
|
||||
egl_image: khronos_egl::Image,
|
||||
egl_data: Rc<egl_data::EGLData>,
|
||||
|
||||
pub render_data: egl_data::RenderData,
|
||||
|
||||
pub tasks: SyncEventQueue<DisplayTask>,
|
||||
}
|
||||
|
||||
impl Drop for Display {
|
||||
fn drop(&mut self) {
|
||||
let _ = self
|
||||
.egl_data
|
||||
.egl
|
||||
.destroy_image(self.egl_data.display, self.egl_image);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DisplayInitParams<'a> {
|
||||
pub wm: Rc<RefCell<window::WindowManager>>,
|
||||
pub config: &'a super::Config,
|
||||
pub renderer: &'a mut GlesRenderer,
|
||||
pub egl_data: Rc<egl_data::EGLData>,
|
||||
pub wayland_env: super::WaylandEnv,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub name: &'a str,
|
||||
pub primary: bool,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
pub fn new(params: DisplayInitParams) -> anyhow::Result<Self> {
|
||||
if params.width > MAX_DISPLAY_SIZE {
|
||||
anyhow::bail!(
|
||||
"display width ({}) is larger than {}",
|
||||
params.width,
|
||||
MAX_DISPLAY_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
if params.height > MAX_DISPLAY_SIZE {
|
||||
anyhow::bail!(
|
||||
"display height ({}) is larger than {}",
|
||||
params.height,
|
||||
MAX_DISPLAY_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
let tex_format = ffi::RGBA;
|
||||
let internal_format = ffi::RGBA8;
|
||||
|
||||
let tex_id = params.renderer.with_context(|gl| {
|
||||
smithay_wrapper::create_framebuffer_texture(
|
||||
gl,
|
||||
u32::from(params.width),
|
||||
u32::from(params.height),
|
||||
tex_format,
|
||||
internal_format,
|
||||
)
|
||||
})?;
|
||||
|
||||
let egl_image = params.egl_data.create_egl_image(tex_id)?;
|
||||
|
||||
let render_data = match params.config.blit_method {
|
||||
BlitMethod::Dmabuf => {
|
||||
egl_data::RenderData::Dmabuf(params.egl_data.create_dmabuf_data(&egl_image)?)
|
||||
}
|
||||
BlitMethod::Software => egl_data::RenderData::Software(None),
|
||||
};
|
||||
|
||||
let opaque = false;
|
||||
let size = (i32::from(params.width), i32::from(params.height)).into();
|
||||
let gles_texture = unsafe {
|
||||
GlesTexture::from_raw(params.renderer, Some(tex_format), opaque, tex_id, size)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
egl_data: params.egl_data,
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
name: String::from(params.name),
|
||||
primary: params.primary,
|
||||
wayland_env: params.wayland_env,
|
||||
wm: params.wm,
|
||||
displayed_windows: Vec::new(),
|
||||
render_data,
|
||||
egl_image,
|
||||
gles_texture,
|
||||
last_pressed_time_ms: 0,
|
||||
no_windows_since: None,
|
||||
overlay_id: None,
|
||||
tasks: SyncEventQueue::new(),
|
||||
visible: true,
|
||||
wants_redraw: true,
|
||||
layout: packet_server::WvrDisplayWindowLayout::Tiling,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_packet(&self, handle: DisplayHandle) -> packet_server::WvrDisplay {
|
||||
packet_server::WvrDisplay {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
name: self.name.clone(),
|
||||
visible: self.visible,
|
||||
handle: handle.as_packet(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_window(
|
||||
&mut self,
|
||||
window_handle: window::WindowHandle,
|
||||
process_handle: process::ProcessHandle,
|
||||
toplevel: &ToplevelSurface,
|
||||
) {
|
||||
log::debug!("Attaching toplevel surface into display");
|
||||
self.displayed_windows.push(DisplayWindow {
|
||||
window_handle,
|
||||
process_handle,
|
||||
toplevel: toplevel.clone(),
|
||||
});
|
||||
self.reposition_windows();
|
||||
}
|
||||
|
||||
pub fn remove_window(&mut self, window_handle: window::WindowHandle) {
|
||||
self.displayed_windows
|
||||
.retain(|disp| disp.window_handle != window_handle);
|
||||
}
|
||||
|
||||
pub fn reposition_windows(&mut self) {
|
||||
let window_count = self.displayed_windows.len();
|
||||
|
||||
match &self.layout {
|
||||
packet_server::WvrDisplayWindowLayout::Tiling => {
|
||||
let mut i = 0;
|
||||
for win in &mut self.displayed_windows {
|
||||
if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.window_handle) {
|
||||
if !window.visible {
|
||||
continue;
|
||||
}
|
||||
let d_cur = i as f32 / window_count as f32;
|
||||
let d_next = (i + 1) as f32 / window_count as f32;
|
||||
|
||||
let left = (d_cur * f32::from(self.width)) as i32;
|
||||
let right = (d_next * f32::from(self.width)) as i32;
|
||||
|
||||
window.set_pos(left, 0);
|
||||
window.set_size((right - left) as u32, u32::from(self.height));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
packet_server::WvrDisplayWindowLayout::Stacking(opts) => {
|
||||
let do_margins = |margins: &packet_server::Margins, window: &mut window::Window| {
|
||||
let top = i32::from(margins.top);
|
||||
let bottom = i32::from(self.height) - i32::from(margins.bottom);
|
||||
let left = i32::from(margins.left);
|
||||
let right = i32::from(self.width) - i32::from(margins.right);
|
||||
let width = right - left;
|
||||
let height = bottom - top;
|
||||
if width < 0 || height < 0 {
|
||||
return; // wrong parameters, do nothing!
|
||||
}
|
||||
|
||||
window.set_pos(left, top);
|
||||
window.set_size(width as u32, height as u32);
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
for win in &mut self.displayed_windows {
|
||||
if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.window_handle) {
|
||||
if !window.visible {
|
||||
continue;
|
||||
}
|
||||
do_margins(
|
||||
if i == 0 {
|
||||
&opts.margins_first
|
||||
} else {
|
||||
&opts.margins_rest
|
||||
},
|
||||
window,
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(
|
||||
&mut self,
|
||||
config: &super::Config,
|
||||
handle: &DisplayHandle,
|
||||
signals: &mut SyncEventQueue<WayVRSignal>,
|
||||
) {
|
||||
if self.visible {
|
||||
if !self.displayed_windows.is_empty() {
|
||||
self.no_windows_since = None;
|
||||
} else if let Some(auto_hide_delay) = config.auto_hide_delay {
|
||||
if let Some(s) = self.no_windows_since {
|
||||
if s + u64::from(auto_hide_delay) < get_millis() {
|
||||
// Auto-hide after specific time
|
||||
signals.send(WayVRSignal::DisplayVisibility(*handle, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(task) = self.tasks.read() {
|
||||
match task {
|
||||
DisplayTask::ProcessCleanup(process_handle) => {
|
||||
let count = self.displayed_windows.len();
|
||||
self.displayed_windows
|
||||
.retain(|win| win.process_handle != process_handle);
|
||||
log::info!(
|
||||
"Cleanup finished for display \"{}\". Current window count: {}",
|
||||
self.name,
|
||||
self.displayed_windows.len()
|
||||
);
|
||||
self.no_windows_since = Some(get_millis());
|
||||
|
||||
if count != self.displayed_windows.len() {
|
||||
signals.send(WayVRSignal::BroadcastStateChanged(
|
||||
packet_server::WvrStateChanged::WindowRemoved,
|
||||
));
|
||||
}
|
||||
|
||||
self.reposition_windows();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick_render(&mut self, renderer: &mut GlesRenderer, time_ms: u64) -> anyhow::Result<()> {
|
||||
renderer.bind(self.gles_texture.clone())?;
|
||||
|
||||
let size = Size::from((i32::from(self.width), i32::from(self.height)));
|
||||
let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
|
||||
|
||||
let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = self
|
||||
.displayed_windows
|
||||
.iter()
|
||||
.flat_map(|display_window| {
|
||||
let wm = self.wm.borrow_mut();
|
||||
if let Some(window) = wm.windows.get(&display_window.window_handle) {
|
||||
if !window.visible {
|
||||
return vec![];
|
||||
}
|
||||
render_elements_from_surface_tree(
|
||||
renderer,
|
||||
display_window.toplevel.wl_surface(),
|
||||
(window.pos_x, window.pos_y),
|
||||
1.0,
|
||||
1.0,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
} else {
|
||||
// Failed to fetch window
|
||||
vec![]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut frame = renderer.render(size, Transform::Normal)?;
|
||||
|
||||
let clear_color = if self.displayed_windows.is_empty() {
|
||||
Color32F::new(1.0, 1.0, 1.0, 0.5)
|
||||
} else {
|
||||
Color32F::new(0.0, 0.0, 0.0, 0.0)
|
||||
};
|
||||
|
||||
frame.clear(clear_color, &[damage])?;
|
||||
|
||||
draw_render_elements(&mut frame, 1.0, &elements, &[damage])?;
|
||||
|
||||
let _sync_point = frame.finish()?;
|
||||
|
||||
for window in &self.displayed_windows {
|
||||
send_frames_surface_tree(window.toplevel.wl_surface(), time_ms as u32);
|
||||
}
|
||||
|
||||
if let egl_data::RenderData::Software(_) = &self.render_data {
|
||||
// Read OpenGL texture into memory. Slow!
|
||||
let pixel_data = renderer.with_context(|gl| unsafe {
|
||||
gl.BindTexture(ffi::TEXTURE_2D, self.gles_texture.tex_id());
|
||||
|
||||
let len = self.width as usize * self.height as usize * 4;
|
||||
let mut data: Box<[u8]> = Box::new_uninit_slice(len).assume_init();
|
||||
gl.ReadPixels(
|
||||
0,
|
||||
0,
|
||||
i32::from(self.width),
|
||||
i32::from(self.height),
|
||||
ffi::RGBA,
|
||||
ffi::UNSIGNED_BYTE,
|
||||
data.as_mut_ptr().cast(),
|
||||
);
|
||||
|
||||
let data: Arc<[u8]> = Arc::from(data);
|
||||
data
|
||||
})?;
|
||||
|
||||
self.render_data =
|
||||
egl_data::RenderData::Software(Some(egl_data::RenderSoftwarePixelsData {
|
||||
data: pixel_data,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_hovered_window(&self, cursor_x: u32, cursor_y: u32) -> Option<window::WindowHandle> {
|
||||
let wm = self.wm.borrow();
|
||||
|
||||
for cell in self.displayed_windows.iter().rev() {
|
||||
if let Some(window) = wm.windows.get(&cell.window_handle) {
|
||||
if !window.visible {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cursor_x as i32) >= window.pos_x
|
||||
&& (cursor_x as i32) < window.pos_x + window.size_x as i32
|
||||
&& (cursor_y as i32) >= window.pos_y
|
||||
&& (cursor_y as i32) < window.pos_y + window.size_y as i32
|
||||
{
|
||||
return Some(cell.window_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub const fn trigger_rerender(&mut self) {
|
||||
self.wants_redraw = true;
|
||||
}
|
||||
|
||||
pub fn set_visible(&mut self, visible: bool) {
|
||||
log::info!("Display \"{}\" visible: {}", self.name.as_str(), visible);
|
||||
if self.visible == visible {
|
||||
return;
|
||||
}
|
||||
self.visible = visible;
|
||||
if visible {
|
||||
self.no_windows_since = None;
|
||||
self.trigger_rerender();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_layout(&mut self, layout: packet_server::WvrDisplayWindowLayout) {
|
||||
log::info!("Display \"{}\" layout: {:?}", self.name.as_str(), layout);
|
||||
if self.layout == layout {
|
||||
return;
|
||||
}
|
||||
self.layout = layout;
|
||||
self.trigger_rerender();
|
||||
self.reposition_windows();
|
||||
}
|
||||
|
||||
pub fn send_mouse_move(
|
||||
&self,
|
||||
config: &super::Config,
|
||||
manager: &mut WayVRCompositor,
|
||||
x: u32,
|
||||
y: u32,
|
||||
) {
|
||||
let current_ms = time::get_millis();
|
||||
if self.last_pressed_time_ms + u64::from(config.click_freeze_time_ms) > current_ms {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(window_handle) = self.get_hovered_window(x, y) {
|
||||
let wm = self.wm.borrow();
|
||||
if let Some(window) = wm.windows.get(&window_handle) {
|
||||
let surf = window.toplevel.wl_surface().clone();
|
||||
let point = Point::<f64, Logical>::from((
|
||||
f64::from(x as i32 - window.pos_x),
|
||||
f64::from(y as i32 - window.pos_y),
|
||||
));
|
||||
|
||||
manager.seat_pointer.motion(
|
||||
&mut manager.state,
|
||||
Some((surf, Point::from((0.0, 0.0)))),
|
||||
&input::pointer::MotionEvent {
|
||||
serial: manager.serial_counter.next_serial(),
|
||||
time: 0,
|
||||
location: point,
|
||||
},
|
||||
);
|
||||
|
||||
manager.seat_pointer.frame(&mut manager.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_mouse_index_number(index: super::MouseIndex) -> u32 {
|
||||
match index {
|
||||
super::MouseIndex::Left => 0x110, /* BTN_LEFT */
|
||||
super::MouseIndex::Center => 0x112, /* BTN_MIDDLE */
|
||||
super::MouseIndex::Right => 0x111, /* BTN_RIGHT */
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_mouse_down(&mut self, manager: &mut WayVRCompositor, index: super::MouseIndex) {
|
||||
// Change keyboard focus to pressed window
|
||||
let loc = manager.seat_pointer.current_location();
|
||||
|
||||
self.last_pressed_time_ms = time::get_millis();
|
||||
|
||||
if let Some(window_handle) =
|
||||
self.get_hovered_window(loc.x.max(0.0) as u32, loc.y.max(0.0) as u32)
|
||||
{
|
||||
let wm = self.wm.borrow();
|
||||
if let Some(window) = wm.windows.get(&window_handle) {
|
||||
let surf = window.toplevel.wl_surface().clone();
|
||||
|
||||
manager.seat_keyboard.set_focus(
|
||||
&mut manager.state,
|
||||
Some(surf),
|
||||
manager.serial_counter.next_serial(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
manager.seat_pointer.button(
|
||||
&mut manager.state,
|
||||
&input::pointer::ButtonEvent {
|
||||
button: Self::get_mouse_index_number(index),
|
||||
serial: manager.serial_counter.next_serial(),
|
||||
time: 0,
|
||||
state: smithay::backend::input::ButtonState::Pressed,
|
||||
},
|
||||
);
|
||||
|
||||
manager.seat_pointer.frame(&mut manager.state);
|
||||
}
|
||||
|
||||
pub fn send_mouse_up(manager: &mut WayVRCompositor, index: super::MouseIndex) {
|
||||
manager.seat_pointer.button(
|
||||
&mut manager.state,
|
||||
&input::pointer::ButtonEvent {
|
||||
button: Self::get_mouse_index_number(index),
|
||||
serial: manager.serial_counter.next_serial(),
|
||||
time: 0,
|
||||
state: smithay::backend::input::ButtonState::Released,
|
||||
},
|
||||
);
|
||||
|
||||
manager.seat_pointer.frame(&mut manager.state);
|
||||
}
|
||||
|
||||
pub fn send_mouse_scroll(manager: &mut WayVRCompositor, delta_y: f32, delta_x: f32) {
|
||||
manager.seat_pointer.axis(
|
||||
&mut manager.state,
|
||||
input::pointer::AxisFrame {
|
||||
source: None,
|
||||
relative_direction: (
|
||||
smithay::backend::input::AxisRelativeDirection::Identical,
|
||||
smithay::backend::input::AxisRelativeDirection::Identical,
|
||||
),
|
||||
time: 0,
|
||||
axis: (f64::from(delta_x), f64::from(-delta_y)),
|
||||
v120: Some((0, (delta_y * -120.0) as i32)),
|
||||
stop: (false, false),
|
||||
},
|
||||
);
|
||||
manager.seat_pointer.frame(&mut manager.state);
|
||||
}
|
||||
|
||||
fn configure_env(&self, cmd: &mut std::process::Command, auth_key: &str) {
|
||||
cmd.env_remove("DISPLAY"); // Goodbye X11
|
||||
cmd.env("WAYLAND_DISPLAY", self.wayland_env.display_num_string());
|
||||
cmd.env("WAYVR_DISPLAY_AUTH", auth_key);
|
||||
}
|
||||
|
||||
pub fn spawn_process(
|
||||
&mut self,
|
||||
exec_path: &str,
|
||||
args: &[&str],
|
||||
env: &[(&str, &str)],
|
||||
) -> anyhow::Result<SpawnProcessResult> {
|
||||
log::info!("Spawning subprocess with exec path \"{exec_path}\"");
|
||||
|
||||
let auth_key = generate_auth_key();
|
||||
|
||||
let mut cmd = std::process::Command::new(exec_path);
|
||||
self.configure_env(&mut cmd, auth_key.as_str());
|
||||
cmd.args(args);
|
||||
|
||||
for e in env {
|
||||
cmd.env(e.0, e.1);
|
||||
}
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(child) => Ok(SpawnProcessResult { auth_key, child }),
|
||||
Err(e) => {
|
||||
anyhow::bail!(
|
||||
"Failed to launch process with path \"{}\": {}. Make sure your exec path exists.",
|
||||
exec_path,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gen_id!(DisplayVec, Display, DisplayCell, DisplayHandle);
|
||||
|
||||
impl DisplayHandle {
|
||||
pub const fn from_packet(handle: packet_server::WvrDisplayHandle) -> Self {
|
||||
Self {
|
||||
generation: handle.generation,
|
||||
idx: handle.idx,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_packet(&self) -> packet_server::WvrDisplayHandle {
|
||||
packet_server::WvrDisplayHandle {
|
||||
idx: self.idx,
|
||||
generation: self.generation,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::egl_ex;
|
||||
use anyhow::anyhow;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EGLData {
|
||||
pub egl: khronos_egl::Instance<khronos_egl::Static>,
|
||||
pub display: khronos_egl::Display,
|
||||
pub config: khronos_egl::Config,
|
||||
pub context: khronos_egl::Context,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bind_egl_function {
|
||||
($func_type:ident, $func:expr) => {
|
||||
std::mem::transmute_copy::<_, $func_type>($func).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DMAbufModifierInfo {
|
||||
pub modifiers: Vec<u64>,
|
||||
pub fourcc: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderDMAbufData {
|
||||
pub fd: i32,
|
||||
pub stride: i32,
|
||||
pub offset: i32,
|
||||
pub mod_info: DMAbufModifierInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderSoftwarePixelsData {
|
||||
pub data: Arc<[u8]>,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RenderData {
|
||||
Dmabuf(RenderDMAbufData),
|
||||
Software(Option<RenderSoftwarePixelsData>), // will be set if the next image data is available
|
||||
}
|
||||
|
||||
impl EGLData {
|
||||
pub fn load_func(&self, func_name: &str) -> anyhow::Result<extern "system" fn()> {
|
||||
let raw_fn = self
|
||||
.egl
|
||||
.get_proc_address(func_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Required EGL function {} not found", func_name))?;
|
||||
Ok(raw_fn)
|
||||
}
|
||||
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
unsafe {
|
||||
let egl = khronos_egl::Instance::new(khronos_egl::Static);
|
||||
|
||||
let display = egl
|
||||
.get_display(khronos_egl::DEFAULT_DISPLAY)
|
||||
.ok_or_else(|| anyhow!(
|
||||
"eglGetDisplay failed. This shouldn't happen unless you don't have any display manager running. Cannot continue, check your EGL installation."
|
||||
))?;
|
||||
|
||||
let (major, minor) = egl.initialize(display)?;
|
||||
log::debug!("EGL version: {major}.{minor}");
|
||||
|
||||
let attrib_list = [
|
||||
khronos_egl::RED_SIZE,
|
||||
8,
|
||||
khronos_egl::GREEN_SIZE,
|
||||
8,
|
||||
khronos_egl::BLUE_SIZE,
|
||||
8,
|
||||
khronos_egl::SURFACE_TYPE,
|
||||
khronos_egl::WINDOW_BIT,
|
||||
khronos_egl::RENDERABLE_TYPE,
|
||||
khronos_egl::OPENGL_BIT,
|
||||
khronos_egl::NONE,
|
||||
];
|
||||
|
||||
let config = egl
|
||||
.choose_first_config(display, &attrib_list)?
|
||||
.ok_or_else(|| anyhow!("Failed to get EGL config"))?;
|
||||
|
||||
egl.bind_api(khronos_egl::OPENGL_ES_API)?;
|
||||
|
||||
log::debug!("eglCreateContext");
|
||||
|
||||
// Require OpenGL ES 3.0
|
||||
let context_attrib_list = [
|
||||
khronos_egl::CONTEXT_MAJOR_VERSION,
|
||||
3,
|
||||
khronos_egl::CONTEXT_MINOR_VERSION,
|
||||
0,
|
||||
khronos_egl::NONE,
|
||||
];
|
||||
|
||||
let context = egl.create_context(display, config, None, &context_attrib_list)?;
|
||||
|
||||
log::debug!("eglMakeCurrent");
|
||||
|
||||
egl.make_current(display, None, None, Some(context))?;
|
||||
|
||||
Ok(Self {
|
||||
egl,
|
||||
display,
|
||||
config,
|
||||
context,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn query_dmabuf_mod_info(&self) -> anyhow::Result<DMAbufModifierInfo> {
|
||||
let target_fourcc = 0x3432_4258; //XB24
|
||||
|
||||
unsafe {
|
||||
use egl_ex::PFNEGLQUERYDMABUFFORMATSEXTPROC;
|
||||
use egl_ex::PFNEGLQUERYDMABUFMODIFIERSEXTPROC;
|
||||
|
||||
let egl_query_dmabuf_formats_ext = bind_egl_function!(
|
||||
PFNEGLQUERYDMABUFFORMATSEXTPROC,
|
||||
&self.load_func("eglQueryDmaBufFormatsEXT")?
|
||||
);
|
||||
|
||||
// Query format count
|
||||
let mut num_formats: khronos_egl::Int = 0;
|
||||
egl_query_dmabuf_formats_ext(
|
||||
self.display.as_ptr(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
&mut num_formats,
|
||||
);
|
||||
|
||||
// Retrieve formt list
|
||||
let mut formats: Vec<i32> = vec![0; num_formats as usize];
|
||||
egl_query_dmabuf_formats_ext(
|
||||
self.display.as_ptr(),
|
||||
num_formats,
|
||||
formats.as_mut_ptr(),
|
||||
&mut num_formats,
|
||||
);
|
||||
|
||||
/*for (idx, format) in formats.iter().enumerate() {
|
||||
let bytes = format.to_le_bytes();
|
||||
log::trace!(
|
||||
"idx {}, format {}{}{}{} (hex {:#x})",
|
||||
idx,
|
||||
bytes[0] as char,
|
||||
bytes[1] as char,
|
||||
bytes[2] as char,
|
||||
bytes[3] as char,
|
||||
format
|
||||
);
|
||||
}*/
|
||||
|
||||
let egl_query_dmabuf_modifiers_ext = bind_egl_function!(
|
||||
PFNEGLQUERYDMABUFMODIFIERSEXTPROC,
|
||||
&self.load_func("eglQueryDmaBufModifiersEXT")?
|
||||
);
|
||||
|
||||
let mut num_mods: khronos_egl::Int = 0;
|
||||
|
||||
// Query modifier count
|
||||
egl_query_dmabuf_modifiers_ext(
|
||||
self.display.as_ptr(),
|
||||
target_fourcc,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut num_mods,
|
||||
);
|
||||
|
||||
if num_mods == 0 {
|
||||
anyhow::bail!("eglQueryDmaBufModifiersEXT modifier count is zero");
|
||||
}
|
||||
|
||||
let mut mods: Vec<u64> = vec![0; num_mods as usize];
|
||||
egl_query_dmabuf_modifiers_ext(
|
||||
self.display.as_ptr(),
|
||||
target_fourcc,
|
||||
num_mods,
|
||||
mods.as_mut_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
&mut num_mods,
|
||||
);
|
||||
|
||||
if mods[0] == 0xFFFF_FFFF_FFFF_FFFF {
|
||||
anyhow::bail!("modifier is -1")
|
||||
}
|
||||
|
||||
log::trace!("Modifier list:");
|
||||
for modifier in &mods {
|
||||
log::trace!("{modifier:#x}");
|
||||
}
|
||||
|
||||
// We should not change these modifier values. Passing all of them to the Vulkan dmabuf
|
||||
// texture system causes significant graphical corruption due to invalid memory layout and
|
||||
// tiling on this specific GPU model (very probably others also have the same issue).
|
||||
// It is not guaranteed that this modifier will be present in other models.
|
||||
// If not, the full list of modifiers will be passed. Further testing is required.
|
||||
// For now, it looks like only NAVI32-based gpus have this problem.
|
||||
let mod_whitelist: [u64; 2] = [
|
||||
0x200_0000_2086_bf04, /* AMD RX 7800 XT, Navi32 */
|
||||
0x200_0000_1866_bf04, /* AMD RX 7600 XT, Navi33 */
|
||||
];
|
||||
|
||||
for modifier in &mod_whitelist {
|
||||
if mods.contains(modifier) {
|
||||
log::warn!("Using whitelisted dmabuf tiling modifier: {modifier:#x}");
|
||||
mods = vec![*modifier, 0x0 /* also important (???) */];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DMAbufModifierInfo {
|
||||
modifiers: mods,
|
||||
fourcc: target_fourcc as u32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_dmabuf_data(
|
||||
&self,
|
||||
egl_image: &khronos_egl::Image,
|
||||
) -> anyhow::Result<RenderDMAbufData> {
|
||||
use egl_ex::PFNEGLEXPORTDMABUFIMAGEMESAPROC as FUNC;
|
||||
unsafe {
|
||||
let egl_export_dmabuf_image_mesa =
|
||||
bind_egl_function!(FUNC, &self.load_func("eglExportDMABUFImageMESA")?);
|
||||
|
||||
let mut fds: [i32; 3] = [0; 3];
|
||||
let mut strides: [i32; 3] = [0; 3];
|
||||
let mut offsets: [i32; 3] = [0; 3];
|
||||
|
||||
if egl_export_dmabuf_image_mesa(
|
||||
self.display.as_ptr(),
|
||||
egl_image.as_ptr(),
|
||||
fds.as_mut_ptr(),
|
||||
strides.as_mut_ptr(),
|
||||
offsets.as_mut_ptr(),
|
||||
) != khronos_egl::TRUE
|
||||
{
|
||||
anyhow::bail!("eglExportDMABUFImageMESA failed");
|
||||
}
|
||||
|
||||
// many planes in RGB data?
|
||||
debug_assert!(fds[1] == 0);
|
||||
debug_assert!(strides[1] == 0);
|
||||
debug_assert!(offsets[1] == 0);
|
||||
|
||||
let mod_info = self.query_dmabuf_mod_info()?;
|
||||
|
||||
Ok(RenderDMAbufData {
|
||||
fd: fds[0],
|
||||
stride: strides[0],
|
||||
offset: offsets[0],
|
||||
mod_info,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_egl_image(&self, gl_tex_id: u32) -> anyhow::Result<khronos_egl::Image> {
|
||||
unsafe {
|
||||
Ok(self.egl.create_image(
|
||||
self.display,
|
||||
self.context,
|
||||
khronos_egl::GL_TEXTURE_2D as std::ffi::c_uint,
|
||||
khronos_egl::ClientBuffer::from_ptr(gl_tex_id as *mut std::ffi::c_void),
|
||||
&[khronos_egl::ATTRIB_NONE],
|
||||
)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
//eglExportDMABUFImageMESA
|
||||
pub type PFNEGLEXPORTDMABUFIMAGEMESAPROC = Option<
|
||||
unsafe extern "C" fn(
|
||||
dpy: khronos_egl::EGLDisplay,
|
||||
image: khronos_egl::EGLImage,
|
||||
fds: *mut i32,
|
||||
strides: *mut khronos_egl::Int,
|
||||
offsets: *mut khronos_egl::Int,
|
||||
) -> khronos_egl::Boolean,
|
||||
>;
|
||||
|
||||
//eglQueryDmaBufModifiersEXT
|
||||
pub type PFNEGLQUERYDMABUFMODIFIERSEXTPROC = Option<
|
||||
unsafe extern "C" fn(
|
||||
dpy: khronos_egl::EGLDisplay,
|
||||
format: khronos_egl::Int,
|
||||
max_modifiers: khronos_egl::Int,
|
||||
modifiers: *mut u64,
|
||||
external_only: *mut khronos_egl::Boolean,
|
||||
num_modifiers: *mut khronos_egl::Int,
|
||||
) -> khronos_egl::Boolean,
|
||||
>;
|
||||
|
||||
//eglQueryDmaBufFormatsEXT
|
||||
pub type PFNEGLQUERYDMABUFFORMATSEXTPROC = Option<
|
||||
unsafe extern "C" fn(
|
||||
dpy: khronos_egl::EGLDisplay,
|
||||
max_formats: khronos_egl::Int,
|
||||
formats: *mut khronos_egl::Int,
|
||||
num_formats: *mut khronos_egl::Int,
|
||||
) -> khronos_egl::Boolean,
|
||||
>;
|
||||
@@ -0,0 +1,33 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Data<DataType> {
|
||||
queue: VecDeque<DataType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SyncEventQueue<DataType> {
|
||||
data: Rc<RefCell<Data<DataType>>>,
|
||||
}
|
||||
|
||||
impl<DataType> SyncEventQueue<DataType> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Rc::new(RefCell::new(Data {
|
||||
queue: VecDeque::default(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send(&self, message: DataType) {
|
||||
let mut data = self.data.borrow_mut();
|
||||
data.queue.push_back(message);
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Option<DataType> {
|
||||
let mut data = self.data.borrow_mut();
|
||||
data.queue.pop_front()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
#[macro_export]
|
||||
macro_rules! gen_id {
|
||||
(
|
||||
$container_name:ident,
|
||||
$instance_name:ident,
|
||||
$cell_name:ident,
|
||||
$handle_name:ident) => {
|
||||
//ThingCell
|
||||
#[derive(Debug)]
|
||||
pub struct $cell_name {
|
||||
pub obj: $instance_name,
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
//ThingVec
|
||||
#[derive(Debug)]
|
||||
pub struct $container_name {
|
||||
// Vec<Option<ThingCell>>
|
||||
pub vec: Vec<Option<$cell_name>>,
|
||||
|
||||
cur_generation: u64,
|
||||
}
|
||||
|
||||
//ThingHandle
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Hash, Eq)]
|
||||
pub struct $handle_name {
|
||||
idx: u32,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl $handle_name {
|
||||
pub const fn reset(&mut self) {
|
||||
self.generation = 0;
|
||||
}
|
||||
|
||||
pub const fn is_set(&self) -> bool {
|
||||
self.generation > 0
|
||||
}
|
||||
|
||||
pub const fn id(&self) -> u32 {
|
||||
self.idx
|
||||
}
|
||||
|
||||
pub const fn new(idx: u32, generation: u64) -> Self {
|
||||
Self { idx, generation }
|
||||
}
|
||||
}
|
||||
|
||||
//ThingVec
|
||||
#[allow(dead_code, clippy::iter_not_returning_iterator)]
|
||||
impl $container_name {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
vec: Vec::new(),
|
||||
cur_generation: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self, callback: &dyn Fn($handle_name, &$instance_name)) {
|
||||
for (idx, opt_cell) in self.vec.iter().enumerate() {
|
||||
if let Some(cell) = opt_cell {
|
||||
let handle = $container_name::get_handle(&cell, idx);
|
||||
callback(handle, &cell.obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_mut(
|
||||
&mut self,
|
||||
callback: &mut dyn FnMut($handle_name, &mut $instance_name),
|
||||
) {
|
||||
for (idx, opt_cell) in self.vec.iter_mut().enumerate() {
|
||||
if let Some(cell) = opt_cell {
|
||||
let handle = $container_name::get_handle(&cell, idx);
|
||||
callback(handle, &mut cell.obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn get_handle(cell: &$cell_name, idx: usize) -> $handle_name {
|
||||
$handle_name {
|
||||
idx: idx as u32,
|
||||
generation: cell.generation,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_unused_idx(&mut self) -> Option<u32> {
|
||||
for (num, obj) in self.vec.iter().enumerate() {
|
||||
if obj.is_none() {
|
||||
return Some(num as u32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn add(&mut self, obj: $instance_name) -> $handle_name {
|
||||
self.cur_generation += 1;
|
||||
let generation = self.cur_generation;
|
||||
|
||||
let unused_idx = self.find_unused_idx();
|
||||
|
||||
let idx = if let Some(idx) = unused_idx {
|
||||
idx
|
||||
} else {
|
||||
self.vec.len() as u32
|
||||
};
|
||||
|
||||
let handle = $handle_name { idx, generation };
|
||||
|
||||
let cell = $cell_name { obj, generation };
|
||||
|
||||
if let Some(idx) = unused_idx {
|
||||
self.vec[idx as usize] = Some(cell);
|
||||
} else {
|
||||
self.vec.push(Some(cell))
|
||||
}
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, handle: &$handle_name) {
|
||||
// Out of bounds, ignore
|
||||
if handle.idx as usize >= self.vec.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove only if the generation matches
|
||||
if let Some(cell) = &self.vec[handle.idx as usize] {
|
||||
if cell.generation == handle.generation {
|
||||
self.vec[handle.idx as usize] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, handle: &$handle_name) -> Option<&$instance_name> {
|
||||
// Out of bounds, ignore
|
||||
if handle.idx as usize >= self.vec.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(cell) = &self.vec[handle.idx as usize] {
|
||||
if cell.generation == handle.generation {
|
||||
return Some(&cell.obj);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, handle: &$handle_name) -> Option<&mut $instance_name> {
|
||||
// Out of bounds, ignore
|
||||
if handle.idx as usize >= self.vec.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(cell) = &mut self.vec[handle.idx as usize] {
|
||||
if cell.generation == handle.generation {
|
||||
return Some(&mut cell.obj);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* Example usage:
|
||||
gen_id!(ThingVec, ThingInstance, ThingCell, ThingHandle);
|
||||
|
||||
struct ThingInstance {}
|
||||
|
||||
impl ThingInstance {}
|
||||
*/
|
||||
@@ -0,0 +1,725 @@
|
||||
pub mod client;
|
||||
mod comp;
|
||||
pub mod display;
|
||||
pub mod egl_data;
|
||||
mod egl_ex;
|
||||
pub mod event_queue;
|
||||
mod handle;
|
||||
mod process;
|
||||
pub mod server_ipc;
|
||||
mod smithay_wrapper;
|
||||
mod time;
|
||||
mod window;
|
||||
use comp::Application;
|
||||
use display::{Display, DisplayInitParams, DisplayVec};
|
||||
use event_queue::SyncEventQueue;
|
||||
use process::ProcessVec;
|
||||
use server_ipc::WayVRServer;
|
||||
use smallvec::SmallVec;
|
||||
use smithay::{
|
||||
backend::{
|
||||
egl,
|
||||
renderer::{gles::GlesRenderer, ImportDma},
|
||||
},
|
||||
input::{keyboard::XkbConfig, SeatState},
|
||||
output::{Mode, Output},
|
||||
reexports::wayland_server::{self, backend::ClientId},
|
||||
wayland::{
|
||||
compositor,
|
||||
dmabuf::{DmabufFeedbackBuilder, DmabufState},
|
||||
selection::data_device::DataDeviceState,
|
||||
shell::xdg::{ToplevelSurface, XdgShellState},
|
||||
shm::ShmState,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
rc::Rc,
|
||||
};
|
||||
use time::get_millis;
|
||||
use wayvr_ipc::{packet_client, packet_server};
|
||||
|
||||
use crate::{hid::MODS_TO_KEYS, state::AppState};
|
||||
|
||||
const STR_INVALID_HANDLE_DISP: &str = "Invalid display handle";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WaylandEnv {
|
||||
pub display_num: u32,
|
||||
}
|
||||
|
||||
impl WaylandEnv {
|
||||
pub fn display_num_string(&self) -> String {
|
||||
// e.g. "wayland-20"
|
||||
format!("wayland-{}", self.display_num)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProcessWayVREnv {
|
||||
pub display_auth: Option<String>,
|
||||
pub display_name: Option<String>, // Externally spawned process by a user script
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExternalProcessRequest {
|
||||
pub env: ProcessWayVREnv,
|
||||
pub client: wayland_server::Client,
|
||||
pub pid: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum WayVRTask {
|
||||
NewToplevel(ClientId, ToplevelSurface),
|
||||
DropToplevel(ClientId, ToplevelSurface),
|
||||
NewExternalProcess(ExternalProcessRequest),
|
||||
DropOverlay(super::overlay::OverlayID),
|
||||
ProcessTerminationRequest(process::ProcessHandle),
|
||||
Haptics(super::input::Haptics),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum WayVRSignal {
|
||||
DisplayVisibility(display::DisplayHandle, bool),
|
||||
DisplayWindowLayout(
|
||||
display::DisplayHandle,
|
||||
packet_server::WvrDisplayWindowLayout,
|
||||
),
|
||||
BroadcastStateChanged(packet_server::WvrStateChanged),
|
||||
}
|
||||
|
||||
pub enum BlitMethod {
|
||||
Dmabuf,
|
||||
Software,
|
||||
}
|
||||
|
||||
impl BlitMethod {
|
||||
pub fn from_string(str: &str) -> Option<Self> {
|
||||
match str {
|
||||
"dmabuf" => Some(Self::Dmabuf),
|
||||
"software" => Some(Self::Software),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub click_freeze_time_ms: u32,
|
||||
pub keyboard_repeat_delay_ms: u32,
|
||||
pub keyboard_repeat_rate: u32,
|
||||
pub auto_hide_delay: Option<u32>, // if None, auto-hide is disabled
|
||||
pub blit_method: BlitMethod,
|
||||
}
|
||||
|
||||
pub struct WayVRState {
|
||||
time_start: u64,
|
||||
pub displays: display::DisplayVec,
|
||||
pub manager: client::WayVRCompositor,
|
||||
wm: Rc<RefCell<window::WindowManager>>,
|
||||
egl_data: Rc<egl_data::EGLData>,
|
||||
pub processes: process::ProcessVec,
|
||||
pub config: Config,
|
||||
dashboard_display: Option<display::DisplayHandle>,
|
||||
pub tasks: SyncEventQueue<WayVRTask>,
|
||||
pub signals: SyncEventQueue<WayVRSignal>,
|
||||
ticks: u64,
|
||||
pub pending_haptic: Option<super::input::Haptics>,
|
||||
cur_modifiers: u8,
|
||||
}
|
||||
|
||||
pub struct WayVR {
|
||||
pub state: WayVRState,
|
||||
pub ipc_server: WayVRServer,
|
||||
}
|
||||
|
||||
pub enum MouseIndex {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub enum TickTask {
|
||||
NewExternalProcess(ExternalProcessRequest), // Call WayVRCompositor::add_client after receiving this message
|
||||
NewDisplay(
|
||||
packet_client::WvrDisplayCreateParams,
|
||||
Option<display::DisplayHandle>, /* existing handle? */
|
||||
),
|
||||
DropOverlay(super::overlay::OverlayID),
|
||||
}
|
||||
|
||||
impl WayVR {
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
pub fn new(config: Config) -> anyhow::Result<Self> {
|
||||
log::info!("Initializing WayVR");
|
||||
let display: wayland_server::Display<Application> = wayland_server::Display::new()?;
|
||||
let dh = display.handle();
|
||||
let compositor = compositor::CompositorState::new::<Application>(&dh);
|
||||
let xdg_shell = XdgShellState::new::<Application>(&dh);
|
||||
let mut seat_state = SeatState::new();
|
||||
let shm = ShmState::new::<Application>(&dh, Vec::new());
|
||||
let data_device = DataDeviceState::new::<Application>(&dh);
|
||||
let mut seat = seat_state.new_wl_seat(&dh, "wayvr");
|
||||
|
||||
let dummy_width = 1280;
|
||||
let dummy_height = 720;
|
||||
let dummy_milli_hz = 60000; /* refresh rate in millihertz */
|
||||
|
||||
let output = Output::new(
|
||||
String::from("wayvr_display"),
|
||||
smithay::output::PhysicalProperties {
|
||||
size: (dummy_width, dummy_height).into(),
|
||||
subpixel: smithay::output::Subpixel::None,
|
||||
make: String::from("Completely Legit"),
|
||||
model: String::from("Virtual WayVR Display"),
|
||||
},
|
||||
);
|
||||
|
||||
let mode = Mode {
|
||||
refresh: dummy_milli_hz,
|
||||
size: (dummy_width, dummy_height).into(),
|
||||
};
|
||||
|
||||
let _global = output.create_global::<Application>(&dh);
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
output.set_preferred(mode);
|
||||
|
||||
let egl_data = egl_data::EGLData::new()?;
|
||||
|
||||
let smithay_display = smithay_wrapper::get_egl_display(&egl_data)?;
|
||||
let smithay_context = smithay_wrapper::get_egl_context(&egl_data, &smithay_display)?;
|
||||
|
||||
let render_node = egl::EGLDevice::device_for_display(&smithay_display)
|
||||
.and_then(|device| device.try_get_render_node());
|
||||
|
||||
let gles_renderer = unsafe { GlesRenderer::new(smithay_context)? };
|
||||
|
||||
let dmabuf_default_feedback = match render_node {
|
||||
Ok(Some(node)) => {
|
||||
let dmabuf_formats = gles_renderer.dmabuf_formats();
|
||||
let dmabuf_default_feedback =
|
||||
DmabufFeedbackBuilder::new(node.dev_id(), dmabuf_formats)
|
||||
.build()
|
||||
.unwrap();
|
||||
Some(dmabuf_default_feedback)
|
||||
}
|
||||
Ok(None) => {
|
||||
log::warn!("dmabuf: Failed to query render node");
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("dmabuf: Failed to get egl device for display: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let dmabuf_state = dmabuf_default_feedback.map_or_else(
|
||||
|| {
|
||||
let dmabuf_formats = gles_renderer.dmabuf_formats();
|
||||
let mut dmabuf_state = DmabufState::new();
|
||||
let dmabuf_global =
|
||||
dmabuf_state.create_global::<Application>(&display.handle(), dmabuf_formats);
|
||||
(dmabuf_state, dmabuf_global, None)
|
||||
},
|
||||
|default_feedback| {
|
||||
let mut dmabuf_state = DmabufState::new();
|
||||
let dmabuf_global = dmabuf_state
|
||||
.create_global_with_default_feedback::<Application>(
|
||||
&display.handle(),
|
||||
&default_feedback,
|
||||
);
|
||||
(dmabuf_state, dmabuf_global, Some(default_feedback))
|
||||
},
|
||||
);
|
||||
|
||||
let seat_keyboard = seat.add_keyboard(
|
||||
XkbConfig::default(),
|
||||
config.keyboard_repeat_delay_ms as i32,
|
||||
config.keyboard_repeat_rate as i32,
|
||||
)?;
|
||||
let seat_pointer = seat.add_pointer();
|
||||
|
||||
let tasks = SyncEventQueue::new();
|
||||
|
||||
let state = Application {
|
||||
compositor,
|
||||
xdg_shell,
|
||||
seat_state,
|
||||
shm,
|
||||
data_device,
|
||||
wayvr_tasks: tasks.clone(),
|
||||
redraw_requests: HashSet::new(),
|
||||
dmabuf_state,
|
||||
gles_renderer,
|
||||
};
|
||||
|
||||
let time_start = get_millis();
|
||||
|
||||
let ipc_server = WayVRServer::new()?;
|
||||
|
||||
let state = WayVRState {
|
||||
time_start,
|
||||
manager: client::WayVRCompositor::new(state, display, seat_keyboard, seat_pointer)?,
|
||||
displays: DisplayVec::new(),
|
||||
processes: ProcessVec::new(),
|
||||
egl_data: Rc::new(egl_data),
|
||||
wm: Rc::new(RefCell::new(window::WindowManager::new())),
|
||||
config,
|
||||
dashboard_display: None,
|
||||
ticks: 0,
|
||||
tasks,
|
||||
pending_haptic: None,
|
||||
signals: SyncEventQueue::new(),
|
||||
cur_modifiers: 0,
|
||||
};
|
||||
|
||||
Ok(Self { state, ipc_server })
|
||||
}
|
||||
|
||||
pub fn tick_display(&mut self, display: display::DisplayHandle) -> anyhow::Result<bool> {
|
||||
// millis since the start of wayvr
|
||||
let display = self
|
||||
.state
|
||||
.displays
|
||||
.get_mut(&display)
|
||||
.ok_or_else(|| anyhow::anyhow!(STR_INVALID_HANDLE_DISP))?;
|
||||
|
||||
if !display.wants_redraw {
|
||||
// Nothing changed, do not render
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !display.visible {
|
||||
// Display is invisible, do not render
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let time_ms = get_millis() - self.state.time_start;
|
||||
|
||||
display.tick_render(&mut self.state.manager.state.gles_renderer, time_ms)?;
|
||||
display.wants_redraw = false;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
pub fn tick_events(&mut self, app: &AppState) -> anyhow::Result<Vec<TickTask>> {
|
||||
let mut tasks: Vec<TickTask> = Vec::new();
|
||||
|
||||
self.ipc_server.tick(&mut server_ipc::TickParams {
|
||||
state: &mut self.state,
|
||||
tasks: &mut tasks,
|
||||
app,
|
||||
});
|
||||
|
||||
// Check for redraw events
|
||||
self.state.displays.iter_mut(&mut |_, disp| {
|
||||
for disp_window in &disp.displayed_windows {
|
||||
if self
|
||||
.state
|
||||
.manager
|
||||
.state
|
||||
.check_redraw(disp_window.toplevel.wl_surface())
|
||||
{
|
||||
disp.wants_redraw = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick all child processes
|
||||
let mut to_remove: SmallVec<[(process::ProcessHandle, display::DisplayHandle); 2]> =
|
||||
SmallVec::new();
|
||||
|
||||
self.state.processes.iter_mut(&mut |handle, process| {
|
||||
if !process.is_running() {
|
||||
to_remove.push((handle, process.display_handle()));
|
||||
}
|
||||
});
|
||||
|
||||
for (p_handle, disp_handle) in &to_remove {
|
||||
self.state.processes.remove(p_handle);
|
||||
|
||||
if let Some(display) = self.state.displays.get_mut(disp_handle) {
|
||||
display
|
||||
.tasks
|
||||
.send(display::DisplayTask::ProcessCleanup(*p_handle));
|
||||
display.wants_redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
self.state.displays.iter_mut(&mut |handle, display| {
|
||||
display.tick(&self.state.config, &handle, &mut self.state.signals);
|
||||
});
|
||||
|
||||
if !to_remove.is_empty() {
|
||||
self.state.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||
packet_server::WvrStateChanged::ProcessRemoved,
|
||||
));
|
||||
}
|
||||
|
||||
while let Some(task) = self.state.tasks.read() {
|
||||
match task {
|
||||
WayVRTask::NewExternalProcess(req) => {
|
||||
tasks.push(TickTask::NewExternalProcess(req));
|
||||
}
|
||||
WayVRTask::DropOverlay(overlay_id) => {
|
||||
tasks.push(TickTask::DropOverlay(overlay_id));
|
||||
}
|
||||
WayVRTask::NewToplevel(client_id, toplevel) => {
|
||||
// Attach newly created toplevel surfaces to displays
|
||||
for client in &self.state.manager.clients {
|
||||
if client.client.id() != client_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(process_handle) =
|
||||
process::find_by_pid(&self.state.processes, client.pid)
|
||||
else {
|
||||
log::error!(
|
||||
"WayVR window creation failed: Unexpected process ID {}. It wasn't registered before.",
|
||||
client.pid
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let window_handle = self
|
||||
.state
|
||||
.wm
|
||||
.borrow_mut()
|
||||
.create_window(client.display_handle, &toplevel);
|
||||
|
||||
let Some(display) = self.state.displays.get_mut(&client.display_handle)
|
||||
else {
|
||||
// This shouldn't happen, scream if it does
|
||||
log::error!("Could not attach window handle into display");
|
||||
continue;
|
||||
};
|
||||
|
||||
display.add_window(window_handle, process_handle, &toplevel);
|
||||
self.state.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||
packet_server::WvrStateChanged::WindowCreated,
|
||||
));
|
||||
}
|
||||
}
|
||||
WayVRTask::DropToplevel(client_id, toplevel) => {
|
||||
for client in &self.state.manager.clients {
|
||||
if client.client.id() != client_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut wm = self.state.wm.borrow_mut();
|
||||
let Some(window_handle) = wm.find_window_handle(&toplevel) else {
|
||||
log::warn!("DropToplevel: Couldn't find matching window handle");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(display) = self.state.displays.get_mut(&client.display_handle)
|
||||
else {
|
||||
log::warn!("DropToplevel: Couldn't find matching display");
|
||||
continue;
|
||||
};
|
||||
|
||||
display.remove_window(window_handle);
|
||||
wm.remove_window(window_handle);
|
||||
|
||||
drop(wm);
|
||||
|
||||
display.reposition_windows();
|
||||
}
|
||||
}
|
||||
WayVRTask::ProcessTerminationRequest(process_handle) => {
|
||||
if let Some(process) = self.state.processes.get_mut(&process_handle) {
|
||||
process.terminate();
|
||||
}
|
||||
}
|
||||
WayVRTask::Haptics(haptics) => {
|
||||
self.state.pending_haptic = Some(haptics);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.state
|
||||
.manager
|
||||
.tick_wayland(&mut self.state.displays, &mut self.state.processes)?;
|
||||
|
||||
if self.state.ticks % 200 == 0 {
|
||||
self.state.manager.cleanup_clients();
|
||||
}
|
||||
|
||||
self.state.ticks += 1;
|
||||
|
||||
Ok(tasks)
|
||||
}
|
||||
|
||||
pub fn tick_finish(&mut self) -> anyhow::Result<()> {
|
||||
self.state
|
||||
.manager
|
||||
.state
|
||||
.gles_renderer
|
||||
.with_context(|gl| unsafe {
|
||||
gl.Flush();
|
||||
gl.Finish();
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_primary_display(displays: &DisplayVec) -> Option<display::DisplayHandle> {
|
||||
for (idx, cell) in displays.vec.iter().enumerate() {
|
||||
if let Some(cell) = cell {
|
||||
if cell.obj.primary {
|
||||
return Some(DisplayVec::get_handle(cell, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_display_by_name(
|
||||
displays: &DisplayVec,
|
||||
name: &str,
|
||||
) -> Option<display::DisplayHandle> {
|
||||
for (idx, cell) in displays.vec.iter().enumerate() {
|
||||
if let Some(cell) = cell {
|
||||
if cell.obj.name == name {
|
||||
return Some(DisplayVec::get_handle(cell, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn terminate_process(&mut self, process_handle: process::ProcessHandle) {
|
||||
self.state
|
||||
.tasks
|
||||
.send(WayVRTask::ProcessTerminationRequest(process_handle));
|
||||
}
|
||||
}
|
||||
|
||||
impl WayVRState {
|
||||
pub fn send_mouse_move(&mut self, display: display::DisplayHandle, x: u32, y: u32) {
|
||||
if let Some(display) = self.displays.get(&display) {
|
||||
display.send_mouse_move(&self.config, &mut self.manager, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_mouse_down(&mut self, display: display::DisplayHandle, index: MouseIndex) {
|
||||
if let Some(display) = self.displays.get_mut(&display) {
|
||||
display.send_mouse_down(&mut self.manager, index);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_mouse_up(&mut self, index: MouseIndex) {
|
||||
Display::send_mouse_up(&mut self.manager, index);
|
||||
}
|
||||
|
||||
pub fn send_mouse_scroll(&mut self, delta_y: f32, delta_x: f32) {
|
||||
Display::send_mouse_scroll(&mut self.manager, delta_y, delta_x);
|
||||
}
|
||||
|
||||
pub fn send_key(&mut self, virtual_key: u32, down: bool) {
|
||||
self.manager.send_key(virtual_key, down);
|
||||
}
|
||||
|
||||
pub fn set_modifiers(&mut self, modifiers: u8) {
|
||||
let changed = self.cur_modifiers ^ modifiers;
|
||||
for i in 0..8 {
|
||||
let m = 1 << i;
|
||||
if changed & m != 0 {
|
||||
if let Some(vk) = MODS_TO_KEYS.get(m).into_iter().flatten().next() {
|
||||
self.send_key(*vk as u32, modifiers & m != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cur_modifiers = modifiers;
|
||||
}
|
||||
|
||||
pub fn set_display_visible(&mut self, display: display::DisplayHandle, visible: bool) {
|
||||
if let Some(display) = self.displays.get_mut(&display) {
|
||||
display.set_visible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_display_layout(
|
||||
&mut self,
|
||||
display: display::DisplayHandle,
|
||||
layout: packet_server::WvrDisplayWindowLayout,
|
||||
) {
|
||||
if let Some(display) = self.displays.get_mut(&display) {
|
||||
display.set_layout(layout);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_render_data(
|
||||
&self,
|
||||
display: display::DisplayHandle,
|
||||
) -> Option<&egl_data::RenderData> {
|
||||
self.displays
|
||||
.get(&display)
|
||||
.map(|display| &display.render_data)
|
||||
}
|
||||
|
||||
pub fn create_display(
|
||||
&mut self,
|
||||
width: u16,
|
||||
height: u16,
|
||||
name: &str,
|
||||
primary: bool,
|
||||
) -> anyhow::Result<display::DisplayHandle> {
|
||||
let display = display::Display::new(DisplayInitParams {
|
||||
wm: self.wm.clone(),
|
||||
egl_data: self.egl_data.clone(),
|
||||
renderer: &mut self.manager.state.gles_renderer,
|
||||
wayland_env: self.manager.wayland_env.clone(),
|
||||
config: &self.config,
|
||||
width,
|
||||
height,
|
||||
name,
|
||||
primary,
|
||||
})?;
|
||||
let handle = self.displays.add(display);
|
||||
|
||||
self.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||
packet_server::WvrStateChanged::DisplayCreated,
|
||||
));
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
pub fn destroy_display(&mut self, handle: display::DisplayHandle) -> anyhow::Result<()> {
|
||||
let Some(display) = self.displays.get(&handle) else {
|
||||
anyhow::bail!("Display not found");
|
||||
};
|
||||
|
||||
if let Some(overlay_id) = display.overlay_id {
|
||||
self.tasks.send(WayVRTask::DropOverlay(overlay_id));
|
||||
} else {
|
||||
log::warn!("Destroying display without OverlayID set"); // This shouldn't happen, but log it anyways.
|
||||
}
|
||||
|
||||
let mut process_names = Vec::<String>::new();
|
||||
|
||||
self.processes.iter_mut(&mut |_, process| {
|
||||
if process.display_handle() == handle {
|
||||
process_names.push(process.get_name());
|
||||
}
|
||||
});
|
||||
|
||||
if !display.displayed_windows.is_empty() || !process_names.is_empty() {
|
||||
anyhow::bail!(
|
||||
"Display is not empty. Attached processes: {}",
|
||||
process_names.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
self.manager.cleanup_clients();
|
||||
|
||||
for client in &self.manager.clients {
|
||||
if client.display_handle == handle {
|
||||
// This shouldn't happen, but make sure we are all set to destroy this display
|
||||
anyhow::bail!("Wayland client still exists");
|
||||
}
|
||||
}
|
||||
|
||||
self.displays.remove(&handle);
|
||||
|
||||
self.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||
packet_server::WvrStateChanged::DisplayRemoved,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_or_create_dashboard_display(
|
||||
&mut self,
|
||||
width: u16,
|
||||
height: u16,
|
||||
name: &str,
|
||||
) -> anyhow::Result<(bool /* newly created? */, display::DisplayHandle)> {
|
||||
if let Some(handle) = &self.dashboard_display {
|
||||
// ensure it still exists
|
||||
if self.displays.get(handle).is_some() {
|
||||
return Ok((false, *handle));
|
||||
}
|
||||
}
|
||||
|
||||
let new_disp = self.create_display(width, height, name, false)?;
|
||||
self.dashboard_display = Some(new_disp);
|
||||
|
||||
Ok((true, new_disp))
|
||||
}
|
||||
|
||||
// Check if process with given arguments already exists
|
||||
pub fn process_query(
|
||||
&self,
|
||||
display_handle: display::DisplayHandle,
|
||||
exec_path: &str,
|
||||
args: &[&str],
|
||||
_env: &[(&str, &str)],
|
||||
) -> Option<process::ProcessHandle> {
|
||||
for (idx, cell) in self.processes.vec.iter().enumerate() {
|
||||
if let Some(cell) = &cell {
|
||||
if let process::Process::Managed(process) = &cell.obj {
|
||||
if process.display_handle != display_handle
|
||||
|| process.exec_path != exec_path
|
||||
|| process.args != args
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return Some(process::ProcessVec::get_handle(cell, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn add_external_process(
|
||||
&mut self,
|
||||
display_handle: display::DisplayHandle,
|
||||
pid: u32,
|
||||
) -> process::ProcessHandle {
|
||||
self.processes
|
||||
.add(process::Process::External(process::ExternalProcess {
|
||||
pid,
|
||||
display_handle,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn spawn_process(
|
||||
&mut self,
|
||||
display_handle: display::DisplayHandle,
|
||||
exec_path: &str,
|
||||
args: &[&str],
|
||||
env: &[(&str, &str)],
|
||||
userdata: HashMap<String, String>,
|
||||
) -> anyhow::Result<process::ProcessHandle> {
|
||||
let display = self
|
||||
.displays
|
||||
.get_mut(&display_handle)
|
||||
.ok_or_else(|| anyhow::anyhow!(STR_INVALID_HANDLE_DISP))?;
|
||||
|
||||
let res = display.spawn_process(exec_path, args, env)?;
|
||||
|
||||
let handle = self
|
||||
.processes
|
||||
.add(process::Process::Managed(process::WayVRProcess {
|
||||
auth_key: res.auth_key,
|
||||
child: res.child,
|
||||
display_handle,
|
||||
exec_path: String::from(exec_path),
|
||||
userdata,
|
||||
args: args.iter().map(|x| String::from(*x)).collect(),
|
||||
env: env
|
||||
.iter()
|
||||
.map(|(a, b)| (String::from(*a), String::from(*b)))
|
||||
.collect(),
|
||||
}));
|
||||
|
||||
self.signals.send(WayVRSignal::BroadcastStateChanged(
|
||||
packet_server::WvrStateChanged::ProcessCreated,
|
||||
));
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
use std::{collections::HashMap, io::Read};
|
||||
|
||||
use wayvr_ipc::packet_server;
|
||||
|
||||
use crate::gen_id;
|
||||
|
||||
use super::display;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct WayVRProcess {
|
||||
pub auth_key: String,
|
||||
pub child: std::process::Child,
|
||||
pub display_handle: display::DisplayHandle,
|
||||
|
||||
pub exec_path: String,
|
||||
pub args: Vec<String>,
|
||||
pub env: Vec<(String, String)>,
|
||||
|
||||
pub userdata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExternalProcess {
|
||||
pub pid: u32,
|
||||
pub display_handle: display::DisplayHandle,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Process {
|
||||
Managed(WayVRProcess), // Process spawned by WayVR
|
||||
External(ExternalProcess), // External process not directly controlled by us
|
||||
}
|
||||
|
||||
impl Process {
|
||||
pub const fn display_handle(&self) -> display::DisplayHandle {
|
||||
match self {
|
||||
Self::Managed(p) => p.display_handle,
|
||||
Self::External(p) => p.display_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_running(&mut self) -> bool {
|
||||
match self {
|
||||
Self::Managed(p) => p.is_running(),
|
||||
Self::External(p) => p.is_running(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn terminate(&mut self) {
|
||||
match self {
|
||||
Self::Managed(p) => p.terminate(),
|
||||
Self::External(p) => p.terminate(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> String {
|
||||
match self {
|
||||
Self::Managed(p) => p.get_name().unwrap_or_else(|| String::from("unknown")),
|
||||
Self::External(p) => p.get_name().unwrap_or_else(|| String::from("unknown")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_packet(&self, handle: ProcessHandle) -> packet_server::WvrProcess {
|
||||
match self {
|
||||
Self::Managed(p) => packet_server::WvrProcess {
|
||||
name: p.get_name().unwrap_or_else(|| String::from("unknown")),
|
||||
userdata: p.userdata.clone(),
|
||||
display_handle: p.display_handle.as_packet(),
|
||||
handle: handle.as_packet(),
|
||||
},
|
||||
Self::External(p) => packet_server::WvrProcess {
|
||||
name: p.get_name().unwrap_or_else(|| String::from("unknown")),
|
||||
userdata: HashMap::default(),
|
||||
display_handle: p.display_handle.as_packet(),
|
||||
handle: handle.as_packet(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WayVRProcess {
|
||||
fn drop(&mut self) {
|
||||
log::info!(
|
||||
"Sending SIGTERM (graceful exit) to process {}",
|
||||
self.exec_path.as_str()
|
||||
);
|
||||
self.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_process_env_value(pid: i32, key: &str) -> anyhow::Result<Option<String>> {
|
||||
let path = format!("/proc/{pid}/environ");
|
||||
let mut env_data = String::new();
|
||||
std::fs::File::open(path)?.read_to_string(&mut env_data)?;
|
||||
let lines: Vec<&str> = env_data.split('\0').filter(|s| !s.is_empty()).collect();
|
||||
|
||||
for line in lines {
|
||||
if let Some(cell) = line.split_once('=') {
|
||||
if cell.0 == key {
|
||||
return Ok(Some(String::from(cell.1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
impl WayVRProcess {
|
||||
fn is_running(&mut self) -> bool {
|
||||
match self.child.try_wait() {
|
||||
Ok(Some(_exit_status)) => false,
|
||||
Ok(None) => true,
|
||||
Err(e) => {
|
||||
// this shouldn't happen
|
||||
log::error!("Child::try_wait failed: {e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn terminate(&mut self) {
|
||||
unsafe {
|
||||
// Gracefully stop process
|
||||
libc::kill(self.child.id() as i32, libc::SIGTERM);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> Option<String> {
|
||||
get_exec_name_from_pid(self.child.id())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_exec_name_from_pid(pid: u32) -> Option<String> {
|
||||
let path = format!("/proc/{pid}/exe");
|
||||
match std::fs::read_link(&path) {
|
||||
Ok(buf) => {
|
||||
if let Some(process_name) = buf.file_name().and_then(|s| s.to_str()) {
|
||||
return Some(String::from(process_name));
|
||||
}
|
||||
None
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl ExternalProcess {
|
||||
fn is_running(&self) -> bool {
|
||||
if self.pid == 0 {
|
||||
false
|
||||
} else {
|
||||
std::fs::metadata(format!("/proc/{}", self.pid)).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn terminate(&mut self) {
|
||||
if self.pid != 0 {
|
||||
unsafe {
|
||||
// send SIGINT (^C)
|
||||
libc::kill(self.pid as i32, libc::SIGINT);
|
||||
}
|
||||
}
|
||||
self.pid = 0;
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> Option<String> {
|
||||
get_exec_name_from_pid(self.pid)
|
||||
}
|
||||
}
|
||||
|
||||
gen_id!(ProcessVec, Process, ProcessCell, ProcessHandle);
|
||||
|
||||
pub fn find_by_pid(processes: &ProcessVec, pid: u32) -> Option<ProcessHandle> {
|
||||
log::debug!("Finding process with PID {pid}");
|
||||
|
||||
for (idx, cell) in processes.vec.iter().enumerate() {
|
||||
let Some(cell) = cell else {
|
||||
continue;
|
||||
};
|
||||
match &cell.obj {
|
||||
Process::Managed(wayvr_process) => {
|
||||
if wayvr_process.child.id() == pid {
|
||||
return Some(ProcessVec::get_handle(cell, idx));
|
||||
}
|
||||
}
|
||||
Process::External(external_process) => {
|
||||
if external_process.pid == pid {
|
||||
return Some(ProcessVec::get_handle(cell, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Finding by PID failed, trying WAYVR_DISPLAY_AUTH...");
|
||||
|
||||
if let Ok(Some(value)) = get_process_env_value(pid as i32, "WAYVR_DISPLAY_AUTH") {
|
||||
for (idx, cell) in processes.vec.iter().enumerate() {
|
||||
let Some(cell) = cell else {
|
||||
continue;
|
||||
};
|
||||
if let Process::Managed(wayvr_process) = &cell.obj {
|
||||
if wayvr_process.auth_key == value {
|
||||
return Some(ProcessVec::get_handle(cell, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Process find with PID {pid} failed");
|
||||
None
|
||||
}
|
||||
|
||||
impl ProcessHandle {
|
||||
pub const fn from_packet(handle: packet_server::WvrProcessHandle) -> Self {
|
||||
Self {
|
||||
generation: handle.generation,
|
||||
idx: handle.idx,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_packet(&self) -> packet_server::WvrProcessHandle {
|
||||
packet_server::WvrProcessHandle {
|
||||
idx: self.idx,
|
||||
generation: self.generation,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::{display, process, window, TickTask, WayVRSignal};
|
||||
use bytes::BufMut;
|
||||
use glam::Vec3A;
|
||||
use interprocess::local_socket::{self, traits::Listener, ToNsName};
|
||||
use smallvec::SmallVec;
|
||||
use std::io::{Read, Write};
|
||||
use wayvr_ipc::{
|
||||
ipc::{self},
|
||||
packet_client::{self, PacketClient},
|
||||
packet_server::{self, PacketServer, WlxInputStatePointer},
|
||||
};
|
||||
|
||||
pub struct AuthInfo {
|
||||
pub client_name: String,
|
||||
pub protocol_version: u32, // client protocol version
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
alive: bool,
|
||||
conn: local_socket::Stream,
|
||||
next_packet: Option<u32>,
|
||||
auth: Option<AuthInfo>,
|
||||
}
|
||||
|
||||
pub fn send_packet(conn: &mut local_socket::Stream, data: &[u8]) -> anyhow::Result<()> {
|
||||
let mut bytes = bytes::BytesMut::new();
|
||||
|
||||
// packet size
|
||||
bytes.put_u32(data.len() as u32);
|
||||
|
||||
// packet data
|
||||
bytes.put_slice(data);
|
||||
|
||||
conn.write_all(&bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_check(expected_size: u32, res: std::io::Result<usize>) -> bool {
|
||||
match res {
|
||||
Ok(count) => {
|
||||
if count == 0 {
|
||||
return false;
|
||||
}
|
||||
if count as u32 == expected_size {
|
||||
true // read succeeded
|
||||
} else {
|
||||
log::error!("count {count} is not {expected_size}");
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_e) => {
|
||||
//log::error!("failed to get packet size: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Payload = SmallVec<[u8; 64]>;
|
||||
|
||||
fn read_payload(conn: &mut local_socket::Stream, size: u32) -> Option<Payload> {
|
||||
let mut payload = Payload::new();
|
||||
payload.resize(size as usize, 0);
|
||||
if read_check(size, conn.read(&mut payload)) {
|
||||
Some(payload)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TickParams<'a> {
|
||||
pub state: &'a mut super::WayVRState,
|
||||
pub tasks: &'a mut Vec<TickTask>,
|
||||
pub app: &'a AppState,
|
||||
}
|
||||
|
||||
pub fn gen_args_vec(input: &str) -> Vec<&str> {
|
||||
input.split_whitespace().collect()
|
||||
}
|
||||
|
||||
pub fn gen_env_vec(input: &[String]) -> Vec<(&str, &str)> {
|
||||
let res = input
|
||||
.iter()
|
||||
.filter_map(|e| e.as_str().split_once('='))
|
||||
.collect();
|
||||
res
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
const fn new(conn: local_socket::Stream) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
alive: true,
|
||||
auth: None,
|
||||
next_packet: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn kill(&mut self, reason: &str) {
|
||||
let _dont_care = send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::Disconnect(packet_server::Disconnect {
|
||||
reason: String::from(reason),
|
||||
})),
|
||||
);
|
||||
self.alive = false;
|
||||
}
|
||||
|
||||
fn process_handshake(&mut self, handshake: &packet_client::Handshake) -> anyhow::Result<()> {
|
||||
if self.auth.is_some() {
|
||||
anyhow::bail!("You were already authenticated");
|
||||
}
|
||||
|
||||
if handshake.protocol_version != ipc::PROTOCOL_VERSION {
|
||||
anyhow::bail!(
|
||||
"Unsupported protocol version {}",
|
||||
handshake.protocol_version
|
||||
);
|
||||
}
|
||||
|
||||
if handshake.magic != ipc::CONNECTION_MAGIC {
|
||||
anyhow::bail!("Invalid magic");
|
||||
}
|
||||
|
||||
match handshake.client_name.len() {
|
||||
0 => anyhow::bail!("Client name is empty"),
|
||||
1..32 => {}
|
||||
_ => anyhow::bail!("Client name is too long"),
|
||||
}
|
||||
|
||||
log::info!("IPC: Client \"{}\" connected.", handshake.client_name);
|
||||
|
||||
self.auth = Some(AuthInfo {
|
||||
client_name: handshake.client_name.clone(),
|
||||
protocol_version: handshake.protocol_version,
|
||||
});
|
||||
|
||||
// Send auth response
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::HandshakeSuccess(
|
||||
packet_server::HandshakeSuccess {
|
||||
runtime: String::from("wlx-overlay-s"),
|
||||
},
|
||||
)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_wvr_display_list(
|
||||
&mut self,
|
||||
params: &TickParams,
|
||||
serial: ipc::Serial,
|
||||
) -> anyhow::Result<()> {
|
||||
let list: Vec<packet_server::WvrDisplay> = params
|
||||
.state
|
||||
.displays
|
||||
.vec
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, opt_cell)| {
|
||||
let Some(cell) = opt_cell else {
|
||||
return None;
|
||||
};
|
||||
let display = &cell.obj;
|
||||
Some(display.as_packet(display::DisplayHandle::new(idx as u32, cell.generation)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::WvrDisplayListResponse(
|
||||
serial,
|
||||
packet_server::WvrDisplayList { list },
|
||||
)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_wlx_input_state(
|
||||
&mut self,
|
||||
params: &TickParams,
|
||||
serial: ipc::Serial,
|
||||
) -> anyhow::Result<()> {
|
||||
let input_state = ¶ms.app.input_state;
|
||||
|
||||
let to_arr = |vec: &Vec3A| -> [f32; 3] { [vec.x, vec.y, vec.z] };
|
||||
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::WlxInputStateResponse(
|
||||
serial,
|
||||
packet_server::WlxInputState {
|
||||
hmd_pos: to_arr(&input_state.hmd.translation),
|
||||
left: WlxInputStatePointer {
|
||||
pos: to_arr(&input_state.pointers[0].raw_pose.translation),
|
||||
},
|
||||
right: WlxInputStatePointer {
|
||||
pos: to_arr(&input_state.pointers[0].raw_pose.translation),
|
||||
},
|
||||
},
|
||||
)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_wvr_display_create(
|
||||
&mut self,
|
||||
params: &mut TickParams,
|
||||
serial: ipc::Serial,
|
||||
packet_params: packet_client::WvrDisplayCreateParams,
|
||||
) -> anyhow::Result<()> {
|
||||
let display_handle = params.state.create_display(
|
||||
packet_params.width,
|
||||
packet_params.height,
|
||||
&packet_params.name,
|
||||
false,
|
||||
)?;
|
||||
|
||||
params
|
||||
.tasks
|
||||
.push(TickTask::NewDisplay(packet_params, Some(display_handle)));
|
||||
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::WvrDisplayCreateResponse(
|
||||
serial,
|
||||
display_handle.as_packet(),
|
||||
)),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_wvr_display_remove(
|
||||
&mut self,
|
||||
params: &mut TickParams,
|
||||
serial: ipc::Serial,
|
||||
handle: packet_server::WvrDisplayHandle,
|
||||
) -> anyhow::Result<()> {
|
||||
let res = params
|
||||
.state
|
||||
.destroy_display(display::DisplayHandle::from_packet(handle))
|
||||
.map_err(|e| format!("{e:?}"));
|
||||
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::WvrDisplayRemoveResponse(serial, res)),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_wvr_display_set_visible(
|
||||
params: &mut TickParams,
|
||||
handle: packet_server::WvrDisplayHandle,
|
||||
visible: bool,
|
||||
) {
|
||||
params.state.signals.send(WayVRSignal::DisplayVisibility(
|
||||
display::DisplayHandle::from_packet(handle),
|
||||
visible,
|
||||
));
|
||||
}
|
||||
|
||||
fn handle_wvr_display_set_window_layout(
|
||||
params: &mut TickParams,
|
||||
handle: packet_server::WvrDisplayHandle,
|
||||
layout: packet_server::WvrDisplayWindowLayout,
|
||||
) {
|
||||
params.state.signals.send(WayVRSignal::DisplayWindowLayout(
|
||||
display::DisplayHandle::from_packet(handle),
|
||||
layout,
|
||||
));
|
||||
}
|
||||
|
||||
fn handle_wvr_display_window_list(
|
||||
&mut self,
|
||||
params: &mut TickParams,
|
||||
serial: ipc::Serial,
|
||||
display_handle: packet_server::WvrDisplayHandle,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut send = |list: Option<packet_server::WvrWindowList>| -> anyhow::Result<()> {
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::WvrDisplayWindowListResponse(serial, list)),
|
||||
)
|
||||
};
|
||||
|
||||
let Some(display) = params
|
||||
.state
|
||||
.displays
|
||||
.get(&display::DisplayHandle::from_packet(display_handle.clone()))
|
||||
else {
|
||||
return send(None);
|
||||
};
|
||||
|
||||
send(Some(packet_server::WvrWindowList {
|
||||
list: display
|
||||
.displayed_windows
|
||||
.iter()
|
||||
.filter_map(|disp_win| {
|
||||
params
|
||||
.state
|
||||
.wm
|
||||
.borrow_mut()
|
||||
.windows
|
||||
.get(&disp_win.window_handle)
|
||||
.map(|win| packet_server::WvrWindow {
|
||||
handle: window::WindowHandle::as_packet(&disp_win.window_handle),
|
||||
process_handle: process::ProcessHandle::as_packet(
|
||||
&disp_win.process_handle,
|
||||
),
|
||||
pos_x: win.pos_x,
|
||||
pos_y: win.pos_y,
|
||||
size_x: win.size_x,
|
||||
size_y: win.size_y,
|
||||
visible: win.visible,
|
||||
display_handle: display_handle.clone(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn handle_wvr_window_set_visible(
|
||||
params: &mut TickParams,
|
||||
handle: packet_server::WvrWindowHandle,
|
||||
visible: bool,
|
||||
) {
|
||||
let mut to_resize = None;
|
||||
|
||||
if let Some(window) = params
|
||||
.state
|
||||
.wm
|
||||
.borrow_mut()
|
||||
.windows
|
||||
.get_mut(&window::WindowHandle::from_packet(handle))
|
||||
{
|
||||
window.visible = visible;
|
||||
to_resize = Some(window.display_handle);
|
||||
}
|
||||
|
||||
if let Some(to_resize) = to_resize {
|
||||
if let Some(display) = params.state.displays.get_mut(&to_resize) {
|
||||
display.reposition_windows();
|
||||
display.trigger_rerender();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_wvr_process_launch(
|
||||
&mut self,
|
||||
params: &mut TickParams,
|
||||
serial: ipc::Serial,
|
||||
packet_params: packet_client::WvrProcessLaunchParams,
|
||||
) -> anyhow::Result<()> {
|
||||
let args_vec = gen_args_vec(&packet_params.args);
|
||||
let env_vec = gen_env_vec(&packet_params.env);
|
||||
|
||||
let res = params.state.spawn_process(
|
||||
super::display::DisplayHandle::from_packet(packet_params.target_display),
|
||||
&packet_params.exec,
|
||||
&args_vec,
|
||||
&env_vec,
|
||||
packet_params.userdata,
|
||||
);
|
||||
|
||||
let res = res.map(|r| r.as_packet()).map_err(|e| e.to_string());
|
||||
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::WvrProcessLaunchResponse(serial, res)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_wvr_display_get(
|
||||
&mut self,
|
||||
params: &TickParams,
|
||||
serial: ipc::Serial,
|
||||
display_handle: packet_server::WvrDisplayHandle,
|
||||
) -> anyhow::Result<()> {
|
||||
let native_handle = &display::DisplayHandle::from_packet(display_handle);
|
||||
let disp = params
|
||||
.state
|
||||
.displays
|
||||
.get(native_handle)
|
||||
.map(|disp| disp.as_packet(*native_handle));
|
||||
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::WvrDisplayGetResponse(serial, disp)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_wvr_process_list(
|
||||
&mut self,
|
||||
params: &TickParams,
|
||||
serial: ipc::Serial,
|
||||
) -> anyhow::Result<()> {
|
||||
let list: Vec<packet_server::WvrProcess> = params
|
||||
.state
|
||||
.processes
|
||||
.vec
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, opt_cell)| {
|
||||
let Some(cell) = opt_cell else {
|
||||
return None;
|
||||
};
|
||||
let process = &cell.obj;
|
||||
Some(process.to_packet(process::ProcessHandle::new(idx as u32, cell.generation)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::WvrProcessListResponse(
|
||||
serial,
|
||||
packet_server::WvrProcessList { list },
|
||||
)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This request doesn't return anything to the client
|
||||
fn handle_wvr_process_terminate(
|
||||
params: &mut TickParams,
|
||||
process_handle: packet_server::WvrProcessHandle,
|
||||
) {
|
||||
let native_handle = &process::ProcessHandle::from_packet(process_handle);
|
||||
let process = params.state.processes.get_mut(native_handle);
|
||||
|
||||
let Some(process) = process else {
|
||||
return;
|
||||
};
|
||||
|
||||
process.terminate();
|
||||
}
|
||||
|
||||
fn handle_wvr_process_get(
|
||||
&mut self,
|
||||
params: &TickParams,
|
||||
serial: ipc::Serial,
|
||||
process_handle: packet_server::WvrProcessHandle,
|
||||
) -> anyhow::Result<()> {
|
||||
let native_handle = &process::ProcessHandle::from_packet(process_handle);
|
||||
let process = params
|
||||
.state
|
||||
.processes
|
||||
.get(native_handle)
|
||||
.map(|process| process.to_packet(*native_handle));
|
||||
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
&ipc::data_encode(&PacketServer::WvrProcessGetResponse(serial, process)),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_wlx_haptics(
|
||||
params: &mut TickParams,
|
||||
haptics_params: packet_client::WlxHapticsParams,
|
||||
) {
|
||||
params
|
||||
.state
|
||||
.tasks
|
||||
.send(super::WayVRTask::Haptics(crate::backend::input::Haptics {
|
||||
duration: haptics_params.duration,
|
||||
frequency: haptics_params.frequency,
|
||||
intensity: haptics_params.intensity,
|
||||
}));
|
||||
}
|
||||
|
||||
fn process_payload(&mut self, params: &mut TickParams, payload: Payload) -> anyhow::Result<()> {
|
||||
let packet: PacketClient = ipc::data_decode(&payload)?;
|
||||
|
||||
if let PacketClient::Handshake(handshake) = &packet {
|
||||
self.process_handshake(handshake)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match packet {
|
||||
PacketClient::Handshake(_) => unreachable!(), // handled previously
|
||||
PacketClient::WlxInputState(serial) => {
|
||||
self.handle_wlx_input_state(params, serial)?;
|
||||
}
|
||||
PacketClient::WvrDisplayList(serial) => {
|
||||
self.handle_wvr_display_list(params, serial)?;
|
||||
}
|
||||
PacketClient::WvrDisplayGet(serial, display_handle) => {
|
||||
self.handle_wvr_display_get(params, serial, display_handle)?;
|
||||
}
|
||||
PacketClient::WvrDisplayRemove(serial, display_handle) => {
|
||||
self.handle_wvr_display_remove(params, serial, display_handle)?;
|
||||
}
|
||||
PacketClient::WvrDisplaySetVisible(display_handle, visible) => {
|
||||
Self::handle_wvr_display_set_visible(params, display_handle, visible);
|
||||
}
|
||||
PacketClient::WvrDisplaySetWindowLayout(display_handle, layout) => {
|
||||
Self::handle_wvr_display_set_window_layout(params, display_handle, layout);
|
||||
}
|
||||
PacketClient::WvrDisplayWindowList(serial, display_handle) => {
|
||||
self.handle_wvr_display_window_list(params, serial, display_handle)?;
|
||||
}
|
||||
PacketClient::WvrWindowSetVisible(window_handle, visible) => {
|
||||
Self::handle_wvr_window_set_visible(params, window_handle, visible);
|
||||
}
|
||||
PacketClient::WvrProcessGet(serial, process_handle) => {
|
||||
self.handle_wvr_process_get(params, serial, process_handle)?;
|
||||
}
|
||||
PacketClient::WvrProcessList(serial) => {
|
||||
self.handle_wvr_process_list(params, serial)?;
|
||||
}
|
||||
PacketClient::WvrProcessLaunch(serial, packet_params) => {
|
||||
self.handle_wvr_process_launch(params, serial, packet_params)?;
|
||||
}
|
||||
PacketClient::WvrDisplayCreate(serial, packet_params) => {
|
||||
self.handle_wvr_display_create(params, serial, packet_params)?;
|
||||
}
|
||||
PacketClient::WvrProcessTerminate(process_handle) => {
|
||||
Self::handle_wvr_process_terminate(params, process_handle);
|
||||
}
|
||||
PacketClient::WlxHaptics(haptics_params) => {
|
||||
Self::handle_wlx_haptics(params, haptics_params);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_check_payload(&mut self, params: &mut TickParams, payload: Payload) -> bool {
|
||||
log::debug!("payload size {}", payload.len());
|
||||
|
||||
if let Err(e) = self.process_payload(params, payload) {
|
||||
log::error!("Invalid payload from the client, closing connection: {e}");
|
||||
// send also error message directly to the client before disconnecting
|
||||
self.kill(format!("{e}").as_str());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn read_packet(&mut self, params: &mut TickParams) -> bool {
|
||||
if let Some(payload_size) = self.next_packet {
|
||||
let Some(payload) = read_payload(&mut self.conn, payload_size) else {
|
||||
// still failed to read payload, try in next tick
|
||||
return false;
|
||||
};
|
||||
|
||||
if !self.process_check_payload(params, payload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.next_packet = None;
|
||||
}
|
||||
|
||||
let mut buf_packet_header: [u8; 4] = [0; 4];
|
||||
if !read_check(4, self.conn.read(&mut buf_packet_header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let payload_size = u32::from_be_bytes(buf_packet_header[0..4].try_into().unwrap()); // 0-3 bytes (u32 size)
|
||||
|
||||
let size_limit: u32 = 128 * 1024;
|
||||
|
||||
if payload_size > size_limit {
|
||||
// over 128 KiB?
|
||||
log::error!(
|
||||
"Client sent a packet header with the size over {size_limit} bytes, closing connection."
|
||||
);
|
||||
self.kill("Too big packet received (over 128 KiB)");
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(payload) = read_payload(&mut self.conn, payload_size) else {
|
||||
// failed to read payload, try in next tick
|
||||
self.next_packet = Some(payload_size);
|
||||
return false;
|
||||
};
|
||||
|
||||
if !self.process_check_payload(params, payload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn tick(&mut self, params: &mut TickParams) {
|
||||
while self.read_packet(params) {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection {
|
||||
fn drop(&mut self) {
|
||||
log::info!("Connection closed");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WayVRServer {
|
||||
listener: local_socket::Listener,
|
||||
connections: Vec<Connection>,
|
||||
}
|
||||
|
||||
impl WayVRServer {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let printname = "/tmp/wayvr_ipc.sock";
|
||||
let name = printname.to_ns_name::<local_socket::GenericNamespaced>()?;
|
||||
let opts = local_socket::ListenerOptions::new()
|
||||
.name(name)
|
||||
.nonblocking(local_socket::ListenerNonblockingMode::Both);
|
||||
let listener = match opts.create_sync() {
|
||||
Ok(listener) => listener,
|
||||
Err(e) => anyhow::bail!("Failed to start WayVRServer IPC listener. Reason: {}", e),
|
||||
};
|
||||
|
||||
log::info!("WayVRServer IPC running at {printname}");
|
||||
|
||||
Ok(Self {
|
||||
listener,
|
||||
connections: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn accept_connections(&mut self) {
|
||||
let Ok(conn) = self.listener.accept() else {
|
||||
return; // No new connection or other error
|
||||
};
|
||||
|
||||
self.connections.push(Connection::new(conn));
|
||||
}
|
||||
|
||||
fn tick_connections(&mut self, params: &mut TickParams) {
|
||||
for c in &mut self.connections {
|
||||
c.tick(params);
|
||||
}
|
||||
|
||||
// remove killed connections
|
||||
self.connections.retain(|c| c.alive);
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, params: &mut TickParams) {
|
||||
self.accept_connections();
|
||||
self.tick_connections(params);
|
||||
}
|
||||
|
||||
pub fn broadcast(&mut self, packet: packet_server::PacketServer) {
|
||||
for connection in &mut self.connections {
|
||||
if let Err(e) = send_packet(&mut connection.conn, &ipc::data_encode(&packet)) {
|
||||
log::error!("failed to broadcast packet: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use super::egl_data;
|
||||
use smithay::backend::{egl as smithay_egl, renderer::gles::ffi};
|
||||
|
||||
pub fn get_egl_display(data: &egl_data::EGLData) -> anyhow::Result<smithay_egl::EGLDisplay> {
|
||||
Ok(unsafe { smithay_egl::EGLDisplay::from_raw(data.display.as_ptr(), data.config.as_ptr())? })
|
||||
}
|
||||
|
||||
pub fn get_egl_context(
|
||||
data: &egl_data::EGLData,
|
||||
display: &smithay_egl::EGLDisplay,
|
||||
) -> anyhow::Result<smithay_egl::EGLContext> {
|
||||
let display_ptr = display.get_display_handle().handle;
|
||||
debug_assert!(display_ptr == data.display.as_ptr());
|
||||
let config_ptr = data.config.as_ptr();
|
||||
let context_ptr = data.context.as_ptr();
|
||||
Ok(unsafe { smithay_egl::EGLContext::from_raw(display_ptr, config_ptr, context_ptr)? })
|
||||
}
|
||||
|
||||
pub fn create_framebuffer_texture(
|
||||
gl: &ffi::Gles2,
|
||||
width: u32,
|
||||
height: u32,
|
||||
tex_format: u32,
|
||||
internal_format: u32,
|
||||
) -> u32 {
|
||||
unsafe {
|
||||
let mut tex = 0;
|
||||
gl.GenTextures(1, &mut tex);
|
||||
gl.BindTexture(ffi::TEXTURE_2D, tex);
|
||||
gl.TexParameteri(
|
||||
ffi::TEXTURE_2D,
|
||||
ffi::TEXTURE_MIN_FILTER,
|
||||
ffi::NEAREST as i32,
|
||||
);
|
||||
gl.TexParameteri(
|
||||
ffi::TEXTURE_2D,
|
||||
ffi::TEXTURE_MAG_FILTER,
|
||||
ffi::NEAREST as i32,
|
||||
);
|
||||
gl.TexImage2D(
|
||||
ffi::TEXTURE_2D,
|
||||
0,
|
||||
internal_format as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
0,
|
||||
tex_format,
|
||||
ffi::UNSIGNED_BYTE,
|
||||
std::ptr::null(),
|
||||
);
|
||||
gl.BindTexture(ffi::TEXTURE_2D, 0);
|
||||
tex
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// Returns milliseconds since unix epoch
|
||||
pub fn get_millis() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use smithay::wayland::shell::xdg::ToplevelSurface;
|
||||
use wayvr_ipc::packet_server;
|
||||
|
||||
use crate::gen_id;
|
||||
|
||||
use super::display;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Window {
|
||||
pub pos_x: i32,
|
||||
pub pos_y: i32,
|
||||
pub size_x: u32,
|
||||
pub size_y: u32,
|
||||
pub visible: bool,
|
||||
pub toplevel: ToplevelSurface,
|
||||
pub display_handle: display::DisplayHandle,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(display_handle: display::DisplayHandle, toplevel: &ToplevelSurface) -> Self {
|
||||
Self {
|
||||
pos_x: 0,
|
||||
pos_y: 0,
|
||||
size_x: 0,
|
||||
size_y: 0,
|
||||
visible: true,
|
||||
toplevel: toplevel.clone(),
|
||||
display_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn set_pos(&mut self, pos_x: i32, pos_y: i32) {
|
||||
self.pos_x = pos_x;
|
||||
self.pos_y = pos_y;
|
||||
}
|
||||
|
||||
pub fn set_size(&mut self, size_x: u32, size_y: u32) {
|
||||
self.toplevel.with_pending_state(|state| {
|
||||
//state.bounds = Some((size_x as i32, size_y as i32).into());
|
||||
state.size = Some((size_x as i32, size_y as i32).into());
|
||||
});
|
||||
self.toplevel.send_configure();
|
||||
|
||||
self.size_x = size_x;
|
||||
self.size_y = size_y;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowManager {
|
||||
pub windows: WindowVec,
|
||||
}
|
||||
|
||||
impl WindowManager {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
windows: WindowVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_window_handle(&self, toplevel: &ToplevelSurface) -> Option<WindowHandle> {
|
||||
for (idx, cell) in self.windows.vec.iter().enumerate() {
|
||||
if let Some(cell) = cell {
|
||||
let window = &cell.obj;
|
||||
if window.toplevel == *toplevel {
|
||||
return Some(WindowVec::get_handle(cell, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn create_window(
|
||||
&mut self,
|
||||
display_handle: display::DisplayHandle,
|
||||
toplevel: &ToplevelSurface,
|
||||
) -> WindowHandle {
|
||||
self.windows.add(Window::new(display_handle, toplevel))
|
||||
}
|
||||
|
||||
pub fn remove_window(&mut self, window_handle: WindowHandle) {
|
||||
self.windows.remove(&window_handle);
|
||||
}
|
||||
}
|
||||
|
||||
gen_id!(WindowVec, Window, WindowCell, WindowHandle);
|
||||
|
||||
impl WindowHandle {
|
||||
pub const fn from_packet(handle: packet_server::WvrWindowHandle) -> Self {
|
||||
Self {
|
||||
generation: handle.generation,
|
||||
idx: handle.idx,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_packet(&self) -> packet_server::WvrWindowHandle {
|
||||
packet_server::WvrWindowHandle {
|
||||
idx: self.idx,
|
||||
generation: self.generation,
|
||||
}
|
||||
}
|
||||
}
|
||||
+135
-52
@@ -2,13 +2,12 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config_io;
|
||||
use crate::config_io::get_conf_d_path;
|
||||
use crate::config_io::CONFIG_ROOT_PATH;
|
||||
use crate::gui::modular::ModularUiConfig;
|
||||
use crate::overlays::toast::DisplayMethod;
|
||||
use crate::overlays::toast::ToastTopic;
|
||||
use crate::state::LeftRight;
|
||||
use anyhow::bail;
|
||||
use chrono::Offset;
|
||||
use config::Config;
|
||||
use config::File;
|
||||
use glam::vec3a;
|
||||
@@ -70,70 +69,92 @@ impl AStrSetExt for AStrSet {
|
||||
|
||||
fn arc_rm(&mut self, value: &str) -> bool {
|
||||
let index = self.iter().position(|v| v.as_ref().eq(value));
|
||||
index
|
||||
.map(|i| {
|
||||
self.remove(i);
|
||||
true
|
||||
})
|
||||
.unwrap_or(false)
|
||||
index.is_some_and(|i| {
|
||||
self.remove(i);
|
||||
true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type PwTokenMap = AStrMap<String>;
|
||||
|
||||
pub fn def_watch_pos() -> Vec3A {
|
||||
pub const fn def_watch_pos() -> Vec3A {
|
||||
vec3a(-0.03, -0.01, 0.125)
|
||||
}
|
||||
|
||||
pub fn def_watch_rot() -> Quat {
|
||||
Quat::from_xyzw(-0.7071066, 0.0007963618, 0.7071066, 0.0)
|
||||
pub const fn def_watch_rot() -> Quat {
|
||||
Quat::from_xyzw(-0.707_106_6, 0.000_796_361_8, 0.707_106_6, 0.0)
|
||||
}
|
||||
|
||||
pub fn def_left() -> LeftRight {
|
||||
pub const fn def_left() -> LeftRight {
|
||||
LeftRight::Left
|
||||
}
|
||||
|
||||
pub fn def_pw_tokens() -> PwTokenMap {
|
||||
pub const fn def_pw_tokens() -> PwTokenMap {
|
||||
AStrMap::new()
|
||||
}
|
||||
|
||||
fn def_click_freeze_time_ms() -> u32 {
|
||||
const fn def_mouse_move_interval_ms() -> u32 {
|
||||
10 // 100fps
|
||||
}
|
||||
|
||||
const fn def_click_freeze_time_ms() -> u32 {
|
||||
300
|
||||
}
|
||||
|
||||
pub fn def_true() -> bool {
|
||||
pub const fn def_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn def_false() -> bool {
|
||||
const fn def_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn def_one() -> f32 {
|
||||
const fn def_one() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
pub fn def_half() -> f32 {
|
||||
pub const fn def_half() -> f32 {
|
||||
0.5
|
||||
}
|
||||
|
||||
pub fn def_point7() -> f32 {
|
||||
pub const fn def_point7() -> f32 {
|
||||
0.7
|
||||
}
|
||||
|
||||
fn def_osc_port() -> u16 {
|
||||
pub const fn def_point3() -> f32 {
|
||||
0.3
|
||||
}
|
||||
|
||||
const fn def_osc_port() -> u16 {
|
||||
9000
|
||||
}
|
||||
|
||||
fn def_screens() -> AStrSet {
|
||||
const fn def_empty_vec_string() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn def_timezones() -> Vec<String> {
|
||||
const EMEA: i32 = -60 * 60; // UTC-1
|
||||
const APAC: i32 = 5 * 60 * 60; // UTC+5
|
||||
|
||||
let offset = chrono::Local::now().offset().fix();
|
||||
match offset.local_minus_utc() {
|
||||
i32::MIN..EMEA => vec!["Europe/Paris".into(), "Asia/Tokyo".into()],
|
||||
EMEA..APAC => vec!["America/New_York".into(), "Asia/Tokyo".into()],
|
||||
APAC..=i32::MAX => vec!["Europe/Paris".into(), "America/New_York".into()],
|
||||
}
|
||||
}
|
||||
|
||||
const fn def_screens() -> AStrSet {
|
||||
AStrSet::new()
|
||||
}
|
||||
|
||||
fn def_curve_values() -> AStrMap<f32> {
|
||||
const fn def_curve_values() -> AStrMap<f32> {
|
||||
AStrMap::new()
|
||||
}
|
||||
|
||||
fn def_transforms() -> AStrMap<Affine3A> {
|
||||
const fn def_transforms() -> AStrMap<Affine3A> {
|
||||
AStrMap::new()
|
||||
}
|
||||
|
||||
@@ -141,6 +162,10 @@ fn def_auto() -> Arc<str> {
|
||||
"auto".into()
|
||||
}
|
||||
|
||||
fn def_empty() -> Arc<str> {
|
||||
"".into()
|
||||
}
|
||||
|
||||
fn def_toast_topics() -> IdMap<ToastTopic, DisplayMethod> {
|
||||
IdMap::new()
|
||||
}
|
||||
@@ -149,6 +174,10 @@ fn def_font() -> Arc<str> {
|
||||
"LiberationSans:style=Bold".into()
|
||||
}
|
||||
|
||||
const fn def_max_height() -> u16 {
|
||||
1440
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct GeneralConfig {
|
||||
#[serde(default = "def_watch_pos")]
|
||||
@@ -163,6 +192,9 @@ pub struct GeneralConfig {
|
||||
#[serde(default = "def_click_freeze_time_ms")]
|
||||
pub click_freeze_time_ms: u32,
|
||||
|
||||
#[serde(default = "def_mouse_move_interval_ms")]
|
||||
pub mouse_move_interval_ms: u32,
|
||||
|
||||
#[serde(default = "def_true")]
|
||||
pub notifications_enabled: bool,
|
||||
|
||||
@@ -172,6 +204,9 @@ pub struct GeneralConfig {
|
||||
#[serde(default = "def_toast_topics")]
|
||||
pub notification_topics: IdMap<ToastTopic, DisplayMethod>,
|
||||
|
||||
#[serde(default = "def_empty")]
|
||||
pub notification_sound: Arc<str>,
|
||||
|
||||
#[serde(default = "def_true")]
|
||||
pub keyboard_sound_enabled: bool,
|
||||
|
||||
@@ -240,42 +275,75 @@ pub struct GeneralConfig {
|
||||
|
||||
#[serde(default = "def_font")]
|
||||
pub primary_font: Arc<str>,
|
||||
|
||||
#[serde(default = "def_one")]
|
||||
pub space_drag_multiplier: f32,
|
||||
|
||||
#[serde(default = "def_empty")]
|
||||
pub skybox_texture: Arc<str>,
|
||||
|
||||
#[serde(default = "def_true")]
|
||||
pub use_skybox: bool,
|
||||
|
||||
#[serde(default = "def_true")]
|
||||
pub use_passthrough: bool,
|
||||
|
||||
#[serde(default = "def_max_height")]
|
||||
pub screen_max_height: u16,
|
||||
|
||||
#[serde(default = "def_false")]
|
||||
pub screen_render_down: bool,
|
||||
|
||||
#[serde(default = "def_point3")]
|
||||
pub pointer_lerp_factor: f32,
|
||||
|
||||
#[serde(default = "def_false")]
|
||||
pub space_rotate_unlocked: bool,
|
||||
|
||||
#[serde(default = "def_empty_vec_string")]
|
||||
pub alt_click_down: Vec<String>,
|
||||
|
||||
#[serde(default = "def_empty_vec_string")]
|
||||
pub alt_click_up: Vec<String>,
|
||||
|
||||
#[serde(default = "def_timezones")]
|
||||
pub timezones: Vec<String>,
|
||||
}
|
||||
|
||||
impl GeneralConfig {
|
||||
fn sanitize_range(name: &str, val: f32, from: f32, to: f32) {
|
||||
if !val.is_normal() || val < from || val > to {
|
||||
panic!(
|
||||
"GeneralConfig: {} needs to be between {} and {}",
|
||||
name, from, to
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!(!val.is_normal() || val < from || val > to),
|
||||
"GeneralConfig: {name} needs to be between {from} and {to}"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn load_from_disk() -> GeneralConfig {
|
||||
pub fn load_from_disk() -> Self {
|
||||
let config = load_general();
|
||||
config.post_load();
|
||||
config
|
||||
}
|
||||
|
||||
fn post_load(&self) {
|
||||
GeneralConfig::sanitize_range("keyboard_scale", self.keyboard_scale, 0.05, 5.0);
|
||||
GeneralConfig::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.05, 5.0);
|
||||
Self::sanitize_range("keyboard_scale", self.keyboard_scale, 0.05, 5.0);
|
||||
Self::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.05, 5.0);
|
||||
}
|
||||
}
|
||||
|
||||
const FALLBACKS: [&str; 4] = [
|
||||
const FALLBACKS: [&str; 5] = [
|
||||
include_str!("res/keyboard.yaml"),
|
||||
include_str!("res/watch.yaml"),
|
||||
include_str!("res/settings.yaml"),
|
||||
include_str!("res/anchor.yaml"),
|
||||
include_str!("res/wayvr.yaml"),
|
||||
];
|
||||
|
||||
const FILES: [&str; 4] = [
|
||||
const FILES: [&str; 5] = [
|
||||
"keyboard.yaml",
|
||||
"watch.yaml",
|
||||
"settings.yaml",
|
||||
"anchor.yaml",
|
||||
"wayvr.yaml",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -285,6 +353,8 @@ pub enum ConfigType {
|
||||
Watch,
|
||||
Settings,
|
||||
Anchor,
|
||||
#[allow(dead_code)]
|
||||
WayVR,
|
||||
}
|
||||
|
||||
pub fn load_known_yaml<T>(config_type: ConfigType) -> T
|
||||
@@ -299,8 +369,8 @@ where
|
||||
match serde_yaml::from_str::<T>(yaml) {
|
||||
Ok(d) => return d,
|
||||
Err(e) => {
|
||||
error!("Failed to parse {}, falling back to defaults.", file_name);
|
||||
error!("{}", e);
|
||||
error!("Failed to parse {file_name}, falling back to defaults.");
|
||||
error!("{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,21 +379,27 @@ where
|
||||
}
|
||||
|
||||
pub fn load_custom_ui(name: &str) -> anyhow::Result<ModularUiConfig> {
|
||||
let filename = format!("{}.yaml", name);
|
||||
let filename = format!("{name}.yaml");
|
||||
let Some(yaml_data) = config_io::load(&filename) else {
|
||||
bail!("Could not read file at {}", &filename);
|
||||
};
|
||||
Ok(serde_yaml::from_str(&yaml_data)?)
|
||||
}
|
||||
|
||||
pub fn load_general() -> GeneralConfig {
|
||||
pub fn load_config_with_conf_d<ConfigData>(
|
||||
root_config_filename: &str,
|
||||
ctype: config_io::ConfigRoot,
|
||||
) -> ConfigData
|
||||
where
|
||||
ConfigData: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let mut settings_builder = Config::builder();
|
||||
|
||||
// Add files from conf.d directory
|
||||
let path_conf_d = get_conf_d_path();
|
||||
let path_conf_d = ctype.get_conf_d_path();
|
||||
|
||||
for mut base_conf in [CONFIG_ROOT_PATH.clone(), path_conf_d.clone()] {
|
||||
base_conf.push("config.yaml");
|
||||
for mut base_conf in [config_io::get_config_root(), path_conf_d.clone()] {
|
||||
base_conf.push(root_config_filename);
|
||||
if base_conf.exists() {
|
||||
log::info!("Loading config file: {}", base_conf.to_string_lossy());
|
||||
settings_builder = settings_builder.add_source(File::from(base_conf));
|
||||
@@ -335,13 +411,13 @@ pub fn load_general() -> GeneralConfig {
|
||||
.filter_map(|r| match r {
|
||||
Ok(entry) => Some(entry),
|
||||
Err(e) => {
|
||||
error!("Failed to read conf.d directory: {}", e);
|
||||
error!("Failed to read conf.d directory: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// Sort paths alphabetically
|
||||
paths.sort_by_key(|dir| dir.path());
|
||||
paths.sort_by_key(std::fs::DirEntry::path);
|
||||
for path in paths {
|
||||
log::info!("Loading config file: {}", path.path().to_string_lossy());
|
||||
settings_builder = settings_builder.add_source(File::from(path.path()));
|
||||
@@ -349,18 +425,22 @@ pub fn load_general() -> GeneralConfig {
|
||||
}
|
||||
|
||||
match settings_builder.build() {
|
||||
Ok(settings) => match settings.try_deserialize::<GeneralConfig>() {
|
||||
Ok(settings) => match settings.try_deserialize::<ConfigData>() {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
panic!("Failed to deserialize settings: {}", e);
|
||||
panic!("Failed to deserialize settings: {e}");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
panic!("Failed to build settings: {}", e);
|
||||
panic!("Failed to build settings: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_general() -> GeneralConfig {
|
||||
load_config_with_conf_d::<GeneralConfig>("config.yaml", config_io::ConfigRoot::Generic)
|
||||
}
|
||||
|
||||
// Config that is saved from the settings panel
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -374,13 +454,15 @@ pub struct AutoSettings {
|
||||
pub notifications_sound_enabled: bool,
|
||||
pub realign_on_showhide: bool,
|
||||
pub allow_sliding: bool,
|
||||
pub space_drag_multiplier: f32,
|
||||
}
|
||||
|
||||
fn get_settings_path() -> PathBuf {
|
||||
let mut path = config_io::get_conf_d_path();
|
||||
path.push("zz-saved-config.json5");
|
||||
path
|
||||
config_io::ConfigRoot::Generic
|
||||
.get_conf_d_path()
|
||||
.join("zz-saved-config.json5")
|
||||
}
|
||||
|
||||
pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
|
||||
let conf = AutoSettings {
|
||||
watch_pos: config.watch_pos,
|
||||
@@ -392,6 +474,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
|
||||
notifications_sound_enabled: config.notifications_sound_enabled,
|
||||
realign_on_showhide: config.realign_on_showhide,
|
||||
allow_sliding: config.allow_sliding,
|
||||
space_drag_multiplier: config.space_drag_multiplier,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
|
||||
@@ -410,9 +493,9 @@ pub struct AutoState {
|
||||
}
|
||||
|
||||
fn get_state_path() -> PathBuf {
|
||||
let mut path = config_io::get_conf_d_path();
|
||||
path.push("zz-saved-state.json5");
|
||||
path
|
||||
config_io::ConfigRoot::Generic
|
||||
.get_conf_d_path()
|
||||
.join("zz-saved-state.json5")
|
||||
}
|
||||
|
||||
pub fn save_layout(config: &GeneralConfig) -> anyhow::Result<()> {
|
||||
|
||||
+31
-31
@@ -1,55 +1,55 @@
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
fs::{self, create_dir},
|
||||
path::PathBuf,
|
||||
};
|
||||
use std::{path::PathBuf, sync::LazyLock};
|
||||
|
||||
pub enum ConfigRoot {
|
||||
Generic,
|
||||
#[allow(dead_code)]
|
||||
WayVR,
|
||||
}
|
||||
|
||||
const FALLBACK_CONFIG_PATH: &str = "/tmp/wlxoverlay";
|
||||
|
||||
pub static CONFIG_ROOT_PATH: Lazy<PathBuf> = Lazy::new(|| {
|
||||
static CONFIG_ROOT_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
|
||||
if let Ok(xdg_dirs) = xdg::BaseDirectories::new() {
|
||||
let mut dir = xdg_dirs.get_config_home();
|
||||
dir.push("wlxoverlay");
|
||||
return dir;
|
||||
}
|
||||
//Return fallback config path
|
||||
error!(
|
||||
"Err: Failed to find config path, using {}",
|
||||
FALLBACK_CONFIG_PATH
|
||||
);
|
||||
error!("Err: Failed to find config path, using {FALLBACK_CONFIG_PATH}");
|
||||
PathBuf::from(FALLBACK_CONFIG_PATH)
|
||||
});
|
||||
|
||||
pub fn get_conf_d_path() -> PathBuf {
|
||||
let mut config_root = CONFIG_ROOT_PATH.clone();
|
||||
config_root.push("conf.d");
|
||||
config_root
|
||||
pub fn get_config_root() -> PathBuf {
|
||||
CONFIG_ROOT_PATH.clone()
|
||||
}
|
||||
|
||||
// Make sure config directory is present and return root config path
|
||||
pub fn ensure_config_root() -> PathBuf {
|
||||
let path = CONFIG_ROOT_PATH.clone();
|
||||
let _ = create_dir(&path);
|
||||
impl ConfigRoot {
|
||||
pub fn get_conf_d_path(&self) -> PathBuf {
|
||||
get_config_root().join(match self {
|
||||
Self::Generic => "conf.d",
|
||||
Self::WayVR => "wayvr.conf.d",
|
||||
})
|
||||
}
|
||||
|
||||
let path_conf_d = get_conf_d_path();
|
||||
let _ = create_dir(path_conf_d);
|
||||
path
|
||||
// Make sure config directory is present and return root config path
|
||||
pub fn ensure_dir(&self) -> PathBuf {
|
||||
let path = get_config_root();
|
||||
let _ = std::fs::create_dir(&path);
|
||||
|
||||
let path_conf_d = self.get_conf_d_path();
|
||||
let _ = std::fs::create_dir(path_conf_d);
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config_file_path(filename: &str) -> PathBuf {
|
||||
let mut config_root = CONFIG_ROOT_PATH.clone();
|
||||
config_root.push(filename);
|
||||
config_root
|
||||
pub fn get_config_file_path(filename: &str) -> PathBuf {
|
||||
get_config_root().join(filename)
|
||||
}
|
||||
|
||||
pub fn load(filename: &str) -> Option<String> {
|
||||
let path = get_config_file_path(filename);
|
||||
log::info!("Loading config {}", path.to_string_lossy());
|
||||
log::info!("Loading config: {}", path.to_string_lossy());
|
||||
|
||||
if let Ok(data) = fs::read_to_string(path) {
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
std::fs::read_to_string(path).ok()
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
#[cfg(not(feature = "wayvr"))]
|
||||
compile_error!("WayVR feature is not enabled");
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{BTreeMap, HashMap},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
overlay::RelativeTo,
|
||||
task::{TaskContainer, TaskType},
|
||||
wayvr,
|
||||
},
|
||||
config::load_config_with_conf_d,
|
||||
config_io,
|
||||
gui::modular::button::WayVRAction,
|
||||
overlays::wayvr::WayVRData,
|
||||
};
|
||||
|
||||
// Flat version of RelativeTo
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub enum AttachTo {
|
||||
None,
|
||||
HandLeft,
|
||||
HandRight,
|
||||
Head,
|
||||
Stage,
|
||||
}
|
||||
|
||||
impl AttachTo {
|
||||
pub const fn get_relative_to(&self) -> RelativeTo {
|
||||
match self {
|
||||
Self::None => RelativeTo::None,
|
||||
Self::HandLeft => RelativeTo::Hand(0),
|
||||
Self::HandRight => RelativeTo::Hand(1),
|
||||
Self::Stage => RelativeTo::Stage,
|
||||
Self::Head => RelativeTo::Head,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_packet(input: &wayvr_ipc::packet_client::AttachTo) -> Self {
|
||||
match input {
|
||||
wayvr_ipc::packet_client::AttachTo::None => Self::None,
|
||||
wayvr_ipc::packet_client::AttachTo::HandLeft => Self::HandLeft,
|
||||
wayvr_ipc::packet_client::AttachTo::HandRight => Self::HandRight,
|
||||
wayvr_ipc::packet_client::AttachTo::Head => Self::Head,
|
||||
wayvr_ipc::packet_client::AttachTo::Stage => Self::Stage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct Rotation {
|
||||
pub axis: [f32; 3],
|
||||
pub angle: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct WayVRAppEntry {
|
||||
pub name: String,
|
||||
pub target_display: String,
|
||||
pub exec: String,
|
||||
pub args: Option<String>,
|
||||
pub env: Option<Vec<String>>,
|
||||
pub shown_at_start: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct WayVRDisplay {
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub scale: Option<f32>,
|
||||
pub rotation: Option<Rotation>,
|
||||
pub pos: Option<[f32; 3]>,
|
||||
pub attach_to: Option<AttachTo>,
|
||||
pub primary: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct WayVRCatalog {
|
||||
pub apps: Vec<WayVRAppEntry>,
|
||||
}
|
||||
|
||||
impl WayVRCatalog {
|
||||
pub fn get_app(&self, name: &str) -> Option<&WayVRAppEntry> {
|
||||
self.apps.iter().find(|&app| app.name.as_str() == name)
|
||||
}
|
||||
}
|
||||
|
||||
const fn def_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
const fn def_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
const fn def_autohide_delay() -> u32 {
|
||||
750
|
||||
}
|
||||
|
||||
const fn def_keyboard_repeat_delay() -> u32 {
|
||||
200
|
||||
}
|
||||
|
||||
const fn def_keyboard_repeat_rate() -> u32 {
|
||||
50
|
||||
}
|
||||
|
||||
fn def_blit_method() -> String {
|
||||
String::from("dmabuf")
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct WayVRDashboard {
|
||||
pub exec: String,
|
||||
pub args: Option<String>,
|
||||
pub env: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct WayVRConfig {
|
||||
#[serde(default = "def_false")]
|
||||
pub run_compositor_at_start: bool,
|
||||
|
||||
#[serde(default = "Default::default")]
|
||||
pub catalogs: HashMap<String, WayVRCatalog>,
|
||||
|
||||
#[serde(default = "Default::default")]
|
||||
pub displays: BTreeMap<String, WayVRDisplay>, // sorted alphabetically
|
||||
|
||||
#[serde(default = "Default::default")]
|
||||
pub dashboard: Option<WayVRDashboard>,
|
||||
|
||||
#[serde(default = "def_true")]
|
||||
pub auto_hide: bool,
|
||||
|
||||
#[serde(default = "def_autohide_delay")]
|
||||
pub auto_hide_delay: u32,
|
||||
|
||||
#[serde(default = "def_keyboard_repeat_delay")]
|
||||
pub keyboard_repeat_delay: u32,
|
||||
|
||||
#[serde(default = "def_keyboard_repeat_rate")]
|
||||
pub keyboard_repeat_rate: u32,
|
||||
|
||||
#[serde(default = "def_blit_method")]
|
||||
pub blit_method: String,
|
||||
}
|
||||
|
||||
impl WayVRConfig {
|
||||
pub fn get_catalog(&self, name: &str) -> Option<&WayVRCatalog> {
|
||||
self.catalogs.get(name)
|
||||
}
|
||||
|
||||
pub fn get_display(&self, name: &str) -> Option<&WayVRDisplay> {
|
||||
self.displays.get(name)
|
||||
}
|
||||
|
||||
pub fn get_default_display(&self) -> Option<(String, &WayVRDisplay)> {
|
||||
for (disp_name, disp) in &self.displays {
|
||||
if disp.primary.unwrap_or(false) {
|
||||
return Some((disp_name.clone(), disp));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_wayvr_config(
|
||||
config_general: &crate::config::GeneralConfig,
|
||||
config_wayvr: &Self,
|
||||
) -> anyhow::Result<wayvr::Config> {
|
||||
Ok(wayvr::Config {
|
||||
click_freeze_time_ms: config_general.click_freeze_time_ms,
|
||||
keyboard_repeat_delay_ms: config_wayvr.keyboard_repeat_delay,
|
||||
keyboard_repeat_rate: config_wayvr.keyboard_repeat_rate,
|
||||
blit_method: wayvr::BlitMethod::from_string(&config_wayvr.blit_method)
|
||||
.ok_or_else(|| anyhow::anyhow!("Unknown blit method"))?,
|
||||
auto_hide_delay: if config_wayvr.auto_hide {
|
||||
Some(config_wayvr.auto_hide_delay)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn post_load(
|
||||
&self,
|
||||
config: &crate::config::GeneralConfig,
|
||||
tasks: &mut TaskContainer,
|
||||
) -> anyhow::Result<Option<Rc<RefCell<WayVRData>>>> {
|
||||
let primary_count = self
|
||||
.displays
|
||||
.iter()
|
||||
.filter(|d| d.1.primary.unwrap_or(false))
|
||||
.count();
|
||||
|
||||
if primary_count > 1 {
|
||||
anyhow::bail!("Number of primary displays is more than 1")
|
||||
} else if primary_count == 0 {
|
||||
log::warn!(
|
||||
"No primary display specified. External Wayland applications will not be attached."
|
||||
);
|
||||
}
|
||||
|
||||
for (catalog_name, catalog) in &self.catalogs {
|
||||
for app in &catalog.apps {
|
||||
if let Some(b) = app.shown_at_start {
|
||||
if b {
|
||||
tasks.enqueue(TaskType::WayVR(WayVRAction::AppClick {
|
||||
catalog_name: Arc::from(catalog_name.as_str()),
|
||||
app_name: Arc::from(app.name.as_str()),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.run_compositor_at_start {
|
||||
// Start Wayland server instantly
|
||||
Ok(Some(Rc::new(RefCell::new(WayVRData::new(
|
||||
Self::get_wayvr_config(config, self)?,
|
||||
)?))))
|
||||
} else {
|
||||
// Lazy-init WayVR later if the user requested
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_wayvr() -> WayVRConfig {
|
||||
let config_root_path = config_io::ConfigRoot::WayVR.ensure_dir();
|
||||
log::info!("WayVR Config root path: {config_root_path:?}");
|
||||
log::info!(
|
||||
"WayVR conf.d path: {:?}",
|
||||
config_io::ConfigRoot::WayVR.get_conf_d_path()
|
||||
);
|
||||
|
||||
load_config_with_conf_d::<WayVRConfig>("wayvr.yaml", config_io::ConfigRoot::WayVR)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use image_dds::{ImageFormat, Surface};
|
||||
use std::{io::Read, sync::Arc};
|
||||
use vulkano::{
|
||||
buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer},
|
||||
command_buffer::CopyBufferToImageInfo,
|
||||
format::Format,
|
||||
image::{Image, ImageCreateInfo, ImageType, ImageUsage},
|
||||
memory::allocator::{AllocationCreateInfo, MemoryTypeFilter},
|
||||
DeviceSize,
|
||||
};
|
||||
|
||||
use super::WlxUploadsBuffer;
|
||||
|
||||
pub trait WlxCommandBufferDds {
|
||||
fn texture2d_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>>
|
||||
where
|
||||
R: Read;
|
||||
}
|
||||
|
||||
impl WlxCommandBufferDds for WlxUploadsBuffer {
|
||||
fn texture2d_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let Ok(dds) = image_dds::ddsfile::Dds::read(r) else {
|
||||
anyhow::bail!("Not a valid DDS file.\nSee: https://github.com/galister/wlx-overlay-s/wiki/Custom-Textures");
|
||||
};
|
||||
|
||||
let surface = Surface::from_dds(&dds)?;
|
||||
|
||||
if surface.depth != 1 {
|
||||
anyhow::bail!("Not a 2D texture.")
|
||||
}
|
||||
|
||||
let image = Image::new(
|
||||
self.graphics.memory_allocator.clone(),
|
||||
ImageCreateInfo {
|
||||
image_type: ImageType::Dim2d,
|
||||
format: dds_to_vk(surface.image_format)?,
|
||||
extent: [surface.width, surface.height, surface.depth],
|
||||
usage: ImageUsage::TRANSFER_DST | ImageUsage::TRANSFER_SRC | ImageUsage::SAMPLED,
|
||||
..Default::default()
|
||||
},
|
||||
AllocationCreateInfo::default(),
|
||||
)?;
|
||||
|
||||
let buffer: Subbuffer<[u8]> = Buffer::new_slice(
|
||||
self.graphics.memory_allocator.clone(),
|
||||
BufferCreateInfo {
|
||||
usage: BufferUsage::TRANSFER_SRC,
|
||||
..Default::default()
|
||||
},
|
||||
AllocationCreateInfo {
|
||||
memory_type_filter: MemoryTypeFilter::PREFER_HOST
|
||||
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
|
||||
..Default::default()
|
||||
},
|
||||
surface.data.len() as DeviceSize,
|
||||
)?;
|
||||
|
||||
buffer.write()?.copy_from_slice(surface.data);
|
||||
|
||||
self.command_buffer
|
||||
.copy_buffer_to_image(CopyBufferToImageInfo::buffer_image(buffer, image.clone()))?;
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dds_to_vk(dds_fmt: ImageFormat) -> anyhow::Result<Format> {
|
||||
match dds_fmt {
|
||||
ImageFormat::R8Unorm => Ok(Format::R8_UNORM),
|
||||
ImageFormat::Rgba8Unorm => Ok(Format::R8G8B8A8_UNORM),
|
||||
ImageFormat::Rgba8UnormSrgb => Ok(Format::R8G8B8A8_SRGB),
|
||||
ImageFormat::Rgba16Float => Ok(Format::R16G16B16A16_SFLOAT),
|
||||
ImageFormat::Rgba32Float => Ok(Format::R32G32B32A32_SFLOAT),
|
||||
ImageFormat::Bgra8Unorm => Ok(Format::B8G8R8A8_UNORM),
|
||||
ImageFormat::Bgra8UnormSrgb => Ok(Format::B8G8R8A8_SRGB),
|
||||
// DXT1
|
||||
ImageFormat::BC1RgbaUnorm => Ok(Format::BC1_RGBA_UNORM_BLOCK),
|
||||
ImageFormat::BC1RgbaUnormSrgb => Ok(Format::BC1_RGBA_SRGB_BLOCK),
|
||||
// DXT3
|
||||
ImageFormat::BC2RgbaUnorm => Ok(Format::BC2_UNORM_BLOCK),
|
||||
ImageFormat::BC2RgbaUnormSrgb => Ok(Format::BC2_SRGB_BLOCK),
|
||||
// DXT5
|
||||
ImageFormat::BC3RgbaUnorm => Ok(Format::BC3_UNORM_BLOCK),
|
||||
ImageFormat::BC3RgbaUnormSrgb => Ok(Format::BC3_SRGB_BLOCK),
|
||||
// RGTC1
|
||||
ImageFormat::BC4RUnorm => Ok(Format::BC4_UNORM_BLOCK),
|
||||
ImageFormat::BC4RSnorm => Ok(Format::BC4_SNORM_BLOCK),
|
||||
// RGTC2
|
||||
ImageFormat::BC5RgUnorm => Ok(Format::BC5_UNORM_BLOCK),
|
||||
ImageFormat::BC5RgSnorm => Ok(Format::BC5_SNORM_BLOCK),
|
||||
// BPTC
|
||||
ImageFormat::BC6hRgbUfloat => Ok(Format::BC6H_UFLOAT_BLOCK),
|
||||
ImageFormat::BC6hRgbSfloat => Ok(Format::BC6H_SFLOAT_BLOCK),
|
||||
// BPTC
|
||||
ImageFormat::BC7RgbaUnorm => Ok(Format::BC7_UNORM_BLOCK),
|
||||
ImageFormat::BC7RgbaUnormSrgb => Ok(Format::BC7_SRGB_BLOCK),
|
||||
_ => anyhow::bail!("Unsupported format {:?}", dds_fmt),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
use std::{mem::MaybeUninit, sync::Arc};
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use vulkano::{
|
||||
device::Device,
|
||||
image::{sys::RawImage, ImageCreateInfo, SubresourceLayout},
|
||||
sync::Sharing,
|
||||
VulkanError, VulkanObject,
|
||||
};
|
||||
|
||||
#[allow(clippy::all, clippy::pedantic)]
|
||||
pub(super) unsafe fn create_dmabuf_image(
|
||||
device: Arc<Device>,
|
||||
create_info: ImageCreateInfo,
|
||||
) -> Result<RawImage, VulkanError> {
|
||||
let &ImageCreateInfo {
|
||||
flags,
|
||||
image_type,
|
||||
format,
|
||||
ref view_formats,
|
||||
extent,
|
||||
array_layers,
|
||||
mip_levels,
|
||||
samples,
|
||||
tiling,
|
||||
usage,
|
||||
stencil_usage,
|
||||
ref sharing,
|
||||
initial_layout,
|
||||
ref drm_format_modifiers,
|
||||
ref drm_format_modifier_plane_layouts,
|
||||
external_memory_handle_types,
|
||||
_ne: _,
|
||||
} = &create_info;
|
||||
|
||||
let (sharing_mode, queue_family_index_count, p_queue_family_indices) = match sharing {
|
||||
Sharing::Exclusive => (ash::vk::SharingMode::EXCLUSIVE, 0, &[] as _),
|
||||
Sharing::Concurrent(queue_family_indices) => (
|
||||
ash::vk::SharingMode::CONCURRENT,
|
||||
queue_family_indices.len() as u32,
|
||||
queue_family_indices.as_ptr(),
|
||||
),
|
||||
};
|
||||
|
||||
let mut create_info_vk = ash::vk::ImageCreateInfo {
|
||||
flags: flags.into(),
|
||||
image_type: image_type.into(),
|
||||
format: format.into(),
|
||||
extent: ash::vk::Extent3D {
|
||||
width: extent[0],
|
||||
height: extent[1],
|
||||
depth: extent[2],
|
||||
},
|
||||
mip_levels,
|
||||
array_layers,
|
||||
samples: samples.into(),
|
||||
tiling: tiling.into(),
|
||||
usage: usage.into(),
|
||||
sharing_mode,
|
||||
queue_family_index_count,
|
||||
p_queue_family_indices,
|
||||
initial_layout: initial_layout.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut drm_format_modifier_explicit_info_vk = None;
|
||||
let drm_format_modifier_plane_layouts_vk: SmallVec<[_; 4]>;
|
||||
let mut drm_format_modifier_list_info_vk = None;
|
||||
let mut external_memory_info_vk = None;
|
||||
let mut format_list_info_vk = None;
|
||||
let format_list_view_formats_vk: Vec<_>;
|
||||
let mut stencil_usage_info_vk = None;
|
||||
|
||||
if drm_format_modifiers.len() == 1 {
|
||||
drm_format_modifier_plane_layouts_vk = drm_format_modifier_plane_layouts
|
||||
.iter()
|
||||
.map(|subresource_layout| {
|
||||
let &SubresourceLayout {
|
||||
offset,
|
||||
size,
|
||||
row_pitch,
|
||||
array_pitch,
|
||||
depth_pitch,
|
||||
} = subresource_layout;
|
||||
|
||||
ash::vk::SubresourceLayout {
|
||||
offset,
|
||||
size,
|
||||
row_pitch,
|
||||
array_pitch: array_pitch.unwrap_or(0),
|
||||
depth_pitch: depth_pitch.unwrap_or(0),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let next = drm_format_modifier_explicit_info_vk.insert(
|
||||
ash::vk::ImageDrmFormatModifierExplicitCreateInfoEXT {
|
||||
drm_format_modifier: drm_format_modifiers[0],
|
||||
drm_format_modifier_plane_count: drm_format_modifier_plane_layouts_vk.len() as u32,
|
||||
p_plane_layouts: drm_format_modifier_plane_layouts_vk.as_ptr(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
} else if drm_format_modifiers.len() > 1 {
|
||||
let next = drm_format_modifier_list_info_vk.insert(
|
||||
ash::vk::ImageDrmFormatModifierListCreateInfoEXT {
|
||||
drm_format_modifier_count: drm_format_modifiers.len() as u32,
|
||||
p_drm_format_modifiers: drm_format_modifiers.as_ptr(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
}
|
||||
|
||||
if !external_memory_handle_types.is_empty() {
|
||||
let next = external_memory_info_vk.insert(ash::vk::ExternalMemoryImageCreateInfo {
|
||||
handle_types: external_memory_handle_types.into(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
}
|
||||
|
||||
if !view_formats.is_empty() {
|
||||
format_list_view_formats_vk = view_formats
|
||||
.iter()
|
||||
.copied()
|
||||
.map(ash::vk::Format::from)
|
||||
.collect();
|
||||
|
||||
let next = format_list_info_vk.insert(ash::vk::ImageFormatListCreateInfo {
|
||||
view_format_count: format_list_view_formats_vk.len() as u32,
|
||||
p_view_formats: format_list_view_formats_vk.as_ptr(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
}
|
||||
|
||||
if let Some(stencil_usage) = stencil_usage {
|
||||
let next = stencil_usage_info_vk.insert(ash::vk::ImageStencilUsageCreateInfo {
|
||||
stencil_usage: stencil_usage.into(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
next.p_next = create_info_vk.p_next;
|
||||
create_info_vk.p_next = next as *const _ as *const _;
|
||||
}
|
||||
|
||||
let handle = {
|
||||
let fns = device.fns();
|
||||
let mut output = MaybeUninit::uninit();
|
||||
(fns.v1_0.create_image)(
|
||||
device.handle(),
|
||||
&create_info_vk,
|
||||
std::ptr::null(),
|
||||
output.as_mut_ptr(),
|
||||
)
|
||||
.result()
|
||||
.map_err(VulkanError::from)?;
|
||||
output.assume_init()
|
||||
};
|
||||
|
||||
RawImage::from_handle(device, handle, create_info)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,267 @@
|
||||
use glam::Vec4;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vulkano::format::Format;
|
||||
|
||||
use crate::{
|
||||
graphics::WlxGraphics,
|
||||
gui::{canvas::control::ControlRenderer, GuiColor, KeyCapType},
|
||||
};
|
||||
|
||||
use super::{control::Control, Canvas, Rect};
|
||||
|
||||
pub struct CanvasBuilder<D, S> {
|
||||
canvas: Canvas<D, S>,
|
||||
|
||||
pub fg_color: GuiColor,
|
||||
pub bg_color: GuiColor,
|
||||
pub font_size: isize,
|
||||
}
|
||||
|
||||
impl<D, S> CanvasBuilder<D, S> {
|
||||
pub fn new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
graphics: Arc<WlxGraphics>,
|
||||
format: Format,
|
||||
data: D,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
canvas: Canvas::new(width, height, graphics, format, data)?,
|
||||
bg_color: Vec4::ZERO,
|
||||
fg_color: Vec4::ONE,
|
||||
font_size: 16,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build(self) -> Canvas<D, S> {
|
||||
self.canvas
|
||||
}
|
||||
|
||||
// Creates a panel with bg_color inherited from the canvas
|
||||
pub fn panel(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
corner_radius: radius,
|
||||
bg_color: self.bg_color,
|
||||
on_render_bg: Some(Control::render_rounded_rect),
|
||||
..Control::new()
|
||||
});
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
// Creates a label with fg_color, font_size inherited from the canvas
|
||||
pub fn label(
|
||||
&mut self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
radius: f32,
|
||||
text: Arc<str>,
|
||||
) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
corner_radius: radius,
|
||||
text,
|
||||
fg_color: self.fg_color,
|
||||
size: self.font_size,
|
||||
on_render_fg: Some(Control::render_text),
|
||||
..Control::new()
|
||||
});
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
// Creates a label with fg_color, font_size inherited from the canvas
|
||||
#[allow(dead_code)]
|
||||
pub fn label_centered(
|
||||
&mut self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
radius: f32,
|
||||
text: Arc<str>,
|
||||
) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
corner_radius: radius,
|
||||
text,
|
||||
fg_color: self.fg_color,
|
||||
size: self.font_size,
|
||||
on_render_fg: Some(Control::render_text_centered),
|
||||
..Control::new()
|
||||
});
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
// Creates a sprite. Will not draw anything until set_sprite is called.
|
||||
pub fn sprite(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
corner_radius: 0.,
|
||||
on_render_bg: Some(Control::render_sprite_bg),
|
||||
..Control::new()
|
||||
});
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
// Creates a sprite that highlights on pointer hover. Will not draw anything until set_sprite is called.
|
||||
#[allow(dead_code)]
|
||||
pub fn sprite_interactive(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
corner_radius: 0.,
|
||||
on_render_bg: Some(Control::render_sprite_bg),
|
||||
on_render_hl: Some(Control::render_sprite_hl),
|
||||
..Control::new()
|
||||
});
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
// Creates a button with fg_color, bg_color, font_size inherited from the canvas
|
||||
pub fn button(
|
||||
&mut self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
radius: f32,
|
||||
text: Arc<str>,
|
||||
) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
|
||||
self.canvas.interactive_set_idx(x, y, w, h, idx);
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
corner_radius: radius,
|
||||
text,
|
||||
fg_color: self.fg_color,
|
||||
bg_color: self.bg_color,
|
||||
size: self.font_size,
|
||||
on_render_bg: Some(Control::render_rounded_rect),
|
||||
on_render_fg: Some(Control::render_text_centered),
|
||||
on_render_hl: Some(Control::render_highlight),
|
||||
..Control::new()
|
||||
});
|
||||
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn key_button(
|
||||
&mut self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
radius: f32,
|
||||
cap_type: KeyCapType,
|
||||
label: &[String],
|
||||
) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.interactive_set_idx(x, y, w, h, idx);
|
||||
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
corner_radius: radius,
|
||||
bg_color: self.bg_color,
|
||||
on_render_bg: Some(Control::render_rounded_rect),
|
||||
on_render_hl: Some(Control::render_highlight),
|
||||
..Control::new()
|
||||
});
|
||||
|
||||
let renders = match cap_type {
|
||||
KeyCapType::Regular => {
|
||||
let render: ControlRenderer<D, S> = Control::render_text_centered;
|
||||
let rect = Rect {
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h: h - self.font_size as f32,
|
||||
};
|
||||
vec![(render, rect, 1f32)]
|
||||
}
|
||||
KeyCapType::RegularAltGr => {
|
||||
let render: ControlRenderer<D, S> = Control::render_text;
|
||||
let rect0 = Rect {
|
||||
x: x + 12.,
|
||||
y: y + (self.font_size as f32) + 12.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
let rect1 = Rect {
|
||||
x: w.mul_add(0.5, x) + 12.,
|
||||
y: y + h - (self.font_size as f32) + 8.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
vec![(render, rect0, 1.0), (render, rect1, 0.8)]
|
||||
}
|
||||
KeyCapType::Reversed => {
|
||||
let render: ControlRenderer<D, S> = Control::render_text_centered;
|
||||
let rect0 = Rect {
|
||||
x,
|
||||
y: y + 2.0,
|
||||
w,
|
||||
h: h * 0.5,
|
||||
};
|
||||
let rect1 = Rect {
|
||||
x,
|
||||
y: h.mul_add(0.5, y) + 2.0,
|
||||
w,
|
||||
h: h * 0.5,
|
||||
};
|
||||
vec![(render, rect1, 1.0), (render, rect0, 0.8)]
|
||||
}
|
||||
KeyCapType::ReversedAltGr => {
|
||||
let render: ControlRenderer<D, S> = Control::render_text;
|
||||
let rect0 = Rect {
|
||||
x: x + 12.,
|
||||
y: y + (self.font_size as f32) + 8.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
let rect1 = Rect {
|
||||
x: x + 12.,
|
||||
y: y + h - (self.font_size as f32) + 4.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
let rect2 = Rect {
|
||||
x: w.mul_add(0.5, x) + 8.,
|
||||
y: y + h - (self.font_size as f32) + 4.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
vec![
|
||||
(render, rect1, 1.0),
|
||||
(render, rect0, 0.8),
|
||||
(render, rect2, 0.8),
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
for (idx, (render, rect, alpha)) in renders.into_iter().enumerate() {
|
||||
if idx >= label.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.canvas.controls.push(Control {
|
||||
rect,
|
||||
text: Arc::from(label[idx].as_str()),
|
||||
fg_color: self.fg_color * alpha,
|
||||
size: self.font_size,
|
||||
on_render_fg: Some(render),
|
||||
..Control::new()
|
||||
});
|
||||
}
|
||||
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
use glam::Vec4;
|
||||
use std::sync::Arc;
|
||||
use vulkano::image::view::ImageView;
|
||||
|
||||
use crate::{
|
||||
backend::input::PointerMode, graphics::WlxCommandBuffer, gui::GuiColor, state::AppState,
|
||||
};
|
||||
|
||||
use super::{CanvasData, Rect};
|
||||
|
||||
pub type ControlRenderer<D, S> =
|
||||
fn(&Control<D, S>, &CanvasData<D>, &mut AppState, &mut WlxCommandBuffer) -> anyhow::Result<()>;
|
||||
|
||||
pub type ControlRendererHl<D, S> = fn(
|
||||
&Control<D, S>,
|
||||
&CanvasData<D>,
|
||||
&mut AppState,
|
||||
&mut WlxCommandBuffer,
|
||||
Vec4,
|
||||
) -> anyhow::Result<()>;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct Control<D, S> {
|
||||
pub state: Option<S>,
|
||||
pub rect: Rect,
|
||||
pub corner_radius: f32,
|
||||
pub fg_color: GuiColor,
|
||||
pub bg_color: GuiColor,
|
||||
pub text: Arc<str>,
|
||||
pub size: isize,
|
||||
pub sprite: Option<Arc<ImageView>>,
|
||||
pub sprite_st: Vec4,
|
||||
pub(super) bg_dirty: bool,
|
||||
pub(super) fg_dirty: bool,
|
||||
|
||||
pub on_update: Option<fn(&mut Self, &mut D, &mut AppState)>,
|
||||
pub on_press: Option<fn(&mut Self, &mut D, &mut AppState, PointerMode)>,
|
||||
pub on_release: Option<fn(&mut Self, &mut D, &mut AppState)>,
|
||||
pub on_scroll: Option<fn(&mut Self, &mut D, &mut AppState, f32, f32)>,
|
||||
pub test_highlight: Option<fn(&Self, &mut D, &mut AppState) -> Option<Vec4>>,
|
||||
|
||||
pub(super) on_render_bg: Option<ControlRenderer<D, S>>,
|
||||
pub(super) on_render_hl: Option<ControlRendererHl<D, S>>,
|
||||
pub(super) on_render_fg: Option<ControlRenderer<D, S>>,
|
||||
}
|
||||
|
||||
impl<D, S> Control<D, S> {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
rect: Rect {
|
||||
x: 0.,
|
||||
y: 0.,
|
||||
w: 0.,
|
||||
h: 0.,
|
||||
},
|
||||
corner_radius: 0.,
|
||||
fg_color: Vec4::ONE,
|
||||
bg_color: Vec4::ZERO,
|
||||
text: Arc::from(""),
|
||||
sprite: None,
|
||||
sprite_st: Vec4::new(1., 1., 0., 0.),
|
||||
bg_dirty: true,
|
||||
fg_dirty: true,
|
||||
size: 24,
|
||||
state: None,
|
||||
on_update: None,
|
||||
on_render_bg: None,
|
||||
on_render_hl: None,
|
||||
on_render_fg: None,
|
||||
test_highlight: None,
|
||||
on_press: None,
|
||||
on_release: None,
|
||||
on_scroll: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if *self.text == *text {
|
||||
return;
|
||||
}
|
||||
self.text = text.into();
|
||||
self.fg_dirty = true;
|
||||
}
|
||||
|
||||
pub fn set_sprite(&mut self, sprite: Arc<ImageView>) {
|
||||
self.sprite.replace(sprite);
|
||||
self.bg_dirty = true;
|
||||
}
|
||||
|
||||
pub fn set_sprite_st(&mut self, sprite_st: Vec4) {
|
||||
if self.sprite_st == sprite_st {
|
||||
return;
|
||||
}
|
||||
self.sprite_st = sprite_st;
|
||||
self.bg_dirty = true;
|
||||
}
|
||||
|
||||
pub fn set_fg_color(&mut self, color: GuiColor) {
|
||||
if self.fg_color == color {
|
||||
return;
|
||||
}
|
||||
self.fg_color = color;
|
||||
self.fg_dirty = true;
|
||||
}
|
||||
|
||||
pub fn render_rounded_rect(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
_: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
) -> anyhow::Result<()> {
|
||||
let pass = {
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
self.rect.x,
|
||||
self.rect.y,
|
||||
self.rect.w,
|
||||
self.rect.h,
|
||||
)?;
|
||||
|
||||
let clamped_radius = self
|
||||
.corner_radius
|
||||
.min(self.rect.w / 2.0)
|
||||
.min(self.rect.h / 2.0);
|
||||
|
||||
let skew_radius = [clamped_radius / self.rect.w, clamped_radius / self.rect.h];
|
||||
|
||||
let set0 = canvas.pipeline_bg_color.uniform_buffer(
|
||||
0,
|
||||
vec![
|
||||
self.bg_color.x,
|
||||
self.bg_color.y,
|
||||
self.bg_color.z,
|
||||
self.bg_color.w,
|
||||
skew_radius[0],
|
||||
skew_radius[1],
|
||||
],
|
||||
)?;
|
||||
|
||||
canvas.pipeline_bg_color.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer,
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0],
|
||||
)?
|
||||
};
|
||||
|
||||
cmd_buffer.run_ref(&pass)
|
||||
}
|
||||
|
||||
pub(super) fn render_highlight(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
_: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
color: GuiColor,
|
||||
) -> anyhow::Result<()> {
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
self.rect.x,
|
||||
self.rect.y,
|
||||
self.rect.w,
|
||||
self.rect.h,
|
||||
)?;
|
||||
|
||||
let clamped_radius = self
|
||||
.corner_radius
|
||||
.min(self.rect.w / 2.0)
|
||||
.min(self.rect.h / 2.0);
|
||||
|
||||
let skew_radius = [clamped_radius / self.rect.w, clamped_radius / self.rect.h];
|
||||
|
||||
let set0 = canvas.pipeline_hl_color.uniform_buffer(
|
||||
0,
|
||||
vec![
|
||||
color.x,
|
||||
color.y,
|
||||
color.z,
|
||||
color.w,
|
||||
skew_radius[0],
|
||||
skew_radius[1],
|
||||
],
|
||||
)?;
|
||||
|
||||
let pass = canvas.pipeline_hl_color.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer,
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0],
|
||||
)?;
|
||||
|
||||
cmd_buffer.run_ref(&pass)
|
||||
}
|
||||
|
||||
pub(super) fn render_text(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
app: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut cur_y = self.rect.y;
|
||||
for line in self.text.lines() {
|
||||
let mut cur_x = self.rect.x;
|
||||
for glyph in app
|
||||
.fc
|
||||
.get_glyphs(line, self.size, canvas.graphics.clone())?
|
||||
{
|
||||
if let Some(tex) = glyph.tex.clone() {
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
cur_x + glyph.left,
|
||||
cur_y - glyph.top,
|
||||
glyph.width,
|
||||
glyph.height,
|
||||
)?;
|
||||
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
|
||||
0,
|
||||
ImageView::new_default(tex)?,
|
||||
app.graphics.texture_filtering,
|
||||
)?;
|
||||
let set1 = canvas
|
||||
.pipeline_fg_glyph
|
||||
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
|
||||
let pass = canvas.pipeline_fg_glyph.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer,
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0, set1],
|
||||
)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
}
|
||||
cur_x += glyph.advance;
|
||||
}
|
||||
cur_y += (self.size as f32) * 1.5;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn render_text_centered(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
app: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
) -> anyhow::Result<()> {
|
||||
let (w, h) = app
|
||||
.fc
|
||||
.get_text_size(&self.text, self.size, canvas.graphics.clone())?;
|
||||
|
||||
let mut cur_y =
|
||||
(self.size as f32).mul_add(-0.25, h.mul_add(-0.5, self.rect.y + (self.rect.h)));
|
||||
for line in self.text.lines() {
|
||||
let mut cur_x = w.mul_add(-0.5, self.rect.w.mul_add(0.5, self.rect.x));
|
||||
for glyph in app
|
||||
.fc
|
||||
.get_glyphs(line, self.size, canvas.graphics.clone())?
|
||||
{
|
||||
if let Some(tex) = glyph.tex.clone() {
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
cur_x + glyph.left,
|
||||
cur_y - glyph.top,
|
||||
glyph.width,
|
||||
glyph.height,
|
||||
)?;
|
||||
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
|
||||
0,
|
||||
ImageView::new_default(tex)?,
|
||||
app.graphics.texture_filtering,
|
||||
)?;
|
||||
let set1 = canvas
|
||||
.pipeline_fg_glyph
|
||||
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
|
||||
let pass = canvas.pipeline_fg_glyph.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer,
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0, set1],
|
||||
)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
}
|
||||
cur_x += glyph.advance;
|
||||
}
|
||||
cur_y += (self.size as f32) * 1.5;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn render_sprite_bg(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
app: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(view) = self.sprite.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
self.rect.x,
|
||||
self.rect.y,
|
||||
self.rect.w,
|
||||
self.rect.h,
|
||||
)?;
|
||||
let set0 = canvas.pipeline_bg_sprite.uniform_sampler(
|
||||
0,
|
||||
view.clone(),
|
||||
app.graphics.texture_filtering,
|
||||
)?;
|
||||
|
||||
let uniform = vec![
|
||||
self.sprite_st.x,
|
||||
self.sprite_st.y,
|
||||
self.sprite_st.z,
|
||||
self.sprite_st.w,
|
||||
self.fg_color.x,
|
||||
self.fg_color.y,
|
||||
self.fg_color.z,
|
||||
self.fg_color.w,
|
||||
];
|
||||
|
||||
let set1 = canvas.pipeline_bg_sprite.uniform_buffer(1, uniform)?;
|
||||
|
||||
let pass = canvas.pipeline_bg_sprite.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer,
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0, set1],
|
||||
)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn render_sprite_hl(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
app: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
color: GuiColor,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(view) = self.sprite.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
self.rect.x,
|
||||
self.rect.y,
|
||||
self.rect.w,
|
||||
self.rect.h,
|
||||
)?;
|
||||
let set0 = canvas.pipeline_hl_sprite.uniform_sampler(
|
||||
0,
|
||||
view.clone(),
|
||||
app.graphics.texture_filtering,
|
||||
)?;
|
||||
|
||||
let uniform = vec![
|
||||
self.sprite_st.x,
|
||||
self.sprite_st.y,
|
||||
self.sprite_st.z,
|
||||
self.sprite_st.w,
|
||||
color.x,
|
||||
color.y,
|
||||
color.z,
|
||||
color.w,
|
||||
];
|
||||
|
||||
let set1 = canvas.pipeline_hl_sprite.uniform_buffer(1, uniform)?;
|
||||
|
||||
let pass = canvas.pipeline_hl_sprite.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer,
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0, set1],
|
||||
)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
pub mod builder;
|
||||
pub mod control;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use glam::{Vec2, Vec4};
|
||||
use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::view::ImageView};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
input::{Haptics, InteractionHandler, PointerHit},
|
||||
overlay::{FrameMeta, OverlayBackend, OverlayRenderer, ShouldRender},
|
||||
},
|
||||
graphics::{CommandBuffers, WlxGraphics, WlxPipeline, BLEND_ALPHA, SWAPCHAIN_FORMAT},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
const RES_DIVIDER: usize = 4;
|
||||
|
||||
pub struct Rect {
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
}
|
||||
|
||||
pub struct CanvasData<D> {
|
||||
pub data: D,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
|
||||
graphics: Arc<WlxGraphics>,
|
||||
|
||||
pipeline_bg_color: Arc<WlxPipeline>,
|
||||
pipeline_bg_sprite: Arc<WlxPipeline>,
|
||||
pipeline_fg_glyph: Arc<WlxPipeline>,
|
||||
pipeline_hl_color: Arc<WlxPipeline>,
|
||||
pipeline_hl_sprite: Arc<WlxPipeline>,
|
||||
}
|
||||
|
||||
pub struct Canvas<D, S> {
|
||||
controls: Vec<control::Control<D, S>>,
|
||||
data: CanvasData<D>,
|
||||
|
||||
hover_controls: [Option<usize>; 2],
|
||||
pressed_controls: [Option<usize>; 2],
|
||||
|
||||
interact_map: Vec<Option<u16>>,
|
||||
interact_stride: usize,
|
||||
interact_rows: usize,
|
||||
|
||||
pipeline_final: Arc<WlxPipeline>,
|
||||
|
||||
view_fore: Arc<ImageView>,
|
||||
view_back: Arc<ImageView>,
|
||||
|
||||
format: Format,
|
||||
|
||||
back_dirty: bool,
|
||||
high_dirty: bool,
|
||||
fore_dirty: bool,
|
||||
}
|
||||
|
||||
impl<D, S> Canvas<D, S> {
|
||||
fn new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
graphics: Arc<WlxGraphics>,
|
||||
format: Format,
|
||||
data: D,
|
||||
) -> anyhow::Result<Self> {
|
||||
let tex_fore = graphics.render_texture(width as _, height as _, format)?;
|
||||
let tex_back = graphics.render_texture(width as _, height as _, format)?;
|
||||
|
||||
let view_fore = ImageView::new_default(tex_fore)?;
|
||||
let view_back = ImageView::new_default(tex_back)?;
|
||||
|
||||
let Ok(shaders) = graphics.shared_shaders.read() else {
|
||||
anyhow::bail!("Failed to lock shared shaders for reading");
|
||||
};
|
||||
|
||||
let vert = shaders.get("vert_common").unwrap().clone(); // want panic
|
||||
|
||||
let pipeline_bg_color = graphics.create_pipeline(
|
||||
vert.clone(),
|
||||
shaders.get("frag_color").unwrap().clone(), // want panic
|
||||
format,
|
||||
Some(BLEND_ALPHA),
|
||||
)?;
|
||||
|
||||
let pipeline_fg_glyph = graphics.create_pipeline(
|
||||
vert.clone(),
|
||||
shaders.get("frag_glyph").unwrap().clone(), // want panic
|
||||
format,
|
||||
Some(BLEND_ALPHA),
|
||||
)?;
|
||||
|
||||
let pipeline_bg_sprite = graphics.create_pipeline(
|
||||
vert.clone(),
|
||||
shaders.get("frag_sprite2").unwrap().clone(), // want panic
|
||||
format,
|
||||
Some(BLEND_ALPHA),
|
||||
)?;
|
||||
|
||||
let pipeline_hl_color = graphics.create_pipeline(
|
||||
vert.clone(),
|
||||
shaders.get("frag_color").unwrap().clone(), // want panic
|
||||
SWAPCHAIN_FORMAT,
|
||||
Some(BLEND_ALPHA),
|
||||
)?;
|
||||
|
||||
let pipeline_hl_sprite = graphics.create_pipeline(
|
||||
vert.clone(),
|
||||
shaders.get("frag_sprite2_hl").unwrap().clone(), // want panic
|
||||
SWAPCHAIN_FORMAT,
|
||||
Some(BLEND_ALPHA),
|
||||
)?;
|
||||
|
||||
let pipeline_final = graphics.create_pipeline(
|
||||
vert,
|
||||
shaders.get("frag_srgb").unwrap().clone(), // want panic
|
||||
SWAPCHAIN_FORMAT,
|
||||
Some(BLEND_ALPHA),
|
||||
)?;
|
||||
|
||||
let stride = width / RES_DIVIDER;
|
||||
let rows = height / RES_DIVIDER;
|
||||
|
||||
Ok(Self {
|
||||
data: CanvasData {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
graphics: graphics.clone(),
|
||||
pipeline_bg_color,
|
||||
pipeline_bg_sprite,
|
||||
pipeline_fg_glyph,
|
||||
pipeline_hl_color,
|
||||
pipeline_hl_sprite,
|
||||
},
|
||||
controls: Vec::new(),
|
||||
hover_controls: [None, None],
|
||||
pressed_controls: [None, None],
|
||||
interact_map: vec![None; stride * rows],
|
||||
interact_stride: stride,
|
||||
interact_rows: rows,
|
||||
pipeline_final,
|
||||
view_fore,
|
||||
view_back,
|
||||
format,
|
||||
back_dirty: false,
|
||||
high_dirty: false,
|
||||
fore_dirty: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn interactive_set_idx(&mut self, x: f32, y: f32, w: f32, h: f32, idx: usize) {
|
||||
let (x, y, w, h) = (x as usize, y as usize, w as usize, h as usize);
|
||||
|
||||
let x_min = (x / RES_DIVIDER).max(0);
|
||||
let y_min = (y / RES_DIVIDER).max(0);
|
||||
let x_max = (x_min + (w / RES_DIVIDER)).min(self.interact_stride - 1);
|
||||
let y_max = (y_min + (h / RES_DIVIDER)).min(self.interact_rows - 1);
|
||||
|
||||
for y in y_min..y_max {
|
||||
for x in x_min..x_max {
|
||||
self.interact_map[y * self.interact_stride + x] = Some(idx as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn interactive_get_idx(&self, uv: Vec2) -> Option<usize> {
|
||||
let x = (uv.x * self.data.width as f32) as usize;
|
||||
let y = (uv.y * self.data.height as f32) as usize;
|
||||
let x = (x / RES_DIVIDER).max(0).min(self.interact_stride - 1);
|
||||
let y = (y / RES_DIVIDER).max(0).min(self.interact_rows - 1);
|
||||
self.interact_map[y * self.interact_stride + x].map(|x| x as usize)
|
||||
}
|
||||
|
||||
pub const fn data_mut(&mut self) -> &mut D {
|
||||
&mut self.data.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, S> InteractionHandler for Canvas<D, S> {
|
||||
fn on_left(&mut self, _app: &mut AppState, pointer: usize) {
|
||||
self.high_dirty = true;
|
||||
|
||||
self.hover_controls[pointer] = None;
|
||||
}
|
||||
fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
|
||||
// render on every frame if we are being hovered
|
||||
self.high_dirty = true;
|
||||
|
||||
let old = self.hover_controls[hit.pointer];
|
||||
if let Some(i) = self.interactive_get_idx(hit.uv) {
|
||||
self.hover_controls[hit.pointer] = Some(i);
|
||||
} else {
|
||||
self.hover_controls[hit.pointer] = None;
|
||||
}
|
||||
if old == self.hover_controls[hit.pointer] {
|
||||
None
|
||||
} else {
|
||||
Some(Haptics {
|
||||
intensity: 0.1,
|
||||
duration: 0.01,
|
||||
frequency: 5.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
|
||||
let idx = if pressed {
|
||||
self.interactive_get_idx(hit.uv)
|
||||
} else {
|
||||
self.pressed_controls[hit.pointer]
|
||||
};
|
||||
|
||||
if let Some(idx) = idx {
|
||||
let c = &mut self.controls[idx];
|
||||
if pressed {
|
||||
if let Some(ref mut f) = c.on_press {
|
||||
self.pressed_controls[hit.pointer] = Some(idx);
|
||||
f(c, &mut self.data.data, app, hit.mode);
|
||||
}
|
||||
} else if let Some(ref mut f) = c.on_release {
|
||||
self.pressed_controls[hit.pointer] = None;
|
||||
f(c, &mut self.data.data, app);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta_y: f32, delta_x: f32) {
|
||||
let idx = self.hover_controls[hit.pointer];
|
||||
|
||||
if let Some(idx) = idx {
|
||||
let c = &mut self.controls[idx];
|
||||
if let Some(ref mut f) = c.on_scroll {
|
||||
f(c, &mut self.data.data, app, delta_y, delta_x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, S> OverlayRenderer for Canvas<D, S> {
|
||||
fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
|
||||
for c in &mut self.controls {
|
||||
if let Some(fun) = c.on_update {
|
||||
fun(c, &mut self.data.data, app);
|
||||
}
|
||||
if c.fg_dirty {
|
||||
self.fore_dirty = true;
|
||||
c.fg_dirty = false;
|
||||
}
|
||||
if c.bg_dirty {
|
||||
self.back_dirty = true;
|
||||
c.bg_dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.back_dirty || self.fore_dirty || self.high_dirty {
|
||||
Ok(ShouldRender::Should)
|
||||
} else {
|
||||
Ok(ShouldRender::Can)
|
||||
}
|
||||
}
|
||||
fn render(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
tgt: Arc<ImageView>,
|
||||
buf: &mut CommandBuffers,
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
self.high_dirty = false;
|
||||
|
||||
let mut cmd_buffer = self
|
||||
.data
|
||||
.graphics
|
||||
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
|
||||
if self.back_dirty {
|
||||
cmd_buffer.begin_rendering(self.view_back.clone())?;
|
||||
for c in &mut self.controls {
|
||||
if let Some(fun) = c.on_render_bg {
|
||||
fun(c, &self.data, app, &mut cmd_buffer)?;
|
||||
}
|
||||
}
|
||||
cmd_buffer.end_rendering()?;
|
||||
self.back_dirty = false;
|
||||
}
|
||||
if self.fore_dirty {
|
||||
cmd_buffer.begin_rendering(self.view_fore.clone())?;
|
||||
for c in &mut self.controls {
|
||||
if let Some(fun) = c.on_render_fg {
|
||||
fun(c, &self.data, app, &mut cmd_buffer)?;
|
||||
}
|
||||
}
|
||||
cmd_buffer.end_rendering()?;
|
||||
self.fore_dirty = false;
|
||||
}
|
||||
|
||||
let set0_fg = self.pipeline_final.uniform_sampler(
|
||||
0,
|
||||
self.view_fore.clone(),
|
||||
app.graphics.texture_filtering,
|
||||
)?;
|
||||
let set0_bg = self.pipeline_final.uniform_sampler(
|
||||
0,
|
||||
self.view_back.clone(),
|
||||
app.graphics.texture_filtering,
|
||||
)?;
|
||||
let set1 = self.pipeline_final.uniform_buffer(1, vec![alpha])?;
|
||||
|
||||
let pass_fore = self
|
||||
.pipeline_final
|
||||
.create_pass_for_target(tgt.clone(), vec![set0_fg, set1.clone()])?;
|
||||
|
||||
let pass_back = self
|
||||
.pipeline_final
|
||||
.create_pass_for_target(tgt.clone(), vec![set0_bg, set1])?;
|
||||
|
||||
cmd_buffer.begin_rendering(tgt)?;
|
||||
cmd_buffer.run_ref(&pass_back)?;
|
||||
|
||||
for (i, c) in self.controls.iter_mut().enumerate() {
|
||||
if let Some(render) = c.on_render_hl {
|
||||
if let Some(test) = c.test_highlight {
|
||||
if let Some(hl_color) = test(c, &mut self.data.data, app) {
|
||||
render(c, &self.data, app, &mut cmd_buffer, hl_color)?;
|
||||
}
|
||||
}
|
||||
if self.hover_controls.contains(&Some(i)) {
|
||||
render(
|
||||
c,
|
||||
&self.data,
|
||||
app,
|
||||
&mut cmd_buffer,
|
||||
Vec4::new(1., 1., 1., 0.3),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mostly static text
|
||||
cmd_buffer.run_ref(&pass_fore)?;
|
||||
|
||||
cmd_buffer.end_rendering()?;
|
||||
buf.push(cmd_buffer.build()?);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
Some(FrameMeta {
|
||||
extent: [self.data.width as _, self.data.height as _, 1],
|
||||
format: self.format,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, S> OverlayBackend for Canvas<D, S> {
|
||||
fn set_renderer(&mut self, _renderer: Box<dyn OverlayRenderer>) {}
|
||||
fn set_interaction(&mut self, _interaction: Box<dyn InteractionHandler>) {}
|
||||
}
|
||||
+51
-23
@@ -5,7 +5,7 @@ use freetype::{bitmap::PixelMode, face::LoadFlag, Face, Library};
|
||||
use idmap::IdMap;
|
||||
use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::Image};
|
||||
|
||||
use crate::graphics::WlxGraphics;
|
||||
use crate::graphics::{WlxGraphics, WlxUploadsBuffer};
|
||||
|
||||
pub struct FontCache {
|
||||
primary_font: Arc<str>,
|
||||
@@ -39,7 +39,7 @@ impl FontCache {
|
||||
let ft = Library::init()?;
|
||||
let fc = FontConfig::default();
|
||||
|
||||
Ok(FontCache {
|
||||
Ok(Self {
|
||||
primary_font,
|
||||
fc,
|
||||
ft,
|
||||
@@ -55,14 +55,15 @@ impl FontCache {
|
||||
) -> anyhow::Result<(f32, f32)> {
|
||||
let sizef = size as f32;
|
||||
|
||||
let height = sizef + ((text.lines().count() as f32) - 1f32) * (sizef * 1.5);
|
||||
let height = ((text.lines().count() as f32) - 1f32).mul_add(sizef * 1.5, sizef);
|
||||
let mut cmd_buffer = None;
|
||||
|
||||
let mut max_w = sizef * 0.33;
|
||||
for line in text.lines() {
|
||||
let w: f32 = line
|
||||
.chars()
|
||||
.filter_map(|c| {
|
||||
self.get_glyph_for_cp(c as usize, size, graphics.clone())
|
||||
self.get_glyph_for_cp(c as usize, size, graphics.clone(), &mut cmd_buffer)
|
||||
.map(|glyph| glyph.advance)
|
||||
.ok()
|
||||
})
|
||||
@@ -72,6 +73,11 @@ impl FontCache {
|
||||
max_w = w;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cmd_buffer) = cmd_buffer {
|
||||
cmd_buffer.build_and_execute_now()?;
|
||||
}
|
||||
|
||||
Ok((max_w, height))
|
||||
}
|
||||
|
||||
@@ -82,11 +88,23 @@ impl FontCache {
|
||||
graphics: Arc<WlxGraphics>,
|
||||
) -> anyhow::Result<Vec<Rc<Glyph>>> {
|
||||
let mut glyphs = Vec::new();
|
||||
let mut cmd_buffer = None;
|
||||
|
||||
for line in text.lines() {
|
||||
for c in line.chars() {
|
||||
glyphs.push(self.get_glyph_for_cp(c as usize, size, graphics.clone())?);
|
||||
glyphs.push(self.get_glyph_for_cp(
|
||||
c as usize,
|
||||
size,
|
||||
graphics.clone(),
|
||||
&mut cmd_buffer,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cmd_buffer) = cmd_buffer {
|
||||
cmd_buffer.build_and_execute_now()?;
|
||||
}
|
||||
|
||||
Ok(glyphs)
|
||||
}
|
||||
|
||||
@@ -124,40 +142,39 @@ impl FontCache {
|
||||
let pattern = pattern.font_match(&mut self.fc);
|
||||
|
||||
if let Some(path) = pattern.filename() {
|
||||
log::debug!(
|
||||
"Loading font: {} {}pt",
|
||||
pattern.name().unwrap_or(path),
|
||||
size
|
||||
);
|
||||
let name = pattern.name().unwrap_or(path);
|
||||
log::debug!("Loading font: {name} {size}pt");
|
||||
|
||||
let font_idx = pattern.face_index().unwrap_or(0);
|
||||
|
||||
let face = match self.ft.new_face(path, font_idx as _) {
|
||||
Ok(face) => face,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load font at {}: {:?}", path, e);
|
||||
log::warn!("Failed to load font at {path}: {e:?}");
|
||||
coll.cp_map.insert(cp, 0);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
match face.set_char_size(size << 6, size << 6, 96, 96) {
|
||||
Ok(_) => {}
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to set font size: {:?}", e);
|
||||
log::warn!("Failed to set font size: {e:?}");
|
||||
coll.cp_map.insert(cp, 0);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let idx = coll.fonts.len();
|
||||
for cp in 0..0xFFFF {
|
||||
for (cp, _) in face.chars() {
|
||||
if coll.cp_map.contains_key(cp) {
|
||||
continue;
|
||||
}
|
||||
let g = face.get_char_index(cp);
|
||||
if g.is_some() {
|
||||
coll.cp_map.insert(cp, idx);
|
||||
}
|
||||
coll.cp_map.insert(cp, idx);
|
||||
}
|
||||
|
||||
if !coll.cp_map.contains_key(cp) {
|
||||
log::warn!("Got font '{name}' for CP 0x{cp:x}, but CP is not present in font!",);
|
||||
coll.cp_map.insert(cp, 0);
|
||||
}
|
||||
|
||||
let zero_glyph = Rc::new(Glyph {
|
||||
@@ -185,11 +202,12 @@ impl FontCache {
|
||||
cp: usize,
|
||||
size: isize,
|
||||
graphics: Arc<WlxGraphics>,
|
||||
cmd_buffer: &mut Option<WlxUploadsBuffer>,
|
||||
) -> anyhow::Result<Rc<Glyph>> {
|
||||
let key = self.get_font_for_cp(cp, size);
|
||||
|
||||
let Some(font) = &mut self.collections[size].fonts.get_mut(key) else {
|
||||
log::warn!("No font found for codepoint: {}", cp);
|
||||
log::warn!("No font found for codepoint: 0x{cp:x}");
|
||||
return Ok(self.collections[size].zero_glyph.clone());
|
||||
};
|
||||
|
||||
@@ -221,9 +239,19 @@ impl FontCache {
|
||||
_ => return Ok(self.collections[size].zero_glyph.clone()),
|
||||
};
|
||||
|
||||
let mut cmd_buffer = graphics.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
let texture = cmd_buffer.texture2d(bmp.width() as _, bmp.rows() as _, format, &buf)?;
|
||||
cmd_buffer.build_and_execute_now()?;
|
||||
if cmd_buffer.is_none() {
|
||||
*cmd_buffer = Some(graphics.create_uploads_command_buffer(
|
||||
graphics.transfer_queue.clone(),
|
||||
CommandBufferUsage::OneTimeSubmit,
|
||||
)?);
|
||||
}
|
||||
|
||||
let texture = cmd_buffer.as_mut().unwrap().texture2d_raw(
|
||||
bmp.width() as _,
|
||||
bmp.rows() as _,
|
||||
format,
|
||||
&buf,
|
||||
)?;
|
||||
|
||||
let g = Glyph {
|
||||
tex: Some(texture),
|
||||
|
||||
+13
-798
@@ -1,37 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::bail;
|
||||
use glam::{Vec2, Vec4};
|
||||
use vulkano::{
|
||||
command_buffer::CommandBufferUsage,
|
||||
format::Format,
|
||||
image::{sampler::Filter, view::ImageView, ImageLayout},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
input::{Haptics, InteractionHandler, PointerHit, PointerMode},
|
||||
overlay::{OverlayBackend, OverlayRenderer},
|
||||
},
|
||||
graphics::{
|
||||
WlxCommandBuffer, WlxGraphics, WlxPass, WlxPipeline, WlxPipelineLegacy, BLEND_ALPHA,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use self::modular::GuiColor;
|
||||
use glam::Vec4;
|
||||
|
||||
pub mod canvas;
|
||||
pub mod font;
|
||||
pub mod modular;
|
||||
|
||||
const RES_DIVIDER: usize = 4;
|
||||
|
||||
struct Rect {
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
}
|
||||
pub type GuiColor = Vec4;
|
||||
pub static FALLBACK_COLOR: LazyLock<GuiColor> = LazyLock::new(|| Vec4::new(1., 0., 1., 1.));
|
||||
|
||||
// Parses a color from a HTML hex string
|
||||
pub fn color_parse(html_hex: &str) -> anyhow::Result<GuiColor> {
|
||||
@@ -42,9 +18,9 @@ pub fn color_parse(html_hex: &str) -> anyhow::Result<GuiColor> {
|
||||
u8::from_str_radix(&html_hex[5..7], 16),
|
||||
) {
|
||||
return Ok(Vec4::new(
|
||||
r as f32 / 255.,
|
||||
g as f32 / 255.,
|
||||
b as f32 / 255.,
|
||||
f32::from(r) / 255.,
|
||||
f32::from(g) / 255.,
|
||||
f32::from(b) / 255.,
|
||||
1.,
|
||||
));
|
||||
}
|
||||
@@ -56,780 +32,19 @@ pub fn color_parse(html_hex: &str) -> anyhow::Result<GuiColor> {
|
||||
u8::from_str_radix(&html_hex[7..9], 16),
|
||||
) {
|
||||
return Ok(Vec4::new(
|
||||
r as f32 / 255.,
|
||||
g as f32 / 255.,
|
||||
b as f32 / 255.,
|
||||
a as f32 / 255.,
|
||||
f32::from(r) / 255.,
|
||||
f32::from(g) / 255.,
|
||||
f32::from(b) / 255.,
|
||||
f32::from(a) / 255.,
|
||||
));
|
||||
}
|
||||
}
|
||||
bail!(
|
||||
anyhow::bail!(
|
||||
"Invalid color string: '{}', must be 7 characters long (e.g. #FF00FF)",
|
||||
&html_hex
|
||||
)
|
||||
}
|
||||
|
||||
pub struct CanvasBuilder<D, S> {
|
||||
canvas: Canvas<D, S>,
|
||||
|
||||
pub fg_color: GuiColor,
|
||||
pub bg_color: GuiColor,
|
||||
pub font_size: isize,
|
||||
}
|
||||
|
||||
impl<D, S> CanvasBuilder<D, S> {
|
||||
pub fn new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
graphics: Arc<WlxGraphics>,
|
||||
format: Format,
|
||||
data: D,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
canvas: Canvas::new(width, height, graphics, format, data)?,
|
||||
bg_color: Vec4::ZERO,
|
||||
fg_color: Vec4::ONE,
|
||||
font_size: 16,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build(self) -> Canvas<D, S> {
|
||||
self.canvas
|
||||
}
|
||||
|
||||
// Creates a panel with bg_color inherited from the canvas
|
||||
pub fn panel(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
bg_color: self.bg_color,
|
||||
on_render_bg: Some(Control::render_rect),
|
||||
..Control::new()
|
||||
});
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
// Creates a label with fg_color, font_size inherited from the canvas
|
||||
pub fn label(&mut self, x: f32, y: f32, w: f32, h: f32, text: Arc<str>) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
text,
|
||||
fg_color: self.fg_color,
|
||||
size: self.font_size,
|
||||
on_render_fg: Some(Control::render_text),
|
||||
..Control::new()
|
||||
});
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
// Creates a label with fg_color, font_size inherited from the canvas
|
||||
#[allow(dead_code)]
|
||||
pub fn label_centered(
|
||||
&mut self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
text: Arc<str>,
|
||||
) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
text,
|
||||
fg_color: self.fg_color,
|
||||
size: self.font_size,
|
||||
on_render_fg: Some(Control::render_text_centered),
|
||||
..Control::new()
|
||||
});
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
// Creates a button with fg_color, bg_color, font_size inherited from the canvas
|
||||
pub fn button(&mut self, x: f32, y: f32, w: f32, h: f32, text: Arc<str>) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
|
||||
self.canvas.interactive_set_idx(x, y, w, h, idx);
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
text,
|
||||
fg_color: self.fg_color,
|
||||
bg_color: self.bg_color,
|
||||
size: self.font_size,
|
||||
on_render_bg: Some(Control::render_rect),
|
||||
on_render_fg: Some(Control::render_text_centered),
|
||||
on_render_hl: Some(Control::render_highlight),
|
||||
..Control::new()
|
||||
});
|
||||
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
|
||||
pub fn key_button(
|
||||
&mut self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
cap_type: KeyCapType,
|
||||
label: &[String],
|
||||
) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
self.canvas.interactive_set_idx(x, y, w, h, idx);
|
||||
|
||||
self.canvas.controls.push(Control {
|
||||
rect: Rect { x, y, w, h },
|
||||
bg_color: self.bg_color,
|
||||
on_render_bg: Some(Control::render_rect),
|
||||
on_render_hl: Some(Control::render_highlight),
|
||||
..Control::new()
|
||||
});
|
||||
|
||||
let renders = match cap_type {
|
||||
KeyCapType::Regular => {
|
||||
let render: ControlRenderer<D, S> = Control::render_text_centered;
|
||||
let rect = Rect {
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h: h - self.font_size as f32,
|
||||
};
|
||||
vec![(render, rect, 1f32)]
|
||||
}
|
||||
KeyCapType::RegularAltGr => {
|
||||
let render: ControlRenderer<D, S> = Control::render_text;
|
||||
let rect0 = Rect {
|
||||
x: x + 12.,
|
||||
y: y + (self.font_size as f32) + 12.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
let rect1 = Rect {
|
||||
x: x + w * 0.5 + 12.,
|
||||
y: y + h - (self.font_size as f32) + 8.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
vec![(render, rect0, 1.0), (render, rect1, 0.8)]
|
||||
}
|
||||
KeyCapType::Reversed => {
|
||||
let render: ControlRenderer<D, S> = Control::render_text_centered;
|
||||
let rect0 = Rect {
|
||||
x,
|
||||
y: y + 2.0,
|
||||
w,
|
||||
h: h * 0.5,
|
||||
};
|
||||
let rect1 = Rect {
|
||||
x,
|
||||
y: y + h * 0.5 + 2.0,
|
||||
w,
|
||||
h: h * 0.5,
|
||||
};
|
||||
vec![(render, rect1, 1.0), (render, rect0, 0.8)]
|
||||
}
|
||||
KeyCapType::ReversedAltGr => {
|
||||
let render: ControlRenderer<D, S> = Control::render_text;
|
||||
let rect0 = Rect {
|
||||
x: x + 12.,
|
||||
y: y + (self.font_size as f32) + 8.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
let rect1 = Rect {
|
||||
x: x + 12.,
|
||||
y: y + h - (self.font_size as f32) + 4.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
let rect2 = Rect {
|
||||
x: x + w * 0.5 + 8.,
|
||||
y: y + h - (self.font_size as f32) + 4.,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
vec![
|
||||
(render, rect1, 1.0),
|
||||
(render, rect0, 0.8),
|
||||
(render, rect2, 0.8),
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
for (idx, (render, rect, alpha)) in renders.into_iter().enumerate() {
|
||||
if idx >= label.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.canvas.controls.push(Control {
|
||||
rect,
|
||||
text: Arc::from(label[idx].as_str()),
|
||||
fg_color: self.fg_color * alpha,
|
||||
size: self.font_size,
|
||||
on_render_fg: Some(render),
|
||||
..Control::new()
|
||||
});
|
||||
}
|
||||
|
||||
&mut self.canvas.controls[idx]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CanvasData<D> {
|
||||
pub data: D,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
|
||||
graphics: Arc<WlxGraphics>,
|
||||
|
||||
pipeline_bg_color: Arc<WlxPipeline<WlxPipelineLegacy>>,
|
||||
pipeline_fg_glyph: Arc<WlxPipeline<WlxPipelineLegacy>>,
|
||||
pipeline_final: Arc<WlxPipeline<WlxPipelineLegacy>>,
|
||||
}
|
||||
|
||||
pub struct Canvas<D, S> {
|
||||
controls: Vec<Control<D, S>>,
|
||||
canvas: CanvasData<D>,
|
||||
|
||||
hover_controls: [Option<usize>; 2],
|
||||
pressed_controls: [Option<usize>; 2],
|
||||
|
||||
interact_map: Vec<Option<u16>>,
|
||||
interact_stride: usize,
|
||||
interact_rows: usize,
|
||||
|
||||
view_final: Arc<ImageView>,
|
||||
|
||||
pass_fg: WlxPass<WlxPipelineLegacy>,
|
||||
pass_bg: WlxPass<WlxPipelineLegacy>,
|
||||
}
|
||||
|
||||
impl<D, S> Canvas<D, S> {
|
||||
fn new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
graphics: Arc<WlxGraphics>,
|
||||
format: Format,
|
||||
data: D,
|
||||
) -> anyhow::Result<Self> {
|
||||
let tex_fg = graphics.render_texture(width as _, height as _, format)?;
|
||||
let tex_bg = graphics.render_texture(width as _, height as _, format)?;
|
||||
let tex_final = graphics.render_texture(width as _, height as _, format)?;
|
||||
|
||||
let view_fg = ImageView::new_default(tex_fg.clone())?;
|
||||
let view_bg = ImageView::new_default(tex_bg.clone())?;
|
||||
let view_final = ImageView::new_default(tex_final.clone())?;
|
||||
|
||||
let Ok(shaders) = graphics.shared_shaders.read() else {
|
||||
bail!("Failed to lock shared shaders for reading");
|
||||
};
|
||||
|
||||
let pipeline_bg_color = graphics.create_pipeline(
|
||||
view_bg.clone(),
|
||||
shaders.get("vert_common").unwrap().clone(), // want panic
|
||||
shaders.get("frag_color").unwrap().clone(), // want panic
|
||||
format,
|
||||
Some(BLEND_ALPHA),
|
||||
)?;
|
||||
|
||||
let pipeline_fg_glyph = graphics.create_pipeline(
|
||||
view_fg.clone(),
|
||||
shaders.get("vert_common").unwrap().clone(), // want panic
|
||||
shaders.get("frag_glyph").unwrap().clone(), // want panic
|
||||
format,
|
||||
Some(BLEND_ALPHA),
|
||||
)?;
|
||||
|
||||
let vertex_buffer =
|
||||
graphics.upload_verts(width as _, height as _, 0., 0., width as _, height as _)?;
|
||||
|
||||
let pipeline_final = graphics.create_pipeline_with_layouts(
|
||||
view_final.clone(),
|
||||
shaders.get("vert_common").unwrap().clone(), // want panic
|
||||
shaders.get("frag_sprite").unwrap().clone(), // want panic
|
||||
format,
|
||||
Some(BLEND_ALPHA),
|
||||
ImageLayout::TransferSrcOptimal,
|
||||
ImageLayout::TransferSrcOptimal,
|
||||
)?;
|
||||
|
||||
let set_fg = pipeline_final.uniform_sampler(0, view_fg.clone(), Filter::Linear)?;
|
||||
let set_bg = pipeline_final.uniform_sampler(0, view_bg.clone(), Filter::Linear)?;
|
||||
let pass_fg = pipeline_final.create_pass(
|
||||
[width as _, height as _],
|
||||
vertex_buffer.clone(),
|
||||
graphics.quad_indices.clone(),
|
||||
vec![set_fg],
|
||||
)?;
|
||||
let pass_bg = pipeline_final.create_pass(
|
||||
[width as _, height as _],
|
||||
vertex_buffer.clone(),
|
||||
graphics.quad_indices.clone(),
|
||||
vec![set_bg],
|
||||
)?;
|
||||
|
||||
let stride = width / RES_DIVIDER;
|
||||
let rows = height / RES_DIVIDER;
|
||||
|
||||
Ok(Self {
|
||||
canvas: CanvasData {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
graphics: graphics.clone(),
|
||||
pipeline_bg_color,
|
||||
pipeline_fg_glyph,
|
||||
pipeline_final,
|
||||
},
|
||||
controls: Vec::new(),
|
||||
hover_controls: [None, None],
|
||||
pressed_controls: [None, None],
|
||||
interact_map: vec![None; stride * rows],
|
||||
interact_stride: stride,
|
||||
interact_rows: rows,
|
||||
view_final,
|
||||
pass_fg,
|
||||
pass_bg,
|
||||
})
|
||||
}
|
||||
|
||||
fn interactive_set_idx(&mut self, x: f32, y: f32, w: f32, h: f32, idx: usize) {
|
||||
let (x, y, w, h) = (x as usize, y as usize, w as usize, h as usize);
|
||||
|
||||
let x_min = (x / RES_DIVIDER).max(0);
|
||||
let y_min = (y / RES_DIVIDER).max(0);
|
||||
let x_max = (x_min + (w / RES_DIVIDER)).min(self.interact_stride - 1);
|
||||
let y_max = (y_min + (h / RES_DIVIDER)).min(self.interact_rows - 1);
|
||||
|
||||
for y in y_min..y_max {
|
||||
for x in x_min..x_max {
|
||||
self.interact_map[y * self.interact_stride + x] = Some(idx as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn interactive_get_idx(&self, uv: Vec2) -> Option<usize> {
|
||||
let x = (uv.x * self.canvas.width as f32) as usize;
|
||||
let y = (uv.y * self.canvas.height as f32) as usize;
|
||||
let x = (x / RES_DIVIDER).max(0).min(self.interact_stride - 1);
|
||||
let y = (y / RES_DIVIDER).max(0).min(self.interact_rows - 1);
|
||||
self.interact_map[y * self.interact_stride + x].map(|x| x as usize)
|
||||
}
|
||||
|
||||
fn render_bg(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
let mut cmd_buffer = self
|
||||
.canvas
|
||||
.graphics
|
||||
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
cmd_buffer.begin_render_pass(&self.canvas.pipeline_bg_color)?;
|
||||
for c in self.controls.iter_mut() {
|
||||
if let Some(fun) = c.on_render_bg {
|
||||
fun(c, &self.canvas, app, &mut cmd_buffer)?;
|
||||
}
|
||||
}
|
||||
cmd_buffer.end_render_pass()?;
|
||||
cmd_buffer.build_and_execute_now()
|
||||
}
|
||||
|
||||
fn render_fg(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
let mut cmd_buffer = self
|
||||
.canvas
|
||||
.graphics
|
||||
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
cmd_buffer.begin_render_pass(&self.canvas.pipeline_fg_glyph)?;
|
||||
for c in self.controls.iter_mut() {
|
||||
if let Some(fun) = c.on_render_fg {
|
||||
fun(c, &self.canvas, app, &mut cmd_buffer)?;
|
||||
}
|
||||
}
|
||||
cmd_buffer.end_render_pass()?;
|
||||
cmd_buffer.build_and_execute_now()
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, S> InteractionHandler for Canvas<D, S> {
|
||||
fn on_left(&mut self, _app: &mut AppState, pointer: usize) {
|
||||
self.hover_controls[pointer] = None;
|
||||
}
|
||||
fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
|
||||
let old = self.hover_controls[hit.pointer];
|
||||
if let Some(i) = self.interactive_get_idx(hit.uv) {
|
||||
self.hover_controls[hit.pointer] = Some(i);
|
||||
} else {
|
||||
self.hover_controls[hit.pointer] = None;
|
||||
}
|
||||
if old != self.hover_controls[hit.pointer] {
|
||||
Some(Haptics {
|
||||
intensity: 0.1,
|
||||
duration: 0.01,
|
||||
frequency: 5.0,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
|
||||
let idx = if pressed {
|
||||
self.interactive_get_idx(hit.uv)
|
||||
} else {
|
||||
self.pressed_controls[hit.pointer]
|
||||
};
|
||||
|
||||
if let Some(idx) = idx {
|
||||
let c = &mut self.controls[idx];
|
||||
if pressed {
|
||||
if let Some(ref mut f) = c.on_press {
|
||||
self.pressed_controls[hit.pointer] = Some(idx);
|
||||
f(c, &mut self.canvas.data, app, hit.mode);
|
||||
}
|
||||
} else if let Some(ref mut f) = c.on_release {
|
||||
self.pressed_controls[hit.pointer] = None;
|
||||
f(c, &mut self.canvas.data, app);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta: f32) {
|
||||
let idx = self.hover_controls[hit.pointer];
|
||||
|
||||
if let Some(idx) = idx {
|
||||
let c = &mut self.controls[idx];
|
||||
if let Some(ref mut f) = c.on_scroll {
|
||||
f(c, &mut self.canvas.data, app, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, S> OverlayRenderer for Canvas<D, S> {
|
||||
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.render_bg(app)?;
|
||||
self.render_fg(app)
|
||||
}
|
||||
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
let mut dirty = false;
|
||||
|
||||
for c in self.controls.iter_mut() {
|
||||
if let Some(fun) = c.on_update {
|
||||
fun(c, &mut self.canvas.data, app);
|
||||
}
|
||||
if c.dirty {
|
||||
dirty = true;
|
||||
c.dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
if dirty {
|
||||
self.render_bg(app)?;
|
||||
self.render_fg(app)?;
|
||||
}
|
||||
|
||||
/*
|
||||
let image = self.view_final.image().clone();
|
||||
if self.first_render {
|
||||
self.first_render = false;
|
||||
} else {
|
||||
self.canvas
|
||||
.graphics
|
||||
.transition_layout(
|
||||
image.clone(),
|
||||
ImageLayout::TransferSrcOptimal,
|
||||
ImageLayout::ColorAttachmentOptimal,
|
||||
)
|
||||
.wait(None)
|
||||
.unwrap();
|
||||
}
|
||||
*/
|
||||
|
||||
let mut cmd_buffer = self
|
||||
.canvas
|
||||
.graphics
|
||||
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
cmd_buffer.begin_render_pass(&self.canvas.pipeline_final)?;
|
||||
|
||||
// static background
|
||||
cmd_buffer.run_ref(&self.pass_bg)?;
|
||||
|
||||
for (i, c) in self.controls.iter_mut().enumerate() {
|
||||
if let Some(render) = c.on_render_hl {
|
||||
if let Some(test) = c.test_highlight {
|
||||
if let Some(hl_color) = test(c, &mut self.canvas.data, app) {
|
||||
render(c, &self.canvas, app, &mut cmd_buffer, hl_color)?;
|
||||
}
|
||||
}
|
||||
if self.hover_controls.contains(&Some(i)) {
|
||||
render(
|
||||
c,
|
||||
&self.canvas,
|
||||
app,
|
||||
&mut cmd_buffer,
|
||||
Vec4::new(1., 1., 1., 0.3),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mostly static text
|
||||
cmd_buffer.run_ref(&self.pass_fg)?;
|
||||
|
||||
cmd_buffer.end_render_pass()?;
|
||||
cmd_buffer.build_and_execute_now()
|
||||
|
||||
/*
|
||||
self.canvas
|
||||
.graphics
|
||||
.transition_layout(
|
||||
image,
|
||||
ImageLayout::ColorAttachmentOptimal,
|
||||
ImageLayout::TransferSrcOptimal,
|
||||
)
|
||||
.wait(None)
|
||||
.unwrap();
|
||||
*/
|
||||
}
|
||||
fn view(&mut self) -> Option<Arc<ImageView>> {
|
||||
Some(self.view_final.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, S> OverlayBackend for Canvas<D, S> {
|
||||
fn set_renderer(&mut self, _renderer: Box<dyn OverlayRenderer>) {}
|
||||
fn set_interaction(&mut self, _interaction: Box<dyn InteractionHandler>) {}
|
||||
}
|
||||
|
||||
pub type ControlRenderer<D, S> =
|
||||
fn(&Control<D, S>, &CanvasData<D>, &mut AppState, &mut WlxCommandBuffer) -> anyhow::Result<()>;
|
||||
|
||||
pub type ControlRendererHl<D, S> = fn(
|
||||
&Control<D, S>,
|
||||
&CanvasData<D>,
|
||||
&mut AppState,
|
||||
&mut WlxCommandBuffer,
|
||||
Vec4,
|
||||
) -> anyhow::Result<()>;
|
||||
|
||||
pub struct Control<D, S> {
|
||||
pub state: Option<S>,
|
||||
rect: Rect,
|
||||
fg_color: GuiColor,
|
||||
bg_color: GuiColor,
|
||||
text: Arc<str>,
|
||||
size: isize,
|
||||
dirty: bool,
|
||||
|
||||
pub on_update: Option<fn(&mut Self, &mut D, &mut AppState)>,
|
||||
pub on_press: Option<fn(&mut Self, &mut D, &mut AppState, PointerMode)>,
|
||||
pub on_release: Option<fn(&mut Self, &mut D, &mut AppState)>,
|
||||
pub on_scroll: Option<fn(&mut Self, &mut D, &mut AppState, f32)>,
|
||||
pub test_highlight: Option<fn(&Self, &mut D, &mut AppState) -> Option<Vec4>>,
|
||||
|
||||
on_render_bg: Option<ControlRenderer<D, S>>,
|
||||
on_render_hl: Option<ControlRendererHl<D, S>>,
|
||||
on_render_fg: Option<ControlRenderer<D, S>>,
|
||||
}
|
||||
|
||||
impl<D, S> Control<D, S> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
rect: Rect {
|
||||
x: 0.,
|
||||
y: 0.,
|
||||
w: 0.,
|
||||
h: 0.,
|
||||
},
|
||||
fg_color: Vec4::ONE,
|
||||
bg_color: Vec4::ZERO,
|
||||
text: Arc::from(""),
|
||||
dirty: true,
|
||||
size: 24,
|
||||
state: None,
|
||||
on_update: None,
|
||||
on_render_bg: None,
|
||||
on_render_hl: None,
|
||||
on_render_fg: None,
|
||||
test_highlight: None,
|
||||
on_press: None,
|
||||
on_release: None,
|
||||
on_scroll: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
if *self.text == *text {
|
||||
return;
|
||||
}
|
||||
self.text = text.into();
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set_fg_color(&mut self, color: GuiColor) {
|
||||
if self.fg_color == color {
|
||||
return;
|
||||
}
|
||||
self.fg_color = color;
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
fn render_rect(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
_: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
) -> anyhow::Result<()> {
|
||||
let pass = {
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
self.rect.x,
|
||||
self.rect.y,
|
||||
self.rect.w,
|
||||
self.rect.h,
|
||||
)?;
|
||||
let set0 = canvas
|
||||
.pipeline_bg_color
|
||||
.uniform_buffer(0, self.bg_color.to_array().to_vec())?;
|
||||
canvas.pipeline_bg_color.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer,
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0],
|
||||
)?
|
||||
};
|
||||
|
||||
cmd_buffer.run_ref(&pass)
|
||||
}
|
||||
|
||||
fn render_highlight(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
_: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
color: GuiColor,
|
||||
) -> anyhow::Result<()> {
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
self.rect.x,
|
||||
self.rect.y,
|
||||
self.rect.w,
|
||||
self.rect.h,
|
||||
)?;
|
||||
|
||||
let set0 = canvas
|
||||
.pipeline_bg_color
|
||||
.uniform_buffer(0, color.to_array().to_vec())?;
|
||||
|
||||
let pass = canvas.pipeline_bg_color.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer.clone(),
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0],
|
||||
)?;
|
||||
|
||||
cmd_buffer.run_ref(&pass)
|
||||
}
|
||||
|
||||
fn render_text(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
app: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut cur_y = self.rect.y;
|
||||
for line in self.text.lines() {
|
||||
let mut cur_x = self.rect.x;
|
||||
for glyph in app
|
||||
.fc
|
||||
.get_glyphs(line, self.size, canvas.graphics.clone())?
|
||||
{
|
||||
if let Some(tex) = glyph.tex.clone() {
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
cur_x + glyph.left,
|
||||
cur_y - glyph.top,
|
||||
glyph.width,
|
||||
glyph.height,
|
||||
)?;
|
||||
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
|
||||
0,
|
||||
ImageView::new_default(tex)?,
|
||||
Filter::Linear,
|
||||
)?;
|
||||
let set1 = canvas
|
||||
.pipeline_fg_glyph
|
||||
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
|
||||
let pass = canvas.pipeline_fg_glyph.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer,
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0, set1],
|
||||
)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
}
|
||||
cur_x += glyph.advance;
|
||||
}
|
||||
cur_y += (self.size as f32) * 1.5;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn render_text_centered(
|
||||
&self,
|
||||
canvas: &CanvasData<D>,
|
||||
app: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
) -> anyhow::Result<()> {
|
||||
let (w, h) = app
|
||||
.fc
|
||||
.get_text_size(&self.text, self.size, canvas.graphics.clone())?;
|
||||
|
||||
let mut cur_y = self.rect.y + (self.rect.h) - (h * 0.5) - (self.size as f32 * 0.25);
|
||||
for line in self.text.lines() {
|
||||
let mut cur_x = self.rect.x + (self.rect.w * 0.5) - (w * 0.5);
|
||||
for glyph in app
|
||||
.fc
|
||||
.get_glyphs(line, self.size, canvas.graphics.clone())?
|
||||
{
|
||||
if let Some(tex) = glyph.tex.clone() {
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
canvas.height as _,
|
||||
cur_x + glyph.left,
|
||||
cur_y - glyph.top,
|
||||
glyph.width,
|
||||
glyph.height,
|
||||
)?;
|
||||
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
|
||||
0,
|
||||
ImageView::new_default(tex)?,
|
||||
Filter::Linear,
|
||||
)?;
|
||||
let set1 = canvas
|
||||
.pipeline_fg_glyph
|
||||
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
|
||||
let pass = canvas.pipeline_fg_glyph.create_pass(
|
||||
[canvas.width as _, canvas.height as _],
|
||||
vertex_buffer,
|
||||
canvas.graphics.quad_indices.clone(),
|
||||
vec![set0, set1],
|
||||
)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
}
|
||||
cur_x += glyph.advance;
|
||||
}
|
||||
cur_y += (self.size as f32) * 1.5;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum KeyCapType {
|
||||
/// Label is in center of keycap
|
||||
Regular,
|
||||
|
||||
+170
-27
@@ -17,15 +17,28 @@ use crate::{
|
||||
task::{ColorChannel, SystemTask, TaskType},
|
||||
},
|
||||
config::{save_layout, save_settings, AStrSetExt},
|
||||
hid::VirtualKey,
|
||||
overlays::{
|
||||
toast::{Toast, ToastTopic},
|
||||
toast::{error_toast, Toast, ToastTopic},
|
||||
watch::WATCH_NAME,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
#[cfg(any(not(feature = "wayvr"), not(feature = "osc")))]
|
||||
use crate::overlays::toast::error_toast_str;
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
use rosc::OscType;
|
||||
|
||||
use super::{ExecArgs, ModularControl, ModularData};
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub enum PressRelease {
|
||||
Release,
|
||||
Press,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
pub enum ViewAngleKind {
|
||||
/// The cosine of the angle at which the watch becomes fully transparent
|
||||
@@ -47,6 +60,7 @@ pub enum HighlightTest {
|
||||
AutoRealign,
|
||||
NotificationSounds,
|
||||
Notifications,
|
||||
RorateLock,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -55,6 +69,7 @@ pub enum SystemAction {
|
||||
ToggleAutoRealign,
|
||||
ToggleNotificationSounds,
|
||||
ToggleNotifications,
|
||||
ToggleRotateLock,
|
||||
PlayspaceResetOffset,
|
||||
PlayspaceFixFloor,
|
||||
RecalculateExtent,
|
||||
@@ -107,6 +122,26 @@ pub enum WindowAction {
|
||||
Destroy,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub enum WayVRDisplayClickAction {
|
||||
ToggleVisibility,
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[allow(dead_code)] // in case if WayVR feature is disabled
|
||||
pub enum WayVRAction {
|
||||
AppClick {
|
||||
catalog_name: Arc<str>,
|
||||
app_name: Arc<str>,
|
||||
},
|
||||
DisplayClick {
|
||||
display_name: Arc<str>,
|
||||
action: WayVRDisplayClickAction,
|
||||
},
|
||||
ToggleDashboard,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ButtonAction {
|
||||
@@ -114,6 +149,10 @@ pub enum ButtonAction {
|
||||
command: ExecArgs,
|
||||
toast: Option<Arc<str>>,
|
||||
},
|
||||
VirtualKey {
|
||||
keycode: VirtualKey,
|
||||
action: PressRelease,
|
||||
},
|
||||
Watch {
|
||||
action: WatchAction,
|
||||
},
|
||||
@@ -121,6 +160,10 @@ pub enum ButtonAction {
|
||||
target: OverlaySelector,
|
||||
action: OverlayAction,
|
||||
},
|
||||
// Ignored if "wayvr" feature is not enabled
|
||||
WayVR {
|
||||
action: WayVRAction,
|
||||
},
|
||||
Window {
|
||||
target: Arc<str>,
|
||||
action: WindowAction,
|
||||
@@ -134,9 +177,32 @@ pub enum ButtonAction {
|
||||
channel: ColorChannel,
|
||||
delta: f32,
|
||||
},
|
||||
DragMultiplier {
|
||||
delta: f32,
|
||||
},
|
||||
System {
|
||||
action: SystemAction,
|
||||
},
|
||||
SendOscValue {
|
||||
parameter: Arc<str>,
|
||||
values: Option<Vec<OscValue>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
#[cfg(feature = "osc")]
|
||||
pub enum OscValue {
|
||||
Int { value: i32 },
|
||||
Float { value: f32 },
|
||||
String { value: String },
|
||||
Bool { value: bool },
|
||||
}
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
#[cfg(not(feature = "osc"))]
|
||||
pub enum OscValue {
|
||||
None,
|
||||
}
|
||||
|
||||
pub(super) struct PressData {
|
||||
@@ -260,13 +326,19 @@ fn modular_button_up(button: &mut ModularControl, _: &mut (), app: &mut AppState
|
||||
}
|
||||
}
|
||||
|
||||
fn modular_button_scroll(button: &mut ModularControl, _: &mut (), app: &mut AppState, delta: f32) {
|
||||
fn modular_button_scroll(
|
||||
button: &mut ModularControl,
|
||||
_: &mut (),
|
||||
app: &mut AppState,
|
||||
delta_y: f32,
|
||||
_delta_x: f32,
|
||||
) {
|
||||
// want panic
|
||||
let ModularData::Button(data) = button.state.as_mut().unwrap() else {
|
||||
panic!("modular_button_scroll: button state is not Button");
|
||||
};
|
||||
|
||||
let actions = if delta < 0.0 {
|
||||
let actions = if delta_y < 0.0 {
|
||||
data.scroll_down.as_ref()
|
||||
} else {
|
||||
data.scroll_up.as_ref()
|
||||
@@ -295,6 +367,7 @@ fn modular_button_highlight(
|
||||
HighlightTest::AutoRealign => app.session.config.realign_on_showhide,
|
||||
HighlightTest::NotificationSounds => app.session.config.notifications_sound_enabled,
|
||||
HighlightTest::Notifications => app.session.config.notifications_enabled,
|
||||
HighlightTest::RorateLock => !app.session.config.space_rotate_unlocked,
|
||||
};
|
||||
|
||||
if lit {
|
||||
@@ -306,10 +379,24 @@ fn modular_button_highlight(
|
||||
|
||||
fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppState) {
|
||||
match action {
|
||||
ButtonAction::Exec { command, toast } => run_exec(command, toast, press, app),
|
||||
ButtonAction::Exec { command, toast } => run_exec(command, toast.clone(), press, app),
|
||||
ButtonAction::Watch { action } => run_watch(action, app),
|
||||
ButtonAction::Overlay { target, action } => run_overlay(target, action, app),
|
||||
ButtonAction::Window { target, action } => run_window(target, action, app),
|
||||
ButtonAction::WayVR { action } => {
|
||||
#[cfg(feature = "wayvr")]
|
||||
{
|
||||
app.tasks.enqueue(TaskType::WayVR(action.clone()));
|
||||
}
|
||||
#[cfg(not(feature = "wayvr"))]
|
||||
{
|
||||
let _ = &action;
|
||||
error_toast_str(app, "WayVR feature is not enabled");
|
||||
}
|
||||
}
|
||||
ButtonAction::VirtualKey { keycode, action } => app
|
||||
.hid_provider
|
||||
.send_key(*keycode, matches!(*action, PressRelease::Press)),
|
||||
ButtonAction::Toast {
|
||||
message,
|
||||
body,
|
||||
@@ -330,11 +417,43 @@ fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppStat
|
||||
.enqueue(TaskType::System(SystemTask::ColorGain(channel, delta)));
|
||||
}
|
||||
ButtonAction::System { action } => run_system(action, app),
|
||||
ButtonAction::DragMultiplier { delta } => {
|
||||
app.session.config.space_drag_multiplier += delta;
|
||||
}
|
||||
ButtonAction::SendOscValue { parameter, values } => {
|
||||
#[cfg(feature = "osc")]
|
||||
if let Some(ref mut sender) = app.osc_sender {
|
||||
// convert OscValue to OscType
|
||||
let mut converted: Vec<OscType> = Vec::new();
|
||||
|
||||
for value in values.as_ref().unwrap() {
|
||||
let converted_value = match value {
|
||||
OscValue::Bool { value } => OscType::Bool(*value),
|
||||
OscValue::Int { value } => OscType::Int(*value),
|
||||
OscValue::Float { value } => OscType::Float(*value),
|
||||
OscValue::String { value } => OscType::String(value.to_string()),
|
||||
};
|
||||
|
||||
converted.push(converted_value);
|
||||
}
|
||||
|
||||
let _ = sender.send_single_param(parameter.to_string(), converted);
|
||||
audio_thump(app); // play sound for feedback
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "osc"))]
|
||||
{
|
||||
let _ = ¶meter;
|
||||
let _ = &values;
|
||||
error_toast_str(app, "OSC feature is not enabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ENABLED_DISABLED: [&str; 2] = ["enabled", "disabled"];
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
match action {
|
||||
SystemAction::ToggleAllowSliding => {
|
||||
@@ -343,7 +462,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
ToastTopic::System,
|
||||
format!(
|
||||
"Sliding is {}.",
|
||||
ENABLED_DISABLED[app.session.config.allow_sliding as usize]
|
||||
ENABLED_DISABLED[usize::from(app.session.config.allow_sliding)]
|
||||
)
|
||||
.into(),
|
||||
"".into(),
|
||||
@@ -356,7 +475,20 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
ToastTopic::System,
|
||||
format!(
|
||||
"Auto realign is {}.",
|
||||
ENABLED_DISABLED[app.session.config.realign_on_showhide as usize]
|
||||
ENABLED_DISABLED[usize::from(app.session.config.realign_on_showhide)]
|
||||
)
|
||||
.into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
}
|
||||
SystemAction::ToggleRotateLock => {
|
||||
app.session.config.space_rotate_unlocked = !app.session.config.space_rotate_unlocked;
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!(
|
||||
"Space rotate axis lock now {}.",
|
||||
ENABLED_DISABLED[usize::from(!app.session.config.space_rotate_unlocked)]
|
||||
)
|
||||
.into(),
|
||||
"".into(),
|
||||
@@ -375,7 +507,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
let display = 5 - i;
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("Fixing floor in {}", display).into(),
|
||||
format!("Fixing floor in {display}").into(),
|
||||
"Place either controller on the floor.".into(),
|
||||
)
|
||||
.with_timeout(1.0)
|
||||
@@ -393,7 +525,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
ToastTopic::System,
|
||||
format!(
|
||||
"Notifications are {}.",
|
||||
ENABLED_DISABLED[app.session.config.notifications_enabled as usize]
|
||||
ENABLED_DISABLED[usize::from(app.session.config.notifications_enabled)]
|
||||
)
|
||||
.into(),
|
||||
"".into(),
|
||||
@@ -407,7 +539,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
ToastTopic::System,
|
||||
format!(
|
||||
"Notification sounds are {}.",
|
||||
ENABLED_DISABLED[app.session.config.notifications_sound_enabled as usize]
|
||||
ENABLED_DISABLED[usize::from(app.session.config.notifications_sound_enabled)]
|
||||
)
|
||||
.into(),
|
||||
"".into(),
|
||||
@@ -416,23 +548,23 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
}
|
||||
SystemAction::PersistConfig => {
|
||||
if let Err(e) = save_settings(&app.session.config) {
|
||||
log::error!("Failed to save config: {:?}", e);
|
||||
error_toast(app, "Failed to save config", e);
|
||||
}
|
||||
}
|
||||
SystemAction::PersistLayout => {
|
||||
if let Err(e) = save_layout(&app.session.config) {
|
||||
log::error!("Failed to save layout: {:?}", e);
|
||||
error_toast(app, "Failed to save layout", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_exec(args: &ExecArgs, toast: &Option<Arc<str>>, press: &mut PressData, app: &mut AppState) {
|
||||
fn run_exec(args: &ExecArgs, toast: Option<Arc<str>>, press: &mut PressData, app: &mut AppState) {
|
||||
if let Some(proc) = press.child.as_mut() {
|
||||
match proc.try_wait() {
|
||||
Ok(Some(code)) => {
|
||||
if !code.success() {
|
||||
log::error!("Child process exited with code: {}", code);
|
||||
error_toast(app, "Child process exited with code", code);
|
||||
}
|
||||
press.child = None;
|
||||
}
|
||||
@@ -442,11 +574,14 @@ fn run_exec(args: &ExecArgs, toast: &Option<Arc<str>>, press: &mut PressData, ap
|
||||
}
|
||||
Err(e) => {
|
||||
press.child = None;
|
||||
log::error!("Error checking child process: {:?}", e);
|
||||
error_toast(app, "Error checking child process", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
let args = args.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
|
||||
let args = args
|
||||
.iter()
|
||||
.map(std::convert::AsRef::as_ref)
|
||||
.collect::<Vec<&str>>();
|
||||
match process::Command::new(args[0]).args(&args[1..]).spawn() {
|
||||
Ok(proc) => {
|
||||
press.child = Some(proc);
|
||||
@@ -455,11 +590,12 @@ fn run_exec(args: &ExecArgs, toast: &Option<Arc<str>>, press: &mut PressData, ap
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to spawn process {:?}: {:?}", args, e);
|
||||
error_toast(app, &format!("Failed to spawn process {args:?}"), e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn run_watch(data: &WatchAction, app: &mut AppState) {
|
||||
match data {
|
||||
WatchAction::Hide => {
|
||||
@@ -490,7 +626,7 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
OverlaySelector::Name(WATCH_NAME.into()),
|
||||
Box::new(|app, o| {
|
||||
if let RelativeTo::Hand(0) = o.relative_to {
|
||||
if matches!(o.relative_to, RelativeTo::Hand(0)) {
|
||||
o.relative_to = RelativeTo::Hand(1);
|
||||
o.spawn_rotation = app.session.config.watch_rot
|
||||
* Quat::from_rotation_x(PI)
|
||||
@@ -570,6 +706,7 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppState) {
|
||||
match action {
|
||||
OverlayAction::Reset => {
|
||||
@@ -605,8 +742,10 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
|
||||
|
||||
if state_dirty {
|
||||
match save_layout(&app.session.config) {
|
||||
Ok(_) => log::debug!("Saved state"),
|
||||
Err(e) => log::error!("Failed to save state: {:?}", e),
|
||||
Ok(()) => log::debug!("Saved state"),
|
||||
Err(e) => {
|
||||
error_toast(app, "Failed to save state", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -619,17 +758,17 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
|
||||
o.recenter = !o.recenter;
|
||||
o.grabbable = o.recenter;
|
||||
o.show_hide = o.recenter;
|
||||
if !o.recenter {
|
||||
if o.recenter {
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("{} is now locked in place!", o.name).into(),
|
||||
format!("{} is now unlocked!", o.name).into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
} else {
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("{} is now unlocked!", o.name).into(),
|
||||
format!("{} is now locked in place!", o.name).into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
@@ -643,17 +782,17 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
|
||||
overlay.clone(),
|
||||
Box::new(|app, o| {
|
||||
o.interactable = !o.interactable;
|
||||
if !o.interactable {
|
||||
if o.interactable {
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("{} is now non-interactable!", o.name).into(),
|
||||
format!("{} is now interactable!", o.name).into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
} else {
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("{} is now interactable!", o.name).into(),
|
||||
format!("{} is now non-interactable!", o.name).into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
@@ -694,7 +833,11 @@ fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
|
||||
)
|
||||
.with_sound(true)
|
||||
.submit(app);
|
||||
crate::overlays::mirror::new_mirror(name.clone(), false, &app.session)
|
||||
Some(crate::overlays::mirror::new_mirror(
|
||||
name,
|
||||
false,
|
||||
&app.session,
|
||||
))
|
||||
}
|
||||
}),
|
||||
));
|
||||
|
||||
+81
-29
@@ -9,12 +9,23 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::{gui::modular::FALLBACK_COLOR, state::AppState};
|
||||
use crate::{
|
||||
gui::modular::FALLBACK_COLOR,
|
||||
overlays::toast::{error_toast, error_toast_str},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{color_parse_or_default, ExecArgs, GuiColor, ModularControl, ModularData};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum TimezoneDef {
|
||||
Idx(usize),
|
||||
Str(Arc<str>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "source")]
|
||||
pub enum LabelContent {
|
||||
@@ -27,7 +38,10 @@ pub enum LabelContent {
|
||||
},
|
||||
Clock {
|
||||
format: Arc<str>,
|
||||
timezone: Option<Arc<str>>,
|
||||
timezone: Option<TimezoneDef>,
|
||||
},
|
||||
Timezone {
|
||||
timezone: usize,
|
||||
},
|
||||
Timer {
|
||||
format: Arc<str>,
|
||||
@@ -38,6 +52,7 @@ pub enum LabelContent {
|
||||
low_color: Arc<str>,
|
||||
charging_color: Arc<str>,
|
||||
},
|
||||
DragMultiplier,
|
||||
Ipd,
|
||||
}
|
||||
|
||||
@@ -66,9 +81,10 @@ pub enum LabelData {
|
||||
Ipd {
|
||||
last_ipd: f32,
|
||||
},
|
||||
DragMultiplier,
|
||||
}
|
||||
|
||||
pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
|
||||
pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent, app: &AppState) {
|
||||
let state = match content {
|
||||
LabelContent::Battery {
|
||||
device,
|
||||
@@ -83,19 +99,47 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
|
||||
charging_color: color_parse_or_default(charging_color),
|
||||
}),
|
||||
LabelContent::Clock { format, timezone } => {
|
||||
let tz: Option<Tz> = timezone.as_ref().map(|tz| {
|
||||
tz.parse().unwrap_or_else(|_| {
|
||||
log::error!("Failed to parse timezone '{}'", &tz);
|
||||
label.set_fg_color(*FALLBACK_COLOR);
|
||||
Tz::UTC
|
||||
})
|
||||
});
|
||||
let tz_str = match timezone {
|
||||
Some(TimezoneDef::Idx(idx)) => app.session.config.timezones.get(*idx).map_or_else(
|
||||
|| {
|
||||
log::error!("Timezone index out of range '{idx}'");
|
||||
label.set_fg_color(*FALLBACK_COLOR);
|
||||
None
|
||||
},
|
||||
|tz| Some(tz.as_str()),
|
||||
),
|
||||
Some(TimezoneDef::Str(tz_str)) => Some(tz_str.as_ref()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Some(LabelData::Clock {
|
||||
format: format.clone(),
|
||||
timezone: tz,
|
||||
timezone: tz_str.and_then(|tz| {
|
||||
tz.parse()
|
||||
.map_err(|_| {
|
||||
log::error!("Failed to parse timezone '{}'", &tz);
|
||||
label.set_fg_color(*FALLBACK_COLOR);
|
||||
})
|
||||
.ok()
|
||||
}),
|
||||
})
|
||||
}
|
||||
LabelContent::Timezone { timezone } => {
|
||||
if let Some(tz) = app.session.config.timezones.get(*timezone) {
|
||||
let pretty_tz = tz.split('/').next_back().map(|x| x.replace('_', " "));
|
||||
|
||||
if let Some(pretty_tz) = pretty_tz {
|
||||
label.set_text(&pretty_tz);
|
||||
return;
|
||||
}
|
||||
log::error!("Timezone name not valid '{}'", &tz);
|
||||
} else {
|
||||
log::error!("Timezone index out of range '{}'", &timezone);
|
||||
}
|
||||
label.set_fg_color(*FALLBACK_COLOR);
|
||||
label.set_text("Error");
|
||||
None
|
||||
}
|
||||
LabelContent::Timer { format } => Some(LabelData::Timer {
|
||||
format: format.clone(),
|
||||
start: Instant::now(),
|
||||
@@ -110,7 +154,8 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
|
||||
label.set_text(text);
|
||||
None
|
||||
}
|
||||
LabelContent::Ipd => Some(LabelData::Ipd { last_ipd: 0. }),
|
||||
LabelContent::Ipd => Some(LabelData::Ipd { last_ipd: -1. }),
|
||||
LabelContent::DragMultiplier => Some(LabelData::DragMultiplier),
|
||||
};
|
||||
|
||||
if let Some(state) = state {
|
||||
@@ -119,7 +164,9 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut AppState) {
|
||||
// want panic
|
||||
let ModularData::Label(data) = control.state.as_mut().unwrap() else {
|
||||
panic!("Label control has no state");
|
||||
};
|
||||
@@ -136,9 +183,9 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
let tags = ["", "H", "L", "R", "T"];
|
||||
|
||||
if let Some(device) = device {
|
||||
let (text, color) = device
|
||||
.soc
|
||||
.map(|soc| {
|
||||
let (text, color) = device.soc.map_or_else(
|
||||
|| (String::new(), Vec4::ZERO),
|
||||
|soc| {
|
||||
let text = format!(
|
||||
"{}{}",
|
||||
tags[device.role as usize],
|
||||
@@ -152,8 +199,8 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
*normal_color
|
||||
};
|
||||
(text, color)
|
||||
})
|
||||
.unwrap_or_else(|| ("".into(), Vec4::ZERO));
|
||||
},
|
||||
);
|
||||
|
||||
control.set_text(&text);
|
||||
control.set_fg_color(color);
|
||||
@@ -164,11 +211,11 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
LabelData::Clock { format, timezone } => {
|
||||
let format = format.clone();
|
||||
if let Some(tz) = timezone {
|
||||
let date = Local::now().with_timezone(tz);
|
||||
control.set_text(&format!("{}", &date.format(&format)));
|
||||
let date_time = Local::now().with_timezone(tz);
|
||||
control.set_text(&format!("{}", &date_time.format(&format)));
|
||||
} else {
|
||||
let date = Local::now();
|
||||
control.set_text(&format!("{}", &date.format(&format)));
|
||||
let date_time = Local::now();
|
||||
control.set_text(&format!("{}", &date_time.format(&format)));
|
||||
}
|
||||
}
|
||||
LabelData::Timer { format, start } => {
|
||||
@@ -188,15 +235,16 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
if let Some(mut proc) = child.take() {
|
||||
match proc.try_wait() {
|
||||
Ok(Some(code)) => {
|
||||
if !code.success() {
|
||||
log::error!("Child process exited with code: {}", code);
|
||||
} else {
|
||||
if code.success() {
|
||||
if let Some(mut stdout) = proc.stdout.take() {
|
||||
let mut buf = String::new();
|
||||
if stdout.read_to_string(&mut buf).is_ok() {
|
||||
control.set_text(&buf);
|
||||
} else {
|
||||
log::error!("Failed to read stdout for child process");
|
||||
error_toast_str(
|
||||
app,
|
||||
"LabelData::Exec: Failed to read stdout for child process",
|
||||
);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
@@ -204,6 +252,7 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
log::error!("No stdout for child process");
|
||||
return;
|
||||
}
|
||||
error_toast(app, "LabelData::Exec: Child process exited with code", code);
|
||||
}
|
||||
Ok(None) => {
|
||||
*child = Some(proc);
|
||||
@@ -212,7 +261,7 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
}
|
||||
Err(e) => {
|
||||
*child = None;
|
||||
log::error!("Error checking child process: {:?}", e);
|
||||
error_toast(app, "Error checking child process", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -226,7 +275,7 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
*last_exec = Instant::now();
|
||||
let args = command
|
||||
.iter()
|
||||
.map(|s| s.as_ref())
|
||||
.map(std::convert::AsRef::as_ref)
|
||||
.collect::<SmallVec<[&str; 8]>>();
|
||||
|
||||
match process::Command::new(args[0])
|
||||
@@ -238,9 +287,9 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
*child = Some(proc);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to spawn process {:?}: {:?}", args, e);
|
||||
error_toast(app, &format!("Failed to spawn process {args:?}"), e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
LabelData::Ipd { last_ipd } => {
|
||||
@@ -249,5 +298,8 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
control.set_text(&format!("{:.1}", app.input_state.ipd));
|
||||
}
|
||||
}
|
||||
LabelData::DragMultiplier => {
|
||||
control.set_text(&format!("{:.1}", app.session.config.space_drag_multiplier));
|
||||
}
|
||||
}
|
||||
}
|
||||
+238
-20
@@ -1,29 +1,33 @@
|
||||
pub mod button;
|
||||
pub mod label;
|
||||
//pub mod slider;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{fs::File, sync::Arc};
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
use button::{WayVRAction, WayVRDisplayClickAction};
|
||||
|
||||
use glam::Vec4;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
use vulkano::{command_buffer::CommandBufferUsage, image::view::ImageView};
|
||||
|
||||
use crate::{backend::common::OverlaySelector, state::AppState};
|
||||
use crate::{
|
||||
backend::common::OverlaySelector, config::AStrMapExt, config_io,
|
||||
graphics::dds::WlxCommandBufferDds, state::AppState,
|
||||
};
|
||||
|
||||
use self::{
|
||||
button::{modular_button_init, ButtonAction, ButtonData, OverlayAction},
|
||||
label::{modular_label_init, LabelContent, LabelData},
|
||||
};
|
||||
|
||||
use super::{color_parse, Canvas, CanvasBuilder, Control};
|
||||
use super::{
|
||||
canvas::{builder::CanvasBuilder, control::Control, Canvas},
|
||||
color_parse, GuiColor, FALLBACK_COLOR,
|
||||
};
|
||||
|
||||
type ModularControl = Control<(), ModularData>;
|
||||
type ExecArgs = Vec<Arc<str>>;
|
||||
|
||||
pub type GuiColor = Vec4;
|
||||
|
||||
static FALLBACK_COLOR: Lazy<GuiColor> = Lazy::new(|| Vec4::new(1., 0., 1., 1.));
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ModularUiConfig {
|
||||
pub width: f32,
|
||||
@@ -53,10 +57,12 @@ pub struct OverlayListTemplate {
|
||||
pub enum ModularElement {
|
||||
Panel {
|
||||
rect: [f32; 4],
|
||||
corner_radius: Option<f32>,
|
||||
bg_color: Arc<str>,
|
||||
},
|
||||
Label {
|
||||
rect: [f32; 4],
|
||||
corner_radius: Option<f32>,
|
||||
font_size: isize,
|
||||
fg_color: Arc<str>,
|
||||
#[serde(flatten)]
|
||||
@@ -64,13 +70,20 @@ pub enum ModularElement {
|
||||
},
|
||||
CenteredLabel {
|
||||
rect: [f32; 4],
|
||||
corner_radius: Option<f32>,
|
||||
font_size: isize,
|
||||
fg_color: Arc<str>,
|
||||
#[serde(flatten)]
|
||||
data: LabelContent,
|
||||
},
|
||||
Sprite {
|
||||
rect: [f32; 4],
|
||||
sprite: Arc<str>,
|
||||
sprite_st: Option<[f32; 4]>,
|
||||
},
|
||||
Button {
|
||||
rect: [f32; 4],
|
||||
corner_radius: Option<f32>,
|
||||
font_size: isize,
|
||||
fg_color: Arc<str>,
|
||||
bg_color: Arc<str>,
|
||||
@@ -81,6 +94,7 @@ pub enum ModularElement {
|
||||
/// Convenience type to save you from having to create a bunch of labels
|
||||
BatteryList {
|
||||
rect: [f32; 4],
|
||||
corner_radius: Option<f32>,
|
||||
font_size: isize,
|
||||
fg_color: Arc<str>,
|
||||
fg_color_low: Arc<str>,
|
||||
@@ -91,6 +105,7 @@ pub enum ModularElement {
|
||||
},
|
||||
OverlayList {
|
||||
rect: [f32; 4],
|
||||
corner_radius: Option<f32>,
|
||||
font_size: isize,
|
||||
fg_color: Arc<str>,
|
||||
bg_color: Arc<str>,
|
||||
@@ -98,6 +113,23 @@ pub enum ModularElement {
|
||||
#[serde(flatten)]
|
||||
template: Box<OverlayListTemplate>,
|
||||
},
|
||||
// Ignored if "wayvr" feature is not enabled
|
||||
WayVRLauncher {
|
||||
rect: [f32; 4],
|
||||
corner_radius: Option<f32>,
|
||||
font_size: isize,
|
||||
fg_color: Arc<str>,
|
||||
bg_color: Arc<str>,
|
||||
catalog_name: Arc<str>,
|
||||
},
|
||||
// Ignored if "wayvr" feature is not enabled
|
||||
WayVRDisplayList {
|
||||
rect: [f32; 4],
|
||||
corner_radius: Option<f32>,
|
||||
font_size: isize,
|
||||
fg_color: Arc<str>,
|
||||
bg_color: Arc<str>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -117,10 +149,11 @@ pub enum ModularData {
|
||||
Button(Box<ButtonData>),
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::many_single_char_names)]
|
||||
pub fn modular_canvas(
|
||||
size: &[u32; 2],
|
||||
size: [u32; 2],
|
||||
elements: &[ModularElement],
|
||||
state: &AppState,
|
||||
state: &mut AppState,
|
||||
) -> anyhow::Result<Canvas<(), ModularData>> {
|
||||
let mut canvas = CanvasBuilder::new(
|
||||
size[0] as _,
|
||||
@@ -130,39 +163,76 @@ pub fn modular_canvas(
|
||||
(),
|
||||
)?;
|
||||
let empty_str: Arc<str> = Arc::from("");
|
||||
for elem in elements.iter() {
|
||||
for elem in elements {
|
||||
match elem {
|
||||
ModularElement::Panel {
|
||||
rect: [x, y, w, h],
|
||||
corner_radius,
|
||||
bg_color,
|
||||
} => {
|
||||
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.panel(*x, *y, *w, *h);
|
||||
canvas.panel(*x, *y, *w, *h, corner_radius.unwrap_or_default());
|
||||
}
|
||||
ModularElement::Label {
|
||||
rect: [x, y, w, h],
|
||||
corner_radius,
|
||||
font_size,
|
||||
fg_color,
|
||||
data,
|
||||
} => {
|
||||
canvas.font_size = *font_size;
|
||||
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
let label = canvas.label(*x, *y, *w, *h, empty_str.clone());
|
||||
modular_label_init(label, data);
|
||||
let label = canvas.label(
|
||||
*x,
|
||||
*y,
|
||||
*w,
|
||||
*h,
|
||||
corner_radius.unwrap_or_default(),
|
||||
empty_str.clone(),
|
||||
);
|
||||
modular_label_init(label, data, state);
|
||||
}
|
||||
ModularElement::CenteredLabel {
|
||||
rect: [x, y, w, h],
|
||||
corner_radius,
|
||||
font_size,
|
||||
fg_color,
|
||||
data,
|
||||
} => {
|
||||
canvas.font_size = *font_size;
|
||||
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
let label = canvas.label_centered(*x, *y, *w, *h, empty_str.clone());
|
||||
modular_label_init(label, data);
|
||||
let label = canvas.label_centered(
|
||||
*x,
|
||||
*y,
|
||||
*w,
|
||||
*h,
|
||||
corner_radius.unwrap_or_default(),
|
||||
empty_str.clone(),
|
||||
);
|
||||
modular_label_init(label, data, state);
|
||||
}
|
||||
ModularElement::Sprite {
|
||||
rect: [x, y, w, h],
|
||||
sprite,
|
||||
sprite_st,
|
||||
} => match sprite_from_path(sprite.clone(), state) {
|
||||
Ok(view) => {
|
||||
let sprite = canvas.sprite(*x, *y, *w, *h);
|
||||
sprite.fg_color = Vec4::ONE;
|
||||
sprite.set_sprite(view);
|
||||
|
||||
let st = sprite_st
|
||||
.map(|st| Vec4::from_slice(&st))
|
||||
.unwrap_or_else(|| Vec4::new(1., 1., 0., 0.));
|
||||
sprite.set_sprite_st(st);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Could not load custom UI sprite: {e:?}");
|
||||
}
|
||||
},
|
||||
ModularElement::Button {
|
||||
rect: [x, y, w, h],
|
||||
corner_radius,
|
||||
font_size,
|
||||
bg_color,
|
||||
fg_color,
|
||||
@@ -172,11 +242,19 @@ pub fn modular_canvas(
|
||||
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.font_size = *font_size;
|
||||
let button = canvas.button(*x, *y, *w, *h, text.clone());
|
||||
let button = canvas.button(
|
||||
*x,
|
||||
*y,
|
||||
*w,
|
||||
*h,
|
||||
corner_radius.unwrap_or_default(),
|
||||
text.clone(),
|
||||
);
|
||||
modular_button_init(button, data);
|
||||
}
|
||||
ModularElement::BatteryList {
|
||||
rect: [x, y, w, h],
|
||||
corner_radius,
|
||||
font_size,
|
||||
fg_color,
|
||||
fg_color_low,
|
||||
@@ -204,6 +282,7 @@ pub fn modular_canvas(
|
||||
button_y + 2.,
|
||||
button_w - 4.,
|
||||
button_h - 4.,
|
||||
corner_radius.unwrap_or_default(),
|
||||
empty_str.clone(),
|
||||
);
|
||||
modular_label_init(
|
||||
@@ -214,6 +293,7 @@ pub fn modular_canvas(
|
||||
low_color: fg_color_low.clone(),
|
||||
charging_color: fg_color_charging.clone(),
|
||||
},
|
||||
state,
|
||||
);
|
||||
|
||||
button_x += match layout {
|
||||
@@ -228,6 +308,7 @@ pub fn modular_canvas(
|
||||
}
|
||||
ModularElement::OverlayList {
|
||||
rect: [x, y, w, h],
|
||||
corner_radius,
|
||||
font_size,
|
||||
fg_color,
|
||||
bg_color,
|
||||
@@ -246,12 +327,13 @@ pub fn modular_canvas(
|
||||
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.font_size = *font_size;
|
||||
|
||||
for screen in state.screens.iter() {
|
||||
for screen in &state.screens {
|
||||
let button = canvas.button(
|
||||
button_x + 2.,
|
||||
button_y + 2.,
|
||||
button_w - 4.,
|
||||
button_h - 4.,
|
||||
corner_radius.unwrap_or_default(),
|
||||
screen.name.clone(),
|
||||
);
|
||||
|
||||
@@ -338,6 +420,115 @@ pub fn modular_canvas(
|
||||
};
|
||||
}
|
||||
}
|
||||
#[allow(unused_variables)] // needed in case if wayvr feature is not enabled
|
||||
ModularElement::WayVRLauncher {
|
||||
rect: [x, y, w, h],
|
||||
corner_radius,
|
||||
font_size,
|
||||
fg_color,
|
||||
bg_color,
|
||||
catalog_name,
|
||||
} => {
|
||||
#[cfg(feature = "wayvr")]
|
||||
{
|
||||
if let Some(catalog) = state.session.wayvr_config.get_catalog(catalog_name) {
|
||||
let mut button_x = *x;
|
||||
let button_y = *y;
|
||||
|
||||
for app in &catalog.apps {
|
||||
let button_w: f32 = *w / catalog.apps.len() as f32;
|
||||
let button_h: f32 = *h;
|
||||
|
||||
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.font_size = *font_size;
|
||||
|
||||
let button = canvas.button(
|
||||
button_x + 2.,
|
||||
button_y + 2.,
|
||||
button_w - 4.,
|
||||
button_h - 4.,
|
||||
corner_radius.unwrap_or_default(),
|
||||
Arc::from(app.name.as_str()),
|
||||
);
|
||||
|
||||
let data = ButtonData {
|
||||
click_up: Some(vec![ButtonAction::WayVR {
|
||||
action: WayVRAction::AppClick {
|
||||
catalog_name: catalog_name.clone(),
|
||||
app_name: Arc::from(app.name.as_str()),
|
||||
},
|
||||
}]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
modular_button_init(button, &data);
|
||||
button_x += button_w;
|
||||
}
|
||||
} else {
|
||||
log::error!("WayVR catalog \"{catalog_name}\" not found");
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "wayvr"))]
|
||||
{
|
||||
log::error!("WayVR feature is not enabled, ignoring");
|
||||
}
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
ModularElement::WayVRDisplayList {
|
||||
rect: [x, y, w, h],
|
||||
corner_radius,
|
||||
font_size,
|
||||
fg_color,
|
||||
bg_color,
|
||||
} => {
|
||||
#[cfg(feature = "wayvr")]
|
||||
{
|
||||
let mut button_x = *x;
|
||||
let button_y = *y;
|
||||
let displays = &state.session.wayvr_config.displays;
|
||||
for (display_name, display) in displays {
|
||||
let button_w: f32 = (*w / displays.len() as f32).min(80.0);
|
||||
let button_h: f32 = *h;
|
||||
|
||||
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.font_size = *font_size;
|
||||
|
||||
let button = canvas.button(
|
||||
button_x + 2.,
|
||||
button_y + 2.,
|
||||
button_w - 4.,
|
||||
button_h - 4.,
|
||||
corner_radius.unwrap_or_default(),
|
||||
Arc::from(display_name.as_str()),
|
||||
);
|
||||
|
||||
let data = ButtonData {
|
||||
click_up: Some(vec![ButtonAction::WayVR {
|
||||
action: WayVRAction::DisplayClick {
|
||||
display_name: Arc::from(display_name.as_str()),
|
||||
action: WayVRDisplayClickAction::ToggleVisibility,
|
||||
},
|
||||
}]),
|
||||
long_click_up: Some(vec![ButtonAction::WayVR {
|
||||
action: WayVRAction::DisplayClick {
|
||||
display_name: Arc::from(display_name.as_str()),
|
||||
action: WayVRDisplayClickAction::Reset,
|
||||
},
|
||||
}]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
modular_button_init(button, &data);
|
||||
button_x += button_w;
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "wayvr"))]
|
||||
{
|
||||
log::error!("WayVR feature is not enabled, ignoring")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(canvas.build())
|
||||
@@ -345,7 +536,34 @@ pub fn modular_canvas(
|
||||
|
||||
pub fn color_parse_or_default(color: &str) -> GuiColor {
|
||||
color_parse(color).unwrap_or_else(|e| {
|
||||
log::error!("Failed to parse color '{}': {}", color, e);
|
||||
log::error!("Failed to parse color '{color}': {e}");
|
||||
*FALLBACK_COLOR
|
||||
})
|
||||
}
|
||||
|
||||
fn sprite_from_path(path: Arc<str>, app: &mut AppState) -> anyhow::Result<Arc<ImageView>> {
|
||||
if let Some(view) = app.sprites.arc_get(&path) {
|
||||
return Ok(view.clone());
|
||||
}
|
||||
|
||||
let real_path = config_io::get_config_root().join(&*path);
|
||||
|
||||
let Ok(f) = File::open(real_path) else {
|
||||
anyhow::bail!("Could not open custom sprite at: {}", path);
|
||||
};
|
||||
|
||||
let mut command_buffer = app.graphics.create_uploads_command_buffer(
|
||||
app.graphics.transfer_queue.clone(),
|
||||
CommandBufferUsage::OneTimeSubmit,
|
||||
)?;
|
||||
|
||||
match command_buffer.texture2d_dds(f) {
|
||||
Ok(image) => {
|
||||
command_buffer.build_and_execute_now()?;
|
||||
Ok(ImageView::new_default(image)?)
|
||||
}
|
||||
Err(e) => {
|
||||
anyhow::bail!("Could not use custom sprite at: {}\n{:?}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
-110
@@ -1,4 +1,4 @@
|
||||
use glam::Vec2;
|
||||
use glam::{IVec2, Vec2};
|
||||
use idmap::{idmap, IdMap};
|
||||
use idmap_derive::IntegerId;
|
||||
use input_linux::{
|
||||
@@ -6,8 +6,9 @@ use input_linux::{
|
||||
UInputHandle,
|
||||
};
|
||||
use libc::{input_event, timeval};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
use std::mem::transmute;
|
||||
use std::sync::LazyLock;
|
||||
use std::{fs::File, sync::atomic::AtomicBool};
|
||||
use strum::{EnumIter, EnumString, IntoEnumIterator};
|
||||
use xkbcommon::xkb;
|
||||
@@ -33,10 +34,7 @@ pub fn initialize() -> Box<dyn HidProvider> {
|
||||
log::error!("Could not create uinput provider. Keyboard/Mouse input will not work!");
|
||||
log::error!("To check if you're in input group, run: id -nG");
|
||||
if let Ok(user) = std::env::var("USER") {
|
||||
log::error!(
|
||||
"To add yourself to the input group, run: sudo usermod -aG input {}",
|
||||
user
|
||||
);
|
||||
log::error!("To add yourself to the input group, run: sudo usermod -aG input {user}");
|
||||
log::error!("After adding yourself to the input group, you will need to reboot.");
|
||||
}
|
||||
Box::new(DummyProvider {})
|
||||
@@ -45,7 +43,7 @@ pub fn initialize() -> Box<dyn HidProvider> {
|
||||
pub trait HidProvider {
|
||||
fn mouse_move(&mut self, pos: Vec2);
|
||||
fn send_button(&mut self, button: u16, down: bool);
|
||||
fn wheel(&mut self, delta: i32);
|
||||
fn wheel(&mut self, delta_y: i32, delta_x: i32);
|
||||
fn set_modifiers(&mut self, mods: u8);
|
||||
fn send_key(&self, key: VirtualKey, down: bool);
|
||||
fn set_desktop_extent(&mut self, extent: Vec2);
|
||||
@@ -63,11 +61,12 @@ struct MouseAction {
|
||||
last_requested_pos: Option<Vec2>,
|
||||
pos: Option<Vec2>,
|
||||
button: Option<MouseButtonAction>,
|
||||
scroll: Option<i32>,
|
||||
scroll: Option<IVec2>,
|
||||
}
|
||||
|
||||
pub struct UInputProvider {
|
||||
handle: UInputHandle<File>,
|
||||
keyboard_handle: UInputHandle<File>,
|
||||
mouse_handle: UInputHandle<File>,
|
||||
desktop_extent: Vec2,
|
||||
desktop_origin: Vec2,
|
||||
cur_modifiers: u8,
|
||||
@@ -89,103 +88,100 @@ const EV_ABS: u16 = 0x3;
|
||||
|
||||
impl UInputProvider {
|
||||
fn try_new() -> Option<Self> {
|
||||
if let Ok(file) = File::create("/dev/uinput") {
|
||||
let handle = UInputHandle::new(file);
|
||||
let keyboard_file = File::create("/dev/uinput").ok()?;
|
||||
let keyboard_handle = UInputHandle::new(keyboard_file);
|
||||
|
||||
let id = InputId {
|
||||
bustype: 0x03,
|
||||
vendor: 0x4711,
|
||||
product: 0x0829,
|
||||
version: 5,
|
||||
};
|
||||
let mouse_file = File::create("/dev/uinput").ok()?;
|
||||
let mouse_handle = UInputHandle::new(mouse_file);
|
||||
|
||||
let name = b"WlxOverlay-S Keyboard-Mouse Hybrid Thing\0";
|
||||
let kbd_id = InputId {
|
||||
bustype: 0x03,
|
||||
vendor: 0x4711,
|
||||
product: 0x0829,
|
||||
version: 5,
|
||||
};
|
||||
let mouse_id = InputId {
|
||||
bustype: 0x03,
|
||||
vendor: 0x4711,
|
||||
product: 0x0830,
|
||||
version: 5,
|
||||
};
|
||||
let kbd_name = b"WlxOverlay-S Keyboard\0";
|
||||
let mouse_name = b"WlxOverlay-S Mouse\0";
|
||||
|
||||
let abs_info = vec![
|
||||
AbsoluteInfoSetup {
|
||||
axis: input_linux::AbsoluteAxis::X,
|
||||
info: AbsoluteInfo {
|
||||
value: 0,
|
||||
minimum: 0,
|
||||
maximum: MOUSE_EXTENT as _,
|
||||
fuzz: 0,
|
||||
flat: 0,
|
||||
resolution: 10,
|
||||
},
|
||||
let abs_info = vec![
|
||||
AbsoluteInfoSetup {
|
||||
axis: input_linux::AbsoluteAxis::X,
|
||||
info: AbsoluteInfo {
|
||||
value: 0,
|
||||
minimum: 0,
|
||||
maximum: MOUSE_EXTENT as _,
|
||||
fuzz: 0,
|
||||
flat: 0,
|
||||
resolution: 10,
|
||||
},
|
||||
AbsoluteInfoSetup {
|
||||
axis: input_linux::AbsoluteAxis::Y,
|
||||
info: AbsoluteInfo {
|
||||
value: 0,
|
||||
minimum: 0,
|
||||
maximum: MOUSE_EXTENT as _,
|
||||
fuzz: 0,
|
||||
flat: 0,
|
||||
resolution: 10,
|
||||
},
|
||||
},
|
||||
AbsoluteInfoSetup {
|
||||
axis: input_linux::AbsoluteAxis::Y,
|
||||
info: AbsoluteInfo {
|
||||
value: 0,
|
||||
minimum: 0,
|
||||
maximum: MOUSE_EXTENT as _,
|
||||
fuzz: 0,
|
||||
flat: 0,
|
||||
resolution: 10,
|
||||
},
|
||||
];
|
||||
},
|
||||
];
|
||||
|
||||
if handle.set_evbit(EventKind::Key).is_err() {
|
||||
return None;
|
||||
}
|
||||
if handle.set_evbit(EventKind::Absolute).is_err() {
|
||||
return None;
|
||||
}
|
||||
if handle.set_evbit(EventKind::Relative).is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for btn in MOUSE_LEFT..=MOUSE_MIDDLE {
|
||||
let key: Key = unsafe { transmute(btn) };
|
||||
if handle.set_keybit(key).is_err() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
for key in VirtualKey::iter() {
|
||||
let key: Key = unsafe { transmute((key as u16) - 8) };
|
||||
if handle.set_keybit(key).is_err() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if handle.set_absbit(AbsoluteAxis::X).is_err() {
|
||||
return None;
|
||||
}
|
||||
if handle.set_absbit(AbsoluteAxis::Y).is_err() {
|
||||
return None;
|
||||
}
|
||||
if handle.set_relbit(RelativeAxis::Wheel).is_err() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if handle.create(&id, name, 0, &abs_info).is_ok() {
|
||||
return Some(UInputProvider {
|
||||
handle,
|
||||
desktop_extent: Vec2::ZERO,
|
||||
desktop_origin: Vec2::ZERO,
|
||||
current_action: Default::default(),
|
||||
cur_modifiers: 0,
|
||||
});
|
||||
}
|
||||
keyboard_handle.set_evbit(EventKind::Key).ok()?;
|
||||
for key in VirtualKey::iter() {
|
||||
let mapped_key: Key = unsafe { std::mem::transmute((key as u16) - 8) };
|
||||
keyboard_handle.set_keybit(mapped_key).ok()?;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
keyboard_handle.create(&kbd_id, kbd_name, 0, &[]).ok()?;
|
||||
|
||||
mouse_handle.set_evbit(EventKind::Absolute).ok()?;
|
||||
mouse_handle.set_evbit(EventKind::Relative).ok()?;
|
||||
mouse_handle.set_absbit(AbsoluteAxis::X).ok()?;
|
||||
mouse_handle.set_absbit(AbsoluteAxis::Y).ok()?;
|
||||
mouse_handle.set_relbit(RelativeAxis::WheelHiRes).ok()?;
|
||||
mouse_handle
|
||||
.set_relbit(RelativeAxis::HorizontalWheelHiRes)
|
||||
.ok()?;
|
||||
mouse_handle.set_evbit(EventKind::Key).ok()?;
|
||||
|
||||
for btn in MOUSE_LEFT..=MOUSE_MIDDLE {
|
||||
let mouse_btn: Key = unsafe { transmute(btn) };
|
||||
mouse_handle.set_keybit(mouse_btn).ok()?;
|
||||
}
|
||||
mouse_handle
|
||||
.create(&mouse_id, mouse_name, 0, &abs_info)
|
||||
.ok()?;
|
||||
|
||||
Some(Self {
|
||||
keyboard_handle,
|
||||
mouse_handle,
|
||||
desktop_extent: Vec2::ZERO,
|
||||
desktop_origin: Vec2::ZERO,
|
||||
current_action: MouseAction::default(),
|
||||
cur_modifiers: 0,
|
||||
})
|
||||
}
|
||||
fn send_button_internal(&self, button: u16, down: bool) {
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_KEY, button, down as _),
|
||||
new_event(time, EV_KEY, button, down.into()),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.handle.write(&events) {
|
||||
log::error!("send_button: {}", res.to_string());
|
||||
if let Err(res) = self.mouse_handle.write(&events) {
|
||||
log::error!("send_button: {res}");
|
||||
}
|
||||
}
|
||||
fn mouse_move_internal(&mut self, pos: Vec2) {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!("Mouse move: {:?}", pos);
|
||||
log::trace!("Mouse move: {pos:?}");
|
||||
|
||||
let pos = (pos - self.desktop_origin) * (MOUSE_EXTENT / self.desktop_extent);
|
||||
|
||||
@@ -195,18 +191,24 @@ impl UInputProvider {
|
||||
new_event(time, EV_ABS, AbsoluteAxis::Y as _, pos.y as i32),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.handle.write(&events) {
|
||||
log::error!("{}", res.to_string());
|
||||
if let Err(res) = self.mouse_handle.write(&events) {
|
||||
log::error!("{res}");
|
||||
}
|
||||
}
|
||||
fn wheel_internal(&self, delta: i32) {
|
||||
fn wheel_internal(&self, delta_y: i32, delta_x: i32) {
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_REL, RelativeAxis::Wheel as _, delta),
|
||||
new_event(time, EV_REL, RelativeAxis::WheelHiRes as _, delta_y),
|
||||
new_event(
|
||||
time,
|
||||
EV_REL,
|
||||
RelativeAxis::HorizontalWheelHiRes as _,
|
||||
delta_x,
|
||||
),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.handle.write(&events) {
|
||||
log::error!("wheel: {}", res.to_string());
|
||||
if let Err(res) = self.mouse_handle.write(&events) {
|
||||
log::error!("wheel: {res}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,14 +227,16 @@ impl HidProvider for UInputProvider {
|
||||
self.cur_modifiers = modifiers;
|
||||
}
|
||||
fn send_key(&self, key: VirtualKey, down: bool) {
|
||||
log::debug!("send_key: {:?} {}", key, down);
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!("send_key: {key:?} {down}");
|
||||
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_KEY, (key as u16) - 8, down as _),
|
||||
new_event(time, EV_KEY, (key as u16) - 8, down.into()),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.handle.write(&events) {
|
||||
log::error!("send_key: {}", res.to_string());
|
||||
if let Err(res) = self.keyboard_handle.write(&events) {
|
||||
log::error!("send_key: {res}");
|
||||
}
|
||||
}
|
||||
fn set_desktop_extent(&mut self, extent: Vec2) {
|
||||
@@ -242,7 +246,7 @@ impl HidProvider for UInputProvider {
|
||||
self.desktop_origin = origin;
|
||||
}
|
||||
fn mouse_move(&mut self, pos: Vec2) {
|
||||
if self.current_action.pos.is_none() {
|
||||
if self.current_action.pos.is_none() && self.current_action.scroll.is_none() {
|
||||
self.current_action.pos = Some(pos);
|
||||
}
|
||||
self.current_action.last_requested_pos = Some(pos);
|
||||
@@ -253,9 +257,12 @@ impl HidProvider for UInputProvider {
|
||||
self.current_action.pos = self.current_action.last_requested_pos;
|
||||
}
|
||||
}
|
||||
fn wheel(&mut self, delta: i32) {
|
||||
fn wheel(&mut self, delta_y: i32, delta_x: i32) {
|
||||
if self.current_action.scroll.is_none() {
|
||||
self.current_action.scroll = Some(delta);
|
||||
self.current_action.scroll = Some(IVec2::new(delta_x, delta_y));
|
||||
// Pass mouse motion events only if not scrolling
|
||||
// (allows scrolling on all Chromium-based applications)
|
||||
self.current_action.pos = None;
|
||||
}
|
||||
}
|
||||
fn commit(&mut self) {
|
||||
@@ -266,7 +273,7 @@ impl HidProvider for UInputProvider {
|
||||
self.send_button_internal(button.button, button.down);
|
||||
}
|
||||
if let Some(scroll) = self.current_action.scroll.take() {
|
||||
self.wheel_internal(scroll);
|
||||
self.wheel_internal(scroll.y, scroll.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +281,7 @@ impl HidProvider for UInputProvider {
|
||||
impl HidProvider for DummyProvider {
|
||||
fn mouse_move(&mut self, _pos: Vec2) {}
|
||||
fn send_button(&mut self, _button: u16, _down: bool) {}
|
||||
fn wheel(&mut self, _delta: i32) {}
|
||||
fn wheel(&mut self, _delta_y: i32, _delta_x: i32) {}
|
||||
fn set_modifiers(&mut self, _modifiers: u8) {}
|
||||
fn send_key(&self, _key: VirtualKey, _down: bool) {}
|
||||
fn set_desktop_extent(&mut self, _extent: Vec2) {}
|
||||
@@ -293,7 +300,7 @@ fn get_time() -> timeval {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn new_event(time: timeval, type_: u16, code: u16, value: i32) -> input_event {
|
||||
const fn new_event(time: timeval, type_: u16, code: u16, value: i32) -> input_event {
|
||||
input_event {
|
||||
time,
|
||||
type_,
|
||||
@@ -313,7 +320,7 @@ pub const META: KeyModifier = 0x80;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy, IntegerId, EnumString, EnumIter)]
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, IntegerId, EnumString, EnumIter)]
|
||||
pub enum VirtualKey {
|
||||
Escape = 9,
|
||||
N1, // number row
|
||||
@@ -469,7 +476,7 @@ pub enum VirtualKey {
|
||||
XF86Search = 225,
|
||||
}
|
||||
|
||||
pub static KEYS_TO_MODS: Lazy<IdMap<VirtualKey, KeyModifier>> = Lazy::new(|| {
|
||||
pub static KEYS_TO_MODS: LazyLock<IdMap<VirtualKey, KeyModifier>> = LazyLock::new(|| {
|
||||
idmap! {
|
||||
VirtualKey::LShift => SHIFT,
|
||||
VirtualKey::RShift => SHIFT,
|
||||
@@ -484,7 +491,7 @@ pub static KEYS_TO_MODS: Lazy<IdMap<VirtualKey, KeyModifier>> = Lazy::new(|| {
|
||||
}
|
||||
});
|
||||
|
||||
pub static MODS_TO_KEYS: Lazy<IdMap<KeyModifier, Vec<VirtualKey>>> = Lazy::new(|| {
|
||||
pub static MODS_TO_KEYS: LazyLock<IdMap<KeyModifier, Vec<VirtualKey>>> = LazyLock::new(|| {
|
||||
idmap! {
|
||||
SHIFT => vec![VirtualKey::LShift, VirtualKey::RShift],
|
||||
CAPS_LOCK => vec![VirtualKey::Caps],
|
||||
@@ -514,7 +521,7 @@ macro_rules! key_is {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_key_type(key: VirtualKey) -> KeyType {
|
||||
pub const fn get_key_type(key: VirtualKey) -> KeyType {
|
||||
if key_between!(key, VirtualKey::N1, VirtualKey::Plus)
|
||||
|| key_between!(key, VirtualKey::Q, VirtualKey::Oem6)
|
||||
|| key_between!(key, VirtualKey::A, VirtualKey::Oem3)
|
||||
@@ -533,7 +540,6 @@ pub fn get_key_type(key: VirtualKey) -> KeyType {
|
||||
}
|
||||
|
||||
pub struct XkbKeymap {
|
||||
pub context: xkb::Context,
|
||||
pub keymap: xkb::Keymap,
|
||||
}
|
||||
|
||||
|
||||
+7
-9
@@ -46,11 +46,9 @@ pub fn get_keymap_wl() -> anyhow::Result<XkbKeymap> {
|
||||
// this gets us the wl_keyboard
|
||||
let _ = queue.blocking_dispatch(&mut me);
|
||||
|
||||
if let Some(keymap) = me.keymap.take() {
|
||||
Ok(keymap)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Could not load keymap"))
|
||||
}
|
||||
me.keymap
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not load keymap"))
|
||||
}
|
||||
|
||||
impl Dispatch<WlRegistry, GlobalListContents> for WlKeymapHandler {
|
||||
@@ -84,7 +82,7 @@ impl Dispatch<WlSeat, ()> for WlKeymapHandler {
|
||||
}
|
||||
}
|
||||
wl_seat::Event::Name { name } => {
|
||||
log::debug!("Using WlSeat: {}", name);
|
||||
log::debug!("Using WlSeat: {name}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -120,21 +118,21 @@ impl Dispatch<WlKeyboard, ()> for WlKeymapHandler {
|
||||
|
||||
match maybe_keymap {
|
||||
Ok(Some(keymap)) => {
|
||||
state.keymap = Some(XkbKeymap { context, keymap });
|
||||
state.keymap = Some(XkbKeymap { keymap });
|
||||
}
|
||||
Ok(None) => {
|
||||
log::error!("Could not load keymap: no keymap");
|
||||
log::error!("Default layout will be used.");
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Could not load keymap: {}", err);
|
||||
log::error!("Could not load keymap: {err}");
|
||||
log::error!("Default layout will be used.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::RepeatInfo { rate, delay } => {
|
||||
log::debug!("WlKeyboard RepeatInfo rate: {}, delay: {}", rate, delay);
|
||||
log::debug!("WlKeyboard RepeatInfo rate: {rate}, delay: {delay}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,6 +1,9 @@
|
||||
use xkbcommon::xkb::{
|
||||
self,
|
||||
x11::{get_core_keyboard_device_id, keymap_new_from_device},
|
||||
x11::{
|
||||
get_core_keyboard_device_id, keymap_new_from_device, setup_xkb_extension,
|
||||
SetupXkbExtensionFlags, MIN_MAJOR_XKB_VERSION, MIN_MINOR_XKB_VERSION,
|
||||
},
|
||||
};
|
||||
|
||||
use super::XkbKeymap;
|
||||
@@ -9,6 +12,17 @@ pub fn get_keymap_x11() -> anyhow::Result<XkbKeymap> {
|
||||
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||
|
||||
let (conn, _) = xcb::Connection::connect(None)?;
|
||||
setup_xkb_extension(
|
||||
&conn,
|
||||
MIN_MAJOR_XKB_VERSION,
|
||||
MIN_MINOR_XKB_VERSION,
|
||||
SetupXkbExtensionFlags::NoFlags,
|
||||
&mut 0,
|
||||
&mut 0,
|
||||
&mut 0,
|
||||
&mut 0,
|
||||
);
|
||||
|
||||
let device_id = get_core_keyboard_device_id(&conn);
|
||||
if device_id == -1 {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -17,5 +31,5 @@ pub fn get_keymap_x11() -> anyhow::Result<XkbKeymap> {
|
||||
}
|
||||
let keymap = keymap_new_from_device(&context, &conn, device_id, xkb::KEYMAP_COMPILE_NO_FLAGS);
|
||||
|
||||
Ok(XkbKeymap { context, keymap })
|
||||
Ok(XkbKeymap { keymap })
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 145 KiB |
+96
-55
@@ -1,4 +1,18 @@
|
||||
#[allow(dead_code)]
|
||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
|
||||
#![allow(
|
||||
dead_code,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::cast_lossless,
|
||||
clippy::match_wildcard_for_single_variants,
|
||||
clippy::doc_markdown,
|
||||
clippy::struct_excessive_bools,
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::needless_pass_by_ref_mut,
|
||||
clippy::multiple_crate_versions
|
||||
)]
|
||||
mod backend;
|
||||
mod config;
|
||||
mod config_io;
|
||||
@@ -9,6 +23,9 @@ mod overlays;
|
||||
mod shaders;
|
||||
mod state;
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
mod config_wayvr;
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
@@ -18,10 +35,12 @@ use std::{
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use flexi_logger::{Duplicate, FileSpec, LogSpecification};
|
||||
use sysinfo::Pid;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
/// The lightweight desktop overlay for OpenVR and OpenXR
|
||||
#[derive(Parser, Debug)]
|
||||
#[derive(Default, Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[cfg(feature = "openvr")]
|
||||
@@ -34,6 +53,10 @@ struct Args {
|
||||
#[arg(long)]
|
||||
openxr: bool,
|
||||
|
||||
/// Show the working set of overlay on startup
|
||||
#[arg(long)]
|
||||
show: bool,
|
||||
|
||||
/// Uninstall OpenVR manifest and exit
|
||||
#[arg(long)]
|
||||
uninstall: bool,
|
||||
@@ -56,8 +79,13 @@ struct Args {
|
||||
uidev: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = Args::parse();
|
||||
let mut args = if std::env::args().skip(1).any(|a| !a.is_empty()) {
|
||||
Args::parse()
|
||||
} else {
|
||||
Args::default()
|
||||
};
|
||||
|
||||
if !args.multi && !ensure_single_instance(args.replace) {
|
||||
println!("Looks like WlxOverlay-S is already running.");
|
||||
@@ -65,12 +93,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
logging_init(&mut args)?;
|
||||
logging_init(&mut args);
|
||||
|
||||
log::info!(
|
||||
"Welcome to {} version {}!",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
env!("WLX_BUILD"),
|
||||
);
|
||||
log::info!("It is {}.", chrono::Local::now().format("%c"));
|
||||
|
||||
@@ -105,27 +133,27 @@ fn auto_run(running: Arc<AtomicBool>, args: Args) {
|
||||
#[cfg(feature = "openxr")]
|
||||
if !args_get_openvr(&args) {
|
||||
use crate::backend::openxr::openxr_run;
|
||||
match openxr_run(running.clone()) {
|
||||
match openxr_run(running.clone(), args.show) {
|
||||
Ok(()) => return,
|
||||
Err(BackendError::NotSupported) => (),
|
||||
Err(e) => {
|
||||
log::error!("{}", e.to_string());
|
||||
log::error!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openvr")]
|
||||
if !args_get_openxr(&args) {
|
||||
use crate::backend::openvr::openvr_run;
|
||||
match openvr_run(running.clone()) {
|
||||
match openvr_run(running, args.show) {
|
||||
Ok(()) => return,
|
||||
Err(BackendError::NotSupported) => (),
|
||||
Err(e) => {
|
||||
log::error!("{}", e.to_string());
|
||||
log::error!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
log::error!("No more backends to try");
|
||||
@@ -137,10 +165,10 @@ fn auto_run(running: Arc<AtomicBool>, args: Args) {
|
||||
compile_error!("No desktop support! Enable either wayland or x11 features!");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn args_get_openvr(_args: &Args) -> bool {
|
||||
#[allow(dead_code, unused_variables)]
|
||||
const fn args_get_openvr(args: &Args) -> bool {
|
||||
#[cfg(feature = "openvr")]
|
||||
let ret = _args.openvr;
|
||||
let ret = args.openvr;
|
||||
|
||||
#[cfg(not(feature = "openvr"))]
|
||||
let ret = false;
|
||||
@@ -148,10 +176,10 @@ fn args_get_openvr(_args: &Args) -> bool {
|
||||
ret
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn args_get_openxr(_args: &Args) -> bool {
|
||||
#[allow(dead_code, unused_variables)]
|
||||
const fn args_get_openxr(args: &Args) -> bool {
|
||||
#[cfg(feature = "openxr")]
|
||||
let ret = _args.openxr;
|
||||
let ret = args.openxr;
|
||||
|
||||
#[cfg(not(feature = "openxr"))]
|
||||
let ret = false;
|
||||
@@ -159,54 +187,64 @@ fn args_get_openxr(_args: &Args) -> bool {
|
||||
ret
|
||||
}
|
||||
|
||||
fn logging_init(args: &mut Args) -> anyhow::Result<()> {
|
||||
let log_file = args
|
||||
fn logging_init(args: &mut Args) {
|
||||
let log_file_path = args
|
||||
.log_to
|
||||
.take()
|
||||
.or_else(|| std::env::var("WLX_LOGFILE").ok())
|
||||
.or_else(|| Some("/tmp/wlx.log".to_string()));
|
||||
.unwrap_or_else(|| String::from("/tmp/wlx.log"));
|
||||
|
||||
if let Some(log_to) = log_file.filter(|s| !s.is_empty()) {
|
||||
if let Err(e) = file_logging_init(&log_to) {
|
||||
log::error!("Failed to initialize file logging: {}", e);
|
||||
flexi_logger::Logger::try_with_env_or_str("info")?.start()?;
|
||||
let file_writer = match std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&log_file_path)
|
||||
{
|
||||
Ok(file) => {
|
||||
println!("Logging to {}", &log_file_path);
|
||||
Some(file)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to open log file (path: {e:?}): {log_file_path}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let registry = tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.pretty()
|
||||
.with_writer(std::io::stderr),
|
||||
)
|
||||
.with(
|
||||
/* read RUST_LOG env var */
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy()
|
||||
.add_directive("zbus=warn".parse().unwrap())
|
||||
.add_directive("wlx_capture::wayland=info".parse().unwrap())
|
||||
.add_directive("smithay=debug".parse().unwrap()), /* GLES render spam */
|
||||
);
|
||||
|
||||
if let Some(writer) = file_writer {
|
||||
registry
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(writer)
|
||||
.with_ansi(false),
|
||||
)
|
||||
.init();
|
||||
} else {
|
||||
flexi_logger::Logger::try_with_env_or_str("info")?.start()?;
|
||||
registry.init();
|
||||
}
|
||||
|
||||
log_panics::init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn file_logging_init(log_to: &str) -> anyhow::Result<()> {
|
||||
let file_spec = FileSpec::try_from(PathBuf::from(log_to))?;
|
||||
let log_spec = LogSpecification::env_or_parse("info")?;
|
||||
|
||||
let duplicate = log_spec
|
||||
.module_filters()
|
||||
.iter()
|
||||
.find(|m| m.module_name.is_none())
|
||||
.map(|m| match m.level_filter {
|
||||
log::LevelFilter::Trace => Duplicate::Trace,
|
||||
log::LevelFilter::Debug => Duplicate::Debug,
|
||||
log::LevelFilter::Info => Duplicate::Info,
|
||||
log::LevelFilter::Warn => Duplicate::Warn,
|
||||
_ => Duplicate::Error,
|
||||
});
|
||||
|
||||
flexi_logger::Logger::with(log_spec)
|
||||
.log_to_file(file_spec)
|
||||
.duplicate_to_stderr(duplicate.unwrap_or(Duplicate::Error))
|
||||
.start()?;
|
||||
println!("Logging to: {}", log_to);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_single_instance(replace: bool) -> bool {
|
||||
let mut path = std::env::var("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from("/tmp"));
|
||||
let mut path =
|
||||
std::env::var("XDG_RUNTIME_DIR").map_or_else(|_| PathBuf::from("/tmp"), PathBuf::from);
|
||||
path.push("wlx-overlay-s.pid");
|
||||
|
||||
if path.exists() {
|
||||
@@ -214,7 +252,10 @@ fn ensure_single_instance(replace: bool) -> bool {
|
||||
if let Ok(pid_str) = std::fs::read_to_string(&path) {
|
||||
if let Ok(pid) = pid_str.trim().parse::<u32>() {
|
||||
let mut system = sysinfo::System::new();
|
||||
system.refresh_processes();
|
||||
system.refresh_processes(
|
||||
sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(pid)]),
|
||||
false,
|
||||
);
|
||||
if let Some(proc) = system.process(sysinfo::Pid::from_u32(pid)) {
|
||||
if replace {
|
||||
proc.kill_with(sysinfo::Signal::Term);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use glam::Vec3A;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use crate::backend::overlay::{OverlayData, OverlayState, RelativeTo};
|
||||
use crate::backend::overlay::{OverlayData, OverlayState, RelativeTo, Z_ORDER_ANCHOR};
|
||||
use crate::config::{load_known_yaml, ConfigType};
|
||||
use crate::gui::modular::{modular_canvas, ModularUiConfig};
|
||||
use crate::state::AppState;
|
||||
|
||||
pub static ANCHOR_NAME: Lazy<Arc<str>> = Lazy::new(|| Arc::from("anchor"));
|
||||
pub static ANCHOR_NAME: LazyLock<Arc<str>> = LazyLock::new(|| Arc::from("anchor"));
|
||||
|
||||
pub fn create_anchor<O>(state: &AppState) -> anyhow::Result<OverlayData<O>>
|
||||
pub fn create_anchor<O>(state: &mut AppState) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
@@ -21,13 +20,13 @@ where
|
||||
want_visible: false,
|
||||
interactable: false,
|
||||
grabbable: false,
|
||||
z_order: 67,
|
||||
z_order: Z_ORDER_ANCHOR,
|
||||
spawn_scale: config.width,
|
||||
spawn_point: Vec3A::NEG_Z * 0.5,
|
||||
relative_to: RelativeTo::Stage,
|
||||
..Default::default()
|
||||
},
|
||||
backend: Box::new(modular_canvas(&config.size, &config.elements, state)?),
|
||||
backend: Box::new(modular_canvas(config.size, &config.elements, state)?),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
const SETTINGS_NAME: &str = "settings";
|
||||
|
||||
pub fn create_custom(
|
||||
state: &AppState,
|
||||
state: &mut AppState,
|
||||
name: Arc<str>,
|
||||
) -> Option<(OverlayState, Box<dyn OverlayBackend>)> {
|
||||
let config = if &*name == SETTINGS_NAME {
|
||||
@@ -21,28 +21,28 @@ pub fn create_custom(
|
||||
match load_custom_ui(&name) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load custom UI config for {}: {:?}", name, e);
|
||||
log::error!("Failed to load custom UI config for {name}: {e:?}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let canvas = match modular_canvas(&config.size, &config.elements, state) {
|
||||
let canvas = match modular_canvas(config.size, &config.elements, state) {
|
||||
Ok(canvas) => canvas,
|
||||
Err(e) => {
|
||||
log::error!("Failed to create canvas for {}: {:?}", name, e);
|
||||
log::error!("Failed to create canvas for {name}: {e:?}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let state = OverlayState {
|
||||
name: name.clone(),
|
||||
name,
|
||||
want_visible: true,
|
||||
interactable: true,
|
||||
grabbable: true,
|
||||
spawn_scale: config.width,
|
||||
spawn_point: Vec3A::from_array(config.spawn_pos.unwrap_or([0., 0., -0.5])),
|
||||
interaction_transform: ui_transform(&config.size),
|
||||
interaction_transform: ui_transform(config.size),
|
||||
..Default::default()
|
||||
};
|
||||
let backend = Box::new(canvas);
|
||||
|
||||
+210
-69
@@ -2,25 +2,32 @@ use std::{
|
||||
collections::HashMap,
|
||||
process::{Child, Command},
|
||||
str::FromStr,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
input::PointerMode,
|
||||
overlay::{OverlayData, OverlayState},
|
||||
input::{InteractionHandler, PointerMode},
|
||||
overlay::{
|
||||
FrameMeta, OverlayBackend, OverlayData, OverlayRenderer, OverlayState, ShouldRender,
|
||||
},
|
||||
},
|
||||
config::{self, ConfigType},
|
||||
gui::{color_parse, CanvasBuilder, Control, KeyCapType},
|
||||
graphics::CommandBuffers,
|
||||
gui::{
|
||||
canvas::{builder::CanvasBuilder, control::Control, Canvas},
|
||||
color_parse, KeyCapType,
|
||||
},
|
||||
hid::{
|
||||
get_key_type, KeyModifier, KeyType, VirtualKey, XkbKeymap, ALT, CTRL, KEYS_TO_MODS, META,
|
||||
NUM_LOCK, SHIFT, SUPER,
|
||||
},
|
||||
state::AppState,
|
||||
state::{AppState, KeyboardFocus},
|
||||
};
|
||||
use glam::{vec2, vec3a, Affine2, Vec4};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vulkano::image::view::ImageView;
|
||||
|
||||
const PIXELS_PER_UNIT: f32 = 80.;
|
||||
const BUTTON_PADDING: f32 = 4.;
|
||||
@@ -28,9 +35,40 @@ const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, META];
|
||||
|
||||
pub const KEYBOARD_NAME: &str = "kbd";
|
||||
|
||||
fn send_key(app: &mut AppState, key: VirtualKey, down: bool) {
|
||||
match app.keyboard_focus {
|
||||
KeyboardFocus::PhysicalScreen => {
|
||||
app.hid_provider.send_key(key, down);
|
||||
}
|
||||
KeyboardFocus::WayVR =>
|
||||
{
|
||||
#[cfg(feature = "wayvr")]
|
||||
if let Some(wayvr) = &app.wayvr {
|
||||
wayvr.borrow_mut().data.state.send_key(key as u32, down);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_modifiers(app: &mut AppState, mods: u8) {
|
||||
match app.keyboard_focus {
|
||||
KeyboardFocus::PhysicalScreen => {
|
||||
app.hid_provider.set_modifiers(mods);
|
||||
}
|
||||
KeyboardFocus::WayVR =>
|
||||
{
|
||||
#[cfg(feature = "wayvr")]
|
||||
if let Some(wayvr) = &app.wayvr {
|
||||
wayvr.borrow_mut().data.state.set_modifiers(mods);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn create_keyboard<O>(
|
||||
app: &AppState,
|
||||
keymap: Option<XkbKeymap>,
|
||||
mut keymap: Option<XkbKeymap>,
|
||||
) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
@@ -42,6 +80,14 @@ where
|
||||
|
||||
let data = KeyboardData {
|
||||
modifiers: 0,
|
||||
alt_modifier: match LAYOUT.alt_modifier {
|
||||
AltModifier::Shift => SHIFT,
|
||||
AltModifier::Ctrl => CTRL,
|
||||
AltModifier::Alt => ALT,
|
||||
AltModifier::Super => SUPER,
|
||||
AltModifier::Meta => META,
|
||||
_ => 0,
|
||||
},
|
||||
processes: vec![],
|
||||
};
|
||||
|
||||
@@ -53,25 +99,32 @@ where
|
||||
data,
|
||||
)?;
|
||||
|
||||
canvas.bg_color = color_parse("#101010").unwrap(); //safe
|
||||
canvas.panel(0., 0., size.x, size.y);
|
||||
canvas.bg_color = color_parse("#181926").unwrap(); //safe
|
||||
canvas.panel(0., 0., size.x, size.y, 12.);
|
||||
|
||||
canvas.font_size = 18;
|
||||
canvas.bg_color = color_parse("#202020").unwrap(); //safe
|
||||
canvas.fg_color = color_parse("#cad3f5").unwrap(); //safe
|
||||
canvas.bg_color = color_parse("#1e2030").unwrap(); //safe
|
||||
|
||||
let has_altgr = keymap.as_ref().map_or(false, |k| k.has_altgr());
|
||||
let has_altgr = keymap
|
||||
.as_ref()
|
||||
.is_some_and(super::super::hid::XkbKeymap::has_altgr);
|
||||
|
||||
if !LAYOUT.auto_labels.unwrap_or(true) {
|
||||
keymap = None;
|
||||
}
|
||||
|
||||
let unit_size = size.x / LAYOUT.row_size;
|
||||
let h = unit_size - 2. * BUTTON_PADDING;
|
||||
let h = 2.0f32.mul_add(-BUTTON_PADDING, unit_size);
|
||||
|
||||
for row in 0..LAYOUT.key_sizes.len() {
|
||||
let y = unit_size * (row as f32) + BUTTON_PADDING;
|
||||
let y = unit_size.mul_add(row as f32, BUTTON_PADDING);
|
||||
let mut sum_size = 0f32;
|
||||
|
||||
for col in 0..LAYOUT.key_sizes[row].len() {
|
||||
let my_size = LAYOUT.key_sizes[row][col];
|
||||
let x = unit_size * sum_size + BUTTON_PADDING;
|
||||
let w = unit_size * my_size - 2. * BUTTON_PADDING;
|
||||
let x = unit_size.mul_add(sum_size, BUTTON_PADDING);
|
||||
let w = unit_size.mul_add(my_size, -(2. * BUTTON_PADDING));
|
||||
|
||||
if let Some(key) = LAYOUT.main_layout[row][col].as_ref() {
|
||||
let mut label = Vec::with_capacity(2);
|
||||
@@ -85,7 +138,7 @@ where
|
||||
let label0 = keymap.label_for_key(vk, 0);
|
||||
let label1 = keymap.label_for_key(vk, SHIFT);
|
||||
|
||||
if label0.chars().next().map_or(false, |f| f.is_alphabetic()) {
|
||||
if label0.chars().next().is_some_and(char::is_alphabetic) {
|
||||
label.push(label1);
|
||||
if has_altgr {
|
||||
cap_type = KeyCapType::RegularAltGr;
|
||||
@@ -125,25 +178,27 @@ where
|
||||
});
|
||||
} else if let Some(exec_args) = LAYOUT.exec_commands.get(key) {
|
||||
if exec_args.is_empty() {
|
||||
log::error!("Keyboard: EXEC args empty for {}", key);
|
||||
log::error!("Keyboard: EXEC args empty for {key}");
|
||||
continue;
|
||||
}
|
||||
let mut iter = exec_args.iter().cloned();
|
||||
maybe_state = Some(KeyButtonData::Exec {
|
||||
program: iter.next().unwrap(),
|
||||
args: iter.by_ref().take_while(|arg| arg[..] != *"null").collect(),
|
||||
release_program: iter.next(),
|
||||
release_args: iter.collect(),
|
||||
});
|
||||
if let Some(program) = iter.next() {
|
||||
maybe_state = Some(KeyButtonData::Exec {
|
||||
program,
|
||||
args: iter.by_ref().take_while(|arg| arg[..] != *"null").collect(),
|
||||
release_program: iter.next(),
|
||||
release_args: iter.collect(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log::error!("Unknown key: {}", key);
|
||||
log::error!("Unknown key: {key}");
|
||||
}
|
||||
|
||||
if let Some(state) = maybe_state {
|
||||
if label.is_empty() {
|
||||
label = LAYOUT.label_for_key(key);
|
||||
}
|
||||
let button = canvas.key_button(x, y, w, h, cap_type, &label);
|
||||
let button = canvas.key_button(x, y, w, h, 12., cap_type, &label);
|
||||
button.state = Some(state);
|
||||
button.on_press = Some(key_press);
|
||||
button.on_release = Some(key_release);
|
||||
@@ -174,7 +229,7 @@ where
|
||||
interaction_transform,
|
||||
..Default::default()
|
||||
},
|
||||
backend: Box::new(canvas),
|
||||
backend: Box::new(KeyboardBackend { canvas }),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -187,26 +242,29 @@ fn key_press(
|
||||
) {
|
||||
match control.state.as_mut() {
|
||||
Some(KeyButtonData::Key { vk, pressed }) => {
|
||||
data.key_click(app);
|
||||
key_click(app);
|
||||
|
||||
if let PointerMode::Right = mode {
|
||||
data.modifiers |= SHIFT;
|
||||
app.hid_provider.set_modifiers(data.modifiers);
|
||||
}
|
||||
data.modifiers |= match mode {
|
||||
PointerMode::Right => SHIFT,
|
||||
PointerMode::Middle => data.alt_modifier,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
app.hid_provider.send_key(*vk, true);
|
||||
app.hid_provider.set_modifiers(data.modifiers);
|
||||
|
||||
send_key(app, *vk, true);
|
||||
*pressed = true;
|
||||
}
|
||||
Some(KeyButtonData::Modifier { modifier, sticky }) => {
|
||||
*sticky = data.modifiers & *modifier == 0;
|
||||
data.modifiers |= *modifier;
|
||||
data.key_click(app);
|
||||
app.hid_provider.set_modifiers(data.modifiers);
|
||||
key_click(app);
|
||||
set_modifiers(app, data.modifiers);
|
||||
}
|
||||
Some(KeyButtonData::Macro { verbs }) => {
|
||||
data.key_click(app);
|
||||
key_click(app);
|
||||
for (vk, press) in verbs {
|
||||
app.hid_provider.send_key(*vk, *press);
|
||||
send_key(app, *vk, *press);
|
||||
}
|
||||
}
|
||||
Some(KeyButtonData::Exec { program, args, .. }) => {
|
||||
@@ -214,7 +272,7 @@ fn key_press(
|
||||
data.processes
|
||||
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
|
||||
|
||||
data.key_click(app);
|
||||
key_click(app);
|
||||
if let Ok(child) = Command::new(program).args(args).spawn() {
|
||||
data.processes.push(child);
|
||||
}
|
||||
@@ -230,20 +288,20 @@ fn key_release(
|
||||
) {
|
||||
match control.state.as_mut() {
|
||||
Some(KeyButtonData::Key { vk, pressed }) => {
|
||||
app.hid_provider.send_key(*vk, false);
|
||||
send_key(app, *vk, false);
|
||||
*pressed = false;
|
||||
|
||||
for m in AUTO_RELEASE_MODS.iter() {
|
||||
for m in &AUTO_RELEASE_MODS {
|
||||
if data.modifiers & *m != 0 {
|
||||
data.modifiers &= !*m;
|
||||
app.hid_provider.set_modifiers(data.modifiers);
|
||||
set_modifiers(app, data.modifiers);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(KeyButtonData::Modifier { modifier, sticky }) => {
|
||||
if !*sticky {
|
||||
data.modifiers &= !*modifier;
|
||||
app.hid_provider.set_modifiers(data.modifiers);
|
||||
set_modifiers(app, data.modifiers);
|
||||
}
|
||||
}
|
||||
Some(KeyButtonData::Exec {
|
||||
@@ -265,7 +323,7 @@ fn key_release(
|
||||
}
|
||||
}
|
||||
|
||||
static PRESS_COLOR: Vec4 = Vec4::new(1.0, 1.0, 1.0, 0.5);
|
||||
static PRESS_COLOR: Vec4 = Vec4::new(198. / 255., 160. / 255., 246. / 255., 0.5);
|
||||
|
||||
fn test_highlight(
|
||||
control: &Control<KeyboardData, KeyButtonData>,
|
||||
@@ -287,16 +345,15 @@ fn test_highlight(
|
||||
|
||||
struct KeyboardData {
|
||||
modifiers: KeyModifier,
|
||||
alt_modifier: KeyModifier,
|
||||
processes: Vec<Child>,
|
||||
}
|
||||
|
||||
const KEY_AUDIO_WAV: &[u8] = include_bytes!("../res/421581.wav");
|
||||
|
||||
impl KeyboardData {
|
||||
fn key_click(&mut self, app: &mut AppState) {
|
||||
if app.session.config.keyboard_sound_enabled {
|
||||
app.audio.play(KEY_AUDIO_WAV);
|
||||
}
|
||||
fn key_click(app: &mut AppState) {
|
||||
if app.session.config.keyboard_sound_enabled {
|
||||
app.audio.play(KEY_AUDIO_WAV);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,25 +377,40 @@ enum KeyButtonData {
|
||||
},
|
||||
}
|
||||
|
||||
static LAYOUT: Lazy<Layout> = Lazy::new(Layout::load_from_disk);
|
||||
static LAYOUT: LazyLock<Layout> = LazyLock::new(Layout::load_from_disk);
|
||||
|
||||
static MACRO_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^([A-Za-z0-1_-]+)(?: +(UP|DOWN))?$").unwrap()); // want panic
|
||||
static MACRO_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^([A-Za-z0-9_-]+)(?: +(UP|DOWN))?$").unwrap()); // want panic
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
|
||||
#[repr(usize)]
|
||||
pub enum AltModifier {
|
||||
#[default]
|
||||
None,
|
||||
Shift,
|
||||
Ctrl,
|
||||
Alt,
|
||||
Super,
|
||||
Meta,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct Layout {
|
||||
name: String,
|
||||
row_size: f32,
|
||||
key_sizes: Vec<Vec<f32>>,
|
||||
main_layout: Vec<Vec<Option<String>>>,
|
||||
alt_modifier: AltModifier,
|
||||
exec_commands: HashMap<String, Vec<String>>,
|
||||
macros: HashMap<String, Vec<String>>,
|
||||
labels: HashMap<String, Vec<String>>,
|
||||
auto_labels: Option<bool>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
fn load_from_disk() -> Layout {
|
||||
let mut layout = config::load_known_yaml::<Layout>(ConfigType::Keyboard);
|
||||
fn load_from_disk() -> Self {
|
||||
let mut layout = config::load_known_yaml::<Self>(ConfigType::Keyboard);
|
||||
layout.post_load();
|
||||
layout
|
||||
}
|
||||
@@ -347,25 +419,25 @@ impl Layout {
|
||||
for i in 0..self.key_sizes.len() {
|
||||
let row = &self.key_sizes[i];
|
||||
let width: f32 = row.iter().sum();
|
||||
if (width - self.row_size).abs() > 0.001 {
|
||||
panic!(
|
||||
"Row {} has a width of {}, but the row size is {}",
|
||||
i, width, self.row_size
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
(width - self.row_size).abs() < 0.001,
|
||||
"Row {} has a width of {}, but the row size is {}",
|
||||
i,
|
||||
width,
|
||||
self.row_size
|
||||
);
|
||||
}
|
||||
|
||||
for i in 0..self.main_layout.len() {
|
||||
let row = &self.main_layout[i];
|
||||
let width = row.len();
|
||||
if width != self.key_sizes[i].len() {
|
||||
panic!(
|
||||
"Row {} has {} keys, needs to have {} according to key_sizes",
|
||||
i,
|
||||
width,
|
||||
self.key_sizes[i].len()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
(width == self.key_sizes[i].len()),
|
||||
"Row {} has {} keys, needs to have {} according to key_sizes",
|
||||
i,
|
||||
width,
|
||||
self.key_sizes[i].len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,10 +457,7 @@ impl Layout {
|
||||
}
|
||||
if key.contains('_') {
|
||||
key = key.split('_').next().unwrap_or_else(|| {
|
||||
log::error!(
|
||||
"keyboard.yaml: Key '{}' must not start or end with '_'!",
|
||||
key
|
||||
);
|
||||
log::error!("keyboard.yaml: Key '{key}' must not start or end with '_'!");
|
||||
"???"
|
||||
});
|
||||
}
|
||||
@@ -429,3 +498,75 @@ fn key_events_for_macro(macro_verbs: &Vec<String>) -> Vec<(VirtualKey, bool)> {
|
||||
}
|
||||
key_events
|
||||
}
|
||||
|
||||
struct KeyboardBackend {
|
||||
canvas: Canvas<KeyboardData, KeyButtonData>,
|
||||
}
|
||||
|
||||
impl OverlayBackend for KeyboardBackend {
|
||||
fn set_interaction(&mut self, interaction: Box<dyn crate::backend::input::InteractionHandler>) {
|
||||
self.canvas.set_interaction(interaction);
|
||||
}
|
||||
fn set_renderer(&mut self, renderer: Box<dyn crate::backend::overlay::OverlayRenderer>) {
|
||||
self.canvas.set_renderer(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractionHandler for KeyboardBackend {
|
||||
fn on_pointer(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
hit: &crate::backend::input::PointerHit,
|
||||
pressed: bool,
|
||||
) {
|
||||
self.canvas.on_pointer(app, hit, pressed);
|
||||
}
|
||||
fn on_scroll(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
hit: &crate::backend::input::PointerHit,
|
||||
delta_y: f32,
|
||||
delta_x: f32,
|
||||
) {
|
||||
self.canvas.on_scroll(app, hit, delta_y, delta_x);
|
||||
}
|
||||
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
|
||||
self.canvas.on_left(app, pointer);
|
||||
}
|
||||
fn on_hover(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
hit: &crate::backend::input::PointerHit,
|
||||
) -> Option<crate::backend::input::Haptics> {
|
||||
self.canvas.on_hover(app, hit)
|
||||
}
|
||||
}
|
||||
|
||||
impl OverlayRenderer for KeyboardBackend {
|
||||
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.canvas.init(app)
|
||||
}
|
||||
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
|
||||
self.canvas.should_render(app)
|
||||
}
|
||||
fn render(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
tgt: Arc<ImageView>,
|
||||
buf: &mut CommandBuffers,
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
self.canvas.render(app, tgt, buf, alpha)
|
||||
}
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
self.canvas.frame_meta()
|
||||
}
|
||||
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.canvas.data_mut().modifiers = 0;
|
||||
set_modifiers(app, 0);
|
||||
self.canvas.pause(app)
|
||||
}
|
||||
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.canvas.resume(app)
|
||||
}
|
||||
}
|
||||
+57
-37
@@ -4,17 +4,19 @@ use std::{
|
||||
};
|
||||
|
||||
use futures::{Future, FutureExt};
|
||||
use glam::vec3a;
|
||||
use vulkano::image::view::ImageView;
|
||||
use wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireSelectScreenResult};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
common::OverlaySelector,
|
||||
overlay::{
|
||||
ui_transform, OverlayBackend, OverlayRenderer, OverlayState, SplitOverlayBackend,
|
||||
ui_transform, FrameMeta, OverlayBackend, OverlayRenderer, OverlayState, ShouldRender,
|
||||
SplitOverlayBackend,
|
||||
},
|
||||
task::TaskType,
|
||||
},
|
||||
graphics::CommandBuffers,
|
||||
state::{AppSession, AppState},
|
||||
};
|
||||
|
||||
@@ -45,7 +47,18 @@ impl OverlayRenderer for MirrorRenderer {
|
||||
fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
|
||||
self.renderer
|
||||
.as_mut()
|
||||
.map_or(Ok(ShouldRender::Unable), |r| r.should_render(app))
|
||||
}
|
||||
fn render(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
tgt: Arc<ImageView>,
|
||||
buf: &mut CommandBuffers,
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
if let Some(mut selector) = self.selector.take() {
|
||||
let maybe_pw_result = match selector
|
||||
.poll_unpin(&mut Context::from_waker(futures::task::noop_waker_ref()))
|
||||
@@ -53,55 +66,59 @@ impl OverlayRenderer for MirrorRenderer {
|
||||
Poll::Ready(result) => result,
|
||||
Poll::Pending => {
|
||||
self.selector = Some(selector);
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(pw_result) = maybe_pw_result {
|
||||
let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
|
||||
log::info!("{}: PipeWire node selected: {}", self.name.clone(), node_id,);
|
||||
let capture = PipewireCapture::new(self.name.clone(), node_id);
|
||||
self.renderer = Some(ScreenRenderer::new_raw(
|
||||
self.name.clone(),
|
||||
Box::new(capture),
|
||||
));
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
OverlaySelector::Name(self.name.clone()),
|
||||
Box::new(|app, o| {
|
||||
o.grabbable = true;
|
||||
o.interactable = true;
|
||||
o.reset(app, false);
|
||||
}),
|
||||
));
|
||||
} else {
|
||||
log::warn!("Failed to create pipewire mirror");
|
||||
self.renderer = None;
|
||||
// drop self
|
||||
app.tasks
|
||||
.enqueue(TaskType::DropOverlay(OverlaySelector::Name(
|
||||
match maybe_pw_result {
|
||||
Ok(pw_result) => {
|
||||
let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
|
||||
log::info!("{}: PipeWire node selected: {}", self.name.clone(), node_id);
|
||||
let capture = PipewireCapture::new(self.name.clone(), node_id);
|
||||
self.renderer = Some(ScreenRenderer::new_raw(
|
||||
self.name.clone(),
|
||||
)));
|
||||
Box::new(capture),
|
||||
));
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
OverlaySelector::Name(self.name.clone()),
|
||||
Box::new(|app, o| {
|
||||
o.grabbable = true;
|
||||
o.interactable = true;
|
||||
o.reset(app, false);
|
||||
}),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to create mirror due to PipeWire error: {e:?}");
|
||||
self.renderer = None;
|
||||
// drop self
|
||||
app.tasks
|
||||
.enqueue(TaskType::DropOverlay(OverlaySelector::Name(
|
||||
self.name.clone(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = false;
|
||||
if let Some(renderer) = self.renderer.as_mut() {
|
||||
renderer.render(app)?;
|
||||
if let Some(view) = renderer.view() {
|
||||
let extent = view.image().extent();
|
||||
result = renderer.render(app, tgt, buf, alpha)?;
|
||||
if let Some(meta) = renderer.frame_meta() {
|
||||
let extent = meta.extent;
|
||||
if self.last_extent != extent {
|
||||
self.last_extent = extent;
|
||||
// resized
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
OverlaySelector::Name(self.name.clone()),
|
||||
Box::new(move |_app, o| {
|
||||
o.interaction_transform = ui_transform(&[extent[0], extent[1]]);
|
||||
o.interaction_transform = ui_transform([extent[0], extent[1]]);
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
if let Some(renderer) = self.renderer.as_mut() {
|
||||
@@ -115,8 +132,12 @@ impl OverlayRenderer for MirrorRenderer {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn view(&mut self) -> Option<std::sync::Arc<vulkano::image::view::ImageView>> {
|
||||
self.renderer.as_mut().and_then(|r| r.view())
|
||||
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
Some(FrameMeta {
|
||||
extent: self.last_extent,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,13 +145,12 @@ pub fn new_mirror(
|
||||
name: Arc<str>,
|
||||
show_hide: bool,
|
||||
session: &AppSession,
|
||||
) -> Option<(OverlayState, Box<dyn OverlayBackend>)> {
|
||||
) -> (OverlayState, Box<dyn OverlayBackend>) {
|
||||
let state = OverlayState {
|
||||
name: name.clone(),
|
||||
show_hide,
|
||||
want_visible: true,
|
||||
spawn_scale: 0.5 * session.config.desktop_view_scale,
|
||||
spawn_point: vec3a(0., 0.5, 0.5),
|
||||
..Default::default()
|
||||
};
|
||||
let backend = Box::new(SplitOverlayBackend {
|
||||
@@ -138,5 +158,5 @@ pub fn new_mirror(
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Some((state, backend))
|
||||
(state, backend)
|
||||
}
|
||||
@@ -6,3 +6,6 @@ pub mod mirror;
|
||||
pub mod screen;
|
||||
pub mod toast;
|
||||
pub mod watch;
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
pub mod wayvr;
|
||||
+552
-366
File diff suppressed because it is too large.
Load diff
+63
-38
@@ -1,25 +1,28 @@
|
||||
use std::{f32::consts::PI, ops::Add, sync::Arc, time::Instant};
|
||||
use std::{
|
||||
f32::consts::PI,
|
||||
ops::Add,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use glam::{vec3a, Quat};
|
||||
use idmap_derive::IntegerId;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
common::OverlaySelector,
|
||||
overlay::{OverlayBackend, OverlayState, RelativeTo},
|
||||
overlay::{OverlayBackend, OverlayState, RelativeTo, Z_ORDER_TOAST},
|
||||
task::TaskType,
|
||||
},
|
||||
gui::{color_parse, CanvasBuilder},
|
||||
gui::{canvas::builder::CanvasBuilder, color_parse},
|
||||
state::{AppState, LeftRight},
|
||||
};
|
||||
|
||||
const FONT_SIZE: isize = 16;
|
||||
const PADDING: (f32, f32) = (25., 7.);
|
||||
const PIXELS_TO_METERS: f32 = 1. / 2000.;
|
||||
const TOAST_AUDIO_WAV: &[u8] = include_bytes!("../res/557297.wav");
|
||||
static TOAST_NAME: Lazy<Arc<str>> = Lazy::new(|| "toast".into());
|
||||
static TOAST_NAME: LazyLock<Arc<str>> = LazyLock::new(|| "toast".into());
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum DisplayMethod {
|
||||
@@ -47,8 +50,8 @@ pub struct Toast {
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Toast {
|
||||
pub fn new(topic: ToastTopic, title: Arc<str>, body: Arc<str>) -> Self {
|
||||
Toast {
|
||||
pub const fn new(topic: ToastTopic, title: Arc<str>, body: Arc<str>) -> Self {
|
||||
Self {
|
||||
title,
|
||||
body,
|
||||
opacity: 1.0,
|
||||
@@ -57,15 +60,15 @@ impl Toast {
|
||||
topic,
|
||||
}
|
||||
}
|
||||
pub fn with_timeout(mut self, timeout: f32) -> Self {
|
||||
pub const fn with_timeout(mut self, timeout: f32) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
pub fn with_opacity(mut self, opacity: f32) -> Self {
|
||||
pub const fn with_opacity(mut self, opacity: f32) -> Self {
|
||||
self.opacity = opacity;
|
||||
self
|
||||
}
|
||||
pub fn with_sound(mut self, sound: bool) -> Self {
|
||||
pub const fn with_sound(mut self, sound: bool) -> Self {
|
||||
self.sound = sound;
|
||||
self
|
||||
}
|
||||
@@ -78,6 +81,9 @@ impl Toast {
|
||||
let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout));
|
||||
|
||||
let has_sound = self.sound && app.session.config.notifications_sound_enabled;
|
||||
if has_sound {
|
||||
app.audio.play(app.toast_sound);
|
||||
}
|
||||
|
||||
// drop any toast that was created before us.
|
||||
// (DropOverlay only drops overlays that were
|
||||
@@ -91,7 +97,7 @@ impl Toast {
|
||||
// frame, only the first one gets created
|
||||
app.tasks.enqueue_at(
|
||||
TaskType::CreateOverlay(
|
||||
selector.clone(),
|
||||
selector,
|
||||
Box::new(move |app| {
|
||||
let mut maybe_toast = new_toast(self, app);
|
||||
if let Some((state, _)) = maybe_toast.as_mut() {
|
||||
@@ -108,10 +114,6 @@ impl Toast {
|
||||
),
|
||||
instant,
|
||||
);
|
||||
|
||||
if has_sound {
|
||||
app.audio.play(TOAST_AUDIO_WAV);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,13 +143,19 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
|
||||
}
|
||||
};
|
||||
|
||||
let title = if toast.title.len() > 0 {
|
||||
toast.title
|
||||
} else {
|
||||
let title = if toast.title.is_empty() {
|
||||
"Notification".into()
|
||||
} else {
|
||||
toast.title
|
||||
};
|
||||
|
||||
let mut size = if toast.body.len() > 0 {
|
||||
let mut size = if toast.body.is_empty() {
|
||||
let (w, h) = app
|
||||
.fc
|
||||
.get_text_size(&title, FONT_SIZE, app.graphics.clone())
|
||||
.ok()?;
|
||||
(w, h + 20.)
|
||||
} else {
|
||||
let (w0, _) = app
|
||||
.fc
|
||||
.get_text_size(&title, FONT_SIZE, app.graphics.clone())
|
||||
@@ -157,12 +165,6 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
|
||||
.get_text_size(&toast.body, FONT_SIZE, app.graphics.clone())
|
||||
.ok()?;
|
||||
(w0.max(w1), h1 + 50.)
|
||||
} else {
|
||||
let (w, h) = app
|
||||
.fc
|
||||
.get_text_size(&title, FONT_SIZE, app.graphics.clone())
|
||||
.ok()?;
|
||||
(w, h + 20.)
|
||||
};
|
||||
|
||||
let og_width = size.0;
|
||||
@@ -178,19 +180,19 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
|
||||
.ok()?;
|
||||
|
||||
canvas.font_size = FONT_SIZE;
|
||||
canvas.fg_color = color_parse("#aaaaaa").unwrap(); // want panic
|
||||
canvas.bg_color = color_parse("#333333").unwrap(); // want panic
|
||||
canvas.panel(0., 0., size.0, size.1);
|
||||
canvas.fg_color = color_parse("#cad3f5").unwrap(); // want panic
|
||||
canvas.bg_color = color_parse("#1e2030").unwrap(); // want panic
|
||||
canvas.panel(0., 0., size.0, size.1, 16.);
|
||||
|
||||
if toast.body.len() > 0 {
|
||||
canvas.label(PADDING.0, 54., og_width, size.1 - 54., toast.body);
|
||||
|
||||
canvas.fg_color = color_parse("#101010").unwrap(); // want panic
|
||||
canvas.bg_color = color_parse("#666666").unwrap(); // want panic
|
||||
canvas.panel(0., 0., size.0, 30.);
|
||||
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., title);
|
||||
if toast.body.is_empty() {
|
||||
canvas.label_centered(PADDING.0, 0., og_width, size.1, 16., title);
|
||||
} else {
|
||||
canvas.label_centered(PADDING.0, 0., og_width, size.1, title);
|
||||
canvas.label(PADDING.0, 54., og_width, size.1 - 54., 3., toast.body);
|
||||
|
||||
canvas.fg_color = color_parse("#b8c0e0").unwrap(); // want panic
|
||||
canvas.bg_color = color_parse("#24273a").unwrap(); // want panic
|
||||
canvas.panel(0., 0., size.0, 30., 16.);
|
||||
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., 16., title);
|
||||
}
|
||||
|
||||
let state = OverlayState {
|
||||
@@ -199,7 +201,7 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
|
||||
spawn_scale: size.0 * PIXELS_TO_METERS,
|
||||
spawn_rotation,
|
||||
spawn_point,
|
||||
z_order: 70,
|
||||
z_order: Z_ORDER_TOAST,
|
||||
relative_to,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -207,3 +209,26 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
|
||||
|
||||
Some((state, backend))
|
||||
}
|
||||
|
||||
fn msg_err(app: &mut AppState, message: &str) {
|
||||
Toast::new(ToastTopic::System, "Error".into(), message.into())
|
||||
.with_timeout(3.)
|
||||
.submit(app);
|
||||
}
|
||||
|
||||
// Display the same error in the terminal and as a toast in VR.
|
||||
// Formatted as "Failed to XYZ: Object is not defined"
|
||||
pub fn error_toast<ErrorType>(app: &mut AppState, title: &str, err: ErrorType)
|
||||
where
|
||||
ErrorType: std::fmt::Display + std::fmt::Debug,
|
||||
{
|
||||
log::error!("{title}: {err:?}"); // More detailed version (use Debug)
|
||||
|
||||
// Brief version (use Display)
|
||||
msg_err(app, &format!("{title}: {err}"));
|
||||
}
|
||||
|
||||
pub fn error_toast_str(app: &mut AppState, message: &str) {
|
||||
log::error!("{message}");
|
||||
msg_err(app, message);
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
use glam::Vec3A;
|
||||
|
||||
use crate::{
|
||||
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
|
||||
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo, Z_ORDER_WATCH},
|
||||
config::{load_known_yaml, ConfigType},
|
||||
gui::{
|
||||
canvas::Canvas,
|
||||
modular::{modular_canvas, ModularData, ModularUiConfig},
|
||||
Canvas,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub const WATCH_NAME: &str = "watch";
|
||||
|
||||
pub fn create_watch<O>(state: &AppState) -> anyhow::Result<OverlayData<O>>
|
||||
pub fn create_watch<O>(state: &mut AppState) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
@@ -25,11 +25,11 @@ where
|
||||
name: WATCH_NAME.into(),
|
||||
want_visible: true,
|
||||
interactable: true,
|
||||
z_order: 68,
|
||||
z_order: Z_ORDER_WATCH,
|
||||
spawn_scale: config.width,
|
||||
spawn_point: state.session.config.watch_pos,
|
||||
spawn_rotation: state.session.config.watch_rot,
|
||||
interaction_transform: ui_transform(&config.size),
|
||||
interaction_transform: ui_transform(config.size),
|
||||
relative_to,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -40,11 +40,11 @@ where
|
||||
|
||||
pub fn create_watch_canvas(
|
||||
config: Option<ModularUiConfig>,
|
||||
state: &AppState,
|
||||
state: &mut AppState,
|
||||
) -> anyhow::Result<Canvas<(), ModularData>> {
|
||||
let config = config.unwrap_or_else(|| load_known_yaml::<ModularUiConfig>(ConfigType::Watch));
|
||||
|
||||
modular_canvas(&config.size, &config.elements, state)
|
||||
modular_canvas(config.size, &config.elements, state)
|
||||
}
|
||||
|
||||
pub fn watch_fade<D>(app: &mut AppState, watch: &mut OverlayData<D>)
|
||||
|
||||
@@ -0,0 +1,952 @@
|
||||
use glam::{vec3a, Affine2, Vec3, Vec3A};
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
|
||||
use vulkano::{
|
||||
command_buffer::CommandBufferUsage,
|
||||
image::{view::ImageView, SubresourceLayout},
|
||||
};
|
||||
use wayvr_ipc::packet_server::{self, PacketServer, WvrStateChanged};
|
||||
use wlx_capture::frame::{DmabufFrame, FourCC, FrameFormat, FramePlane};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
common::{OverlayContainer, OverlaySelector},
|
||||
input::{self, InteractionHandler},
|
||||
overlay::{
|
||||
ui_transform, FrameMeta, OverlayData, OverlayID, OverlayRenderer, OverlayState,
|
||||
ShouldRender, SplitOverlayBackend, Z_ORDER_DASHBOARD,
|
||||
},
|
||||
task::TaskType,
|
||||
wayvr::{
|
||||
self, display,
|
||||
server_ipc::{gen_args_vec, gen_env_vec},
|
||||
WayVR,
|
||||
},
|
||||
},
|
||||
config_wayvr,
|
||||
graphics::{CommandBuffers, WlxGraphics, WlxPipeline, SWAPCHAIN_FORMAT},
|
||||
gui::modular::button::{WayVRAction, WayVRDisplayClickAction},
|
||||
state::{self, AppState, KeyboardFocus},
|
||||
};
|
||||
|
||||
use super::toast::error_toast;
|
||||
|
||||
// Hard-coded for now
|
||||
const DASHBOARD_WIDTH: u16 = 1920;
|
||||
const DASHBOARD_HEIGHT: u16 = 1080;
|
||||
const DASHBOARD_DISPLAY_NAME: &str = "_DASHBOARD";
|
||||
|
||||
pub struct WayVRContext {
|
||||
wayvr: Rc<RefCell<WayVRData>>,
|
||||
display: wayvr::display::DisplayHandle,
|
||||
}
|
||||
|
||||
impl WayVRContext {
|
||||
pub const fn new(wvr: Rc<RefCell<WayVRData>>, display: wayvr::display::DisplayHandle) -> Self {
|
||||
Self {
|
||||
wayvr: wvr,
|
||||
display,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OverlayToCreate {
|
||||
pub conf_display: config_wayvr::WayVRDisplay,
|
||||
pub disp_handle: display::DisplayHandle,
|
||||
}
|
||||
|
||||
pub struct WayVRData {
|
||||
display_handle_map: HashMap<display::DisplayHandle, OverlayID>,
|
||||
overlays_to_create: Vec<OverlayToCreate>,
|
||||
dashboard_executed: bool,
|
||||
pub data: WayVR,
|
||||
}
|
||||
|
||||
impl WayVRData {
|
||||
pub fn new(config: wayvr::Config) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
display_handle_map: HashMap::default(),
|
||||
data: WayVR::new(config)?,
|
||||
overlays_to_create: Vec::new(),
|
||||
dashboard_executed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_unique_display_name(&self, mut candidate: String) -> String {
|
||||
let mut num = 0;
|
||||
|
||||
while !self
|
||||
.data
|
||||
.state
|
||||
.displays
|
||||
.vec
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|d| d.obj.name == candidate)
|
||||
{
|
||||
if num > 0 {
|
||||
candidate = format!("{candidate} ({num})");
|
||||
}
|
||||
num += 1;
|
||||
}
|
||||
|
||||
candidate
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WayVRInteractionHandler {
|
||||
context: Rc<RefCell<WayVRContext>>,
|
||||
mouse_transform: Affine2,
|
||||
}
|
||||
|
||||
impl WayVRInteractionHandler {
|
||||
pub const fn new(context: Rc<RefCell<WayVRContext>>, mouse_transform: Affine2) -> Self {
|
||||
Self {
|
||||
context,
|
||||
mouse_transform,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractionHandler for WayVRInteractionHandler {
|
||||
fn on_hover(
|
||||
&mut self,
|
||||
_app: &mut state::AppState,
|
||||
hit: &input::PointerHit,
|
||||
) -> Option<input::Haptics> {
|
||||
let ctx = self.context.borrow();
|
||||
|
||||
let wayvr = &mut ctx.wayvr.borrow_mut().data;
|
||||
|
||||
if let Some(disp) = wayvr.state.displays.get(&ctx.display) {
|
||||
let pos = self.mouse_transform.transform_point2(hit.uv);
|
||||
let x = ((pos.x * f32::from(disp.width)) as i32).max(0);
|
||||
let y = ((pos.y * f32::from(disp.height)) as i32).max(0);
|
||||
|
||||
let ctx = self.context.borrow();
|
||||
wayvr.state.send_mouse_move(ctx.display, x as u32, y as u32);
|
||||
}
|
||||
|
||||
wayvr.state.pending_haptic.take()
|
||||
}
|
||||
|
||||
fn on_left(&mut self, _app: &mut state::AppState, _pointer: usize) {
|
||||
// Ignore event
|
||||
}
|
||||
|
||||
fn on_pointer(&mut self, _app: &mut state::AppState, hit: &input::PointerHit, pressed: bool) {
|
||||
if let Some(index) = match hit.mode {
|
||||
input::PointerMode::Left => Some(wayvr::MouseIndex::Left),
|
||||
input::PointerMode::Middle => Some(wayvr::MouseIndex::Center),
|
||||
input::PointerMode::Right => Some(wayvr::MouseIndex::Right),
|
||||
_ => {
|
||||
// Unknown pointer event, ignore
|
||||
None
|
||||
}
|
||||
} {
|
||||
let ctx = self.context.borrow();
|
||||
let wayvr = &mut ctx.wayvr.borrow_mut().data;
|
||||
if pressed {
|
||||
wayvr.state.send_mouse_down(ctx.display, index);
|
||||
} else {
|
||||
wayvr.state.send_mouse_up(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_scroll(
|
||||
&mut self,
|
||||
_app: &mut state::AppState,
|
||||
_hit: &input::PointerHit,
|
||||
delta_y: f32,
|
||||
delta_x: f32,
|
||||
) {
|
||||
let ctx = self.context.borrow();
|
||||
ctx.wayvr
|
||||
.borrow_mut()
|
||||
.data
|
||||
.state
|
||||
.send_mouse_scroll(delta_y, delta_x);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WayVRRenderer {
|
||||
pipeline: Arc<WlxPipeline>,
|
||||
vk_image: Option<Arc<vulkano::image::Image>>,
|
||||
vk_image_view: Option<Arc<vulkano::image::view::ImageView>>,
|
||||
context: Rc<RefCell<WayVRContext>>,
|
||||
graphics: Arc<WlxGraphics>,
|
||||
}
|
||||
|
||||
impl WayVRRenderer {
|
||||
pub fn new(
|
||||
app: &state::AppState,
|
||||
wvr: Rc<RefCell<WayVRData>>,
|
||||
display: wayvr::display::DisplayHandle,
|
||||
) -> anyhow::Result<Self> {
|
||||
let Ok(shaders) = app.graphics.shared_shaders.read() else {
|
||||
anyhow::bail!("Failed to lock shared shaders for reading");
|
||||
};
|
||||
|
||||
let pipeline = app.graphics.create_pipeline(
|
||||
shaders.get("vert_common").unwrap().clone(), // want panic
|
||||
shaders.get("frag_srgb").unwrap().clone(), // want panic
|
||||
SWAPCHAIN_FORMAT,
|
||||
None,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
pipeline,
|
||||
context: Rc::new(RefCell::new(WayVRContext::new(wvr, display))),
|
||||
vk_image: None,
|
||||
vk_image_view: None,
|
||||
graphics: app.graphics.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_create_display_by_name(
|
||||
app: &mut AppState,
|
||||
wayvr: &mut WayVRData,
|
||||
disp_name: &str,
|
||||
) -> anyhow::Result<display::DisplayHandle> {
|
||||
let disp_handle =
|
||||
if let Some(disp) = WayVR::get_display_by_name(&wayvr.data.state.displays, disp_name) {
|
||||
disp
|
||||
} else {
|
||||
let conf_display = app
|
||||
.session
|
||||
.wayvr_config
|
||||
.get_display(disp_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Cannot find display named \"{}\"", disp_name))?
|
||||
.clone();
|
||||
|
||||
let disp_handle = wayvr.data.state.create_display(
|
||||
conf_display.width,
|
||||
conf_display.height,
|
||||
disp_name,
|
||||
conf_display.primary.unwrap_or(false),
|
||||
)?;
|
||||
|
||||
wayvr.overlays_to_create.push(OverlayToCreate {
|
||||
conf_display,
|
||||
disp_handle,
|
||||
});
|
||||
|
||||
disp_handle
|
||||
};
|
||||
|
||||
Ok(disp_handle)
|
||||
}
|
||||
|
||||
fn executable_exists_in_path(command: &str) -> bool {
|
||||
let Ok(path) = std::env::var("PATH") else {
|
||||
return false; // very unlikely to happen
|
||||
};
|
||||
for dir in path.split(':') {
|
||||
let exec_path = std::path::PathBuf::from(dir).join(command);
|
||||
if exec_path.exists() && exec_path.is_file() {
|
||||
return true; // executable found
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn toggle_dashboard<O>(
|
||||
app: &mut AppState,
|
||||
overlays: &mut OverlayContainer<O>,
|
||||
wayvr: &mut WayVRData,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let conf_dash = &app.session.wayvr_config.dashboard;
|
||||
|
||||
let Some(conf_dash) = &conf_dash else {
|
||||
anyhow::bail!("Dashboard is not configured");
|
||||
};
|
||||
|
||||
if !wayvr.dashboard_executed && !executable_exists_in_path(&conf_dash.exec) {
|
||||
anyhow::bail!("Executable \"{}\" not found", &conf_dash.exec);
|
||||
}
|
||||
|
||||
let (newly_created, disp_handle) = wayvr.data.state.get_or_create_dashboard_display(
|
||||
DASHBOARD_WIDTH,
|
||||
DASHBOARD_HEIGHT,
|
||||
DASHBOARD_DISPLAY_NAME,
|
||||
)?;
|
||||
|
||||
if newly_created {
|
||||
log::info!("Creating dashboard overlay");
|
||||
|
||||
let mut overlay = create_overlay::<O>(
|
||||
app,
|
||||
wayvr,
|
||||
DASHBOARD_DISPLAY_NAME,
|
||||
OverlayToCreate {
|
||||
disp_handle,
|
||||
conf_display: config_wayvr::WayVRDisplay {
|
||||
attach_to: None,
|
||||
width: DASHBOARD_WIDTH,
|
||||
height: DASHBOARD_HEIGHT,
|
||||
scale: None,
|
||||
rotation: None,
|
||||
pos: None,
|
||||
primary: None,
|
||||
},
|
||||
},
|
||||
)?;
|
||||
|
||||
overlay.state.curvature = Some(0.15);
|
||||
overlay.state.want_visible = true;
|
||||
overlay.state.spawn_scale = 2.0;
|
||||
overlay.state.spawn_point = vec3a(0.0, -0.35, -1.75);
|
||||
overlay.state.z_order = Z_ORDER_DASHBOARD;
|
||||
overlay.state.reset(app, true);
|
||||
|
||||
let Some(conf_dash) = &app.session.wayvr_config.dashboard else {
|
||||
unreachable!(); /* safe, not possible to trigger */
|
||||
};
|
||||
|
||||
overlays.add(overlay);
|
||||
|
||||
let args_vec = &conf_dash
|
||||
.args
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |args| gen_args_vec(args.as_str()));
|
||||
|
||||
let env_vec = &conf_dash
|
||||
.env
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |env| gen_env_vec(env));
|
||||
|
||||
let mut userdata = HashMap::new();
|
||||
userdata.insert(String::from("type"), String::from("dashboard"));
|
||||
|
||||
// Start dashboard specified in the WayVR config
|
||||
let _process_handle_unused = wayvr.data.state.spawn_process(
|
||||
disp_handle,
|
||||
&conf_dash.exec,
|
||||
args_vec,
|
||||
env_vec,
|
||||
userdata,
|
||||
)?;
|
||||
|
||||
wayvr.dashboard_executed = true;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let display = wayvr.data.state.displays.get(&disp_handle).unwrap(); // safe
|
||||
let Some(overlay_id) = display.overlay_id else {
|
||||
anyhow::bail!("Overlay ID not set for dashboard display");
|
||||
};
|
||||
|
||||
let cur_visibility = !display.visible;
|
||||
|
||||
wayvr
|
||||
.data
|
||||
.ipc_server
|
||||
.broadcast(PacketServer::WvrStateChanged(if cur_visibility {
|
||||
WvrStateChanged::DashboardShown
|
||||
} else {
|
||||
WvrStateChanged::DashboardHidden
|
||||
}));
|
||||
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
OverlaySelector::Id(overlay_id),
|
||||
Box::new(move |app, o| {
|
||||
// Toggle visibility
|
||||
o.want_visible = cur_visibility;
|
||||
if cur_visibility {
|
||||
o.reset(app, true);
|
||||
}
|
||||
}),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_overlay<O>(
|
||||
app: &mut AppState,
|
||||
data: &mut WayVRData,
|
||||
name: &str,
|
||||
cell: OverlayToCreate,
|
||||
) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let conf_display = &cell.conf_display;
|
||||
let disp_handle = cell.disp_handle;
|
||||
|
||||
let mut overlay = create_wayvr_display_overlay::<O>(
|
||||
app,
|
||||
conf_display.width,
|
||||
conf_display.height,
|
||||
disp_handle,
|
||||
conf_display.scale.unwrap_or(1.0),
|
||||
name,
|
||||
)?;
|
||||
|
||||
data.display_handle_map
|
||||
.insert(disp_handle, overlay.state.id);
|
||||
|
||||
if let Some(attach_to) = &conf_display.attach_to {
|
||||
overlay.state.relative_to = attach_to.get_relative_to();
|
||||
}
|
||||
|
||||
if let Some(rot) = &conf_display.rotation {
|
||||
overlay.state.spawn_rotation =
|
||||
glam::Quat::from_axis_angle(Vec3::from_slice(&rot.axis), f32::to_radians(rot.angle));
|
||||
}
|
||||
|
||||
if let Some(pos) = &conf_display.pos {
|
||||
overlay.state.spawn_point = Vec3A::from_slice(pos);
|
||||
}
|
||||
|
||||
let display = data.data.state.displays.get_mut(&disp_handle).unwrap(); // Never fails
|
||||
display.overlay_id = Some(overlay.state.id);
|
||||
|
||||
Ok(overlay)
|
||||
}
|
||||
|
||||
fn create_queued_displays<O>(
|
||||
app: &mut AppState,
|
||||
data: &mut WayVRData,
|
||||
overlays: &mut OverlayContainer<O>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let overlays_to_create = std::mem::take(&mut data.overlays_to_create);
|
||||
|
||||
for cell in overlays_to_create {
|
||||
let Some(disp) = data.data.state.displays.get(&cell.disp_handle) else {
|
||||
continue; // this shouldn't happen
|
||||
};
|
||||
|
||||
let name = disp.name.clone();
|
||||
|
||||
let overlay = create_overlay::<O>(app, data, name.as_str(), cell)?;
|
||||
overlays.add(overlay); // Insert freshly created WayVR overlay into wlx stack
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn tick_events<O>(app: &mut AppState, overlays: &mut OverlayContainer<O>) -> anyhow::Result<()>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let Some(r_wayvr) = app.wayvr.clone() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut wayvr = r_wayvr.borrow_mut();
|
||||
|
||||
while let Some(signal) = wayvr.data.state.signals.read() {
|
||||
match signal {
|
||||
wayvr::WayVRSignal::DisplayVisibility(display_handle, visible) => {
|
||||
if let Some(overlay_id) = wayvr.display_handle_map.get(&display_handle) {
|
||||
let overlay_id = *overlay_id;
|
||||
wayvr
|
||||
.data
|
||||
.state
|
||||
.set_display_visible(display_handle, visible);
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
OverlaySelector::Id(overlay_id),
|
||||
Box::new(move |_app, o| {
|
||||
o.want_visible = visible;
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
wayvr::WayVRSignal::DisplayWindowLayout(display_handle, layout) => {
|
||||
wayvr.data.state.set_display_layout(display_handle, layout);
|
||||
}
|
||||
wayvr::WayVRSignal::BroadcastStateChanged(packet) => {
|
||||
wayvr
|
||||
.data
|
||||
.ipc_server
|
||||
.broadcast(packet_server::PacketServer::WvrStateChanged(packet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let res = wayvr.data.tick_events(app)?;
|
||||
drop(wayvr);
|
||||
|
||||
for result in res {
|
||||
match result {
|
||||
wayvr::TickTask::NewExternalProcess(request) => {
|
||||
let config = &app.session.wayvr_config;
|
||||
|
||||
let disp_name = request.env.display_name.map_or_else(
|
||||
|| {
|
||||
config
|
||||
.get_default_display()
|
||||
.map(|(display_name, _)| display_name)
|
||||
},
|
||||
|display_name| {
|
||||
config
|
||||
.get_display(display_name.as_str())
|
||||
.map(|_| display_name)
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(disp_name) = disp_name {
|
||||
let mut wayvr = r_wayvr.borrow_mut();
|
||||
|
||||
log::info!("Registering external process with PID {}", request.pid);
|
||||
|
||||
let disp_handle = get_or_create_display_by_name(app, &mut wayvr, &disp_name)?;
|
||||
|
||||
wayvr
|
||||
.data
|
||||
.state
|
||||
.add_external_process(disp_handle, request.pid);
|
||||
|
||||
wayvr
|
||||
.data
|
||||
.state
|
||||
.manager
|
||||
.add_client(wayvr::client::WayVRClient {
|
||||
client: request.client,
|
||||
display_handle: disp_handle,
|
||||
pid: request.pid,
|
||||
});
|
||||
}
|
||||
}
|
||||
wayvr::TickTask::NewDisplay(cpar, disp_handle) => {
|
||||
log::info!("Creating new display with name \"{}\"", cpar.name);
|
||||
|
||||
let mut wayvr = r_wayvr.borrow_mut();
|
||||
|
||||
let unique_name = wayvr.get_unique_display_name(cpar.name);
|
||||
|
||||
let disp_handle = match disp_handle {
|
||||
Some(d) => d,
|
||||
None => wayvr.data.state.create_display(
|
||||
cpar.width,
|
||||
cpar.height,
|
||||
&unique_name,
|
||||
false,
|
||||
)?,
|
||||
};
|
||||
|
||||
wayvr.overlays_to_create.push(OverlayToCreate {
|
||||
disp_handle,
|
||||
conf_display: config_wayvr::WayVRDisplay {
|
||||
attach_to: Some(config_wayvr::AttachTo::from_packet(&cpar.attach_to)),
|
||||
width: cpar.width,
|
||||
height: cpar.height,
|
||||
pos: None,
|
||||
primary: None,
|
||||
rotation: None,
|
||||
scale: cpar.scale,
|
||||
},
|
||||
});
|
||||
}
|
||||
wayvr::TickTask::DropOverlay(overlay_id) => {
|
||||
app.tasks
|
||||
.enqueue(TaskType::DropOverlay(OverlaySelector::Id(overlay_id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut wayvr = r_wayvr.borrow_mut();
|
||||
create_queued_displays(app, &mut wayvr, overlays)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl WayVRRenderer {
|
||||
fn ensure_software_data(
|
||||
&mut self,
|
||||
data: &wayvr::egl_data::RenderSoftwarePixelsData,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut upload = self.graphics.create_uploads_command_buffer(
|
||||
self.graphics.transfer_queue.clone(),
|
||||
CommandBufferUsage::OneTimeSubmit,
|
||||
)?;
|
||||
|
||||
let tex = upload.texture2d_raw(
|
||||
u32::from(data.width),
|
||||
u32::from(data.height),
|
||||
vulkano::format::Format::R8G8B8A8_UNORM,
|
||||
&data.data,
|
||||
)?;
|
||||
|
||||
// FIXME: can we use _buffers_ here?
|
||||
upload.build_and_execute_now()?;
|
||||
|
||||
//buffers.push(upload.build()?);
|
||||
|
||||
self.vk_image = Some(tex.clone());
|
||||
self.vk_image_view = Some(ImageView::new_default(tex).unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_dmabuf_data(
|
||||
&mut self,
|
||||
data: &wayvr::egl_data::RenderDMAbufData,
|
||||
) -> anyhow::Result<()> {
|
||||
if self.vk_image.is_some() {
|
||||
return Ok(()); // already initialized and automatically updated due to direct zero-copy textue access
|
||||
}
|
||||
|
||||
// First init
|
||||
let mut planes = [FramePlane::default(); 4];
|
||||
planes[0].fd = Some(data.fd);
|
||||
planes[0].offset = data.offset as u32;
|
||||
planes[0].stride = data.stride;
|
||||
|
||||
let ctx = self.context.borrow_mut();
|
||||
let wayvr = ctx.wayvr.borrow_mut();
|
||||
let Some(disp) = wayvr.data.state.displays.get(&ctx.display) else {
|
||||
anyhow::bail!("Failed to fetch WayVR display")
|
||||
};
|
||||
|
||||
let frame = DmabufFrame {
|
||||
format: FrameFormat {
|
||||
width: u32::from(disp.width),
|
||||
height: u32::from(disp.height),
|
||||
fourcc: FourCC {
|
||||
value: data.mod_info.fourcc,
|
||||
},
|
||||
modifier: data.mod_info.modifiers[0], /* possibly not proper? */
|
||||
..Default::default()
|
||||
},
|
||||
num_planes: 1,
|
||||
planes,
|
||||
};
|
||||
|
||||
drop(wayvr);
|
||||
|
||||
let layouts: Vec<SubresourceLayout> = vec![SubresourceLayout {
|
||||
offset: data.offset as _,
|
||||
size: 0,
|
||||
row_pitch: data.stride as _,
|
||||
array_pitch: None,
|
||||
depth_pitch: None,
|
||||
}];
|
||||
|
||||
let tex = self.graphics.dmabuf_texture_ex(
|
||||
frame,
|
||||
vulkano::image::ImageTiling::DrmFormatModifier,
|
||||
layouts,
|
||||
&data.mod_info.modifiers,
|
||||
)?;
|
||||
|
||||
self.vk_image = Some(tex.clone());
|
||||
self.vk_image_view = Some(vulkano::image::view::ImageView::new_default(tex).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl OverlayRenderer for WayVRRenderer {
|
||||
fn init(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> {
|
||||
let ctx = self.context.borrow_mut();
|
||||
let wayvr = &mut ctx.wayvr.borrow_mut().data;
|
||||
wayvr.state.set_display_visible(ctx.display, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resume(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> {
|
||||
let ctx = self.context.borrow_mut();
|
||||
let wayvr = &mut ctx.wayvr.borrow_mut().data;
|
||||
wayvr.state.set_display_visible(ctx.display, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_render(&mut self, _app: &mut AppState) -> anyhow::Result<ShouldRender> {
|
||||
let ctx = self.context.borrow();
|
||||
let mut wayvr = ctx.wayvr.borrow_mut();
|
||||
let redrawn = match wayvr.data.tick_display(ctx.display) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
log::error!("tick_display failed: {e}");
|
||||
return Ok(ShouldRender::Unable);
|
||||
}
|
||||
};
|
||||
|
||||
if redrawn {
|
||||
Ok(ShouldRender::Should)
|
||||
} else {
|
||||
Ok(ShouldRender::Can)
|
||||
}
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
app: &mut state::AppState,
|
||||
tgt: Arc<ImageView>,
|
||||
buf: &mut CommandBuffers,
|
||||
alpha: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
let ctx = self.context.borrow();
|
||||
let wayvr = ctx.wayvr.borrow_mut();
|
||||
|
||||
let data = wayvr
|
||||
.data
|
||||
.state
|
||||
.get_render_data(ctx.display)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to fetch render data"))?
|
||||
.clone();
|
||||
|
||||
drop(wayvr);
|
||||
drop(ctx);
|
||||
|
||||
match data {
|
||||
wayvr::egl_data::RenderData::Dmabuf(data) => {
|
||||
self.ensure_dmabuf_data(&data)?;
|
||||
}
|
||||
wayvr::egl_data::RenderData::Software(data) => {
|
||||
if let Some(new_frame) = &data {
|
||||
self.ensure_software_data(new_frame)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(view) = self.vk_image_view.as_ref() else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let set0 =
|
||||
self.pipeline
|
||||
.uniform_sampler(0, view.clone(), app.graphics.texture_filtering)?;
|
||||
|
||||
let set1 = self.pipeline.uniform_buffer(1, vec![alpha])?;
|
||||
|
||||
let pass = self
|
||||
.pipeline
|
||||
.create_pass_for_target(tgt.clone(), vec![set0, set1])?;
|
||||
|
||||
let mut cmd_buffer = app
|
||||
.graphics
|
||||
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
cmd_buffer.begin_rendering(tgt)?;
|
||||
cmd_buffer.run_ref(&pass)?;
|
||||
cmd_buffer.end_rendering()?;
|
||||
buf.push(cmd_buffer.build()?);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
let ctx = self.context.borrow();
|
||||
let wayvr = ctx.wayvr.borrow_mut();
|
||||
wayvr
|
||||
.data
|
||||
.state
|
||||
.displays
|
||||
.get(&ctx.display)
|
||||
.map(|disp| FrameMeta {
|
||||
extent: [disp.width as _, disp.height as _, 1],
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn create_wayvr_display_overlay<O>(
|
||||
app: &mut state::AppState,
|
||||
display_width: u16,
|
||||
display_height: u16,
|
||||
display_handle: wayvr::display::DisplayHandle,
|
||||
display_scale: f32,
|
||||
name: &str,
|
||||
) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let transform = ui_transform([u32::from(display_width), u32::from(display_height)]);
|
||||
|
||||
let state = OverlayState {
|
||||
name: format!("WayVR - {name}").into(),
|
||||
keyboard_focus: Some(KeyboardFocus::WayVR),
|
||||
want_visible: true,
|
||||
interactable: true,
|
||||
grabbable: true,
|
||||
spawn_scale: display_scale,
|
||||
spawn_point: vec3a(0.0, -0.1, -1.0),
|
||||
interaction_transform: transform,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let wayvr = app.get_wayvr()?;
|
||||
|
||||
let renderer = WayVRRenderer::new(app, wayvr, display_handle)?;
|
||||
let context = renderer.context.clone();
|
||||
|
||||
let backend = Box::new(SplitOverlayBackend {
|
||||
renderer: Box::new(renderer),
|
||||
interaction: Box::new(WayVRInteractionHandler::new(context, Affine2::IDENTITY)),
|
||||
});
|
||||
|
||||
Ok(OverlayData {
|
||||
state,
|
||||
backend,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn show_display<O>(wayvr: &mut WayVRData, overlays: &mut OverlayContainer<O>, display_name: &str)
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
if let Some(display) = WayVR::get_display_by_name(&wayvr.data.state.displays, display_name) {
|
||||
if let Some(overlay_id) = wayvr.display_handle_map.get(&display) {
|
||||
if let Some(overlay) = overlays.mut_by_id(*overlay_id) {
|
||||
overlay.state.want_visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
wayvr.data.state.set_display_visible(display, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn action_app_click<O>(
|
||||
app: &mut AppState,
|
||||
overlays: &mut OverlayContainer<O>,
|
||||
catalog_name: &Arc<str>,
|
||||
app_name: &Arc<str>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let wayvr = app.get_wayvr()?;
|
||||
|
||||
let catalog = app
|
||||
.session
|
||||
.wayvr_config
|
||||
.get_catalog(catalog_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get catalog \"{}\"", catalog_name))?
|
||||
.clone();
|
||||
|
||||
if let Some(app_entry) = catalog.get_app(app_name) {
|
||||
let mut wayvr = wayvr.borrow_mut();
|
||||
|
||||
let disp_handle = get_or_create_display_by_name(
|
||||
app,
|
||||
&mut wayvr,
|
||||
&app_entry.target_display.to_lowercase(),
|
||||
)?;
|
||||
|
||||
let args_vec = &app_entry
|
||||
.args
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |args| gen_args_vec(args.as_str()));
|
||||
|
||||
let env_vec = &app_entry
|
||||
.env
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |env| gen_env_vec(env));
|
||||
|
||||
// Terminate existing process if required
|
||||
if let Some(process_handle) =
|
||||
wayvr
|
||||
.data
|
||||
.state
|
||||
.process_query(disp_handle, &app_entry.exec, args_vec, env_vec)
|
||||
{
|
||||
// Terminate process
|
||||
wayvr.data.terminate_process(process_handle);
|
||||
} else {
|
||||
// Spawn process
|
||||
wayvr.data.state.spawn_process(
|
||||
disp_handle,
|
||||
&app_entry.exec,
|
||||
args_vec,
|
||||
env_vec,
|
||||
HashMap::default(),
|
||||
)?;
|
||||
|
||||
show_display::<O>(&mut wayvr, overlays, app_entry.target_display.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn action_display_click<O>(
|
||||
app: &mut AppState,
|
||||
overlays: &mut OverlayContainer<O>,
|
||||
display_name: &Arc<str>,
|
||||
action: &WayVRDisplayClickAction,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let wayvr = app.get_wayvr()?;
|
||||
let mut wayvr = wayvr.borrow_mut();
|
||||
|
||||
let Some(handle) = WayVR::get_display_by_name(&wayvr.data.state.displays, display_name) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(display) = wayvr.data.state.displays.get_mut(&handle) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(overlay_id) = display.overlay_id else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(overlay) = overlays.mut_by_id(overlay_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match action {
|
||||
WayVRDisplayClickAction::ToggleVisibility => {
|
||||
// Toggle visibility
|
||||
overlay.state.want_visible = !overlay.state.want_visible;
|
||||
}
|
||||
WayVRDisplayClickAction::Reset => {
|
||||
// Show it at the front
|
||||
overlay.state.want_visible = true;
|
||||
overlay.state.reset(app, true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn wayvr_action<O>(app: &mut AppState, overlays: &mut OverlayContainer<O>, action: &WayVRAction)
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
match action {
|
||||
WayVRAction::AppClick {
|
||||
catalog_name,
|
||||
app_name,
|
||||
} => {
|
||||
if let Err(e) = action_app_click(app, overlays, catalog_name, app_name) {
|
||||
// Happens if something went wrong with initialization
|
||||
// or input exec path is invalid. Do nothing, just print an error
|
||||
error_toast(app, "action_app_click failed", e);
|
||||
}
|
||||
}
|
||||
WayVRAction::DisplayClick {
|
||||
display_name,
|
||||
action,
|
||||
} => {
|
||||
if let Err(e) = action_display_click::<O>(app, overlays, display_name, action) {
|
||||
error_toast(app, "action_display_click failed", e);
|
||||
}
|
||||
}
|
||||
WayVRAction::ToggleDashboard => {
|
||||
let wayvr = app.get_wayvr().unwrap(); /* safe */
|
||||
let mut wayvr = wayvr.borrow_mut();
|
||||
|
||||
if let Err(e) = toggle_dashboard::<O>(app, overlays, &mut wayvr) {
|
||||
error_toast(app, "toggle_dashboard failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,17 @@ spawn_pos: [0, 0, -1]
|
||||
elements:
|
||||
- type: Panel
|
||||
rect: [98, 0, 4, 200]
|
||||
corner_radius: 0
|
||||
bg_color: "#ffff00"
|
||||
|
||||
- type: Panel
|
||||
rect: [0, 98, 200, 4]
|
||||
corner_radius: 0
|
||||
bg_color: "#ffff00"
|
||||
|
||||
- type: Label
|
||||
rect: [8, 90, 600, 70]
|
||||
corner_radius: 0
|
||||
font_size: 18
|
||||
fg_color: "#ffff00"
|
||||
source: Static
|
||||
|
||||
@@ -43,6 +43,15 @@ main_layout:
|
||||
- ["LShift", "Oem102", "Z", "X", "C", "V", "B", "N", "M", "Comma", "Period", "Oem2", "RShift", ~, "Up", ~, "KP_1", "KP_2", "KP_3", "KP_Enter"]
|
||||
- ["LCtrl", "LSuper", "LAlt", "Space", "Meta", "RSuper", "Menu", "RCtrl", ~, "Left", "Down", "Right", ~, "KP_0", "KP_Decimal", ~]
|
||||
|
||||
# When using the purple pointer...
|
||||
# None - No special functionality when using purple pointer (Default)
|
||||
# Shift - Use same functionality as the orange pointer
|
||||
# Ctrl - Use Main layout with Ctrl modifier
|
||||
# Alt - Use Main layout with Alt modifier
|
||||
# Super - Use Main layout with Super (WinKey) modifier
|
||||
# Meta - Use Main layout with Meta (AltGr) modifier
|
||||
alt_modifier: None
|
||||
|
||||
# Shell commands to be used in a layout.
|
||||
# Value is an array of string arguments.
|
||||
exec_commands:
|
||||
|
||||
+150
-88
@@ -12,20 +12,23 @@ spawn_pos: [0, -0.1, -0.5]
|
||||
elements:
|
||||
- type: Panel
|
||||
rect: [0, 0, 600, 800]
|
||||
bg_color: "#102030"
|
||||
corner_radius: 8
|
||||
bg_color: "#1e2030"
|
||||
|
||||
- type: Label
|
||||
rect: [15, 35, 600, 70]
|
||||
corner_radius: 6
|
||||
font_size: 24
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Settings
|
||||
|
||||
- type: Button
|
||||
rect: [560, 0, 40, 40]
|
||||
corner_radius: 8
|
||||
font_size: 16
|
||||
bg_color: "#880000"
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#ed8796"
|
||||
fg_color: "#24273a"
|
||||
text: X
|
||||
click_down:
|
||||
- type: Window
|
||||
@@ -34,33 +37,38 @@ elements:
|
||||
|
||||
- type: Panel
|
||||
rect: [50, 53, 500, 1]
|
||||
bg_color: "#c0c0c0"
|
||||
corner_radius: 6
|
||||
bg_color: "#6e738d"
|
||||
|
||||
####### Watch Section #######
|
||||
|
||||
- type: Label
|
||||
rect: [15, 85, 570, 24]
|
||||
corner_radius: 6
|
||||
font_size: 18
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Watch
|
||||
|
||||
- type: Panel
|
||||
rect: [250, 105, 1, 100]
|
||||
bg_color: "#c0c0c0"
|
||||
corner_radius: 6
|
||||
bg_color: "#6e738d"
|
||||
|
||||
- type: Label
|
||||
rect: [288, 105, 100, 24]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Visibility
|
||||
|
||||
- type: Button
|
||||
rect: [270, 120, 100, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Hide"
|
||||
click_down:
|
||||
- type: Watch
|
||||
@@ -68,9 +76,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [270, 170, 100, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Swap Hand"
|
||||
click_down:
|
||||
- type: Watch
|
||||
@@ -78,20 +87,23 @@ elements:
|
||||
|
||||
- type: Panel
|
||||
rect: [390, 105, 1, 100]
|
||||
bg_color: "#c0c0c0"
|
||||
corner_radius: 6
|
||||
bg_color: "#6e738d"
|
||||
|
||||
- type: Label
|
||||
rect: [430, 105, 120, 24]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Watch Fade
|
||||
|
||||
- type: Button
|
||||
rect: [410, 120, 140, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Cutoff Point"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -107,9 +119,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [410, 170, 140, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Cutoff Strength"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -125,16 +138,18 @@ elements:
|
||||
|
||||
- type: Label
|
||||
rect: [25, 140, 90, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Rotation
|
||||
|
||||
- type: Button
|
||||
rect: [108, 120, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "X"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -150,9 +165,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [153, 120, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Y"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -168,9 +184,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [198, 120, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Z"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -186,16 +203,18 @@ elements:
|
||||
|
||||
- type: Label
|
||||
rect: [25, 190, 90, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Position
|
||||
|
||||
- type: Button
|
||||
rect: [108, 170, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "X"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -211,9 +230,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [153, 170, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Y"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -229,9 +249,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [198, 170, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#306060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Z"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -247,28 +268,32 @@ elements:
|
||||
|
||||
- type: Panel
|
||||
rect: [50, 220, 500, 1]
|
||||
bg_color: "#c0c0c0"
|
||||
corner_radius: 6
|
||||
bg_color: "#6e738d"
|
||||
|
||||
####### Mirror Section #######
|
||||
- type: Label
|
||||
rect: [15, 255, 570, 24]
|
||||
corner_radius: 6
|
||||
font_size: 18
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Mirrors
|
||||
|
||||
- type: Label
|
||||
rect: [25, 290, 30, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: M1
|
||||
|
||||
- type: Button
|
||||
rect: [60, 270, 110, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#707070"
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#494d64"
|
||||
text: "Show/Hide"
|
||||
click_down: # ToggleVisible if exists, else create
|
||||
- type: Overlay
|
||||
@@ -280,9 +305,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [185, 270, 60, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#707070"
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#494d64"
|
||||
text: "Lock"
|
||||
click_down:
|
||||
- type: Overlay
|
||||
@@ -291,9 +317,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [258, 270, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#880000"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#ed8796"
|
||||
text: "X"
|
||||
click_down:
|
||||
- type: Window
|
||||
@@ -302,16 +329,18 @@ elements:
|
||||
|
||||
- type: Label
|
||||
rect: [25, 340, 30, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: M2
|
||||
|
||||
- type: Button
|
||||
rect: [60, 320, 110, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#707070"
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#494d64"
|
||||
text: "Show/Hide"
|
||||
click_down:
|
||||
- type: Overlay
|
||||
@@ -323,9 +352,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [185, 320, 60, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#707070"
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#494d64"
|
||||
text: "Lock"
|
||||
click_down:
|
||||
- type: Overlay
|
||||
@@ -334,9 +364,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [258, 320, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#880000"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#ed8796"
|
||||
text: "X"
|
||||
click_down:
|
||||
- type: Window
|
||||
@@ -345,16 +376,18 @@ elements:
|
||||
|
||||
- type: Label
|
||||
rect: [25, 390, 30, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: M3
|
||||
|
||||
- type: Button
|
||||
rect: [60, 370, 110, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#707070"
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#494d64"
|
||||
text: "Show/Hide"
|
||||
click_down:
|
||||
- type: Overlay
|
||||
@@ -366,9 +399,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [185, 370, 60, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#707070"
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#494d64"
|
||||
text: "Lock"
|
||||
click_down:
|
||||
- type: Overlay
|
||||
@@ -377,9 +411,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [258, 370, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#880000"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#ed8796"
|
||||
text: "X"
|
||||
click_down:
|
||||
- type: Window
|
||||
@@ -388,29 +423,33 @@ elements:
|
||||
|
||||
- type: Panel
|
||||
rect: [300, 240, 1, 200]
|
||||
bg_color: "#c0c0c0"
|
||||
corner_radius: 6
|
||||
bg_color: "#6e738d"
|
||||
|
||||
####### Color Gain Section #######
|
||||
|
||||
- type: Label
|
||||
rect: [325, 255, 90, 24]
|
||||
corner_radius: 6
|
||||
font_size: 18
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Color Gain
|
||||
|
||||
- type: Label
|
||||
rect: [470, 255, 90, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: (SteamVR)
|
||||
|
||||
- type: Button
|
||||
rect: [330, 270, 60, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#707070"
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#494d64"
|
||||
text: "All"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -426,9 +465,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [405, 270, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#701010"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#e78284"
|
||||
text: "R"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -444,9 +484,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [450, 270, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#107010"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#a6d189"
|
||||
text: "G"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -462,9 +503,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [495, 270, 30, 30]
|
||||
corner_radius: 15
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#101070"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#8caaee"
|
||||
text: "B"
|
||||
click_down:
|
||||
- type: Toast
|
||||
@@ -480,55 +522,68 @@ elements:
|
||||
|
||||
- type: Panel
|
||||
rect: [325, 315, 225, 1]
|
||||
bg_color: "#c0c0c0"
|
||||
corner_radius: 6
|
||||
bg_color: "#6e738d"
|
||||
|
||||
####### Playspace Section #######
|
||||
|
||||
- type: Label
|
||||
rect: [325, 345, 90, 24]
|
||||
corner_radius: 6
|
||||
font_size: 18
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Playspace
|
||||
|
||||
- type: Button
|
||||
rect: [330, 360, 220, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#206060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Fix Floor"
|
||||
click_down:
|
||||
- type: System
|
||||
action: PlayspaceFixFloor
|
||||
- type: Window
|
||||
target: "settings"
|
||||
action: Destroy
|
||||
|
||||
- type: Button
|
||||
rect: [330, 410, 220, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#206060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Reset Offset"
|
||||
click_down:
|
||||
- type: System
|
||||
action: PlayspaceResetOffset
|
||||
- type: Window
|
||||
target: "settings"
|
||||
action: Destroy
|
||||
|
||||
####### Notifications Section #######
|
||||
|
||||
- type: Panel
|
||||
rect: [50, 460, 500, 1]
|
||||
bg_color: "#c0c0c0"
|
||||
corner_radius: 6
|
||||
bg_color: "#6e738d"
|
||||
|
||||
- type: Label
|
||||
rect: [325, 490, 90, 24]
|
||||
corner_radius: 6
|
||||
font_size: 18
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Notifications
|
||||
|
||||
- type: Button
|
||||
rect: [330, 505, 220, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#401010"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#e64553"
|
||||
text: "Enabled"
|
||||
click_down:
|
||||
- type: System
|
||||
@@ -537,9 +592,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [330, 555, 220, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#401010"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#e64553"
|
||||
text: "Sound Enabled"
|
||||
click_down:
|
||||
- type: System
|
||||
@@ -549,16 +605,18 @@ elements:
|
||||
####### Behavior Section #######
|
||||
- type: Label
|
||||
rect: [15, 490, 570, 24]
|
||||
corner_radius: 6
|
||||
font_size: 18
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Static
|
||||
text: Behavior
|
||||
|
||||
- type: Button
|
||||
rect: [30, 505, 220, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#401010"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#e64553"
|
||||
text: "Auto-Realign"
|
||||
click_down:
|
||||
- type: System
|
||||
@@ -567,9 +625,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [30, 555, 220, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#401010"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#e64553"
|
||||
text: "Grab+Scroll Slide"
|
||||
click_down:
|
||||
- type: System
|
||||
@@ -580,13 +639,15 @@ elements:
|
||||
|
||||
- type: Panel
|
||||
rect: [50, 605, 500, 1]
|
||||
bg_color: "#c0c0c0"
|
||||
corner_radius: 6
|
||||
bg_color: "#6e738d"
|
||||
|
||||
- type: Button
|
||||
rect: [330, 625, 220, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#206060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Save Config"
|
||||
click_down:
|
||||
- type: System
|
||||
@@ -596,9 +657,10 @@ elements:
|
||||
|
||||
- type: Button
|
||||
rect: [30, 625, 250, 30]
|
||||
corner_radius: 6
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#206060"
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#eed49f"
|
||||
text: "Save Overlay Layout"
|
||||
click_down:
|
||||
- type: System
|
||||
|
||||
Binary file not shown.
+76
-50
@@ -1,4 +1,4 @@
|
||||
# looking to make changes?
|
||||
# looking to make changes?
|
||||
# drop me in ~/.config/wlxoverlay/watch.yaml
|
||||
#
|
||||
|
||||
@@ -9,29 +9,44 @@ size: [400, 200]
|
||||
elements:
|
||||
# background panel
|
||||
- type: Panel
|
||||
rect: [0, 0, 400, 200]
|
||||
bg_color: "#353535"
|
||||
rect: [0, 30, 400, 130]
|
||||
corner_radius: 20
|
||||
bg_color: "#24273a"
|
||||
|
||||
- type: Button
|
||||
rect: [2, 162, 26, 36]
|
||||
font_size: 14
|
||||
bg_color: "#808040"
|
||||
fg_color: "#ffffff"
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
bg_color: "#c6a0f6"
|
||||
fg_color: "#24273a"
|
||||
text: "C"
|
||||
click_up: # destroy if exists, otherwise create
|
||||
- type: Window
|
||||
target: settings
|
||||
action: ShowUi # only triggers if not exists
|
||||
- type: Window
|
||||
- type: Window
|
||||
target: settings
|
||||
action: Destroy # only triggers if exists since before current frame
|
||||
|
||||
# Dashboard toggle button
|
||||
- type: Button
|
||||
rect: [32, 162, 48, 36]
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
bg_color: "#2288FF"
|
||||
fg_color: "#24273a"
|
||||
text: "Dash"
|
||||
click_up:
|
||||
- type: WayVR
|
||||
action: ToggleDashboard
|
||||
|
||||
# Keyboard button
|
||||
- type: Button
|
||||
rect: [32, 162, 60, 36]
|
||||
font_size: 14
|
||||
fg_color: "#FFFFFF"
|
||||
bg_color: "#406050"
|
||||
rect: [84, 162, 48, 36]
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
fg_color: "#24273a"
|
||||
bg_color: "#a6da95"
|
||||
text: Kbd
|
||||
click_up:
|
||||
- type: Overlay
|
||||
@@ -52,52 +67,56 @@ elements:
|
||||
scroll_up:
|
||||
- type: Overlay
|
||||
target: "kbd"
|
||||
action:
|
||||
action:
|
||||
Opacity: { delta: 0.025 }
|
||||
scroll_down:
|
||||
- type: Overlay
|
||||
target: "kbd"
|
||||
action:
|
||||
action:
|
||||
Opacity: { delta: -0.025 }
|
||||
|
||||
# bottom row, of keyboard + overlays
|
||||
- type: OverlayList
|
||||
rect: [94, 160, 306, 40]
|
||||
font_size: 14
|
||||
fg_color: "#FFFFFF"
|
||||
bg_color: "#405060"
|
||||
rect: [134, 160, 266, 40]
|
||||
corner_radius: 4
|
||||
font_size: 15
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#1e2030"
|
||||
layout: Horizontal
|
||||
click_up: ToggleVisible
|
||||
long_click_up: Reset
|
||||
right_up: ToggleImmovable
|
||||
middle_up: ToggleInteraction
|
||||
scroll_up:
|
||||
scroll_up:
|
||||
Opacity: { delta: 0.025 }
|
||||
scroll_down:
|
||||
scroll_down:
|
||||
Opacity: { delta: -0.025 }
|
||||
|
||||
# local clock
|
||||
- type: Label
|
||||
rect: [19, 90, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 46 # Use 32 for 12-hour time
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Clock
|
||||
format: "%H:%M" # 23:59
|
||||
#format: "%I:%M %p" # 11:59 PM
|
||||
|
||||
|
||||
# local date
|
||||
- type: Label
|
||||
rect: [20, 117, 200, 20]
|
||||
corner_radius: 4
|
||||
font_size: 14
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Clock
|
||||
format: "%x" # local date representation
|
||||
|
||||
# local day-of-week
|
||||
- type: Label
|
||||
rect: [20, 137, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 14
|
||||
fg_color: "#ffffff"
|
||||
fg_color: "#cad3f5"
|
||||
source: Clock
|
||||
format: "%A" # Tuesday
|
||||
#format: "%a" # Tue
|
||||
@@ -105,62 +124,69 @@ elements:
|
||||
# alt clock 1
|
||||
- type: Label
|
||||
rect: [210, 90, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 24 # Use 18 for 12-hour time
|
||||
fg_color: "#99BBAA"
|
||||
fg_color: "#8bd5ca"
|
||||
source: Clock
|
||||
timezone: "Asia/Tokyo" # change TZ1 here
|
||||
timezone: 0
|
||||
format: "%H:%M" # 23:59
|
||||
#format: "%I:%M %p" # 11:59 PM
|
||||
- type: Label
|
||||
rect: [210, 60, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 14
|
||||
fg_color: "#99BBAA"
|
||||
source: Static
|
||||
text: "Tokyo" # change TZ1 label here
|
||||
|
||||
fg_color: "#8bd5ca"
|
||||
source: Timezone
|
||||
timezone: 0
|
||||
|
||||
# alt clock 2
|
||||
- type: Label
|
||||
rect: [210, 150, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 24 # Use 18 for 12-hour time
|
||||
fg_color: "#AA99BB"
|
||||
fg_color: "#b7bdf8"
|
||||
source: Clock
|
||||
timezone: "America/Chicago" # change TZ2 here
|
||||
timezone: 1
|
||||
format: "%H:%M" # 23:59
|
||||
#format: "%I:%M %p" # 11:59 PM
|
||||
- type: Label
|
||||
rect: [210, 120, 200, 50]
|
||||
corner_radius: 4
|
||||
font_size: 14
|
||||
fg_color: "#AA99BB"
|
||||
source: Static
|
||||
text: "Chicago" # change TZ2 label here
|
||||
fg_color: "#b7bdf8"
|
||||
source: Timezone
|
||||
timezone: 1
|
||||
|
||||
# batteries
|
||||
- type: BatteryList
|
||||
rect: [0, 0, 400, 30]
|
||||
font_size: 14
|
||||
fg_color: "#99BBAA"
|
||||
rect: [0, 5, 400, 30]
|
||||
corner_radius: 4
|
||||
font_size: 16
|
||||
fg_color: "#8bd5ca"
|
||||
fg_color_low: "#B06060"
|
||||
fg_color_charging: "#6080A0"
|
||||
num_devices: 9
|
||||
layout: Horizontal
|
||||
low_threshold: 20
|
||||
low_threshold: 33
|
||||
|
||||
# volume buttons
|
||||
- type: Button
|
||||
rect: [327, 52, 46, 32]
|
||||
font_size: 14
|
||||
fg_color: "#FFFFFF"
|
||||
bg_color: "#505050"
|
||||
text: "+"
|
||||
rect: [315, 52, 70, 32]
|
||||
corner_radius: 4
|
||||
font_size: 13
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#5b6078"
|
||||
text: "Vol +"
|
||||
click_down:
|
||||
- type: Exec
|
||||
command: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%" ]
|
||||
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%"]
|
||||
- type: Button
|
||||
rect: [327, 116, 46, 32]
|
||||
font_size: 14
|
||||
fg_color: "#FFFFFF"
|
||||
bg_color: "#505050"
|
||||
text: "-"
|
||||
rect: [315, 116, 70, 32]
|
||||
corner_radius: 4
|
||||
font_size: 13
|
||||
fg_color: "#cad3f5"
|
||||
bg_color: "#5b6078"
|
||||
text: "Vol -"
|
||||
click_down:
|
||||
- type: Exec
|
||||
command: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%" ]
|
||||
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%"]
|
||||
@@ -0,0 +1,78 @@
|
||||
# This is an example WayVR panel configuration. It demonstrates all the capabilities of this module.
|
||||
# looking to make changes?
|
||||
# drop me in ~/.config/wlxoverlay/wayvr.yaml
|
||||
#
|
||||
|
||||
version: 1
|
||||
|
||||
# If your gpu has some issues with zero-copy textures, you can set this option to "software".
|
||||
#
|
||||
# Possible options:
|
||||
# "dmabuf": Use zero-copy texture access (from EGL to Vulkan) - no performance impact
|
||||
# "software": Read pixel data to memory via glReadPixels() every time a content has been updated. Minor performance impact on large resolutions
|
||||
blit_method: "dmabuf"
|
||||
|
||||
# Set to true if you want to make Wyland server instantly available.
|
||||
# By default, WayVR starts only when it's needed.
|
||||
# (this option is primarily used for remote starting external processes and development purposes)
|
||||
run_compositor_at_start: false
|
||||
|
||||
# Automatically close overlays with zero window count?
|
||||
auto_hide: true
|
||||
|
||||
# For how long an overlay should be visible in case if there are no windows present? (in milliseconds, auto_hide needs to be enabled)
|
||||
# This value shouldn't be set at 0, because some programs could re-initialize a window during startup (splash screens for example)
|
||||
auto_hide_delay: 750
|
||||
|
||||
# In milliseconds
|
||||
keyboard_repeat_delay: 200
|
||||
|
||||
# Chars per second
|
||||
keyboard_repeat_rate: 50
|
||||
|
||||
# WayVR-compatible dashboard.
|
||||
# For now, there is only one kind of dashboard with WayVR IPC support (WayVR Dashboard).
|
||||
# Build instructions: https://github.com/olekolek1000/wayvr-dashboard
|
||||
# exec: Executable path, for example /home/USER/wayvr_dashboard/src-tauri/target/release/wayvr_dashboard
|
||||
# GDK_BACKEND=wayland: Force-use Wayland for GTK apps
|
||||
dashboard:
|
||||
exec: "wayvr_dashboard"
|
||||
args: ""
|
||||
env: ["GDK_BACKEND=wayland"]
|
||||
|
||||
displays:
|
||||
watch:
|
||||
width: 400
|
||||
height: 600
|
||||
scale: 0.4
|
||||
attach_to: "HandRight" # HandLeft, HandRight
|
||||
pos: [0.0, 0.0, 0.125]
|
||||
rotation: {axis: [1.0, 0.0, 0.0], angle: -45.0}
|
||||
disp1:
|
||||
width: 640
|
||||
height: 480
|
||||
primary: true # Required if you want to attach external processes (not spawned by WayVR itself) without WAYVR_DISPLAY_NAME set
|
||||
disp2:
|
||||
width: 1280
|
||||
height: 720
|
||||
scale: 2.0
|
||||
|
||||
catalogs:
|
||||
default_catalog:
|
||||
apps:
|
||||
- name: "Calc"
|
||||
target_display: "disp1"
|
||||
exec: "kcalc"
|
||||
env: ["FOO=bar"]
|
||||
shown_at_start: false
|
||||
|
||||
- name: "htop"
|
||||
target_display: "watch"
|
||||
exec: "konsole"
|
||||
args: "-e htop"
|
||||
|
||||
- name: "Browser"
|
||||
target_display: "disp2"
|
||||
exec: "cage"
|
||||
args: "chromium -- --incognito"
|
||||
|
||||
+137
-8
@@ -27,16 +27,32 @@ pub mod frag_color {
|
||||
|
||||
layout (set = 0, binding = 0) uniform ColorBlock {
|
||||
uniform vec4 in_color;
|
||||
uniform vec2 corner_radius;
|
||||
};
|
||||
|
||||
void main()
|
||||
{
|
||||
out_color.r = corner_radius.r;
|
||||
out_color = in_color;
|
||||
|
||||
vec2 uv_circ = ((1. - corner_radius) - (abs(in_uv + vec2(-0.5)) * 2.))/corner_radius;
|
||||
float dist = length(uv_circ);
|
||||
|
||||
out_color.a = mix(out_color.a, 0.,
|
||||
float(dist > 1.)
|
||||
* float(uv_circ.x < 0.)
|
||||
* float(uv_circ.y < 0.));
|
||||
}
|
||||
",
|
||||
}
|
||||
}
|
||||
|
||||
//layout (location = 1) in float corner_radius;
|
||||
//out_color = in_color;
|
||||
// Some equation that determines whether to keep the pixel
|
||||
// Use Lerp not if
|
||||
//out_color.a = 0;
|
||||
|
||||
pub mod frag_glyph {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
@@ -61,6 +77,52 @@ pub mod frag_glyph {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_sprite2 {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
src: r"#version 310 es
|
||||
precision highp float;
|
||||
|
||||
layout (location = 0) in vec2 in_uv;
|
||||
layout (location = 0) out vec4 out_color;
|
||||
|
||||
layout (set = 0, binding = 0) uniform sampler2D in_texture;
|
||||
layout (set = 1, binding = 0) uniform UniBlock {
|
||||
uniform vec4 st;
|
||||
uniform vec4 mul;
|
||||
};
|
||||
|
||||
void main()
|
||||
{
|
||||
out_color = texture(in_texture, (in_uv * st.xy) + st.zw) * mul;
|
||||
}
|
||||
",
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_sprite2_hl {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
src: r"#version 310 es
|
||||
precision highp float;
|
||||
|
||||
layout (location = 0) in vec2 in_uv;
|
||||
layout (location = 0) out vec4 out_color;
|
||||
|
||||
layout (set = 0, binding = 0) uniform sampler2D in_texture;
|
||||
layout (set = 1, binding = 0) uniform UniBlock {
|
||||
uniform vec4 st;
|
||||
uniform vec4 mul;
|
||||
};
|
||||
|
||||
void main()
|
||||
{
|
||||
out_color = texture(in_texture, (in_uv * st.xy) + st.zw).a * mul;
|
||||
}
|
||||
",
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_sprite {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
@@ -80,6 +142,31 @@ pub mod frag_sprite {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_grid {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
src: r"#version 310 es
|
||||
precision highp float;
|
||||
|
||||
layout (location = 0) in vec2 in_uv;
|
||||
layout (location = 0) out vec4 out_color;
|
||||
|
||||
void main()
|
||||
{
|
||||
float fade = max(1.0 - 2.0 * length(in_uv.xy + vec2(-0.5, -0.5)), 0.0);
|
||||
float grid;
|
||||
|
||||
if (fract(in_uv.x / 0.0005) < 0.01 || fract(in_uv.y / 0.0005) < 0.01) {
|
||||
grid = 1.0;
|
||||
} else {
|
||||
grid = 0.0;
|
||||
}
|
||||
out_color = vec4(1.0, 1.0, 1.0, grid * fade);
|
||||
}
|
||||
",
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_screen {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
@@ -90,17 +177,14 @@ pub mod frag_screen {
|
||||
layout (location = 0) out vec4 out_color;
|
||||
|
||||
layout (set = 0, binding = 0) uniform sampler2D in_texture;
|
||||
layout (set = 1, binding = 0) uniform AlphaBlock {
|
||||
uniform float alpha;
|
||||
};
|
||||
|
||||
void main()
|
||||
{
|
||||
out_color = texture(in_texture, in_uv);
|
||||
|
||||
// linear to srgb
|
||||
bvec4 cutoff = lessThan(out_color, vec4(0.0031308));
|
||||
vec4 higher = (pow(out_color, vec4(0.41666)) * vec4(1.055)) - vec4(0.055);
|
||||
vec4 lower = out_color*vec4(12.92);
|
||||
out_color = mix(higher, lower, cutoff);
|
||||
out_color.a = 1.0;
|
||||
out_color.a = alpha;
|
||||
}
|
||||
",
|
||||
}
|
||||
@@ -130,7 +214,52 @@ pub mod frag_srgb {
|
||||
vec4 lower = out_color/vec4(12.92);
|
||||
|
||||
out_color = mix(higher, lower, cutoff);
|
||||
out_color.a = alpha;
|
||||
out_color.a *= alpha;
|
||||
}
|
||||
",
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_swapchain {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
src: r"#version 310 es
|
||||
precision highp float;
|
||||
|
||||
layout (location = 0) in vec2 in_uv;
|
||||
layout (location = 0) out vec4 out_color;
|
||||
|
||||
layout (set = 0, binding = 0) uniform sampler2D in_texture;
|
||||
layout (set = 1, binding = 0) uniform AlphaBlock {
|
||||
uniform float alpha;
|
||||
};
|
||||
|
||||
void main()
|
||||
{
|
||||
out_color = texture(in_texture, in_uv);
|
||||
out_color.a *= alpha;
|
||||
}
|
||||
",
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_line {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
src: r"#version 310 es
|
||||
precision highp float;
|
||||
|
||||
layout (location = 0) in vec2 in_uv;
|
||||
layout (location = 0) out vec4 out_color;
|
||||
|
||||
layout (set = 0, binding = 0) uniform ColorBlock {
|
||||
uniform vec4 in_color;
|
||||
uniform vec2 unused;
|
||||
};
|
||||
|
||||
void main()
|
||||
{
|
||||
out_color = in_color;
|
||||
}
|
||||
",
|
||||
}
|
||||
|
||||
+135
-44
@@ -1,23 +1,44 @@
|
||||
use std::{io::Cursor, path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::bail;
|
||||
use glam::{Affine3A, Vec3};
|
||||
use glam::Affine3A;
|
||||
use idmap::IdMap;
|
||||
use rodio::{Decoder, OutputStream, OutputStreamHandle, Source};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
use vulkano::image::view::ImageView;
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
use {
|
||||
crate::config_wayvr::{self, WayVRConfig},
|
||||
crate::overlays::wayvr::WayVRData,
|
||||
std::{cell::RefCell, rc::Rc},
|
||||
};
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
use crate::backend::osc::OscSender;
|
||||
|
||||
use crate::{
|
||||
backend::{input::InputState, task::TaskContainer},
|
||||
config::GeneralConfig,
|
||||
backend::{input::InputState, overlay::OverlayID, task::TaskContainer},
|
||||
config::{AStrMap, GeneralConfig},
|
||||
config_io,
|
||||
graphics::WlxGraphics,
|
||||
gui::font::FontCache,
|
||||
hid::HidProvider,
|
||||
overlays::toast::{DisplayMethod, ToastTopic},
|
||||
shaders::{frag_color, frag_glyph, frag_screen, frag_sprite, frag_srgb, vert_common},
|
||||
shaders::{
|
||||
frag_color, frag_glyph, frag_grid, frag_line, frag_screen, frag_sprite, frag_sprite2,
|
||||
frag_sprite2_hl, frag_srgb, frag_swapchain, vert_common,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum KeyboardFocus {
|
||||
PhysicalScreen,
|
||||
|
||||
#[allow(dead_code)] // Not available if "wayvr" feature is disabled
|
||||
WayVR, // (for now without wayland window id data, it's handled internally),
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub fc: FontCache,
|
||||
pub session: AppSession,
|
||||
@@ -28,6 +49,15 @@ pub struct AppState {
|
||||
pub audio: AudioOutput,
|
||||
pub screens: SmallVec<[ScreenMeta; 8]>,
|
||||
pub anchor: Affine3A,
|
||||
pub sprites: AStrMap<Arc<ImageView>>,
|
||||
pub keyboard_focus: KeyboardFocus,
|
||||
pub toast_sound: &'static [u8],
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
pub osc_sender: Option<OscSender>,
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
pub wayvr: Option<Rc<RefCell<WayVRData>>>, // Dynamically created if requested
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -44,51 +74,128 @@ impl AppState {
|
||||
let shader = frag_color::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_color", shader);
|
||||
|
||||
let shader = frag_line::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_line", shader);
|
||||
|
||||
let shader = frag_srgb::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_srgb", shader);
|
||||
|
||||
let shader = frag_glyph::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_glyph", shader);
|
||||
|
||||
let shader = frag_grid::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_grid", shader);
|
||||
|
||||
let shader = frag_sprite::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_sprite", shader);
|
||||
|
||||
let shader = frag_sprite2::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_sprite2", shader);
|
||||
|
||||
let shader = frag_sprite2_hl::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_sprite2_hl", shader);
|
||||
|
||||
let shader = frag_screen::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_screen", shader);
|
||||
|
||||
let shader = frag_srgb::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_srgb", shader);
|
||||
let shader = frag_swapchain::load(graphics.device.clone())?;
|
||||
shaders.insert("frag_swapchain", shader);
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
let mut tasks = TaskContainer::new();
|
||||
|
||||
#[cfg(not(feature = "wayvr"))]
|
||||
let tasks = TaskContainer::new();
|
||||
|
||||
let session = AppSession::load();
|
||||
|
||||
Ok(AppState {
|
||||
#[cfg(feature = "wayvr")]
|
||||
let wayvr = session
|
||||
.wayvr_config
|
||||
.post_load(&session.config, &mut tasks)?;
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
let osc_sender = crate::backend::osc::OscSender::new(session.config.osc_out_port).ok();
|
||||
|
||||
let toast_sound_wav = Self::try_load_bytes(
|
||||
&session.config.notification_sound,
|
||||
include_bytes!("res/557297.wav"),
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
fc: FontCache::new(session.config.primary_font.clone())?,
|
||||
session,
|
||||
tasks: TaskContainer::new(),
|
||||
tasks,
|
||||
graphics,
|
||||
input_state: InputState::new(),
|
||||
hid_provider: crate::hid::initialize(),
|
||||
audio: AudioOutput::new(),
|
||||
screens: smallvec![],
|
||||
anchor: Affine3A::IDENTITY,
|
||||
sprites: AStrMap::new(),
|
||||
keyboard_focus: KeyboardFocus::PhysicalScreen,
|
||||
toast_sound: toast_sound_wav,
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
osc_sender,
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
wayvr,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayvr")]
|
||||
#[allow(dead_code)]
|
||||
pub fn get_wayvr(&mut self) -> anyhow::Result<Rc<RefCell<WayVRData>>> {
|
||||
if let Some(wvr) = &self.wayvr {
|
||||
Ok(wvr.clone())
|
||||
} else {
|
||||
let wayvr = Rc::new(RefCell::new(WayVRData::new(
|
||||
WayVRConfig::get_wayvr_config(&self.session.config, &self.session.wayvr_config)?,
|
||||
)?));
|
||||
self.wayvr = Some(wayvr.clone());
|
||||
Ok(wayvr)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_load_bytes(path: &str, fallback_data: &'static [u8]) -> &'static [u8] {
|
||||
if path.is_empty() {
|
||||
return fallback_data;
|
||||
}
|
||||
|
||||
let real_path = config_io::get_config_root().join(path);
|
||||
|
||||
if std::fs::File::open(real_path.clone()).is_err() {
|
||||
log::warn!("Could not open file at: {path}");
|
||||
return fallback_data;
|
||||
}
|
||||
|
||||
match std::fs::read(real_path) {
|
||||
// Box is used here to work around `f`'s limited lifetime
|
||||
Ok(f) => Box::leak(Box::new(f)).as_slice(),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to read file at: {path}");
|
||||
log::warn!("{e:?}");
|
||||
fallback_data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppSession {
|
||||
pub config_root_path: PathBuf,
|
||||
pub config: GeneralConfig,
|
||||
|
||||
pub toast_topics: IdMap<ToastTopic, DisplayMethod>,
|
||||
#[cfg(feature = "wayvr")]
|
||||
pub wayvr_config: WayVRConfig,
|
||||
|
||||
pub color_norm: Vec3,
|
||||
pub color_shift: Vec3,
|
||||
pub color_alt: Vec3,
|
||||
pub color_grab: Vec3,
|
||||
pub toast_topics: IdMap<ToastTopic, DisplayMethod>,
|
||||
}
|
||||
|
||||
impl AppSession {
|
||||
pub fn load() -> Self {
|
||||
let config_root_path = config_io::ensure_config_root();
|
||||
log::info!("Config root path: {}", config_root_path.to_string_lossy());
|
||||
let config_root_path = config_io::ConfigRoot::Generic.ensure_dir();
|
||||
log::info!("Config root path: {config_root_path:?}");
|
||||
let config = GeneralConfig::load_from_disk();
|
||||
|
||||
let mut toast_topics = IdMap::new();
|
||||
@@ -100,30 +207,14 @@ impl AppSession {
|
||||
toast_topics.insert(*k, *v);
|
||||
});
|
||||
|
||||
AppSession {
|
||||
config_root_path,
|
||||
#[cfg(feature = "wayvr")]
|
||||
let wayvr_config = config_wayvr::load_wayvr();
|
||||
|
||||
Self {
|
||||
config,
|
||||
#[cfg(feature = "wayvr")]
|
||||
wayvr_config,
|
||||
toast_topics,
|
||||
color_norm: Vec3 {
|
||||
x: 0.,
|
||||
y: 1.,
|
||||
z: 1.,
|
||||
},
|
||||
color_shift: Vec3 {
|
||||
x: 1.,
|
||||
y: 1.,
|
||||
z: 0.,
|
||||
},
|
||||
color_alt: Vec3 {
|
||||
x: 1.,
|
||||
y: 0.,
|
||||
z: 1.,
|
||||
},
|
||||
color_grab: Vec3 {
|
||||
x: 1.,
|
||||
y: 0.,
|
||||
z: 0.,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,8 +225,8 @@ pub struct AudioOutput {
|
||||
}
|
||||
|
||||
impl AudioOutput {
|
||||
pub fn new() -> Self {
|
||||
AudioOutput {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
audio_stream: None,
|
||||
first_try: true,
|
||||
}
|
||||
@@ -162,7 +253,7 @@ impl AudioOutput {
|
||||
let source = match Decoder::new_wav(cursor) {
|
||||
Ok(source) => source,
|
||||
Err(e) => {
|
||||
log::error!("Failed to play sound: {:?}", e);
|
||||
log::error!("Failed to play sound: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -172,7 +263,7 @@ impl AudioOutput {
|
||||
|
||||
pub struct ScreenMeta {
|
||||
pub name: Arc<str>,
|
||||
pub id: usize,
|
||||
pub id: OverlayID,
|
||||
pub native_handle: u32,
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user