mirror of
https://github.com/wayvr-org/wayvr.git
synced 2026-10-28 22:02:16 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10db0f690a | ||
|
|
072540fbb5 | ||
|
|
d225250b77 | ||
|
|
c7aa88647c | ||
|
|
abc132c553 | ||
|
|
371d3c2757 | ||
|
|
c06ec6d974 | ||
|
|
b3ae583cb4 | ||
|
|
8fb9ef4a05 | ||
|
|
40383f2529 | ||
|
|
b2429bce7f | ||
|
|
59c0bc870c | ||
|
|
4abfa3c3ac | ||
|
|
7037740d46 | ||
|
|
8ae65083f9 | ||
|
|
0fd356f306 | ||
|
|
52a56cfd90 | ||
|
|
eefffccbdf | ||
|
|
344eaca3a8 | ||
|
|
7d42ee9721 | ||
|
|
bb88137533 | ||
|
|
8eb6df9644 | ||
|
|
a0d93e821e | ||
|
|
8461dbe7ac | ||
|
|
d5f7181a05 | ||
|
|
8bad2e1116 | ||
|
|
d5610f6fe6 | ||
|
|
95855ccea8 | ||
|
|
415ef91a43 | ||
|
|
7641a662dc | ||
|
|
3a3cda7a45 | ||
|
|
65d62d79fe | ||
|
|
1eebde3944 | ||
|
|
698c447e34 | ||
|
|
a0a853825c | ||
|
|
b8a0e3630d | ||
|
|
773ff6885d | ||
|
|
4a45683650 | ||
|
|
009626704c | ||
|
|
84b08b99f7 | ||
|
|
93a4a6702b | ||
|
|
6ee0cb4fd1 | ||
|
|
9a693862ba | ||
|
|
c70833b86a | ||
|
|
5afb5ed4ab | ||
|
|
065ce8f136 | ||
|
|
1fa842bb44 | ||
|
|
f6a9db5759 | ||
|
|
42ab3a3199 | ||
|
|
1772e3aefd | ||
|
|
f2a5ec0c37 | ||
|
|
461657e4c3 | ||
|
|
7ca6df6d3b | ||
|
|
be81c344d3 | ||
|
|
42aef89822 | ||
|
|
7184a9f21f | ||
|
|
d99a58da9e | ||
|
|
8e799d1b8f | ||
|
|
5db7524787 | ||
|
|
8f9c3f5e3a | ||
|
|
031efb2d79 | ||
|
|
0b087cb9a9 | ||
|
|
5989d32075 | ||
|
|
b19b0cb40d | ||
|
|
6e2c3fc523 | ||
|
|
399168fc38 | ||
|
|
97aab43d09 | ||
|
|
fc158ed6eb | ||
|
|
93fe2bfca2 | ||
|
|
944b7c1835 | ||
|
|
78b51179ee | ||
|
|
0283916ffa | ||
|
|
63e8cd59b2 | ||
|
|
1d1a4f01ae | ||
|
|
7e2f172b74 | ||
|
|
17fb1211ee | ||
|
|
1bee936db1 | ||
|
|
e3ae9a9890 | ||
|
|
2d0f733c67 | ||
|
|
6cb683a86b | ||
|
|
dc40d7e42b | ||
|
|
c02edb7a43 | ||
|
|
e845ac8d53 | ||
|
|
aae649b1f2 | ||
|
|
7eb5201c90 | ||
|
|
7d987f5cb8 | ||
|
|
106af7dd03 | ||
|
|
ab0d91ea20 | ||
|
|
7f6e8909e6 | ||
|
|
4843aeef5d | ||
|
|
810c0cac63 | ||
|
|
1438d3e7eb |
No files matched your search
@@ -0,0 +1,28 @@
|
||||
name: Check Full
|
||||
|
||||
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
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Check Wayland+OpenVR
|
||||
|
||||
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
|
||||
- name: Build
|
||||
run: cargo build --verbose --no-default-features --features=wayland,openvr
|
||||
- name: Run tests
|
||||
run: cargo test --verbose --no-default-features --features=wayland,openvr
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Check Wayland+OpenXR
|
||||
|
||||
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
|
||||
- name: Build
|
||||
run: cargo build --verbose --no-default-features --features=wayland,openxr
|
||||
- name: Run tests
|
||||
run: cargo test --verbose --no-default-features --features=wayland,openxr
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Check X11+OpenVR
|
||||
|
||||
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
|
||||
- name: Build
|
||||
run: cargo build --verbose --no-default-features --features=x11,openvr
|
||||
- name: Run tests
|
||||
run: cargo test --verbose --no-default-features --features=x11,openvr
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Check X11+OpenXR
|
||||
|
||||
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
|
||||
- name: Build
|
||||
run: cargo build --verbose --no-default-features --features=x11,openxr
|
||||
- name: Run tests
|
||||
run: cargo test --verbose --no-default-features --features=x11,openxr
|
||||
@@ -7,19 +7,23 @@ on:
|
||||
|
||||
env:
|
||||
APPDIR: WlxOverlay-S.AppDir
|
||||
CARGO_TERM_COLOR: always
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
|
||||
jobs:
|
||||
make_release:
|
||||
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 fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-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
|
||||
rustup update
|
||||
|
||||
test -f linuxdeploy-x86_64.AppImage || wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
|
||||
|
||||
Generated
+1645
-457
File diff suppressed because it is too large.
Load diff
+50
-30
@@ -4,53 +4,73 @@ debug = true
|
||||
|
||||
[package]
|
||||
name = "wlx-overlay-s"
|
||||
version = "0.3.2"
|
||||
version = "0.4.1"
|
||||
edition = "2021"
|
||||
|
||||
# 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.8.5"
|
||||
clap = { version = "4.5.1", features = ["derive"] }
|
||||
cstr = "0.2.11"
|
||||
ctrlc = { version = "3.4.2", features = ["termination"] }
|
||||
anyhow = "1.0.86"
|
||||
ash = "^0.38.0"
|
||||
chrono = "0.4.38"
|
||||
chrono-tz = "0.9.0"
|
||||
clap = { version = "4.5.6", features = ["derive"] }
|
||||
config = "0.14.0"
|
||||
cstr = "0.2.12"
|
||||
ctrlc = { version = "3.4.4", features = ["termination"] }
|
||||
dbus = { version = "0.9.7" }
|
||||
flexi_logger = "0.27.4"
|
||||
flexi_logger = "0.28.1"
|
||||
fontconfig-rs = "0.1.1"
|
||||
freetype-rs = "0.32.0"
|
||||
futures = "0.3.29"
|
||||
glam = { version = "0.24.1", features = ["approx"] }
|
||||
idmap = "0.2.21"
|
||||
freetype-rs = "0.36.0"
|
||||
futures = "0.3.30"
|
||||
glam = { version = "0.27.0", features = ["approx", "serde"] }
|
||||
idmap = { version = "0.2.21", features = ["serde"] }
|
||||
idmap-derive = "0.1.2"
|
||||
input-linux = "0.6.0"
|
||||
json = { version = "0.12.4", optional = true }
|
||||
libc = "0.2.153"
|
||||
json5 = "0.4.1"
|
||||
libc = "0.2.155"
|
||||
libloading = "0.8.3"
|
||||
log = "0.4.21"
|
||||
once_cell = "1.19.0"
|
||||
openxr = { version = "0.17.1", features = ["linked"], 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 = ["wav", "hound"] }
|
||||
ovr_overlay = { features = [
|
||||
"ovr_input",
|
||||
"ovr_system",
|
||||
], git = "https://github.com/galister/ovr_overlay_oyasumi", optional = true }
|
||||
regex = "1.10.4"
|
||||
rodio = { version = "0.18.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"
|
||||
serde_yaml = "0.9.25"
|
||||
smallvec = "1.11.0"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
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.1", default-features = false }
|
||||
serde = { version = "1.0.203", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.117"
|
||||
serde_yaml = "0.9.34"
|
||||
smallvec = "1.13.2"
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
sysinfo = { version = "0.30.12" }
|
||||
thiserror = "1.0.61"
|
||||
vulkano = { git = "https://github.com/vulkano-rs/vulkano", rev = "b9f3e89" }
|
||||
vulkano-shaders = { git = "https://github.com/vulkano-rs/vulkano", rev = "b9f3e89" }
|
||||
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.3.11", default-features = false }
|
||||
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" }
|
||||
xcb = { version = "1.4.0", optional = true, features = [
|
||||
"as-raw-xcb-connection",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = ["openvr", "openxr", "osc", "x11", "wayland"]
|
||||
default = ["openvr", "openxr", "osc", "x11", "wayland"]
|
||||
openvr = ["dep:ovr_overlay", "dep:json"]
|
||||
openxr = ["dep:openxr"]
|
||||
osc = ["dep:rosc"]
|
||||
x11 = ["wlx-capture/xshm"]
|
||||
wayland = ["wlx-capture/pipewire", "wlx-capture/wlr"]
|
||||
|
||||
x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"]
|
||||
wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"]
|
||||
pipewire = ["wlx-capture/pipewire"]
|
||||
uidev = ["dep:winit"]
|
||||
xcb = ["dep:xcb"]
|
||||
as-raw-xcb-connection = []
|
||||
@@ -33,12 +33,25 @@ You may also want to [build from source](https://github.com/galister/wlx-overlay
|
||||
|
||||
**If you get a screen share pop-up, check the terminal and select the screens in the order it tells you to.**
|
||||
|
||||
If you selected the screens in the wrong order:
|
||||
- `rm ~/.config/wlxoverlay/conf.d/pw_tokens.yaml` then restart
|
||||
|
||||
SteamVR users: WlxOverlay-S will register itself for auto-start, so you will not need to start it every time.
|
||||
|
||||
**Please continue reading the guide below.**
|
||||
|
||||
# Getting Started
|
||||
|
||||
### Working Set
|
||||
|
||||
Your working set consists of your 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.
|
||||
|
||||
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.)
|
||||
|
||||
### 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. \
|
||||
@@ -67,6 +80,8 @@ The click depends on the laser color:
|
||||
- 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.
|
||||
|
||||
See the bindings section on how to grab, move and resize screens.
|
||||
|
||||
### The keyboard
|
||||
@@ -90,8 +105,16 @@ Typing
|
||||
If your bindings are not supported, please reach out. \
|
||||
We would like to work with you and include additional bindings.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
Check [here](https://github.com/galister/wlx-overlay-s/wiki/Troubleshooting) for tips.
|
||||
|
||||
# Known Issues
|
||||
|
||||
## Auto-Start not working on SteamVR 2.4.4
|
||||
|
||||
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.)
|
||||
|
||||
## Modifiers get stuck in weird ways
|
||||
|
||||
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.
|
||||
@@ -102,4 +125,6 @@ This seems to be an issue specific to Electron apps (Discord, Element, Slack, Sp
|
||||
|
||||
## X11 limitations
|
||||
|
||||
DPI scaling and upright screens are not supported on X11. These might display incorrectly or mess up your mouse position.
|
||||
- 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.
|
||||
+230
-191
@@ -1,27 +1,27 @@
|
||||
use std::{
|
||||
collections::{BinaryHeap, VecDeque},
|
||||
f32::consts::PI,
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
#[cfg(feature = "openxr")]
|
||||
use openxr as xr;
|
||||
|
||||
use glam::{Affine3A, Vec2, Vec3A, Vec3Swizzles};
|
||||
use glam::{Affine3A, Vec3, Vec3A};
|
||||
use idmap::IdMap;
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
config::AStrSetExt,
|
||||
hid::{get_keymap_wl, get_keymap_x11},
|
||||
overlays::{
|
||||
anchor::create_anchor,
|
||||
keyboard::create_keyboard,
|
||||
screen::WlxClientAlias,
|
||||
watch::{create_watch, WATCH_NAME},
|
||||
},
|
||||
state::{AppState, ScreenMeta},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::overlay::{OverlayBackend, OverlayData, OverlayState};
|
||||
use super::overlay::OverlayData;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BackendError {
|
||||
@@ -38,12 +38,22 @@ pub enum BackendError {
|
||||
Fatal(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
fn create_wl_client() -> Option<WlxClientAlias> {
|
||||
wlx_capture::wayland::WlxClient::new()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
fn create_wl_client() -> Option<WlxClientAlias> {
|
||||
None
|
||||
}
|
||||
|
||||
pub struct OverlayContainer<T>
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
overlays: IdMap<usize, OverlayData<T>>,
|
||||
pub extent: Vec2,
|
||||
wl: Option<WlxClientAlias>,
|
||||
}
|
||||
|
||||
impl<T> OverlayContainer<T>
|
||||
@@ -52,44 +62,204 @@ where
|
||||
{
|
||||
pub fn new(app: &mut AppState) -> anyhow::Result<Self> {
|
||||
let mut overlays = IdMap::new();
|
||||
let (screens, extent) = if std::env::var("WAYLAND_DISPLAY").is_ok() {
|
||||
crate::overlays::screen::get_screens_wayland(&app.session)?
|
||||
} else {
|
||||
crate::overlays::screen::get_screens_x11(&app.session)?
|
||||
};
|
||||
let mut wl = create_wl_client();
|
||||
|
||||
let keymap;
|
||||
|
||||
app.screens.clear();
|
||||
for screen in screens.iter() {
|
||||
app.screens.push(ScreenMeta {
|
||||
name: screen.state.name.clone(),
|
||||
id: screen.state.id,
|
||||
});
|
||||
let data = if let Some(wl) = wl.as_mut() {
|
||||
keymap = get_keymap_wl()
|
||||
.map_err(|f| log::warn!("Could not load keyboard layout: {}", f))
|
||||
.ok();
|
||||
crate::overlays::screen::create_screens_wayland(wl, app)?
|
||||
} else {
|
||||
keymap = get_keymap_x11()
|
||||
.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);
|
||||
crate::overlays::screen::create_screens_xshm(app)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut show_screens = app.session.config.show_screens.clone();
|
||||
if show_screens.is_empty() {
|
||||
if let Some((_, s, _)) = data.screens.first() {
|
||||
show_screens.arc_set(s.name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
OverlayData::<T> {
|
||||
state,
|
||||
backend,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
app.screens.push(meta);
|
||||
}
|
||||
|
||||
let anchor = create_anchor(app)?;
|
||||
overlays.insert(anchor.state.id, anchor);
|
||||
|
||||
let mut watch = create_watch::<T>(app)?;
|
||||
watch.state.want_visible = true;
|
||||
overlays.insert(watch.state.id, watch);
|
||||
|
||||
let mut keyboard = create_keyboard(app)?;
|
||||
let mut keyboard = create_keyboard(app, keymap)?;
|
||||
keyboard.state.show_hide = true;
|
||||
keyboard.state.want_visible = false;
|
||||
overlays.insert(keyboard.state.id, keyboard);
|
||||
|
||||
let mut show_screens = app.session.config.show_screens.clone();
|
||||
if show_screens.is_empty() {
|
||||
if let Some(s) = screens.first() {
|
||||
show_screens.push(s.state.name.clone());
|
||||
Ok(Self { overlays, wl })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
pub fn update(&mut self, _app: &mut AppState) -> anyhow::Result<Vec<OverlayData<T>>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
#[cfg(feature = "wayland")]
|
||||
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},
|
||||
watch::create_watch_canvas,
|
||||
};
|
||||
use glam::vec2;
|
||||
use wlx_capture::wayland::OutputChangeEvent;
|
||||
|
||||
let mut removed_overlays = vec![];
|
||||
let Some(wl) = self.wl.as_mut() else {
|
||||
return Ok(removed_overlays);
|
||||
};
|
||||
|
||||
wl.dispatch_pending();
|
||||
|
||||
let mut create_ran = false;
|
||||
let mut extent_dirty = false;
|
||||
let mut watch_dirty = false;
|
||||
|
||||
let mut maybe_token_store = None;
|
||||
|
||||
for ev in wl.iter_events().collect::<Vec<_>>() {
|
||||
match ev {
|
||||
OutputChangeEvent::Create(_) => {
|
||||
if create_ran {
|
||||
continue;
|
||||
}
|
||||
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,
|
||||
OverlayData::<T> {
|
||||
state,
|
||||
backend,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
app.screens.push(meta);
|
||||
watch_dirty = true;
|
||||
}
|
||||
}
|
||||
OutputChangeEvent::Destroy(id) => {
|
||||
let Some(idx) = app.screens.iter().position(|s| s.native_handle == id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let meta = &app.screens[idx];
|
||||
let removed = self.overlays.remove(meta.id).unwrap();
|
||||
removed_overlays.push(removed);
|
||||
log::info!("{}: Destroyed", meta.name);
|
||||
app.screens.remove(idx);
|
||||
watch_dirty = true;
|
||||
extent_dirty = true;
|
||||
}
|
||||
OutputChangeEvent::Logical(id) => {
|
||||
let Some(meta) = app.screens.iter().find(|s| s.native_handle == id) else {
|
||||
continue;
|
||||
};
|
||||
let output = wl.outputs.get(id).unwrap();
|
||||
let Some(overlay) = self.overlays.get_mut(meta.id) else {
|
||||
continue;
|
||||
};
|
||||
let logical_pos =
|
||||
vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32);
|
||||
let logical_size =
|
||||
vec2(output.logical_size.0 as f32, output.logical_size.1 as f32);
|
||||
let transform = output.transform.into();
|
||||
overlay
|
||||
.backend
|
||||
.set_interaction(Box::new(create_screen_interaction(
|
||||
logical_pos,
|
||||
logical_size,
|
||||
transform,
|
||||
)));
|
||||
extent_dirty = true;
|
||||
}
|
||||
OutputChangeEvent::Physical(id) => {
|
||||
let Some(meta) = app.screens.iter().find(|s| s.native_handle == id) else {
|
||||
continue;
|
||||
};
|
||||
let output = wl.outputs.get(id).unwrap();
|
||||
let Some(overlay) = self.overlays.get_mut(meta.id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let has_wlr_dmabuf = wl.maybe_wlr_dmabuf_mgr.is_some();
|
||||
let has_wlr_screencopy = wl.maybe_wlr_screencopy_mgr.is_some();
|
||||
|
||||
let pw_token_store = maybe_token_store.get_or_insert_with(|| {
|
||||
load_pw_token_config().unwrap_or_else(|e| {
|
||||
log::warn!("Failed to load PipeWire token config: {:?}", e);
|
||||
Default::default()
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(renderer) = create_screen_renderer_wl(
|
||||
output,
|
||||
has_wlr_dmabuf,
|
||||
has_wlr_screencopy,
|
||||
pw_token_store,
|
||||
&app.session,
|
||||
) {
|
||||
overlay.backend.set_renderer(Box::new(renderer));
|
||||
}
|
||||
extent_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for mut screen in screens {
|
||||
if show_screens.contains(&screen.state.name) {
|
||||
screen.state.show_hide = true;
|
||||
screen.state.want_visible = false;
|
||||
}
|
||||
overlays.insert(screen.state.id, screen);
|
||||
if extent_dirty && !create_ran {
|
||||
let extent = wl.get_desktop_extent();
|
||||
let origin = wl.get_desktop_origin();
|
||||
app.hid_provider
|
||||
.set_desktop_extent(vec2(extent.0 as f32, extent.1 as f32));
|
||||
app.hid_provider
|
||||
.set_desktop_origin(vec2(origin.0 as f32, origin.1 as f32));
|
||||
}
|
||||
Ok(Self { overlays, extent })
|
||||
|
||||
if watch_dirty {
|
||||
let watch = self.mut_by_name(WATCH_NAME).unwrap(); // want panic
|
||||
match create_watch_canvas(None, app) {
|
||||
Ok(canvas) => {
|
||||
watch.backend = Box::new(canvas);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to create watch canvas: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(removed_overlays)
|
||||
}
|
||||
|
||||
pub fn mut_by_selector(&mut self, selector: &OverlaySelector) -> Option<&mut OverlayData<T>> {
|
||||
@@ -147,10 +317,20 @@ where
|
||||
.values()
|
||||
.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));
|
||||
let hmd = snap_upright(app.input_state.hmd, Vec3A::Y);
|
||||
app.anchor = hmd * *ANCHOR_LOCAL;
|
||||
}
|
||||
|
||||
self.overlays.values_mut().for_each(|o| {
|
||||
if o.state.show_hide {
|
||||
o.state.want_visible = !any_shown;
|
||||
if o.state.want_visible && o.state.recenter {
|
||||
if o.state.want_visible
|
||||
&& app.session.config.realign_on_showhide
|
||||
&& o.state.recenter
|
||||
{
|
||||
o.state.reset(app, false);
|
||||
}
|
||||
}
|
||||
@@ -162,170 +342,29 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[derive(Clone, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum OverlaySelector {
|
||||
Id(usize),
|
||||
Name(Arc<str>),
|
||||
}
|
||||
|
||||
struct AppTask {
|
||||
pub not_before: Instant,
|
||||
pub task: TaskType,
|
||||
}
|
||||
pub fn snap_upright(transform: Affine3A, up_dir: Vec3A) -> Affine3A {
|
||||
if transform.x_axis.dot(up_dir).abs() < 0.2 {
|
||||
let scale = transform.x_axis.length();
|
||||
let col_z = transform.z_axis.normalize();
|
||||
let col_y = up_dir;
|
||||
let col_x = col_y.cross(col_z);
|
||||
let col_y = col_z.cross(col_x).normalize();
|
||||
let col_x = col_x.normalize();
|
||||
|
||||
impl PartialEq<AppTask> for AppTask {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.not_before == other.not_before
|
||||
Affine3A::from_cols(
|
||||
col_x * scale,
|
||||
col_y * scale,
|
||||
col_z * scale,
|
||||
transform.translation,
|
||||
)
|
||||
} else {
|
||||
transform
|
||||
}
|
||||
}
|
||||
impl PartialOrd<AppTask> for AppTask {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
impl Eq for AppTask {}
|
||||
impl Ord for AppTask {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.not_before.cmp(&other.not_before).reverse()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SystemTask {
|
||||
ColorGain(ColorChannel, f32),
|
||||
ResetPlayspace,
|
||||
FixFloor,
|
||||
}
|
||||
|
||||
pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send;
|
||||
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),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
pub enum ColorChannel {
|
||||
R,
|
||||
G,
|
||||
B,
|
||||
All,
|
||||
}
|
||||
|
||||
pub struct TaskContainer {
|
||||
tasks: BinaryHeap<AppTask>,
|
||||
}
|
||||
|
||||
impl TaskContainer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tasks: BinaryHeap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enqueue(&mut self, task: TaskType) {
|
||||
self.tasks.push(AppTask {
|
||||
not_before: Instant::now(),
|
||||
task,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn enqueue_at(&mut self, task: TaskType, not_before: Instant) {
|
||||
self.tasks.push(AppTask { not_before, task });
|
||||
}
|
||||
|
||||
pub fn retrieve_due(&mut self, dest_buf: &mut VecDeque<TaskType>) {
|
||||
let now = Instant::now();
|
||||
|
||||
while let Some(task) = self.tasks.peek() {
|
||||
if task.not_before > now {
|
||||
break;
|
||||
}
|
||||
|
||||
// Safe unwrap because we peeked.
|
||||
dest_buf.push_back(self.tasks.pop().unwrap().task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn raycast_plane(
|
||||
source: &Affine3A,
|
||||
source_fwd: Vec3A,
|
||||
plane: &Affine3A,
|
||||
plane_norm: Vec3A,
|
||||
) -> Option<(f32, Vec2)> {
|
||||
let plane_normal = plane.transform_vector3a(plane_norm);
|
||||
let ray_dir = source.transform_vector3a(source_fwd);
|
||||
|
||||
let d = plane.translation.dot(-plane_normal);
|
||||
let dist = -(d + source.translation.dot(plane_normal)) / ray_dir.dot(plane_normal);
|
||||
|
||||
let hit_local = plane
|
||||
.inverse()
|
||||
.transform_point3a(source.translation + ray_dir * dist)
|
||||
.xy();
|
||||
|
||||
Some((dist, hit_local))
|
||||
}
|
||||
|
||||
pub fn raycast_cylinder(
|
||||
source: &Affine3A,
|
||||
source_fwd: Vec3A,
|
||||
plane: &Affine3A,
|
||||
curvature: f32,
|
||||
) -> Option<(f32, Vec2)> {
|
||||
// this is solved locally; (0,0) is the center of the cylinder, and the cylinder is aligned with the Y axis
|
||||
let size = plane.x_axis.length();
|
||||
let to_local = Affine3A {
|
||||
matrix3: plane.matrix3.mul_scalar(1.0 / size),
|
||||
translation: plane.translation,
|
||||
}
|
||||
.inverse();
|
||||
|
||||
let r = 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 d = ray_dir.xz();
|
||||
let s = ray_origin.xz();
|
||||
|
||||
let a = d.dot(d);
|
||||
let b = d.dot(s);
|
||||
let c = s.dot(s) - r * r;
|
||||
|
||||
let d = (b * b) - (a * 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 t = t1.max(t2);
|
||||
|
||||
if t < f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut hit_local = ray_origin + ray_dir * t;
|
||||
if hit_local.z > 0.0 {
|
||||
// hitting the opposite half of the cylinder
|
||||
return None;
|
||||
}
|
||||
|
||||
let max_angle = 2.0 * (size / (2.0 * r));
|
||||
let x_angle = (hit_local.x / r).asin();
|
||||
|
||||
hit_local.x = x_angle / max_angle;
|
||||
hit_local.y /= size;
|
||||
|
||||
Some((t, hit_local.xy()))
|
||||
}
|
||||
+149
-21
@@ -1,22 +1,19 @@
|
||||
use std::f32::consts::PI;
|
||||
use std::{collections::VecDeque, time::Instant};
|
||||
|
||||
use glam::{Affine3A, Vec2, Vec3A};
|
||||
use glam::{Affine3A, Vec2, Vec3, Vec3A, Vec3Swizzles};
|
||||
|
||||
#[cfg(feature = "openvr")]
|
||||
use ovr_overlay::TrackedDeviceIndex;
|
||||
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 super::{
|
||||
common::{raycast_cylinder, raycast_plane, OverlayContainer},
|
||||
overlay::OverlayData,
|
||||
};
|
||||
use super::task::{TaskContainer, TaskType};
|
||||
use super::{common::OverlayContainer, overlay::OverlayData};
|
||||
|
||||
pub struct TrackedDevice {
|
||||
#[cfg(feature = "openvr")]
|
||||
pub index: TrackedDeviceIndex,
|
||||
pub valid: bool,
|
||||
pub soc: Option<f32>,
|
||||
pub charging: bool,
|
||||
pub role: TrackedDeviceRole,
|
||||
@@ -34,6 +31,7 @@ pub enum TrackedDeviceRole {
|
||||
|
||||
pub struct InputState {
|
||||
pub hmd: Affine3A,
|
||||
pub ipd: f32,
|
||||
pub pointers: [Pointer; 2],
|
||||
pub devices: Vec<TrackedDevice>,
|
||||
}
|
||||
@@ -42,6 +40,7 @@ impl InputState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hmd: Affine3A::IDENTITY,
|
||||
ipd: 0.0,
|
||||
pointers: [Pointer::new(0), Pointer::new(1)],
|
||||
devices: Vec::new(),
|
||||
}
|
||||
@@ -195,6 +194,7 @@ pub struct PointerState {
|
||||
pub space_rotate: bool,
|
||||
pub click_modifier_right: bool,
|
||||
pub click_modifier_middle: bool,
|
||||
pub move_mouse: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
@@ -243,6 +243,8 @@ struct RayHit {
|
||||
pub struct GrabData {
|
||||
pub offset: Vec3A,
|
||||
pub grabbed_id: usize,
|
||||
pub old_curvature: Option<f32>,
|
||||
pub grab_all: bool,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
@@ -281,11 +283,17 @@ fn interact_hand<O>(
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let hmd = &app.input_state.hmd;
|
||||
let hmd = app.input_state.hmd;
|
||||
let mut pointer = &mut app.input_state.pointers[idx];
|
||||
if let Some(grab_data) = pointer.interaction.grabbed {
|
||||
if let Some(grabbed) = overlays.mut_by_id(grab_data.grabbed_id) {
|
||||
pointer.handle_grabbed(grabbed, hmd);
|
||||
pointer.handle_grabbed(
|
||||
grabbed,
|
||||
&hmd,
|
||||
&app.anchor,
|
||||
&mut app.tasks,
|
||||
&mut app.session.config,
|
||||
);
|
||||
} else {
|
||||
log::warn!("Grabbed overlay {} does not exist", grab_data.grabbed_id);
|
||||
pointer.interaction.grabbed = None;
|
||||
@@ -347,7 +355,7 @@ where
|
||||
log::trace!("Hit: {} {:?}", hovered.state.name, hit);
|
||||
|
||||
if pointer.now.grab && !pointer.before.grab && hovered.state.grabbable {
|
||||
pointer.start_grab(hovered);
|
||||
pointer.start_grab(hovered, &mut app.tasks);
|
||||
return (
|
||||
hit.dist,
|
||||
Some(Haptics {
|
||||
@@ -454,7 +462,7 @@ impl Pointer {
|
||||
None
|
||||
}
|
||||
|
||||
fn start_grab<O>(&mut self, overlay: &mut OverlayData<O>)
|
||||
fn start_grab<O>(&mut self, overlay: &mut OverlayData<O>, tasks: &mut TaskContainer)
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
@@ -466,12 +474,33 @@ impl Pointer {
|
||||
self.interaction.grabbed = Some(GrabData {
|
||||
offset,
|
||||
grabbed_id: overlay.state.id,
|
||||
old_curvature: overlay.state.curvature,
|
||||
grab_all: matches!(self.interaction.mode, PointerMode::Right),
|
||||
});
|
||||
tasks.enqueue(TaskType::Overlay(
|
||||
OverlaySelector::Name(ANCHOR_NAME.clone()),
|
||||
Box::new(|app, o| {
|
||||
o.transform = app.anchor
|
||||
* Affine3A::from_scale_rotation_translation(
|
||||
Vec3::ONE * o.spawn_scale,
|
||||
o.spawn_rotation,
|
||||
o.spawn_point.into(),
|
||||
);
|
||||
o.dirty = true;
|
||||
o.want_visible = true;
|
||||
}),
|
||||
));
|
||||
log::info!("Hand {}: grabbed {}", self.idx, overlay.state.name);
|
||||
}
|
||||
|
||||
fn handle_grabbed<O>(&mut self, overlay: &mut OverlayData<O>, hmd: &Affine3A)
|
||||
where
|
||||
fn handle_grabbed<O>(
|
||||
&mut self,
|
||||
overlay: &mut OverlayData<O>,
|
||||
hmd: &Affine3A,
|
||||
anchor: &Affine3A,
|
||||
tasks: &mut TaskContainer,
|
||||
config: &mut GeneralConfig,
|
||||
) where
|
||||
O: Default,
|
||||
{
|
||||
if self.now.grab {
|
||||
@@ -491,7 +520,7 @@ impl Pointer {
|
||||
.transform
|
||||
.matrix3
|
||||
.mul_scalar(1.0 - 0.025 * self.now.scroll);
|
||||
} else {
|
||||
} else if config.allow_sliding && self.now.scroll.is_finite() {
|
||||
grab_data.offset.z -= self.now.scroll * 0.05;
|
||||
}
|
||||
overlay.state.transform.translation = self.pose.transform_point3a(grab_data.offset);
|
||||
@@ -502,11 +531,33 @@ impl Pointer {
|
||||
self.interaction.grabbed = None;
|
||||
}
|
||||
} else {
|
||||
overlay.state.saved_point = Some(
|
||||
hmd.inverse()
|
||||
.transform_point3a(overlay.state.transform.translation),
|
||||
);
|
||||
if overlay.state.anchored {
|
||||
overlay.state.saved_transform =
|
||||
Some(snap_upright(*anchor, Vec3A::Y).inverse() * overlay.state.transform);
|
||||
|
||||
if let Some(grab_data) = self.interaction.grabbed.as_ref() {
|
||||
if overlay.state.curvature != grab_data.old_curvature {
|
||||
if let Some(val) = overlay.state.curvature {
|
||||
config.curve_values.arc_set(overlay.state.name.clone(), val);
|
||||
} else {
|
||||
let ref_name = overlay.state.name.as_ref();
|
||||
config.curve_values.arc_rm(ref_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
config.transform_values.arc_set(
|
||||
overlay.state.name.clone(),
|
||||
overlay.state.saved_transform.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
self.interaction.grabbed = None;
|
||||
tasks.enqueue(TaskType::Overlay(
|
||||
OverlaySelector::Name(ANCHOR_NAME.clone()),
|
||||
Box::new(|_app, o| {
|
||||
o.want_visible = false;
|
||||
}),
|
||||
));
|
||||
log::info!("Hand {}: dropped {}", self.idx, overlay.state.name);
|
||||
}
|
||||
}
|
||||
@@ -535,3 +586,80 @@ impl Pointer {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn raycast_plane(
|
||||
source: &Affine3A,
|
||||
source_fwd: Vec3A,
|
||||
plane: &Affine3A,
|
||||
plane_norm: Vec3A,
|
||||
) -> Option<(f32, Vec2)> {
|
||||
let plane_normal = plane.transform_vector3a(plane_norm);
|
||||
let ray_dir = source.transform_vector3a(source_fwd);
|
||||
|
||||
let d = plane.translation.dot(-plane_normal);
|
||||
let dist = -(d + source.translation.dot(plane_normal)) / ray_dir.dot(plane_normal);
|
||||
|
||||
let hit_local = plane
|
||||
.inverse()
|
||||
.transform_point3a(source.translation + ray_dir * dist)
|
||||
.xy();
|
||||
|
||||
Some((dist, hit_local))
|
||||
}
|
||||
|
||||
fn raycast_cylinder(
|
||||
source: &Affine3A,
|
||||
source_fwd: Vec3A,
|
||||
plane: &Affine3A,
|
||||
curvature: f32,
|
||||
) -> Option<(f32, Vec2)> {
|
||||
// this is solved locally; (0,0) is the center of the cylinder, and the cylinder is aligned with the Y axis
|
||||
let size = plane.x_axis.length();
|
||||
let to_local = Affine3A {
|
||||
matrix3: plane.matrix3.mul_scalar(1.0 / size),
|
||||
translation: plane.translation,
|
||||
}
|
||||
.inverse();
|
||||
|
||||
let r = 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 d = ray_dir.xz();
|
||||
let s = ray_origin.xz();
|
||||
|
||||
let a = d.dot(d);
|
||||
let b = d.dot(s);
|
||||
let c = s.dot(s) - r * r;
|
||||
|
||||
let d = (b * b) - (a * 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 t = t1.max(t2);
|
||||
|
||||
if t < f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut hit_local = ray_origin + ray_dir * t;
|
||||
if hit_local.z > 0.0 {
|
||||
// hitting the opposite half of the cylinder
|
||||
return None;
|
||||
}
|
||||
|
||||
let max_angle = 2.0 * (size / (2.0 * r));
|
||||
let x_angle = (hit_local.x / r).asin();
|
||||
|
||||
hit_local.x = x_angle / max_angle;
|
||||
hit_local.y /= size;
|
||||
|
||||
Some((t, hit_local.xy()))
|
||||
}
|
||||
@@ -8,7 +8,12 @@ pub mod openvr;
|
||||
#[cfg(feature = "openxr")]
|
||||
pub mod openxr;
|
||||
|
||||
#[cfg(feature = "uidev")]
|
||||
pub mod uidev;
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
pub mod osc;
|
||||
|
||||
pub mod overlay;
|
||||
|
||||
pub mod task;
|
||||
@@ -1,7 +1,6 @@
|
||||
use dbus::{blocking::Connection, channel::MatchingReceiver, message::MatchRule};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc, Arc,
|
||||
@@ -9,7 +8,10 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{config::def_true, config_io, overlays::toast::Toast, state::AppState};
|
||||
use crate::{
|
||||
overlays::toast::{Toast, ToastTopic},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub struct NotificationManager {
|
||||
rx_toast: mpsc::Receiver<Toast>,
|
||||
@@ -167,9 +169,13 @@ impl NotificationManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
let toast = Toast::new(msg.title, msg.content.unwrap_or_else(|| "".into()))
|
||||
.with_timeout(msg.timeout.unwrap_or(5.))
|
||||
.with_sound(msg.volume.unwrap_or(-1.) >= 0.); // XSOverlay still plays at 0,
|
||||
let toast = Toast::new(
|
||||
ToastTopic::XSNotification,
|
||||
msg.title,
|
||||
msg.content.unwrap_or_else(|| "".into()),
|
||||
)
|
||||
.with_timeout(msg.timeout.unwrap_or(5.))
|
||||
.with_sound(msg.volume.unwrap_or(-1.) >= 0.); // XSOverlay still plays at 0,
|
||||
|
||||
match sender.try_send(toast) {
|
||||
Ok(_) => {}
|
||||
@@ -204,9 +210,11 @@ fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
|
||||
summary
|
||||
};
|
||||
|
||||
Ok(Toast::new(title.into(), body.into())
|
||||
.with_timeout(5.0)
|
||||
.with_opacity(1.0))
|
||||
Ok(
|
||||
Toast::new(ToastTopic::DesktopNotification, title.into(), body.into())
|
||||
.with_timeout(5.0)
|
||||
.with_opacity(1.0),
|
||||
)
|
||||
// leave the audio part to the desktop env
|
||||
}
|
||||
|
||||
@@ -227,29 +235,3 @@ struct XsoMessage {
|
||||
sourceApp: Option<Arc<str>>,
|
||||
alwaysShow: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct NotifiConf {
|
||||
#[serde(default = "def_true")]
|
||||
pub notifications_enabled: bool,
|
||||
|
||||
#[serde(default = "def_true")]
|
||||
pub notifications_sound_enabled: bool,
|
||||
}
|
||||
|
||||
fn get_config_path() -> PathBuf {
|
||||
let mut path = config_io::get_conf_d_path();
|
||||
path.push("notifications.yaml");
|
||||
path
|
||||
}
|
||||
pub fn save_notifications(app: &mut AppState) -> anyhow::Result<()> {
|
||||
let conf = NotifiConf {
|
||||
notifications_enabled: app.session.config.notifications_enabled,
|
||||
notifications_sound_enabled: app.session.config.notifications_sound_enabled,
|
||||
};
|
||||
|
||||
let yaml = serde_yaml::to_string(&conf)?;
|
||||
std::fs::write(get_config_path(), yaml)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use glam::Affine3A;
|
||||
use ovr_overlay::{pose::Matrix3x4, settings::SettingsManager, sys::HmdMatrix34_t};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::backend::common::{BackendError, ColorChannel};
|
||||
use crate::backend::{common::BackendError, task::ColorChannel};
|
||||
|
||||
pub trait Affine3AConvert {
|
||||
fn from_affine(affine: &Affine3A) -> Self;
|
||||
|
||||
@@ -39,6 +39,7 @@ 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 INPUT_ANY: InputValueHandle = InputValueHandle(ovr_overlay::sys::k_ulInvalidInputValueHandle);
|
||||
|
||||
@@ -54,6 +55,7 @@ pub(super) struct OpenVrInputSource {
|
||||
space_rotate_hnd: ActionHandle,
|
||||
click_modifier_right_hnd: ActionHandle,
|
||||
click_modifier_middle_hnd: ActionHandle,
|
||||
move_mouse_hnd: ActionHandle,
|
||||
}
|
||||
|
||||
pub(super) struct OpenVrHandSource {
|
||||
@@ -77,6 +79,7 @@ impl OpenVrInputSource {
|
||||
let space_rotate_hnd = input.get_action_handle(PATH_SPACE_ROTATE)?;
|
||||
let click_modifier_right_hnd = input.get_action_handle(PATH_CLICK_MODIFIER_RIGHT)?;
|
||||
let click_modifier_middle_hnd = input.get_action_handle(PATH_CLICK_MODIFIER_MIDDLE)?;
|
||||
let move_mouse_hnd = input.get_action_handle(PATH_MOVE_MOUSE)?;
|
||||
|
||||
let input_hnd: Vec<InputValueHandle> = INPUT_SOURCES
|
||||
.iter()
|
||||
@@ -112,6 +115,7 @@ impl OpenVrInputSource {
|
||||
space_rotate_hnd,
|
||||
click_modifier_right_hnd,
|
||||
click_modifier_middle_hnd,
|
||||
move_mouse_hnd,
|
||||
hands,
|
||||
})
|
||||
}
|
||||
@@ -212,6 +216,11 @@ impl OpenVrInputSource {
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
|
||||
app_hand.now.move_mouse = input
|
||||
.get_digital_action_data(self.move_mouse_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.bState)
|
||||
.unwrap_or(false);
|
||||
|
||||
app_hand.now.scroll = input
|
||||
.get_analog_action_data(self.scroll_hnd, hand.input_hnd)
|
||||
.map(|x| x.0.y)
|
||||
@@ -300,8 +309,6 @@ fn get_tracked_device(
|
||||
}
|
||||
|
||||
Some(TrackedDevice {
|
||||
valid: true,
|
||||
index,
|
||||
soc,
|
||||
charging,
|
||||
role,
|
||||
@@ -311,7 +318,11 @@ fn get_tracked_device(
|
||||
pub fn set_action_manifest(input: &mut InputManager) -> anyhow::Result<()> {
|
||||
let action_path = CONFIG_ROOT_PATH.join("actions.json");
|
||||
|
||||
File::create(&action_path)?.write_all(include_bytes!("../../res/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);
|
||||
}
|
||||
|
||||
let binding_path = CONFIG_ROOT_PATH.join("actions_binding_knuckles.json");
|
||||
if !binding_path.is_file() {
|
||||
|
||||
@@ -80,7 +80,7 @@ impl LinePool {
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
data.data.sort_order = 69;
|
||||
data.state.z_order = 69;
|
||||
data.state.dirty = true;
|
||||
|
||||
self.lines.insert(id, data);
|
||||
|
||||
@@ -20,7 +20,7 @@ use vulkano::{
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
common::SystemTask,
|
||||
common::{BackendError, OverlayContainer},
|
||||
input::interact,
|
||||
notifications::NotificationManager,
|
||||
openvr::{
|
||||
@@ -31,14 +31,16 @@ use crate::{
|
||||
overlay::OpenVrOverlayData,
|
||||
},
|
||||
overlay::OverlayData,
|
||||
task::{SystemTask, TaskType},
|
||||
},
|
||||
graphics::WlxGraphics,
|
||||
overlays::watch::{watch_fade, WATCH_NAME},
|
||||
overlays::{
|
||||
toast::{Toast, ToastTopic},
|
||||
watch::{watch_fade, WATCH_NAME},
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::common::{BackendError, OverlayContainer, TaskType};
|
||||
|
||||
pub mod helpers;
|
||||
pub mod input;
|
||||
pub mod lines;
|
||||
@@ -93,6 +95,14 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
AppState::from_graphics(graphics)?
|
||||
};
|
||||
|
||||
if let Ok(ipd) = system_mgr.get_tracked_device_property::<f32>(
|
||||
TrackedDeviceIndex::HMD,
|
||||
ETrackedDeviceProperty::Prop_UserIpdMeters_Float,
|
||||
) {
|
||||
state.input_state.ipd = (ipd * 10000.0).round() * 0.1;
|
||||
log::info!("IPD: {:.1} mm", state.input_state.ipd);
|
||||
}
|
||||
|
||||
let _ = install_manifest(&mut app_mgr);
|
||||
|
||||
let mut overlays = OverlayContainer::<OpenVrOverlayData>::new(&mut state)?;
|
||||
@@ -107,8 +117,6 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
let mut osc_sender =
|
||||
crate::backend::osc::OscSender::new(state.session.config.osc_out_port).ok();
|
||||
|
||||
state.hid_provider.set_desktop_extent(overlays.extent);
|
||||
|
||||
set_action_manifest(&mut input_mgr)?;
|
||||
|
||||
let mut input_source = OpenVrInputSource::new(&mut input_mgr)?;
|
||||
@@ -162,6 +170,24 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
| EVREventType::VREvent_SceneApplicationChanged => {
|
||||
playspace.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
|
||||
}
|
||||
EVREventType::VREvent_IpdChanged => {
|
||||
if let Ok(ipd) = system_mgr.get_tracked_device_property::<f32>(
|
||||
TrackedDeviceIndex::HMD,
|
||||
ETrackedDeviceProperty::Prop_UserIpdMeters_Float,
|
||||
) {
|
||||
let ipd = (ipd * 10000.0).round() * 0.1;
|
||||
if (ipd - state.input_state.ipd).abs() > 0.05 {
|
||||
log::info!("IPD: {:.1} mm -> {:.1} mm", state.input_state.ipd, ipd);
|
||||
Toast::new(
|
||||
ToastTopic::IpdChange,
|
||||
"IPD".into(),
|
||||
format!("{:.1} mm", ipd).into(),
|
||||
)
|
||||
.submit(&mut state);
|
||||
}
|
||||
state.input_state.ipd = ipd;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -174,12 +200,20 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
notifications.submit_pending(&mut state);
|
||||
|
||||
state.tasks.retrieve_due(&mut due_tasks);
|
||||
|
||||
let mut removed_overlays = overlays.update(&mut state)?;
|
||||
for o in removed_overlays.iter_mut() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
TaskType::CreateOverlay(sel, f) => {
|
||||
@@ -262,6 +296,8 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
}
|
||||
}
|
||||
|
||||
state.hid_provider.commit();
|
||||
|
||||
lines.update(universe.clone(), &mut overlay_mgr, &mut state)?;
|
||||
|
||||
for o in overlays.iter_mut() {
|
||||
@@ -290,8 +326,6 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
// chaperone
|
||||
|
||||
// close font handles?
|
||||
|
||||
state.hid_provider.on_new_frame();
|
||||
}
|
||||
|
||||
log::warn!("OpenVR shutdown");
|
||||
|
||||
@@ -11,7 +11,6 @@ use vulkano::{Handle, VulkanObject};
|
||||
use crate::{
|
||||
backend::overlay::{OverlayData, RelativeTo},
|
||||
graphics::WlxGraphics,
|
||||
overlays::watch::WATCH_NAME,
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
@@ -23,7 +22,6 @@ pub(super) struct OpenVrOverlayData {
|
||||
pub(super) last_image: Option<u64>,
|
||||
pub(super) visible: bool,
|
||||
pub(super) color: Vec4,
|
||||
pub(super) sort_order: u32,
|
||||
pub(crate) width: f32,
|
||||
pub(super) override_width: bool,
|
||||
pub(super) relative_to: RelativeTo,
|
||||
@@ -45,11 +43,6 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
};
|
||||
log::debug!("{}: initialize", self.state.name);
|
||||
|
||||
//watch
|
||||
if *self.state.name == *WATCH_NAME {
|
||||
self.data.sort_order = 68;
|
||||
}
|
||||
|
||||
self.data.handle = Some(handle);
|
||||
self.data.color = Vec4::ONE;
|
||||
|
||||
@@ -189,7 +182,7 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
log::debug!("{}: No overlay handle", self.state.name);
|
||||
return;
|
||||
};
|
||||
if let Err(e) = overlay.set_sort_order(handle, self.data.sort_order) {
|
||||
if let Err(e) = overlay.set_sort_order(handle, self.state.z_order) {
|
||||
log::error!("{}: Failed to set overlay z order: {}", self.state.name, e);
|
||||
}
|
||||
}
|
||||
@@ -260,7 +253,7 @@ impl OverlayData<OpenVrOverlayData> {
|
||||
m_pQueue: graphics.queue.handle().as_raw() as *mut _,
|
||||
m_nQueueFamilyIndex: graphics.queue.queue_family_index(),
|
||||
};
|
||||
log::debug!(
|
||||
log::trace!(
|
||||
"{}: UploadTex {:?}, {}x{}, {:?}",
|
||||
self.state.name,
|
||||
format,
|
||||
|
||||
@@ -114,7 +114,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 +200,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(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use glam::{Affine3A, Quat, Vec3, Vec3A};
|
||||
use openxr as xr;
|
||||
use sysinfo::Process;
|
||||
use xr::OverlaySessionCreateFlagsEXTX;
|
||||
|
||||
pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
@@ -21,6 +24,17 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
let mut enabled_extensions = xr::ExtensionSet::default();
|
||||
enabled_extensions.khr_vulkan_enable2 = true;
|
||||
enabled_extensions.extx_overlay = true;
|
||||
if available_extensions.khr_binding_modification && available_extensions.ext_dpad_binding {
|
||||
enabled_extensions.khr_binding_modification = true;
|
||||
enabled_extensions.ext_dpad_binding = true;
|
||||
} else {
|
||||
log::warn!("Missing EXT_dpad_binding extension.");
|
||||
}
|
||||
if available_extensions.ext_hp_mixed_reality_controller {
|
||||
enabled_extensions.ext_hp_mixed_reality_controller = true;
|
||||
} else {
|
||||
log::warn!("Missing EXT_hp_mixed_reality_controller extension.");
|
||||
}
|
||||
|
||||
//#[cfg(not(debug_assertions))]
|
||||
let layers = [];
|
||||
@@ -74,6 +88,7 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
|
||||
|
||||
Ok((xr_instance, system))
|
||||
}
|
||||
|
||||
pub(super) unsafe fn create_overlay_session(
|
||||
instance: &xr::Instance,
|
||||
system: xr::SystemId,
|
||||
@@ -109,34 +124,21 @@ pub(super) unsafe fn create_overlay_session(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn hmd_pose_from_views(views: &[xr::View]) -> Affine3A {
|
||||
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 = unsafe { std::mem::transmute(views[0].pose.orientation) };
|
||||
let rot1 = unsafe { std::mem::transmute(views[1].pose.orientation) };
|
||||
quat_lerp(rot0, rot1, 0.5)
|
||||
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)
|
||||
};
|
||||
|
||||
Affine3A::from_rotation_translation(rot, pos)
|
||||
}
|
||||
|
||||
fn quat_lerp(a: Quat, mut b: Quat, t: f32) -> Quat {
|
||||
let l2 = a.dot(b);
|
||||
if l2 < 0.0 {
|
||||
b = -b;
|
||||
}
|
||||
|
||||
Quat::from_xyzw(
|
||||
a.x - t * (a.x - b.x),
|
||||
a.y - t * (a.y - b.y),
|
||||
a.z - t * (a.z - b.z),
|
||||
a.w - t * (a.w - b.w),
|
||||
)
|
||||
.normalize()
|
||||
(Affine3A::from_rotation_translation(rot, pos), ipd)
|
||||
}
|
||||
|
||||
pub(super) fn transform_to_norm_quat(transform: &Affine3A) -> Quat {
|
||||
@@ -171,3 +173,49 @@ pub(super) fn transform_to_posef(transform: &Affine3A) -> xr::Posef {
|
||||
let rotation = transform_to_norm_quat(transform);
|
||||
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
|
||||
}
|
||||
+399
-336
@@ -2,9 +2,11 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use glam::{bool, Affine3A, Quat, Vec3};
|
||||
use openxr as xr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
backend::input::{Haptics, Pointer},
|
||||
config_io,
|
||||
state::{AppSession, AppState},
|
||||
};
|
||||
|
||||
@@ -13,26 +15,7 @@ use super::XrState;
|
||||
type XrSession = xr::Session<xr::Vulkan>;
|
||||
static DOUBLE_CLICK_TIME: Duration = Duration::from_millis(500);
|
||||
|
||||
pub(super) struct DoubleClickCounter {
|
||||
pub(super) last_click: Option<Instant>,
|
||||
}
|
||||
|
||||
impl DoubleClickCounter {
|
||||
pub(super) fn new() -> Self {
|
||||
Self { last_click: None }
|
||||
}
|
||||
|
||||
// submit a click. returns true if it should count as a double click
|
||||
pub(super) fn click(&mut self) -> bool {
|
||||
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) };
|
||||
double_click
|
||||
}
|
||||
}
|
||||
pub(super) struct OpenXrAction {}
|
||||
|
||||
pub(super) struct OpenXrInputSource {
|
||||
action_set: xr::ActionSet,
|
||||
@@ -44,20 +27,130 @@ pub(super) struct OpenXrHand {
|
||||
space: xr::Space,
|
||||
}
|
||||
|
||||
pub struct CustomClickAction {
|
||||
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,
|
||||
}
|
||||
|
||||
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),
|
||||
&[],
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
action_f32,
|
||||
action_f32_double,
|
||||
action_bool,
|
||||
action_bool_double,
|
||||
last_click: None,
|
||||
held: false,
|
||||
})
|
||||
}
|
||||
pub fn state(
|
||||
&mut self,
|
||||
before: bool,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct OpenXrHandSource {
|
||||
action_pose: xr::Action<xr::Posef>,
|
||||
action_click: xr::Action<f32>,
|
||||
action_grab: xr::Action<f32>,
|
||||
action_click: CustomClickAction,
|
||||
action_grab: CustomClickAction,
|
||||
action_alt_click: CustomClickAction,
|
||||
action_show_hide: CustomClickAction,
|
||||
action_space_drag: CustomClickAction,
|
||||
action_space_rotate: CustomClickAction,
|
||||
action_modifier_right: CustomClickAction,
|
||||
action_modifier_middle: CustomClickAction,
|
||||
action_move_mouse: CustomClickAction,
|
||||
action_scroll: xr::Action<f32>,
|
||||
action_alt_click: xr::Action<f32>,
|
||||
action_show_hide: xr::Action<bool>,
|
||||
action_click_modifier_right: xr::Action<bool>,
|
||||
action_click_modifier_middle: xr::Action<bool>,
|
||||
action_haptics: xr::Action<xr::Haptic>,
|
||||
}
|
||||
|
||||
impl OpenXrInputSource {
|
||||
pub fn new(xr: &XrState) -> Result<Self, xr::sys::Result> {
|
||||
pub fn new(xr: &XrState) -> anyhow::Result<Self> {
|
||||
let mut action_set =
|
||||
xr.session
|
||||
.instance()
|
||||
@@ -94,7 +187,7 @@ impl OpenXrInputSource {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update(&self, xr: &XrState, state: &mut AppState) -> Result<(), xr::sys::Result> {
|
||||
pub fn update(&mut self, xr: &XrState, state: &mut AppState) -> anyhow::Result<()> {
|
||||
xr.session.sync_actions(&[(&self.action_set).into()])?;
|
||||
|
||||
for i in 0..2 {
|
||||
@@ -116,11 +209,11 @@ impl OpenXrHand {
|
||||
}
|
||||
|
||||
pub(super) fn update(
|
||||
&self,
|
||||
&mut self,
|
||||
pointer: &mut Pointer,
|
||||
xr: &XrState,
|
||||
session: &AppSession,
|
||||
) -> Result<(), xr::sys::Result> {
|
||||
) -> anyhow::Result<()> {
|
||||
let location = self.space.locate(&xr.stage, xr.predicted_display_time)?;
|
||||
if location
|
||||
.location_flags
|
||||
@@ -131,31 +224,15 @@ impl OpenXrHand {
|
||||
pointer.pose = Affine3A::from_rotation_translation(quat, pos);
|
||||
}
|
||||
|
||||
let click_sensitivity = if pointer.before.click {
|
||||
session.config.xr_click_sensitivity_release
|
||||
} else {
|
||||
session.config.xr_click_sensitivity
|
||||
};
|
||||
|
||||
pointer.now.click = self
|
||||
.source
|
||||
.action_click
|
||||
.state(&xr.session, xr::Path::NULL)?
|
||||
.current_state
|
||||
> click_sensitivity;
|
||||
|
||||
let grab_sensitivity = if pointer.before.grab {
|
||||
session.config.xr_grab_sensitivity_release
|
||||
} else {
|
||||
session.config.xr_grab_sensitivity
|
||||
};
|
||||
.state(pointer.before.click, xr, session)?;
|
||||
|
||||
pointer.now.grab = self
|
||||
.source
|
||||
.action_grab
|
||||
.state(&xr.session, xr::Path::NULL)?
|
||||
.current_state
|
||||
> grab_sensitivity;
|
||||
.state(pointer.before.grab, xr, session)?;
|
||||
|
||||
pointer.now.scroll = self
|
||||
.source
|
||||
@@ -163,36 +240,42 @@ impl OpenXrHand {
|
||||
.state(&xr.session, xr::Path::NULL)?
|
||||
.current_state;
|
||||
|
||||
let alt_click_sensitivity = if pointer.before.alt_click {
|
||||
session.config.xr_alt_click_sensitivity_release
|
||||
} else {
|
||||
session.config.xr_alt_click_sensitivity
|
||||
};
|
||||
pointer.now.alt_click =
|
||||
self.source
|
||||
.action_alt_click
|
||||
.state(pointer.before.alt_click, xr, session)?;
|
||||
|
||||
pointer.now.alt_click = self
|
||||
.source
|
||||
.action_alt_click
|
||||
.state(&xr.session, xr::Path::NULL)?
|
||||
.current_state
|
||||
> alt_click_sensitivity;
|
||||
pointer.now.show_hide =
|
||||
self.source
|
||||
.action_show_hide
|
||||
.state(pointer.before.show_hide, xr, session)?;
|
||||
|
||||
pointer.now.show_hide = self
|
||||
.source
|
||||
.action_show_hide
|
||||
.state(&xr.session, xr::Path::NULL)?
|
||||
.current_state;
|
||||
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
|
||||
.action_click_modifier_right
|
||||
.state(&xr.session, xr::Path::NULL)?
|
||||
.current_state;
|
||||
pointer.now.click_modifier_middle = self.source.action_modifier_middle.state(
|
||||
pointer.before.click_modifier_middle,
|
||||
xr,
|
||||
session,
|
||||
)?;
|
||||
|
||||
pointer.now.click_modifier_middle = self
|
||||
.source
|
||||
.action_click_modifier_middle
|
||||
.state(&xr.session, xr::Path::NULL)?
|
||||
.current_state;
|
||||
pointer.now.move_mouse =
|
||||
self.source
|
||||
.action_move_mouse
|
||||
.state(pointer.before.move_mouse, xr, session)?;
|
||||
|
||||
pointer.now.space_drag =
|
||||
self.source
|
||||
.action_space_drag
|
||||
.state(pointer.before.space_drag, xr, session)?;
|
||||
|
||||
pointer.now.space_rotate =
|
||||
self.source
|
||||
.action_space_rotate
|
||||
.state(pointer.before.space_rotate, xr, session)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -200,48 +283,18 @@ impl OpenXrHand {
|
||||
|
||||
// supported action types: Haptic, Posef, Vector2f, f32, bool
|
||||
impl OpenXrHandSource {
|
||||
pub(super) fn new(action_set: &mut xr::ActionSet, side: &str) -> Result<Self, xr::sys::Result> {
|
||||
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),
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let action_click = action_set.create_action::<f32>(
|
||||
&format!("{}_click", side),
|
||||
&format!("{} hand click", side),
|
||||
&[],
|
||||
)?;
|
||||
let action_grab = action_set.create_action::<f32>(
|
||||
&format!("{}_grab", side),
|
||||
&format!("{} hand grab", side),
|
||||
&[],
|
||||
)?;
|
||||
let action_scroll = action_set.create_action::<f32>(
|
||||
&format!("{}_scroll", side),
|
||||
&format!("{} hand scroll", side),
|
||||
&[],
|
||||
)?;
|
||||
let action_alt_click = action_set.create_action::<f32>(
|
||||
&format!("{}_alt_click", side),
|
||||
&format!("{} hand alt click", side),
|
||||
&[],
|
||||
)?;
|
||||
let action_show_hide = action_set.create_action::<bool>(
|
||||
&format!("{}_show_hide", side),
|
||||
&format!("{} hand show/hide", side),
|
||||
&[],
|
||||
)?;
|
||||
let action_click_modifier_right = action_set.create_action::<bool>(
|
||||
&format!("{}_click_modifier_right", side),
|
||||
&format!("{} hand right click modifier", side),
|
||||
&[],
|
||||
)?;
|
||||
let action_click_modifier_middle = action_set.create_action::<bool>(
|
||||
&format!("{}_click_modifier_middle", side),
|
||||
&format!("{} hand middle click modifier", side),
|
||||
&[],
|
||||
)?;
|
||||
let action_haptics = action_set.create_action::<xr::Haptic>(
|
||||
&format!("{}_haptics", side),
|
||||
&format!("{} hand haptics", side),
|
||||
@@ -250,247 +303,257 @@ impl OpenXrHandSource {
|
||||
|
||||
Ok(Self {
|
||||
action_pose,
|
||||
action_click,
|
||||
action_grab,
|
||||
action_click: CustomClickAction::new(action_set, "click", side)?,
|
||||
action_grab: CustomClickAction::new(action_set, "grab", side)?,
|
||||
action_scroll,
|
||||
action_alt_click,
|
||||
action_show_hide,
|
||||
action_click_modifier_right,
|
||||
action_click_modifier_middle,
|
||||
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_space_rotate: CustomClickAction::new(action_set, "space_rotate", 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn suggest_bindings(
|
||||
instance: &xr::Instance,
|
||||
hands: &[&OpenXrHandSource; 2],
|
||||
) -> Result<(), xr::sys::Result> {
|
||||
let path = instance.string_to_path("/interaction_profiles/khr/simple_controller")?;
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// not fully functional, but helpful for debugging
|
||||
instance.suggest_interaction_profile_bindings(
|
||||
path,
|
||||
&[
|
||||
xr::Binding::new(
|
||||
&hands[0].action_pose,
|
||||
instance.string_to_path("/user/hand/left/input/aim/pose")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_pose,
|
||||
instance.string_to_path("/user/hand/right/input/aim/pose")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_click,
|
||||
instance.string_to_path("/user/hand/left/input/select/click")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_click,
|
||||
instance.string_to_path("/user/hand/right/input/select/click")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_show_hide,
|
||||
instance.string_to_path("/user/hand/left/input/menu/click")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_haptics,
|
||||
instance.string_to_path("/user/hand/left/output/haptic")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_haptics,
|
||||
instance.string_to_path("/user/hand/right/output/haptic")?,
|
||||
),
|
||||
],
|
||||
)?;
|
||||
fn is_bool(maybe_type_str: &Option<String>) -> bool {
|
||||
maybe_type_str
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.split('/')
|
||||
.last()
|
||||
.map(|last| matches!(last, "click" | "touch"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
let path = instance.string_to_path("/interaction_profiles/oculus/touch_controller")?;
|
||||
instance.suggest_interaction_profile_bindings(
|
||||
path,
|
||||
&[
|
||||
xr::Binding::new(
|
||||
&hands[0].action_pose,
|
||||
instance.string_to_path("/user/hand/left/input/aim/pose")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_pose,
|
||||
instance.string_to_path("/user/hand/right/input/aim/pose")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_click,
|
||||
instance.string_to_path("/user/hand/left/input/trigger/value")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_click,
|
||||
instance.string_to_path("/user/hand/right/input/trigger/value")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_grab,
|
||||
instance.string_to_path("/user/hand/left/input/squeeze/value")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_grab,
|
||||
instance.string_to_path("/user/hand/right/input/squeeze/value")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_scroll,
|
||||
instance.string_to_path("/user/hand/left/input/thumbstick/y")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_scroll,
|
||||
instance.string_to_path("/user/hand/right/input/thumbstick/y")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_show_hide,
|
||||
instance.string_to_path("/user/hand/left/input/y/click")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_click_modifier_right,
|
||||
instance.string_to_path("/user/hand/left/input/y/touch")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_click_modifier_right,
|
||||
instance.string_to_path("/user/hand/right/input/b/touch")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_click_modifier_middle,
|
||||
instance.string_to_path("/user/hand/left/input/x/touch")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_click_modifier_middle,
|
||||
instance.string_to_path("/user/hand/right/input/a/touch")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_haptics,
|
||||
instance.string_to_path("/user/hand/left/output/haptic")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_haptics,
|
||||
instance.string_to_path("/user/hand/right/output/haptic")?,
|
||||
),
|
||||
],
|
||||
)?;
|
||||
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));
|
||||
} else {
|
||||
$bindings.push(xr::Binding::new(&$left.action_bool, p));
|
||||
}
|
||||
} else {
|
||||
if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$left.action_f32_double, p));
|
||||
} else {
|
||||
$bindings.push(xr::Binding::new(&$left.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));
|
||||
} else {
|
||||
$bindings.push(xr::Binding::new(&$right.action_bool, p));
|
||||
}
|
||||
} else {
|
||||
if action.double_click.unwrap_or(false) {
|
||||
$bindings.push(xr::Binding::new(&$right.action_f32_double, p));
|
||||
} else {
|
||||
$bindings.push(xr::Binding::new(&$right.action_f32, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let path = instance.string_to_path("/interaction_profiles/valve/index_controller")?;
|
||||
instance.suggest_interaction_profile_bindings(
|
||||
path,
|
||||
&[
|
||||
xr::Binding::new(
|
||||
&hands[0].action_pose,
|
||||
instance.string_to_path("/user/hand/left/input/aim/pose")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_pose,
|
||||
instance.string_to_path("/user/hand/right/input/aim/pose")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_click,
|
||||
instance.string_to_path("/user/hand/left/input/trigger/value")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_click,
|
||||
instance.string_to_path("/user/hand/right/input/trigger/value")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_grab,
|
||||
instance.string_to_path("/user/hand/left/input/squeeze/force")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_grab,
|
||||
instance.string_to_path("/user/hand/right/input/squeeze/force")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_scroll,
|
||||
instance.string_to_path("/user/hand/left/input/thumbstick/y")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_scroll,
|
||||
instance.string_to_path("/user/hand/right/input/thumbstick/y")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_alt_click,
|
||||
instance.string_to_path("/user/hand/left/input/trackpad/force")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_alt_click,
|
||||
instance.string_to_path("/user/hand/right/input/trackpad/force")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_show_hide,
|
||||
instance.string_to_path("/user/hand/left/input/b/click")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_click_modifier_right,
|
||||
instance.string_to_path("/user/hand/left/input/b/touch")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_click_modifier_right,
|
||||
instance.string_to_path("/user/hand/right/input/b/touch")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_click_modifier_middle,
|
||||
instance.string_to_path("/user/hand/left/input/a/touch")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_click_modifier_middle,
|
||||
instance.string_to_path("/user/hand/right/input/a/touch")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_haptics,
|
||||
instance.string_to_path("/user/hand/left/output/haptic")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_haptics,
|
||||
instance.string_to_path("/user/hand/right/output/haptic")?,
|
||||
),
|
||||
],
|
||||
)?;
|
||||
fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) -> anyhow::Result<()> {
|
||||
let profiles = load_action_profiles()?;
|
||||
|
||||
let path = instance.string_to_path("/interaction_profiles/htc/vive_controller")?;
|
||||
instance.suggest_interaction_profile_bindings(
|
||||
path,
|
||||
&[
|
||||
xr::Binding::new(
|
||||
&hands[0].action_pose,
|
||||
instance.string_to_path("/user/hand/left/input/aim/pose")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_pose,
|
||||
instance.string_to_path("/user/hand/right/input/aim/pose")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_click,
|
||||
instance.string_to_path("/user/hand/right/input/trigger/value")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_grab,
|
||||
instance.string_to_path("/user/hand/left/input/squeeze/click")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_grab,
|
||||
instance.string_to_path("/user/hand/right/input/squeeze/click")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_scroll,
|
||||
instance.string_to_path("/user/hand/left/input/trackpad/y")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_scroll,
|
||||
instance.string_to_path("/user/hand/right/input/trackpad/y")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_show_hide,
|
||||
instance.string_to_path("/user/hand/left/input/menu/click")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[0].action_haptics,
|
||||
instance.string_to_path("/user/hand/left/output/haptic")?,
|
||||
),
|
||||
xr::Binding::new(
|
||||
&hands[1].action_haptics,
|
||||
instance.string_to_path("/user/hand/right/output/haptic")?,
|
||||
),
|
||||
],
|
||||
)?;
|
||||
for profile in profiles {
|
||||
let Ok(profile_path) = instance.string_to_path(&profile.profile) else {
|
||||
log::debug!("Profile not supported: {}", profile.profile);
|
||||
continue;
|
||||
};
|
||||
|
||||
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.right, instance) {
|
||||
bindings.push(xr::Binding::new(&hands[1].action_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.right, instance) {
|
||||
bindings.push(xr::Binding::new(&hands[1].action_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.right, instance) {
|
||||
bindings.push(xr::Binding::new(&hands[1].action_scroll, p));
|
||||
}
|
||||
}
|
||||
|
||||
add_custom!(
|
||||
profile.click,
|
||||
hands[0].action_click,
|
||||
hands[1].action_click,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.alt_click,
|
||||
&hands[0].action_alt_click,
|
||||
&hands[1].action_alt_click,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.grab,
|
||||
&hands[0].action_grab,
|
||||
&hands[1].action_grab,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.show_hide,
|
||||
&hands[0].action_show_hide,
|
||||
&hands[1].action_show_hide,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.space_drag,
|
||||
&hands[0].action_space_drag,
|
||||
&hands[1].action_space_drag,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.space_rotate,
|
||||
&hands[0].action_space_rotate,
|
||||
&hands[1].action_space_rotate,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.click_modifier_right,
|
||||
&hands[0].action_modifier_right,
|
||||
&hands[1].action_modifier_right,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.click_modifier_middle,
|
||||
&hands[0].action_modifier_middle,
|
||||
&hands[1].action_modifier_middle,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
add_custom!(
|
||||
profile.move_mouse,
|
||||
&hands[0].action_move_mouse,
|
||||
&hands[1].action_move_mouse,
|
||||
bindings,
|
||||
instance
|
||||
);
|
||||
|
||||
if instance
|
||||
.suggest_interaction_profile_bindings(profile_path, &bindings)
|
||||
.is_err()
|
||||
{
|
||||
log::error!("Bad bindings for {}", &profile.profile[22..]);
|
||||
log::error!("Verify config: ~/.config/wlxoverlay/openxr_actions.json5");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct OpenXrActionConfAction {
|
||||
left: Option<String>,
|
||||
right: Option<String>,
|
||||
threshold: Option<[f32; 2]>,
|
||||
double_click: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct OpenXrActionConfProfile {
|
||||
profile: String,
|
||||
pose: Option<OpenXrActionConfAction>,
|
||||
click: Option<OpenXrActionConfAction>,
|
||||
grab: Option<OpenXrActionConfAction>,
|
||||
alt_click: Option<OpenXrActionConfAction>,
|
||||
show_hide: Option<OpenXrActionConfAction>,
|
||||
space_drag: Option<OpenXrActionConfAction>,
|
||||
space_rotate: Option<OpenXrActionConfAction>,
|
||||
click_modifier_right: Option<OpenXrActionConfAction>,
|
||||
click_modifier_middle: Option<OpenXrActionConfAction>,
|
||||
move_mouse: Option<OpenXrActionConfAction>,
|
||||
scroll: Option<OpenXrActionConfAction>,
|
||||
haptic: Option<OpenXrActionConfAction>,
|
||||
}
|
||||
|
||||
const DEFAULT_PROFILES: &str = include_str!("openxr_actions.json5");
|
||||
|
||||
fn load_action_profiles() -> anyhow::Result<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);
|
||||
};
|
||||
|
||||
match serde_json5::from_str::<Vec<OpenXrActionConfProfile>>(&conf) {
|
||||
Ok(override_profiles) => {
|
||||
override_profiles.into_iter().for_each(|new| {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(profiles)
|
||||
}
|
||||
@@ -148,7 +148,6 @@ impl LinePool {
|
||||
1.0,
|
||||
)?)
|
||||
.eye_visibility(xr::EyeVisibility::BOTH)
|
||||
.layer_flags(xr::CompositionLayerFlags::CORRECT_CHROMATIC_ABERRATION)
|
||||
.space(&xr.stage)
|
||||
.size(xr::Extent2Df {
|
||||
width: LINE_WIDTH,
|
||||
|
||||
+57
-20
@@ -13,23 +13,26 @@ use vulkano::{command_buffer::CommandBufferUsage, Handle, VulkanObject};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
common::{OverlayContainer, TaskType},
|
||||
common::{BackendError, OverlayContainer},
|
||||
input::interact,
|
||||
notifications::NotificationManager,
|
||||
openxr::{input::DoubleClickCounter, lines::LinePool, overlay::OpenXrOverlayData},
|
||||
openxr::{lines::LinePool, overlay::OpenXrOverlayData},
|
||||
overlay::OverlayData,
|
||||
task::{SystemTask, TaskType},
|
||||
},
|
||||
graphics::WlxGraphics,
|
||||
overlays::watch::{watch_fade, WATCH_NAME},
|
||||
overlays::{
|
||||
toast::{Toast, ToastTopic},
|
||||
watch::{watch_fade, WATCH_NAME},
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::common::BackendError;
|
||||
|
||||
mod helpers;
|
||||
mod input;
|
||||
mod lines;
|
||||
mod overlay;
|
||||
mod playspace;
|
||||
mod swapchain;
|
||||
|
||||
const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO;
|
||||
@@ -42,6 +45,7 @@ struct XrState {
|
||||
session: xr::Session<xr::Vulkan>,
|
||||
predicted_display_time: xr::Time,
|
||||
stage: Arc<xr::Space>,
|
||||
stage_offset: Affine3A,
|
||||
}
|
||||
|
||||
pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
@@ -70,13 +74,14 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
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))
|
||||
.ok();
|
||||
|
||||
#[cfg(feature = "osc")]
|
||||
let mut osc_sender =
|
||||
crate::backend::osc::OscSender::new(app_state.session.config.osc_out_port).ok();
|
||||
|
||||
app_state.hid_provider.set_desktop_extent(overlays.extent);
|
||||
|
||||
let (session, mut frame_wait, mut frame_stream) = unsafe {
|
||||
let raw_session = helpers::create_overlay_session(
|
||||
&xr_instance,
|
||||
@@ -106,6 +111,7 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
session,
|
||||
predicted_display_time: xr::Time::from_nanos(0),
|
||||
stage: Arc::new(stage),
|
||||
stage_offset: Affine3A::IDENTITY,
|
||||
};
|
||||
|
||||
let pointer_lines = [
|
||||
@@ -115,12 +121,11 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
|
||||
let watch_id = overlays.get_by_name(WATCH_NAME).unwrap().state.id; // want panic
|
||||
|
||||
let input_source = input::OpenXrInputSource::new(&xr_state)?;
|
||||
let mut input_source = input::OpenXrInputSource::new(&xr_state)?;
|
||||
|
||||
let mut session_running = false;
|
||||
let mut event_storage = xr::EventDataBuffer::new();
|
||||
|
||||
let mut show_hide_counter = DoubleClickCounter::new();
|
||||
let mut due_tasks = VecDeque::with_capacity(4);
|
||||
|
||||
'main_loop: loop {
|
||||
@@ -198,12 +203,14 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
.pointers
|
||||
.iter()
|
||||
.any(|p| p.now.show_hide && !p.before.show_hide)
|
||||
&& show_hide_counter.click()
|
||||
{
|
||||
overlays.show_hide(&mut app_state);
|
||||
}
|
||||
|
||||
watch_fade(&mut app_state, overlays.mut_by_id(watch_id).unwrap()); // want panic
|
||||
if let Some(ref mut space_mover) = playspace {
|
||||
space_mover.update(&mut overlays, &app_state);
|
||||
}
|
||||
|
||||
for o in overlays.iter_mut() {
|
||||
o.after_input(&mut app_state)?;
|
||||
@@ -220,7 +227,18 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
&xr_state.stage,
|
||||
)?;
|
||||
|
||||
app_state.input_state.hmd = helpers::hmd_pose_from_views(&views);
|
||||
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;
|
||||
Toast::new(
|
||||
ToastTopic::IpdChange,
|
||||
"IPD".into(),
|
||||
format!("{:.1} mm", ipd).into(),
|
||||
)
|
||||
.submit(&mut app_state);
|
||||
}
|
||||
|
||||
overlays
|
||||
.iter_mut()
|
||||
@@ -240,6 +258,8 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
}
|
||||
}
|
||||
|
||||
app_state.hid_provider.commit();
|
||||
|
||||
let watch = overlays.mut_by_id(watch_id).unwrap(); // want panic
|
||||
let watch_transform = watch.state.transform;
|
||||
if !watch.state.want_visible {
|
||||
@@ -269,7 +289,8 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
o.render(&mut app_state)?;
|
||||
|
||||
let dist_sq = (app_state.input_state.hmd.translation - o.state.transform.translation)
|
||||
.length_squared();
|
||||
.length_squared()
|
||||
+ (100f32 - o.state.z_order as f32);
|
||||
|
||||
if !dist_sq.is_normal() {
|
||||
continue;
|
||||
@@ -308,6 +329,11 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
&frame_ref,
|
||||
)?;
|
||||
|
||||
let removed_overlays = overlays.update(&mut app_state)?;
|
||||
for o in removed_overlays {
|
||||
delete_queue.push((o, cur_frame + 5));
|
||||
}
|
||||
|
||||
notifications.submit_pending(&mut app_state);
|
||||
|
||||
app_state.tasks.retrieve_due(&mut due_tasks);
|
||||
@@ -317,6 +343,8 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
TaskType::Overlay(sel, f) => {
|
||||
if let Some(o) = overlays.mut_by_selector(&sel) {
|
||||
f(&mut app_state, &mut o.state);
|
||||
} else {
|
||||
log::warn!("Overlay not found for task: {:?}", sel);
|
||||
}
|
||||
}
|
||||
TaskType::CreateOverlay(sel, f) => {
|
||||
@@ -338,22 +366,31 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
||||
TaskType::DropOverlay(sel) => {
|
||||
if let Some(o) = overlays.mut_by_selector(&sel) {
|
||||
if o.state.birthframe < cur_frame {
|
||||
let o = overlays.remove_by_selector(&sel);
|
||||
// set for deletion after all images are done showing
|
||||
delete_queue.push((o, cur_frame + 5));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TaskType::System(_task) => {
|
||||
// Not implemented
|
||||
}
|
||||
TaskType::System(task) => match task {
|
||||
SystemTask::FixFloor => {
|
||||
if let Some(ref mut playspace) = playspace {
|
||||
playspace.fix_floor(&app_state.input_state);
|
||||
}
|
||||
}
|
||||
SystemTask::ResetPlayspace => {
|
||||
if let Some(ref mut playspace) = playspace {
|
||||
playspace.reset_offset();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
delete_queue.retain(|(_, frame)| *frame > cur_frame);
|
||||
|
||||
app_state.hid_provider.on_new_frame();
|
||||
|
||||
let watch = overlays.mut_by_id(watch_id).unwrap(); // want panic
|
||||
watch.state.transform = watch_transform;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
// Available bindings:
|
||||
//
|
||||
// -- click --
|
||||
// primary click to interact with the watch or overlays. required
|
||||
//
|
||||
// -- grab --
|
||||
// used to manipulate position, size, orientation of overlays in 3D space
|
||||
//
|
||||
// -- show_hide --
|
||||
// used to quickly hide and show your last selection of screens + keyboard
|
||||
//
|
||||
// -- space_drag --
|
||||
// move your stage (playspace drag)
|
||||
//
|
||||
// -- space_rotate --
|
||||
// rotate your stage (playspace rotate, WIP)
|
||||
//
|
||||
// -- click_modifier_right --
|
||||
// while this is held, your pointer will turn ORANGE and your mouse clicks will be RIGHT clicks
|
||||
//
|
||||
// -- click_modifier_middle --
|
||||
// while this is held, your pointer will turn PURPLE and your mouse clicks will be MIDDLE clicks
|
||||
//
|
||||
// -- move_mouse --
|
||||
// when using `focus_follows_mouse_mode`, you need to hold this for the mouse to move
|
||||
//
|
||||
// -- pose, haptic --
|
||||
// do not mess with these, unless you know what you're doing
|
||||
|
||||
[
|
||||
// Fallback controller, intended for testing
|
||||
{
|
||||
profile: "/interaction_profiles/khr/simple_controller",
|
||||
pose: {
|
||||
left: "/user/hand/left/input/aim/pose",
|
||||
right: "/user/hand/right/input/aim/pose"
|
||||
},
|
||||
haptic: {
|
||||
left: "/user/hand/left/output/haptic",
|
||||
right: "/user/hand/right/output/haptic"
|
||||
},
|
||||
click: {
|
||||
// left trigger is click
|
||||
left: "/user/hand/left/input/select/click",
|
||||
},
|
||||
grab: {
|
||||
// right trigger is grab
|
||||
right: "/user/hand/right/input/select/click"
|
||||
},
|
||||
show_hide: {
|
||||
left: "/user/hand/left/input/menu/click"
|
||||
}
|
||||
},
|
||||
|
||||
// Oculus Touch Controller. Compatible with Quest 2, Quest 3, Quest Pro
|
||||
{
|
||||
profile: "/interaction_profiles/oculus/touch_controller",
|
||||
pose: {
|
||||
left: "/user/hand/left/input/aim/pose",
|
||||
right: "/user/hand/right/input/aim/pose"
|
||||
},
|
||||
haptic: {
|
||||
left: "/user/hand/left/output/haptic",
|
||||
right: "/user/hand/right/output/haptic"
|
||||
},
|
||||
click: {
|
||||
left: "/user/hand/left/input/trigger/value",
|
||||
right: "/user/hand/right/input/trigger/value"
|
||||
},
|
||||
grab: {
|
||||
left: "/user/hand/left/input/squeeze/value",
|
||||
right: "/user/hand/right/input/squeeze/value"
|
||||
},
|
||||
scroll: {
|
||||
left: "/user/hand/left/input/thumbstick/y",
|
||||
right: "/user/hand/right/input/thumbstick/y"
|
||||
},
|
||||
show_hide: {
|
||||
double_click: true,
|
||||
left: "/user/hand/left/input/y/click",
|
||||
},
|
||||
space_drag: {
|
||||
left: "/user/hand/left/input/menu/click",
|
||||
},
|
||||
click_modifier_right: {
|
||||
left: "/user/hand/left/input/y/touch",
|
||||
right: "/user/hand/right/input/b/touch"
|
||||
},
|
||||
click_modifier_middle: {
|
||||
left: "/user/hand/left/input/x/touch",
|
||||
right: "/user/hand/right/input/a/touch"
|
||||
},
|
||||
move_mouse: {
|
||||
// used with focus_follows_mouse_mode
|
||||
left: "/user/hand/left/input/trigger/touch",
|
||||
right: "/user/hand/right/input/trigger/touch"
|
||||
}
|
||||
},
|
||||
|
||||
// Index controller
|
||||
{
|
||||
profile: "/interaction_profiles/valve/index_controller",
|
||||
pose: {
|
||||
left: "/user/hand/left/input/aim/pose",
|
||||
right: "/user/hand/right/input/aim/pose"
|
||||
},
|
||||
haptic: {
|
||||
left: "/user/hand/left/output/haptic",
|
||||
right: "/user/hand/right/output/haptic"
|
||||
},
|
||||
click: {
|
||||
left: "/user/hand/left/input/trigger/value",
|
||||
right: "/user/hand/right/input/trigger/value"
|
||||
},
|
||||
alt_click: {
|
||||
// left trackpad is space_drag
|
||||
right: "/user/hand/right/input/trackpad/force",
|
||||
},
|
||||
grab: {
|
||||
left: "/user/hand/left/input/squeeze/force",
|
||||
right: "/user/hand/right/input/squeeze/force"
|
||||
},
|
||||
scroll: {
|
||||
left: "/user/hand/left/input/thumbstick/y",
|
||||
right: "/user/hand/right/input/thumbstick/y"
|
||||
},
|
||||
show_hide: {
|
||||
double_click: true,
|
||||
left: "/user/hand/left/input/b/click",
|
||||
},
|
||||
space_drag: {
|
||||
left: "/user/hand/left/input/trackpad/force",
|
||||
// right trackpad is alt_click
|
||||
},
|
||||
click_modifier_right: {
|
||||
left: "/user/hand/left/input/b/touch",
|
||||
right: "/user/hand/right/input/b/touch"
|
||||
},
|
||||
click_modifier_middle: {
|
||||
left: "/user/hand/left/input/a/touch",
|
||||
right: "/user/hand/right/input/a/touch"
|
||||
},
|
||||
move_mouse: {
|
||||
// used with focus_follows_mouse_mode
|
||||
left: "/user/hand/left/input/trigger/touch",
|
||||
right: "/user/hand/right/input/trigger/touch"
|
||||
}
|
||||
},
|
||||
|
||||
// Vive controller
|
||||
{
|
||||
profile: "/interaction_profiles/htc/vive_controller",
|
||||
pose: {
|
||||
left: "/user/hand/left/input/aim/pose",
|
||||
right: "/user/hand/right/input/aim/pose"
|
||||
},
|
||||
click: {
|
||||
left: "/user/hand/left/input/trigger/value",
|
||||
right: "/user/hand/right/input/trigger/value"
|
||||
},
|
||||
grab: {
|
||||
left: "/user/hand/left/input/squeeze/click",
|
||||
right: "/user/hand/right/input/squeeze/click"
|
||||
},
|
||||
scroll: {
|
||||
left: "/user/hand/left/input/trackpad/y",
|
||||
right: "/user/hand/right/input/trackpad/y"
|
||||
},
|
||||
show_hide: {
|
||||
left: "/user/hand/left/input/menu/click",
|
||||
},
|
||||
space_drag: {
|
||||
right: "/user/hand/right/input/menu/click",
|
||||
},
|
||||
haptic: {
|
||||
left: "/user/hand/left/output/haptic",
|
||||
right: "/user/hand/right/output/haptic"
|
||||
}
|
||||
},
|
||||
|
||||
// Windows Mixed Reality controller
|
||||
{
|
||||
profile: "/interaction_profiles/microsoft/motion_controller",
|
||||
pose: {
|
||||
left: "/user/hand/left/input/aim/pose",
|
||||
right: "/user/hand/right/input/aim/pose"
|
||||
},
|
||||
haptic: {
|
||||
left: "/user/hand/left/output/haptic",
|
||||
right: "/user/hand/right/output/haptic"
|
||||
},
|
||||
click: {
|
||||
left: "/user/hand/left/input/trigger/value",
|
||||
right: "/user/hand/right/input/trigger/value"
|
||||
},
|
||||
grab: {
|
||||
left: "/user/hand/left/input/squeeze/click",
|
||||
right: "/user/hand/right/input/squeeze/click"
|
||||
},
|
||||
scroll: {
|
||||
left: "/user/hand/left/input/thumbstick/y",
|
||||
right: "/user/hand/right/input/thumbstick/y"
|
||||
},
|
||||
show_hide: {
|
||||
left: "/user/hand/left/input/menu/click",
|
||||
},
|
||||
space_drag: {
|
||||
right: "/user/hand/right/input/menu/click",
|
||||
},
|
||||
},
|
||||
|
||||
// HP Reverb G2 controller
|
||||
{
|
||||
profile: "/interaction_profiles/hp/mixed_reality_controller",
|
||||
pose: {
|
||||
left: "/user/hand/left/input/aim/pose",
|
||||
right: "/user/hand/right/input/aim/pose"
|
||||
},
|
||||
haptic: {
|
||||
left: "/user/hand/left/output/haptic",
|
||||
right: "/user/hand/right/output/haptic"
|
||||
},
|
||||
click: {
|
||||
left: "/user/hand/left/input/trigger/value",
|
||||
right: "/user/hand/right/input/trigger/value"
|
||||
},
|
||||
grab: {
|
||||
left: "/user/hand/left/input/squeeze/value",
|
||||
right: "/user/hand/right/input/squeeze/value"
|
||||
},
|
||||
scroll: {
|
||||
left: "/user/hand/left/input/thumbstick/y",
|
||||
right: "/user/hand/right/input/thumbstick/y"
|
||||
},
|
||||
show_hide: {
|
||||
left: "/user/hand/left/input/menu/click",
|
||||
},
|
||||
space_drag: {
|
||||
right: "/user/hand/right/input/menu/click",
|
||||
},
|
||||
},
|
||||
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
use glam::Vec3A;
|
||||
use openxr as xr;
|
||||
use std::{f32::consts::PI, sync::Arc};
|
||||
use xr::{CompositionLayerFlags, EyeVisibility};
|
||||
use xr::EyeVisibility;
|
||||
|
||||
use super::{helpers, swapchain::SwapchainRenderData, CompositionLayer, XrState};
|
||||
use crate::{
|
||||
@@ -78,7 +78,6 @@ impl OverlayData<OpenXrOverlayData> {
|
||||
.pose(posef)
|
||||
.sub_image(sub_image)
|
||||
.eye_visibility(EyeVisibility::BOTH)
|
||||
.layer_flags(CompositionLayerFlags::CORRECT_CHROMATIC_ABERRATION)
|
||||
.space(&xr.stage)
|
||||
.radius(radius)
|
||||
.central_angle(angle)
|
||||
@@ -90,7 +89,6 @@ impl OverlayData<OpenXrOverlayData> {
|
||||
.pose(posef)
|
||||
.sub_image(sub_image)
|
||||
.eye_visibility(EyeVisibility::BOTH)
|
||||
.layer_flags(CompositionLayerFlags::CORRECT_CHROMATIC_ABERRATION)
|
||||
.space(&xr.stage)
|
||||
.size(xr::Extent2Df {
|
||||
width: scale_x,
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
use std::ffi::c_void;
|
||||
|
||||
use glam::{Affine3A, Quat, Vec3A};
|
||||
use libloading::{Library, Symbol};
|
||||
|
||||
use crate::{
|
||||
backend::{common::OverlayContainer, input::InputState},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::{helpers, overlay::OpenXrOverlayData};
|
||||
|
||||
#[repr(C)]
|
||||
struct XrtPose {
|
||||
orientation: [f32; 4],
|
||||
position: [f32; 3],
|
||||
}
|
||||
|
||||
struct MoverData<T> {
|
||||
pose: Affine3A,
|
||||
hand: usize,
|
||||
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>>,
|
||||
rotate: Option<MoverData<Quat>>,
|
||||
|
||||
libmonado: Library,
|
||||
mnd_root: *mut c_void,
|
||||
api_impl: ApiImpl,
|
||||
}
|
||||
|
||||
impl PlayspaceMover {
|
||||
pub fn try_new() -> anyhow::Result<Self> {
|
||||
unsafe {
|
||||
let libmonado = helpers::find_libmonado()?;
|
||||
|
||||
let root_create: Symbol<extern "C" fn(*mut *mut c_void) -> i32> =
|
||||
libmonado.get(b"mnd_root_create\0")?;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if let ApiImpl::None = api_impl {
|
||||
anyhow::bail!("Monado does not support playspace mover.");
|
||||
}
|
||||
|
||||
let mut root: *mut c_void = std::ptr::null_mut();
|
||||
|
||||
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,
|
||||
rotate: None,
|
||||
|
||||
libmonado,
|
||||
mnd_root: root,
|
||||
api_impl,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, overlays: &mut OverlayContainer<OpenXrOverlayData>, state: &AppState) {
|
||||
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 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,
|
||||
);
|
||||
|
||||
let mut space_transform = Affine3A::from_rotation_y(rel_y);
|
||||
let offset = (space_transform.transform_vector3a(state.input_state.hmd.translation)
|
||||
- state.input_state.hmd.translation)
|
||||
* -1.0;
|
||||
let mut overlay_transform = Affine3A::from_rotation_y(-rel_y);
|
||||
|
||||
overlay_transform.translation = offset;
|
||||
space_transform.translation = offset;
|
||||
|
||||
overlays.iter_mut().for_each(|overlay| {
|
||||
if overlay.state.grabbable {
|
||||
overlay.state.dirty = true;
|
||||
overlay.state.transform.translation =
|
||||
overlay_transform.transform_point3a(overlay.state.transform.translation);
|
||||
}
|
||||
});
|
||||
|
||||
data.pose *= space_transform;
|
||||
data.hand_pose = new_hand;
|
||||
|
||||
self.apply_offset(data.pose);
|
||||
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);
|
||||
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 {
|
||||
self.last_transform = data.pose;
|
||||
log::info!("End space drag");
|
||||
return;
|
||||
}
|
||||
|
||||
let new_hand = data
|
||||
.pose
|
||||
.transform_point3a(state.input_state.pointers[data.hand].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");
|
||||
return;
|
||||
}
|
||||
|
||||
let overlay_offset = data.pose.inverse().transform_vector3a(relative_pos) * -1.0;
|
||||
|
||||
overlays.iter_mut().for_each(|overlay| {
|
||||
if overlay.state.grabbable {
|
||||
overlay.state.dirty = true;
|
||||
overlay.state.transform.translation += overlay_offset;
|
||||
}
|
||||
});
|
||||
|
||||
data.pose.translation += relative_pos;
|
||||
data.hand_pose = new_hand;
|
||||
|
||||
self.apply_offset(data.pose);
|
||||
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);
|
||||
self.drag = Some(MoverData {
|
||||
pose: self.last_transform,
|
||||
hand: i,
|
||||
hand_pose: hand_pos,
|
||||
});
|
||||
log::info!("Start space drag");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_offset(&mut self) {
|
||||
if self.drag.is_some() {
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn fix_floor(&mut self, input: &InputState) {
|
||||
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;
|
||||
}
|
||||
|
||||
let y1 = input.pointers[0].pose.translation.y;
|
||||
let y2 = input.pointers[1].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 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
-30
@@ -10,13 +10,16 @@ use anyhow::Ok;
|
||||
use glam::{Affine2, Affine3A, Mat3A, Quat, Vec2, Vec3, Vec3A};
|
||||
use vulkano::image::view::ImageView;
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::{config::AStrMapExt, state::AppState};
|
||||
|
||||
use super::input::{DummyInteractionHandler, Haptics, InteractionHandler, PointerHit};
|
||||
|
||||
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub trait OverlayBackend: OverlayRenderer + InteractionHandler {}
|
||||
pub trait OverlayBackend: OverlayRenderer + InteractionHandler {
|
||||
fn set_renderer(&mut self, renderer: Box<dyn OverlayRenderer>);
|
||||
fn set_interaction(&mut self, interaction: Box<dyn InteractionHandler>);
|
||||
}
|
||||
|
||||
pub struct OverlayState {
|
||||
pub id: usize,
|
||||
@@ -26,14 +29,15 @@ pub struct OverlayState {
|
||||
pub grabbable: bool,
|
||||
pub interactable: bool,
|
||||
pub recenter: bool,
|
||||
pub anchored: bool,
|
||||
pub dirty: bool,
|
||||
pub alpha: f32,
|
||||
pub z_order: u32,
|
||||
pub transform: Affine3A,
|
||||
pub saved_point: Option<Vec3A>,
|
||||
pub saved_scale: Option<f32>,
|
||||
pub spawn_scale: f32, // aka width
|
||||
pub spawn_point: Vec3A,
|
||||
pub spawn_rotation: Quat,
|
||||
pub saved_transform: Option<Affine3A>,
|
||||
pub relative_to: RelativeTo,
|
||||
pub curvature: Option<f32>,
|
||||
pub primary_pointer: Option<usize>,
|
||||
@@ -51,15 +55,16 @@ impl Default for OverlayState {
|
||||
grabbable: false,
|
||||
recenter: false,
|
||||
interactable: false,
|
||||
anchored: false,
|
||||
dirty: true,
|
||||
alpha: 1.0,
|
||||
z_order: 0,
|
||||
relative_to: RelativeTo::None,
|
||||
curvature: None,
|
||||
saved_point: None,
|
||||
saved_scale: None,
|
||||
spawn_scale: 1.0,
|
||||
spawn_point: Vec3A::NEG_Z,
|
||||
spawn_rotation: Quat::IDENTITY,
|
||||
saved_transform: None,
|
||||
transform: Affine3A::IDENTITY,
|
||||
primary_pointer: None,
|
||||
interaction_transform: Affine2::IDENTITY,
|
||||
@@ -95,43 +100,51 @@ where
|
||||
impl OverlayState {
|
||||
pub fn parent_transform(&self, app: &AppState) -> Option<Affine3A> {
|
||||
match self.relative_to {
|
||||
RelativeTo::None => None,
|
||||
RelativeTo::Head => Some(app.input_state.hmd),
|
||||
RelativeTo::Hand(idx) => Some(app.input_state.pointers[idx].pose),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_anchor(&self, app: &AppState) -> Affine3A {
|
||||
if self.anchored {
|
||||
app.anchor
|
||||
} else {
|
||||
// fake anchor that's always in front of HMD
|
||||
app.input_state.hmd
|
||||
}
|
||||
}
|
||||
|
||||
fn get_transform(&self) -> Affine3A {
|
||||
self.saved_transform.unwrap_or_else(|| {
|
||||
Affine3A::from_scale_rotation_translation(
|
||||
Vec3::ONE * self.spawn_scale,
|
||||
self.spawn_rotation,
|
||||
self.spawn_point.into(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn auto_movement(&mut self, app: &mut AppState) {
|
||||
if let Some(parent) = self.parent_transform(app) {
|
||||
let scale = self.saved_scale.unwrap_or(self.spawn_scale);
|
||||
let point = self.saved_point.unwrap_or(self.spawn_point);
|
||||
self.transform = parent
|
||||
* Affine3A::from_scale_rotation_translation(
|
||||
Vec3::ONE * scale,
|
||||
self.spawn_rotation,
|
||||
point.into(),
|
||||
);
|
||||
|
||||
self.transform = parent * self.get_transform();
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, app: &mut AppState, hard_reset: bool) {
|
||||
if hard_reset {
|
||||
self.saved_point = None;
|
||||
self.saved_scale = None;
|
||||
self.saved_transform = None;
|
||||
}
|
||||
|
||||
let scale = self.saved_scale.unwrap_or(self.spawn_scale);
|
||||
let point = self.saved_point.unwrap_or(self.spawn_point);
|
||||
self.transform = self
|
||||
.parent_transform(app)
|
||||
.unwrap_or_else(|| self.get_anchor(app))
|
||||
* self.get_transform();
|
||||
|
||||
let translation = app.input_state.hmd.transform_point3a(point);
|
||||
self.transform = Affine3A::from_scale_rotation_translation(
|
||||
Vec3::ONE * scale,
|
||||
Quat::IDENTITY,
|
||||
translation.into(),
|
||||
);
|
||||
self.realign(&app.input_state.hmd);
|
||||
if self.grabbable && hard_reset {
|
||||
self.realign(&app.input_state.hmd);
|
||||
}
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
@@ -163,7 +176,6 @@ impl OverlayState {
|
||||
}
|
||||
|
||||
let scale = self.transform.x_axis.length();
|
||||
self.saved_scale = Some(scale);
|
||||
|
||||
let col_z = (self.transform.translation - hmd.translation).normalize();
|
||||
let col_y = up_dir;
|
||||
@@ -182,7 +194,28 @@ where
|
||||
T: Default,
|
||||
{
|
||||
pub fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.state.reset(app, true);
|
||||
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;
|
||||
if let Some(transform) = app
|
||||
.session
|
||||
.config
|
||||
.transform_values
|
||||
.arc_get(self.state.name.as_ref())
|
||||
{
|
||||
self.state.saved_transform = Some(*transform);
|
||||
hard_reset = false;
|
||||
} else {
|
||||
hard_reset = true;
|
||||
}
|
||||
self.state.reset(app, hard_reset);
|
||||
}
|
||||
self.backend.init(app)
|
||||
}
|
||||
pub fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
@@ -236,10 +269,15 @@ impl OverlayRenderer for FallbackRenderer {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub enum RelativeTo {
|
||||
/// Stays in place unless rencentered
|
||||
#[default]
|
||||
None,
|
||||
/// Stays in position relative to HMD
|
||||
Head,
|
||||
/// Stays in position relative to hand
|
||||
Hand(usize),
|
||||
/// Stays in place, no recentering
|
||||
Stage,
|
||||
}
|
||||
|
||||
pub struct SplitOverlayBackend {
|
||||
@@ -256,7 +294,14 @@ impl Default for SplitOverlayBackend {
|
||||
}
|
||||
}
|
||||
|
||||
impl OverlayBackend for SplitOverlayBackend {}
|
||||
impl OverlayBackend for SplitOverlayBackend {
|
||||
fn set_renderer(&mut self, renderer: Box<dyn OverlayRenderer>) {
|
||||
self.renderer = renderer;
|
||||
}
|
||||
fn set_interaction(&mut self, interaction: Box<dyn InteractionHandler>) {
|
||||
self.interaction = interaction;
|
||||
}
|
||||
}
|
||||
impl OverlayRenderer for SplitOverlayBackend {
|
||||
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.renderer.init(app)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
use std::{
|
||||
cmp,
|
||||
collections::{BinaryHeap, VecDeque},
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::{
|
||||
common::OverlaySelector,
|
||||
overlay::{OverlayBackend, OverlayState},
|
||||
};
|
||||
|
||||
static TASK_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
struct AppTask {
|
||||
pub not_before: Instant,
|
||||
pub id: usize,
|
||||
pub task: TaskType,
|
||||
}
|
||||
|
||||
impl PartialEq<AppTask> for AppTask {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other) == cmp::Ordering::Equal
|
||||
}
|
||||
}
|
||||
impl PartialOrd<AppTask> for AppTask {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
impl Eq for AppTask {}
|
||||
impl Ord for AppTask {
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||
self.not_before
|
||||
.cmp(&other.not_before)
|
||||
.then(self.id.cmp(&other.id))
|
||||
.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SystemTask {
|
||||
ColorGain(ColorChannel, f32),
|
||||
ResetPlayspace,
|
||||
FixFloor,
|
||||
}
|
||||
|
||||
pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send;
|
||||
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),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Copy)]
|
||||
pub enum ColorChannel {
|
||||
R,
|
||||
G,
|
||||
B,
|
||||
All,
|
||||
}
|
||||
|
||||
pub struct TaskContainer {
|
||||
tasks: BinaryHeap<AppTask>,
|
||||
}
|
||||
|
||||
impl TaskContainer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tasks: BinaryHeap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enqueue(&mut self, task: TaskType) {
|
||||
self.tasks.push(AppTask {
|
||||
not_before: Instant::now(),
|
||||
id: TASK_AUTO_INCREMENT.fetch_add(1, atomic::Ordering::Relaxed),
|
||||
task,
|
||||
});
|
||||
}
|
||||
|
||||
/// Enqueue a task to be executed at a specific time.
|
||||
/// If the time is in the past, the task will be executed immediately.
|
||||
/// Multiple tasks enqueued for the same instant will be executed in order of submission.
|
||||
pub fn enqueue_at(&mut self, task: TaskType, not_before: Instant) {
|
||||
self.tasks.push(AppTask {
|
||||
not_before,
|
||||
id: TASK_AUTO_INCREMENT.fetch_add(1, atomic::Ordering::Relaxed),
|
||||
task,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn retrieve_due(&mut self, dest_buf: &mut VecDeque<TaskType>) {
|
||||
let now = Instant::now();
|
||||
|
||||
while let Some(task) = self.tasks.peek() {
|
||||
if task.not_before > now {
|
||||
break;
|
||||
}
|
||||
|
||||
// Safe unwrap because we peeked.
|
||||
dest_buf.push_back(self.tasks.pop().unwrap().task);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use vulkano::{
|
||||
command_buffer::CommandBufferUsage,
|
||||
image::{sampler::Filter, view::ImageView, ImageUsage},
|
||||
swapchain::{
|
||||
acquire_next_image, Surface, Swapchain, SwapchainCreateInfo, SwapchainPresentInfo,
|
||||
},
|
||||
sync::GpuFuture,
|
||||
Validated, VulkanError,
|
||||
};
|
||||
use winit::{
|
||||
dpi::LogicalSize,
|
||||
event::{Event, WindowEvent},
|
||||
event_loop::ControlFlow,
|
||||
window::Window,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::load_custom_ui,
|
||||
config_io,
|
||||
graphics::{DynamicPass, DynamicPipeline, WlxGraphics, BLEND_ALPHA},
|
||||
gui::{
|
||||
modular::{modular_canvas, ModularData},
|
||||
Canvas,
|
||||
},
|
||||
hid::USE_UINPUT,
|
||||
state::{AppState, ScreenMeta},
|
||||
};
|
||||
|
||||
use super::{
|
||||
input::{TrackedDevice, TrackedDeviceRole},
|
||||
overlay::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>>,
|
||||
}
|
||||
|
||||
impl PreviewState {
|
||||
fn new(
|
||||
state: &mut AppState,
|
||||
surface: Arc<Surface>,
|
||||
window: Arc<Window>,
|
||||
panel_name: &str,
|
||||
) -> anyhow::Result<Self> {
|
||||
let config = load_custom_ui(panel_name)?;
|
||||
|
||||
let last_size = {
|
||||
let size_u64 = LAST_SIZE.load(std::sync::atomic::Ordering::Relaxed);
|
||||
[size_u64 as u32, (size_u64 >> 32) as u32]
|
||||
};
|
||||
|
||||
if last_size != config.size {
|
||||
let logical_size = LogicalSize::new(config.size[0], config.size[1]);
|
||||
let _ = window.request_inner_size(logical_size);
|
||||
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,
|
||||
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 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 {
|
||||
canvas,
|
||||
pipeline,
|
||||
pass,
|
||||
swapchain,
|
||||
images,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uidev_run(panel_name: &str) -> anyhow::Result<()> {
|
||||
let (graphics, event_loop, window, surface) = WlxGraphics::new_window()?;
|
||||
window.set_resizable(false);
|
||||
window.set_title("WlxOverlay UI Preview");
|
||||
|
||||
USE_UINPUT.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let mut state = AppState::from_graphics(graphics.clone())?;
|
||||
add_dummy_devices(&mut state);
|
||||
add_dummy_screens(&mut state);
|
||||
|
||||
let mut preview = Some(PreviewState::new(
|
||||
&mut state,
|
||||
surface.clone(),
|
||||
window.clone(),
|
||||
panel_name,
|
||||
)?);
|
||||
|
||||
let watch_path = config_io::CONFIG_ROOT_PATH.join(format!("{}.yaml", panel_name));
|
||||
let mut path_last_modified = watch_path.metadata()?.modified()?;
|
||||
let mut recreate = false;
|
||||
let mut last_draw = std::time::Instant::now();
|
||||
|
||||
event_loop.run(move |event, elwt| {
|
||||
elwt.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
match event {
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
..
|
||||
} => {
|
||||
elwt.exit();
|
||||
}
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::Resized(_),
|
||||
..
|
||||
} => {
|
||||
recreate = true;
|
||||
}
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::RedrawRequested,
|
||||
..
|
||||
} => {
|
||||
let new_modified = watch_path.metadata().unwrap().modified().unwrap();
|
||||
if new_modified > path_last_modified {
|
||||
recreate = true;
|
||||
path_last_modified = new_modified;
|
||||
}
|
||||
|
||||
if recreate {
|
||||
drop(preview.take());
|
||||
preview = Some(
|
||||
PreviewState::new(&mut state, surface.clone(), window.clone(), panel_name)
|
||||
.unwrap(),
|
||||
);
|
||||
recreate = false;
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
{
|
||||
let preview = preview.as_mut().unwrap();
|
||||
let (image_index, _, acquire_future) =
|
||||
match acquire_next_image(preview.swapchain.clone(), None)
|
||||
.map_err(Validated::unwrap)
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(VulkanError::OutOfDate) => {
|
||||
recreate = true;
|
||||
return;
|
||||
}
|
||||
Err(e) => panic!("failed to acquire next image: {e}"),
|
||||
};
|
||||
|
||||
if let Err(e) = preview.canvas.render(&mut state) {
|
||||
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)
|
||||
.unwrap()
|
||||
.then_swapchain_present(
|
||||
graphics.queue.clone(),
|
||||
SwapchainPresentInfo::swapchain_image_index(
|
||||
preview.swapchain.clone(),
|
||||
image_index,
|
||||
),
|
||||
)
|
||||
.then_signal_fence_and_flush()
|
||||
.unwrap()
|
||||
.wait(None)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Event::AboutToWait => {
|
||||
if last_draw.elapsed().as_millis() > 100 {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_swapchain(
|
||||
graphics: &WlxGraphics,
|
||||
surface: Arc<Surface>,
|
||||
extent: [u32; 2],
|
||||
) -> anyhow::Result<(Arc<Swapchain>, Vec<Arc<ImageView>>)> {
|
||||
let surface_capabilities = graphics
|
||||
.device
|
||||
.physical_device()
|
||||
.surface_capabilities(&surface, Default::default())
|
||||
.unwrap();
|
||||
|
||||
let (swapchain, images) = Swapchain::new(
|
||||
graphics.device.clone(),
|
||||
surface.clone(),
|
||||
SwapchainCreateInfo {
|
||||
min_image_count: surface_capabilities.min_image_count.max(2),
|
||||
image_format: graphics.native_format,
|
||||
image_extent: extent,
|
||||
image_usage: ImageUsage::COLOR_ATTACHMENT,
|
||||
composite_alpha: surface_capabilities
|
||||
.supported_composite_alpha
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap(),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let image_views = images
|
||||
.into_iter()
|
||||
.map(|image| ImageView::new_default(image).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok((swapchain, image_views))
|
||||
}
|
||||
|
||||
fn add_dummy_devices(app: &mut AppState) {
|
||||
app.input_state.devices.push(TrackedDevice {
|
||||
role: TrackedDeviceRole::Hmd,
|
||||
soc: Some(0.42),
|
||||
charging: true,
|
||||
});
|
||||
app.input_state.devices.push(TrackedDevice {
|
||||
role: TrackedDeviceRole::LeftHand,
|
||||
soc: Some(0.72),
|
||||
charging: false,
|
||||
});
|
||||
app.input_state.devices.push(TrackedDevice {
|
||||
role: TrackedDeviceRole::RightHand,
|
||||
soc: Some(0.73),
|
||||
charging: false,
|
||||
});
|
||||
app.input_state.devices.push(TrackedDevice {
|
||||
role: TrackedDeviceRole::Tracker,
|
||||
soc: Some(0.65),
|
||||
charging: false,
|
||||
});
|
||||
app.input_state.devices.push(TrackedDevice {
|
||||
role: TrackedDeviceRole::Tracker,
|
||||
soc: Some(0.67),
|
||||
charging: false,
|
||||
});
|
||||
app.input_state.devices.push(TrackedDevice {
|
||||
role: TrackedDeviceRole::Tracker,
|
||||
soc: Some(0.69),
|
||||
charging: false,
|
||||
});
|
||||
}
|
||||
|
||||
fn add_dummy_screens(app: &mut AppState) {
|
||||
app.screens.push(ScreenMeta {
|
||||
name: "HDMI-A-1".into(),
|
||||
id: 0,
|
||||
native_handle: 0,
|
||||
});
|
||||
app.screens.push(ScreenMeta {
|
||||
name: "DP-2".into(),
|
||||
id: 0,
|
||||
native_handle: 0,
|
||||
});
|
||||
app.screens.push(ScreenMeta {
|
||||
name: "DP-3".into(),
|
||||
id: 0,
|
||||
native_handle: 0,
|
||||
});
|
||||
}
|
||||
+222
-46
@@ -1,29 +1,100 @@
|
||||
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::load_with_fallback;
|
||||
use crate::overlays::toast::DisplayMethod;
|
||||
use crate::overlays::toast::ToastTopic;
|
||||
use crate::state::LeftRight;
|
||||
use anyhow::bail;
|
||||
use config::Config;
|
||||
use config::File;
|
||||
use glam::vec3a;
|
||||
use glam::Affine3A;
|
||||
use glam::Quat;
|
||||
use glam::Vec3A;
|
||||
use idmap::IdMap;
|
||||
use log::error;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn def_watch_pos() -> [f32; 3] {
|
||||
[-0.03, -0.01, 0.125]
|
||||
pub type AStrMap<V> = Vec<(Arc<str>, V)>;
|
||||
|
||||
pub trait AStrMapExt<V> {
|
||||
fn arc_set(&mut self, key: Arc<str>, value: V) -> bool;
|
||||
fn arc_get(&self, key: &str) -> Option<&V>;
|
||||
fn arc_rm(&mut self, key: &str) -> Option<V>;
|
||||
}
|
||||
|
||||
pub fn def_watch_rot() -> [f32; 4] {
|
||||
[-0.7071066, 0.0007963618, 0.7071066, 0.0]
|
||||
impl<V> AStrMapExt<V> for AStrMap<V> {
|
||||
fn arc_set(&mut self, key: Arc<str>, value: V) -> bool {
|
||||
let index = self.iter().position(|(k, _)| k.as_ref().eq(key.as_ref()));
|
||||
index.map(|i| self.remove(i).1);
|
||||
self.push((key, value));
|
||||
true
|
||||
}
|
||||
|
||||
fn arc_get(&self, key: &str) -> Option<&V> {
|
||||
self.iter()
|
||||
.find_map(|(k, v)| if k.as_ref().eq(key) { Some(v) } else { None })
|
||||
}
|
||||
|
||||
fn arc_rm(&mut self, key: &str) -> Option<V> {
|
||||
let index = self.iter().position(|(k, _)| k.as_ref().eq(key));
|
||||
index.map(|i| self.remove(i).1)
|
||||
}
|
||||
}
|
||||
|
||||
pub type AStrSet = Vec<Arc<str>>;
|
||||
|
||||
pub trait AStrSetExt {
|
||||
fn arc_set(&mut self, value: Arc<str>) -> bool;
|
||||
fn arc_get(&self, value: &str) -> bool;
|
||||
fn arc_rm(&mut self, value: &str) -> bool;
|
||||
}
|
||||
|
||||
impl AStrSetExt for AStrSet {
|
||||
fn arc_set(&mut self, value: Arc<str>) -> bool {
|
||||
if self.iter().any(|v| v.as_ref().eq(value.as_ref())) {
|
||||
return false;
|
||||
}
|
||||
self.push(value);
|
||||
true
|
||||
}
|
||||
|
||||
fn arc_get(&self, value: &str) -> bool {
|
||||
self.iter().any(|v| v.as_ref().eq(value))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub type PwTokenMap = AStrMap<String>;
|
||||
|
||||
pub 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 fn def_left() -> LeftRight {
|
||||
LeftRight::Left
|
||||
}
|
||||
|
||||
pub fn def_pw_tokens() -> Vec<(String, String)> {
|
||||
Vec::new()
|
||||
pub fn def_pw_tokens() -> PwTokenMap {
|
||||
AStrMap::new()
|
||||
}
|
||||
|
||||
fn def_click_freeze_time_ms() -> u32 {
|
||||
@@ -54,21 +125,37 @@ fn def_osc_port() -> u16 {
|
||||
9000
|
||||
}
|
||||
|
||||
fn def_screens() -> Vec<Arc<str>> {
|
||||
vec![]
|
||||
fn def_screens() -> AStrSet {
|
||||
AStrSet::new()
|
||||
}
|
||||
|
||||
fn def_curve_values() -> AStrMap<f32> {
|
||||
AStrMap::new()
|
||||
}
|
||||
|
||||
fn def_transforms() -> AStrMap<Affine3A> {
|
||||
AStrMap::new()
|
||||
}
|
||||
|
||||
fn def_auto() -> Arc<str> {
|
||||
"auto".into()
|
||||
}
|
||||
|
||||
fn def_toast_topics() -> IdMap<ToastTopic, DisplayMethod> {
|
||||
IdMap::new()
|
||||
}
|
||||
|
||||
fn def_font() -> Arc<str> {
|
||||
"LiberationSans:style=Bold".into()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct GeneralConfig {
|
||||
#[serde(default = "def_watch_pos")]
|
||||
pub watch_pos: [f32; 3],
|
||||
pub watch_pos: Vec3A,
|
||||
|
||||
#[serde(default = "def_watch_rot")]
|
||||
pub watch_rot: [f32; 4],
|
||||
pub watch_rot: Quat,
|
||||
|
||||
#[serde(default = "def_left")]
|
||||
pub watch_hand: LeftRight,
|
||||
@@ -82,6 +169,9 @@ pub struct GeneralConfig {
|
||||
#[serde(default = "def_true")]
|
||||
pub notifications_sound_enabled: bool,
|
||||
|
||||
#[serde(default = "def_toast_topics")]
|
||||
pub notification_topics: IdMap<ToastTopic, DisplayMethod>,
|
||||
|
||||
#[serde(default = "def_true")]
|
||||
pub keyboard_sound_enabled: bool,
|
||||
|
||||
@@ -100,9 +190,6 @@ pub struct GeneralConfig {
|
||||
#[serde(default = "def_one")]
|
||||
pub long_press_duration: f32,
|
||||
|
||||
#[serde(default = "def_pw_tokens")]
|
||||
pub pw_tokens: Vec<(String, String)>,
|
||||
|
||||
#[serde(default = "def_osc_port")]
|
||||
pub osc_out_port: u16,
|
||||
|
||||
@@ -113,7 +200,13 @@ pub struct GeneralConfig {
|
||||
pub double_cursor_fix: bool,
|
||||
|
||||
#[serde(default = "def_screens")]
|
||||
pub show_screens: Vec<Arc<str>>,
|
||||
pub show_screens: AStrSet,
|
||||
|
||||
#[serde(default = "def_curve_values")]
|
||||
pub curve_values: AStrMap<f32>,
|
||||
|
||||
#[serde(default = "def_transforms")]
|
||||
pub transform_values: AStrMap<Affine3A>,
|
||||
|
||||
#[serde(default = "def_auto")]
|
||||
pub capture_method: Arc<str>,
|
||||
@@ -135,6 +228,21 @@ pub struct GeneralConfig {
|
||||
|
||||
#[serde(default = "def_half")]
|
||||
pub xr_alt_click_sensitivity_release: f32,
|
||||
|
||||
#[serde(default = "def_true")]
|
||||
pub allow_sliding: bool,
|
||||
|
||||
#[serde(default = "def_true")]
|
||||
pub realign_on_showhide: bool,
|
||||
|
||||
#[serde(default = "def_false")]
|
||||
pub focus_follows_mouse_mode: bool,
|
||||
|
||||
#[serde(default = "def_font")]
|
||||
pub primary_font: Arc<str>,
|
||||
|
||||
#[serde(default = "def_one")]
|
||||
pub space_drag_multiplier: f32,
|
||||
}
|
||||
|
||||
impl GeneralConfig {
|
||||
@@ -159,13 +267,19 @@ impl GeneralConfig {
|
||||
}
|
||||
}
|
||||
|
||||
const FALLBACKS: [&str; 3] = [
|
||||
const FALLBACKS: [&str; 4] = [
|
||||
include_str!("res/keyboard.yaml"),
|
||||
include_str!("res/watch.yaml"),
|
||||
include_str!("res/settings.yaml"),
|
||||
include_str!("res/anchor.yaml"),
|
||||
];
|
||||
|
||||
const FILES: [&str; 3] = ["keyboard.yaml", "watch.yaml", "settings.yaml"];
|
||||
const FILES: [&str; 4] = [
|
||||
"keyboard.yaml",
|
||||
"watch.yaml",
|
||||
"settings.yaml",
|
||||
"anchor.yaml",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(usize)]
|
||||
@@ -173,6 +287,7 @@ pub enum ConfigType {
|
||||
Keyboard,
|
||||
Watch,
|
||||
Settings,
|
||||
Anchor,
|
||||
}
|
||||
|
||||
pub fn load_known_yaml<T>(config_type: ConfigType) -> T
|
||||
@@ -205,10 +320,19 @@ pub fn load_custom_ui(name: &str) -> anyhow::Result<ModularUiConfig> {
|
||||
}
|
||||
|
||||
pub fn load_general() -> GeneralConfig {
|
||||
let mut yaml_data = String::new();
|
||||
let mut settings_builder = Config::builder();
|
||||
|
||||
// Add files from conf.d directory
|
||||
let path_conf_d = get_conf_d_path();
|
||||
|
||||
for mut base_conf in [CONFIG_ROOT_PATH.clone(), path_conf_d.clone()] {
|
||||
base_conf.push("config.yaml");
|
||||
if base_conf.exists() {
|
||||
log::info!("Loading config file: {}", base_conf.to_string_lossy());
|
||||
settings_builder = settings_builder.add_source(File::from(base_conf));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(paths_unsorted) = std::fs::read_dir(path_conf_d) {
|
||||
let mut paths: Vec<_> = paths_unsorted
|
||||
.filter_map(|r| match r {
|
||||
@@ -222,37 +346,89 @@ pub fn load_general() -> GeneralConfig {
|
||||
// Sort paths alphabetically
|
||||
paths.sort_by_key(|dir| dir.path());
|
||||
for path in paths {
|
||||
let file_type = match path.file_type() {
|
||||
Ok(file_type) => file_type,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to get file type of {}: {}",
|
||||
path.path().to_string_lossy(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::info!("Loading config file {}", path.path().to_string_lossy());
|
||||
|
||||
if let Ok(data) = std::fs::read_to_string(path.path()) {
|
||||
yaml_data.push('\n'); // Just in case, if end of the config file was not newline
|
||||
yaml_data.push_str(data.as_str());
|
||||
} else {
|
||||
// Shouldn't happen anyways
|
||||
error!("Failed to load {}", path.path().to_string_lossy());
|
||||
}
|
||||
log::info!("Loading config file: {}", path.path().to_string_lossy());
|
||||
settings_builder = settings_builder.add_source(File::from(path.path()));
|
||||
}
|
||||
}
|
||||
|
||||
if yaml_data.is_empty() {
|
||||
yaml_data.push_str(load_with_fallback!("config.yaml", "res/config.yaml").as_str());
|
||||
match settings_builder.build() {
|
||||
Ok(settings) => match settings.try_deserialize::<GeneralConfig>() {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
panic!("Failed to deserialize settings: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
panic!("Failed to build settings: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serde_yaml::from_str(&yaml_data).expect("Failed to parse config.yaml")
|
||||
// Config that is saved from the settings panel
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AutoSettings {
|
||||
pub watch_pos: Vec3A,
|
||||
pub watch_rot: Quat,
|
||||
pub watch_hand: LeftRight,
|
||||
pub watch_view_angle_min: f32,
|
||||
pub watch_view_angle_max: f32,
|
||||
pub notifications_enabled: bool,
|
||||
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
|
||||
}
|
||||
pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
|
||||
let conf = AutoSettings {
|
||||
watch_pos: config.watch_pos,
|
||||
watch_rot: config.watch_rot,
|
||||
watch_hand: config.watch_hand,
|
||||
watch_view_angle_min: config.watch_view_angle_min,
|
||||
watch_view_angle_max: config.watch_view_angle_max,
|
||||
notifications_enabled: config.notifications_enabled,
|
||||
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
|
||||
std::fs::write(get_settings_path(), json)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Config that is saved after manipulating overlays
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AutoState {
|
||||
pub show_screens: AStrSet,
|
||||
pub curve_values: AStrMap<f32>,
|
||||
pub transform_values: AStrMap<Affine3A>,
|
||||
}
|
||||
|
||||
fn get_state_path() -> PathBuf {
|
||||
let mut path = config_io::get_conf_d_path();
|
||||
path.push("zz-saved-state.json5");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn save_layout(config: &GeneralConfig) -> anyhow::Result<()> {
|
||||
let conf = AutoState {
|
||||
show_screens: config.show_screens.clone(),
|
||||
curve_values: config.curve_values.clone(),
|
||||
transform_values: config.transform_values.clone(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
|
||||
std::fs::write(get_state_path(), json)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -53,19 +53,3 @@ pub fn load(filename: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! load_with_fallback {
|
||||
($filename: expr, $fallback: expr) => {
|
||||
if let Some(data) = config_io::load($filename) {
|
||||
data
|
||||
} else {
|
||||
log::info!(
|
||||
"Config {}/{} does not exist, using internal fallback",
|
||||
config_io::CONFIG_ROOT_PATH.to_string_lossy(),
|
||||
$filename
|
||||
);
|
||||
include_str!($fallback).to_string()
|
||||
}
|
||||
};
|
||||
}
|
||||
+234
-57
@@ -42,8 +42,8 @@ use vulkano::{
|
||||
allocator::StandardDescriptorSetAllocator, DescriptorSet, WriteDescriptorSet,
|
||||
},
|
||||
device::{
|
||||
physical::PhysicalDevice, Device, DeviceCreateInfo, DeviceExtensions, Features, Queue,
|
||||
QueueCreateInfo, QueueFlags,
|
||||
physical::PhysicalDevice, Device, DeviceCreateInfo, DeviceExtensions, DeviceFeatures,
|
||||
Queue, QueueCreateInfo, QueueFlags,
|
||||
},
|
||||
format::Format,
|
||||
image::{
|
||||
@@ -92,10 +92,12 @@ use vulkano::{
|
||||
};
|
||||
|
||||
use wlx_capture::frame::{
|
||||
DmabufFrame, FourCC, DRM_FORMAT_ABGR8888, DRM_FORMAT_ARGB8888, DRM_FORMAT_XBGR8888,
|
||||
DRM_FORMAT_XRGB8888,
|
||||
DmabufFrame, FourCC, DRM_FORMAT_ABGR2101010, DRM_FORMAT_ABGR8888, DRM_FORMAT_ARGB8888,
|
||||
DRM_FORMAT_XBGR2101010, DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888,
|
||||
};
|
||||
|
||||
pub const DRM_FORMAT_MOD_INVALID: u64 = 0xff_ffff_ffff_ffff;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(BufferContents, Vertex, Copy, Clone, Debug)]
|
||||
pub struct Vert2Uv {
|
||||
@@ -133,6 +135,15 @@ pub struct WlxGraphics {
|
||||
pub shared_shaders: RwLock<HashMap<&'static str, Arc<ShaderModule>>>,
|
||||
}
|
||||
|
||||
fn get_dmabuf_extensions() -> DeviceExtensions {
|
||||
DeviceExtensions {
|
||||
khr_external_memory: true,
|
||||
khr_external_memory_fd: true,
|
||||
ext_external_memory_dma_buf: true,
|
||||
..DeviceExtensions::empty()
|
||||
}
|
||||
}
|
||||
|
||||
static VULKAN_LIBRARY: OnceLock<Arc<vulkano::VulkanLibrary>> = OnceLock::new();
|
||||
fn get_vulkan_library() -> &'static Arc<vulkano::VulkanLibrary> {
|
||||
VULKAN_LIBRARY.get_or_init(|| vulkano::VulkanLibrary::new().unwrap()) // want panic
|
||||
@@ -181,7 +192,7 @@ impl WlxGraphics {
|
||||
let target_version = vulkano::Version::V1_3;
|
||||
let library = get_vulkan_library();
|
||||
|
||||
let vk_app_info_raw = vk::ApplicationInfo::builder()
|
||||
let vk_app_info_raw = vk::ApplicationInfo::default()
|
||||
.application_version(0)
|
||||
.engine_version(0)
|
||||
.api_version(vk_target_version);
|
||||
@@ -191,7 +202,7 @@ impl WlxGraphics {
|
||||
.create_vulkan_instance(
|
||||
system,
|
||||
get_instance_proc_addr,
|
||||
&vk::InstanceCreateInfo::builder()
|
||||
&vk::InstanceCreateInfo::default()
|
||||
.application_info(&vk_app_info_raw)
|
||||
.enabled_extension_names(&instance_extensions_raw)
|
||||
as *const _ as *const _,
|
||||
@@ -243,14 +254,18 @@ impl WlxGraphics {
|
||||
.position(|(_, q)| q.queue_flags.intersects(QueueFlags::GRAPHICS))
|
||||
.expect("Vulkan device has no graphics queue") as u32;
|
||||
|
||||
let device_extensions = DeviceExtensions {
|
||||
khr_swapchain: true,
|
||||
khr_external_memory: true,
|
||||
khr_external_memory_fd: true,
|
||||
ext_external_memory_dma_buf: true,
|
||||
ext_image_drm_format_modifier: true,
|
||||
..DeviceExtensions::empty()
|
||||
};
|
||||
let mut device_extensions = DeviceExtensions::empty();
|
||||
let dmabuf_extensions = get_dmabuf_extensions();
|
||||
|
||||
if physical_device
|
||||
.supported_extensions()
|
||||
.contains(&dmabuf_extensions)
|
||||
{
|
||||
device_extensions = device_extensions.union(&dmabuf_extensions);
|
||||
device_extensions.ext_image_drm_format_modifier = physical_device
|
||||
.supported_extensions()
|
||||
.ext_image_drm_format_modifier;
|
||||
}
|
||||
|
||||
let device_extensions_raw = device_extensions
|
||||
.into_iter()
|
||||
@@ -264,26 +279,23 @@ impl WlxGraphics {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let features = Features {
|
||||
let features = DeviceFeatures {
|
||||
dynamic_rendering: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let queue_priorities = [1.0];
|
||||
|
||||
let queue_create_infos = [vk::DeviceQueueCreateInfo::builder()
|
||||
let queue_create_infos = [vk::DeviceQueueCreateInfo::default()
|
||||
.queue_family_index(queue_family_index)
|
||||
.queue_priorities(&queue_priorities)
|
||||
.build()];
|
||||
.queue_priorities(&queue_priorities)];
|
||||
|
||||
let mut device_create_info = vk::DeviceCreateInfo::builder()
|
||||
let mut device_create_info = vk::DeviceCreateInfo::default()
|
||||
.queue_create_infos(&queue_create_infos)
|
||||
.enabled_extension_names(&device_extensions_raw)
|
||||
.build();
|
||||
.enabled_extension_names(&device_extensions_raw);
|
||||
|
||||
let mut dynamic_rendering = PhysicalDeviceDynamicRenderingFeatures::builder()
|
||||
.dynamic_rendering(true)
|
||||
.build();
|
||||
let mut dynamic_rendering =
|
||||
PhysicalDeviceDynamicRenderingFeatures::default().dynamic_rendering(true);
|
||||
|
||||
dynamic_rendering.p_next = device_create_info.p_next as _;
|
||||
device_create_info.p_next = (&mut dynamic_rendering) as *const _ as *const c_void;
|
||||
@@ -316,6 +328,15 @@ impl WlxGraphics {
|
||||
)
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
" DMA-buf supported: {}",
|
||||
device.enabled_extensions().ext_external_memory_dma_buf
|
||||
);
|
||||
log::debug!(
|
||||
" DRM format modifiers supported: {}",
|
||||
device.enabled_extensions().ext_image_drm_format_modifier
|
||||
);
|
||||
|
||||
// Drop the CStrings
|
||||
device_extensions_raw
|
||||
.into_iter()
|
||||
@@ -366,6 +387,7 @@ impl WlxGraphics {
|
||||
//#[cfg(debug_assertions)]
|
||||
//let layers = vec!["VK_LAYER_KHRONOS_validation".to_owned()];
|
||||
//#[cfg(not(debug_assertions))]
|
||||
|
||||
let layers = vec![];
|
||||
|
||||
log::debug!("Instance exts for runtime: {:?}", &vk_instance_extensions);
|
||||
@@ -382,31 +404,38 @@ impl WlxGraphics {
|
||||
},
|
||||
)?;
|
||||
|
||||
let device_extensions = DeviceExtensions {
|
||||
khr_external_memory: true,
|
||||
khr_external_memory_fd: true,
|
||||
ext_external_memory_dma_buf: true,
|
||||
ext_image_drm_format_modifier: true,
|
||||
..DeviceExtensions::empty()
|
||||
};
|
||||
|
||||
log::debug!("Device exts for app: {:?}", &device_extensions);
|
||||
let dmabuf_extensions = get_dmabuf_extensions();
|
||||
|
||||
let (physical_device, my_extensions, queue_family_index) = instance
|
||||
.enumerate_physical_devices()?
|
||||
.filter_map(|p| {
|
||||
let runtime_extensions = vk_device_extensions_fn(&p);
|
||||
let mut my_extensions = vk_device_extensions_fn(&p);
|
||||
|
||||
if !p.supported_extensions().contains(&my_extensions) {
|
||||
log::debug!(
|
||||
"Not using {} due to missing extensions:",
|
||||
p.properties().device_name,
|
||||
);
|
||||
for (ext, missing) in p.supported_extensions().difference(&my_extensions) {
|
||||
if missing {
|
||||
log::debug!(" {}", ext);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if p.supported_extensions().contains(&dmabuf_extensions) {
|
||||
my_extensions = my_extensions.union(&dmabuf_extensions);
|
||||
my_extensions.ext_image_drm_format_modifier =
|
||||
p.supported_extensions().ext_image_drm_format_modifier;
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Device exts for {}: {:?}",
|
||||
p.properties().device_name,
|
||||
&runtime_extensions
|
||||
&my_extensions
|
||||
);
|
||||
let my_extensions = runtime_extensions.union(&device_extensions);
|
||||
if p.supported_extensions().contains(&my_extensions) {
|
||||
Some((p, my_extensions))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some((p, my_extensions))
|
||||
})
|
||||
.filter_map(|(p, my_extensions)| {
|
||||
p.queue_family_properties()
|
||||
@@ -434,9 +463,9 @@ impl WlxGraphics {
|
||||
physical_device,
|
||||
DeviceCreateInfo {
|
||||
enabled_extensions: my_extensions,
|
||||
enabled_features: Features {
|
||||
enabled_features: DeviceFeatures {
|
||||
dynamic_rendering: true,
|
||||
..Features::empty()
|
||||
..DeviceFeatures::empty()
|
||||
},
|
||||
queue_create_infos: vec![QueueCreateInfo {
|
||||
queue_family_index,
|
||||
@@ -446,6 +475,15 @@ impl WlxGraphics {
|
||||
},
|
||||
)?;
|
||||
|
||||
log::debug!(
|
||||
" DMA-buf supported: {}",
|
||||
device.enabled_extensions().ext_external_memory_dma_buf
|
||||
);
|
||||
log::debug!(
|
||||
" DRM format modifiers supported: {}",
|
||||
device.enabled_extensions().ext_image_drm_format_modifier
|
||||
);
|
||||
|
||||
let queue = queues
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("no GPU queues available"))?;
|
||||
@@ -481,6 +519,140 @@ impl WlxGraphics {
|
||||
Ok(Arc::new(me))
|
||||
}
|
||||
|
||||
#[cfg(feature = "uidev")]
|
||||
pub fn new_window() -> anyhow::Result<(
|
||||
Arc<Self>,
|
||||
winit::event_loop::EventLoop<()>,
|
||||
Arc<winit::window::Window>,
|
||||
Arc<vulkano::swapchain::Surface>,
|
||||
)> {
|
||||
use vulkano::swapchain::Surface;
|
||||
use winit::{event_loop::EventLoop, window::Window};
|
||||
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let mut vk_instance_extensions = Surface::required_extensions(&event_loop).unwrap();
|
||||
vk_instance_extensions.khr_get_physical_device_properties2 = true;
|
||||
log::debug!("Instance exts for runtime: {:?}", &vk_instance_extensions);
|
||||
|
||||
let instance = Instance::new(
|
||||
get_vulkan_library().clone(),
|
||||
InstanceCreateInfo {
|
||||
flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
|
||||
enabled_extensions: vk_instance_extensions,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let window = Arc::new(
|
||||
event_loop
|
||||
.create_window(Window::default_attributes())
|
||||
.unwrap(),
|
||||
);
|
||||
let surface = Surface::from_window(instance.clone(), window.clone())?;
|
||||
|
||||
let mut device_extensions = DeviceExtensions::empty();
|
||||
device_extensions.khr_swapchain = true;
|
||||
|
||||
log::debug!("Device exts for app: {:?}", &device_extensions);
|
||||
|
||||
let (physical_device, my_extensions, queue_family_index) = instance
|
||||
.enumerate_physical_devices()?
|
||||
.filter_map(|p| {
|
||||
if p.supported_extensions().contains(&device_extensions) {
|
||||
Some((p, device_extensions))
|
||||
} else {
|
||||
log::debug!(
|
||||
"Not using {} because it does not implement the following device extensions:",
|
||||
p.properties().device_name,
|
||||
);
|
||||
for (ext, missing) in p.supported_extensions().difference(&device_extensions) {
|
||||
if missing {
|
||||
log::debug!(" {}", ext);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(|(p, my_extensions)| {
|
||||
p.queue_family_properties()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.position(|(i, q)| q.queue_flags.intersects(QueueFlags::GRAPHICS)
|
||||
&& p.surface_support(i as u32, &surface).unwrap_or(false))
|
||||
.map(|i| (p, my_extensions, i as u32))
|
||||
})
|
||||
.min_by_key(|(p, _, _)| match p.properties().device_type {
|
||||
PhysicalDeviceType::DiscreteGpu => 0,
|
||||
PhysicalDeviceType::IntegratedGpu => 1,
|
||||
PhysicalDeviceType::VirtualGpu => 2,
|
||||
PhysicalDeviceType::Cpu => 3,
|
||||
PhysicalDeviceType::Other => 4,
|
||||
_ => 5,
|
||||
})
|
||||
.expect("no suitable physical device found");
|
||||
|
||||
log::info!(
|
||||
"Using vkPhysicalDevice: {}",
|
||||
physical_device.properties().device_name,
|
||||
);
|
||||
|
||||
let (device, mut queues) = Device::new(
|
||||
physical_device,
|
||||
DeviceCreateInfo {
|
||||
enabled_extensions: my_extensions,
|
||||
enabled_features: DeviceFeatures {
|
||||
dynamic_rendering: true,
|
||||
..DeviceFeatures::empty()
|
||||
},
|
||||
queue_create_infos: vec![QueueCreateInfo {
|
||||
queue_family_index,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let native_format = device
|
||||
.physical_device()
|
||||
.surface_formats(&surface, Default::default())
|
||||
.unwrap()[0]
|
||||
.0;
|
||||
log::info!("Using surface format: {:?}", native_format);
|
||||
|
||||
let queue = queues
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("no GPU queues available"))?;
|
||||
|
||||
let memory_allocator = memory_allocator(device.clone());
|
||||
let command_buffer_allocator = Arc::new(StandardCommandBufferAllocator::new(
|
||||
device.clone(),
|
||||
StandardCommandBufferAllocatorCreateInfo {
|
||||
secondary_buffer_count: 32,
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
let descriptor_set_allocator = Arc::new(StandardDescriptorSetAllocator::new(
|
||||
device.clone(),
|
||||
Default::default(),
|
||||
));
|
||||
|
||||
let (quad_verts, quad_indices) = Self::default_quad(memory_allocator.clone())?;
|
||||
|
||||
let me = Self {
|
||||
instance,
|
||||
device,
|
||||
queue,
|
||||
memory_allocator,
|
||||
native_format,
|
||||
command_buffer_allocator,
|
||||
descriptor_set_allocator,
|
||||
quad_indices,
|
||||
quad_verts,
|
||||
shared_shaders: RwLock::new(HashMap::new()),
|
||||
};
|
||||
|
||||
Ok((Arc::new(me), event_loop, window, surface))
|
||||
}
|
||||
fn default_quad(
|
||||
memory_allocator: Arc<StandardMemoryAllocator>,
|
||||
) -> anyhow::Result<(Vert2Buf, IndexBuf)> {
|
||||
@@ -600,18 +772,24 @@ impl WlxGraphics {
|
||||
|
||||
let format = fourcc_to_vk(frame.format.fourcc)?;
|
||||
|
||||
let layouts: Vec<SubresourceLayout> = (0..frame.num_planes)
|
||||
.map(|i| {
|
||||
let mut tiling: ImageTiling = ImageTiling::Optimal;
|
||||
let mut modifiers: Vec<u64> = vec![];
|
||||
let mut layouts: Vec<SubresourceLayout> = vec![];
|
||||
|
||||
if frame.format.modifier != DRM_FORMAT_MOD_INVALID {
|
||||
(0..frame.num_planes).for_each(|i| {
|
||||
let plane = &frame.planes[i];
|
||||
SubresourceLayout {
|
||||
layouts.push(SubresourceLayout {
|
||||
offset: plane.offset as _,
|
||||
size: 0,
|
||||
row_pitch: plane.stride as _,
|
||||
array_pitch: None,
|
||||
depth_pitch: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
});
|
||||
modifiers.push(frame.format.modifier);
|
||||
});
|
||||
tiling = ImageTiling::DrmFormatModifier;
|
||||
};
|
||||
|
||||
let image = unsafe {
|
||||
RawImage::new_unchecked(
|
||||
@@ -621,8 +799,8 @@ impl WlxGraphics {
|
||||
extent,
|
||||
usage: ImageUsage::SAMPLED,
|
||||
external_memory_handle_types: ExternalMemoryHandleTypes::DMA_BUF,
|
||||
tiling: ImageTiling::DrmFormatModifier,
|
||||
drm_format_modifiers: vec![frame.format.modifier],
|
||||
tiling,
|
||||
drm_format_modifiers: modifiers,
|
||||
drm_format_modifier_plane_layouts: layouts,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -830,10 +1008,7 @@ impl WlxGraphics {
|
||||
(fns.v1_0.queue_submit)(
|
||||
self.queue.handle(),
|
||||
1,
|
||||
[SubmitInfo::builder()
|
||||
.command_buffers(&[command_buffer.handle()])
|
||||
.build()]
|
||||
.as_ptr(),
|
||||
[SubmitInfo::default().command_buffers(&[command_buffer.handle()])].as_ptr(),
|
||||
fence.handle(),
|
||||
)
|
||||
}
|
||||
@@ -986,7 +1161,7 @@ impl WlxPipeline<WlxPipelineDynamic> {
|
||||
let vep = vert.entry_point("main").unwrap(); // want panic
|
||||
let fep = frag.entry_point("main").unwrap(); // want panic
|
||||
|
||||
let vertex_input_state = Vert2Uv::per_vertex().definition(&vep.info().input_interface)?;
|
||||
let vertex_input_state = Vert2Uv::per_vertex().definition(&vep)?;
|
||||
|
||||
let stages = smallvec![
|
||||
vulkano::pipeline::PipelineShaderStageCreateInfo::new(vep),
|
||||
@@ -1143,7 +1318,7 @@ impl WlxPipeline<WlxPipelineLegacy> {
|
||||
let vep = vert.entry_point("main").unwrap(); // want panic
|
||||
let fep = frag.entry_point("main").unwrap(); // want panic
|
||||
|
||||
let vertex_input_state = Vert2Uv::per_vertex().definition(&vep.info().input_interface)?;
|
||||
let vertex_input_state = Vert2Uv::per_vertex().definition(&vep)?;
|
||||
|
||||
let stages = smallvec![
|
||||
vulkano::pipeline::PipelineShaderStageCreateInfo::new(vep),
|
||||
@@ -1414,6 +1589,8 @@ pub fn fourcc_to_vk(fourcc: FourCC) -> anyhow::Result<Format> {
|
||||
DRM_FORMAT_XBGR8888 => Ok(Format::R8G8B8A8_UNORM),
|
||||
DRM_FORMAT_ARGB8888 => Ok(Format::B8G8R8A8_UNORM),
|
||||
DRM_FORMAT_XRGB8888 => Ok(Format::B8G8R8A8_UNORM),
|
||||
DRM_FORMAT_ABGR2101010 => Ok(Format::A2B10G10R10_UNORM_PACK32),
|
||||
DRM_FORMAT_XBGR2101010 => Ok(Format::A2B10G10R10_UNORM_PACK32),
|
||||
_ => bail!("Unsupported format {}", fourcc),
|
||||
}
|
||||
}
|
||||
|
||||
+23
-10
@@ -7,9 +7,8 @@ use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::Image};
|
||||
|
||||
use crate::graphics::WlxGraphics;
|
||||
|
||||
const PRIMARY_FONT: &str = "LiberationSans";
|
||||
|
||||
pub struct FontCache {
|
||||
primary_font: Arc<str>,
|
||||
fc: FontConfig,
|
||||
ft: Library,
|
||||
collections: IdMap<isize, FontCollection>,
|
||||
@@ -18,6 +17,7 @@ pub struct FontCache {
|
||||
struct FontCollection {
|
||||
fonts: Vec<Font>,
|
||||
cp_map: IdMap<usize, usize>,
|
||||
zero_glyph: Rc<Glyph>,
|
||||
}
|
||||
|
||||
struct Font {
|
||||
@@ -35,11 +35,12 @@ pub struct Glyph {
|
||||
}
|
||||
|
||||
impl FontCache {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
pub fn new(primary_font: Arc<str>) -> anyhow::Result<Self> {
|
||||
let ft = Library::init()?;
|
||||
let fc = FontConfig::default();
|
||||
|
||||
Ok(FontCache {
|
||||
primary_font,
|
||||
fc,
|
||||
ft,
|
||||
collections: IdMap::new(),
|
||||
@@ -96,6 +97,14 @@ impl FontCache {
|
||||
FontCollection {
|
||||
fonts: Vec::new(),
|
||||
cp_map: IdMap::new(),
|
||||
zero_glyph: Rc::new(Glyph {
|
||||
tex: None,
|
||||
top: 0.,
|
||||
left: 0.,
|
||||
width: 0.,
|
||||
height: 0.,
|
||||
advance: size as f32 / 3.,
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -105,7 +114,8 @@ impl FontCache {
|
||||
return *font;
|
||||
}
|
||||
|
||||
let pattern_str = format!("{PRIMARY_FONT}-{size}:style=bold:charset={cp:04x}");
|
||||
let primary_font = self.primary_font.clone();
|
||||
let pattern_str = format!("{primary_font}:size={size}:charset={cp:04x}");
|
||||
let mut pattern = OwnedPattern::from_str(&pattern_str).unwrap(); // safe because PRIMARY_FONT is const
|
||||
self.fc
|
||||
.substitute(&mut pattern, fontconfig::MatchKind::Pattern);
|
||||
@@ -145,7 +155,7 @@ impl FontCache {
|
||||
continue;
|
||||
}
|
||||
let g = face.get_char_index(cp);
|
||||
if g > 0 {
|
||||
if g.is_some() {
|
||||
coll.cp_map.insert(cp, idx);
|
||||
}
|
||||
}
|
||||
@@ -178,25 +188,28 @@ impl FontCache {
|
||||
) -> anyhow::Result<Rc<Glyph>> {
|
||||
let key = self.get_font_for_cp(cp, size);
|
||||
|
||||
let font = &mut self.collections[size].fonts[key];
|
||||
let Some(font) = &mut self.collections[size].fonts.get_mut(key) else {
|
||||
log::warn!("No font found for codepoint: {}", cp);
|
||||
return Ok(self.collections[size].zero_glyph.clone());
|
||||
};
|
||||
|
||||
if let Some(glyph) = font.glyphs.get(cp) {
|
||||
return Ok(glyph.clone());
|
||||
}
|
||||
|
||||
if font.face.load_char(cp, LoadFlag::DEFAULT).is_err() {
|
||||
return Ok(font.glyphs[0].clone());
|
||||
return Ok(self.collections[size].zero_glyph.clone());
|
||||
}
|
||||
|
||||
let glyph = font.face.glyph();
|
||||
if glyph.render_glyph(freetype::RenderMode::Normal).is_err() {
|
||||
return Ok(font.glyphs[0].clone());
|
||||
return Ok(self.collections[size].zero_glyph.clone());
|
||||
}
|
||||
|
||||
let bmp = glyph.bitmap();
|
||||
let buf = bmp.buffer().to_vec();
|
||||
if buf.is_empty() {
|
||||
return Ok(font.glyphs[0].clone());
|
||||
return Ok(self.collections[size].zero_glyph.clone());
|
||||
}
|
||||
|
||||
let metrics = glyph.metrics();
|
||||
@@ -205,7 +218,7 @@ impl FontCache {
|
||||
Ok(PixelMode::Gray) => Format::R8_UNORM,
|
||||
Ok(PixelMode::Gray2) => Format::R16_SFLOAT,
|
||||
Ok(PixelMode::Gray4) => Format::R32_SFLOAT,
|
||||
_ => return Ok(font.glyphs[0].clone()),
|
||||
_ => return Ok(self.collections[size].zero_glyph.clone()),
|
||||
};
|
||||
|
||||
let mut cmd_buffer = graphics.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
|
||||
+159
-56
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::bail;
|
||||
use glam::{Vec2, Vec3, Vec4};
|
||||
use glam::{Vec2, Vec4};
|
||||
use vulkano::{
|
||||
command_buffer::CommandBufferUsage,
|
||||
format::Format,
|
||||
@@ -19,6 +19,8 @@ use crate::{
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use self::modular::GuiColor;
|
||||
|
||||
pub mod font;
|
||||
pub mod modular;
|
||||
|
||||
@@ -32,14 +34,33 @@ struct Rect {
|
||||
}
|
||||
|
||||
// Parses a color from a HTML hex string
|
||||
pub fn color_parse(html_hex: &str) -> anyhow::Result<Vec3> {
|
||||
pub fn color_parse(html_hex: &str) -> anyhow::Result<GuiColor> {
|
||||
if html_hex.len() == 7 {
|
||||
if let (Ok(r), Ok(g), Ok(b)) = (
|
||||
u8::from_str_radix(&html_hex[1..3], 16),
|
||||
u8::from_str_radix(&html_hex[3..5], 16),
|
||||
u8::from_str_radix(&html_hex[5..7], 16),
|
||||
) {
|
||||
return Ok(Vec3::new(r as f32 / 255., g as f32 / 255., b as f32 / 255.));
|
||||
return Ok(Vec4::new(
|
||||
r as f32 / 255.,
|
||||
g as f32 / 255.,
|
||||
b as f32 / 255.,
|
||||
1.,
|
||||
));
|
||||
}
|
||||
} else if html_hex.len() == 9 {
|
||||
if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
|
||||
u8::from_str_radix(&html_hex[1..3], 16),
|
||||
u8::from_str_radix(&html_hex[3..5], 16),
|
||||
u8::from_str_radix(&html_hex[5..7], 16),
|
||||
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.,
|
||||
));
|
||||
}
|
||||
}
|
||||
bail!(
|
||||
@@ -51,8 +72,8 @@ pub fn color_parse(html_hex: &str) -> anyhow::Result<Vec3> {
|
||||
pub struct CanvasBuilder<D, S> {
|
||||
canvas: Canvas<D, S>,
|
||||
|
||||
pub fg_color: Vec3,
|
||||
pub bg_color: Vec3,
|
||||
pub fg_color: GuiColor,
|
||||
pub bg_color: GuiColor,
|
||||
pub font_size: isize,
|
||||
}
|
||||
|
||||
@@ -66,8 +87,8 @@ impl<D, S> CanvasBuilder<D, S> {
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
canvas: Canvas::new(width, height, graphics, format, data)?,
|
||||
bg_color: Vec3::ZERO,
|
||||
fg_color: Vec3::ONE,
|
||||
bg_color: Vec4::ZERO,
|
||||
fg_color: Vec4::ONE,
|
||||
font_size: 16,
|
||||
})
|
||||
}
|
||||
@@ -150,6 +171,7 @@ impl<D, S> CanvasBuilder<D, S> {
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
cap_type: KeyCapType,
|
||||
label: &[String],
|
||||
) -> &mut Control<D, S> {
|
||||
let idx = self.canvas.controls.len();
|
||||
@@ -163,27 +185,88 @@ impl<D, S> CanvasBuilder<D, S> {
|
||||
..Control::new()
|
||||
});
|
||||
|
||||
for (i, item) in label.iter().enumerate().take(label.len().min(2)) {
|
||||
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: if i == 0 {
|
||||
Rect {
|
||||
x: x + 4.,
|
||||
y: y + (self.font_size as f32) + 4.,
|
||||
w,
|
||||
h,
|
||||
}
|
||||
} else {
|
||||
Rect {
|
||||
x: x + w * 0.5,
|
||||
y: y + h - (self.font_size as f32) + 4.,
|
||||
w,
|
||||
h,
|
||||
}
|
||||
},
|
||||
text: Arc::from(item.as_str()),
|
||||
fg_color: self.fg_color,
|
||||
rect,
|
||||
text: Arc::from(label[idx].as_str()),
|
||||
fg_color: self.fg_color * alpha,
|
||||
size: self.font_size,
|
||||
on_render_fg: Some(Control::render_text),
|
||||
on_render_fg: Some(render),
|
||||
..Control::new()
|
||||
});
|
||||
}
|
||||
@@ -211,7 +294,7 @@ pub struct Canvas<D, S> {
|
||||
hover_controls: [Option<usize>; 2],
|
||||
pressed_controls: [Option<usize>; 2],
|
||||
|
||||
interact_map: Vec<Option<u8>>,
|
||||
interact_map: Vec<Option<u16>>,
|
||||
interact_stride: usize,
|
||||
interact_rows: usize,
|
||||
|
||||
@@ -320,7 +403,7 @@ impl<D, S> Canvas<D, S> {
|
||||
|
||||
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 u8);
|
||||
self.interact_map[y * self.interact_stride + x] = Some(idx as u16);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -514,13 +597,27 @@ impl<D, S> OverlayRenderer for Canvas<D, S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, S> OverlayBackend for Canvas<D, S> {}
|
||||
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: Vec3,
|
||||
bg_color: Vec3,
|
||||
fg_color: GuiColor,
|
||||
bg_color: GuiColor,
|
||||
text: Arc<str>,
|
||||
size: isize,
|
||||
dirty: bool,
|
||||
@@ -531,15 +628,9 @@ pub struct Control<D, S> {
|
||||
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<
|
||||
fn(&Self, &CanvasData<D>, &mut AppState, &mut WlxCommandBuffer) -> anyhow::Result<()>,
|
||||
>,
|
||||
on_render_hl: Option<
|
||||
fn(&Self, &CanvasData<D>, &mut AppState, &mut WlxCommandBuffer, Vec4) -> anyhow::Result<()>,
|
||||
>,
|
||||
on_render_fg: Option<
|
||||
fn(&Self, &CanvasData<D>, &mut AppState, &mut WlxCommandBuffer) -> anyhow::Result<()>,
|
||||
>,
|
||||
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> {
|
||||
@@ -551,8 +642,8 @@ impl<D, S> Control<D, S> {
|
||||
w: 0.,
|
||||
h: 0.,
|
||||
},
|
||||
fg_color: Vec3::ONE,
|
||||
bg_color: Vec3::ZERO,
|
||||
fg_color: Vec4::ONE,
|
||||
bg_color: Vec4::ZERO,
|
||||
text: Arc::from(""),
|
||||
dirty: true,
|
||||
size: 24,
|
||||
@@ -578,7 +669,7 @@ impl<D, S> Control<D, S> {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set_fg_color(&mut self, color: Vec3) {
|
||||
pub fn set_fg_color(&mut self, color: GuiColor) {
|
||||
if self.fg_color == color {
|
||||
return;
|
||||
}
|
||||
@@ -601,10 +692,9 @@ impl<D, S> Control<D, S> {
|
||||
self.rect.w,
|
||||
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, 1.],
|
||||
)?;
|
||||
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,
|
||||
@@ -621,7 +711,7 @@ impl<D, S> Control<D, S> {
|
||||
canvas: &CanvasData<D>,
|
||||
_: &mut AppState,
|
||||
cmd_buffer: &mut WlxCommandBuffer,
|
||||
color: Vec4,
|
||||
color: GuiColor,
|
||||
) -> anyhow::Result<()> {
|
||||
let vertex_buffer = canvas.graphics.upload_verts(
|
||||
canvas.width as _,
|
||||
@@ -673,10 +763,9 @@ impl<D, S> Control<D, S> {
|
||||
ImageView::new_default(tex)?,
|
||||
Filter::Linear,
|
||||
)?;
|
||||
let set1 = canvas.pipeline_fg_glyph.uniform_buffer(
|
||||
1,
|
||||
vec![self.fg_color.x, self.fg_color.y, self.fg_color.z, 1.],
|
||||
)?;
|
||||
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,
|
||||
@@ -722,10 +811,9 @@ impl<D, S> Control<D, S> {
|
||||
ImageView::new_default(tex)?,
|
||||
Filter::Linear,
|
||||
)?;
|
||||
let set1 = canvas.pipeline_fg_glyph.uniform_buffer(
|
||||
1,
|
||||
vec![self.fg_color.x, self.fg_color.y, self.fg_color.z, 1.],
|
||||
)?;
|
||||
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,
|
||||
@@ -741,3 +829,18 @@ impl<D, S> Control<D, S> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum KeyCapType {
|
||||
/// Label is in center of keycap
|
||||
Regular,
|
||||
/// Label on the top
|
||||
/// AltGr symbol on bottom
|
||||
RegularAltGr,
|
||||
/// Primary symbol on bottom
|
||||
/// Shift symbol on top
|
||||
Reversed,
|
||||
/// Primary symbol on bottom-left
|
||||
/// Shift symbol on top-left
|
||||
/// AltGr symbol on bottom-right
|
||||
ReversedAltGr,
|
||||
}
|
||||
+158
-46
@@ -6,19 +6,20 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use glam::{Quat, Vec3A};
|
||||
use glam::{Quat, Vec4};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
common::{ColorChannel, OverlaySelector, SystemTask, TaskType},
|
||||
common::OverlaySelector,
|
||||
input::PointerMode,
|
||||
notifications::save_notifications,
|
||||
overlay::RelativeTo,
|
||||
task::{ColorChannel, SystemTask, TaskType},
|
||||
},
|
||||
config::{save_layout, save_settings, AStrSetExt},
|
||||
overlays::{
|
||||
toast::Toast,
|
||||
watch::{save_watch, WATCH_NAME},
|
||||
toast::{Toast, ToastTopic},
|
||||
watch::WATCH_NAME,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -40,14 +41,25 @@ pub enum Axis {
|
||||
Z,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub enum HighlightTest {
|
||||
AllowSliding,
|
||||
AutoRealign,
|
||||
NotificationSounds,
|
||||
Notifications,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub enum SystemAction {
|
||||
ToggleAllowSliding,
|
||||
ToggleAutoRealign,
|
||||
ToggleNotificationSounds,
|
||||
ToggleNotifications,
|
||||
PlayspaceResetOffset,
|
||||
PlayspaceFixFloor,
|
||||
RecalculateExtent,
|
||||
PersistConfig,
|
||||
PersistLayout,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -122,6 +134,9 @@ pub enum ButtonAction {
|
||||
channel: ColorChannel,
|
||||
delta: f32,
|
||||
},
|
||||
DragMultiplier {
|
||||
delta: f32,
|
||||
},
|
||||
System {
|
||||
action: SystemAction,
|
||||
},
|
||||
@@ -167,6 +182,7 @@ pub struct ButtonData {
|
||||
pub(super) long_middle_up: Option<Vec<ButtonAction>>,
|
||||
pub(super) scroll_down: Option<Vec<ButtonAction>>,
|
||||
pub(super) scroll_up: Option<Vec<ButtonAction>>,
|
||||
pub(super) highlight: Option<HighlightTest>,
|
||||
}
|
||||
|
||||
pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) {
|
||||
@@ -174,6 +190,7 @@ pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) {
|
||||
button.on_press = Some(modular_button_dn);
|
||||
button.on_release = Some(modular_button_up);
|
||||
button.on_scroll = Some(modular_button_scroll);
|
||||
button.test_highlight = Some(modular_button_highlight);
|
||||
}
|
||||
|
||||
fn modular_button_dn(
|
||||
@@ -265,6 +282,31 @@ fn modular_button_scroll(button: &mut ModularControl, _: &mut (), app: &mut AppS
|
||||
}
|
||||
}
|
||||
|
||||
fn modular_button_highlight(
|
||||
button: &ModularControl,
|
||||
_: &mut (),
|
||||
app: &mut AppState,
|
||||
) -> Option<Vec4> {
|
||||
// want panic
|
||||
let ModularData::Button(data) = button.state.as_ref().unwrap() else {
|
||||
panic!("modular_button_highlight: button state is not Button");
|
||||
};
|
||||
|
||||
if let Some(test) = &data.highlight {
|
||||
let lit = match test {
|
||||
HighlightTest::AllowSliding => app.session.config.allow_sliding,
|
||||
HighlightTest::AutoRealign => app.session.config.realign_on_showhide,
|
||||
HighlightTest::NotificationSounds => app.session.config.notifications_sound_enabled,
|
||||
HighlightTest::Notifications => app.session.config.notifications_enabled,
|
||||
};
|
||||
|
||||
if lit {
|
||||
return Some(Vec4::new(1.0, 1.0, 1.0, 0.5));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppState) {
|
||||
match action {
|
||||
ButtonAction::Exec { command, toast } => run_exec(command, toast, press, app),
|
||||
@@ -276,9 +318,13 @@ fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppStat
|
||||
body,
|
||||
seconds,
|
||||
} => {
|
||||
Toast::new(message.clone(), body.clone().unwrap_or_else(|| "".into()))
|
||||
.with_timeout(seconds.unwrap_or(5.))
|
||||
.submit(app);
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
message.clone(),
|
||||
body.clone().unwrap_or_else(|| "".into()),
|
||||
)
|
||||
.with_timeout(seconds.unwrap_or(5.))
|
||||
.submit(app);
|
||||
}
|
||||
ButtonAction::ColorAdjust { channel, delta } => {
|
||||
let channel = *channel;
|
||||
@@ -287,11 +333,42 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ENABLED_DISABLED: [&str; 2] = ["enabled", "disabled"];
|
||||
|
||||
fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
match action {
|
||||
SystemAction::ToggleAllowSliding => {
|
||||
app.session.config.allow_sliding = !app.session.config.allow_sliding;
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!(
|
||||
"Sliding is {}.",
|
||||
ENABLED_DISABLED[app.session.config.allow_sliding as usize]
|
||||
)
|
||||
.into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
}
|
||||
SystemAction::ToggleAutoRealign => {
|
||||
app.session.config.realign_on_showhide = !app.session.config.realign_on_showhide;
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!(
|
||||
"Auto realign is {}.",
|
||||
ENABLED_DISABLED[app.session.config.realign_on_showhide as usize]
|
||||
)
|
||||
.into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
}
|
||||
SystemAction::PlayspaceResetOffset => {
|
||||
app.tasks
|
||||
.enqueue(TaskType::System(SystemTask::ResetPlayspace));
|
||||
@@ -303,6 +380,7 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
let at = now.add(i * sec);
|
||||
let display = 5 - i;
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("Fixing floor in {}", display).into(),
|
||||
"Place either controller on the floor.".into(),
|
||||
)
|
||||
@@ -318,13 +396,10 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
SystemAction::ToggleNotifications => {
|
||||
app.session.config.notifications_enabled = !app.session.config.notifications_enabled;
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!(
|
||||
"Notifications are {}.",
|
||||
if app.session.config.notifications_enabled {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
}
|
||||
ENABLED_DISABLED[app.session.config.notifications_enabled as usize]
|
||||
)
|
||||
.into(),
|
||||
"".into(),
|
||||
@@ -335,13 +410,10 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
app.session.config.notifications_sound_enabled =
|
||||
!app.session.config.notifications_sound_enabled;
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!(
|
||||
"Notification sounds are {}.",
|
||||
if app.session.config.notifications_sound_enabled {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
}
|
||||
ENABLED_DISABLED[app.session.config.notifications_sound_enabled as usize]
|
||||
)
|
||||
.into(),
|
||||
"".into(),
|
||||
@@ -349,11 +421,13 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||
.submit(app);
|
||||
}
|
||||
SystemAction::PersistConfig => {
|
||||
if let Err(e) = save_watch(app) {
|
||||
log::error!("Failed to save watch config: {:?}", e);
|
||||
};
|
||||
if let Err(e) = save_notifications(app) {
|
||||
log::error!("Failed to save notifications config: {:?}", e);
|
||||
if let Err(e) = save_settings(&app.session.config) {
|
||||
log::error!("Failed to save config: {:?}", e);
|
||||
}
|
||||
}
|
||||
SystemAction::PersistLayout => {
|
||||
if let Err(e) = save_layout(&app.session.config) {
|
||||
log::error!("Failed to save layout: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,7 +457,7 @@ fn run_exec(args: &ExecArgs, toast: &Option<Arc<str>>, press: &mut PressData, ap
|
||||
Ok(proc) => {
|
||||
press.child = Some(proc);
|
||||
if let Some(toast) = toast.as_ref() {
|
||||
Toast::new(toast.clone(), "".into()).submit(app);
|
||||
Toast::new(ToastTopic::System, toast.clone(), "".into()).submit(app);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -398,11 +472,11 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
OverlaySelector::Name(WATCH_NAME.into()),
|
||||
Box::new(|app, o| {
|
||||
if o.saved_scale.is_none() {
|
||||
o.saved_scale = Some(o.spawn_scale);
|
||||
if o.saved_transform.is_none() {
|
||||
o.want_visible = false;
|
||||
o.saved_scale = Some(0.0);
|
||||
o.saved_transform = Some(o.transform);
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
"Watch hidden".into(),
|
||||
"Use show/hide binding to restore.".into(),
|
||||
)
|
||||
@@ -410,8 +484,9 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
|
||||
.submit(app);
|
||||
} else {
|
||||
o.want_visible = true;
|
||||
o.saved_scale = None;
|
||||
Toast::new("Watch restored".into(), "".into()).submit(app);
|
||||
o.saved_transform = None;
|
||||
Toast::new(ToastTopic::System, "Watch restored".into(), "".into())
|
||||
.submit(app);
|
||||
}
|
||||
}),
|
||||
));
|
||||
@@ -423,20 +498,24 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
|
||||
Box::new(|app, o| {
|
||||
if let RelativeTo::Hand(0) = o.relative_to {
|
||||
o.relative_to = RelativeTo::Hand(1);
|
||||
o.spawn_rotation = Quat::from_slice(&app.session.config.watch_rot)
|
||||
o.spawn_rotation = app.session.config.watch_rot
|
||||
* Quat::from_rotation_x(PI)
|
||||
* Quat::from_rotation_z(PI);
|
||||
o.spawn_point = Vec3A::from_slice(&app.session.config.watch_pos);
|
||||
o.spawn_point = app.session.config.watch_pos;
|
||||
o.spawn_point.x *= -1.;
|
||||
} else {
|
||||
o.relative_to = RelativeTo::Hand(0);
|
||||
o.spawn_rotation = Quat::from_slice(&app.session.config.watch_rot);
|
||||
o.spawn_point = Vec3A::from_slice(&app.session.config.watch_pos);
|
||||
o.spawn_rotation = app.session.config.watch_rot;
|
||||
o.spawn_point = app.session.config.watch_pos;
|
||||
}
|
||||
o.dirty = true;
|
||||
Toast::new("Watch switched".into(), "Check your other hand".into())
|
||||
.with_timeout(3.)
|
||||
.submit(app);
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
"Watch switched".into(),
|
||||
"Check your other hand".into(),
|
||||
)
|
||||
.with_timeout(3.)
|
||||
.submit(app);
|
||||
}),
|
||||
));
|
||||
audio_thump(app);
|
||||
@@ -473,7 +552,7 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
|
||||
OverlaySelector::Name(WATCH_NAME.into()),
|
||||
Box::new(move |app, o| {
|
||||
o.spawn_rotation *= rot;
|
||||
app.session.config.watch_rot = o.spawn_rotation.into();
|
||||
app.session.config.watch_rot = o.spawn_rotation;
|
||||
o.dirty = true;
|
||||
}),
|
||||
));
|
||||
@@ -489,7 +568,7 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
|
||||
OverlaySelector::Name(WATCH_NAME.into()),
|
||||
Box::new(move |app, o| {
|
||||
o.spawn_point[axis] += delta;
|
||||
app.session.config.watch_pos = o.spawn_point.into();
|
||||
app.session.config.watch_pos = o.spawn_point;
|
||||
o.dirty = true;
|
||||
}),
|
||||
));
|
||||
@@ -504,7 +583,12 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
|
||||
overlay.clone(),
|
||||
Box::new(|app, o| {
|
||||
o.reset(app, true);
|
||||
Toast::new(format!("{} has been reset!", o.name).into(), "".into()).submit(app);
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("{} has been reset!", o.name).into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
}),
|
||||
));
|
||||
}
|
||||
@@ -517,6 +601,20 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
|
||||
o.show_hide = o.want_visible;
|
||||
o.reset(app, false);
|
||||
}
|
||||
|
||||
let mut state_dirty = false;
|
||||
if !o.want_visible {
|
||||
state_dirty |= app.session.config.show_screens.arc_rm(o.name.as_ref());
|
||||
} else if o.want_visible {
|
||||
state_dirty |= app.session.config.show_screens.arc_set(o.name.clone());
|
||||
}
|
||||
|
||||
if state_dirty {
|
||||
match save_layout(&app.session.config) {
|
||||
Ok(_) => log::debug!("Saved state"),
|
||||
Err(e) => log::error!("Failed to save state: {:?}", e),
|
||||
}
|
||||
}
|
||||
}),
|
||||
));
|
||||
}
|
||||
@@ -529,13 +627,18 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
|
||||
o.show_hide = o.recenter;
|
||||
if !o.recenter {
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("{} is now locked in place!", o.name).into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
} else {
|
||||
Toast::new(format!("{} is now unlocked!", o.name).into(), "".into())
|
||||
.submit(app);
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("{} is now unlocked!", o.name).into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
}
|
||||
}),
|
||||
));
|
||||
@@ -548,13 +651,18 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
|
||||
o.interactable = !o.interactable;
|
||||
if !o.interactable {
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("{} is now non-interactable!", o.name).into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
} else {
|
||||
Toast::new(format!("{} is now interactable!", o.name).into(), "".into())
|
||||
.submit(app);
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
format!("{} is now interactable!", o.name).into(),
|
||||
"".into(),
|
||||
)
|
||||
.submit(app);
|
||||
}
|
||||
}),
|
||||
));
|
||||
@@ -585,9 +693,13 @@ fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
|
||||
Box::new({
|
||||
let name = window.clone();
|
||||
move |app| {
|
||||
Toast::new("Check your desktop for popup.".into(), "".into())
|
||||
.with_sound(true)
|
||||
.submit(app);
|
||||
Toast::new(
|
||||
ToastTopic::System,
|
||||
"Check your desktop for popup.".into(),
|
||||
"".into(),
|
||||
)
|
||||
.with_sound(true)
|
||||
.submit(app);
|
||||
crate::overlays::mirror::new_mirror(name.clone(), false, &app.session)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use glam::Vec3;
|
||||
use glam::Vec4;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
io::Read,
|
||||
@@ -13,7 +13,7 @@ use crate::{gui::modular::FALLBACK_COLOR, state::AppState};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{color_parse_or_default, ExecArgs, ModularControl, ModularData};
|
||||
use super::{color_parse_or_default, ExecArgs, GuiColor, ModularControl, ModularData};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "source")]
|
||||
@@ -38,15 +38,17 @@ pub enum LabelContent {
|
||||
low_color: Arc<str>,
|
||||
charging_color: Arc<str>,
|
||||
},
|
||||
DragMultiplier,
|
||||
Ipd,
|
||||
}
|
||||
|
||||
pub enum LabelData {
|
||||
Battery {
|
||||
device: usize,
|
||||
low_threshold: f32,
|
||||
normal_color: Vec3,
|
||||
low_color: Vec3,
|
||||
charging_color: Vec3,
|
||||
normal_color: GuiColor,
|
||||
low_color: GuiColor,
|
||||
charging_color: GuiColor,
|
||||
},
|
||||
Clock {
|
||||
format: Arc<str>,
|
||||
@@ -62,6 +64,10 @@ pub enum LabelData {
|
||||
command: Vec<Arc<str>>,
|
||||
child: Option<process::Child>,
|
||||
},
|
||||
Ipd {
|
||||
last_ipd: f32,
|
||||
},
|
||||
DragMultiplier,
|
||||
}
|
||||
|
||||
pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
|
||||
@@ -82,7 +88,7 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
|
||||
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);
|
||||
label.set_fg_color(*FALLBACK_COLOR);
|
||||
Tz::UTC
|
||||
})
|
||||
});
|
||||
@@ -106,6 +112,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::DragMultiplier => Some(LabelData::DragMultiplier),
|
||||
};
|
||||
|
||||
if let Some(state) = state {
|
||||
@@ -148,7 +156,7 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
};
|
||||
(text, color)
|
||||
})
|
||||
.unwrap_or_else(|| ("".into(), Vec3::ZERO));
|
||||
.unwrap_or_else(|| ("".into(), Vec4::ZERO));
|
||||
|
||||
control.set_text(&text);
|
||||
control.set_fg_color(color);
|
||||
@@ -238,5 +246,14 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
|
||||
};
|
||||
}
|
||||
}
|
||||
LabelData::Ipd { last_ipd } => {
|
||||
if (app.input_state.ipd - *last_ipd).abs() > 0.05 {
|
||||
*last_ipd = app.input_state.ipd;
|
||||
control.set_text(&format!("{:.1}", app.input_state.ipd));
|
||||
}
|
||||
}
|
||||
LabelData::DragMultiplier => {
|
||||
control.set_text(&format!("{:.1}", app.session.config.space_drag_multiplier));
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-16
@@ -4,7 +4,8 @@ pub mod label;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use glam::Vec3;
|
||||
use glam::Vec4;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{backend::common::OverlaySelector, state::AppState};
|
||||
@@ -19,11 +20,9 @@ use super::{color_parse, Canvas, CanvasBuilder, Control};
|
||||
type ModularControl = Control<(), ModularData>;
|
||||
type ExecArgs = Vec<Arc<str>>;
|
||||
|
||||
const FALLBACK_COLOR: Vec3 = Vec3 {
|
||||
x: 1.,
|
||||
y: 0.,
|
||||
z: 1.,
|
||||
};
|
||||
pub type GuiColor = Vec4;
|
||||
|
||||
static FALLBACK_COLOR: Lazy<GuiColor> = Lazy::new(|| Vec4::new(1., 0., 1., 1.));
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ModularUiConfig {
|
||||
@@ -63,6 +62,13 @@ pub enum ModularElement {
|
||||
#[serde(flatten)]
|
||||
data: LabelContent,
|
||||
},
|
||||
CenteredLabel {
|
||||
rect: [f32; 4],
|
||||
font_size: isize,
|
||||
fg_color: Arc<str>,
|
||||
#[serde(flatten)]
|
||||
data: LabelContent,
|
||||
},
|
||||
Button {
|
||||
rect: [f32; 4],
|
||||
font_size: isize,
|
||||
@@ -120,7 +126,7 @@ pub fn modular_canvas(
|
||||
size[0] as _,
|
||||
size[1] as _,
|
||||
state.graphics.clone(),
|
||||
state.format,
|
||||
state.graphics.native_format,
|
||||
(),
|
||||
)?;
|
||||
let empty_str: Arc<str> = Arc::from("");
|
||||
@@ -130,7 +136,7 @@ pub fn modular_canvas(
|
||||
rect: [x, y, w, h],
|
||||
bg_color,
|
||||
} => {
|
||||
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
|
||||
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.panel(*x, *y, *w, *h);
|
||||
}
|
||||
ModularElement::Label {
|
||||
@@ -140,10 +146,21 @@ pub fn modular_canvas(
|
||||
data,
|
||||
} => {
|
||||
canvas.font_size = *font_size;
|
||||
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
|
||||
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);
|
||||
}
|
||||
ModularElement::CenteredLabel {
|
||||
rect: [x, y, w, h],
|
||||
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);
|
||||
}
|
||||
ModularElement::Button {
|
||||
rect: [x, y, w, h],
|
||||
font_size,
|
||||
@@ -152,8 +169,8 @@ pub fn modular_canvas(
|
||||
text,
|
||||
data,
|
||||
} => {
|
||||
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
|
||||
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
|
||||
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());
|
||||
modular_button_init(button, data);
|
||||
@@ -177,7 +194,7 @@ pub fn modular_canvas(
|
||||
ListLayout::Vertical => (*w, *h / num_buttons),
|
||||
};
|
||||
|
||||
let fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
|
||||
let fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
|
||||
canvas.font_size = *font_size;
|
||||
canvas.fg_color = fg_color;
|
||||
|
||||
@@ -225,8 +242,8 @@ pub fn modular_canvas(
|
||||
ListLayout::Vertical => (*w, *h / num_buttons),
|
||||
};
|
||||
|
||||
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
|
||||
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
|
||||
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;
|
||||
|
||||
for screen in state.screens.iter() {
|
||||
@@ -326,9 +343,9 @@ pub fn modular_canvas(
|
||||
Ok(canvas.build())
|
||||
}
|
||||
|
||||
pub fn color_parse_or_default(color: &str) -> Vec3 {
|
||||
pub fn color_parse_or_default(color: &str) -> GuiColor {
|
||||
color_parse(color).unwrap_or_else(|e| {
|
||||
log::error!("Failed to parse color '{}': {}", color, e);
|
||||
FALLBACK_COLOR
|
||||
*FALLBACK_COLOR
|
||||
})
|
||||
}
|
||||
+193
-37
@@ -7,11 +7,25 @@ use input_linux::{
|
||||
};
|
||||
use libc::{input_event, timeval};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::fs::File;
|
||||
use std::mem::transmute;
|
||||
use std::{fs::File, sync::atomic::AtomicBool};
|
||||
use strum::{EnumIter, EnumString, IntoEnumIterator};
|
||||
use xkbcommon::xkb;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
mod wayland;
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
mod x11;
|
||||
|
||||
pub static USE_UINPUT: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
pub fn initialize() -> Box<dyn HidProvider> {
|
||||
if !USE_UINPUT.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
log::info!("Uinput disabled by user.");
|
||||
return Box::new(DummyProvider {});
|
||||
}
|
||||
|
||||
if let Some(uinput) = UInputProvider::try_new() {
|
||||
log::info!("Initialized uinput.");
|
||||
return Box::new(uinput);
|
||||
@@ -30,19 +44,34 @@ pub fn initialize() -> Box<dyn HidProvider> {
|
||||
|
||||
pub trait HidProvider {
|
||||
fn mouse_move(&mut self, pos: Vec2);
|
||||
fn send_button(&self, button: u16, down: bool);
|
||||
fn wheel(&self, delta: i32);
|
||||
fn send_button(&mut self, button: u16, down: bool);
|
||||
fn wheel(&mut self, delta: i32);
|
||||
fn set_modifiers(&mut self, mods: u8);
|
||||
fn send_key(&self, key: u16, down: bool);
|
||||
fn send_key(&self, key: VirtualKey, down: bool);
|
||||
fn set_desktop_extent(&mut self, extent: Vec2);
|
||||
fn on_new_frame(&mut self);
|
||||
fn set_desktop_origin(&mut self, origin: Vec2);
|
||||
fn commit(&mut self);
|
||||
}
|
||||
|
||||
struct MouseButtonAction {
|
||||
button: u16,
|
||||
down: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MouseAction {
|
||||
last_requested_pos: Option<Vec2>,
|
||||
pos: Option<Vec2>,
|
||||
button: Option<MouseButtonAction>,
|
||||
scroll: Option<i32>,
|
||||
}
|
||||
|
||||
pub struct UInputProvider {
|
||||
handle: UInputHandle<File>,
|
||||
desktop_extent: Vec2,
|
||||
mouse_moved: bool,
|
||||
desktop_origin: Vec2,
|
||||
cur_modifiers: u8,
|
||||
current_action: MouseAction,
|
||||
}
|
||||
|
||||
pub struct DummyProvider;
|
||||
@@ -135,26 +164,30 @@ impl UInputProvider {
|
||||
return Some(UInputProvider {
|
||||
handle,
|
||||
desktop_extent: Vec2::ZERO,
|
||||
mouse_moved: false,
|
||||
desktop_origin: Vec2::ZERO,
|
||||
current_action: Default::default(),
|
||||
cur_modifiers: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl HidProvider for UInputProvider {
|
||||
fn mouse_move(&mut self, pos: Vec2) {
|
||||
if self.mouse_moved {
|
||||
return;
|
||||
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_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.handle.write(&events) {
|
||||
log::error!("send_button: {}", res.to_string());
|
||||
}
|
||||
self.mouse_moved = true;
|
||||
|
||||
}
|
||||
fn mouse_move_internal(&mut self, pos: Vec2) {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!("Mouse move: {:?}", pos);
|
||||
|
||||
let pos = pos * (MOUSE_EXTENT / self.desktop_extent);
|
||||
let pos = (pos - self.desktop_origin) * (MOUSE_EXTENT / self.desktop_extent);
|
||||
|
||||
let time = get_time();
|
||||
let events = [
|
||||
@@ -166,17 +199,7 @@ impl HidProvider for UInputProvider {
|
||||
log::error!("{}", res.to_string());
|
||||
}
|
||||
}
|
||||
fn send_button(&self, button: u16, down: bool) {
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_KEY, button, down as _),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.handle.write(&events) {
|
||||
log::error!("send_button: {}", res.to_string());
|
||||
}
|
||||
}
|
||||
fn wheel(&self, delta: i32) {
|
||||
fn wheel_internal(&self, delta: i32) {
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_REL, RelativeAxis::Wheel as _, delta),
|
||||
@@ -186,22 +209,26 @@ impl HidProvider for UInputProvider {
|
||||
log::error!("wheel: {}", res.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HidProvider for UInputProvider {
|
||||
fn set_modifiers(&mut self, modifiers: u8) {
|
||||
let changed = self.cur_modifiers ^ modifiers;
|
||||
for i in 0..7 {
|
||||
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 u16, modifiers & m != 0);
|
||||
self.send_key(*vk, modifiers & m != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cur_modifiers = modifiers;
|
||||
}
|
||||
fn send_key(&self, key: u16, down: bool) {
|
||||
fn send_key(&self, key: VirtualKey, down: bool) {
|
||||
log::debug!("send_key: {:?} {}", key, down);
|
||||
let time = get_time();
|
||||
let events = [
|
||||
new_event(time, EV_KEY, key - 8, down as _),
|
||||
new_event(time, EV_KEY, (key as u16) - 8, down as _),
|
||||
new_event(time, EV_SYN, 0, 0),
|
||||
];
|
||||
if let Err(res) = self.handle.write(&events) {
|
||||
@@ -209,22 +236,50 @@ impl HidProvider for UInputProvider {
|
||||
}
|
||||
}
|
||||
fn set_desktop_extent(&mut self, extent: Vec2) {
|
||||
log::info!("Desktop extent: {:?}", extent);
|
||||
self.desktop_extent = extent;
|
||||
}
|
||||
fn on_new_frame(&mut self) {
|
||||
self.mouse_moved = false;
|
||||
fn set_desktop_origin(&mut self, origin: Vec2) {
|
||||
self.desktop_origin = origin;
|
||||
}
|
||||
fn mouse_move(&mut self, pos: Vec2) {
|
||||
if self.current_action.pos.is_none() {
|
||||
self.current_action.pos = Some(pos);
|
||||
}
|
||||
self.current_action.last_requested_pos = Some(pos);
|
||||
}
|
||||
fn send_button(&mut self, button: u16, down: bool) {
|
||||
if self.current_action.button.is_none() {
|
||||
self.current_action.button = Some(MouseButtonAction { button, down });
|
||||
self.current_action.pos = self.current_action.last_requested_pos;
|
||||
}
|
||||
}
|
||||
fn wheel(&mut self, delta: i32) {
|
||||
if self.current_action.scroll.is_none() {
|
||||
self.current_action.scroll = Some(delta);
|
||||
}
|
||||
}
|
||||
fn commit(&mut self) {
|
||||
if let Some(pos) = self.current_action.pos.take() {
|
||||
self.mouse_move_internal(pos);
|
||||
}
|
||||
if let Some(button) = self.current_action.button.take() {
|
||||
self.send_button_internal(button.button, button.down);
|
||||
}
|
||||
if let Some(scroll) = self.current_action.scroll.take() {
|
||||
self.wheel_internal(scroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HidProvider for DummyProvider {
|
||||
fn mouse_move(&mut self, _pos: Vec2) {}
|
||||
fn send_button(&self, _button: u16, _down: bool) {}
|
||||
fn wheel(&self, _delta: i32) {}
|
||||
fn send_button(&mut self, _button: u16, _down: bool) {}
|
||||
fn wheel(&mut self, _delta: i32) {}
|
||||
fn set_modifiers(&mut self, _modifiers: u8) {}
|
||||
fn send_key(&self, _key: u16, _down: bool) {}
|
||||
fn send_key(&self, _key: VirtualKey, _down: bool) {}
|
||||
fn set_desktop_extent(&mut self, _extent: Vec2) {}
|
||||
fn on_new_frame(&mut self) {}
|
||||
fn set_desktop_origin(&mut self, _origin: Vec2) {}
|
||||
fn commit(&mut self) {}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -440,3 +495,104 @@ pub static MODS_TO_KEYS: Lazy<IdMap<KeyModifier, Vec<VirtualKey>>> = Lazy::new(|
|
||||
META => vec![VirtualKey::Meta],
|
||||
}
|
||||
});
|
||||
|
||||
pub enum KeyType {
|
||||
Symbol,
|
||||
NumPad,
|
||||
Other,
|
||||
}
|
||||
|
||||
macro_rules! key_between {
|
||||
($key:expr, $start:expr, $end:expr) => {
|
||||
$key as u32 >= $start as u32 && $key as u32 <= $end as u32
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! key_is {
|
||||
($key:expr, $val:expr) => {
|
||||
$key as u32 == $val as u32
|
||||
};
|
||||
}
|
||||
|
||||
pub 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)
|
||||
|| key_between!(key, VirtualKey::Oem5, VirtualKey::Oem2)
|
||||
|| key_is!(key, VirtualKey::Oem102)
|
||||
{
|
||||
KeyType::Symbol
|
||||
} else if key_between!(key, VirtualKey::KP_7, VirtualKey::KP_0)
|
||||
&& !key_is!(key, VirtualKey::KP_Subtract)
|
||||
&& !key_is!(key, VirtualKey::KP_Add)
|
||||
{
|
||||
KeyType::NumPad
|
||||
} else {
|
||||
KeyType::Other
|
||||
}
|
||||
}
|
||||
|
||||
pub struct XkbKeymap {
|
||||
pub context: xkb::Context,
|
||||
pub keymap: xkb::Keymap,
|
||||
}
|
||||
|
||||
impl XkbKeymap {
|
||||
pub fn label_for_key(&self, key: VirtualKey, modifier: KeyModifier) -> String {
|
||||
let mut state = xkb::State::new(&self.keymap);
|
||||
if modifier > 0 {
|
||||
if let Some(mod_key) = MODS_TO_KEYS.get(modifier) {
|
||||
state.update_key(
|
||||
xkb::Keycode::from(mod_key[0] as u32),
|
||||
xkb::KeyDirection::Down,
|
||||
);
|
||||
}
|
||||
}
|
||||
state.key_get_utf8(xkb::Keycode::from(key as u32))
|
||||
}
|
||||
|
||||
pub fn has_altgr(&self) -> bool {
|
||||
let state0 = xkb::State::new(&self.keymap);
|
||||
let mut state1 = xkb::State::new(&self.keymap);
|
||||
state1.update_key(
|
||||
xkb::Keycode::from(VirtualKey::Meta as u32),
|
||||
xkb::KeyDirection::Down,
|
||||
);
|
||||
|
||||
for key in [
|
||||
VirtualKey::N0,
|
||||
VirtualKey::N1,
|
||||
VirtualKey::N2,
|
||||
VirtualKey::N3,
|
||||
VirtualKey::N4,
|
||||
VirtualKey::N5,
|
||||
VirtualKey::N6,
|
||||
VirtualKey::N7,
|
||||
VirtualKey::N8,
|
||||
VirtualKey::N9,
|
||||
] {
|
||||
let sym0 = state0.key_get_one_sym(xkb::Keycode::from(key as u32));
|
||||
let sym1 = state1.key_get_one_sym(xkb::Keycode::from(key as u32));
|
||||
if sym0 != sym1 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub use wayland::get_keymap_wl;
|
||||
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
pub fn get_keymap_wl() -> anyhow::Result<XkbKeymap> {
|
||||
anyhow::bail!("Wayland support not enabled.")
|
||||
}
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
pub use x11::get_keymap_x11;
|
||||
|
||||
#[cfg(not(feature = "x11"))]
|
||||
pub fn get_keymap_x11() -> anyhow::Result<XkbKeymap> {
|
||||
anyhow::bail!("X11 support not enabled.")
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
use wlx_capture::wayland::wayland_client::{
|
||||
globals::{registry_queue_init, GlobalListContents},
|
||||
protocol::{
|
||||
wl_keyboard::{self, WlKeyboard},
|
||||
wl_registry::WlRegistry,
|
||||
wl_seat::{self, Capability, WlSeat},
|
||||
},
|
||||
Connection, Dispatch, Proxy, QueueHandle,
|
||||
};
|
||||
use xkbcommon::xkb;
|
||||
|
||||
use super::XkbKeymap;
|
||||
|
||||
struct WlKeymapHandler {
|
||||
seat: WlSeat,
|
||||
keyboard: Option<WlKeyboard>,
|
||||
keymap: Option<XkbKeymap>,
|
||||
}
|
||||
|
||||
impl Drop for WlKeymapHandler {
|
||||
fn drop(&mut self) {
|
||||
if let Some(keyboard) = &self.keyboard {
|
||||
keyboard.release();
|
||||
}
|
||||
self.seat.release();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_keymap_wl() -> anyhow::Result<XkbKeymap> {
|
||||
let connection = Connection::connect_to_env()?;
|
||||
let (globals, mut queue) = registry_queue_init::<WlKeymapHandler>(&connection)?;
|
||||
let qh = queue.handle();
|
||||
let seat: WlSeat = globals
|
||||
.bind(&qh, 4..=9, ())
|
||||
.expect(WlSeat::interface().name);
|
||||
|
||||
let mut me = WlKeymapHandler {
|
||||
seat,
|
||||
keyboard: None,
|
||||
keymap: None,
|
||||
};
|
||||
|
||||
// this gets us the wl_seat
|
||||
let _ = queue.blocking_dispatch(&mut me);
|
||||
|
||||
// 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"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlRegistry, GlobalListContents> for WlKeymapHandler {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &WlRegistry,
|
||||
_event: <WlRegistry as Proxy>::Event,
|
||||
_data: &GlobalListContents,
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlSeat, ()> for WlKeymapHandler {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
proxy: &WlSeat,
|
||||
event: <WlSeat as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_seat::Event::Capabilities { capabilities } => {
|
||||
let capability = capabilities
|
||||
.into_result()
|
||||
.unwrap_or(wl_seat::Capability::empty());
|
||||
if capability.contains(Capability::Keyboard) {
|
||||
state.keyboard = Some(proxy.get_keyboard(qhandle, ()));
|
||||
}
|
||||
}
|
||||
wl_seat::Event::Name { name } => {
|
||||
log::debug!("Using WlSeat: {}", name);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlKeyboard, ()> for WlKeymapHandler {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_proxy: &WlKeyboard,
|
||||
event: <WlKeyboard as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
wl_keyboard::Event::Keymap { format, fd, size } => {
|
||||
let format = format
|
||||
.into_result()
|
||||
.unwrap_or(wl_keyboard::KeymapFormat::NoKeymap);
|
||||
|
||||
if matches!(format, wl_keyboard::KeymapFormat::XkbV1) {
|
||||
let context = xkb::Context::new(xkb::CONTEXT_NO_DEFAULT_INCLUDES);
|
||||
let maybe_keymap = unsafe {
|
||||
xkb::Keymap::new_from_fd(
|
||||
&context,
|
||||
fd,
|
||||
size as _,
|
||||
xkb::KEYMAP_FORMAT_TEXT_V1,
|
||||
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
||||
)
|
||||
};
|
||||
|
||||
match maybe_keymap {
|
||||
Ok(Some(keymap)) => {
|
||||
state.keymap = Some(XkbKeymap { context, 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!("Default layout will be used.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::RepeatInfo { rate, delay } => {
|
||||
log::debug!("WlKeyboard RepeatInfo rate: {}, delay: {}", rate, delay);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use xkbcommon::xkb::{
|
||||
self,
|
||||
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;
|
||||
|
||||
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!(
|
||||
"get_core_keyboard_device_id returned -1. Check your XKB installation."
|
||||
));
|
||||
}
|
||||
let keymap = keymap_new_from_device(&context, &conn, device_id, xkb::KEYMAP_COMPILE_NO_FLAGS);
|
||||
|
||||
Ok(XkbKeymap { context, keymap })
|
||||
}
|
||||
+114
-14
@@ -9,16 +9,19 @@ mod overlays;
|
||||
mod shaders;
|
||||
mod state;
|
||||
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use flexi_logger::FileSpec;
|
||||
use flexi_logger::{Duplicate, FileSpec, LogSpecification};
|
||||
|
||||
/// 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")]
|
||||
@@ -35,28 +38,45 @@ struct Args {
|
||||
#[arg(long)]
|
||||
uninstall: bool,
|
||||
|
||||
/// Replace running WlxOverlay-S instance
|
||||
#[arg(long)]
|
||||
replace: bool,
|
||||
|
||||
/// Allow multiple running instances of WlxOverlay-S (things may break!)
|
||||
#[arg(long)]
|
||||
multi: bool,
|
||||
|
||||
/// Path to write logs to
|
||||
#[arg(short, long, value_name = "FOLDER")]
|
||||
#[arg(short, long, value_name = "FILE_PATH")]
|
||||
log_to: Option<String>,
|
||||
|
||||
#[cfg(feature = "uidev")]
|
||||
/// Show a desktop window of a UI panel for development
|
||||
#[arg(short, long, value_name = "UI_NAME")]
|
||||
uidev: Option<String>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
if let Some(ref log_to) = args.log_to {
|
||||
flexi_logger::Logger::try_with_env_or_str("info")?
|
||||
.log_to_file(FileSpec::default().directory(log_to))
|
||||
.log_to_stdout()
|
||||
.start()?;
|
||||
println!("Logging to: {}", &log_to);
|
||||
let mut args = if std::env::args().skip(1).any(|a| !a.is_empty()) {
|
||||
Args::parse()
|
||||
} else {
|
||||
flexi_logger::Logger::try_with_env_or_str("info")?.start()?;
|
||||
Args::default()
|
||||
};
|
||||
|
||||
if !args.multi && !ensure_single_instance(args.replace) {
|
||||
println!("Looks like WlxOverlay-S is already running.");
|
||||
println!("Use --replace and I will terminate it for you.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
logging_init(&mut args)?;
|
||||
|
||||
log::info!(
|
||||
"Welcome to {} version {}!",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
log::info!("It is {}.", chrono::Local::now().format("%c"));
|
||||
|
||||
#[cfg(feature = "openvr")]
|
||||
if args.uninstall {
|
||||
@@ -64,6 +84,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(feature = "uidev")]
|
||||
if let Some(panel_name) = args.uidev.as_ref() {
|
||||
crate::backend::uidev::uidev_run(panel_name.as_str())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let _ = ctrlc::set_handler({
|
||||
let running = running.clone();
|
||||
@@ -136,3 +162,77 @@ fn args_get_openxr(_args: &Args) -> bool {
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
fn logging_init(args: &mut Args) -> anyhow::Result<()> {
|
||||
let log_file = args
|
||||
.log_to
|
||||
.take()
|
||||
.or_else(|| std::env::var("WLX_LOGFILE").ok())
|
||||
.or_else(|| Some("/tmp/wlx.log".to_string()));
|
||||
|
||||
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()?;
|
||||
}
|
||||
} else {
|
||||
flexi_logger::Logger::try_with_env_or_str("info")?.start()?;
|
||||
}
|
||||
|
||||
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"));
|
||||
path.push("wlx-overlay-s.pid");
|
||||
|
||||
if path.exists() {
|
||||
// load contents
|
||||
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();
|
||||
if let Some(proc) = system.process(sysinfo::Pid::from_u32(pid)) {
|
||||
if replace {
|
||||
proc.kill_with(sysinfo::Signal::Term);
|
||||
proc.wait();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pid = std::process::id().to_string();
|
||||
std::fs::write(path, pid).unwrap();
|
||||
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use glam::Vec3A;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::backend::overlay::{OverlayData, OverlayState, RelativeTo};
|
||||
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 fn create_anchor<O>(state: &AppState) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let config = load_known_yaml::<ModularUiConfig>(ConfigType::Anchor);
|
||||
|
||||
Ok(OverlayData {
|
||||
state: OverlayState {
|
||||
name: ANCHOR_NAME.clone(),
|
||||
want_visible: false,
|
||||
interactable: false,
|
||||
grabbable: false,
|
||||
z_order: 67,
|
||||
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)?),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
+94
-20
@@ -10,8 +10,11 @@ use crate::{
|
||||
overlay::{OverlayData, OverlayState},
|
||||
},
|
||||
config::{self, ConfigType},
|
||||
gui::{color_parse, CanvasBuilder, Control},
|
||||
hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER},
|
||||
gui::{color_parse, CanvasBuilder, Control, KeyCapType},
|
||||
hid::{
|
||||
get_key_type, KeyModifier, KeyType, VirtualKey, XkbKeymap, ALT, CTRL, KEYS_TO_MODS, META,
|
||||
NUM_LOCK, SHIFT, SUPER,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
use glam::{vec2, vec3a, Affine2, Vec4};
|
||||
@@ -25,7 +28,10 @@ const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, META];
|
||||
|
||||
pub const KEYBOARD_NAME: &str = "kbd";
|
||||
|
||||
pub fn create_keyboard<O>(app: &AppState) -> anyhow::Result<OverlayData<O>>
|
||||
pub fn create_keyboard<O>(
|
||||
app: &AppState,
|
||||
keymap: Option<XkbKeymap>,
|
||||
) -> anyhow::Result<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
@@ -43,7 +49,7 @@ where
|
||||
size.x as _,
|
||||
size.y as _,
|
||||
app.graphics.clone(),
|
||||
app.format,
|
||||
app.graphics.native_format,
|
||||
data,
|
||||
)?;
|
||||
|
||||
@@ -53,6 +59,8 @@ where
|
||||
canvas.font_size = 18;
|
||||
canvas.bg_color = color_parse("#202020").unwrap(); //safe
|
||||
|
||||
let has_altgr = keymap.as_ref().map_or(false, |k| k.has_altgr());
|
||||
|
||||
let unit_size = size.x / LAYOUT.row_size;
|
||||
let h = unit_size - 2. * BUTTON_PADDING;
|
||||
|
||||
@@ -66,8 +74,43 @@ where
|
||||
let w = unit_size * my_size - 2. * BUTTON_PADDING;
|
||||
|
||||
if let Some(key) = LAYOUT.main_layout[row][col].as_ref() {
|
||||
let mut label = Vec::with_capacity(2);
|
||||
let mut maybe_state: Option<KeyButtonData> = None;
|
||||
let mut cap_type = KeyCapType::Regular;
|
||||
|
||||
if let Ok(vk) = VirtualKey::from_str(key) {
|
||||
if let Some(keymap) = keymap.as_ref() {
|
||||
match get_key_type(vk) {
|
||||
KeyType::Symbol => {
|
||||
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()) {
|
||||
label.push(label1);
|
||||
if has_altgr {
|
||||
cap_type = KeyCapType::RegularAltGr;
|
||||
label.push(keymap.label_for_key(vk, META));
|
||||
} else {
|
||||
cap_type = KeyCapType::Regular;
|
||||
}
|
||||
} else {
|
||||
label.push(label0);
|
||||
label.push(label1);
|
||||
if has_altgr {
|
||||
label.push(keymap.label_for_key(vk, META));
|
||||
cap_type = KeyCapType::ReversedAltGr;
|
||||
} else {
|
||||
cap_type = KeyCapType::Reversed;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyType::NumPad => {
|
||||
label.push(keymap.label_for_key(vk, NUM_LOCK));
|
||||
}
|
||||
KeyType::Other => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mods) = KEYS_TO_MODS.get(vk) {
|
||||
maybe_state = Some(KeyButtonData::Modifier {
|
||||
modifier: *mods,
|
||||
@@ -85,20 +128,22 @@ where
|
||||
log::error!("Keyboard: EXEC args empty for {}", key);
|
||||
continue;
|
||||
}
|
||||
let mut iter = exec_args.iter().cloned();
|
||||
maybe_state = Some(KeyButtonData::Exec {
|
||||
program: exec_args
|
||||
.first()
|
||||
.unwrap() // safe because we checked is_empty
|
||||
.clone(),
|
||||
args: exec_args.iter().skip(1).cloned().collect(),
|
||||
program: iter.next().unwrap(),
|
||||
args: iter.by_ref().take_while(|arg| arg[..] != *"null").collect(),
|
||||
release_program: iter.next(),
|
||||
release_args: iter.collect(),
|
||||
});
|
||||
} else {
|
||||
log::error!("Unknown key: {}", key);
|
||||
}
|
||||
|
||||
if let Some(state) = maybe_state {
|
||||
let label = LAYOUT.label_for_key(key);
|
||||
let button = canvas.key_button(x, y, w, h, &label);
|
||||
if label.is_empty() {
|
||||
label = LAYOUT.label_for_key(key);
|
||||
}
|
||||
let button = canvas.key_button(x, y, w, h, cap_type, &label);
|
||||
button.state = Some(state);
|
||||
button.on_press = Some(key_press);
|
||||
button.on_release = Some(key_release);
|
||||
@@ -122,9 +167,10 @@ where
|
||||
name: KEYBOARD_NAME.into(),
|
||||
grabbable: true,
|
||||
recenter: true,
|
||||
anchored: true,
|
||||
interactable: true,
|
||||
spawn_scale: width,
|
||||
spawn_point: vec3a(0., -0.5, -1.),
|
||||
spawn_point: vec3a(0., -0.5, 0.),
|
||||
interaction_transform,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -148,7 +194,7 @@ fn key_press(
|
||||
app.hid_provider.set_modifiers(data.modifiers);
|
||||
}
|
||||
|
||||
app.hid_provider.send_key(*vk as _, true);
|
||||
app.hid_provider.send_key(*vk, true);
|
||||
*pressed = true;
|
||||
}
|
||||
Some(KeyButtonData::Modifier { modifier, sticky }) => {
|
||||
@@ -160,10 +206,10 @@ fn key_press(
|
||||
Some(KeyButtonData::Macro { verbs }) => {
|
||||
data.key_click(app);
|
||||
for (vk, press) in verbs {
|
||||
app.hid_provider.send_key(*vk as _, *press);
|
||||
app.hid_provider.send_key(*vk, *press);
|
||||
}
|
||||
}
|
||||
Some(KeyButtonData::Exec { program, args }) => {
|
||||
Some(KeyButtonData::Exec { program, args, .. }) => {
|
||||
// Reap previous processes
|
||||
data.processes
|
||||
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
|
||||
@@ -184,7 +230,7 @@ fn key_release(
|
||||
) {
|
||||
match control.state.as_mut() {
|
||||
Some(KeyButtonData::Key { vk, pressed }) => {
|
||||
app.hid_provider.send_key(*vk as _, false);
|
||||
app.hid_provider.send_key(*vk, false);
|
||||
*pressed = false;
|
||||
|
||||
for m in AUTO_RELEASE_MODS.iter() {
|
||||
@@ -200,6 +246,21 @@ fn key_release(
|
||||
app.hid_provider.set_modifiers(data.modifiers);
|
||||
}
|
||||
}
|
||||
Some(KeyButtonData::Exec {
|
||||
release_program,
|
||||
release_args,
|
||||
..
|
||||
}) => {
|
||||
// Reap previous processes
|
||||
data.processes
|
||||
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
|
||||
|
||||
if let Some(program) = release_program {
|
||||
if let Ok(child) = Command::new(program).args(release_args).spawn() {
|
||||
data.processes.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -240,10 +301,23 @@ impl KeyboardData {
|
||||
}
|
||||
|
||||
enum KeyButtonData {
|
||||
Key { vk: VirtualKey, pressed: bool },
|
||||
Modifier { modifier: KeyModifier, sticky: bool },
|
||||
Macro { verbs: Vec<(VirtualKey, bool)> },
|
||||
Exec { program: String, args: Vec<String> },
|
||||
Key {
|
||||
vk: VirtualKey,
|
||||
pressed: bool,
|
||||
},
|
||||
Modifier {
|
||||
modifier: KeyModifier,
|
||||
sticky: bool,
|
||||
},
|
||||
Macro {
|
||||
verbs: Vec<(VirtualKey, bool)>,
|
||||
},
|
||||
Exec {
|
||||
program: String,
|
||||
args: Vec<String>,
|
||||
release_program: Option<String>,
|
||||
release_args: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
static LAYOUT: Lazy<Layout> = Lazy::new(Layout::load_from_disk);
|
||||
|
||||
@@ -9,10 +9,11 @@ use wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireSel
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
common::{OverlaySelector, TaskType},
|
||||
common::OverlaySelector,
|
||||
overlay::{
|
||||
ui_transform, OverlayBackend, OverlayRenderer, OverlayState, SplitOverlayBackend,
|
||||
},
|
||||
task::TaskType,
|
||||
},
|
||||
state::{AppSession, AppState},
|
||||
};
|
||||
@@ -30,7 +31,7 @@ pub struct MirrorRenderer {
|
||||
}
|
||||
impl MirrorRenderer {
|
||||
pub fn new(name: Arc<str>) -> Self {
|
||||
let selector = Box::pin(pipewire_select_screen(None, false, false, false));
|
||||
let selector = Box::pin(pipewire_select_screen(None, false, false, false, false));
|
||||
Self {
|
||||
name,
|
||||
renderer: None,
|
||||
@@ -57,12 +58,9 @@ impl OverlayRenderer for MirrorRenderer {
|
||||
};
|
||||
|
||||
if let Ok(pw_result) = maybe_pw_result {
|
||||
log::info!(
|
||||
"{}: PipeWire node selected: {}",
|
||||
self.name.clone(),
|
||||
pw_result.node_id
|
||||
);
|
||||
let capture = PipewireCapture::new(self.name.clone(), pw_result.node_id, 60);
|
||||
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),
|
||||
@@ -132,7 +130,7 @@ pub fn new_mirror(
|
||||
show_hide,
|
||||
want_visible: true,
|
||||
spawn_scale: 0.5 * session.config.desktop_view_scale,
|
||||
spawn_point: vec3a(0., 0.5, -0.5),
|
||||
spawn_point: vec3a(0., 0.5, 0.5),
|
||||
..Default::default()
|
||||
};
|
||||
let backend = Box::new(SplitOverlayBackend {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod anchor;
|
||||
pub mod custom;
|
||||
pub mod keyboard;
|
||||
#[cfg(feature = "wayland")]
|
||||
|
||||
+383
-225
@@ -1,9 +1,11 @@
|
||||
use core::slice;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
f32::consts::PI,
|
||||
ops::Add,
|
||||
ptr,
|
||||
sync::Arc,
|
||||
sync::{atomic::AtomicU64, Arc},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use vulkano::{
|
||||
@@ -13,19 +15,24 @@ use vulkano::{
|
||||
};
|
||||
use wlx_capture::{
|
||||
frame::{
|
||||
DrmFormat, MouseMeta, WlxFrame, DRM_FORMAT_ABGR8888, DRM_FORMAT_ARGB8888,
|
||||
DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888,
|
||||
DrmFormat, MouseMeta, WlxFrame, DRM_FORMAT_ABGR2101010, DRM_FORMAT_ABGR8888,
|
||||
DRM_FORMAT_ARGB8888, DRM_FORMAT_XBGR2101010, DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888,
|
||||
},
|
||||
WlxCapture,
|
||||
};
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
#[cfg(feature = "pipewire")]
|
||||
use {
|
||||
crate::config_io,
|
||||
glam::Vec3,
|
||||
std::{collections::HashMap, error::Error, f32::consts::PI, ops::Deref, path::PathBuf},
|
||||
std::error::Error,
|
||||
std::{ops::Deref, path::PathBuf},
|
||||
wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireStream},
|
||||
};
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
use {
|
||||
crate::config::AStrMapExt,
|
||||
wlx_capture::{
|
||||
pipewire::{pipewire_select_screen, PipewireCapture},
|
||||
wayland::{wayland_client::protocol::wl_output, WlxClient, WlxOutput},
|
||||
wlr_dmabuf::WlrDmabufCapture,
|
||||
wlr_screencopy::WlrScreencopyCapture,
|
||||
@@ -35,26 +42,47 @@ use {
|
||||
#[cfg(feature = "x11")]
|
||||
use wlx_capture::xshm::{XshmCapture, XshmScreen};
|
||||
|
||||
use glam::{vec2, vec3a, Affine2, Quat, Vec2};
|
||||
use glam::{vec2, vec3a, Affine2, Quat, Vec2, Vec3};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
input::{Haptics, InteractionHandler, PointerHit, PointerMode},
|
||||
overlay::{OverlayData, OverlayRenderer, OverlayState, SplitOverlayBackend},
|
||||
overlay::{OverlayRenderer, OverlayState, SplitOverlayBackend},
|
||||
},
|
||||
config::{def_pw_tokens, PwTokenMap},
|
||||
graphics::{
|
||||
fourcc_to_vk, WlxCommandBuffer, WlxPipeline, WlxPipelineLegacy, DRM_FORMAT_MOD_INVALID,
|
||||
},
|
||||
config::def_pw_tokens,
|
||||
graphics::{fourcc_to_vk, WlxCommandBuffer, WlxPipeline, WlxPipelineLegacy},
|
||||
hid::{MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT},
|
||||
state::{AppSession, AppState},
|
||||
state::{AppSession, AppState, ScreenMeta},
|
||||
};
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub(crate) type WlxClientAlias = wlx_capture::wayland::WlxClient;
|
||||
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
pub(crate) type WlxClientAlias = ();
|
||||
|
||||
const CURSOR_SIZE: f32 = 16. / 1440.;
|
||||
|
||||
static DRM_FORMATS: once_cell::sync::OnceCell<Vec<DrmFormat>> = once_cell::sync::OnceCell::new();
|
||||
|
||||
static START: Lazy<Instant> = Lazy::new(Instant::now);
|
||||
static NEXT_MOVE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn can_move() -> bool {
|
||||
START.elapsed().as_millis() as u64 > NEXT_MOVE.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn set_next_move(millis_from_now: u64) {
|
||||
NEXT_MOVE.store(
|
||||
START.elapsed().as_millis() as u64 + millis_from_now,
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
|
||||
pub struct ScreenInteractionHandler {
|
||||
next_scroll: Instant,
|
||||
next_move: Instant,
|
||||
mouse_transform: Affine2,
|
||||
}
|
||||
impl ScreenInteractionHandler {
|
||||
@@ -80,7 +108,6 @@ impl ScreenInteractionHandler {
|
||||
|
||||
ScreenInteractionHandler {
|
||||
next_scroll: Instant::now(),
|
||||
next_move: Instant::now(),
|
||||
mouse_transform: transform,
|
||||
}
|
||||
}
|
||||
@@ -90,7 +117,10 @@ impl InteractionHandler for ScreenInteractionHandler {
|
||||
fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!("Hover: {:?}", hit.uv);
|
||||
if self.next_move < Instant::now() {
|
||||
if can_move()
|
||||
&& (!app.session.config.focus_follows_mouse_mode
|
||||
|| app.input_state.pointers[hit.pointer].now.move_mouse)
|
||||
{
|
||||
let pos = self.mouse_transform.transform_point2(hit.uv);
|
||||
app.hid_provider.mouse_move(pos);
|
||||
}
|
||||
@@ -104,12 +134,14 @@ impl InteractionHandler for ScreenInteractionHandler {
|
||||
};
|
||||
|
||||
if pressed {
|
||||
self.next_move = Instant::now()
|
||||
+ Duration::from_millis(app.session.config.click_freeze_time_ms as u64);
|
||||
set_next_move(app.session.config.click_freeze_time_ms as u64);
|
||||
}
|
||||
|
||||
app.hid_provider.send_button(btn, pressed);
|
||||
|
||||
if !pressed {
|
||||
return;
|
||||
}
|
||||
let pos = self.mouse_transform.transform_point2(hit.uv);
|
||||
app.hid_provider.mouse_move(pos);
|
||||
}
|
||||
@@ -117,8 +149,8 @@ impl InteractionHandler for ScreenInteractionHandler {
|
||||
if self.next_scroll > Instant::now() {
|
||||
return;
|
||||
}
|
||||
let max_millis = if matches!(hit.mode, PointerMode::Right) {
|
||||
50.0
|
||||
let max_millis = if matches!(hit.mode, PointerMode::Left) {
|
||||
200.0
|
||||
} else {
|
||||
100.0
|
||||
};
|
||||
@@ -130,6 +162,7 @@ impl InteractionHandler for ScreenInteractionHandler {
|
||||
fn on_left(&mut self, _app: &mut AppState, _hand: usize) {}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ScreenPipeline {
|
||||
view: Arc<ImageView>,
|
||||
mouse: Option<Arc<ImageView>>,
|
||||
@@ -299,10 +332,25 @@ impl ScreenRenderer {
|
||||
)> {
|
||||
let name = output.name.clone();
|
||||
let embed_mouse = !session.config.double_cursor_fix;
|
||||
let select_screen_result =
|
||||
futures::executor::block_on(pipewire_select_screen(token, embed_mouse, true, true))?;
|
||||
log::info!(
|
||||
"On screen share prompt, pick: {} {} {} (pos {}, {})",
|
||||
&output.name,
|
||||
&output.make,
|
||||
&output.model,
|
||||
&output.logical_pos.0,
|
||||
&output.logical_pos.1,
|
||||
);
|
||||
let select_screen_result = futures::executor::block_on(pipewire_select_screen(
|
||||
token,
|
||||
embed_mouse,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
))?;
|
||||
|
||||
let capture = PipewireCapture::new(name, select_screen_result.node_id, 60);
|
||||
let node_id = select_screen_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
|
||||
|
||||
let capture = PipewireCapture::new(name, node_id);
|
||||
|
||||
Ok((
|
||||
ScreenRenderer {
|
||||
@@ -336,10 +384,18 @@ impl OverlayRenderer for ScreenRenderer {
|
||||
}
|
||||
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
if !self.capture.is_ready() {
|
||||
let supports_dmabuf = self.capture.supports_dmbuf();
|
||||
let supports_dmabuf = app
|
||||
.graphics
|
||||
.device
|
||||
.enabled_extensions()
|
||||
.ext_external_memory_dma_buf
|
||||
&& self.capture.supports_dmbuf();
|
||||
|
||||
let allow_dmabuf = &*app.session.config.capture_method != "pw_fallback"
|
||||
&& &*app.session.config.capture_method != "screencopy";
|
||||
|
||||
let capture_method = app.session.config.capture_method.clone();
|
||||
|
||||
let drm_formats = DRM_FORMATS.get_or_init({
|
||||
let graphics = app.graphics.clone();
|
||||
move || {
|
||||
@@ -348,7 +404,7 @@ impl OverlayRenderer for ScreenRenderer {
|
||||
return vec![];
|
||||
}
|
||||
if !allow_dmabuf {
|
||||
log::info!("Not using DMA-buf capture due to pw_fallback");
|
||||
log::info!("Not using DMA-buf capture due to {}", capture_method);
|
||||
return vec![];
|
||||
}
|
||||
log::warn!("Using DMA-buf capture. If screens are blank for you, switch to SHM using:");
|
||||
@@ -359,7 +415,10 @@ impl OverlayRenderer for ScreenRenderer {
|
||||
DRM_FORMAT_XBGR8888.into(),
|
||||
DRM_FORMAT_ARGB8888.into(),
|
||||
DRM_FORMAT_XRGB8888.into(),
|
||||
DRM_FORMAT_ABGR2101010.into(),
|
||||
DRM_FORMAT_XBGR2101010.into(),
|
||||
];
|
||||
|
||||
let mut final_formats = vec![];
|
||||
|
||||
for &f in &possible_formats {
|
||||
@@ -370,7 +429,7 @@ impl OverlayRenderer for ScreenRenderer {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
final_formats.push(DrmFormat {
|
||||
let mut fmt = DrmFormat {
|
||||
fourcc: f,
|
||||
modifiers: props
|
||||
.drm_format_modifier_properties
|
||||
@@ -379,7 +438,13 @@ impl OverlayRenderer for ScreenRenderer {
|
||||
.filter(|m| m.drm_format_modifier_plane_count == 1)
|
||||
.map(|m| m.drm_format_modifier)
|
||||
.collect(),
|
||||
})
|
||||
};
|
||||
fmt.modifiers.push(DRM_FORMAT_MOD_INVALID); // implicit modifiers support
|
||||
final_formats.push(fmt);
|
||||
}
|
||||
log::debug!("Supported DRM formats:");
|
||||
for f in &final_formats {
|
||||
log::debug!(" {} {:?}", f.fourcc, f.modifiers);
|
||||
}
|
||||
final_formats
|
||||
}
|
||||
@@ -410,6 +475,7 @@ impl OverlayRenderer for ScreenRenderer {
|
||||
);
|
||||
}
|
||||
}
|
||||
self.capture.request_new_frame();
|
||||
}
|
||||
WlxFrame::MemFd(frame) => {
|
||||
let mut upload = app
|
||||
@@ -500,36 +566,22 @@ impl OverlayRenderer for ScreenRenderer {
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
/// Panics if id is not a valid output id
|
||||
fn try_create_screen<O>(
|
||||
wl: &WlxClient,
|
||||
id: u32,
|
||||
pw_token_store: &mut HashMap<String, String>,
|
||||
pub fn create_screen_renderer_wl(
|
||||
output: &WlxOutput,
|
||||
has_wlr_dmabuf: bool,
|
||||
has_wlr_screencopy: bool,
|
||||
pw_token_store: &mut PwTokenMap,
|
||||
session: &AppSession,
|
||||
) -> Option<OverlayData<O>>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let output = &wl.outputs.get(id).unwrap(); // safe due to contract
|
||||
log::info!(
|
||||
"{}: Res {}x{} Size {:?} Pos {:?}",
|
||||
output.name,
|
||||
output.size.0,
|
||||
output.size.1,
|
||||
output.logical_size,
|
||||
output.logical_pos,
|
||||
);
|
||||
|
||||
) -> Option<ScreenRenderer> {
|
||||
let mut capture: Option<ScreenRenderer> = None;
|
||||
|
||||
if (&*session.config.capture_method == "auto" || &*session.config.capture_method == "dmabuf")
|
||||
&& wl.maybe_wlr_dmabuf_mgr.is_some()
|
||||
&& has_wlr_dmabuf
|
||||
{
|
||||
log::info!("{}: Using Wlr DMA-Buf", &output.name);
|
||||
capture = ScreenRenderer::new_wlr_dmabuf(output);
|
||||
}
|
||||
|
||||
if &*session.config.capture_method == "screencopy" && wl.maybe_wlr_screencopy_mgr.is_some() {
|
||||
if &*session.config.capture_method == "screencopy" && has_wlr_screencopy {
|
||||
log::info!("{}: Using Wlr Screencopy Wl-SHM", &output.name);
|
||||
capture = ScreenRenderer::new_wlr_screencopy(output);
|
||||
}
|
||||
@@ -540,7 +592,7 @@ where
|
||||
let display_name = output.name.deref();
|
||||
|
||||
// Find existing token by display
|
||||
let token = pw_token_store.get(display_name).map(|s| s.as_str());
|
||||
let token = pw_token_store.arc_get(display_name).map(|s| s.as_str());
|
||||
|
||||
if let Some(t) = token {
|
||||
log::info!(
|
||||
@@ -555,10 +607,7 @@ where
|
||||
capture = Some(renderer);
|
||||
|
||||
if let Some(token) = restore_token {
|
||||
if pw_token_store
|
||||
.insert(String::from(display_name), token.clone())
|
||||
.is_none()
|
||||
{
|
||||
if pw_token_store.arc_set(display_name.into(), token.clone()) {
|
||||
log::info!("Adding Pipewire token {}", token);
|
||||
}
|
||||
}
|
||||
@@ -572,191 +621,283 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(capture) = capture {
|
||||
let backend = Box::new(SplitOverlayBackend {
|
||||
renderer: Box::new(capture),
|
||||
interaction: Box::new(ScreenInteractionHandler::new(
|
||||
vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32),
|
||||
vec2(output.logical_size.0 as f32, output.logical_size.1 as f32),
|
||||
output.transform.into(),
|
||||
)),
|
||||
});
|
||||
capture
|
||||
}
|
||||
|
||||
let axis = Vec3::new(0., 0., 1.);
|
||||
pub fn create_screen_interaction(
|
||||
logical_pos: Vec2,
|
||||
logical_size: Vec2,
|
||||
transform: Transform,
|
||||
) -> ScreenInteractionHandler {
|
||||
ScreenInteractionHandler::new(logical_pos, logical_size, transform)
|
||||
}
|
||||
|
||||
let transform = output.transform.into();
|
||||
|
||||
let angle = if session.config.upright_screen_fix {
|
||||
match transform {
|
||||
Transform::_90 | Transform::Flipped90 => PI / 2.,
|
||||
Transform::_180 | Transform::Flipped180 => PI,
|
||||
Transform::_270 | Transform::Flipped270 => -PI / 2.,
|
||||
_ => 0.,
|
||||
}
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
let center = Vec2 { x: 0.5, y: 0.5 };
|
||||
let interaction_transform = match transform {
|
||||
Transform::_90 | Transform::Flipped90 => Affine2::from_cols(
|
||||
Vec2::NEG_Y * (output.size.0 as f32 / output.size.1 as f32),
|
||||
Vec2::NEG_X,
|
||||
center,
|
||||
),
|
||||
Transform::_180 | Transform::Flipped180 => Affine2::from_cols(
|
||||
Vec2::NEG_X,
|
||||
Vec2::NEG_Y * (-output.size.0 as f32 / output.size.1 as f32),
|
||||
center,
|
||||
),
|
||||
Transform::_270 | Transform::Flipped270 => Affine2::from_cols(
|
||||
Vec2::Y * (output.size.0 as f32 / output.size.1 as f32),
|
||||
Vec2::X,
|
||||
center,
|
||||
),
|
||||
_ => Affine2::from_cols(
|
||||
Vec2::X,
|
||||
Vec2::Y * (-output.size.0 as f32 / output.size.1 as f32),
|
||||
center,
|
||||
),
|
||||
};
|
||||
|
||||
Some(OverlayData {
|
||||
state: OverlayState {
|
||||
name: output.name.clone(),
|
||||
show_hide: session
|
||||
.config
|
||||
.show_screens
|
||||
.iter()
|
||||
.any(|s| s.as_ref() == output.name.as_ref()),
|
||||
grabbable: true,
|
||||
recenter: true,
|
||||
interactable: true,
|
||||
spawn_scale: 1.5 * session.config.desktop_view_scale,
|
||||
spawn_point: vec3a(0., 0.5, -1.),
|
||||
spawn_rotation: Quat::from_axis_angle(axis, angle),
|
||||
interaction_transform,
|
||||
..Default::default()
|
||||
},
|
||||
backend,
|
||||
..Default::default()
|
||||
})
|
||||
fn create_screen_state(
|
||||
name: Arc<str>,
|
||||
res: (i32, i32),
|
||||
transform: Transform,
|
||||
session: &AppSession,
|
||||
) -> OverlayState {
|
||||
let angle = if session.config.upright_screen_fix {
|
||||
match transform {
|
||||
Transform::_90 | Transform::Flipped90 => PI / 2.,
|
||||
Transform::_180 | Transform::Flipped180 => PI,
|
||||
Transform::_270 | Transform::Flipped270 => -PI / 2.,
|
||||
_ => 0.,
|
||||
}
|
||||
} else {
|
||||
log::warn!("{}: Will not be used", &output.name);
|
||||
None
|
||||
0.
|
||||
};
|
||||
|
||||
let center = Vec2 { x: 0.5, y: 0.5 };
|
||||
let interaction_transform = match transform {
|
||||
Transform::_90 | Transform::Flipped90 => Affine2::from_cols(
|
||||
Vec2::NEG_Y * (res.0 as f32 / res.1 as f32),
|
||||
Vec2::NEG_X,
|
||||
center,
|
||||
),
|
||||
Transform::_180 | Transform::Flipped180 => Affine2::from_cols(
|
||||
Vec2::NEG_X,
|
||||
Vec2::NEG_Y * (-res.0 as f32 / res.1 as f32),
|
||||
center,
|
||||
),
|
||||
Transform::_270 | Transform::Flipped270 => {
|
||||
Affine2::from_cols(Vec2::Y * (res.0 as f32 / res.1 as f32), Vec2::X, center)
|
||||
}
|
||||
_ => Affine2::from_cols(Vec2::X, Vec2::Y * (-res.0 as f32 / res.1 as f32), center),
|
||||
};
|
||||
|
||||
OverlayState {
|
||||
name: name.clone(),
|
||||
grabbable: true,
|
||||
recenter: true,
|
||||
anchored: true,
|
||||
interactable: true,
|
||||
spawn_scale: 1.5 * session.config.desktop_view_scale,
|
||||
spawn_point: vec3a(0., 0.5, 0.),
|
||||
spawn_rotation: Quat::from_axis_angle(Vec3::Z, angle),
|
||||
interaction_transform,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Default)]
|
||||
pub struct TokenConf {
|
||||
#[serde(default = "def_pw_tokens")]
|
||||
pub pw_tokens: Vec<(String, String)>,
|
||||
pub pw_tokens: PwTokenMap,
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
#[cfg(feature = "pipewire")]
|
||||
fn get_pw_token_path() -> PathBuf {
|
||||
let mut path = config_io::get_conf_d_path();
|
||||
path.push("pw_tokens.yaml");
|
||||
path
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub fn save_pw_token_config(tokens: &HashMap<String, String>) -> Result<(), Box<dyn Error>> {
|
||||
let mut conf = TokenConf::default();
|
||||
|
||||
for (name, token) in tokens {
|
||||
conf.pw_tokens.push((name.clone(), token.clone()));
|
||||
}
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
pub fn save_pw_token_config(tokens: PwTokenMap) -> Result<(), Box<dyn Error>> {
|
||||
let conf = TokenConf { pw_tokens: tokens };
|
||||
let yaml = serde_yaml::to_string(&conf)?;
|
||||
std::fs::write(get_pw_token_path(), yaml)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub fn load_pw_token_config() -> Result<HashMap<String, String>, Box<dyn Error>> {
|
||||
let mut map: HashMap<String, String> = HashMap::new();
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
pub fn load_pw_token_config() -> Result<PwTokenMap, Box<dyn Error>> {
|
||||
let yaml = std::fs::read_to_string(get_pw_token_path())?;
|
||||
let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?;
|
||||
Ok(conf.pw_tokens)
|
||||
}
|
||||
|
||||
for (name, token) in conf.pw_tokens {
|
||||
map.insert(name, token);
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
pub(crate) struct ScreenCreateData {
|
||||
pub screens: Vec<(ScreenMeta, OverlayState, Box<SplitOverlayBackend>)>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
pub fn get_screens_wayland<O>(_session: &AppSession) -> anyhow::Result<(Vec<OverlayData<O>>, Vec2)>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
pub fn create_screens_wayland(
|
||||
_wl: &mut WlxClientAlias,
|
||||
_app: &AppState,
|
||||
) -> anyhow::Result<ScreenCreateData> {
|
||||
anyhow::bail!("Wayland support not enabled")
|
||||
}
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub fn get_screens_wayland<O>(session: &AppSession) -> anyhow::Result<(Vec<OverlayData<O>>, Vec2)>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
let mut overlays = vec![];
|
||||
let mut wl = WlxClient::new().ok_or_else(|| anyhow::anyhow!("Failed to connect to Wayland"))?;
|
||||
pub fn create_screens_wayland(
|
||||
wl: &mut WlxClientAlias,
|
||||
app: &mut AppState,
|
||||
) -> anyhow::Result<ScreenCreateData> {
|
||||
use crate::config::AStrMap;
|
||||
|
||||
let mut screens = vec![];
|
||||
|
||||
// Load existing Pipewire tokens from file
|
||||
let mut pw_tokens: HashMap<String, String> = if let Ok(conf) = load_pw_token_config() {
|
||||
let mut pw_tokens: PwTokenMap = if let Ok(conf) = load_pw_token_config() {
|
||||
conf
|
||||
} else {
|
||||
HashMap::new()
|
||||
AStrMap::new()
|
||||
};
|
||||
|
||||
let pw_tokens_copy = pw_tokens.clone();
|
||||
let has_wlr_dmabuf = wl.maybe_wlr_dmabuf_mgr.is_some();
|
||||
let has_wlr_screencopy = wl.maybe_wlr_screencopy_mgr.is_some();
|
||||
|
||||
let mut origin = (i32::MAX, i32::MAX);
|
||||
for output in wl.outputs.values() {
|
||||
origin.0 = origin.0.min(output.logical_pos.0);
|
||||
origin.1 = origin.1.min(output.logical_pos.1);
|
||||
}
|
||||
for (id, output) in wl.outputs.iter() {
|
||||
if app.screens.iter().any(|s| s.name == output.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::info!("Desktop origin: {:?}", origin);
|
||||
log::info!(
|
||||
"{}: Init screen of res {:?}, logical {:?} at {:?}",
|
||||
output.name,
|
||||
output.size,
|
||||
output.logical_size,
|
||||
output.logical_pos,
|
||||
);
|
||||
|
||||
// adjust all outputs so that the top-left corner is at (0, 0)
|
||||
for output in wl.outputs.values_mut() {
|
||||
output.logical_pos.0 -= origin.0;
|
||||
output.logical_pos.1 -= origin.1;
|
||||
}
|
||||
if let Some(renderer) = create_screen_renderer_wl(
|
||||
output,
|
||||
has_wlr_dmabuf,
|
||||
has_wlr_screencopy,
|
||||
&mut pw_tokens,
|
||||
&app.session,
|
||||
) {
|
||||
let logical_pos = vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32);
|
||||
let logical_size = vec2(output.logical_size.0 as f32, output.logical_size.1 as f32);
|
||||
let transform = output.transform.into();
|
||||
let interaction = create_screen_interaction(logical_pos, logical_size, transform);
|
||||
let state =
|
||||
create_screen_state(output.name.clone(), output.size, transform, &app.session);
|
||||
|
||||
for id in wl.outputs.keys() {
|
||||
if let Some(overlay) = try_create_screen(&wl, *id, &mut pw_tokens, session) {
|
||||
overlays.push(overlay);
|
||||
let meta = ScreenMeta {
|
||||
name: wl.outputs[id].name.clone(),
|
||||
id: state.id,
|
||||
native_handle: *id,
|
||||
};
|
||||
|
||||
let backend = Box::new(SplitOverlayBackend {
|
||||
renderer: Box::new(renderer),
|
||||
interaction: Box::new(interaction),
|
||||
});
|
||||
screens.push((meta, state, backend));
|
||||
}
|
||||
}
|
||||
|
||||
if pw_tokens_copy != pw_tokens {
|
||||
// Token list changed, re-create token config file
|
||||
if let Err(err) = save_pw_token_config(&pw_tokens) {
|
||||
if let Err(err) = save_pw_token_config(pw_tokens) {
|
||||
log::error!("Failed to save Pipewire token config: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let extent = wl.get_desktop_extent();
|
||||
Ok((overlays, Vec2::new(extent.0 as f32, extent.1 as f32)))
|
||||
let origin = wl.get_desktop_origin();
|
||||
|
||||
app.hid_provider
|
||||
.set_desktop_extent(vec2(extent.0 as f32, extent.1 as f32));
|
||||
app.hid_provider
|
||||
.set_desktop_origin(vec2(origin.0 as f32, origin.1 as f32));
|
||||
|
||||
Ok(ScreenCreateData { screens })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "x11"))]
|
||||
pub fn get_screens_x11<O>(_session: &AppSession) -> anyhow::Result<(Vec<OverlayData<O>>, Vec2)>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
pub fn create_screens_xshm(_app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
||||
anyhow::bail!("X11 support not enabled")
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "x11", feature = "pipewire")))]
|
||||
pub fn create_screens_x11pw(_app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
||||
anyhow::bail!("Pipewire support not enabled")
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "x11", feature = "pipewire"))]
|
||||
pub fn create_screens_x11pw(app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
||||
use crate::config::{AStrMap, AStrMapExt};
|
||||
use anyhow::bail;
|
||||
|
||||
// Load existing Pipewire tokens from file
|
||||
let mut pw_tokens: PwTokenMap = if let Ok(conf) = load_pw_token_config() {
|
||||
conf
|
||||
} else {
|
||||
AStrMap::new()
|
||||
};
|
||||
let pw_tokens_copy = pw_tokens.clone();
|
||||
let token = pw_tokens.arc_get("x11").map(|s| s.as_str());
|
||||
let embed_mouse = !app.session.config.double_cursor_fix;
|
||||
let select_screen_result =
|
||||
futures::executor::block_on(pipewire_select_screen(token, embed_mouse, true, true, true))?;
|
||||
if let Some(restore_token) = select_screen_result.restore_token {
|
||||
if pw_tokens.arc_set("x11".into(), restore_token.clone()) {
|
||||
log::info!("Adding Pipewire token {}", restore_token);
|
||||
}
|
||||
}
|
||||
if pw_tokens_copy != pw_tokens {
|
||||
// Token list changed, re-create token config file
|
||||
if let Err(err) = save_pw_token_config(pw_tokens) {
|
||||
log::error!("Failed to save Pipewire token config: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let monitors = match XshmCapture::get_monitors() {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
bail!(e.to_string());
|
||||
}
|
||||
};
|
||||
log::info!("Got {} monitors", monitors.len());
|
||||
log::info!("Got {} streams", select_screen_result.streams.len());
|
||||
|
||||
let mut extent = vec2(0., 0.);
|
||||
let screens = select_screen_result
|
||||
.streams
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, s)| {
|
||||
let m = best_match(&s, monitors.iter().map(AsRef::as_ref)).unwrap();
|
||||
log::info!("Stream {i} is {}", m.name);
|
||||
extent.x = extent.x.max((m.monitor.x() + m.monitor.width()) as f32);
|
||||
extent.y = extent.y.max((m.monitor.y() + m.monitor.height()) as f32);
|
||||
|
||||
let size = (m.monitor.width(), m.monitor.height());
|
||||
let interaction = create_screen_interaction(
|
||||
vec2(m.monitor.x() as f32, m.monitor.y() as f32),
|
||||
vec2(m.monitor.width() as f32, m.monitor.height() as f32),
|
||||
Transform::Normal,
|
||||
);
|
||||
|
||||
let state = create_screen_state(m.name.clone(), size, Transform::Normal, &app.session);
|
||||
|
||||
let meta = ScreenMeta {
|
||||
name: m.name.clone(),
|
||||
id: state.id,
|
||||
native_handle: 0,
|
||||
};
|
||||
|
||||
let renderer = ScreenRenderer {
|
||||
name: m.name.clone(),
|
||||
capture: Box::new(PipewireCapture::new(m.name.clone(), s.node_id)),
|
||||
pipeline: None,
|
||||
last_view: None,
|
||||
extent: extent_from_res(size),
|
||||
};
|
||||
|
||||
let backend = Box::new(SplitOverlayBackend {
|
||||
renderer: Box::new(renderer),
|
||||
interaction: Box::new(interaction),
|
||||
});
|
||||
(meta, state, backend)
|
||||
})
|
||||
.collect();
|
||||
|
||||
app.hid_provider.set_desktop_extent(extent);
|
||||
app.hid_provider.set_desktop_origin(vec2(0.0, 0.0));
|
||||
|
||||
Ok(ScreenCreateData { screens })
|
||||
}
|
||||
|
||||
#[cfg(feature = "x11")]
|
||||
pub fn get_screens_x11<O>(session: &AppSession) -> anyhow::Result<(Vec<OverlayData<O>>, Vec2)>
|
||||
where
|
||||
O: Default,
|
||||
{
|
||||
pub fn create_screens_xshm(app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
|
||||
use anyhow::bail;
|
||||
|
||||
let mut extent = vec2(0., 0.);
|
||||
@@ -768,64 +909,53 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
let overlays = monitors
|
||||
let screens = monitors
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
log::info!(
|
||||
"{}: Res {}x{}, Pos {}x{}",
|
||||
s.name,
|
||||
s.monitor.width(),
|
||||
s.monitor.height(),
|
||||
s.monitor.x(),
|
||||
s.monitor.y()
|
||||
);
|
||||
let size = (s.monitor.width(), s.monitor.height());
|
||||
let capture: ScreenRenderer = ScreenRenderer::new_xshm(s.clone());
|
||||
|
||||
let backend = Box::new(SplitOverlayBackend {
|
||||
renderer: Box::new(capture),
|
||||
interaction: Box::new(ScreenInteractionHandler::new(
|
||||
vec2(s.monitor.x() as f32, s.monitor.y() as f32),
|
||||
vec2(s.monitor.width() as f32, s.monitor.height() as f32),
|
||||
Transform::Normal,
|
||||
)),
|
||||
});
|
||||
|
||||
let interaction_transform = Affine2::from_translation(Vec2 { x: 0.5, y: 0.5 })
|
||||
* Affine2::from_scale(Vec2 {
|
||||
x: 1.,
|
||||
y: -size.0 as f32 / size.1 as f32,
|
||||
});
|
||||
|
||||
extent.x = extent.x.max((s.monitor.x() + s.monitor.width()) as f32);
|
||||
extent.y = extent.y.max((s.monitor.y() + s.monitor.height()) as f32);
|
||||
OverlayData {
|
||||
state: OverlayState {
|
||||
name: s.name.clone(),
|
||||
show_hide: session
|
||||
.config
|
||||
.show_screens
|
||||
.iter()
|
||||
.any(|x| x.as_ref() == s.name.as_ref()),
|
||||
grabbable: true,
|
||||
recenter: true,
|
||||
interactable: true,
|
||||
spawn_scale: 1.5 * session.config.desktop_view_scale,
|
||||
spawn_point: vec3a(0., 0.5, -1.),
|
||||
spawn_rotation: Quat::IDENTITY,
|
||||
interaction_transform,
|
||||
..Default::default()
|
||||
},
|
||||
backend,
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
let size = (s.monitor.width(), s.monitor.height());
|
||||
let pos = (s.monitor.x(), s.monitor.y());
|
||||
let renderer = ScreenRenderer::new_xshm(s.clone());
|
||||
|
||||
log::info!(
|
||||
"{}: Init X11 screen of res {:?} at {:?}",
|
||||
s.name.clone(),
|
||||
size,
|
||||
pos,
|
||||
);
|
||||
|
||||
let interaction = create_screen_interaction(
|
||||
vec2(s.monitor.x() as f32, s.monitor.y() as f32),
|
||||
vec2(size.0 as f32, size.1 as f32),
|
||||
Transform::Normal,
|
||||
);
|
||||
|
||||
let state = create_screen_state(s.name.clone(), size, Transform::Normal, &app.session);
|
||||
|
||||
let meta = ScreenMeta {
|
||||
name: s.name.clone(),
|
||||
id: state.id,
|
||||
native_handle: 0,
|
||||
};
|
||||
|
||||
let backend = Box::new(SplitOverlayBackend {
|
||||
renderer: Box::new(renderer),
|
||||
interaction: Box::new(interaction),
|
||||
});
|
||||
(meta, state, backend)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((overlays, extent))
|
||||
app.hid_provider.set_desktop_extent(extent);
|
||||
app.hid_provider.set_desktop_origin(vec2(0.0, 0.0));
|
||||
|
||||
Ok(ScreenCreateData { screens })
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Transform {
|
||||
Normal,
|
||||
_90,
|
||||
@@ -861,3 +991,31 @@ fn extent_from_res(res: (i32, i32)) -> [u32; 3] {
|
||||
let h = (res.1 as f32 / res.0 as f32 * w as f32) as u32;
|
||||
[w, h, 1]
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "pipewire", feature = "x11"))]
|
||||
fn best_match<'a>(
|
||||
stream: &PipewireStream,
|
||||
mut streams: impl Iterator<Item = &'a XshmScreen>,
|
||||
) -> Option<&'a XshmScreen> {
|
||||
let mut best = streams.next();
|
||||
log::debug!("stream: {:?}", stream.position);
|
||||
log::debug!("first: {:?}", best.map(|b| &b.monitor));
|
||||
let Some(position) = stream.position else {
|
||||
return best;
|
||||
};
|
||||
|
||||
let mut best_dist = best
|
||||
.map(|b| (b.monitor.x() - position.0).abs() + (b.monitor.y() - position.1).abs())
|
||||
.unwrap_or(i32::MAX);
|
||||
for stream in streams {
|
||||
log::debug!("checking: {:?}", stream.monitor);
|
||||
let dist =
|
||||
(stream.monitor.x() - position.0).abs() + (stream.monitor.y() - position.1).abs();
|
||||
if dist < best_dist {
|
||||
best = Some(stream);
|
||||
best_dist = dist;
|
||||
}
|
||||
}
|
||||
log::debug!("best: {:?}", best.map(|b| &b.monitor));
|
||||
best
|
||||
}
|
||||
+81
-25
@@ -1,26 +1,40 @@
|
||||
use std::{
|
||||
ops::Add,
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
time::Instant,
|
||||
};
|
||||
use std::{f32::consts::PI, ops::Add, sync::Arc, time::Instant};
|
||||
|
||||
use glam::vec3a;
|
||||
use glam::{vec3a, Quat};
|
||||
use idmap_derive::IntegerId;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
common::{OverlaySelector, TaskType},
|
||||
common::OverlaySelector,
|
||||
overlay::{OverlayBackend, OverlayState, RelativeTo},
|
||||
task::TaskType,
|
||||
},
|
||||
gui::{color_parse, CanvasBuilder},
|
||||
state::AppState,
|
||||
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 AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum DisplayMethod {
|
||||
Hide,
|
||||
Center,
|
||||
Watch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntegerId, Serialize, Deserialize)]
|
||||
pub enum ToastTopic {
|
||||
System,
|
||||
DesktopNotification,
|
||||
XSNotification,
|
||||
IpdChange,
|
||||
}
|
||||
|
||||
pub struct Toast {
|
||||
pub title: Arc<str>,
|
||||
@@ -28,17 +42,19 @@ pub struct Toast {
|
||||
pub opacity: f32,
|
||||
pub timeout: f32,
|
||||
pub sound: bool,
|
||||
pub topic: ToastTopic,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Toast {
|
||||
pub fn new(title: Arc<str>, body: Arc<str>) -> Self {
|
||||
pub fn new(topic: ToastTopic, title: Arc<str>, body: Arc<str>) -> Self {
|
||||
Toast {
|
||||
title,
|
||||
body,
|
||||
opacity: 1.0,
|
||||
timeout: 3.0,
|
||||
sound: false,
|
||||
topic,
|
||||
}
|
||||
}
|
||||
pub fn with_timeout(mut self, timeout: f32) -> Self {
|
||||
@@ -57,36 +73,74 @@ impl Toast {
|
||||
self.submit_at(app, Instant::now());
|
||||
}
|
||||
pub fn submit_at(self, app: &mut AppState, instant: Instant) {
|
||||
let auto_increment = AUTO_INCREMENT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let name: Arc<str> = format!("toast-{}", auto_increment).into();
|
||||
let selector = OverlaySelector::Name(name.clone());
|
||||
let selector = OverlaySelector::Name(TOAST_NAME.clone());
|
||||
|
||||
let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout));
|
||||
|
||||
let has_sound = self.sound && app.session.config.notifications_sound_enabled;
|
||||
|
||||
// drop any toast that was created before us.
|
||||
// (DropOverlay only drops overlays that were
|
||||
// created before current frame)
|
||||
app.tasks
|
||||
.enqueue_at(TaskType::DropOverlay(selector.clone()), instant);
|
||||
|
||||
// CreateOverlay only creates the overlay if
|
||||
// the selector doesn't exist yet, so in case
|
||||
// multiple toasts are submitted for the same
|
||||
// frame, only the first one gets created
|
||||
app.tasks.enqueue_at(
|
||||
TaskType::CreateOverlay(
|
||||
selector.clone(),
|
||||
Box::new(move |app| new_toast(self, name, app)),
|
||||
Box::new(move |app| {
|
||||
let mut maybe_toast = new_toast(self, app);
|
||||
if let Some((state, _)) = maybe_toast.as_mut() {
|
||||
state.auto_movement(app);
|
||||
app.tasks.enqueue_at(
|
||||
// at timeout, drop the overlay by ID instead
|
||||
// in order to avoid dropping any newer toasts
|
||||
TaskType::DropOverlay(OverlaySelector::Id(state.id)),
|
||||
destroy_at,
|
||||
);
|
||||
}
|
||||
maybe_toast
|
||||
}),
|
||||
),
|
||||
instant,
|
||||
);
|
||||
|
||||
app.tasks
|
||||
.enqueue_at(TaskType::DropOverlay(selector), destroy_at);
|
||||
|
||||
if has_sound {
|
||||
app.audio.play(TOAST_AUDIO_WAV);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_toast(
|
||||
toast: Toast,
|
||||
name: Arc<str>,
|
||||
app: &mut AppState,
|
||||
) -> Option<(OverlayState, Box<dyn OverlayBackend>)> {
|
||||
fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn OverlayBackend>)> {
|
||||
let current_method = app
|
||||
.session
|
||||
.toast_topics
|
||||
.get(toast.topic)
|
||||
.copied()
|
||||
.unwrap_or(DisplayMethod::Hide);
|
||||
|
||||
let (spawn_point, spawn_rotation, relative_to) = match current_method {
|
||||
DisplayMethod::Hide => return None,
|
||||
DisplayMethod::Center => (vec3a(0., -0.2, -0.5), Quat::IDENTITY, RelativeTo::Head),
|
||||
DisplayMethod::Watch => {
|
||||
let mut watch_pos = app.session.config.watch_pos + vec3a(-0.005, -0.05, 0.02);
|
||||
let mut watch_rot = app.session.config.watch_rot;
|
||||
let relative_to = match app.session.config.watch_hand {
|
||||
LeftRight::Left => RelativeTo::Hand(0),
|
||||
LeftRight::Right => {
|
||||
watch_pos.x = -watch_pos.x;
|
||||
watch_rot = watch_rot * Quat::from_rotation_x(PI) * Quat::from_rotation_z(PI);
|
||||
RelativeTo::Hand(1)
|
||||
}
|
||||
};
|
||||
(watch_pos, watch_rot, relative_to)
|
||||
}
|
||||
};
|
||||
|
||||
let title = if toast.title.len() > 0 {
|
||||
toast.title
|
||||
} else {
|
||||
@@ -140,11 +194,13 @@ fn new_toast(
|
||||
}
|
||||
|
||||
let state = OverlayState {
|
||||
name,
|
||||
name: TOAST_NAME.clone(),
|
||||
want_visible: true,
|
||||
spawn_scale: size.0 * PIXELS_TO_METERS,
|
||||
spawn_point: vec3a(0., -0.2, -0.5),
|
||||
relative_to: RelativeTo::Head,
|
||||
spawn_rotation,
|
||||
spawn_point,
|
||||
z_order: 70,
|
||||
relative_to,
|
||||
..Default::default()
|
||||
};
|
||||
let backend = Box::new(canvas.build());
|
||||
|
||||
+20
-52
@@ -1,15 +1,13 @@
|
||||
use glam::{Quat, Vec3A};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use glam::Vec3A;
|
||||
|
||||
use crate::{
|
||||
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
|
||||
config::{
|
||||
def_half, def_left, def_point7, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType,
|
||||
config::{load_known_yaml, ConfigType},
|
||||
gui::{
|
||||
modular::{modular_canvas, ModularData, ModularUiConfig},
|
||||
Canvas,
|
||||
},
|
||||
config_io,
|
||||
gui::modular::{modular_canvas, ModularUiConfig},
|
||||
state::{AppState, LeftRight},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub const WATCH_NAME: &str = "watch";
|
||||
@@ -20,8 +18,6 @@ where
|
||||
{
|
||||
let config = load_known_yaml::<ModularUiConfig>(ConfigType::Watch);
|
||||
|
||||
let canvas = modular_canvas(&config.size, &config.elements, state)?;
|
||||
|
||||
let relative_to = RelativeTo::Hand(state.session.config.watch_hand as usize);
|
||||
|
||||
Ok(OverlayData {
|
||||
@@ -29,23 +25,33 @@ where
|
||||
name: WATCH_NAME.into(),
|
||||
want_visible: true,
|
||||
interactable: true,
|
||||
z_order: 68,
|
||||
spawn_scale: config.width,
|
||||
spawn_point: Vec3A::from_slice(&state.session.config.watch_pos),
|
||||
spawn_rotation: Quat::from_slice(&state.session.config.watch_rot),
|
||||
spawn_point: state.session.config.watch_pos,
|
||||
spawn_rotation: state.session.config.watch_rot,
|
||||
interaction_transform: ui_transform(&config.size),
|
||||
relative_to,
|
||||
..Default::default()
|
||||
},
|
||||
backend: Box::new(canvas),
|
||||
backend: Box::new(create_watch_canvas(Some(config), state)?),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_watch_canvas(
|
||||
config: Option<ModularUiConfig>,
|
||||
state: &AppState,
|
||||
) -> anyhow::Result<Canvas<(), ModularData>> {
|
||||
let config = config.unwrap_or_else(|| load_known_yaml::<ModularUiConfig>(ConfigType::Watch));
|
||||
|
||||
modular_canvas(&config.size, &config.elements, state)
|
||||
}
|
||||
|
||||
pub fn watch_fade<D>(app: &mut AppState, watch: &mut OverlayData<D>)
|
||||
where
|
||||
D: Default,
|
||||
{
|
||||
if watch.state.saved_scale.is_some_and(|s| s < f32::EPSILON) {
|
||||
if watch.state.saved_transform.is_some() {
|
||||
watch.state.want_visible = false;
|
||||
return;
|
||||
}
|
||||
@@ -69,41 +75,3 @@ where
|
||||
watch.state.alpha = watch.state.alpha.clamp(0., 1.);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct WatchConf {
|
||||
#[serde(default = "def_watch_pos")]
|
||||
pub watch_pos: [f32; 3],
|
||||
|
||||
#[serde(default = "def_watch_rot")]
|
||||
pub watch_rot: [f32; 4],
|
||||
|
||||
#[serde(default = "def_left")]
|
||||
pub watch_hand: LeftRight,
|
||||
|
||||
#[serde(default = "def_half")]
|
||||
pub watch_view_angle_min: f32,
|
||||
|
||||
#[serde(default = "def_point7")]
|
||||
pub watch_view_angle_max: f32,
|
||||
}
|
||||
|
||||
fn get_config_path() -> PathBuf {
|
||||
let mut path = config_io::get_conf_d_path();
|
||||
path.push("watch_state.yaml");
|
||||
path
|
||||
}
|
||||
pub fn save_watch(app: &mut AppState) -> anyhow::Result<()> {
|
||||
let conf = WatchConf {
|
||||
watch_pos: app.session.config.watch_pos,
|
||||
watch_rot: app.session.config.watch_rot,
|
||||
watch_hand: app.session.config.watch_hand,
|
||||
watch_view_angle_min: app.session.config.watch_view_angle_min,
|
||||
watch_view_angle_max: app.session.config.watch_view_angle_max,
|
||||
};
|
||||
|
||||
let yaml = serde_yaml::to_string(&conf)?;
|
||||
std::fs::write(get_config_path(), yaml)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -35,6 +35,11 @@
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/default/in/MoveMouse",
|
||||
"type": "boolean",
|
||||
"requirement": "optional"
|
||||
},
|
||||
{
|
||||
"name": "/actions/default/in/SpaceDrag",
|
||||
"type": "boolean",
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
"inputs" : {
|
||||
"click" : {
|
||||
"output" : "/actions/default/in/click"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/default/in/movemouse"
|
||||
}
|
||||
},
|
||||
"mode" : "button",
|
||||
@@ -80,6 +83,9 @@
|
||||
"inputs" : {
|
||||
"click" : {
|
||||
"output" : "/actions/default/in/click"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/default/in/movemouse"
|
||||
}
|
||||
},
|
||||
"mode" : "button",
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
"inputs" : {
|
||||
"click" : {
|
||||
"output" : "/actions/default/in/click"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/default/in/movemouse"
|
||||
}
|
||||
},
|
||||
"mode": "button",
|
||||
@@ -76,6 +79,9 @@
|
||||
"inputs" : {
|
||||
"click" : {
|
||||
"output" : "/actions/default/in/click"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/default/in/movemouse"
|
||||
}
|
||||
},
|
||||
"mode": "button",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# looking to make changes?
|
||||
# drop me in ~/.config/wlxoverlay/anchor.yaml
|
||||
#
|
||||
|
||||
width: 0.1
|
||||
|
||||
size: [200, 200]
|
||||
|
||||
# +X: right, +Y: up, +Z: back
|
||||
spawn_pos: [0, 0, -1]
|
||||
|
||||
elements:
|
||||
- type: Panel
|
||||
rect: [98, 0, 4, 200]
|
||||
bg_color: "#ffff00"
|
||||
|
||||
- type: Panel
|
||||
rect: [0, 98, 200, 4]
|
||||
bg_color: "#ffff00"
|
||||
|
||||
- type: Label
|
||||
rect: [8, 90, 600, 70]
|
||||
font_size: 18
|
||||
fg_color: "#ffff00"
|
||||
source: Static
|
||||
text: Center
|
||||
@@ -11,3 +11,17 @@ keyboard_sound_enabled: true
|
||||
keyboard_scale: 1.0
|
||||
desktop_view_scale: 1.0
|
||||
watch_scale: 1.0
|
||||
|
||||
# Enable / disable sliding windows back and forth with the scroll action
|
||||
# Default: true
|
||||
allow_sliding: true
|
||||
|
||||
# Enable / disable realigning the working set windows when they are shown/hidden
|
||||
# Default: true
|
||||
realign_on_showhide: true
|
||||
|
||||
# When enabled, the mouse pointer will not be moved on the screen, unless the trigger is touched
|
||||
# allowing for moving both pointers off the screens to the keyboard, while keeping the cursor position
|
||||
# unchanged, for when the desktop is configured to move the focus with the mouse cursor
|
||||
# Default: false
|
||||
focus_follows_mouse_mode: false
|
||||
+49
-46
@@ -26,7 +26,7 @@ key_sizes:
|
||||
- [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0.5, 1, 1, 1, 0.5, 1, 1, 1, 1]
|
||||
- [1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 0.5, 1, 1, 1, 0.5, 1, 1, 1, 1]
|
||||
- [1.75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.25, 4, 1, 1, 1, 1]
|
||||
- [2.25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.75, 1.5, 1, 1.5, 1, 1, 1, 1]
|
||||
- [1.25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.75, 1.5, 1, 1.5, 1, 1, 1, 1]
|
||||
- [1.25, 1.25, 1.25, 6.25, 1.25, 1.25, 1.25, 1.25, 0.5, 1, 1, 1, 0.5, 2, 1, 1]
|
||||
|
||||
# The main (blue) layout of the keyboard.
|
||||
@@ -40,7 +40,7 @@ main_layout:
|
||||
- ["Oem3", "N1", "N2", "N3", "N4", "N5", "N6", "N7", "N8", "N9", "N0", "Minus", "Plus", "BackSpace", ~, "Insert", "Home", "Prior", ~, "NumLock", "KP_Divide", "KP_Multiply", "KP_Subtract"]
|
||||
- ["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "Oem4", "Oem6", "Oem5", ~, "Delete", "End", "Next", ~, "KP_7", "KP_8", "KP_9", "KP_Add"]
|
||||
- ["XF86Favorites", "A", "S", "D", "F", "G", "H", "J", "K", "L", "Oem1", "Oem7", "Return", ~, "KP_4", "KP_5", "KP_6", ~]
|
||||
- ["LShift", "Z", "X", "C", "V", "B", "N", "M", "Comma", "Period", "Oem2", "RShift", ~, "Up", ~, "KP_1", "KP_2", "KP_3", "KP_Enter"]
|
||||
- ["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", ~]
|
||||
|
||||
# Shell commands to be used in a layout.
|
||||
@@ -62,48 +62,51 @@ macros:
|
||||
# Value: Array of strings. 0th element is the upper row, 1st element is lower row.
|
||||
# For empty labels, use [] (do not use ~)
|
||||
labels:
|
||||
"N1": ["1", "!"]
|
||||
"N2": ["2", "@"]
|
||||
"N3": ["3", "#"]
|
||||
"N4": ["4", "$"]
|
||||
"N5": ["5", "%"]
|
||||
"N6": ["6", "^"]
|
||||
"N7": ["7", "&"]
|
||||
"N8": ["8", "*"]
|
||||
"N9": ["9", "("]
|
||||
"N0": ["0", ")"]
|
||||
"Escape": ["Esc"]
|
||||
"Prior": ["PgUp"]
|
||||
"Next": ["PgDn"]
|
||||
"NumLock": ["Num"]
|
||||
"Space": []
|
||||
"LAlt": ["Alt"]
|
||||
"LCtrl": ["Ctrl"]
|
||||
"RCtrl": ["Ctrl"]
|
||||
"LSuper": ["Super"]
|
||||
"RSuper": ["Super"]
|
||||
"LShift": ["Shift"]
|
||||
"RShift": ["Shift"]
|
||||
"Insert": ["Ins"]
|
||||
"Delete": ["Del"]
|
||||
"Minus": ["-", "_"]
|
||||
"Plus": ["=", "+"]
|
||||
"BackSpace": ["<<"]
|
||||
"Comma": [" ,", "<"]
|
||||
"Period": [" .", ">"]
|
||||
"Oem1": [" ;", ":"]
|
||||
"Oem2": [" /", "?"]
|
||||
"Oem3": ["`", "~"]
|
||||
"Oem4": [" [", "{"]
|
||||
"Oem5": [" \\", "|"]
|
||||
"Oem6": [" ]", "}"]
|
||||
"Oem7": [" '", "\""]
|
||||
"Oem102": [" \\", "|"]
|
||||
"KP_Divide": [" /"]
|
||||
"KP_Add": [" +"]
|
||||
"KP_Multiply": [" *"]
|
||||
"KP_Decimal": [" ."]
|
||||
"KP_Subtract": [" -"]
|
||||
"KP_Enter": ["Ent"]
|
||||
"XF86Favorites": ["Rofi"]
|
||||
"Escape": ["Esc"]
|
||||
"Prior": ["PgUp"]
|
||||
"Next": ["PgDn"]
|
||||
"NumLock": ["Num"]
|
||||
"Space": []
|
||||
"LAlt": ["Alt"]
|
||||
"LCtrl": ["Ctrl"]
|
||||
"RCtrl": ["Ctrl"]
|
||||
"LSuper": ["Super"]
|
||||
"RSuper": ["Super"]
|
||||
"LShift": ["Shift"]
|
||||
"RShift": ["Shift"]
|
||||
"Insert": ["Ins"]
|
||||
"Delete": ["Del"]
|
||||
"BackSpace": ["<<"]
|
||||
"KP_Divide": [" /"]
|
||||
"KP_Add": [" +"]
|
||||
"KP_Multiply": [" *"]
|
||||
"KP_Decimal": [" ."]
|
||||
"KP_Subtract": [" -"]
|
||||
"KP_Enter": ["Ent"]
|
||||
"Print": ["Prn"]
|
||||
"Scroll": ["Scr"]
|
||||
"Pause": ["Brk"]
|
||||
"XF86Favorites": ["Menu"] # fallback labels below
|
||||
"N1": ["1", "!"]
|
||||
"N2": ["2", "@"]
|
||||
"N3": ["3", "#"]
|
||||
"N4": ["4", "$"]
|
||||
"N5": ["5", "%"]
|
||||
"N6": ["6", "^"]
|
||||
"N7": ["7", "&"]
|
||||
"N8": ["8", "*"]
|
||||
"N9": ["9", "("]
|
||||
"N0": ["0", ")"]
|
||||
"Minus": ["-", "_"]
|
||||
"Plus": ["=", "+"]
|
||||
"Comma": [" ,", "<"]
|
||||
"Period": [" .", ">"]
|
||||
"Oem1": [" ;", ":"]
|
||||
"Oem2": [" /", "?"]
|
||||
"Oem3": ["`", "~"]
|
||||
"Oem4": [" [", "{"]
|
||||
"Oem5": [" \\", "|"]
|
||||
"Oem6": [" ]", "}"]
|
||||
"Oem7": [" '", "\""]
|
||||
"Oem102": [" \\", "|"]
|
||||
|
||||
+50
-2
@@ -528,21 +528,55 @@ elements:
|
||||
rect: [330, 505, 220, 30]
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#606020"
|
||||
bg_color: "#401010"
|
||||
text: "Enabled"
|
||||
click_down:
|
||||
- type: System
|
||||
action: ToggleNotifications
|
||||
highlight: Notifications
|
||||
|
||||
- type: Button
|
||||
rect: [330, 555, 220, 30]
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#606020"
|
||||
bg_color: "#401010"
|
||||
text: "Sound Enabled"
|
||||
click_down:
|
||||
- type: System
|
||||
action: ToggleNotificationSounds
|
||||
highlight: NotificationSounds
|
||||
|
||||
####### Behavior Section #######
|
||||
- type: Label
|
||||
rect: [15, 490, 570, 24]
|
||||
font_size: 18
|
||||
fg_color: "#ffffff"
|
||||
source: Static
|
||||
text: Behavior
|
||||
|
||||
- type: Button
|
||||
rect: [30, 505, 220, 30]
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#401010"
|
||||
text: "Auto-Realign"
|
||||
click_down:
|
||||
- type: System
|
||||
action: ToggleAutoRealign
|
||||
highlight: AutoRealign
|
||||
|
||||
- type: Button
|
||||
rect: [30, 555, 220, 30]
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#401010"
|
||||
text: "Grab+Scroll Slide"
|
||||
click_down:
|
||||
- type: System
|
||||
action: ToggleAllowSliding
|
||||
highlight: AllowSliding
|
||||
|
||||
####### Footer Section #######
|
||||
|
||||
- type: Panel
|
||||
rect: [50, 605, 500, 1]
|
||||
@@ -557,4 +591,18 @@ elements:
|
||||
click_down:
|
||||
- type: System
|
||||
action: PersistConfig
|
||||
- type: Toast
|
||||
message: Settings saved successfully.
|
||||
|
||||
- type: Button
|
||||
rect: [30, 625, 250, 30]
|
||||
font_size: 12
|
||||
fg_color: "#ffffff"
|
||||
bg_color: "#206060"
|
||||
text: "Save Overlay Layout"
|
||||
click_down:
|
||||
- type: System
|
||||
action: PersistLayout
|
||||
- type: Toast
|
||||
message: Saved. You will see this layout on next startup.
|
||||
|
||||
+6
-6
@@ -79,11 +79,11 @@ elements:
|
||||
# local clock
|
||||
- type: Label
|
||||
rect: [19, 90, 200, 50]
|
||||
font_size: 46
|
||||
font_size: 46 # Use 32 for 12-hour time
|
||||
fg_color: "#ffffff"
|
||||
source: Clock
|
||||
format: "%H:%M" # 23:59
|
||||
#format: "%h:%M %p" # 11:59 PM
|
||||
#format: "%I:%M %p" # 11:59 PM
|
||||
|
||||
# local date
|
||||
- type: Label
|
||||
@@ -105,12 +105,12 @@ elements:
|
||||
# alt clock 1
|
||||
- type: Label
|
||||
rect: [210, 90, 200, 50]
|
||||
font_size: 24
|
||||
font_size: 24 # Use 18 for 12-hour time
|
||||
fg_color: "#99BBAA"
|
||||
source: Clock
|
||||
timezone: "Asia/Tokyo" # change TZ1 here
|
||||
format: "%H:%M" # 23:59
|
||||
#format: "%h:%M %p" # 11:59 PM
|
||||
#format: "%I:%M %p" # 11:59 PM
|
||||
- type: Label
|
||||
rect: [210, 60, 200, 50]
|
||||
font_size: 14
|
||||
@@ -121,12 +121,12 @@ elements:
|
||||
# alt clock 2
|
||||
- type: Label
|
||||
rect: [210, 150, 200, 50]
|
||||
font_size: 24
|
||||
font_size: 24 # Use 18 for 12-hour time
|
||||
fg_color: "#AA99BB"
|
||||
source: Clock
|
||||
timezone: "America/Chicago" # change TZ2 here
|
||||
format: "%H:%M" # 23:59
|
||||
#format: "%h:%M %p" # 11:59 PM
|
||||
#format: "%I:%M %p" # 11:59 PM
|
||||
- type: Label
|
||||
rect: [210, 120, 200, 50]
|
||||
font_size: 14
|
||||
|
||||
+23
-7
@@ -1,19 +1,20 @@
|
||||
use std::{io::Cursor, path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::bail;
|
||||
use glam::Vec3;
|
||||
use glam::{Affine3A, Vec3};
|
||||
use idmap::IdMap;
|
||||
use rodio::{Decoder, OutputStream, OutputStreamHandle, Source};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use vulkano::format::Format;
|
||||
|
||||
use crate::{
|
||||
backend::{common::TaskContainer, input::InputState},
|
||||
backend::{input::InputState, task::TaskContainer},
|
||||
config::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},
|
||||
};
|
||||
|
||||
@@ -22,11 +23,11 @@ pub struct AppState {
|
||||
pub session: AppSession,
|
||||
pub tasks: TaskContainer,
|
||||
pub graphics: Arc<WlxGraphics>,
|
||||
pub format: vulkano::format::Format,
|
||||
pub input_state: InputState,
|
||||
pub hid_provider: Box<dyn HidProvider>,
|
||||
pub audio: AudioOutput,
|
||||
pub screens: SmallVec<[ScreenMeta; 8]>,
|
||||
pub anchor: Affine3A,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -56,16 +57,18 @@ impl AppState {
|
||||
shaders.insert("frag_srgb", shader);
|
||||
}
|
||||
|
||||
let session = AppSession::load();
|
||||
|
||||
Ok(AppState {
|
||||
fc: FontCache::new()?,
|
||||
session: AppSession::load(),
|
||||
fc: FontCache::new(session.config.primary_font.clone())?,
|
||||
session,
|
||||
tasks: TaskContainer::new(),
|
||||
graphics,
|
||||
format: Format::R8G8B8A8_UNORM,
|
||||
input_state: InputState::new(),
|
||||
hid_provider: crate::hid::initialize(),
|
||||
audio: AudioOutput::new(),
|
||||
screens: smallvec![],
|
||||
anchor: Affine3A::IDENTITY,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -74,6 +77,8 @@ pub struct AppSession {
|
||||
pub config_root_path: PathBuf,
|
||||
pub config: GeneralConfig,
|
||||
|
||||
pub toast_topics: IdMap<ToastTopic, DisplayMethod>,
|
||||
|
||||
pub color_norm: Vec3,
|
||||
pub color_shift: Vec3,
|
||||
pub color_alt: Vec3,
|
||||
@@ -86,9 +91,19 @@ impl AppSession {
|
||||
log::info!("Config root path: {}", config_root_path.to_string_lossy());
|
||||
let config = GeneralConfig::load_from_disk();
|
||||
|
||||
let mut toast_topics = IdMap::new();
|
||||
toast_topics.insert(ToastTopic::System, DisplayMethod::Center);
|
||||
toast_topics.insert(ToastTopic::DesktopNotification, DisplayMethod::Center);
|
||||
toast_topics.insert(ToastTopic::XSNotification, DisplayMethod::Center);
|
||||
|
||||
config.notification_topics.iter().for_each(|(k, v)| {
|
||||
toast_topics.insert(*k, *v);
|
||||
});
|
||||
|
||||
AppSession {
|
||||
config_root_path,
|
||||
config,
|
||||
toast_topics,
|
||||
color_norm: Vec3 {
|
||||
x: 0.,
|
||||
y: 1.,
|
||||
@@ -158,6 +173,7 @@ impl AudioOutput {
|
||||
pub struct ScreenMeta {
|
||||
pub name: Arc<str>,
|
||||
pub id: usize,
|
||||
pub native_handle: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Default)]
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,787 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="127.99999"
|
||||
height="127.99999"
|
||||
viewBox="0 0 266.92913 266.92913"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1">
|
||||
<linearGradient
|
||||
id="linearGradient207">
|
||||
<stop
|
||||
style="stop-color:#131a23;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop206" />
|
||||
<stop
|
||||
style="stop-color:#1c1c2a;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop207" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient205">
|
||||
<stop
|
||||
style="stop-color:#141e2c;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop204" />
|
||||
<stop
|
||||
style="stop-color:#121220;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop205" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient203">
|
||||
<stop
|
||||
style="stop-color:#13bead;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop202" />
|
||||
<stop
|
||||
style="stop-color:#003d5e;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop203" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient201">
|
||||
<stop
|
||||
style="stop-color:#296c43;stop-opacity:1;"
|
||||
offset="0.18888886"
|
||||
id="stop199" />
|
||||
<stop
|
||||
style="stop-color:#133d59;stop-opacity:1;"
|
||||
offset="0.61091447"
|
||||
id="stop200" />
|
||||
<stop
|
||||
style="stop-color:#9b2adf;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop201" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient3">
|
||||
<stop
|
||||
style="stop-color:#6ef8d2;stop-opacity:1;"
|
||||
offset="0.18888886"
|
||||
id="stop2" />
|
||||
<stop
|
||||
style="stop-color:#529ac5;stop-opacity:1;"
|
||||
offset="0.61091447"
|
||||
id="stop1" />
|
||||
<stop
|
||||
style="stop-color:#ad47da;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3" />
|
||||
</linearGradient>
|
||||
<rect
|
||||
x="469.23965"
|
||||
y="156.99583"
|
||||
width="427.99774"
|
||||
height="256.79865"
|
||||
id="rect253" />
|
||||
<linearGradient
|
||||
id="linearGradient252">
|
||||
<stop
|
||||
style="stop-color:#4fffe5;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop252" />
|
||||
<stop
|
||||
style="stop-color:#009ee3;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop253" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient245">
|
||||
<stop
|
||||
style="stop-color:#2bedda;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop245" />
|
||||
<stop
|
||||
style="stop-color:#2ab7ff;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop246" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient243">
|
||||
<stop
|
||||
style="stop-color:#6700de;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop243" />
|
||||
<stop
|
||||
style="stop-color:#3a007e;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop244" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient240">
|
||||
<stop
|
||||
style="stop-color:#171f2f;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop239" />
|
||||
<stop
|
||||
style="stop-color:#151524;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop240" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient215">
|
||||
<stop
|
||||
style="stop-color:#1d2a3b;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop215" />
|
||||
<stop
|
||||
style="stop-color:#161625;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop216" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient205"
|
||||
id="linearGradient229"
|
||||
x1="99.80159"
|
||||
y1="131.5479"
|
||||
x2="202.55092"
|
||||
y2="87.0336"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient215"
|
||||
id="linearGradient233"
|
||||
x1="92.869743"
|
||||
y1="181.052"
|
||||
x2="121.33655"
|
||||
y2="183.86719"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.1342199,0,0,1,-15.127262,-23.265656)" />
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient207"
|
||||
id="linearGradient237"
|
||||
x1="205.14536"
|
||||
y1="157.47878"
|
||||
x2="248.50272"
|
||||
y2="157.47878"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(3.3018171,-23.265656)" />
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient240"
|
||||
id="linearGradient239"
|
||||
x1="216.49715"
|
||||
y1="115.60864"
|
||||
x2="264.67487"
|
||||
y2="115.60864"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.86737908,0,0,1.6992902,9.5668621,-102.30952)" />
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient243"
|
||||
id="linearGradient244"
|
||||
x1="131.10333"
|
||||
y1="249.97314"
|
||||
x2="217.58331"
|
||||
y2="249.97314"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient245"
|
||||
id="linearGradient246"
|
||||
x1="67.408493"
|
||||
y1="29.699856"
|
||||
x2="68.862946"
|
||||
y2="141.21988"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.61796247,0,0,1.2554659,16.242695,-39.997664)" />
|
||||
<radialGradient
|
||||
xlink:href="#linearGradient252"
|
||||
id="radialGradient253"
|
||||
cx="171.10719"
|
||||
cy="62.119579"
|
||||
fx="171.10719"
|
||||
fy="62.119579"
|
||||
r="61.437805"
|
||||
gradientTransform="matrix(1.3097725,0.31767857,-0.15662969,0.9822146,-47.993998,-30.138803)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
xlink:href="#linearGradient3"
|
||||
id="radialGradient2"
|
||||
cx="187.37721"
|
||||
cy="76.353279"
|
||||
fx="187.37721"
|
||||
fy="76.353279"
|
||||
r="59.889355"
|
||||
gradientTransform="matrix(1.0943327,1.0413486,-1.0278224,0.84312646,53.734372,-179.01419)"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB"
|
||||
id="filter3"
|
||||
x="0"
|
||||
y="0"
|
||||
width="1"
|
||||
height="1">
|
||||
<feColorMatrix
|
||||
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 -0 "
|
||||
result="colormatrix"
|
||||
id="feColorMatrix3" />
|
||||
<feComposite
|
||||
in2="colormatrix"
|
||||
operator="arithmetic"
|
||||
k2="0.830816"
|
||||
result="composite"
|
||||
id="feComposite3"
|
||||
k1="0"
|
||||
k3="0"
|
||||
k4="0" />
|
||||
</filter>
|
||||
<meshgradient
|
||||
id="meshgradient34"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow127">
|
||||
<meshpatch
|
||||
id="meshpatch127">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop127" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop128" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -94.6866,-11.9178 -144.745,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop129" />
|
||||
<stop
|
||||
path="c -3.12413,-56.6274 3.07869,-85.7321 2.79848,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop130" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<meshgradient
|
||||
id="meshgradient1064"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow1068">
|
||||
<meshpatch
|
||||
id="meshpatch1068">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1065" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1066" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -88.2028,-11.9178 -138.261,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1067" />
|
||||
<stop
|
||||
path="c -3.12375,-56.6274 -3.40501,-85.7321 -3.68559,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1068" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<meshgradient
|
||||
id="meshgradient1068"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow1072">
|
||||
<meshpatch
|
||||
id="meshpatch1072">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1069" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1070" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -88.2028,-11.9178 -138.261,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1071" />
|
||||
<stop
|
||||
path="c -3.12375,-56.6274 -3.40501,-85.7321 -3.68559,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1072" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<meshgradient
|
||||
id="meshgradient1072"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow1076">
|
||||
<meshpatch
|
||||
id="meshpatch1076">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1073" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1074" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -88.2028,-11.9178 -138.261,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1075" />
|
||||
<stop
|
||||
path="c -3.12375,-56.6274 -3.40501,-85.7321 -3.68559,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1076" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<meshgradient
|
||||
id="meshgradient1076"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow1080">
|
||||
<meshpatch
|
||||
id="meshpatch1080">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1077" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1078" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -88.2028,-11.9178 -138.261,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1079" />
|
||||
<stop
|
||||
path="c -3.12375,-56.6274 -3.40501,-85.7321 -3.68559,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1080" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<meshgradient
|
||||
id="meshgradient1080"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow1084">
|
||||
<meshpatch
|
||||
id="meshpatch1084">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1081" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1082" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -88.2028,-11.9178 -138.261,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1083" />
|
||||
<stop
|
||||
path="c -3.12375,-56.6274 -3.40501,-85.7321 -3.68559,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1084" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<meshgradient
|
||||
id="meshgradient1096"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow1100">
|
||||
<meshpatch
|
||||
id="meshpatch1100">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1097" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1098" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -88.2028,-11.9178 -138.261,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1099" />
|
||||
<stop
|
||||
path="c -3.12375,-56.6274 -3.40501,-85.7321 -3.68559,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1100" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<meshgradient
|
||||
id="meshgradient1100"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow1104">
|
||||
<meshpatch
|
||||
id="meshpatch1104">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1101" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1102" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -88.2028,-11.9178 -138.261,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1103" />
|
||||
<stop
|
||||
path="c -3.12375,-56.6274 -3.40501,-85.7321 -3.68559,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1104" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<meshgradient
|
||||
id="meshgradient1104"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow1108">
|
||||
<meshpatch
|
||||
id="meshpatch1108">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1105" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1106" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -88.2028,-11.9178 -138.261,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1107" />
|
||||
<stop
|
||||
path="c -3.12375,-56.6274 -3.40501,-85.7321 -3.68559,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1108" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<meshgradient
|
||||
id="meshgradient1108"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow1112">
|
||||
<meshpatch
|
||||
id="meshpatch1112">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1109" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1110" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -88.2028,-11.9178 -138.261,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop1111" />
|
||||
<stop
|
||||
path="c -3.12375,-56.6274 -3.40501,-85.7321 -3.68559,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop1112" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<radialGradient
|
||||
xlink:href="#linearGradient201"
|
||||
id="radialGradient158"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0943327,1.0413486,-1.0278224,0.84312646,53.734372,-179.01419)"
|
||||
cx="187.37721"
|
||||
cy="76.353279"
|
||||
fx="187.37721"
|
||||
fy="76.353279"
|
||||
r="59.889355" />
|
||||
<meshgradient
|
||||
id="meshgradient194"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x="110.558"
|
||||
y="74.668777">
|
||||
<meshrow
|
||||
id="meshrow198">
|
||||
<meshpatch
|
||||
id="meshpatch198">
|
||||
<stop
|
||||
path="c 49.4918,5.29683e-08 91.3225,-12.4833 140.812,-12.4833"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop195" />
|
||||
<stop
|
||||
path="c 0,54.6408 1.13459,114.673 1.13459,169.312"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop196" />
|
||||
<stop
|
||||
path="c -49.4918,-0.000864729 -94.6866,-11.9178 -144.745,-15.039"
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
id="stop197" />
|
||||
<stop
|
||||
path="c -3.12413,-56.6274 3.07869,-85.7321 2.79848,-141.79"
|
||||
style="stop-color:#000000;stop-opacity:1"
|
||||
id="stop198" />
|
||||
</meshpatch>
|
||||
</meshrow>
|
||||
</meshgradient>
|
||||
<linearGradient
|
||||
xlink:href="#linearGradient203"
|
||||
id="linearGradient202"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.61796247,0,0,1.2554659,16.242695,-39.997664)"
|
||||
x1="67.408493"
|
||||
y1="29.699856"
|
||||
x2="68.862946"
|
||||
y2="141.21988" />
|
||||
</defs>
|
||||
<g
|
||||
id="g149"
|
||||
transform="matrix(1.1640862,0,0,1.1786589,-51.763614,-39.950388)"
|
||||
style="fill:url(#meshgradient194);fill-opacity:1;stroke-width:1.8854;stroke-dasharray:none">
|
||||
<path
|
||||
id="path131"
|
||||
transform="matrix(1.2719058,0,0,1.4270074,-21.946491,51.827348)"
|
||||
d="m 106.41227,15.347935 c 5.88573,0.05091 14.45523,0.501242 24.74512,0.715711 10.75212,0.224103 21.71002,0.09046 33.3855,-0.902397 11.56688,-0.983619 21.25587,-2.601614 29.09629,-4.255097 7.50414,-1.5825667 12.0939,-2.8954835 12.0939,-2.8954835 2.38337,-0.6395079 4.6644,-0.4231372 6.34705,0.6809039 1.68166,1.1033914 2.62591,3.0114696 2.63347,5.2816606 0.11676,35.059188 0.35021,105.156527 0.35021,105.156527 0.008,2.27829 -0.8997,4.26525 -2.47303,5.49493 -1.56983,1.22696 -3.65403,1.58421 -5.76806,1.04798 -8.07471,-2.0482 -17.54467,-4.64445 -27.45415,-6.65824 -10.5929,-2.15267 -20.5883,-3.45795 -32.2097,-4.43925 -10.9767,-0.92687 -21.52112,-1.45411 -29.00668,-1.96936 -7.50092,-0.51631 -11.93293,-0.9747 -11.93293,-0.9747 -2.31451,-0.2304 -4.46367,-1.1889 -5.99471,-2.64509 -1.528202,-1.4535 -2.309552,-3.2706 -2.248995,-5.03805 0.9352,-27.295135 2.805035,-81.869035 2.805035,-81.869035 0.0602,-1.757937 0.66734,-3.488375 1.68977,-4.751741 1.03008,-1.272815 2.41472,-1.992478 3.94191,-1.979269 z"
|
||||
style="display:inline;fill:url(#radialGradient158);fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
id="g7"
|
||||
transform="matrix(1.1060694,0,0,1.1060694,-41.526161,-29.183396)"
|
||||
style="fill:url(#meshgradient34);fill-opacity:1;stroke-width:1.8854;stroke-dasharray:none">
|
||||
<path
|
||||
id="rect244-6"
|
||||
style="display:inline;fill:url(#radialGradient2)"
|
||||
transform="matrix(1.2719058,0,0,1.4270074,-21.946491,51.827348)"
|
||||
d="m 103.72073,15.347885 c 5.98477,-0.05894 14.96096,0.42147 25.94373,0.682435 11.51485,0.273609 23.27966,0.167618 35.92949,-0.96063 12.5099,-1.115767 22.96911,-2.985754 31.43041,-4.904201 8.09368,-1.8350955 13.0266,-3.3586482 13.0266,-3.3586482 1.23185,-0.3563967 2.41212,-0.2500229 3.28358,0.3172073 0.87112,0.567012 1.36142,1.55023 1.36534,2.7272038 0.12592,37.8098291 0.37768,113.4068081 0.37768,113.4068081 0.004,1.1787 -0.4644,2.20311 -1.28837,2.8401 -0.82297,0.63621 -1.92793,0.82944 -3.06101,0.55093 -8.7386,-2.14792 -19.5453,-5.16545 -29.8955,-7.33259 -11.43622,-2.39454 -22.05044,-3.78746 -34.70545,-4.82724 -11.8829,-0.97635 -23.31917,-1.5259 -31.3494,-2.12486 -8.04581,-0.60011 -12.6987,-1.18299 -12.6987,-1.18299 -1.16984,-0.13877 -2.263662,-0.64729 -3.049003,-1.40643 -0.784523,-0.75834 -1.195889,-1.70248 -1.164526,-2.61785 1.008573,-29.436636 3.025119,-88.292244 3.025119,-88.292244 0.0313,-0.913325 0.34145,-1.806507 0.86287,-2.46653 0.52344,-0.662578 1.22186,-1.043132 1.96714,-1.050471 z" />
|
||||
<g
|
||||
id="g2"
|
||||
style="display:inline;opacity:1;fill:url(#meshgradient1108);fill-opacity:1;stroke-width:3.24661;stroke-dasharray:none;filter:url(#filter3)"
|
||||
transform="matrix(0.54226583,0,0,0.62191996,101.81226,71.639539)">
|
||||
<g
|
||||
id="layer7"
|
||||
style="display:none;fill:url(#meshgradient1064);fill-opacity:1;stroke-width:3.24661;stroke-dasharray:none">
|
||||
<path
|
||||
style="opacity:1;fill:url(#radialGradient253);fill-opacity:1;stroke:#0e2a40;stroke-width:3.24661;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect252"
|
||||
width="139.75923"
|
||||
height="126.21101"
|
||||
x="119.69108"
|
||||
y="35.196575"
|
||||
ry="9.0267553"
|
||||
d="m 120.36331,50.488531 c 22.919,0.09504 49.98768,-0.476422 79.20913,-3.036874 C 226.38036,45.102683 248.155,41.4726 248.155,41.4726 c 5.36963,-0.857646 9.72815,2.426665 9.77024,7.53464 0.30139,36.574682 0.90397,109.7021 0.90397,109.7021 0.0421,5.11079 -4.05701,8.89265 -9.06813,8.53726 -20.37405,-1.4449 -49.43903,-4.07704 -74.2521,-6.05433 -24.79299,-1.97568 -45.45503,-3.26366 -45.45503,-3.26366 -5.07373,-0.32266 -9.49277,-3.98864 -9.87382,-8.21873 -2.75244,-30.55483 -8.25566,-91.646173 -8.25566,-91.646173 -0.38171,-4.237397 3.3585,-7.596243 8.43884,-7.575176 z" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.33507603,0,0,0.37995311,-19.880019,-12.768516)"
|
||||
id="text253"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:74.6667px;line-height:0.9;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold';text-align:center;white-space:pre;shape-inside:url(#rect253);display:inline;opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:9.099;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
x="-154.80215"
|
||||
y="0"><tspan
|
||||
x="469.24023"
|
||||
y="209.40346"
|
||||
id="tspan3"><tspan
|
||||
style="text-align:start"
|
||||
id="tspan1">Overlay</tspan></tspan></text>
|
||||
</g>
|
||||
<g
|
||||
id="layer3"
|
||||
style="display:inline;fill:url(#meshgradient1068);fill-opacity:1;stroke-width:3.24661;stroke-dasharray:none">
|
||||
<path
|
||||
id="rect3"
|
||||
style="fill:url(#linearGradient244)"
|
||||
transform="matrix(1.1028866,0,0,1.1661471,-18.842135,-68.983005)"
|
||||
d="m 141.5197,232.74856 c 14.50571,1.24952 29.33813,2.83394 45.80344,5.11698 14.71882,2.04087 26.37713,4.00976 26.37713,4.00976 2.86398,0.46696 5.26545,3.19012 5.36908,6.10293 0.0517,1.45262 0.15501,4.35698 0.15501,4.35698 0.0496,1.39306 -0.44478,2.62569 -1.36817,3.42126 -0.92361,0.79576 -2.20065,1.08963 -3.55361,0.82714 -12.99897,-2.52197 -27.24986,-5.06329 -43.77534,-7.27441 -14.99629,-2.00652 -27.73287,-3.15138 -27.73287,-3.15138 -1.48432,-0.14097 -2.96452,-0.77187 -4.10659,-1.75252 -1.14295,-0.98142 -1.85405,-2.23289 -1.97368,-3.48564 -0.12476,-1.30648 -0.37422,-3.91866 -0.37422,-3.91866 -0.24905,-2.60802 2.0839,-4.51913 5.17982,-4.25244 z" />
|
||||
</g>
|
||||
<g
|
||||
id="layer4"
|
||||
style="display:inline;fill:url(#meshgradient1072);fill-opacity:1;stroke-width:3.24661;stroke-dasharray:none">
|
||||
<path
|
||||
style="fill:url(#linearGradient237);fill-opacity:1"
|
||||
id="rect210"
|
||||
width="43.357353"
|
||||
height="99.615921"
|
||||
x="208.44717"
|
||||
y="84.405167"
|
||||
d="m 209.66955,92.272568 c 6.7322,-0.16598 14.38233,-0.41674 22.69259,-0.80776 7.59945,-0.357574 13.7461,-0.74835 13.7461,-0.74835 2.2821,-0.141431 4.15788,1.500306 4.18508,3.694161 0.37814,30.498741 1.13418,91.477921 1.13418,91.477921 0.0272,2.19437 -1.71574,3.74227 -3.86875,3.47186 -11.70171,-1.46967 -34.04157,-4.39345 -34.04157,-4.39345 -2.07685,-0.26609 -3.83887,-2.12889 -3.92956,-4.15029 -1.26828,-28.26615 -3.80407,-84.781487 -3.80407,-84.781487 -0.0907,-2.021933 1.64509,-3.707356 3.886,-3.762605 z"
|
||||
transform="translate(-0.71814209,4.1703544)" />
|
||||
<path
|
||||
style="fill:url(#linearGradient233);fill-opacity:1"
|
||||
id="rect211"
|
||||
width="51.68074"
|
||||
height="91.151398"
|
||||
x="82.748856"
|
||||
y="123.05398"
|
||||
d="m 80.47685,123.1104 c 0.686911,0.0328 32.06099,1.49986 32.7479,1.53266 10.95124,0.50481 19.67641,0.89883 19.67641,0.89883 2.32431,25.95786 6.97152,77.858 6.97152,77.858 -9.90209,-0.93081 -20.61604,-1.79499 -33.20992,-2.65118 C 95.354127,199.9799 86.14795,199.45109 86.14795,199.45109 169.55658,1322.2439 -2.9317791,-999.68243 80.476849,123.1104 Z" />
|
||||
</g>
|
||||
<g
|
||||
id="layer1"
|
||||
style="display:inline;fill:url(#meshgradient1080);fill-opacity:1;stroke-width:3.24661;stroke-dasharray:none">
|
||||
<path
|
||||
id="rect2"
|
||||
style="fill:#f7f2f1"
|
||||
transform="matrix(0.88631797,0,0,1.14015,18.230141,-61.294282)"
|
||||
d="m 177.90192,216.30228 c 4.14717,0.40612 8.19723,2.30928 11.27702,5.3317 3.07755,3.02023 4.91903,6.89742 5.1651,10.74356 0.28509,3.88205 -1.05797,7.41 -3.71104,9.77442 -2.6581,2.3689 -6.41781,3.38872 -10.51095,2.89322 -4.04996,-0.52413 -8.11166,-2.50573 -11.25007,-5.47805 -3.14352,-2.97716 -5.11709,-6.71723 -5.4435,-10.44075 -0.36747,-3.76009 0.95436,-7.21327 3.6799,-9.62456 2.72107,-2.40733 6.61999,-3.57393 10.79354,-3.19954 z" />
|
||||
<g
|
||||
id="g1"
|
||||
style="display:inline;fill:url(#meshgradient1076);fill-opacity:1;stroke-width:3.66303;stroke-dasharray:none"
|
||||
transform="matrix(0.92452299,0,0,0.88631797,9.9395114,1.6271212)">
|
||||
<path
|
||||
id="circle1"
|
||||
style="fill:#f7f2f1;fill-opacity:1"
|
||||
d="m 139.93601,95.963847 c -20.29293,-0.327742 -35.37169,12.686533 -33.94622,29.478073 2.20095,10.94672 6.63093,32.87821 6.63093,32.87821 -9.05268,15.33583 -10.18571,22.03628 -10.29884,33.07659 2.96627,19.00354 13.81154,24.51969 38.94596,29.90868 36.15907,5.19528 42.04946,4.56032 68.66554,4.20072 12.27862,-2.60897 18.25127,-5.37981 21.68277,-9.55692 3.43986,-4.18728 4.29703,-9.72457 6.56239,-18.09473 0.66388,-8.15956 2.03668,-24.58454 2.03668,-24.58454 7.50482,-1.34376 12.8393,-7.80874 12.7282,-15.64436 -0.13129,-7.84626 -5.69971,-14.5686 -13.48151,-16.4621 -0.098,-4.17663 -0.29393,-12.52739 -0.29393,-12.52739 -0.46041,-19.61437 -17.3574,-33.566518 -37.94796,-32.963422 -20.46321,0.599259 -40.93211,0.619777 -61.28401,0.291189 z" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="layer2"
|
||||
style="display:inline;fill:url(#meshgradient1100);fill-opacity:1"
|
||||
transform="translate(21.838235,-8.7352941)">
|
||||
<g
|
||||
id="g214"
|
||||
style="display:inline;fill:url(#meshgradient1096);fill-opacity:1"
|
||||
transform="matrix(1.0198935,0,0,1,-23.322991,-14.530363)">
|
||||
<path
|
||||
id="path209"
|
||||
style="fill:url(#linearGradient229);fill-opacity:1"
|
||||
d="m 123.92491,83.962019 c -10.23363,3.055336 -30.4478,8.769077 -30.4478,8.769077 -1.266574,0.354181 -2.340048,1.022231 -3.091203,1.92289 -3.66068,4.389277 -10.991047,13.100584 -10.991047,13.100584 -0.758487,0.89849 -1.228249,1.96258 -1.36958,3.1033 -1.113915,8.99066 -3.416566,26.93286 -3.416566,26.93286 -0.124786,0.95989 0.722438,1.91928 1.904,2.15268 1.454515,0.2873 4.36818,0.86207 4.36818,0.86207 1.305308,0.25742 2.410726,1.09852 2.841498,2.16349 2.078656,5.13893 6.273055,15.43228 6.273055,15.43228 0.465412,1.13999 1.309593,2.18759 2.431102,3.01695 7.964041,5.8894 23.925201,17.82116 23.925201,17.82116 0.49221,0.37121 1.16511,0.48621 1.69621,0.28956 0.53113,-0.19667 0.81283,-0.66523 0.71061,-1.18144 -1.86375,-9.41224 -5.57902,-28.15964 -5.57902,-28.15964 -0.23671,-1.19457 -0.32758,-2.38954 -0.27137,-3.5669 0.24655,-5.16358 0.73595,-15.50177 0.73595,-15.50177 0.0557,-1.18033 0.23718,-2.34151 0.54212,-3.47069 0.34418,-1.27445 1.03202,-3.82514 1.03202,-3.82514 0.033,-0.12252 0.17246,-0.19517 0.32739,-0.17055 0.15493,0.0246 0.27838,0.13905 0.28986,0.2687 0.68381,7.72149 2.05103,23.15982 2.05103,23.15982 0.0905,1.02212 1.08936,1.92605 2.31385,2.09508 4.27477,0.59008 12.79471,1.77943 12.79471,1.77943 1.41308,0.1981 2.79841,0.64835 4.06229,1.32189 1.23086,0.65595 3.68783,1.97172 3.68783,1.97172 1.24659,0.66873 2.65004,1.02753 4.02626,1.02688 9.42581,-0.004 28.10015,-0.0477 28.10015,-0.0477 1.19102,-0.004 2.0812,-0.80851 2.07272,-1.87422 -0.1172,-14.72073 -0.2846,-44.28083 -0.2846,-44.28083 -0.0172,-0.34101 0.1913,-0.64824 0.5365,-0.79062 0.34517,-0.14237 0.76579,-0.0946 1.08168,0.12342 2.88494,14.55408 8.50435,43.91811 8.50435,43.91811 0.28446,1.50523 1.85925,2.65367 3.49696,2.54532 5.65269,-0.37401 16.95539,-1.1968 16.95539,-1.1968 1.75939,-0.13279 3.10121,-1.36401 3.27324,-3.00869 0.38599,-3.69047 1.17078,-11.0956 1.17078,-11.0956 0.0962,-0.90427 1.18054,-1.35932 2.03877,-0.85048 8.26249,4.89881 24.64155,15.05184 24.64155,15.05184 0.37183,0.23372 0.83479,0.26413 1.21337,0.0786 0.37855,-0.18553 0.61464,-0.55861 0.61807,-0.97696 0.0955,-11.66072 0.31069,-35.03859 0.31069,-35.03859 0.0109,-1.14289 0.97667,-2.09035 2.19074,-2.14539 3.12174,-0.14153 9.39326,-0.468 9.39326,-0.468 0.37027,-0.0201 0.69934,-0.25043 0.83496,-0.58446 0.13561,-0.33403 0.0512,-0.70638 -0.21436,-0.9442 -7.81079,-6.99479 -16.43955,-14.515563 -26.1194,-22.595271 -8.77789,-7.326845 -15.91626,-13.010686 -15.91626,-13.010686 -1.01856,-0.815898 -2.41915,-1.15111 -3.79248,-0.916782 -10.94116,1.866865 -22.85862,3.659431 -36.06911,5.337963 -11.95682,1.519241 -21.45555,2.496218 -21.45555,2.496218 -1.39301,0.148085 -2.834,-0.06908 -4.13536,-0.619375 -6.3046,-2.370273 -13.32808,-2.495056 -19.29264,-0.421108 z"
|
||||
transform="matrix(0.88631797,0,0,0.88631797,18.192351,24.242627)" />
|
||||
</g>
|
||||
<path
|
||||
id="path213"
|
||||
style="fill:url(#linearGradient239);fill-opacity:1"
|
||||
d="m 207.52109,60.934045 c -8.23842,3.588919 -24.48552,10.018705 -24.48552,10.018705 -1.51861,0.582881 -2.68634,1.721139 -3.18088,3.103388 -0.49485,1.383116 -0.2657,2.868097 0.61858,4.054476 2.75693,3.698772 8.20887,11.192989 8.20887,11.192989 1.4022,2.680164 4.71448,4.097029 7.92934,3.326928 1.72645,12.227559 5.07462,36.906589 5.07462,36.906589 0.21342,1.59436 1.08581,3.07164 2.42179,4.10728 1.33617,1.03578 3.03056,1.54758 4.71517,1.40807 7.54666,-0.625 22.90972,-2.17677 22.90972,-2.17677 1.729,-0.18647 3.31955,-1.04033 4.42227,-2.38314 1.10188,-1.3418 1.62109,-3.05726 1.43659,-4.75363 -2.02293,-18.60064 -6.18002,-55.217054 -6.18002,-55.217054 -0.19199,-1.664457 -1.08333,-3.096741 -2.48051,-3.984458 -1.39688,-0.887527 -3.18296,-1.157397 -4.96247,-0.76854 -3.32097,0.231699 -6.25797,-1.378914 -7.5354,-4.069393 -2.152,-2.217526 -5.88818,-2.503869 -8.91215,-0.76544 z"
|
||||
transform="translate(-0.2555589,0.54640561)" />
|
||||
<path
|
||||
style="fill:#171924;fill-opacity:1"
|
||||
id="path214"
|
||||
transform="matrix(1.0516212,0,0,0.80145135,-20.213033,20.898021)"
|
||||
d="m 209.95493,101.15277 c -0.67313,0.43587 -2.01669,1.30008 -2.01669,1.30008 -0.85322,0.5476 -1.90748,0.72789 -2.89215,0.49915 -0.98401,-0.2286 -1.80183,-0.8432 -2.23994,-1.67953 -5.44838,-10.400807 -16.60644,-30.574511 -16.60644,-30.574511 -1.03491,-1.840162 -0.3485,-4.128735 1.62421,-5.411585 0.67928,-0.441735 2.03939,-1.332269 2.03939,-1.332269 1.81519,-1.191236 4.29764,-0.767452 5.24275,0.925976 5.59588,10.02662 16.52383,30.690614 16.52383,30.690614 0.99835,1.923525 0.27865,4.317068 -1.67496,5.582075 z" />
|
||||
<path
|
||||
style="opacity:1;fill:none;fill-opacity:1"
|
||||
d="m 122.67967,139.36755 c -0.82357,-9.77509 -2.46964,-29.32102 -2.46964,-29.32102"
|
||||
id="path257" />
|
||||
</g>
|
||||
<g
|
||||
id="layer6"
|
||||
transform="matrix(1.2837662,0,0,1.2438198,-26.230112,-69.412133)"
|
||||
style="display:none;fill:url(#meshgradient1104);fill-opacity:1;stroke-width:2.56926;stroke-dasharray:none">
|
||||
<path
|
||||
id="rect248"
|
||||
style="display:inline;fill:#e8dad4;fill-opacity:1;stroke-width:2.65734"
|
||||
d="m 77.184461,185.85877 c -3.118706,-0.17264 -5.488205,2.29474 -5.304089,5.53146 0.13927,2.44833 0.417725,7.34351 0.417725,7.34351 -1.402851,0.59241 -2.653274,1.38571 -3.674486,2.33019 -0.236417,0.22009 -0.709644,0.65998 -0.709644,0.65998 -1.977545,1.8376 -2.974523,4.13549 -2.748122,6.38664 0.226626,2.25339 1.661126,4.27793 4.013076,5.63294 8.752329,5.04251 26.562156,15.20978 26.562156,15.20978 2.382687,1.37696 5.445823,1.97949 8.478093,1.67643 3.03406,-0.30324 5.79105,-1.48794 7.67929,-3.30723 0.22606,-0.21761 0.67741,-0.65271 0.67741,-0.65271 1.88467,-1.81934 2.75357,-4.12747 2.43717,-6.41566 -0.31636,-2.28783 -1.79377,-4.36696 -4.11671,-5.76332 -8.70483,-5.23272 -26.118916,-15.30199 -26.118916,-15.30199 -0.343354,-0.18115 -0.701144,-0.34676 -1.071679,-0.49602 -0.1545,-2.22112 -0.463409,-6.66204 -0.463409,-6.66204 -0.225903,-3.24762 -2.937381,-5.99923 -6.057865,-6.17196 z" />
|
||||
<path
|
||||
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:2.56926;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
||||
id="rect249"
|
||||
width="17.034109"
|
||||
height="46.878986"
|
||||
x="-35.362507"
|
||||
y="197.14072"
|
||||
ry="7.0004072"
|
||||
transform="rotate(-25.912557)"
|
||||
d="m -25.607053,190.65997 c 0.954332,0.11489 2.864353,0.34332 2.864353,0.34332 3.654339,0.43613 6.929644,3.61962 7.341128,7.17129 1.161399,10.02447 3.841123,30.33504 3.841123,30.33504 0.484383,3.61069 -2.136673,6.11741 -5.920033,5.60148 -0.98671,-0.13456 -2.964879,-0.40587 -2.964879,-0.40587 -3.801916,-0.5225 -7.270603,-3.85538 -7.733816,-7.43704 -0.727933,-5.62853 -1.524569,-12.07229 -2.279132,-18.88247 -0.69303,-6.25482 -1.162571,-11.20924 -1.162571,-11.20924 -0.337087,-3.50469 2.36905,-5.95528 6.013827,-5.51651 z" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.57015;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path249"
|
||||
transform="matrix(0.95785111,0.28726513,-0.15416741,0.98804474,0,0)"
|
||||
d="m 98.986205,156.01933 c 0.04397,2.3882 -1.279417,4.68165 -3.685404,6.38016 -2.403397,1.69667 -5.680418,2.65151 -9.059264,2.63957 -3.384867,-0.012 -6.567546,-0.99058 -8.877987,-2.71373 -2.307258,-1.72078 -3.55527,-4.04087 -3.529229,-6.42695 0.02604,-2.38578 1.32169,-4.65607 3.610982,-6.31122 2.29219,-1.65725 5.412248,-2.5773 8.720324,-2.56317 3.304267,0.0141 6.534395,0.96178 8.95209,2.64405 2.41885,1.68308 3.824526,3.96359 3.868488,6.35129 z" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:2.57357;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
||||
id="rect250"
|
||||
width="7.8031187"
|
||||
height="16.862799"
|
||||
x="-110.84097"
|
||||
y="164.54688"
|
||||
ry="3.9034963"
|
||||
transform="matrix(0.73335892,-0.67984167,0.73746343,0.67538706,0,0)"
|
||||
d="m -101.87538,159.04263 c 1.84328,0.21768 3.408551,2.02367 3.513601,4.07777 0.146486,2.86432 0.519191,8.6697 0.519191,8.6697 0.142102,2.08946 -1.251587,3.59084 -3.146462,3.34707 -1.89521,-0.24382 -3.52814,-2.13228 -3.65221,-4.20913 -0.17333,-2.90133 -0.43388,-8.62107 -0.43388,-8.62107 -0.0819,-2.0358 1.35712,-3.48195 3.19976,-3.26434 z" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:2.57425;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
||||
id="rect250-8"
|
||||
width="6.729383"
|
||||
height="14.503226"
|
||||
x="-53.165482"
|
||||
y="183.37346"
|
||||
ry="3.3572891"
|
||||
transform="matrix(0.85907063,-0.51185706,0.58537704,0.8107612,0,0)"
|
||||
d="m -48.057815,179.27958 c 0.0045,5.2e-4 0.01343,0.002 0.01343,0.002 1.684503,0.1953 3.181426,1.74377 3.35774,3.48214 0.245974,2.42518 0.771465,7.30015 0.771465,7.30015 0.191885,1.75025 -1.023546,2.9947 -2.736088,2.78403 -0.0045,-5.6e-4 -0.01362,-0.002 -0.01362,-0.002 -1.712814,-0.21071 -3.24197,-1.791 -3.423893,-3.53301 -0.254136,-2.43348 -0.724711,-7.27128 -0.724711,-7.27128 -0.163482,-1.72813 1.071529,-2.95718 2.755678,-2.76192 z" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:#ffeadd;fill-opacity:1"
|
||||
id="rect251"
|
||||
width="24.398001"
|
||||
height="29.236685"
|
||||
x="-0.062498044"
|
||||
y="210.98186"
|
||||
ry="8.1815834"
|
||||
transform="rotate(-13.890784)"
|
||||
d="m 10.397126,201.89974 c 2.668721,0.27854 8.029635,0.82565 8.029635,0.82565 4.527911,0.45965 8.540026,4.00409 8.974897,7.97222 0.411258,3.75267 1.268639,11.27213 1.268639,11.27213 0.458902,3.97834 -2.893831,6.77697 -7.584891,6.26362 -2.765816,-0.30267 -8.307752,-0.91835 -8.307752,-0.91835 -4.7137162,-0.52769 -8.7550485,-4.14078 -9.0912544,-8.07049 -0.3185485,-3.72333 -0.896104,-11.14698 -0.896104,-11.14698 -0.2937067,-3.91148 3.1144248,-6.66667 7.6068304,-6.1978 z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="layer5"
|
||||
transform="matrix(1.2055888,0,0,1.4323266,-81.666225,20.057586)"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="display:inline;fill:url(#linearGradient202)"
|
||||
id="path201"
|
||||
width="71.254013"
|
||||
height="163.40518"
|
||||
x="21.551163"
|
||||
y="-25.557249"
|
||||
d="m 24.990746,-25.557249 h 64.374846 a 3.4395833,3.4395833 45 0 1 3.439584,3.439583 V 134.40835 a 3.4395833,3.4395833 135 0 1 -3.439584,3.43958 H 24.990746 a 3.4395833,3.4395833 45 0 1 -3.439583,-3.43958 l 0,-156.526016 a 3.4395833,3.4395833 135 0 1 3.439583,-3.439583 z"
|
||||
transform="matrix(1.117986,0,0,1.0637076,65.897574,19.454539)" />
|
||||
<path
|
||||
style="fill:url(#linearGradient246)"
|
||||
id="rect244"
|
||||
width="71.254013"
|
||||
height="163.40518"
|
||||
x="21.551163"
|
||||
y="-25.557249"
|
||||
d="m 23.138663,-25.557249 h 68.079013 a 1.5875,1.5875 45 0 1 1.5875,1.5875 V 136.26043 a 1.5875,1.5875 135 0 1 -1.5875,1.5875 H 23.138663 a 1.5875,1.5875 45 0 1 -1.5875,-1.5875 V -23.969749 a 1.5875,1.5875 135 0 1 1.5875,-1.5875 z"
|
||||
transform="matrix(0.99914067,0,0,1.0180357,72.889826,22.067296)" />
|
||||
<g
|
||||
id="text246"
|
||||
style="font-size:160px;line-height:0.9;font-family:Cantarell;-inkscape-font-specification:Cantarell;white-space:pre;fill:#171924;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round"
|
||||
transform="matrix(0.46400539,0,0,0.36491553,52.372565,-26.641262)"
|
||||
aria-label="W L X">
|
||||
<path
|
||||
style="font-weight:bold;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold';text-align:center;text-anchor:middle"
|
||||
d="m 199.41504,194.08435 h -19.375 l -5,-20.85937 q -3.90625,-14.76563 -7.03125,-28.82813 l -2.73438,11.48438 -3.4375,13.82812 -6.09375,24.375 h -19.375 L 119.88379,88.693725 h 20.23437 l 6.95313,54.296875 q 0.9375,7.5 1.25,20.78125 l 1.17187,-5.9375 3.125,-13.125 7.73438,-26.48437 h 15.23437 l 7.73438,26.48437 q 1.01562,3.51563 1.71875,6.95313 0.78125,3.35937 2.57812,12.10937 l 0.15625,-2.10937 q 0.46875,-12.57813 1.25,-18.67188 l 6.5625,-54.296875 h 20.23438 z"
|
||||
id="path210" />
|
||||
<path
|
||||
style="font-weight:bold;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold';text-align:center;text-anchor:middle"
|
||||
d="M 136.36816,341.4649 V 236.07428 h 23.04688 v 87.57812 h 48.125 v 17.8125 z"
|
||||
id="path211" />
|
||||
<path
|
||||
style="font-weight:bold;-inkscape-font-specification:'Cantarell Bold';fill:#f6f6f8"
|
||||
d="m 193.8916,488.84546 h 24.8 l -44.64,-63.84 -29.76,-47.2 h -24.48 l 42.4,62.4 z m -76.96,0 h 23.68 l 27.04,-43.2 -10.4,-16.32 z m 59.2,-55.84 38.24,-55.2 h -24 l -24.96,39.04 z"
|
||||
id="path212" />
|
||||
</g>
|
||||
</g>
|
||||
<script
|
||||
id="mesh_polyfill"
|
||||
type="text/javascript">
|
||||
!function(){const t="http://www.w3.org/2000/svg",e="http://www.w3.org/1999/xlink",s="http://www.w3.org/1999/xhtml",r=2;if(document.createElementNS(t,"meshgradient").x)return;const n=(t,e,s,r)=>{let n=new x(.5*(e.x+s.x),.5*(e.y+s.y)),o=new x(.5*(t.x+e.x),.5*(t.y+e.y)),i=new x(.5*(s.x+r.x),.5*(s.y+r.y)),a=new x(.5*(n.x+o.x),.5*(n.y+o.y)),h=new x(.5*(n.x+i.x),.5*(n.y+i.y)),l=new x(.5*(a.x+h.x),.5*(a.y+h.y));return[[t,o,a,l],[l,h,i,r]]},o=t=>{let e=t[0].distSquared(t[1]),s=t[2].distSquared(t[3]),r=.25*t[0].distSquared(t[2]),n=.25*t[1].distSquared(t[3]),o=e>s?e:s,i=r>n?r:n;return 18*(o>i?o:i)},i=(t,e)=>Math.sqrt(t.distSquared(e)),a=(t,e)=>t.scale(2/3).add(e.scale(1/3)),h=t=>{let e,s,r,n,o,i,a,h=new g;return t.match(/(\w+\(\s*[^)]+\))+/g).forEach(t=>{let l=t.match(/[\w.-]+/g),d=l.shift();switch(d){case"translate":2===l.length?e=new g(1,0,0,1,l[0],l[1]):(console.error("mesh.js: translate does not have 2 arguments!"),e=new g(1,0,0,1,0,0)),h=h.append(e);break;case"scale":1===l.length?s=new g(l[0],0,0,l[0],0,0):2===l.length?s=new g(l[0],0,0,l[1],0,0):(console.error("mesh.js: scale does not have 1 or 2 arguments!"),s=new g(1,0,0,1,0,0)),h=h.append(s);break;case"rotate":if(3===l.length&&(e=new g(1,0,0,1,l[1],l[2]),h=h.append(e)),l[0]){r=l[0]*Math.PI/180;let t=Math.cos(r),e=Math.sin(r);Math.abs(t)<1e-16&&(t=0),Math.abs(e)<1e-16&&(e=0),a=new g(t,e,-e,t,0,0),h=h.append(a)}else console.error("math.js: No argument to rotate transform!");3===l.length&&(e=new g(1,0,0,1,-l[1],-l[2]),h=h.append(e));break;case"skewX":l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),o=new g(1,0,n,1,0,0),h=h.append(o)):console.error("math.js: No argument to skewX transform!");break;case"skewY":l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),i=new g(1,n,0,1,0,0),h=h.append(i)):console.error("math.js: No argument to skewY transform!");break;case"matrix":6===l.length?h=h.append(new g(...l)):console.error("math.js: Incorrect number of arguments for matrix!");break;default:console.error("mesh.js: Unhandled transform type: "+d)}}),h},l=t=>{let e=[],s=t.split(/[ ,]+/);for(let t=0,r=s.length-1;t<r;t+=2)e.push(new x(parseFloat(s[t]),parseFloat(s[t+1])));return e},d=(t,e)=>{for(let s in e)t.setAttribute(s,e[s])},c=(t,e,s,r,n)=>{let o,i,a=[0,0,0,0];for(let h=0;h<3;++h)e[h]<t[h]&&e[h]<s[h]||t[h]<e[h]&&s[h]<e[h]?a[h]=0:(a[h]=.5*((e[h]-t[h])/r+(s[h]-e[h])/n),o=Math.abs(3*(e[h]-t[h])/r),i=Math.abs(3*(s[h]-e[h])/n),a[h]>o?a[h]=o:a[h]>i&&(a[h]=i));return a},u=[[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],[-3,3,0,0,-2,-1,0,0,0,0,0,0,0,0,0,0],[2,-2,0,0,1,1,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0],[0,0,0,0,0,0,0,0,-3,3,0,0,-2,-1,0,0],[0,0,0,0,0,0,0,0,2,-2,0,0,1,1,0,0],[-3,0,3,0,0,0,0,0,-2,0,-1,0,0,0,0,0],[0,0,0,0,-3,0,3,0,0,0,0,0,-2,0,-1,0],[9,-9,-9,9,6,3,-6,-3,6,-6,3,-3,4,2,2,1],[-6,6,6,-6,-3,-3,3,3,-4,4,-2,2,-2,-2,-1,-1],[2,0,-2,0,0,0,0,0,1,0,1,0,0,0,0,0],[0,0,0,0,2,0,-2,0,0,0,0,0,1,0,1,0],[-6,6,6,-6,-4,-2,4,2,-3,3,-3,3,-2,-1,-2,-1],[4,-4,-4,4,2,2,-2,-2,2,-2,2,-2,1,1,1,1]],f=t=>{let e=[];for(let s=0;s<16;++s){e[s]=0;for(let r=0;r<16;++r)e[s]+=u[s][r]*t[r]}return e},p=(t,e,s)=>{const r=e*e,n=s*s,o=e*e*e,i=s*s*s;return t[0]+t[1]*e+t[2]*r+t[3]*o+t[4]*s+t[5]*s*e+t[6]*s*r+t[7]*s*o+t[8]*n+t[9]*n*e+t[10]*n*r+t[11]*n*o+t[12]*i+t[13]*i*e+t[14]*i*r+t[15]*i*o},y=t=>{let e=[],s=[],r=[];for(let s=0;s<4;++s)e[s]=[],e[s][0]=n(t[0][s],t[1][s],t[2][s],t[3][s]),e[s][1]=[],e[s][1].push(...n(...e[s][0][0])),e[s][1].push(...n(...e[s][0][1])),e[s][2]=[],e[s][2].push(...n(...e[s][1][0])),e[s][2].push(...n(...e[s][1][1])),e[s][2].push(...n(...e[s][1][2])),e[s][2].push(...n(...e[s][1][3]));for(let t=0;t<8;++t){s[t]=[];for(let r=0;r<4;++r)s[t][r]=[],s[t][r][0]=n(e[0][2][t][r],e[1][2][t][r],e[2][2][t][r],e[3][2][t][r]),s[t][r][1]=[],s[t][r][1].push(...n(...s[t][r][0][0])),s[t][r][1].push(...n(...s[t][r][0][1])),s[t][r][2]=[],s[t][r][2].push(...n(...s[t][r][1][0])),s[t][r][2].push(...n(...s[t][r][1][1])),s[t][r][2].push(...n(...s[t][r][1][2])),s[t][r][2].push(...n(...s[t][r][1][3]))}for(let t=0;t<8;++t){r[t]=[];for(let e=0;e<8;++e)r[t][e]=[],r[t][e][0]=s[t][0][2][e],r[t][e][1]=s[t][1][2][e],r[t][e][2]=s[t][2][2][e],r[t][e][3]=s[t][3][2][e]}return r};class x{constructor(t,e){this.x=t||0,this.y=e||0}toString(){return`(x=${this.x}, y=${this.y})`}clone(){return new x(this.x,this.y)}add(t){return new x(this.x+t.x,this.y+t.y)}scale(t){return void 0===t.x?new x(this.x*t,this.y*t):new x(this.x*t.x,this.y*t.y)}distSquared(t){let e=this.x-t.x,s=this.y-t.y;return e*e+s*s}transform(t){let e=this.x*t.a+this.y*t.c+t.e,s=this.x*t.b+this.y*t.d+t.f;return new x(e,s)}}class g{constructor(t,e,s,r,n,o){void 0===t?(this.a=1,this.b=0,this.c=0,thiLine truncated
|
||||
</script>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 57 KiB |
Reference in new issue
Block a user