mirror of
https://github.com/wayvr-org/wayvr.git
synced 2026-10-28 18:08:40 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f425820d7 | ||
|
|
6a2d8b9f90 | ||
|
|
583a5f7fe0 | ||
|
|
03f2eaf97f | ||
|
|
21331b5c42 | ||
|
|
72a82530ae | ||
|
|
13d9284bb8 | ||
|
|
b0a33a1181 | ||
|
|
4f38d1ad44 | ||
|
|
c6bc880bc3 | ||
|
|
0a2be76150 | ||
|
|
1a87823847 | ||
|
|
e763e15bdc | ||
|
|
6a3b60f298 | ||
|
|
397b3eedd1 | ||
|
|
9d18628354 | ||
|
|
dc985f6da6 | ||
|
|
850601b3b9 | ||
|
|
8a4c7a400f | ||
|
|
d9287f3250 | ||
|
|
378ea120c8 | ||
|
|
f4cc49ca69 | ||
|
|
57ff3aa1dd | ||
|
|
0808383e70 | ||
|
|
c71911998e | ||
|
|
50c3f49989 | ||
|
|
b6a07c43a5 | ||
|
|
ee3fbff44c | ||
|
|
c29bbbb142 | ||
|
|
aca177635e | ||
|
|
c3c8694e1a | ||
|
|
1de9cea397 | ||
|
|
939a537d20 | ||
|
|
26124be6ef | ||
|
|
3d0611052f | ||
|
|
dcc2cd0575 | ||
|
|
57ff3ec3bc | ||
|
|
e14b35bece | ||
|
|
88da3c832a | ||
|
|
7b4576bd52 | ||
|
|
a98b7a210f | ||
|
|
32e0e7656a | ||
|
|
f27c320231 | ||
|
|
a56ed68b44 | ||
|
|
7a6040bfee | ||
|
|
17addcde78 | ||
|
|
5a45d4dd2f | ||
|
|
9a928b1c0c | ||
|
|
3764873209 | ||
|
|
106028be67 | ||
|
|
d58268b8ec | ||
|
|
787a3851b3 | ||
|
|
2bfa78e9db | ||
|
|
71068a0215 | ||
|
|
5575e4f49f | ||
|
|
a991c6b84a | ||
|
|
bdc500973c | ||
|
|
80bd65235d | ||
|
|
39595faaad | ||
|
|
1fc6cb4a36 | ||
|
|
10db0f690a | ||
|
|
072540fbb5 | ||
|
|
d225250b77 | ||
|
|
c7aa88647c | ||
|
|
abc132c553 | ||
|
|
371d3c2757 | ||
|
|
c06ec6d974 | ||
|
|
b3ae583cb4 | ||
|
|
8fb9ef4a05 | ||
|
|
40383f2529 | ||
|
|
b2429bce7f | ||
|
|
59c0bc870c | ||
|
|
4abfa3c3ac | ||
|
|
7037740d46 | ||
|
|
8ae65083f9 |
No files matched your search
@@ -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
|
||||
@@ -2,7 +2,7 @@ name: Check Full
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
#branches: [ "main" ]
|
||||
branches: [ "!main" ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
Generated
+825
-512
File diff suppressed because it is too large.
Load diff
+37
-28
@@ -4,74 +4,83 @@ debug = true
|
||||
|
||||
[package]
|
||||
name = "wlx-overlay-s"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.79"
|
||||
ash = "^0.37.2"
|
||||
chrono = "0.4.29"
|
||||
anyhow = "1.0.86"
|
||||
ash = "^0.38.0"
|
||||
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"
|
||||
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 }
|
||||
sysinfo = { version = "0.31.2" }
|
||||
thiserror = "1.0.61"
|
||||
vulkano = { git = "https://github.com/vulkano-rs/vulkano", rev = "b9f3e89" }
|
||||
vulkano-shaders = { git = "https://github.com/vulkano-rs/vulkano", rev = "b9f3e89" }
|
||||
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.3.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 = []
|
||||
@@ -4,12 +4,14 @@ 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)
|
||||
|
||||

|
||||
|
||||
# Join the Linux VR Community
|
||||
|
||||
We are available on either:
|
||||
@@ -80,9 +82,9 @@ The click depends on the laser color:
|
||||
- Purple laser: Middle click
|
||||
- Stick up/down: Scroll wheel
|
||||
|
||||
To **curve screens**, grab them with one hand, then with the other hand, hover the laser over the overlay and use the scroll action.
|
||||
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
|
||||
|
||||
@@ -111,20 +113,23 @@ Check [here](https://github.com/galister/wlx-overlay-s/wiki/Troubleshooting) for
|
||||
|
||||
# Known Issues
|
||||
|
||||
## Auto-Start not working on SteamVR 2.4.4
|
||||
## Mouse is not where it should be
|
||||
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)
|
||||
|
||||
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.)
|
||||
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 capture can generally seem slow. This is because zero-copy GPU capture is not supported on the general X11 desktop. Consider trying Wayland or Picom.
|
||||
- DPI scaling is not supported and may cause the mouse to not follow the laser properly.
|
||||
- Upright screens are not supported and can cause the mouse to act weirdly.
|
||||
- Screen changes (connecting / disconnecting a display, resolution changes, etc) are not handled at runtime. Restart the overlay for these to take effect.
|
||||
@@ -0,0 +1,40 @@
|
||||
use regex::Regex;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let mut wlx_build = get_version().unwrap_or(format!("{}-unknown", env!("CARGO_PKG_VERSION")));
|
||||
|
||||
match std::env::var("GITHUB_JOB").as_deref() {
|
||||
Ok("make_release") => {
|
||||
wlx_build = format!("{} (Release)", &wlx_build);
|
||||
}
|
||||
Ok("build_appimage") => {
|
||||
wlx_build = format!("{} (AppImage)", &wlx_build);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
println!("cargo:rustc-env=WLX_BUILD={}", &wlx_build);
|
||||
}
|
||||
|
||||
fn get_version() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let re = Regex::new(r"v([0-9.]+)-([0-9]+)-g([a-f0-9]+)").unwrap(); // safe
|
||||
let output = Command::new("git")
|
||||
.args(["describe", "--tags", "--abbrev=7", "--dirty"])
|
||||
.output()?;
|
||||
|
||||
let mut output_str = String::from_utf8(output.stdout)?;
|
||||
|
||||
if output_str.is_empty() {
|
||||
let output = Command::new("git")
|
||||
.args(["describe", "--tags", "--abbrev=7", "--dirty", "--always"])
|
||||
.output()?;
|
||||
|
||||
output_str = format!(
|
||||
"{}-{}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
String::from_utf8(output.stdout)?
|
||||
);
|
||||
}
|
||||
|
||||
Ok(re.replace_all(&output_str, "${1}.r${2}.${3}").into_owned())
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
+12
-6
@@ -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,
|
||||
@@ -366,7 +370,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 +383,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 +393,8 @@ where
|
||||
} else {
|
||||
hovered.state.curvature = Some(new);
|
||||
}
|
||||
} else {
|
||||
hovered.state.curvature = None;
|
||||
}
|
||||
} else {
|
||||
hovered.backend.on_scroll(app, &hit, scroll);
|
||||
@@ -547,7 +553,7 @@ impl Pointer {
|
||||
}
|
||||
config.transform_values.arc_set(
|
||||
overlay.state.name.clone(),
|
||||
overlay.state.saved_transform.unwrap(),
|
||||
overlay.state.saved_transform.unwrap(), // safe
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,8 @@ impl PlayspaceMover {
|
||||
let new_hand = data
|
||||
.pose
|
||||
.transform_point3a(state.input_state.pointers[data.hand].raw_pose.translation);
|
||||
let relative_pos = new_hand - data.hand_pose;
|
||||
let relative_pos =
|
||||
(new_hand - data.hand_pose) * state.session.config.space_drag_multiplier;
|
||||
|
||||
if relative_pos.length_squared() > 1000.0 {
|
||||
log::warn!("Space drag too fast, ignoring");
|
||||
@@ -199,6 +200,15 @@ impl PlayspaceMover {
|
||||
|
||||
set_working_copy(&self.universe, chaperone_mgr, &mat);
|
||||
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
|
||||
|
||||
if self.drag.is_some() {
|
||||
log::info!("Space drag interrupted by fix floor");
|
||||
self.drag = None;
|
||||
}
|
||||
if self.rotate.is_some() {
|
||||
log::info!("Space rotate interrupted by fix floor");
|
||||
self.rotate = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn playspace_changed(
|
||||
|
||||
@@ -1,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
@@ -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>,
|
||||
|
||||
@@ -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
@@ -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>),
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -46,6 +46,7 @@ pub enum SystemTask {
|
||||
ColorGain(ColorChannel, f32),
|
||||
ResetPlayspace,
|
||||
FixFloor,
|
||||
ShowHide,
|
||||
}
|
||||
|
||||
pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send;
|
||||
|
||||
@@ -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<_>>();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub(crate) mod dds;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
os::fd::{FromRawFd, IntoRawFd},
|
||||
@@ -42,8 +44,8 @@ use vulkano::{
|
||||
allocator::StandardDescriptorSetAllocator, DescriptorSet, WriteDescriptorSet,
|
||||
},
|
||||
device::{
|
||||
physical::PhysicalDevice, Device, DeviceCreateInfo, DeviceExtensions, Features, Queue,
|
||||
QueueCreateInfo, QueueFlags,
|
||||
physical::PhysicalDevice, Device, DeviceCreateInfo, DeviceExtensions, DeviceFeatures,
|
||||
Queue, QueueCreateInfo, QueueFlags,
|
||||
},
|
||||
format::Format,
|
||||
image::{
|
||||
@@ -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();
|
||||
@@ -196,7 +195,7 @@ impl WlxGraphics {
|
||||
let target_version = vulkano::Version::V1_3;
|
||||
let library = get_vulkan_library();
|
||||
|
||||
let vk_app_info_raw = vk::ApplicationInfo::builder()
|
||||
let vk_app_info_raw = vk::ApplicationInfo::default()
|
||||
.application_version(0)
|
||||
.engine_version(0)
|
||||
.api_version(vk_target_version);
|
||||
@@ -206,7 +205,7 @@ impl WlxGraphics {
|
||||
.create_vulkan_instance(
|
||||
system,
|
||||
get_instance_proc_addr,
|
||||
&vk::InstanceCreateInfo::builder()
|
||||
&vk::InstanceCreateInfo::default()
|
||||
.application_info(&vk_app_info_raw)
|
||||
.enabled_extension_names(&instance_extensions_raw)
|
||||
as *const _ as *const _,
|
||||
@@ -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
|
||||
@@ -278,30 +286,33 @@ impl WlxGraphics {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let features = Features {
|
||||
let features = DeviceFeatures {
|
||||
dynamic_rendering: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let queue_priorities = [1.0];
|
||||
|
||||
let queue_create_infos = [vk::DeviceQueueCreateInfo::builder()
|
||||
let queue_create_infos = [vk::DeviceQueueCreateInfo::default()
|
||||
.queue_family_index(queue_family_index)
|
||||
.queue_priorities(&queue_priorities)
|
||||
.build()];
|
||||
.queue_priorities(&queue_priorities)];
|
||||
|
||||
let mut device_create_info = vk::DeviceCreateInfo::builder()
|
||||
let mut device_create_info = vk::DeviceCreateInfo::default()
|
||||
.queue_create_infos(&queue_create_infos)
|
||||
.enabled_extension_names(&device_extensions_raw)
|
||||
.build();
|
||||
.enabled_extension_names(&device_extensions_raw);
|
||||
|
||||
let mut dynamic_rendering = PhysicalDeviceDynamicRenderingFeatures::builder()
|
||||
.dynamic_rendering(true)
|
||||
.build();
|
||||
let mut dynamic_rendering =
|
||||
PhysicalDeviceDynamicRenderingFeatures::default().dynamic_rendering(true);
|
||||
|
||||
dynamic_rendering.p_next = device_create_info.p_next as _;
|
||||
device_create_info.p_next = (&mut dynamic_rendering) as *const _ as *const c_void;
|
||||
|
||||
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 +341,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 +381,7 @@ impl WlxGraphics {
|
||||
device,
|
||||
queue,
|
||||
native_format: Format::R8G8B8A8_UNORM,
|
||||
texture_filtering,
|
||||
memory_allocator,
|
||||
command_buffer_allocator,
|
||||
descriptor_set_allocator,
|
||||
@@ -380,6 +401,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 +418,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 +435,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,13 +477,19 @@ 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 {
|
||||
enabled_extensions: my_extensions,
|
||||
enabled_features: Features {
|
||||
enabled_features: DeviceFeatures {
|
||||
dynamic_rendering: true,
|
||||
..Features::empty()
|
||||
..DeviceFeatures::empty()
|
||||
},
|
||||
queue_create_infos: vec![QueueCreateInfo {
|
||||
queue_family_index,
|
||||
@@ -468,6 +499,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 +533,7 @@ impl WlxGraphics {
|
||||
queue,
|
||||
memory_allocator,
|
||||
native_format: Format::R8G8B8A8_UNORM,
|
||||
texture_filtering,
|
||||
command_buffer_allocator,
|
||||
descriptor_set_allocator,
|
||||
quad_indices,
|
||||
@@ -511,9 +552,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 +568,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);
|
||||
@@ -580,9 +625,9 @@ impl WlxGraphics {
|
||||
physical_device,
|
||||
DeviceCreateInfo {
|
||||
enabled_extensions: my_extensions,
|
||||
enabled_features: Features {
|
||||
enabled_features: DeviceFeatures {
|
||||
dynamic_rendering: true,
|
||||
..Features::empty()
|
||||
..DeviceFeatures::empty()
|
||||
},
|
||||
queue_create_infos: vec![QueueCreateInfo {
|
||||
queue_family_index,
|
||||
@@ -595,7 +640,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 +669,7 @@ impl WlxGraphics {
|
||||
queue,
|
||||
memory_allocator,
|
||||
native_format,
|
||||
texture_filtering: Filter::Linear,
|
||||
command_buffer_allocator,
|
||||
descriptor_set_allocator,
|
||||
quad_indices,
|
||||
@@ -988,10 +1034,7 @@ impl WlxGraphics {
|
||||
(fns.v1_0.queue_submit)(
|
||||
self.queue.handle(),
|
||||
1,
|
||||
[SubmitInfo::builder()
|
||||
.command_buffers(&[command_buffer.handle()])
|
||||
.build()]
|
||||
.as_ptr(),
|
||||
[SubmitInfo::default().command_buffers(&[command_buffer.handle()])].as_ptr(),
|
||||
fence.handle(),
|
||||
)
|
||||
}
|
||||
@@ -1042,7 +1085,7 @@ impl WlxCommandBuffer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn texture2d(
|
||||
pub fn texture2d_raw(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
@@ -1122,7 +1165,6 @@ pub struct WlxPipelineDynamic {}
|
||||
pub struct WlxPipelineLegacy {
|
||||
pub render_pass: Arc<RenderPass>,
|
||||
pub framebuffer: Arc<Framebuffer>,
|
||||
pub view: Arc<ImageView>,
|
||||
}
|
||||
|
||||
pub struct WlxPipeline<D> {
|
||||
@@ -1144,7 +1186,7 @@ impl WlxPipeline<WlxPipelineDynamic> {
|
||||
let vep = vert.entry_point("main").unwrap(); // want panic
|
||||
let fep = frag.entry_point("main").unwrap(); // want panic
|
||||
|
||||
let vertex_input_state = Vert2Uv::per_vertex().definition(&vep.info().input_interface)?;
|
||||
let vertex_input_state = Vert2Uv::per_vertex().definition(&vep)?;
|
||||
|
||||
let stages = smallvec![
|
||||
vulkano::pipeline::PipelineShaderStageCreateInfo::new(vep),
|
||||
@@ -1301,7 +1343,7 @@ impl WlxPipeline<WlxPipelineLegacy> {
|
||||
let vep = vert.entry_point("main").unwrap(); // want panic
|
||||
let fep = frag.entry_point("main").unwrap(); // want panic
|
||||
|
||||
let vertex_input_state = Vert2Uv::per_vertex().definition(&vep.info().input_interface)?;
|
||||
let vertex_input_state = Vert2Uv::per_vertex().definition(&vep)?;
|
||||
|
||||
let stages = smallvec![
|
||||
vulkano::pipeline::PipelineShaderStageCreateInfo::new(vep),
|
||||
@@ -1356,7 +1398,6 @@ impl WlxPipeline<WlxPipelineLegacy> {
|
||||
data: WlxPipelineLegacy {
|
||||
render_pass,
|
||||
framebuffer,
|
||||
view: render_target,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1616,3 +1657,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
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
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]
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -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
@@ -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,
|
||||
|
||||
@@ -47,6 +47,7 @@ pub enum HighlightTest {
|
||||
AutoRealign,
|
||||
NotificationSounds,
|
||||
Notifications,
|
||||
RorateLock,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -55,6 +56,7 @@ pub enum SystemAction {
|
||||
ToggleAutoRealign,
|
||||
ToggleNotificationSounds,
|
||||
ToggleNotifications,
|
||||
ToggleRotateLock,
|
||||
PlayspaceResetOffset,
|
||||
PlayspaceFixFloor,
|
||||
RecalculateExtent,
|
||||
@@ -134,6 +136,9 @@ pub enum ButtonAction {
|
||||
channel: ColorChannel,
|
||||
delta: f32,
|
||||
},
|
||||
DragMultiplier {
|
||||
delta: f32,
|
||||
},
|
||||
System {
|
||||
action: SystemAction,
|
||||
},
|
||||
@@ -295,6 +300,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 {
|
||||
@@ -330,6 +336,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 +372,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));
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -242,7 +242,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 +256,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) {
|
||||
@@ -533,7 +536,6 @@ pub fn get_key_type(key: VirtualKey) -> KeyType {
|
||||
}
|
||||
|
||||
pub struct XkbKeymap {
|
||||
pub context: xkb::Context,
|
||||
pub keymap: xkb::Keymap,
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
@@ -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 })
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 145 KiB |
+15
-6
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+19
-13
@@ -10,7 +10,10 @@ use crate::{
|
||||
overlay::{OverlayData, OverlayState},
|
||||
},
|
||||
config::{self, ConfigType},
|
||||
gui::{color_parse, CanvasBuilder, Control, KeyCapType},
|
||||
gui::{
|
||||
canvas::{builder::CanvasBuilder, control::Control},
|
||||
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);
|
||||
@@ -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 {
|
||||
|
||||
+30
-25
@@ -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 {
|
||||
|
||||
+76
-29
@@ -15,7 +15,7 @@ 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,
|
||||
},
|
||||
WlxCapture,
|
||||
@@ -26,9 +26,12 @@ use {
|
||||
crate::config_io,
|
||||
std::error::Error,
|
||||
std::{ops::Deref, path::PathBuf},
|
||||
wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireStream},
|
||||
wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture},
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "x11", feature = "pipewire"))]
|
||||
use wlx_capture::pipewire::PipewireStream;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
use {
|
||||
crate::config::AStrMapExt,
|
||||
@@ -49,7 +52,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 +217,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 +232,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 +283,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 +294,7 @@ impl ScreenRenderer {
|
||||
capture,
|
||||
pipeline: None,
|
||||
last_view: None,
|
||||
extent: [0; 3],
|
||||
extent: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +308,7 @@ impl ScreenRenderer {
|
||||
capture: Box::new(capture),
|
||||
pipeline: None,
|
||||
last_view: None,
|
||||
extent: extent_from_res(output.size),
|
||||
extent: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -317,7 +322,7 @@ impl ScreenRenderer {
|
||||
capture: Box::new(capture),
|
||||
pipeline: None,
|
||||
last_view: None,
|
||||
extent: extent_from_res(output.size),
|
||||
extent: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -358,7 +363,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 +378,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 +466,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 +508,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 +530,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 +553,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 +599,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 +613,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 +701,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 {
|
||||
@@ -879,7 +920,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 +1028,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]
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user