Compare commits

...
155 Commits
Author SHA1 Message Date
galister 10db0f690a fix image_drm_format_modifier being a hard requirement 2024-06-22 20:58:30 +09:00
galister 072540fbb5 replace no-dmabuf feature with auto check 2024-06-22 20:55:55 +09:00
galister d225250b77 feat: openxr space rotate 2024-06-17 15:15:16 +09:00
galister c7aa88647c feat: space drag multiplier 2024-06-17 14:36:21 +09:00
galister abc132c553 cargo update 2024-06-17 10:07:00 +09:00
HavenOfTheRaven 371d3c2757 fixed error (#51) 2024-06-17 00:04:44 +00:00
HavenOfTheRaven c06ec6d974 Update Dependencies (#48)
* updated dependencies

* updated vulkano and ash with fixes
2024-06-16 14:49:45 +00:00
Ben Scholzen (DASPRiD) b3ae583cb4 fix(hid): setup xkb extension before querying keyboard device ID 2024-06-16 01:27:33 +09:00
galister 8fb9ef4a05 openvr: fix autolaunch 2024-06-14 09:28:32 +09:00
galister 40383f2529 release v0.4.1 2024-06-09 10:08:02 +09:00
alexdenerqal b2429bce7f fix(openxr): correct default bindings for move_mouse 2024-06-09 09:57:57 +09:00
galister 59c0bc870c keyboard: use fallback layout if failing to load 2024-06-09 09:53:46 +09:00
galister 4abfa3c3ac fix double-click hold not working on OpenXR 2024-06-09 00:19:42 +09:00
galister 7037740d46 try harder to find libmonado 2024-06-09 00:14:17 +09:00
galister 8ae65083f9 update cargo lock 2024-06-08 10:13:48 +09:00
galister 0fd356f306 release v0.4.0 2024-06-08 09:00:46 +09:00
galister 52a56cfd90 more detailed screen prompt 2024-06-08 08:54:07 +09:00
galister eefffccbdf ci: add libxkbcommon-x11-dev 2024-06-08 08:09:54 +09:00
galister 344eaca3a8 sysinfo o longer optional 2024-06-08 07:49:30 +09:00
galister 7d42ee9721 fix: meta/altgr key not working 2024-06-07 12:22:20 +09:00
galister bb88137533 keyboard debug logging 2024-06-07 12:15:20 +09:00
galister 8eb6df9644 warn if unable to write actions.json 2024-06-07 11:13:47 +09:00
galister a0d93e821e fix: keyboard interact element overflow 2024-06-06 16:53:39 +09:00
galister 8461dbe7ac message phrasing 2024-06-06 13:59:57 +09:00
galister d5f7181a05 feat: ensure single instance 2024-06-06 13:59:02 +09:00
galister 8bad2e1116 fix: anchor at wrong position 2024-06-06 13:27:42 +09:00
galister d5610f6fe6 fix(openxr): haptics 2024-06-06 13:02:47 +09:00
galister 95855ccea8 fix(openxr): Z order 2024-06-06 13:02:47 +09:00
galisterandYuxuan Shui 415ef91a43 feat: PipeWire capture on X11
Co-authored-by: Yuxuan Shui <yshuiv7@gmail.com>
2024-06-06 01:08:20 +09:00
galister 7641a662dc do not require ext_image_drm_format_modifier 2024-06-05 20:13:47 +09:00
galister 3a3cda7a45 feat: libxkbcommon layouts 2024-06-05 19:54:11 +09:00
galister 65d62d79fe fix: openxr playspace mover behavior 2024-06-04 23:02:55 +09:00
galister 1eebde3944 feat: save active layout 2024-06-04 20:43:59 +09:00
galister 698c447e34 fix: mouse click when pointing at 2 screens 2024-06-04 20:42:27 +09:00
galister a0a853825c fix: settings spawn in wrong spot 2024-06-04 17:51:31 +09:00
galister b8a0e3630d feat: configurable openxr bindings 2024-06-04 17:13:44 +09:00
galister 773ff6885d pipewire: better desktop compatibility (Niri) 2024-06-03 22:57:42 +09:00
galisterandRinLovesYou 4a45683650 feat(openxr): Monado playspace mover
Co-authored-by: RinLovesYou <xxklintan123xx@gmail.com>
2024-06-03 11:04:45 +09:00
galister 009626704c adjust scroll speed on analog sticks 2024-06-03 10:22:25 +09:00
nvkomata 84b08b99f7 new icon 2024-06-03 02:05:12 +02:00
galister 93a4a6702b docs: working set explanation 2024-05-09 08:59:02 +00:00
galister 6ee0cb4fd1 fix: font load error panic 2024-05-04 00:50:53 +09:00
galister 9a693862ba new logo, thanks @nvkomata 2024-05-04 00:27:43 +09:00
galister c70833b86a feat: persist layout between sessions 2024-04-25 02:13:18 +02:00
galister 5afb5ed4ab refactor 2024-04-24 06:34:01 +02:00
galister 065ce8f136 fix: dont overload xr runtime with notify spam 2024-04-24 06:34:01 +02:00
galister 1fa842bb44 feat: anchor point for working set 2024-04-24 10:19:26 +09:00
galister f6a9db5759 feat: dummy screens and battery states for uidev (#36) 2024-04-17 12:34:55 +00:00
Garrett 42ab3a3199 Add CenteredLabel (#35) 2024-04-17 05:40:04 +00:00
galister 1772e3aefd fix: uidev dynamic update + flicker (#34) 2024-04-17 04:26:28 +00:00
galister f2a5ec0c37 feat: no-dmabuf feature (#33) 2024-04-16 00:43:52 +00:00
galister 461657e4c3 fix: ui-dev on xorg (#30)
* fix: ui-dev on xorg

* recreate swapchain on resize

* only request window size after canvas size change

* only redraw after canvas changed

* redraw if run_ref failed

* redraw at 1s interval
2024-04-15 14:46:32 +00:00
galister 7ca6df6d3b log panics 2024-04-15 22:25:26 +09:00
galister be81c344d3 ci: dont run checks on pushes, only PRs 2024-04-14 18:58:48 +09:00
galister 42aef89822 feat: configurable primary_font 2024-04-14 18:16:26 +09:00
galister 7184a9f21f feat: canvas alpha 2024-04-14 17:48:44 +09:00
galister d99a58da9e feat: improve screen alighment on show/hide 2024-04-14 17:17:20 +09:00
galister 8e799d1b8f fix: request wlr-dmabuf frames in every case 2024-04-14 17:15:54 +09:00
galister 5db7524787 feat: ui dev window (#29)
* feat: ui dev window

* remove unneeded dep
2024-04-14 05:36:47 +00:00
galister 8f9c3f5e3a pw token removal instuctions 2024-04-13 23:57:32 +00:00
Chris Dandhare_ware 031efb2d79 Add bindings for WMR controllers (#28)
* Add bindings for WMR controllers

* Add support for HP Mixed Reality Controllers

---------

Co-authored-by: hare_ware <3469405-hare_ware@users.noreply.gitlab.com>
2024-04-12 09:55:43 +00:00
galister 0b087cb9a9 fix: only move mouse on depress, not release 2024-04-12 11:58:18 +09:00
Jarett Millard 5989d32075 fix: 12-hour format strings in watch.yaml (#27) 2024-04-11 01:52:15 +00:00
galister b19b0cb40d feat: log to /tmp by default 2024-04-10 16:30:26 +09:00
galister 6e2c3fc523 fix: log to file 2024-04-10 14:57:23 +09:00
galister 399168fc38 fix: ci typo 2024-04-10 10:59:07 +09:00
galister 97aab43d09 chore: deps 2024-04-10 10:48:29 +09:00
galister fc158ed6eb fix: ci pipeline 2024-04-10 08:44:08 +09:00
galister 93fe2bfca2 chore: package updates 2024-04-10 08:42:58 +09:00
galister 944b7c1835 chore: clippy 2024-04-10 08:38:19 +09:00
galister 78b51179ee fix: x11 builds 2024-04-10 08:33:27 +09:00
galister 0283916ffa add ci builds for various configs 2024-04-10 08:07:25 +09:00
alexdenerqal 63e8cd59b2 fix: loading pipewire tokens (#24) 2024-04-07 15:23:56 +00:00
alexdenerqal 1d1a4f01ae feat: add focus follows the mouse mode (#23) 2024-04-07 15:12:47 +00:00
HavenOfTheRaven 7e2f172b74 fixed compiling issue for when x11 is not included as a feature (#22) 2024-04-06 04:46:00 +00:00
galister 17fb1211ee openvr: print missing vulkan extensions 2024-04-06 06:57:03 +09:00
galister 1bee936db1 feat: persist screen visibility & curvature 2024-04-05 01:59:03 +09:00
galister e3ae9a9890 rework config system 2024-04-04 22:46:39 +09:00
galister 2d0f733c67 feat(settings-panel): auto-realign toggle 2024-04-04 21:17:26 +09:00
galister 6cb683a86b fix(openxr): move removed screens to delete_queue 2024-04-04 20:14:35 +09:00
galister dc40d7e42b docs: x11 doesn't support runtime screen changes 2024-04-04 11:10:19 +00:00
galister c02edb7a43 feat: handle screen changes at runtime 2024-04-04 20:06:53 +09:00
galister e845ac8d53 add curved screens to readme 2024-04-04 03:16:29 +00:00
alexdenerqal aae649b1f2 feat: add option to disable realigning working set windows on show/hide (#21) 2024-03-30 08:32:52 +00:00
galister 7eb5201c90 fix: only realign grabbable 2024-03-29 14:26:45 +01:00
galister 7d987f5cb8 fix: watch notifications 2024-03-29 14:09:39 +01:00
galister 106af7dd03 chore: clippy 2024-03-29 12:54:13 +01:00
alexdenerqal ab0d91ea20 feat: allow disabling the sliding of screens back and forth (#20)
fix: sliding screens with smooth trackpad scrolling
knuckles smooth trackpad scrolling can return NaN
2024-03-29 11:49:45 +00:00
galister 7f6e8909e6 feat: highlight toggle buttons when on 2024-03-29 12:48:31 +01:00
galister 4843aeef5d feat: toast topics 2024-03-29 12:21:12 +01:00
galister 810c0cac63 feat: ipd label 2024-03-28 11:51:15 +01:00
galister 1438d3e7eb add known auto-start issue 2024-03-28 10:03:09 +00:00
galister 5ab506e192 Release v0.3.2 2024-03-23 21:55:09 +01:00
galister 7ab036024d chore: more clippy 2024-03-23 16:05:42 +01:00
galister e70aaa002e chore: clippy 2024-03-23 15:41:54 +01:00
galister a1d3b9684d fix: end async work on soft-shutdown 2024-03-23 15:35:01 +01:00
galister 9fe9832a8d openxr: separate release threshold for grab, click, altclick 2024-03-21 21:47:08 +01:00
galister 0b813aeb10 docs: wivrn love + update known issues 2024-03-21 21:10:23 +01:00
galister 155f653f32 omg: curved screens 2024-03-21 21:04:20 +01:00
galister 82374a60a0 Release v0.3.1 2024-03-19 18:19:32 +01:00
galister 0d41bf1a3e openvr: correct size for portrait overlays 2024-03-19 18:16:23 +01:00
galister 3907e089de openvr: re-upload line texs on show-hide to prevent disappearing lasers 2024-03-19 17:16:35 +01:00
Aleksander 72bac6465e Fix ui_transform (#17) 2024-03-19 16:14:56 +00:00
Stuart Irwin 271e037692 Save screen scaling changes (#16) 2024-03-17 21:22:18 +00:00
galister bc78450c6a openxr: use squeeze/force for knuckles 2024-03-17 17:11:25 +01:00
galister dce634edd1 openxr: click/grab/altclick sensitivity option 2024-03-17 16:41:32 +01:00
galister 69d14eebe5 fix when desktop origin other than (0,0) 2024-03-17 16:30:16 +01:00
galister 54dfb297ac improve error handling 2024-03-17 16:11:05 +01:00
galister 1d1230b1e7 chore: update wlx-capture & other deps 2024-03-17 13:08:49 +01:00
galister 67b6b90158 fonts: dont panic on load error 2024-03-17 11:25:11 +01:00
galister 31ec035db7 pipewire: log screen capture start failures 2024-03-16 11:27:10 +01:00
galister deb43f0d94 do not log toasts 2024-03-11 09:07:35 +01:00
Stuart Irwin 4434d82981 Add uptime timer module (#15) 2024-03-11 07:51:43 +00:00
libjared 8ba5d68e5a Update keyboard.yaml (#14)
- update virtual key list URL
- remove references to xmodmap
- ensure layouts are consistent with length
- remove alt_layout for now
2024-03-10 22:31:54 +00:00
galister 0c6e319fbe Release v0.3.0 2024-03-10 21:40:14 +01:00
galister 4679961cb2 openvr: smoother rotate 2024-03-10 20:56:13 +01:00
galister cb12357d68 openvr: dont overwrite bindings 2024-03-10 16:55:40 +01:00
galister b401fb28fd openvr: space rotate, recenter height adjust 2024-03-10 16:28:37 +01:00
galister 06478bbfc9 openvr: recenter rework 2024-03-10 14:40:50 +01:00
galister b57b77b06a openvr: reset offsets reloads from disk 2024-03-10 14:08:12 +01:00
galister 5a077e394c openvr: reload playspace on scene app change 2024-03-10 13:23:00 +01:00
galister a08c260898 openvr: chaperone movement fix 2024-03-10 13:19:08 +01:00
galister 45d08cd89b x11: fix cursor size 2024-03-10 00:10:28 +01:00
galister 49b9cc361d x11: fix screen gamma 2024-03-10 00:02:10 +01:00
galister 2f8dcaf110 use linked openxr 2024-03-09 21:21:48 +01:00
galister 473c6e2038 cleanup 2024-03-09 17:15:37 +01:00
galister 4c91470b5c Release v0.2.6 2024-03-09 16:54:52 +01:00
galister 85e8e80538 update ovr_overlay 2024-03-09 01:59:11 +01:00
galister 49e1e8639d Release v0.2.5 2024-03-09 01:26:32 +01:00
galister 88bcd42e56 openvr: seated space drag 2024-03-09 01:24:24 +01:00
galister 38395c9183 openvr: universe-aware overlays 2024-03-08 23:08:30 +01:00
galister 19bc4a5df2 Release v0.2.4 2024-03-08 00:11:03 +01:00
galister ed096fffce fix space drag 2024-03-08 00:00:49 +01:00
galister cd297376a8 openvr: throttle using WaitFrameSync 2024-03-07 17:57:07 +01:00
galister e7456a9b0a WIP: space drag rework 2024-03-07 13:17:12 +01:00
galister 507c0677c8 ShowUi & ShowMirror: no implicit show/hide 2024-03-06 20:43:40 +01:00
galister 1d9b60fc07 update ovr_overlay 2024-03-06 19:09:57 +01:00
galister 85d9ea9127 simplify a thing 2024-03-06 19:03:40 +01:00
galister a55ade6741 modular-ui: use boxes where it makes sense 2024-03-06 18:45:14 +01:00
galister 5800d7d99d refactor: graphics type aliases 2024-03-06 18:41:34 +01:00
galister 27d77ad638 clippy 2024-03-06 18:20:56 +01:00
galister 2aed560950 osc: only interactable in openOverlayCount 2024-03-06 18:20:20 +01:00
galister 1a70ea4fcc settings: notifications section rearrange 2024-03-06 18:18:27 +01:00
galister c73a01fb7f watch: save fade settings 2024-03-06 18:16:34 +01:00
galister 97fd6c1fc0 openvr: support appimage autostart 2024-03-06 18:14:33 +01:00
galister 39ce2a7c00 increase X11 screen width cap to 2560 2024-03-02 03:00:44 +01:00
galister 0030ad43eb add instructions for input group 2024-03-02 03:00:44 +01:00
galister 3006e52795 Update README.md 2024-03-02 00:11:10 +00:00
galister cfb432622e add building from source & aur 2024-03-01 22:51:56 +01:00
galister b49add2e89 reduce warnings when --no-default-features 2024-03-01 22:26:45 +01:00
galister da1936823f fix build 2024-03-01 22:14:37 +01:00
galister f42572d4cb fix more no-default-features builds 2024-03-01 22:13:06 +01:00
galister d562d4b236 fix dependency tree when --no-default-features 2024-03-01 21:43:50 +01:00
galister 75dcfc8096 fix pw required with no wayland feature 2024-03-01 21:39:54 +01:00
galister c49e8b66b8 fix p 2024-03-01 20:19:44 +01:00
62 changed files with 7937 additions and 2501 deletions

No files matched your search

+28
View File
@@ -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
+28
View File
@@ -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
+28
View File
@@ -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 -3
View File
@@ -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"
@@ -37,7 +41,7 @@ jobs:
- name: Package AppImage
run: |
export VERSION=$GITHUB_REF_NAME
./linuxdeploy-x86_64.AppImage -dAppRun.desktop -iWlxOverlay.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
./linuxdeploy-x86_64.AppImage -dwlx-overlay-s.desktop -iwlx-overlay-s.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
mv WlxOverlay-S-$VERSION-x86_64.AppImage WlxOverlay-S-x86_64.AppImage
- name: Create Release
Generated
+1904 -801
View File
File diff suppressed because it is too large. Load diff
+52 -32
View File
@@ -4,53 +4,73 @@ debug = true
[package]
name = "wlx-overlay-s"
version = "0.2.3"
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.147"
log = "0.4.20"
once_cell = "1.18.0"
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" }
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 = []
+37 -13
View File
@@ -18,24 +18,40 @@ We are available on either:
Questions/issues specific to WlxOverlay-S will be handled in the `wlxoverlay` chat room.
# First Setup
# Setup
Recommend grabbing [rustup](https://rustup.rs/) if you don't have it yet.
1. Grab the latest AppImage from [Releases](https://github.com/galister/wlx-overlay-s/releases).
1. `chmod +x WlxOverlay-S-*.AppImage`
1. Start Monado, WiVRn or SteamVR.
1. Run the overlay
Start Monado or SteamVR.
AUR package is [wlx-overlay-s-git](https://aur.archlinux.org/packages/wlx-overlay-s-git).
```sh
cargo run --release
```
You may also want to [build from source](https://github.com/galister/wlx-overlay-s/wiki/Building-from-Source).
# First Start
**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. \
@@ -64,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
@@ -87,20 +105,26 @@ 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
## SteamVR: laser pointers not visible
## Auto-Start not working on SteamVR 2.4.4
This seems to be a rare issue with SteamVR startup overlays. Restarting the overlay will fix the issue.
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.
## Scroll wheel doesn't work
This seems to be an issue specific to Electron apps (Discord, Element, Slack, Spotify) on Wayland. Scrolling will work when using these in your web browser.
## WiVRn support
While WiVRn technically supports EXTX_overlay, I do not recommend using this software with WiVRn at this time, due to WiVRn not being optimized for overlay apps. You will likely get ghosting or stuttering while rotating your head.
## 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 -134
View File
@@ -1,26 +1,27 @@
use std::{
collections::{BinaryHeap, VecDeque},
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};
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 {
@@ -37,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>
@@ -51,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>> {
@@ -146,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);
}
}
@@ -161,114 +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(
source: &Affine3A,
source_fwd: Vec3A,
plane: &Affine3A,
plane_norm: Vec3A,
) -> Option<(Vec3A, f32)> {
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);
if dist < 0.0 {
// plane is behind the caster
return None;
}
let hit_pos = source.translation + ray_dir * dist;
Some((hit_pos, dist))
}
+202 -34
View File
@@ -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, 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(),
}
@@ -71,6 +70,9 @@ impl InputState {
if hand.now.space_drag != hand.before.space_drag {
log::debug!("Hand {}: space_drag {}", hand.idx, hand.now.space_drag);
}
if hand.now.space_rotate != hand.before.space_rotate {
log::debug!("Hand {}: space_rotate {}", hand.idx, hand.now.space_rotate);
}
if hand.now.click_modifier_right != hand.before.click_modifier_right {
log::debug!(
"Hand {}: click_modifier_right {}",
@@ -159,6 +161,7 @@ impl Default for InteractionState {
pub struct Pointer {
pub idx: usize,
pub pose: Affine3A,
pub raw_pose: Affine3A,
pub now: PointerState,
pub before: PointerState,
pub last_click: Instant,
@@ -171,6 +174,7 @@ impl Pointer {
Self {
idx,
pose: Affine3A::IDENTITY,
raw_pose: Affine3A::IDENTITY,
now: Default::default(),
before: Default::default(),
last_click: Instant::now(),
@@ -187,8 +191,10 @@ pub struct PointerState {
pub alt_click: bool,
pub show_hide: bool,
pub space_drag: bool,
pub space_rotate: bool,
pub click_modifier_right: bool,
pub click_modifier_middle: bool,
pub move_mouse: bool,
}
#[derive(Debug, Clone, Copy, Default)]
@@ -228,7 +234,8 @@ impl InteractionHandler for DummyInteractionHandler {
#[derive(Debug, Clone, Copy, Default)]
struct RayHit {
overlay: usize,
hit_pos: Vec3A,
global_pos: Vec3A,
local_pos: Vec2,
dist: f32,
}
@@ -236,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)]
@@ -274,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;
@@ -340,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 {
@@ -356,7 +371,28 @@ where
if pointer.now.scroll.abs() > 0.1 {
let scroll = pointer.now.scroll;
hovered.backend.on_scroll(app, &hit, scroll);
if app.input_state.pointers[1 - idx]
.interaction
.grabbed
.is_some_and(|x| x.grabbed_id == hit.overlay)
{
let is_portrait = hovered.view().is_some_and(|v| {
let extent = v.image().extent();
extent[0] >= extent[1]
});
if is_portrait {
let cur = hovered.state.curvature.unwrap_or(0.0);
let new = (cur - scroll * 0.01).min(0.35);
if new <= f32::EPSILON {
hovered.state.curvature = None;
} else {
hovered.state.curvature = Some(new);
}
}
} else {
hovered.backend.on_scroll(app, &hit, scroll);
}
pointer = &mut app.input_state.pointers[idx];
}
@@ -387,7 +423,11 @@ impl Pointer {
continue;
}
if let Some(hit) = self.ray_test(overlay.state.id, &overlay.state.transform) {
if let Some(hit) = self.ray_test(
overlay.state.id,
&overlay.state.transform,
&overlay.state.curvature,
) {
if hit.dist.is_infinite() || hit.dist.is_nan() {
continue;
}
@@ -402,12 +442,8 @@ impl Pointer {
let uv = overlay
.state
.transform
.inverse()
.transform_point3a(hit.hit_pos)
.truncate();
let uv = overlay.state.interaction_transform.transform_point2(uv);
.interaction_transform
.transform_point2(hit.local_pos);
if uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 {
continue;
@@ -426,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,
{
@@ -438,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 {
@@ -463,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);
@@ -474,24 +531,135 @@ 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);
}
}
fn ray_test(&self, overlay: usize, plane: &Affine3A) -> Option<RayHit> {
let Some((hit_pos, dist)) = raycast(&self.pose, Vec3A::NEG_Z, plane, Vec3A::NEG_Z) else {
fn ray_test(
&self,
overlay: usize,
transform: &Affine3A,
curvature: &Option<f32>,
) -> Option<RayHit> {
let (dist, local_pos) = match curvature {
Some(curvature) => raycast_cylinder(&self.pose, Vec3A::NEG_Z, transform, *curvature),
_ => raycast_plane(&self.pose, Vec3A::NEG_Z, transform, Vec3A::NEG_Z),
}?;
if dist < 0.0 {
// hit is behind us
return None;
};
}
Some(RayHit {
overlay,
hit_pos,
global_pos: self.pose.transform_point3a(Vec3A::NEG_Z * dist),
local_pos,
dist,
})
}
}
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()))
}
+5
View File
@@ -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;
+37 -39
View File
@@ -1,17 +1,23 @@
use dbus::{blocking::Connection, channel::MatchingReceiver, message::MatchRule};
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use std::{
path::PathBuf,
sync::{mpsc, Arc},
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
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>,
tx_toast: mpsc::SyncSender<Toast>,
dbus_data: Option<Connection>,
running: Arc<AtomicBool>,
}
impl NotificationManager {
@@ -21,6 +27,7 @@ impl NotificationManager {
rx_toast,
tx_toast,
dbus_data: None,
running: Arc::new(AtomicBool::new(true)),
}
}
@@ -96,7 +103,7 @@ impl NotificationManager {
let sender2 = self.tx_toast.clone();
let result = c.add_match(rule_with_eavesdrop, move |_: (), _, msg| {
if let Ok(toast) = parse_dbus(&msg) {
if let Ok(toast) = parse_dbus(msg) {
match sender2.try_send(toast) {
Ok(_) => {}
Err(e) => {
@@ -125,7 +132,7 @@ impl NotificationManager {
pub fn run_udp(&mut self) {
let sender = self.tx_toast.clone();
// NOTE: We're detaching the thread, as there's no simple way to gracefully stop it other than app shutdown.
let running = self.running.clone();
let _ = std::thread::spawn(move || {
let addr = "127.0.0.1:42069";
let socket = match std::net::UdpSocket::bind(addr) {
@@ -135,9 +142,13 @@ impl NotificationManager {
return;
}
};
if let Err(err) = socket.set_read_timeout(Some(Duration::from_millis(200))) {
log::error!("Failed to set read timeout: {:?}", err);
}
let mut buf = [0u8; 1024 * 16]; // vrcx embeds icons as b64
loop {
while running.load(Ordering::Relaxed) {
if let Ok((num_bytes, _)) = socket.recv_from(&mut buf) {
let json_str = match std::str::from_utf8(&buf[..num_bytes]) {
Ok(s) => s,
@@ -158,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(_) => {}
@@ -170,10 +185,17 @@ impl NotificationManager {
}
}
}
log::info!("Notification listener stopped.");
});
}
}
impl Drop for NotificationManager {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
}
}
fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
let mut args = msg.iter_init();
let app_name: String = args.read()?;
@@ -188,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
}
@@ -211,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(())
}
+5 -5
View File
@@ -4,15 +4,15 @@ 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;
fn from_affine(affine: &Affine3A) -> Self;
fn to_affine(&self) -> Affine3A;
}
impl Affine3AConvert for Matrix3x4 {
fn from_affine(affine: Affine3A) -> Self {
fn from_affine(affine: &Affine3A) -> Self {
Matrix3x4([
[
affine.matrix3.x_axis.x,
@@ -46,7 +46,7 @@ impl Affine3AConvert for Matrix3x4 {
}
impl Affine3AConvert for HmdMatrix34_t {
fn from_affine(affine: Affine3A) -> Self {
fn from_affine(affine: &Affine3A) -> Self {
HmdMatrix34_t {
m: [
[
@@ -101,7 +101,7 @@ impl From<OVRError> for BackendError {
use cstr::cstr;
const STEAMVR_SECTION: &CStr = cstr!("steamvr");
const COLOR_GAIN_CSTR: [&'static CStr; 3] = [
const COLOR_GAIN_CSTR: [&CStr; 3] = [
cstr!("hmdDisplayColorGainR"),
cstr!("hmdDisplayColorGainG"),
cstr!("hmdDisplayColorGainB"),
+35 -9
View File
@@ -36,8 +36,10 @@ const PATH_SCROLL: &str = "/actions/default/in/Scroll";
const PATH_ALT_CLICK: &str = "/actions/default/in/AltClick";
const PATH_SHOW_HIDE: &str = "/actions/default/in/ShowHide";
const PATH_SPACE_DRAG: &str = "/actions/default/in/SpaceDrag";
const PATH_SPACE_ROTATE: &str = "/actions/default/in/SpaceRotate";
const PATH_CLICK_MODIFIER_RIGHT: &str = "/actions/default/in/ClickModifierRight";
const PATH_CLICK_MODIFIER_MIDDLE: &str = "/actions/default/in/ClickModifierMiddle";
const PATH_MOVE_MOUSE: &str = "/actions/default/in/MoveMouse";
const INPUT_ANY: InputValueHandle = InputValueHandle(ovr_overlay::sys::k_ulInvalidInputValueHandle);
@@ -50,12 +52,15 @@ pub(super) struct OpenVrInputSource {
alt_click_hnd: ActionHandle,
show_hide_hnd: ActionHandle,
space_drag_hnd: ActionHandle,
space_rotate_hnd: ActionHandle,
click_modifier_right_hnd: ActionHandle,
click_modifier_middle_hnd: ActionHandle,
move_mouse_hnd: ActionHandle,
}
pub(super) struct OpenVrHandSource {
has_pose: bool,
device: Option<TrackedDeviceIndex>,
input_hnd: InputValueHandle,
pose_hnd: ActionHandle,
haptics_hnd: ActionHandle,
@@ -71,8 +76,10 @@ impl OpenVrInputSource {
let alt_click_hnd = input.get_action_handle(PATH_ALT_CLICK)?;
let show_hide_hnd = input.get_action_handle(PATH_SHOW_HIDE)?;
let space_drag_hnd = input.get_action_handle(PATH_SPACE_DRAG)?;
let space_rotate_hnd = input.get_action_handle(PATH_SPACE_ROTATE)?;
let click_modifier_right_hnd = input.get_action_handle(PATH_CLICK_MODIFIER_RIGHT)?;
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()
@@ -91,6 +98,7 @@ impl OpenVrInputSource {
let hands: [OpenVrHandSource; 2] = array::from_fn(|i| OpenVrHandSource {
has_pose: false,
device: None,
input_hnd: input_hnd[i],
pose_hnd: pose_hnd[i],
haptics_hnd: haptics_hnd[i],
@@ -104,8 +112,10 @@ impl OpenVrInputSource {
alt_click_hnd,
show_hide_hnd,
space_drag_hnd,
space_rotate_hnd,
click_modifier_right_hnd,
click_modifier_middle_hnd,
move_mouse_hnd,
hands,
})
}
@@ -124,6 +134,7 @@ impl OpenVrInputSource {
pub fn update(
&mut self,
universe: ETrackingUniverseOrigin,
input: &mut InputManager,
system: &mut SystemManager,
app: &mut AppState,
@@ -138,12 +149,19 @@ impl OpenVrInputSource {
let _ = input.update_actions(&mut [aas]);
let universe = ETrackingUniverseOrigin::TrackingUniverseStanding;
let devices = system.get_device_to_absolute_tracking_pose(universe.clone(), 0.005);
app.input_state.hmd = devices[0].mDeviceToAbsoluteTracking.to_affine();
for i in 0..2 {
let hand = &mut self.hands[i];
let app_hand = &mut app.input_state.pointers[i];
if let Some(device) = hand.device {
app_hand.raw_pose = devices[device.0 as usize]
.mDeviceToAbsoluteTracking
.to_affine();
}
hand.has_pose = false;
let _ = input
@@ -183,6 +201,11 @@ impl OpenVrInputSource {
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.space_rotate = input
.get_digital_action_data(self.space_rotate_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.click_modifier_right = input
.get_digital_action_data(self.click_modifier_right_hnd, hand.input_hnd)
.map(|x| x.0.bState)
@@ -193,19 +216,20 @@ 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)
.unwrap_or(0.0);
}
let devices = system.get_device_to_absolute_tracking_pose(universe, 0.005);
app.input_state.hmd = devices[0].mDeviceToAbsoluteTracking.to_affine();
}
pub fn update_devices(&mut self, system: &mut SystemManager, app: &mut AppState) {
app.input_state.devices.clear();
for idx in 0..TrackedDeviceIndex::MAX {
let device = TrackedDeviceIndex::new(idx as _).unwrap(); // safe
if !system.is_tracked_device_connected(device) {
@@ -220,9 +244,11 @@ impl OpenVrInputSource {
let role = system.get_controller_role_for_tracked_device_index(device);
match role {
ETrackedControllerRole::TrackedControllerRole_LeftHand => {
self.hands[0].device = Some(device);
TrackedDeviceRole::LeftHand
}
ETrackedControllerRole::TrackedControllerRole_RightHand => {
self.hands[1].device = Some(device);
TrackedDeviceRole::RightHand
}
_ => continue,
@@ -283,8 +309,6 @@ fn get_tracked_device(
}
Some(TrackedDevice {
valid: true,
index,
soc,
charging,
role,
@@ -294,8 +318,10 @@ fn get_tracked_device(
pub fn set_action_manifest(input: &mut InputManager) -> anyhow::Result<()> {
let action_path = CONFIG_ROOT_PATH.join("actions.json");
if !action_path.is_file() {
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");
+11 -2
View File
@@ -5,6 +5,7 @@ use std::sync::Arc;
use glam::{Affine3A, Vec3, Vec3A, Vec4};
use idmap::IdMap;
use ovr_overlay::overlay::OverlayManager;
use ovr_overlay::sys::ETrackingUniverseOrigin;
use vulkano::command_buffer::CommandBufferUsage;
use vulkano::format::Format;
use vulkano::image::view::ImageView;
@@ -74,11 +75,12 @@ impl LinePool {
}),
data: OpenVrOverlayData {
width: 0.002,
override_width: true,
..Default::default()
},
..Default::default()
};
data.data.sort_order = 69;
data.state.z_order = 69;
data.state.dirty = true;
self.lines.insert(id, data);
@@ -141,6 +143,7 @@ impl LinePool {
pub fn update(
&mut self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
app: &mut AppState,
) -> anyhow::Result<()> {
@@ -152,12 +155,18 @@ impl LinePool {
data.state.dirty = false;
}
data.upload_transform(overlay);
data.upload_transform(universe.clone(), overlay);
data.upload_color(overlay);
}
}
Ok(())
}
pub fn mark_dirty(&mut self) {
for data in self.lines.values_mut() {
data.state.dirty = true;
}
}
}
struct StaticRenderer {
+10 -4
View File
@@ -9,12 +9,18 @@ use crate::config_io::CONFIG_ROOT_PATH;
const APP_KEY: &str = "galister.wlxoverlay-s";
pub(super) fn install_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Result<()> {
let executable_pathbuf = std::env::current_exe()?;
let executable_path = executable_pathbuf
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid executable path"))?;
let manifest_path = CONFIG_ROOT_PATH.join("wlx-overlay-s.vrmanifest");
let appimage_path = std::env::var("APPIMAGE");
let executable_pathbuf = std::env::current_exe()?;
let executable_path = match appimage_path {
Ok(ref path) => path,
Err(_) => executable_pathbuf
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid executable path"))?,
};
if let Ok(true) = app_mgr.is_application_installed(APP_KEY) {
if let Ok(mut file) = File::open(&manifest_path) {
let mut buf = String::new();
+95 -43
View File
@@ -1,7 +1,7 @@
use std::{
collections::VecDeque,
sync::{
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
time::{Duration, Instant},
@@ -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;
@@ -46,6 +48,8 @@ pub mod manifest;
pub mod overlay;
pub mod playspace;
static FRAME_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub fn openvr_uninstall() {
let app_type = EVRApplicationType::VRApplication_Overlay;
let Ok(context) = ovr_overlay::Context::init(app_type) else {
@@ -67,15 +71,15 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
log::info!("Using OpenVR runtime");
let mut app_mgr = context.applications_mngr();
let mut input_mngr = context.input_mngr();
let mut system_mngr = context.system_mngr();
let mut overlay_mngr = context.overlay_mngr();
let mut settings_mngr = context.settings_mngr();
let mut input_mgr = context.input_mngr();
let mut system_mgr = context.system_mngr();
let mut overlay_mgr = context.overlay_mngr();
let mut settings_mgr = context.settings_mngr();
let mut chaperone_mgr = context.chaperone_setup_mngr();
let mut compositor_mngr = context.compositor_mngr();
let mut compositor_mgr = context.compositor_mngr();
let device_extensions_fn = |device: &PhysicalDevice| {
let names = compositor_mngr.get_vulkan_device_extensions_required(device.handle().as_raw());
let names = compositor_mgr.get_vulkan_device_extensions_required(device.handle().as_raw());
let ext = DeviceExtensions::from_iter(names.iter().map(|s| s.as_str()));
ext
};
@@ -91,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)?;
@@ -98,18 +110,18 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
notifications.run_dbus();
notifications.run_udp();
let mut space_mover = playspace::PlayspaceMover::new();
let mut playspace = playspace::PlayspaceMover::new();
playspace.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
#[cfg(feature = "osc")]
let mut osc_sender =
crate::backend::osc::OscSender::new(state.session.config.osc_out_port).ok();
state.hid_provider.set_desktop_extent(overlays.extent);
set_action_manifest(&mut input_mgr)?;
set_action_manifest(&mut input_mngr)?;
let mut input_source = OpenVrInputSource::new(&mut input_mgr)?;
let mut input_source = OpenVrInputSource::new(&mut input_mngr)?;
let Ok(refresh_rate) = system_mngr.get_tracked_device_property::<f32>(
let Ok(refresh_rate) = system_mgr.get_tracked_device_property::<f32>(
TrackedDeviceIndex::HMD,
ETrackedDeviceProperty::Prop_DisplayFrequency_Float,
) else {
@@ -122,7 +134,9 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let watch_id = overlays.get_by_name(WATCH_NAME).unwrap().state.id; // want panic
let frame_time = (1000.0 / refresh_rate).floor() * 0.001;
// want at least half refresh rate
let frame_timeout = 2 * (1000.0 / refresh_rate).floor() as u32;
let mut next_device_update = Instant::now();
let mut due_tasks = VecDeque::with_capacity(4);
@@ -130,12 +144,16 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let pointer_lines = [lines.allocate(), lines.allocate()];
'main_loop: loop {
let _ = overlay_mgr.wait_frame_sync(frame_timeout);
if !running.load(Ordering::Relaxed) {
log::warn!("Received shutdown signal.");
break 'main_loop;
}
while let Some(event) = system_mngr.poll_next_event() {
let cur_frame = FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
while let Some(event) = system_mgr.poll_next_event() {
match event.event_type {
EVREventType::VREvent_Quit => {
log::warn!("Received quit event, shutting down.");
@@ -146,24 +164,56 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
| EVREventType::VREvent_TrackedDeviceUpdated => {
next_device_update = Instant::now();
}
EVREventType::VREvent_SeatedZeroPoseReset
| EVREventType::VREvent_StandingZeroPoseReset
| EVREventType::VREvent_ChaperoneUniverseHasChanged
| 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;
}
}
_ => {}
}
}
if next_device_update <= Instant::now() {
input_source.update_devices(&mut system_mngr, &mut state);
input_source.update_devices(&mut system_mgr, &mut state);
next_device_update = Instant::now() + Duration::from_secs(30);
}
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) => {
@@ -171,9 +221,10 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
continue;
};
let Some((state, backend)) = f(&mut state) else {
let Some((mut state, backend)) = f(&mut state) else {
continue;
};
state.birthframe = cur_frame;
overlays.add(OverlayData {
state,
@@ -183,26 +234,35 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
}
TaskType::DropOverlay(sel) => {
if let Some(o) = overlays.mut_by_selector(&sel) {
o.destroy(&mut overlay_mngr);
overlays.remove_by_selector(&sel);
if o.state.birthframe < cur_frame {
o.destroy(&mut overlay_mgr);
overlays.remove_by_selector(&sel);
}
}
}
TaskType::System(task) => match task {
SystemTask::ColorGain(channel, value) => {
let _ = adjust_gain(&mut settings_mngr, channel, value);
let _ = adjust_gain(&mut settings_mgr, channel, value);
}
SystemTask::FixFloor => {
space_mover.fix_floor(&mut chaperone_mgr, &state.input_state);
playspace.fix_floor(&mut chaperone_mgr, &state.input_state);
}
SystemTask::ResetPlayspace => {
space_mover.reset_offset(&mut chaperone_mgr);
playspace.reset_offset(&mut chaperone_mgr, &state.input_state);
}
},
}
}
let universe = playspace.get_universe();
state.input_state.pre_update();
input_source.update(&mut input_mngr, &mut system_mngr, &mut state);
input_source.update(
universe.clone(),
&mut input_mgr,
&mut system_mgr,
&mut state,
);
state.input_state.post_update();
if state
@@ -211,6 +271,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
.iter()
.any(|p| p.now.show_hide && !p.before.show_hide)
{
lines.mark_dirty(); // workaround to prevent lines from not showing
overlays.show_hide(&mut state);
}
@@ -219,7 +280,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
.for_each(|o| o.state.auto_movement(&mut state));
watch_fade(&mut state, overlays.mut_by_id(watch_id).unwrap()); // want panic
space_mover.update(&mut chaperone_mgr, &mut overlays, &state);
playspace.update(&mut chaperone_mgr, &mut overlays, &state);
let lengths_haptics = interact(&mut overlays, &mut state);
for (idx, (len, haptics)) in lengths_haptics.iter().enumerate() {
@@ -231,14 +292,16 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
&state.input_state.hmd,
);
if let Some(haptics) = haptics {
input_source.haptics(&mut input_mngr, idx, haptics)
input_source.haptics(&mut input_mgr, idx, haptics)
}
}
lines.update(&mut overlay_mngr, &mut state)?;
state.hid_provider.commit();
lines.update(universe.clone(), &mut overlay_mgr, &mut state)?;
for o in overlays.iter_mut() {
o.after_input(&mut overlay_mngr, &mut state)?;
o.after_input(&mut overlay_mgr, &mut state)?;
}
#[cfg(feature = "osc")]
@@ -258,22 +321,11 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
overlays
.iter_mut()
.for_each(|o| o.after_render(&mut overlay_mngr, &state.graphics));
.for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &state.graphics));
// chaperone
// close font handles?
state.hid_provider.on_new_frame();
let mut seconds_since_vsync = 0f32;
std::thread::sleep(Duration::from_secs_f32(
if system_mngr.get_time_since_last_vsync(&mut seconds_since_vsync, &mut 0u64) {
(frame_time - seconds_since_vsync).max(0.0)
} else {
frame_time
},
));
}
log::warn!("OpenVR shutdown");
+31 -20
View File
@@ -1,3 +1,5 @@
use core::f32;
use glam::Vec4;
use ovr_overlay::{
overlay::{OverlayHandle, OverlayManager},
@@ -9,7 +11,6 @@ use vulkano::{Handle, VulkanObject};
use crate::{
backend::overlay::{OverlayData, RelativeTo},
graphics::WlxGraphics,
overlays::watch::WATCH_NAME,
state::AppState,
};
@@ -21,9 +22,8 @@ pub(super) struct OpenVrOverlayData {
pub(super) last_image: Option<u64>,
pub(super) visible: bool,
pub(super) color: Vec4,
pub(super) curvature: f32,
pub(super) sort_order: u32,
pub(crate) width: f32,
pub(super) override_width: bool,
pub(super) relative_to: RelativeTo,
}
@@ -43,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;
@@ -79,10 +74,17 @@ impl OverlayData<OpenVrOverlayData> {
Ok(())
}
pub(super) fn after_render(&mut self, overlay: &mut OverlayManager, graphics: &WlxGraphics) {
pub(super) fn after_render(
&mut self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
graphics: &WlxGraphics,
) {
if self.data.visible {
if self.state.dirty {
self.upload_transform(overlay);
self.upload_curvature(overlay);
self.upload_transform(universe, overlay);
self.upload_alpha(overlay);
self.state.dirty = false;
}
@@ -166,7 +168,7 @@ impl OverlayData<OpenVrOverlayData> {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
if let Err(e) = overlay.set_curvature(handle, self.data.curvature) {
if let Err(e) = overlay.set_curvature(handle, self.state.curvature.unwrap_or(0.0)) {
log::error!(
"{}: Failed to set overlay curvature: {}",
self.state.name,
@@ -180,24 +182,24 @@ 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);
}
}
pub(super) fn upload_transform(&self, overlay: &mut OverlayManager) {
pub(super) fn upload_transform(
&self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
) {
let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
let transform = Matrix3x4::from_affine(self.state.transform);
let transform = Matrix3x4::from_affine(&self.state.transform);
if let Err(e) = overlay.set_transform_absolute(
handle,
ETrackingUniverseOrigin::TrackingUniverseStanding,
&transform,
) {
if let Err(e) = overlay.set_transform_absolute(handle, universe, &transform) {
log::error!(
"{}: Failed to set overlay transform: {}",
self.state.name,
@@ -228,6 +230,15 @@ impl OverlayData<OpenVrOverlayData> {
}
let dimensions = image.extent();
if !self.data.override_width {
let new_width = ((dimensions[0] as f32) / (dimensions[1] as f32)).min(1.0);
if (new_width - self.data.width).abs() > f32::EPSILON {
log::info!("{}: New width {}", self.state.name, new_width);
self.data.width = new_width;
self.upload_width(overlay);
}
}
let format = image.format();
let mut texture = VRVulkanTextureData_t {
@@ -242,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,
+249 -44
View File
@@ -1,25 +1,35 @@
use glam::Vec3A;
use ovr_overlay::{chaperone_setup::ChaperoneSetupManager, sys::EChaperoneConfigFile};
use glam::{Affine3A, Quat, Vec3, Vec3A};
use ovr_overlay::{
chaperone_setup::ChaperoneSetupManager,
compositor::CompositorManager,
sys::{EChaperoneConfigFile, ETrackingUniverseOrigin, HmdMatrix34_t},
};
use crate::{
backend::{common::OverlayContainer, input::InputState},
state::AppState,
};
use super::overlay::OpenVrOverlayData;
use super::{helpers::Affine3AConvert, overlay::OpenVrOverlayData};
struct MoverData<T> {
pose: Affine3A,
hand: usize,
hand_pose: T,
}
pub(super) struct PlayspaceMover {
drag_hand: Option<usize>,
offset: Vec3A,
start_position: Vec3A,
universe: ETrackingUniverseOrigin,
drag: Option<MoverData<Vec3A>>,
rotate: Option<MoverData<Quat>>,
}
impl PlayspaceMover {
pub fn new() -> Self {
Self {
drag_hand: None,
offset: Vec3A::ZERO,
start_position: Vec3A::ZERO,
universe: ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated,
drag: None,
rotate: None,
}
}
@@ -29,68 +39,263 @@ impl PlayspaceMover {
overlays: &mut OverlayContainer<OpenVrOverlayData>,
state: &AppState,
) {
if let Some(hand) = self.drag_hand {
let pointer = &state.input_state.pointers[hand];
if !pointer.now.space_drag {
self.drag_hand = None;
log::info!("End space drag");
let universe = self.universe.clone();
if let Some(data) = self.rotate.as_mut() {
let pointer = &state.input_state.pointers[data.hand];
if !pointer.now.space_rotate {
self.rotate = None;
log::info!("End space rotate");
return;
}
let hand_pos = state.input_state.pointers[hand].pose.translation;
let relative_pos = hand_pos - self.start_position;
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 += relative_pos * -1.0;
overlay.state.transform.translation =
overlay_transform.transform_point3a(overlay.state.transform.translation);
}
});
self.offset += relative_pos;
self.apply_offset(chaperone_mgr);
data.pose *= space_transform;
data.hand_pose = new_hand;
if self.universe == ETrackingUniverseOrigin::TrackingUniverseStanding {
apply_chaperone_transform(space_transform.inverse(), chaperone_mgr);
}
set_working_copy(&universe, chaperone_mgr, &data.pose);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
} else {
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
if pointer.now.space_rotate {
let Some(mat) = get_working_copy(&universe, chaperone_mgr) else {
log::warn!("Can't space rotate - failed to get zero pose");
return;
};
let hand_pose = Quat::from_affine3(&(mat * pointer.raw_pose));
self.rotate = Some(MoverData {
pose: mat,
hand: i,
hand_pose,
});
self.drag = None;
log::info!("Start space rotate");
return;
}
}
}
if let Some(data) = self.drag.as_mut() {
let pointer = &state.input_state.pointers[data.hand];
if !pointer.now.space_drag {
self.drag = None;
log::info!("End space drag");
return;
}
let new_hand = data
.pose
.transform_point3a(state.input_state.pointers[data.hand].raw_pose.translation);
let relative_pos =
(new_hand - data.hand_pose) * state.session.config.space_drag_multiplier;
if relative_pos.length_squared() > 1000.0 {
log::warn!("Space drag too fast, ignoring");
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;
if self.universe == ETrackingUniverseOrigin::TrackingUniverseStanding {
apply_chaperone_offset(overlay_offset, chaperone_mgr);
}
set_working_copy(&universe, chaperone_mgr, &data.pose);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
} else {
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
if pointer.now.space_drag {
self.drag_hand = Some(i);
self.start_position = pointer.pose.translation;
let Some(mat) = get_working_copy(&universe, chaperone_mgr) else {
log::warn!("Can't space drag - failed to get zero pose");
return;
};
let hand_pos = mat.transform_point3a(pointer.raw_pose.translation);
self.drag = Some(MoverData {
pose: mat,
hand: i,
hand_pose: hand_pos,
});
self.rotate = None;
log::info!("Start space drag");
break;
return;
}
}
}
}
pub fn reset_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager) {
self.offset = Vec3A::ZERO;
self.apply_offset(chaperone_mgr);
pub fn reset_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager, input: &InputState) {
let mut height = 1.6;
if let Some(mat) = get_working_copy(&self.universe, chaperone_mgr) {
height = input.hmd.translation.y - mat.translation.y;
if self.universe == ETrackingUniverseOrigin::TrackingUniverseStanding {
apply_chaperone_transform(mat, chaperone_mgr);
}
}
let xform = if self.universe == ETrackingUniverseOrigin::TrackingUniverseSeated {
Affine3A::from_translation(Vec3::NEG_Y * height)
} else {
Affine3A::IDENTITY
};
set_working_copy(&self.universe, chaperone_mgr, &xform);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
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;
}
}
pub fn fix_floor(&mut self, chaperone_mgr: &mut ChaperoneSetupManager, input: &InputState) {
let y1 = input.pointers[0].pose.translation.y;
let y2 = input.pointers[1].pose.translation.y;
self.offset.y += y1.min(y2) - 0.03;
self.apply_offset(chaperone_mgr);
}
pub fn reset(&mut self) {
self.offset = Vec3A::ZERO;
self.start_position = Vec3A::ZERO;
}
fn apply_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager) {
let Some(mut zero_pose) =
chaperone_mgr.get_working_standing_zero_pose_to_raw_tracking_pose()
else {
log::warn!("Can't space drag - failed to get zero pose");
let Some(mut mat) = get_working_copy(&self.universe, chaperone_mgr) else {
log::warn!("Can't fix floor - failed to get zero pose");
return;
};
let offset = y1.min(y2) - 0.03;
mat.translation.y += offset;
zero_pose.m[0][3] = self.offset.x;
zero_pose.m[1][3] = self.offset.y;
zero_pose.m[2][3] = self.offset.z;
chaperone_mgr.set_working_standing_zero_pose_to_raw_tracking_pose(&zero_pose);
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(
&mut self,
compositor_mgr: &mut CompositorManager,
_chaperone_mgr: &mut ChaperoneSetupManager,
) {
let new_universe = compositor_mgr.get_tracking_space();
if new_universe != self.universe {
log::info!(
"Playspace changed: {} -> {}",
universe_str(&self.universe),
universe_str(&new_universe)
);
self.universe = new_universe;
}
if self.drag.is_some() {
log::info!("Space drag interrupted by external change");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by external change");
self.rotate = None;
}
}
pub fn get_universe(&self) -> ETrackingUniverseOrigin {
self.universe.clone()
}
}
fn universe_str(universe: &ETrackingUniverseOrigin) -> &'static str {
match universe {
ETrackingUniverseOrigin::TrackingUniverseSeated => "Seated",
ETrackingUniverseOrigin::TrackingUniverseStanding => "Standing",
ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated => "Raw",
}
}
fn get_working_copy(
universe: &ETrackingUniverseOrigin,
chaperone_mgr: &mut ChaperoneSetupManager,
) -> Option<Affine3A> {
chaperone_mgr.revert_working_copy();
let mat = match universe {
ETrackingUniverseOrigin::TrackingUniverseStanding => {
chaperone_mgr.get_working_standing_zero_pose_to_raw_tracking_pose()
}
_ => chaperone_mgr.get_working_seated_zero_pose_to_raw_tracking_pose(),
};
mat.map(|m| m.to_affine())
}
fn set_working_copy(
universe: &ETrackingUniverseOrigin,
chaperone_mgr: &mut ChaperoneSetupManager,
mat: &Affine3A,
) {
let mat = HmdMatrix34_t::from_affine(mat);
match universe {
ETrackingUniverseOrigin::TrackingUniverseStanding => {
chaperone_mgr.set_working_standing_zero_pose_to_raw_tracking_pose(&mat)
}
_ => chaperone_mgr.set_working_seated_zero_pose_to_raw_tracking_pose(&mat),
};
}
fn apply_chaperone_offset(offset: Vec3A, chaperone_mgr: &mut ChaperoneSetupManager) {
let mut quads = chaperone_mgr.get_live_collision_bounds_info();
quads.iter_mut().for_each(|quad| {
quad.vCorners.iter_mut().for_each(|corner| {
corner.v[0] += offset.x;
corner.v[2] += offset.z;
});
});
chaperone_mgr.set_working_collision_bounds_info(quads.as_mut_slice());
}
fn apply_chaperone_transform(transform: Affine3A, chaperone_mgr: &mut ChaperoneSetupManager) {
let mut quads = chaperone_mgr.get_live_collision_bounds_info();
quads.iter_mut().for_each(|quad| {
quad.vCorners.iter_mut().for_each(|corner| {
let coord = transform.transform_point3a(Vec3A::from_slice(&corner.v));
corner.v[0] = coord.x;
corner.v[2] = coord.z;
});
});
chaperone_mgr.set_working_collision_bounds_info(quads.as_mut_slice());
}
+81 -27
View File
@@ -1,12 +1,13 @@
use std::path::PathBuf;
use anyhow::{bail, ensure};
use glam::{Affine3A, Quat, Vec3};
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> {
let Ok(entry) = (unsafe { xr::Entry::load() }) else {
bail!("OpenXR Loader not found.");
};
let entry = xr::Entry::linked();
let Ok(available_extensions) = entry.enumerate_extensions() else {
bail!("Failed to enumerate OpenXR extensions.");
@@ -23,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 = [];
@@ -76,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,
@@ -111,42 +124,31 @@ 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)
(Affine3A::from_rotation_translation(rot, pos), ipd)
}
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()
}
pub(super) fn transform_to_posef(transform: &Affine3A) -> xr::Posef {
let translation = transform.translation;
pub(super) fn transform_to_norm_quat(transform: &Affine3A) -> Quat {
let norm_mat3 = transform
.matrix3
.mul_scalar(1.0 / transform.matrix3.x_axis.length());
let mut rotation = Quat::from_mat3a(&norm_mat3).normalize();
Quat::from_mat3a(&norm_mat3).normalize()
}
pub(super) fn translation_rotation_to_posef(translation: Vec3A, mut rotation: Quat) -> xr::Posef {
if !rotation.is_finite() {
rotation = Quat::IDENTITY;
}
@@ -165,3 +167,55 @@ pub(super) fn transform_to_posef(transform: &Affine3A) -> xr::Posef {
},
}
}
pub(super) fn transform_to_posef(transform: &Affine3A) -> xr::Posef {
let translation = transform.translation;
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
}
+406 -331
View File
@@ -2,10 +2,12 @@ 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},
state::AppState,
config_io,
state::{AppSession, AppState},
};
use super::XrState;
@@ -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,16 +187,11 @@ 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 {
self.hands[i].update(
&mut state.input_state.pointers[i],
&xr.stage,
&xr.session,
xr.predicted_display_time,
)?;
self.hands[i].update(&mut state.input_state.pointers[i], xr, &state.session)?;
}
Ok(())
}
@@ -121,13 +209,12 @@ impl OpenXrHand {
}
pub(super) fn update(
&self,
&mut self,
pointer: &mut Pointer,
stage: &xr::Space,
session: &XrSession,
time: xr::Time,
) -> Result<(), xr::sys::Result> {
let location = self.space.locate(stage, time)?;
xr: &XrState,
session: &AppSession,
) -> anyhow::Result<()> {
let location = self.space.locate(&xr.stage, xr.predicted_display_time)?;
if location
.location_flags
.contains(xr::SpaceLocationFlags::ORIENTATION_VALID)
@@ -140,47 +227,55 @@ impl OpenXrHand {
pointer.now.click = self
.source
.action_click
.state(session, xr::Path::NULL)?
.current_state
> 0.7;
.state(pointer.before.click, xr, session)?;
pointer.now.grab = self
.source
.action_grab
.state(session, xr::Path::NULL)?
.current_state
> 0.7;
.state(pointer.before.grab, xr, session)?;
pointer.now.scroll = self
.source
.action_scroll
.state(session, xr::Path::NULL)?
.state(&xr.session, xr::Path::NULL)?
.current_state;
pointer.now.alt_click = self
.source
.action_alt_click
.state(session, xr::Path::NULL)?
.current_state
> 0.7;
pointer.now.alt_click =
self.source
.action_alt_click
.state(pointer.before.alt_click, xr, session)?;
pointer.now.show_hide = self
.source
.action_show_hide
.state(session, xr::Path::NULL)?
.current_state;
pointer.now.show_hide =
self.source
.action_show_hide
.state(pointer.before.show_hide, xr, session)?;
pointer.now.click_modifier_right = self
.source
.action_click_modifier_right
.state(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_middle = self
.source
.action_click_modifier_middle
.state(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.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(())
}
@@ -188,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),
@@ -238,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/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_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)
}
+3 -4
View File
@@ -19,7 +19,7 @@ use crate::{
use super::{
swapchain::{create_swapchain_render_data, SwapchainRenderData},
XrState,
CompositionLayer, XrState,
};
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
@@ -135,7 +135,7 @@ impl LinePool {
&'a mut self,
xr: &'a XrState,
command_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<Vec<xr::CompositionLayerQuad<xr::Vulkan>>> {
) -> anyhow::Result<Vec<CompositionLayer>> {
let mut quads = Vec::new();
for line in self.lines.values_mut() {
@@ -148,14 +148,13 @@ 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,
height: inner.length,
});
quads.push(quad);
quads.push(CompositionLayer::Quad(quad));
}
}
+84 -27
View File
@@ -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,19 +289,25 @@ 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;
}
if let Some(quad) = o.present_xr(&xr_state, &mut command_buffer)? {
layers.push((dist_sq, quad));
};
let maybe_layer = o.present_xr(&xr_state, &mut command_buffer)?;
if let CompositionLayer::None = maybe_layer {
continue;
}
layers.push((dist_sq, maybe_layer));
}
for quad in lines.present_xr(&xr_state, &mut command_buffer)? {
layers.push((0.0, quad));
for maybe_layer in lines.present_xr(&xr_state, &mut command_buffer)? {
if let CompositionLayer::None = maybe_layer {
continue;
}
layers.push((0.0, maybe_layer));
}
command_buffer.build_and_execute_now()?;
@@ -290,7 +316,11 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let frame_ref = layers
.iter()
.map(|f| &f.1 as &xr::CompositionLayerBase<xr::Vulkan>)
.map(|f| match f.1 {
CompositionLayer::Quad(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
CompositionLayer::Cylinder(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
CompositionLayer::None => unreachable!(),
})
.collect::<Vec<_>>();
frame_stream.end(
@@ -299,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);
@@ -308,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) => {
@@ -315,9 +352,10 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
continue;
};
let Some((state, backend)) = f(&mut app_state) else {
let Some((mut state, backend)) = f(&mut app_state) else {
continue;
};
state.birthframe = cur_frame;
overlays.add(OverlayData {
state,
@@ -326,23 +364,42 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
});
}
TaskType::DropOverlay(sel) => {
let 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
if let Some(o) = overlays.mut_by_selector(&sel) {
if o.state.birthframe < cur_frame {
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) => 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;
}
Ok(())
}
pub(super) enum CompositionLayer<'a> {
None,
Quad(xr::CompositionLayerQuad<'a, xr::Vulkan>),
Cylinder(xr::CompositionLayerCylinderKHR<'a, xr::Vulkan>),
}
+243
View File
@@ -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",
},
},
]
+36 -18
View File
@@ -1,8 +1,9 @@
use glam::Vec3A;
use openxr as xr;
use std::sync::Arc;
use xr::{CompositionLayerFlags, EyeVisibility};
use std::{f32::consts::PI, sync::Arc};
use xr::EyeVisibility;
use super::{helpers, swapchain::SwapchainRenderData, XrState};
use super::{helpers, swapchain::SwapchainRenderData, CompositionLayer, XrState};
use crate::{
backend::{openxr::swapchain::create_swapchain_render_data, overlay::OverlayData},
graphics::WlxCommandBuffer,
@@ -23,7 +24,7 @@ impl OverlayData<OpenXrOverlayData> {
&'a mut self,
xr: &'a XrState,
command_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<Option<xr::CompositionLayerQuad<xr::Vulkan>>> {
) -> anyhow::Result<CompositionLayer> {
if let Some(new_view) = self.view() {
self.data.last_view = Some(new_view);
}
@@ -32,7 +33,7 @@ impl OverlayData<OpenXrOverlayData> {
view.clone()
} else {
log::warn!("{}: Will not show - image not ready", self.state.name);
return Ok(None);
return Ok(CompositionLayer::None);
};
let extent = my_view.image().extent();
@@ -55,10 +56,8 @@ impl OverlayData<OpenXrOverlayData> {
};
let sub_image = data.acquire_present_release(command_buffer, my_view, self.state.alpha)?;
let posef = helpers::transform_to_posef(&self.state.transform);
let aspect_ratio = extent[1] as f32 / extent[0] as f32;
let (scale_x, scale_y) = if aspect_ratio < 1.0 {
let major = self.state.transform.matrix3.col(0).length();
(major, major * aspect_ratio)
@@ -67,17 +66,36 @@ impl OverlayData<OpenXrOverlayData> {
(major / aspect_ratio, major)
};
let quad = xr::CompositionLayerQuad::new()
.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,
height: scale_y,
});
Ok(Some(quad))
if let Some(curvature) = self.state.curvature {
let radius = scale_x / (2.0 * PI * curvature);
let quat = helpers::transform_to_norm_quat(&self.state.transform);
let center_point = self.state.transform.translation + quat.mul_vec3a(Vec3A::Z * radius);
let posef = helpers::translation_rotation_to_posef(center_point, quat);
let angle = 2.0 * (scale_x / (2.0 * radius));
let cylinder = xr::CompositionLayerCylinderKHR::new()
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
.space(&xr.stage)
.radius(radius)
.central_angle(angle)
.aspect_ratio(aspect_ratio);
Ok(CompositionLayer::Cylinder(cylinder))
} else {
let posef = helpers::transform_to_posef(&self.state.transform);
let quad = xr::CompositionLayerQuad::new()
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
.space(&xr.stage)
.size(xr::Extent2Df {
width: scale_x,
height: scale_y,
});
Ok(CompositionLayer::Quad(quad))
}
}
pub(super) fn after_input(&mut self, app: &mut AppState) -> anyhow::Result<()> {
+246
View File
@@ -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 => {}
}
}
}
+5 -1
View File
@@ -65,7 +65,11 @@ impl OscSender {
match o.state.name.as_ref() {
WATCH_NAME => has_wrist = true,
KEYBOARD_NAME => has_keyboard = true,
_ => num_overlays += 1,
_ => {
if o.state.interactable {
num_overlays += 1
}
}
}
}
+92 -34
View File
@@ -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,17 +29,20 @@ 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>,
pub interaction_transform: Affine2,
pub birthframe: usize,
}
impl Default for OverlayState {
@@ -49,17 +55,20 @@ impl Default for OverlayState {
grabbable: false,
recenter: false,
interactable: false,
anchored: false,
dirty: true,
alpha: 1.0,
z_order: 0,
relative_to: RelativeTo::None,
saved_point: None,
saved_scale: None,
curvature: 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,
birthframe: 0,
}
}
}
@@ -91,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;
}
@@ -177,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<()> {
@@ -231,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 {
@@ -251,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)
@@ -285,10 +335,18 @@ impl InteractionHandler for SplitOverlayBackend {
}
pub fn ui_transform(extent: &[u32; 2]) -> Affine2 {
let aspect = extent[0] as f32 / extent[1] as f32;
let scale = if aspect < 1.0 {
Vec2 {
x: 1.0 / aspect,
y: -1.0,
}
} else {
Vec2 {
x: 1.0,
y: -1.0 * aspect,
}
};
let center = Vec2 { x: 0.5, y: 0.5 };
Affine2::from_cols(
Vec2::X,
Vec2::NEG_Y * (extent[0] as f32 / extent[1] as f32),
center,
)
Affine2::from_scale_angle_translation(scale, 0.0, center)
}
+113
View File
@@ -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);
}
}
}
+316
View File
@@ -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,
});
}
+262 -51
View File
@@ -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 {
@@ -42,11 +113,11 @@ fn def_one() -> f32 {
1.0
}
fn def_half() -> f32 {
pub fn def_half() -> f32 {
0.5
}
fn def_point7() -> f32 {
pub fn def_point7() -> f32 {
0.7
}
@@ -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,10 +200,49 @@ 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>,
#[serde(default = "def_point7")]
pub xr_grab_sensitivity: f32,
#[serde(default = "def_point7")]
pub xr_click_sensitivity: f32,
#[serde(default = "def_point7")]
pub xr_alt_click_sensitivity: f32,
#[serde(default = "def_half")]
pub xr_grab_sensitivity_release: f32,
#[serde(default = "def_half")]
pub xr_click_sensitivity_release: f32,
#[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 {
@@ -136,18 +262,24 @@ impl GeneralConfig {
}
fn post_load(&self) {
GeneralConfig::sanitize_range("keyboard_scale", self.keyboard_scale, 0.0, 5.0);
GeneralConfig::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.0, 5.0);
GeneralConfig::sanitize_range("keyboard_scale", self.keyboard_scale, 0.05, 5.0);
GeneralConfig::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.05, 5.0);
}
}
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)]
@@ -155,6 +287,7 @@ pub enum ConfigType {
Keyboard,
Watch,
Settings,
Anchor,
}
pub fn load_known_yaml<T>(config_type: ConfigType) -> T
@@ -165,14 +298,12 @@ where
let file_name = FILES[config_type as usize];
let maybe_override = config_io::load(file_name);
for yaml in [maybe_override.as_deref(), Some(fallback)].iter() {
if let Some(yaml_data) = yaml {
match serde_yaml::from_str::<T>(yaml_data) {
Ok(d) => return d,
Err(e) => {
error!("Failed to parse {}, falling back to defaults.", file_name);
error!("{}", e);
}
for yaml in [maybe_override.as_deref(), Some(fallback)].iter().flatten() {
match serde_yaml::from_str::<T>(yaml) {
Ok(d) => return d,
Err(e) => {
error!("Failed to parse {}, falling back to defaults.", file_name);
error!("{}", e);
}
}
}
@@ -189,35 +320,115 @@ 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();
if let Ok(paths_unsorted) = std::fs::read_dir(path_conf_d) {
// Sort paths alphabetically
let mut paths: Vec<_> = paths_unsorted.map(|r| r.unwrap()).collect(); // TODO safe unwrap?
paths.sort_by_key(|dir| dir.path());
for path in paths {
if !path.file_type().unwrap().is_file() {
// TODO safe unwrap?
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());
}
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 yaml_data.is_empty() {
yaml_data.push_str(load_with_fallback!("config.yaml", "res/config.yaml").as_str());
if let Ok(paths_unsorted) = std::fs::read_dir(path_conf_d) {
let mut paths: Vec<_> = paths_unsorted
.filter_map(|r| match r {
Ok(entry) => Some(entry),
Err(e) => {
error!("Failed to read conf.d directory: {}", e);
None
}
})
.collect();
// Sort paths alphabetically
paths.sort_by_key(|dir| dir.path());
for path in paths {
log::info!("Loading config file: {}", path.path().to_string_lossy());
settings_builder = settings_builder.add_source(File::from(path.path()));
}
}
serde_yaml::from_str(&yaml_data).expect("Failed to parse config.yaml")
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);
}
}
}
// 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(())
}
-16
View File
@@ -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()
}
};
}
+272 -92
View File
@@ -10,14 +10,20 @@ use ash::vk::SubmitInfo;
use smallvec::smallvec;
#[cfg(feature = "openvr")]
use vulkano::{
device::physical::PhysicalDeviceType, instance::InstanceCreateFlags,
instance::InstanceExtensions,
};
use vulkano::{device::physical::PhysicalDeviceType, instance::InstanceCreateFlags};
#[cfg(feature = "openxr")]
use {ash::vk, std::os::raw::c_void};
pub type Vert2Buf = Subbuffer<[Vert2Uv]>;
pub type IndexBuf = Subbuffer<[u16]>;
pub type LegacyPipeline = WlxPipeline<WlxPipelineLegacy>;
pub type DynamicPipeline = WlxPipeline<WlxPipelineDynamic>;
pub type LegacyPass = WlxPass<WlxPipelineLegacy>;
pub type DynamicPass = WlxPass<WlxPipelineDynamic>;
use vulkano::{
buffer::{
allocator::{SubbufferAllocator, SubbufferAllocatorCreateInfo},
@@ -36,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::{
@@ -47,7 +53,7 @@ use vulkano::{
Image, ImageCreateInfo, ImageLayout, ImageTiling, ImageType, ImageUsage, SampleCount,
SubresourceLayout,
},
instance::{Instance, InstanceCreateInfo},
instance::{Instance, InstanceCreateInfo, InstanceExtensions},
memory::{
allocator::{
AllocationCreateInfo, GenericMemoryAllocatorCreateInfo, MemoryAllocator,
@@ -86,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 {
@@ -121,12 +129,21 @@ pub struct WlxGraphics {
pub command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
pub descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
pub quad_verts: Subbuffer<[Vert2Uv]>,
pub quad_indices: Subbuffer<[u16]>,
pub quad_verts: Vert2Buf,
pub quad_indices: IndexBuf,
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
@@ -175,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);
@@ -185,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 _,
@@ -237,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()
@@ -258,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;
@@ -310,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()
@@ -360,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);
@@ -376,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()
@@ -428,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,
@@ -440,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"))?;
@@ -475,9 +519,143 @@ 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<(Subbuffer<[Vert2Uv]>, Subbuffer<[u16]>)> {
) -> anyhow::Result<(Vert2Buf, IndexBuf)> {
let vertices = [
Vert2Uv {
in_pos: [0., 0.],
@@ -535,7 +713,7 @@ impl WlxGraphics {
y: f32,
w: f32,
h: f32,
) -> anyhow::Result<Subbuffer<[Vert2Uv]>> {
) -> anyhow::Result<Vert2Buf> {
let rw = width;
let rh = height;
@@ -594,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(
@@ -615,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()
},
@@ -707,8 +891,8 @@ impl WlxGraphics {
frag: Arc<ShaderModule>,
format: Format,
blend: Option<AttachmentBlend>,
) -> anyhow::Result<Arc<WlxPipeline<WlxPipelineLegacy>>> {
Ok(Arc::new(WlxPipeline::<WlxPipelineLegacy>::new(
) -> anyhow::Result<Arc<LegacyPipeline>> {
Ok(Arc::new(LegacyPipeline::new(
render_target,
self.clone(),
vert,
@@ -727,8 +911,8 @@ impl WlxGraphics {
blend: Option<AttachmentBlend>,
initial_layout: ImageLayout,
final_layout: ImageLayout,
) -> anyhow::Result<Arc<WlxPipeline<WlxPipelineLegacy>>> {
Ok(Arc::new(WlxPipeline::<WlxPipelineLegacy>::new_with_layout(
) -> anyhow::Result<Arc<LegacyPipeline>> {
Ok(Arc::new(LegacyPipeline::new_with_layout(
render_target,
self.clone(),
vert,
@@ -747,8 +931,8 @@ impl WlxGraphics {
frag: Arc<ShaderModule>,
format: Format,
blend: Option<AttachmentBlend>,
) -> anyhow::Result<Arc<WlxPipeline<WlxPipelineDynamic>>> {
Ok(Arc::new(WlxPipeline::<WlxPipelineDynamic>::new(
) -> anyhow::Result<Arc<DynamicPipeline>> {
Ok(Arc::new(DynamicPipeline::new(
self.clone(),
vert,
frag,
@@ -824,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(),
)
}
@@ -844,10 +1025,7 @@ pub struct WlxCommandBuffer {
#[allow(dead_code)]
impl WlxCommandBuffer {
pub fn begin_render_pass(
&mut self,
pipeline: &WlxPipeline<WlxPipelineLegacy>,
) -> anyhow::Result<()> {
pub fn begin_render_pass(&mut self, pipeline: &LegacyPipeline) -> anyhow::Result<()> {
self.command_buffer.begin_render_pass(
RenderPassBeginInfo {
clear_values: vec![Some([0.0, 0.0, 0.0, 0.0].into())],
@@ -983,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),
@@ -1034,11 +1212,11 @@ impl WlxPipeline<WlxPipelineDynamic> {
pub fn create_pass(
self: &Arc<Self>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<WlxPass<WlxPipelineDynamic>> {
WlxPass::<WlxPipelineDynamic>::new(
) -> anyhow::Result<DynamicPass> {
DynamicPass::new(
self.clone(),
dimensions,
vertex_buffer,
@@ -1140,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),
@@ -1203,11 +1381,11 @@ impl WlxPipeline<WlxPipelineLegacy> {
pub fn create_pass(
self: &Arc<Self>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<WlxPass<WlxPipelineLegacy>> {
WlxPass::<WlxPipelineLegacy>::new(
) -> anyhow::Result<LegacyPass> {
LegacyPass::new(
self.clone(),
dimensions,
vertex_buffer,
@@ -1281,18 +1459,18 @@ impl<D> WlxPipeline<D> {
#[allow(dead_code)]
pub struct WlxPass<D> {
pipeline: Arc<WlxPipeline<D>>,
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
pub command_buffer: Arc<CommandBuffer>,
}
impl WlxPass<WlxPipelineLegacy> {
fn new(
pipeline: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline: Arc<LegacyPipeline>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<Self> {
let viewport = Viewport {
@@ -1349,10 +1527,10 @@ impl WlxPass<WlxPipelineLegacy> {
impl WlxPass<WlxPipelineDynamic> {
fn new(
pipeline: Arc<WlxPipeline<WlxPipelineDynamic>>,
pipeline: Arc<DynamicPipeline>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<Self> {
let viewport = Viewport {
@@ -1411,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),
}
}
+43 -23
View File
@@ -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,10 +114,9 @@ impl FontCache {
return *font;
}
let pattern_str = format!("{PRIMARY_FONT}-{size}:style=bold:charset={cp:04x}");
let mut pattern =
OwnedPattern::from_str(&pattern_str).expect("Failed to create fontconfig pattern");
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);
pattern.default_substitute();
@@ -124,12 +132,22 @@ impl FontCache {
let font_idx = pattern.face_index().unwrap_or(0);
let face = self
.ft
.new_face(path, font_idx as _)
.expect("Failed to load font face");
face.set_char_size(size << 6, size << 6, 96, 96)
.expect("Failed to set font size");
let face = match self.ft.new_face(path, font_idx as _) {
Ok(face) => face,
Err(e) => {
log::warn!("Failed to load font at {}: {:?}", path, e);
coll.cp_map.insert(cp, 0);
return 0;
}
};
match face.set_char_size(size << 6, size << 6, 96, 96) {
Ok(_) => {}
Err(e) => {
log::warn!("Failed to set font size: {:?}", e);
coll.cp_map.insert(cp, 0);
return 0;
}
};
let idx = coll.fonts.len();
for cp in 0..0xFFFF {
@@ -137,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);
}
}
@@ -156,11 +174,10 @@ impl FontCache {
let font = Font { face, glyphs };
coll.fonts.push(font);
idx
} else {
coll.cp_map.insert(cp, 0);
0
return idx;
}
coll.cp_map.insert(cp, 0);
0
}
fn get_glyph_for_cp(
@@ -171,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();
@@ -198,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
View File
@@ -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,
}
+166 -84
View File
@@ -1,27 +1,25 @@
use std::{
f32::consts::PI,
io::Cursor,
ops::Add,
process::{self, Child},
sync::Arc,
time::{Duration, Instant},
};
use glam::{Quat, Vec3A};
use rodio::{Decoder, Source};
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::{
mirror,
toast::Toast,
watch::{save_watch, WATCH_NAME},
toast::{Toast, ToastTopic},
watch::WATCH_NAME,
},
state::AppState,
};
@@ -43,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)]
@@ -125,6 +134,9 @@ pub enum ButtonAction {
channel: ColorChannel,
delta: f32,
},
DragMultiplier {
delta: f32,
},
System {
action: SystemAction,
},
@@ -170,13 +182,15 @@ 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) {
button.state = Some(ModularData::Button(data.clone()));
button.state = Some(ModularData::Button(Box::new(data.clone())));
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(
@@ -268,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),
@@ -279,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;
@@ -290,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));
@@ -306,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(),
)
@@ -321,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(),
@@ -338,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(),
@@ -352,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);
}
}
}
@@ -386,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) => {
@@ -401,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(),
)
@@ -413,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);
}
}),
));
@@ -426,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);
@@ -475,8 +551,8 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(WATCH_NAME.into()),
Box::new(move |app, o| {
o.spawn_rotation = o.spawn_rotation * rot;
app.session.config.watch_rot = o.spawn_rotation.into();
o.spawn_rotation *= rot;
app.session.config.watch_rot = o.spawn_rotation;
o.dirty = true;
}),
));
@@ -492,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;
}),
));
@@ -507,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);
}),
));
}
@@ -520,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),
}
}
}),
));
}
@@ -532,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);
}
}),
));
@@ -551,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);
}
}),
));
@@ -577,47 +682,32 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
}
}
#[cfg(feature = "wayland")]
fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
use crate::overlays::custom;
let selector = OverlaySelector::Name(window.clone());
match action {
WindowAction::ShowMirror => {
app.tasks.enqueue(TaskType::Overlay(
selector,
Box::new(|app, o| {
o.want_visible = !o.want_visible;
if o.recenter {
o.show_hide = o.want_visible;
o.reset(app, false);
}
}),
));
#[cfg(feature = "wayland")]
app.tasks.enqueue(TaskType::CreateOverlay(
OverlaySelector::Name(window.clone()),
Box::new({
let name = window.clone();
move |app| {
Toast::new("Check your desktop for popup.".into(), "".into())
.with_sound(true)
.submit(app);
mirror::new_mirror(name.clone(), false, &app.session)
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)
}
}),
));
#[cfg(not(feature = "wayland"))]
log::warn!("Mirror not available without Wayland feature.");
}
WindowAction::ShowUi => {
app.tasks.enqueue(TaskType::Overlay(
selector,
Box::new(|app, o| {
o.want_visible = !o.want_visible;
if o.recenter {
o.show_hide = o.want_visible;
o.reset(app, false);
}
}),
));
app.tasks.enqueue(TaskType::CreateOverlay(
OverlaySelector::Name(window.clone()),
Box::new({
@@ -633,16 +723,8 @@ fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
}
}
#[cfg(not(feature = "wayland"))]
fn run_action_window(_: Arc<str>, _: &WindowAction, _: &mut AppState) {
log::warn!("Cannot run Window action without Wayland feature.");
}
const THUMP_AUDIO_WAV: &[u8] = include_bytes!("../../res/380885.wav");
fn audio_thump(app: &mut AppState) {
if let Some(handle) = app.audio.get_handle() {
let wav = include_bytes!("../../res/380885.wav");
let cursor = Cursor::new(wav);
let source = Decoder::new_wav(cursor).unwrap();
let _ = handle.play_raw(source.convert_samples());
}
app.audio.play(THUMP_AUDIO_WAV);
}
+46 -10
View File
@@ -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")]
@@ -29,32 +29,45 @@ pub enum LabelContent {
format: Arc<str>,
timezone: Option<Arc<str>>,
},
Timer {
format: Arc<str>,
},
Battery {
device: usize,
low_threshold: f32,
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>,
timezone: Option<Tz>,
},
Timer {
format: Arc<str>,
start: Instant,
},
Exec {
last_exec: Instant,
interval: f32,
command: Vec<Arc<str>>,
child: Option<process::Child>,
},
Ipd {
last_ipd: f32,
},
DragMultiplier,
}
pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
@@ -75,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
})
});
@@ -85,6 +98,10 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
timezone: tz,
})
}
LabelContent::Timer { format } => Some(LabelData::Timer {
format: format.clone(),
start: Instant::now(),
}),
LabelContent::Exec { command, interval } => Some(LabelData::Exec {
last_exec: Instant::now(),
interval: *interval,
@@ -92,13 +109,15 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
child: None,
}),
LabelContent::Static { text } => {
label.set_text(&text);
label.set_text(text);
None
}
LabelContent::Ipd => Some(LabelData::Ipd { last_ipd: 0. }),
LabelContent::DragMultiplier => Some(LabelData::DragMultiplier),
};
if let Some(state) = state {
label.state = Some(ModularData::Label(state));
label.state = Some(ModularData::Label(Box::new(state)));
label.on_update = Some(label_update);
}
}
@@ -107,7 +126,7 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
let ModularData::Label(data) = control.state.as_mut().unwrap() else {
panic!("Label control has no state");
};
match data {
match data.as_mut() {
LabelData::Battery {
device,
low_threshold,
@@ -137,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);
@@ -155,6 +174,14 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
control.set_text(&format!("{}", &date.format(&format)));
}
}
LabelData::Timer { format, start } => {
let mut format = format.clone().to_lowercase();
let duration = start.elapsed().as_secs();
format = format.replace("%s", &format!("{:02}", (duration % 60)));
format = format.replace("%m", &format!("{:02}", ((duration / 60) % 60)));
format = format.replace("%h", &format!("{:02}", ((duration / 60) / 60)));
control.set_text(&format);
}
LabelData::Exec {
last_exec,
interval,
@@ -219,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));
}
}
}
+39 -22
View File
@@ -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,
@@ -70,7 +76,7 @@ pub enum ModularElement {
bg_color: Arc<str>,
text: Arc<str>,
#[serde(flatten)]
data: ButtonData,
data: Box<ButtonData>,
},
/// Convenience type to save you from having to create a bunch of labels
BatteryList {
@@ -90,7 +96,7 @@ pub enum ModularElement {
bg_color: Arc<str>,
layout: ListLayout,
#[serde(flatten)]
template: OverlayListTemplate,
template: Box<OverlayListTemplate>,
},
}
@@ -107,8 +113,8 @@ pub enum ListLayout {
}
pub enum ModularData {
Label(LabelData),
Button(ButtonData),
Label(Box<LabelData>),
Button(Box<ButtonData>),
}
pub fn modular_canvas(
@@ -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,9 +146,20 @@ pub fn modular_canvas(
data,
} => {
canvas.font_size = *font_size;
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
let mut label = canvas.label(*x, *y, *w, *h, empty_str.clone());
modular_label_init(&mut label, data);
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],
@@ -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
})
}
+201 -38
View File
@@ -7,35 +7,71 @@ 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);
}
log::error!("Could not create uinput provider. Keyboard/Mouse input will not work!");
log::error!("Check if you're in `input` group: `id -nG`");
log::error!("To check if you're in input group, run: id -nG");
if let Ok(user) = std::env::var("USER") {
log::error!(
"To add yourself to the input group, run: sudo usermod -aG input {}",
user
);
log::error!("After adding yourself to the input group, you will need to reboot.");
}
Box::new(DummyProvider {})
}
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;
@@ -128,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 = [
@@ -159,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),
@@ -179,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) {
@@ -202,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]
@@ -433,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.")
}
+142
View File
@@ -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);
}
_ => {}
}
}
}
+35
View File
@@ -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 })
}
+146 -18
View File
@@ -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(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();
@@ -72,16 +98,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
});
auto_run(running, args.openvr, args.openxr);
auto_run(running, args);
Ok(())
}
fn auto_run(running: Arc<AtomicBool>, openvr: bool, openxr: bool) {
fn auto_run(running: Arc<AtomicBool>, args: Args) {
use backend::common::BackendError;
#[cfg(feature = "openxr")]
if !openvr {
if !args_get_openvr(&args) {
use crate::backend::openxr::openxr_run;
match openxr_run(running.clone()) {
Ok(()) => return,
@@ -94,7 +120,7 @@ fn auto_run(running: Arc<AtomicBool>, openvr: bool, openxr: bool) {
}
#[cfg(feature = "openvr")]
if !openxr {
if !args_get_openxr(&args) {
use crate::backend::openvr::openvr_run;
match openvr_run(running.clone()) {
Ok(()) => return,
@@ -107,4 +133,106 @@ fn auto_run(running: Arc<AtomicBool>, openvr: bool, openxr: bool) {
}
log::error!("No more backends to try");
#[cfg(not(any(feature = "openvr", feature = "openxr")))]
compile_error!("No VR support! Enable either openvr or openxr features!");
#[cfg(not(any(feature = "wayland", feature = "x11")))]
compile_error!("No desktop support! Enable either wayland or x11 features!");
}
#[allow(dead_code)]
fn args_get_openvr(_args: &Args) -> bool {
#[cfg(feature = "openvr")]
let ret = _args.openvr;
#[cfg(not(feature = "openvr"))]
let ret = false;
ret
}
#[allow(dead_code)]
fn args_get_openxr(_args: &Args) -> bool {
#[cfg(feature = "openxr")]
let ret = _args.openxr;
#[cfg(not(feature = "openxr"))]
let ret = false;
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
}
+33
View File
@@ -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()
})
}
+111 -35
View File
@@ -1,6 +1,5 @@
use std::{
collections::HashMap,
io::Cursor,
process::{Child, Command},
str::FromStr,
};
@@ -11,14 +10,16 @@ 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};
use once_cell::sync::Lazy;
use regex::Regex;
use rodio::{Decoder, Source};
use serde::{Deserialize, Serialize};
const PIXELS_PER_UNIT: f32 = 80.;
@@ -27,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,
{
@@ -45,7 +49,7 @@ where
size.x as _,
size.y as _,
app.graphics.clone(),
app.format,
app.graphics.native_format,
data,
)?;
@@ -55,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;
@@ -68,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,
@@ -83,20 +124,26 @@ where
verbs: key_events_for_macro(macro_verbs),
});
} else if let Some(exec_args) = LAYOUT.exec_commands.get(key) {
if exec_args.is_empty() {
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()
.expect("Keyboard: Invalid EXEC args")
.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);
@@ -120,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()
},
@@ -146,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 }) => {
@@ -158,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(_))));
@@ -182,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() {
@@ -198,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);
}
}
}
_ => {}
}
}
@@ -227,33 +290,40 @@ struct KeyboardData {
processes: Vec<Child>,
}
const KEY_AUDIO_WAV: &[u8] = include_bytes!("../res/421581.wav");
impl KeyboardData {
fn key_click(&mut self, app: &mut AppState) {
if !app.session.config.keyboard_sound_enabled {
return;
}
if let Some(handle) = app.audio.get_handle() {
// https://freesound.org/people/UberBosser/sounds/421581/
let wav = include_bytes!("../res/421581.wav");
let cursor = Cursor::new(wav);
let source = Decoder::new_wav(cursor).unwrap();
let _ = handle.play_raw(source.convert_samples());
if app.session.config.keyboard_sound_enabled {
app.audio.play(KEY_AUDIO_WAV);
}
}
}
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);
static MACRO_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^([A-Za-z0-1_-]+)(?: +(UP|DOWN))?$").unwrap());
Lazy::new(|| Regex::new(r"^([A-Za-z0-1_-]+)(?: +(UP|DOWN))?$").unwrap()); // want panic
#[derive(Debug, Deserialize, Serialize)]
pub struct Layout {
@@ -314,11 +384,17 @@ impl Layout {
key = &key[3..];
}
if key.contains('_') {
key = key.split('_').next().unwrap();
key = key.split('_').next().unwrap_or_else(|| {
log::error!(
"keyboard.yaml: Key '{}' must not start or end with '_'!",
key
);
"???"
});
}
vec![format!(
"{}{}",
key.chars().next().unwrap().to_uppercase(),
key.chars().next().unwrap().to_uppercase(), // safe because we checked is_empty
&key[1..].to_lowercase()
)]
}
+28 -21
View File
@@ -1,35 +1,41 @@
use std::{sync::Arc, thread::JoinHandle};
use std::{
sync::Arc,
task::{Context, Poll},
};
use futures::executor;
use futures::{Future, FutureExt};
use glam::vec3a;
use wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireSelectScreenResult};
use crate::{
backend::{
common::{OverlaySelector, TaskType},
common::OverlaySelector,
overlay::{
ui_transform, OverlayBackend, OverlayRenderer, OverlayState, SplitOverlayBackend,
},
task::TaskType,
},
state::{AppSession, AppState},
};
use super::screen::ScreenRenderer;
type PinnedSelectorFuture = core::pin::Pin<
Box<dyn Future<Output = Result<PipewireSelectScreenResult, wlx_capture::pipewire::AshpdError>>>,
>;
pub struct MirrorRenderer {
name: Arc<str>,
renderer: Option<ScreenRenderer>,
selector: Option<JoinHandle<Option<PipewireSelectScreenResult>>>,
selector: Option<PinnedSelectorFuture>,
last_extent: [u32; 3],
}
impl MirrorRenderer {
pub fn new(name: Arc<str>) -> Self {
let selector = Box::pin(pipewire_select_screen(None, false, false, false, false));
Self {
name,
renderer: None,
selector: Some(std::thread::spawn(|| {
executor::block_on(pipewire_select_screen(None, false, false, false)).ok()
})),
selector: Some(selector),
last_extent: [0; 3],
}
}
@@ -40,20 +46,21 @@ impl OverlayRenderer for MirrorRenderer {
Ok(())
}
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
if let Some(selector) = self.selector.take() {
if !selector.is_finished() {
self.selector = Some(selector);
return Ok(());
}
if let Some(mut selector) = self.selector.take() {
let maybe_pw_result = match selector
.poll_unpin(&mut Context::from_waker(futures::task::noop_waker_ref()))
{
Poll::Ready(result) => result,
Poll::Pending => {
self.selector = Some(selector);
return Ok(());
}
};
// safe unwrap because we know it's finished
if let Some(pw_result) = selector.join().unwrap() {
log::info!(
"{}: PipeWire node selected: {}",
self.name.clone(),
pw_result.node_id
);
let capture = PipewireCapture::new(self.name.clone(), pw_result.node_id, 60);
if let Ok(pw_result) = maybe_pw_result {
let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
log::info!("{}: PipeWire node selected: {}", self.name.clone(), node_id,);
let capture = PipewireCapture::new(self.name.clone(), node_id);
self.renderer = Some(ScreenRenderer::new_raw(
self.name.clone(),
Box::new(capture),
@@ -123,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
View File
@@ -1,3 +1,4 @@
pub mod anchor;
pub mod custom;
pub mod keyboard;
#[cfg(feature = "wayland")]
+441 -227
View File
@@ -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,21 +15,25 @@ 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,
},
wayland::wayland_client::protocol::wl_output::Transform,
WlxCapture,
};
#[cfg(feature = "pipewire")]
use {
crate::config_io,
std::error::Error,
std::{ops::Deref, path::PathBuf},
wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireStream},
};
#[cfg(feature = "wayland")]
use {
crate::config_io,
glam::Vec3,
std::{collections::HashMap, error::Error, f32::consts::PI, ops::Deref, path::PathBuf},
crate::config::AStrMapExt,
wlx_capture::{
pipewire::{pipewire_select_screen, PipewireCapture},
wayland::{WlxClient, WlxOutput},
wayland::{wayland_client::protocol::wl_output, WlxClient, WlxOutput},
wlr_dmabuf::WlrDmabufCapture,
wlr_screencopy::WlrScreencopyCapture,
},
@@ -36,24 +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 {
@@ -79,7 +108,6 @@ impl ScreenInteractionHandler {
ScreenInteractionHandler {
next_scroll: Instant::now(),
next_move: Instant::now(),
mouse_transform: transform,
}
}
@@ -89,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);
}
@@ -103,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);
}
@@ -116,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
};
@@ -129,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>>,
@@ -208,18 +242,21 @@ impl ScreenPipeline {
cmd.run_ref(&pass)?;
if let (Some(mouse), Some(mouse_view)) = (mouse, self.mouse.clone()) {
let size = CURSOR_SIZE * self.extentf[1];
let half_size = size * 0.5;
let vertex_buffer = app.graphics.upload_verts(
self.extentf[0],
self.extentf[1],
mouse.x * self.extentf[0] - 2.,
mouse.y * self.extentf[1] - 2.,
4.0,
4.0,
mouse.x * self.extentf[0] - half_size,
mouse.y * self.extentf[1] - half_size,
size,
size,
)?;
let set0 = self
.pipeline
.uniform_sampler(0, mouse_view.clone(), Filter::Linear)?;
.uniform_sampler(0, mouse_view.clone(), Filter::Nearest)?;
let pass = self.pipeline.create_pass(
self.extentf,
@@ -289,19 +326,33 @@ impl ScreenRenderer {
output: &WlxOutput,
token: Option<&str>,
session: &AppSession,
) -> Option<(
) -> anyhow::Result<(
ScreenRenderer,
Option<String>, /* pipewire restore token */
)> {
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))
.ok()?;
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
Some((
let capture = PipewireCapture::new(name, node_id);
Ok((
ScreenRenderer {
name: output.name.clone(),
capture: Box::new(capture),
@@ -333,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 || {
@@ -345,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:");
@@ -356,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 {
@@ -367,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
@@ -376,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
}
@@ -407,6 +475,7 @@ impl OverlayRenderer for ScreenRenderer {
);
}
}
self.capture.request_new_frame();
}
WlxFrame::MemFd(frame) => {
let mut upload = app
@@ -497,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);
}
@@ -537,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!(
@@ -547,188 +602,302 @@ where
);
}
if let Some((renderer, restore_token)) = ScreenRenderer::new_pw(output, token, session) {
capture = Some(renderer);
match ScreenRenderer::new_pw(output, token, session) {
Ok((renderer, restore_token)) => {
capture = Some(renderer);
if let Some(token) = restore_token {
if pw_token_store
.insert(String::from(display_name), token.clone())
.is_none()
{
log::info!("Adding Pipewire token {}", token);
if let Some(token) = restore_token {
if pw_token_store.arc_set(display_name.into(), token.clone()) {
log::info!("Adding Pipewire token {}", token);
}
}
}
Err(e) => {
log::warn!(
"{}: Failed to create Pipewire capture: {:?}",
&output.name,
e
);
}
}
}
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,
)),
});
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 angle = if session.config.upright_screen_fix {
match output.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 output.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 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();
for id in wl.outputs.keys() {
if let Some(overlay) = try_create_screen(&wl, *id, &mut pw_tokens, session) {
overlays.push(overlay);
for (id, output) in wl.outputs.iter() {
if app.screens.iter().any(|s| s.name == output.name) {
continue;
}
log::info!(
"{}: Init screen of res {:?}, logical {:?} at {:?}",
output.name,
output.size,
output.logical_size,
output.logical_pos,
);
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);
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.);
@@ -740,68 +909,113 @@ 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,
_180,
_270,
Flipped90,
Flipped180,
Flipped270,
}
#[cfg(feature = "wayland")]
impl From<wl_output::Transform> for Transform {
fn from(t: wl_output::Transform) -> Transform {
match t {
wl_output::Transform::Normal => Transform::Normal,
wl_output::Transform::_90 => Transform::_90,
wl_output::Transform::_180 => Transform::_180,
wl_output::Transform::_270 => Transform::_270,
wl_output::Transform::Flipped => Transform::Flipped180,
wl_output::Transform::Flipped90 => Transform::Flipped90,
wl_output::Transform::Flipped180 => Transform::Flipped180,
wl_output::Transform::Flipped270 => Transform::Flipped270,
_ => Transform::Normal,
}
}
}
fn extent_from_res(res: (i32, i32)) -> [u32; 3] {
// screens above a certain resolution will have severe aliasing
// TODO make dynamic. maybe don't go above HMD resolution?
let w = res.0.min(1920) as u32;
let w = res.0.min(2560) as u32;
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
}
+83 -36
View File
@@ -1,28 +1,40 @@
use std::{
io::Cursor,
ops::Add,
sync::{atomic::AtomicUsize, Arc},
time::Instant,
};
use std::{f32::consts::PI, ops::Add, sync::Arc, time::Instant};
use rodio::{Decoder, Source};
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>,
@@ -30,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 {
@@ -59,41 +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 {
if let Some(handle) = app.audio.get_handle() {
let wav = include_bytes!("../res/557297.wav");
let cursor = Cursor::new(wav);
let source = Decoder::new_wav(cursor).unwrap();
let _ = handle.play_raw(source.convert_samples());
}
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 {
@@ -136,7 +183,6 @@ fn new_toast(
canvas.panel(0., 0., size.0, size.1);
if toast.body.len() > 0 {
log::info!("{}: {}", title, toast.body);
canvas.label(PADDING.0, 54., og_width, size.1 - 54., toast.body);
canvas.fg_color = color_parse("#101010").unwrap(); // want panic
@@ -144,16 +190,17 @@ fn new_toast(
canvas.panel(0., 0., size.0, 30.);
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., title);
} else {
log::info!("Toast: {}", title);
canvas.label_centered(PADDING.0, 0., og_width, size.1, title);
}
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());
+21 -43
View File
@@ -1,13 +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_left, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType},
config_io,
gui::modular::{modular_canvas, ModularUiConfig},
state::{AppState, LeftRight},
config::{load_known_yaml, ConfigType},
gui::{
modular::{modular_canvas, ModularData, ModularUiConfig},
Canvas,
},
state::AppState,
};
pub const WATCH_NAME: &str = "watch";
@@ -18,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 {
@@ -27,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;
}
@@ -67,33 +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,
}
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.clone(),
watch_rot: app.session.config.watch_rot.clone(),
watch_hand: app.session.config.watch_hand,
};
let yaml = serde_yaml::to_string(&conf)?;
std::fs::write(get_config_path(), yaml)?;
Ok(())
}
+10
View File
@@ -35,11 +35,21 @@
"type": "boolean",
"requirement": "optional"
},
{
"name": "/actions/default/in/MoveMouse",
"type": "boolean",
"requirement": "optional"
},
{
"name": "/actions/default/in/SpaceDrag",
"type": "boolean",
"requirement": "optional"
},
{
"name": "/actions/default/in/SpaceRotate",
"type": "boolean",
"requirement": "optional"
},
{
"name": "/actions/default/in/LeftHand",
"type": "pose",
+6
View File
@@ -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",
+6
View File
@@ -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",
+26
View File
@@ -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
+14
View File
@@ -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
+59 -76
View File
@@ -22,44 +22,24 @@ row_size: 23
# Specifies the size of each key. The sum of any given row must equal RowSize
key_sizes:
- [1.5,0.5, 1, 1, 1, 1,0.5,1, 1, 1, 1,0.5,1, 1, 1, 1, 0.5, 1, 1, 1, 4.5]
- [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.25, 1.25, 6.25, 1.25, 1.25, 1.25, 1.25, 0.5, 1, 1, 1, 0.5, 2, 1, 1]
- [1.5,0.5, 1, 1, 1, 1,0.5,1, 1, 1, 1,0.5,1, 1, 1, 1, 0.5, 1, 1, 1, 0.5, 1, 1, 1, 1]
- [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]
- [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.
# Accepted are:
# - virtual keys, for a full list go to https://github.com/galister/X11Overlay/blob/master/Types/VirtualKey.cs
# - ExecCommands (defined below)
# - Macros (defined below)
# - virtual keys. For a full list, look at enum VirtualKey in https://github.com/galister/wlx-overlay-s/blob/main/src/hid.rs
# - exec_commands (defined below)
# - macros (defined below)
# - ~ (null) will leave an empty space with the corresponding size from key_sizes
main_layout:
- ["Escape", ~, "F1", "F2", "F3", "F4", ~, "F5", "F6", "F7", "F8", ~, "F9", "F10", "F11", "F12", ~, "Print", "Scroll", "Pause", ~]
- ["Escape", ~, "F1", "F2", "F3", "F4", ~, "F5", "F6", "F7", "F8", ~, "F9", "F10", "F11", "F12", ~, "Print", "Scroll", "Pause", ~, "COPY", "PASTE", ~, "KILL"]
- ["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"]
- ["LCtrl", "LSuper", "LAlt", "Space", "Meta", "RSuper", "Menu", "RCtrl", ~, "Left", "Down", "Right", ~, "KP_0", "KP_Decimal", ~]
# When using the purple pointer...
# None - No special functionality when using purple pointer
# Shift - Use same functionality as the orange pointer
# Ctrl - Use Main layout with Ctrl modifier
# Super - Use Main layout with Super (WinKey) modifier
# Meta - Use Main layout with Meta (AltGr) modifier
# Layout - Use the alt_layout defined below
alt_layout_mode: Ctrl
# The alt (fn) layout of the keyboard. Only takes effect when alt_layout_mode: Layout
# Accepted values are the same as for MainLayout.
# The default layout here is a dummy. Change it to fit your liking.
alt_layout:
- ["Escape", ~, "F1", "F2", "F3", "F4", ~, "F5", "F6", "F7", "F8", ~, "F9", "F10", "F11", "F12", ~, "Print", "Scroll", "Pause", ~, "COPY", "PASTE"]
- ["Oem3", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "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", "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", ~]
@@ -70,7 +50,7 @@ exec_commands:
# Series of keypresses to be used in a layout.
# Format: keyName [DOWN|UP]
# keyName must be a valid key from `xmodmap -pke`
# keyName must be a valid virtual key from the VirtualKey enum (see above)
# DOWN|UP: can be omitted for an implicit "keyName DOWN, keyName UP"
macros:
KILL: [ "LSuper DOWN", "LCtrl DOWN", "Escape", "LCtrl UP", "LSuper UP" ]
@@ -78,52 +58,55 @@ macros:
PASTE: [ "LCtrl DOWN", "V", "LCtrl UP" ]
# Custom labels to use.
# Key: element of MainLayout / AltLayout
# Key: element of main_layout
# 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": [" \\", "|"]
+66 -9
View File
@@ -270,10 +270,13 @@ elements:
fg_color: "#ffffff"
bg_color: "#707070"
text: "Show/Hide"
click_down:
click_down: # ToggleVisible if exists, else create
- type: Overlay
target: M1
action: ToggleVisible # only fires if overlay exists
- type: Window
target: M1
action: ShowMirror
action: ShowMirror # only fires if not exists
- type: Button
rect: [185, 270, 60, 30]
@@ -311,6 +314,9 @@ elements:
bg_color: "#707070"
text: "Show/Hide"
click_down:
- type: Overlay
target: M2
action: ToggleVisible
- type: Window
target: M2
action: ShowMirror
@@ -351,6 +357,9 @@ elements:
bg_color: "#707070"
text: "Show/Hide"
click_down:
- type: Overlay
target: M3
action: ToggleVisible
- type: Window
target: M3
action: ShowMirror
@@ -509,38 +518,72 @@ elements:
bg_color: "#c0c0c0"
- type: Label
rect: [325, 480, 90, 24]
rect: [325, 490, 90, 24]
font_size: 18
fg_color: "#ffffff"
source: Static
text: Notifications
- type: Button
rect: [330, 495, 220, 30]
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, 545, 220, 30]
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, 595, 500, 1]
rect: [50, 605, 500, 1]
bg_color: "#c0c0c0"
- type: Button
rect: [330, 615, 220, 30]
rect: [330, 625, 220, 30]
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
@@ -548,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.
+11 -8
View File
@@ -18,10 +18,13 @@ elements:
bg_color: "#808040"
fg_color: "#ffffff"
text: "C"
click_up:
click_up: # destroy if exists, otherwise create
- type: Window
target: settings
action: ShowUi
action: ShowUi # only triggers if not exists
- type: Window
target: settings
action: Destroy # only triggers if exists since before current frame
# Keyboard button
- type: Button
@@ -76,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
@@ -102,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
@@ -118,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
+6
View File
@@ -94,6 +94,12 @@ pub mod frag_screen {
void main()
{
out_color = texture(in_texture, in_uv);
// linear to srgb
bvec4 cutoff = lessThan(out_color, vec4(0.0031308));
vec4 higher = (pow(out_color, vec4(0.41666)) * vec4(1.055)) - vec4(0.055);
vec4 lower = out_color*vec4(12.92);
out_color = mix(higher, lower, cutoff);
out_color.a = 1.0;
}
",
+42 -11
View File
@@ -1,19 +1,20 @@
use std::{path::PathBuf, sync::Arc};
use std::{io::Cursor, path::PathBuf, sync::Arc};
use anyhow::bail;
use glam::Vec3;
use rodio::{OutputStream, OutputStreamHandle};
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.,
@@ -126,23 +141,39 @@ impl AudioOutput {
}
}
pub fn get_handle(&mut self) -> Option<&OutputStreamHandle> {
fn get_handle(&mut self) -> Option<&OutputStreamHandle> {
if self.audio_stream.is_none() && self.first_try {
self.first_try = false;
if let Ok((stream, handle)) = OutputStream::try_default() {
self.audio_stream = Some((stream, handle));
} else {
log::error!("Failed to open audio stream");
log::error!("Failed to open audio stream. Audio will not work.");
return None;
}
}
self.audio_stream.as_ref().map(|(_, h)| h)
}
pub fn play(&mut self, wav_bytes: &'static [u8]) {
let Some(handle) = self.get_handle() else {
return;
};
let cursor = Cursor::new(wav_bytes);
let source = match Decoder::new_wav(cursor) {
Ok(source) => source,
Err(e) => {
log::error!("Failed to play sound: {:?}", e);
return;
}
};
let _ = handle.play_raw(source.convert_samples());
}
}
pub struct ScreenMeta {
pub name: Arc<str>,
pub id: usize,
pub native_handle: u32,
}
#[derive(Serialize, Deserialize, Clone, Copy, Default)]
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+787
View File
@@ -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&#10;L&#10;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=&quot;http://www.w3.org/2000/svg&quot;,e=&quot;http://www.w3.org/1999/xlink&quot;,s=&quot;http://www.w3.org/1999/xhtml&quot;,r=2;if(document.createElementNS(t,&quot;meshgradient&quot;).x)return;const n=(t,e,s,r)=&gt;{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=&gt;{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&gt;s?e:s,i=r&gt;n?r:n;return 18*(o&gt;i?o:i)},i=(t,e)=&gt;Math.sqrt(t.distSquared(e)),a=(t,e)=&gt;t.scale(2/3).add(e.scale(1/3)),h=t=&gt;{let e,s,r,n,o,i,a,h=new g;return t.match(/(\w+\(\s*[^)]+\))+/g).forEach(t=&gt;{let l=t.match(/[\w.-]+/g),d=l.shift();switch(d){case&quot;translate&quot;:2===l.length?e=new g(1,0,0,1,l[0],l[1]):(console.error(&quot;mesh.js: translate does not have 2 arguments!&quot;),e=new g(1,0,0,1,0,0)),h=h.append(e);break;case&quot;scale&quot;: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(&quot;mesh.js: scale does not have 1 or 2 arguments!&quot;),s=new g(1,0,0,1,0,0)),h=h.append(s);break;case&quot;rotate&quot;:if(3===l.length&amp;&amp;(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)&lt;1e-16&amp;&amp;(t=0),Math.abs(e)&lt;1e-16&amp;&amp;(e=0),a=new g(t,e,-e,t,0,0),h=h.append(a)}else console.error(&quot;math.js: No argument to rotate transform!&quot;);3===l.length&amp;&amp;(e=new g(1,0,0,1,-l[1],-l[2]),h=h.append(e));break;case&quot;skewX&quot;: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(&quot;math.js: No argument to skewX transform!&quot;);break;case&quot;skewY&quot;: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(&quot;math.js: No argument to skewY transform!&quot;);break;case&quot;matrix&quot;:6===l.length?h=h.append(new g(...l)):console.error(&quot;math.js: Incorrect number of arguments for matrix!&quot;);break;default:console.error(&quot;mesh.js: Unhandled transform type: &quot;+d)}}),h},l=t=&gt;{let e=[],s=t.split(/[ ,]+/);for(let t=0,r=s.length-1;t&lt;r;t+=2)e.push(new x(parseFloat(s[t]),parseFloat(s[t+1])));return e},d=(t,e)=&gt;{for(let s in e)t.setAttribute(s,e[s])},c=(t,e,s,r,n)=&gt;{let o,i,a=[0,0,0,0];for(let h=0;h&lt;3;++h)e[h]&lt;t[h]&amp;&amp;e[h]&lt;s[h]||t[h]&lt;e[h]&amp;&amp;s[h]&lt;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]&gt;o?a[h]=o:a[h]&gt;i&amp;&amp;(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=&gt;{let e=[];for(let s=0;s&lt;16;++s){e[s]=0;for(let r=0;r&lt;16;++r)e[s]+=u[s][r]*t[r]}return e},p=(t,e,s)=&gt;{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=&gt;{let e=[],s=[],r=[];for(let s=0;s&lt;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&lt;8;++t){s[t]=[];for(let r=0;r&lt;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&lt;8;++t){r[t]=[];for(let e=0;e&lt;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