Compare commits

...
83 Commits
Author SHA1 Message Date
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
56 changed files with 4312 additions and 1987 deletions

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
name: Build AppImage
on:
push:
branches:
- 'main'
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
rustup update
test -f linuxdeploy-x86_64.AppImage || wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
chmod +x linuxdeploy-x86_64.AppImage
test -d ${APPDIR} && rm -rf ${APPDIR}
mkdir -p ${APPDIR}/usr/bin
- name: Cargo Build
run: |
cargo build --release
mv target/release/wlx-overlay-s ${APPDIR}/usr/bin
chmod +x ${APPDIR}/usr/bin/wlx-overlay-s
- name: Package AppImage
run: |
export VERSION=$GITHUB_REF_NAME
./linuxdeploy-x86_64.AppImage -dwlx-overlay-s.desktop -iwlx-overlay-s.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
mv WlxOverlay-S-$VERSION-x86_64.AppImage WlxOverlay-S-x86_64.AppImage
- name: Upload AppImage
uses: actions/upload-artifact@v4
with:
name: WlxOverlay-S-${{ github.ref_name }}-x86_64.AppImage
path: ./WlxOverlay-S-x86_64.AppImage
+1 -1
View File
@@ -2,7 +2,7 @@ name: Check Full
on:
pull_request:
#branches: [ "main" ]
branches: [ "!main" ]
env:
CARGO_TERM_COLOR: always
Generated
+815 -499
View File
File diff suppressed because it is too large. Load diff
+34 -25
View File
@@ -4,74 +4,83 @@ debug = true
[package]
name = "wlx-overlay-s"
version = "0.4.0"
version = "0.6.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.86"
ash = "^0.37.2"
chrono = "0.4.29"
chrono = "0.4.38"
chrono-tz = "0.9.0"
clap = { version = "4.5.1", features = ["derive"] }
clap = { version = "4.5.6", features = ["derive"] }
config = "0.14.0"
cstr = "0.2.12"
ctrlc = { version = "3.4.2", features = ["termination"] }
ctrlc = { version = "3.4.4", features = ["termination"] }
dbus = { version = "0.9.7" }
flexi_logger = "0.28.0"
flexi_logger = "0.28.1"
fontconfig-rs = "0.1.1"
freetype-rs = "0.36.0"
futures = "0.3.29"
glam = { version = "0.27.0", features = ["approx", "serde"] }
freetype-rs = "0.36.0" # latest version supported on ubuntu 22.04
futures = "0.3.30"
glam = { version = "0.28.0", features = ["approx", "mint", "serde"] }
idmap = { version = "0.2.21", features = ["serde"] }
idmap-derive = "0.1.2"
input-linux = "0.6.0"
input-linux = "0.7.0"
json = { version = "0.12.4", optional = true }
json5 = "0.4.1"
libc = "0.2.153"
libloading = "0.8.3"
libc = "0.2.155"
log = "0.4.21"
once_cell = "1.19.0"
openxr = { version = "0.17.1", features = ["linked"], optional = true }
openxr = { git = "https://github.com/galister/openxrs", rev = "af4a55d", features = [
"linked",
"mint",
], optional = true }
ovr_overlay = { features = [
"ovr_input",
"ovr_system",
], git = "https://github.com/galister/ovr_overlay_oyasumi", optional = true }
regex = "1.9.5"
rodio = { version = "0.17.1", default-features = false, features = [
regex = "1.10.4"
rodio = { version = "0.19.0", 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 = { version = "1.0.203", features = ["derive", "rc"] }
serde_json = "1.0.117"
serde_yaml = "0.9.34"
smallvec = "1.11.0"
smallvec = "1.13.2"
strum = { version = "0.26.2", features = ["derive"] }
sysinfo = { version = "0.30.0" }
thiserror = "1.0.56"
sysinfo = { version = "0.31.2" }
thiserror = "1.0.61"
vulkano = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
vulkano-shaders = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.3.11", default-features = false }
winit = { version = "0.29.15", optional = true }
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.3.13", default-features = false }
libmonado-rs = { git = "https://github.com/technobaboo/libmonado-rs", rev = "a495f6d", optional = true }
winit = { version = "0.30.0", optional = true }
xdg = "2.5.2"
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
serde_json5 = "0.1.0"
xkbcommon = { version = "0.7.0" }
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"
[build-dependencies]
regex = { version = "*" }
[features]
default = ["openvr", "openxr", "osc", "x11", "wayland"]
openvr = ["dep:ovr_overlay", "dep:json"]
openxr = ["dep:openxr"]
openxr = ["dep:openxr", "dep:libmonado-rs"]
osc = ["dep:rosc"]
x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"]
wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"]
pipewire = ["wlx-capture/pipewire"]
uidev = ["dep:winit"]
no-dmabuf = []
xcb = ["dep:xcb"]
as-raw-xcb-connection = []
+37 -24
View File
@@ -4,21 +4,20 @@ A lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops, inspired by XS
WlxOverlay-S allows 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
1. Grab the latest AppImage from [Releases](https://github.com/galister/wlx-overlay-s/releases).
1. `chmod +x WlxOverlay-S-*.AppImage`
@@ -29,18 +28,21 @@ AUR package is [wlx-overlay-s-git](https://aur.archlinux.org/packages/wlx-overla
You may also want to [build from source](https://github.com/galister/wlx-overlay-s/wiki/Building-from-Source).
# First Start
## First Start
**If you get a screen share pop-up, check the terminal and select the screens in the order it tells you to.**
**When the screen share pop-up appears, check the terminal and select the screens in the order it requests.**
If you selected the screens in the wrong order:
- `rm ~/.config/wlxoverlay/conf.d/pw_tokens.yaml` then restart
SteamVR users: WlxOverlay-S will register itself for auto-start, so you will not need to start it every time.
- `rm ~/.config/wlxoverlay/conf.d/pw_tokens.yaml` then restart
**SteamVR users**: WlxOverlay-S will register itself for auto-start, so you will not need to start it every time.
**Envision users**: Set `wlx-overlay-s --openxr --show` as the _Autostart Command_ on your Envision profile! This will give you a home environment with a [customizable background](https://github.com/galister/wlx-overlay-s/wiki/OpenXR-Skybox)!
**Please continue reading the guide below.**
# Getting Started
## Getting Started
### Working Set
@@ -49,6 +51,7 @@ Your working set consists of your currently selected overlays; screens, mirrors,
The working set appears in front of you when shown, and can be re-centered by hiding and showing again.
Show and hide your working set using:
- Non-vive controller: double-tap B or Y on your left controller.
- Vive controller: double-tap the menu button on your left controller (OpenXR, for SteamVR, you might need to bind `showhide` yourself.)
@@ -56,6 +59,7 @@ Show and hide your working set using:
Much of the functionality in WlxOverlay-S depends on what color of laser you are using 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
@@ -75,14 +79,15 @@ Check your left wrist for the watch. The watch is your primary tool for controll
Hovering a pointer over a screen will move the mouse. If there are more than one pointers hovering a screen, the pointer that was last used to click will take precedence.
The click depends on the laser color:
- Blue laser: Left click
- Orange laser: Right click
- Purple laser: Middle click
- Stick up/down: Scroll wheel
To **curve screens**, grab them with one hand, then with the other hand, hover the laser over the overlay and use the scroll action.
To **curve a screen**, grab it with one hand. Then, using your other hand, hover the laser over the screen and use the scroll action.
See the bindings section on how to grab, move and resize screens.
See the [bindings](#default-bindings) section on how to grab, move and resize screens.
### The keyboard
@@ -90,6 +95,7 @@ The keyboard is fully customizable via the [keyboard.yaml](https://raw.githubuse
Download it into your `~/.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.
@@ -102,29 +108,36 @@ 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.
# Troubleshooting
## Troubleshooting
Check [here](https://github.com/galister/wlx-overlay-s/wiki/Troubleshooting) for tips.
# Known Issues
## Known Issues
## Auto-Start not working on SteamVR 2.4.4
### Mouse is not where it should be
This seems to only affect SteamVR 2.4.4 (stable as of writing). Try switching to `temp_v1.27.5` or `beta` using the SteamVR beta selection if this bothers you. (The `linux-v1.14` branch will not work.)
Hyprland users: Hyprland v0.41.0 changed their absolute input implementation to one that does not respect existing absolute input standards. Make your voice heard: [Hyprland#6023](https://github.com/hyprwm/Hyprland/issues/6023)・[Hyprland#6889](https://github.com/hyprwm/Hyprland/issues/6889)
## Modifiers get stuck in weird ways
Niri users: use on Niri 0.1.7 or later.
Other desktops: You may have selected the screens in the wrong order, see [First Start](#first-start).
### 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 your desktop not react to click or keys due to a modifier being stuck somewhere. Restarting the overlay fixes this.
## Scroll wheel doesn't work
This seems to be an issue specific to Electron apps (Discord, Element, Slack, Spotify) on Wayland. Scrolling will work when using these in your web browser.
## X11 limitations
### 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())
}
+6 -3
View File
@@ -68,10 +68,14 @@ where
app.screens.clear();
let data = if let Some(wl) = wl.as_mut() {
keymap = get_keymap_wl().ok();
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().ok();
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) => {
@@ -91,7 +95,6 @@ where
for (meta, mut state, backend) in data.screens {
if show_screens.arc_get(state.name.as_ref()) {
state.show_hide = true;
state.want_visible = false;
}
overlays.insert(
state.id,
+23 -15
View File
@@ -73,6 +73,9 @@ impl InputState {
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 {}",
@@ -192,6 +195,7 @@ pub struct PointerState {
pub show_hide: 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,
@@ -309,15 +313,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
@@ -366,7 +372,10 @@ 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 {
@@ -376,12 +385,9 @@ where
.grabbed
.is_some_and(|x| x.grabbed_id == hit.overlay)
{
let is_portrait = hovered.view().is_some_and(|v| {
let extent = v.image().extent();
extent[0] >= extent[1]
});
let can_curve = hovered.extent().is_some_and(|e| e[0] >= e[1]);
if is_portrait {
if can_curve {
let cur = hovered.state.curvature.unwrap_or(0.0);
let new = (cur - scroll * 0.01).min(0.35);
if new <= f32::EPSILON {
@@ -389,6 +395,8 @@ where
} else {
hovered.state.curvature = Some(new);
}
} else {
hovered.state.curvature = None;
}
} else {
hovered.backend.on_scroll(app, &hit, scroll);
@@ -547,7 +555,7 @@ impl Pointer {
}
config.transform_values.arc_set(
overlay.state.name.clone(),
overlay.state.saved_transform.unwrap(),
overlay.state.saved_transform.unwrap(), // safe
);
}
+3
View File
@@ -2,6 +2,9 @@ pub mod common;
pub mod input;
pub mod notifications;
#[allow(clippy::all)]
mod notifications_dbus;
#[cfg(feature = "openvr")]
pub mod openvr;
+61 -1
View File
@@ -1,4 +1,9 @@
use dbus::{blocking::Connection, channel::MatchingReceiver, message::MatchRule};
use dbus::{
arg::{PropMap, Variant},
blocking::Connection,
channel::MatchingReceiver,
message::MatchRule,
};
use serde::Deserialize;
use std::{
sync::{
@@ -9,6 +14,7 @@ use std::{
};
use crate::{
backend::notifications_dbus::OrgFreedesktopNotifications,
overlays::toast::{Toast, ToastTopic},
state::AppState,
};
@@ -196,6 +202,60 @@ impl Drop for NotificationManager {
}
}
pub struct DbusNotificationSender {
connection: Connection,
}
impl DbusNotificationSender {
pub fn new() -> anyhow::Result<Self> {
Ok(Self {
connection: Connection::new_session()?,
})
}
pub fn notify_send(
&self,
summary: &str,
body: &str,
urgency: u8,
timeout: i32,
replaces_id: u32,
transient: bool,
) -> anyhow::Result<u32> {
let proxy = self.connection.with_proxy(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
Duration::from_millis(1000),
);
let mut hints = PropMap::new();
hints.insert("urgency".to_string(), Variant(Box::new(urgency)));
hints.insert("transient".to_string(), Variant(Box::new(transient)));
Ok(proxy.notify(
"WlxOverlay-S",
replaces_id,
"",
summary,
body,
vec![],
hints,
timeout,
)?)
}
pub fn notify_close(&self, id: u32) -> anyhow::Result<()> {
let proxy = self.connection.with_proxy(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
Duration::from_millis(1000),
);
proxy.close_notification(id)?;
Ok(())
}
}
fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
let mut args = msg.iter_init();
let app_name: String = args.read()?;
+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)
}
}
+4 -1
View File
@@ -32,7 +32,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
@@ -189,4 +189,7 @@ impl OverlayRenderer for StaticRenderer {
fn view(&mut self) -> Option<Arc<ImageView>> {
Some(self.view.clone())
}
fn extent(&mut self) -> Option<[u32; 3]> {
Some(self.view.image().extent())
}
}
+12 -1
View File
@@ -1,5 +1,6 @@
use std::{
collections::VecDeque,
ops::Add,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
@@ -61,7 +62,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");
@@ -95,6 +96,13 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
AppState::from_graphics(graphics)?
};
if show_by_default {
state.tasks.enqueue_at(
TaskType::System(SystemTask::ShowHide),
Instant::now().add(Duration::from_secs(1)),
)
}
if let Ok(ipd) = system_mgr.get_tracked_device_property::<f32>(
TrackedDeviceIndex::HMD,
ETrackedDeviceProperty::Prop_UserIpdMeters_Float,
@@ -250,6 +258,9 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
SystemTask::ResetPlayspace => {
playspace.reset_offset(&mut chaperone_mgr, &state.input_state);
}
SystemTask::ShowHide => {
overlays.show_hide(&mut state);
}
},
}
}
+11 -1
View File
@@ -114,7 +114,8 @@ impl PlayspaceMover {
let new_hand = data
.pose
.transform_point3a(state.input_state.pointers[data.hand].raw_pose.translation);
let relative_pos = new_hand - data.hand_pose;
let relative_pos =
(new_hand - data.hand_pose) * state.session.config.space_drag_multiplier;
if relative_pos.length_squared() > 1000.0 {
log::warn!("Space drag too fast, ignoring");
@@ -199,6 +200,15 @@ impl PlayspaceMover {
set_working_copy(&self.universe, chaperone_mgr, &mat);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
if self.drag.is_some() {
log::info!("Space drag interrupted by fix floor");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by fix floor");
self.rotate = None;
}
}
pub fn playspace_changed(
+22 -56
View File
@@ -1,9 +1,6 @@
use std::path::PathBuf;
use anyhow::{bail, ensure};
use glam::{Affine3A, Quat, Vec3, Vec3A};
use openxr as xr;
use sysinfo::Process;
use xr::OverlaySessionCreateFlagsEXTX;
pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
@@ -35,6 +32,17 @@ pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
} else {
log::warn!("Missing EXT_hp_mixed_reality_controller extension.");
}
if available_extensions.khr_composition_layer_cylinder {
enabled_extensions.khr_composition_layer_cylinder = true;
} else {
log::warn!("Missing EXT_composition_layer_cylinder extension.");
}
if available_extensions.khr_composition_layer_equirect2 {
enabled_extensions.khr_composition_layer_equirect2 = true;
} else {
log::warn!("Missing EXT_composition_layer_equirect2 extension.");
}
//#[cfg(not(debug_assertions))]
let layers = [];
@@ -124,21 +132,14 @@ pub(super) unsafe fn create_overlay_session(
}
}
pub(super) fn hmd_pose_from_views(views: &[xr::View]) -> (Affine3A, f32) {
let ipd;
let pos = {
let pos0: Vec3 = unsafe { std::mem::transmute(views[0].pose.position) };
let pos1: Vec3 = unsafe { std::mem::transmute(views[1].pose.position) };
ipd = (pos0.distance(pos1) * 1000.0).round() * 0.1;
(pos0 + pos1) * 0.5
};
let rot = {
let rot0: Quat = unsafe { std::mem::transmute(views[0].pose.orientation) };
let rot1: Quat = unsafe { std::mem::transmute(views[1].pose.orientation) };
rot0.lerp(rot1, 0.5)
};
type Vec3M = mint::Vector3<f32>;
type QuatM = mint::Quaternion<f32>;
(Affine3A::from_rotation_translation(rot, pos), ipd)
pub(super) fn ipd_from_views(views: &[xr::View]) -> f32 {
let p0: Vec3 = Vec3M::from(views[0].pose.position).into();
let p1: Vec3 = Vec3M::from(views[1].pose.position).into();
(p0.distance(p1) * 1000.0).round() * 0.1
}
pub(super) fn transform_to_norm_quat(transform: &Affine3A) -> Quat {
@@ -174,43 +175,8 @@ pub(super) fn transform_to_posef(transform: &Affine3A) -> xr::Posef {
translation_rotation_to_posef(translation, rotation)
}
pub(super) fn find_libmonado() -> anyhow::Result<libloading::Library> {
//check env var first
if let Ok(path) = std::env::var("LIBMONADO_PATH") {
let path = PathBuf::from(path);
if path.exists() {
return Ok(unsafe { libloading::Library::new(path)? });
} else {
bail!("LIBMONADO_PATH points to a non-existing file.");
}
}
const PROC_NAMES: [&str; 1] = ["monado-service"];
let mut system = sysinfo::System::new();
system.refresh_processes();
for p in system.processes().values() {
for proc_name in PROC_NAMES.iter() {
if p.name().contains(proc_name) {
if let Some(lib) = proc_load_libmonado(p) {
return Ok(lib);
}
}
}
}
bail!("Could not find libmonado.");
}
fn proc_load_libmonado(proc: &Process) -> Option<libloading::Library> {
let path = proc
.exe()?
.parent()?
.parent()?
.join("lib")
.join("libmonado.so");
if path.exists() {
Some(unsafe { libloading::Library::new(path).ok()? })
} else {
None
}
pub(super) fn posef_to_transform(pose: &xr::Posef) -> Affine3A {
let rotation = QuatM::from(pose.orientation).into();
let translation = Vec3M::from(pose.position).into();
Affine3A::from_rotation_translation(rotation, translation)
}
+252 -98
View File
@@ -1,19 +1,29 @@
use std::time::{Duration, Instant};
use std::{
array::from_fn,
mem::transmute,
time::{Duration, Instant},
};
use glam::{bool, Affine3A, Quat, Vec3};
use openxr as xr;
use libmonado_rs::{Device, Monado};
use openxr::{self as xr, Quaternionf, Vector3f};
use serde::{Deserialize, Serialize};
use crate::{
backend::input::{Haptics, Pointer},
backend::input::{Haptics, Pointer, TrackedDevice, TrackedDeviceRole},
config_io,
state::{AppSession, AppState},
};
use super::XrState;
use super::{helpers::posef_to_transform, XrState};
type XrSession = xr::Session<xr::Vulkan>;
static DOUBLE_CLICK_TIME: Duration = Duration::from_millis(500);
static CLICK_TIMES: [Duration; 3] = [
Duration::ZERO,
Duration::from_millis(500),
Duration::from_millis(750),
];
pub(super) struct OpenXrAction {}
@@ -27,47 +37,97 @@ pub(super) struct OpenXrHand {
space: xr::Space,
}
pub struct CustomClickAction {
pub struct MultiClickHandler<const COUNT: usize> {
name: String,
action_f32: xr::Action<f32>,
action_bool: xr::Action<bool>,
action_f32_double: xr::Action<f32>,
action_bool_double: xr::Action<bool>,
last_click: Option<Instant>,
held: bool,
previous: [Instant; COUNT],
held_active: bool,
held_inactive: bool,
}
impl<const COUNT: usize> MultiClickHandler<COUNT> {
fn new(action_set: &xr::ActionSet, action_name: &str, side: &str) -> anyhow::Result<Self> {
let name = format!("{}_{}-{}", side, COUNT, action_name);
let name_f32 = format!("{}_value", &name);
let action_bool = action_set.create_action::<bool>(&name, &name, &[])?;
let action_f32 = action_set.create_action::<f32>(&name_f32, &name_f32, &[])?;
Ok(Self {
name,
action_f32,
action_bool,
previous: from_fn(|_| Instant::now()),
held_active: false,
held_inactive: false,
})
}
fn check<G>(&mut self, session: &xr::Session<G>, threshold: f32) -> anyhow::Result<bool> {
let res = self.action_bool.state(session, xr::Path::NULL)?;
let mut state = res.is_active && res.current_state;
if !state {
let res = self.action_f32.state(session, xr::Path::NULL)?;
state = res.is_active && res.current_state > threshold;
}
if !state {
self.held_active = false;
self.held_inactive = false;
return Ok(false);
}
if self.held_active {
return Ok(true);
}
if self.held_inactive {
return Ok(false);
}
let passed = self
.previous
.iter()
.all(|instant| instant.elapsed() < CLICK_TIMES[COUNT]);
if passed {
log::trace!("{}: passed", self.name);
self.held_active = true;
self.held_inactive = false;
// reset to no prior clicks
let long_ago = Instant::now() - Duration::from_secs(10);
self.previous
.iter_mut()
.for_each(|instant| *instant = long_ago)
} else if COUNT > 0 {
log::trace!("{}: rotate", self.name);
self.previous.rotate_right(1);
self.previous[0] = Instant::now();
self.held_inactive = true;
}
Ok(passed)
}
}
pub struct CustomClickAction {
single: MultiClickHandler<0>,
double: MultiClickHandler<1>,
triple: MultiClickHandler<2>,
}
impl CustomClickAction {
pub fn new(action_set: &xr::ActionSet, name: &str, side: &str) -> anyhow::Result<Self> {
let action_f32 = action_set.create_action::<f32>(
&format!("{}_{}_value", side, name),
&format!("{} hand {} value", side, name),
&[],
)?;
let action_f32_double = action_set.create_action::<f32>(
&format!("{}_{}_value_double", side, name),
&format!("{} hand {} value double", side, name),
&[],
)?;
let action_bool = action_set.create_action::<bool>(
&format!("{}_{}", side, name),
&format!("{} hand {}", side, name),
&[],
)?;
let action_bool_double = action_set.create_action::<bool>(
&format!("{}_{}_double", side, name),
&format!("{} hand {} double", side, name),
&[],
)?;
let single = MultiClickHandler::new(action_set, name, side)?;
let double = MultiClickHandler::new(action_set, name, side)?;
let triple = MultiClickHandler::new(action_set, name, side)?;
Ok(Self {
action_f32,
action_f32_double,
action_bool,
action_bool_double,
last_click: None,
held: false,
single,
double,
triple,
})
}
pub fn state(
@@ -76,56 +136,15 @@ impl CustomClickAction {
state: &XrState,
session: &AppSession,
) -> anyhow::Result<bool> {
let res = self.action_bool.state(&state.session, xr::Path::NULL)?;
if res.is_active && res.current_state {
return Ok(true);
}
let res = self
.action_bool_double
.state(&state.session, xr::Path::NULL)?;
if res.is_active && self.check_double_click(res.current_state) {
return Ok(true);
}
let threshold = if before {
session.config.xr_click_sensitivity_release
} else {
session.config.xr_click_sensitivity
};
let res = self.action_f32.state(&state.session, xr::Path::NULL)?;
if res.is_active && res.current_state > threshold {
return Ok(true);
}
let res = self.action_f32.state(&state.session, xr::Path::NULL)?;
if res.is_active && self.check_double_click(res.current_state > threshold) {
return Ok(true);
}
Ok(false)
}
// submit a click. returns true if it should count as a double click
fn check_double_click(&mut self, state: bool) -> bool {
if !state {
self.held = false;
return false;
}
if self.held {
return false;
}
let now = Instant::now();
let double_click = match self.last_click {
Some(last_click) => now - last_click < DOUBLE_CLICK_TIME,
None => false,
};
self.last_click = if double_click { None } else { Some(now) };
self.held = true;
double_click
Ok(self.single.check(&state.session, threshold)?
|| self.double.check(&state.session, threshold)?
|| self.triple.check(&state.session, threshold)?)
}
}
@@ -136,6 +155,8 @@ pub(super) struct OpenXrHandSource {
action_alt_click: CustomClickAction,
action_show_hide: CustomClickAction,
action_space_drag: CustomClickAction,
action_space_rotate: CustomClickAction,
action_space_reset: CustomClickAction,
action_modifier_right: CustomClickAction,
action_modifier_middle: CustomClickAction,
action_move_mouse: CustomClickAction,
@@ -184,11 +205,93 @@ impl OpenXrInputSource {
pub fn update(&mut self, xr: &XrState, state: &mut AppState) -> anyhow::Result<()> {
xr.session.sync_actions(&[(&self.action_set).into()])?;
let loc = xr.view.locate(&xr.stage, xr.predicted_display_time)?;
let hmd = posef_to_transform(&loc.pose);
if loc
.location_flags
.contains(xr::SpaceLocationFlags::ORIENTATION_VALID)
{
state.input_state.hmd.matrix3 = hmd.matrix3;
}
if loc
.location_flags
.contains(xr::SpaceLocationFlags::POSITION_VALID)
{
state.input_state.hmd.translation = hmd.translation;
}
for i in 0..2 {
self.hands[i].update(&mut state.input_state.pointers[i], xr, &state.session)?;
}
Ok(())
}
fn update_device_battery_status(
device: &mut Device,
role: TrackedDeviceRole,
app: &mut AppState,
) {
if let Ok(status) = device.battery_status() {
if status.present {
app.input_state.devices.push(TrackedDevice {
soc: Some(status.charge),
charging: status.charging,
role,
});
log::debug!(
"Device {} role {:#?}: {:.0}% (charging {})",
device.index,
role,
status.charge * 100.0f32,
status.charging
);
}
}
}
pub fn update_devices(&mut self, app: &mut AppState, monado: &mut Monado) {
app.input_state.devices.clear();
let roles = [
("head", TrackedDeviceRole::Hmd),
("eyes", TrackedDeviceRole::None),
("left", TrackedDeviceRole::LeftHand),
("right", TrackedDeviceRole::RightHand),
("gamepad", TrackedDeviceRole::None),
("hand-tracking-left", TrackedDeviceRole::LeftHand),
("hand-tracking-right", TrackedDeviceRole::RightHand),
];
let mut seen = Vec::<u32>::with_capacity(32);
for (mnd_role, wlx_role) in roles {
let device = monado.device_from_role(mnd_role);
if let Ok(mut device) = device {
if !seen.contains(&device.index) {
seen.push(device.index);
Self::update_device_battery_status(&mut device, wlx_role, app);
}
}
}
if let Ok(devices) = monado.devices() {
for mut device in devices {
if !seen.contains(&device.index) {
let role = if device.id >= 4 && device.id <= 8 {
TrackedDeviceRole::Tracker
} else {
TrackedDeviceRole::None
};
Self::update_device_battery_status(&mut device, role, app);
}
}
}
app.input_state.devices.sort_by(|a, b| {
(a.soc.is_none() as u8)
.cmp(&(b.soc.is_none() as u8))
.then((a.role as u8).cmp(&(b.role as u8)))
.then(a.soc.unwrap_or(999.).total_cmp(&b.soc.unwrap_or(999.)))
});
}
}
impl OpenXrHand {
@@ -213,9 +316,21 @@ impl OpenXrHand {
.location_flags
.contains(xr::SpaceLocationFlags::ORIENTATION_VALID)
{
let quat = unsafe { std::mem::transmute::<_, Quat>(location.pose.orientation) };
let pos = unsafe { std::mem::transmute::<_, Vec3>(location.pose.position) };
pointer.pose = Affine3A::from_rotation_translation(quat, pos);
let (cur_quat, cur_pos) = (Quat::from_affine3(&pointer.pose), pointer.pose.translation);
let (new_quat, new_pos) = unsafe {
(
transmute::<Quaternionf, Quat>(location.pose.orientation),
transmute::<Vector3f, Vec3>(location.pose.position),
)
};
let lerp_factor =
(1.0 / (xr.fps / 100.0) * session.config.pointer_lerp_factor).clamp(0.1, 1.0);
pointer.raw_pose = Affine3A::from_rotation_translation(new_quat, new_pos);
pointer.pose = Affine3A::from_rotation_translation(
cur_quat.lerp(new_quat, lerp_factor),
cur_pos.lerp(new_pos.into(), lerp_factor).into(),
);
}
pointer.now.click = self
@@ -266,6 +381,16 @@ impl OpenXrHand {
.action_space_drag
.state(pointer.before.space_drag, xr, session)?;
pointer.now.space_rotate =
self.source
.action_space_rotate
.state(pointer.before.space_rotate, xr, session)?;
pointer.now.space_reset =
self.source
.action_space_reset
.state(pointer.before.space_reset, xr, session)?;
Ok(())
}
}
@@ -298,6 +423,8 @@ impl OpenXrHandSource {
action_alt_click: CustomClickAction::new(action_set, "alt_click", side)?,
action_show_hide: CustomClickAction::new(action_set, "show_hide", side)?,
action_space_drag: CustomClickAction::new(action_set, "space_drag", side)?,
action_space_rotate: CustomClickAction::new(action_set, "space_rotate", side)?,
action_space_reset: CustomClickAction::new(action_set, "space_reset", side)?,
action_modifier_right: CustomClickAction::new(
action_set,
"click_modifier_right",
@@ -329,7 +456,7 @@ fn to_path(maybe_path_str: &Option<String>, instance: &xr::Instance) -> Option<x
fn is_bool(maybe_type_str: &Option<String>) -> bool {
maybe_type_str
.as_ref()
.unwrap()
.unwrap() // want panic
.split('/')
.last()
.map(|last| matches!(last, "click" | "touch"))
@@ -341,31 +468,39 @@ macro_rules! add_custom {
if let Some(action) = $action.as_ref() {
if let Some(p) = to_path(&action.left, $instance) {
if is_bool(&action.left) {
if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.action_bool_double, p));
if action.triple_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.triple.action_bool, p));
} else if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.double.action_bool, p));
} else {
$bindings.push(xr::Binding::new(&$left.action_bool, p));
$bindings.push(xr::Binding::new(&$left.single.action_bool, p));
}
} else {
if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.action_f32_double, p));
if action.triple_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.triple.action_f32, p));
} else if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.double.action_f32, p));
} else {
$bindings.push(xr::Binding::new(&$left.action_f32, p));
$bindings.push(xr::Binding::new(&$left.single.action_f32, p));
}
}
}
if let Some(p) = to_path(&action.right, $instance) {
if is_bool(&action.right) {
if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.action_bool_double, p));
if action.triple_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.triple.action_bool, p));
} else if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.double.action_bool, p));
} else {
$bindings.push(xr::Binding::new(&$right.action_bool, p));
$bindings.push(xr::Binding::new(&$right.single.action_bool, p));
}
} else {
if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.action_f32_double, p));
if action.triple_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.triple.action_f32, p));
} else if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.double.action_f32, p));
} else {
$bindings.push(xr::Binding::new(&$right.action_f32, p));
$bindings.push(xr::Binding::new(&$right.single.action_f32, p));
}
}
}
@@ -451,6 +586,22 @@ fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) ->
instance
);
add_custom!(
profile.space_rotate,
&hands[0].action_space_rotate,
&hands[1].action_space_rotate,
bindings,
instance
);
add_custom!(
profile.space_reset,
&hands[0].action_space_reset,
&hands[1].action_space_reset,
bindings,
instance
);
add_custom!(
profile.click_modifier_right,
&hands[0].action_modifier_right,
@@ -493,6 +644,7 @@ struct OpenXrActionConfAction {
right: Option<String>,
threshold: Option<[f32; 2]>,
double_click: Option<bool>,
triple_click: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -504,6 +656,8 @@ struct OpenXrActionConfProfile {
alt_click: Option<OpenXrActionConfAction>,
show_hide: Option<OpenXrActionConfAction>,
space_drag: Option<OpenXrActionConfAction>,
space_rotate: Option<OpenXrActionConfAction>,
space_reset: Option<OpenXrActionConfAction>,
click_modifier_right: Option<OpenXrActionConfAction>,
click_modifier_middle: Option<OpenXrActionConfAction>,
move_mouse: Option<OpenXrActionConfAction>,
+4 -4
View File
@@ -7,7 +7,6 @@ use std::{
atomic::{AtomicUsize, Ordering},
Arc,
},
usize,
};
use vulkano::{command_buffer::CommandBufferUsage, format::Format, image::view::ImageView};
@@ -18,7 +17,7 @@ use crate::{
};
use super::{
swapchain::{create_swapchain_render_data, SwapchainRenderData},
swapchain::{create_swapchain_render_data, SwapchainOpts, SwapchainRenderData},
CompositionLayer, XrState,
};
@@ -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),
},
@@ -69,7 +68,8 @@ impl LinePool {
) -> anyhow::Result<usize> {
let id = 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 {
+101 -9
View File
@@ -1,14 +1,17 @@
use std::{
collections::VecDeque,
ops::Add,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
time::Duration,
time::{Duration, Instant},
};
use glam::{Affine3A, Vec3};
use libmonado_rs::Monado;
use openxr as xr;
use skybox::create_skybox;
use vulkano::{command_buffer::CommandBufferUsage, Handle, VulkanObject};
use crate::{
@@ -33,6 +36,7 @@ mod input;
mod lines;
mod overlay;
mod playspace;
mod skybox;
mod swapchain;
const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO;
@@ -44,11 +48,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) => {
@@ -66,6 +72,13 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
AppState::from_graphics(graphics)?
};
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())?;
@@ -74,10 +87,17 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
notifications.run_udp();
let mut delete_queue = vec![];
let mut playspace = playspace::PlayspaceMover::try_new()
.map_err(|e| log::warn!("Will not use Monado playspace mover: {}", e))
let mut monado = Monado::auto_connect()
.map_err(|e| log::warn!("Will not use libmonado: {}", e))
.ok();
let mut playspace = monado.as_mut().and_then(|m| {
playspace::PlayspaceMover::new(m)
.map_err(|e| log::warn!("Will not use Monado playspace mover: {}", e))
.ok()
});
#[cfg(feature = "osc")]
let mut osc_sender =
crate::backend::osc::OscSender::new(app_state.session.config.osc_out_port).ok();
@@ -105,15 +125,21 @@ 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 = create_skybox(&xr_state, &app_state);
let pointer_lines = [
lines.allocate(&xr_state, app_state.graphics.clone())?,
lines.allocate(&xr_state, app_state.graphics.clone())?,
@@ -126,8 +152,13 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let mut session_running = false;
let mut event_storage = xr::EventDataBuffer::new();
let mut next_device_update = Instant::now();
let mut due_tasks = VecDeque::with_capacity(4);
let mut fps_counter: VecDeque<Instant> = VecDeque::new();
let mut main_session_visible = false;
'main_loop: loop {
let cur_frame = FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
@@ -171,10 +202,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 {
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;
@@ -184,6 +235,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(
@@ -209,7 +278,11 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
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);
space_mover.update(
&mut overlays,
&app_state,
monado.as_mut().unwrap(), // safe
);
}
for o in overlays.iter_mut() {
@@ -227,8 +300,7 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
&xr_state.stage,
)?;
let (hmd, ipd) = helpers::hmd_pose_from_views(&views);
app_state.input_state.hmd = hmd;
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;
@@ -276,6 +348,18 @@ 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));
}
}
}
for o in overlays.iter_mut() {
if !o.state.want_visible {
continue;
@@ -319,6 +403,7 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
.map(|f| match f.1 {
CompositionLayer::Quad(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
CompositionLayer::Cylinder(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
CompositionLayer::Equirect2(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
CompositionLayer::None => unreachable!(),
})
.collect::<Vec<_>>();
@@ -376,14 +461,20 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
TaskType::System(task) => match task {
SystemTask::FixFloor => {
if let Some(ref mut playspace) = playspace {
playspace.fix_floor(&app_state.input_state);
playspace.fix_floor(
&app_state.input_state,
monado.as_mut().unwrap(), // safe
);
}
}
SystemTask::ResetPlayspace => {
if let Some(ref mut playspace) = playspace {
playspace.reset_offset();
playspace.reset_offset(monado.as_mut().unwrap()); // safe
}
}
SystemTask::ShowHide => {
overlays.show_hide(&mut app_state);
}
_ => {}
},
}
@@ -402,4 +493,5 @@ pub(super) enum CompositionLayer<'a> {
None,
Quad(xr::CompositionLayerQuad<'a, xr::Vulkan>),
Cylinder(xr::CompositionLayerCylinderKHR<'a, xr::Vulkan>),
Equirect2(xr::CompositionLayerEquirect2KHR<'a, xr::Vulkan>),
}
+9 -2
View File
@@ -15,6 +15,9 @@
// -- 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
//
@@ -90,7 +93,7 @@
left: "/user/hand/left/input/x/touch",
right: "/user/hand/right/input/a/touch"
},
mouse_move: {
move_mouse: {
// used with focus_follows_mouse_mode
left: "/user/hand/left/input/trigger/touch",
right: "/user/hand/right/input/trigger/touch"
@@ -132,6 +135,10 @@
left: "/user/hand/left/input/trackpad/force",
// right trackpad is alt_click
},
space_reset: {
double_click: true,
left: "/user/hand/left/input/trackpad/force",
},
click_modifier_right: {
left: "/user/hand/left/input/b/touch",
right: "/user/hand/right/input/b/touch"
@@ -140,7 +147,7 @@
left: "/user/hand/left/input/a/touch",
right: "/user/hand/right/input/a/touch"
},
mouse_move: {
move_mouse: {
// used with focus_follows_mouse_mode
left: "/user/hand/left/input/trigger/touch",
right: "/user/hand/right/input/trigger/touch"
+15 -5
View File
@@ -1,11 +1,14 @@
use glam::Vec3A;
use openxr as xr;
use openxr::{self as xr, CompositionLayerFlags};
use std::{f32::consts::PI, sync::Arc};
use xr::EyeVisibility;
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,
};
@@ -35,13 +38,18 @@ impl OverlayData<OpenXrOverlayData> {
log::warn!("{}: Will not show - image not ready", self.state.name);
return Ok(CompositionLayer::None);
};
let extent = my_view.image().extent();
let extent = self.extent().unwrap(); // want panic
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,
@@ -75,6 +83,7 @@ impl OverlayData<OpenXrOverlayData> {
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)
@@ -86,6 +95,7 @@ impl OverlayData<OpenXrOverlayData> {
} else {
let posef = helpers::transform_to_posef(&self.state.transform);
let quad = xr::CompositionLayerQuad::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
+112 -86
View File
@@ -1,16 +1,15 @@
use std::ffi::c_void;
use glam::{Affine3A, Quat, Vec3A};
use libloading::{Library, Symbol};
use libmonado_rs::{Monado, Pose, ReferenceSpaceType};
use crate::{
backend::{common::OverlayContainer, input::InputState},
state::AppState,
};
use super::{helpers, overlay::OpenXrOverlayData};
use super::overlay::OpenXrOverlayData;
#[repr(C)]
#[derive(Default, Debug)]
struct XrtPose {
orientation: [f32; 4],
position: [f32; 3],
@@ -22,68 +21,99 @@ struct MoverData<T> {
hand_pose: T,
}
// Legacy implementation
type PlaySpaceMove = extern "C" fn(*mut c_void, f32, f32, f32) -> i32;
// New implementation
type ApplyStageOffset = extern "C" fn(*mut c_void, *const XrtPose) -> i32;
enum ApiImpl {
None,
PlaySpaceMove(PlaySpaceMove),
ApplyStageOffset(ApplyStageOffset),
}
pub(super) struct PlayspaceMover {
last_transform: Affine3A,
drag: Option<MoverData<Vec3A>>,
libmonado: Library,
mnd_root: *mut c_void,
api_impl: ApiImpl,
rotate: Option<MoverData<Quat>>,
}
impl PlayspaceMover {
pub fn try_new() -> anyhow::Result<Self> {
unsafe {
let libmonado = helpers::find_libmonado()?;
pub fn new(monado: &mut Monado) -> anyhow::Result<Self> {
log::info!("Monado: using space offset API");
let root_create: Symbol<extern "C" fn(*mut *mut c_void) -> i32> =
libmonado.get(b"mnd_root_create\0")?;
let Ok(stage) = monado.get_reference_space_offset(ReferenceSpaceType::Stage) else {
anyhow::bail!("Space offsets not supported.");
};
let mut api_impl = ApiImpl::None;
if let Ok(playspace_move) = libmonado.get(b"mnd_root_playspace_move\0") {
log::info!("Monado: using playspace_move");
api_impl = ApiImpl::PlaySpaceMove(*playspace_move);
} else if let Ok(apply_stage_offset) = libmonado.get(b"mnd_root_apply_stage_offset\0") {
log::info!("Monado: using apply_stage_offset");
api_impl = ApiImpl::ApplyStageOffset(*apply_stage_offset);
}
log::debug!("STAGE is at {:?}, {:?}", stage.position, stage.orientation);
if let ApiImpl::None = api_impl {
anyhow::bail!("Monado does not support playspace mover.");
}
// initial offset
let last_transform =
Affine3A::from_rotation_translation(stage.orientation.into(), stage.position.into());
let mut root: *mut c_void = std::ptr::null_mut();
Ok(Self {
last_transform,
let ret = root_create(&mut root);
if ret != 0 {
anyhow::bail!("Failed to create root, code: {}", ret);
}
Ok(Self {
last_transform: Affine3A::IDENTITY,
drag: None,
libmonado,
mnd_root: root,
api_impl,
})
}
drag: None,
rotate: None,
})
}
pub fn update(&mut self, overlays: &mut OverlayContainer<OpenXrOverlayData>, state: &AppState) {
pub fn update(
&mut self,
overlays: &mut OverlayContainer<OpenXrOverlayData>,
state: &AppState,
monado: &mut Monado,
) {
for pointer in state.input_state.pointers.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 {
@@ -94,8 +124,9 @@ impl PlayspaceMover {
let new_hand = data
.pose
.transform_point3a(state.input_state.pointers[data.hand].pose.translation);
let relative_pos = new_hand - data.hand_pose;
.transform_point3a(state.input_state.pointers[data.hand].raw_pose.translation);
let relative_pos =
(new_hand - data.hand_pose) * state.session.config.space_drag_multiplier;
if relative_pos.length_squared() > 1000.0 {
log::warn!("Space drag too fast, ignoring");
@@ -114,14 +145,14 @@ impl PlayspaceMover {
data.pose.translation += relative_pos;
data.hand_pose = new_hand;
self.apply_offset(data.pose);
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.pose.translation);
.transform_point3a(pointer.raw_pose.translation);
self.drag = Some(MoverData {
pose: self.last_transform,
hand: i,
@@ -134,47 +165,42 @@ impl PlayspaceMover {
}
}
pub fn reset_offset(&mut self) {
pub fn reset_offset(&mut self, monado: &mut Monado) {
if self.drag.is_some() {
log::info!("Cannot reset offset while dragging.");
return;
log::info!("Space drag interrupted by manual reset");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by manual reset");
self.rotate = None;
}
self.last_transform = Affine3A::IDENTITY;
self.apply_offset(self.last_transform);
self.apply_offset(self.last_transform, monado);
}
pub fn fix_floor(&mut self, input: &InputState) {
pub fn fix_floor(&mut self, input: &InputState, monado: &mut Monado) {
if self.drag.is_some() {
log::info!("Cannot fix floor while dragging.");
return;
log::info!("Space drag interrupted by fix floor");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by fix floor");
self.rotate = None;
}
let y1 = input.pointers[0].pose.translation.y;
let y2 = input.pointers[1].pose.translation.y;
let y1 = input.pointers[0].raw_pose.translation.y;
let y2 = input.pointers[1].raw_pose.translation.y;
let delta = y1.min(y2) - 0.03;
self.last_transform.translation.y += delta;
self.apply_offset(self.last_transform);
self.apply_offset(self.last_transform, monado);
}
fn apply_offset(&self, transform: Affine3A) {
match self.api_impl {
ApiImpl::PlaySpaceMove(playspace_move) => {
(playspace_move)(
self.mnd_root,
transform.translation.x,
transform.translation.y,
transform.translation.z,
);
}
ApiImpl::ApplyStageOffset(apply_stage_offset) => {
let xrt_pose = XrtPose {
orientation: Quat::from_affine3(&transform).into(),
position: transform.translation.into(),
};
(apply_stage_offset)(self.mnd_root, &xrt_pose);
}
ApiImpl::None => {}
}
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::CONFIG_ROOT_PATH,
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_ROOT_PATH.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>> {
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))
+17
View File
@@ -224,6 +224,9 @@ where
pub fn view(&mut self) -> Option<Arc<ImageView>> {
self.backend.view()
}
pub fn extent(&mut self) -> Option<[u32; 3]> {
self.backend.extent()
}
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;
@@ -239,11 +242,19 @@ where
}
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 extent(&mut self) -> Option<[u32; 3]>;
}
pub struct FallbackRenderer;
@@ -264,6 +275,9 @@ impl OverlayRenderer for FallbackRenderer {
fn view(&mut self) -> Option<Arc<ImageView>> {
None
}
fn extent(&mut self) -> Option<[u32; 3]> {
None
}
}
// Boilerplate and dummies
@@ -318,6 +332,9 @@ impl OverlayRenderer for SplitOverlayBackend {
fn view(&mut self) -> Option<Arc<ImageView>> {
self.renderer.view()
}
fn extent(&mut self) -> Option<[u32; 3]> {
self.renderer.extent()
}
}
impl InteractionHandler for SplitOverlayBackend {
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
+1
View File
@@ -46,6 +46,7 @@ pub enum SystemTask {
ColorGain(ColorChannel, f32),
ResetPlayspace,
FixFloor,
ShowHide,
}
pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send;
+6 -5
View File
@@ -2,7 +2,7 @@ use std::sync::Arc;
use vulkano::{
command_buffer::CommandBufferUsage,
image::{sampler::Filter, view::ImageView, ImageUsage},
image::{view::ImageView, ImageUsage},
swapchain::{
acquire_next_image, Surface, Swapchain, SwapchainCreateInfo, SwapchainPresentInfo,
},
@@ -21,8 +21,8 @@ use crate::{
config_io,
graphics::{DynamicPass, DynamicPipeline, WlxGraphics, BLEND_ALPHA},
gui::{
canvas::Canvas,
modular::{modular_canvas, ModularData},
Canvas,
},
hid::USE_UINPUT,
state::{AppState, ScreenMeta},
@@ -87,7 +87,7 @@ impl PreviewState {
)
}?;
let set0 = pipeline
.uniform_sampler(0, view.clone(), Filter::Linear)
.uniform_sampler(0, view.clone(), pipeline.graphics.texture_filtering)
.unwrap();
let pass = pipeline
@@ -237,7 +237,7 @@ fn create_swapchain(
.device
.physical_device()
.surface_capabilities(&surface, Default::default())
.unwrap();
.unwrap(); // want panic
let (swapchain, images) = Swapchain::new(
graphics.device.clone(),
@@ -251,13 +251,14 @@ fn create_swapchain(
.supported_composite_alpha
.into_iter()
.next()
.unwrap(),
.unwrap(), // want panic
..Default::default()
},
)?;
let image_views = images
.into_iter()
// want panic
.map(|image| ImageView::new_default(image).unwrap())
.collect::<Vec<_>>();
+35
View File
@@ -121,6 +121,10 @@ pub fn def_point7() -> f32 {
0.7
}
pub fn def_point3() -> f32 {
0.3
}
fn def_osc_port() -> u16 {
9000
}
@@ -141,6 +145,10 @@ fn def_auto() -> Arc<str> {
"auto".into()
}
fn def_empty() -> Arc<str> {
"".into()
}
fn def_toast_topics() -> IdMap<ToastTopic, DisplayMethod> {
IdMap::new()
}
@@ -149,6 +157,10 @@ 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")]
@@ -240,6 +252,27 @@ pub struct GeneralConfig {
#[serde(default = "def_font")]
pub primary_font: Arc<str>,
#[serde(default = "def_one")]
pub space_drag_multiplier: f32,
#[serde(default = "def_empty")]
pub skybox_texture: Arc<str>,
#[serde(default = "def_true")]
pub use_skybox: bool,
#[serde(default = "def_max_height")]
pub screen_max_height: u16,
#[serde(default = "def_false")]
pub screen_render_down: bool,
#[serde(default = "def_point3")]
pub pointer_lerp_factor: f32,
#[serde(default = "def_false")]
pub space_rotate_unlocked: bool,
}
impl GeneralConfig {
@@ -374,6 +407,7 @@ pub struct AutoSettings {
pub notifications_sound_enabled: bool,
pub realign_on_showhide: bool,
pub allow_sliding: bool,
pub space_drag_multiplier: f32,
}
fn get_settings_path() -> PathBuf {
@@ -392,6 +426,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
notifications_sound_enabled: config.notifications_sound_enabled,
realign_on_showhide: config.realign_on_showhide,
allow_sliding: config.allow_sliding,
space_drag_multiplier: config.space_drag_multiplier,
};
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
+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),
}
}
+99 -41
View File
@@ -1,3 +1,5 @@
pub(crate) mod dds;
use std::{
collections::HashMap,
os::fd::{FromRawFd, IntoRawFd},
@@ -124,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>,
@@ -135,17 +138,13 @@ pub struct WlxGraphics {
pub shared_shaders: RwLock<HashMap<&'static str, Arc<ShaderModule>>>,
}
fn get_device_extensions() -> DeviceExtensions {
#[cfg(not(feature = "no-dmabuf"))]
return DeviceExtensions {
fn get_dmabuf_extensions() -> DeviceExtensions {
DeviceExtensions {
khr_external_memory: true,
khr_external_memory_fd: true,
ext_external_memory_dma_buf: true,
ext_image_drm_format_modifier: true,
..DeviceExtensions::empty()
};
#[cfg(feature = "no-dmabuf")]
return DeviceExtensions::empty();
}
}
static VULKAN_LIBRARY: OnceLock<Arc<vulkano::VulkanLibrary>> = OnceLock::new();
@@ -258,12 +257,21 @@ impl WlxGraphics {
.position(|(_, q)| q.queue_flags.intersects(QueueFlags::GRAPHICS))
.expect("Vulkan device has no graphics queue") as u32;
let mut device_extensions = get_device_extensions();
if !physical_device
let mut device_extensions = DeviceExtensions::empty();
let dmabuf_extensions = get_dmabuf_extensions();
if physical_device
.supported_extensions()
.ext_image_drm_format_modifier
.contains(&dmabuf_extensions)
{
device_extensions.ext_image_drm_format_modifier = false;
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
@@ -295,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(
@@ -330,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()
@@ -361,6 +383,7 @@ impl WlxGraphics {
device,
queue,
native_format: Format::R8G8B8A8_UNORM,
texture_filtering,
memory_allocator,
command_buffer_allocator,
descriptor_set_allocator,
@@ -380,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);
@@ -396,30 +420,16 @@ impl WlxGraphics {
},
)?;
let device_extensions = get_device_extensions();
log::debug!("Device exts for app: {:?}", &device_extensions);
let dmabuf_extensions = get_dmabuf_extensions();
let (physical_device, my_extensions, queue_family_index) = instance
.enumerate_physical_devices()?
.filter_map(|p| {
let runtime_extensions = vk_device_extensions_fn(&p);
log::debug!(
"Device exts for {}: {:?}",
p.properties().device_name,
&runtime_extensions
);
let mut my_extensions = runtime_extensions.union(&device_extensions);
if p.supported_extensions().contains(&my_extensions) {
Some((p, my_extensions))
} else {
// try without DRM format modifiers
my_extensions.ext_image_drm_format_modifier = false;
if p.supported_extensions().contains(&my_extensions) {
return Some((p, my_extensions));
}
let mut my_extensions = vk_device_extensions_fn(&p);
if !p.supported_extensions().contains(&my_extensions) {
log::debug!(
"Not using {} because it does not implement the following device extensions:",
"Not using {} due to missing extensions:",
p.properties().device_name,
);
for (ext, missing) in p.supported_extensions().difference(&my_extensions) {
@@ -427,8 +437,25 @@ impl WlxGraphics {
log::debug!(" {}", ext);
}
}
None
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()
@@ -452,6 +479,12 @@ impl WlxGraphics {
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 {
@@ -468,6 +501,15 @@ impl WlxGraphics {
},
)?;
log::debug!(
" DMA-buf supported: {}",
device.enabled_extensions().ext_external_memory_dma_buf
);
log::debug!(
" DRM format modifiers supported: {}",
device.enabled_extensions().ext_image_drm_format_modifier
);
let queue = queues
.next()
.ok_or_else(|| anyhow::anyhow!("no GPU queues available"))?;
@@ -493,6 +535,7 @@ impl WlxGraphics {
queue,
memory_allocator,
native_format: Format::R8G8B8A8_UNORM,
texture_filtering,
command_buffer_allocator,
descriptor_set_allocator,
quad_indices,
@@ -511,9 +554,9 @@ impl WlxGraphics {
Arc<vulkano::swapchain::Surface>,
)> {
use vulkano::swapchain::Surface;
use winit::{event_loop::EventLoop, window::WindowBuilder};
use winit::{event_loop::EventLoop, window::Window};
let event_loop = EventLoop::new().unwrap();
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);
@@ -527,10 +570,14 @@ impl WlxGraphics {
},
)?;
let window = Arc::new(WindowBuilder::new().build(&event_loop).unwrap());
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 = get_device_extensions();
let mut device_extensions = DeviceExtensions::empty();
device_extensions.khr_swapchain = true;
log::debug!("Device exts for app: {:?}", &device_extensions);
@@ -595,7 +642,7 @@ impl WlxGraphics {
let native_format = device
.physical_device()
.surface_formats(&surface, Default::default())
.unwrap()[0]
.unwrap()[0] // want panic
.0;
log::info!("Using surface format: {:?}", native_format);
@@ -624,6 +671,7 @@ impl WlxGraphics {
queue,
memory_allocator,
native_format,
texture_filtering: Filter::Linear,
command_buffer_allocator,
descriptor_set_allocator,
quad_indices,
@@ -1042,7 +1090,7 @@ impl WlxCommandBuffer {
Ok(())
}
pub fn texture2d(
pub fn texture2d_raw(
&mut self,
width: u32,
height: u32,
@@ -1122,7 +1170,6 @@ pub struct WlxPipelineDynamic {}
pub struct WlxPipelineLegacy {
pub render_pass: Arc<RenderPass>,
pub framebuffer: Arc<Framebuffer>,
pub view: Arc<ImageView>,
}
pub struct WlxPipeline<D> {
@@ -1356,7 +1403,6 @@ impl WlxPipeline<WlxPipelineLegacy> {
data: WlxPipelineLegacy {
render_pass,
framebuffer,
view: render_target,
},
})
}
@@ -1616,3 +1662,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
)
}
+265
View File
@@ -0,0 +1,265 @@
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.
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]
}
}
+386
View File
@@ -0,0 +1,386 @@
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(())
}
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(())
}
}
+386
View File
@@ -0,0 +1,386 @@
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::{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 extent(&mut self) -> Option<[u32; 3]> {
Some(self.view_final.image().extent())
}
}
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>) {}
}
+1 -1
View File
@@ -222,7 +222,7 @@ impl FontCache {
};
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 {
+6 -792
View File
@@ -1,37 +1,12 @@
use std::sync::Arc;
use anyhow::bail;
use glam::{Vec2, Vec4};
use vulkano::{
command_buffer::CommandBufferUsage,
format::Format,
image::{sampler::Filter, view::ImageView, ImageLayout},
};
use crate::{
backend::{
input::{Haptics, InteractionHandler, PointerHit, PointerMode},
overlay::{OverlayBackend, OverlayRenderer},
},
graphics::{
WlxCommandBuffer, WlxGraphics, WlxPass, WlxPipeline, WlxPipelineLegacy, BLEND_ALPHA,
},
state::AppState,
};
use self::modular::GuiColor;
use glam::Vec4;
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<GuiColor> {
@@ -63,773 +38,12 @@ pub fn color_parse(html_hex: &str) -> anyhow::Result<GuiColor> {
));
}
}
bail!(
anyhow::bail!(
"Invalid color string: '{}', must be 7 characters long (e.g. #FF00FF)",
&html_hex
)
}
pub struct CanvasBuilder<D, S> {
canvas: Canvas<D, S>,
pub fg_color: GuiColor,
pub bg_color: GuiColor,
pub font_size: isize,
}
impl<D, S> CanvasBuilder<D, S> {
pub fn new(
width: usize,
height: usize,
graphics: Arc<WlxGraphics>,
format: Format,
data: D,
) -> anyhow::Result<Self> {
Ok(Self {
canvas: Canvas::new(width, height, graphics, format, data)?,
bg_color: Vec4::ZERO,
fg_color: Vec4::ONE,
font_size: 16,
})
}
pub fn build(self) -> Canvas<D, S> {
self.canvas
}
// Creates a panel with bg_color inherited from the canvas
pub fn panel(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
bg_color: self.bg_color,
on_render_bg: Some(Control::render_rect),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a label with fg_color, font_size inherited from the canvas
pub fn label(&mut self, x: f32, y: f32, w: f32, h: f32, text: Arc<str>) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
text,
fg_color: self.fg_color,
size: self.font_size,
on_render_fg: Some(Control::render_text),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a label with fg_color, font_size inherited from the canvas
#[allow(dead_code)]
pub fn label_centered(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
text: Arc<str>,
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
text,
fg_color: self.fg_color,
size: self.font_size,
on_render_fg: Some(Control::render_text_centered),
..Control::new()
});
&mut self.canvas.controls[idx]
}
// Creates a button with fg_color, bg_color, font_size inherited from the canvas
pub fn button(&mut self, x: f32, y: f32, w: f32, h: f32, text: Arc<str>) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.interactive_set_idx(x, y, w, h, idx);
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
text,
fg_color: self.fg_color,
bg_color: self.bg_color,
size: self.font_size,
on_render_bg: Some(Control::render_rect),
on_render_fg: Some(Control::render_text_centered),
on_render_hl: Some(Control::render_highlight),
..Control::new()
});
&mut self.canvas.controls[idx]
}
pub fn key_button(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
cap_type: KeyCapType,
label: &[String],
) -> &mut Control<D, S> {
let idx = self.canvas.controls.len();
self.canvas.interactive_set_idx(x, y, w, h, idx);
self.canvas.controls.push(Control {
rect: Rect { x, y, w, h },
bg_color: self.bg_color,
on_render_bg: Some(Control::render_rect),
on_render_hl: Some(Control::render_highlight),
..Control::new()
});
let renders = match cap_type {
KeyCapType::Regular => {
let render: ControlRenderer<D, S> = Control::render_text_centered;
let rect = Rect {
x,
y,
w,
h: h - self.font_size as f32,
};
vec![(render, rect, 1f32)]
}
KeyCapType::RegularAltGr => {
let render: ControlRenderer<D, S> = Control::render_text;
let rect0 = Rect {
x: x + 12.,
y: y + (self.font_size as f32) + 12.,
w,
h,
};
let rect1 = Rect {
x: x + w * 0.5 + 12.,
y: y + h - (self.font_size as f32) + 8.,
w,
h,
};
vec![(render, rect0, 1.0), (render, rect1, 0.8)]
}
KeyCapType::Reversed => {
let render: ControlRenderer<D, S> = Control::render_text_centered;
let rect0 = Rect {
x,
y: y + 2.0,
w,
h: h * 0.5,
};
let rect1 = Rect {
x,
y: y + h * 0.5 + 2.0,
w,
h: h * 0.5,
};
vec![(render, rect1, 1.0), (render, rect0, 0.8)]
}
KeyCapType::ReversedAltGr => {
let render: ControlRenderer<D, S> = Control::render_text;
let rect0 = Rect {
x: x + 12.,
y: y + (self.font_size as f32) + 8.,
w,
h,
};
let rect1 = Rect {
x: x + 12.,
y: y + h - (self.font_size as f32) + 4.,
w,
h,
};
let rect2 = Rect {
x: x + w * 0.5 + 8.,
y: y + h - (self.font_size as f32) + 4.,
w,
h,
};
vec![
(render, rect1, 1.0),
(render, rect0, 0.8),
(render, rect2, 0.8),
]
}
};
for (idx, (render, rect, alpha)) in renders.into_iter().enumerate() {
if idx >= label.len() {
break;
}
self.canvas.controls.push(Control {
rect,
text: Arc::from(label[idx].as_str()),
fg_color: self.fg_color * alpha,
size: self.font_size,
on_render_fg: Some(render),
..Control::new()
});
}
&mut self.canvas.controls[idx]
}
}
pub struct CanvasData<D> {
pub data: D,
pub width: usize,
pub height: usize,
graphics: Arc<WlxGraphics>,
pipeline_bg_color: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline_fg_glyph: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline_final: Arc<WlxPipeline<WlxPipelineLegacy>>,
}
pub struct Canvas<D, S> {
controls: Vec<Control<D, S>>,
canvas: CanvasData<D>,
hover_controls: [Option<usize>; 2],
pressed_controls: [Option<usize>; 2],
interact_map: Vec<Option<u16>>,
interact_stride: usize,
interact_rows: usize,
view_final: Arc<ImageView>,
pass_fg: WlxPass<WlxPipelineLegacy>,
pass_bg: WlxPass<WlxPipelineLegacy>,
}
impl<D, S> Canvas<D, S> {
fn new(
width: usize,
height: usize,
graphics: Arc<WlxGraphics>,
format: Format,
data: D,
) -> anyhow::Result<Self> {
let tex_fg = graphics.render_texture(width as _, height as _, format)?;
let tex_bg = graphics.render_texture(width as _, height as _, format)?;
let tex_final = graphics.render_texture(width as _, height as _, format)?;
let view_fg = ImageView::new_default(tex_fg.clone())?;
let view_bg = ImageView::new_default(tex_bg.clone())?;
let view_final = ImageView::new_default(tex_final.clone())?;
let Ok(shaders) = graphics.shared_shaders.read() else {
bail!("Failed to lock shared shaders for reading");
};
let pipeline_bg_color = graphics.create_pipeline(
view_bg.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_color").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let pipeline_fg_glyph = graphics.create_pipeline(
view_fg.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_glyph").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
)?;
let vertex_buffer =
graphics.upload_verts(width as _, height as _, 0., 0., width as _, height as _)?;
let pipeline_final = graphics.create_pipeline_with_layouts(
view_final.clone(),
shaders.get("vert_common").unwrap().clone(), // want panic
shaders.get("frag_sprite").unwrap().clone(), // want panic
format,
Some(BLEND_ALPHA),
ImageLayout::TransferSrcOptimal,
ImageLayout::TransferSrcOptimal,
)?;
let set_fg = pipeline_final.uniform_sampler(0, view_fg.clone(), Filter::Linear)?;
let set_bg = pipeline_final.uniform_sampler(0, view_bg.clone(), Filter::Linear)?;
let pass_fg = pipeline_final.create_pass(
[width as _, height as _],
vertex_buffer.clone(),
graphics.quad_indices.clone(),
vec![set_fg],
)?;
let pass_bg = pipeline_final.create_pass(
[width as _, height as _],
vertex_buffer.clone(),
graphics.quad_indices.clone(),
vec![set_bg],
)?;
let stride = width / RES_DIVIDER;
let rows = height / RES_DIVIDER;
Ok(Self {
canvas: CanvasData {
data,
width,
height,
graphics: graphics.clone(),
pipeline_bg_color,
pipeline_fg_glyph,
pipeline_final,
},
controls: Vec::new(),
hover_controls: [None, None],
pressed_controls: [None, None],
interact_map: vec![None; stride * rows],
interact_stride: stride,
interact_rows: rows,
view_final,
pass_fg,
pass_bg,
})
}
fn interactive_set_idx(&mut self, x: f32, y: f32, w: f32, h: f32, idx: usize) {
let (x, y, w, h) = (x as usize, y as usize, w as usize, h as usize);
let x_min = (x / RES_DIVIDER).max(0);
let y_min = (y / RES_DIVIDER).max(0);
let x_max = (x_min + (w / RES_DIVIDER)).min(self.interact_stride - 1);
let y_max = (y_min + (h / RES_DIVIDER)).min(self.interact_rows - 1);
for y in y_min..y_max {
for x in x_min..x_max {
self.interact_map[y * self.interact_stride + x] = Some(idx as u16);
}
}
}
fn interactive_get_idx(&self, uv: Vec2) -> Option<usize> {
let x = (uv.x * self.canvas.width as f32) as usize;
let y = (uv.y * self.canvas.height as f32) as usize;
let x = (x / RES_DIVIDER).max(0).min(self.interact_stride - 1);
let y = (y / RES_DIVIDER).max(0).min(self.interact_rows - 1);
self.interact_map[y * self.interact_stride + x].map(|x| x as usize)
}
fn render_bg(&mut self, app: &mut AppState) -> anyhow::Result<()> {
let mut cmd_buffer = self
.canvas
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_render_pass(&self.canvas.pipeline_bg_color)?;
for c in self.controls.iter_mut() {
if let Some(fun) = c.on_render_bg {
fun(c, &self.canvas, app, &mut cmd_buffer)?;
}
}
cmd_buffer.end_render_pass()?;
cmd_buffer.build_and_execute_now()
}
fn render_fg(&mut self, app: &mut AppState) -> anyhow::Result<()> {
let mut cmd_buffer = self
.canvas
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_render_pass(&self.canvas.pipeline_fg_glyph)?;
for c in self.controls.iter_mut() {
if let Some(fun) = c.on_render_fg {
fun(c, &self.canvas, app, &mut cmd_buffer)?;
}
}
cmd_buffer.end_render_pass()?;
cmd_buffer.build_and_execute_now()
}
}
impl<D, S> InteractionHandler for Canvas<D, S> {
fn on_left(&mut self, _app: &mut AppState, pointer: usize) {
self.hover_controls[pointer] = None;
}
fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
let old = self.hover_controls[hit.pointer];
if let Some(i) = self.interactive_get_idx(hit.uv) {
self.hover_controls[hit.pointer] = Some(i);
} else {
self.hover_controls[hit.pointer] = None;
}
if old != self.hover_controls[hit.pointer] {
Some(Haptics {
intensity: 0.1,
duration: 0.01,
frequency: 5.0,
})
} else {
None
}
}
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
let idx = if pressed {
self.interactive_get_idx(hit.uv)
} else {
self.pressed_controls[hit.pointer]
};
if let Some(idx) = idx {
let c = &mut self.controls[idx];
if pressed {
if let Some(ref mut f) = c.on_press {
self.pressed_controls[hit.pointer] = Some(idx);
f(c, &mut self.canvas.data, app, hit.mode);
}
} else if let Some(ref mut f) = c.on_release {
self.pressed_controls[hit.pointer] = None;
f(c, &mut self.canvas.data, app);
}
}
}
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta: f32) {
let idx = self.hover_controls[hit.pointer];
if let Some(idx) = idx {
let c = &mut self.controls[idx];
if let Some(ref mut f) = c.on_scroll {
f(c, &mut self.canvas.data, app, delta);
}
}
}
}
impl<D, S> OverlayRenderer for Canvas<D, S> {
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.render_bg(app)?;
self.render_fg(app)
}
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
let mut dirty = false;
for c in self.controls.iter_mut() {
if let Some(fun) = c.on_update {
fun(c, &mut self.canvas.data, app);
}
if c.dirty {
dirty = true;
c.dirty = false;
}
}
if dirty {
self.render_bg(app)?;
self.render_fg(app)?;
}
/*
let image = self.view_final.image().clone();
if self.first_render {
self.first_render = false;
} else {
self.canvas
.graphics
.transition_layout(
image.clone(),
ImageLayout::TransferSrcOptimal,
ImageLayout::ColorAttachmentOptimal,
)
.wait(None)
.unwrap();
}
*/
let mut cmd_buffer = self
.canvas
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_render_pass(&self.canvas.pipeline_final)?;
// static background
cmd_buffer.run_ref(&self.pass_bg)?;
for (i, c) in self.controls.iter_mut().enumerate() {
if let Some(render) = c.on_render_hl {
if let Some(test) = c.test_highlight {
if let Some(hl_color) = test(c, &mut self.canvas.data, app) {
render(c, &self.canvas, app, &mut cmd_buffer, hl_color)?;
}
}
if self.hover_controls.contains(&Some(i)) {
render(
c,
&self.canvas,
app,
&mut cmd_buffer,
Vec4::new(1., 1., 1., 0.3),
)?;
}
}
}
// mostly static text
cmd_buffer.run_ref(&self.pass_fg)?;
cmd_buffer.end_render_pass()?;
cmd_buffer.build_and_execute_now()
/*
self.canvas
.graphics
.transition_layout(
image,
ImageLayout::ColorAttachmentOptimal,
ImageLayout::TransferSrcOptimal,
)
.wait(None)
.unwrap();
*/
}
fn view(&mut self) -> Option<Arc<ImageView>> {
Some(self.view_final.clone())
}
}
impl<D, S> OverlayBackend for Canvas<D, S> {
fn set_renderer(&mut self, _renderer: Box<dyn OverlayRenderer>) {}
fn set_interaction(&mut self, _interaction: Box<dyn InteractionHandler>) {}
}
pub type ControlRenderer<D, S> =
fn(&Control<D, S>, &CanvasData<D>, &mut AppState, &mut WlxCommandBuffer) -> anyhow::Result<()>;
pub type ControlRendererHl<D, S> = fn(
&Control<D, S>,
&CanvasData<D>,
&mut AppState,
&mut WlxCommandBuffer,
Vec4,
) -> anyhow::Result<()>;
pub struct Control<D, S> {
pub state: Option<S>,
rect: Rect,
fg_color: GuiColor,
bg_color: GuiColor,
text: Arc<str>,
size: isize,
dirty: bool,
pub on_update: Option<fn(&mut Self, &mut D, &mut AppState)>,
pub on_press: Option<fn(&mut Self, &mut D, &mut AppState, PointerMode)>,
pub on_release: Option<fn(&mut Self, &mut D, &mut AppState)>,
pub on_scroll: Option<fn(&mut Self, &mut D, &mut AppState, f32)>,
pub test_highlight: Option<fn(&Self, &mut D, &mut AppState) -> Option<Vec4>>,
on_render_bg: Option<ControlRenderer<D, S>>,
on_render_hl: Option<ControlRendererHl<D, S>>,
on_render_fg: Option<ControlRenderer<D, S>>,
}
impl<D, S> Control<D, S> {
fn new() -> Self {
Self {
rect: Rect {
x: 0.,
y: 0.,
w: 0.,
h: 0.,
},
fg_color: Vec4::ONE,
bg_color: Vec4::ZERO,
text: Arc::from(""),
dirty: true,
size: 24,
state: None,
on_update: None,
on_render_bg: None,
on_render_hl: None,
on_render_fg: None,
test_highlight: None,
on_press: None,
on_release: None,
on_scroll: None,
}
}
#[inline(always)]
pub fn set_text(&mut self, text: &str) {
if *self.text == *text {
return;
}
self.text = text.into();
self.dirty = true;
}
#[inline(always)]
pub fn set_fg_color(&mut self, color: GuiColor) {
if self.fg_color == color {
return;
}
self.fg_color = color;
self.dirty = true;
}
fn render_rect(
&self,
canvas: &CanvasData<D>,
_: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let pass = {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let set0 = canvas
.pipeline_bg_color
.uniform_buffer(0, self.bg_color.to_array().to_vec())?;
canvas.pipeline_bg_color.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0],
)?
};
cmd_buffer.run_ref(&pass)
}
fn render_highlight(
&self,
canvas: &CanvasData<D>,
_: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
color: GuiColor,
) -> anyhow::Result<()> {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
self.rect.x,
self.rect.y,
self.rect.w,
self.rect.h,
)?;
let set0 = canvas
.pipeline_bg_color
.uniform_buffer(0, color.to_array().to_vec())?;
let pass = canvas.pipeline_bg_color.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer.clone(),
canvas.graphics.quad_indices.clone(),
vec![set0],
)?;
cmd_buffer.run_ref(&pass)
}
fn render_text(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let mut cur_y = self.rect.y;
for line in self.text.lines() {
let mut cur_x = self.rect.x;
for glyph in app
.fc
.get_glyphs(line, self.size, canvas.graphics.clone())?
{
if let Some(tex) = glyph.tex.clone() {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
cur_x + glyph.left,
cur_y - glyph.top,
glyph.width,
glyph.height,
)?;
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
0,
ImageView::new_default(tex)?,
Filter::Linear,
)?;
let set1 = canvas
.pipeline_fg_glyph
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
let pass = canvas.pipeline_fg_glyph.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
}
cur_x += glyph.advance;
}
cur_y += (self.size as f32) * 1.5;
}
Ok(())
}
fn render_text_centered(
&self,
canvas: &CanvasData<D>,
app: &mut AppState,
cmd_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<()> {
let (w, h) = app
.fc
.get_text_size(&self.text, self.size, canvas.graphics.clone())?;
let mut cur_y = self.rect.y + (self.rect.h) - (h * 0.5) - (self.size as f32 * 0.25);
for line in self.text.lines() {
let mut cur_x = self.rect.x + (self.rect.w * 0.5) - (w * 0.5);
for glyph in app
.fc
.get_glyphs(line, self.size, canvas.graphics.clone())?
{
if let Some(tex) = glyph.tex.clone() {
let vertex_buffer = canvas.graphics.upload_verts(
canvas.width as _,
canvas.height as _,
cur_x + glyph.left,
cur_y - glyph.top,
glyph.width,
glyph.height,
)?;
let set0 = canvas.pipeline_fg_glyph.uniform_sampler(
0,
ImageView::new_default(tex)?,
Filter::Linear,
)?;
let set1 = canvas
.pipeline_fg_glyph
.uniform_buffer(1, self.fg_color.to_array().to_vec())?;
let pass = canvas.pipeline_fg_glyph.create_pass(
[canvas.width as _, canvas.height as _],
vertex_buffer,
canvas.graphics.quad_indices.clone(),
vec![set0, set1],
)?;
cmd_buffer.run_ref(&pass)?;
}
cur_x += glyph.advance;
}
cur_y += (self.size as f32) * 1.5;
}
Ok(())
}
}
pub enum KeyCapType {
/// Label is in center of keycap
Regular,
+36
View File
@@ -17,6 +17,7 @@ use crate::{
task::{ColorChannel, SystemTask, TaskType},
},
config::{save_layout, save_settings, AStrSetExt},
hid::VirtualKey,
overlays::{
toast::{Toast, ToastTopic},
watch::WATCH_NAME,
@@ -26,6 +27,12 @@ use crate::{
use super::{ExecArgs, ModularControl, ModularData};
#[derive(Deserialize, Clone)]
pub enum PressRelease {
Release,
Press,
}
#[derive(Deserialize, Clone, Copy)]
pub enum ViewAngleKind {
/// The cosine of the angle at which the watch becomes fully transparent
@@ -47,6 +54,7 @@ pub enum HighlightTest {
AutoRealign,
NotificationSounds,
Notifications,
RorateLock,
}
#[derive(Deserialize, Clone)]
@@ -55,6 +63,7 @@ pub enum SystemAction {
ToggleAutoRealign,
ToggleNotificationSounds,
ToggleNotifications,
ToggleRotateLock,
PlayspaceResetOffset,
PlayspaceFixFloor,
RecalculateExtent,
@@ -114,6 +123,10 @@ pub enum ButtonAction {
command: ExecArgs,
toast: Option<Arc<str>>,
},
VirtualKey {
keycode: VirtualKey,
action: PressRelease,
},
Watch {
action: WatchAction,
},
@@ -134,6 +147,9 @@ pub enum ButtonAction {
channel: ColorChannel,
delta: f32,
},
DragMultiplier {
delta: f32,
},
System {
action: SystemAction,
},
@@ -295,6 +311,7 @@ fn modular_button_highlight(
HighlightTest::AutoRealign => app.session.config.realign_on_showhide,
HighlightTest::NotificationSounds => app.session.config.notifications_sound_enabled,
HighlightTest::Notifications => app.session.config.notifications_enabled,
HighlightTest::RorateLock => !app.session.config.space_rotate_unlocked,
};
if lit {
@@ -310,6 +327,9 @@ fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppStat
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::VirtualKey { keycode, action } => app
.hid_provider
.send_key(*keycode, matches!(*action, PressRelease::Press)),
ButtonAction::Toast {
message,
body,
@@ -330,6 +350,9 @@ 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;
}
}
}
@@ -363,6 +386,19 @@ fn run_system(action: &SystemAction, app: &mut AppState) {
)
.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));
+8 -1
View File
@@ -38,6 +38,7 @@ pub enum LabelContent {
low_color: Arc<str>,
charging_color: Arc<str>,
},
DragMultiplier,
Ipd,
}
@@ -66,6 +67,7 @@ pub enum LabelData {
Ipd {
last_ipd: f32,
},
DragMultiplier,
}
pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
@@ -110,7 +112,8 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
label.set_text(text);
None
}
LabelContent::Ipd => Some(LabelData::Ipd { last_ipd: 0. }),
LabelContent::Ipd => Some(LabelData::Ipd { last_ipd: -1. }),
LabelContent::DragMultiplier => Some(LabelData::DragMultiplier),
};
if let Some(state) = state {
@@ -120,6 +123,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");
};
@@ -249,5 +253,8 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
control.set_text(&format!("{:.1}", app.input_state.ipd));
}
}
LabelData::DragMultiplier => {
control.set_text(&format!("{:.1}", app.session.config.space_drag_multiplier));
}
}
}
+100 -14
View File
@@ -1,29 +1,30 @@
pub mod button;
pub mod label;
//pub mod slider;
use std::sync::Arc;
use std::{fs::File, sync::Arc};
use glam::Vec4;
use once_cell::sync::Lazy;
use serde::Deserialize;
use vulkano::{command_buffer::CommandBufferUsage, image::view::ImageView};
use crate::{backend::common::OverlaySelector, state::AppState};
use crate::{
backend::common::OverlaySelector, config::AStrMapExt, config_io::CONFIG_ROOT_PATH,
graphics::dds::WlxCommandBufferDds, state::AppState,
};
use self::{
button::{modular_button_init, ButtonAction, ButtonData, OverlayAction},
label::{modular_label_init, LabelContent, LabelData},
};
use super::{color_parse, Canvas, CanvasBuilder, Control};
use super::{
canvas::{builder::CanvasBuilder, control::Control, Canvas},
color_parse, GuiColor, FALLBACK_COLOR,
};
type ModularControl = Control<(), ModularData>;
type ExecArgs = Vec<Arc<str>>;
pub type GuiColor = Vec4;
static FALLBACK_COLOR: Lazy<GuiColor> = Lazy::new(|| Vec4::new(1., 0., 1., 1.));
#[derive(Deserialize)]
pub struct ModularUiConfig {
pub width: f32,
@@ -53,10 +54,12 @@ pub struct OverlayListTemplate {
pub enum ModularElement {
Panel {
rect: [f32; 4],
corner_radius: Option<f32>,
bg_color: Arc<str>,
},
Label {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
#[serde(flatten)]
@@ -64,13 +67,20 @@ pub enum ModularElement {
},
CenteredLabel {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
#[serde(flatten)]
data: LabelContent,
},
Sprite {
rect: [f32; 4],
sprite: Arc<str>,
sprite_st: Option<[f32; 4]>,
},
Button {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
@@ -81,6 +91,7 @@ pub enum ModularElement {
/// Convenience type to save you from having to create a bunch of labels
BatteryList {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
fg_color_low: Arc<str>,
@@ -91,6 +102,7 @@ pub enum ModularElement {
},
OverlayList {
rect: [f32; 4],
corner_radius: Option<f32>,
font_size: isize,
fg_color: Arc<str>,
bg_color: Arc<str>,
@@ -120,7 +132,7 @@ 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 _,
@@ -134,35 +146,72 @@ 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.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());
let label = canvas.label(
*x,
*y,
*w,
*h,
corner_radius.unwrap_or_default(),
empty_str.clone(),
);
modular_label_init(label, data);
}
ModularElement::CenteredLabel {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
data,
} => {
canvas.font_size = *font_size;
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
let label = canvas.label_centered(*x, *y, *w, *h, empty_str.clone());
let label = canvas.label_centered(
*x,
*y,
*w,
*h,
corner_radius.unwrap_or_default(),
empty_str.clone(),
);
modular_label_init(label, data);
}
ModularElement::Sprite {
rect: [x, y, w, h],
sprite,
sprite_st,
} => match sprite_from_path(sprite.clone(), state) {
Ok(view) => {
let sprite = canvas.sprite(*x, *y, *w, *h);
sprite.fg_color = Vec4::ONE;
sprite.set_sprite(view);
let st = sprite_st
.map(|st| Vec4::from_slice(&st))
.unwrap_or_else(|| Vec4::new(1., 1., 0., 0.));
sprite.set_sprite_st(st);
}
Err(e) => {
log::warn!("Could not load custom UI sprite: {:?}", e);
}
},
ModularElement::Button {
rect: [x, y, w, h],
corner_radius,
font_size,
bg_color,
fg_color,
@@ -172,11 +221,19 @@ pub fn modular_canvas(
canvas.bg_color = color_parse(bg_color).unwrap_or(*FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(*FALLBACK_COLOR);
canvas.font_size = *font_size;
let button = canvas.button(*x, *y, *w, *h, text.clone());
let button = canvas.button(
*x,
*y,
*w,
*h,
corner_radius.unwrap_or_default(),
text.clone(),
);
modular_button_init(button, data);
}
ModularElement::BatteryList {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
fg_color_low,
@@ -204,6 +261,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(
@@ -228,6 +286,7 @@ pub fn modular_canvas(
}
ModularElement::OverlayList {
rect: [x, y, w, h],
corner_radius,
font_size,
fg_color,
bg_color,
@@ -252,6 +311,7 @@ pub fn modular_canvas(
button_y + 2.,
button_w - 4.,
button_h - 4.,
corner_radius.unwrap_or_default(),
screen.name.clone(),
);
@@ -349,3 +409,29 @@ pub fn color_parse_or_default(color: &str) -> GuiColor {
*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_ROOT_PATH.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);
}
}
}
+6 -3
View File
@@ -7,6 +7,7 @@ use input_linux::{
};
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};
@@ -242,7 +243,7 @@ impl HidProvider for UInputProvider {
self.desktop_origin = origin;
}
fn mouse_move(&mut self, pos: Vec2) {
if self.current_action.pos.is_none() {
if self.current_action.pos.is_none() && self.current_action.scroll.is_none() {
self.current_action.pos = Some(pos);
}
self.current_action.last_requested_pos = Some(pos);
@@ -256,6 +257,9 @@ impl HidProvider for UInputProvider {
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) {
@@ -313,7 +317,7 @@ pub const META: KeyModifier = 0x80;
#[allow(non_camel_case_types)]
#[repr(u16)]
#[derive(Debug, PartialEq, Clone, Copy, IntegerId, EnumString, EnumIter)]
#[derive(Debug, Deserialize, PartialEq, Clone, Copy, IntegerId, EnumString, EnumIter)]
pub enum VirtualKey {
Escape = 9,
N1, // number row
@@ -533,7 +537,6 @@ pub fn get_key_type(key: VirtualKey) -> KeyType {
}
pub struct XkbKeymap {
pub context: xkb::Context,
pub keymap: xkb::Keymap,
}
+1 -1
View File
@@ -120,7 +120,7 @@ impl Dispatch<WlKeyboard, ()> for WlKeymapHandler {
match maybe_keymap {
Ok(Some(keymap)) => {
state.keymap = Some(XkbKeymap { context, keymap });
state.keymap = Some(XkbKeymap { keymap });
}
Ok(None) => {
log::error!("Could not load keymap: no keymap");
+21 -2
View File
@@ -1,6 +1,9 @@
use xkbcommon::xkb::{
self,
x11::{get_core_keyboard_device_id, keymap_new_from_device},
x11::{
get_core_keyboard_device_id, keymap_new_from_device, setup_xkb_extension,
SetupXkbExtensionFlags, MIN_MAJOR_XKB_VERSION, MIN_MINOR_XKB_VERSION,
},
};
use super::XkbKeymap;
@@ -9,8 +12,24 @@ pub fn get_keymap_x11() -> anyhow::Result<XkbKeymap> {
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
let (conn, _) = xcb::Connection::connect(None)?;
setup_xkb_extension(
&conn,
MIN_MAJOR_XKB_VERSION,
MIN_MINOR_XKB_VERSION,
SetupXkbExtensionFlags::NoFlags,
&mut 0,
&mut 0,
&mut 0,
&mut 0,
);
let device_id = get_core_keyboard_device_id(&conn);
if device_id == -1 {
return Err(anyhow::anyhow!(
"get_core_keyboard_device_id returned -1. Check your XKB installation."
));
}
let keymap = keymap_new_from_device(&context, &conn, device_id, xkb::KEYMAP_COMPILE_NO_FLAGS);
Ok(XkbKeymap { context, keymap })
Ok(XkbKeymap { keymap })
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

+15 -6
View File
@@ -19,9 +19,10 @@ use std::{
use clap::Parser;
use flexi_logger::{Duplicate, FileSpec, LogSpecification};
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")]
@@ -34,6 +35,10 @@ struct Args {
#[arg(long)]
openxr: bool,
/// Show the working set of overlay on startup
#[arg(long)]
show: bool,
/// Uninstall OpenVR manifest and exit
#[arg(long)]
uninstall: bool,
@@ -57,7 +62,11 @@ struct Args {
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = Args::parse();
let mut args = if std::env::args().skip(1).any(|a| !a.is_empty()) {
Args::parse()
} else {
Args::default()
};
if !args.multi && !ensure_single_instance(args.replace) {
println!("Looks like WlxOverlay-S is already running.");
@@ -70,7 +79,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
log::info!(
"Welcome to {} version {}!",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
env!("WLX_BUILD"),
);
log::info!("It is {}.", chrono::Local::now().format("%c"));
@@ -105,7 +114,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) => {
@@ -118,7 +127,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) => {
@@ -214,7 +223,7 @@ fn ensure_single_instance(replace: bool) -> bool {
if let Ok(pid_str) = std::fs::read_to_string(&path) {
if let Ok(pid) = pid_str.trim().parse::<u32>() {
let mut system = sysinfo::System::new();
system.refresh_processes();
system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(pid)]));
if let Some(proc) = system.process(sysinfo::Pid::from_u32(pid)) {
if replace {
proc.kill_with(sysinfo::Signal::Term);
+1 -1
View File
@@ -9,7 +9,7 @@ use crate::state::AppState;
pub static ANCHOR_NAME: Lazy<Arc<str>> = Lazy::new(|| Arc::from("anchor"));
pub fn create_anchor<O>(state: &AppState) -> anyhow::Result<OverlayData<O>>
pub fn create_anchor<O>(state: &mut AppState) -> anyhow::Result<OverlayData<O>>
where
O: 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 {
+87 -16
View File
@@ -6,11 +6,14 @@ use std::{
use crate::{
backend::{
input::PointerMode,
overlay::{OverlayData, OverlayState},
input::{InteractionHandler, PointerMode},
overlay::{OverlayBackend, OverlayData, OverlayRenderer, OverlayState},
},
config::{self, ConfigType},
gui::{color_parse, CanvasBuilder, Control, KeyCapType},
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,
@@ -53,11 +56,12 @@ where
data,
)?;
canvas.bg_color = color_parse("#101010").unwrap(); //safe
canvas.panel(0., 0., size.x, size.y);
canvas.bg_color = color_parse("#181926").unwrap(); //safe
canvas.panel(0., 0., size.x, size.y, 12.);
canvas.font_size = 18;
canvas.bg_color = color_parse("#202020").unwrap(); //safe
canvas.fg_color = color_parse("#cad3f5").unwrap(); //safe
canvas.bg_color = color_parse("#1e2030").unwrap(); //safe
let has_altgr = keymap.as_ref().map_or(false, |k| k.has_altgr());
@@ -129,12 +133,14 @@ where
continue;
}
let mut iter = exec_args.iter().cloned();
maybe_state = Some(KeyButtonData::Exec {
program: iter.next().unwrap(),
args: iter.by_ref().take_while(|arg| arg[..] != *"null").collect(),
release_program: iter.next(),
release_args: iter.collect(),
});
if let Some(program) = iter.next() {
maybe_state = Some(KeyButtonData::Exec {
program,
args: iter.by_ref().take_while(|arg| arg[..] != *"null").collect(),
release_program: iter.next(),
release_args: iter.collect(),
})
};
} else {
log::error!("Unknown key: {}", key);
}
@@ -143,7 +149,7 @@ where
if label.is_empty() {
label = LAYOUT.label_for_key(key);
}
let button = canvas.key_button(x, y, w, h, cap_type, &label);
let button = canvas.key_button(x, y, w, h, 12., cap_type, &label);
button.state = Some(state);
button.on_press = Some(key_press);
button.on_release = Some(key_release);
@@ -174,7 +180,7 @@ where
interaction_transform,
..Default::default()
},
backend: Box::new(canvas),
backend: Box::new(KeyboardBackend { canvas }),
..Default::default()
})
}
@@ -265,7 +271,7 @@ fn key_release(
}
}
static PRESS_COLOR: Vec4 = Vec4::new(1.0, 1.0, 1.0, 0.5);
static PRESS_COLOR: Vec4 = Vec4::new(198. / 255., 160. / 255., 246. / 255., 0.5);
fn test_highlight(
control: &Control<KeyboardData, KeyButtonData>,
@@ -323,7 +329,7 @@ enum KeyButtonData {
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()); // want panic
Lazy::new(|| Regex::new(r"^([A-Za-z0-9_-]+)(?: +(UP|DOWN))?$").unwrap()); // want panic
#[derive(Debug, Deserialize, Serialize)]
pub struct Layout {
@@ -429,3 +435,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 extent(&mut self) -> Option<[u32; 3]> {
self.canvas.extent()
}
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;
app.hid_provider.set_modifiers(0);
self.canvas.pause(app)
}
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.canvas.resume(app)
}
}
+30 -25
View File
@@ -4,7 +4,6 @@ use std::{
};
use futures::{Future, FutureExt};
use glam::vec3a;
use wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireSelectScreenResult};
use crate::{
@@ -57,30 +56,33 @@ impl OverlayRenderer for MirrorRenderer {
}
};
if let Ok(pw_result) = maybe_pw_result {
let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
log::info!("{}: PipeWire node selected: {}", self.name.clone(), node_id,);
let capture = PipewireCapture::new(self.name.clone(), node_id);
self.renderer = Some(ScreenRenderer::new_raw(
self.name.clone(),
Box::new(capture),
));
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(self.name.clone()),
Box::new(|app, o| {
o.grabbable = true;
o.interactable = true;
o.reset(app, false);
}),
));
} else {
log::warn!("Failed to create pipewire mirror");
self.renderer = None;
// drop self
app.tasks
.enqueue(TaskType::DropOverlay(OverlaySelector::Name(
match maybe_pw_result {
Ok(pw_result) => {
let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
log::info!("{}: PipeWire node selected: {}", self.name.clone(), node_id);
let capture = PipewireCapture::new(self.name.clone(), node_id);
self.renderer = Some(ScreenRenderer::new_raw(
self.name.clone(),
)));
Box::new(capture),
));
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(self.name.clone()),
Box::new(|app, o| {
o.grabbable = true;
o.interactable = true;
o.reset(app, false);
}),
));
}
Err(e) => {
log::warn!("Failed to create mirror due to PipeWire error: {:?}", e);
self.renderer = None;
// drop self
app.tasks
.enqueue(TaskType::DropOverlay(OverlaySelector::Name(
self.name.clone(),
)));
}
}
}
@@ -118,6 +120,10 @@ 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 extent(&mut self) -> Option<[u32; 3]> {
Some(self.last_extent)
}
}
pub fn new_mirror(
@@ -130,7 +136,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 {
+150 -42
View File
@@ -15,9 +15,10 @@ use vulkano::{
};
use wlx_capture::{
frame::{
DrmFormat, MouseMeta, WlxFrame, DRM_FORMAT_ABGR2101010, DRM_FORMAT_ABGR8888,
DrmFormat, FrameFormat, MouseMeta, WlxFrame, DRM_FORMAT_ABGR2101010, DRM_FORMAT_ABGR8888,
DRM_FORMAT_ARGB8888, DRM_FORMAT_XBGR2101010, DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888,
},
pipewire::PipewireSelectScreenResult,
WlxCapture,
};
@@ -25,10 +26,13 @@ use wlx_capture::{
use {
crate::config_io,
std::error::Error,
std::{ops::Deref, path::PathBuf},
wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireStream},
std::{ops::Deref, path::PathBuf, task},
wlx_capture::pipewire::PipewireCapture,
};
#[cfg(all(feature = "x11", feature = "pipewire"))]
use wlx_capture::pipewire::PipewireStream;
#[cfg(feature = "wayland")]
use {
crate::config::AStrMapExt,
@@ -49,7 +53,7 @@ use crate::{
input::{Haptics, InteractionHandler, PointerHit, PointerMode},
overlay::{OverlayRenderer, OverlayState, SplitOverlayBackend},
},
config::{def_pw_tokens, PwTokenMap},
config::{def_pw_tokens, GeneralConfig, PwTokenMap},
graphics::{
fourcc_to_vk, WlxCommandBuffer, WlxPipeline, WlxPipelineLegacy, DRM_FORMAT_MOD_INVALID,
},
@@ -214,7 +218,7 @@ impl ScreenPipeline {
];
let mouse_tex =
uploads.texture2d(4, 4, vulkano::format::Format::R8G8B8A8_UNORM, &mouse_bytes)?;
uploads.texture2d_raw(4, 4, vulkano::format::Format::R8G8B8A8_UNORM, &mouse_bytes)?;
self.mouse = Some(ImageView::new_default(mouse_tex)?);
Ok(())
}
@@ -229,7 +233,9 @@ impl ScreenPipeline {
.graphics
.create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
let view = ImageView::new_default(image)?;
let set0 = self.pipeline.uniform_sampler(0, view, Filter::Linear)?;
let set0 = self
.pipeline
.uniform_sampler(0, view, app.graphics.texture_filtering)?;
let pass = self.pipeline.create_pass(
self.extentf,
@@ -278,7 +284,7 @@ pub struct ScreenRenderer {
capture: Box<dyn WlxCapture>,
pipeline: Option<ScreenPipeline>,
last_view: Option<Arc<ImageView>>,
extent: [u32; 3],
extent: Option<[u32; 3]>,
}
impl ScreenRenderer {
@@ -289,7 +295,7 @@ impl ScreenRenderer {
capture,
pipeline: None,
last_view: None,
extent: [0; 3],
extent: None,
}
}
@@ -303,7 +309,7 @@ impl ScreenRenderer {
capture: Box::new(capture),
pipeline: None,
last_view: None,
extent: extent_from_res(output.size),
extent: None,
})
}
@@ -317,7 +323,7 @@ impl ScreenRenderer {
capture: Box::new(capture),
pipeline: None,
last_view: None,
extent: extent_from_res(output.size),
extent: None,
})
}
@@ -332,21 +338,22 @@ impl ScreenRenderer {
)> {
let name = output.name.clone();
let embed_mouse = !session.config.double_cursor_fix;
log::info!(
"On screen share prompt, pick: {} {} {} (pos {}, {})",
&output.name,
&output.make,
&output.model,
&output.logical_pos.0,
&output.logical_pos.1,
);
let select_screen_result = futures::executor::block_on(pipewire_select_screen(
let select_screen_result = select_pw_screen(
&format!(
"Now select: {} {} {} @ {},{}",
&output.name,
&output.make,
&output.model,
&output.logical_pos.0,
&output.logical_pos.1
),
token,
embed_mouse,
true,
true,
false,
))?;
)?;
let node_id = select_screen_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element
@@ -358,7 +365,7 @@ impl ScreenRenderer {
capture: Box::new(capture),
pipeline: None,
last_view: None,
extent: extent_from_res(output.size),
extent: None,
},
select_screen_result.restore_token,
))
@@ -373,7 +380,7 @@ impl ScreenRenderer {
capture: Box::new(capture),
pipeline: None,
last_view: None,
extent: extent_from_res((screen.monitor.width(), screen.monitor.height())),
extent: None,
}
}
}
@@ -461,11 +468,28 @@ impl OverlayRenderer for ScreenRenderer {
log::error!("Invalid frame");
continue;
}
self.extent.get_or_insert_with(|| {
extent_from_format(frame.format, &app.session.config)
});
match app.graphics.dmabuf_texture(frame) {
Ok(new) => {
let view = ImageView::new_default(new.clone())?;
self.last_view = Some(view);
let pipeline = match self.pipeline {
Some(ref mut p) => Some(p),
None if app.session.config.screen_render_down => {
log::info!("{}: Using render-down pass.", self.name);
let pipeline = ScreenPipeline::new(&self.extent.unwrap(), app)?; // safe
self.last_view = Some(pipeline.view.clone());
self.pipeline = Some(pipeline);
self.pipeline.as_mut()
}
None => None,
};
if let Some(pipeline) = pipeline {
pipeline.render(new.clone(), None, app)?;
} else {
let view = ImageView::new_default(new.clone())?;
self.last_view = Some(view);
}
}
Err(e) => {
log::error!(
@@ -486,6 +510,9 @@ impl OverlayRenderer for ScreenRenderer {
log::error!("No fd");
continue;
};
self.extent.get_or_insert_with(|| {
extent_from_format(frame.format, &app.session.config)
});
log::debug!("{}: New MemFd frame", self.name);
let format = fourcc_to_vk(frame.format.fourcc)?;
@@ -505,8 +532,12 @@ impl OverlayRenderer for ScreenRenderer {
let data = unsafe { slice::from_raw_parts(map, len) };
let image =
upload.texture2d(frame.format.width, frame.format.height, format, data)?;
let image = upload.texture2d_raw(
frame.format.width,
frame.format.height,
format,
data,
)?;
upload.build_and_execute_now()?;
unsafe { libc::munmap(map as *mut _, len) };
@@ -524,16 +555,23 @@ impl OverlayRenderer for ScreenRenderer {
let data = unsafe { slice::from_raw_parts(frame.ptr as *const u8, frame.size) };
let image =
upload.texture2d(frame.format.width, frame.format.height, format, data)?;
let image = upload.texture2d_raw(
frame.format.width,
frame.format.height,
format,
data,
)?;
let pipeline = Some(match self.pipeline {
Some(ref mut p) => p,
_ => {
let mut pipeline = ScreenPipeline::new(&self.extent, app)?;
log::info!("{}: Using render-down pass.", self.name);
let extent = extent_from_format(frame.format, &app.session.config);
let mut pipeline = ScreenPipeline::new(&extent, app)?;
self.last_view = Some(pipeline.view.clone());
pipeline.ensure_mouse_initialized(&mut upload)?;
self.pipeline = Some(pipeline);
self.extent = Some(extent);
self.pipeline.as_mut().unwrap() // safe
}
});
@@ -563,6 +601,9 @@ impl OverlayRenderer for ScreenRenderer {
fn view(&mut self) -> Option<Arc<ImageView>> {
self.last_view.clone()
}
fn extent(&mut self) -> Option<[u32; 3]> {
self.extent
}
}
#[cfg(feature = "wayland")]
@@ -574,9 +615,7 @@ pub fn create_screen_renderer_wl(
session: &AppSession,
) -> Option<ScreenRenderer> {
let mut capture: Option<ScreenRenderer> = None;
if (&*session.config.capture_method == "auto" || &*session.config.capture_method == "dmabuf")
&& has_wlr_dmabuf
{
if (&*session.config.capture_method == "wlr-dmabuf") && has_wlr_dmabuf {
log::info!("{}: Using Wlr DMA-Buf", &output.name);
capture = ScreenRenderer::new_wlr_dmabuf(output);
}
@@ -664,7 +703,11 @@ fn create_screen_state(
Transform::_270 | Transform::Flipped270 => {
Affine2::from_cols(Vec2::Y * (res.0 as f32 / res.1 as f32), Vec2::X, center)
}
_ => Affine2::from_cols(Vec2::X, Vec2::Y * (-res.0 as f32 / res.1 as f32), center),
_ if res.1 > res.0 => {
// Xorg upright screens
Affine2::from_cols(Vec2::X * (res.1 as f32 / res.0 as f32), Vec2::NEG_Y, center)
}
_ => Affine2::from_cols(Vec2::X, Vec2::NEG_Y * (res.0 as f32 / res.1 as f32), center),
};
OverlayState {
@@ -825,8 +868,16 @@ pub fn create_screens_x11pw(app: &mut AppState) -> anyhow::Result<ScreenCreateDa
let pw_tokens_copy = pw_tokens.clone();
let token = pw_tokens.arc_get("x11").map(|s| s.as_str());
let embed_mouse = !app.session.config.double_cursor_fix;
let select_screen_result =
futures::executor::block_on(pipewire_select_screen(token, embed_mouse, true, true, true))?;
let select_screen_result = select_pw_screen(
"Select ALL screens on the screencast pop-up!",
token,
embed_mouse,
true,
true,
true,
)?;
if let Some(restore_token) = select_screen_result.restore_token {
if pw_tokens.arc_set("x11".into(), restore_token.clone()) {
log::info!("Adding Pipewire token {}", restore_token);
@@ -879,7 +930,11 @@ pub fn create_screens_x11pw(app: &mut AppState) -> anyhow::Result<ScreenCreateDa
capture: Box::new(PipewireCapture::new(m.name.clone(), s.node_id)),
pipeline: None,
last_view: None,
extent: extent_from_res(size),
extent: Some(extent_from_res(
size.0 as _,
size.1 as _,
&app.session.config,
)),
};
let backend = Box::new(SplitOverlayBackend {
@@ -983,12 +1038,14 @@ impl From<wl_output::Transform> for Transform {
}
}
fn extent_from_res(res: (i32, i32)) -> [u32; 3] {
// screens above a certain resolution will have severe aliasing
fn extent_from_format(fmt: FrameFormat, config: &GeneralConfig) -> [u32; 3] {
extent_from_res(fmt.width, fmt.height, config)
}
// TODO make dynamic. maybe don't go above HMD resolution?
let w = res.0.min(2560) as u32;
let h = (res.1 as f32 / res.0 as f32 * w as f32) as u32;
fn extent_from_res(width: u32, height: u32, config: &GeneralConfig) -> [u32; 3] {
// screens above a certain resolution will have severe aliasing
let h = height.min(config.screen_max_height as u32);
let w = (width as f32 / height as f32 * h as f32) as u32;
[w, h, 1]
}
@@ -1019,3 +1076,54 @@ fn best_match<'a>(
log::debug!("best: {:?}", best.map(|b| &b.monitor));
best
}
#[cfg(feature = "pipewire")]
fn select_pw_screen(
instructions: &str,
token: Option<&str>,
embed_mouse: bool,
screens_only: bool,
persist: bool,
multiple: bool,
) -> Result<PipewireSelectScreenResult, wlx_capture::pipewire::AshpdError> {
use crate::backend::notifications::DbusNotificationSender;
use wlx_capture::pipewire::pipewire_select_screen;
let future = async move {
let print_at = Instant::now() + Duration::from_millis(250);
let mut notify = None;
let f = pipewire_select_screen(token, embed_mouse, screens_only, persist, multiple);
futures::pin_mut!(f);
loop {
match futures::poll!(&mut f) {
task::Poll::Ready(result) => return result,
task::Poll::Pending => {
if Instant::now() >= print_at {
log::info!("{}", instructions);
if let Ok(sender) = DbusNotificationSender::new() {
if let Ok(id) = sender.notify_send(instructions, "", 2, 0, 0, true) {
notify = Some((sender, id));
}
}
break;
}
futures::future::lazy(|_| {
std::thread::sleep(Duration::from_millis(10));
})
.await;
continue;
}
}
}
let result = f.await;
if let Some((sender, id)) = notify {
let _ = sender.notify_close(id);
}
result
};
futures::executor::block_on(future)
}
+10 -10
View File
@@ -11,7 +11,7 @@ use crate::{
overlay::{OverlayBackend, OverlayState, RelativeTo},
task::TaskType,
},
gui::{color_parse, CanvasBuilder},
gui::{canvas::builder::CanvasBuilder, color_parse},
state::{AppState, LeftRight},
};
@@ -178,19 +178,19 @@ fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box<dyn
.ok()?;
canvas.font_size = FONT_SIZE;
canvas.fg_color = color_parse("#aaaaaa").unwrap(); // want panic
canvas.bg_color = color_parse("#333333").unwrap(); // want panic
canvas.panel(0., 0., size.0, size.1);
canvas.fg_color = color_parse("#cad3f5").unwrap(); // want panic
canvas.bg_color = color_parse("#1e2030").unwrap(); // want panic
canvas.panel(0., 0., size.0, size.1, 16.);
if toast.body.len() > 0 {
canvas.label(PADDING.0, 54., og_width, size.1 - 54., toast.body);
canvas.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 {
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 {
+3 -3
View File
@@ -4,15 +4,15 @@ use crate::{
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
config::{load_known_yaml, ConfigType},
gui::{
canvas::Canvas,
modular::{modular_canvas, ModularData, ModularUiConfig},
Canvas,
},
state::AppState,
};
pub const WATCH_NAME: &str = "watch";
pub fn create_watch<O>(state: &AppState) -> anyhow::Result<OverlayData<O>>
pub fn create_watch<O>(state: &mut AppState) -> anyhow::Result<OverlayData<O>>
where
O: Default,
{
@@ -40,7 +40,7 @@ where
pub fn create_watch_canvas(
config: Option<ModularUiConfig>,
state: &AppState,
state: &mut AppState,
) -> anyhow::Result<Canvas<(), ModularData>> {
let config = config.unwrap_or_else(|| load_known_yaml::<ModularUiConfig>(ConfigType::Watch));
+3
View File
@@ -12,14 +12,17 @@ spawn_pos: [0, 0, -1]
elements:
- type: Panel
rect: [98, 0, 4, 200]
corner_radius: 0
bg_color: "#ffff00"
- type: Panel
rect: [0, 98, 200, 4]
corner_radius: 0
bg_color: "#ffff00"
- type: Label
rect: [8, 90, 600, 70]
corner_radius: 0
font_size: 18
fg_color: "#ffff00"
source: Static
+47 -25
View File
@@ -62,29 +62,51 @@ macros:
# Value: Array of strings. 0th element is the upper row, 1st element is lower row.
# For empty labels, use [] (do not use ~)
labels:
"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": ["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": [" \\", "|"]
+150 -88
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,55 +522,68 @@ elements:
- type: Panel
rect: [325, 315, 225, 1]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
####### Playspace Section #######
- type: Label
rect: [325, 345, 90, 24]
corner_radius: 6
font_size: 18
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Playspace
- type: Button
rect: [330, 360, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Fix Floor"
click_down:
- type: System
action: PlayspaceFixFloor
- type: Window
target: "settings"
action: Destroy
- type: Button
rect: [330, 410, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Reset Offset"
click_down:
- type: System
action: PlayspaceResetOffset
- type: Window
target: "settings"
action: Destroy
####### Notifications Section #######
- type: Panel
rect: [50, 460, 500, 1]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
- type: Label
rect: [325, 490, 90, 24]
corner_radius: 6
font_size: 18
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Notifications
- type: Button
rect: [330, 505, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#401010"
fg_color: "#24273a"
bg_color: "#e64553"
text: "Enabled"
click_down:
- type: System
@@ -537,9 +592,10 @@ elements:
- type: Button
rect: [330, 555, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#401010"
fg_color: "#24273a"
bg_color: "#e64553"
text: "Sound Enabled"
click_down:
- type: System
@@ -549,16 +605,18 @@ elements:
####### Behavior Section #######
- type: Label
rect: [15, 490, 570, 24]
corner_radius: 6
font_size: 18
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Static
text: Behavior
- type: Button
rect: [30, 505, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#401010"
fg_color: "#24273a"
bg_color: "#e64553"
text: "Auto-Realign"
click_down:
- type: System
@@ -567,9 +625,10 @@ elements:
- type: Button
rect: [30, 555, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#401010"
fg_color: "#24273a"
bg_color: "#e64553"
text: "Grab+Scroll Slide"
click_down:
- type: System
@@ -580,13 +639,15 @@ elements:
- type: Panel
rect: [50, 605, 500, 1]
bg_color: "#c0c0c0"
corner_radius: 6
bg_color: "#6e738d"
- type: Button
rect: [330, 625, 220, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Save Config"
click_down:
- type: System
@@ -596,9 +657,10 @@ elements:
- type: Button
rect: [30, 625, 250, 30]
corner_radius: 6
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
fg_color: "#24273a"
bg_color: "#eed49f"
text: "Save Overlay Layout"
click_down:
- type: System
Binary file not shown.
+46 -32
View File
@@ -9,14 +9,16 @@ 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
@@ -29,9 +31,10 @@ elements:
# Keyboard button
- type: Button
rect: [32, 162, 60, 36]
font_size: 14
fg_color: "#FFFFFF"
bg_color: "#406050"
corner_radius: 4
font_size: 15
fg_color: "#24273a"
bg_color: "#a6da95"
text: Kbd
click_up:
- type: Overlay
@@ -63,9 +66,10 @@ elements:
# bottom row, of keyboard + overlays
- type: OverlayList
rect: [94, 160, 306, 40]
font_size: 14
fg_color: "#FFFFFF"
bg_color: "#405060"
corner_radius: 4
font_size: 15
fg_color: "#cad3f5"
bg_color: "#1e2030"
layout: Horizontal
click_up: ToggleVisible
long_click_up: Reset
@@ -79,8 +83,9 @@ elements:
# local clock
- type: Label
rect: [19, 90, 200, 50]
corner_radius: 4
font_size: 46 # Use 32 for 12-hour time
fg_color: "#ffffff"
fg_color: "#cad3f5"
source: Clock
format: "%H:%M" # 23:59
#format: "%I:%M %p" # 11:59 PM
@@ -88,16 +93,18 @@ elements:
# 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 +112,69 @@ elements:
# alt clock 1
- type: Label
rect: [210, 90, 200, 50]
corner_radius: 4
font_size: 24 # Use 18 for 12-hour time
fg_color: "#99BBAA"
fg_color: "#8bd5ca"
source: Clock
timezone: "Asia/Tokyo" # change TZ1 here
format: "%H:%M" # 23:59
#format: "%I:%M %p" # 11:59 PM
- type: Label
rect: [210, 60, 200, 50]
corner_radius: 4
font_size: 14
fg_color: "#99BBAA"
fg_color: "#8bd5ca"
source: Static
text: "Tokyo" # 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: "#AA99BB"
fg_color: "#b7bdf8"
source: Clock
timezone: "America/Chicago" # 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: "#AA99BB"
fg_color: "#b7bdf8"
source: Static
text: "Chicago" # change TZ2 label here
# 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%" ]
- 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%" ]
+110 -1
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",
@@ -97,7 +183,7 @@ pub mod frag_screen {
// linear to srgb
bvec4 cutoff = lessThan(out_color, vec4(0.0031308));
vec4 higher = (pow(out_color, vec4(0.41666)) * vec4(1.055)) - vec4(0.055);
vec4 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;
@@ -135,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;
}
",
}
}
+21 -33
View File
@@ -1,21 +1,25 @@
use std::{io::Cursor, path::PathBuf, sync::Arc};
use std::{io::Cursor, sync::Arc};
use anyhow::bail;
use glam::{Affine3A, Vec3};
use glam::Affine3A;
use idmap::IdMap;
use rodio::{Decoder, OutputStream, OutputStreamHandle, Source};
use serde::{Deserialize, Serialize};
use smallvec::{smallvec, SmallVec};
use vulkano::image::view::ImageView;
use crate::{
backend::{input::InputState, task::TaskContainer},
config::GeneralConfig,
config::{AStrMap, GeneralConfig},
config_io,
graphics::WlxGraphics,
gui::font::FontCache,
hid::HidProvider,
overlays::toast::{DisplayMethod, ToastTopic},
shaders::{frag_color, frag_glyph, frag_screen, frag_sprite, frag_srgb, vert_common},
shaders::{
frag_color, frag_glyph, frag_grid, frag_screen, frag_sprite, frag_sprite2, frag_sprite2_hl,
frag_swapchain, vert_common,
},
};
pub struct AppState {
@@ -28,6 +32,7 @@ pub struct AppState {
pub audio: AudioOutput,
pub screens: SmallVec<[ScreenMeta; 8]>,
pub anchor: Affine3A,
pub sprites: AStrMap<Arc<ImageView>>,
}
impl AppState {
@@ -47,14 +52,23 @@ 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);
}
let session = AppSession::load();
@@ -69,20 +83,15 @@ impl AppState {
audio: AudioOutput::new(),
screens: smallvec![],
anchor: Affine3A::IDENTITY,
sprites: AStrMap::new(),
})
}
}
pub struct AppSession {
pub config_root_path: PathBuf,
pub config: GeneralConfig,
pub toast_topics: IdMap<ToastTopic, DisplayMethod>,
pub color_norm: Vec3,
pub color_shift: Vec3,
pub color_alt: Vec3,
pub color_grab: Vec3,
}
impl AppSession {
@@ -101,29 +110,8 @@ impl AppSession {
});
AppSession {
config_root_path,
config,
toast_topics,
color_norm: Vec3 {
x: 0.,
y: 1.,
z: 1.,
},
color_shift: Vec3 {
x: 1.,
y: 1.,
z: 0.,
},
color_alt: Vec3 {
x: 1.,
y: 0.,
z: 1.,
},
color_grab: Vec3 {
x: 1.,
y: 0.,
z: 0.,
},
}
}
}