Compare commits

...
275 Commits
Author SHA1 Message Date
galister 507ff7353e Release 25.2 2025-02-20 06:26:35 +09:00
galister c5a82c2879 update libmonado 2025-02-20 06:26:35 +09:00
Diamond White 965174c781 seperate combined UInputHandle into two handles for mouse and keyboard each (#155) 2025-02-12 19:24:42 +00:00
Aleksander bf3f3dafbe Merge pull request #154 from olekolek1000/wayvr_dashboard_ipc
WayVR: Do not hard-error in case if the user bought NVIDIA
2025-02-11 22:43:38 +01:00
Aleksander 4701d2b734 WayVR: Do not hard-error in case if the user bought NVIDIA 2025-02-11 22:30:38 +01:00
Aleksander e44872f831 Merge pull request #146 from olekolek1000/wayvr_dashboard_ipc
WayVR: Layouting system, state changing feedback, process userdata, various IPC changes
2025-01-31 20:12:04 +01:00
Aleksander 3c67abcebb WayVR: Layouting system, state changing feedback, process userdata, various IPC changes
WayVR:
- Layouting system (tiled and stacked)

IPC:
- Implemented routes: `WvrDisplaySetWindowLayout`, `WvrDisplayWindowList`, `WvrWindowSetVisible`, `WvrProcessGet`,

- Packet broadcasting
- State change feedback to the client (displays/windows/processes)
2025-01-31 20:00:56 +01:00
Jay da129b62b3 Add UI button actions to send arbitrary OSC values. (#140)
* osc: start adding osc buttons

a button action that sends an osc parameter.
struggling with borrows in openxr.rs and openvr.rs when getting the osc sender.

* osc: fix osc sender buttons

by passing a ref to the device list to send_params instead of the entire app state.

* osc: fix warnings

* osc: conditionally use OscSender crate in state.rs

* osc: fix button.rs compile errors without osc/wayvr features

* osc: add other types: int, bool, string. play thump noise when sent.

* osc: fix build without osc feature

i just want to use OscType directly, but since it doesn't derive serde::Deserialize, i can't just have one OscType action or list of actions...

* merge typed actions to one action, support multiple values.

i added a new struct OscValue that has Deserialize, and now the action just converts that to the corresponding OscType when sending the parameters.
perhaps not the most elegant solution, but it's the best i can think of without modifying the rosc crate.

* run `cargo fmt`
2025-01-26 11:23:18 +00:00
Aleksander 52298ccfdf Merge pull request #145 from olekolek1000/wayvr_dashboard_ipc
WayVR: Improvements (WayVR Dash 0.1.1)
2025-01-23 20:49:52 +01:00
Aleksander 8766999e28 WayVR: Haptics support, fix warnings, EGL fix, display visibility IPC, bump dashboard resolution to 1080p 2025-01-23 20:26:34 +01:00
Aleksander 5d9f80ea40 Merge pull request #144 from olekolek1000/wayvr_dashboard_ipc
WayVR: def_false fix
2025-01-21 18:53:43 +01:00
Aleksander ba5228c643 WayVR: def_false fix 2025-01-21 18:42:00 +01:00
Aleksander 8ba0b2ca04 Merge pull request #143 from olekolek1000/wayvr_dashboard_ipc
WayVR: Add config default values (Fixes #142)
2025-01-20 23:37:00 +01:00
Aleksander b22af57000 WayVR: Add config default values (Fixes #142) 2025-01-20 23:22:48 +01:00
galister 2bf8984ba1 fix uidev build (#141) 2025-01-20 21:27:47 +00:00
Aleksander 3c792608e7 config_io: Support wayvr.conf.d directory, refactoring (#139) 2025-01-20 14:26:17 +00:00
Aleksander b209f79188 Merge pull request #137 from olekolek1000/wayvr_dashboard_ipc
WayVR: Display removal support (implement `WvrDisplayRemove`)
2025-01-19 15:43:11 +01:00
Aleksander 80d9b02ac7 WayVR: Display removal support 2025-01-19 15:20:52 +01:00
Aleksander 244f8cfa80 Merge pull request #136 from olekolek1000/dash_button
Dashboard button in the watch, Toast error messages
2025-01-19 14:12:44 +01:00
Aleksander 6c95607d44 Watch: Add "Toggle Dashboard" button by default, Toast: Show user-specific error messages in various places, WayVR: Modify example env vars 2025-01-19 00:53:59 +01:00
Aleksander bbed686a5e Merge pull request #135 from olekolek1000/wayvr_dashboard_ipc
WayVR: Add dmabuf (backend_drm) to the compositor
2025-01-16 22:39:19 +01:00
Aleksander 83026deaab WayVR: Add dmabuf (backend_drm) to the compositor. Fixes crash for webkitgtk apps 2025-01-16 22:27:13 +01:00
Aleksander aa2e74f6b6 Merge pull request #134 from olekolek1000/wayvr_dashboard_ipc
WayVR: Update IPC, send runtime info, update deps
2025-01-14 22:54:27 +01:00
Aleksander d8180bb4ed WayVR: Update IPC, send runtime info, update deps 2025-01-14 22:42:18 +01:00
Aleksander ff150100b4 Merge pull request #132 from olekolek1000/wayvr_dashboard_ipc
WayVR: IPC: Fix process launcher calling create_display two times
2025-01-12 17:04:17 +01:00
Aleksander de6c5b8ad9 WayVR: Fix process launching calling create_display two times 2025-01-12 16:30:07 +01:00
eb3087f230 Merge Staging into main (#130)
* config: add use_passthrough

* do not use ALPHA_BLEND if passthrough is off

* keyboard.yaml auto_labels

* OSC: Finish XSOverlay parity (#124)

* osc-battery - utilise match return value to set parameter

this just lets parameter be a `&str` instead of a `String`, and reduces repetition.

* osc-battery - fix error where trackers were 1-indexed instead of 0-indexed

* osc-battery: xsoverlay parity! add average tracker and controller battery parameters.

this should now be in parity with xsoverlay's parameters.

gone back to 1-indexing trackers because it's more user-friendly, so other programs can standardise more easily.

* osc battery: ...that was not 1-indexed.

no idea how I made that mistake but the sent tracker parameters were actually still 0-indexed.

* Update Envision section (#123)

add directions for appimage autostart

* Add Flatpak instructions (#127)

* Add Flatpak instructions

fix small inconsistencies between instructions

* Clarify Flatpak Steam setup for SteamVR and Monado/WiVRn users

* Additional instructions

* default AltLayout is None

* WayVR: IPC [1]: Local socket server, handshake receiver

* WayVR : IPC [2]: `ListDisplays`, `GetDisplay` commands

* WayVR: IPC [3]: `ListProcesses`, `TerminateProcess` commands

* WayVR: IPC [4]: Minor refactoring

* WayVR: Move IPC to separate Git repository

* Restore default features in Cargo.toml

* WayVR: Implement `WvrDisplayCreate`, Implement `WvrProcessLaunch`, Refactoring

* WayVR: Dashboard toggle support, minor refactoring

* Update Cargo.toml

* fix formatting for openxr_actions.json5

---------

Co-authored-by: galister <22305755+galister@users.noreply.github.com>
Co-authored-by: Jay <157681441+cubee-cb@users.noreply.github.com>
Co-authored-by: Marcus Howser <mhowser.git@mailbox.org>
Co-authored-by: Joshua Vandaële <joshua@vandaele.software>
2025-01-11 08:46:54 +00:00
galister ab6b5c0b46 default AltLayout is None 2025-01-04 22:05:43 +09:00
Joshua Vandaële 9b2df6be08 Add Flatpak instructions (#127)
* Add Flatpak instructions

fix small inconsistencies between instructions

* Clarify Flatpak Steam setup for SteamVR and Monado/WiVRn users

* Additional instructions
2025-01-02 12:52:08 +00:00
Marcus Howser 1d4f4337d0 Update Envision section (#123)
add directions for appimage autostart
2024-12-29 08:32:17 +00:00
Jay c6ff314e93 OSC: Finish XSOverlay parity (#124)
* osc-battery - utilise match return value to set parameter

this just lets parameter be a `&str` instead of a `String`, and reduces repetition.

* osc-battery - fix error where trackers were 1-indexed instead of 0-indexed

* osc-battery: xsoverlay parity! add average tracker and controller battery parameters.

this should now be in parity with xsoverlay's parameters.

gone back to 1-indexing trackers because it's more user-friendly, so other programs can standardise more easily.

* osc battery: ...that was not 1-indexed.

no idea how I made that mistake but the sent tracker parameters were actually still 0-indexed.
2024-12-29 08:21:59 +00:00
galister 1903271d05 keyboard.yaml auto_labels 2024-12-23 17:16:24 +09:00
galister 4de6116985 do not use ALPHA_BLEND if passthrough is off 2024-12-08 02:19:06 +09:00
galister 65432ebde4 config: add use_passthrough 2024-12-07 07:26:19 +01:00
cubee a71f4de0ff fix osc timer and give battery parameters their own timer 2024-11-24 15:13:46 +01:00
cubee 18b2e96079 make parameter String instead of &str
this means parameter can be immutable if left unset at initialisation, and we can use format!() macro in the match statements. silly workarounds no more!
2024-11-24 15:13:46 +01:00
cubee c09da86659 add parameters for device charging state 2024-11-24 15:13:46 +01:00
cubee e818259b5b default to -1 if a device has no battery level.
this is just to make sure the unwrap() doesn't crash; -1 (or any negative value) is used as the "disconnected" value.
2024-11-24 15:13:46 +01:00
cubee c96ead7cf0 send osc parameters based on device role, cleanup usings 2024-11-24 15:13:46 +01:00
cubee 546b38204c use AppState to get device roles
i _understand_ now!
basically, i did it the "wrong" way first by modifying openxr.rs and openvr.rs to pass battery levels to osc, when really i could have just made osc get the pre-formatted device details from AppState. live and learn, i'm new to rust anyway.
2024-11-24 15:13:46 +01:00
galister 12218828d4 persist keyboard hide state across sessions 2024-11-24 17:37:35 +09:00
galister 26a872e685 bump max curvature to 180deg 2024-11-24 17:23:21 +09:00
galister add121bd52 screen_render_down default on 2024-11-24 14:35:31 +09:00
galister 3b3138cfe9 update wlx-capture 2024-11-23 21:30:38 +09:00
galister 8d6ff1d3a7 EMEA starts at UTC-1 2024-11-23 20:07:13 +09:00
galister 51174b64d3 move timezones out of watch.yaml + sane defaults 2024-11-23 20:01:49 +09:00
galister 54737b02d3 mouse_move_interval_ms defaults to 10 2024-11-23 18:32:47 +09:00
galister 2c31d57afe add mouse_move_interval_ms setting 2024-11-23 18:29:12 +09:00
galister 30736575be update wlx-capture 2024-11-23 18:04:07 +09:00
galister 689fbbe5e4 mute zbus spam 2024-11-20 22:50:22 +01:00
galister bdf02ea206 update deps, back to env_logger, fix screen transform 2024-11-20 22:50:22 +01:00
galister 5ef2e40b5f build appimage on staging 2024-11-20 22:50:22 +01:00
galister e23444aa89 apply pipewire transforms to screens 2024-11-20 22:50:22 +01:00
Aleksander a3f9f7cc46 Merge pull request #106 from olekolek1000/wayvr_output
WayVR: wl_output protocol support
2024-11-18 20:10:49 +01:00
Aleksander 02faf693dc WayVR: WlOutput support 2024-11-18 19:31:01 +01:00
Aleksander 502cf5d372 WayVR: Display auto-hide support (Fixes #98), keyboard settings in config 2024-11-05 16:40:03 +01:00
Aleksander d212098a98 WayVR: Add Navi33 GPU to the whitelist (7600xt) 2024-11-05 15:28:23 +01:00
galister ced753235e clippy 2024-11-05 03:45:22 +09:00
galister 9da1c509e3 fix font loading for 3+ byte codepoints 2024-11-05 03:41:21 +09:00
galister 058baebd0c fix skybox appearing after closing an app in passtrhu mode 2024-11-04 21:23:30 +09:00
galister 5a060c28f2 support wivrn passthru mode 2024-11-04 21:19:21 +09:00
Aleksander 96db24d02e WayVR: Modify readme, Various improvements
- Add "Quick setup" and images in readme
- Implement `click_freeze_time_ms`
- Automatically show hidden display if AppClick has been triggered
2024-11-03 16:26:37 +01:00
Anon_11487 24096b7b5a Configurable Purple laser modifier 2024-11-03 09:34:44 +01:00
Anon_11487 f941f76339 Enable CTRL modifier key with purple laser 2024-11-03 09:34:44 +01:00
Aleksander 50b30565da Merge pull request #92 from olekolek1000/wayvr_extprocess
WayVR: External process support, various tweaks and bugfixes
2024-10-27 21:41:17 +01:00
Aleksander 7e09c010db WayVR: External process support, various tweaks and bugfixes
- Support for foreign wayland clients, WayVR process is now separated into
  managed and external one
- Add `run_compositor_at_start` global param
- Add `primary` display param
- Export WAYLAND_DISPLAY number into XDG_RUNTIME_DIR directory
- Bugfix: Redraw event is not triggered after despawning a process
- Sanitization in WayVRConfig::post_load()
2024-10-27 21:26:33 +01:00
Aleksander 66fb2fc48c Merge pull request #91 from olekolek1000/wayvr_redraw_optim
WayVR: Redraw only if needed
2024-10-26 22:09:14 +02:00
Aleksander 9195a338fd WayVR: Redraw only if needed 2024-10-26 21:57:11 +02:00
Aleksander cf5eabdfdf WayVR: shown_at_start app param support 2024-10-26 18:44:08 +02:00
galister 7cb8e4a9d2 docs: troubleshooting details 2024-10-25 08:39:25 +00:00
Aleksander 0b1a7e8bc8 Merge pull request #89 from olekolek1000/wayvr_relativeto
WayVR: `attach_to`, `pos` and `rotation` params
2024-10-24 22:54:52 +02:00
Aleksander 8d82993704 WayVR: attach_to, pos and rotation spawn parameters in wayvr.yaml config script 2024-10-24 22:44:37 +02:00
galister cf03beef07 add alt_click 2024-10-22 05:55:25 +09:00
Aleksander 83bf0fff5d WayVR: Process cleanup support, Refactoring 2024-10-21 20:46:52 +02:00
Aleksander 07d7afa96f WayVR: WayVRDisplayList ui type, toggle display visibility and pause rendering of them 2024-10-21 20:46:52 +02:00
Aleksander 79d91e3d02 Add libwayland-dev and libegl-dev deps for CI
They are required to compile with `wayvr` target.
2024-10-21 20:46:52 +02:00
Aleksander d9dddbad11 WayVR: Initial GUI integration
The format of the wayvr.yaml configuration file is subject to change at any time.
2024-10-21 20:46:52 +02:00
Aleksander edfa77e07c Integrate WayVR into wlx directly 2024-10-18 21:11:56 +02:00
Aleksander f84d57dc42 Merge pull request #85 from olekolek1000/uidev_fix
Fix compilation issues with uidev target or without wayland target
2024-10-17 22:58:10 +02:00
Aleksander bc9bac8c40 Fix compilation issues with uidev target or without wayland target 2024-10-17 22:53:17 +02:00
Aleksander 9dbd1f994e Use strongly-typed OverlayID instead of usize 2024-10-17 21:09:19 +02:00
Sefa Eyeoglu 022ae53aea contrib: add systemd service for Monado autostart
Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2024-10-16 20:11:44 +02:00
galister 64b3263309 readme: improve wording 2024-09-30 09:26:06 +00:00
galister 85aa0fb1bc release 0.6 2024-09-28 01:54:53 +09:00
galister 7f72abc62a release modifiers on keyboard hide 2024-09-28 01:53:20 +09:00
galister 83ae798215 add virtualkey action for modular button 2024-09-27 02:23:13 +09:00
galister bdbdbea20b screen notification popups 2024-09-26 23:51:46 +09:00
galister a077f2dda4 fix dragging cursor off-screen 2024-09-26 23:51:44 +09:00
galister 8d35b45c2b rollback vulkano 2024-09-24 11:45:29 +02:00
galister 6af5623fa7 update readme 2024-09-19 06:18:06 +00:00
galister 62ef7a1210 update readme 2024-09-19 06:47:01 +09:00
galister 3f425820d7 release 0.5.0 2024-09-19 05:29:27 +09:00
galister 6a2d8b9f90 fresh watch style 2024-09-19 03:09:09 +09:00
galister 583a5f7fe0 openxr: toggle for space rotate axis lock 2024-09-18 04:06:37 +09:00
galister 03f2eaf97f fps-stabilize openxr smoothing 2024-09-16 07:05:26 +09:00
galister 21331b5c42 fps-aware openxr pointer smoothing 2024-09-16 05:09:10 +09:00
galister 72a82530ae fix openxr space rotate 2024-09-16 05:01:15 +09:00
galister 13d9284bb8 move mouse move-scroll exclusivity to hid module 2024-09-11 06:03:22 +09:00
galister b0a33a1181 openxr: laser pointer smoothing 2024-09-09 07:54:19 +09:00
galister 4f38d1ad44 make pipewire default on wlroots 2024-08-24 02:21:43 +09:00
galister c6bc880bc3 add logging for screen pipeline 2024-08-20 16:49:35 +09:00
galister 0a2be76150 change some version stuff 2024-08-20 16:40:08 +09:00
galister 1a87823847 rename github action 2024-08-20 14:04:44 +09:00
galister e763e15bdc build dev appimages 2024-08-20 14:02:12 +09:00
galister 6a3b60f298 feat: screen_render_down 2024-08-20 13:54:18 +09:00
Adalyn Black 397b3eedd1 Use git hash if no tags can be found 2024-08-19 17:35:58 +02:00
hypevhs 9d18628354 feat: XR binding for playspace reset 2024-08-15 21:30:39 +02:00
galister dc985f6da6 docs: update known issues 2024-08-13 07:43:37 +00:00
raku-cat 850601b3b9 Update wlx-capture 2024-08-12 12:16:10 +00:00
galister 8a4c7a400f dirty versions 2024-08-11 06:41:17 +09:00
galister d9287f3250 Release 0.4.4 2024-08-11 06:23:38 +09:00
hypevhs 378ea120c8 Fix mirrors spawning behind head 2024-08-10 20:34:55 +00:00
galister f4cc49ca69 fix keyboard macro regex 2024-08-09 23:25:59 +00:00
galister 57ff3aa1dd clippy 2024-08-09 10:19:57 +00:00
galister 0808383e70 corner_radius is optional 2024-08-09 10:19:57 +00:00
galister c71911998e upright screen fix 2024-08-09 08:38:37 +09:00
galister 50c3f49989 hmd pose from view space 2024-08-09 08:38:37 +09:00
Aleksander b6a07c43a5 Fix scrolling for all Electron-based applications 2024-08-07 18:12:20 +00:00
galister ee3fbff44c release 0.4.3 2024-08-04 06:57:11 +09:00
galister c29bbbb142 review unwraps 2024-08-03 20:50:34 +09:00
Aleksander aca177635e Input source: Update monado devices only if they are available
Fixes .unwrap() panic in the simulated Monado driver
2024-08-02 20:20:34 +00:00
galister c3c8694e1a update deps 2024-08-02 16:49:55 +00:00
Torge Matthies 1de9cea397 feat: openxr battery status 2024-08-02 16:33:09 +00:00
galister 939a537d20 fix: ipd label not visible on uidev 2024-08-01 18:36:03 +09:00
Adalyn Black 26124be6ef Improved the clarity of some sentences in the README 2024-07-31 09:45:03 +00:00
galister 3d0611052f switch to using libmonado-rs 2024-07-31 05:28:34 +09:00
galister dcc2cd0575 openxr: configurable screen downscale 2024-07-29 22:14:25 +09:00
galister 57ff3ec3bc settings: auto-close on fix floor & reset offset 2024-07-25 09:10:03 +09:00
Adalyn Black e14b35bece Cattpuccin Macchiato Re-theme 2024-07-24 23:59:39 +00:00
Adalyn Black 88da3c832a Removed unused render_rect function 2024-07-24 23:59:39 +00:00
Adalyn Black 7b4576bd52 Removed unused imports 2024-07-24 23:59:39 +00:00
Adalyn Black a98b7a210f Switched to shader for corner rounding 2024-07-24 23:59:39 +00:00
Adalyn Black 32e0e7656a Moved corner radius property to be separate from the rect struct 2024-07-24 23:59:39 +00:00
Adalyn Black f27c320231 feat: rounded corners
A proof of concept commit for rounded corners. Currently unoptimized and in need of reorganization too. May also make keyboard keys invisible
2024-07-24 23:59:39 +00:00
galister a56ed68b44 mirrors: print pw errors 2024-07-24 21:06:37 +00:00
galister 7a6040bfee feat: ui sprites + openxr skybox 2024-07-24 21:06:37 +00:00
galister 17addcde78 fix --show behavior 2024-07-23 19:09:34 +09:00
Torge Matthies 5a45d4dd2f openxr: fix triple-click bindings on left hand reacting to the wrong hand 2024-07-22 23:27:36 +00:00
galister 9a928b1c0c Monado: support new space offset API 2024-07-20 03:33:07 +09:00
galister 3764873209 docs: hyprland-specific mouse issue 2024-07-18 09:52:10 +00:00
galister 106028be67 reset click counter on pass 2024-07-17 14:22:06 +00:00
galister d58268b8ec downgrade freetype 2024-07-17 14:22:06 +00:00
galister 787a3851b3 openxr: add triple-click option 2024-07-17 14:22:06 +00:00
galister 2bfa78e9db x11: fix upright screens 2024-07-13 17:27:47 +09:00
galister 71068a0215 docs: space-drag crash to known issues 2024-07-12 10:11:31 +00:00
galister 5575e4f49f fix: openxr swapchain banding 2024-07-11 19:17:41 +09:00
galister a991c6b84a feat: toggle to show by default 2024-07-11 16:59:57 +09:00
galister bdc500973c feat: cubic filtering 2024-07-11 16:23:48 +09:00
galister 80bd65235d add screenshot to readme 2024-06-27 11:04:06 +00:00
galister 39595faaad update deps 2024-06-26 12:02:29 +09:00
galister 1fc6cb4a36 bump version 2024-06-23 09:57:29 +09:00
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
95 changed files with 17284 additions and 4155 deletions

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
name: Build AppImage
on:
push:
branches:
- 'main'
- 'staging'
env:
APPDIR: WlxOverlay-S.AppDir
CARGO_TERM_COLOR: always
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
jobs:
build_appimage:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.3
- name: Prepare Environment
run: |
sudo add-apt-repository -syn universe
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
sudo apt-get update
sudo apt-get install -y fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-dev libxkbcommon-x11-0 libxkbcommon-x11-dev libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1 libwayland-dev libegl-dev
rustup update
test -f linuxdeploy-x86_64.AppImage || wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
chmod +x linuxdeploy-x86_64.AppImage
test -d ${APPDIR} && rm -rf ${APPDIR}
mkdir -p ${APPDIR}/usr/bin
- name: Cargo Build
run: |
cargo build --release
mv target/release/wlx-overlay-s ${APPDIR}/usr/bin
chmod +x ${APPDIR}/usr/bin/wlx-overlay-s
- name: Package AppImage
run: |
export VERSION=$GITHUB_REF_NAME
./linuxdeploy-x86_64.AppImage -dwlx-overlay-s.desktop -iwlx-overlay-s.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
mv WlxOverlay-S-$VERSION-x86_64.AppImage WlxOverlay-S-x86_64.AppImage
- name: Upload AppImage
uses: actions/upload-artifact@v4
with:
name: WlxOverlay-S-${{ github.ref_name }}-x86_64.AppImage
path: ./WlxOverlay-S-x86_64.AppImage
+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 libwayland-dev libegl-dev
- 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+OpenVR+WayVR
on:
pull_request:
#branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.3
- name: Prepare Environment
run: |
sudo add-apt-repository -syn universe
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
sudo apt-get update -y
sudo apt-get install -y fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-dev libxkbcommon-x11-0 libxkbcommon-x11-dev libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1 libwayland-dev libegl-dev
- name: Build
run: cargo build --verbose --no-default-features --features=wayland,openxr,openvr,wayvr
- name: Run tests
run: cargo test --verbose --no-default-features --features=wayland,openxr,openvr,wayvr
@@ -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
+6 -2
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 libwayland-dev libegl-dev
rustup update
test -f linuxdeploy-x86_64.AppImage || wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
Generated
+2692 -1085
View File
File diff suppressed because it is too large. Load diff
+91 -32
View File
@@ -4,53 +4,112 @@ debug = true
[package]
name = "wlx-overlay-s"
version = "0.2.6"
version = "25.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.79"
anyhow = "1.0.89"
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"] }
chrono = "0.4.38"
chrono-tz = "0.10.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"
env_logger = "0.11.5"
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" # latest version supported on ubuntu 22.04
futures = "0.3.30"
glam = { version = "0.29.2", features = ["approx", "mint", "serde"] }
idmap = { version = "0.2.21", features = ["serde"] }
idmap-derive = "0.1.2"
input-linux = "0.6.0"
input-linux = "0.7.0"
json = { version = "0.12.4", optional = true }
libc = "0.2.147"
log = "0.4.20"
once_cell = "1.18.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"] }
json5 = "0.4.1"
libc = "0.2.155"
log = "0.4.21"
once_cell = "1.19.0"
openxr = { git = "https://github.com/galister/openxrs", rev = "af4a55d", features = [
"linked",
"mint",
], optional = true }
ovr_overlay = { features = [
"ovr_input",
"ovr_system",
], git = "https://github.com/galister/ovr_overlay_oyasumi", optional = true }
regex = "1.10.4"
rodio = { version = "0.20.1", default-features = false, features = [
"wav",
"hound",
] }
rosc = { version = "0.10.1", optional = true }
serde = { version = "1.0.188", features = ["derive", "rc"] }
serde_json = "1.0.113"
serde_yaml = "0.9.25"
smallvec = "1.11.0"
strum = { version = "0.25.0", features = ["derive"] }
thiserror = "1.0.56"
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.32.0" }
thiserror = "2.0.3"
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", default-features = false }
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.4.2", default-features = false }
libmonado = { git = "https://github.com/technobaboo/libmonado-rs", rev = "256895b18c8f9368174fad8a6232ff07764eeacb", optional = true }
winit = { version = "0.30.0", optional = true }
xdg = "2.5.2"
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
serde_json5 = "0.1.0"
xkbcommon = { version = "0.8.0" }
xcb = { version = "1.4.0", optional = true, features = [
"as-raw-xcb-connection",
] }
image_dds = { version = "0.6.0", default-features = false, features = [
"ddsfile",
] }
mint = "0.5.9"
################################
#WayVR-only deps
################################
khronos-egl = { version = "6.0.0", features = ["static"], optional = true }
smithay = { git = "https://github.com/Smithay/smithay.git", default-features = false, features = [
"renderer_gl",
"backend_egl",
"backend_drm",
"xwayland",
"wayland_frontend",
], optional = true }
uuid = { version = "1.10.0", features = ["v4", "fast-rng"], optional = true }
wayland-client = { version = "0.31.6", optional = true }
wayland-egl = { version = "0.32.4", optional = true }
interprocess = { version = "2.2.2", optional = true }
bytes = { version = "1.9.0", optional = true }
wayvr_ipc = { git = "https://github.com/olekolek1000/wayvr-ipc.git", rev = "3c411d09ba1bba2609288e29739c0f1ec736b012", default-features = false, optional = true }
################################
[build-dependencies]
regex = { version = "*" }
[features]
default = ["openvr", "openxr", "osc", "x11", "wayland"]
default = ["openvr", "openxr", "osc", "x11", "wayland", "wayvr"]
openvr = ["dep:ovr_overlay", "dep:json"]
openxr = ["dep:openxr"]
openxr = ["dep:openxr", "dep:libmonado"]
osc = ["dep:rosc"]
x11 = ["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"]
wayvr = [
"dep:khronos-egl",
"dep:smithay",
"dep:uuid",
"dep:wayland-client",
"dep:wayland-egl",
"dep:interprocess",
"dep:bytes",
"dep:wayvr_ipc",
]
as-raw-xcb-connection = []
+96 -27
View File
@@ -2,47 +2,81 @@
A lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops, inspired by XSOverlay.
WlxOverlay-S allows you to access your desktop screens while in VR.
WlxOverlay-S lets you to access your desktop screens while in VR.
Compared to similar software, WlxOverlay-S aims to run alongside your other VR games or experiences and have as little performance impact as possible. The UI looks and rendering methods are kept simple and efficient as much as possible.
In comparison to similar overlays, WlxOverlay-S aims to run alongside VR games and experiences while having as little performance impact as possible. The UI appearance and rendering techniques are kept as simple and efficient as possible, while still allowing a high degree of customizability.
This is the coming-together of two of my previous projects:
- [WlxOverlay](https://github.com/galister/WlxOverlay) (SteamVR overlay written in C#)
- [WlxOverlay-X](https://github.com/galister/wlx-overlay-x) (OpenXR overlay using StereoKit, written in Rust)
![Screenshot of WlxOverlay-S being used as an OpenXR home environment](https://github.com/galister/wlx-overlay-s/blob/guide/wlx-s.png?raw=true)
# Join the Linux VR Community
## Join the Linux VR Community
We are available on either:
- Discord: https://discord.gg/gHwJ2vwSWV
- Discord: <https://discord.gg/gHwJ2vwSWV>
- Matrix Space: `#linux-vr-adventures:matrix.org`
Questions/issues specific to WlxOverlay-S will be handled in the `wlxoverlay` chat room.
# Setup
## Setup
### General Setup
1. Grab the latest AppImage from [Releases](https://github.com/galister/wlx-overlay-s/releases).
1. `chmod +x WlxOverlay-S-*.AppImage`
1. Start Monado or SteamVR.
1. Start Monado, WiVRn or SteamVR.
1. Run the overlay
**Note:** If you are using Monado or WiVRn, no additional setup steps are required for Flatpak Steam compatibility—most people use WlxOverlay-S seamlessly with Monado/WiVRn.
### SteamVR via Steam Flatpak
For users specifically running **SteamVR via Steam Flatpak**, follow these steps:
1. Grab the latest AppImage from [Releases](https://github.com/galister/wlx-overlay-s/releases).
1. `WlxOverlay-S-*.AppImage --appimage-extract`
1. `chmod +x squashfs-root/AppRun`
1. Move the newly created `squashfs-root` folder to a location accessible by the Steam Flatpak.
1. `flatpak override com.valvesoftware.Steam --user --filesystem=xdg-run/pipewire-0/:rw`
1. Restart Steam.
1. Start SteamVR.
1. `flatpak run --command='/path/to/squashfs-root/AppRun' com.valvesoftware.Steam`
AUR package is [wlx-overlay-s-git](https://aur.archlinux.org/packages/wlx-overlay-s-git).
You may also want to [build from source](https://github.com/galister/wlx-overlay-s/wiki/Building-from-Source).
# First Start
## First Start
**If you get a screen share pop-up, check the terminal and select the screens in the order it tells you to.**
**When the screen share pop-up appears, check the terminal and select the screens in the order it requests.**
SteamVR users: WlxOverlay-S will register itself for auto-start, so you will not need to start it every time.
In case screens were selected 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 there is no need to start it every time.
**Envision users**: Set `wlx-overlay-s --openxr --show` as the _Autostart Command_ on your Envision profile! This will show a home environment with headset passthrough by default or a [customizable background](https://github.com/galister/wlx-overlay-s/wiki/OpenXR-Skybox)! If you are using the appimage instead, set the _Autostart Command_ to the location of tha appimage binary, e.g `/full/path/to/wlx-overlay-s.appimage --openxr --show`.
**Please continue reading the guide below.**
# Getting Started
## Getting Started
### Working Set
The working set consists of all currently selected overlays; screens, mirrors, keyboard, etc.
The working set appears in front of the headset when shown, and can be re-centered by hiding and showing again.
Show and hide the working set using:
- Non-vive controller: double-tap B or Y on the left controller.
- Vive controller: double-tap the menu button on the left controller (for SteamVR, the `showhide` binding must be bound)
### Pointer Modes AKA Laser Colors
Much of the functionality in WlxOverlay-S depends on what color of laser you are using to interact with a UI element. \
Much of the functionality in WlxOverlay-S depends on what color of laser is used to interact with a UI element. \
Using the default settings, there are 3 modes:
- Regular Mode: Blue laser
- Right-click Mode: Orange laser
- Middle-click Mode: Purple laser
@@ -53,7 +87,7 @@ The guide here uses the colors for ease of getting started.
### The Watch
Check your left wrist for the watch. The watch is your primary tool for controlling the app.
Check your left wrist for the watch. The watch is the primary tool for controlling the app.
![Watch usage guide](https://github.com/galister/wlx-overlay-s/blob/guide/wlx-watch.png)
@@ -62,24 +96,28 @@ Check your left wrist for the watch. The watch is your primary tool for controll
Hovering a pointer over a screen will move the mouse. If there are more than one pointers hovering a screen, the pointer that was last used to click will take precedence.
The click depends on the laser color:
- Blue laser: Left click
- Orange laser: Right click
- Purple laser: Middle click
- Stick up/down: Scroll wheel
See the bindings section on how to grab, move and resize screens.
To **curve a screen**, grab it with one hand. Then, using the other hand, hover the laser over the screen and use the scroll action.
See the [bindings](#default-bindings) section on how to grab, move and resize screens.
### The keyboard
The keyboard is fully customizable via the [keyboard.yaml](https://raw.githubusercontent.com/galister/wlx-overlay-s/main/src/res/keyboard.yaml) file. \
Download it into your `~/.config/wlxoverlay/` folder and edit it to your liking.
Download it into the `~/.config/wlxoverlay/` folder and edit it to your liking.
Typing
- Use the BLUE laser when typing regularly.
- While using ORANGE laser, all keystrokes will have SHIFT applied.
- Purple laser has no effect as of now.
**Modifier Keys** are sticky. They will remain pressed until you press a non-modifier key, or toggle them off.
**Modifier Keys** are sticky. They will remain pressed until a non-modifier key is pressed, the modifier gets toggled off, or the keyboard gets hidden.
### Default Bindings
@@ -87,23 +125,54 @@ Typing
![Touch Controller Bindings](https://github.com/galister/wlx-overlay-s/blob/guide/wlx-oculus.png)
To customize bindings on OpenXR, refer to the [OpenXR Bindings wiki page](https://github.com/galister/wlx-overlay-s/wiki/OpenXR-Bindings).
If your bindings are not supported, please reach out. \
We would like to work with you and include additional bindings.
# Known Issues
## Troubleshooting
## SteamVR: laser pointers not visible
When an error is detected, we often print tips for fixing into the log file.
This seems to be a rare issue with SteamVR startup overlays. Restarting the overlay will fix the issue.
Logs will be at `/tmp/wlx.log` for most distros.
## Scroll wheel doesn't work
Check [here](https://github.com/galister/wlx-overlay-s/wiki/Troubleshooting) for tips.
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.
## Known Issues
## WiVRn support
### Mouse is not where it should be
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.
Hyprland users: Hyprland v0.41.0 changed their absolute input implementation to one that does not respect existing absolute input standards. Make your voice heard: [Hyprland#6023](https://github.com/hyprwm/Hyprland/issues/6023)・[Hyprland#6889](https://github.com/hyprwm/Hyprland/issues/6889)
## X11 limitations
Niri users: use on Niri 0.1.7 or later.
DPI scaling and upright screens are not supported on X11. These might display incorrectly or mess up your mouse position.
X11 users might be dealing with a [Phantom Monitor](https://wiki.archlinux.org/title/Xrandr#Disabling_phantom_monitor).
Other desktops: The screens may have been selected in the wrong order, see [First Start](#first-start).
### Crashes, blank screens
There are some driver-desktop combinations that don't play nice with DMA-buf capture.
Disabling DMA-buf capture is a good first step to try when encountering an app crash or gpu driver reset.
```bash
echo 'capture_method: pw_fallback' > ~/.config/wlxoverlay/conf.d/pw_fallback.yaml
```
Without DMA-buf capture, capturing screens takes CPU power, so let's try and not show too many screens at the same time.
### Space-drag crashes SteamVR
This has been idenfitied as an issue with SteamVR versions 2.5.5 and above (latest tested 2.7.2). One way to avoid the crash is by switching to the `temp-v1.27.5` branch of SteamVR (via beta selection) and selecting [Steam-Play-None](https://github.com/Scrumplex/Steam-Play-None) under the compatibility tab.
### Modifiers get stuck in weird ways
This is a rare issue that can make KDE Plasma not react to click or keys due to what seems to be a race condition with modifiers. Restarting the overlay fixes this.
### X11 limitations
- X11 capture can generally seem slow. This is because zero-copy GPU capture is not supported on the general X11 desktop. Consider trying Wayland or Picom.
- DPI scaling is not supported and may cause the mouse to not follow the laser properly.
- Upright screens are not supported and can cause the mouse to act weirdly.
- Screen changes (connecting / disconnecting a display, resolution changes, etc) are not handled at runtime. Restart the overlay for these to take effect.
+40
View File
@@ -0,0 +1,40 @@
use regex::Regex;
use std::process::Command;
fn main() {
let mut wlx_build = get_version().unwrap_or(format!("{}-unknown", env!("CARGO_PKG_VERSION")));
match std::env::var("GITHUB_JOB").as_deref() {
Ok("make_release") => {
wlx_build = format!("{} (Release)", &wlx_build);
}
Ok("build_appimage") => {
wlx_build = format!("{} (AppImage)", &wlx_build);
}
_ => {}
}
println!("cargo:rustc-env=WLX_BUILD={}", &wlx_build);
}
fn get_version() -> Result<String, Box<dyn std::error::Error>> {
let re = Regex::new(r"v([0-9.]+)-([0-9]+)-g([a-f0-9]+)").unwrap(); // safe
let output = Command::new("git")
.args(["describe", "--tags", "--abbrev=7", "--dirty"])
.output()?;
let mut output_str = String::from_utf8(output.stdout)?;
if output_str.is_empty() {
let output = Command::new("git")
.args(["describe", "--tags", "--abbrev=7", "--dirty", "--always"])
.output()?;
output_str = format!(
"{}-{}",
env!("CARGO_PKG_VERSION"),
String::from_utf8(output.stdout)?
);
}
Ok(re.replace_all(&output_str, "${1}.r${2}.${3}").into_owned())
}
+90
View File
@@ -0,0 +1,90 @@
<p align="center">
<img src="https://raw.githubusercontent.com/galister/wlx-overlay-s/refs/heads/guide/wayvr/logo.svg" height="120"/>
</p>
**WayVR acts as a bridge between Wayland applications and wlx-overlay-s panels, allowing you to display your applications within a VR environment. Internally, WayVR utilizes Smithay to run a Wayland compositor.**
# >> Quick setup <<
#### Configure your applications list
Go to `src/res/wayvr.yaml` to configure your desired application list. This configuration file represents all currently available WayVR options. Feel free to adjust it to your liking.
#### Add WayVR Launcher to your watch
Copy `watch_wayvr_example.yaml` to `~/.config/wlxoverlay/watch.yaml`. This file contains pre-configured **WayVRLauncher** and **WayVRDisplayList** widget types. By default, the _default_catalog_ is used.
That's it; you're all set!
###### _Make sure you have `wayvr` feature enabled in Cargo.toml (enabled by default)_
![alt text](https://raw.githubusercontent.com/galister/wlx-overlay-s/refs/heads/guide/wayvr/watch.jpg)
# Overview
### Features
- Display Wayland applications without GPU overhead (zero-copy via dma-buf)
- Mouse and keyboard input, with precision scrolling support
- Tested on AMD and Nvidia
### Supported software
- Basically all Qt and GTK applications (they work out of the box)
- Most XWayland applications via `cage`
### XWayland
WayVR does not have native XWayland support. You can run X11 applications (or these who require DISPLAY set) by wrapping them in a `cage` program, like so:
```yaml
- name: "Xeyes"
target_display: "Disp1"
exec: "cage"
args: "xeyes -- -fg blue"
```
instead of:
```yaml
- name: "Xeyes"
target_display: "Disp1"
exec: "xeyes"
args: "-fg blue"
```
in `wayvr.yaml` configuration file, in your desired catalog.
### Launching external apps inside WayVR
To launch your app externally:
```sh
DISPLAY= WAYLAND_DISPLAY=wayland-$(cat $XDG_RUNTIME_DIR/wayvr.disp) yourapp
```
or (in the most cases):
```
DISPLAY= WAYLAND_DISPLAY=wayland-20 yourapp
```
Setting `DISPLAY` to an empty string forces various apps to use Wayland instead of X11.
# Troubleshooting
### My application doesn't launch but others do!
Even though some applications support Wayland, some still check for the `DISPLAY` environment variable and an available X11 server, throwing an error. This can also be fixed by running `cage` on top of them.
### Image corruption
dma-buf textures may display various graphical glitches due to unsupported dma-buf tiling modifiers between GLES<->Vulkan on Radeon RDNA3 graphics cards. Current situation: https://gitlab.freedesktop.org/mesa/mesa/-/issues/11629). Nvidia should work out of the box, without any isues. Alternatively, you can run wlx-overlay-s with `LIBGL_ALWAYS_SOFTWARE=1` to mitigate that (only the Smithay compositor will run in software renderer mode, wlx will still be accelerated).
### Floating windows
Context menus are not functional in most cases yet, including drag & drop support.
### Forced window shadows in GTK
GNOME still insists on rendering client-side decorations instead of server-side ones. This results in all GTK applications looking odd due to additional window shadows. [Fix here, "Client-side decorations"](https://wiki.archlinux.org/title/GTK)
+195
View File
@@ -0,0 +1,195 @@
# looking to make changes?
# drop me in ~/.config/wlxoverlay/watch.yaml
#
width: 0.115
size: [400, 272]
elements:
# background panel
- type: Panel
rect: [0, 30, 400, 130]
corner_radius: 20
bg_color: "#24273a"
- type: Button
rect: [2, 162, 26, 36]
corner_radius: 4
font_size: 15
bg_color: "#c6a0f6"
fg_color: "#24273a"
text: "C"
click_up: # destroy if exists, otherwise create
- type: Window
target: settings
action: ShowUi # only triggers if not exists
- type: Window
target: settings
action: Destroy # only triggers if exists since before current frame
# Keyboard button
- type: Button
rect: [32, 162, 60, 36]
corner_radius: 4
font_size: 15
fg_color: "#24273a"
bg_color: "#a6da95"
text: Kbd
click_up:
- type: Overlay
target: "kbd"
action: ToggleVisible
long_click_up:
- type: Overlay
target: "kbd"
action: Reset
right_up:
- type: Overlay
target: "kbd"
action: ToggleImmovable
middle_up:
- type: Overlay
target: "kbd"
action: ToggleInteraction
scroll_up:
- type: Overlay
target: "kbd"
action:
Opacity: { delta: 0.025 }
scroll_down:
- type: Overlay
target: "kbd"
action:
Opacity: { delta: -0.025 }
# bottom row, of keyboard + overlays
- type: OverlayList
rect: [94, 160, 306, 40]
corner_radius: 4
font_size: 15
fg_color: "#cad3f5"
bg_color: "#1e2030"
layout: Horizontal
click_up: ToggleVisible
long_click_up: Reset
right_up: ToggleImmovable
middle_up: ToggleInteraction
scroll_up:
Opacity: { delta: 0.025 }
scroll_down:
Opacity: { delta: -0.025 }
- type: WayVRLauncher
rect: [0, 200, 400, 36]
corner_radius: 4
font_size: 15
fg_color: "#24273a"
bg_color: "#e590c4"
catalog_name: "default_catalog"
- type: WayVRDisplayList
rect: [0, 236, 400, 36]
corner_radius: 4
font_size: 15
fg_color: "#24273a"
bg_color: "#ca68a4"
# local clock
- type: Label
rect: [19, 90, 200, 50]
corner_radius: 4
font_size: 46 # Use 32 for 12-hour time
fg_color: "#cad3f5"
source: Clock
format: "%H:%M" # 23:59
#format: "%I:%M %p" # 11:59 PM
# local date
- type: Label
rect: [20, 117, 200, 20]
corner_radius: 4
font_size: 14
fg_color: "#cad3f5"
source: Clock
format: "%x" # local date representation
# local day-of-week
- type: Label
rect: [20, 137, 200, 50]
corner_radius: 4
font_size: 14
fg_color: "#cad3f5"
source: Clock
format: "%A" # Tuesday
#format: "%a" # Tue
# alt clock 1
- type: Label
rect: [210, 90, 200, 50]
corner_radius: 4
font_size: 24 # Use 18 for 12-hour time
fg_color: "#8bd5ca"
source: Clock
timezone: 0 # change TZ1 here
format: "%H:%M" # 23:59
#format: "%I:%M %p" # 11:59 PM
- type: Label
rect: [210, 60, 200, 50]
corner_radius: 4
font_size: 14
fg_color: "#8bd5ca"
source: Timezone
timezone: 0 # change TZ1 label here
# alt clock 2
- type: Label
rect: [210, 150, 200, 50]
corner_radius: 4
font_size: 24 # Use 18 for 12-hour time
fg_color: "#b7bdf8"
source: Clock
timezone: 1 # change TZ2 here
format: "%H:%M" # 23:59
#format: "%I:%M %p" # 11:59 PM
- type: Label
rect: [210, 120, 200, 50]
corner_radius: 4
font_size: 14
fg_color: "#b7bdf8"
source: Timezone
timezone: 1 # change TZ2 label here
# batteries
- type: BatteryList
rect: [0, 5, 400, 30]
corner_radius: 4
font_size: 16
fg_color: "#8bd5ca"
fg_color_low: "#B06060"
fg_color_charging: "#6080A0"
num_devices: 9
layout: Horizontal
low_threshold: 33
# volume buttons
- type: Button
rect: [315, 52, 70, 32]
corner_radius: 4
font_size: 13
fg_color: "#cad3f5"
bg_color: "#5b6078"
text: "Vol +"
click_down:
- type: Exec
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%"]
- type: Button
rect: [315, 116, 70, 32]
corner_radius: 4
font_size: 13
fg_color: "#cad3f5"
bg_color: "#5b6078"
text: "Vol -"
click_down:
- type: Exec
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%"]
+12
View File
@@ -0,0 +1,12 @@
[Unit]
Description=wlx-overlay-s - Lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops
After=monado.service
BindsTo=monado.service
Requires=monado.socket
Requires=graphical-session.target
[Service]
ExecStart=@prefix@/bin/wlx-overlay-s
[Install]
WantedBy=monado.service
+243 -148
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::{
keyboard::create_keyboard,
anchor::create_anchor,
keyboard::{create_keyboard, KEYBOARD_NAME},
screen::WlxClientAlias,
watch::{create_watch, WATCH_NAME},
},
state::{AppState, ScreenMeta},
state::AppState,
};
use super::overlay::{OverlayBackend, OverlayData, OverlayState};
use super::overlay::{OverlayData, OverlayID};
#[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,203 @@ 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 mut watch = create_watch::<T>(app)?;
watch.state.want_visible = true;
overlays.insert(watch.state.id, watch);
let mut keyboard = create_keyboard(app)?;
keyboard.state.show_hide = true;
keyboard.state.want_visible = false;
overlays.insert(keyboard.state.id, keyboard);
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) = screens.first() {
show_screens.push(s.state.name.clone());
if let Some((_, s, _)) = data.screens.first() {
show_screens.arc_set(s.name.clone());
}
}
for mut screen in screens {
if show_screens.contains(&screen.state.name) {
screen.state.show_hide = true;
screen.state.want_visible = false;
for (meta, mut state, backend) in data.screens {
if show_screens.arc_get(state.name.as_ref()) {
state.show_hide = true;
}
overlays.insert(screen.state.id, screen);
overlays.insert(
state.id.0,
OverlayData::<T> {
state,
backend,
..Default::default()
},
);
app.screens.push(meta);
}
Ok(Self { overlays, extent })
let anchor = create_anchor(app)?;
overlays.insert(anchor.state.id.0, anchor);
let mut watch = create_watch::<T>(app)?;
watch.state.want_visible = true;
overlays.insert(watch.state.id.0, watch);
let mut keyboard = create_keyboard(app, keymap)?;
keyboard.state.show_hide = show_screens.arc_get(KEYBOARD_NAME);
keyboard.state.want_visible = false;
overlays.insert(keyboard.state.id.0, keyboard);
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.0,
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.0).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.0) 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.0) 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;
}
}
}
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));
}
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>> {
@@ -100,7 +270,7 @@ where
pub fn remove_by_selector(&mut self, selector: &OverlaySelector) -> Option<OverlayData<T>> {
match selector {
OverlaySelector::Id(id) => self.overlays.remove(id),
OverlaySelector::Id(id) => self.overlays.remove(id.0),
OverlaySelector::Name(name) => {
let id = self
.overlays
@@ -112,12 +282,12 @@ where
}
}
pub fn get_by_id(&mut self, id: usize) -> Option<&OverlayData<T>> {
self.overlays.get(id)
pub fn get_by_id(&mut self, id: OverlayID) -> Option<&OverlayData<T>> {
self.overlays.get(id.0)
}
pub fn mut_by_id(&mut self, id: usize) -> Option<&mut OverlayData<T>> {
self.overlays.get_mut(id)
pub fn mut_by_id(&mut self, id: OverlayID) -> Option<&mut OverlayData<T>> {
self.overlays.get_mut(id.0)
}
pub fn get_by_name<'a>(&'a mut self, name: &str) -> Option<&'a OverlayData<T>> {
@@ -137,7 +307,7 @@ where
}
pub fn add(&mut self, overlay: OverlayData<T>) {
self.overlays.insert(overlay.state.id, overlay);
self.overlays.insert(overlay.state.id.0, overlay);
}
pub fn show_hide(&mut self, app: &mut AppState) {
@@ -146,10 +316,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 +341,29 @@ where
}
}
#[derive(Clone, Deserialize)]
#[derive(Clone, Deserialize, Debug)]
#[serde(untagged)]
pub enum OverlaySelector {
Id(usize),
Id(OverlayID),
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))
}
+271 -53
View File
@@ -1,22 +1,21 @@
use std::f32::consts::PI;
use std::process::{Child, Command};
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::state::AppState;
use crate::backend::common::{snap_upright, OverlaySelector};
use crate::config::{AStrMapExt, GeneralConfig};
use crate::overlays::anchor::ANCHOR_NAME;
use crate::state::{AppSession, AppState, KeyboardFocus};
use super::{
common::{raycast, OverlayContainer},
overlay::OverlayData,
};
use super::overlay::{OverlayID, OverlayState};
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,16 +33,20 @@ pub enum TrackedDeviceRole {
pub struct InputState {
pub hmd: Affine3A,
pub ipd: f32,
pub pointers: [Pointer; 2],
pub devices: Vec<TrackedDevice>,
processes: Vec<Child>,
}
impl InputState {
pub fn new() -> Self {
Self {
hmd: Affine3A::IDENTITY,
ipd: 0.0,
pointers: [Pointer::new(0), Pointer::new(1)],
devices: Vec::new(),
processes: Vec::new(),
}
}
@@ -52,7 +55,7 @@ impl InputState {
self.pointers[1].before = self.pointers[1].now;
}
pub fn post_update(&mut self) {
pub fn post_update(&mut self, session: &AppSession) {
for hand in &mut self.pointers {
#[cfg(debug_assertions)]
{
@@ -68,9 +71,22 @@ impl InputState {
if hand.now.show_hide != hand.before.show_hide {
log::debug!("Hand {}: show_hide {}", hand.idx, hand.now.show_hide);
}
if hand.now.toggle_dashboard != hand.before.toggle_dashboard {
log::debug!(
"Hand {}: toggle_dashboard {}",
hand.idx,
hand.now.toggle_dashboard
);
}
if hand.now.space_drag != hand.before.space_drag {
log::debug!("Hand {}: space_drag {}", hand.idx, hand.now.space_drag);
}
if hand.now.space_rotate != hand.before.space_rotate {
log::debug!("Hand {}: space_rotate {}", hand.idx, hand.now.space_rotate);
}
if hand.now.space_reset != hand.before.space_reset {
log::debug!("Hand {}: space_reset {}", hand.idx, hand.now.space_reset);
}
if hand.now.click_modifier_right != hand.before.click_modifier_right {
log::debug!(
"Hand {}: click_modifier_right {}",
@@ -128,6 +144,24 @@ impl InputState {
}
_ => {}
};
if hand.now.alt_click != hand.before.alt_click {
// Reap previous processes
self.processes
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
let mut args = if hand.now.alt_click {
session.config.alt_click_down.iter()
} else {
session.config.alt_click_up.iter()
};
if let Some(program) = args.next() {
if let Ok(child) = Command::new(program).args(args).spawn() {
self.processes.push(child);
}
}
}
}
}
}
@@ -135,8 +169,8 @@ impl InputState {
pub struct InteractionState {
pub mode: PointerMode,
pub grabbed: Option<GrabData>,
pub clicked_id: Option<usize>,
pub hovered_id: Option<usize>,
pub clicked_id: Option<OverlayID>,
pub hovered_id: Option<OverlayID>,
pub release_actions: VecDeque<Box<dyn Fn()>>,
pub next_push: Instant,
pub haptics: Option<f32>,
@@ -188,21 +222,26 @@ pub struct PointerState {
pub grab: bool,
pub alt_click: bool,
pub show_hide: bool,
pub toggle_dashboard: bool,
pub space_drag: bool,
pub space_rotate: bool,
pub space_reset: bool,
pub click_modifier_right: bool,
pub click_modifier_middle: bool,
pub move_mouse: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PointerHit {
pub pointer: usize,
pub overlay: usize,
pub overlay: OverlayID,
pub mode: PointerMode,
pub primary: bool,
pub uv: Vec2,
pub dist: f32,
}
#[derive(Clone)]
pub struct Haptics {
pub intensity: f32,
pub duration: f32,
@@ -229,15 +268,18 @@ impl InteractionHandler for DummyInteractionHandler {
#[derive(Debug, Clone, Copy, Default)]
struct RayHit {
overlay: usize,
hit_pos: Vec3A,
overlay: OverlayID,
global_pos: Vec3A,
local_pos: Vec2,
dist: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GrabData {
pub offset: Vec3A,
pub grabbed_id: usize,
pub grabbed_id: OverlayID,
pub old_curvature: Option<f32>,
pub grab_all: bool,
}
#[repr(u8)]
@@ -250,6 +292,15 @@ pub enum PointerMode {
Special,
}
fn update_focus(focus: &mut KeyboardFocus, state: &OverlayState) {
if let Some(f) = &state.keyboard_focus {
if *focus != *f {
log::info!("Setting keyboard focus to {:?}", *f);
*focus = *f;
}
}
}
pub fn interact<O>(
overlays: &mut OverlayContainer<O>,
app: &mut AppState,
@@ -276,13 +327,19 @@ 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);
log::warn!("Grabbed overlay {} does not exist", grab_data.grabbed_id.0);
pointer.interaction.grabbed = None;
}
return (0.1, None);
@@ -296,15 +353,17 @@ where
pointer = &mut app.input_state.pointers[idx];
pointer.interaction.hovered_id = None;
}
if let Some(clicked_id) = pointer.interaction.clicked_id.take() {
if let Some(clicked) = overlays.mut_by_id(clicked_id) {
let hit = PointerHit {
pointer: pointer.idx,
overlay: clicked_id,
mode: pointer.interaction.mode,
..Default::default()
};
clicked.backend.on_pointer(app, &hit, false);
if !pointer.now.click && pointer.before.click {
if let Some(clicked_id) = pointer.interaction.clicked_id.take() {
if let Some(clicked) = overlays.mut_by_id(clicked_id) {
let hit = PointerHit {
pointer: pointer.idx,
overlay: clicked_id,
mode: pointer.interaction.mode,
..Default::default()
};
clicked.backend.on_pointer(app, &hit, false);
}
}
}
return (0.0, None); // no hit
@@ -322,7 +381,7 @@ where
}
}
let Some(hovered) = overlays.mut_by_id(hit.overlay) else {
log::warn!("Hit overlay {} does not exist", hit.overlay);
log::warn!("Hit overlay {} does not exist", hit.overlay.0);
return (0.0, None); // no hit
};
@@ -342,7 +401,8 @@ where
log::trace!("Hit: {} {:?}", hovered.state.name, hit);
if pointer.now.grab && !pointer.before.grab && hovered.state.grabbable {
pointer.start_grab(hovered);
update_focus(&mut app.keyboard_focus, &hovered.state);
pointer.start_grab(hovered, &mut app.tasks);
return (
hit.dist,
Some(Haptics {
@@ -353,17 +413,43 @@ where
);
}
// Pass mouse motion events only if not scrolling
// (allows scrolling on all Chromium-based applications)
let haptics = hovered.backend.on_hover(app, &hit);
pointer = &mut app.input_state.pointers[idx];
if pointer.now.scroll.abs() > 0.1 {
let scroll = pointer.now.scroll;
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 can_curve = hovered
.frame_transform()
.is_some_and(|e| e.extent[0] >= e.extent[1]);
if can_curve {
let cur = hovered.state.curvature.unwrap_or(0.0);
let new = (cur - scroll * 0.01).min(0.5);
if new <= f32::EPSILON {
hovered.state.curvature = None;
} else {
hovered.state.curvature = Some(new);
}
} else {
hovered.state.curvature = None;
}
} else {
hovered.backend.on_scroll(app, &hit, scroll);
}
pointer = &mut app.input_state.pointers[idx];
}
if pointer.now.click && !pointer.before.click {
pointer.interaction.clicked_id = Some(hit.overlay);
update_focus(&mut app.keyboard_focus, &hovered.state);
hovered.backend.on_pointer(app, &hit, true);
} else if !pointer.now.click && pointer.before.click {
if let Some(clicked_id) = pointer.interaction.clicked_id.take() {
@@ -389,7 +475,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;
}
@@ -404,12 +494,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;
@@ -428,7 +514,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,
{
@@ -440,12 +526,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 {
@@ -465,35 +572,146 @@ 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);
overlay.state.realign(hmd);
overlay.state.dirty = true;
} else {
log::error!("Grabbed overlay {} does not exist", overlay.state.id);
log::error!("Grabbed overlay {} does not exist", overlay.state.id.0);
self.interaction.grabbed = None;
}
} else {
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(), // safe
);
}
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: OverlayID,
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()))
}
+11
View File
@@ -2,13 +2,24 @@ pub mod common;
pub mod input;
pub mod notifications;
#[allow(clippy::all)]
mod notifications_dbus;
#[cfg(feature = "openvr")]
pub mod openvr;
#[cfg(feature = "openxr")]
pub mod openxr;
#[cfg(feature = "uidev")]
pub mod uidev;
#[cfg(feature = "osc")]
pub mod osc;
#[cfg(feature = "wayvr")]
pub mod wayvr;
pub mod overlay;
pub mod task;
+97 -39
View File
@@ -1,17 +1,29 @@
use dbus::{blocking::Connection, channel::MatchingReceiver, message::MatchRule};
use serde::{Deserialize, Serialize};
use dbus::{
arg::{PropMap, Variant},
blocking::Connection,
channel::MatchingReceiver,
message::MatchRule,
};
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::{
backend::notifications_dbus::OrgFreedesktopNotifications,
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 +33,7 @@ impl NotificationManager {
rx_toast,
tx_toast,
dbus_data: None,
running: Arc::new(AtomicBool::new(true)),
}
}
@@ -125,7 +138,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 +148,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 +175,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 +191,71 @@ impl NotificationManager {
}
}
}
log::info!("Notification listener stopped.");
});
}
}
impl Drop for NotificationManager {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
}
}
pub struct DbusNotificationSender {
connection: Connection,
}
impl DbusNotificationSender {
pub fn new() -> anyhow::Result<Self> {
Ok(Self {
connection: Connection::new_session()?,
})
}
pub fn notify_send(
&self,
summary: &str,
body: &str,
urgency: u8,
timeout: i32,
replaces_id: u32,
transient: bool,
) -> anyhow::Result<u32> {
let proxy = self.connection.with_proxy(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
Duration::from_millis(1000),
);
let mut hints = PropMap::new();
hints.insert("urgency".to_string(), Variant(Box::new(urgency)));
hints.insert("transient".to_string(), Variant(Box::new(transient)));
Ok(proxy.notify(
"WlxOverlay-S",
replaces_id,
"",
summary,
body,
vec![],
hints,
timeout,
)?)
}
pub fn notify_close(&self, id: u32) -> anyhow::Result<()> {
let proxy = self.connection.with_proxy(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
Duration::from_millis(1000),
);
proxy.close_notification(id)?;
Ok(())
}
}
fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
let mut args = msg.iter_init();
let app_name: String = args.read()?;
@@ -188,9 +270,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 +295,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(())
}
+267
View File
@@ -0,0 +1,267 @@
// This code was autogenerated with `dbus-codegen-rust -g -m None -d org.freedesktop.Notifications -p /org/freedesktop/Notifications`, see https://github.com/diwic/dbus-rs
use dbus as dbus;
#[allow(unused_imports)]
use dbus::arg;
use dbus::blocking;
pub trait OrgFreedesktopDBusProperties {
fn get<R0: for<'b> arg::Get<'b> + 'static>(&self, interface_name: &str, property_name: &str) -> Result<R0, dbus::Error>;
fn get_all(&self, interface_name: &str) -> Result<arg::PropMap, dbus::Error>;
fn set<I2: arg::Arg + arg::Append>(&self, interface_name: &str, property_name: &str, value: I2) -> Result<(), dbus::Error>;
}
#[derive(Debug)]
pub struct OrgFreedesktopDBusPropertiesPropertiesChanged {
pub interface_name: String,
pub changed_properties: arg::PropMap,
pub invalidated_properties: Vec<String>,
}
impl arg::AppendAll for OrgFreedesktopDBusPropertiesPropertiesChanged {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.interface_name, i);
arg::RefArg::append(&self.changed_properties, i);
arg::RefArg::append(&self.invalidated_properties, i);
}
}
impl arg::ReadAll for OrgFreedesktopDBusPropertiesPropertiesChanged {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(OrgFreedesktopDBusPropertiesPropertiesChanged {
interface_name: i.read()?,
changed_properties: i.read()?,
invalidated_properties: i.read()?,
})
}
}
impl dbus::message::SignalArgs for OrgFreedesktopDBusPropertiesPropertiesChanged {
const NAME: &'static str = "PropertiesChanged";
const INTERFACE: &'static str = "org.freedesktop.DBus.Properties";
}
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target=T>> OrgFreedesktopDBusProperties for blocking::Proxy<'a, C> {
fn get<R0: for<'b> arg::Get<'b> + 'static>(&self, interface_name: &str, property_name: &str) -> Result<R0, dbus::Error> {
self.method_call("org.freedesktop.DBus.Properties", "Get", (interface_name, property_name, ))
.and_then(|r: (arg::Variant<R0>, )| Ok((r.0).0, ))
}
fn get_all(&self, interface_name: &str) -> Result<arg::PropMap, dbus::Error> {
self.method_call("org.freedesktop.DBus.Properties", "GetAll", (interface_name, ))
.and_then(|r: (arg::PropMap, )| Ok(r.0, ))
}
fn set<I2: arg::Arg + arg::Append>(&self, interface_name: &str, property_name: &str, value: I2) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.DBus.Properties", "Set", (interface_name, property_name, arg::Variant(value), ))
}
}
pub trait OrgFreedesktopDBusIntrospectable {
fn introspect(&self) -> Result<String, dbus::Error>;
}
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target=T>> OrgFreedesktopDBusIntrospectable for blocking::Proxy<'a, C> {
fn introspect(&self) -> Result<String, dbus::Error> {
self.method_call("org.freedesktop.DBus.Introspectable", "Introspect", ())
.and_then(|r: (String, )| Ok(r.0, ))
}
}
pub trait OrgFreedesktopDBusPeer {
fn ping(&self) -> Result<(), dbus::Error>;
fn get_machine_id(&self) -> Result<String, dbus::Error>;
}
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target=T>> OrgFreedesktopDBusPeer for blocking::Proxy<'a, C> {
fn ping(&self) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.DBus.Peer", "Ping", ())
}
fn get_machine_id(&self) -> Result<String, dbus::Error> {
self.method_call("org.freedesktop.DBus.Peer", "GetMachineId", ())
.and_then(|r: (String, )| Ok(r.0, ))
}
}
pub trait OrgFreedesktopNotifications {
fn set_noti_window_visibility(&self, value: bool) -> Result<(), dbus::Error>;
fn toggle_dnd(&self) -> Result<bool, dbus::Error>;
fn set_dnd(&self, state: bool) -> Result<(), dbus::Error>;
fn get_dnd(&self) -> Result<bool, dbus::Error>;
fn manually_close_notification(&self, id: u32, timeout: bool) -> Result<(), dbus::Error>;
fn close_all_notifications(&self) -> Result<(), dbus::Error>;
fn hide_latest_notification(&self, close: bool) -> Result<(), dbus::Error>;
fn get_capabilities(&self) -> Result<Vec<String>, dbus::Error>;
fn notify(&self, app_name: &str, replaces_id: u32, app_icon: &str, summary: &str, body: &str, actions: Vec<&str>, hints: arg::PropMap, expire_timeout: i32) -> Result<u32, dbus::Error>;
fn close_notification(&self, id: u32) -> Result<(), dbus::Error>;
fn get_server_information(&self) -> Result<(String, String, String, String), dbus::Error>;
fn dnd(&self) -> Result<bool, dbus::Error>;
fn set_dnd_(&self, value: bool) -> Result<(), dbus::Error>;
}
#[derive(Debug)]
pub struct OrgFreedesktopNotificationsOnDndToggle {
pub dnd: bool,
}
impl arg::AppendAll for OrgFreedesktopNotificationsOnDndToggle {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.dnd, i);
}
}
impl arg::ReadAll for OrgFreedesktopNotificationsOnDndToggle {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(OrgFreedesktopNotificationsOnDndToggle {
dnd: i.read()?,
})
}
}
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsOnDndToggle {
const NAME: &'static str = "OnDndToggle";
const INTERFACE: &'static str = "org.freedesktop.Notifications";
}
#[derive(Debug)]
pub struct OrgFreedesktopNotificationsNotificationClosed {
pub id: u32,
pub reason: u32,
}
impl arg::AppendAll for OrgFreedesktopNotificationsNotificationClosed {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.id, i);
arg::RefArg::append(&self.reason, i);
}
}
impl arg::ReadAll for OrgFreedesktopNotificationsNotificationClosed {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(OrgFreedesktopNotificationsNotificationClosed {
id: i.read()?,
reason: i.read()?,
})
}
}
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsNotificationClosed {
const NAME: &'static str = "NotificationClosed";
const INTERFACE: &'static str = "org.freedesktop.Notifications";
}
#[derive(Debug)]
pub struct OrgFreedesktopNotificationsActionInvoked {
pub id: u32,
pub action_key: String,
}
impl arg::AppendAll for OrgFreedesktopNotificationsActionInvoked {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.id, i);
arg::RefArg::append(&self.action_key, i);
}
}
impl arg::ReadAll for OrgFreedesktopNotificationsActionInvoked {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(OrgFreedesktopNotificationsActionInvoked {
id: i.read()?,
action_key: i.read()?,
})
}
}
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsActionInvoked {
const NAME: &'static str = "ActionInvoked";
const INTERFACE: &'static str = "org.freedesktop.Notifications";
}
#[derive(Debug)]
pub struct OrgFreedesktopNotificationsNotificationReplied {
pub id: u32,
pub text: String,
}
impl arg::AppendAll for OrgFreedesktopNotificationsNotificationReplied {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.id, i);
arg::RefArg::append(&self.text, i);
}
}
impl arg::ReadAll for OrgFreedesktopNotificationsNotificationReplied {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(OrgFreedesktopNotificationsNotificationReplied {
id: i.read()?,
text: i.read()?,
})
}
}
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsNotificationReplied {
const NAME: &'static str = "NotificationReplied";
const INTERFACE: &'static str = "org.freedesktop.Notifications";
}
impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target=T>> OrgFreedesktopNotifications for blocking::Proxy<'a, C> {
fn set_noti_window_visibility(&self, value: bool) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "SetNotiWindowVisibility", (value, ))
}
fn toggle_dnd(&self) -> Result<bool, dbus::Error> {
self.method_call("org.freedesktop.Notifications", "ToggleDnd", ())
.and_then(|r: (bool, )| Ok(r.0, ))
}
fn set_dnd(&self, state: bool) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "SetDnd", (state, ))
}
fn get_dnd(&self) -> Result<bool, dbus::Error> {
self.method_call("org.freedesktop.Notifications", "GetDnd", ())
.and_then(|r: (bool, )| Ok(r.0, ))
}
fn manually_close_notification(&self, id: u32, timeout: bool) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "ManuallyCloseNotification", (id, timeout, ))
}
fn close_all_notifications(&self) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "CloseAllNotifications", ())
}
fn hide_latest_notification(&self, close: bool) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "HideLatestNotification", (close, ))
}
fn get_capabilities(&self) -> Result<Vec<String>, dbus::Error> {
self.method_call("org.freedesktop.Notifications", "GetCapabilities", ())
.and_then(|r: (Vec<String>, )| Ok(r.0, ))
}
fn notify(&self, app_name: &str, replaces_id: u32, app_icon: &str, summary: &str, body: &str, actions: Vec<&str>, hints: arg::PropMap, expire_timeout: i32) -> Result<u32, dbus::Error> {
self.method_call("org.freedesktop.Notifications", "Notify", (app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout, ))
.and_then(|r: (u32, )| Ok(r.0, ))
}
fn close_notification(&self, id: u32) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "CloseNotification", (id, ))
}
fn get_server_information(&self) -> Result<(String, String, String, String), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "GetServerInformation", ())
}
fn dnd(&self) -> Result<bool, dbus::Error> {
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(self, "org.freedesktop.Notifications", "Dnd")
}
fn set_dnd_(&self, value: bool) -> Result<(), dbus::Error> {
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::set(self, "org.freedesktop.Notifications", "Dnd", value)
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ use glam::Affine3A;
use ovr_overlay::{pose::Matrix3x4, settings::SettingsManager, sys::HmdMatrix34_t};
use thiserror::Error;
use crate::backend::common::{BackendError, ColorChannel};
use crate::backend::{common::BackendError, task::ColorChannel};
pub trait Affine3AConvert {
fn from_affine(affine: &Affine3A) -> Self;
+39 -12
View File
@@ -13,7 +13,7 @@ use ovr_overlay::{
use crate::{
backend::input::{Haptics, TrackedDevice, TrackedDeviceRole},
config_io::CONFIG_ROOT_PATH,
config_io,
state::AppState,
};
@@ -30,14 +30,17 @@ const PATH_HAPTICS: [&str; 2] = [
"/actions/default/out/HapticsRight",
];
const PATH_ALT_CLICK: &str = "/actions/default/in/AltClick";
const PATH_CLICK_MODIFIER_MIDDLE: &str = "/actions/default/in/ClickModifierMiddle";
const PATH_CLICK_MODIFIER_RIGHT: &str = "/actions/default/in/ClickModifierRight";
const PATH_CLICK: &str = "/actions/default/in/Click";
const PATH_GRAB: &str = "/actions/default/in/Grab";
const PATH_MOVE_MOUSE: &str = "/actions/default/in/MoveMouse";
const PATH_SCROLL: &str = "/actions/default/in/Scroll";
const PATH_ALT_CLICK: &str = "/actions/default/in/AltClick";
const PATH_SHOW_HIDE: &str = "/actions/default/in/ShowHide";
const PATH_SPACE_DRAG: &str = "/actions/default/in/SpaceDrag";
const PATH_CLICK_MODIFIER_RIGHT: &str = "/actions/default/in/ClickModifierRight";
const PATH_CLICK_MODIFIER_MIDDLE: &str = "/actions/default/in/ClickModifierMiddle";
const PATH_SPACE_ROTATE: &str = "/actions/default/in/SpaceRotate";
const PATH_TOGGLE_DASHBOARD: &str = "/actions/default/in/ToggleDashboard";
const INPUT_ANY: InputValueHandle = InputValueHandle(ovr_overlay::sys::k_ulInvalidInputValueHandle);
@@ -49,9 +52,12 @@ pub(super) struct OpenVrInputSource {
scroll_hnd: ActionHandle,
alt_click_hnd: ActionHandle,
show_hide_hnd: ActionHandle,
toggle_dashboard_hnd: ActionHandle,
space_drag_hnd: ActionHandle,
space_rotate_hnd: ActionHandle,
click_modifier_right_hnd: ActionHandle,
click_modifier_middle_hnd: ActionHandle,
move_mouse_hnd: ActionHandle,
}
pub(super) struct OpenVrHandSource {
@@ -71,9 +77,12 @@ impl OpenVrInputSource {
let scroll_hnd = input.get_action_handle(PATH_SCROLL)?;
let alt_click_hnd = input.get_action_handle(PATH_ALT_CLICK)?;
let show_hide_hnd = input.get_action_handle(PATH_SHOW_HIDE)?;
let toggle_dashboard_hnd = input.get_action_handle(PATH_TOGGLE_DASHBOARD)?;
let space_drag_hnd = input.get_action_handle(PATH_SPACE_DRAG)?;
let space_rotate_hnd = input.get_action_handle(PATH_SPACE_ROTATE)?;
let click_modifier_right_hnd = input.get_action_handle(PATH_CLICK_MODIFIER_RIGHT)?;
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()
@@ -105,9 +114,12 @@ impl OpenVrInputSource {
scroll_hnd,
alt_click_hnd,
show_hide_hnd,
toggle_dashboard_hnd,
space_drag_hnd,
space_rotate_hnd,
click_modifier_right_hnd,
click_modifier_middle_hnd,
move_mouse_hnd,
hands,
})
}
@@ -188,11 +200,21 @@ impl OpenVrInputSource {
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.toggle_dashboard = input
.get_digital_action_data(self.toggle_dashboard_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.space_drag = input
.get_digital_action_data(self.space_drag_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.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)
@@ -203,6 +225,11 @@ impl OpenVrInputSource {
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.move_mouse = input
.get_digital_action_data(self.move_mouse_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.scroll = input
.get_analog_action_data(self.scroll_hnd, hand.input_hnd)
.map(|x| x.0.y)
@@ -291,8 +318,6 @@ fn get_tracked_device(
}
Some(TrackedDevice {
valid: true,
index,
soc,
charging,
role,
@@ -300,25 +325,27 @@ fn get_tracked_device(
}
pub fn set_action_manifest(input: &mut InputManager) -> anyhow::Result<()> {
let action_path = CONFIG_ROOT_PATH.join("actions.json");
let action_path = config_io::get_config_root().join("actions.json");
if !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");
let binding_path = config_io::get_config_root().join("actions_binding_knuckles.json");
if !binding_path.is_file() {
File::create(&binding_path)?
.write_all(include_bytes!("../../res/actions_binding_knuckles.json"))?;
}
let binding_path = CONFIG_ROOT_PATH.join("actions_binding_vive.json");
let binding_path = config_io::get_config_root().join("actions_binding_vive.json");
if !binding_path.is_file() {
File::create(&binding_path)?
.write_all(include_bytes!("../../res/actions_binding_vive.json"))?;
}
let binding_path = CONFIG_ROOT_PATH.join("actions_binding_oculus.json");
let binding_path = config_io::get_config_root().join("actions_binding_oculus.json");
if !binding_path.is_file() {
File::create(&binding_path)?
.write_all(include_bytes!("../../res/actions_binding_oculus.json"))?;
+20 -5
View File
@@ -11,13 +11,15 @@ use vulkano::format::Format;
use vulkano::image::view::ImageView;
use vulkano::image::ImageLayout;
use crate::backend::overlay::{OverlayData, OverlayRenderer, OverlayState, SplitOverlayBackend};
use crate::backend::overlay::{
FrameTransform, OverlayData, OverlayRenderer, OverlayState, SplitOverlayBackend, Z_ORDER_LINES,
};
use crate::graphics::WlxGraphics;
use crate::state::AppState;
use super::overlay::OpenVrOverlayData;
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
static LINE_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
pub(super) struct LinePool {
lines: IdMap<usize, OverlayData<OpenVrOverlayData>>,
@@ -32,7 +34,7 @@ impl LinePool {
let buf = vec![255; 16];
let texture = command_buffer.texture2d(2, 2, Format::R8G8B8A8_UNORM, &buf)?;
let texture = command_buffer.texture2d_raw(2, 2, Format::R8G8B8A8_UNORM, &buf)?;
command_buffer.build_and_execute_now()?;
graphics
@@ -59,7 +61,7 @@ impl LinePool {
}
pub fn allocate(&mut self) -> usize {
let id = AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
let id = LINE_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
let mut data = OverlayData::<OpenVrOverlayData> {
state: OverlayState {
@@ -75,11 +77,12 @@ impl LinePool {
}),
data: OpenVrOverlayData {
width: 0.002,
override_width: true,
..Default::default()
},
..Default::default()
};
data.data.sort_order = 69;
data.state.z_order = Z_ORDER_LINES;
data.state.dirty = true;
self.lines.insert(id, data);
@@ -160,6 +163,12 @@ impl LinePool {
}
Ok(())
}
pub fn mark_dirty(&mut self) {
for data in self.lines.values_mut() {
data.state.dirty = true;
}
}
}
struct StaticRenderer {
@@ -182,4 +191,10 @@ impl OverlayRenderer for StaticRenderer {
fn view(&mut self) -> Option<Arc<ImageView>> {
Some(self.view.clone())
}
fn frame_transform(&mut self) -> Option<FrameTransform> {
Some(FrameTransform {
extent: self.view.image().extent(),
..Default::default()
})
}
}
+3 -3
View File
@@ -4,12 +4,12 @@ use anyhow::bail;
use json::{array, object};
use ovr_overlay::applications::ApplicationsManager;
use crate::config_io::CONFIG_ROOT_PATH;
use crate::config_io;
const APP_KEY: &str = "galister.wlxoverlay-s";
pub(super) fn install_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Result<()> {
let manifest_path = CONFIG_ROOT_PATH.join("wlx-overlay-s.vrmanifest");
let manifest_path = config_io::get_config_root().join("wlx-overlay-s.vrmanifest");
let appimage_path = std::env::var("APPIMAGE");
let executable_pathbuf = std::env::current_exe()?;
@@ -76,7 +76,7 @@ pub(super) fn install_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Res
}
pub(super) fn uninstall_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Result<()> {
let manifest_path = CONFIG_ROOT_PATH.join("wlx-overlay-s.vrmanifest");
let manifest_path = config_io::get_config_root().join("wlx-overlay-s.vrmanifest");
if let Ok(true) = app_mgr.is_application_installed(APP_KEY) {
if let Err(e) = app_mgr.remove_application_manifest(&manifest_path) {
+89 -17
View File
@@ -1,5 +1,6 @@
use std::{
collections::VecDeque,
ops::Add,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
@@ -20,7 +21,7 @@ use vulkano::{
use crate::{
backend::{
common::SystemTask,
common::{BackendError, OverlayContainer},
input::interact,
notifications::NotificationManager,
openvr::{
@@ -31,13 +32,18 @@ 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};
#[cfg(feature = "wayvr")]
use crate::{gui::modular::button::WayVRAction, overlays::wayvr::wayvr_action};
pub mod helpers;
pub mod input;
@@ -59,7 +65,7 @@ pub fn openvr_uninstall() {
let _ = uninstall_manifest(&mut app_mgr);
}
pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
pub fn openvr_run(running: Arc<AtomicBool>, show_by_default: bool) -> Result<(), BackendError> {
let app_type = EVRApplicationType::VRApplication_Overlay;
let Ok(context) = ovr_overlay::Context::init(app_type) else {
log::warn!("Will not use OpenVR: Context init failed");
@@ -93,6 +99,21 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
AppState::from_graphics(graphics)?
};
if show_by_default {
state.tasks.enqueue_at(
TaskType::System(SystemTask::ShowHide),
Instant::now().add(Duration::from_secs(1)),
)
}
if let Ok(ipd) = system_mgr.get_tracked_device_property::<f32>(
TrackedDeviceIndex::HMD,
ETrackedDeviceProperty::Prop_UserIpdMeters_Float,
) {
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)?;
@@ -103,12 +124,6 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let mut playspace = playspace::PlayspaceMover::new();
playspace.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
#[cfg(feature = "osc")]
let mut osc_sender =
crate::backend::osc::OscSender::new(state.session.config.osc_out_port).ok();
state.hid_provider.set_desktop_extent(overlays.extent);
set_action_manifest(&mut input_mgr)?;
let mut input_source = OpenVrInputSource::new(&mut input_mgr)?;
@@ -158,9 +173,28 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
}
EVREventType::VREvent_SeatedZeroPoseReset
| EVREventType::VREvent_StandingZeroPoseReset
| EVREventType::VREvent_ChaperoneUniverseHasChanged => {
| 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;
}
}
_ => {}
}
}
@@ -173,12 +207,20 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
notifications.submit_pending(&mut state);
state.tasks.retrieve_due(&mut due_tasks);
let mut removed_overlays = overlays.update(&mut state)?;
for o in removed_overlays.iter_mut() {
o.destroy(&mut overlay_mgr);
}
while let Some(task) = due_tasks.pop_front() {
match task {
TaskType::Global(f) => f(&mut state),
TaskType::Overlay(sel, f) => {
if let Some(o) = overlays.mut_by_selector(&sel) {
f(&mut state, &mut o.state);
} else {
log::warn!("Overlay not found for task: {:?}", sel);
}
}
TaskType::CreateOverlay(sel, f) => {
@@ -213,9 +255,16 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
playspace.fix_floor(&mut chaperone_mgr, &state.input_state);
}
SystemTask::ResetPlayspace => {
playspace.reset_offset(&mut chaperone_mgr);
playspace.reset_offset(&mut chaperone_mgr, &state.input_state);
}
SystemTask::ShowHide => {
overlays.show_hide(&mut state);
}
},
#[cfg(feature = "wayvr")]
TaskType::WayVR(action) => {
wayvr_action(&mut state, &mut overlays, &action);
}
}
}
@@ -228,7 +277,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
&mut system_mgr,
&mut state,
);
state.input_state.post_update();
state.input_state.post_update(&state.session);
if state
.input_state
@@ -236,9 +285,20 @@ 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);
}
#[cfg(feature = "wayvr")]
if state
.input_state
.pointers
.iter()
.any(|p| p.now.toggle_dashboard && !p.before.toggle_dashboard)
{
wayvr_action(&mut state, &mut overlays, &WayVRAction::ToggleDashboard);
}
overlays
.iter_mut()
.for_each(|o| o.state.auto_movement(&mut state));
@@ -260,6 +320,8 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
}
}
state.hid_provider.commit();
lines.update(universe.clone(), &mut overlay_mgr, &mut state)?;
for o in overlays.iter_mut() {
@@ -267,10 +329,17 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
}
#[cfg(feature = "osc")]
if let Some(ref mut sender) = osc_sender {
let _ = sender.send_params(&overlays);
if let Some(ref mut sender) = state.osc_sender {
let _ = sender.send_params(&overlays, &state.input_state.devices);
};
#[cfg(feature = "wayvr")]
if let Err(e) =
crate::overlays::wayvr::tick_events::<OpenVrOverlayData>(&mut state, &mut overlays)
{
log::error!("WayVR tick_events failed: {:?}", e);
}
log::trace!("Rendering frame");
for o in overlays.iter_mut() {
@@ -285,11 +354,14 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
.iter_mut()
.for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &state.graphics));
#[cfg(feature = "wayvr")]
if let Some(wayvr) = &state.wayvr {
wayvr.borrow_mut().data.tick_finish()?;
}
// chaperone
// close font handles?
state.hid_provider.on_new_frame();
}
log::warn!("OpenVR shutdown");
+26 -13
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;
@@ -87,6 +82,8 @@ impl OverlayData<OpenVrOverlayData> {
) {
if self.data.visible {
if self.state.dirty {
self.upload_curvature(overlay);
self.upload_transform(universe, overlay);
self.upload_alpha(overlay);
self.state.dirty = false;
@@ -171,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,
@@ -185,13 +182,13 @@ 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,
&mut self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
) {
@@ -200,7 +197,14 @@ impl OverlayData<OpenVrOverlayData> {
return;
};
let transform = Matrix3x4::from_affine(&self.state.transform);
let effective = self.state.transform
* self
.backend
.frame_transform()
.map(|f| f.transform)
.unwrap_or_default();
let transform = Matrix3x4::from_affine(&effective);
if let Err(e) = overlay.set_transform_absolute(handle, universe, &transform) {
log::error!(
@@ -233,6 +237,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 {
@@ -247,7 +260,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,
+138 -28
View File
@@ -1,4 +1,4 @@
use glam::{Affine3A, Vec3A};
use glam::{Affine3A, Quat, Vec3, Vec3A};
use ovr_overlay::{
chaperone_setup::ChaperoneSetupManager,
compositor::CompositorManager,
@@ -12,22 +12,24 @@ use crate::{
use super::{helpers::Affine3AConvert, overlay::OpenVrOverlayData};
struct DragData {
struct MoverData<T> {
pose: Affine3A,
hand: usize,
hand_pos: Vec3A,
hand_pose: T,
}
pub(super) struct PlayspaceMover {
universe: ETrackingUniverseOrigin,
last: Option<DragData>,
drag: Option<MoverData<Vec3A>>,
rotate: Option<MoverData<Quat>>,
}
impl PlayspaceMover {
pub fn new() -> Self {
Self {
universe: ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated,
last: None,
drag: None,
rotate: None,
}
}
@@ -38,10 +40,73 @@ impl PlayspaceMover {
state: &AppState,
) {
let universe = self.universe.clone();
if let Some(data) = self.last.as_mut() {
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 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;
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.last = None;
self.drag = None;
log::info!("End space drag");
return;
}
@@ -49,27 +114,29 @@ impl PlayspaceMover {
let new_hand = data
.pose
.transform_point3a(state.input_state.pointers[data.hand].raw_pose.translation);
let relative_pos = new_hand - data.hand_pos;
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);
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;
overlay.state.transform.translation += overlay_offset;
}
});
chaperone_mgr.revert_working_copy();
apply_chaperone_offset(overlay_offset, chaperone_mgr);
data.pose.translation += relative_pos;
data.hand_pos = new_hand;
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 {
@@ -80,25 +147,44 @@ impl PlayspaceMover {
return;
};
let hand_pos = mat.transform_point3a(pointer.raw_pose.translation);
self.last = Some(DragData {
self.drag = Some(MoverData {
pose: mat,
hand: i,
hand_pos,
hand_pose: hand_pos,
});
self.rotate = None;
log::info!("Start space drag");
break;
return;
}
}
}
}
pub fn reset_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager) {
set_working_copy(&self.universe, chaperone_mgr, &Affine3A::IDENTITY);
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.last.is_some() {
if self.drag.is_some() {
log::info!("Space drag interrupted by manual reset");
self.last = None;
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by manual reset");
self.rotate = None;
}
}
@@ -109,9 +195,20 @@ impl PlayspaceMover {
log::warn!("Can't fix floor - failed to get zero pose");
return;
};
mat.translation.y += y1.min(y2) - 0.03;
let offset = y1.min(y2) - 0.03;
mat.translation.y += offset;
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(
@@ -129,9 +226,13 @@ impl PlayspaceMover {
self.universe = new_universe;
}
if self.last.is_some() {
if self.drag.is_some() {
log::info!("Space drag interrupted by external change");
self.last = None;
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by external change");
self.rotate = None;
}
}
@@ -177,15 +278,24 @@ fn set_working_copy(
}
fn apply_chaperone_offset(offset: Vec3A, chaperone_mgr: &mut ChaperoneSetupManager) {
let mut quads = chaperone_mgr.get_working_collision_bounds_info();
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;
if corner.v[1] != 0.0 {
corner.v[1] += offset.y;
}
corner.v[2] += offset.z;
});
});
chaperone_mgr.set_working_collision_bounds_info(&mut quads);
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());
}
+48 -31
View File
@@ -1,5 +1,5 @@
use anyhow::{bail, ensure};
use glam::{Affine3A, Quat, Vec3};
use glam::{Affine3A, Quat, Vec3, Vec3A};
use openxr as xr;
use xr::OverlaySessionCreateFlagsEXTX;
@@ -21,6 +21,28 @@ 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.");
}
if available_extensions.khr_composition_layer_cylinder {
enabled_extensions.khr_composition_layer_cylinder = true;
} else {
log::warn!("Missing EXT_composition_layer_cylinder extension.");
}
if available_extensions.khr_composition_layer_equirect2 {
enabled_extensions.khr_composition_layer_equirect2 = true;
} else {
log::warn!("Missing EXT_composition_layer_equirect2 extension.");
}
//#[cfg(not(debug_assertions))]
let layers = [];
@@ -74,6 +96,7 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
Ok((xr_instance, system))
}
pub(super) unsafe fn create_overlay_session(
instance: &xr::Instance,
system: xr::SystemId,
@@ -109,42 +132,24 @@ pub(super) unsafe fn create_overlay_session(
}
}
pub(super) fn hmd_pose_from_views(views: &[xr::View]) -> Affine3A {
let pos = {
let pos0: Vec3 = unsafe { std::mem::transmute(views[0].pose.position) };
let pos1: Vec3 = unsafe { std::mem::transmute(views[1].pose.position) };
(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)
};
type Vec3M = mint::Vector3<f32>;
type QuatM = mint::Quaternion<f32>;
Affine3A::from_rotation_translation(rot, pos)
pub(super) fn ipd_from_views(views: &[xr::View]) -> f32 {
let p0: Vec3 = Vec3M::from(views[0].pose.position).into();
let p1: Vec3 = Vec3M::from(views[1].pose.position).into();
(p0.distance(p1) * 1000.0).round() * 0.1
}
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;
}
@@ -163,3 +168,15 @@ 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 posef_to_transform(pose: &xr::Posef) -> Affine3A {
let rotation = QuatM::from(pose.orientation).into();
let translation = Vec3M::from(pose.position).into();
Affine3A::from_rotation_translation(rotation, translation)
}
File diff suppressed because it is too large. Load diff
+9 -10
View File
@@ -7,7 +7,6 @@ use std::{
atomic::{AtomicUsize, Ordering},
Arc,
},
usize,
};
use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::view::ImageView};
@@ -18,11 +17,11 @@ use crate::{
};
use super::{
swapchain::{create_swapchain_render_data, SwapchainRenderData},
XrState,
swapchain::{create_swapchain_render_data, SwapchainOpts, SwapchainRenderData},
CompositionLayer, XrState,
};
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
static LINE_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
pub(super) const LINE_WIDTH: f32 = 0.002;
pub(super) struct LinePool {
@@ -47,7 +46,7 @@ impl LinePool {
let views: anyhow::Result<Vec<Arc<ImageView>>> = colors
.into_iter()
.map(
|color| match command_buffer.texture2d(1, 1, Format::R8G8B8A8_UNORM, &color) {
|color| match command_buffer.texture2d_raw(1, 1, Format::R8G8B8A8_UNORM, &color) {
Ok(tex) => ImageView::new_default(tex).map_err(|e| anyhow::anyhow!(e)),
Err(e) => Err(e),
},
@@ -67,9 +66,10 @@ impl LinePool {
xr: &XrState,
graphics: Arc<WlxGraphics>,
) -> anyhow::Result<usize> {
let id = AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
let id = LINE_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
let srd = create_swapchain_render_data(xr, graphics, [1, 1, 1])?;
let srd =
create_swapchain_render_data(xr, graphics, [1, 1, 1], SwapchainOpts::new().srgb())?;
self.lines.insert(
id,
LineContainer {
@@ -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<'a>>> {
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));
}
}
+217 -35
View File
@@ -1,35 +1,45 @@
use std::{
collections::VecDeque,
ops::Add,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
time::Duration,
time::{Duration, Instant},
};
use glam::{Affine3A, Vec3};
use libmonado::Monado;
use openxr as xr;
use skybox::create_skybox;
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;
#[cfg(feature = "wayvr")]
use crate::{gui::modular::button::WayVRAction, overlays::wayvr::wayvr_action};
mod helpers;
mod input;
mod lines;
mod overlay;
mod playspace;
mod skybox;
mod swapchain;
const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO;
@@ -41,10 +51,13 @@ struct XrState {
system: xr::SystemId,
session: xr::Session<xr::Vulkan>,
predicted_display_time: xr::Time,
fps: f32,
stage: Arc<xr::Space>,
view: Arc<xr::Space>,
stage_offset: Affine3A,
}
pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
pub fn openxr_run(running: Arc<AtomicBool>, show_by_default: bool) -> Result<(), BackendError> {
let (xr_instance, system) = match helpers::init_xr() {
Ok((xr_instance, system)) => (xr_instance, system),
Err(e) => {
@@ -53,15 +66,30 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
}
};
let environment_blend_mode =
xr_instance.enumerate_environment_blend_modes(system, VIEW_TYPE)?[0];
log::info!("Using environment blend mode: {:?}", environment_blend_mode);
let mut app_state = {
let graphics = WlxGraphics::new_openxr(xr_instance.clone(), system)?;
AppState::from_graphics(graphics)?
};
let environment_blend_mode = {
let modes = xr_instance.enumerate_environment_blend_modes(system, VIEW_TYPE)?;
if modes.contains(&xr::EnvironmentBlendMode::ALPHA_BLEND)
&& app_state.session.config.use_passthrough
{
xr::EnvironmentBlendMode::ALPHA_BLEND
} else {
modes[0]
}
};
log::info!("Using environment blend mode: {:?}", environment_blend_mode);
if show_by_default {
app_state.tasks.enqueue_at(
TaskType::System(SystemTask::ShowHide),
Instant::now().add(Duration::from_secs(1)),
)
}
let mut overlays = OverlayContainer::<OpenXrOverlayData>::new(&mut app_state)?;
let mut lines = LinePool::new(app_state.graphics.clone())?;
@@ -71,11 +99,15 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let mut delete_queue = vec![];
#[cfg(feature = "osc")]
let mut osc_sender =
crate::backend::osc::OscSender::new(app_state.session.config.osc_out_port).ok();
let mut monado = Monado::auto_connect()
.map_err(|e| log::warn!("Will not use libmonado: {}", e))
.ok();
app_state.hid_provider.set_desktop_extent(overlays.extent);
let mut playspace = monado.as_mut().and_then(|m| {
playspace::PlayspaceMover::new(m)
.map_err(|e| log::warn!("Will not use Monado playspace mover: {}", e))
.ok()
});
let (session, mut frame_wait, mut frame_stream) = unsafe {
let raw_session = helpers::create_overlay_session(
@@ -100,12 +132,23 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let stage =
session.create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)?;
let view = session.create_reference_space(xr::ReferenceSpaceType::VIEW, xr::Posef::IDENTITY)?;
let mut xr_state = XrState {
instance: xr_instance,
system,
session,
predicted_display_time: xr::Time::from_nanos(0),
fps: 30.0,
stage: Arc::new(stage),
view: Arc::new(view),
stage_offset: Affine3A::IDENTITY,
};
let mut skybox = if environment_blend_mode == xr::EnvironmentBlendMode::OPAQUE {
create_skybox(&xr_state, &app_state)
} else {
None
};
let pointer_lines = [
@@ -115,14 +158,18 @@ 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 next_device_update = Instant::now();
let mut due_tasks = VecDeque::with_capacity(4);
let mut fps_counter: VecDeque<Instant> = VecDeque::new();
let mut main_session_visible = false;
'main_loop: loop {
let cur_frame = FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
@@ -166,10 +213,30 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
EventsLost(e) => {
log::warn!("lost {} events", e.lost_event_count());
}
MainSessionVisibilityChangedEXTX(e) => {
if main_session_visible != e.visible() {
main_session_visible = e.visible();
log::info!("Main session visible: {}", main_session_visible);
if main_session_visible {
log::debug!("Destroying skybox.");
skybox = None;
} else if environment_blend_mode == xr::EnvironmentBlendMode::OPAQUE {
log::debug!("Allocating skybox.");
skybox = create_skybox(&xr_state, &app_state);
}
}
}
_ => {}
}
}
if next_device_update <= Instant::now() {
if let Some(monado) = &mut monado {
input_source.update_devices(&mut app_state, monado);
next_device_update = Instant::now() + Duration::from_secs(30);
}
}
if !session_running {
std::thread::sleep(Duration::from_millis(100));
continue 'main_loop;
@@ -179,6 +246,24 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
frame_stream.begin()?;
xr_state.predicted_display_time = xr_frame_state.predicted_display_time;
xr_state.fps = {
fps_counter.push_back(Instant::now());
while let Some(time) = fps_counter.front() {
if time.elapsed().as_secs_f32() > 1. {
fps_counter.pop_front();
} else {
break;
}
}
let total_elapsed = fps_counter
.front()
.map(|time| time.elapsed().as_secs_f32())
.unwrap_or(0f32);
fps_counter.len() as f32 / total_elapsed
};
if !xr_frame_state.should_render {
frame_stream.end(
@@ -191,27 +276,43 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
app_state.input_state.pre_update();
input_source.update(&xr_state, &mut app_state)?;
app_state.input_state.post_update();
app_state.input_state.post_update(&app_state.session);
if app_state
.input_state
.pointers
.iter()
.any(|p| p.now.show_hide && !p.before.show_hide)
&& show_hide_counter.click()
{
overlays.show_hide(&mut app_state);
}
#[cfg(feature = "wayvr")]
if app_state
.input_state
.pointers
.iter()
.any(|p| p.now.toggle_dashboard && !p.before.toggle_dashboard)
{
wayvr_action(&mut app_state, &mut overlays, &WayVRAction::ToggleDashboard);
}
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,
monado.as_mut().unwrap(), // safe
);
}
for o in overlays.iter_mut() {
o.after_input(&mut app_state)?;
}
#[cfg(feature = "osc")]
if let Some(ref mut sender) = osc_sender {
let _ = sender.send_params(&overlays);
if let Some(ref mut sender) = app_state.osc_sender {
let _ = sender.send_params(&overlays, &app_state.input_state.devices);
};
let (_, views) = xr_state.session.locate_views(
@@ -220,7 +321,17 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
&xr_state.stage,
)?;
app_state.input_state.hmd = helpers::hmd_pose_from_views(&views);
let ipd = helpers::ipd_from_views(&views);
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 +351,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 {
@@ -256,6 +369,25 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
if !main_session_visible {
if let Some(skybox) = skybox.as_mut() {
for (idx, layer) in skybox
.present_xr(&xr_state, app_state.input_state.hmd, &mut command_buffer)?
.into_iter()
.enumerate()
{
layers.push((200.0 - 50.0 * (idx as f32), layer));
}
}
}
#[cfg(feature = "wayvr")]
if let Err(e) =
crate::overlays::wayvr::tick_events::<OpenXrOverlayData>(&mut app_state, &mut overlays)
{
log::error!("WayVR tick_events failed: {:?}", e);
}
for o in overlays.iter_mut() {
if !o.state.want_visible {
continue;
@@ -269,19 +401,30 @@ 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));
}
#[cfg(feature = "wayvr")]
if let Some(wayvr) = &app_state.wayvr {
wayvr.borrow_mut().data.tick_finish()?;
}
command_buffer.build_and_execute_now()?;
@@ -290,7 +433,12 @@ 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::Equirect2(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
CompositionLayer::None => unreachable!(),
})
.collect::<Vec<_>>();
frame_stream.end(
@@ -299,6 +447,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 +461,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) => {
@@ -329,25 +484,52 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
TaskType::DropOverlay(sel) => {
if let Some(o) = overlays.mut_by_selector(&sel) {
if o.state.birthframe < cur_frame {
let o = overlays.remove_by_selector(&sel);
// set for deletion after all images are done showing
delete_queue.push((o, cur_frame + 5));
log::debug!("{}: destroy", o.state.name);
if let Some(o) = overlays.remove_by_selector(&sel) {
// set for deletion after all images are done showing
delete_queue.push((o, cur_frame + 5));
}
}
}
}
TaskType::System(_task) => {
// Not implemented
TaskType::System(task) => match task {
SystemTask::FixFloor => {
if let Some(ref mut playspace) = playspace {
playspace.fix_floor(
&app_state.input_state,
monado.as_mut().unwrap(), // safe
);
}
}
SystemTask::ResetPlayspace => {
if let Some(ref mut playspace) = playspace {
playspace.reset_offset(monado.as_mut().unwrap()); // safe
}
}
SystemTask::ShowHide => {
overlays.show_hide(&mut app_state);
}
_ => {}
},
#[cfg(feature = "wayvr")]
TaskType::WayVR(action) => {
wayvr_action(&mut app_state, &mut overlays, &action);
}
}
}
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>),
Equirect2(xr::CompositionLayerEquirect2KHR<'a, xr::Vulkan>),
}
+257
View File
@@ -0,0 +1,257 @@
// 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)
//
// -- toggle_dashboard --
// run or toggle visibility of a previously configured WayVR-compatible dashboard
//
// -- space_rotate --
// rotate your stage (playspace rotate, WIP)
//
// -- space_reset --
// reset your stage (reset the offset from playspace drag)
//
// -- click_modifier_right --
// while this is held, your pointer will turn ORANGE and your mouse clicks will be RIGHT clicks
//
// -- 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"
},
toggle_dashboard: {
double_click: false,
right: "/user/hand/right/input/system/click",
},
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
},
space_reset: {
double_click: true,
left: "/user/hand/left/input/trackpad/force",
},
click_modifier_right: {
left: "/user/hand/left/input/b/touch",
right: "/user/hand/right/input/b/touch"
},
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",
},
},
]
+56 -25
View File
@@ -1,10 +1,14 @@
use openxr as xr;
use std::sync::Arc;
use xr::{CompositionLayerFlags, EyeVisibility};
use glam::Vec3A;
use openxr::{self as xr, CompositionLayerFlags};
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},
backend::{
openxr::swapchain::{create_swapchain_render_data, SwapchainOpts},
overlay::OverlayData,
},
graphics::WlxCommandBuffer,
state::AppState,
};
@@ -23,7 +27,7 @@ impl OverlayData<OpenXrOverlayData> {
&'a mut self,
xr: &'a XrState,
command_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<Option<xr::CompositionLayerQuad<xr::Vulkan>>> {
) -> anyhow::Result<CompositionLayer<'a>> {
if let Some(new_view) = self.view() {
self.data.last_view = Some(new_view);
}
@@ -32,15 +36,21 @@ 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();
let frame_transform = self.frame_transform().unwrap(); // want panic
let extent = frame_transform.extent;
let data = match self.data.swapchain {
Some(ref mut data) => data,
None => {
let srd =
create_swapchain_render_data(xr, command_buffer.graphics.clone(), extent)?;
let srd = create_swapchain_render_data(
xr,
command_buffer.graphics.clone(),
extent,
SwapchainOpts::new(),
)?;
log::debug!(
"{}: Created swapchain {}x{}, {} images, {} MB",
self.state.name,
@@ -55,29 +65,50 @@ 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 transform = self.state.transform * frame_transform.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();
let major = transform.matrix3.col(0).length();
(major, major * aspect_ratio)
} else {
let major = self.state.transform.matrix3.col(1).length();
let major = transform.matrix3.col(1).length();
(major / aspect_ratio, major)
};
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(&transform);
let center_point = transform.translation + quat.mul_vec3a(Vec3A::Z * radius);
let posef = helpers::translation_rotation_to_posef(center_point, quat);
let angle = 2.0 * (scale_x / (2.0 * radius));
let cylinder = xr::CompositionLayerCylinderKHR::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
.space(&xr.stage)
.radius(radius)
.central_angle(angle)
.aspect_ratio(aspect_ratio);
Ok(CompositionLayer::Cylinder(cylinder))
} else {
let posef = helpers::transform_to_posef(&transform);
let quad = xr::CompositionLayerQuad::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
.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<()> {
+206
View File
@@ -0,0 +1,206 @@
use glam::{Affine3A, Quat, Vec3A};
use libmonado::{Monado, Pose, ReferenceSpaceType};
use crate::{
backend::{common::OverlayContainer, input::InputState},
state::AppState,
};
use super::overlay::OpenXrOverlayData;
#[repr(C)]
#[derive(Default, Debug)]
struct XrtPose {
orientation: [f32; 4],
position: [f32; 3],
}
struct MoverData<T> {
pose: Affine3A,
hand: usize,
hand_pose: T,
}
pub(super) struct PlayspaceMover {
last_transform: Affine3A,
drag: Option<MoverData<Vec3A>>,
rotate: Option<MoverData<Quat>>,
}
impl PlayspaceMover {
pub fn new(monado: &mut Monado) -> anyhow::Result<Self> {
log::info!("Monado: using space offset API");
let Ok(stage) = monado.get_reference_space_offset(ReferenceSpaceType::Stage) else {
anyhow::bail!("Space offsets not supported.");
};
log::debug!("STAGE is at {:?}, {:?}", stage.position, stage.orientation);
// initial offset
let last_transform =
Affine3A::from_rotation_translation(stage.orientation.into(), stage.position.into());
Ok(Self {
last_transform,
drag: None,
rotate: None,
})
}
pub fn update(
&mut self,
overlays: &mut OverlayContainer<OpenXrOverlayData>,
state: &AppState,
monado: &mut Monado,
) {
for pointer in state.input_state.pointers.iter() {
if pointer.now.space_reset {
if !pointer.before.space_reset {
log::info!("Space reset");
self.reset_offset(monado);
}
return;
}
}
if let Some(mut data) = self.rotate.take() {
let pointer = &state.input_state.pointers[data.hand];
if !pointer.now.space_rotate {
self.last_transform = data.pose;
log::info!("End space rotate");
return;
}
let new_hand =
Quat::from_affine3(&(data.pose * state.input_state.pointers[data.hand].raw_pose));
let dq = new_hand * data.hand_pose.conjugate();
let mut space_transform = if state.session.config.space_rotate_unlocked {
Affine3A::from_quat(dq)
} else {
let rel_y = f32::atan2(
2.0 * (dq.y * dq.w + dq.x * dq.z),
(2.0 * (dq.w * dq.w + dq.x * dq.x)) - 1.0,
);
Affine3A::from_rotation_y(rel_y)
};
let offset = (space_transform.transform_vector3a(state.input_state.hmd.translation)
- state.input_state.hmd.translation)
* -1.0;
space_transform.translation = offset;
data.pose *= space_transform;
data.hand_pose = new_hand;
self.apply_offset(data.pose, monado);
self.rotate = Some(data);
} else {
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
if pointer.now.space_rotate {
let hand_pose = Quat::from_affine3(&(self.last_transform * pointer.raw_pose));
self.rotate = Some(MoverData {
pose: self.last_transform,
hand: i,
hand_pose,
});
self.drag = None;
log::info!("Start space rotate");
return;
}
}
}
if let Some(mut data) = self.drag.take() {
let pointer = &state.input_state.pointers[data.hand];
if !pointer.now.space_drag {
self.last_transform = data.pose;
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;
self.apply_offset(data.pose, monado);
self.drag = Some(data);
} else {
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
if pointer.now.space_drag {
let hand_pos = self
.last_transform
.transform_point3a(pointer.raw_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, monado: &mut Monado) {
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, monado);
}
pub fn fix_floor(&mut self, input: &InputState, monado: &mut Monado) {
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].raw_pose.translation.y;
let y2 = input.pointers[1].raw_pose.translation.y;
let delta = y1.min(y2) - 0.03;
self.last_transform.translation.y += delta;
self.apply_offset(self.last_transform, monado);
}
fn apply_offset(&self, transform: Affine3A, monado: &mut Monado) {
let pose = Pose {
position: transform.translation.into(),
orientation: Quat::from_affine3(&transform).into(),
};
let _ = monado.set_reference_space_offset(ReferenceSpaceType::Stage, pose);
}
}
+168
View File
@@ -0,0 +1,168 @@
use std::{f32::consts::PI, fs::File, sync::Arc};
use glam::{Affine3A, Quat, Vec3A};
use once_cell::sync::Lazy;
use openxr::{self as xr, CompositionLayerFlags};
use vulkano::{command_buffer::CommandBufferUsage, image::view::ImageView};
use crate::{
backend::openxr::{helpers::translation_rotation_to_posef, swapchain::SwapchainOpts},
config_io,
graphics::{dds::WlxCommandBufferDds, format_is_srgb, WlxCommandBuffer},
state::AppState,
};
use super::{
swapchain::{create_swapchain_render_data, SwapchainRenderData},
CompositionLayer, XrState,
};
pub(super) struct Skybox {
view: Arc<ImageView>,
srd: Option<(SwapchainRenderData, SwapchainRenderData)>,
}
impl Skybox {
pub fn new(app: &AppState) -> anyhow::Result<Self> {
let mut command_buffer = app
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
let mut maybe_image = None;
'custom_tex: {
if app.session.config.skybox_texture.is_empty() {
break 'custom_tex;
}
let real_path = config_io::get_config_root().join(&*app.session.config.skybox_texture);
let Ok(f) = File::open(real_path) else {
log::warn!(
"Could not open custom skybox texture at: {}",
app.session.config.skybox_texture
);
break 'custom_tex;
};
match command_buffer.texture2d_dds(f) {
Ok(image) => {
maybe_image = Some(image);
}
Err(e) => {
log::warn!(
"Could not use custom skybox texture at: {}",
app.session.config.skybox_texture
);
log::warn!("{:?}", e);
}
}
}
if maybe_image.is_none() {
let p = include_bytes!("../../res/table_mountain_2.dds");
maybe_image = Some(command_buffer.texture2d_dds(p.as_slice())?);
}
command_buffer.build_and_execute_now()?;
let view = ImageView::new_default(maybe_image.unwrap())?; // safe unwrap
Ok(Self { view, srd: None })
}
pub(super) fn present_xr<'a>(
&'a mut self,
xr: &'a XrState,
hmd: Affine3A,
command_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<Vec<CompositionLayer<'a>>> {
let (sky_image, grid_image) = if let Some((ref mut srd_sky, ref mut srd_grid)) = self.srd {
(srd_sky.present_last()?, srd_grid.present_last()?)
} else {
log::debug!("Render skybox.");
let mut opts = SwapchainOpts::new().immutable();
opts.srgb = format_is_srgb(self.view.image().format());
let srd_sky = create_swapchain_render_data(
xr,
command_buffer.graphics.clone(),
self.view.image().extent(),
opts,
)?;
let srd_grid = create_swapchain_render_data(
xr,
command_buffer.graphics.clone(),
[1024, 1024, 1],
SwapchainOpts::new().immutable().grid(),
)?;
self.srd = Some((srd_sky, srd_grid));
let (srd_sky, srd_grid) = self.srd.as_mut().unwrap(); // safe unwrap
(
srd_sky.acquire_present_release(command_buffer, self.view.clone(), 1.0)?,
srd_grid.acquire_compute_release(command_buffer)?,
)
};
let pose = xr::Posef {
orientation: xr::Quaternionf::IDENTITY,
position: xr::Vector3f {
x: hmd.translation.x,
y: hmd.translation.y,
z: hmd.translation.z,
},
};
// cover the entire sphere
const HORIZ_ANGLE: f32 = 2.0 * PI;
const HI_VERT_ANGLE: f32 = 0.5 * PI;
const LO_VERT_ANGLE: f32 = -0.5 * PI;
let mut layers = vec![];
let sky = xr::CompositionLayerEquirect2KHR::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(pose)
.radius(10.0)
.sub_image(sky_image)
.eye_visibility(xr::EyeVisibility::BOTH)
.space(&xr.stage)
.central_horizontal_angle(HORIZ_ANGLE)
.upper_vertical_angle(HI_VERT_ANGLE)
.lower_vertical_angle(LO_VERT_ANGLE);
layers.push(CompositionLayer::Equirect2(sky));
static GRID_POSE: Lazy<xr::Posef> = Lazy::new(|| {
translation_rotation_to_posef(Vec3A::ZERO, Quat::from_rotation_x(PI * -0.5))
});
let grid = xr::CompositionLayerQuad::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(*GRID_POSE)
.size(xr::Extent2Df {
width: 10.0,
height: 10.0,
})
.sub_image(grid_image)
.eye_visibility(xr::EyeVisibility::BOTH)
.space(&xr.stage);
layers.push(CompositionLayer::Quad(grid));
Ok(layers)
}
}
pub(super) fn create_skybox(xr: &XrState, app: &AppState) -> Option<Skybox> {
if !app.session.config.use_skybox {
return None;
}
xr.instance
.exts()
.khr_composition_layer_equirect2
.and_then(|_| Skybox::new(app).ok())
}
+114 -13
View File
@@ -6,7 +6,8 @@ use openxr as xr;
use smallvec::SmallVec;
use vulkano::{
image::{sampler::Filter, sys::RawImage, view::ImageView, ImageCreateInfo, ImageUsage},
format::Format,
image::{sys::RawImage, view::ImageView, ImageCreateInfo, ImageUsage},
pipeline::graphics::color_blend::AttachmentBlend,
Handle,
};
@@ -15,15 +16,47 @@ use crate::graphics::{WlxCommandBuffer, WlxGraphics, WlxPipeline, WlxPipelineDyn
use super::XrState;
#[derive(Default)]
pub(super) struct SwapchainOpts {
pub immutable: bool,
pub srgb: bool,
pub grid: bool,
}
impl SwapchainOpts {
pub fn new() -> Self {
Default::default()
}
pub fn immutable(mut self) -> Self {
self.immutable = true;
self
}
pub fn srgb(mut self) -> Self {
self.srgb = true;
self
}
pub fn grid(mut self) -> Self {
self.grid = true;
self
}
}
pub(super) fn create_swapchain_render_data(
xr: &XrState,
graphics: Arc<WlxGraphics>,
extent: [u32; 3],
opts: SwapchainOpts,
) -> anyhow::Result<SwapchainRenderData> {
let create_flags = if opts.immutable {
xr::SwapchainCreateFlags::STATIC_IMAGE
} else {
xr::SwapchainCreateFlags::EMPTY
};
let swapchain = xr.session.create_swapchain(&xr::SwapchainCreateInfo {
create_flags: xr::SwapchainCreateFlags::EMPTY,
create_flags,
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT | xr::SwapchainUsageFlags::SAMPLED,
format: graphics.native_format as _,
format: Format::R8G8B8A8_SRGB as _,
sample_count: 1,
width: extent[0],
height: extent[1],
@@ -35,10 +68,22 @@ pub(super) fn create_swapchain_render_data(
let Ok(shaders) = graphics.shared_shaders.read() else {
bail!("Failed to lock shared shaders for reading");
};
let image_fmt = if opts.srgb {
Format::R8G8B8A8_SRGB
} else {
Format::R8G8B8A8_UNORM
};
let frag_shader = if opts.grid {
"frag_grid"
} else {
"frag_swapchain"
};
let pipeline = graphics.create_pipeline_dynamic(
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_srgb").unwrap().clone(), // want panic
graphics.native_format,
shaders.get(frag_shader).unwrap().clone(), // want panic
image_fmt,
Some(AttachmentBlend::alpha()),
)?;
@@ -53,7 +98,7 @@ pub(super) fn create_swapchain_render_data(
graphics.device.clone(),
vk_image,
ImageCreateInfo {
format: graphics.native_format,
format: image_fmt, // actually SRGB but we lie
extent,
usage: ImageUsage::COLOR_ATTACHMENT,
..Default::default()
@@ -71,6 +116,7 @@ pub(super) fn create_swapchain_render_data(
pipeline,
images,
extent,
target_extent: [0, 0, 0],
})
}
@@ -78,6 +124,7 @@ pub(super) struct SwapchainRenderData {
pub(super) swapchain: xr::Swapchain<xr::Vulkan>,
pub(super) pipeline: Arc<WlxPipeline<WlxPipelineDynamic>>,
pub(super) extent: [u32; 3],
pub(super) target_extent: [u32; 3],
pub(super) images: SmallVec<[Arc<ImageView>; 4]>,
}
@@ -94,16 +141,18 @@ impl SwapchainRenderData {
let render_target = &mut self.images[idx];
command_buffer.begin_rendering(render_target.clone())?;
let target_extent = render_target.image().extent();
self.target_extent = render_target.image().extent();
let set0 = self
.pipeline
.uniform_sampler(0, view.clone(), Filter::Linear)?;
let set0 = self.pipeline.uniform_sampler(
0,
view.clone(),
command_buffer.graphics.texture_filtering,
)?;
let set1 = self.pipeline.uniform_buffer(1, vec![alpha])?;
let pass = self.pipeline.create_pass(
[target_extent[0] as _, target_extent[1] as _],
[self.target_extent[0] as _, self.target_extent[1] as _],
command_buffer.graphics.quad_verts.clone(),
command_buffer.graphics.quad_indices.clone(),
vec![set0, set1],
@@ -118,8 +167,60 @@ impl SwapchainRenderData {
.image_rect(xr::Rect2Di {
offset: xr::Offset2Di { x: 0, y: 0 },
extent: xr::Extent2Di {
width: target_extent[0] as _,
height: target_extent[1] as _,
width: self.target_extent[0] as _,
height: self.target_extent[1] as _,
},
})
.image_array_index(0))
}
pub(super) fn acquire_compute_release(
&mut self,
command_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<xr::SwapchainSubImage<xr::Vulkan>> {
let idx = self.swapchain.acquire_image()? as usize;
self.swapchain.wait_image(xr::Duration::INFINITE)?;
let render_target = &mut self.images[idx];
command_buffer.begin_rendering(render_target.clone())?;
self.target_extent = render_target.image().extent();
let pass = self.pipeline.create_pass(
[self.target_extent[0] as _, self.target_extent[1] as _],
command_buffer.graphics.quad_verts.clone(),
command_buffer.graphics.quad_indices.clone(),
vec![],
)?;
command_buffer.run_ref(&pass)?;
command_buffer.end_rendering()?;
self.swapchain.release_image()?;
Ok(xr::SwapchainSubImage::new()
.swapchain(&self.swapchain)
.image_rect(xr::Rect2Di {
offset: xr::Offset2Di { x: 0, y: 0 },
extent: xr::Extent2Di {
width: self.target_extent[0] as _,
height: self.target_extent[1] as _,
},
})
.image_array_index(0))
}
pub(super) fn present_last(&self) -> anyhow::Result<xr::SwapchainSubImage<xr::Vulkan>> {
debug_assert!(
self.target_extent[0] * self.target_extent[1] != 0,
"present_last: target_extent zero"
);
Ok(xr::SwapchainSubImage::new()
.swapchain(&self.swapchain)
.image_rect(xr::Rect2Di {
offset: xr::Offset2Di { x: 0, y: 0 },
extent: xr::Extent2Di {
width: self.target_extent[0] as _,
height: self.target_extent[1] as _,
},
})
.image_array_index(0))
+124 -36
View File
@@ -8,10 +8,13 @@ use rosc::{OscMessage, OscPacket, OscType};
use crate::overlays::{keyboard::KEYBOARD_NAME, watch::WATCH_NAME};
use super::common::OverlayContainer;
use crate::backend::input::TrackedDeviceRole;
use super::{common::OverlayContainer, input::TrackedDevice};
pub struct OscSender {
last_sent: Instant,
last_sent_overlay: Instant,
last_sent_battery: Instant,
upstream: UdpSocket,
}
@@ -29,7 +32,8 @@ impl OscSender {
Ok(Self {
upstream,
last_sent: Instant::now(),
last_sent_overlay: Instant::now(),
last_sent_battery: Instant::now(),
})
}
@@ -46,49 +50,133 @@ impl OscSender {
Ok(())
}
pub fn send_params<D>(&mut self, overlays: &OverlayContainer<D>) -> anyhow::Result<()>
pub fn send_params<D>(
&mut self,
overlays: &OverlayContainer<D>,
devices: &Vec<TrackedDevice>,
) -> anyhow::Result<()>
where
D: Default,
{
if self.last_sent.elapsed().as_millis() < 100 {
return Ok(());
}
// send overlay data every 0.1 seconds
if self.last_sent_overlay.elapsed().as_millis() >= 100 {
self.last_sent_overlay = Instant::now();
let mut num_overlays = 0;
let mut has_keyboard = false;
let mut has_wrist = false;
let mut num_overlays = 0;
let mut has_keyboard = false;
let mut has_wrist = false;
for o in overlays.iter() {
if !o.state.want_visible {
continue;
}
match o.state.name.as_ref() {
WATCH_NAME => has_wrist = true,
KEYBOARD_NAME => has_keyboard = true,
_ => {
if o.state.interactable {
num_overlays += 1
for o in overlays.iter() {
if !o.state.want_visible {
continue;
}
match o.state.name.as_ref() {
WATCH_NAME => has_wrist = true,
KEYBOARD_NAME => has_keyboard = true,
_ => {
if o.state.interactable {
num_overlays += 1
}
}
}
}
self.send_message(
"/avatar/parameters/isOverlayOpen".into(),
vec![OscType::Bool(num_overlays > 0)],
)?;
self.send_message(
"/avatar/parameters/isKeyboardOpen".into(),
vec![OscType::Bool(has_keyboard)],
)?;
self.send_message(
"/avatar/parameters/isWristVisible".into(),
vec![OscType::Bool(has_wrist)],
)?;
self.send_message(
"/avatar/parameters/openOverlayCount".into(),
vec![OscType::Int(num_overlays)],
)?;
}
self.send_message(
"/avatar/parameters/isOverlayOpen".into(),
vec![OscType::Bool(num_overlays > 0)],
)?;
self.send_message(
"/avatar/parameters/isKeyboardOpen".into(),
vec![OscType::Bool(has_keyboard)],
)?;
self.send_message(
"/avatar/parameters/isWristVisible".into(),
vec![OscType::Bool(has_wrist)],
)?;
self.send_message(
"/avatar/parameters/openOverlayCount".into(),
vec![OscType::Int(num_overlays)],
)?;
// send battery levels every 10 seconds
if self.last_sent_battery.elapsed().as_millis() >= 10000 {
self.last_sent_battery = Instant::now();
let mut tracker_count: i8 = 0;
let mut controller_count: i8 = 0;
let mut tracker_total_bat = 0.0;
let mut controller_total_bat = 0.0;
for device in devices {
let tracker_param;
// soc is the battery level (set to device status.charge)
let level = device.soc.unwrap_or(-1.0);
let parameter = match device.role {
TrackedDeviceRole::None => continue,
TrackedDeviceRole::Hmd => {
// legacy OVR Toolkit style (int)
// as of 20 Nov 2024 OVR Toolkit uses int 0-100, but this may change in a future update.
//TODO: update this once their implementation matches their docs
self.send_message(
"/avatar/parameters/hmdBattery".into(),
vec![OscType::Int((level * 100.0f32).round() as i32)],
)?;
"headset"
}
TrackedDeviceRole::LeftHand => {
controller_count += 1;
controller_total_bat += level;
"leftController"
}
TrackedDeviceRole::RightHand => {
controller_count += 1;
controller_total_bat += level;
"rightController"
}
TrackedDeviceRole::Tracker => {
tracker_count += 1;
tracker_total_bat += level;
tracker_param = format!("tracker{tracker_count}");
tracker_param.as_str()
}
};
// send device battery parameters
self.send_message(
format!("/avatar/parameters/{parameter}Battery").into(),
vec![OscType::Float(level)],
)?;
self.send_message(
format!("/avatar/parameters/{parameter}Charging").into(),
vec![OscType::Bool(device.charging)],
)?;
}
// send average controller and tracker battery parameters
self.send_message(
format!("/avatar/parameters/averageControllerBattery").into(),
vec![OscType::Float(
controller_total_bat / controller_count as f32,
)],
)?;
self.send_message(
format!("/avatar/parameters/averageTrackerBattery").into(),
vec![OscType::Float(tracker_total_bat / tracker_count as f32)],
)?;
}
Ok(())
}
pub fn send_single_param(
&mut self,
parameter: String,
values: Vec<OscType>,
) -> anyhow::Result<()> {
self.send_message(parameter, values)?;
Ok(())
}
+132 -37
View File
@@ -8,33 +8,53 @@ use std::{
use anyhow::Ok;
use glam::{Affine2, Affine3A, Mat3A, Quat, Vec2, Vec3, Vec3A};
use serde::Deserialize;
use vulkano::image::view::ImageView;
use crate::state::AppState;
use crate::{
config::AStrMapExt,
state::{AppState, KeyboardFocus},
};
use super::input::{DummyInteractionHandler, Haptics, InteractionHandler, PointerHit};
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
static OVERLAY_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
pub trait OverlayBackend: OverlayRenderer + InteractionHandler {}
pub trait OverlayBackend: OverlayRenderer + InteractionHandler {
fn set_renderer(&mut self, renderer: Box<dyn OverlayRenderer>);
fn set_interaction(&mut self, interaction: Box<dyn InteractionHandler>);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
pub struct OverlayID(pub usize);
pub const Z_ORDER_TOAST: u32 = 70;
pub const Z_ORDER_LINES: u32 = 69;
pub const Z_ORDER_WATCH: u32 = 68;
pub const Z_ORDER_ANCHOR: u32 = 67;
pub const Z_ORDER_DEFAULT: u32 = 0;
pub const Z_ORDER_DASHBOARD: u32 = Z_ORDER_DEFAULT;
pub struct OverlayState {
pub id: usize,
pub id: OverlayID,
pub name: Arc<str>,
pub want_visible: bool,
pub show_hide: bool,
pub grabbable: bool,
pub interactable: bool,
pub recenter: bool,
pub anchored: bool,
pub keyboard_focus: Option<KeyboardFocus>,
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,
@@ -43,21 +63,24 @@ pub struct OverlayState {
impl Default for OverlayState {
fn default() -> Self {
OverlayState {
id: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed),
id: OverlayID(OVERLAY_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed)),
name: Arc::from(""),
want_visible: false,
show_hide: false,
grabbable: false,
recenter: false,
interactable: false,
anchored: false,
keyboard_focus: None,
dirty: true,
alpha: 1.0,
z_order: Z_ORDER_DEFAULT,
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,
@@ -93,43 +116,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;
}
@@ -179,7 +210,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<()> {
@@ -188,6 +240,9 @@ where
pub fn view(&mut self) -> Option<Arc<ImageView>> {
self.backend.view()
}
pub fn frame_transform(&mut self) -> Option<FrameTransform> {
self.backend.frame_transform()
}
pub fn set_visible(&mut self, app: &mut AppState, visible: bool) -> anyhow::Result<()> {
let old_visible = self.state.want_visible;
self.state.want_visible = visible;
@@ -202,12 +257,26 @@ where
}
}
#[derive(Default)]
pub struct FrameTransform {
pub extent: [u32; 3],
pub transform: Affine3A,
}
pub trait OverlayRenderer {
/// Called once, before the first frame is rendered
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()>;
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()>;
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()>;
/// Called when the presentation layer is ready to present a new frame
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()>;
/// Called to retrieve the current image to be displayed
fn view(&mut self) -> Option<Arc<ImageView>>;
/// Called to retrieve the effective extent of the image
/// Used for creating swapchains.
///
/// Muse not be None if view() is also not None
fn frame_transform(&mut self) -> Option<FrameTransform>;
}
pub struct FallbackRenderer;
@@ -228,15 +297,23 @@ impl OverlayRenderer for FallbackRenderer {
fn view(&mut self) -> Option<Arc<ImageView>> {
None
}
fn frame_transform(&mut self) -> Option<FrameTransform> {
None
}
}
// Boilerplate and dummies
#[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 {
@@ -253,7 +330,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)
@@ -270,6 +354,9 @@ impl OverlayRenderer for SplitOverlayBackend {
fn view(&mut self) -> Option<Arc<ImageView>> {
self.renderer.view()
}
fn frame_transform(&mut self) -> Option<FrameTransform> {
self.renderer.frame_transform()
}
}
impl InteractionHandler for SplitOverlayBackend {
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
@@ -287,10 +374,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)
}
+119
View File
@@ -0,0 +1,119 @@
use std::{
cmp,
collections::{BinaryHeap, VecDeque},
sync::atomic::{self, AtomicUsize},
time::Instant,
};
use serde::Deserialize;
use crate::state::AppState;
#[cfg(feature = "wayvr")]
use crate::gui::modular::button::WayVRAction;
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,
ShowHide,
}
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),
#[cfg(feature = "wayvr")]
WayVR(WayVRAction),
}
#[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);
}
}
}
+317
View File
@@ -0,0 +1,317 @@
use std::sync::Arc;
use vulkano::{
command_buffer::CommandBufferUsage,
image::{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::{
canvas::Canvas,
modular::{modular_canvas, ModularData},
},
hid::USE_UINPUT,
state::{AppState, ScreenMeta},
};
use super::{
input::{TrackedDevice, TrackedDeviceRole},
overlay::{OverlayID, OverlayRenderer},
};
static LAST_SIZE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
struct PreviewState {
canvas: Canvas<(), ModularData>,
pipeline: Arc<DynamicPipeline>,
pass: DynamicPass,
swapchain: Arc<Swapchain>,
images: Vec<Arc<ImageView>>,
}
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(), pipeline.graphics.texture_filtering)
.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::get_config_root().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(); // want panic
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(), // want panic
..Default::default()
},
)?;
let image_views = images
.into_iter()
// want panic
.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: OverlayID(0),
native_handle: 0,
});
app.screens.push(ScreenMeta {
name: "DP-2".into(),
id: OverlayID(0),
native_handle: 0,
});
app.screens.push(ScreenMeta {
name: "DP-3".into(),
id: OverlayID(0),
native_handle: 0,
});
}
+255
View File
@@ -0,0 +1,255 @@
use std::{io::Read, os::unix::net::UnixStream, path::PathBuf, sync::Arc};
use smithay::{
backend::input::Keycode,
input::{keyboard::KeyboardHandle, pointer::PointerHandle},
reexports::wayland_server,
utils::SerialCounter,
};
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
use super::{
comp::{self, ClientState},
display, process, ProcessWayVREnv,
};
pub struct WayVRClient {
pub client: wayland_server::Client,
pub display_handle: display::DisplayHandle,
pub pid: u32,
}
pub struct WayVRCompositor {
pub state: comp::Application,
pub seat_keyboard: KeyboardHandle<comp::Application>,
pub seat_pointer: PointerHandle<comp::Application>,
pub serial_counter: SerialCounter,
pub wayland_env: super::WaylandEnv,
display: wayland_server::Display<comp::Application>,
listener: wayland_server::ListeningSocket,
toplevel_surf_count: u32, // for logging purposes
pub clients: Vec<WayVRClient>,
}
fn get_wayvr_env_from_pid(pid: i32) -> anyhow::Result<ProcessWayVREnv> {
let path = format!("/proc/{}/environ", pid);
let mut env_data = String::new();
std::fs::File::open(path)?.read_to_string(&mut env_data)?;
let lines: Vec<&str> = env_data.split('\0').filter(|s| !s.is_empty()).collect();
let mut env = ProcessWayVREnv {
display_auth: None,
display_name: None,
};
for line in lines {
if let Some((key, value)) = line.split_once('=') {
if key == "WAYVR_DISPLAY_AUTH" {
env.display_auth = Some(String::from(value));
} else if key == "WAYVR_DISPLAY_NAME" {
env.display_name = Some(String::from(value));
}
}
}
Ok(env)
}
impl WayVRCompositor {
pub fn new(
state: comp::Application,
display: wayland_server::Display<comp::Application>,
seat_keyboard: KeyboardHandle<comp::Application>,
seat_pointer: PointerHandle<comp::Application>,
) -> anyhow::Result<Self> {
let (wayland_env, listener) = create_wayland_listener()?;
Ok(Self {
state,
display,
seat_keyboard,
seat_pointer,
listener,
wayland_env,
serial_counter: SerialCounter::new(),
clients: Vec::new(),
toplevel_surf_count: 0,
})
}
pub fn add_client(&mut self, client: WayVRClient) {
self.clients.push(client);
}
pub fn cleanup_clients(&mut self) {
self.clients.retain(|client| {
let Some(data) = client.client.get_data::<ClientState>() else {
return false;
};
if *data.disconnected.lock().unwrap() {
return false;
}
true
});
}
fn accept_connection(
&mut self,
stream: UnixStream,
displays: &mut display::DisplayVec,
processes: &mut process::ProcessVec,
) -> anyhow::Result<()> {
let client = self
.display
.handle()
.insert_client(stream, Arc::new(comp::ClientState::default()))
.unwrap();
let creds = client.get_credentials(&self.display.handle())?;
let process_env = get_wayvr_env_from_pid(creds.pid)?;
// Find suitable auth key from the process list
for p in processes.vec.iter().flatten() {
if let process::Process::Managed(process) = &p.obj {
if let Some(auth_key) = &process_env.display_auth {
// Find process with matching auth key
if process.auth_key.as_str() == auth_key {
// Check if display handle is valid
if displays.get(&process.display_handle).is_some() {
// Add client
self.add_client(WayVRClient {
client,
display_handle: process.display_handle,
pid: creds.pid as u32,
});
return Ok(());
}
}
}
}
}
// This is a new process which we didn't met before.
// Treat external processes exclusively (spawned by the user or external program)
log::warn!(
"External process ID {} connected to this Wayland server",
creds.pid
);
self.state
.wayvr_tasks
.send(WayVRTask::NewExternalProcess(ExternalProcessRequest {
env: process_env,
client,
pid: creds.pid as u32,
}));
Ok(())
}
fn accept_connections(
&mut self,
displays: &mut display::DisplayVec,
processes: &mut process::ProcessVec,
) -> anyhow::Result<()> {
if let Some(stream) = self.listener.accept()? {
if let Err(e) = self.accept_connection(stream, displays, processes) {
log::error!("Failed to accept connection: {}", e);
}
}
Ok(())
}
pub fn tick_wayland(
&mut self,
displays: &mut display::DisplayVec,
processes: &mut process::ProcessVec,
) -> anyhow::Result<()> {
if let Err(e) = self.accept_connections(displays, processes) {
log::error!("accept_connections failed: {}", e);
}
self.display.dispatch_clients(&mut self.state)?;
self.display.flush_clients()?;
let surf_count = self.state.xdg_shell.toplevel_surfaces().len() as u32;
if surf_count != self.toplevel_surf_count {
self.toplevel_surf_count = surf_count;
log::info!("Toplevel surface count changed: {}", surf_count);
}
Ok(())
}
pub fn send_key(&mut self, virtual_key: u32, down: bool) {
let state = if down {
smithay::backend::input::KeyState::Pressed
} else {
smithay::backend::input::KeyState::Released
};
self.seat_keyboard.input::<(), _>(
&mut self.state,
Keycode::new(virtual_key),
state,
self.serial_counter.next_serial(),
0,
|_, _, _| smithay::input::keyboard::FilterResult::Forward,
);
}
}
const STARTING_WAYLAND_ADDR_IDX: u32 = 20;
fn export_display_number(display_num: u32) -> anyhow::Result<()> {
let mut path = std::env::var("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"));
path.push("wayvr.disp");
std::fs::write(path, format!("{}\n", display_num))?;
Ok(())
}
fn create_wayland_listener() -> anyhow::Result<(super::WaylandEnv, wayland_server::ListeningSocket)>
{
let mut env = super::WaylandEnv {
display_num: STARTING_WAYLAND_ADDR_IDX,
};
let listener = loop {
let display_str = env.display_num_string();
log::debug!("Trying to open socket \"{}\"", display_str);
match wayland_server::ListeningSocket::bind(display_str.as_str()) {
Ok(listener) => {
log::debug!("Listening to {}", display_str);
break listener;
}
Err(e) => {
log::debug!(
"Failed to open socket \"{}\" (reason: {}), trying next...",
display_str,
e
);
env.display_num += 1;
if env.display_num > STARTING_WAYLAND_ADDR_IDX + 20 {
// Highly unlikely for the user to have 20 Wayland displays enabled at once. Return error instead.
anyhow::bail!("Failed to create wayland-server socket")
}
}
}
};
let _ = export_display_number(env.display_num);
Ok((env, listener))
}
+233
View File
@@ -0,0 +1,233 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::utils::on_commit_buffer_handler;
use smithay::backend::renderer::ImportDma;
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server;
use smithay::reexports::wayland_server::protocol::{wl_buffer, wl_seat, wl_surface};
use smithay::reexports::wayland_server::Resource;
use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::dmabuf::{
DmabufFeedback, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier,
};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{
delegate_compositor, delegate_data_device, delegate_dmabuf, delegate_output, delegate_seat,
delegate_shm, delegate_xdg_shell,
};
use std::collections::HashSet;
use std::os::fd::OwnedFd;
use std::sync::{Arc, Mutex};
use smithay::utils::Serial;
use smithay::wayland::compositor::{
self, with_surface_tree_downward, SurfaceAttributes, TraversalAction,
};
use smithay::wayland::selection::data_device::{
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
};
use smithay::wayland::selection::SelectionHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
};
use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
use wayland_server::protocol::wl_surface::WlSurface;
use wayland_server::Client;
use super::event_queue::SyncEventQueue;
use super::WayVRTask;
pub struct Application {
pub gles_renderer: GlesRenderer,
pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
pub compositor: compositor::CompositorState,
pub xdg_shell: XdgShellState,
pub seat_state: SeatState<Application>,
pub shm: ShmState,
pub data_device: DataDeviceState,
pub wayvr_tasks: SyncEventQueue<WayVRTask>,
pub redraw_requests: HashSet<wayland_server::backend::ObjectId>,
}
impl Application {
pub fn check_redraw(&mut self, surface: &WlSurface) -> bool {
self.redraw_requests.remove(&surface.id())
}
}
impl compositor::CompositorHandler for Application {
fn compositor_state(&mut self) -> &mut compositor::CompositorState {
&mut self.compositor
}
fn client_compositor_state<'a>(
&self,
client: &'a Client,
) -> &'a compositor::CompositorClientState {
&client.get_data::<ClientState>().unwrap().compositor_state
}
fn commit(&mut self, surface: &WlSurface) {
on_commit_buffer_handler::<Self>(surface);
self.redraw_requests.insert(surface.id());
}
}
impl SeatHandler for Application {
type KeyboardFocus = WlSurface;
type PointerFocus = WlSurface;
type TouchFocus = WlSurface;
fn seat_state(&mut self) -> &mut SeatState<Self> {
&mut self.seat_state
}
fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
fn cursor_image(
&mut self,
_seat: &Seat<Self>,
_image: smithay::input::pointer::CursorImageStatus,
) {
}
}
impl BufferHandler for Application {
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
}
impl ClientDndGrabHandler for Application {}
impl ServerDndGrabHandler for Application {
fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat<Self>) {}
}
impl DataDeviceHandler for Application {
fn data_device_state(&self) -> &DataDeviceState {
&self.data_device
}
}
impl SelectionHandler for Application {
type SelectionUserData = ();
}
#[derive(Default)]
pub struct ClientState {
compositor_state: compositor::CompositorClientState,
pub disconnected: Arc<Mutex<bool>>,
}
impl ClientData for ClientState {
fn initialized(&self, client_id: ClientId) {
log::debug!("Client ID {:?} connected", client_id);
}
fn disconnected(&self, client_id: ClientId, reason: DisconnectReason) {
*self.disconnected.lock().unwrap() = true;
log::debug!(
"Client ID {:?} disconnected. Reason: {:?}",
client_id,
reason
);
}
}
impl AsMut<compositor::CompositorState> for Application {
fn as_mut(&mut self) -> &mut compositor::CompositorState {
&mut self.compositor
}
}
impl XdgShellHandler for Application {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
&mut self.xdg_shell
}
fn new_toplevel(&mut self, surface: ToplevelSurface) {
if let Some(client) = surface.wl_surface().client() {
self.wayvr_tasks
.send(WayVRTask::NewToplevel(client.id(), surface.clone()));
}
surface.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Activated);
});
surface.send_configure();
}
fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) {
// Handle popup creation here
}
fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {
// Handle popup grab here
}
fn reposition_request(
&mut self,
_surface: PopupSurface,
_positioner: PositionerState,
_token: u32,
) {
// Handle popup reposition here
}
}
impl ShmHandler for Application {
fn shm_state(&self) -> &ShmState {
&self.shm
}
}
impl OutputHandler for Application {}
impl DmabufHandler for Application {
fn dmabuf_state(&mut self) -> &mut DmabufState {
&mut self.dmabuf_state.0
}
fn dmabuf_imported(
&mut self,
_global: &DmabufGlobal,
dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
if self.gles_renderer.import_dmabuf(&dmabuf, None).is_ok() {
let _ = notifier.successful::<Application>();
} else {
notifier.failed();
}
}
}
delegate_dmabuf!(Application);
delegate_xdg_shell!(Application);
delegate_compositor!(Application);
delegate_shm!(Application);
delegate_seat!(Application);
delegate_data_device!(Application);
delegate_output!(Application);
pub fn send_frames_surface_tree(surface: &wl_surface::WlSurface, time: u32) {
with_surface_tree_downward(
surface,
(),
|_, _, &()| TraversalAction::DoChildren(()),
|_surf, states, &()| {
// the surface may not have any user_data if it is a subsurface and has not
// yet been commited
for callback in states
.cached_state
.get::<SurfaceAttributes>()
.current()
.frame_callbacks
.drain(..)
{
callback.done(time);
}
},
|_, _, &()| true,
);
}
+554
View File
@@ -0,0 +1,554 @@
use std::{cell::RefCell, rc::Rc};
use smithay::{
backend::renderer::{
element::{
surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement},
Kind,
},
gles::{ffi, GlesRenderer, GlesTexture},
utils::draw_render_elements,
Bind, Color32F, Frame, Renderer,
},
input,
utils::{Logical, Point, Rectangle, Size, Transform},
wayland::shell::xdg::ToplevelSurface,
};
use wayvr_ipc::packet_server;
use crate::{
backend::{overlay::OverlayID, wayvr::time::get_millis},
gen_id,
};
use super::{
client::WayVRCompositor, comp::send_frames_surface_tree, egl_data, event_queue::SyncEventQueue,
process, smithay_wrapper, time, window, WayVRSignal,
};
fn generate_auth_key() -> String {
let uuid = uuid::Uuid::new_v4();
uuid.to_string()
}
#[derive(Debug)]
pub struct DisplayWindow {
pub window_handle: window::WindowHandle,
pub toplevel: ToplevelSurface,
pub process_handle: process::ProcessHandle,
}
pub struct SpawnProcessResult {
pub auth_key: String,
pub child: std::process::Child,
}
#[derive(Debug)]
pub enum DisplayTask {
ProcessCleanup(process::ProcessHandle),
}
const MAX_DISPLAY_SIZE: u16 = 8192;
#[derive(Debug)]
pub struct Display {
// Display info stuff
pub width: u16,
pub height: u16,
pub name: String,
pub visible: bool,
pub layout: packet_server::WvrDisplayWindowLayout,
pub overlay_id: Option<OverlayID>,
pub wants_redraw: bool,
pub primary: bool,
pub wm: Rc<RefCell<window::WindowManager>>,
pub displayed_windows: Vec<DisplayWindow>,
wayland_env: super::WaylandEnv,
last_pressed_time_ms: u64,
pub no_windows_since: Option<u64>,
// Render data stuff
gles_texture: GlesTexture, // TODO: drop texture
egl_image: khronos_egl::Image,
egl_data: Rc<egl_data::EGLData>,
pub dmabuf_data: egl_data::DMAbufData,
pub tasks: SyncEventQueue<DisplayTask>,
}
impl Drop for Display {
fn drop(&mut self) {
let _ = self
.egl_data
.egl
.destroy_image(self.egl_data.display, self.egl_image);
}
}
pub struct DisplayInitParams<'a> {
pub wm: Rc<RefCell<window::WindowManager>>,
pub renderer: &'a mut GlesRenderer,
pub egl_data: Rc<egl_data::EGLData>,
pub wayland_env: super::WaylandEnv,
pub width: u16,
pub height: u16,
pub name: &'a str,
pub primary: bool,
}
impl Display {
pub fn new(params: DisplayInitParams) -> anyhow::Result<Self> {
if params.width > MAX_DISPLAY_SIZE {
anyhow::bail!(
"display width ({}) is larger than {}",
params.width,
MAX_DISPLAY_SIZE
);
}
if params.height > MAX_DISPLAY_SIZE {
anyhow::bail!(
"display height ({}) is larger than {}",
params.height,
MAX_DISPLAY_SIZE
);
}
let tex_format = ffi::RGBA;
let internal_format = ffi::RGBA8;
let tex_id = params.renderer.with_context(|gl| {
smithay_wrapper::create_framebuffer_texture(
gl,
params.width as u32,
params.height as u32,
tex_format,
internal_format,
)
})?;
let egl_image = params.egl_data.create_egl_image(tex_id)?;
let dmabuf_data = params.egl_data.create_dmabuf_data(&egl_image)?;
let opaque = false;
let size = (params.width as i32, params.height as i32).into();
let gles_texture = unsafe {
GlesTexture::from_raw(params.renderer, Some(tex_format), opaque, tex_id, size)
};
Ok(Self {
egl_data: params.egl_data,
width: params.width,
height: params.height,
name: String::from(params.name),
primary: params.primary,
wayland_env: params.wayland_env,
wm: params.wm,
displayed_windows: Vec::new(),
dmabuf_data,
egl_image,
gles_texture,
last_pressed_time_ms: 0,
no_windows_since: None,
overlay_id: None,
tasks: SyncEventQueue::new(),
visible: true,
wants_redraw: true,
layout: packet_server::WvrDisplayWindowLayout::Tiling,
})
}
pub fn as_packet(&self, handle: DisplayHandle) -> packet_server::WvrDisplay {
packet_server::WvrDisplay {
width: self.width,
height: self.height,
name: self.name.clone(),
visible: self.visible,
handle: handle.as_packet(),
}
}
pub fn add_window(
&mut self,
window_handle: window::WindowHandle,
process_handle: process::ProcessHandle,
toplevel: &ToplevelSurface,
) {
log::debug!("Attaching toplevel surface into display");
self.displayed_windows.push(DisplayWindow {
window_handle,
process_handle,
toplevel: toplevel.clone(),
});
self.reposition_windows();
}
pub fn reposition_windows(&mut self) {
let window_count = self.displayed_windows.len();
match &self.layout {
packet_server::WvrDisplayWindowLayout::Tiling => {
let mut i = 0;
for win in self.displayed_windows.iter_mut() {
if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.window_handle) {
if !window.visible {
continue;
}
let d_cur = i as f32 / window_count as f32;
let d_next = (i + 1) as f32 / window_count as f32;
let left = (d_cur * self.width as f32) as i32;
let right = (d_next * self.width as f32) as i32;
window.set_pos(left, 0);
window.set_size((right - left) as u32, self.height as u32);
i += 1;
}
}
}
packet_server::WvrDisplayWindowLayout::Stacking(opts) => {
let do_margins = |margins: &packet_server::Margins, window: &mut window::Window| {
let top = margins.top as i32;
let bottom = self.height as i32 - margins.bottom as i32;
let left = margins.left as i32;
let right = self.width as i32 - margins.right as i32;
let width = right - left;
let height = bottom - top;
if width < 0 || height < 0 {
return; // wrong parameters, do nothing!
}
window.set_pos(left, top);
window.set_size(width as u32, height as u32);
};
let mut i = 0;
for win in self.displayed_windows.iter_mut() {
if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.window_handle) {
if !window.visible {
continue;
}
do_margins(
if i == 0 {
&opts.margins_first
} else {
&opts.margins_rest
},
window,
);
i += 1;
}
}
}
}
}
pub fn tick(
&mut self,
config: &super::Config,
handle: &DisplayHandle,
signals: &mut SyncEventQueue<WayVRSignal>,
) {
if self.visible {
if !self.displayed_windows.is_empty() {
self.no_windows_since = None;
} else if let Some(auto_hide_delay) = config.auto_hide_delay {
if let Some(s) = self.no_windows_since {
if s + (auto_hide_delay as u64) < get_millis() {
// Auto-hide after specific time
signals.send(WayVRSignal::DisplayVisibility(*handle, false));
}
}
}
}
while let Some(task) = self.tasks.read() {
match task {
DisplayTask::ProcessCleanup(process_handle) => {
let count = self.displayed_windows.len();
self.displayed_windows
.retain(|win| win.process_handle != process_handle);
log::info!(
"Cleanup finished for display \"{}\". Current window count: {}",
self.name,
self.displayed_windows.len()
);
self.no_windows_since = Some(get_millis());
if count != self.displayed_windows.len() {
signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::WindowRemoved,
));
}
self.reposition_windows();
}
}
}
}
pub fn tick_render(&self, renderer: &mut GlesRenderer, time_ms: u64) -> anyhow::Result<()> {
renderer.bind(self.gles_texture.clone())?;
let size = Size::from((self.width as i32, self.height as i32));
let damage: Rectangle<i32, smithay::utils::Physical> =
Rectangle::from_loc_and_size((0, 0), size);
let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = self
.displayed_windows
.iter()
.flat_map(|display_window| {
let wm = self.wm.borrow_mut();
if let Some(window) = wm.windows.get(&display_window.window_handle) {
if !window.visible {
return vec![];
}
render_elements_from_surface_tree(
renderer,
display_window.toplevel.wl_surface(),
(window.pos_x, window.pos_y),
1.0,
1.0,
Kind::Unspecified,
)
} else {
// Failed to fetch window
vec![]
}
})
.collect();
let mut frame = renderer.render(size, Transform::Normal)?;
let clear_color = if self.displayed_windows.is_empty() {
Color32F::new(1.0, 1.0, 1.0, 0.5)
} else {
Color32F::new(0.0, 0.0, 0.0, 0.0)
};
frame.clear(clear_color, &[damage])?;
draw_render_elements(&mut frame, 1.0, &elements, &[damage])?;
let _sync_point = frame.finish()?;
for window in &self.displayed_windows {
send_frames_surface_tree(window.toplevel.wl_surface(), time_ms as u32);
}
Ok(())
}
fn get_hovered_window(&self, cursor_x: u32, cursor_y: u32) -> Option<window::WindowHandle> {
let wm = self.wm.borrow();
for cell in self.displayed_windows.iter().rev() {
if let Some(window) = wm.windows.get(&cell.window_handle) {
if !window.visible {
continue;
}
if (cursor_x as i32) >= window.pos_x
&& (cursor_x as i32) < window.pos_x + window.size_x as i32
&& (cursor_y as i32) >= window.pos_y
&& (cursor_y as i32) < window.pos_y + window.size_y as i32
{
return Some(cell.window_handle);
}
}
}
None
}
pub fn trigger_rerender(&mut self) {
self.wants_redraw = true;
}
pub fn set_visible(&mut self, visible: bool) {
log::info!("Display \"{}\" visible: {}", self.name.as_str(), visible);
if self.visible == visible {
return;
}
self.visible = visible;
if visible {
self.no_windows_since = None;
self.trigger_rerender();
}
}
pub fn set_layout(&mut self, layout: packet_server::WvrDisplayWindowLayout) {
log::info!("Display \"{}\" layout: {:?}", self.name.as_str(), layout);
if self.layout == layout {
return;
}
self.layout = layout;
self.trigger_rerender();
self.reposition_windows();
}
pub fn send_mouse_move(
&self,
config: &super::Config,
manager: &mut WayVRCompositor,
x: u32,
y: u32,
) {
let current_ms = time::get_millis();
if self.last_pressed_time_ms + config.click_freeze_time_ms as u64 > current_ms {
return;
}
if let Some(window_handle) = self.get_hovered_window(x, y) {
let wm = self.wm.borrow();
if let Some(window) = wm.windows.get(&window_handle) {
let surf = window.toplevel.wl_surface().clone();
let point = Point::<f64, Logical>::from((
(x as i32 - window.pos_x) as f64,
(y as i32 - window.pos_y) as f64,
));
manager.seat_pointer.motion(
&mut manager.state,
Some((surf, Point::from((0.0, 0.0)))),
&input::pointer::MotionEvent {
serial: manager.serial_counter.next_serial(),
time: 0,
location: point,
},
);
manager.seat_pointer.frame(&mut manager.state);
}
}
}
fn get_mouse_index_number(index: super::MouseIndex) -> u32 {
match index {
super::MouseIndex::Left => 0x110, /* BTN_LEFT */
super::MouseIndex::Center => 0x112, /* BTN_MIDDLE */
super::MouseIndex::Right => 0x111, /* BTN_RIGHT */
}
}
pub fn send_mouse_down(&mut self, manager: &mut WayVRCompositor, index: super::MouseIndex) {
// Change keyboard focus to pressed window
let loc = manager.seat_pointer.current_location();
self.last_pressed_time_ms = time::get_millis();
if let Some(window_handle) =
self.get_hovered_window(loc.x.max(0.0) as u32, loc.y.max(0.0) as u32)
{
let wm = self.wm.borrow();
if let Some(window) = wm.windows.get(&window_handle) {
let surf = window.toplevel.wl_surface().clone();
if manager.seat_keyboard.current_focus().is_none() {
manager.seat_keyboard.set_focus(
&mut manager.state,
Some(surf),
manager.serial_counter.next_serial(),
);
}
}
}
manager.seat_pointer.button(
&mut manager.state,
&input::pointer::ButtonEvent {
button: Self::get_mouse_index_number(index),
serial: manager.serial_counter.next_serial(),
time: 0,
state: smithay::backend::input::ButtonState::Pressed,
},
);
manager.seat_pointer.frame(&mut manager.state);
}
pub fn send_mouse_up(&self, manager: &mut WayVRCompositor, index: super::MouseIndex) {
manager.seat_pointer.button(
&mut manager.state,
&input::pointer::ButtonEvent {
button: Self::get_mouse_index_number(index),
serial: manager.serial_counter.next_serial(),
time: 0,
state: smithay::backend::input::ButtonState::Released,
},
);
manager.seat_pointer.frame(&mut manager.state);
}
pub fn send_mouse_scroll(&self, manager: &mut WayVRCompositor, delta: f32) {
manager.seat_pointer.axis(
&mut manager.state,
input::pointer::AxisFrame {
source: None,
relative_direction: (
smithay::backend::input::AxisRelativeDirection::Identical,
smithay::backend::input::AxisRelativeDirection::Identical,
),
time: 0,
axis: (0.0, -delta as f64),
v120: Some((0, (delta * -120.0) as i32)),
stop: (false, false),
},
);
manager.seat_pointer.frame(&mut manager.state);
}
fn configure_env(&self, cmd: &mut std::process::Command, auth_key: &str) {
cmd.env_remove("DISPLAY"); // Goodbye X11
cmd.env("WAYLAND_DISPLAY", self.wayland_env.display_num_string());
cmd.env("WAYVR_DISPLAY_AUTH", auth_key);
}
pub fn spawn_process(
&mut self,
exec_path: &str,
args: &[&str],
env: &[(&str, &str)],
) -> anyhow::Result<SpawnProcessResult> {
log::info!("Spawning subprocess with exec path \"{}\"", exec_path);
let auth_key = generate_auth_key();
let mut cmd = std::process::Command::new(exec_path);
self.configure_env(&mut cmd, auth_key.as_str());
cmd.args(args);
for e in env {
cmd.env(e.0, e.1);
}
match cmd.spawn() {
Ok(child) => Ok(SpawnProcessResult { auth_key, child }),
Err(e) => {
anyhow::bail!(
"Failed to launch process with path \"{}\": {}. Make sure your exec path exists.",
exec_path,
e
);
}
}
}
}
gen_id!(DisplayVec, Display, DisplayCell, DisplayHandle);
impl DisplayHandle {
pub fn from_packet(handle: packet_server::WvrDisplayHandle) -> Self {
Self {
generation: handle.generation,
idx: handle.idx,
}
}
pub fn as_packet(&self) -> packet_server::WvrDisplayHandle {
packet_server::WvrDisplayHandle {
idx: self.idx,
generation: self.generation,
}
}
}
+258
View File
@@ -0,0 +1,258 @@
use super::egl_ex;
use anyhow::anyhow;
#[derive(Debug)]
pub struct EGLData {
pub egl: khronos_egl::Instance<khronos_egl::Static>,
pub display: khronos_egl::Display,
pub config: khronos_egl::Config,
pub context: khronos_egl::Context,
}
#[macro_export]
macro_rules! bind_egl_function {
($func_type:ident, $func:expr) => {
std::mem::transmute_copy::<_, $func_type>($func).unwrap()
};
}
#[derive(Debug, Clone)]
pub struct DMAbufModifierInfo {
pub modifiers: Vec<u64>,
pub fourcc: u32,
}
#[derive(Debug, Clone)]
pub struct DMAbufData {
pub fd: i32,
pub stride: i32,
pub offset: i32,
pub mod_info: DMAbufModifierInfo,
}
impl EGLData {
pub fn load_func(&self, func_name: &str) -> anyhow::Result<extern "system" fn()> {
let raw_fn = self.egl.get_proc_address(func_name).ok_or(anyhow::anyhow!(
"Required EGL function {} not found",
func_name
))?;
Ok(raw_fn)
}
pub fn new() -> anyhow::Result<EGLData> {
unsafe {
let egl = khronos_egl::Instance::new(khronos_egl::Static);
let display = egl
.get_display(khronos_egl::DEFAULT_DISPLAY)
.ok_or(anyhow!(
"eglGetDisplay failed. This shouldn't happen unless you don't have any display manager running. Cannot continue, check your EGL installation."
))?;
let (major, minor) = egl.initialize(display)?;
log::debug!("EGL version: {}.{}", major, minor);
let attrib_list = [
khronos_egl::RED_SIZE,
8,
khronos_egl::GREEN_SIZE,
8,
khronos_egl::BLUE_SIZE,
8,
khronos_egl::SURFACE_TYPE,
khronos_egl::WINDOW_BIT,
khronos_egl::RENDERABLE_TYPE,
khronos_egl::OPENGL_BIT,
khronos_egl::NONE,
];
let config = egl
.choose_first_config(display, &attrib_list)?
.ok_or(anyhow!("Failed to get EGL config"))?;
egl.bind_api(khronos_egl::OPENGL_ES_API)?;
log::debug!("eglCreateContext");
// Require OpenGL ES 3.0
let context_attrib_list = [
khronos_egl::CONTEXT_MAJOR_VERSION,
3,
khronos_egl::CONTEXT_MINOR_VERSION,
0,
khronos_egl::NONE,
];
let context = egl.create_context(display, config, None, &context_attrib_list)?;
log::debug!("eglMakeCurrent");
egl.make_current(display, None, None, Some(context))?;
Ok(EGLData {
egl,
display,
config,
context,
})
}
}
fn query_dmabuf_mod_info(&self) -> anyhow::Result<DMAbufModifierInfo> {
let target_fourcc = 0x34324258; //XB24
unsafe {
use egl_ex::PFNEGLQUERYDMABUFFORMATSEXTPROC;
use egl_ex::PFNEGLQUERYDMABUFMODIFIERSEXTPROC;
let egl_query_dmabuf_formats_ext = bind_egl_function!(
PFNEGLQUERYDMABUFFORMATSEXTPROC,
&self.load_func("eglQueryDmaBufFormatsEXT")?
);
// Query format count
let mut num_formats: khronos_egl::Int = 0;
egl_query_dmabuf_formats_ext(
self.display.as_ptr(),
0,
std::ptr::null_mut(),
&mut num_formats,
);
// Retrieve formt list
let mut formats: Vec<i32> = vec![0; num_formats as usize];
egl_query_dmabuf_formats_ext(
self.display.as_ptr(),
num_formats,
formats.as_mut_ptr(),
&mut num_formats,
);
/*for (idx, format) in formats.iter().enumerate() {
let bytes = format.to_le_bytes();
log::trace!(
"idx {}, format {}{}{}{} (hex {:#x})",
idx,
bytes[0] as char,
bytes[1] as char,
bytes[2] as char,
bytes[3] as char,
format
);
}*/
let egl_query_dmabuf_modifiers_ext = bind_egl_function!(
PFNEGLQUERYDMABUFMODIFIERSEXTPROC,
&self.load_func("eglQueryDmaBufModifiersEXT")?
);
let mut num_mods: khronos_egl::Int = 0;
// Query modifier count
egl_query_dmabuf_modifiers_ext(
self.display.as_ptr(),
target_fourcc,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut num_mods,
);
if num_mods == 0 {
anyhow::bail!("eglQueryDmaBufModifiersEXT modifier count is zero");
}
let mut mods: Vec<u64> = vec![0; num_mods as usize];
egl_query_dmabuf_modifiers_ext(
self.display.as_ptr(),
target_fourcc,
num_mods,
mods.as_mut_ptr(),
std::ptr::null_mut(),
&mut num_mods,
);
if mods[0] == 0xFFFFFFFFFFFFFFFF {
anyhow::bail!("modifier is -1")
}
log::trace!("Modifier list:");
for modifier in &mods {
log::trace!("{:#x}", modifier);
}
// We should not change these modifier values. Passing all of them to the Vulkan dmabuf
// texture system causes significant graphical corruption due to invalid memory layout and
// tiling on this specific GPU model (very probably others also have the same issue).
// It is not guaranteed that this modifier will be present in other models.
// If not, the full list of modifiers will be passed. Further testing is required.
// For now, it looks like only NAVI32-based gpus have this problem.
let mod_whitelist: [u64; 2] = [
0x20000002086bf04, /* AMD RX 7800 XT, Navi32 */
0x20000001866bf04, /* AMD RX 7600 XT, Navi33 */
];
for modifier in &mod_whitelist {
if mods.contains(modifier) {
log::warn!("Using whitelisted dmabuf tiling modifier: {:#x}", modifier);
mods = vec![*modifier, 0x0 /* also important (???) */];
break;
}
}
Ok(DMAbufModifierInfo {
modifiers: mods,
fourcc: target_fourcc as u32,
})
}
}
pub fn create_dmabuf_data(&self, egl_image: &khronos_egl::Image) -> anyhow::Result<DMAbufData> {
use egl_ex::PFNEGLEXPORTDMABUFIMAGEMESAPROC as FUNC;
unsafe {
let egl_export_dmabuf_image_mesa =
bind_egl_function!(FUNC, &self.load_func("eglExportDMABUFImageMESA")?);
let mut fds: [i32; 3] = [0; 3];
let mut strides: [i32; 3] = [0; 3];
let mut offsets: [i32; 3] = [0; 3];
if egl_export_dmabuf_image_mesa(
self.display.as_ptr(),
egl_image.as_ptr(),
fds.as_mut_ptr(),
strides.as_mut_ptr(),
offsets.as_mut_ptr(),
) != khronos_egl::TRUE
{
anyhow::bail!("eglExportDMABUFImageMESA failed");
}
// many planes in RGB data?
debug_assert!(fds[1] == 0);
debug_assert!(strides[1] == 0);
debug_assert!(offsets[1] == 0);
let mod_info = self.query_dmabuf_mod_info()?;
Ok(DMAbufData {
fd: fds[0],
stride: strides[0],
offset: offsets[0],
mod_info,
})
}
}
pub fn create_egl_image(&self, gl_tex_id: u32) -> anyhow::Result<khronos_egl::Image> {
unsafe {
Ok(self.egl.create_image(
self.display,
self.context,
khronos_egl::GL_TEXTURE_2D as std::ffi::c_uint,
khronos_egl::ClientBuffer::from_ptr(gl_tex_id as *mut std::ffi::c_void),
&[khronos_egl::ATTRIB_NONE],
)?)
}
}
}
+34
View File
@@ -0,0 +1,34 @@
#![allow(clippy::all)]
//eglExportDMABUFImageMESA
pub type PFNEGLEXPORTDMABUFIMAGEMESAPROC = Option<
unsafe extern "C" fn(
dpy: khronos_egl::EGLDisplay,
image: khronos_egl::EGLImage,
fds: *mut i32,
strides: *mut khronos_egl::Int,
offsets: *mut khronos_egl::Int,
) -> khronos_egl::Boolean,
>;
//eglQueryDmaBufModifiersEXT
pub type PFNEGLQUERYDMABUFMODIFIERSEXTPROC = Option<
unsafe extern "C" fn(
dpy: khronos_egl::EGLDisplay,
format: khronos_egl::Int,
max_modifiers: khronos_egl::Int,
modifiers: *mut u64,
external_only: *mut khronos_egl::Boolean,
num_modifiers: *mut khronos_egl::Int,
) -> khronos_egl::Boolean,
>;
//eglQueryDmaBufFormatsEXT
pub type PFNEGLQUERYDMABUFFORMATSEXTPROC = Option<
unsafe extern "C" fn(
dpy: khronos_egl::EGLDisplay,
max_formats: khronos_egl::Int,
formats: *mut khronos_egl::Int,
num_formats: *mut khronos_egl::Int,
) -> khronos_egl::Boolean,
>;
+33
View File
@@ -0,0 +1,33 @@
#![allow(dead_code)]
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
#[derive(Debug)]
struct Data<DataType> {
queue: VecDeque<DataType>,
}
#[derive(Debug, Clone)]
pub struct SyncEventQueue<DataType> {
data: Rc<RefCell<Data<DataType>>>,
}
impl<DataType> SyncEventQueue<DataType> {
pub fn new() -> Self {
Self {
data: Rc::new(RefCell::new(Data {
queue: Default::default(),
})),
}
}
pub fn send(&self, message: DataType) {
let mut data = self.data.borrow_mut();
data.queue.push_back(message);
}
pub fn read(&self) -> Option<DataType> {
let mut data = self.data.borrow_mut();
data.queue.pop_front()
}
}
+175
View File
@@ -0,0 +1,175 @@
#[macro_export]
macro_rules! gen_id {
(
$container_name:ident,
$instance_name:ident,
$cell_name:ident,
$handle_name:ident) => {
//ThingCell
#[derive(Debug)]
pub struct $cell_name {
pub obj: $instance_name,
pub generation: u64,
}
//ThingVec
#[derive(Debug)]
pub struct $container_name {
// Vec<Option<ThingCell>>
pub vec: Vec<Option<$cell_name>>,
cur_generation: u64,
}
//ThingHandle
#[derive(Default, Debug, Clone, Copy, PartialEq, Hash, Eq)]
pub struct $handle_name {
idx: u32,
generation: u64,
}
#[allow(dead_code)]
impl $handle_name {
pub fn reset(&mut self) {
self.generation = 0;
}
pub fn is_set(&self) -> bool {
self.generation > 0
}
pub fn id(&self) -> u32 {
self.idx
}
pub fn new(idx: u32, generation: u64) -> Self {
Self { idx, generation }
}
}
//ThingVec
#[allow(dead_code)]
impl $container_name {
pub fn new() -> Self {
Self {
vec: Vec::new(),
cur_generation: 0,
}
}
pub fn iter(&self, callback: &dyn Fn($handle_name, &$instance_name)) {
for (idx, opt_cell) in self.vec.iter().enumerate() {
if let Some(cell) = opt_cell {
let handle = $container_name::get_handle(&cell, idx);
callback(handle, &cell.obj);
}
}
}
pub fn iter_mut(
&mut self,
callback: &mut dyn FnMut($handle_name, &mut $instance_name),
) {
for (idx, opt_cell) in self.vec.iter_mut().enumerate() {
if let Some(cell) = opt_cell {
let handle = $container_name::get_handle(&cell, idx);
callback(handle, &mut cell.obj);
}
}
}
pub fn get_handle(cell: &$cell_name, idx: usize) -> $handle_name {
$handle_name {
idx: idx as u32,
generation: cell.generation,
}
}
fn find_unused_idx(&mut self) -> Option<u32> {
for (num, obj) in self.vec.iter().enumerate() {
if obj.is_none() {
return Some(num as u32);
}
}
None
}
pub fn add(&mut self, obj: $instance_name) -> $handle_name {
self.cur_generation += 1;
let generation = self.cur_generation;
let unused_idx = self.find_unused_idx();
let idx = if let Some(idx) = unused_idx {
idx
} else {
self.vec.len() as u32
};
let handle = $handle_name { idx, generation };
let cell = $cell_name { obj, generation };
if let Some(idx) = unused_idx {
self.vec[idx as usize] = Some(cell);
} else {
self.vec.push(Some(cell))
}
handle
}
pub fn remove(&mut self, handle: &$handle_name) {
// Out of bounds, ignore
if handle.idx as usize >= self.vec.len() {
return;
}
// Remove only if the generation matches
if let Some(cell) = &self.vec[handle.idx as usize] {
if cell.generation == handle.generation {
self.vec[handle.idx as usize] = None;
}
}
}
pub fn get(&self, handle: &$handle_name) -> Option<&$instance_name> {
// Out of bounds, ignore
if handle.idx as usize >= self.vec.len() {
return None;
}
if let Some(cell) = &self.vec[handle.idx as usize] {
if cell.generation == handle.generation {
return Some(&cell.obj);
}
}
None
}
pub fn get_mut(&mut self, handle: &$handle_name) -> Option<&mut $instance_name> {
// Out of bounds, ignore
if handle.idx as usize >= self.vec.len() {
return None;
}
if let Some(cell) = &mut self.vec[handle.idx as usize] {
if cell.generation == handle.generation {
return Some(&mut cell.obj);
}
}
None
}
}
};
}
/* Example usage:
gen_id!(ThingVec, ThingInstance, ThingCell, ThingHandle);
struct ThingInstance {}
impl ThingInstance {}
*/
+658
View File
@@ -0,0 +1,658 @@
pub mod client;
mod comp;
pub mod display;
pub mod egl_data;
mod egl_ex;
pub mod event_queue;
mod handle;
mod process;
pub mod server_ipc;
mod smithay_wrapper;
mod time;
mod window;
use comp::Application;
use display::{DisplayInitParams, DisplayVec};
use event_queue::SyncEventQueue;
use process::ProcessVec;
use server_ipc::WayVRServer;
use smallvec::SmallVec;
use smithay::{
backend::{
egl,
renderer::{gles::GlesRenderer, ImportDma},
},
input::SeatState,
output::{Mode, Output},
reexports::wayland_server::{self, backend::ClientId},
wayland::{
compositor,
dmabuf::{DmabufFeedbackBuilder, DmabufState},
selection::data_device::DataDeviceState,
shell::xdg::{ToplevelSurface, XdgShellState},
shm::ShmState,
},
};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
rc::Rc,
};
use time::get_millis;
use wayvr_ipc::{packet_client, packet_server};
const STR_INVALID_HANDLE_DISP: &str = "Invalid display handle";
const STR_INVALID_HANDLE_PROCESS: &str = "Invalid process handle";
#[derive(Debug, Clone)]
pub struct WaylandEnv {
pub display_num: u32,
}
impl WaylandEnv {
pub fn display_num_string(&self) -> String {
// e.g. "wayland-20"
format!("wayland-{}", self.display_num)
}
}
#[derive(Clone)]
pub struct ProcessWayVREnv {
pub display_auth: Option<String>,
pub display_name: Option<String>, // Externally spawned process by a user script
}
#[derive(Clone)]
pub struct ExternalProcessRequest {
pub env: ProcessWayVREnv,
pub client: wayland_server::Client,
pub pid: u32,
}
#[derive(Clone)]
pub enum WayVRTask {
NewToplevel(ClientId, ToplevelSurface),
NewExternalProcess(ExternalProcessRequest),
DropOverlay(super::overlay::OverlayID),
ProcessTerminationRequest(process::ProcessHandle),
Haptics(super::input::Haptics),
}
#[derive(Clone)]
pub enum WayVRSignal {
DisplayVisibility(display::DisplayHandle, bool),
DisplayWindowLayout(
display::DisplayHandle,
packet_server::WvrDisplayWindowLayout,
),
BroadcastStateChanged(packet_server::WvrStateChanged),
}
pub struct Config {
pub click_freeze_time_ms: u32,
pub keyboard_repeat_delay_ms: u32,
pub keyboard_repeat_rate: u32,
pub auto_hide_delay: Option<u32>, // if None, auto-hide is disabled
}
pub struct WayVRState {
time_start: u64,
pub displays: display::DisplayVec,
pub manager: client::WayVRCompositor,
wm: Rc<RefCell<window::WindowManager>>,
egl_data: Rc<egl_data::EGLData>,
pub processes: process::ProcessVec,
config: Config,
dashboard_display: Option<display::DisplayHandle>,
pub tasks: SyncEventQueue<WayVRTask>,
pub signals: SyncEventQueue<WayVRSignal>,
ticks: u64,
pub pending_haptic: Option<super::input::Haptics>,
}
pub struct WayVR {
pub state: WayVRState,
pub ipc_server: WayVRServer,
}
pub enum MouseIndex {
Left,
Center,
Right,
}
pub enum TickTask {
NewExternalProcess(ExternalProcessRequest), // Call WayVRCompositor::add_client after receiving this message
NewDisplay(
packet_client::WvrDisplayCreateParams,
Option<display::DisplayHandle>, /* existing handle? */
),
DropOverlay(super::overlay::OverlayID),
}
impl WayVR {
pub fn new(config: Config) -> anyhow::Result<Self> {
log::info!("Initializing WayVR");
let display: wayland_server::Display<Application> = wayland_server::Display::new()?;
let dh = display.handle();
let compositor = compositor::CompositorState::new::<Application>(&dh);
let xdg_shell = XdgShellState::new::<Application>(&dh);
let mut seat_state = SeatState::new();
let shm = ShmState::new::<Application>(&dh, Vec::new());
let data_device = DataDeviceState::new::<Application>(&dh);
let mut seat = seat_state.new_wl_seat(&dh, "wayvr");
let dummy_width = 1280;
let dummy_height = 720;
let dummy_milli_hz = 60000; /* refresh rate in millihertz */
let output = Output::new(
String::from("wayvr_display"),
smithay::output::PhysicalProperties {
size: (dummy_width, dummy_height).into(),
subpixel: smithay::output::Subpixel::None,
make: String::from("Completely Legit"),
model: String::from("Virtual WayVR Display"),
},
);
let mode = Mode {
refresh: dummy_milli_hz,
size: (dummy_width, dummy_height).into(),
};
let _global = output.create_global::<Application>(&dh);
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
let egl_data = egl_data::EGLData::new()?;
let smithay_display = smithay_wrapper::get_egl_display(&egl_data)?;
let smithay_context = smithay_wrapper::get_egl_context(&egl_data, &smithay_display)?;
let render_node = egl::EGLDevice::device_for_display(&smithay_display)
.and_then(|device| device.try_get_render_node());
let gles_renderer = unsafe { GlesRenderer::new(smithay_context)? };
let dmabuf_default_feedback = match render_node {
Ok(Some(node)) => {
let dmabuf_formats = gles_renderer.dmabuf_formats();
let dmabuf_default_feedback =
DmabufFeedbackBuilder::new(node.dev_id(), dmabuf_formats)
.build()
.unwrap();
Some(dmabuf_default_feedback)
}
Ok(None) => {
log::warn!("dmabuf: Failed to query render node");
None
}
Err(err) => {
log::warn!("dmabuf: Failed to get egl device for display: {}", err);
None
}
};
let dmabuf_state = if let Some(default_feedback) = dmabuf_default_feedback {
let mut dmabuf_state = DmabufState::new();
let dmabuf_global = dmabuf_state.create_global_with_default_feedback::<Application>(
&display.handle(),
&default_feedback,
);
(dmabuf_state, dmabuf_global, Some(default_feedback))
} else {
let dmabuf_formats = gles_renderer.dmabuf_formats();
let mut dmabuf_state = DmabufState::new();
let dmabuf_global =
dmabuf_state.create_global::<Application>(&display.handle(), dmabuf_formats);
(dmabuf_state, dmabuf_global, None)
};
let seat_keyboard = seat.add_keyboard(
Default::default(),
config.keyboard_repeat_delay_ms as i32,
config.keyboard_repeat_rate as i32,
)?;
let seat_pointer = seat.add_pointer();
let tasks = SyncEventQueue::new();
let state = Application {
compositor,
xdg_shell,
seat_state,
shm,
data_device,
wayvr_tasks: tasks.clone(),
redraw_requests: HashSet::new(),
dmabuf_state,
gles_renderer,
};
let time_start = get_millis();
let ipc_server = WayVRServer::new()?;
let state = WayVRState {
time_start,
manager: client::WayVRCompositor::new(state, display, seat_keyboard, seat_pointer)?,
displays: DisplayVec::new(),
processes: ProcessVec::new(),
egl_data: Rc::new(egl_data),
wm: Rc::new(RefCell::new(window::WindowManager::new())),
config,
dashboard_display: None,
ticks: 0,
tasks,
pending_haptic: None,
signals: SyncEventQueue::new(),
};
Ok(Self { state, ipc_server })
}
pub fn tick_display(&mut self, display: display::DisplayHandle) -> anyhow::Result<()> {
// millis since the start of wayvr
let display = self
.state
.displays
.get_mut(&display)
.ok_or(anyhow::anyhow!(STR_INVALID_HANDLE_DISP))?;
if !display.wants_redraw {
// Nothing changed, do not render
return Ok(());
}
if !display.visible {
// Display is invisible, do not render
return Ok(());
}
let time_ms = get_millis() - self.state.time_start;
display.tick_render(&mut self.state.manager.state.gles_renderer, time_ms)?;
display.wants_redraw = false;
Ok(())
}
pub fn tick_events(&mut self) -> anyhow::Result<Vec<TickTask>> {
let mut tasks: Vec<TickTask> = Vec::new();
self.ipc_server.tick(&mut server_ipc::TickParams {
state: &mut self.state,
tasks: &mut tasks,
})?;
// Check for redraw events
self.state.displays.iter_mut(&mut |_, disp| {
for disp_window in &disp.displayed_windows {
if self
.state
.manager
.state
.check_redraw(disp_window.toplevel.wl_surface())
{
disp.wants_redraw = true;
}
}
});
// Tick all child processes
let mut to_remove: SmallVec<[(process::ProcessHandle, display::DisplayHandle); 2]> =
SmallVec::new();
self.state.processes.iter_mut(&mut |handle, process| {
if !process.is_running() {
to_remove.push((handle, process.display_handle()));
}
});
for (p_handle, disp_handle) in &to_remove {
self.state.processes.remove(p_handle);
if let Some(display) = self.state.displays.get_mut(disp_handle) {
display
.tasks
.send(display::DisplayTask::ProcessCleanup(*p_handle));
display.wants_redraw = true;
}
}
self.state.displays.iter_mut(&mut |handle, display| {
display.tick(&self.state.config, &handle, &mut self.state.signals);
});
if !to_remove.is_empty() {
self.state.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::ProcessRemoved,
));
}
while let Some(task) = self.state.tasks.read() {
match task {
WayVRTask::NewExternalProcess(req) => {
tasks.push(TickTask::NewExternalProcess(req));
}
WayVRTask::DropOverlay(overlay_id) => {
tasks.push(TickTask::DropOverlay(overlay_id));
}
WayVRTask::NewToplevel(client_id, toplevel) => {
// Attach newly created toplevel surfaces to displays
for client in &self.state.manager.clients {
if client.client.id() == client_id {
if let Some(process_handle) =
process::find_by_pid(&self.state.processes, client.pid)
{
let window_handle = self
.state
.wm
.borrow_mut()
.create_window(client.display_handle, &toplevel);
if let Some(display) =
self.state.displays.get_mut(&client.display_handle)
{
display.add_window(window_handle, process_handle, &toplevel);
self.state.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::WindowCreated,
));
} else {
// This shouldn't happen, scream if it does
log::error!("Could not attach window handle into display");
}
} else {
log::error!(
"WayVR window creation failed: Unexpected process ID {}. It wasn't registered before.",
client.pid
);
}
break;
}
}
}
WayVRTask::ProcessTerminationRequest(process_handle) => {
if let Some(process) = self.state.processes.get_mut(&process_handle) {
process.terminate();
}
}
WayVRTask::Haptics(haptics) => {
self.state.pending_haptic = Some(haptics);
}
}
}
self.state
.manager
.tick_wayland(&mut self.state.displays, &mut self.state.processes)?;
if self.state.ticks % 200 == 0 {
self.state.manager.cleanup_clients();
}
self.state.ticks += 1;
Ok(tasks)
}
pub fn tick_finish(&mut self) -> anyhow::Result<()> {
self.state
.manager
.state
.gles_renderer
.with_context(|gl| unsafe {
gl.Flush();
gl.Finish();
})?;
Ok(())
}
pub fn get_primary_display(displays: &DisplayVec) -> Option<display::DisplayHandle> {
for (idx, cell) in displays.vec.iter().enumerate() {
if let Some(cell) = cell {
if cell.obj.primary {
return Some(DisplayVec::get_handle(cell, idx));
}
}
}
None
}
pub fn get_display_by_name(
displays: &DisplayVec,
name: &str,
) -> Option<display::DisplayHandle> {
for (idx, cell) in displays.vec.iter().enumerate() {
if let Some(cell) = cell {
if cell.obj.name == name {
return Some(DisplayVec::get_handle(cell, idx));
}
}
}
None
}
pub fn terminate_process(&mut self, process_handle: process::ProcessHandle) {
self.state
.tasks
.send(WayVRTask::ProcessTerminationRequest(process_handle));
}
}
impl WayVRState {
pub fn send_mouse_move(&mut self, display: display::DisplayHandle, x: u32, y: u32) {
if let Some(display) = self.displays.get(&display) {
display.send_mouse_move(&self.config, &mut self.manager, x, y);
}
}
pub fn send_mouse_down(&mut self, display: display::DisplayHandle, index: MouseIndex) {
if let Some(display) = self.displays.get_mut(&display) {
display.send_mouse_down(&mut self.manager, index);
}
}
pub fn send_mouse_up(&mut self, display: display::DisplayHandle, index: MouseIndex) {
if let Some(display) = self.displays.get(&display) {
display.send_mouse_up(&mut self.manager, index);
}
}
pub fn send_mouse_scroll(&mut self, display: display::DisplayHandle, delta: f32) {
if let Some(display) = self.displays.get(&display) {
display.send_mouse_scroll(&mut self.manager, delta);
}
}
pub fn send_key(&mut self, virtual_key: u32, down: bool) {
self.manager.send_key(virtual_key, down);
}
pub fn set_display_visible(&mut self, display: display::DisplayHandle, visible: bool) {
if let Some(display) = self.displays.get_mut(&display) {
display.set_visible(visible);
}
}
pub fn set_display_layout(
&mut self,
display: display::DisplayHandle,
layout: packet_server::WvrDisplayWindowLayout,
) {
if let Some(display) = self.displays.get_mut(&display) {
display.set_layout(layout);
}
}
pub fn get_dmabuf_data(&self, display: display::DisplayHandle) -> Option<egl_data::DMAbufData> {
self.displays
.get(&display)
.map(|display| display.dmabuf_data.clone())
}
pub fn create_display(
&mut self,
width: u16,
height: u16,
name: &str,
primary: bool,
) -> anyhow::Result<display::DisplayHandle> {
let display = display::Display::new(DisplayInitParams {
wm: self.wm.clone(),
egl_data: self.egl_data.clone(),
renderer: &mut self.manager.state.gles_renderer,
wayland_env: self.manager.wayland_env.clone(),
width,
height,
name,
primary,
})?;
let handle = self.displays.add(display);
self.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::DisplayCreated,
));
Ok(handle)
}
pub fn destroy_display(&mut self, handle: display::DisplayHandle) -> anyhow::Result<()> {
let Some(display) = self.displays.get(&handle) else {
anyhow::bail!("Display not found");
};
if let Some(overlay_id) = display.overlay_id {
self.tasks.send(WayVRTask::DropOverlay(overlay_id));
} else {
log::warn!("Destroying display without OverlayID set"); // This shouldn't happen, but log it anyways.
}
let mut process_names = Vec::<String>::new();
self.processes.iter_mut(&mut |_, process| {
if process.display_handle() == handle {
process_names.push(process.get_name());
}
});
if !display.displayed_windows.is_empty() || !process_names.is_empty() {
anyhow::bail!(
"Display is not empty. Attached processes: {}",
process_names.join(", ")
);
}
self.manager.cleanup_clients();
for client in self.manager.clients.iter() {
if client.display_handle == handle {
// This shouldn't happen, but make sure we are all set to destroy this display
anyhow::bail!("Wayland client still exists");
}
}
self.displays.remove(&handle);
self.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::DisplayRemoved,
));
Ok(())
}
pub fn get_or_create_dashboard_display(
&mut self,
width: u16,
height: u16,
name: &str,
) -> anyhow::Result<(bool /* newly created? */, display::DisplayHandle)> {
if let Some(handle) = &self.dashboard_display {
// ensure it still exists
if self.displays.get(handle).is_some() {
return Ok((false, *handle));
}
}
let new_disp = self.create_display(width, height, name, false)?;
self.dashboard_display = Some(new_disp);
Ok((true, new_disp))
}
// Check if process with given arguments already exists
pub fn process_query(
&self,
display_handle: display::DisplayHandle,
exec_path: &str,
args: &[&str],
_env: &[(&str, &str)],
) -> Option<process::ProcessHandle> {
for (idx, cell) in self.processes.vec.iter().enumerate() {
if let Some(cell) = &cell {
if let process::Process::Managed(process) = &cell.obj {
if process.display_handle != display_handle
|| process.exec_path != exec_path
|| process.args != args
{
continue;
}
return Some(process::ProcessVec::get_handle(cell, idx));
}
}
}
None
}
pub fn add_external_process(
&mut self,
display_handle: display::DisplayHandle,
pid: u32,
) -> process::ProcessHandle {
self.processes
.add(process::Process::External(process::ExternalProcess {
display_handle,
pid,
}))
}
pub fn spawn_process(
&mut self,
display_handle: display::DisplayHandle,
exec_path: &str,
args: &[&str],
env: &[(&str, &str)],
userdata: HashMap<String, String>,
) -> anyhow::Result<process::ProcessHandle> {
let display = self
.displays
.get_mut(&display_handle)
.ok_or(anyhow::anyhow!(STR_INVALID_HANDLE_DISP))?;
let res = display.spawn_process(exec_path, args, env)?;
let handle = self
.processes
.add(process::Process::Managed(process::WayVRProcess {
auth_key: res.auth_key,
child: res.child,
display_handle,
exec_path: String::from(exec_path),
userdata,
args: args.iter().map(|x| String::from(*x)).collect(),
env: env
.iter()
.map(|(a, b)| (String::from(*a), String::from(*b)))
.collect(),
}));
self.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::ProcessCreated,
));
Ok(handle)
}
}
+189
View File
@@ -0,0 +1,189 @@
use std::collections::HashMap;
use wayvr_ipc::packet_server;
use crate::gen_id;
use super::display;
#[derive(Debug)]
pub struct WayVRProcess {
pub auth_key: String,
pub child: std::process::Child,
pub display_handle: display::DisplayHandle,
pub exec_path: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
pub userdata: HashMap<String, String>,
}
#[derive(Debug)]
pub struct ExternalProcess {
pub pid: u32,
pub display_handle: display::DisplayHandle,
}
#[derive(Debug)]
pub enum Process {
Managed(WayVRProcess), // Process spawned by WayVR
External(ExternalProcess), // External process not directly controlled by us
}
impl Process {
pub fn display_handle(&self) -> display::DisplayHandle {
match self {
Process::Managed(p) => p.display_handle,
Process::External(p) => p.display_handle,
}
}
pub fn is_running(&mut self) -> bool {
match self {
Process::Managed(p) => p.is_running(),
Process::External(p) => p.is_running(),
}
}
pub fn terminate(&mut self) {
match self {
Process::Managed(p) => p.terminate(),
Process::External(p) => p.terminate(),
}
}
pub fn get_name(&self) -> String {
match self {
Process::Managed(p) => p.get_name().unwrap_or(String::from("unknown")),
Process::External(p) => p.get_name().unwrap_or(String::from("unknown")),
}
}
pub fn to_packet(&self, handle: ProcessHandle) -> packet_server::WvrProcess {
match self {
Process::Managed(p) => packet_server::WvrProcess {
name: p.get_name().unwrap_or(String::from("unknown")),
userdata: p.userdata.clone(),
display_handle: p.display_handle.as_packet(),
handle: handle.as_packet(),
},
Process::External(p) => packet_server::WvrProcess {
name: p.get_name().unwrap_or(String::from("unknown")),
userdata: Default::default(),
display_handle: p.display_handle.as_packet(),
handle: handle.as_packet(),
},
}
}
}
impl Drop for WayVRProcess {
fn drop(&mut self) {
log::info!(
"Sending SIGTERM (graceful exit) to process {}",
self.exec_path.as_str()
);
self.terminate();
}
}
impl WayVRProcess {
fn is_running(&mut self) -> bool {
match self.child.try_wait() {
Ok(Some(_exit_status)) => false,
Ok(None) => true,
Err(e) => {
// this shouldn't happen
log::error!("Child::try_wait failed: {}", e);
false
}
}
}
fn terminate(&mut self) {
unsafe {
// Gracefully stop process
libc::kill(self.child.id() as i32, libc::SIGTERM);
}
}
pub fn get_name(&self) -> Option<String> {
get_exec_name_from_pid(self.child.id())
}
}
fn get_exec_name_from_pid(pid: u32) -> Option<String> {
let path = format!("/proc/{}/exe", pid);
match std::fs::read_link(&path) {
Ok(buf) => {
if let Some(process_name) = buf.file_name().and_then(|s| s.to_str()) {
return Some(String::from(process_name));
}
None
}
Err(_) => None,
}
}
impl ExternalProcess {
fn is_running(&self) -> bool {
if self.pid == 0 {
false
} else {
std::fs::metadata(format!("/proc/{}", self.pid)).is_ok()
}
}
fn terminate(&mut self) {
if self.pid != 0 {
unsafe {
// send SIGINT (^C)
libc::kill(self.pid as i32, libc::SIGINT);
}
}
self.pid = 0;
}
pub fn get_name(&self) -> Option<String> {
get_exec_name_from_pid(self.pid)
}
}
gen_id!(ProcessVec, Process, ProcessCell, ProcessHandle);
pub fn find_by_pid(processes: &ProcessVec, pid: u32) -> Option<ProcessHandle> {
for (idx, cell) in processes.vec.iter().enumerate() {
if let Some(cell) = cell {
match &cell.obj {
Process::Managed(wayvr_process) => {
if wayvr_process.child.id() == pid {
return Some(ProcessVec::get_handle(cell, idx));
}
}
Process::External(external_process) => {
if external_process.pid == pid {
return Some(ProcessVec::get_handle(cell, idx));
}
}
}
}
}
None
}
impl ProcessHandle {
pub fn from_packet(handle: packet_server::WvrProcessHandle) -> Self {
Self {
generation: handle.generation,
idx: handle.idx,
}
}
pub fn as_packet(&self) -> packet_server::WvrProcessHandle {
packet_server::WvrProcessHandle {
idx: self.idx,
generation: self.generation,
}
}
}
+642
View File
@@ -0,0 +1,642 @@
use super::{display, process, window, TickTask, WayVRSignal};
use bytes::BufMut;
use interprocess::local_socket::{self, traits::Listener, ToNsName};
use smallvec::SmallVec;
use std::io::{Read, Write};
use wayvr_ipc::{
ipc::{self},
packet_client::{self, PacketClient},
packet_server::{self, PacketServer},
};
pub struct AuthInfo {
pub client_name: String,
pub protocol_version: u32, // client protocol version
}
pub struct Connection {
alive: bool,
conn: local_socket::Stream,
next_packet: Option<u32>,
auth: Option<AuthInfo>,
}
pub fn send_packet(conn: &mut local_socket::Stream, data: &[u8]) -> anyhow::Result<()> {
let mut bytes = bytes::BytesMut::new();
// packet size
bytes.put_u32(data.len() as u32);
// packet data
bytes.put_slice(data);
conn.write_all(&bytes)?;
Ok(())
}
fn read_check(expected_size: u32, res: std::io::Result<usize>) -> bool {
match res {
Ok(count) => {
if count == 0 {
return false;
}
if count as u32 != expected_size {
log::error!("count {} is not {}", count, expected_size);
false
} else {
true // read succeeded
}
}
Err(_e) => {
//log::error!("failed to get packet size: {}", e);
false
}
}
}
type Payload = SmallVec<[u8; 64]>;
fn read_payload(conn: &mut local_socket::Stream, size: u32) -> Option<Payload> {
let mut payload = Payload::new();
payload.resize(size as usize, 0);
if !read_check(size, conn.read(&mut payload)) {
None
} else {
Some(payload)
}
}
pub struct TickParams<'a> {
pub state: &'a mut super::WayVRState,
pub tasks: &'a mut Vec<TickTask>,
}
pub fn gen_args_vec(input: &str) -> Vec<&str> {
input.split_whitespace().collect()
}
pub fn gen_env_vec(input: &[String]) -> Vec<(&str, &str)> {
let res = input
.iter()
.filter_map(|e| e.as_str().split_once('='))
.collect();
res
}
impl Connection {
fn new(conn: local_socket::Stream) -> Self {
Self {
conn,
alive: true,
auth: None,
next_packet: None,
}
}
fn kill(&mut self, reason: &str) {
let _dont_care = send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::Disconnect(packet_server::Disconnect {
reason: String::from(reason),
})),
);
self.alive = false;
}
fn process_handshake(&mut self, handshake: &packet_client::Handshake) -> anyhow::Result<()> {
if self.auth.is_some() {
anyhow::bail!("You were already authenticated");
}
if handshake.protocol_version != ipc::PROTOCOL_VERSION {
anyhow::bail!(
"Unsupported protocol version {}",
handshake.protocol_version
);
}
if handshake.magic != ipc::CONNECTION_MAGIC {
anyhow::bail!("Invalid magic");
}
match handshake.client_name.len() {
0 => anyhow::bail!("Client name is empty"),
1..32 => {}
_ => anyhow::bail!("Client name is too long"),
}
log::info!("IPC: Client \"{}\" connected.", handshake.client_name);
self.auth = Some(AuthInfo {
client_name: handshake.client_name.clone(),
protocol_version: handshake.protocol_version,
});
// Send auth response
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::HandshakeSuccess(
packet_server::HandshakeSuccess {
runtime: String::from("wlx-overlay-s"),
},
)),
)?;
Ok(())
}
fn handle_wvr_display_list(
&mut self,
params: &TickParams,
serial: ipc::Serial,
) -> anyhow::Result<()> {
let list: Vec<packet_server::WvrDisplay> = params
.state
.displays
.vec
.iter()
.enumerate()
.filter_map(|(idx, opt_cell)| {
let Some(cell) = opt_cell else {
return None;
};
let display = &cell.obj;
Some(display.as_packet(display::DisplayHandle::new(idx as u32, cell.generation)))
})
.collect();
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayListResponse(
serial,
packet_server::WvrDisplayList { list },
)),
)?;
Ok(())
}
fn handle_wvr_display_create(
&mut self,
params: &mut TickParams,
serial: ipc::Serial,
packet_params: packet_client::WvrDisplayCreateParams,
) -> anyhow::Result<()> {
let display_handle = params.state.create_display(
packet_params.width,
packet_params.height,
&packet_params.name,
false,
)?;
params.tasks.push(TickTask::NewDisplay(
packet_params.clone(),
Some(display_handle),
));
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayCreateResponse(
serial,
display_handle.as_packet(),
)),
)?;
Ok(())
}
fn handle_wvr_display_remove(
&mut self,
params: &mut TickParams,
serial: ipc::Serial,
handle: packet_server::WvrDisplayHandle,
) -> anyhow::Result<()> {
let res = params
.state
.destroy_display(display::DisplayHandle::from_packet(handle))
.map_err(|e| format!("{:?}", e));
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayRemoveResponse(serial, res)),
)?;
Ok(())
}
fn handle_wvr_display_set_visible(
&mut self,
params: &mut TickParams,
handle: packet_server::WvrDisplayHandle,
visible: bool,
) -> anyhow::Result<()> {
params.state.signals.send(WayVRSignal::DisplayVisibility(
display::DisplayHandle::from_packet(handle),
visible,
));
Ok(())
}
fn handle_wvr_display_set_window_layout(
&mut self,
params: &mut TickParams,
handle: packet_server::WvrDisplayHandle,
layout: packet_server::WvrDisplayWindowLayout,
) -> anyhow::Result<()> {
params.state.signals.send(WayVRSignal::DisplayWindowLayout(
display::DisplayHandle::from_packet(handle),
layout,
));
Ok(())
}
fn handle_wvr_display_window_list(
&mut self,
params: &mut TickParams,
serial: ipc::Serial,
display_handle: packet_server::WvrDisplayHandle,
) -> anyhow::Result<()> {
let mut send = |list: Option<packet_server::WvrWindowList>| -> anyhow::Result<()> {
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayWindowListResponse(serial, list)),
)
};
let Some(display) = params
.state
.displays
.get(&display::DisplayHandle::from_packet(display_handle.clone()))
else {
return send(None);
};
send(Some(packet_server::WvrWindowList {
list: display
.displayed_windows
.iter()
.filter_map(|disp_win| {
params
.state
.wm
.borrow_mut()
.windows
.get(&disp_win.window_handle)
.map(|win| packet_server::WvrWindow {
handle: window::WindowHandle::as_packet(&disp_win.window_handle),
process_handle: process::ProcessHandle::as_packet(
&disp_win.process_handle,
),
pos_x: win.pos_x,
pos_y: win.pos_y,
size_x: win.size_x,
size_y: win.size_y,
visible: win.visible,
display_handle: display_handle.clone(),
})
})
.collect::<Vec<_>>(),
}))
}
fn handle_wvr_window_set_visible(
&mut self,
params: &mut TickParams,
handle: packet_server::WvrWindowHandle,
visible: bool,
) -> anyhow::Result<()> {
let mut to_resize = None;
if let Some(window) = params
.state
.wm
.borrow_mut()
.windows
.get_mut(&window::WindowHandle::from_packet(handle))
{
window.visible = visible;
to_resize = Some(window.display_handle);
}
if let Some(to_resize) = to_resize {
if let Some(display) = params.state.displays.get_mut(&to_resize) {
display.reposition_windows();
display.trigger_rerender();
}
}
Ok(())
}
fn handle_wvr_process_launch(
&mut self,
params: &mut TickParams,
serial: ipc::Serial,
packet_params: packet_client::WvrProcessLaunchParams,
) -> anyhow::Result<()> {
let args_vec = gen_args_vec(&packet_params.args);
let env_vec = gen_env_vec(&packet_params.env);
let res = params.state.spawn_process(
super::display::DisplayHandle::from_packet(packet_params.target_display),
&packet_params.exec,
&args_vec,
&env_vec,
packet_params.userdata,
);
let res = res.map(|r| r.as_packet()).map_err(|e| e.to_string());
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrProcessLaunchResponse(serial, res)),
)?;
Ok(())
}
fn handle_wvr_display_get(
&mut self,
params: &TickParams,
serial: ipc::Serial,
display_handle: packet_server::WvrDisplayHandle,
) -> anyhow::Result<()> {
let native_handle = &display::DisplayHandle::from_packet(display_handle.clone());
let disp = params
.state
.displays
.get(native_handle)
.map(|disp| disp.as_packet(*native_handle));
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayGetResponse(serial, disp)),
)?;
Ok(())
}
fn handle_wvr_process_list(
&mut self,
params: &TickParams,
serial: ipc::Serial,
) -> anyhow::Result<()> {
let list: Vec<packet_server::WvrProcess> = params
.state
.processes
.vec
.iter()
.enumerate()
.filter_map(|(idx, opt_cell)| {
let Some(cell) = opt_cell else {
return None;
};
let process = &cell.obj;
Some(process.to_packet(process::ProcessHandle::new(idx as u32, cell.generation)))
})
.collect();
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrProcessListResponse(
serial,
packet_server::WvrProcessList { list },
)),
)?;
Ok(())
}
// This request doesn't return anything to the client
fn handle_wvr_process_terminate(
&mut self,
params: &mut TickParams,
process_handle: packet_server::WvrProcessHandle,
) -> anyhow::Result<()> {
let native_handle = &process::ProcessHandle::from_packet(process_handle.clone());
let process = params.state.processes.get_mut(native_handle);
let Some(process) = process else {
return Ok(());
};
process.terminate();
Ok(())
}
fn handle_wvr_process_get(
&mut self,
params: &TickParams,
serial: ipc::Serial,
process_handle: packet_server::WvrProcessHandle,
) -> anyhow::Result<()> {
let native_handle = &process::ProcessHandle::from_packet(process_handle.clone());
let process = params
.state
.processes
.get(native_handle)
.map(|process| process.to_packet(*native_handle));
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrProcessGetResponse(serial, process)),
)?;
Ok(())
}
fn handle_wlx_haptics(
&mut self,
params: &mut TickParams,
haptics_params: packet_client::WlxHapticsParams,
) -> anyhow::Result<()> {
params
.state
.tasks
.send(super::WayVRTask::Haptics(crate::backend::input::Haptics {
duration: haptics_params.duration,
frequency: haptics_params.frequency,
intensity: haptics_params.intensity,
}));
Ok(())
}
fn process_payload(&mut self, params: &mut TickParams, payload: Payload) -> anyhow::Result<()> {
let packet: PacketClient = ipc::data_decode(&payload)?;
if let PacketClient::Handshake(handshake) = &packet {
self.process_handshake(handshake)?;
return Ok(());
}
match packet {
PacketClient::Handshake(_) => unreachable!(), // handled previously
PacketClient::WvrDisplayList(serial) => {
self.handle_wvr_display_list(params, serial)?;
}
PacketClient::WvrDisplayGet(serial, display_handle) => {
self.handle_wvr_display_get(params, serial, display_handle)?;
}
PacketClient::WvrDisplayRemove(serial, display_handle) => {
self.handle_wvr_display_remove(params, serial, display_handle)?;
}
PacketClient::WvrDisplaySetVisible(display_handle, visible) => {
self.handle_wvr_display_set_visible(params, display_handle, visible)?;
}
PacketClient::WvrDisplaySetWindowLayout(display_handle, layout) => {
self.handle_wvr_display_set_window_layout(params, display_handle, layout)?;
}
PacketClient::WvrDisplayWindowList(serial, display_handle) => {
self.handle_wvr_display_window_list(params, serial, display_handle)?;
}
PacketClient::WvrWindowSetVisible(window_handle, visible) => {
self.handle_wvr_window_set_visible(params, window_handle, visible)?;
}
PacketClient::WvrProcessGet(serial, process_handle) => {
self.handle_wvr_process_get(params, serial, process_handle)?;
}
PacketClient::WvrProcessList(serial) => {
self.handle_wvr_process_list(params, serial)?;
}
PacketClient::WvrProcessLaunch(serial, packet_params) => {
self.handle_wvr_process_launch(params, serial, packet_params)?;
}
PacketClient::WvrDisplayCreate(serial, packet_params) => {
self.handle_wvr_display_create(params, serial, packet_params)?;
}
PacketClient::WvrProcessTerminate(process_handle) => {
self.handle_wvr_process_terminate(params, process_handle)?;
}
PacketClient::WlxHaptics(haptics_params) => {
self.handle_wlx_haptics(params, haptics_params)?;
}
}
Ok(())
}
fn process_check_payload(&mut self, params: &mut TickParams, payload: Payload) -> bool {
log::debug!("payload size {}", payload.len());
if let Err(e) = self.process_payload(params, payload) {
log::error!("Invalid payload from the client, closing connection: {}", e);
// send also error message directly to the client before disconnecting
self.kill(format!("{}", e).as_str());
false
} else {
true
}
}
fn read_packet(&mut self, params: &mut TickParams) -> bool {
if let Some(payload_size) = self.next_packet {
let Some(payload) = read_payload(&mut self.conn, payload_size) else {
// still failed to read payload, try in next tick
return false;
};
if !self.process_check_payload(params, payload) {
return false;
}
self.next_packet = None;
}
let mut buf_packet_header: [u8; 4] = [0; 4];
if !read_check(4, self.conn.read(&mut buf_packet_header)) {
return false;
}
let payload_size = u32::from_be_bytes(buf_packet_header[0..4].try_into().unwrap()); // 0-3 bytes (u32 size)
let size_limit: u32 = 128 * 1024;
if payload_size > size_limit {
// over 128 KiB?
log::error!(
"Client sent a packet header with the size over {} bytes, closing connection.",
size_limit
);
self.kill("Too big packet received (over 128 KiB)");
return false;
}
let Some(payload) = read_payload(&mut self.conn, payload_size) else {
// failed to read payload, try in next tick
self.next_packet = Some(payload_size);
return false;
};
if !self.process_check_payload(params, payload) {
return false;
}
true
}
fn tick(&mut self, params: &mut TickParams) {
while self.read_packet(params) {}
}
}
impl Drop for Connection {
fn drop(&mut self) {
log::info!("Connection closed");
}
}
pub struct WayVRServer {
listener: local_socket::Listener,
connections: Vec<Connection>,
}
impl WayVRServer {
pub fn new() -> anyhow::Result<Self> {
let printname = "/tmp/wayvr_ipc.sock";
let name = printname.to_ns_name::<local_socket::GenericNamespaced>()?;
let opts = local_socket::ListenerOptions::new()
.name(name)
.nonblocking(local_socket::ListenerNonblockingMode::Both);
let listener = match opts.create_sync() {
Ok(listener) => listener,
Err(e) => anyhow::bail!("Failed to start WayVRServer IPC listener. Reason: {}", e),
};
log::info!("WayVRServer IPC running at {}", printname);
Ok(Self {
listener,
connections: Vec::new(),
})
}
fn accept_connections(&mut self) {
let Ok(conn) = self.listener.accept() else {
return; // No new connection or other error
};
self.connections.push(Connection::new(conn));
}
fn tick_connections(&mut self, params: &mut TickParams) {
for c in &mut self.connections {
c.tick(params);
}
// remove killed connections
self.connections.retain(|c| c.alive);
}
pub fn tick(&mut self, params: &mut TickParams) -> anyhow::Result<()> {
self.accept_connections();
self.tick_connections(params);
Ok(())
}
pub fn broadcast(&mut self, packet: packet_server::PacketServer) -> anyhow::Result<()> {
for connection in &mut self.connections {
send_packet(&mut connection.conn, &ipc::data_encode(&packet))?;
}
Ok(())
}
}
+54
View File
@@ -0,0 +1,54 @@
use super::egl_data;
use smithay::backend::{egl as smithay_egl, renderer::gles::ffi};
pub fn get_egl_display(data: &egl_data::EGLData) -> anyhow::Result<smithay_egl::EGLDisplay> {
Ok(unsafe { smithay_egl::EGLDisplay::from_raw(data.display.as_ptr(), data.config.as_ptr())? })
}
pub fn get_egl_context(
data: &egl_data::EGLData,
display: &smithay_egl::EGLDisplay,
) -> anyhow::Result<smithay_egl::EGLContext> {
let display_ptr = display.get_display_handle().handle;
debug_assert!(display_ptr == data.display.as_ptr());
let config_ptr = data.config.as_ptr();
let context_ptr = data.context.as_ptr();
Ok(unsafe { smithay_egl::EGLContext::from_raw(display_ptr, config_ptr, context_ptr)? })
}
pub fn create_framebuffer_texture(
gl: &ffi::Gles2,
width: u32,
height: u32,
tex_format: u32,
internal_format: u32,
) -> u32 {
unsafe {
let mut tex = 0;
gl.GenTextures(1, &mut tex);
gl.BindTexture(ffi::TEXTURE_2D, tex);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_MIN_FILTER,
ffi::NEAREST as i32,
);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_MAG_FILTER,
ffi::NEAREST as i32,
);
gl.TexImage2D(
ffi::TEXTURE_2D,
0,
internal_format as i32,
width as i32,
height as i32,
0,
tex_format,
ffi::UNSIGNED_BYTE,
std::ptr::null(),
);
gl.BindTexture(ffi::TEXTURE_2D, 0);
tex
}
}
+9
View File
@@ -0,0 +1,9 @@
use std::time::{SystemTime, UNIX_EPOCH};
// Returns milliseconds since unix epoch
pub fn get_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
+98
View File
@@ -0,0 +1,98 @@
use smithay::wayland::shell::xdg::ToplevelSurface;
use wayvr_ipc::packet_server;
use crate::gen_id;
use super::display;
#[derive(Debug)]
pub struct Window {
pub pos_x: i32,
pub pos_y: i32,
pub size_x: u32,
pub size_y: u32,
pub visible: bool,
pub toplevel: ToplevelSurface,
pub display_handle: display::DisplayHandle,
}
impl Window {
pub fn new(display_handle: display::DisplayHandle, toplevel: &ToplevelSurface) -> Self {
Self {
pos_x: 0,
pos_y: 0,
size_x: 0,
size_y: 0,
visible: true,
toplevel: toplevel.clone(),
display_handle,
}
}
pub fn set_pos(&mut self, pos_x: i32, pos_y: i32) {
self.pos_x = pos_x;
self.pos_y = pos_y;
}
pub fn set_size(&mut self, size_x: u32, size_y: u32) {
self.toplevel.with_pending_state(|state| {
//state.bounds = Some((size_x as i32, size_y as i32).into());
state.size = Some((size_x as i32, size_y as i32).into());
});
self.toplevel.send_configure();
self.size_x = size_x;
self.size_y = size_y;
}
}
#[derive(Debug)]
pub struct WindowManager {
pub windows: WindowVec,
}
impl WindowManager {
pub fn new() -> Self {
Self {
windows: WindowVec::new(),
}
}
pub fn find_window_handle(&self, toplevel: &ToplevelSurface) -> Option<WindowHandle> {
for (idx, cell) in self.windows.vec.iter().enumerate() {
if let Some(cell) = cell {
let window = &cell.obj;
if window.toplevel == *toplevel {
return Some(WindowVec::get_handle(cell, idx));
}
}
}
None
}
pub fn create_window(
&mut self,
display_handle: display::DisplayHandle,
toplevel: &ToplevelSurface,
) -> WindowHandle {
self.windows.add(Window::new(display_handle, toplevel))
}
}
gen_id!(WindowVec, Window, WindowCell, WindowHandle);
impl WindowHandle {
pub fn from_packet(handle: packet_server::WvrWindowHandle) -> Self {
Self {
generation: handle.generation,
idx: handle.idx,
}
}
pub fn as_packet(&self) -> packet_server::WvrWindowHandle {
packet_server::WvrWindowHandle {
idx: self.idx,
generation: self.generation,
}
}
}
+337 -44
View File
@@ -1,29 +1,103 @@
use std::path::PathBuf;
use std::sync::Arc;
use crate::config_io;
use crate::config_io::get_conf_d_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 chrono::Offset;
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_mouse_move_interval_ms() -> u32 {
10 // 100fps
}
fn def_click_freeze_time_ms() -> u32 {
@@ -50,25 +124,70 @@ pub fn def_point7() -> f32 {
0.7
}
pub fn def_point3() -> f32 {
0.3
}
fn def_osc_port() -> u16 {
9000
}
fn def_screens() -> Vec<Arc<str>> {
vec![]
fn def_empty_vec_string() -> Vec<String> {
Vec::new()
}
fn def_timezones() -> Vec<String> {
let offset = chrono::Local::now().offset().fix();
const EMEA: i32 = -60 * 60; // UTC-1
const APAC: i32 = 5 * 60 * 60; // UTC+5
match offset.local_minus_utc() {
i32::MIN..EMEA => vec!["Europe/Paris".into(), "Asia/Tokyo".into()],
EMEA..APAC => vec!["America/New_York".into(), "Asia/Tokyo".into()],
APAC..=i32::MAX => vec!["Europe/Paris".into(), "America/New_York".into()],
}
}
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_empty() -> Arc<str> {
"".into()
}
fn def_toast_topics() -> IdMap<ToastTopic, DisplayMethod> {
IdMap::new()
}
fn def_font() -> Arc<str> {
"LiberationSans:style=Bold".into()
}
fn def_max_height() -> u16 {
1440
}
#[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,
@@ -76,12 +195,18 @@ pub struct GeneralConfig {
#[serde(default = "def_click_freeze_time_ms")]
pub click_freeze_time_ms: u32,
#[serde(default = "def_mouse_move_interval_ms")]
pub mouse_move_interval_ms: u32,
#[serde(default = "def_true")]
pub notifications_enabled: bool,
#[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 +225,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 +235,79 @@ 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,
#[serde(default = "def_empty")]
pub skybox_texture: Arc<str>,
#[serde(default = "def_true")]
pub use_skybox: bool,
#[serde(default = "def_true")]
pub use_passthrough: bool,
#[serde(default = "def_max_height")]
pub screen_max_height: u16,
#[serde(default = "def_true")]
pub screen_render_down: bool,
#[serde(default = "def_point3")]
pub pointer_lerp_factor: f32,
#[serde(default = "def_false")]
pub space_rotate_unlocked: bool,
#[serde(default = "def_empty_vec_string")]
pub alt_click_down: Vec<String>,
#[serde(default = "def_empty_vec_string")]
pub alt_click_up: Vec<String>,
#[serde(default = "def_timezones")]
pub timezones: Vec<String>,
}
impl GeneralConfig {
@@ -136,18 +327,26 @@ 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; 5] = [
include_str!("res/keyboard.yaml"),
include_str!("res/watch.yaml"),
include_str!("res/settings.yaml"),
include_str!("res/anchor.yaml"),
include_str!("res/wayvr.yaml"),
];
const FILES: [&str; 3] = ["keyboard.yaml", "watch.yaml", "settings.yaml"];
const FILES: [&str; 5] = [
"keyboard.yaml",
"watch.yaml",
"settings.yaml",
"anchor.yaml",
"wayvr.yaml",
];
#[derive(Clone, Copy)]
#[repr(usize)]
@@ -155,6 +354,9 @@ pub enum ConfigType {
Keyboard,
Watch,
Settings,
Anchor,
#[allow(dead_code)]
WayVR,
}
pub fn load_known_yaml<T>(config_type: ConfigType) -> T
@@ -186,36 +388,127 @@ pub fn load_custom_ui(name: &str) -> anyhow::Result<ModularUiConfig> {
Ok(serde_yaml::from_str(&yaml_data)?)
}
pub fn load_general() -> GeneralConfig {
let mut yaml_data = String::new();
pub fn load_config_with_conf_d<ConfigData>(
root_config_filename: &str,
ctype: config_io::ConfigRoot,
) -> ConfigData
where
ConfigData: for<'de> Deserialize<'de>,
{
let mut settings_builder = Config::builder();
// Add files from conf.d directory
let path_conf_d = get_conf_d_path();
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;
}
let path_conf_d = ctype.get_conf_d_path();
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_io::get_config_root(), path_conf_d.clone()] {
base_conf.push(root_config_filename);
if base_conf.exists() {
log::info!("Loading config file: {}", base_conf.to_string_lossy());
settings_builder = settings_builder.add_source(File::from(base_conf));
}
}
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::<ConfigData>() {
Ok(config) => config,
Err(e) => {
panic!("Failed to deserialize settings: {}", e);
}
},
Err(e) => {
panic!("Failed to build settings: {}", e);
}
}
}
pub fn load_general() -> GeneralConfig {
load_config_with_conf_d::<GeneralConfig>("config.yaml", config_io::ConfigRoot::Generic)
}
// Config that is saved from the settings panel
#[derive(Serialize)]
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 {
config_io::ConfigRoot::Generic
.get_conf_d_path()
.join("zz-saved-config.json5")
}
pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
let conf = AutoSettings {
watch_pos: config.watch_pos,
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 {
config_io::ConfigRoot::Generic
.get_conf_d_path()
.join("zz-saved-state.json5")
}
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(())
}
+30 -38
View File
@@ -1,13 +1,16 @@
use log::error;
use once_cell::sync::Lazy;
use std::{
fs::{self, create_dir},
path::PathBuf,
};
use std::path::PathBuf;
pub enum ConfigRoot {
Generic,
#[allow(dead_code)]
WayVR,
}
const FALLBACK_CONFIG_PATH: &str = "/tmp/wlxoverlay";
pub static CONFIG_ROOT_PATH: Lazy<PathBuf> = Lazy::new(|| {
static CONFIG_ROOT_PATH: Lazy<PathBuf> = Lazy::new(|| {
if let Ok(xdg_dirs) = xdg::BaseDirectories::new() {
let mut dir = xdg_dirs.get_config_home();
dir.push("wlxoverlay");
@@ -21,51 +24,40 @@ pub static CONFIG_ROOT_PATH: Lazy<PathBuf> = Lazy::new(|| {
PathBuf::from(FALLBACK_CONFIG_PATH)
});
pub fn get_conf_d_path() -> PathBuf {
let mut config_root = CONFIG_ROOT_PATH.clone();
config_root.push("conf.d");
config_root
pub fn get_config_root() -> PathBuf {
CONFIG_ROOT_PATH.clone()
}
// Make sure config directory is present and return root config path
pub fn ensure_config_root() -> PathBuf {
let path = CONFIG_ROOT_PATH.clone();
let _ = create_dir(&path);
impl ConfigRoot {
pub fn get_conf_d_path(&self) -> PathBuf {
get_config_root().join(match self {
ConfigRoot::Generic => "conf.d",
ConfigRoot::WayVR => "wayvr.conf.d",
})
}
let path_conf_d = get_conf_d_path();
let _ = create_dir(path_conf_d);
path
// Make sure config directory is present and return root config path
pub fn ensure_dir(&self) -> PathBuf {
let path = get_config_root();
let _ = std::fs::create_dir(&path);
let path_conf_d = self.get_conf_d_path();
let _ = std::fs::create_dir(path_conf_d);
path
}
}
fn get_config_file_path(filename: &str) -> PathBuf {
let mut config_root = CONFIG_ROOT_PATH.clone();
config_root.push(filename);
config_root
pub fn get_config_file_path(filename: &str) -> PathBuf {
get_config_root().join(filename)
}
pub fn load(filename: &str) -> Option<String> {
let path = get_config_file_path(filename);
log::info!("Loading config {}", path.to_string_lossy());
log::info!("Loading config: {}", path.to_string_lossy());
if let Ok(data) = fs::read_to_string(path) {
if let Ok(data) = std::fs::read_to_string(path) {
Some(data)
} else {
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()
}
};
}
+245
View File
@@ -0,0 +1,245 @@
#[cfg(not(feature = "wayvr"))]
compile_error!("WayVR feature is not enabled");
use std::{
cell::RefCell,
collections::{BTreeMap, HashMap},
rc::Rc,
sync::Arc,
};
use serde::{Deserialize, Serialize};
use crate::{
backend::{
overlay::RelativeTo,
task::{TaskContainer, TaskType},
wayvr,
},
config::load_config_with_conf_d,
config_io,
gui::modular::button::WayVRAction,
overlays::wayvr::WayVRData,
};
// Flat version of RelativeTo
#[derive(Clone, Deserialize, Serialize)]
pub enum AttachTo {
None,
HandLeft,
HandRight,
Head,
Stage,
}
impl AttachTo {
pub fn get_relative_to(&self) -> RelativeTo {
match self {
AttachTo::None => RelativeTo::None,
AttachTo::HandLeft => RelativeTo::Hand(0),
AttachTo::HandRight => RelativeTo::Hand(1),
AttachTo::Stage => RelativeTo::Stage,
AttachTo::Head => RelativeTo::Head,
}
}
pub fn from_packet(input: &wayvr_ipc::packet_client::AttachTo) -> AttachTo {
match input {
wayvr_ipc::packet_client::AttachTo::None => AttachTo::None,
wayvr_ipc::packet_client::AttachTo::HandLeft => AttachTo::HandLeft,
wayvr_ipc::packet_client::AttachTo::HandRight => AttachTo::HandRight,
wayvr_ipc::packet_client::AttachTo::Head => AttachTo::Head,
wayvr_ipc::packet_client::AttachTo::Stage => AttachTo::Stage,
}
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct Rotation {
pub axis: [f32; 3],
pub angle: f32,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct WayVRAppEntry {
pub name: String,
pub target_display: String,
pub exec: String,
pub args: Option<String>,
pub env: Option<Vec<String>>,
pub shown_at_start: Option<bool>,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct WayVRDisplay {
pub width: u16,
pub height: u16,
pub scale: Option<f32>,
pub rotation: Option<Rotation>,
pub pos: Option<[f32; 3]>,
pub attach_to: Option<AttachTo>,
pub primary: Option<bool>,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct WayVRCatalog {
pub apps: Vec<WayVRAppEntry>,
}
impl WayVRCatalog {
pub fn get_app(&self, name: &str) -> Option<&WayVRAppEntry> {
self.apps.iter().find(|&app| app.name.as_str() == name)
}
}
fn def_false() -> bool {
false
}
fn def_true() -> bool {
true
}
fn def_autohide_delay() -> u32 {
750
}
fn def_keyboard_repeat_delay() -> u32 {
200
}
fn def_keyboard_repeat_rate() -> u32 {
50
}
fn def_version() -> u32 {
1
}
#[derive(Deserialize, Serialize)]
pub struct WayVRDashboard {
pub exec: String,
pub args: Option<String>,
pub env: Option<Vec<String>>,
}
#[derive(Deserialize, Serialize)]
pub struct WayVRConfig {
#[serde(default = "def_version")]
pub version: u32,
#[serde(default = "def_false")]
pub run_compositor_at_start: bool,
#[serde(default = "Default::default")]
pub catalogs: HashMap<String, WayVRCatalog>,
#[serde(default = "Default::default")]
pub displays: BTreeMap<String, WayVRDisplay>, // sorted alphabetically
#[serde(default = "Default::default")]
pub dashboard: Option<WayVRDashboard>,
#[serde(default = "def_true")]
pub auto_hide: bool,
#[serde(default = "def_autohide_delay")]
pub auto_hide_delay: u32,
#[serde(default = "def_keyboard_repeat_delay")]
pub keyboard_repeat_delay: u32,
#[serde(default = "def_keyboard_repeat_rate")]
pub keyboard_repeat_rate: u32,
}
impl WayVRConfig {
pub fn get_catalog(&self, name: &str) -> Option<&WayVRCatalog> {
self.catalogs.get(name)
}
pub fn get_display(&self, name: &str) -> Option<&WayVRDisplay> {
self.displays.get(name)
}
pub fn get_default_display(&self) -> Option<(String, &WayVRDisplay)> {
for (disp_name, disp) in &self.displays {
if disp.primary.unwrap_or(false) {
return Some((disp_name.clone(), disp));
}
}
None
}
pub fn get_wayvr_config(
config_general: &crate::config::GeneralConfig,
config_wayvr: &crate::config_wayvr::WayVRConfig,
) -> wayvr::Config {
wayvr::Config {
click_freeze_time_ms: config_general.click_freeze_time_ms,
keyboard_repeat_delay_ms: config_wayvr.keyboard_repeat_delay,
keyboard_repeat_rate: config_wayvr.keyboard_repeat_rate,
auto_hide_delay: if config_wayvr.auto_hide {
Some(config_wayvr.auto_hide_delay)
} else {
None
},
}
}
pub fn post_load(
&self,
config: &crate::config::GeneralConfig,
tasks: &mut TaskContainer,
) -> anyhow::Result<Option<Rc<RefCell<WayVRData>>>> {
let primary_count = self
.displays
.iter()
.filter(|d| d.1.primary.unwrap_or(false))
.count();
if primary_count > 1 {
anyhow::bail!("Number of primary displays is more than 1")
} else if primary_count == 0 {
log::warn!("No primary display specified");
}
for (catalog_name, catalog) in &self.catalogs {
for app in &catalog.apps {
if let Some(b) = app.shown_at_start {
if b {
tasks.enqueue(TaskType::WayVR(WayVRAction::AppClick {
catalog_name: Arc::from(catalog_name.as_str()),
app_name: Arc::from(app.name.as_str()),
}));
}
}
}
}
if self.run_compositor_at_start {
// Start Wayland server instantly
Ok(Some(Rc::new(RefCell::new(WayVRData::new(
Self::get_wayvr_config(config, self),
)?))))
} else {
// Lazy-init WayVR later if the user requested
Ok(None)
}
}
}
pub fn load_wayvr() -> WayVRConfig {
let config_root_path = config_io::ConfigRoot::WayVR.ensure_dir();
log::info!("WayVR Config root path: {:?}", config_root_path);
log::info!(
"WayVR conf.d path: {:?}",
config_io::ConfigRoot::WayVR.get_conf_d_path()
);
let config = load_config_with_conf_d::<WayVRConfig>("wayvr.yaml", config_io::ConfigRoot::WayVR);
if config.version != def_version() {
panic!("WayVR config version {} is not supported", config.version);
}
config
}
+102
View File
@@ -0,0 +1,102 @@
use image_dds::{ImageFormat, Surface};
use std::{io::Read, sync::Arc};
use vulkano::{
buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer},
command_buffer::CopyBufferToImageInfo,
format::Format,
image::{Image, ImageCreateInfo, ImageType, ImageUsage},
memory::allocator::{AllocationCreateInfo, MemoryTypeFilter},
DeviceSize,
};
use super::WlxCommandBuffer;
pub trait WlxCommandBufferDds {
fn texture2d_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>>
where
R: Read;
}
impl WlxCommandBufferDds for WlxCommandBuffer {
fn texture2d_dds<R>(&mut self, r: R) -> anyhow::Result<Arc<Image>>
where
R: Read,
{
let Ok(dds) = image_dds::ddsfile::Dds::read(r) else {
anyhow::bail!("Not a valid DDS file.\nSee: https://github.com/galister/wlx-overlay-s/wiki/Custom-Textures");
};
let surface = Surface::from_dds(&dds)?;
if surface.depth != 1 {
anyhow::bail!("Not a 2D texture.")
}
let image = Image::new(
self.graphics.memory_allocator.clone(),
ImageCreateInfo {
image_type: ImageType::Dim2d,
format: dds_to_vk(surface.image_format)?,
extent: [surface.width, surface.height, surface.depth],
usage: ImageUsage::TRANSFER_DST | ImageUsage::TRANSFER_SRC | ImageUsage::SAMPLED,
..Default::default()
},
AllocationCreateInfo::default(),
)?;
let buffer: Subbuffer<[u8]> = Buffer::new_slice(
self.graphics.memory_allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::TRANSFER_SRC,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_HOST
| MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
surface.data.len() as DeviceSize,
)?;
buffer.write()?.copy_from_slice(surface.data);
self.command_buffer
.copy_buffer_to_image(CopyBufferToImageInfo::buffer_image(buffer, image.clone()))?;
Ok(image)
}
}
pub fn dds_to_vk(dds_fmt: ImageFormat) -> anyhow::Result<Format> {
match dds_fmt {
ImageFormat::R8Unorm => Ok(Format::R8_UNORM),
ImageFormat::Rgba8Unorm => Ok(Format::R8G8B8A8_UNORM),
ImageFormat::Rgba8UnormSrgb => Ok(Format::R8G8B8A8_SRGB),
ImageFormat::Rgba16Float => Ok(Format::R16G16B16A16_SFLOAT),
ImageFormat::Rgba32Float => Ok(Format::R32G32B32A32_SFLOAT),
ImageFormat::Bgra8Unorm => Ok(Format::B8G8R8A8_UNORM),
ImageFormat::Bgra8UnormSrgb => Ok(Format::B8G8R8A8_SRGB),
// DXT1
ImageFormat::BC1RgbaUnorm => Ok(Format::BC1_RGBA_UNORM_BLOCK),
ImageFormat::BC1RgbaUnormSrgb => Ok(Format::BC1_RGBA_SRGB_BLOCK),
// DXT3
ImageFormat::BC2RgbaUnorm => Ok(Format::BC2_UNORM_BLOCK),
ImageFormat::BC2RgbaUnormSrgb => Ok(Format::BC2_SRGB_BLOCK),
// DXT5
ImageFormat::BC3RgbaUnorm => Ok(Format::BC3_UNORM_BLOCK),
ImageFormat::BC3RgbaUnormSrgb => Ok(Format::BC3_SRGB_BLOCK),
// RGTC1
ImageFormat::BC4RUnorm => Ok(Format::BC4_UNORM_BLOCK),
ImageFormat::BC4RSnorm => Ok(Format::BC4_SNORM_BLOCK),
// RGTC2
ImageFormat::BC5RgUnorm => Ok(Format::BC5_UNORM_BLOCK),
ImageFormat::BC5RgSnorm => Ok(Format::BC5_SNORM_BLOCK),
// BPTC
ImageFormat::BC6hRgbUfloat => Ok(Format::BC6H_UFLOAT_BLOCK),
ImageFormat::BC6hRgbSfloat => Ok(Format::BC6H_SFLOAT_BLOCK),
// BPTC
ImageFormat::BC7RgbaUnorm => Ok(Format::BC7_UNORM_BLOCK),
ImageFormat::BC7RgbaUnormSrgb => Ok(Format::BC7_SRGB_BLOCK),
_ => anyhow::bail!("Unsupported format {:?}", dds_fmt),
}
}
+282 -52
View File
@@ -1,3 +1,5 @@
pub(crate) mod dds;
use std::{
collections::HashMap,
os::fd::{FromRawFd, IntoRawFd},
@@ -92,10 +94,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 {
@@ -122,6 +126,7 @@ pub struct WlxGraphics {
pub queue: Arc<Queue>,
pub native_format: Format,
pub texture_filtering: Filter,
pub memory_allocator: Arc<StandardMemoryAllocator>,
pub command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
@@ -133,6 +138,15 @@ pub struct WlxGraphics {
pub shared_shaders: RwLock<HashMap<&'static str, Arc<ShaderModule>>>,
}
fn get_dmabuf_extensions() -> DeviceExtensions {
DeviceExtensions {
khr_external_memory: true,
khr_external_memory_fd: true,
ext_external_memory_dma_buf: true,
..DeviceExtensions::empty()
}
}
static VULKAN_LIBRARY: OnceLock<Arc<vulkano::VulkanLibrary>> = OnceLock::new();
fn get_vulkan_library() -> &'static Arc<vulkano::VulkanLibrary> {
VULKAN_LIBRARY.get_or_init(|| vulkano::VulkanLibrary::new().unwrap()) // want panic
@@ -243,14 +257,22 @@ 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;
}
if physical_device.supported_extensions().ext_filter_cubic {
device_extensions.ext_filter_cubic = true;
}
let device_extensions_raw = device_extensions
.into_iter()
@@ -281,13 +303,18 @@ impl WlxGraphics {
.enabled_extension_names(&device_extensions_raw)
.build();
let mut dynamic_rendering = PhysicalDeviceDynamicRenderingFeatures::builder()
.dynamic_rendering(true)
.build();
let mut dynamic_rendering =
PhysicalDeviceDynamicRenderingFeatures::builder().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;
let texture_filtering = if physical_device.supported_extensions().ext_filter_cubic {
Filter::Cubic
} else {
Filter::Linear
};
let (device, mut queues) = unsafe {
let vk_device = xr_instance
.create_vulkan_device(
@@ -316,6 +343,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()
@@ -347,6 +383,7 @@ impl WlxGraphics {
device,
queue,
native_format: Format::R8G8B8A8_UNORM,
texture_filtering,
memory_allocator,
command_buffer_allocator,
descriptor_set_allocator,
@@ -366,6 +403,7 @@ impl WlxGraphics {
//#[cfg(debug_assertions)]
//let layers = vec!["VK_LAYER_KHRONOS_validation".to_owned()];
//#[cfg(not(debug_assertions))]
let layers = vec![];
log::debug!("Instance exts for runtime: {:?}", &vk_instance_extensions);
@@ -382,29 +420,185 @@ 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()
let dmabuf_extensions = get_dmabuf_extensions();
let (physical_device, my_extensions, queue_family_index) = instance
.enumerate_physical_devices()?
.filter_map(|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;
}
if p.supported_extensions().ext_filter_cubic {
my_extensions.ext_filter_cubic = true;
}
log::debug!(
"Device exts for {}: {:?}",
p.properties().device_name,
&my_extensions
);
Some((p, my_extensions))
})
.filter_map(|(p, my_extensions)| {
p.queue_family_properties()
.iter()
.enumerate()
.position(|(_, q)| q.queue_flags.intersects(QueueFlags::GRAPHICS))
.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 texture_filtering = if physical_device.supported_extensions().ext_filter_cubic {
Filter::Cubic
} else {
Filter::Linear
};
let (device, mut queues) = Device::new(
physical_device,
DeviceCreateInfo {
enabled_extensions: my_extensions,
enabled_features: Features {
dynamic_rendering: true,
..Features::empty()
},
queue_create_infos: vec![QueueCreateInfo {
queue_family_index,
..Default::default()
}],
..Default::default()
},
)?;
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"))?;
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: Format::R8G8B8A8_UNORM,
texture_filtering,
command_buffer_allocator,
descriptor_set_allocator,
quad_indices,
quad_verts,
shared_shaders: RwLock::new(HashMap::new()),
};
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::{
device::physical::PhysicalDeviceType, instance::InstanceCreateFlags, swapchain::Surface,
};
use winit::{event_loop::EventLoop, window::Window};
let event_loop = EventLoop::new().unwrap(); // want panic
let mut vk_instance_extensions = Surface::required_extensions(&event_loop).unwrap();
vk_instance_extensions.khr_get_physical_device_properties2 = true;
log::debug!("Instance exts for runtime: {:?}", &vk_instance_extensions);
let instance = Instance::new(
get_vulkan_library().clone(),
InstanceCreateInfo {
flags: InstanceCreateFlags::ENUMERATE_PORTABILITY,
enabled_extensions: vk_instance_extensions,
..Default::default()
},
)?;
let window = Arc::new(
event_loop
.create_window(Window::default_attributes())
.unwrap(), // want panic
);
let surface = Surface::from_window(instance.clone(), window.clone())?;
let mut device_extensions = DeviceExtensions::empty();
device_extensions.khr_swapchain = true;
log::debug!("Device exts for app: {:?}", &device_extensions);
let (physical_device, my_extensions, queue_family_index) = instance
.enumerate_physical_devices()?
.filter_map(|p| {
let runtime_extensions = vk_device_extensions_fn(&p);
log::debug!(
"Device exts for {}: {:?}",
p.properties().device_name,
&runtime_extensions
);
let my_extensions = runtime_extensions.union(&device_extensions);
if p.supported_extensions().contains(&my_extensions) {
Some((p, my_extensions))
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
}
})
@@ -412,7 +606,8 @@ impl WlxGraphics {
p.queue_family_properties()
.iter()
.enumerate()
.position(|(_, q)| q.queue_flags.intersects(QueueFlags::GRAPHICS))
.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 {
@@ -446,6 +641,13 @@ impl WlxGraphics {
},
)?;
let native_format = device
.physical_device()
.surface_formats(&surface, Default::default())
.unwrap()[0] // want panic
.0;
log::info!("Using surface format: {:?}", native_format);
let queue = queues
.next()
.ok_or_else(|| anyhow::anyhow!("no GPU queues available"))?;
@@ -470,7 +672,8 @@ impl WlxGraphics {
device,
queue,
memory_allocator,
native_format: Format::R8G8B8A8_UNORM,
native_format,
texture_filtering: Filter::Linear,
command_buffer_allocator,
descriptor_set_allocator,
quad_indices,
@@ -478,9 +681,8 @@ impl WlxGraphics {
shared_shaders: RwLock::new(HashMap::new()),
};
Ok(Arc::new(me))
Ok((Arc::new(me), event_loop, window, surface))
}
fn default_quad(
memory_allocator: Arc<StandardMemoryAllocator>,
) -> anyhow::Result<(Vert2Buf, IndexBuf)> {
@@ -595,24 +797,17 @@ impl WlxGraphics {
)?)
}
pub fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>> {
pub fn dmabuf_texture_ex(
&self,
frame: DmabufFrame,
tiling: ImageTiling,
layouts: Vec<SubresourceLayout>,
modifiers: Vec<u64>,
) -> anyhow::Result<Arc<Image>> {
let extent = [frame.format.width, frame.format.height, 1];
let format = fourcc_to_vk(frame.format.fourcc)?;
let layouts: Vec<SubresourceLayout> = (0..frame.num_planes)
.map(|i| {
let plane = &frame.planes[i];
SubresourceLayout {
offset: plane.offset as _,
size: 0,
row_pitch: plane.stride as _,
array_pitch: None,
depth_pitch: None,
}
})
.collect();
let image = unsafe {
RawImage::new_unchecked(
self.device.clone(),
@@ -621,8 +816,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()
},
@@ -653,7 +848,7 @@ impl WlxGraphics {
let file = std::fs::File::from_raw_fd(fd);
let new_file = file.try_clone()?;
file.into_raw_fd();
let _ = file.into_raw_fd();
let memory = DeviceMemory::allocate_unchecked(
self.device.clone(),
@@ -679,6 +874,29 @@ impl WlxGraphics {
}
}
pub fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>> {
let mut modifiers: Vec<u64> = vec![];
let mut tiling: ImageTiling = ImageTiling::Optimal;
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];
layouts.push(SubresourceLayout {
offset: plane.offset as _,
size: 0,
row_pitch: plane.stride as _,
array_pitch: None,
depth_pitch: None,
});
modifiers.push(frame.format.modifier);
});
tiling = ImageTiling::DrmFormatModifier;
};
self.dmabuf_texture_ex(frame, tiling, layouts, modifiers)
}
pub fn render_texture(
&self,
width: u32,
@@ -884,7 +1102,7 @@ impl WlxCommandBuffer {
Ok(())
}
pub fn texture2d(
pub fn texture2d_raw(
&mut self,
width: u32,
height: u32,
@@ -964,7 +1182,6 @@ pub struct WlxPipelineDynamic {}
pub struct WlxPipelineLegacy {
pub render_pass: Arc<RenderPass>,
pub framebuffer: Arc<Framebuffer>,
pub view: Arc<ImageView>,
}
pub struct WlxPipeline<D> {
@@ -1198,7 +1415,6 @@ impl WlxPipeline<WlxPipelineLegacy> {
data: WlxPipelineLegacy {
render_pass,
framebuffer,
view: render_target,
},
})
}
@@ -1414,6 +1630,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),
}
}
@@ -1456,3 +1674,15 @@ fn memory_allocator(device: Arc<Device>) -> Arc<StandardMemoryAllocator> {
Arc::new(StandardMemoryAllocator::new(device, create_info))
}
pub fn format_is_srgb(format: Format) -> bool {
matches!(
format,
Format::R8G8B8A8_SRGB
| Format::B8G8R8A8_SRGB
| Format::BC1_RGBA_SRGB_BLOCK
| Format::BC2_SRGB_BLOCK
| Format::BC3_SRGB_BLOCK
| Format::BC7_SRGB_BLOCK
)
}
+266
View File
@@ -0,0 +1,266 @@
use glam::Vec4;
use std::sync::Arc;
use vulkano::format::Format;
use crate::{
graphics::WlxGraphics,
gui::{canvas::control::ControlRenderer, GuiColor, KeyCapType},
};
use super::{control::Control, Canvas, Rect};
pub struct CanvasBuilder<D, S> {
canvas: Canvas<D, S>,
pub fg_color: GuiColor,
pub bg_color: GuiColor,
pub font_size: isize,
}
impl<D, S> CanvasBuilder<D, S> {
pub fn new(
width: usize,
height: usize,
graphics: Arc<WlxGraphics>,
format: Format,
data: D,
) -> anyhow::Result<Self> {
Ok(Self {
canvas: Canvas::new(width, height, graphics, format, data)?,
bg_color: Vec4::ZERO,
fg_color: Vec4::ONE,
font_size: 16,
})
}
pub fn build(self) -> Canvas<D, S> {
self.canvas
}
// Creates a panel with bg_color inherited from the canvas
pub fn panel(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
bg_color: self.bg_color,
on_render_bg: Some(Control::render_rounded_rect),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a label with fg_color, font_size inherited from the canvas
pub fn label(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
text: Arc<str>,
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
text,
fg_color: self.fg_color,
size: self.font_size,
on_render_fg: Some(Control::render_text),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a label with fg_color, font_size inherited from the canvas
#[allow(dead_code)]
pub fn label_centered(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
text: Arc<str>,
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
text,
fg_color: self.fg_color,
size: self.font_size,
on_render_fg: Some(Control::render_text_centered),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a sprite. Will not draw anything until set_sprite is called.
pub fn sprite(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: 0.,
on_render_bg: Some(Control::render_sprite_bg),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a sprite that highlights on pointer hover. Will not draw anything until set_sprite is called.
#[allow(dead_code)]
pub fn sprite_interactive(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: 0.,
on_render_bg: Some(Control::render_sprite_bg),
on_render_hl: Some(Control::render_sprite_hl),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a button with fg_color, bg_color, font_size inherited from the canvas
pub fn button(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
text: Arc<str>,
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.interactive_set_idx(x, y, w, h, idx);
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
text,
fg_color: self.fg_color,
bg_color: self.bg_color,
size: self.font_size,
on_render_bg: Some(Control::render_rounded_rect),
on_render_fg: Some(Control::render_text_centered),
on_render_hl: Some(Control::render_highlight),
..Control::new()
});
&mut self.canvas.controls[idx]
}
pub fn key_button(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
cap_type: KeyCapType,
label: &[String],
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.interactive_set_idx(x, y, w, h, idx);
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
corner_radius: radius,
bg_color: self.bg_color,
on_render_bg: Some(Control::render_rounded_rect),
on_render_hl: Some(Control::render_highlight),
..Control::new()
});
let renders = match cap_type {
KeyCapType::Regular => {
let render: ControlRenderer<D, S> = Control::render_text_centered;
let rect = Rect {
x,
y,
w,
h: h - self.font_size as f32,
};
vec![(render, rect, 1f32)]
}
KeyCapType::RegularAltGr => {
let render: ControlRenderer<D, S> = Control::render_text;
let rect0 = Rect {
x: x + 12.,
y: y + (self.font_size as f32) + 12.,
w,
h,
};
let rect1 = Rect {
x: x + w * 0.5 + 12.,
y: y + h - (self.font_size as f32) + 8.,
w,
h,
};
vec![(render, rect0, 1.0), (render, rect1, 0.8)]
}
KeyCapType::Reversed => {
let render: ControlRenderer<D, S> = Control::render_text_centered;
let rect0 = Rect {
x,
y: y + 2.0,
w,
h: h * 0.5,
};
let rect1 = Rect {
x,
y: y + h * 0.5 + 2.0,
w,
h: h * 0.5,
};
vec![(render, rect1, 1.0), (render, rect0, 0.8)]
}
KeyCapType::ReversedAltGr => {
let render: ControlRenderer<D, S> = Control::render_text;
let rect0 = Rect {
x: x + 12.,
y: y + (self.font_size as f32) + 8.,
w,
h,
};
let rect1 = Rect {
x: x + 12.,
y: y + h - (self.font_size as f32) + 4.,
w,
h,
};
let rect2 = Rect {
x: x + w * 0.5 + 8.,
y: y + h - (self.font_size as f32) + 4.,
w,
h,
};
vec![
(render, rect1, 1.0),
(render, rect0, 0.8),
(render, rect2, 0.8),
]
}
};
for (idx, (render, rect, alpha)) in renders.into_iter().enumerate() {
if idx >= label.len() {
break;
}
self.canvas.controls.push(Control {
rect,
text: Arc::from(label[idx].as_str()),
fg_color: self.fg_color * alpha,
size: self.font_size,
on_render_fg: Some(render),
..Control::new()
});
}
&mut self.canvas.controls[idx]
}
}
+387
View File
@@ -0,0 +1,387 @@
use glam::Vec4;
use std::sync::Arc;
use vulkano::image::view::ImageView;
use crate::{
backend::input::PointerMode, graphics::WlxCommandBuffer, gui::GuiColor, state::AppState,
};
use super::{CanvasData, Rect};
pub type ControlRenderer<D, S> =
fn(&Control<D, S>, &CanvasData<D>, &mut AppState, &mut WlxCommandBuffer) -> anyhow::Result<()>;
pub type ControlRendererHl<D, S> = fn(
&Control<D, S>,
&CanvasData<D>,
&mut AppState,
&mut WlxCommandBuffer,
Vec4,
) -> anyhow::Result<()>;
pub(crate) struct Control<D, S> {
pub state: Option<S>,
pub rect: Rect,
pub corner_radius: f32,
pub fg_color: GuiColor,
pub bg_color: GuiColor,
pub text: Arc<str>,
pub size: isize,
pub sprite: Option<Arc<ImageView>>,
pub sprite_st: Vec4,
pub(super) dirty: bool,
pub on_update: Option<fn(&mut Self, &mut D, &mut AppState)>,
pub on_press: Option<fn(&mut Self, &mut D, &mut AppState, PointerMode)>,
pub on_release: Option<fn(&mut Self, &mut D, &mut AppState)>,
pub on_scroll: Option<fn(&mut Self, &mut D, &mut AppState, f32)>,
pub test_highlight: Option<fn(&Self, &mut D, &mut AppState) -> Option<Vec4>>,
pub(super) on_render_bg: Option<ControlRenderer<D, S>>,
pub(super) on_render_hl: Option<ControlRendererHl<D, S>>,
pub(super) on_render_fg: Option<ControlRenderer<D, S>>,
}
impl<D, S> Control<D, S> {
pub(super) fn new() -> Self {
Self {
rect: Rect {
x: 0.,
y: 0.,
w: 0.,
h: 0.,
},
corner_radius: 0.,
fg_color: Vec4::ONE,
bg_color: Vec4::ZERO,
text: Arc::from(""),
sprite: None,
sprite_st: Vec4::new(1., 1., 0., 0.),
dirty: true,
size: 24,
state: None,
on_update: None,
on_render_bg: None,
on_render_hl: None,
on_render_fg: None,
test_highlight: None,
on_press: None,
on_release: None,
on_scroll: None,
}
}
#[inline(always)]
pub fn set_text(&mut self, text: &str) {
if *self.text == *text {
return;
}
self.text = text.into();
self.dirty = true;
}
#[inline(always)]
pub fn set_sprite(&mut self, sprite: Arc<ImageView>) {
self.sprite.replace(sprite);
self.dirty = true;
}
#[inline(always)]
pub fn set_sprite_st(&mut self, sprite_st: Vec4) {
if self.sprite_st == sprite_st {
return;
}
self.sprite_st = sprite_st;
self.dirty = true;
}
#[inline(always)]
pub fn set_fg_color(&mut self, color: GuiColor) {
if self.fg_color == color {
return;
}
self.fg_color = color;
self.dirty = true;
}
pub fn render_rounded_rect(
&self,
canvas: &CanvasData<D>,
_: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let pass = {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let clamped_radius = self
.corner_radius
.min(self.rect.w / 2.0)
.min(self.rect.h / 2.0);
let skew_radius = [clamped_radius / self.rect.w, clamped_radius / self.rect.h];
let set0 = canvas.pipeline_bg_color.uniform_buffer(
0,
vec![
self.bg_color.x,
self.bg_color.y,
self.bg_color.z,
self.bg_color.w,
skew_radius[0],
skew_radius[1],
],
)?;
canvas.pipeline_bg_color.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0],
)?
};
cmd_buffer.run_ref(&pass)
}
pub(super) fn render_highlight(
&self,
canvas: &CanvasData<D>,
_: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
color: GuiColor,
) -> anyhow::Result<()> {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let clamped_radius = self
.corner_radius
.min(self.rect.w / 2.0)
.min(self.rect.h / 2.0);
let skew_radius = [clamped_radius / self.rect.w, clamped_radius / self.rect.h];
let set0 = canvas.pipeline_bg_color.uniform_buffer(
0,
vec![
color.x,
color.y,
color.z,
color.w,
skew_radius[0],
skew_radius[1],
],
)?;
let pass = canvas.pipeline_bg_color.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer.clone(),
canvas.graphics.quad_indices.clone(),
vec![set0],
)?;
cmd_buffer.run_ref(&pass)
}
pub(super) fn render_text(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let mut cur_y = self.rect.y;
for line in self.text.lines() {
let mut cur_x = self.rect.x;
for glyph in app
.fc
.get_glyphs(line, self.size, canvas.graphics.clone())?
{
if let Some(tex) = glyph.tex.clone() {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
cur_x + glyph.left,
cur_y - glyph.top,
glyph.width,
glyph.height,
)?;
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
0,
ImageView::new_default(tex)?,
app.graphics.texture_filtering,
)?;
let set1 = canvas
.pipeline_fg_glyph
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
let pass = canvas.pipeline_fg_glyph.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
}
cur_x += glyph.advance;
}
cur_y += (self.size as f32) * 1.5;
}
Ok(())
}
pub(super) fn render_text_centered(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let (w, h) = app
.fc
.get_text_size(&self.text, self.size, canvas.graphics.clone())?;
let mut cur_y = self.rect.y + (self.rect.h) - (h * 0.5) - (self.size as f32 * 0.25);
for line in self.text.lines() {
let mut cur_x = self.rect.x + (self.rect.w * 0.5) - (w * 0.5);
for glyph in app
.fc
.get_glyphs(line, self.size, canvas.graphics.clone())?
{
if let Some(tex) = glyph.tex.clone() {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
cur_x + glyph.left,
cur_y - glyph.top,
glyph.width,
glyph.height,
)?;
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
0,
ImageView::new_default(tex)?,
app.graphics.texture_filtering,
)?;
let set1 = canvas
.pipeline_fg_glyph
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
let pass = canvas.pipeline_fg_glyph.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
}
cur_x += glyph.advance;
}
cur_y += (self.size as f32) * 1.5;
}
Ok(())
}
pub(super) fn render_sprite_bg(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let Some(view) = self.sprite.as_ref() else {
return Ok(());
};
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let set0 = canvas.pipeline_bg_sprite.uniform_sampler(
0,
view.clone(),
app.graphics.texture_filtering,
)?;
let uniform = vec![
self.sprite_st.x,
self.sprite_st.y,
self.sprite_st.z,
self.sprite_st.w,
self.fg_color.x,
self.fg_color.y,
self.fg_color.z,
self.fg_color.w,
];
let set1 = canvas.pipeline_bg_sprite.uniform_buffer(1, uniform)?;
let pass = canvas.pipeline_bg_sprite.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
Ok(())
}
#[allow(dead_code)]
pub(super) fn render_sprite_hl(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
color: GuiColor,
) -> anyhow::Result<()> {
let Some(view) = self.sprite.as_ref() else {
return Ok(());
};
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let set0 = canvas.pipeline_hl_sprite.uniform_sampler(
0,
view.clone(),
app.graphics.texture_filtering,
)?;
let uniform = vec![
self.sprite_st.x,
self.sprite_st.y,
self.sprite_st.z,
self.sprite_st.w,
color.x,
color.y,
color.z,
color.w,
];
let set1 = canvas.pipeline_hl_sprite.uniform_buffer(1, uniform)?;
let pass = canvas.pipeline_hl_sprite.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
Ok(())
}
}
+389
View File
@@ -0,0 +1,389 @@
pub(crate) mod builder;
pub(crate) mod control;
use std::sync::Arc;
use glam::{Vec2, Vec4};
use vulkano::{
command_buffer::CommandBufferUsage,
format::Format,
image::{view::ImageView, ImageLayout},
};
use crate::{
backend::{
input::{Haptics, InteractionHandler, PointerHit},
overlay::{FrameTransform, OverlayBackend, OverlayRenderer},
},
graphics::{WlxGraphics, WlxPass, WlxPipeline, WlxPipelineLegacy, BLEND_ALPHA},
state::AppState,
};
const RES_DIVIDER: usize = 4;
pub struct Rect {
x: f32,
y: f32,
w: f32,
h: f32,
}
pub struct CanvasData<D> {
pub data: D,
pub width: usize,
pub height: usize,
graphics: Arc<WlxGraphics>,
pipeline_bg_color: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline_fg_glyph: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline_bg_sprite: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline_hl_sprite: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline_final: Arc<WlxPipeline<WlxPipelineLegacy>>,
}
pub struct Canvas<D, S> {
controls: Vec<control::Control<D, S>>,
canvas: CanvasData<D>,
hover_controls: [Option<usize>; 2],
pressed_controls: [Option<usize>; 2],
interact_map: Vec<Option<u16>>,
interact_stride: usize,
interact_rows: usize,
view_final: Arc<ImageView>,
pass_fg: WlxPass<WlxPipelineLegacy>,
pass_bg: WlxPass<WlxPipelineLegacy>,
}
impl<D, S> Canvas<D, S> {
fn new(
width: usize,
height: usize,
graphics: Arc<WlxGraphics>,
format: Format,
data: D,
) -> anyhow::Result<Self> {
let tex_fg = graphics.render_texture(width as _, height as _, format)?;
let tex_bg = graphics.render_texture(width as _, height as _, format)?;
let tex_final = graphics.render_texture(width as _, height as _, format)?;
let view_fg = ImageView::new_default(tex_fg.clone())?;
let view_bg = ImageView::new_default(tex_bg.clone())?;
let view_final = ImageView::new_default(tex_final.clone())?;
let Ok(shaders) = graphics.shared_shaders.read() else {
anyhow::bail!("Failed to lock shared shaders for reading");
};
let pipeline_bg_color = graphics.create_pipeline(
view_bg.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_color").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let pipeline_fg_glyph = graphics.create_pipeline(
view_fg.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_glyph").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let pipeline_bg_sprite = graphics.create_pipeline(
view_fg.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_sprite2").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let pipeline_hl_sprite = graphics.create_pipeline(
view_fg.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_sprite2_hl").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let vertex_buffer =
graphics.upload_verts(width as _, height as _, 0., 0., width as _, height as _)?;
let pipeline_final = graphics.create_pipeline_with_layouts(
view_final.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_sprite").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
ImageLayout::TransferSrcOptimal,
ImageLayout::TransferSrcOptimal,
)?;
let set_fg =
pipeline_final.uniform_sampler(0, view_fg.clone(), graphics.texture_filtering)?;
let set_bg =
pipeline_final.uniform_sampler(0, view_bg.clone(), graphics.texture_filtering)?;
let pass_fg = pipeline_final.create_pass(
[width as _, height as _],
vertex_buffer.clone(),
graphics.quad_indices.clone(),
vec![set_fg],
)?;
let pass_bg = pipeline_final.create_pass(
[width as _, height as _],
vertex_buffer.clone(),
graphics.quad_indices.clone(),
vec![set_bg],
)?;
let stride = width / RES_DIVIDER;
let rows = height / RES_DIVIDER;
Ok(Self {
canvas: CanvasData {
data,
width,
height,
graphics: graphics.clone(),
pipeline_bg_color,
pipeline_fg_glyph,
pipeline_bg_sprite,
pipeline_hl_sprite,
pipeline_final,
},
controls: Vec::new(),
hover_controls: [None, None],
pressed_controls: [None, None],
interact_map: vec![None; stride * rows],
interact_stride: stride,
interact_rows: rows,
view_final,
pass_fg,
pass_bg,
})
}
fn interactive_set_idx(&mut self, x: f32, y: f32, w: f32, h: f32, idx: usize) {
let (x, y, w, h) = (x as usize, y as usize, w as usize, h as usize);
let x_min = (x / RES_DIVIDER).max(0);
let y_min = (y / RES_DIVIDER).max(0);
let x_max = (x_min + (w / RES_DIVIDER)).min(self.interact_stride - 1);
let y_max = (y_min + (h / RES_DIVIDER)).min(self.interact_rows - 1);
for y in y_min..y_max {
for x in x_min..x_max {
self.interact_map[y * self.interact_stride + x] = Some(idx as u16);
}
}
}
fn interactive_get_idx(&self, uv: Vec2) -> Option<usize> {
let x = (uv.x * self.canvas.width as f32) as usize;
let y = (uv.y * self.canvas.height as f32) as usize;
let x = (x / RES_DIVIDER).max(0).min(self.interact_stride - 1);
let y = (y / RES_DIVIDER).max(0).min(self.interact_rows - 1);
self.interact_map[y * self.interact_stride + x].map(|x| x as usize)
}
fn render_bg(&mut self, app: &mut AppState) -> anyhow::Result<()> {
let mut cmd_buffer = self
.canvas
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_render_pass(&self.canvas.pipeline_bg_color)?;
for c in self.controls.iter_mut() {
if let Some(fun) = c.on_render_bg {
fun(c, &self.canvas, app, &mut cmd_buffer)?;
}
}
cmd_buffer.end_render_pass()?;
cmd_buffer.build_and_execute_now()
}
fn render_fg(&mut self, app: &mut AppState) -> anyhow::Result<()> {
let mut cmd_buffer = self
.canvas
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_render_pass(&self.canvas.pipeline_fg_glyph)?;
for c in self.controls.iter_mut() {
if let Some(fun) = c.on_render_fg {
fun(c, &self.canvas, app, &mut cmd_buffer)?;
}
}
cmd_buffer.end_render_pass()?;
cmd_buffer.build_and_execute_now()
}
pub fn data_mut(&mut self) -> &mut D {
&mut self.canvas.data
}
}
impl<D, S> InteractionHandler for Canvas<D, S> {
fn on_left(&mut self, _app: &mut AppState, pointer: usize) {
self.hover_controls[pointer] = None;
}
fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
let old = self.hover_controls[hit.pointer];
if let Some(i) = self.interactive_get_idx(hit.uv) {
self.hover_controls[hit.pointer] = Some(i);
} else {
self.hover_controls[hit.pointer] = None;
}
if old != self.hover_controls[hit.pointer] {
Some(Haptics {
intensity: 0.1,
duration: 0.01,
frequency: 5.0,
})
} else {
None
}
}
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
let idx = if pressed {
self.interactive_get_idx(hit.uv)
} else {
self.pressed_controls[hit.pointer]
};
if let Some(idx) = idx {
let c = &mut self.controls[idx];
if pressed {
if let Some(ref mut f) = c.on_press {
self.pressed_controls[hit.pointer] = Some(idx);
f(c, &mut self.canvas.data, app, hit.mode);
}
} else if let Some(ref mut f) = c.on_release {
self.pressed_controls[hit.pointer] = None;
f(c, &mut self.canvas.data, app);
}
}
}
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta: f32) {
let idx = self.hover_controls[hit.pointer];
if let Some(idx) = idx {
let c = &mut self.controls[idx];
if let Some(ref mut f) = c.on_scroll {
f(c, &mut self.canvas.data, app, delta);
}
}
}
}
impl<D, S> OverlayRenderer for Canvas<D, S> {
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.render_bg(app)?;
self.render_fg(app)
}
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
let mut dirty = false;
for c in self.controls.iter_mut() {
if let Some(fun) = c.on_update {
fun(c, &mut self.canvas.data, app);
}
if c.dirty {
dirty = true;
c.dirty = false;
}
}
if dirty {
self.render_bg(app)?;
self.render_fg(app)?;
}
/*
let image = self.view_final.image().clone();
if self.first_render {
self.first_render = false;
} else {
self.canvas
.graphics
.transition_layout(
image.clone(),
ImageLayout::TransferSrcOptimal,
ImageLayout::ColorAttachmentOptimal,
)
.wait(None)
.unwrap();
}
*/
let mut cmd_buffer = self
.canvas
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_render_pass(&self.canvas.pipeline_final)?;
// static background
cmd_buffer.run_ref(&self.pass_bg)?;
for (i, c) in self.controls.iter_mut().enumerate() {
if let Some(render) = c.on_render_hl {
if let Some(test) = c.test_highlight {
if let Some(hl_color) = test(c, &mut self.canvas.data, app) {
render(c, &self.canvas, app, &mut cmd_buffer, hl_color)?;
}
}
if self.hover_controls.contains(&Some(i)) {
render(
c,
&self.canvas,
app,
&mut cmd_buffer,
Vec4::new(1., 1., 1., 0.3),
)?;
}
}
}
// mostly static text
cmd_buffer.run_ref(&self.pass_fg)?;
cmd_buffer.end_render_pass()?;
cmd_buffer.build_and_execute_now()
/*
self.canvas
.graphics
.transition_layout(
image,
ImageLayout::ColorAttachmentOptimal,
ImageLayout::TransferSrcOptimal,
)
.wait(None)
.unwrap();
*/
}
fn view(&mut self) -> Option<Arc<ImageView>> {
Some(self.view_final.clone())
}
fn frame_transform(&mut self) -> Option<FrameTransform> {
Some(FrameTransform {
extent: self.view_final.image().extent(),
..Default::default()
})
}
}
impl<D, S> OverlayBackend for Canvas<D, S> {
fn set_renderer(&mut self, _renderer: Box<dyn OverlayRenderer>) {}
fn set_interaction(&mut self, _interaction: Box<dyn InteractionHandler>) {}
}
+52 -33
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();
@@ -116,30 +124,39 @@ impl FontCache {
let pattern = pattern.font_match(&mut self.fc);
if let Some(path) = pattern.filename() {
log::debug!(
"Loading font: {} {}pt",
pattern.name().unwrap_or(path),
size
);
let name = pattern.name().unwrap_or(path);
log::debug!("Loading font: {} {}pt", name, size);
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 {
for (cp, _) in face.chars() {
if coll.cp_map.contains_key(cp) {
continue;
}
let g = face.get_char_index(cp);
if g > 0 {
coll.cp_map.insert(cp, idx);
}
coll.cp_map.insert(cp, idx);
}
if !coll.cp_map.contains_key(cp) {
log::warn!("Got font '{name}' for CP 0x{cp:x}, but CP is not present in font!",);
coll.cp_map.insert(cp, 0);
}
let zero_glyph = Rc::new(Glyph {
@@ -156,11 +173,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 +187,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: 0x{cp:x}");
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,11 +217,11 @@ 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)?;
let texture = cmd_buffer.texture2d(bmp.width() as _, bmp.rows() as _, format, &buf)?;
let texture = cmd_buffer.texture2d_raw(bmp.width() as _, bmp.rows() as _, format, &buf)?;
cmd_buffer.build_and_execute_now()?;
let g = Glyph {
+40 -723
View File
@@ -1,743 +1,60 @@
use std::sync::Arc;
use anyhow::bail;
use glam::{Vec2, Vec3, Vec4};
use vulkano::{
command_buffer::CommandBufferUsage,
format::Format,
image::{sampler::Filter, view::ImageView, ImageLayout},
};
use crate::{
backend::{
input::{Haptics, InteractionHandler, PointerHit, PointerMode},
overlay::{OverlayBackend, OverlayRenderer},
},
graphics::{
WlxCommandBuffer, WlxGraphics, WlxPass, WlxPipeline, WlxPipelineLegacy, BLEND_ALPHA,
},
state::AppState,
};
use glam::Vec4;
use once_cell::sync::Lazy;
pub mod canvas;
pub mod font;
pub mod modular;
const RES_DIVIDER: usize = 4;
struct Rect {
x: f32,
y: f32,
w: f32,
h: f32,
}
pub type GuiColor = Vec4;
pub(super) static FALLBACK_COLOR: Lazy<GuiColor> = Lazy::new(|| Vec4::new(1., 0., 1., 1.));
// 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!(
anyhow::bail!(
"Invalid color string: '{}', must be 7 characters long (e.g. #FF00FF)",
&html_hex
)
}
pub struct CanvasBuilder<D, S> {
canvas: Canvas<D, S>,
pub fg_color: Vec3,
pub bg_color: Vec3,
pub font_size: isize,
}
impl<D, S> CanvasBuilder<D, S> {
pub fn new(
width: usize,
height: usize,
graphics: Arc<WlxGraphics>,
format: Format,
data: D,
) -> anyhow::Result<Self> {
Ok(Self {
canvas: Canvas::new(width, height, graphics, format, data)?,
bg_color: Vec3::ZERO,
fg_color: Vec3::ONE,
font_size: 16,
})
}
pub fn build(self) -> Canvas<D, S> {
self.canvas
}
// Creates a panel with bg_color inherited from the canvas
pub fn panel(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
bg_color: self.bg_color,
on_render_bg: Some(Control::render_rect),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a label with fg_color, font_size inherited from the canvas
pub fn label(&mut self, x: f32, y: f32, w: f32, h: f32, text: Arc<str>) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
text,
fg_color: self.fg_color,
size: self.font_size,
on_render_fg: Some(Control::render_text),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a label with fg_color, font_size inherited from the canvas
#[allow(dead_code)]
pub fn label_centered(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
text: Arc<str>,
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
text,
fg_color: self.fg_color,
size: self.font_size,
on_render_fg: Some(Control::render_text_centered),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a button with fg_color, bg_color, font_size inherited from the canvas
pub fn button(&mut self, x: f32, y: f32, w: f32, h: f32, text: Arc<str>) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.interactive_set_idx(x, y, w, h, idx);
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
text,
fg_color: self.fg_color,
bg_color: self.bg_color,
size: self.font_size,
on_render_bg: Some(Control::render_rect),
on_render_fg: Some(Control::render_text_centered),
on_render_hl: Some(Control::render_highlight),
..Control::new()
});
&mut self.canvas.controls[idx]
}
pub fn key_button(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
label: &[String],
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.interactive_set_idx(x, y, w, h, idx);
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
bg_color: self.bg_color,
on_render_bg: Some(Control::render_rect),
on_render_hl: Some(Control::render_highlight),
..Control::new()
});
for (i, item) in label.iter().enumerate().take(label.len().min(2)) {
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,
size: self.font_size,
on_render_fg: Some(Control::render_text),
..Control::new()
});
}
&mut self.canvas.controls[idx]
}
}
pub struct CanvasData<D> {
pub data: D,
pub width: usize,
pub height: usize,
graphics: Arc<WlxGraphics>,
pipeline_bg_color: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline_fg_glyph: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline_final: Arc<WlxPipeline<WlxPipelineLegacy>>,
}
pub struct Canvas<D, S> {
controls: Vec<Control<D, S>>,
canvas: CanvasData<D>,
hover_controls: [Option<usize>; 2],
pressed_controls: [Option<usize>; 2],
interact_map: Vec<Option<u8>>,
interact_stride: usize,
interact_rows: usize,
view_final: Arc<ImageView>,
pass_fg: WlxPass<WlxPipelineLegacy>,
pass_bg: WlxPass<WlxPipelineLegacy>,
}
impl<D, S> Canvas<D, S> {
fn new(
width: usize,
height: usize,
graphics: Arc<WlxGraphics>,
format: Format,
data: D,
) -> anyhow::Result<Self> {
let tex_fg = graphics.render_texture(width as _, height as _, format)?;
let tex_bg = graphics.render_texture(width as _, height as _, format)?;
let tex_final = graphics.render_texture(width as _, height as _, format)?;
let view_fg = ImageView::new_default(tex_fg.clone())?;
let view_bg = ImageView::new_default(tex_bg.clone())?;
let view_final = ImageView::new_default(tex_final.clone())?;
let Ok(shaders) = graphics.shared_shaders.read() else {
bail!("Failed to lock shared shaders for reading");
};
let pipeline_bg_color = graphics.create_pipeline(
view_bg.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_color").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let pipeline_fg_glyph = graphics.create_pipeline(
view_fg.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_glyph").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let vertex_buffer =
graphics.upload_verts(width as _, height as _, 0., 0., width as _, height as _)?;
let pipeline_final = graphics.create_pipeline_with_layouts(
view_final.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_sprite").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
ImageLayout::TransferSrcOptimal,
ImageLayout::TransferSrcOptimal,
)?;
let set_fg = pipeline_final.uniform_sampler(0, view_fg.clone(), Filter::Linear)?;
let set_bg = pipeline_final.uniform_sampler(0, view_bg.clone(), Filter::Linear)?;
let pass_fg = pipeline_final.create_pass(
[width as _, height as _],
vertex_buffer.clone(),
graphics.quad_indices.clone(),
vec![set_fg],
)?;
let pass_bg = pipeline_final.create_pass(
[width as _, height as _],
vertex_buffer.clone(),
graphics.quad_indices.clone(),
vec![set_bg],
)?;
let stride = width / RES_DIVIDER;
let rows = height / RES_DIVIDER;
Ok(Self {
canvas: CanvasData {
data,
width,
height,
graphics: graphics.clone(),
pipeline_bg_color,
pipeline_fg_glyph,
pipeline_final,
},
controls: Vec::new(),
hover_controls: [None, None],
pressed_controls: [None, None],
interact_map: vec![None; stride * rows],
interact_stride: stride,
interact_rows: rows,
view_final,
pass_fg,
pass_bg,
})
}
fn interactive_set_idx(&mut self, x: f32, y: f32, w: f32, h: f32, idx: usize) {
let (x, y, w, h) = (x as usize, y as usize, w as usize, h as usize);
let x_min = (x / RES_DIVIDER).max(0);
let y_min = (y / RES_DIVIDER).max(0);
let x_max = (x_min + (w / RES_DIVIDER)).min(self.interact_stride - 1);
let y_max = (y_min + (h / RES_DIVIDER)).min(self.interact_rows - 1);
for y in y_min..y_max {
for x in x_min..x_max {
self.interact_map[y * self.interact_stride + x] = Some(idx as u8);
}
}
}
fn interactive_get_idx(&self, uv: Vec2) -> Option<usize> {
let x = (uv.x * self.canvas.width as f32) as usize;
let y = (uv.y * self.canvas.height as f32) as usize;
let x = (x / RES_DIVIDER).max(0).min(self.interact_stride - 1);
let y = (y / RES_DIVIDER).max(0).min(self.interact_rows - 1);
self.interact_map[y * self.interact_stride + x].map(|x| x as usize)
}
fn render_bg(&mut self, app: &mut AppState) -> anyhow::Result<()> {
let mut cmd_buffer = self
.canvas
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_render_pass(&self.canvas.pipeline_bg_color)?;
for c in self.controls.iter_mut() {
if let Some(fun) = c.on_render_bg {
fun(c, &self.canvas, app, &mut cmd_buffer)?;
}
}
cmd_buffer.end_render_pass()?;
cmd_buffer.build_and_execute_now()
}
fn render_fg(&mut self, app: &mut AppState) -> anyhow::Result<()> {
let mut cmd_buffer = self
.canvas
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_render_pass(&self.canvas.pipeline_fg_glyph)?;
for c in self.controls.iter_mut() {
if let Some(fun) = c.on_render_fg {
fun(c, &self.canvas, app, &mut cmd_buffer)?;
}
}
cmd_buffer.end_render_pass()?;
cmd_buffer.build_and_execute_now()
}
}
impl<D, S> InteractionHandler for Canvas<D, S> {
fn on_left(&mut self, _app: &mut AppState, pointer: usize) {
self.hover_controls[pointer] = None;
}
fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
let old = self.hover_controls[hit.pointer];
if let Some(i) = self.interactive_get_idx(hit.uv) {
self.hover_controls[hit.pointer] = Some(i);
} else {
self.hover_controls[hit.pointer] = None;
}
if old != self.hover_controls[hit.pointer] {
Some(Haptics {
intensity: 0.1,
duration: 0.01,
frequency: 5.0,
})
} else {
None
}
}
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
let idx = if pressed {
self.interactive_get_idx(hit.uv)
} else {
self.pressed_controls[hit.pointer]
};
if let Some(idx) = idx {
let c = &mut self.controls[idx];
if pressed {
if let Some(ref mut f) = c.on_press {
self.pressed_controls[hit.pointer] = Some(idx);
f(c, &mut self.canvas.data, app, hit.mode);
}
} else if let Some(ref mut f) = c.on_release {
self.pressed_controls[hit.pointer] = None;
f(c, &mut self.canvas.data, app);
}
}
}
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta: f32) {
let idx = self.hover_controls[hit.pointer];
if let Some(idx) = idx {
let c = &mut self.controls[idx];
if let Some(ref mut f) = c.on_scroll {
f(c, &mut self.canvas.data, app, delta);
}
}
}
}
impl<D, S> OverlayRenderer for Canvas<D, S> {
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.render_bg(app)?;
self.render_fg(app)
}
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
let mut dirty = false;
for c in self.controls.iter_mut() {
if let Some(fun) = c.on_update {
fun(c, &mut self.canvas.data, app);
}
if c.dirty {
dirty = true;
c.dirty = false;
}
}
if dirty {
self.render_bg(app)?;
self.render_fg(app)?;
}
/*
let image = self.view_final.image().clone();
if self.first_render {
self.first_render = false;
} else {
self.canvas
.graphics
.transition_layout(
image.clone(),
ImageLayout::TransferSrcOptimal,
ImageLayout::ColorAttachmentOptimal,
)
.wait(None)
.unwrap();
}
*/
let mut cmd_buffer = self
.canvas
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_render_pass(&self.canvas.pipeline_final)?;
// static background
cmd_buffer.run_ref(&self.pass_bg)?;
for (i, c) in self.controls.iter_mut().enumerate() {
if let Some(render) = c.on_render_hl {
if let Some(test) = c.test_highlight {
if let Some(hl_color) = test(c, &mut self.canvas.data, app) {
render(c, &self.canvas, app, &mut cmd_buffer, hl_color)?;
}
}
if self.hover_controls.contains(&Some(i)) {
render(
c,
&self.canvas,
app,
&mut cmd_buffer,
Vec4::new(1., 1., 1., 0.3),
)?;
}
}
}
// mostly static text
cmd_buffer.run_ref(&self.pass_fg)?;
cmd_buffer.end_render_pass()?;
cmd_buffer.build_and_execute_now()
/*
self.canvas
.graphics
.transition_layout(
image,
ImageLayout::ColorAttachmentOptimal,
ImageLayout::TransferSrcOptimal,
)
.wait(None)
.unwrap();
*/
}
fn view(&mut self) -> Option<Arc<ImageView>> {
Some(self.view_final.clone())
}
}
impl<D, S> OverlayBackend for Canvas<D, S> {}
pub struct Control<D, S> {
pub state: Option<S>,
rect: Rect,
fg_color: Vec3,
bg_color: Vec3,
text: Arc<str>,
size: isize,
dirty: bool,
pub on_update: Option<fn(&mut Self, &mut D, &mut AppState)>,
pub on_press: Option<fn(&mut Self, &mut D, &mut AppState, PointerMode)>,
pub on_release: Option<fn(&mut Self, &mut D, &mut AppState)>,
pub on_scroll: Option<fn(&mut Self, &mut D, &mut AppState, f32)>,
pub test_highlight: Option<fn(&Self, &mut D, &mut AppState) -> Option<Vec4>>,
on_render_bg: Option<
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<()>,
>,
}
impl<D, S> Control<D, S> {
fn new() -> Self {
Self {
rect: Rect {
x: 0.,
y: 0.,
w: 0.,
h: 0.,
},
fg_color: Vec3::ONE,
bg_color: Vec3::ZERO,
text: Arc::from(""),
dirty: true,
size: 24,
state: None,
on_update: None,
on_render_bg: None,
on_render_hl: None,
on_render_fg: None,
test_highlight: None,
on_press: None,
on_release: None,
on_scroll: None,
}
}
#[inline(always)]
pub fn set_text(&mut self, text: &str) {
if *self.text == *text {
return;
}
self.text = text.into();
self.dirty = true;
}
#[inline(always)]
pub fn set_fg_color(&mut self, color: Vec3) {
if self.fg_color == color {
return;
}
self.fg_color = color;
self.dirty = true;
}
fn render_rect(
&self,
canvas: &CanvasData<D>,
_: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let pass = {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let set0 = canvas.pipeline_bg_color.uniform_buffer(
0,
vec![self.bg_color.x, self.bg_color.y, self.bg_color.z, 1.],
)?;
canvas.pipeline_bg_color.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0],
)?
};
cmd_buffer.run_ref(&pass)
}
fn render_highlight(
&self,
canvas: &CanvasData<D>,
_: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
color: Vec4,
) -> anyhow::Result<()> {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let set0 = canvas
.pipeline_bg_color
.uniform_buffer(0, color.to_array().to_vec())?;
let pass = canvas.pipeline_bg_color.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer.clone(),
canvas.graphics.quad_indices.clone(),
vec![set0],
)?;
cmd_buffer.run_ref(&pass)
}
fn render_text(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let mut cur_y = self.rect.y;
for line in self.text.lines() {
let mut cur_x = self.rect.x;
for glyph in app
.fc
.get_glyphs(line, self.size, canvas.graphics.clone())?
{
if let Some(tex) = glyph.tex.clone() {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
cur_x + glyph.left,
cur_y - glyph.top,
glyph.width,
glyph.height,
)?;
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
0,
ImageView::new_default(tex)?,
Filter::Linear,
)?;
let set1 = canvas.pipeline_fg_glyph.uniform_buffer(
1,
vec![self.fg_color.x, self.fg_color.y, self.fg_color.z, 1.],
)?;
let pass = canvas.pipeline_fg_glyph.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
}
cur_x += glyph.advance;
}
cur_y += (self.size as f32) * 1.5;
}
Ok(())
}
fn render_text_centered(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let (w, h) = app
.fc
.get_text_size(&self.text, self.size, canvas.graphics.clone())?;
let mut cur_y = self.rect.y + (self.rect.h) - (h * 0.5) - (self.size as f32 * 0.25);
for line in self.text.lines() {
let mut cur_x = self.rect.x + (self.rect.w * 0.5) - (w * 0.5);
for glyph in app
.fc
.get_glyphs(line, self.size, canvas.graphics.clone())?
{
if let Some(tex) = glyph.tex.clone() {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
cur_x + glyph.left,
cur_y - glyph.top,
glyph.width,
glyph.height,
)?;
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
0,
ImageView::new_default(tex)?,
Filter::Linear,
)?;
let set1 = canvas.pipeline_fg_glyph.uniform_buffer(
1,
vec![self.fg_color.x, self.fg_color.y, self.fg_color.z, 1.],
)?;
let pass = canvas.pipeline_fg_glyph.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
}
cur_x += glyph.advance;
}
cur_y += (self.size as f32) * 1.5;
}
Ok(())
}
pub enum KeyCapType {
/// Label is in center of keycap
Regular,
/// 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,
}
+285 -57
View File
@@ -1,32 +1,44 @@
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},
hid::VirtualKey,
overlays::{
toast::Toast,
watch::{save_watch, WATCH_NAME},
toast::{error_toast, Toast, ToastTopic},
watch::WATCH_NAME,
},
state::AppState,
};
#[cfg(any(not(feature = "wayvr"), not(feature = "osc")))]
use crate::overlays::toast::error_toast_str;
#[cfg(feature = "osc")]
use rosc::OscType;
use super::{ExecArgs, ModularControl, ModularData};
#[derive(Deserialize, Clone)]
pub enum PressRelease {
Release,
Press,
}
#[derive(Deserialize, Clone, Copy)]
pub enum ViewAngleKind {
/// The cosine of the angle at which the watch becomes fully transparent
@@ -42,14 +54,27 @@ pub enum Axis {
Z,
}
#[derive(Deserialize, Clone)]
pub enum HighlightTest {
AllowSliding,
AutoRealign,
NotificationSounds,
Notifications,
RorateLock,
}
#[derive(Deserialize, Clone)]
pub enum SystemAction {
ToggleAllowSliding,
ToggleAutoRealign,
ToggleNotificationSounds,
ToggleNotifications,
ToggleRotateLock,
PlayspaceResetOffset,
PlayspaceFixFloor,
RecalculateExtent,
PersistConfig,
PersistLayout,
}
#[derive(Deserialize, Clone)]
@@ -97,6 +122,26 @@ pub enum WindowAction {
Destroy,
}
#[derive(Deserialize, Clone)]
pub enum WayVRDisplayClickAction {
ToggleVisibility,
Reset,
}
#[derive(Deserialize, Clone)]
#[allow(dead_code)] // in case if WayVR feature is disabled
pub enum WayVRAction {
AppClick {
catalog_name: Arc<str>,
app_name: Arc<str>,
},
DisplayClick {
display_name: Arc<str>,
action: WayVRDisplayClickAction,
},
ToggleDashboard,
}
#[derive(Deserialize, Clone)]
#[serde(tag = "type")]
pub enum ButtonAction {
@@ -104,6 +149,10 @@ pub enum ButtonAction {
command: ExecArgs,
toast: Option<Arc<str>>,
},
VirtualKey {
keycode: VirtualKey,
action: PressRelease,
},
Watch {
action: WatchAction,
},
@@ -111,6 +160,10 @@ pub enum ButtonAction {
target: OverlaySelector,
action: OverlayAction,
},
// Ignored if "wayvr" feature is not enabled
WayVR {
action: WayVRAction,
},
Window {
target: Arc<str>,
action: WindowAction,
@@ -124,9 +177,32 @@ pub enum ButtonAction {
channel: ColorChannel,
delta: f32,
},
DragMultiplier {
delta: f32,
},
System {
action: SystemAction,
},
SendOscValue {
parameter: Arc<str>,
values: Option<Vec<OscValue>>,
},
}
#[derive(Deserialize, Clone)]
#[serde(tag = "type")]
#[cfg(feature = "osc")]
pub enum OscValue {
Int { value: i32 },
Float { value: f32 },
String { value: String },
Bool { value: bool },
}
#[derive(Deserialize, Clone)]
#[serde(tag = "type")]
#[cfg(not(feature = "osc"))]
pub enum OscValue {
None,
}
pub(super) struct PressData {
@@ -169,6 +245,7 @@ pub struct ButtonData {
pub(super) long_middle_up: Option<Vec<ButtonAction>>,
pub(super) scroll_down: Option<Vec<ButtonAction>>,
pub(super) scroll_up: Option<Vec<ButtonAction>>,
pub(super) highlight: Option<HighlightTest>,
}
pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) {
@@ -176,6 +253,7 @@ pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) {
button.on_press = Some(modular_button_dn);
button.on_release = Some(modular_button_up);
button.on_scroll = Some(modular_button_scroll);
button.test_highlight = Some(modular_button_highlight);
}
fn modular_button_dn(
@@ -267,20 +345,64 @@ 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,
HighlightTest::RorateLock => !app.session.config.space_rotate_unlocked,
};
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),
ButtonAction::Watch { action } => run_watch(action, app),
ButtonAction::Overlay { target, action } => run_overlay(target, action, app),
ButtonAction::Window { target, action } => run_window(target, action, app),
ButtonAction::WayVR { action } => {
#[cfg(feature = "wayvr")]
{
app.tasks.enqueue(TaskType::WayVR(action.clone()));
}
#[cfg(not(feature = "wayvr"))]
{
let _ = &action;
error_toast_str(app, "WayVR feature is not enabled");
}
}
ButtonAction::VirtualKey { keycode, action } => app
.hid_provider
.send_key(*keycode, matches!(*action, PressRelease::Press)),
ButtonAction::Toast {
message,
body,
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;
@@ -289,11 +411,83 @@ fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppStat
.enqueue(TaskType::System(SystemTask::ColorGain(channel, delta)));
}
ButtonAction::System { action } => run_system(action, app),
ButtonAction::DragMultiplier { delta } => {
app.session.config.space_drag_multiplier += delta;
}
ButtonAction::SendOscValue { parameter, values } => {
#[cfg(feature = "osc")]
if let Some(ref mut sender) = app.osc_sender {
// convert OscValue to OscType
let mut converted: Vec<OscType> = Vec::new();
for value in values.as_ref().unwrap() {
let converted_value = match value {
OscValue::Bool { value } => OscType::Bool(*value),
OscValue::Int { value } => OscType::Int(*value),
OscValue::Float { value } => OscType::Float(*value),
OscValue::String { value } => OscType::String(value.to_string()),
};
converted.push(converted_value);
}
let _ = sender.send_single_param(parameter.to_string(), converted);
audio_thump(app); // play sound for feedback
};
#[cfg(not(feature = "osc"))]
{
let _ = &parameter;
let _ = &values;
error_toast_str(app, "OSC feature is not enabled");
}
}
}
}
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::ToggleRotateLock => {
app.session.config.space_rotate_unlocked = !app.session.config.space_rotate_unlocked;
Toast::new(
ToastTopic::System,
format!(
"Space rotate axis lock now {}.",
ENABLED_DISABLED[!app.session.config.space_rotate_unlocked as usize]
)
.into(),
"".into(),
)
.submit(app);
}
SystemAction::PlayspaceResetOffset => {
app.tasks
.enqueue(TaskType::System(SystemTask::ResetPlayspace));
@@ -305,6 +499,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(),
)
@@ -320,13 +515,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(),
@@ -337,13 +529,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(),
@@ -351,11 +540,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) {
error_toast(app, "Failed to save config", e);
}
}
SystemAction::PersistLayout => {
if let Err(e) = save_layout(&app.session.config) {
error_toast(app, "Failed to save layout", e);
}
}
}
@@ -366,7 +557,7 @@ fn run_exec(args: &ExecArgs, toast: &Option<Arc<str>>, press: &mut PressData, ap
match proc.try_wait() {
Ok(Some(code)) => {
if !code.success() {
log::error!("Child process exited with code: {}", code);
error_toast(app, "Child process exited with code", code);
}
press.child = None;
}
@@ -376,7 +567,7 @@ fn run_exec(args: &ExecArgs, toast: &Option<Arc<str>>, press: &mut PressData, ap
}
Err(e) => {
press.child = None;
log::error!("Error checking child process: {:?}", e);
error_toast(app, "Error checking child process", e);
}
}
}
@@ -385,11 +576,11 @@ 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) => {
log::error!("Failed to spawn process {:?}: {:?}", args, e);
error_toast(app, &format!("Failed to spawn process {:?}", args), e);
}
};
}
@@ -400,11 +591,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(),
)
@@ -412,8 +603,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);
}
}),
));
@@ -425,20 +617,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,7 +671,7 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
OverlaySelector::Name(WATCH_NAME.into()),
Box::new(move |app, o| {
o.spawn_rotation *= rot;
app.session.config.watch_rot = o.spawn_rotation.into();
app.session.config.watch_rot = o.spawn_rotation;
o.dirty = true;
}),
));
@@ -491,7 +687,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;
}),
));
@@ -506,7 +702,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);
}),
));
}
@@ -519,6 +720,22 @@ 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) => {
error_toast(app, "Failed to save state", e);
}
}
}
}),
));
}
@@ -531,13 +748,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);
}
}),
));
@@ -550,13 +772,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);
}
}),
));
@@ -587,9 +814,13 @@ fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
Box::new({
let name = window.clone();
move |app| {
Toast::new("Check your desktop for popup.".into(), "".into())
.with_sound(true)
.submit(app);
Toast::new(
ToastTopic::System,
"Check your desktop for popup.".into(),
"".into(),
)
.with_sound(true)
.submit(app);
crate::overlays::mirror::new_mirror(name.clone(), false, &app.session)
}
}),
@@ -613,11 +844,8 @@ fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
}
}
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);
}
+109 -21
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,
@@ -9,11 +9,22 @@ use std::{
time::Instant,
};
use crate::{gui::modular::FALLBACK_COLOR, state::AppState};
use crate::{
gui::modular::FALLBACK_COLOR,
overlays::toast::{error_toast, error_toast_str},
state::AppState,
};
use serde::Deserialize;
use super::{color_parse_or_default, ExecArgs, ModularControl, ModularData};
use super::{color_parse_or_default, ExecArgs, GuiColor, ModularControl, ModularData};
#[derive(Deserialize)]
#[serde(untagged)]
pub enum TimezoneDef {
Idx(usize),
Str(Arc<str>),
}
#[derive(Deserialize)]
#[serde(tag = "source")]
@@ -27,7 +38,13 @@ pub enum LabelContent {
},
Clock {
format: Arc<str>,
timezone: Option<Arc<str>>,
timezone: Option<TimezoneDef>,
},
Timezone {
timezone: usize,
},
Timer {
format: Arc<str>,
},
Battery {
device: usize,
@@ -35,29 +52,39 @@ pub enum LabelContent {
low_color: Arc<str>,
charging_color: Arc<str>,
},
DragMultiplier,
Ipd,
}
pub enum LabelData {
Battery {
device: usize,
low_threshold: f32,
normal_color: Vec3,
low_color: Vec3,
charging_color: Vec3,
normal_color: GuiColor,
low_color: GuiColor,
charging_color: GuiColor,
},
Clock {
format: Arc<str>,
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) {
pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent, app: &AppState) {
let state = match content {
LabelContent::Battery {
device,
@@ -72,19 +99,53 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
charging_color: color_parse_or_default(charging_color),
}),
LabelContent::Clock { format, timezone } => {
let tz: Option<Tz> = timezone.as_ref().map(|tz| {
tz.parse().unwrap_or_else(|_| {
log::error!("Failed to parse timezone '{}'", &tz);
label.set_fg_color(FALLBACK_COLOR);
Tz::UTC
})
});
let tz_str = match timezone {
Some(TimezoneDef::Idx(idx)) => {
if let Some(tz) = app.session.config.timezones.get(*idx) {
Some(tz.as_str())
} else {
log::error!("Timezone index out of range '{}'", idx);
label.set_fg_color(*FALLBACK_COLOR);
None
}
}
Some(TimezoneDef::Str(tz_str)) => Some(tz_str.as_ref()),
None => None,
};
Some(LabelData::Clock {
format: format.clone(),
timezone: tz,
timezone: tz_str.and_then(|tz| {
tz.parse()
.map_err(|_| {
log::error!("Failed to parse timezone '{}'", &tz);
label.set_fg_color(*FALLBACK_COLOR);
})
.ok()
}),
})
}
LabelContent::Timezone { timezone } => {
if let Some(tz) = app.session.config.timezones.get(*timezone) {
let pretty_tz = tz.split('/').last().map(|x| x.replace("_", " "));
if let Some(pretty_tz) = pretty_tz {
label.set_text(&pretty_tz);
return;
} else {
log::error!("Timezone name not valid '{}'", &tz);
}
} else {
log::error!("Timezone index out of range '{}'", &timezone);
}
label.set_fg_color(*FALLBACK_COLOR);
label.set_text("Error");
None
}
LabelContent::Timer { format } => Some(LabelData::Timer {
format: format.clone(),
start: Instant::now(),
}),
LabelContent::Exec { command, interval } => Some(LabelData::Exec {
last_exec: Instant::now(),
interval: *interval,
@@ -95,6 +156,8 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
label.set_text(text);
None
}
LabelContent::Ipd => Some(LabelData::Ipd { last_ipd: -1. }),
LabelContent::DragMultiplier => Some(LabelData::DragMultiplier),
};
if let Some(state) = state {
@@ -104,6 +167,7 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
}
pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut AppState) {
// want panic
let ModularData::Label(data) = control.state.as_mut().unwrap() else {
panic!("Label control has no state");
};
@@ -137,7 +201,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 +219,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,
@@ -165,14 +237,21 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
match proc.try_wait() {
Ok(Some(code)) => {
if !code.success() {
log::error!("Child process exited with code: {}", code);
error_toast(
app,
"LabelData::Exec: Child process exited with code",
code,
);
} else {
if let Some(mut stdout) = proc.stdout.take() {
let mut buf = String::new();
if stdout.read_to_string(&mut buf).is_ok() {
control.set_text(&buf);
} else {
log::error!("Failed to read stdout for child process");
error_toast_str(
app,
"LabelData::Exec: Failed to read stdout for child process",
);
return;
}
return;
@@ -188,7 +267,7 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
}
Err(e) => {
*child = None;
log::error!("Error checking child process: {:?}", e);
error_toast(app, "Error checking child process", e);
return;
}
}
@@ -214,10 +293,19 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
*child = Some(proc);
}
Err(e) => {
log::error!("Failed to spawn process {:?}: {:?}", args, e);
error_toast(app, &format!("Failed to spawn process {:?}", args), e);
}
};
}
}
LabelData::Ipd { last_ipd } => {
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));
}
}
}
+259 -26
View File
@@ -1,30 +1,33 @@
pub mod button;
pub mod label;
//pub mod slider;
use std::sync::Arc;
use std::{fs::File, sync::Arc};
use glam::Vec3;
#[cfg(feature = "wayvr")]
use button::{WayVRAction, WayVRDisplayClickAction};
use glam::Vec4;
use serde::Deserialize;
use vulkano::{command_buffer::CommandBufferUsage, image::view::ImageView};
use crate::{backend::common::OverlaySelector, state::AppState};
use crate::{
backend::common::OverlaySelector, config::AStrMapExt, config_io,
graphics::dds::WlxCommandBufferDds, state::AppState,
};
use self::{
button::{modular_button_init, ButtonAction, ButtonData, OverlayAction},
label::{modular_label_init, LabelContent, LabelData},
};
use super::{color_parse, Canvas, CanvasBuilder, Control};
use super::{
canvas::{builder::CanvasBuilder, control::Control, Canvas},
color_parse, GuiColor, FALLBACK_COLOR,
};
type ModularControl = Control<(), ModularData>;
type ExecArgs = Vec<Arc<str>>;
const FALLBACK_COLOR: Vec3 = Vec3 {
x: 1.,
y: 0.,
z: 1.,
};
#[derive(Deserialize)]
pub struct ModularUiConfig {
pub width: f32,
@@ -54,17 +57,33 @@ pub struct OverlayListTemplate {
pub enum ModularElement {
Panel {
rect: [f32; 4],
corner_radius: Option<f32>,
bg_color: Arc<str>,
},
Label {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
#[serde(flatten)]
data: LabelContent,
},
CenteredLabel {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
#[serde(flatten)]
data: LabelContent,
},
Sprite {
rect: [f32; 4],
sprite: Arc<str>,
sprite_st: Option<[f32; 4]>,
},
Button {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
@@ -75,6 +94,7 @@ pub enum ModularElement {
/// Convenience type to save you from having to create a bunch of labels
BatteryList {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
fg_color_low: Arc<str>,
@@ -85,6 +105,7 @@ pub enum ModularElement {
},
OverlayList {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
@@ -92,6 +113,23 @@ pub enum ModularElement {
#[serde(flatten)]
template: Box<OverlayListTemplate>,
},
// Ignored if "wayvr" feature is not enabled
WayVRLauncher {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
catalog_name: Arc<str>,
},
// Ignored if "wayvr" feature is not enabled
WayVRDisplayList {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
},
}
#[derive(Deserialize, Clone)]
@@ -114,13 +152,13 @@ pub enum ModularData {
pub fn modular_canvas(
size: &[u32; 2],
elements: &[ModularElement],
state: &AppState,
state: &mut AppState,
) -> anyhow::Result<Canvas<(), ModularData>> {
let mut canvas = CanvasBuilder::new(
size[0] as _,
size[1] as _,
state.graphics.clone(),
state.format,
state.graphics.native_format,
(),
)?;
let empty_str: Arc<str> = Arc::from("");
@@ -128,38 +166,94 @@ pub fn modular_canvas(
match elem {
ModularElement::Panel {
rect: [x, y, w, h],
corner_radius,
bg_color,
} => {
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
canvas.panel(*x, *y, *w, *h);
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
canvas.panel(*x, *y, *w, *h, corner_radius.unwrap_or_default());
}
ModularElement::Label {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
data,
} => {
canvas.font_size = *font_size;
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
let label = canvas.label(*x, *y, *w, *h, empty_str.clone());
modular_label_init(label, data);
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
let label = canvas.label(
*x,
*y,
*w,
*h,
corner_radius.unwrap_or_default(),
empty_str.clone(),
);
modular_label_init(label, data, state);
}
ModularElement::CenteredLabel {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
data,
} => {
canvas.font_size = *font_size;
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
let label = canvas.label_centered(
*x,
*y,
*w,
*h,
corner_radius.unwrap_or_default(),
empty_str.clone(),
);
modular_label_init(label, data, state);
}
ModularElement::Sprite {
rect: [x, y, w, h],
sprite,
sprite_st,
} => match sprite_from_path(sprite.clone(), state) {
Ok(view) => {
let sprite = canvas.sprite(*x, *y, *w, *h);
sprite.fg_color = Vec4::ONE;
sprite.set_sprite(view);
let st = sprite_st
.map(|st| Vec4::from_slice(&st))
.unwrap_or_else(|| Vec4::new(1., 1., 0., 0.));
sprite.set_sprite_st(st);
}
Err(e) => {
log::warn!("Could not load custom UI sprite: {:?}", e);
}
},
ModularElement::Button {
rect: [x, y, w, h],
corner_radius,
font_size,
bg_color,
fg_color,
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());
let button = canvas.button(
*x,
*y,
*w,
*h,
corner_radius.unwrap_or_default(),
text.clone(),
);
modular_button_init(button, data);
}
ModularElement::BatteryList {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
fg_color_low,
@@ -177,7 +271,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;
@@ -187,6 +281,7 @@ pub fn modular_canvas(
button_y + 2.,
button_w - 4.,
button_h - 4.,
corner_radius.unwrap_or_default(),
empty_str.clone(),
);
modular_label_init(
@@ -197,6 +292,7 @@ pub fn modular_canvas(
low_color: fg_color_low.clone(),
charging_color: fg_color_charging.clone(),
},
state,
);
button_x += match layout {
@@ -211,6 +307,7 @@ pub fn modular_canvas(
}
ModularElement::OverlayList {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
bg_color,
@@ -225,8 +322,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() {
@@ -235,6 +332,7 @@ pub fn modular_canvas(
button_y + 2.,
button_w - 4.,
button_h - 4.,
corner_radius.unwrap_or_default(),
screen.name.clone(),
);
@@ -321,14 +419,149 @@ pub fn modular_canvas(
};
}
}
#[allow(unused_variables)] // needed in case if wayvr feature is not enabled
ModularElement::WayVRLauncher {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
bg_color,
catalog_name,
} => {
#[cfg(feature = "wayvr")]
{
if let Some(catalog) = state.session.wayvr_config.get_catalog(catalog_name) {
let mut button_x = *x;
let button_y = *y;
for app in &catalog.apps {
let button_w: f32 = *w / catalog.apps.len() as f32;
let button_h: f32 = *h;
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
canvas.font_size = *font_size;
let button = canvas.button(
button_x + 2.,
button_y + 2.,
button_w - 4.,
button_h - 4.,
corner_radius.unwrap_or_default(),
Arc::from(app.name.as_str()),
);
let data = ButtonData {
click_up: Some(vec![ButtonAction::WayVR {
action: WayVRAction::AppClick {
catalog_name: catalog_name.clone(),
app_name: Arc::from(app.name.as_str()),
},
}]),
..Default::default()
};
modular_button_init(button, &data);
button_x += button_w;
}
} else {
log::error!("WayVR catalog \"{}\" not found", catalog_name);
}
}
#[cfg(not(feature = "wayvr"))]
{
log::error!("WayVR feature is not enabled, ignoring");
}
}
#[allow(unused_variables)]
ModularElement::WayVRDisplayList {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
bg_color,
} => {
#[cfg(feature = "wayvr")]
{
let mut button_x = *x;
let button_y = *y;
let displays = &state.session.wayvr_config.displays;
for (display_name, display) in displays {
let button_w: f32 = (*w / displays.len() as f32).min(80.0);
let button_h: f32 = *h;
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
canvas.font_size = *font_size;
let button = canvas.button(
button_x + 2.,
button_y + 2.,
button_w - 4.,
button_h - 4.,
corner_radius.unwrap_or_default(),
Arc::from(display_name.as_str()),
);
let data = ButtonData {
click_up: Some(vec![ButtonAction::WayVR {
action: WayVRAction::DisplayClick {
display_name: Arc::from(display_name.as_str()),
action: WayVRDisplayClickAction::ToggleVisibility,
},
}]),
long_click_up: Some(vec![ButtonAction::WayVR {
action: WayVRAction::DisplayClick {
display_name: Arc::from(display_name.as_str()),
action: WayVRDisplayClickAction::Reset,
},
}]),
..Default::default()
};
modular_button_init(button, &data);
button_x += button_w;
}
}
#[cfg(not(feature = "wayvr"))]
{
log::error!("WayVR feature is not enabled, ignoring")
}
}
}
}
Ok(canvas.build())
}
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
})
}
fn sprite_from_path(path: Arc<str>, app: &mut AppState) -> anyhow::Result<Arc<ImageView>> {
if let Some(view) = app.sprites.arc_get(&path) {
return Ok(view.clone());
}
let real_path = config_io::get_config_root().join(&*path);
let Ok(f) = File::open(real_path) else {
anyhow::bail!("Could not open custom sprite at: {}", path);
};
let mut command_buffer = app
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
match command_buffer.texture2d_dds(f) {
Ok(image) => {
command_buffer.build_and_execute_now()?;
Ok(ImageView::new_default(image)?)
}
Err(e) => {
anyhow::bail!("Could not use custom sprite at: {}\n{:?}", path, e);
}
}
}
-442
View File
@@ -1,442 +0,0 @@
use glam::Vec2;
use idmap::{idmap, IdMap};
use idmap_derive::IntegerId;
use input_linux::{
AbsoluteAxis, AbsoluteInfo, AbsoluteInfoSetup, EventKind, InputId, Key, RelativeAxis,
UInputHandle,
};
use libc::{input_event, timeval};
use once_cell::sync::Lazy;
use std::fs::File;
use std::mem::transmute;
use strum::{EnumIter, EnumString, IntoEnumIterator};
pub fn initialize() -> Box<dyn HidProvider> {
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!("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 set_modifiers(&mut self, mods: u8);
fn send_key(&self, key: u16, down: bool);
fn set_desktop_extent(&mut self, extent: Vec2);
fn on_new_frame(&mut self);
}
pub struct UInputProvider {
handle: UInputHandle<File>,
desktop_extent: Vec2,
mouse_moved: bool,
cur_modifiers: u8,
}
pub struct DummyProvider;
pub const MOUSE_LEFT: u16 = 0x110;
pub const MOUSE_RIGHT: u16 = 0x111;
pub const MOUSE_MIDDLE: u16 = 0x112;
const MOUSE_EXTENT: f32 = 32768.;
const EV_SYN: u16 = 0x0;
const EV_KEY: u16 = 0x1;
const EV_REL: u16 = 0x2;
const EV_ABS: u16 = 0x3;
impl UInputProvider {
fn try_new() -> Option<Self> {
if let Ok(file) = File::create("/dev/uinput") {
let handle = UInputHandle::new(file);
let id = InputId {
bustype: 0x03,
vendor: 0x4711,
product: 0x0829,
version: 5,
};
let name = b"WlxOverlay-S Keyboard-Mouse Hybrid Thing\0";
let abs_info = vec![
AbsoluteInfoSetup {
axis: input_linux::AbsoluteAxis::X,
info: AbsoluteInfo {
value: 0,
minimum: 0,
maximum: MOUSE_EXTENT as _,
fuzz: 0,
flat: 0,
resolution: 10,
},
},
AbsoluteInfoSetup {
axis: input_linux::AbsoluteAxis::Y,
info: AbsoluteInfo {
value: 0,
minimum: 0,
maximum: MOUSE_EXTENT as _,
fuzz: 0,
flat: 0,
resolution: 10,
},
},
];
if handle.set_evbit(EventKind::Key).is_err() {
return None;
}
if handle.set_evbit(EventKind::Absolute).is_err() {
return None;
}
if handle.set_evbit(EventKind::Relative).is_err() {
return None;
}
for btn in MOUSE_LEFT..=MOUSE_MIDDLE {
let key: Key = unsafe { transmute(btn) };
if handle.set_keybit(key).is_err() {
return None;
}
}
for key in VirtualKey::iter() {
let key: Key = unsafe { transmute((key as u16) - 8) };
if handle.set_keybit(key).is_err() {
return None;
}
}
if handle.set_absbit(AbsoluteAxis::X).is_err() {
return None;
}
if handle.set_absbit(AbsoluteAxis::Y).is_err() {
return None;
}
if handle.set_relbit(RelativeAxis::Wheel).is_err() {
return None;
}
if handle.create(&id, name, 0, &abs_info).is_ok() {
return Some(UInputProvider {
handle,
desktop_extent: Vec2::ZERO,
mouse_moved: false,
cur_modifiers: 0,
});
}
}
None
}
}
impl HidProvider for UInputProvider {
fn mouse_move(&mut self, pos: Vec2) {
if self.mouse_moved {
return;
}
self.mouse_moved = true;
#[cfg(debug_assertions)]
log::trace!("Mouse move: {:?}", pos);
let pos = pos * (MOUSE_EXTENT / self.desktop_extent);
let time = get_time();
let events = [
new_event(time, EV_ABS, AbsoluteAxis::X as _, pos.x as i32),
new_event(time, EV_ABS, AbsoluteAxis::Y as _, pos.y as i32),
new_event(time, EV_SYN, 0, 0),
];
if let Err(res) = self.handle.write(&events) {
log::error!("{}", res.to_string());
}
}
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) {
let time = get_time();
let events = [
new_event(time, EV_REL, RelativeAxis::Wheel as _, delta),
new_event(time, EV_SYN, 0, 0),
];
if let Err(res) = self.handle.write(&events) {
log::error!("wheel: {}", res.to_string());
}
}
fn set_modifiers(&mut self, modifiers: u8) {
let changed = self.cur_modifiers ^ modifiers;
for i in 0..7 {
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.cur_modifiers = modifiers;
}
fn send_key(&self, key: u16, down: bool) {
let time = get_time();
let events = [
new_event(time, EV_KEY, key - 8, down as _),
new_event(time, EV_SYN, 0, 0),
];
if let Err(res) = self.handle.write(&events) {
log::error!("send_key: {}", res.to_string());
}
}
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;
}
}
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 set_modifiers(&mut self, _modifiers: u8) {}
fn send_key(&self, _key: u16, _down: bool) {}
fn set_desktop_extent(&mut self, _extent: Vec2) {}
fn on_new_frame(&mut self) {}
}
#[inline]
fn get_time() -> timeval {
let mut time = timeval {
tv_sec: 0,
tv_usec: 0,
};
unsafe { libc::gettimeofday(&mut time, std::ptr::null_mut()) };
time
}
#[inline]
fn new_event(time: timeval, type_: u16, code: u16, value: i32) -> input_event {
input_event {
time,
type_,
code,
value,
}
}
pub type KeyModifier = u8;
pub const SHIFT: KeyModifier = 0x01;
pub const CAPS_LOCK: KeyModifier = 0x02;
pub const CTRL: KeyModifier = 0x04;
pub const ALT: KeyModifier = 0x08;
pub const NUM_LOCK: KeyModifier = 0x10;
pub const SUPER: KeyModifier = 0x40;
pub const META: KeyModifier = 0x80;
#[allow(non_camel_case_types)]
#[repr(u16)]
#[derive(Debug, PartialEq, Clone, Copy, IntegerId, EnumString, EnumIter)]
pub enum VirtualKey {
Escape = 9,
N1, // number row
N2,
N3,
N4,
N5,
N6,
N7,
N8,
N9,
N0,
Minus,
Plus,
BackSpace,
Tab,
Q,
W,
E,
R,
T,
Y,
U,
I,
O,
P,
Oem4, // [ {
Oem6, // ] }
Return,
LCtrl,
A,
S,
D,
F,
G,
H,
J,
K,
L,
Oem1, // ; :
Oem7, // ' "
Oem3, // ` ~
LShift,
Oem5, // \ |
Z,
X,
C,
V,
B,
N,
M,
Comma, // , <
Period, // . >
Oem2, // / ?
RShift,
KP_Multiply,
LAlt,
Space,
Caps,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
NumLock,
Scroll,
KP_7, // KeyPad
KP_8,
KP_9,
KP_Subtract,
KP_4,
KP_5,
KP_6,
KP_Add,
KP_1,
KP_2,
KP_3,
KP_0,
KP_Decimal,
Oem102 = 94, // Optional key usually between LShift and Z
F11,
F12,
AbntC1,
Katakana,
Hiragana,
Henkan,
Kana,
Muhenkan,
KP_Enter = 104,
RCtrl,
KP_Divide,
Print,
Meta, // Right Alt aka AltGr
Home = 110,
Up,
Prior,
Left,
Right,
End,
Down,
Next,
Insert,
Delete,
XF86AudioMute = 121,
XF86AudioLowerVolume,
XF86AudioRaiseVolume,
Pause = 127,
AbntC2 = 129,
Hangul,
Hanja,
LSuper = 133,
RSuper,
Menu,
Help = 146,
XF86MenuKB,
XF86Sleep = 150,
XF86Xfer = 155,
XF86Launch1,
XF86Launch2,
XF86WWW,
XF86Mail = 163,
XF86Favorites,
XF86MyComputer,
XF86Back,
XF86Forward,
XF86AudioNext = 171,
XF86AudioPlay,
XF86AudioPrev,
XF86AudioStop,
XF86HomePage = 180,
XF86Reload,
F13 = 191,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
Hyper = 207,
XF86Launch3,
XF86Launch4,
XF86LaunchB,
XF86Search = 225,
}
pub static KEYS_TO_MODS: Lazy<IdMap<VirtualKey, KeyModifier>> = Lazy::new(|| {
idmap! {
VirtualKey::LShift => SHIFT,
VirtualKey::RShift => SHIFT,
VirtualKey::Caps => CAPS_LOCK,
VirtualKey::LCtrl => CTRL,
VirtualKey::RCtrl => CTRL,
VirtualKey::LAlt => ALT,
VirtualKey::NumLock => NUM_LOCK,
VirtualKey::LSuper => SUPER,
VirtualKey::RSuper => SUPER,
VirtualKey::Meta => META,
}
});
pub static MODS_TO_KEYS: Lazy<IdMap<KeyModifier, Vec<VirtualKey>>> = Lazy::new(|| {
idmap! {
SHIFT => vec![VirtualKey::LShift, VirtualKey::RShift],
CAPS_LOCK => vec![VirtualKey::Caps],
CTRL => vec![VirtualKey::LCtrl, VirtualKey::RCtrl],
ALT => vec![VirtualKey::LAlt],
NUM_LOCK => vec![VirtualKey::NumLock],
SUPER => vec![VirtualKey::LSuper, VirtualKey::RSuper],
META => vec![VirtualKey::Meta],
}
});
+594
View File
@@ -0,0 +1,594 @@
use glam::Vec2;
use idmap::{idmap, IdMap};
use idmap_derive::IntegerId;
use input_linux::{
AbsoluteAxis, AbsoluteInfo, AbsoluteInfoSetup, EventKind, InputId, Key, RelativeAxis,
UInputHandle,
};
use libc::{input_event, timeval};
use once_cell::sync::Lazy;
use serde::Deserialize;
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!("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(&mut self, button: u16, down: bool);
fn wheel(&mut self, delta: i32);
fn set_modifiers(&mut self, mods: u8);
fn send_key(&self, key: VirtualKey, down: bool);
fn set_desktop_extent(&mut self, extent: Vec2);
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 {
keyboard_handle: UInputHandle<File>,
mouse_handle: UInputHandle<File>,
desktop_extent: Vec2,
desktop_origin: Vec2,
cur_modifiers: u8,
current_action: MouseAction,
}
pub struct DummyProvider;
pub const MOUSE_LEFT: u16 = 0x110;
pub const MOUSE_RIGHT: u16 = 0x111;
pub const MOUSE_MIDDLE: u16 = 0x112;
const MOUSE_EXTENT: f32 = 32768.;
const EV_SYN: u16 = 0x0;
const EV_KEY: u16 = 0x1;
const EV_REL: u16 = 0x2;
const EV_ABS: u16 = 0x3;
impl UInputProvider {
fn try_new() -> Option<Self> {
let keyboard_file = File::create("/dev/uinput").ok()?;
let keyboard_handle = UInputHandle::new(keyboard_file);
let mouse_file = File::create("/dev/uinput").ok()?;
let mouse_handle = UInputHandle::new(mouse_file);
let kbd_id = InputId {
bustype: 0x03,
vendor: 0x4711,
product: 0x0829,
version: 5,
};
let mouse_id = InputId {
bustype: 0x03,
vendor: 0x4711,
product: 0x0830,
version: 5,
};
let kbd_name = b"WlxOverlay-S Keyboard\0";
let mouse_name = b"WlxOverlay-S Mouse\0";
let abs_info = vec![
AbsoluteInfoSetup {
axis: input_linux::AbsoluteAxis::X,
info: AbsoluteInfo {
value: 0,
minimum: 0,
maximum: MOUSE_EXTENT as _,
fuzz: 0,
flat: 0,
resolution: 10,
},
},
AbsoluteInfoSetup {
axis: input_linux::AbsoluteAxis::Y,
info: AbsoluteInfo {
value: 0,
minimum: 0,
maximum: MOUSE_EXTENT as _,
fuzz: 0,
flat: 0,
resolution: 10,
},
},
];
keyboard_handle.set_evbit(EventKind::Key).ok()?;
for key in VirtualKey::iter() {
let mapped_key: Key = unsafe { std::mem::transmute((key as u16) - 8) };
keyboard_handle.set_keybit(mapped_key).ok()?;
}
keyboard_handle.create(&kbd_id, kbd_name, 0, &[]).ok()?;
mouse_handle.set_evbit(EventKind::Absolute).ok()?;
mouse_handle.set_evbit(EventKind::Relative).ok()?;
mouse_handle.set_absbit(AbsoluteAxis::X).ok()?;
mouse_handle.set_absbit(AbsoluteAxis::Y).ok()?;
mouse_handle.set_relbit(RelativeAxis::Wheel).ok()?;
mouse_handle.set_evbit(EventKind::Key).ok()?;
for btn in MOUSE_LEFT..=MOUSE_MIDDLE {
let mouse_btn: Key = unsafe { transmute(btn) };
mouse_handle.set_keybit(mouse_btn).ok()?;
}
mouse_handle.create(&mouse_id, mouse_name, 0, &abs_info).ok()?;
Some(UInputProvider {
keyboard_handle,
mouse_handle,
desktop_extent: Vec2::ZERO,
desktop_origin: Vec2::ZERO,
current_action: Default::default(),
cur_modifiers: 0,
})
}
fn send_button_internal(&self, button: u16, down: bool) {
let time = get_time();
let events = [
new_event(time, EV_KEY, button, down as _),
new_event(time, EV_SYN, 0, 0),
];
if let Err(res) = self.mouse_handle.write(&events) {
log::error!("send_button: {}", res.to_string());
}
}
fn mouse_move_internal(&mut self, pos: Vec2) {
#[cfg(debug_assertions)]
log::trace!("Mouse move: {:?}", pos);
let pos = (pos - self.desktop_origin) * (MOUSE_EXTENT / self.desktop_extent);
let time = get_time();
let events = [
new_event(time, EV_ABS, AbsoluteAxis::X as _, pos.x as i32),
new_event(time, EV_ABS, AbsoluteAxis::Y as _, pos.y as i32),
new_event(time, EV_SYN, 0, 0),
];
if let Err(res) = self.mouse_handle.write(&events) {
log::error!("{}", res.to_string());
}
}
fn wheel_internal(&self, delta: i32) {
let time = get_time();
let events = [
new_event(time, EV_REL, RelativeAxis::Wheel as _, delta),
new_event(time, EV_SYN, 0, 0),
];
if let Err(res) = self.mouse_handle.write(&events) {
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..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, modifiers & m != 0);
}
}
}
self.cur_modifiers = modifiers;
}
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 as u16) - 8, down as _),
new_event(time, EV_SYN, 0, 0),
];
if let Err(res) = self.keyboard_handle.write(&events) {
log::error!("send_key: {}", res.to_string());
}
}
fn set_desktop_extent(&mut self, extent: Vec2) {
self.desktop_extent = extent;
}
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.scroll.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);
// Pass mouse motion events only if not scrolling
// (allows scrolling on all Chromium-based applications)
self.current_action.pos = None;
}
}
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(&mut self, _button: u16, _down: bool) {}
fn wheel(&mut self, _delta: i32) {}
fn set_modifiers(&mut self, _modifiers: u8) {}
fn send_key(&self, _key: VirtualKey, _down: bool) {}
fn set_desktop_extent(&mut self, _extent: Vec2) {}
fn set_desktop_origin(&mut self, _origin: Vec2) {}
fn commit(&mut self) {}
}
#[inline]
fn get_time() -> timeval {
let mut time = timeval {
tv_sec: 0,
tv_usec: 0,
};
unsafe { libc::gettimeofday(&mut time, std::ptr::null_mut()) };
time
}
#[inline]
fn new_event(time: timeval, type_: u16, code: u16, value: i32) -> input_event {
input_event {
time,
type_,
code,
value,
}
}
pub type KeyModifier = u8;
pub const SHIFT: KeyModifier = 0x01;
pub const CAPS_LOCK: KeyModifier = 0x02;
pub const CTRL: KeyModifier = 0x04;
pub const ALT: KeyModifier = 0x08;
pub const NUM_LOCK: KeyModifier = 0x10;
pub const SUPER: KeyModifier = 0x40;
pub const META: KeyModifier = 0x80;
#[allow(non_camel_case_types)]
#[repr(u16)]
#[derive(Debug, Deserialize, PartialEq, Clone, Copy, IntegerId, EnumString, EnumIter)]
pub enum VirtualKey {
Escape = 9,
N1, // number row
N2,
N3,
N4,
N5,
N6,
N7,
N8,
N9,
N0,
Minus,
Plus,
BackSpace,
Tab,
Q,
W,
E,
R,
T,
Y,
U,
I,
O,
P,
Oem4, // [ {
Oem6, // ] }
Return,
LCtrl,
A,
S,
D,
F,
G,
H,
J,
K,
L,
Oem1, // ; :
Oem7, // ' "
Oem3, // ` ~
LShift,
Oem5, // \ |
Z,
X,
C,
V,
B,
N,
M,
Comma, // , <
Period, // . >
Oem2, // / ?
RShift,
KP_Multiply,
LAlt,
Space,
Caps,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
NumLock,
Scroll,
KP_7, // KeyPad
KP_8,
KP_9,
KP_Subtract,
KP_4,
KP_5,
KP_6,
KP_Add,
KP_1,
KP_2,
KP_3,
KP_0,
KP_Decimal,
Oem102 = 94, // Optional key usually between LShift and Z
F11,
F12,
AbntC1,
Katakana,
Hiragana,
Henkan,
Kana,
Muhenkan,
KP_Enter = 104,
RCtrl,
KP_Divide,
Print,
Meta, // Right Alt aka AltGr
Home = 110,
Up,
Prior,
Left,
Right,
End,
Down,
Next,
Insert,
Delete,
XF86AudioMute = 121,
XF86AudioLowerVolume,
XF86AudioRaiseVolume,
Pause = 127,
AbntC2 = 129,
Hangul,
Hanja,
LSuper = 133,
RSuper,
Menu,
Help = 146,
XF86MenuKB,
XF86Sleep = 150,
XF86Xfer = 155,
XF86Launch1,
XF86Launch2,
XF86WWW,
XF86Mail = 163,
XF86Favorites,
XF86MyComputer,
XF86Back,
XF86Forward,
XF86AudioNext = 171,
XF86AudioPlay,
XF86AudioPrev,
XF86AudioStop,
XF86HomePage = 180,
XF86Reload,
F13 = 191,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
Hyper = 207,
XF86Launch3,
XF86Launch4,
XF86LaunchB,
XF86Search = 225,
}
pub static KEYS_TO_MODS: Lazy<IdMap<VirtualKey, KeyModifier>> = Lazy::new(|| {
idmap! {
VirtualKey::LShift => SHIFT,
VirtualKey::RShift => SHIFT,
VirtualKey::Caps => CAPS_LOCK,
VirtualKey::LCtrl => CTRL,
VirtualKey::RCtrl => CTRL,
VirtualKey::LAlt => ALT,
VirtualKey::NumLock => NUM_LOCK,
VirtualKey::LSuper => SUPER,
VirtualKey::RSuper => SUPER,
VirtualKey::Meta => META,
}
});
pub static MODS_TO_KEYS: Lazy<IdMap<KeyModifier, Vec<VirtualKey>>> = Lazy::new(|| {
idmap! {
SHIFT => vec![VirtualKey::LShift, VirtualKey::RShift],
CAPS_LOCK => vec![VirtualKey::Caps],
CTRL => vec![VirtualKey::LCtrl, VirtualKey::RCtrl],
ALT => vec![VirtualKey::LAlt],
NUM_LOCK => vec![VirtualKey::NumLock],
SUPER => vec![VirtualKey::LSuper, VirtualKey::RSuper],
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 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 { 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 { keymap })
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

+129 -17
View File
@@ -9,16 +9,22 @@ mod overlays;
mod shaders;
mod state;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
#[cfg(feature = "wayvr")]
mod config_wayvr;
use std::{
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use clap::Parser;
use flexi_logger::FileSpec;
use sysinfo::Pid;
/// 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")]
@@ -31,32 +37,53 @@ struct Args {
#[arg(long)]
openxr: bool,
/// Show the working set of overlay on startup
#[arg(long)]
show: bool,
/// Uninstall OpenVR manifest and exit
#[arg(long)]
uninstall: bool,
/// Replace running WlxOverlay-S instance
#[arg(long)]
replace: bool,
/// Allow multiple running instances of WlxOverlay-S (things may break!)
#[arg(long)]
multi: bool,
/// Path to write logs to
#[arg(short, long, value_name = "FOLDER")]
#[arg(short, long, value_name = "FILE_PATH")]
log_to: Option<String>,
#[cfg(feature = "uidev")]
/// Show a desktop window of a UI panel for development
#[arg(short, long, value_name = "UI_NAME")]
uidev: Option<String>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
if let Some(ref log_to) = args.log_to {
flexi_logger::Logger::try_with_env_or_str("info")?
.log_to_file(FileSpec::default().directory(log_to))
.log_to_stdout()
.start()?;
println!("Logging to: {}", &log_to);
let mut args = if std::env::args().skip(1).any(|a| !a.is_empty()) {
Args::parse()
} else {
flexi_logger::Logger::try_with_env_or_str("info")?.start()?;
Args::default()
};
if !args.multi && !ensure_single_instance(args.replace) {
println!("Looks like WlxOverlay-S is already running.");
println!("Use --replace and I will terminate it for you.");
return Ok(());
}
logging_init(&mut args)?;
log::info!(
"Welcome to {} version {}!",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
env!("WLX_BUILD"),
);
log::info!("It is {}.", chrono::Local::now().format("%c"));
#[cfg(feature = "openvr")]
if args.uninstall {
@@ -64,6 +91,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();
@@ -83,7 +116,7 @@ fn auto_run(running: Arc<AtomicBool>, args: Args) {
#[cfg(feature = "openxr")]
if !args_get_openvr(&args) {
use crate::backend::openxr::openxr_run;
match openxr_run(running.clone()) {
match openxr_run(running.clone(), args.show) {
Ok(()) => return,
Err(BackendError::NotSupported) => (),
Err(e) => {
@@ -96,7 +129,7 @@ fn auto_run(running: Arc<AtomicBool>, args: Args) {
#[cfg(feature = "openvr")]
if !args_get_openxr(&args) {
use crate::backend::openvr::openvr_run;
match openvr_run(running.clone()) {
match openvr_run(running.clone(), args.show) {
Ok(()) => return,
Err(BackendError::NotSupported) => (),
Err(e) => {
@@ -136,3 +169,82 @@ fn args_get_openxr(_args: &Args) -> bool {
ret
}
fn logging_init(args: &mut Args) -> anyhow::Result<()> {
let log_file = args
.log_to
.take()
.or_else(|| std::env::var("WLX_LOGFILE").ok())
.or_else(|| Some("/tmp/wlx.log".to_string()));
if let Some(log_to) = log_file.filter(|s| !s.is_empty()) {
if let Err(e) = file_logging_init(&log_to) {
log::error!("Failed to initialize file logging: {}", e);
env_logger::init();
}
} else {
env_logger::init();
}
log_panics::init();
Ok(())
}
fn file_logging_init(log_to: &str) -> anyhow::Result<()> {
use std::io::Write;
let target = Box::new(std::fs::File::create(log_to)?);
env_logger::Builder::new()
.target(env_logger::Target::Pipe(target))
.filter(None, log::LevelFilter::Info)
.filter_module("zbus", log::LevelFilter::Warn)
.parse_default_env()
.format(|buf, record| {
eprintln!("[{}] {}", record.level(), record.args());
writeln!(
buf,
"[{} {} {}] {}",
chrono::Local::now().format("%H:%M:%S%.3f"),
record.level(),
record.module_path().unwrap_or_default(),
record.args()
)
})
.init();
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(
sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(pid)]),
false,
);
if let Some(proc) = system.process(sysinfo::Pid::from_u32(pid)) {
if replace {
proc.kill_with(sysinfo::Signal::Term);
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, Z_ORDER_ANCHOR};
use crate::config::{load_known_yaml, ConfigType};
use crate::gui::modular::{modular_canvas, ModularUiConfig};
use crate::state::AppState;
pub static ANCHOR_NAME: Lazy<Arc<str>> = Lazy::new(|| Arc::from("anchor"));
pub fn create_anchor<O>(state: &mut 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: Z_ORDER_ANCHOR,
spawn_scale: config.width,
spawn_point: Vec3A::NEG_Z * 0.5,
relative_to: RelativeTo::Stage,
..Default::default()
},
backend: Box::new(modular_canvas(&config.size, &config.elements, state)?),
..Default::default()
})
}
+1 -1
View File
@@ -12,7 +12,7 @@ use crate::{
const SETTINGS_NAME: &str = "settings";
pub fn create_custom(
state: &AppState,
state: &mut AppState,
name: Arc<str>,
) -> Option<(OverlayState, Box<dyn OverlayBackend>)> {
let config = if &*name == SETTINGS_NAME {
+253 -52
View File
@@ -1,24 +1,28 @@
use std::{
collections::HashMap,
io::Cursor,
process::{Child, Command},
str::FromStr,
};
use crate::{
backend::{
input::PointerMode,
overlay::{OverlayData, OverlayState},
input::{InteractionHandler, PointerMode},
overlay::{FrameTransform, OverlayBackend, OverlayData, OverlayRenderer, OverlayState},
},
config::{self, ConfigType},
gui::{color_parse, CanvasBuilder, Control},
hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER},
state::AppState,
gui::{
canvas::{builder::CanvasBuilder, control::Control, Canvas},
color_parse, KeyCapType,
},
hid::{
get_key_type, KeyModifier, KeyType, VirtualKey, XkbKeymap, ALT, CTRL, KEYS_TO_MODS, META,
NUM_LOCK, SHIFT, SUPER,
},
state::{AppState, KeyboardFocus},
};
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 +31,34 @@ 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>>
fn send_key(app: &mut AppState, key: VirtualKey, down: bool) {
match app.keyboard_focus {
KeyboardFocus::PhysicalScreen => {
app.hid_provider.send_key(key, down);
}
KeyboardFocus::WayVR =>
{
#[cfg(feature = "wayvr")]
if let Some(wayvr) = &app.wayvr {
wayvr.borrow_mut().data.state.send_key(key as u32, down);
}
}
}
}
fn set_modifiers(app: &mut AppState, mods: u8) {
match app.keyboard_focus {
KeyboardFocus::PhysicalScreen => {
app.hid_provider.set_modifiers(mods);
}
KeyboardFocus::WayVR => {}
}
}
pub fn create_keyboard<O>(
app: &AppState,
mut keymap: Option<XkbKeymap>,
) -> anyhow::Result<OverlayData<O>>
where
O: Default,
{
@@ -38,6 +69,14 @@ where
let data = KeyboardData {
modifiers: 0,
alt_modifier: match LAYOUT.alt_modifier {
AltModifier::Shift => SHIFT,
AltModifier::Ctrl => CTRL,
AltModifier::Alt => ALT,
AltModifier::Super => SUPER,
AltModifier::Meta => META,
_ => 0,
},
processes: vec![],
};
@@ -45,15 +84,22 @@ where
size.x as _,
size.y as _,
app.graphics.clone(),
app.format,
app.graphics.native_format,
data,
)?;
canvas.bg_color = color_parse("#101010").unwrap(); //safe
canvas.panel(0., 0., size.x, size.y);
canvas.bg_color = color_parse("#181926").unwrap(); //safe
canvas.panel(0., 0., size.x, size.y, 12.);
canvas.font_size = 18;
canvas.bg_color = color_parse("#202020").unwrap(); //safe
canvas.fg_color = color_parse("#cad3f5").unwrap(); //safe
canvas.bg_color = color_parse("#1e2030").unwrap(); //safe
let has_altgr = keymap.as_ref().map_or(false, |k| k.has_altgr());
if !LAYOUT.auto_labels.unwrap_or(true) {
keymap = None;
}
let unit_size = size.x / LAYOUT.row_size;
let h = unit_size - 2. * BUTTON_PADDING;
@@ -68,8 +114,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 +164,28 @@ where
verbs: key_events_for_macro(macro_verbs),
});
} else if let Some(exec_args) = LAYOUT.exec_commands.get(key) {
maybe_state = Some(KeyButtonData::Exec {
program: exec_args
.first()
.expect("Keyboard: Invalid EXEC args")
.clone(),
args: exec_args.iter().skip(1).cloned().collect(),
});
if exec_args.is_empty() {
log::error!("Keyboard: EXEC args empty for {}", key);
continue;
}
let mut iter = exec_args.iter().cloned();
if let Some(program) = iter.next() {
maybe_state = Some(KeyButtonData::Exec {
program,
args: iter.by_ref().take_while(|arg| arg[..] != *"null").collect(),
release_program: iter.next(),
release_args: iter.collect(),
})
};
} else {
log::error!("Unknown key: {}", key);
}
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, 12., cap_type, &label);
button.state = Some(state);
button.on_press = Some(key_press);
button.on_release = Some(key_release);
@@ -120,13 +209,14 @@ 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()
},
backend: Box::new(canvas),
backend: Box::new(KeyboardBackend { canvas }),
..Default::default()
})
}
@@ -141,27 +231,30 @@ fn key_press(
Some(KeyButtonData::Key { vk, pressed }) => {
data.key_click(app);
if let PointerMode::Right = mode {
data.modifiers |= SHIFT;
app.hid_provider.set_modifiers(data.modifiers);
}
data.modifiers |= match mode {
PointerMode::Right => SHIFT,
PointerMode::Middle => data.alt_modifier,
_ => 0,
};
app.hid_provider.send_key(*vk as _, true);
app.hid_provider.set_modifiers(data.modifiers);
send_key(app, *vk, true);
*pressed = true;
}
Some(KeyButtonData::Modifier { modifier, sticky }) => {
*sticky = data.modifiers & *modifier == 0;
data.modifiers |= *modifier;
data.key_click(app);
app.hid_provider.set_modifiers(data.modifiers);
set_modifiers(app, data.modifiers);
}
Some(KeyButtonData::Macro { verbs }) => {
data.key_click(app);
for (vk, press) in verbs {
app.hid_provider.send_key(*vk as _, *press);
send_key(app, *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,27 +275,42 @@ fn key_release(
) {
match control.state.as_mut() {
Some(KeyButtonData::Key { vk, pressed }) => {
app.hid_provider.send_key(*vk as _, false);
send_key(app, *vk, false);
*pressed = false;
for m in AUTO_RELEASE_MODS.iter() {
if data.modifiers & *m != 0 {
data.modifiers &= !*m;
app.hid_provider.set_modifiers(data.modifiers);
set_modifiers(app, data.modifiers);
}
}
}
Some(KeyButtonData::Modifier { modifier, sticky }) => {
if !*sticky {
data.modifiers &= !*modifier;
app.hid_provider.set_modifiers(data.modifiers);
set_modifiers(app, data.modifiers);
}
}
Some(KeyButtonData::Exec {
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);
}
}
}
_ => {}
}
}
static PRESS_COLOR: Vec4 = Vec4::new(1.0, 1.0, 1.0, 0.5);
static PRESS_COLOR: Vec4 = Vec4::new(198. / 255., 160. / 255., 246. / 255., 0.5);
fn test_highlight(
control: &Control<KeyboardData, KeyButtonData>,
@@ -224,36 +332,56 @@ fn test_highlight(
struct KeyboardData {
modifiers: KeyModifier,
alt_modifier: KeyModifier,
processes: Vec<Child>,
}
const KEY_AUDIO_WAV: &[u8] = include_bytes!("../res/421581.wav");
impl KeyboardData {
fn key_click(&mut self, app: &mut AppState) {
if !app.session.config.keyboard_sound_enabled {
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-9_-]+)(?: +(UP|DOWN))?$").unwrap()); // want panic
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
#[repr(usize)]
pub enum AltModifier {
#[default]
None,
Shift,
Ctrl,
Alt,
Super,
Meta,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Layout {
@@ -261,9 +389,11 @@ pub struct Layout {
row_size: f32,
key_sizes: Vec<Vec<f32>>,
main_layout: Vec<Vec<Option<String>>>,
alt_modifier: AltModifier,
exec_commands: HashMap<String, Vec<String>>,
macros: HashMap<String, Vec<String>>,
labels: HashMap<String, Vec<String>>,
auto_labels: Option<bool>,
}
impl Layout {
@@ -314,11 +444,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()
)]
}
@@ -353,3 +489,68 @@ fn key_events_for_macro(macro_verbs: &Vec<String>) -> Vec<(VirtualKey, bool)> {
}
key_events
}
struct KeyboardBackend {
canvas: Canvas<KeyboardData, KeyButtonData>,
}
impl OverlayBackend for KeyboardBackend {
fn set_interaction(&mut self, interaction: Box<dyn crate::backend::input::InteractionHandler>) {
self.canvas.set_interaction(interaction)
}
fn set_renderer(&mut self, renderer: Box<dyn crate::backend::overlay::OverlayRenderer>) {
self.canvas.set_renderer(renderer)
}
}
impl InteractionHandler for KeyboardBackend {
fn on_pointer(
&mut self,
app: &mut AppState,
hit: &crate::backend::input::PointerHit,
pressed: bool,
) {
self.canvas.on_pointer(app, hit, pressed)
}
fn on_scroll(
&mut self,
app: &mut AppState,
hit: &crate::backend::input::PointerHit,
delta: f32,
) {
self.canvas.on_scroll(app, hit, delta)
}
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
self.canvas.on_left(app, pointer)
}
fn on_hover(
&mut self,
app: &mut AppState,
hit: &crate::backend::input::PointerHit,
) -> Option<crate::backend::input::Haptics> {
self.canvas.on_hover(app, hit)
}
}
impl OverlayRenderer for KeyboardBackend {
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.canvas.init(app)
}
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.canvas.render(app)
}
fn frame_transform(&mut self) -> Option<FrameTransform> {
self.canvas.frame_transform()
}
fn view(&mut self) -> Option<std::sync::Arc<vulkano::image::view::ImageView>> {
self.canvas.view()
}
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.canvas.data_mut().modifiers = 0;
set_modifiers(app, 0);
self.canvas.pause(app)
}
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.canvas.resume(app)
}
}
+58 -42
View File
@@ -1,35 +1,41 @@
use std::{sync::Arc, thread::JoinHandle};
use std::{
sync::Arc,
task::{Context, Poll},
};
use futures::executor;
use glam::vec3a;
use futures::{Future, FutureExt};
use wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireSelectScreenResult};
use crate::{
backend::{
common::{OverlaySelector, TaskType},
common::OverlaySelector,
overlay::{
ui_transform, OverlayBackend, OverlayRenderer, OverlayState, SplitOverlayBackend,
ui_transform, FrameTransform, 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,40 +46,44 @@ 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);
self.renderer = Some(ScreenRenderer::new_raw(
self.name.clone(),
Box::new(capture),
));
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(self.name.clone()),
Box::new(|app, o| {
o.grabbable = true;
o.interactable = true;
o.reset(app, false);
}),
));
} else {
log::warn!("Failed to create pipewire mirror");
self.renderer = None;
// drop self
app.tasks
.enqueue(TaskType::DropOverlay(OverlaySelector::Name(
match maybe_pw_result {
Ok(pw_result) => {
let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
log::info!("{}: PipeWire node selected: {}", self.name.clone(), node_id);
let capture = PipewireCapture::new(self.name.clone(), node_id);
self.renderer = Some(ScreenRenderer::new_raw(
self.name.clone(),
)));
Box::new(capture),
));
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(self.name.clone()),
Box::new(|app, o| {
o.grabbable = true;
o.interactable = true;
o.reset(app, false);
}),
));
}
Err(e) => {
log::warn!("Failed to create mirror due to PipeWire error: {:?}", e);
self.renderer = None;
// drop self
app.tasks
.enqueue(TaskType::DropOverlay(OverlaySelector::Name(
self.name.clone(),
)));
}
}
}
@@ -111,6 +121,13 @@ impl OverlayRenderer for MirrorRenderer {
fn view(&mut self) -> Option<std::sync::Arc<vulkano::image::view::ImageView>> {
self.renderer.as_mut().and_then(|r| r.view())
}
fn frame_transform(&mut self) -> Option<FrameTransform> {
Some(FrameTransform {
extent: self.last_extent,
..Default::default()
})
}
}
pub fn new_mirror(
@@ -123,7 +140,6 @@ 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),
..Default::default()
};
let backend = Box::new(SplitOverlayBackend {
+4
View File
@@ -1,3 +1,4 @@
pub mod anchor;
pub mod custom;
pub mod keyboard;
#[cfg(feature = "wayland")]
@@ -5,3 +6,6 @@ pub mod mirror;
pub mod screen;
pub mod toast;
pub mod watch;
#[cfg(feature = "wayvr")]
pub mod wayvr;
+581 -253
View File
File diff suppressed because it is too large. Load diff
+117 -47
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},
overlay::{OverlayBackend, OverlayState, RelativeTo},
common::OverlaySelector,
overlay::{OverlayBackend, OverlayState, RelativeTo, Z_ORDER_TOAST},
task::TaskType,
},
gui::{color_parse, CanvasBuilder},
state::AppState,
gui::{canvas::builder::CanvasBuilder, color_parse},
state::{AppState, LeftRight},
};
const FONT_SIZE: isize = 16;
const PADDING: (f32, f32) = (25., 7.);
const PIXELS_TO_METERS: f32 = 1. / 2000.;
const TOAST_AUDIO_WAV: &[u8] = include_bytes!("../res/557297.wav");
static TOAST_NAME: Lazy<Arc<str>> = Lazy::new(|| "toast".into());
static 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 {
@@ -131,32 +178,55 @@ fn new_toast(
.ok()?;
canvas.font_size = FONT_SIZE;
canvas.fg_color = color_parse("#aaaaaa").unwrap(); // want panic
canvas.bg_color = color_parse("#333333").unwrap(); // want panic
canvas.panel(0., 0., size.0, size.1);
canvas.fg_color = color_parse("#cad3f5").unwrap(); // want panic
canvas.bg_color = color_parse("#1e2030").unwrap(); // want panic
canvas.panel(0., 0., size.0, size.1, 16.);
if toast.body.len() > 0 {
log::info!("{}: {}", title, toast.body);
canvas.label(PADDING.0, 54., og_width, size.1 - 54., toast.body);
canvas.label(PADDING.0, 54., og_width, size.1 - 54., 3., toast.body);
canvas.fg_color = color_parse("#101010").unwrap(); // want panic
canvas.bg_color = color_parse("#666666").unwrap(); // want panic
canvas.panel(0., 0., size.0, 30.);
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., title);
canvas.fg_color = color_parse("#b8c0e0").unwrap(); // want panic
canvas.bg_color = color_parse("#24273a").unwrap(); // want panic
canvas.panel(0., 0., size.0, 30., 16.);
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., 16., title);
} else {
log::info!("Toast: {}", title);
canvas.label_centered(PADDING.0, 0., og_width, size.1, title);
canvas.label_centered(PADDING.0, 0., og_width, size.1, 16., 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: Z_ORDER_TOAST,
relative_to,
..Default::default()
};
let backend = Box::new(canvas.build());
Some((state, backend))
}
fn msg_err(app: &mut AppState, message: &str) {
Toast::new(ToastTopic::System, "Error".into(), message.into())
.with_timeout(3.)
.submit(app);
}
// Display the same error in the terminal and as a toast in VR.
// Formatted as "Failed to XYZ: Object is not defined"
pub fn error_toast<ErrorType>(app: &mut AppState, title: &str, err: ErrorType)
where
ErrorType: std::fmt::Display + std::fmt::Debug,
{
log::error!("{}: {:?}", title, err); // More detailed version (use Debug)
// Brief version (use Display)
msg_err(app, &format!("{}: {}", title, err));
}
pub fn error_toast_str(app: &mut AppState, message: &str) {
log::error!("{}", message);
msg_err(app, message);
}
+22 -54
View File
@@ -1,27 +1,23 @@
use glam::{Quat, Vec3A};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use glam::Vec3A;
use crate::{
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
config::{
def_half, def_left, def_point7, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType,
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo, Z_ORDER_WATCH},
config::{load_known_yaml, ConfigType},
gui::{
canvas::Canvas,
modular::{modular_canvas, ModularData, ModularUiConfig},
},
config_io,
gui::modular::{modular_canvas, ModularUiConfig},
state::{AppState, LeftRight},
state::AppState,
};
pub const WATCH_NAME: &str = "watch";
pub fn create_watch<O>(state: &AppState) -> anyhow::Result<OverlayData<O>>
pub fn create_watch<O>(state: &mut AppState) -> anyhow::Result<OverlayData<O>>
where
O: Default,
{
let config = load_known_yaml::<ModularUiConfig>(ConfigType::Watch);
let canvas = modular_canvas(&config.size, &config.elements, state)?;
let relative_to = RelativeTo::Hand(state.session.config.watch_hand as usize);
Ok(OverlayData {
@@ -29,23 +25,33 @@ where
name: WATCH_NAME.into(),
want_visible: true,
interactable: true,
z_order: Z_ORDER_WATCH,
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: &mut AppState,
) -> anyhow::Result<Canvas<(), ModularData>> {
let config = config.unwrap_or_else(|| load_known_yaml::<ModularUiConfig>(ConfigType::Watch));
modular_canvas(&config.size, &config.elements, state)
}
pub fn watch_fade<D>(app: &mut AppState, watch: &mut OverlayData<D>)
where
D: Default,
{
if watch.state.saved_scale.is_some_and(|s| s < f32::EPSILON) {
if watch.state.saved_transform.is_some() {
watch.state.want_visible = false;
return;
}
@@ -69,41 +75,3 @@ where
watch.state.alpha = watch.state.alpha.clamp(0., 1.);
}
}
#[derive(Deserialize, Serialize)]
pub struct WatchConf {
#[serde(default = "def_watch_pos")]
pub watch_pos: [f32; 3],
#[serde(default = "def_watch_rot")]
pub watch_rot: [f32; 4],
#[serde(default = "def_left")]
pub watch_hand: LeftRight,
#[serde(default = "def_half")]
pub watch_view_angle_min: f32,
#[serde(default = "def_point7")]
pub watch_view_angle_max: f32,
}
fn get_config_path() -> PathBuf {
let mut path = config_io::get_conf_d_path();
path.push("watch_state.yaml");
path
}
pub fn save_watch(app: &mut AppState) -> anyhow::Result<()> {
let conf = WatchConf {
watch_pos: app.session.config.watch_pos,
watch_rot: app.session.config.watch_rot,
watch_hand: app.session.config.watch_hand,
watch_view_angle_min: app.session.config.watch_view_angle_min,
watch_view_angle_max: app.session.config.watch_view_angle_max,
};
let yaml = serde_yaml::to_string(&conf)?;
std::fs::write(get_config_path(), yaml)?;
Ok(())
}
+842
View File
@@ -0,0 +1,842 @@
use glam::{vec3a, Affine2, Vec3, Vec3A};
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use vulkano::image::SubresourceLayout;
use wayvr_ipc::packet_server;
use wlx_capture::frame::{DmabufFrame, FourCC, FrameFormat, FramePlane};
use crate::{
backend::{
common::{OverlayContainer, OverlaySelector},
input::{self, InteractionHandler},
overlay::{
ui_transform, FrameTransform, OverlayData, OverlayID, OverlayRenderer, OverlayState,
SplitOverlayBackend, Z_ORDER_DASHBOARD,
},
task::TaskType,
wayvr::{
self, display,
server_ipc::{gen_args_vec, gen_env_vec},
WayVR,
},
},
config_wayvr,
graphics::WlxGraphics,
gui::modular::button::{WayVRAction, WayVRDisplayClickAction},
state::{self, AppState, KeyboardFocus},
};
use super::toast::error_toast;
// Hard-coded for now
const DASHBOARD_WIDTH: u16 = 1920;
const DASHBOARD_HEIGHT: u16 = 1080;
const DASHBOARD_DISPLAY_NAME: &str = "_DASHBOARD";
pub struct WayVRContext {
wayvr: Rc<RefCell<WayVRData>>,
display: wayvr::display::DisplayHandle,
}
impl WayVRContext {
pub fn new(
wvr: Rc<RefCell<WayVRData>>,
display: wayvr::display::DisplayHandle,
) -> anyhow::Result<Self> {
Ok(Self {
wayvr: wvr.clone(),
display,
})
}
}
struct OverlayToCreate {
pub conf_display: config_wayvr::WayVRDisplay,
pub disp_handle: display::DisplayHandle,
}
pub struct WayVRData {
display_handle_map: HashMap<display::DisplayHandle, OverlayID>,
overlays_to_create: Vec<OverlayToCreate>,
dashboard_executed: bool,
pub data: WayVR,
}
impl WayVRData {
pub fn new(config: wayvr::Config) -> anyhow::Result<Self> {
Ok(Self {
display_handle_map: Default::default(),
data: WayVR::new(config)?,
overlays_to_create: Vec::new(),
dashboard_executed: false,
})
}
fn get_unique_display_name(&self, mut candidate: String) -> String {
let mut num = 0;
while !self
.data
.state
.displays
.vec
.iter()
.flatten()
.any(|d| d.obj.name == candidate)
{
if num > 0 {
candidate = format!("{} ({})", candidate, num);
}
num += 1;
}
candidate
}
}
pub struct WayVRInteractionHandler {
context: Rc<RefCell<WayVRContext>>,
mouse_transform: Affine2,
}
impl WayVRInteractionHandler {
pub fn new(context: Rc<RefCell<WayVRContext>>, mouse_transform: Affine2) -> Self {
Self {
context,
mouse_transform,
}
}
}
impl InteractionHandler for WayVRInteractionHandler {
fn on_hover(
&mut self,
_app: &mut state::AppState,
hit: &input::PointerHit,
) -> Option<input::Haptics> {
let ctx = self.context.borrow();
let wayvr = &mut ctx.wayvr.borrow_mut().data;
if let Some(disp) = wayvr.state.displays.get(&ctx.display) {
let pos = self.mouse_transform.transform_point2(hit.uv);
let x = ((pos.x * disp.width as f32) as i32).max(0);
let y = ((pos.y * disp.height as f32) as i32).max(0);
let ctx = self.context.borrow();
wayvr.state.send_mouse_move(ctx.display, x as u32, y as u32);
}
wayvr.state.pending_haptic.take()
}
fn on_left(&mut self, _app: &mut state::AppState, _pointer: usize) {
// Ignore event
}
fn on_pointer(&mut self, _app: &mut state::AppState, hit: &input::PointerHit, pressed: bool) {
if let Some(index) = match hit.mode {
input::PointerMode::Left => Some(wayvr::MouseIndex::Left),
input::PointerMode::Middle => Some(wayvr::MouseIndex::Center),
input::PointerMode::Right => Some(wayvr::MouseIndex::Right),
_ => {
// Unknown pointer event, ignore
None
}
} {
let ctx = self.context.borrow();
let wayvr = &mut ctx.wayvr.borrow_mut().data;
if pressed {
wayvr.state.send_mouse_down(ctx.display, index);
} else {
wayvr.state.send_mouse_up(ctx.display, index);
}
}
}
fn on_scroll(&mut self, _app: &mut state::AppState, _hit: &input::PointerHit, delta: f32) {
let ctx = self.context.borrow();
ctx.wayvr
.borrow_mut()
.data
.state
.send_mouse_scroll(ctx.display, delta);
}
}
pub struct WayVRRenderer {
dmabuf_image: Option<Arc<vulkano::image::Image>>,
view: Option<Arc<vulkano::image::view::ImageView>>,
context: Rc<RefCell<WayVRContext>>,
graphics: Arc<WlxGraphics>,
}
impl WayVRRenderer {
pub fn new(
app: &state::AppState,
wvr: Rc<RefCell<WayVRData>>,
display: wayvr::display::DisplayHandle,
) -> anyhow::Result<Self> {
Ok(Self {
context: Rc::new(RefCell::new(WayVRContext::new(wvr, display)?)),
dmabuf_image: None,
view: None,
graphics: app.graphics.clone(),
})
}
}
fn get_or_create_display_by_name(
app: &mut AppState,
wayvr: &mut WayVRData,
disp_name: &str,
) -> anyhow::Result<display::DisplayHandle> {
let disp_handle =
if let Some(disp) = WayVR::get_display_by_name(&wayvr.data.state.displays, disp_name) {
disp
} else {
let conf_display = app
.session
.wayvr_config
.get_display(disp_name)
.ok_or(anyhow::anyhow!(
"Cannot find display named \"{}\"",
disp_name
))?
.clone();
let disp_handle = wayvr.data.state.create_display(
conf_display.width,
conf_display.height,
disp_name,
conf_display.primary.unwrap_or(false),
)?;
wayvr.overlays_to_create.push(OverlayToCreate {
conf_display,
disp_handle,
});
disp_handle
};
Ok(disp_handle)
}
fn executable_exists_in_path(command: &str) -> bool {
let Ok(path) = std::env::var("PATH") else {
return false; // very unlikely to happen
};
for dir in path.split(':') {
let exec_path = std::path::PathBuf::from(dir).join(command);
if exec_path.exists() && exec_path.is_file() {
return true; // executable found
}
}
false
}
fn toggle_dashboard<O>(
app: &mut AppState,
overlays: &mut OverlayContainer<O>,
wayvr: &mut WayVRData,
) -> anyhow::Result<()>
where
O: Default,
{
let conf_dash = &app.session.wayvr_config.dashboard;
let Some(conf_dash) = &conf_dash else {
anyhow::bail!("Dashboard is not configured");
};
if !wayvr.dashboard_executed && !executable_exists_in_path(&conf_dash.exec) {
anyhow::bail!("Executable \"{}\" not found", &conf_dash.exec);
}
let (newly_created, disp_handle) = wayvr.data.state.get_or_create_dashboard_display(
DASHBOARD_WIDTH,
DASHBOARD_HEIGHT,
DASHBOARD_DISPLAY_NAME,
)?;
if newly_created {
log::info!("Creating dashboard overlay");
let mut overlay = create_overlay::<O>(
app,
wayvr,
DASHBOARD_DISPLAY_NAME,
OverlayToCreate {
disp_handle,
conf_display: config_wayvr::WayVRDisplay {
attach_to: None,
width: DASHBOARD_WIDTH,
height: DASHBOARD_HEIGHT,
scale: None,
rotation: None,
pos: None,
primary: None,
},
},
)?;
overlay.state.want_visible = true;
overlay.state.spawn_scale = 2.0;
overlay.state.spawn_point = vec3a(0.0, -0.35, -1.75);
overlay.state.z_order = Z_ORDER_DASHBOARD;
overlay.state.reset(app, true);
let Some(conf_dash) = &app.session.wayvr_config.dashboard else {
unreachable!(); /* safe, not possible to trigger */
};
// FIXME: overlay curvature needs to be dispatched for some unknown reason, this value is not set otherwise
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Id(overlay.state.id),
Box::new(move |_app, o| {
o.curvature = Some(0.15);
}),
));
overlays.add(overlay);
let args_vec = match &conf_dash.args {
Some(args) => gen_args_vec(args),
None => vec![],
};
let env_vec = match &conf_dash.env {
Some(env) => gen_env_vec(env),
None => vec![],
};
let mut userdata = HashMap::new();
userdata.insert(String::from("type"), String::from("dashboard"));
// Start dashboard specified in the WayVR config
let _process_handle_unused = wayvr.data.state.spawn_process(
disp_handle,
&conf_dash.exec,
&args_vec,
&env_vec,
userdata,
)?;
wayvr.dashboard_executed = true;
return Ok(());
}
let display = wayvr.data.state.displays.get(&disp_handle).unwrap(); // safe
let Some(overlay_id) = display.overlay_id else {
anyhow::bail!("Overlay ID not set for dashboard display");
};
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Id(overlay_id),
Box::new(move |app, o| {
// Toggle visibility
o.want_visible = !o.want_visible;
if o.want_visible {
o.reset(app, true);
}
}),
));
Ok(())
}
fn create_overlay<O>(
app: &mut AppState,
data: &mut WayVRData,
name: &str,
cell: OverlayToCreate,
) -> anyhow::Result<OverlayData<O>>
where
O: Default,
{
let conf_display = &cell.conf_display;
let disp_handle = cell.disp_handle;
let mut overlay = create_wayvr_display_overlay::<O>(
app,
conf_display.width,
conf_display.height,
disp_handle,
conf_display.scale.unwrap_or(1.0),
name,
)?;
data.display_handle_map
.insert(disp_handle, overlay.state.id);
if let Some(attach_to) = &conf_display.attach_to {
overlay.state.relative_to = attach_to.get_relative_to();
}
if let Some(rot) = &conf_display.rotation {
overlay.state.spawn_rotation =
glam::Quat::from_axis_angle(Vec3::from_slice(&rot.axis), f32::to_radians(rot.angle));
}
if let Some(pos) = &conf_display.pos {
overlay.state.spawn_point = Vec3A::from_slice(pos);
}
let display = data.data.state.displays.get_mut(&disp_handle).unwrap(); // Never fails
display.overlay_id = Some(overlay.state.id);
Ok(overlay)
}
fn create_queued_displays<O>(
app: &mut AppState,
data: &mut WayVRData,
overlays: &mut OverlayContainer<O>,
) -> anyhow::Result<()>
where
O: Default,
{
let overlays_to_create = std::mem::take(&mut data.overlays_to_create);
for cell in overlays_to_create {
let Some(disp) = data.data.state.displays.get(&cell.disp_handle) else {
continue; // this shouldn't happen
};
let name = disp.name.clone();
let overlay = create_overlay::<O>(app, data, name.as_str(), cell)?;
overlays.add(overlay); // Insert freshly created WayVR overlay into wlx stack
}
Ok(())
}
pub fn tick_events<O>(app: &mut AppState, overlays: &mut OverlayContainer<O>) -> anyhow::Result<()>
where
O: Default,
{
let Some(r_wayvr) = app.wayvr.clone() else {
return Ok(());
};
let mut wayvr = r_wayvr.borrow_mut();
while let Some(signal) = wayvr.data.state.signals.read() {
match signal {
wayvr::WayVRSignal::DisplayVisibility(display_handle, visible) => {
if let Some(overlay_id) = wayvr.display_handle_map.get(&display_handle) {
let overlay_id = *overlay_id;
wayvr
.data
.state
.set_display_visible(display_handle, visible);
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Id(overlay_id),
Box::new(move |_app, o| {
o.want_visible = visible;
}),
));
}
}
wayvr::WayVRSignal::DisplayWindowLayout(display_handle, layout) => {
wayvr.data.state.set_display_layout(display_handle, layout);
}
wayvr::WayVRSignal::BroadcastStateChanged(packet) => {
wayvr
.data
.ipc_server
.broadcast(packet_server::PacketServer::WvrStateChanged(packet))?;
}
}
}
let res = wayvr.data.tick_events()?;
drop(wayvr);
for result in res {
match result {
wayvr::TickTask::NewExternalProcess(req) => {
let config = &app.session.wayvr_config;
let disp_name = if let Some(display_name) = req.env.display_name {
config
.get_display(display_name.as_str())
.map(|_| display_name)
} else {
config
.get_default_display()
.map(|(display_name, _)| display_name)
};
if let Some(disp_name) = disp_name {
let mut wayvr = r_wayvr.borrow_mut();
log::info!("Registering external process with PID {}", req.pid);
let disp_handle = get_or_create_display_by_name(app, &mut wayvr, &disp_name)?;
wayvr.data.state.add_external_process(disp_handle, req.pid);
wayvr
.data
.state
.manager
.add_client(wayvr::client::WayVRClient {
client: req.client,
display_handle: disp_handle,
pid: req.pid,
});
}
}
wayvr::TickTask::NewDisplay(cpar, disp_handle) => {
log::info!("Creating new display with name \"{}\"", cpar.name);
let mut wayvr = r_wayvr.borrow_mut();
let unique_name = wayvr.get_unique_display_name(cpar.name);
let disp_handle = match disp_handle {
Some(d) => d,
None => wayvr.data.state.create_display(
cpar.width,
cpar.height,
&unique_name,
false,
)?,
};
wayvr.overlays_to_create.push(OverlayToCreate {
disp_handle,
conf_display: config_wayvr::WayVRDisplay {
attach_to: Some(config_wayvr::AttachTo::from_packet(&cpar.attach_to)),
width: cpar.width,
height: cpar.height,
pos: None,
primary: None,
rotation: None,
scale: cpar.scale,
},
});
}
wayvr::TickTask::DropOverlay(overlay_id) => {
app.tasks
.enqueue(TaskType::DropOverlay(OverlaySelector::Id(overlay_id)));
}
}
}
let mut wayvr = r_wayvr.borrow_mut();
create_queued_displays(app, &mut wayvr, overlays)?;
Ok(())
}
impl WayVRRenderer {
fn ensure_dmabuf(&mut self, data: wayvr::egl_data::DMAbufData) -> anyhow::Result<()> {
if self.dmabuf_image.is_none() {
// First init
let mut planes = [FramePlane::default(); 4];
planes[0].fd = Some(data.fd);
planes[0].offset = data.offset as u32;
planes[0].stride = data.stride;
let ctx = self.context.borrow_mut();
let wayvr = ctx.wayvr.borrow_mut();
if let Some(disp) = wayvr.data.state.displays.get(&ctx.display) {
let frame = DmabufFrame {
format: FrameFormat {
width: disp.width as u32,
height: disp.height as u32,
fourcc: FourCC {
value: data.mod_info.fourcc,
},
modifier: data.mod_info.modifiers[0], /* possibly not proper? */
..Default::default()
},
num_planes: 1,
planes,
};
drop(wayvr);
let layouts: Vec<SubresourceLayout> = vec![SubresourceLayout {
offset: data.offset as _,
size: 0,
row_pitch: data.stride as _,
array_pitch: None,
depth_pitch: None,
}];
let tex = self.graphics.dmabuf_texture_ex(
frame,
vulkano::image::ImageTiling::DrmFormatModifier,
layouts,
data.mod_info.modifiers,
)?;
self.dmabuf_image = Some(tex.clone());
self.view = Some(vulkano::image::view::ImageView::new_default(tex).unwrap());
} else {
anyhow::bail!("Failed to fetch WayVR display")
}
}
Ok(())
}
}
impl OverlayRenderer for WayVRRenderer {
fn init(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> {
Ok(())
}
fn pause(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> {
let ctx = self.context.borrow_mut();
let wayvr = &mut ctx.wayvr.borrow_mut().data;
wayvr.state.set_display_visible(ctx.display, false);
Ok(())
}
fn resume(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> {
let ctx = self.context.borrow_mut();
let wayvr = &mut ctx.wayvr.borrow_mut().data;
wayvr.state.set_display_visible(ctx.display, true);
Ok(())
}
fn render(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> {
let ctx = self.context.borrow();
let mut wayvr = ctx.wayvr.borrow_mut();
match wayvr.data.tick_display(ctx.display) {
Ok(_) => {}
Err(e) => {
log::error!("tick_display failed: {}", e);
return Ok(()); // do not proceed further
}
}
let dmabuf_data = wayvr
.data
.state
.get_dmabuf_data(ctx.display)
.ok_or(anyhow::anyhow!("Failed to fetch dmabuf data"))?
.clone();
drop(wayvr);
drop(ctx);
self.ensure_dmabuf(dmabuf_data.clone())?;
Ok(())
}
fn view(&mut self) -> Option<Arc<vulkano::image::view::ImageView>> {
self.view.clone()
}
fn frame_transform(&mut self) -> Option<FrameTransform> {
self.view.as_ref().map(|view| FrameTransform {
extent: view.image().extent(),
..Default::default()
})
}
}
#[allow(dead_code)]
pub fn create_wayvr_display_overlay<O>(
app: &mut state::AppState,
display_width: u16,
display_height: u16,
display_handle: wayvr::display::DisplayHandle,
display_scale: f32,
name: &str,
) -> anyhow::Result<OverlayData<O>>
where
O: Default,
{
let transform = ui_transform(&[display_width as u32, display_height as u32]);
let state = OverlayState {
name: format!("WayVR - {}", name).into(),
keyboard_focus: Some(KeyboardFocus::WayVR),
want_visible: true,
interactable: true,
grabbable: true,
spawn_scale: display_scale,
spawn_point: vec3a(0.0, -0.1, -1.0),
interaction_transform: transform,
..Default::default()
};
let wayvr = app.get_wayvr()?;
let renderer = WayVRRenderer::new(app, wayvr, display_handle)?;
let context = renderer.context.clone();
let backend = Box::new(SplitOverlayBackend {
renderer: Box::new(renderer),
interaction: Box::new(WayVRInteractionHandler::new(context, Affine2::IDENTITY)),
});
Ok(OverlayData {
state,
backend,
..Default::default()
})
}
fn show_display<O>(wayvr: &mut WayVRData, overlays: &mut OverlayContainer<O>, display_name: &str)
where
O: Default,
{
if let Some(display) = WayVR::get_display_by_name(&wayvr.data.state.displays, display_name) {
if let Some(overlay_id) = wayvr.display_handle_map.get(&display) {
if let Some(overlay) = overlays.mut_by_id(*overlay_id) {
overlay.state.want_visible = true;
}
}
wayvr.data.state.set_display_visible(display, true);
}
}
fn action_app_click<O>(
app: &mut AppState,
overlays: &mut OverlayContainer<O>,
catalog_name: &Arc<str>,
app_name: &Arc<str>,
) -> anyhow::Result<()>
where
O: Default,
{
let wayvr = app.get_wayvr()?.clone();
let catalog = app
.session
.wayvr_config
.get_catalog(catalog_name)
.ok_or(anyhow::anyhow!(
"Failed to get catalog \"{}\"",
catalog_name
))?
.clone();
if let Some(app_entry) = catalog.get_app(app_name) {
let mut wayvr = wayvr.borrow_mut();
let disp_handle =
get_or_create_display_by_name(app, &mut wayvr, &app_entry.target_display)?;
let args_vec = match &app_entry.args {
Some(args) => gen_args_vec(args),
None => vec![],
};
let env_vec = match &app_entry.env {
Some(env) => gen_env_vec(env),
None => vec![],
};
// Terminate existing process if required
if let Some(process_handle) =
wayvr
.data
.state
.process_query(disp_handle, &app_entry.exec, &args_vec, &env_vec)
{
// Terminate process
wayvr.data.terminate_process(process_handle);
} else {
// Spawn process
wayvr.data.state.spawn_process(
disp_handle,
&app_entry.exec,
&args_vec,
&env_vec,
Default::default(),
)?;
show_display::<O>(&mut wayvr, overlays, app_entry.target_display.as_str());
}
}
Ok(())
}
pub fn action_display_click<O>(
app: &mut AppState,
overlays: &mut OverlayContainer<O>,
display_name: &Arc<str>,
action: &WayVRDisplayClickAction,
) -> anyhow::Result<()>
where
O: Default,
{
let wayvr = app.get_wayvr()?;
let mut wayvr = wayvr.borrow_mut();
let Some(handle) = WayVR::get_display_by_name(&wayvr.data.state.displays, display_name) else {
return Ok(());
};
let Some(display) = wayvr.data.state.displays.get_mut(&handle) else {
return Ok(());
};
let Some(overlay_id) = display.overlay_id else {
return Ok(());
};
let Some(overlay) = overlays.mut_by_id(overlay_id) else {
return Ok(());
};
match action {
WayVRDisplayClickAction::ToggleVisibility => {
// Toggle visibility
overlay.state.want_visible = !overlay.state.want_visible;
}
WayVRDisplayClickAction::Reset => {
// Show it at the front
overlay.state.want_visible = true;
overlay.state.reset(app, true);
}
}
Ok(())
}
pub fn wayvr_action<O>(app: &mut AppState, overlays: &mut OverlayContainer<O>, action: &WayVRAction)
where
O: Default,
{
match action {
WayVRAction::AppClick {
catalog_name,
app_name,
} => {
if let Err(e) = action_app_click(app, overlays, catalog_name, app_name) {
// Happens if something went wrong with initialization
// or input exec path is invalid. Do nothing, just print an error
error_toast(app, "action_app_click failed", e);
}
}
WayVRAction::DisplayClick {
display_name,
action,
} => {
if let Err(e) = action_display_click::<O>(app, overlays, display_name, action) {
error_toast(app, "action_display_click failed", e);
}
}
WayVRAction::ToggleDashboard => {
let wayvr = app.get_wayvr().unwrap(); /* safe */
let mut wayvr = wayvr.borrow_mut();
if let Err(e) = toggle_dashboard::<O>(app, overlays, &mut wayvr) {
error_toast(app, "toggle_dashboard failed", e);
}
}
}
}
+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",
+29
View File
@@ -0,0 +1,29 @@
# 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]
corner_radius: 0
bg_color: "#ffff00"
- type: Panel
rect: [0, 98, 200, 4]
corner_radius: 0
bg_color: "#ffff00"
- type: Label
rect: [8, 90, 600, 70]
corner_radius: 0
font_size: 18
fg_color: "#ffff00"
source: Static
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
+68 -76
View File
@@ -22,47 +22,36 @@ 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", ~]
# When using the purple pointer...
# None - No special functionality when using purple pointer (Default)
# Shift - Use same functionality as the orange pointer
# Ctrl - Use Main layout with Ctrl modifier
# Alt - Use Main layout with Alt modifier
# Super - Use Main layout with Super (WinKey) modifier
# Meta - Use Main layout with Meta (AltGr) modifier
alt_modifier: None
# Shell commands to be used in a layout.
# Value is an array of string arguments.
exec_commands:
@@ -70,7 +59,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 +67,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": [" \\", "|"]
+191 -81
View File
@@ -12,20 +12,23 @@ spawn_pos: [0, -0.1, -0.5]
elements:
- type: Panel
rect: [0, 0, 600, 800]
bg_color: "#102030"
corner_radius: 8
bg_color: "#1e2030"
- type: Label
rect: [15, 35, 600, 70]
corner_radius: 6
font_size: 24
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Settings
- type: Button
rect: [560, 0, 40, 40]
corner_radius: 8
font_size: 16
bg_color: "#880000"
fg_color: "#ffffff"
bg_color: "#ed8796"
fg_color: "#24273a"
text: X
click_down:
- type: Window
@@ -34,33 +37,38 @@ elements:
- type: Panel
rect: [50, 53, 500, 1]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
####### Watch Section #######
- type: Label
rect: [15, 85, 570, 24]
corner_radius: 6
font_size: 18
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Watch
- type: Panel
rect: [250, 105, 1, 100]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
- type: Label
rect: [288, 105, 100, 24]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Visibility
- type: Button
rect: [270, 120, 100, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Hide"
click_down:
- type: Watch
@@ -68,9 +76,10 @@ elements:
- type: Button
rect: [270, 170, 100, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Swap Hand"
click_down:
- type: Watch
@@ -78,20 +87,23 @@ elements:
- type: Panel
rect: [390, 105, 1, 100]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
- type: Label
rect: [430, 105, 120, 24]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Watch Fade
- type: Button
rect: [410, 120, 140, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Cutoff Point"
click_down:
- type: Toast
@@ -107,9 +119,10 @@ elements:
- type: Button
rect: [410, 170, 140, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Cutoff Strength"
click_down:
- type: Toast
@@ -125,16 +138,18 @@ elements:
- type: Label
rect: [25, 140, 90, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Rotation
- type: Button
rect: [108, 120, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "X"
click_down:
- type: Toast
@@ -150,9 +165,10 @@ elements:
- type: Button
rect: [153, 120, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Y"
click_down:
- type: Toast
@@ -168,9 +184,10 @@ elements:
- type: Button
rect: [198, 120, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Z"
click_down:
- type: Toast
@@ -186,16 +203,18 @@ elements:
- type: Label
rect: [25, 190, 90, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Position
- type: Button
rect: [108, 170, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "X"
click_down:
- type: Toast
@@ -211,9 +230,10 @@ elements:
- type: Button
rect: [153, 170, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Y"
click_down:
- type: Toast
@@ -229,9 +249,10 @@ elements:
- type: Button
rect: [198, 170, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#306060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Z"
click_down:
- type: Toast
@@ -247,28 +268,32 @@ elements:
- type: Panel
rect: [50, 220, 500, 1]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
####### Mirror Section #######
- type: Label
rect: [15, 255, 570, 24]
corner_radius: 6
font_size: 18
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Mirrors
- type: Label
rect: [25, 290, 30, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: M1
- type: Button
rect: [60, 270, 110, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#707070"
fg_color: "#cad3f5"
bg_color: "#494d64"
text: "Show/Hide"
click_down: # ToggleVisible if exists, else create
- type: Overlay
@@ -280,9 +305,10 @@ elements:
- type: Button
rect: [185, 270, 60, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#707070"
fg_color: "#cad3f5"
bg_color: "#494d64"
text: "Lock"
click_down:
- type: Overlay
@@ -291,9 +317,10 @@ elements:
- type: Button
rect: [258, 270, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#880000"
fg_color: "#24273a"
bg_color: "#ed8796"
text: "X"
click_down:
- type: Window
@@ -302,16 +329,18 @@ elements:
- type: Label
rect: [25, 340, 30, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: M2
- type: Button
rect: [60, 320, 110, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#707070"
fg_color: "#cad3f5"
bg_color: "#494d64"
text: "Show/Hide"
click_down:
- type: Overlay
@@ -323,9 +352,10 @@ elements:
- type: Button
rect: [185, 320, 60, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#707070"
fg_color: "#cad3f5"
bg_color: "#494d64"
text: "Lock"
click_down:
- type: Overlay
@@ -334,9 +364,10 @@ elements:
- type: Button
rect: [258, 320, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#880000"
fg_color: "#24273a"
bg_color: "#ed8796"
text: "X"
click_down:
- type: Window
@@ -345,16 +376,18 @@ elements:
- type: Label
rect: [25, 390, 30, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: M3
- type: Button
rect: [60, 370, 110, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#707070"
fg_color: "#cad3f5"
bg_color: "#494d64"
text: "Show/Hide"
click_down:
- type: Overlay
@@ -366,9 +399,10 @@ elements:
- type: Button
rect: [185, 370, 60, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#707070"
fg_color: "#cad3f5"
bg_color: "#494d64"
text: "Lock"
click_down:
- type: Overlay
@@ -377,9 +411,10 @@ elements:
- type: Button
rect: [258, 370, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#880000"
fg_color: "#24273a"
bg_color: "#ed8796"
text: "X"
click_down:
- type: Window
@@ -388,29 +423,33 @@ elements:
- type: Panel
rect: [300, 240, 1, 200]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
####### Color Gain Section #######
- type: Label
rect: [325, 255, 90, 24]
corner_radius: 6
font_size: 18
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Color Gain
- type: Label
rect: [470, 255, 90, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: (SteamVR)
- type: Button
rect: [330, 270, 60, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#707070"
fg_color: "#cad3f5"
bg_color: "#494d64"
text: "All"
click_down:
- type: Toast
@@ -426,9 +465,10 @@ elements:
- type: Button
rect: [405, 270, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#701010"
fg_color: "#24273a"
bg_color: "#e78284"
text: "R"
click_down:
- type: Toast
@@ -444,9 +484,10 @@ elements:
- type: Button
rect: [450, 270, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#107010"
fg_color: "#24273a"
bg_color: "#a6d189"
text: "G"
click_down:
- type: Toast
@@ -462,9 +503,10 @@ elements:
- type: Button
rect: [495, 270, 30, 30]
corner_radius: 15
font_size: 12
fg_color: "#ffffff"
bg_color: "#101070"
fg_color: "#24273a"
bg_color: "#8caaee"
text: "B"
click_down:
- type: Toast
@@ -480,81 +522,149 @@ elements:
- type: Panel
rect: [325, 315, 225, 1]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
####### Playspace Section #######
- type: Label
rect: [325, 345, 90, 24]
corner_radius: 6
font_size: 18
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Playspace
- type: Button
rect: [330, 360, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Fix Floor"
click_down:
- type: System
action: PlayspaceFixFloor
- type: Window
target: "settings"
action: Destroy
- type: Button
rect: [330, 410, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Reset Offset"
click_down:
- type: System
action: PlayspaceResetOffset
- type: Window
target: "settings"
action: Destroy
####### Notifications Section #######
- type: Panel
rect: [50, 460, 500, 1]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
- type: Label
rect: [325, 490, 90, 24]
corner_radius: 6
font_size: 18
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Notifications
- type: Button
rect: [330, 505, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#606020"
fg_color: "#24273a"
bg_color: "#e64553"
text: "Enabled"
click_down:
- type: System
action: ToggleNotifications
highlight: Notifications
- type: Button
rect: [330, 555, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#606020"
fg_color: "#24273a"
bg_color: "#e64553"
text: "Sound Enabled"
click_down:
- type: System
action: ToggleNotificationSounds
highlight: NotificationSounds
####### Behavior Section #######
- type: Label
rect: [15, 490, 570, 24]
corner_radius: 6
font_size: 18
fg_color: "#cad3f5"
source: Static
text: Behavior
- type: Button
rect: [30, 505, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#24273a"
bg_color: "#e64553"
text: "Auto-Realign"
click_down:
- type: System
action: ToggleAutoRealign
highlight: AutoRealign
- type: Button
rect: [30, 555, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#24273a"
bg_color: "#e64553"
text: "Grab+Scroll Slide"
click_down:
- type: System
action: ToggleAllowSliding
highlight: AllowSliding
####### Footer Section #######
- type: Panel
rect: [50, 605, 500, 1]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
- type: Button
rect: [330, 625, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Save Config"
click_down:
- type: System
action: PersistConfig
- type: Toast
message: Settings saved successfully.
- type: Button
rect: [30, 625, 250, 30]
corner_radius: 6
font_size: 12
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Save Overlay Layout"
click_down:
- type: System
action: PersistLayout
- type: Toast
message: Saved. You will see this layout on next startup.
Binary file not shown.
+82 -56
View File
@@ -1,4 +1,4 @@
# looking to make changes?
# looking to make changes?
# drop me in ~/.config/wlxoverlay/watch.yaml
#
@@ -9,29 +9,44 @@ size: [400, 200]
elements:
# background panel
- type: Panel
rect: [0, 0, 400, 200]
bg_color: "#353535"
rect: [0, 30, 400, 130]
corner_radius: 20
bg_color: "#24273a"
- type: Button
rect: [2, 162, 26, 36]
font_size: 14
bg_color: "#808040"
fg_color: "#ffffff"
corner_radius: 4
font_size: 15
bg_color: "#c6a0f6"
fg_color: "#24273a"
text: "C"
click_up: # destroy if exists, otherwise create
- type: Window
target: settings
action: ShowUi # only triggers if not exists
- type: Window
- type: Window
target: settings
action: Destroy # only triggers if exists since before current frame
# Dashboard toggle button
- type: Button
rect: [32, 162, 48, 36]
corner_radius: 4
font_size: 15
bg_color: "#2288FF"
fg_color: "#24273a"
text: "Dash"
click_up:
- type: WayVR
action: ToggleDashboard
# Keyboard button
- type: Button
rect: [32, 162, 60, 36]
font_size: 14
fg_color: "#FFFFFF"
bg_color: "#406050"
rect: [84, 162, 48, 36]
corner_radius: 4
font_size: 15
fg_color: "#24273a"
bg_color: "#a6da95"
text: Kbd
click_up:
- type: Overlay
@@ -52,52 +67,56 @@ elements:
scroll_up:
- type: Overlay
target: "kbd"
action:
action:
Opacity: { delta: 0.025 }
scroll_down:
- type: Overlay
target: "kbd"
action:
action:
Opacity: { delta: -0.025 }
# bottom row, of keyboard + overlays
- type: OverlayList
rect: [94, 160, 306, 40]
font_size: 14
fg_color: "#FFFFFF"
bg_color: "#405060"
rect: [134, 160, 266, 40]
corner_radius: 4
font_size: 15
fg_color: "#cad3f5"
bg_color: "#1e2030"
layout: Horizontal
click_up: ToggleVisible
long_click_up: Reset
right_up: ToggleImmovable
middle_up: ToggleInteraction
scroll_up:
scroll_up:
Opacity: { delta: 0.025 }
scroll_down:
scroll_down:
Opacity: { delta: -0.025 }
# local clock
- type: Label
rect: [19, 90, 200, 50]
font_size: 46
fg_color: "#ffffff"
corner_radius: 4
font_size: 46 # Use 32 for 12-hour time
fg_color: "#cad3f5"
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
rect: [20, 117, 200, 20]
corner_radius: 4
font_size: 14
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Clock
format: "%x" # local date representation
# local day-of-week
- type: Label
rect: [20, 137, 200, 50]
corner_radius: 4
font_size: 14
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Clock
format: "%A" # Tuesday
#format: "%a" # Tue
@@ -105,62 +124,69 @@ elements:
# alt clock 1
- type: Label
rect: [210, 90, 200, 50]
font_size: 24
fg_color: "#99BBAA"
corner_radius: 4
font_size: 24 # Use 18 for 12-hour time
fg_color: "#8bd5ca"
source: Clock
timezone: "Asia/Tokyo" # change TZ1 here
timezone: 0
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]
corner_radius: 4
font_size: 14
fg_color: "#99BBAA"
source: Static
text: "Tokyo" # change TZ1 label here
fg_color: "#8bd5ca"
source: Timezone
timezone: 0
# alt clock 2
- type: Label
rect: [210, 150, 200, 50]
font_size: 24
fg_color: "#AA99BB"
corner_radius: 4
font_size: 24 # Use 18 for 12-hour time
fg_color: "#b7bdf8"
source: Clock
timezone: "America/Chicago" # change TZ2 here
timezone: 1
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]
corner_radius: 4
font_size: 14
fg_color: "#AA99BB"
source: Static
text: "Chicago" # change TZ2 label here
fg_color: "#b7bdf8"
source: Timezone
timezone: 1
# batteries
- type: BatteryList
rect: [0, 0, 400, 30]
font_size: 14
fg_color: "#99BBAA"
rect: [0, 5, 400, 30]
corner_radius: 4
font_size: 16
fg_color: "#8bd5ca"
fg_color_low: "#B06060"
fg_color_charging: "#6080A0"
num_devices: 9
layout: Horizontal
low_threshold: 20
low_threshold: 33
# volume buttons
- type: Button
rect: [327, 52, 46, 32]
font_size: 14
fg_color: "#FFFFFF"
bg_color: "#505050"
text: "+"
rect: [315, 52, 70, 32]
corner_radius: 4
font_size: 13
fg_color: "#cad3f5"
bg_color: "#5b6078"
text: "Vol +"
click_down:
- type: Exec
command: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%" ]
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%"]
- type: Button
rect: [327, 116, 46, 32]
font_size: 14
fg_color: "#FFFFFF"
bg_color: "#505050"
text: "-"
rect: [315, 116, 70, 32]
corner_radius: 4
font_size: 13
fg_color: "#cad3f5"
bg_color: "#5b6078"
text: "Vol -"
click_down:
- type: Exec
command: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%" ]
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%"]
+71
View File
@@ -0,0 +1,71 @@
# This is an example WayVR panel configuration. It demonstrates all the capabilities of this module.
# looking to make changes?
# drop me in ~/.config/wlxoverlay/wayvr.yaml
#
version: 1
# Set to true if you want to make Wyland server instantly available.
# By default, WayVR starts only when it's needed.
# (this option is primarily used for remote starting external processes and development purposes)
run_compositor_at_start: false
# Automatically close overlays with zero window count?
auto_hide: true
# For how long an overlay should be visible in case if there are no windows present? (in milliseconds, auto_hide needs to be enabled)
# This value shouldn't be set at 0, because some programs could re-initialize a window during startup (splash screens for example)
auto_hide_delay: 750
# In milliseconds
keyboard_repeat_delay: 200
# Chars per second
keyboard_repeat_rate: 50
# WayVR-compatible dashboard.
# For now, there is only one kind of dashboard with WayVR IPC support (WayVR Dashboard).
# Build instructions: https://github.com/olekolek1000/wayvr-dashboard
# exec: Executable path, for example /home/USER/wayvr_dashboard/src-tauri/target/release/wayvr_dashboard
# GDK_BACKEND=wayland: Force-use Wayland for GTK apps
dashboard:
exec: "wayvr_dashboard"
args: ""
env: ["GDK_BACKEND=wayland"]
displays:
Watch:
width: 400
height: 600
scale: 0.4
attach_to: "HandRight" # HandLeft, HandRight
pos: [0.0, 0.0, 0.125]
rotation: {axis: [1.0, 0.0, 0.0], angle: -45.0}
Disp1:
width: 640
height: 480
primary: true # Required if you want to attach external processes (not spawned by WayVR itself) without WAYVR_DISPLAY_NAME set
Disp2:
width: 1280
height: 720
scale: 2.0
catalogs:
default_catalog:
apps:
- name: "Calc"
target_display: "Disp1"
exec: "kcalc"
env: ["FOO=bar"]
shown_at_start: false
- name: "htop"
target_display: "Watch"
exec: "konsole"
args: "-e htop"
- name: "Browser"
target_display: "Disp2"
exec: "cage"
args: "chromium -- --incognito"
+115
View File
@@ -27,15 +27,30 @@ pub mod frag_color {
layout (set = 0, binding = 0) uniform ColorBlock {
uniform vec4 in_color;
uniform vec2 corner_radius;
};
void main()
{
out_color.r = corner_radius.r;
out_color = in_color;
vec2 uv_circ = ((1. - corner_radius) - (abs(in_uv + vec2(-0.5)) * 2.))/corner_radius;
float dist = length(uv_circ);
out_color.a = mix(out_color.a, 0.,
float(dist > 1.)
* float(uv_circ.x < 0.)
* float(uv_circ.y < 0.));
}
",
}
}
//layout (location = 1) in float corner_radius;
//out_color = in_color;
// Some equation that determines whether to keep the pixel
// Use Lerp not if
//out_color.a = 0;
pub mod frag_glyph {
vulkano_shaders::shader! {
@@ -61,6 +76,52 @@ pub mod frag_glyph {
}
}
pub mod frag_sprite2 {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform UniBlock {
uniform vec4 st;
uniform vec4 mul;
};
void main()
{
out_color = texture(in_texture, (in_uv * st.xy) + st.zw) * mul;
}
",
}
}
pub mod frag_sprite2_hl {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform UniBlock {
uniform vec4 st;
uniform vec4 mul;
};
void main()
{
out_color = texture(in_texture, (in_uv * st.xy) + st.zw).a * mul;
}
",
}
}
pub mod frag_sprite {
vulkano_shaders::shader! {
ty: "fragment",
@@ -80,6 +141,31 @@ pub mod frag_sprite {
}
}
pub mod frag_grid {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
void main()
{
float fade = max(1.0 - 2.0 * length(in_uv.xy + vec2(-0.5, -0.5)), 0.0);
float grid;
if (fract(in_uv.x / 0.0005) < 0.01 || fract(in_uv.y / 0.0005) < 0.01) {
grid = 1.0;
} else {
grid = 0.0;
}
out_color = vec4(1.0, 1.0, 1.0, grid * fade);
}
",
}
}
pub mod frag_screen {
vulkano_shaders::shader! {
ty: "fragment",
@@ -94,6 +180,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.4166667)) * vec4(1.055)) - vec4(0.055);
vec4 lower = out_color*vec4(12.92);
out_color = mix(higher, lower, cutoff);
out_color.a = 1.0;
}
",
@@ -129,3 +221,26 @@ pub mod frag_srgb {
",
}
}
pub mod frag_swapchain {
vulkano_shaders::shader! {
ty: "fragment",
src: r"#version 310 es
precision highp float;
layout (location = 0) in vec2 in_uv;
layout (location = 0) out vec4 out_color;
layout (set = 0, binding = 0) uniform sampler2D in_texture;
layout (set = 1, binding = 0) uniform AlphaBlock {
uniform float alpha;
};
void main()
{
out_color = texture(in_texture, in_uv);
out_color.a *= alpha;
}
",
}
}
+132 -46
View File
@@ -1,32 +1,62 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::bail;
use glam::Vec3;
use rodio::{OutputStream, OutputStreamHandle};
use glam::Affine3A;
use idmap::IdMap;
use rodio::{Decoder, OutputStream, OutputStreamHandle, Source};
use serde::{Deserialize, Serialize};
use smallvec::{smallvec, SmallVec};
use vulkano::format::Format;
use std::{io::Cursor, sync::Arc};
use vulkano::image::view::ImageView;
#[cfg(feature = "wayvr")]
use {
crate::config_wayvr::{self, WayVRConfig},
crate::overlays::wayvr::WayVRData,
std::{cell::RefCell, rc::Rc},
};
#[cfg(feature = "osc")]
use crate::backend::osc::OscSender;
use crate::{
backend::{common::TaskContainer, input::InputState},
config::GeneralConfig,
backend::{input::InputState, overlay::OverlayID, task::TaskContainer},
config::{AStrMap, GeneralConfig},
config_io,
graphics::WlxGraphics,
gui::font::FontCache,
hid::HidProvider,
shaders::{frag_color, frag_glyph, frag_screen, frag_sprite, frag_srgb, vert_common},
overlays::toast::{DisplayMethod, ToastTopic},
shaders::{
frag_color, frag_glyph, frag_grid, frag_screen, frag_sprite, frag_sprite2, frag_sprite2_hl,
frag_swapchain, vert_common,
},
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum KeyboardFocus {
PhysicalScreen,
#[allow(dead_code)] // Not available if "wayvr" feature is disabled
WayVR, // (for now without wayland window id data, it's handled internally),
}
pub struct AppState {
pub fc: FontCache,
pub session: AppSession,
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,
pub sprites: AStrMap<Arc<ImageView>>,
pub keyboard_focus: KeyboardFocus,
#[cfg(feature = "osc")]
pub osc_sender: Option<OscSender>,
#[cfg(feature = "wayvr")]
pub wayvr: Option<Rc<RefCell<WayVRData>>>, // Dynamically created if requested
}
impl AppState {
@@ -46,69 +76,109 @@ impl AppState {
let shader = frag_glyph::load(graphics.device.clone())?;
shaders.insert("frag_glyph", shader);
let shader = frag_grid::load(graphics.device.clone())?;
shaders.insert("frag_grid", shader);
let shader = frag_sprite::load(graphics.device.clone())?;
shaders.insert("frag_sprite", shader);
let shader = frag_sprite2::load(graphics.device.clone())?;
shaders.insert("frag_sprite2", shader);
let shader = frag_sprite2_hl::load(graphics.device.clone())?;
shaders.insert("frag_sprite2_hl", shader);
let shader = frag_screen::load(graphics.device.clone())?;
shaders.insert("frag_screen", shader);
let shader = frag_srgb::load(graphics.device.clone())?;
shaders.insert("frag_srgb", shader);
let shader = frag_swapchain::load(graphics.device.clone())?;
shaders.insert("frag_swapchain", shader);
}
#[cfg(feature = "wayvr")]
let mut tasks = TaskContainer::new();
#[cfg(not(feature = "wayvr"))]
let tasks = TaskContainer::new();
let session = AppSession::load();
#[cfg(feature = "wayvr")]
let wayvr = session
.wayvr_config
.post_load(&session.config, &mut tasks)?;
#[cfg(feature = "osc")]
let osc_sender = crate::backend::osc::OscSender::new(session.config.osc_out_port).ok();
Ok(AppState {
fc: FontCache::new()?,
session: AppSession::load(),
tasks: TaskContainer::new(),
fc: FontCache::new(session.config.primary_font.clone())?,
session,
tasks,
graphics,
format: Format::R8G8B8A8_UNORM,
input_state: InputState::new(),
hid_provider: crate::hid::initialize(),
audio: AudioOutput::new(),
screens: smallvec![],
anchor: Affine3A::IDENTITY,
sprites: AStrMap::new(),
keyboard_focus: KeyboardFocus::PhysicalScreen,
#[cfg(feature = "osc")]
osc_sender,
#[cfg(feature = "wayvr")]
wayvr,
})
}
#[cfg(feature = "wayvr")]
#[allow(dead_code)]
pub fn get_wayvr(&mut self) -> anyhow::Result<Rc<RefCell<WayVRData>>> {
if let Some(wvr) = &self.wayvr {
Ok(wvr.clone())
} else {
let wayvr = Rc::new(RefCell::new(WayVRData::new(
WayVRConfig::get_wayvr_config(&self.session.config, &self.session.wayvr_config),
)?));
self.wayvr = Some(wayvr.clone());
Ok(wayvr)
}
}
}
pub struct AppSession {
pub config_root_path: PathBuf,
pub config: GeneralConfig,
pub color_norm: Vec3,
pub color_shift: Vec3,
pub color_alt: Vec3,
pub color_grab: Vec3,
#[cfg(feature = "wayvr")]
pub wayvr_config: WayVRConfig,
pub toast_topics: IdMap<ToastTopic, DisplayMethod>,
}
impl AppSession {
pub fn load() -> Self {
let config_root_path = config_io::ensure_config_root();
log::info!("Config root path: {}", config_root_path.to_string_lossy());
let config_root_path = config_io::ConfigRoot::Generic.ensure_dir();
log::info!("Config root path: {:?}", config_root_path);
let config = GeneralConfig::load_from_disk();
let mut toast_topics = IdMap::new();
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);
});
#[cfg(feature = "wayvr")]
let wayvr_config = config_wayvr::load_wayvr();
AppSession {
config_root_path,
config,
color_norm: Vec3 {
x: 0.,
y: 1.,
z: 1.,
},
color_shift: Vec3 {
x: 1.,
y: 1.,
z: 0.,
},
color_alt: Vec3 {
x: 1.,
y: 0.,
z: 1.,
},
color_grab: Vec3 {
x: 1.,
y: 0.,
z: 0.,
},
toast_topics,
#[cfg(feature = "wayvr")]
wayvr_config,
}
}
}
@@ -126,23 +196,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 id: OverlayID,
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