diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 582645a7..8eba3098 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -22,25 +22,28 @@ If this is a regression, please mention which version was working previously. **Kernel version**: **VR Runtime**: -- [ ] Monado/WiVRn -- [ ] SteamVR/ALVR +- [ ] Monado +- [ ] WiVRn +- [ ] ALVR +- [ ] SteamLink +- [ ] SteamVR (not SteamLink) **GPU models and driver versions**: ## Overlay Logs - diff --git a/.github/workflows/build-all-features.yml b/.github/workflows/build-all-features.yml index ea748df3..81267f63 100644 --- a/.github/workflows/build-all-features.yml +++ b/.github/workflows/build-all-features.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index 826fc778..4776d417 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -1,7 +1,6 @@ name: Build AppImage on: - workflow_dispatch: push: branches: - 'main' diff --git a/.github/workflows/build-default.yml b/.github/workflows/build-default.yml index a20e5dfa..3ad4d140 100644 --- a/.github/workflows/build-default.yml +++ b/.github/workflows/build-default.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-wayland-openvr.yml b/.github/workflows/build-wayland-openvr.yml index 9b540dc5..dd3beb57 100644 --- a/.github/workflows/build-wayland-openvr.yml +++ b/.github/workflows/build-wayland-openvr.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-wayland-openxr.yml b/.github/workflows/build-wayland-openxr.yml index 42a5c33c..9ff5c9d4 100644 --- a/.github/workflows/build-wayland-openxr.yml +++ b/.github/workflows/build-wayland-openxr.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-x11-openvr.yml b/.github/workflows/build-x11-openvr.yml index cacd5e5c..2d54b81f 100644 --- a/.github/workflows/build-x11-openvr.yml +++ b/.github/workflows/build-x11-openvr.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/build-x11-openxr.yml b/.github/workflows/build-x11-openxr.yml index 7e045dee..697f0806 100644 --- a/.github/workflows/build-x11-openxr.yml +++ b/.github/workflows/build-x11-openxr.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 3225e372..01286bc0 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -13,7 +13,7 @@ env: jobs: make_release: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 defaults: run: working-directory: ./wayvr diff --git a/.github/workflows/scripts/appimage_prepare_env.sh b/.github/workflows/scripts/appimage_prepare_env.sh index 7b7f14f3..857d81c7 100755 --- a/.github/workflows/scripts/appimage_prepare_env.sh +++ b/.github/workflows/scripts/appimage_prepare_env.sh @@ -1,7 +1,5 @@ #!/bin/sh -sudo add-apt-repository -syn universe -sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596 sudo apt-get update sudo apt-get install -y fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-dev libxkbcommon-x11-0 libxkbcommon-x11-dev libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1 libwayland-dev libegl-dev libxcb-glx0 libxcb-glx0-dev rustup update diff --git a/Cargo.lock b/Cargo.lock index 89da8af5..0be28f52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,17 +33,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - [[package]] name = "ahash" version = "0.8.12" @@ -154,6 +143,16 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width 0.2.2", +] + [[package]] name = "anstream" version = "0.6.21" @@ -206,9 +205,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "appendlist" @@ -244,27 +243,6 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -[[package]] -name = "arboard" -version = "3.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" -dependencies = [ - "clipboard-win", - "image", - "log", - "objc2 0.6.3", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "wl-clipboard-rs", - "x11rb", -] - [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -697,12 +675,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - [[package]] name = "bcdec_rs" version = "0.2.0" @@ -715,6 +687,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ + "annotate-snippets", "bitflags 2.10.0", "cexpr", "clang-sys", @@ -838,37 +811,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "cached-path" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e79c94134331f0107c9f5b7caaf15f118ec91de67dcd3fa1145921e9fc27fa" -dependencies = [ - "flate2", - "fs2", - "glob", - "infer", - "log", - "rand 0.10.0", - "reqwest", - "serde", - "serde_json", - "sha2", - "tar", - "tempfile", - "thiserror 2.0.17", - "zip", -] - [[package]] name = "calloop" version = "0.13.0" @@ -947,22 +889,11 @@ dependencies = [ "nom 7.1.3", ] -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - [[package]] name = "cfg-expr" -version = "0.15.8" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" dependencies = [ "smallvec", "target-lexicon", @@ -990,17 +921,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.0", -] - [[package]] name = "chrono" version = "0.4.42" @@ -1025,16 +945,6 @@ dependencies = [ "phf", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -1086,15 +996,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - [[package]] name = "cmake" version = "0.1.57" @@ -1161,7 +1062,7 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml 0.9.10+spec-1.1.0", + "toml", "winnow", "yaml-rust2", ] @@ -1186,12 +1087,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - [[package]] name = "convert_case" version = "0.4.0" @@ -1212,9 +1107,6 @@ name = "cookie-factory" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] [[package]] name = "core-foundation" @@ -1347,15 +1239,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -1548,6 +1431,7 @@ dependencies = [ "smol", "smol-hyper", "strum", + "uuid", "wayvr-ipc", "wgui", "wlx-common", @@ -1589,22 +1473,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "deflate64" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "pem-rfc7468", - "zeroize", -] - [[package]] name = "deranged" version = "0.5.5" @@ -1635,7 +1503,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] @@ -1887,12 +1754,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - [[package]] name = "etagere" version = "0.2.15" @@ -1954,26 +1815,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.113", -] - [[package]] name = "fdeflate" version = "0.3.7" @@ -1983,17 +1824,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - [[package]] name = "find-msvc-tools" version = "0.1.6" @@ -2019,7 +1849,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", ] @@ -2130,22 +1959,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fst" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" - [[package]] name = "futures" version = "0.3.31" @@ -2298,26 +2111,10 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.0", - "wasip2", - "wasip3", - "wasm-bindgen", -] - [[package]] name = "gif" version = "0.14.1" @@ -2474,21 +2271,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "hmac-sha256" -version = "1.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" - [[package]] name = "hound" version = "3.5.1" @@ -2562,45 +2344,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - [[package]] name = "iana-time-zone" version = "0.1.64" @@ -2706,12 +2449,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2789,9 +2526,8 @@ dependencies = [ "png 0.18.0", "ravif", "rayon", - "tiff", - "zune-core 0.5.0", - "zune-jpeg 0.5.8", + "zune-core", + "zune-jpeg", ] [[package]] @@ -2848,8 +2584,6 @@ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", - "serde", - "serde_core", ] [[package]] @@ -2858,24 +2592,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "input-linux" version = "0.7.1" @@ -2922,22 +2638,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -3104,24 +2804,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lebe" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - [[package]] name = "libc" version = "0.2.179" @@ -3201,39 +2889,29 @@ dependencies = [ [[package]] name = "libspa" -version = "0.8.0" -source = "git+https://gitlab.freedesktop.org/galister/pipewire-rs.git?rev=6ef2cd1af41e5e8844c63c37ea9a01f0758079b0#6ef2cd1af41e5e8844c63c37ea9a01f0758079b0" +version = "0.9.2" +source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs.git?rev=670b0aabf04263ece5dc1e752e5d64aeae3056e8#670b0aabf04263ece5dc1e752e5d64aeae3056e8" dependencies = [ "bitflags 2.10.0", "cc", - "convert_case 0.6.0", "cookie-factory", "libc", "libspa-sys", - "nix 0.27.1", - "nom 7.1.3", + "nom 8.0.0", + "rustix 1.1.3", "system-deps", ] [[package]] name = "libspa-sys" -version = "0.8.0" -source = "git+https://gitlab.freedesktop.org/galister/pipewire-rs.git?rev=6ef2cd1af41e5e8844c63c37ea9a01f0758079b0#6ef2cd1af41e5e8844c63c37ea9a01f0758079b0" +version = "0.9.2" +source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs.git?rev=670b0aabf04263ece5dc1e752e5d64aeae3056e8#670b0aabf04263ece5dc1e752e5d64aeae3056e8" dependencies = [ "bindgen", "cc", "system-deps", ] -[[package]] -name = "libz-rs-sys" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" -dependencies = [ - "zlib-rs", -] - [[package]] name = "linebender_resource_handle" version = "0.1.1" @@ -3310,21 +2988,6 @@ dependencies = [ "hashbrown 0.16.1", ] -[[package]] -name = "lzma-rust2" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" - -[[package]] -name = "lzma-rust2" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" -dependencies = [ - "sha2", -] - [[package]] name = "mach2" version = "0.4.3" @@ -3340,17 +3003,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.4.13", -] - -[[package]] -name = "matrixmultiply" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", + "regex-automata", ] [[package]] @@ -3369,16 +3022,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memmap" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "memmap2" version = "0.9.9" @@ -3495,21 +3138,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ndarray" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "portable-atomic", - "portable-atomic-util", - "rawpointer", -] - [[package]] name = "ndk" version = "0.9.0" @@ -3546,17 +3174,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.29.0" @@ -3644,15 +3261,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.2.0" @@ -3762,18 +3370,6 @@ dependencies = [ "objc2-quartz-core 0.2.2", ] -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-graphics", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-audio-toolbox" version = "0.3.2" @@ -3858,19 +3454,6 @@ dependencies = [ "objc2 0.6.3", ] -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.10.0", - "dispatch2", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-io-surface", -] - [[package]] name = "objc2-core-image" version = "0.2.2" @@ -3935,17 +3518,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", -] - [[package]] name = "objc2-link-presentation" version = "0.2.2" @@ -3954,7 +3526,7 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2", "objc2 0.5.2", - "objc2-app-kit 0.2.2", + "objc2-app-kit", "objc2-foundation 0.2.2", ] @@ -4179,40 +3751,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "ort" -version = "2.0.0-rc.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" -dependencies = [ - "ndarray", - "ort-sys", - "smallvec", - "tracing", - "ureq", -] - -[[package]] -name = "ort-sys" -version = "2.0.0-rc.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" -dependencies = [ - "hmac-sha256", - "lzma-rust2 0.15.7", - "ureq", -] - -[[package]] -name = "os_pipe" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "ouroboros" version = "0.18.5" @@ -4320,25 +3858,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -4468,24 +3987,21 @@ dependencies = [ [[package]] name = "pipewire" -version = "0.8.0" -source = "git+https://gitlab.freedesktop.org/galister/pipewire-rs.git?rev=6ef2cd1af41e5e8844c63c37ea9a01f0758079b0#6ef2cd1af41e5e8844c63c37ea9a01f0758079b0" +version = "0.9.2" +source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs.git?rev=670b0aabf04263ece5dc1e752e5d64aeae3056e8#670b0aabf04263ece5dc1e752e5d64aeae3056e8" dependencies = [ - "anyhow", "bitflags 2.10.0", "libc", "libspa", "libspa-sys", - "nix 0.27.1", - "once_cell", "pipewire-sys", - "thiserror 1.0.69", + "rustix 1.1.3", ] [[package]] name = "pipewire-sys" -version = "0.8.0" -source = "git+https://gitlab.freedesktop.org/galister/pipewire-rs.git?rev=6ef2cd1af41e5e8844c63c37ea9a01f0758079b0#6ef2cd1af41e5e8844c63c37ea9a01f0758079b0" +version = "0.9.2" +source = "git+https://gitlab.freedesktop.org/pipewire/pipewire-rs.git?rev=670b0aabf04263ece5dc1e752e5d64aeae3056e8#670b0aabf04263ece5dc1e752e5d64aeae3056e8" dependencies = [ "bindgen", "libspa-sys", @@ -4568,12 +4084,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppmd-rust" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4599,7 +4109,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit", ] [[package]] @@ -4779,12 +4289,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "rand" version = "0.7.3" @@ -4820,17 +4324,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rand" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.0", -] - [[package]] name = "rand_chacha" version = "0.2.2" @@ -4888,12 +4381,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_core" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" - [[package]] name = "rand_hc" version = "0.2.0" @@ -4995,12 +4482,6 @@ dependencies = [ "objc2-quartz-core 0.3.2", ] -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - [[package]] name = "rayon" version = "1.11.0" @@ -5067,24 +4548,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.13", - "regex-syntax 0.8.8", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "fst", - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -5095,59 +4566,15 @@ checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.8", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "native-tls", - "percent-encoding", - "pin-project-lite", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "resvg" version = "0.45.1" @@ -5330,15 +4757,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "zeroize", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -5521,15 +4939,6 @@ dependencies = [ "syn 2.0.113", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.0.4" @@ -5539,18 +4948,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -5564,17 +4961,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest", -] - [[package]] name = "sha2" version = "0.10.9" @@ -5582,7 +4968,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -5870,17 +5256,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "socks" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" -dependencies = [ - "byteorder", - "libc", - "winapi", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5929,28 +5304,6 @@ dependencies = [ "syn 2.0.113", ] -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "super-swipe-type" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22126517bdf94f41b3d0af625fdf69b5ff131b7dd19a59866d3c00a56072af88" -dependencies = [ - "anyhow", - "cached-path", - "fst", - "memmap", - "ort", - "regex", - "regex-automata 0.1.10", - "vector2", -] - [[package]] name = "svg_fmt" version = "0.4.5" @@ -6049,15 +5402,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -6094,14 +5438,14 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.2.2" +version = "7.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.23", + "toml", "version-compare", ] @@ -6117,22 +5461,11 @@ dependencies = [ "slotmap", ] -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tempfile" @@ -6205,20 +5538,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "tiff" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" -dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg 0.4.21", -] - [[package]] name = "time" version = "0.3.47" @@ -6227,7 +5546,6 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "js-sys", "num-conv", "powerfmt", "serde_core", @@ -6337,16 +5655,6 @@ dependencies = [ "syn 2.0.113", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -6360,40 +5668,21 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - [[package]] name = "toml" version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ + "indexmap 2.12.1", "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", + "serde_spanned", + "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -6403,19 +5692,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.12.1", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "winnow", -] - [[package]] name = "toml_edit" version = "0.23.10+spec-1.0.0" @@ -6423,7 +5699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap 2.12.1", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "winnow", ] @@ -6438,49 +5714,10 @@ dependencies = [ ] [[package]] -name = "tower" -version = "0.5.3" +name = "toml_writer" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tracing" @@ -6535,7 +5772,7 @@ dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex-automata 0.4.13", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -6544,17 +5781,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tree_magic_mini" -version = "3.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" -dependencies = [ - "memchr", - "nom 8.0.0", - "petgraph", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -6570,12 +5796,6 @@ dependencies = [ "core_maths", ] -[[package]] -name = "typed-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" - [[package]] name = "typeid" version = "1.0.3" @@ -6664,48 +5884,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "ureq" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" -dependencies = [ - "base64", - "der", - "log", - "native-tls", - "percent-encoding", - "rustls-pki-types", - "socks", - "ureq-proto", - "utf-8", - "webpki-root-certs", -] - -[[package]] -name = "ureq-proto" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" -dependencies = [ - "base64", - "http", - "httparse", - "log", -] - [[package]] name = "url" version = "2.5.7" @@ -6740,12 +5924,6 @@ dependencies = [ "xmlwriter", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -6794,12 +5972,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vector2" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f562f8d4f41867568374e1a63bd12914795f558319f9bf9b0582c4d96a8de04" - [[package]] name = "version-compare" version = "0.2.1" @@ -6913,16 +6085,7 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen 0.46.0", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -6983,40 +6146,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.12.1", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.10.0", - "hashbrown 0.15.5", - "indexmap 2.12.1", - "semver", -] - [[package]] name = "wayland-backend" version = "0.3.12" @@ -7173,7 +6302,6 @@ name = "wayvr" version = "26.2.1" dependencies = [ "anyhow", - "arboard", "ash", "bytes", "chrono", @@ -7213,7 +6341,6 @@ dependencies = [ "smithay", "smol", "strum", - "super-swipe-type", "sysinfo", "thiserror 2.0.17", "tracing", @@ -7282,15 +6409,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "weezl" version = "0.1.12" @@ -7793,7 +6911,7 @@ dependencies = [ "memmap2", "ndk", "objc2 0.5.2", - "objc2-app-kit 0.2.2", + "objc2-app-kit", "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", @@ -7836,112 +6954,6 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.12.1", - "prettyplease", - "syn 2.0.113", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.113", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.10.0", - "indexmap 2.12.1", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.12.1", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "wl-clipboard-rs" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" -dependencies = [ - "libc", - "log", - "os_pipe", - "rustix 1.1.3", - "thiserror 2.0.17", - "tree_magic_mini", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-wlr", -] - [[package]] name = "wlx-capture" version = "0.6.0" @@ -8020,16 +7032,6 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.3", -] - [[package]] name = "xcb" version = "1.7.0" @@ -8272,26 +7274,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.113", -] - [[package]] name = "zerotrie" version = "0.2.3" @@ -8325,91 +7307,12 @@ dependencies = [ "syn 2.0.113", ] -[[package]] -name = "zip" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b680f2a0cd479b4cff6e1233c483fdead418106eae419dc60200ae9850f6d004" -dependencies = [ - "aes", - "bzip2", - "constant_time_eq", - "crc32fast", - "deflate64", - "flate2", - "getrandom 0.4.2", - "hmac", - "indexmap 2.12.1", - "lzma-rust2 0.16.2", - "memchr", - "pbkdf2", - "ppmd-rust", - "sha1", - "time", - "typed-path", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zlib-rs" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" - [[package]] name = "zmij" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30e0d8dffbae3d840f64bda38e28391faef673a7b5a6017840f2a106c8145868" -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - [[package]] name = "zune-core" version = "0.5.0" @@ -8425,22 +7328,13 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - [[package]] name = "zune-jpeg" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" dependencies = [ - "zune-core 0.5.0", + "zune-core", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5488eca7..1550b9d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,42 @@ +[workspace] +resolver = "3" +members = [ + "dash-frontend", + "scripts/prost_build", + "uidev", + "wayvr", + "wayvr-ipc", + "wayvrctl", + "wgui", + "wlx-capture", + "wlx-common", +] + +[workspace.dependencies] +anyhow = "1.0.100" +clap = { version = "4.5.53", features = ["derive"] } +glam = { version = "0.30.9", features = ["mint", "serde"] } +idmap = "0.2.2" +idmap-derive = "0.2.22" +log = "0.4.29" +regex = "1.12.2" +rust-embed = "8.9.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1.0.145" +slotmap = "1.1.1" +smol = "2.0.2" +strum = { version = "0.27.2", features = ["derive"] } +uuid = { version = "1.19.0", features = ["fast-rng", "v4", "serde"] } +vulkano = { version = "0.35.2", default-features = false, features = [ + "macros", +] } +vulkano-shaders = "0.35.0" +wayland-client = { version = "0.31.11" } +xdg = "3.0.0" + +[patch.crates-io] +vulkano = { git = "https://github.com/galister/vulkano.git", rev = "cf7f92867928a56ce16b376037c1120f2b167678" } + [profile.dev] opt-level = 1 debug = true @@ -19,41 +58,3 @@ incremental = true [profile.release-with-debug] inherits = "release" debug = true - -[workspace] -members = [ - "uidev", - "wgui", - "wlx-common", - "wayvr", - "wlx-capture", - "dash-frontend", - "wayvr-ipc", - "wayvrctl", - "scripts/prost_build", -] -resolver = "3" - -[patch.crates-io] -vulkano = { git = "https://github.com/galister/vulkano.git", rev = "cf7f92867928a56ce16b376037c1120f2b167678" } - -[workspace.dependencies] -anyhow = "1.0.100" -smol = "2.0.2" -glam = { version = "0.30.9", features = ["mint", "serde"] } -clap = { version = "4.5.53", features = ["derive"] } -xdg = "3.0.0" -idmap = "0.2.2" -idmap-derive = "0.2.22" -log = "0.4.29" -regex = "1.12.2" -rust-embed = "8.9.0" -serde = { version = "1", features = ["derive"] } -serde_json = "1.0.145" -slotmap = "1.1.1" -strum = { version = "0.27.2", features = ["derive"] } -vulkano = { version = "0.35.2", default-features = false, features = [ - "macros", -] } -vulkano-shaders = "0.35.0" -wayland-client = { version = "0.31.11" } diff --git a/dash-frontend/Cargo.toml b/dash-frontend/Cargo.toml index 06f1b010..0f2a839f 100644 --- a/dash-frontend/Cargo.toml +++ b/dash-frontend/Cargo.toml @@ -8,26 +8,25 @@ authors = ["galister", "oo8dev"] repository = "https://github.com/wlx-team/wayvr" [dependencies] -wayvr-ipc = { path = "../wayvr-ipc", default-features = false } -wgui = { path = "../wgui/" } -wlx-common = { path = "../wlx-common" } - anyhow.workspace = true +async-native-tls = "0.5.0" +chrono = "0.4.42" glam = { workspace = true, features = ["mint", "serde"] } +http-body-util = "0.1.3" +hyper = { version = "1.8.1", features = ["client", "http1", "http2"] } +keyvalues-parser = { git = "https://github.com/CosmicHorrorDev/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" } log.workspace = true -xdg.workspace = true rust-embed.workspace = true serde = { workspace = true, features = ["rc"] } serde_json.workspace = true -strum.workspace = true - -chrono = "0.4.42" -keyvalues-parser = { git = "https://github.com/CosmicHorrorDev/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" } smol = { workspace = true } -hyper = { version = "1.8.1", features = ["client", "http1", "http2"] } -http-body-util = "0.1.3" -async-native-tls = "0.5.0" smol-hyper = "0.1.1" +strum.workspace = true +uuid.workspace = true +wayvr-ipc = { path = "../wayvr-ipc", default-features = false } +wgui = { path = "../wgui/" } +wlx-common = { path = "../wlx-common" } +xdg.workspace = true [features] default = ["monado"] diff --git a/dash-frontend/assets/dashboard/builtin-skybox-preview.jpg b/dash-frontend/assets/dashboard/builtin-skybox-preview.jpg new file mode 100644 index 00000000..7ca3e583 Binary files /dev/null and b/dash-frontend/assets/dashboard/builtin-skybox-preview.jpg differ diff --git a/dash-frontend/assets/dashboard/check.svg b/dash-frontend/assets/dashboard/check.svg new file mode 100644 index 00000000..d4ba7af0 --- /dev/null +++ b/dash-frontend/assets/dashboard/check.svg @@ -0,0 +1 @@ + diff --git a/dash-frontend/assets/dashboard/download.svg b/dash-frontend/assets/dashboard/download.svg new file mode 100644 index 00000000..f446969b --- /dev/null +++ b/dash-frontend/assets/dashboard/download.svg @@ -0,0 +1 @@ + diff --git a/dash-frontend/assets/dashboard/error.svg b/dash-frontend/assets/dashboard/error.svg new file mode 100644 index 00000000..fa2fe4b7 --- /dev/null +++ b/dash-frontend/assets/dashboard/error.svg @@ -0,0 +1 @@ + diff --git a/dash-frontend/assets/dashboard/loading.svg b/dash-frontend/assets/dashboard/loading.svg new file mode 100644 index 00000000..1c800c12 --- /dev/null +++ b/dash-frontend/assets/dashboard/loading.svg @@ -0,0 +1 @@ + diff --git a/dash-frontend/assets/dashboard/trash.svg b/dash-frontend/assets/dashboard/trash.svg new file mode 100644 index 00000000..914fd402 --- /dev/null +++ b/dash-frontend/assets/dashboard/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dash-frontend/assets/gui/dashboard.xml b/dash-frontend/assets/gui/dashboard.xml index 7d5a9a4c..8f2b05f4 100644 --- a/dash-frontend/assets/gui/dashboard.xml +++ b/dash-frontend/assets/gui/dashboard.xml @@ -86,7 +86,8 @@ round="8" flex_grow="1" width="100%" - overflow_y="scroll" + overflow="scroll" + position="relative" > + + + + diff --git a/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml b/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml index 3d918b82..c99cbf21 100644 --- a/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml +++ b/dash-frontend/assets/gui/tab/monado_tab_general_settings.xml @@ -29,7 +29,7 @@
-
diff --git a/dash-frontend/assets/gui/tab/settings.xml b/dash-frontend/assets/gui/tab/settings.xml index 9ca21283..61583d75 100644 --- a/dash-frontend/assets/gui/tab/settings.xml +++ b/dash-frontend/assets/gui/tab/settings.xml @@ -48,6 +48,7 @@
+ @@ -59,4 +60,4 @@
- \ No newline at end of file + diff --git a/dash-frontend/assets/gui/view/dialog_box.xml b/dash-frontend/assets/gui/view/dialog_box.xml new file mode 100644 index 00000000..0c3f87cf --- /dev/null +++ b/dash-frontend/assets/gui/view/dialog_box.xml @@ -0,0 +1,15 @@ + + + + +
+
+
+
diff --git a/dash-frontend/assets/gui/view/download_file.xml b/dash-frontend/assets/gui/view/download_file.xml new file mode 100644 index 00000000..63813dd7 --- /dev/null +++ b/dash-frontend/assets/gui/view/download_file.xml @@ -0,0 +1,22 @@ + + + + + + +
+
+
+ + diff --git a/dash-frontend/assets/gui/view/popup_window.xml b/dash-frontend/assets/gui/view/popup_window.xml index 1f1f6fed..e373a2f4 100644 --- a/dash-frontend/assets/gui/view/popup_window.xml +++ b/dash-frontend/assets/gui/view/popup_window.xml @@ -9,6 +9,7 @@ width="100%" height="100%" flex_direction="column" + position="absolute" > - - + position="relative" + > +
+ +
-
\ No newline at end of file + diff --git a/dash-frontend/assets/gui/view/remote_skymap_downloader.xml b/dash-frontend/assets/gui/view/remote_skymap_downloader.xml new file mode 100644 index 00000000..c6e386ae --- /dev/null +++ b/dash-frontend/assets/gui/view/remote_skymap_downloader.xml @@ -0,0 +1,36 @@ + + + + + + + + +
+
+
+ +
+ +
+
+
+
diff --git a/dash-frontend/assets/gui/view/remote_skymap_list.xml b/dash-frontend/assets/gui/view/remote_skymap_list.xml new file mode 100644 index 00000000..e2f3603a --- /dev/null +++ b/dash-frontend/assets/gui/view/remote_skymap_list.xml @@ -0,0 +1,7 @@ + + +
+ +
+
+
diff --git a/dash-frontend/assets/gui/view/skymap_list.xml b/dash-frontend/assets/gui/view/skymap_list.xml new file mode 100644 index 00000000..b32b0ec1 --- /dev/null +++ b/dash-frontend/assets/gui/view/skymap_list.xml @@ -0,0 +1,11 @@ + + +
+
+
+
+
+ + diff --git a/dash-frontend/assets/gui/view/skymap_list_cell.xml b/dash-frontend/assets/gui/view/skymap_list_cell.xml new file mode 100644 index 00000000..93f32e8e --- /dev/null +++ b/dash-frontend/assets/gui/view/skymap_list_cell.xml @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json index 7d5e2e05..7b1b1626 100644 --- a/dash-frontend/assets/lang/en.json +++ b/dash-frontend/assets/lang/en.json @@ -38,6 +38,8 @@ "BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Do not block input when watch is hovered", "BLOCK_POSES_ON_KBD_INTERACTION": "Block poses when interacting with keyboard", "BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blocks the game from receiving poses when the keyboard is hovered and 'Block game input' is enabled", + "BROWSE_ONLINE_CATALOG": "Browse online catalog...", + "BROWSE_SKYMAPS": "Browse skymaps", "CAPTURE_METHOD": "Wayland screen capture", "CAPTURE_METHOD_HELP": "Try changing this if you are\nexperiencing black or glitchy screens", "COLOR_KEYING": "Color keying", @@ -64,14 +66,13 @@ "KEYBOARD_MIDDLE_CLICK": "Keyboard middle click", "KEYBOARD_MIDDLE_CLICK_HELP": "Modifier to use when typing\nwith purple laser", "KEYBOARD_SOUND_ENABLED": "Keyboard sounds", - "KEYBOARD_SWIPE_TO_TYPE_ENABLED": "Keyboard swipe to type", - "KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP": "Only works for English! Results will be drastically worse on non qwerty keyboards.", "LANGUAGE": "Language", "LEFT_HANDED_MOUSE": "Left-handed mouse", "LEFT_HANDED_MOUSE_HELP": "Use this if mouse buttons are swapped", "LONG_PRESS_DURATION": "Long press duration", "LOOK_AND_FEEL": "Look & Feel", "MISC": "Miscellaneous", + "NO_SKYMAPS_FOUND": "No skymaps found", "NOTIFICATIONS_ENABLED": "Enable notifications", "NOTIFICATIONS_SOUND_ENABLED": "Notification sounds", "OPAQUE_BACKGROUND": "Opaque background", @@ -98,7 +99,10 @@ "SCREEN_RENDER_DOWN": "Render screen at lower resolution", "SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens", "SCROLL_SPEED": "Scroll speed", + "SELECT_VARIANT": "Select variant", "SETS_ON_WATCH": "Sets on watch", + "SKYBOX": "Skybox", + "SKYMAP_ALREADY_DOWNLOADED": "This skymap is already downloaded. Select desired action.", "SPACE_DRAG_MULTIPLIER": "Space drag multiplier", "SPACE_DRAG_UNLOCKED": "Allow space drag on all axes", "SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes", @@ -136,8 +140,12 @@ "VOLUME": "Volume" }, "CLOSE_WINDOW": "Close window", + "CREATION_DATE": "Creation date", "DEBUG_INFO": "Debug info", "DISPLAY_BRIGHTNESS": "Display brightness", + "DOWNLOADER": "Downloader", + "DOWNLOAD_AGAIN": "Download again", + "DOWNLOADING_FILE": "Downloading file...", "FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:", "GAME_LAUNCHED": "Game launched", "GAME_LIST": { @@ -151,7 +159,9 @@ "HELLO_USER": "Hello, {USER}!", "HIDE": "Hide", "HOME_SCREEN": "Home", + "MODIFICATION_DATE": "Modification date", "MONADO_RUNTIME": "Monado runtime", + "LOADING": "Loading...", "POPUP_ADD_DISPLAY": { "RESOLUTION": "Resolution" }, @@ -162,8 +172,12 @@ "PROCESS_LIST": "Process list", "REFRESH": "Refresh", "REMOVE": "Remove", + "APPLY": "Apply", + "RELOAD_FROM_DISK": "Reload from disk", "SETTINGS": "Settings", "SHOW": "Show", + "TARGET_PATH": "Target path", "TERMINATE_PROCESS": "Terminate process", + "VERSION": "Version", "WIDTH": "Width" } diff --git a/dash-frontend/src/frontend.rs b/dash-frontend/src/frontend.rs index 26a1970c..192cbc06 100644 --- a/dash-frontend/src/frontend.rs +++ b/dash-frontend/src/frontend.rs @@ -28,7 +28,7 @@ use crate::{ assets, tab::{Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, settings::TabSettings}, util::{ - popup_manager::{MountPopupParams, PopupManager, PopupManagerParams}, + popup_manager::{MountPopupOnceParams, PopupManager, PopupManagerParams}, toast_manager::ToastManager, }, views, @@ -101,7 +101,7 @@ pub enum FrontendTask { SetTab(TabType), RefreshClock, RefreshBackground, - MountPopup(MountPopupParams), + MountPopupOnce(MountPopupOnceParams), RefreshPopupManager, ShowAudioSettings, UpdateAudioSettingsView, @@ -279,10 +279,8 @@ impl Frontend { } fn update_time(&mut self, data: &mut T) -> anyhow::Result<()> { - let mut c = self.layout.start_common(); - let mut common = c.common(); - { + let mut common = self.layout.common(); let mut label = common .state .widgets @@ -303,27 +301,20 @@ impl Frontend { label.set_text(&mut common, Translation::from_raw_text(&text)); } - c.finish()?; Ok(()) } - fn mount_popup(&mut self, params: MountPopupParams, data: &mut T) -> anyhow::Result<()> { + fn mount_popup_once(&mut self, params: MountPopupOnceParams, data: &mut T) -> anyhow::Result<()> { let config = self.interface.general_config(data); - self.popup_manager.mount_popup( - self.globals.clone(), - &mut self.layout, - self.tasks.clone(), - params, - config, - )?; + self + .popup_manager + .mount_popup_once(&self.globals, &mut self.layout, &self.tasks, params, config)?; Ok(()) } fn refresh_popup_manager(&mut self) -> anyhow::Result<()> { - let mut c = self.layout.start_common(); - self.popup_manager.refresh(c.common().alterables); - c.finish()?; + self.popup_manager.refresh(&mut self.layout.alterables); Ok(()) } @@ -356,7 +347,7 @@ impl Frontend { FrontendTask::SetTab(tab_type) => self.set_tab(params.data, tab_type)?, FrontendTask::RefreshClock => self.update_time(params.data)?, FrontendTask::RefreshBackground => self.update_background(params.data)?, - FrontendTask::MountPopup(popup_params) => self.mount_popup(popup_params, params.data)?, + FrontendTask::MountPopupOnce(popup_params) => self.mount_popup_once(popup_params, params.data)?, FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?, FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?, FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?, @@ -369,8 +360,7 @@ impl Frontend { } fn set_tab_title(&mut self, translation: &str, icon: &str) -> anyhow::Result<()> { - let mut c = self.layout.start_common(); - let mut common = c.common(); + let mut common = self.layout.common(); { let mut label = common @@ -386,12 +376,11 @@ impl Frontend { .widgets .cast_as::(self.widgets.id_sprite_titlebar_icon)?; sprite.set_content( - &mut common, + common.alterables, Some(CustomGlyphData::from_assets(&self.globals, AssetPath::BuiltIn(icon))?), ); } - c.finish()?; Ok(()) } diff --git a/dash-frontend/src/tab/apps.rs b/dash-frontend/src/tab/apps.rs index 7e17ff39..cf093f84 100644 --- a/dash-frontend/src/tab/apps.rs +++ b/dash-frontend/src/tab/apps.rs @@ -9,7 +9,6 @@ use wgui::{ assets::AssetPath, components::button::{ButtonClickCallback, ComponentButton}, globals::WguiGlobals, - i18n::Translation, layout::{WidgetID, WidgetPair}, parser::{Fetchable, ParseDocumentParams, ParserState}, task::Tasks, @@ -17,18 +16,17 @@ use wgui::{ use wlx_common::desktop_finder::DesktopEntry; use crate::{ - frontend::{Frontend, FrontendTask, FrontendTasks}, + frontend::{Frontend, FrontendTasks}, tab::{Tab, TabType}, - util::popup_manager::{MountPopupParams, PopupHandle}, - views::{self, app_launcher}, + util::popup_manager::PopupHolder, + views::{self}, }; -enum Task { - CloseLauncher, -} +#[derive(Clone)] +enum Task {} struct State { - view_launcher: Option<(PopupHandle, views::app_launcher::View)>, + view_launcher: PopupHolder, } pub struct TabApps { @@ -47,21 +45,17 @@ impl Tab for TabApps { } fn update(&mut self, frontend: &mut Frontend, _time_ms: u32, data: &mut T) -> anyhow::Result<()> { - let mut state = self.state.borrow_mut(); + let state = self.state.borrow_mut(); for task in self.tasks.drain() { - match task { - Task::CloseLauncher => state.view_launcher = None, - } + match task {} } - self - .app_list - .tick(frontend, &self.state, &self.tasks, &mut self.parser_state)?; + self.app_list.tick(frontend, &self.state, &mut self.parser_state)?; - if let Some((_, launcher)) = &mut state.view_launcher { - launcher.update(&mut frontend.interface, data)?; - } + state + .view_launcher + .with_view_res(|view| view.update(&mut frontend.interface, data))?; Ok(()) } } @@ -79,40 +73,14 @@ fn on_app_click( globals: WguiGlobals, entry: DesktopEntry, state: Rc>, - tasks: Tasks, ) -> ButtonClickCallback { Rc::new(move |_common, _evt| { - frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams { - title: Translation::from_raw_text(&entry.app_name), - on_content: { - // this is awful - let state = state.clone(); - let entry = entry.clone(); - let globals = globals.clone(); - let frontend_tasks = frontend_tasks.clone(); - let tasks = tasks.clone(); - - Rc::new(move |data| { - let on_launched = { - let tasks = tasks.clone(); - Box::new(move || tasks.push(Task::CloseLauncher)) - }; - - let view = app_launcher::View::new(app_launcher::Params { - entry: entry.clone(), - globals: &globals, - layout: data.layout, - parent_id: data.id_content, - frontend_tasks: &frontend_tasks, - config: data.config, - on_launched, - })?; - - state.borrow_mut().view_launcher = Some((data.handle, view)); - Ok(()) - }) - }, - })); + views::app_launcher::mount_popup( + frontend_tasks.clone(), + globals.clone(), + entry.clone(), + state.borrow_mut().view_launcher.clone(), + ); Ok(()) }) } @@ -129,7 +97,9 @@ impl TabApps { pub fn new(frontend: &mut Frontend, parent_id: WidgetID, data: &mut T) -> anyhow::Result { let globals = frontend.layout.state.globals.clone(); let tasks = Tasks::new(); - let state = Rc::new(RefCell::new(State { view_launcher: None })); + let state = Rc::new(RefCell::new(State { + view_launcher: Default::default(), + })); let parser_state = wgui::parser::parse_from_assets(&doc_params(globals.clone()), &mut frontend.layout, parent_id)?; let app_list_parent = parser_state.fetch_widget(&frontend.layout.state, "app_list_parent")?; @@ -334,7 +304,6 @@ impl AppList { &mut self, frontend: &mut Frontend, state: &Rc>, - tasks: &Tasks, parser_state: &mut ParserState, ) -> anyhow::Result<()> { // load 4 entries for a single frame at most @@ -348,7 +317,6 @@ impl AppList { globals.clone(), entry.clone(), state.clone(), - tasks.clone(), )); } else { break; diff --git a/dash-frontend/src/tab/games.rs b/dash-frontend/src/tab/games.rs index fcdc3cad..514b7c3e 100644 --- a/dash-frontend/src/tab/games.rs +++ b/dash-frontend/src/tab/games.rs @@ -10,7 +10,7 @@ use crate::{ frontend::Frontend, tab::{Tab, TabType}, util::steam_utils::SteamUtils, - views::{game_list, running_games_list}, + views::{ViewTrait, ViewUpdateParams, game_list, running_games_list}, }; pub struct TabGames { @@ -19,7 +19,6 @@ pub struct TabGames { view_game_list: game_list::View, view_running_games_list: running_games_list::View, - steam_utils: SteamUtils, marker: PhantomData, } @@ -28,10 +27,20 @@ impl Tab for TabGames { TabType::Games } - fn update(&mut self, frontend: &mut Frontend, time_ms: u32, _data: &mut T) -> anyhow::Result<()> { - self - .view_game_list - .update(&mut frontend.layout, &mut self.steam_utils, &frontend.executor)?; + fn update(&mut self, frontend: &mut Frontend, time_ms: u32, data: &mut T) -> anyhow::Result<()> { + let mut config_change_kind = None; + + self.view_game_list.update(&mut ViewUpdateParams { + layout: &mut frontend.layout, + executor: &frontend.executor, + general_config: frontend.interface.general_config(data), + config_change_kind: &mut config_change_kind, + })?; + + if let Some(kind) = config_change_kind { + frontend.interface.config_changed(data, kind); + } + self.view_running_games_list.update(&mut frontend.layout, time_ms)?; Ok(()) } @@ -54,16 +63,17 @@ impl TabGames { let game_list_parent = state.get_widget_id("game_list_parent")?; let id_running_games_list_parent = state.get_widget_id("running_games_list_parent")?; + let mut steam_utils = SteamUtils::new()?; + let view_game_list = game_list::View::new(game_list::Params { executor: frontend.executor.clone(), frontend_tasks: frontend.tasks.clone(), globals: globals.clone(), layout: &mut frontend.layout, parent_id: game_list_parent, + steam_utils: &steam_utils, })?; - let mut steam_utils = SteamUtils::new()?; - let view_running_games_list = running_games_list::View::new(running_games_list::Params { globals: globals.clone(), layout: &mut frontend.layout, @@ -77,7 +87,6 @@ impl TabGames { view_game_list, view_running_games_list, marker: PhantomData, - steam_utils, }) } } diff --git a/dash-frontend/src/tab/home.rs b/dash-frontend/src/tab/home.rs index 57e967cd..bb541ee1 100644 --- a/dash-frontend/src/tab/home.rs +++ b/dash-frontend/src/tab/home.rs @@ -59,9 +59,12 @@ impl TabHome { parent_id, )?; - let mut c = frontend.layout.start_common(); - let widget_label = state.fetch_widget(&c.layout.state, "label_hello")?.widget; - configure_label_hello(&mut c.common(), widget_label, frontend.interface.general_config(data)); + let widget_label = state.fetch_widget(&frontend.layout.state, "label_hello")?.widget; + configure_label_hello( + &mut frontend.layout.common(), + widget_label, + frontend.interface.general_config(data), + ); let btn_apps = state.fetch_component_as::("btn_apps")?; let btn_games = state.fetch_component_as::("btn_games")?; diff --git a/dash-frontend/src/tab/monado.rs b/dash-frontend/src/tab/monado.rs index 8efe58de..a9c5332e 100644 --- a/dash-frontend/src/tab/monado.rs +++ b/dash-frontend/src/tab/monado.rs @@ -18,7 +18,7 @@ use wgui::{ }; use wlx_common::{ config::GeneralConfig, - dash_interface::{self, MonadoDumpSessionFrame}, + dash_interface::{self, ConfigChangeKind, MonadoDumpSessionFrame}, }; use crate::{ @@ -175,7 +175,9 @@ impl Tab for TabMonado { Task::GeneralSettingsChromaUpdate => { if let Subtab::GeneralSettings(tab) = &mut self.subtab { tab.chroma_update(frontend.interface.general_config(data)); - frontend.interface.config_changed(data); + frontend + .interface + .config_changed(data, ConfigChangeKind::EnvironmentBlend); } } Task::SetBrightness(brightness) => self.set_brightness(frontend, data, brightness), @@ -281,9 +283,7 @@ impl SubtabGeneralSettings { // get brightness let slider_brightness = state.fetch_component_as::("slider_brightness")?; if let Some(brightness) = frontend.interface.monado_brightness_get(data) { - let mut c = frontend.layout.start_common(); - slider_brightness.set_value(&mut c.common(), brightness * 100.0); - c.finish()?; + slider_brightness.set_value_primary(&mut frontend.layout.common(), brightness * 100.0); slider_brightness.on_value_changed({ let tasks = tasks.clone(); @@ -304,16 +304,15 @@ impl SubtabGeneralSettings { let slider_keying_value_range = state.fetch_component_as::("slider_keying_value_range")?; { - let mut lc = frontend.layout.start_common(); - let mut common = lc.common(); + let mut common = frontend.layout.common(); // set initial values let (rgb, range_h, range_s, range_v) = config.chroma_key_params.get_rgb_and_hsv_ranges(); - slider_keying_curve.set_value(&mut common, config.chroma_key_params.curve * SLIDER_MULTIPLIER); - slider_keying_despill.set_value(&mut common, config.chroma_key_params.despill * SLIDER_MULTIPLIER); - slider_keying_hue_range.set_value(&mut common, range_h * SLIDER_MULTIPLIER); - slider_keying_saturation_range.set_value(&mut common, range_s * SLIDER_MULTIPLIER); - slider_keying_value_range.set_value(&mut common, range_v * SLIDER_MULTIPLIER); + slider_keying_curve.set_value_primary(&mut common, config.chroma_key_params.curve * SLIDER_MULTIPLIER); + slider_keying_despill.set_value_primary(&mut common, config.chroma_key_params.despill * SLIDER_MULTIPLIER); + slider_keying_hue_range.set_value_primary(&mut common, range_h * SLIDER_MULTIPLIER); + slider_keying_saturation_range.set_value_primary(&mut common, range_s * SLIDER_MULTIPLIER); + slider_keying_value_range.set_value_primary(&mut common, range_v * SLIDER_MULTIPLIER); cs_keying.set_color(&mut common, rgb); // prepare callbacks @@ -340,8 +339,6 @@ impl SubtabGeneralSettings { slider_keying_saturation_range.on_value_changed(get_slider_callback(&tasks)); slider_keying_value_range.on_value_changed(get_slider_callback(&tasks)); cs_keying.on_changed(get_color_selector_callback(&tasks)); - - lc.finish()?; } Ok(Self { @@ -356,11 +353,11 @@ impl SubtabGeneralSettings { } fn chroma_update(&mut self, config: &mut GeneralConfig) { - let val_curve = self.slider_keying_curve.get_value(); - let val_despill = self.slider_keying_despill.get_value(); - let val_range_h = self.slider_keying_hue_range.get_value(); - let val_range_s = self.slider_keying_saturation_range.get_value(); - let val_range_v = self.slider_keying_value_range.get_value(); + let val_curve = self.slider_keying_curve.get_value_primary(); + let val_despill = self.slider_keying_despill.get_value_primary(); + let val_range_h = self.slider_keying_hue_range.get_value_primary(); + let val_range_s = self.slider_keying_saturation_range.get_value_primary(); + let val_range_v = self.slider_keying_value_range.get_value_primary(); let val_rgb = self.cs_keying.get_color(); config.chroma_key_params.despill = val_despill / SLIDER_MULTIPLIER; diff --git a/dash-frontend/src/tab/settings/mod.rs b/dash-frontend/src/tab/settings/mod.rs index 55eb02a9..32ec394a 100644 --- a/dash-frontend/src/tab/settings/mod.rs +++ b/dash-frontend/src/tab/settings/mod.rs @@ -5,7 +5,7 @@ use wgui::{ assets::AssetPath, components::tabs::ComponentTabs, drawing, - event::{CallbackDataCommon, EventAlterables}, + event::StyleSetRequest, globals::WguiGlobals, i18n::Translation, layout::{Layout, WidgetID}, @@ -20,11 +20,16 @@ use wgui::{ }, windowing::context_menu::{self, Blueprint, ContextMenu, TickResult}, }; -use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode}; +use wlx_common::{ + config::GeneralConfig, + config_io::ConfigRoot, + dash_interface::{ConfigChangeKind, RecenterMode}, +}; use crate::{ - frontend::{Frontend, FrontendTask}, + frontend::{Frontend, FrontendTask, FrontendTasks}, tab::{Tab, TabType, settings::macros::MacroParams}, + views::ViewUpdateParams, }; mod macros; @@ -33,6 +38,7 @@ mod tab_controls; mod tab_features; mod tab_look_and_feel; mod tab_misc; +mod tab_skybox; mod tab_troubleshooting; #[derive(Clone)] @@ -43,6 +49,7 @@ enum TabNameEnum { Misc, AutostartApps, Troubleshooting, + Skybox, } impl TabNameEnum { @@ -54,6 +61,7 @@ impl TabNameEnum { "misc" => Some(TabNameEnum::Misc), "autostart_apps" => Some(TabNameEnum::AutostartApps), "troubleshooting" => Some(TabNameEnum::Troubleshooting), + "skybox" => Some(TabNameEnum::Skybox), _ => None, } } @@ -75,14 +83,29 @@ enum Task { SetTab(TabNameEnum), } +struct SettingsMountParams<'a> { + mp: &'a mut MacroParams<'a>, + frontend_tasks: &'a FrontendTasks, + id_parent: WidgetID, +} + +trait SettingsTab { + fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> { + Ok(()) + } +} + pub struct TabSettings { pub state: ParserState, app_button_ids: Vec>, context_menu: ContextMenu, + current_tab: Option>, + tasks: Tasks, marker: PhantomData, + frontend_tasks: FrontendTasks, } impl Tab for TabSettings { @@ -91,7 +114,22 @@ impl Tab for TabSettings { } fn update(&mut self, frontend: &mut Frontend, _time_ms: u32, data: &mut T) -> anyhow::Result<()> { - let mut changed = false; + if let Some(tab) = &mut self.current_tab { + let mut config_change_kind = None; + + tab.update(&mut ViewUpdateParams { + layout: &mut frontend.layout, + executor: &frontend.executor, + general_config: frontend.interface.general_config(data), + config_change_kind: &mut config_change_kind, + })?; + + if let Some(kind) = config_change_kind { + frontend.interface.config_changed(data, kind); + } + } + + let mut changed = None; for task in self.tasks.drain() { match task { Task::SetTab(tab) => { @@ -104,7 +142,7 @@ impl Tab for TabSettings { } let config = frontend.interface.general_config(data); *setting.mut_bool(config) = n; - changed = true; + changed = Some(setting.change_kind()); } Task::UpdateFloat(setting, n) => { self.tasks.push(Task::SettingUpdated(setting)); @@ -113,7 +151,7 @@ impl Tab for TabSettings { } let config = frontend.interface.general_config(data); *setting.mut_f32(config) = n; - changed = true; + changed = Some(setting.change_kind()); } Task::UpdateInt(setting, n) => { self.tasks.push(Task::SettingUpdated(setting)); @@ -122,7 +160,7 @@ impl Tab for TabSettings { } let config = frontend.interface.general_config(data); *setting.mut_i32(config) = n; - changed = true; + changed = Some(setting.change_kind()); } Task::ClearPipewireTokens => { let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("pw_tokens.yaml")) @@ -161,7 +199,7 @@ impl Tab for TabSettings { let config = frontend.interface.general_config(data); config.autostart_apps.remove(idx); frontend.layout.remove_widget(widget); - changed = true; + changed = Some(ConfigChangeKind::OverlayConfig); } } Task::SettingUpdated(setting) => match setting { @@ -179,15 +217,10 @@ impl Tab for TabSettings { let mut s = name.splitn(5, ';'); (s.next(), s.next(), s.next(), s.next(), s.next()) } { + let mut common = frontend.layout.common(); let mut label = self .state - .fetch_widget_as::(&frontend.layout.state, &format!("{id}_value"))?; - - let mut alterables = EventAlterables::default(); - let mut common = CallbackDataCommon { - alterables: &mut alterables, - state: &frontend.layout.state, - }; + .fetch_widget_as::(&common.state, &format!("{id}_value"))?; let translation = Translation { text: text.into(), @@ -199,12 +232,12 @@ impl Tab for TabSettings { let setting = SettingType::from_str(setting).expect("Invalid Enum string"); let config = frontend.interface.general_config(data); setting.set_enum(config, value); - changed = true; + changed = Some(ConfigChangeKind::OverlayConfig); } // Notify overlays of the change - if changed { - frontend.interface.config_changed(data); + if let Some(changed) = changed { + frontend.interface.config_changed(data, changed); } Ok(()) @@ -231,7 +264,6 @@ enum SettingType { InvertScrollDirectionY, KeyboardMiddleClick, KeyboardSoundEnabled, - KeyboardSwipeToTypeEnabled, Language, LeftHandedMouse, LongPressDuration, @@ -258,6 +290,13 @@ enum SettingType { } impl SettingType { + pub fn change_kind(self) -> ConfigChangeKind { + match self { + Self::UseSkybox | Self::UsePassthrough => ConfigChangeKind::EnvironmentBlend, + _ => ConfigChangeKind::OverlayConfig, + } + } + pub fn mut_bool(self, config: &mut GeneralConfig) -> &mut bool { match self { Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x, @@ -284,7 +323,6 @@ impl SettingType { Self::HideUsername => &mut config.hide_username, Self::OpaqueBackground => &mut config.opaque_background, Self::XwaylandByDefault => &mut config.xwayland_by_default, - Self::KeyboardSwipeToTypeEnabled => &mut config.keyboard_swipe_to_type_enabled, _ => panic!("Requested bool for non-bool SettingType"), } } @@ -383,7 +421,6 @@ impl SettingType { Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"), Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"), Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"), - Self::KeyboardSwipeToTypeEnabled => Ok("APP_SETTINGS.KEYBOARD_SWIPE_TO_TYPE_ENABLED"), Self::Language => Ok("APP_SETTINGS.LANGUAGE"), Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"), Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"), @@ -420,7 +457,6 @@ impl SettingType { Self::GridOpacity => Some("APP_SETTINGS.GRID_OPACITY_HELP"), Self::HandsfreePointer => Some("APP_SETTINGS.HANDSFREE_POINTER_HELP"), Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"), - Self::KeyboardSwipeToTypeEnabled => Some("APP_SETTINGS.KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP"), Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"), Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"), Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"), @@ -440,7 +476,6 @@ impl SettingType { | Self::UiGradientIntensity | Self::UprightScreenFix | Self::DoubleCursorFix - | Self::ScreenRenderDown | Self::Language | Self::CaptureMethod ) @@ -504,6 +539,7 @@ impl TabSettings { let root = self.state.get_widget_id("settings_root")?; frontend.layout.remove_children(root); let globals = frontend.layout.state.globals.clone(); + self.current_tab = None; let mut mp = MacroParams { layout: &mut frontend.layout, @@ -514,31 +550,43 @@ impl TabSettings { idx: 9001, }; + let settings_mount_params = SettingsMountParams { + mp: &mut mp, + id_parent: root, + frontend_tasks: &self.frontend_tasks, + }; + match name { TabNameEnum::LookAndFeel => { - tab_look_and_feel::mount(&mut mp, root)?; + self.current_tab = Some(Box::new(tab_look_and_feel::State::mount(settings_mount_params)?)); } TabNameEnum::Features => { - tab_features::mount(&mut mp, root)?; + self.current_tab = Some(Box::new(tab_features::State::mount(settings_mount_params)?)); } TabNameEnum::Controls => { - tab_controls::mount(&mut mp, root)?; + self.current_tab = Some(Box::new(tab_controls::State::mount(settings_mount_params)?)); } TabNameEnum::Misc => { - tab_misc::mount(&mut mp, root)?; + self.current_tab = Some(Box::new(tab_misc::State::mount(settings_mount_params)?)); } TabNameEnum::AutostartApps => { - tab_autostart_apps::mount(&mut mp, root, &mut self.app_button_ids)?; + self.current_tab = Some(Box::new(tab_autostart_apps::State::mount( + settings_mount_params, + &mut self.app_button_ids, + )?)); } TabNameEnum::Troubleshooting => { - tab_troubleshooting::mount(&mut mp, root)?; + self.current_tab = Some(Box::new(tab_troubleshooting::State::mount(settings_mount_params)?)); + } + TabNameEnum::Skybox => { + self.current_tab = Some(Box::new(tab_skybox::State::mount(settings_mount_params)?)); } } Ok(()) } - pub fn new(frontend: &mut Frontend, parent_id: WidgetID, _data: &mut T) -> anyhow::Result { + pub fn new(frontend: &mut Frontend, parent_id: WidgetID, data: &mut T) -> anyhow::Result { let doc_params = ParseDocumentParams { globals: frontend.layout.state.globals.clone(), path: AssetPath::BuiltIn("gui/tab/settings.xml"), @@ -548,6 +596,16 @@ impl TabSettings { let parser_state = wgui::parser::parse_from_assets(&doc_params, &mut frontend.layout, parent_id)?; let tasks = Tasks::default(); let tabs = parser_state.fetch_component_as::("tabs")?; + + if !frontend.interface.get_feats(data).openxr { + let skybox_btn = tabs.get_tab_button("skybox").unwrap(); + frontend + .layout + .common() + .alterables + .set_style(skybox_btn.get_rect(), StyleSetRequest::Display(taffy::Display::None)); + } + tabs.on_select({ let tasks = tasks.clone(); Rc::new(move |_common, evt| { @@ -566,6 +624,8 @@ impl TabSettings { state: parser_state, marker: PhantomData, context_menu: ContextMenu::default(), + current_tab: None, + frontend_tasks: frontend.tasks.clone(), }) } } diff --git a/dash-frontend/src/tab/settings/tab_autostart_apps.rs b/dash-frontend/src/tab/settings/tab_autostart_apps.rs index 6483fc94..76986f88 100644 --- a/dash-frontend/src/tab/settings/tab_autostart_apps.rs +++ b/dash-frontend/src/tab/settings/tab_autostart_apps.rs @@ -1,19 +1,33 @@ use std::rc::Rc; -use crate::tab::settings::macros::{MacroParams, options_autostart_app, options_category}; -use wgui::layout::WidgetID; +use crate::tab::settings::{ + SettingsMountParams, SettingsTab, + macros::{options_autostart_app, options_category}, +}; -pub fn mount(mp: &mut MacroParams, parent: WidgetID, app_button_ids: &mut Vec>) -> anyhow::Result<()> { - *app_button_ids = Vec::new(); +pub struct State {} - if !mp.config.autostart_apps.is_empty() { - let c = options_category(mp, parent, "APP_SETTINGS.AUTOSTART_APPS", "dashboard/apps.svg")?; +impl SettingsTab for State {} - // todo: prevent clone - let autostart_apps = mp.config.autostart_apps.clone(); - for app in autostart_apps { - options_autostart_app(mp, c, &app.name, app_button_ids)?; +impl State { + pub fn mount(par: SettingsMountParams, app_button_ids: &mut Vec>) -> anyhow::Result { + *app_button_ids = Vec::new(); + + if !par.mp.config.autostart_apps.is_empty() { + let c = options_category( + par.mp, + par.id_parent, + "APP_SETTINGS.AUTOSTART_APPS", + "dashboard/apps.svg", + )?; + + // todo: prevent clone + let autostart_apps = par.mp.config.autostart_apps.clone(); + for app in autostart_apps { + options_autostart_app(par.mp, c, &app.name, app_button_ids)?; + } } + + Ok(State {}) } - Ok(()) } diff --git a/dash-frontend/src/tab/settings/tab_controls.rs b/dash-frontend/src/tab/settings/tab_controls.rs index 27b1f10b..266df0c9 100644 --- a/dash-frontend/src/tab/settings/tab_controls.rs +++ b/dash-frontend/src/tab/settings/tab_controls.rs @@ -1,23 +1,33 @@ use crate::tab::settings::{ - SettingType, - macros::{MacroParams, options_category, options_checkbox, options_dropdown, options_slider_f32, options_slider_i32}, + SettingType, SettingsMountParams, SettingsTab, + macros::{options_category, options_checkbox, options_dropdown, options_slider_f32, options_slider_i32}, }; -use wgui::layout::WidgetID; -pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { - let c = options_category(mp, parent, "APP_SETTINGS.CONTROLS", "dashboard/controller.svg")?; - options_dropdown::(mp, c, &SettingType::KeyboardMiddleClick)?; - options_dropdown::(mp, c, &SettingType::HandsfreePointer)?; - options_checkbox(mp, c, SettingType::FocusFollowsMouseMode)?; - options_checkbox(mp, c, SettingType::LeftHandedMouse)?; - options_checkbox(mp, c, SettingType::AllowSliding)?; - options_checkbox(mp, c, SettingType::InvertScrollDirectionX)?; - options_checkbox(mp, c, SettingType::InvertScrollDirectionY)?; - options_slider_f32(mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?; - options_slider_f32(mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?; - options_slider_f32(mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1)?; - options_slider_f32(mp, c, SettingType::XrClickSensitivity, 0.1, 1.0, 0.1)?; - options_slider_f32(mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1)?; - options_slider_i32(mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?; - Ok(()) +pub struct State {} + +impl SettingsTab for State {} + +impl State { + pub fn mount(par: SettingsMountParams) -> anyhow::Result { + let c = options_category( + par.mp, + par.id_parent, + "APP_SETTINGS.CONTROLS", + "dashboard/controller.svg", + )?; + options_dropdown::(par.mp, c, &SettingType::KeyboardMiddleClick)?; + options_dropdown::(par.mp, c, &SettingType::HandsfreePointer)?; + options_checkbox(par.mp, c, SettingType::FocusFollowsMouseMode)?; + options_checkbox(par.mp, c, SettingType::LeftHandedMouse)?; + options_checkbox(par.mp, c, SettingType::AllowSliding)?; + options_checkbox(par.mp, c, SettingType::InvertScrollDirectionX)?; + options_checkbox(par.mp, c, SettingType::InvertScrollDirectionY)?; + options_slider_f32(par.mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?; + options_slider_f32(par.mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?; + options_slider_f32(par.mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1)?; + options_slider_f32(par.mp, c, SettingType::XrClickSensitivity, 0.1, 1.0, 0.1)?; + options_slider_f32(par.mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1)?; + options_slider_i32(par.mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?; + Ok(State {}) + } } diff --git a/dash-frontend/src/tab/settings/tab_features.rs b/dash-frontend/src/tab/settings/tab_features.rs index 264a40fd..992bf06c 100644 --- a/dash-frontend/src/tab/settings/tab_features.rs +++ b/dash-frontend/src/tab/settings/tab_features.rs @@ -1,20 +1,24 @@ use crate::tab::settings::{ - SettingType, - macros::{MacroParams, options_category, options_checkbox, options_slider_f32}, + SettingType, SettingsMountParams, SettingsTab, + macros::{options_category, options_checkbox, options_slider_f32}, }; -use wgui::layout::WidgetID; -pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { - let c = options_category(mp, parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?; - options_checkbox(mp, c, SettingType::NotificationsEnabled)?; - options_checkbox(mp, c, SettingType::NotificationsSoundEnabled)?; - options_checkbox(mp, c, SettingType::KeyboardSoundEnabled)?; - options_checkbox(mp, c, SettingType::KeyboardSwipeToTypeEnabled)?; - options_checkbox(mp, c, SettingType::SpaceDragUnlocked)?; - options_checkbox(mp, c, SettingType::SpaceRotateUnlocked)?; - options_slider_f32(mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5)?; - options_checkbox(mp, c, SettingType::BlockGameInput)?; - options_checkbox(mp, c, SettingType::BlockGameInputIgnoreWatch)?; - options_checkbox(mp, c, SettingType::BlockPosesOnKbdInteraction)?; - Ok(()) +pub struct State {} + +impl SettingsTab for State {} + +impl State { + pub fn mount(par: SettingsMountParams) -> anyhow::Result { + let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?; + options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?; + options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?; + options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?; + options_checkbox(par.mp, c, SettingType::SpaceDragUnlocked)?; + options_checkbox(par.mp, c, SettingType::SpaceRotateUnlocked)?; + options_slider_f32(par.mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5)?; + options_checkbox(par.mp, c, SettingType::BlockGameInput)?; + options_checkbox(par.mp, c, SettingType::BlockGameInputIgnoreWatch)?; + options_checkbox(par.mp, c, SettingType::BlockPosesOnKbdInteraction)?; + Ok(State {}) + } } diff --git a/dash-frontend/src/tab/settings/tab_look_and_feel.rs b/dash-frontend/src/tab/settings/tab_look_and_feel.rs index 6fe40420..c632e667 100644 --- a/dash-frontend/src/tab/settings/tab_look_and_feel.rs +++ b/dash-frontend/src/tab/settings/tab_look_and_feel.rs @@ -1,22 +1,29 @@ use crate::tab::settings::{ - SettingType, - macros::{MacroParams, options_category, options_checkbox, options_dropdown, options_slider_f32}, + SettingType, SettingsMountParams, SettingsTab, + macros::{options_category, options_checkbox, options_dropdown, options_slider_f32}, }; -use wgui::layout::WidgetID; -pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { - let c = options_category(mp, parent, "APP_SETTINGS.LOOK_AND_FEEL", "dashboard/palette.svg")?; - options_dropdown::(mp, c, &SettingType::Language)?; - options_checkbox(mp, c, SettingType::OpaqueBackground)?; - options_checkbox(mp, c, SettingType::HideUsername)?; - options_checkbox(mp, c, SettingType::HideGrabHelp)?; - options_slider_f32(mp, c, SettingType::UiAnimationSpeed, 0.5, 5.0, 0.1)?; // min, max, step - options_slider_f32(mp, c, SettingType::UiGradientIntensity, 0.0, 1.0, 0.05)?; // min, max, step - options_slider_f32(mp, c, SettingType::UiRoundMultiplier, 0.5, 5.0, 0.1)?; - options_checkbox(mp, c, SettingType::SetsOnWatch)?; - options_checkbox(mp, c, SettingType::UseSkybox)?; - options_slider_f32(mp, c, SettingType::GridOpacity, 0.0, 1.0, 0.05)?; // min, max, step - options_checkbox(mp, c, SettingType::UsePassthrough)?; - options_checkbox(mp, c, SettingType::Clock12h)?; - Ok(()) +pub struct State {} + +impl SettingsTab for State {} + +impl State { + pub fn mount(par: SettingsMountParams) -> anyhow::Result { + let c = options_category( + par.mp, + par.id_parent, + "APP_SETTINGS.LOOK_AND_FEEL", + "dashboard/palette.svg", + )?; + options_dropdown::(par.mp, c, &SettingType::Language)?; + options_checkbox(par.mp, c, SettingType::OpaqueBackground)?; + options_checkbox(par.mp, c, SettingType::HideUsername)?; + options_checkbox(par.mp, c, SettingType::HideGrabHelp)?; + options_slider_f32(par.mp, c, SettingType::UiAnimationSpeed, 0.5, 5.0, 0.1)?; // min, max, step + options_slider_f32(par.mp, c, SettingType::UiGradientIntensity, 0.0, 1.0, 0.05)?; // min, max, step + options_slider_f32(par.mp, c, SettingType::UiRoundMultiplier, 0.5, 5.0, 0.1)?; + options_checkbox(par.mp, c, SettingType::SetsOnWatch)?; + options_checkbox(par.mp, c, SettingType::Clock12h)?; + Ok(State {}) + } } diff --git a/dash-frontend/src/tab/settings/tab_misc.rs b/dash-frontend/src/tab/settings/tab_misc.rs index 9fa6f429..29b57522 100644 --- a/dash-frontend/src/tab/settings/tab_misc.rs +++ b/dash-frontend/src/tab/settings/tab_misc.rs @@ -1,15 +1,20 @@ use crate::tab::settings::{ - SettingType, - macros::{MacroParams, options_category, options_checkbox, options_dropdown}, + SettingType, SettingsMountParams, SettingsTab, + macros::{options_category, options_checkbox, options_dropdown}, }; -use wgui::layout::WidgetID; -pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { - let c = options_category(mp, parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?; - options_dropdown::(mp, c, &SettingType::CaptureMethod)?; - options_checkbox(mp, c, SettingType::XwaylandByDefault)?; - options_checkbox(mp, c, SettingType::UprightScreenFix)?; - options_checkbox(mp, c, SettingType::DoubleCursorFix)?; - options_checkbox(mp, c, SettingType::ScreenRenderDown)?; - Ok(()) +pub struct State {} + +impl SettingsTab for State {} + +impl State { + pub fn mount(par: SettingsMountParams) -> anyhow::Result { + let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?; + options_dropdown::(par.mp, c, &SettingType::CaptureMethod)?; + options_checkbox(par.mp, c, SettingType::XwaylandByDefault)?; + options_checkbox(par.mp, c, SettingType::UprightScreenFix)?; + options_checkbox(par.mp, c, SettingType::DoubleCursorFix)?; + options_checkbox(par.mp, c, SettingType::ScreenRenderDown)?; + Ok(State {}) + } } diff --git a/dash-frontend/src/tab/settings/tab_skybox.rs b/dash-frontend/src/tab/settings/tab_skybox.rs new file mode 100644 index 00000000..66c2edf8 --- /dev/null +++ b/dash-frontend/src/tab/settings/tab_skybox.rs @@ -0,0 +1,70 @@ +use wgui::{assets::AssetPath, i18n::Translation, layout::Layout, task::Tasks}; + +use crate::{ + frontend::FrontendTasks, + tab::settings::{ + SettingType, SettingsMountParams, SettingsTab, + macros::{options_category, options_checkbox, options_slider_f32}, + }, + util::{popup_manager::PopupHolder, wgui_simple}, + views::{self, ViewUpdateParams, skymap_list}, +}; + +#[derive(Clone)] +enum Task { + ShowSkymapList, +} + +pub struct State { + popup_skymap_list: PopupHolder, + tasks: Tasks, + frontend_tasks: FrontendTasks, +} + +impl SettingsTab for State { + fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { + self.popup_skymap_list.update(par)?; + + for task in self.tasks.drain() { + match task { + Task::ShowSkymapList => self.show_skymap_list(par.layout), + } + } + + Ok(()) + } +} + +impl State { + pub fn mount(par: SettingsMountParams) -> anyhow::Result { + let id_category = options_category(par.mp, par.id_parent, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?; + options_checkbox(par.mp, id_category, SettingType::UseSkybox)?; + options_checkbox(par.mp, id_category, SettingType::UsePassthrough)?; + options_slider_f32(par.mp, id_category, SettingType::GridOpacity, 0.0, 1.0, 0.05)?; + + let tasks = Tasks::::new(); + + // "Browse skymaps" button + wgui_simple::create_button(wgui_simple::CreateButtonParams { + id_parent: id_category, + layout: par.mp.layout, + content: Translation::from_translation_key("APP_SETTINGS.BROWSE_SKYMAPS"), + icon_builtin: AssetPath::BuiltIn("dashboard/globe.svg"), + on_click: tasks.get_button_click_callback(Task::ShowSkymapList), + })?; + + Ok(Self { + popup_skymap_list: Default::default(), + frontend_tasks: par.frontend_tasks.clone(), + tasks, + }) + } + + fn show_skymap_list(&mut self, layout: &mut Layout) { + views::skymap_list::mount_popup( + self.frontend_tasks.clone(), + layout.state.globals.clone(), + self.popup_skymap_list.clone(), + ); + } +} diff --git a/dash-frontend/src/tab/settings/tab_troubleshooting.rs b/dash-frontend/src/tab/settings/tab_troubleshooting.rs index 9e16fd4b..8635efe5 100644 --- a/dash-frontend/src/tab/settings/tab_troubleshooting.rs +++ b/dash-frontend/src/tab/settings/tab_troubleshooting.rs @@ -1,45 +1,55 @@ use crate::tab::settings::{ - Task, - macros::{MacroParams, options_category, options_danger_button}, + SettingsMountParams, SettingsTab, Task, + macros::{options_category, options_danger_button}, }; -use wgui::layout::WidgetID; -pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { - let c = options_category(mp, parent, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/cpu.svg")?; - options_danger_button( - mp, - c, - "APP_SETTINGS.RESET_PLAYSPACE", - "dashboard/recenter.svg", - Task::ResetPlayspace, - )?; - options_danger_button( - mp, - c, - "APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS", - "dashboard/display.svg", - Task::ClearPipewireTokens, - )?; - options_danger_button( - mp, - c, - "APP_SETTINGS.CLEAR_SAVED_STATE", - "dashboard/binary.svg", - Task::ClearSavedState, - )?; - options_danger_button( - mp, - c, - "APP_SETTINGS.DELETE_ALL_CONFIGS", - "dashboard/circle.svg", - Task::DeleteAllConfigs, - )?; - options_danger_button( - mp, - c, - "APP_SETTINGS.RESTART_SOFTWARE", - "dashboard/refresh.svg", - Task::RestartSoftware, - )?; - Ok(()) +pub struct State {} + +impl SettingsTab for State {} + +impl State { + pub fn mount(par: SettingsMountParams) -> anyhow::Result { + let c = options_category( + par.mp, + par.id_parent, + "APP_SETTINGS.TROUBLESHOOTING", + "dashboard/cpu.svg", + )?; + options_danger_button( + par.mp, + c, + "APP_SETTINGS.RESET_PLAYSPACE", + "dashboard/recenter.svg", + Task::ResetPlayspace, + )?; + options_danger_button( + par.mp, + c, + "APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS", + "dashboard/display.svg", + Task::ClearPipewireTokens, + )?; + options_danger_button( + par.mp, + c, + "APP_SETTINGS.CLEAR_SAVED_STATE", + "dashboard/binary.svg", + Task::ClearSavedState, + )?; + options_danger_button( + par.mp, + c, + "APP_SETTINGS.DELETE_ALL_CONFIGS", + "dashboard/circle.svg", + Task::DeleteAllConfigs, + )?; + options_danger_button( + par.mp, + c, + "APP_SETTINGS.RESTART_SOFTWARE", + "dashboard/refresh.svg", + Task::RestartSoftware, + )?; + Ok(State {}) + } } diff --git a/dash-frontend/src/util/cached_fetcher.rs b/dash-frontend/src/util/cached_fetcher.rs index 5c7f84bc..2671a748 100644 --- a/dash-frontend/src/util/cached_fetcher.rs +++ b/dash-frontend/src/util/cached_fetcher.rs @@ -1,9 +1,8 @@ +use crate::util::{networking::http_client, steam_utils::AppID}; use anyhow::Context; use serde::Deserialize; use wlx_common::{async_executor::AsyncExecutor, cache_dir}; -use crate::util::{http_client, steam_utils::AppID}; - pub struct CoverArt { // can be empty in case if data couldn't be fetched (use a fallback image then) pub compressed_image_data: Vec, @@ -24,7 +23,7 @@ pub async fn request_image(executor: AsyncExecutor, app_id: AppID) -> anyhow::Re app_id ); - match http_client::get(&executor, &url).await { + match http_client::get_simple(&executor, &url).await { Ok(response) => { log::info!("Success"); cache_dir::set_data(&cache_file_path, &response.data).await?; @@ -69,7 +68,7 @@ async fn get_app_details_json_internal( // Fetch from Steam API log::info!("Fetching app detail ID {}", app_id); let url = format!("https://store.steampowered.com/api/appdetails?appids={}", app_id); - let response = http_client::get(&executor, &url).await?; + let response = http_client::get_simple(&executor, &url).await?; let res_utf8 = String::from_utf8(response.data)?; let root = serde_json::from_str::(&res_utf8)?; let body = root.get(&app_id).context("invalid body")?; diff --git a/dash-frontend/src/util/mod.rs b/dash-frontend/src/util/mod.rs index e5290607..521889d0 100644 --- a/dash-frontend/src/util/mod.rs +++ b/dash-frontend/src/util/mod.rs @@ -1,5 +1,5 @@ pub mod cached_fetcher; -pub mod http_client; +pub mod networking; pub mod pactl_wrapper; pub mod popup_manager; pub mod steam_utils; diff --git a/dash-frontend/src/util/http_client.rs b/dash-frontend/src/util/networking/http_client.rs similarity index 70% rename from dash-frontend/src/util/http_client.rs rename to dash-frontend/src/util/networking/http_client.rs index 875d509e..812620c3 100644 --- a/dash-frontend/src/util/http_client.rs +++ b/dash-frontend/src/util/networking/http_client.rs @@ -18,10 +18,33 @@ pub struct HttpClientResponse { pub data: Vec, } -pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result { - log::info!("fetching URL \"{}\"", url); +impl HttpClientResponse { + pub fn as_json(self) -> anyhow::Result + where + T: for<'a> serde::Deserialize<'a>, + { + let utf8 = str::from_utf8(&self.data)?; + Ok(serde_json::from_str::(utf8)?) + } +} - let url: hyper::Uri = url.try_into()?; +pub struct ProgressFuncData { + pub bytes_downloaded: u64, + pub file_size: u64, +} + +pub type ProgressFunc = Box; + +pub struct GetParams<'a> { + pub executor: &'a AsyncExecutor, + pub url: &'a str, + pub on_progress: Option, +} + +pub async fn get(params: GetParams<'_>) -> anyhow::Result { + log::info!("fetching URL \"{}\"", params.url); + + let url: hyper::Uri = params.url.try_into()?; let req = Request::builder() .header( hyper::header::HOST, @@ -30,23 +53,56 @@ pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result anyhow::Result { + get(GetParams { + executor, + url, + on_progress: None, + }) + .await } async fn fetch( diff --git a/dash-frontend/src/util/networking/image_fetch.rs b/dash-frontend/src/util/networking/image_fetch.rs new file mode 100644 index 00000000..75d7c892 --- /dev/null +++ b/dash-frontend/src/util/networking/image_fetch.rs @@ -0,0 +1,16 @@ +use std::rc::Rc; + +use wgui::{globals::WguiGlobals, renderer_vk::text::custom_glyph::CustomGlyphData}; +use wlx_common::async_executor::AsyncExecutor; + +use crate::util::networking::http_client; + +pub async fn fetch_to_glyph_data( + globals: &WguiGlobals, + executor: &AsyncExecutor, + url: &str, +) -> anyhow::Result<(CustomGlyphData, Rc>)> { + let res = http_client::get_simple(executor, url).await?; + let glyph_data = CustomGlyphData::from_bytes_raster(globals, url, &res.data)?; + Ok((glyph_data, Rc::new(res.data))) +} diff --git a/dash-frontend/src/util/networking/mod.rs b/dash-frontend/src/util/networking/mod.rs new file mode 100644 index 00000000..b6c625a8 --- /dev/null +++ b/dash-frontend/src/util/networking/mod.rs @@ -0,0 +1,6 @@ +pub mod http_client; +pub mod image_fetch; +pub mod skymap_catalog; + +// pub const WAYVR_ROOT_URL: &'static str = "https://wayvr.org"; +pub const WAYVR_SKYMAPS_ROOT: &'static str = "https://wayvr.org/skymaps"; diff --git a/dash-frontend/src/util/networking/skymap_catalog.rs b/dash-frontend/src/util/networking/skymap_catalog.rs new file mode 100644 index 00000000..d7d21ca2 --- /dev/null +++ b/dash-frontend/src/util/networking/skymap_catalog.rs @@ -0,0 +1,195 @@ +use crate::util::networking::{self, WAYVR_SKYMAPS_ROOT, http_client}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use wlx_common::{async_executor::AsyncExecutor, config_io}; +pub type SkymapUuid = uuid::Uuid; + +#[derive(Copy, Clone, Serialize, Deserialize, Debug)] +pub enum SkymapResolution { + Res2k, + Res4k, + Res8k, +} + +impl SkymapResolution { + pub const fn get_display_str(&self) -> &'static str { + match self { + SkymapResolution::Res2k => "2K (2 MiB VRAM)", + SkymapResolution::Res4k => "4K (8 MiB VRAM)", + SkymapResolution::Res8k => "8K (33 MiB VRAM)", + } + } + + pub const fn get_display_str_simple(&self) -> &'static str { + match self { + SkymapResolution::Res2k => "2K", + SkymapResolution::Res4k => "4K", + SkymapResolution::Res8k => "8K", + } + } + + pub fn from_display_str_simple(text: &str) -> Option { + match text { + "2K" => Some(SkymapResolution::Res2k), + "4K" => Some(SkymapResolution::Res4k), + "8K" => Some(SkymapResolution::Res8k), + _ => None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SkymapCatalogEntryFiles { + pub size_8k: Option, // "my_skymap_8k.dds" + pub size_4k: Option, // "my_skymap_4k.dds" + pub size_2k: String, // we should have *at least* this + pub preview: String, +} + +impl SkymapCatalogEntryFiles { + pub fn get_url_preview(&self) -> String { + format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, self.preview) + } + + pub fn get_filename_from_res(&self, res: SkymapResolution) -> Option { + match res { + SkymapResolution::Res2k => Some(&self.size_2k), + SkymapResolution::Res4k => self.size_4k.as_ref(), + SkymapResolution::Res8k => self.size_8k.as_ref(), + } + .map(|raw_filename| { + // sanitize filename, do not allow "../" just in case + PathBuf::from(raw_filename) + .file_name() + .map(|s| String::from(s.to_string_lossy())) + })? + } + + // example result: "https://wayvr.org/skymaps/files/my_skymap_8k.dds" + pub fn get_url_from_res(&self, res: SkymapResolution) -> Option { + let Some(filename) = self.get_filename_from_res(res) else { + return None; + }; + + Some(format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, filename)) + } + + pub fn get_preview_path(&self) -> PathBuf { + config_io::get_skymaps_root().join(&self.preview) + } + + pub fn save_preview_to_file(&self, data: &[u8]) -> anyhow::Result<()> { + std::fs::write(self.get_preview_path(), data)?; + Ok(()) + } + + pub fn remove_preview_file(&self) { + let _dont_care = std::fs::remove_file(self.get_preview_path()); + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SkymapCatalogEntry { + pub uuid: SkymapUuid, + pub created_at: String, + pub modified_at: String, + pub version: u32, + pub name: String, + pub description: String, + pub author: String, + pub files: SkymapCatalogEntryFiles, +} + +impl SkymapCatalogEntry { + pub fn get_destination_path(&self, resolution: SkymapResolution) -> Option { + let Some(filename) = self.files.get_filename_from_res(resolution) else { + return None; + }; + + Some(config_io::get_skymaps_root().join(filename)) + } + + pub fn get_destination_metadata_path(&self) -> PathBuf { + config_io::get_skymaps_root().join(format!("{}.json", self.uuid)) + } + + pub fn is_downloaded(&self, resolution: SkymapResolution) -> anyhow::Result { + let Some(full_path) = self.get_destination_path(resolution) else { + return Ok(false); + }; + + Ok(std::fs::exists(full_path)?) + } + + pub fn has_any_downloaded(&self) -> bool { + self.is_downloaded(SkymapResolution::Res2k).unwrap_or(false) + || self.is_downloaded(SkymapResolution::Res4k).unwrap_or(false) + || self.is_downloaded(SkymapResolution::Res8k).unwrap_or(false) + } + + pub fn remove_file(&self, resolution: SkymapResolution) { + let Some(full_path) = self.get_destination_path(resolution) else { + return; + }; + + let _dont_care = std::fs::remove_file(full_path); + } + + pub fn save_metadata(&self) -> anyhow::Result<()> { + let json = serde_json::to_string_pretty(self)?; + std::fs::write(self.get_destination_metadata_path(), json)?; + Ok(()) + } + + pub fn remove_metadata(&self) { + let _dont_care = std::fs::remove_file(self.get_destination_metadata_path()); + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SkymapCatalog { + pub version: u32, + pub r#type: String, + pub entries: Vec, +} + +impl SkymapCatalog { + fn validate(&self) -> anyhow::Result<()> { + if self.version != 1 { + anyhow::bail!("Unsupported version"); + } + + if self.r#type != "wayvr_skymaps" { + anyhow::bail!("Unsupported type"); + } + Ok(()) + } +} + +pub async fn request_catalog(executor: &AsyncExecutor) -> anyhow::Result { + log::info!("Fetching skymap list"); + + let res = http_client::get_simple(executor, &format!("{}/catalog.json", networking::WAYVR_SKYMAPS_ROOT)).await?; + let catalog = res.as_json::()?; + catalog.validate()?; + + Ok(catalog) +} + +pub fn get_entries_from_disk() -> anyhow::Result> { + let mut entries = Vec::::new(); + + let skymaps_root = config_io::get_skymaps_root(); + + for uuid in config_io::get_skymaps_uuids().unwrap_or_default() { + let metadata_path = skymaps_root.join(format!("{}.json", uuid)); + let Ok(data) = std::fs::read_to_string(metadata_path) else { + continue; + }; + + let entry = serde_json::from_str::(&data)?; + entries.push(entry); + } + + Ok(entries) +} diff --git a/dash-frontend/src/util/popup_manager.rs b/dash-frontend/src/util/popup_manager.rs index ff46f515..fa7b7cfd 100644 --- a/dash-frontend/src/util/popup_manager.rs +++ b/dash-frontend/src/util/popup_manager.rs @@ -16,7 +16,10 @@ use wgui::{ }; use wlx_common::config::GeneralConfig; -use crate::frontend::{FrontendTask, FrontendTasks}; +use crate::{ + frontend::{FrontendTask, FrontendTasks}, + views::{ViewTrait, ViewUpdateParams}, +}; pub struct PopupManagerParams { pub parent_id: WidgetID, @@ -34,15 +37,140 @@ pub struct MountedPopup { frontend_tasks: FrontendTasks, } +#[derive(Default)] struct MountedPopupState { mounted_popup: Option, + closed_callback: Option, } -#[derive(Clone)] +#[derive(Default, Clone)] pub struct PopupHandle { state: Rc>, } +struct PopupHolderState { + popup_handle: PopupHandle, + view: Option, + on_view_close: Option>, +} + +// we can't use #[derive(Default)] due to the fact that ViewType can't be Default. +impl Default for PopupHolderState { + fn default() -> Self { + Self { + popup_handle: Default::default(), + view: None, + on_view_close: None, + } + } +} + +pub struct PopupHolder { + state: Rc>>, +} + +impl Default for PopupHolder { + fn default() -> Self { + Self { + state: Rc::new(RefCell::new(PopupHolderState::default())), + } + } +} + +impl PopupHolderState { + fn close(&mut self) { + if self.view.is_some() { + self.view = None; + if let Some(on_close) = self.on_view_close.take() { + on_close(); + } + } + self.popup_handle.close(); + } +} + +impl Drop for PopupHolderState { + fn drop(&mut self) { + self.close(); + } +} + +// we can't derive(Clone) due to the fact that ViewType is non-cloneable +impl Clone for PopupHolder { + fn clone(&self) -> Self { + Self { + state: self.state.clone(), + } + } +} + +impl PopupHolder { + pub fn update(&self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { + let mut state = self.state.borrow_mut(); + let Some(view) = &mut state.view else { + return Ok(()); + }; + + view.update(par) + } + + pub fn set_view(&self, handle: PopupHandle, view: ViewType, on_view_close: Option>) { + let mut state = self.state.borrow_mut(); + state.view = Some(view); + state.popup_handle = handle; + state.on_view_close = on_view_close; + } + + // Get underlying ViewType object in a closure and return its value + // example usage: + // + // ```rs + // holder.with_view(|view| { + // view.foo(); + // }) + // ``` + // + pub fn with_view(&self, f: F) -> Option + where + F: FnOnce(&mut ViewType) -> R, + { + let mut state = self.state.borrow_mut(); + if let Some(view) = state.view.as_mut() { + Some(f(view)) + } else { + None + } + } + + // Same as with_view, but the closure expects a simple anyhow::Result<()> type + pub fn with_view_res(&self, f: F) -> anyhow::Result<()> + where + F: FnOnce(&mut ViewType) -> anyhow::Result<()>, + { + if let Some(res) = self.with_view(f) { + return res; + } + Ok(()) + } + + pub fn get_close_callback(&self, layout: &Layout) -> Box + where + ViewType: 'static, + { + let layout_tasks = layout.tasks.clone(); + let weak_state = Rc::downgrade(&self.state); + Box::new(move || { + // we can't borrow State here yet, dispatch it. + layout_tasks.push(LayoutTask::Dispatch(Box::new(move |_common| { + if let Some(state) = weak_state.upgrade() { + state.borrow_mut().close(); + } + Ok(()) + }))); + }) + } +} + impl PopupHandle { pub fn close(&self) { self.state.borrow_mut().mounted_popup = None; // Drop will be called @@ -61,10 +189,26 @@ pub struct PopupContentFuncData<'a> { pub id_content: WidgetID, } +type PopupClosedCallback = Box; + +// we need to implement Clone here, but the underlying function can be called only once. +// on_content will be cleared after the first call #[derive(Clone)] -pub struct MountPopupParams { - pub title: Translation, - pub on_content: Rc anyhow::Result<()>>, +pub struct MountPopupOnceParams { + title: Translation, + on_content: Rc anyhow::Result>>>>, +} + +impl MountPopupOnceParams { + pub fn new( + title: Translation, + on_content: Box anyhow::Result>, + ) -> Self { + Self { + title, + on_content: Rc::new(RefCell::new(Some(on_content))), + } + } } impl Drop for MountedPopup { @@ -78,10 +222,16 @@ impl State { fn refresh_stack(&mut self, alterables: &mut EventAlterables) { // show only the topmost popup self.popup_stack.retain(|weak| { - let Some(popup) = weak.upgrade() else { - return false; + let retain = { + let Some(popup) = weak.upgrade() else { + return false; + }; + popup.borrow_mut().mounted_popup.is_some() }; - popup.borrow_mut().mounted_popup.is_some() + if !retain { + log::debug!("removing popup from popup_stack"); + } + retain }); for (idx, popup) in self.popup_stack.iter().enumerate() { @@ -116,16 +266,13 @@ impl PopupManager { state.refresh_stack(alterables); } - /// Mount a new popup on top of the existing popup stack. - /// Only the topmost popup is visible. - pub fn mount_popup( - &mut self, - globals: WguiGlobals, + fn mount_popup_prepare( + &self, + globals: &WguiGlobals, layout: &mut Layout, - frontend_tasks: FrontendTasks, - params: MountPopupParams, - config: &GeneralConfig, - ) -> anyhow::Result<()> { + frontend_tasks: &FrontendTasks, + popup_title: &Translation, + ) -> anyhow::Result<(PopupHandle, WidgetID /* content widget ID */)> { let doc_params = &ParseDocumentParams { globals: globals.clone(), path: AssetPath::BuiltIn("gui/view/popup_window.xml"), @@ -138,7 +285,7 @@ impl PopupManager { { let mut label_title = state.fetch_widget_as::(&layout.state, "popup_title")?; - label_title.set_text_simple(&mut globals.get(), params.title); + label_title.set_text_simple(&mut globals.get(), popup_title.clone()); } let but_back = state.fetch_component_as::("but_back")?; @@ -152,6 +299,7 @@ impl PopupManager { let mounted_popup_state = MountedPopupState { mounted_popup: Some(mounted_popup), + closed_callback: None, }; let popup_handle = PopupHandle { @@ -159,28 +307,57 @@ impl PopupManager { }; let mut state = self.state.borrow_mut(); + log::debug!("pushing popup to popup_stack"); state.popup_stack.push(Rc::downgrade(&popup_handle.state)); but_back.on_click({ let popup_handle = Rc::downgrade(&popup_handle.state); Rc::new(move |_common, _evt| { if let Some(popup_handle) = popup_handle.upgrade() { - popup_handle.borrow_mut().mounted_popup = None; // will call Drop + if let Some(closed_callback) = { + let mut state = popup_handle.borrow_mut(); + state.mounted_popup = None; // will call Drop + state.closed_callback.take() + } { + log::debug!("closed_callback called"); + closed_callback(); + } } Ok(()) }) }); frontend_tasks.push(FrontendTask::RefreshPopupManager); + Ok((popup_handle, id_content)) + } + + /// Mount a new popup on top of the existing popup stack. + /// Only the topmost popup is visible. + pub fn mount_popup_once( + &mut self, + globals: &WguiGlobals, + layout: &mut Layout, + frontend_tasks: &FrontendTasks, + params: MountPopupOnceParams, + config: &GeneralConfig, + ) -> anyhow::Result<()> { + let mut func = params.on_content.borrow_mut(); + let Some(on_content_func) = func.take() else { + anyhow::bail!("mount_popup_once called more than once"); + }; + + let (popup_handle, id_content) = self.mount_popup_prepare(globals, layout, frontend_tasks, ¶ms.title)?; // mount user-set popup content - (*params.on_content)(PopupContentFuncData { + let closed_callback = on_content_func(PopupContentFuncData { layout, handle: popup_handle.clone(), id_content, config, })?; + popup_handle.state.borrow_mut().closed_callback = Some(closed_callback); + Ok(()) } } diff --git a/dash-frontend/src/util/steam_utils.rs b/dash-frontend/src/util/steam_utils.rs index a2c6e778..3c7faa3a 100644 --- a/dash-frontend/src/util/steam_utils.rs +++ b/dash-frontend/src/util/steam_utils.rs @@ -2,6 +2,7 @@ use keyvalues_parser::{Obj, Vdf}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +#[derive(Clone)] pub struct SteamUtils { steam_root: PathBuf, } diff --git a/dash-frontend/src/util/wgui_simple.rs b/dash-frontend/src/util/wgui_simple.rs index 44dad5ee..af4d393c 100644 --- a/dash-frontend/src/util/wgui_simple.rs +++ b/dash-frontend/src/util/wgui_simple.rs @@ -1,12 +1,52 @@ +use glam::{Mat4, Vec2}; use wgui::{ + animation::{Animation, AnimationEasing}, + assets::AssetPath, + components::{self, button::ButtonClickCallback}, + drawing, i18n::Translation, - layout::{Layout, WidgetID}, - renderer_vk::text::TextStyle, - widget::label::{WidgetLabel, WidgetLabelParams}, + layout::{Layout, LayoutTask, WidgetID}, + parser::{Fetchable, ParseDocumentParams}, + renderer_vk::{ + text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData}, + util::centered_matrix, + }, + taffy::{self, prelude::length}, + widget::{ + ConstructEssentials, + label::{WidgetLabel, WidgetLabelParams}, + sprite::{WidgetSprite, WidgetSpriteParams}, + }, }; +pub struct CreateButtonParams<'a> { + pub id_parent: WidgetID, + pub layout: &'a mut Layout, + pub content: Translation, + pub icon_builtin: AssetPath<'a>, + pub on_click: ButtonClickCallback, +} + +pub fn create_button(par: CreateButtonParams) -> anyhow::Result<()> { + let (_, button) = components::button::construct( + &mut ConstructEssentials { + layout: par.layout, + parent: par.id_parent, + }, + components::button::Params { + text: Some(par.content), + sprite_src: Some(par.icon_builtin), + ..Default::default() + }, + )?; + + button.on_click(par.on_click); + + Ok(()) +} + #[allow(dead_code)] -pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> { +pub fn create_label(layout: &mut Layout, id_parent: WidgetID, content: Translation) -> anyhow::Result<()> { let label = WidgetLabel::create( &mut layout.state, WidgetLabelParams { @@ -18,7 +58,102 @@ pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) }, ); + layout.add_child(id_parent, label, Default::default())?; + + Ok(()) +} + +pub fn create_label_error(layout: &mut Layout, parent: WidgetID, content: String) -> anyhow::Result<()> { + let label = WidgetLabel::create( + &mut layout.state, + WidgetLabelParams { + content: Translation::from_raw_text_string(content), + style: TextStyle { + wrap: true, + color: Some(drawing::Color::new(1.0, 0.5, 0.0, 1.0)), + weight: Some(FontWeight::Bold), + ..Default::default() + }, + }, + ); + layout.add_child(parent, label, Default::default())?; Ok(()) } + +pub fn create_icon(layout: &mut Layout, id_parent: WidgetID, size: Vec2, path: AssetPath) -> anyhow::Result { + let widget_sprite = WidgetSprite::create(WidgetSpriteParams { + color: None, + glyph_data: Some(CustomGlyphData::from_assets(&layout.state.globals, path)?), + }); + + let size = taffy::Size { + width: length(size.x), + height: length(size.y), + }; + + let (widget, _) = layout.add_child( + id_parent, + widget_sprite, + taffy::Style { + min_size: size.clone(), + max_size: size.clone(), + size: size.clone(), + ..Default::default() + }, + )?; + + Ok(widget.id) +} + +pub struct CreateLoadingParams<'a> { + pub layout: &'a mut Layout, + pub parent_id: WidgetID, + pub with_text: bool, +} + +pub fn create_loading(par: CreateLoadingParams) -> anyhow::Result { + let doc_params = ParseDocumentParams { + globals: par.layout.state.globals.clone(), + path: AssetPath::BuiltIn("gui/t_loading.xml"), + extra: Default::default(), + }; + + let mut parser_state = wgui::parser::parse_from_assets(&doc_params, par.layout, par.parent_id)?; + + let data = parser_state.realize_template( + &doc_params, + if par.with_text { + "LoadingWithText" + } else { + "LoadingWithoutText" + }, + par.layout, + par.parent_id, + Default::default(), + )?; + + let id_root = data.get_widget_id("root")?; + let id_sprite_loading = data.get_widget_id("sprite_loading")?; + + par.layout.animations.add(Animation::new( + id_sprite_loading, + 60 * 30, /* spin it for 30 seconds at most */ + AnimationEasing::Linear, + Box::new(move |common, data| { + // spin it + data.data.transform = centered_matrix(data.widget_boundary.size, &Mat4::from_rotation_z(data.pos * 400.0)); + if data.pos == 1.0 { + // remove the spinner, do not waste energy + common + .alterables + .tasks + .push(LayoutTask::RemoveWidget(id_sprite_loading)); + } + common.alterables.mark_redraw(); + }), + )); + + Ok(id_root) +} diff --git a/dash-frontend/src/views/app_launcher.rs b/dash-frontend/src/views/app_launcher.rs index 76c5b524..af9c398b 100644 --- a/dash-frontend/src/views/app_launcher.rs +++ b/dash-frontend/src/views/app_launcher.rs @@ -14,7 +14,11 @@ use wgui::{ }; use wlx_common::{config::GeneralConfig, dash_interface::BoxDashInterface, desktop_finder::DesktopEntry}; -use crate::frontend::{FrontendTask, FrontendTasks, SoundType}; +use crate::{ + frontend::{FrontendTask, FrontendTasks, SoundType}, + util::popup_manager::{MountPopupOnceParams, PopupHolder}, + views::{ViewTrait, ViewUpdateParams}, +}; #[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)] enum PosMode { @@ -66,7 +70,7 @@ struct LaunchParams<'a, T> { interface: &'a mut BoxDashInterface, auto_start: bool, data: &'a mut T, - on_launched: &'a dyn Fn(), + on_launched: Option>, } pub struct View { @@ -91,7 +95,7 @@ pub struct View { auto_start: bool, - on_launched: Box, + on_launched: Option>, } pub struct Params<'a> { @@ -101,7 +105,13 @@ pub struct Params<'a> { pub parent_id: WidgetID, pub config: &'a GeneralConfig, pub frontend_tasks: &'a FrontendTasks, - pub on_launched: Box, + pub on_launched: Box, +} + +impl ViewTrait for View { + fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> { + Ok(()) + } } impl View { @@ -280,7 +290,7 @@ impl View { entry: params.entry, frontend_tasks: params.frontend_tasks.clone(), globals: params.globals.clone(), - on_launched: params.on_launched, + on_launched: Some(params.on_launched), }) } @@ -316,7 +326,7 @@ impl View { auto_start: self.auto_start, interface, data, - on_launched: &self.on_launched, + on_launched: self.on_launched.take(), }); } @@ -334,7 +344,7 @@ impl View { )))); } - fn launch(params: LaunchParams) -> anyhow::Result<()> { + fn launch(mut params: LaunchParams) -> anyhow::Result<()> { let mut env = Vec::::new(); if params.compositor_mode == CompositorMode::Native { @@ -390,7 +400,9 @@ impl View { params.frontend_tasks.push(FrontendTask::PlaySound(SoundType::Launch)); - (*params.on_launched)(); + if let Some(on_launched) = params.on_launched.take() { + on_launched(); + } // we're done! Ok(()) @@ -420,3 +432,26 @@ impl View { [width as u32, height as u32] } } + +pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, entry: DesktopEntry, popup: PopupHolder) { + frontend_tasks + .clone() + .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( + Translation::from_raw_text(&entry.app_name), + Box::new(move |data| { + let on_launched = popup.get_close_callback(data.layout); + let view = View::new(Params { + entry: entry.clone(), + globals: &globals, + layout: data.layout, + parent_id: data.id_content, + frontend_tasks: &frontend_tasks, + config: data.config, + on_launched, + })?; + + popup.set_view(data.handle, view, None); + Ok(popup.get_close_callback(data.layout)) + }), + ))); +} diff --git a/dash-frontend/src/views/audio_settings.rs b/dash-frontend/src/views/audio_settings.rs index 8284ea50..9546d961 100644 --- a/dash-frontend/src/views/audio_settings.rs +++ b/dash-frontend/src/views/audio_settings.rs @@ -653,9 +653,6 @@ impl View { } fn update_button_highlights(&self, layout: &mut Layout) -> anyhow::Result<()> { - let mut c = layout.start_common(); - let mut common = c.common(); - let num: u8 = match &self.mode { CurrentMode::Sinks => 0, CurrentMode::Sources => 1, @@ -663,20 +660,21 @@ impl View { CurrentMode::CardProfileSelector(_) => 255, }; + let mut com = layout.common(); + let mut perform = |btn_num: u8, btn: &Rc| { let color = if num == btn_num { - common.state.theme.accent_color + com.state.theme.accent_color } else { - common.state.theme.button_color + com.state.theme.button_color }; - btn.set_color(&mut common, color); + btn.set_color(&mut com, color); }; perform(0, &self.btn_sinks); perform(1, &self.btn_sources); perform(2, &self.btn_cards); - c.finish()?; Ok(()) } @@ -801,14 +799,12 @@ impl View { par, )?; - let mut c = params.layout.start_common(); - let mut common = c.common(); - + let mut common = params.layout.common(); let checkbox = data.fetch_component_as::("checkbox")?; let btn_mute = data.fetch_component_as::("btn_mute")?; let slider = data.fetch_component_as::("slider")?; - slider.set_value(&mut common, params.control.on_volume_request()? / VOLUME_MULT); + slider.set_value_primary(&mut common, params.control.on_volume_request()? / VOLUME_MULT); checkbox.set_checked(&mut common, params.checked); @@ -838,8 +834,6 @@ impl View { }) }); - c.finish()?; - Ok(()) } diff --git a/dash-frontend/src/views/dialog_box.rs b/dash-frontend/src/views/dialog_box.rs new file mode 100644 index 00000000..eba712af --- /dev/null +++ b/dash-frontend/src/views/dialog_box.rs @@ -0,0 +1,127 @@ +use std::{collections::HashMap, rc::Rc}; + +use crate::{ + frontend::{FrontendTask, FrontendTasks}, + util::popup_manager::{MountPopupOnceParams, PopupHolder}, + views::{ViewTrait, ViewUpdateParams}, +}; +use wgui::{ + assets::AssetPath, + components::button::ComponentButton, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, WidgetID}, + parser::{Fetchable, ParseDocumentParams, ParserState}, + task::Tasks, + widget::label::WidgetLabel, +}; + +pub struct ButtonEntry { + pub content: Translation, // button text + pub icon: &'static str, // sprite_src_builtin + pub action: &'static str, // action name (will be passed into on_action_click) +} + +pub struct Params { + pub globals: WguiGlobals, + pub entries: Vec, + pub message: Translation, + pub on_action_click: Box, +} + +#[derive(Clone)] +enum Task { + ActionClicked(&'static str), +} + +pub struct View { + tasks: Tasks, + + #[allow(dead_code)] + parser_state: ParserState, + + on_action_click: Option>, + on_close_request: Option>, +} + +fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> { + ParseDocumentParams { + globals: globals.clone(), + path: AssetPath::BuiltIn("gui/view/dialog_box.xml"), + extra: Default::default(), + } +} + +impl ViewTrait for View { + fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> { + for task in self.tasks.drain() { + match task { + Task::ActionClicked(action) => { + if let Some(func) = self.on_action_click.take() { + func(action); + } + + if let Some(on_close) = self.on_close_request.take() { + on_close(); + } + } + } + } + Ok(()) + } +} + +impl View { + pub fn new( + layout: &mut Layout, + id_parent: WidgetID, + on_close_request: Box, + par: Params, + ) -> anyhow::Result { + let tasks = Tasks::::new(); + + let mut parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), layout, id_parent)?; + let id_buttons = parser_state.get_widget_id("buttons")?; + + { + let label_message = parser_state.fetch_widget(&layout.state, "label_message")?.widget; + label_message + .cast::()? + .set_text(&mut layout.common(), par.message); + } + + for entry in par.entries { + let mut t_par = HashMap::, Rc>::new(); + t_par.insert(Rc::from("icon"), Rc::from(entry.icon)); + + let data = + parser_state.realize_template(&doc_params(&par.globals), "DialogBoxButton", layout, id_buttons, t_par)?; + + let button = data.fetch_component_as::("btn")?; + button.set_text(&mut layout.common(), entry.content.clone()); + button.on_click(tasks.get_button_click_callback(Task::ActionClicked(entry.action))); + } + + Ok(Self { + tasks, + parser_state, + on_action_click: Some(par.on_action_click), + on_close_request: Some(on_close_request), + }) + } +} + +pub fn mount_popup(popup: PopupHolder, frontend_tasks: FrontendTasks, params: Params) { + frontend_tasks + .clone() + .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( + Translation::from_raw_text("Info"), + Box::new(move |data| { + let on_close_request = popup.get_close_callback(data.layout); + let view = View::new(data.layout, data.id_content, on_close_request, params)?; + + popup.set_view(data.handle, view, None); + Ok(popup.get_close_callback(data.layout)) + }), + ))); +} diff --git a/dash-frontend/src/views/download_file.rs b/dash-frontend/src/views/download_file.rs new file mode 100644 index 00000000..30b299ff --- /dev/null +++ b/dash-frontend/src/views/download_file.rs @@ -0,0 +1,265 @@ +use crate::{ + frontend::{FrontendTask, FrontendTasks}, + util::{ + networking::http_client::{self, ProgressFuncData}, + popup_manager::{MountPopupOnceParams, PopupHolder}, + wgui_simple, + }, + views::{ViewTrait, ViewUpdateParams}, +}; +use glam::Vec2; +use std::path::PathBuf; +use wgui::{ + assets::AssetPath, + components::button::ComponentButton, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, WidgetID}, + parser::{Fetchable, ParseDocumentParams, ParserState}, + task::Tasks, + widget::label::WidgetLabel, +}; +use wlx_common::async_executor::AsyncExecutor; + +pub struct Params { + pub globals: WguiGlobals, + pub executor: AsyncExecutor, + pub target_path: PathBuf, + pub url: String, + pub on_downloaded: Box, +} + +#[derive(Clone)] +enum Task { + StartDownload(/*url*/ String, /*target path*/ PathBuf), + SetStatusText(String), + ShowIconSuccess, + ShowIconError, + Close, +} + +pub struct View { + globals: WguiGlobals, + tasks: Tasks, + executor: AsyncExecutor, + + #[allow(dead_code)] + parser_state: ParserState, + + id_label_status: WidgetID, + id_loading_parent: WidgetID, + id_content: WidgetID, + on_close_request: Option>, + on_downloaded: Option>, +} + +fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> { + ParseDocumentParams { + globals: globals.clone(), + path: AssetPath::BuiltIn("gui/view/download_file.xml"), + extra: Default::default(), + } +} + +impl ViewTrait for View { + fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { + for task in self.tasks.drain() { + match task { + Task::StartDownload(url, path) => { + if let Some(on_downloaded) = self.on_downloaded.take() { + self + .executor + .spawn(View::download( + self.tasks.clone(), + self.executor.clone(), + url, + path, + on_downloaded, + )) + .detach(); + } + } + Task::SetStatusText(text) => { + let widgets = &mut par.layout.state.widgets; + widgets + .fetch(self.id_label_status)? + .cast::()? + .set_text(&mut par.layout.common(), Translation::from_raw_text_string(text)); + } + Task::ShowIconSuccess => { + par.layout.remove_children(self.id_loading_parent); + wgui_simple::create_icon( + par.layout, + self.id_loading_parent, + Vec2::splat(32.0), + AssetPath::BuiltIn("dashboard/check.svg"), + )?; + + // "Close window" button + self + .parser_state + .realize_template( + &doc_params(&self.globals), + "btn_close", + par.layout, + self.id_content, + Default::default(), + )? + .fetch_component_as::("btn")? + .on_click(self.tasks.get_button_click_callback(Task::Close)); + } + Task::ShowIconError => { + par.layout.remove_children(self.id_loading_parent); + wgui_simple::create_icon( + par.layout, + self.id_loading_parent, + Vec2::splat(32.0), + AssetPath::BuiltIn("dashboard/error.svg"), + )?; + } + Task::Close => { + if let Some(on_close) = self.on_close_request.take() { + on_close(); + } + } + } + } + Ok(()) + } +} + +fn handle_async_result(error_reason: &'static str, tasks: &Tasks, result: anyhow::Result) -> Option +where + E: std::fmt::Debug, +{ + match result { + Ok(res) => Some(res), + Err(e) => { + tasks.push(Task::ShowIconError); + tasks.push(Task::SetStatusText(format!("{}: {:?}", error_reason, e))); + None + } + } +} + +impl View { + pub fn new( + layout: &mut Layout, + id_parent: WidgetID, + on_close_request: Box, + par: Params, + ) -> anyhow::Result { + let tasks = Tasks::::new(); + + let parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), layout, id_parent)?; + let id_label_status = parser_state.get_widget_id("label_status")?; + let id_content = parser_state.get_widget_id("content")?; + let id_loading_parent = parser_state.get_widget_id("loading_parent")?; + + wgui_simple::create_loading(wgui_simple::CreateLoadingParams { + parent_id: id_loading_parent, + layout: layout, + with_text: false, + })?; + + let str_target_path = par.globals.i18n().translate("TARGET_PATH"); + + { + let label_target_path = parser_state.fetch_widget(&layout.state, "label_target_path")?.widget; + label_target_path.cast::()?.set_text( + &mut layout.common(), + Translation::from_raw_text_string(format!("{}: {}", str_target_path, par.target_path.display())), + ); + } + + tasks.push(Task::StartDownload(par.url, par.target_path)); + + Ok(Self { + tasks, + globals: par.globals.clone(), + executor: par.executor.clone(), + parser_state, + id_label_status, + id_loading_parent, + id_content, + on_close_request: Some(on_close_request), + on_downloaded: Some(par.on_downloaded), + }) + } + + async fn download( + tasks: Tasks, + executor: AsyncExecutor, + url: String, + target_path: PathBuf, + on_downloaded: Box, + ) -> Option<()> { + tasks.push(Task::SetStatusText(String::from("Connecting to the server..."))); + + // start downloading from the server with progress reporting + let res = handle_async_result( + "Download failed", + &tasks, + http_client::get(http_client::GetParams { + executor: &executor, + url: &url, + on_progress: Some(Box::new({ + let tasks = tasks.clone(); + move |data: ProgressFuncData| { + tasks.push(Task::SetStatusText(format!( + "{}/{} KiB ({}%)", + data.bytes_downloaded / 1024, + data.file_size / 1024, + (data.bytes_downloaded as f32 / data.file_size as f32 * 100.0).round() + ))) + } + })), + }) + .await, + )?; + + tasks.push(Task::SetStatusText(String::from("Writing to file..."))); + + // create skymaps directory if it doesn't exist yet + if let Some(parent) = target_path.parent() { + handle_async_result( + "Directory creation failed", + &tasks, + smol::fs::create_dir_all(parent).await, + )?; + } + + handle_async_result( + "File write failed", + &tasks, + smol::fs::write(target_path, res.data).await, + )?; + + tasks.push(Task::SetStatusText(String::from("Download finished"))); + tasks.push(Task::ShowIconSuccess); + + on_downloaded(); + + None + } +} + +pub fn mount_popup( + popup: PopupHolder, + frontend_tasks: FrontendTasks, + on_view_close: Box, + params: Params, +) { + frontend_tasks + .clone() + .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( + Translation::from_translation_key("DOWNLOADER"), + Box::new(move |data| { + let on_close_request = popup.get_close_callback(data.layout); + let view = View::new(data.layout, data.id_content, on_close_request, params)?; + + popup.set_view(data.handle, view, Some(on_view_close)); + Ok(popup.get_close_callback(data.layout)) + }), + ))); +} diff --git a/dash-frontend/src/views/game_cover.rs b/dash-frontend/src/views/game_cover.rs index 6adc4110..5258b041 100644 --- a/dash-frontend/src/views/game_cover.rs +++ b/dash-frontend/src/views/game_cover.rs @@ -30,6 +30,7 @@ use wlx_common::async_executor::AsyncExecutor; use crate::util::{ cached_fetcher::{self, CoverArt}, steam_utils::{self, AppID}, + wgui_simple, }; pub struct ViewCommon { @@ -48,6 +49,7 @@ pub struct Params<'a, 'b> { pub struct View { pub button: Rc, id_image_parent: WidgetID, + id_loading: WidgetID, app_name: String, app_id: AppID, } @@ -143,6 +145,8 @@ impl View { layout: &mut Layout, cover_art: &CoverArt, ) -> anyhow::Result<()> { + layout.remove_widget(self.id_loading); + if cover_art.compressed_image_data.is_empty() { // mount placeholder let img = view_common.get_placeholder_image()?.clone(); @@ -271,6 +275,12 @@ impl View { rect_gradient_style(taffy::AlignSelf::End, 0.05), )?; + let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams { + layout: params.ess.layout, + parent_id: image_parent.id, + with_text: false, + })?; + // request cover image data from the internet or disk cache params .executor @@ -286,6 +296,7 @@ impl View { id_image_parent: image_parent.id, app_name: params.manifest.name.clone(), app_id: params.manifest.app_id.clone(), + id_loading, }) } } diff --git a/dash-frontend/src/views/game_launcher.rs b/dash-frontend/src/views/game_launcher.rs index 03d26d77..17722bc1 100644 --- a/dash-frontend/src/views/game_launcher.rs +++ b/dash-frontend/src/views/game_launcher.rs @@ -4,9 +4,10 @@ use crate::{ frontend::{FrontendTask, FrontendTasks, SoundType}, util::{ cached_fetcher::{self, CoverArt}, + popup_manager::{MountPopupOnceParams, PopupHolder}, steam_utils::{self, AppID, AppManifest}, }, - views::game_cover, + views::{ViewTrait, ViewUpdateParams, game_cover}, }; use wgui::{ assets::AssetPath, @@ -34,13 +35,14 @@ pub struct Params<'a> { pub layout: &'a mut Layout, pub parent_id: WidgetID, pub frontend_tasks: &'a FrontendTasks, - pub on_launched: Box, + pub on_launched: Box, } + pub struct View { #[allow(dead_code)] state: ParserState, tasks: Tasks, - on_launched: Box, + on_launched: Option>, frontend_tasks: FrontendTasks, game_cover_view_common: game_cover::ViewCommon, @@ -48,6 +50,30 @@ pub struct View { app_id: AppID, } +impl ViewTrait for View { + fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { + loop { + let tasks = self.tasks.drain(); + if tasks.is_empty() { + break; + } + for task in tasks { + match task { + Task::FillAppDetails(details) => self.action_fill_app_details(&mut par.layout, details)?, + Task::Launch => self.action_launch(), + Task::SetCoverArt(cover_art) => { + let _ = self + .view_cover + .set_cover_art(&mut self.game_cover_view_common, &mut par.layout, &cover_art); + } + } + } + } + + Ok(()) + } +} + impl View { async fn fetch_details(executor: AsyncExecutor, tasks: Tasks, app_id: AppID) { let Some(details) = cached_fetcher::get_app_details_json(executor, app_id).await else { @@ -104,7 +130,7 @@ impl View { Ok(Self { state, tasks, - on_launched: params.on_launched, + on_launched: Some(params.on_launched), frontend_tasks: params.frontend_tasks.clone(), game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()), view_cover, @@ -112,43 +138,20 @@ impl View { }) } - pub fn update(&mut self, layout: &mut Layout) -> anyhow::Result<()> { - loop { - let tasks = self.tasks.drain(); - if tasks.is_empty() { - break; - } - for task in tasks { - match task { - Task::FillAppDetails(details) => self.action_fill_app_details(layout, details)?, - Task::Launch => self.action_launch(), - Task::SetCoverArt(cover_art) => { - let _ = self - .view_cover - .set_cover_art(&mut self.game_cover_view_common, layout, &cover_art); - } - } - } - } - - Ok(()) - } - fn action_fill_app_details( &mut self, layout: &mut Layout, mut details: cached_fetcher::AppDetailsJSONData, ) -> anyhow::Result<()> { - let mut c = layout.start_common(); - { - let label_author = self.state.fetch_widget(&c.layout.state, "label_author")?.widget; - let label_description = self.state.fetch_widget(&c.layout.state, "label_description")?.widget; + let mut c = layout.common(); + let label_author = self.state.fetch_widget(&c.state, "label_author")?.widget; + let label_description = self.state.fetch_widget(&c.state, "label_description")?.widget; if let Some(developer) = details.developers.pop() { label_author .cast::()? - .set_text(&mut c.common(), Translation::from_raw_text_string(developer)); + .set_text(&mut c, Translation::from_raw_text_string(developer)); } let desc = if let Some(desc) = &details.short_description { @@ -162,11 +165,10 @@ impl View { if let Some(desc) = desc { label_description .cast::()? - .set_text(&mut c.common(), Translation::from_raw_text(desc)); + .set_text(&mut c, Translation::from_raw_text(desc)); } } - c.finish()?; Ok(()) } @@ -190,6 +192,37 @@ impl View { } } - (*self.on_launched)(); + if let Some(on_launched) = self.on_launched.take() { + on_launched(); + } } } + +pub fn mount_popup( + frontend_tasks: FrontendTasks, + executor: AsyncExecutor, + globals: WguiGlobals, + manifest: AppManifest, + popup: PopupHolder, +) { + frontend_tasks + .clone() + .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( + Translation::from_raw_text(&manifest.name), + Box::new(move |data| { + let on_launched = popup.get_close_callback(data.layout); + let view = View::new(Params { + manifest: manifest.clone(), + executor: executor.clone(), + globals: &globals, + layout: data.layout, + parent_id: data.id_content, + frontend_tasks: &frontend_tasks, + on_launched, + })?; + + popup.set_view(data.handle, view, None); + Ok(popup.get_close_callback(data.layout)) + }), + ))); +} diff --git a/dash-frontend/src/views/game_list.rs b/dash-frontend/src/views/game_list.rs index 5de3cf64..ab7e6448 100644 --- a/dash-frontend/src/views/game_list.rs +++ b/dash-frontend/src/views/game_list.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use std::{collections::HashMap, rc::Rc}; use wgui::{ assets::AssetPath, @@ -16,20 +16,19 @@ use wgui::{ use wlx_common::async_executor::AsyncExecutor; use crate::{ - frontend::{FrontendTask, FrontendTasks}, + frontend::FrontendTasks, util::{ cached_fetcher::CoverArt, - popup_manager::{MountPopupParams, PopupHandle}, + popup_manager::PopupHolder, steam_utils::{self, AppID, SteamUtils}, }, - views::{self, game_cover, game_launcher}, + views::{self, ViewTrait, ViewUpdateParams, game_cover}, }; #[derive(Clone)] enum Task { AppManifestClicked(steam_utils::AppManifest), SetCoverArt(AppID, Rc), - CloseLauncher, LoadManifests, FillPage(u32), PrevPage, @@ -42,6 +41,7 @@ pub struct Params<'a> { pub frontend_tasks: FrontendTasks, pub layout: &'a mut Layout, pub parent_id: WidgetID, + pub steam_utils: &'a SteamUtils, } const MAX_GAMES_PER_PAGE: u32 = 30; @@ -50,10 +50,6 @@ pub struct GameCoverCell { view_cover: game_cover::View, } -struct State { - view_launcher: Option<(PopupHandle, views::game_launcher::View)>, -} - pub struct View { #[allow(dead_code)] parser_state: ParserState, @@ -63,12 +59,37 @@ pub struct View { id_list_parent: WidgetID, game_cover_view_common: game_cover::ViewCommon, executor: AsyncExecutor, - state: Rc>, mounted_game_covers: HashMap, all_manifests: Vec, cur_page: u32, page_count: u32, id_label_page: WidgetID, + view_launcher: PopupHolder, + steam_utils: SteamUtils, +} + +impl ViewTrait for View { + fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { + loop { + let tasks = self.tasks.drain(); + if tasks.is_empty() { + break; + } + for task in tasks { + match task { + Task::LoadManifests => self.load_manifests(), + Task::FillPage(page_idx) => self.fill_page(&mut par.layout, &mut par.executor, page_idx)?, + Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?, + Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(&mut par.layout, app_id, cover_art), + Task::PrevPage => self.page_prev(), + Task::NextPage => self.page_next(), + } + } + } + + self.view_launcher.update(par)?; + Ok(()) + } } impl View { @@ -105,46 +126,15 @@ impl View { id_list_parent: list_parent.id, mounted_game_covers: HashMap::new(), game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()), - state: Rc::new(RefCell::new(State { view_launcher: None })), executor: params.executor, all_manifests: Vec::new(), cur_page: 0, page_count: 0, id_label_page, + view_launcher: Default::default(), + steam_utils: params.steam_utils.clone(), }) } - - pub fn update( - &mut self, - layout: &mut Layout, - steam_utils: &mut SteamUtils, - executor: &AsyncExecutor, - ) -> anyhow::Result<()> { - loop { - let tasks = self.tasks.drain(); - if tasks.is_empty() { - break; - } - for task in tasks { - match task { - Task::LoadManifests => self.load_manifests(steam_utils), - Task::FillPage(page_idx) => self.fill_page(layout, executor, page_idx)?, - Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?, - Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(layout, app_id, cover_art), - Task::CloseLauncher => self.state.borrow_mut().view_launcher = None, - Task::PrevPage => self.page_prev(), - Task::NextPage => self.page_next(), - } - } - } - - let mut state = self.state.borrow_mut(); - if let Some((_, view)) = &mut state.view_launcher { - view.update(layout)?; - } - - Ok(()) - } } fn fill_game_list( @@ -187,8 +177,11 @@ fn fill_game_list( } impl View { - fn load_manifests(&mut self, steam_utils: &mut SteamUtils) { - match steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc) { + fn load_manifests(&mut self) { + match self + .steam_utils + .list_installed_games(steam_utils::GameSortMethod::PlayDateDesc) + { Ok(manifests) => { self.page_count = (manifests.len() as u32 + MAX_GAMES_PER_PAGE) / MAX_GAMES_PER_PAGE; self.all_manifests = manifests; @@ -233,16 +226,14 @@ impl View { } // set page text - let mut c = layout.start_common(); { - let mut common = c.common(); - let mut widget = common.state.widgets.cast_as::(self.id_label_page)?; + let mut c = layout.common(); + let mut widget = c.state.widgets.cast_as::(self.id_label_page)?; widget.set_text( - &mut common, + &mut c, Translation::from_raw_text_string(format!("{}/{}", self.cur_page + 1, self.page_count)), ); } - c.finish()?; fill_game_list( &mut ConstructEssentials { @@ -283,36 +274,13 @@ impl View { } fn action_app_manifest_clicked(&mut self, manifest: steam_utils::AppManifest) -> anyhow::Result<()> { - self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams { - title: Translation::from_raw_text(&manifest.name), - on_content: { - let state = self.state.clone(); - let tasks = self.tasks.clone(); - let executor = self.executor.clone(); - let globals = self.globals.clone(); - let frontend_tasks = self.frontend_tasks.clone(); - - Rc::new(move |data| { - let on_launched = { - let tasks = tasks.clone(); - Box::new(move || tasks.push(Task::CloseLauncher)) - }; - - let view = game_launcher::View::new(game_launcher::Params { - manifest: manifest.clone(), - executor: executor.clone(), - globals: &globals, - layout: data.layout, - parent_id: data.id_content, - frontend_tasks: &frontend_tasks, - on_launched, - })?; - - state.borrow_mut().view_launcher = Some((data.handle, view)); - Ok(()) - }) - }, - })); + views::game_launcher::mount_popup( + self.frontend_tasks.clone(), + self.executor.clone(), + self.globals.clone(), + manifest, + self.view_launcher.clone(), + ); Ok(()) } diff --git a/dash-frontend/src/views/mod.rs b/dash-frontend/src/views/mod.rs index d2cd9369..918c67fc 100644 --- a/dash-frontend/src/views/mod.rs +++ b/dash-frontend/src/views/mod.rs @@ -1,6 +1,25 @@ +use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_interface::ConfigChangeKind}; + pub mod app_launcher; pub mod audio_settings; +pub mod dialog_box; +pub mod download_file; pub mod game_cover; pub mod game_launcher; pub mod game_list; +pub mod remote_skymap_downloader; +pub mod remote_skymap_list; pub mod running_games_list; +pub mod skymap_list; +pub mod skymap_list_cell; + +pub struct ViewUpdateParams<'a> { + pub layout: &'a mut wgui::layout::Layout, + pub executor: &'a AsyncExecutor, + pub general_config: &'a mut GeneralConfig, + pub config_change_kind: &'a mut Option, +} + +pub trait ViewTrait { + fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()>; +} diff --git a/dash-frontend/src/views/remote_skymap_downloader.rs b/dash-frontend/src/views/remote_skymap_downloader.rs new file mode 100644 index 00000000..4a2aa537 --- /dev/null +++ b/dash-frontend/src/views/remote_skymap_downloader.rs @@ -0,0 +1,396 @@ +use std::{collections::HashMap, rc::Rc}; + +use crate::{ + frontend::{FrontendTask, FrontendTasks}, + util::{ + networking::{self, skymap_catalog::SkymapResolution}, + popup_manager::{MountPopupOnceParams, PopupHolder}, + }, + views::{self, ViewTrait, ViewUpdateParams}, +}; +use anyhow::Context; +use wgui::{ + assets::AssetPath, + components::button::ComponentButton, + drawing::Color, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, WidgetID}, + parser::{Fetchable, ParseDocumentParams, ParserState}, + renderer_vk::text::custom_glyph::CustomGlyphData, + task::Tasks, + widget::{image::WidgetImage, label::WidgetLabel}, +}; +use wlx_common::{async_executor::AsyncExecutor, config_io, dash_interface::ConfigChangeKind}; + +pub struct Params<'a> { + pub globals: &'a WguiGlobals, + pub layout: &'a mut Layout, + pub executor: &'a AsyncExecutor, + pub frontend_tasks: FrontendTasks, + pub parent_id: WidgetID, + pub entry: networking::skymap_catalog::SkymapCatalogEntry, + pub preview_image: CustomGlyphData, + pub preview_image_compressed: Rc>, + pub on_updated_library: Rc, +} + +#[derive(Clone)] +enum Task { + Refresh, + ResolutionClicked(SkymapResolution), + DownloadFinished, + RunDownload(SkymapResolution), + RemoveFile(SkymapResolution), + SetSkymap(SkymapResolution), +} + +pub struct View { + entry: networking::skymap_catalog::SkymapCatalogEntry, + frontend_tasks: FrontendTasks, + globals: WguiGlobals, + tasks: Tasks, + executor: AsyncExecutor, + + id_resolution_buttons: WidgetID, + + #[allow(dead_code)] + parser_state: ParserState, + + popup_download: PopupHolder, + popup_dialog_box: PopupHolder, + + preview_image_compressed: Rc>, + on_updated_library: Rc, +} + +fn mount_resolution_button( + layout: &mut Layout, + parser_state: &mut ParserState, + doc_params: &ParseDocumentParams, + parent_id: WidgetID, + res: SkymapResolution, + tasks: &Tasks, + already_downloaded: bool, +) -> anyhow::Result<()> { + let mut t = HashMap::, Rc>::new(); + t.insert(Rc::from("text"), Rc::from(res.get_display_str())); + t.insert( + Rc::from("sprite"), + Rc::from(match already_downloaded { + true => "dashboard/check.svg", + false => "dashboard/download.svg", + }), + ); + let data = parser_state.realize_template(doc_params, "ResolutionButton", layout, parent_id, t)?; + let button = data.fetch_component_as::("button")?; + + if already_downloaded { + button.set_color(&mut layout.common(), Color::new(0.0, 0.4, 0.0, 1.0)); // green + } + tasks.handle_button(&button, Task::ResolutionClicked(res)); + Ok(()) +} + +impl ViewTrait for View { + fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { + for task in self.tasks.drain() { + match task { + Task::ResolutionClicked(resolution) => { + self.resolution_clicked(resolution)?; + } + Task::Refresh => { + self.refresh(par.layout)?; + } + Task::DownloadFinished => { + self.download_finished()?; + } + Task::RunDownload(resolution) => { + self.run_download(resolution)?; + } + Task::RemoveFile(resolution) => { + self.remove_file(resolution)?; + } + Task::SetSkymap(resolution) => { + let skymap_file_path = self + .entry + .get_destination_path(resolution) + .context("Skymap not found" /* you shouldn't really see this, like ever. */)?; + + par.general_config.skybox_texture = skymap_file_path + .to_str() + .context("Skymap filename not valid UTF-8")? + .into(); + *par.config_change_kind = Some(ConfigChangeKind::EnvironmentBlend); + } + } + } + + self.popup_download.update(par)?; + self.popup_dialog_box.update(par)?; + Ok(()) + } +} + +fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> { + ParseDocumentParams { + globals: globals.clone(), + path: AssetPath::BuiltIn("gui/view/remote_skymap_downloader.xml"), + extra: Default::default(), + } +} + +impl View { + pub fn new(par: Params) -> anyhow::Result { + let tasks = Tasks::::new(); + + let parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), par.layout, par.parent_id)?; + let id_resolution_buttons = parser_state.get_widget_id("resolution_buttons")?; + + let str_version = par.globals.i18n().translate("VERSION"); + let str_creation_date = par.globals.i18n().translate("CREATION_DATE"); + let str_modification_date = par.globals.i18n().translate("MODIFICATION_DATE"); + + let image = parser_state.fetch_widget(&par.layout.state, "image")?.widget; + let mut image = image.cast::()?; + image.set_content(&mut par.layout.alterables, Some(par.preview_image)); + + // Set author label + parser_state + .fetch_widget_as::(&par.layout.state, "label_author")? + .set_text_simple( + &mut par.globals.get(), + Translation::from_raw_text_string(format!("by {}", par.entry.author)), + ); + + // Set description label + parser_state + .fetch_widget_as::(&par.layout.state, "label_description")? + .set_text_simple( + &mut par.globals.get(), + Translation::from_raw_text(&par.entry.description), + ); + + // Set version label + parser_state + .fetch_widget_as::(&par.layout.state, "label_version")? + .set_text_simple( + &mut par.globals.get(), + Translation::from_raw_text_string(format!("{}: {}", str_version, par.entry.version)), + ); + + // Set creation date label + parser_state + .fetch_widget_as::(&par.layout.state, "label_creation_date")? + .set_text_simple( + &mut par.globals.get(), + Translation::from_raw_text_string(format!("{}: {}", str_creation_date, par.entry.created_at)), + ); + + // Set modification date label + parser_state + .fetch_widget_as::(&par.layout.state, "label_modification_date")? + .set_text_simple( + &mut par.globals.get(), + Translation::from_raw_text_string(format!("{}: {}", str_modification_date, par.entry.created_at)), + ); + + tasks.push(Task::Refresh); + + Ok(Self { + tasks, + globals: par.globals.clone(), + executor: par.executor.clone(), + entry: par.entry, + parser_state, + frontend_tasks: par.frontend_tasks, + popup_download: Default::default(), + popup_dialog_box: Default::default(), + id_resolution_buttons, + preview_image_compressed: par.preview_image_compressed, + on_updated_library: par.on_updated_library, + }) + } + + fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> { + layout.remove_children(self.id_resolution_buttons); + + let files = &self.entry.files; + let mut mount_res = |res: SkymapResolution| -> anyhow::Result<()> { + mount_resolution_button( + layout, + &mut self.parser_state, + &doc_params(&self.globals), + self.id_resolution_buttons, + res, + &self.tasks, + self.entry.is_downloaded(res)?, + ) + }; + + mount_res(SkymapResolution::Res2k)?; + if files.size_4k.is_some() { + mount_res(SkymapResolution::Res4k)?; + } + if files.size_8k.is_some() { + mount_res(SkymapResolution::Res8k)?; + } + Ok(()) + } + + fn resolution_clicked(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> { + let is_downloaded = self.entry.is_downloaded(resolution).unwrap_or(false); + if !is_downloaded { + self.tasks.push(Task::RunDownload(resolution)); + } else { + self.show_dialog_box_action(resolution)?; + } + Ok(()) + } + + fn show_dialog_box_action(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> { + const ACTION_REMOVE: &'static str = "remove"; + const ACTION_DOWNLOAD_AGAIN: &'static str = "download_again"; + const ACTION_APPLY: &'static str = "apply"; + + let tasks = self.tasks.clone(); + + views::dialog_box::mount_popup( + self.popup_dialog_box.clone(), + self.frontend_tasks.clone(), + views::dialog_box::Params { + globals: self.globals.clone(), + message: Translation::from_translation_key("APP_SETTINGS.SKYMAP_ALREADY_DOWNLOADED"), + entries: vec![ + views::dialog_box::ButtonEntry { + content: Translation::from_translation_key("REMOVE"), + icon: "dashboard/trash.svg", + action: ACTION_REMOVE, + }, + views::dialog_box::ButtonEntry { + content: Translation::from_translation_key("DOWNLOAD_AGAIN"), + icon: "dashboard/download.svg", + action: ACTION_DOWNLOAD_AGAIN, + }, + views::dialog_box::ButtonEntry { + content: Translation::from_translation_key("APPLY"), + icon: "dashboard/check.svg", + action: ACTION_APPLY, + }, + ], + on_action_click: Box::new(move |action| match action { + ACTION_REMOVE => { + tasks.push(Task::RemoveFile(resolution)); + tasks.push(Task::Refresh); + } + ACTION_DOWNLOAD_AGAIN => { + tasks.push(Task::RunDownload(resolution)); + tasks.push(Task::Refresh); + } + ACTION_APPLY => { + tasks.push(Task::SetSkymap(resolution)); + tasks.push(Task::Refresh); + } + _ => unreachable!(), + }), + }, + ); + + Ok(()) + } + + fn download_finished(&mut self) -> anyhow::Result<()> { + self.entry.save_metadata()?; + let mut uuids = config_io::get_skymaps_uuids().unwrap_or_default(); + let uuid_str = self.entry.uuid.to_string(); + if !uuids.contains(&uuid_str) { + uuids.push(uuid_str); + } + config_io::set_skymaps_uuids(&uuids)?; + + // Save preview image + self.entry.files.save_preview_to_file(&self.preview_image_compressed)?; + + (*self.on_updated_library)(); + + Ok(()) + } + + fn run_download(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> { + let Some(url) = self.entry.files.get_url_from_res(resolution) else { + return Ok(()); + }; + + let Some(target_path) = self.entry.get_destination_path(resolution) else { + return Ok(()); + }; + + views::download_file::mount_popup( + self.popup_download.clone(), + self.frontend_tasks.clone(), + self.tasks.make_callback_box(Task::Refresh), + views::download_file::Params { + globals: self.globals.clone(), + executor: self.executor.clone(), + target_path, + url, + on_downloaded: self.tasks.make_callback_box(Task::DownloadFinished), + }, + ); + Ok(()) + } + + fn remove_file(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> { + self.entry.remove_file(resolution); + + if !self.entry.has_any_downloaded() { + // all skymaps of this uuid are removed, clean-up files + self.entry.remove_metadata(); + + // remove uuid of this entry from downloaded skymaps uuid and save the file again + let mut uuids = config_io::get_skymaps_uuids().unwrap_or_default(); + uuids.retain(|uuid| *uuid != self.entry.uuid.to_string()); + config_io::set_skymaps_uuids(&uuids)?; + + // remove "_preview.dds" files from the disk too + self.entry.files.remove_preview_file(); + } + + (*self.on_updated_library)(); + + Ok(()) + } +} + +pub fn mount_popup( + frontend_tasks: FrontendTasks, + executor: AsyncExecutor, + globals: WguiGlobals, + entry: networking::skymap_catalog::SkymapCatalogEntry, + preview_image: CustomGlyphData, + preview_image_compressed: Rc>, + on_updated_library: Rc, + popup: PopupHolder, +) { + frontend_tasks + .clone() + .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( + Translation::from_raw_text(&entry.name), + Box::new(move |data| { + let view = View::new(Params { + globals: &globals, + layout: data.layout, + executor: &executor, + parent_id: data.id_content, + entry, + preview_image, + frontend_tasks: frontend_tasks.clone(), + preview_image_compressed, + on_updated_library, + })?; + + popup.set_view(data.handle, view, None); + Ok(popup.get_close_callback(data.layout)) + }), + ))); +} diff --git a/dash-frontend/src/views/remote_skymap_list.rs b/dash-frontend/src/views/remote_skymap_list.rs new file mode 100644 index 00000000..76c171dc --- /dev/null +++ b/dash-frontend/src/views/remote_skymap_list.rs @@ -0,0 +1,314 @@ +use std::rc::Rc; + +use uuid::Uuid; +use wgui::{ + assets::AssetPath, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, WidgetID}, + parser::{Fetchable, ParseDocumentParams}, + renderer_vk::text::custom_glyph::CustomGlyphData, + task::Tasks, +}; +use wlx_common::async_executor::AsyncExecutor; + +use crate::{ + frontend::{FrontendTask, FrontendTasks}, + util::{ + networking::{ + self, + skymap_catalog::{SkymapCatalog, SkymapCatalogEntry, SkymapUuid}, + }, + popup_manager::{MountPopupOnceParams, PopupHolder}, + wgui_simple, + }, + views::{self, ViewTrait, ViewUpdateParams}, +}; + +pub struct Params<'a> { + pub globals: &'a WguiGlobals, + pub layout: &'a mut Layout, + pub executor: &'a AsyncExecutor, + pub parent_id: WidgetID, + pub frontend_tasks: FrontendTasks, + pub on_updated_library: Rc, +} + +#[derive(Clone)] +enum Task { + SetSkymapCatalog(Rc>), + SetSkymapPreview( + ( + SkymapUuid, + Option<( + CustomGlyphData, /* ready-to-use preview image data */ + Rc>, /* compressed preview image data (should weigh about 10-15 KiB) */ + )>, + ), + ), + ShowRemoteSkymapDownloader(SkymapUuid), + RefreshCells, +} + +struct MountedCell { + skymap_uuid: SkymapUuid, + view: views::skymap_list_cell::View, + preview_image_compressed: Option>>, +} + +pub struct View { + id_parent: WidgetID, + id_loading: WidgetID, + globals: WguiGlobals, + tasks: Tasks, + mounted_cells: Vec, + executor: AsyncExecutor, + frontend_tasks: FrontendTasks, + catalog: Option, + popup_remote_skymap_downloader: PopupHolder, + on_updated_library: Rc, +} + +fn get_entry_by_uuid(catalog: &SkymapCatalog, skymap_uuid: Uuid) -> Option<&SkymapCatalogEntry> { + let Some(entry) = catalog.entries.iter().find(|entry| entry.uuid == skymap_uuid) else { + return None; + }; + + Some(entry) +} + +impl ViewTrait for View { + fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { + self.popup_remote_skymap_downloader.update(par)?; + + for task in self.tasks.drain() { + match task { + Task::SetSkymapCatalog(skymap_catalog) => { + par.layout.remove_widget(self.id_loading); + match &*skymap_catalog { + Ok(skymap_catalog) => { + self.mount_catalog(par.layout, skymap_catalog)?; + } + Err(e) => wgui_simple::create_label_error( + par.layout, + self.id_parent, + format!("Failed to fetch skymap catalog: {:?}", e), + )?, + } + } + Task::SetSkymapPreview((skymap_uuid, opt_preview_image)) => { + if let Some(cell) = &mut self + .mounted_cells + .iter_mut() + .find(|cell| cell.skymap_uuid == skymap_uuid) + { + if let Some((preview_image, preview_image_compressed)) = opt_preview_image { + cell.view.set_image(par.layout, Some(preview_image))?; + cell.preview_image_compressed = Some(preview_image_compressed); + } else { + cell.view.set_image(par.layout, None)?; + } + } + } + Task::ShowRemoteSkymapDownloader(skymap_uuid) => { + if let Some((preview_image, preview_image_compressed)) = self.get_image_preview(skymap_uuid) { + self.show_remote_skymap_downloader(skymap_uuid, preview_image, preview_image_compressed)?; + } else { + log::error!("preview image not present, ignoring request"); + } + } + Task::RefreshCells => { + self.refresh_cells(par.layout)?; + } + } + } + Ok(()) + } +} + +impl View { + async fn skymap_catalog_request_wrapper(tasks: Tasks, executor: AsyncExecutor) { + let res = networking::skymap_catalog::request_catalog(&executor).await; + tasks.push(Task::SetSkymapCatalog(Rc::new(res))) + } + + pub fn new(par: Params) -> anyhow::Result { + let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams { + layout: par.layout, + parent_id: par.parent_id, + with_text: true, + })?; + let tasks = Tasks::::new(); + let fut = View::skymap_catalog_request_wrapper(tasks.clone(), par.executor.clone()); + par.executor.spawn(fut).detach(); + Ok(Self { + id_parent: par.parent_id, + id_loading, + tasks, + globals: par.globals.clone(), + mounted_cells: Vec::new(), + executor: par.executor.clone(), + frontend_tasks: par.frontend_tasks, + catalog: None, + popup_remote_skymap_downloader: Default::default(), + on_updated_library: par.on_updated_library, + }) + } + + async fn request_skymap_preview( + globals: WguiGlobals, + executor: AsyncExecutor, + entry: SkymapCatalogEntry, + tasks: Tasks, + ) { + tasks.push(Task::SetSkymapPreview(( + entry.uuid, + networking::image_fetch::fetch_to_glyph_data(&globals, &executor, &entry.files.get_url_preview()) + .await + .ok(), + ))); + } + + fn refresh_cells(&mut self, layout: &mut Layout) -> anyhow::Result<()> { + let Some(catalog) = &self.catalog else { + debug_assert!(false); + return Ok(()); + }; + + for cell in &mut self.mounted_cells { + if let Some(entry) = get_entry_by_uuid(&catalog, cell.skymap_uuid) { + cell.view.refresh_resolution_pips(layout, entry)?; + } + } + + Ok(()) + } + + fn mount_catalog( + &mut self, + layout: &mut Layout, + catalog: &networking::skymap_catalog::SkymapCatalog, + ) -> anyhow::Result<()> { + let doc_params = &ParseDocumentParams { + globals: self.globals.clone(), + path: AssetPath::BuiltIn("gui/view/remote_skymap_list.xml"), + extra: Default::default(), + }; + + let parser_state = wgui::parser::parse_from_assets(&doc_params, layout, self.id_parent)?; + + let id_list = parser_state.fetch_widget(&layout.state, "list")?.id; + + for entry in &catalog.entries { + let task = View::request_skymap_preview( + self.globals.clone(), + self.executor.clone(), + entry.clone(), + self.tasks.clone(), + ); + + let skymap_uuid = entry.uuid.clone(); + + self.mounted_cells.push(MountedCell { + preview_image_compressed: None, + skymap_uuid: entry.uuid.clone(), + view: views::skymap_list_cell::View::new(views::skymap_list_cell::Params { + id_parent: id_list, + layout, + entry: Some(entry.clone()), + on_click: self + .tasks + .get_button_click_callback(Task::ShowRemoteSkymapDownloader(skymap_uuid)), + })?, + }); + + self.executor.spawn(task).detach(); + } + + self.catalog = Some(catalog.clone()); + Ok(()) + } + + fn show_remote_skymap_downloader( + &mut self, + uuid: SkymapUuid, + preview_image: CustomGlyphData, + preview_image_compressed: Rc>, + ) -> anyhow::Result<()> { + let Some(catalog) = &self.catalog else { + debug_assert!(false); // impossible + return Ok(()); + }; + + let Some(entry) = get_entry_by_uuid(&catalog, uuid) else { + return Ok(()); + }; + + // call our task before calling underlying on_updated_library callback + let on_updated_library = Rc::new({ + let func = self.on_updated_library.clone(); + let tasks = self.tasks.clone(); + move || { + tasks.push(Task::RefreshCells); + (*func)(); + } + }); + + views::remote_skymap_downloader::mount_popup( + self.frontend_tasks.clone(), + self.executor.clone(), + self.globals.clone(), + entry.clone(), + preview_image, + preview_image_compressed, + on_updated_library, + self.popup_remote_skymap_downloader.clone(), + ); + + Ok(()) + } + + fn get_image_preview( + &self, + skymap_uuid: SkymapUuid, + ) -> Option<(CustomGlyphData, Rc> /* preview_image_compressed */)> { + if let Some(cell) = &self.mounted_cells.iter().find(|mc| mc.skymap_uuid == skymap_uuid) { + let Some(image) = cell.view.get_image() else { + return None; + }; + + let Some(preview_image_compressed) = cell.preview_image_compressed.clone() else { + return None; + }; + return Some((image, preview_image_compressed)); + } + None + } +} + +pub fn mount_popup( + frontend_tasks: FrontendTasks, + executor: AsyncExecutor, + globals: WguiGlobals, + on_updated_library: Rc, + popup: PopupHolder, +) { + frontend_tasks + .clone() + .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( + Translation::from_translation_key("APP_SETTINGS.BROWSE_ONLINE_CATALOG"), + Box::new(move |data| { + let view = View::new(Params { + globals: &globals, + layout: data.layout, + executor: &executor, + parent_id: data.id_content, + frontend_tasks, + on_updated_library, + })?; + + popup.set_view(data.handle, view, None); + Ok(popup.get_close_callback(data.layout)) + }), + ))); +} diff --git a/dash-frontend/src/views/skymap_list.rs b/dash-frontend/src/views/skymap_list.rs new file mode 100644 index 00000000..cf97aa2d --- /dev/null +++ b/dash-frontend/src/views/skymap_list.rs @@ -0,0 +1,269 @@ +use anyhow::Context; +use wgui::{ + assets::AssetPath, + components::button::ComponentButton, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, WidgetID}, + parser::{Fetchable, ParseDocumentParams, ParserState}, + renderer_vk::text::custom_glyph::CustomGlyphData, + task::Tasks, +}; +use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, config_io, dash_interface::ConfigChangeKind}; + +use crate::{ + frontend::{FrontendTask, FrontendTasks}, + util::{ + networking::skymap_catalog::{self, SkymapCatalogEntry, SkymapResolution}, + popup_manager::{MountPopupOnceParams, PopupHolder}, + }, + views::{self, ViewTrait, ViewUpdateParams}, +}; + +#[derive(Clone)] +enum Task { + DownloadSkymaps, + Refresh, + ShowSkymapResolutionSelector(SkymapCatalogEntry), + SetSkymap(SkymapCatalogEntry, SkymapResolution), + SetSkymapBuiltIn, +} + +pub struct Params<'a> { + pub globals: WguiGlobals, + pub layout: &'a mut Layout, + pub parent_id: WidgetID, + pub frontend_tasks: &'a FrontendTasks, +} + +struct Cell { + #[allow(dead_code)] + view: views::skymap_list_cell::View, +} + +pub struct View { + #[allow(dead_code)] + parser_state: ParserState, + tasks: Tasks, + list_parent: WidgetID, + frontend_tasks: FrontendTasks, + globals: WguiGlobals, + popup_remote_skymap_list: PopupHolder, + popup_dialog_box: PopupHolder, + cells: Vec, +} + +impl ViewTrait for View { + fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> { + self.popup_remote_skymap_list.update(par)?; + self.popup_dialog_box.update(par)?; + + loop { + let tasks = self.tasks.drain(); + if tasks.is_empty() { + break; + } + for task in tasks { + match task { + Task::DownloadSkymaps => { + self.download_skymaps(&par.executor)?; + } + Task::Refresh => { + self.refresh(&mut par.layout)?; + } + Task::ShowSkymapResolutionSelector(entry) => { + self.show_skymap_resolution_selector(entry); + } + Task::SetSkymap(entry, resolution) => { + self.set_skymap(entry, resolution, par.general_config, par.config_change_kind)?; + } + Task::SetSkymapBuiltIn => { + par.general_config.skybox_texture = "".into(); + *par.config_change_kind = Some(ConfigChangeKind::EnvironmentBlend); + } + } + } + } + + Ok(()) + } +} + +impl View { + pub fn new(params: Params) -> anyhow::Result { + let doc_params = &ParseDocumentParams { + globals: params.globals.clone(), + path: AssetPath::BuiltIn("gui/view/skymap_list.xml"), + extra: Default::default(), + }; + + let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?; + let list_parent = parser_state.fetch_widget(¶ms.layout.state, "list_parent")?.id; + let tasks = Tasks::new(); + + tasks.push(Task::Refresh); + + tasks.handle_button( + &parser_state.fetch_component_as::("btn_download_skymaps")?, + Task::DownloadSkymaps, + ); + + tasks.handle_button( + &parser_state.fetch_component_as::("btn_refresh")?, + Task::Refresh, + ); + + Ok(Self { + parser_state, + tasks, + list_parent, + frontend_tasks: params.frontend_tasks.clone(), + globals: params.globals.clone(), + popup_remote_skymap_list: Default::default(), + popup_dialog_box: Default::default(), + cells: Vec::new(), + }) + } + + fn download_skymaps(&mut self, executor: &AsyncExecutor) -> anyhow::Result<()> { + views::remote_skymap_list::mount_popup( + self.frontend_tasks.clone(), + executor.clone(), + self.globals.clone(), + self.tasks.make_callback_rc(Task::Refresh), /* on_updated_library */ + self.popup_remote_skymap_list.clone(), + ); + Ok(()) + } + + fn set_skymap( + &mut self, + entry: SkymapCatalogEntry, + resolution: SkymapResolution, + config: &mut GeneralConfig, + change_kind: &mut Option, + ) -> anyhow::Result<()> { + let skymap_file_path = entry + .get_destination_path(resolution) + .context("Skymap not found" /* you shouldn't really see this, like ever. */)?; + + config.skybox_texture = config_io::get_skymaps_root() + .join(skymap_file_path) + .to_str() + .context("Skymap filename not valid UTF-8")? + .into(); + *change_kind = Some(ConfigChangeKind::EnvironmentBlend); + + Ok(()) + } + + fn show_skymap_resolution_selector(&mut self, entry: SkymapCatalogEntry) { + let tasks = self.tasks.clone(); + + let mut entries = Vec::::new(); + + let mut add_entry_resolution = |resolution: SkymapResolution| { + if entry.is_downloaded(resolution).unwrap_or(false) { + entries.push(views::dialog_box::ButtonEntry { + content: Translation::from_raw_text(resolution.get_display_str()), + icon: "dashboard/globe.svg", + action: resolution.get_display_str_simple(), + }); + } + }; + + add_entry_resolution(SkymapResolution::Res2k); + add_entry_resolution(SkymapResolution::Res4k); + add_entry_resolution(SkymapResolution::Res8k); + + if entries.is_empty() { + // if the skymap is not downloaded, how did we get there?! + debug_assert!(false); + return; + } + + views::dialog_box::mount_popup( + self.popup_dialog_box.clone(), + self.frontend_tasks.clone(), + views::dialog_box::Params { + globals: self.globals.clone(), + entries, + message: Translation::from_translation_key("APP_SETTINGS.SELECT_VARIANT"), + on_action_click: Box::new(move |action| { + let resolution = SkymapResolution::from_display_str_simple(action).unwrap(); // safety: never fails. + tasks.push(Task::SetSkymap(entry, resolution)) + }), + }, + ); + } + + fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> { + let entries = match skymap_catalog::get_entries_from_disk() { + Ok(entries) => entries, + Err(e) => { + log::error!("failed to get skymap entries: {}", e); + Default::default() + } + }; + + layout.remove_children(self.list_parent); + self.cells.clear(); + + let skymaps_root = config_io::get_skymaps_root(); + + let mut view = views::skymap_list_cell::View::new(views::skymap_list_cell::Params { + id_parent: self.list_parent, + layout, + entry: None, + on_click: self.tasks.get_button_click_callback(Task::SetSkymapBuiltIn), + })?; + // load preview image + let data = include_bytes!("../../assets/dashboard/builtin-skybox-preview.jpg"); + if let Ok(glyph_data) = CustomGlyphData::from_bytes_raster(&self.globals, "builtin-skybox-preview.jpg", data) { + view.set_image(layout, Some(glyph_data))?; + } + + self.cells.push(Cell { view }); + + for entry in &entries { + let mut view = views::skymap_list_cell::View::new(views::skymap_list_cell::Params { + id_parent: self.list_parent, + layout, + entry: Some(entry.clone()), + on_click: self + .tasks + .get_button_click_callback(Task::ShowSkymapResolutionSelector(entry.clone())), + })?; + + // load preview image + if let Ok(data) = std::fs::read(skymaps_root.join(&entry.files.preview)) { + if let Ok(glyph_data) = CustomGlyphData::from_bytes_raster(&self.globals, &entry.files.preview, &data) { + view.set_image(layout, Some(glyph_data))?; + } + } + + self.cells.push(Cell { view }); + } + + Ok(()) + } +} + +pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, popup: PopupHolder) { + frontend_tasks + .clone() + .push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new( + Translation::from_translation_key("APP_SETTINGS.BROWSE_SKYMAPS"), + Box::new(move |data| { + let view = View::new(Params { + globals: globals.clone(), + layout: data.layout, + parent_id: data.id_content, + frontend_tasks: &frontend_tasks, + })?; + + popup.set_view(data.handle, view, None); + Ok(popup.get_close_callback(data.layout)) + }), + ))); +} diff --git a/dash-frontend/src/views/skymap_list_cell.rs b/dash-frontend/src/views/skymap_list_cell.rs new file mode 100644 index 00000000..f0267ce5 --- /dev/null +++ b/dash-frontend/src/views/skymap_list_cell.rs @@ -0,0 +1,168 @@ +use std::{collections::HashMap, rc::Rc}; + +use wgui::{ + assets::AssetPath, + components::button::{ButtonClickCallback, ComponentButton}, + event::EventAlterables, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, WidgetID}, + parser::{Fetchable, ParseDocumentParams, ParserState}, + renderer_vk::text::custom_glyph::CustomGlyphData, + widget::{image::WidgetImage, label::WidgetLabel}, +}; + +use crate::util::{ + networking::{ + self, + skymap_catalog::{SkymapCatalogEntry, SkymapResolution}, + }, + wgui_simple, +}; + +pub struct Params<'a> { + pub id_parent: WidgetID, + pub layout: &'a mut Layout, + pub entry: Option, + pub on_click: ButtonClickCallback, +} + +pub struct View { + #[allow(dead_code)] + parser_state: ParserState, + id_loading: WidgetID, + id_image_preview: WidgetID, + id_resolution_pips: WidgetID, + image: Option, +} + +fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> { + ParseDocumentParams { + globals: globals.clone(), + path: AssetPath::BuiltIn("gui/view/skymap_list_cell.xml"), + extra: Default::default(), + } +} + +fn populate_res_pips( + layout: &mut Layout, + id_parent: WidgetID, + parser_state: &mut ParserState, + entry: &SkymapCatalogEntry, +) -> anyhow::Result<()> { + let globals = layout.state.globals.clone(); + layout.remove_children(id_parent); + + let mut populate_res_pip = |res: SkymapResolution| -> anyhow::Result<()> { + let mut tpar = HashMap::, Rc>::new(); + let downloaded = entry.is_downloaded(res).unwrap_or(false); + tpar.insert( + Rc::from("color"), + if downloaded { + Rc::from("#11aa40") + } else { + Rc::from("#444444") + }, + ); + tpar.insert(Rc::from("text"), res.get_display_str_simple().into()); + parser_state.realize_template(&doc_params(&globals), "ResolutionPip", layout, id_parent, tpar)?; + + Ok(()) + }; + + populate_res_pip(SkymapResolution::Res2k)?; + if entry.files.size_4k.is_some() { + populate_res_pip(SkymapResolution::Res4k)?; + } + if entry.files.size_8k.is_some() { + populate_res_pip(SkymapResolution::Res8k)?; + } + + Ok(()) +} + +impl View { + pub fn new(par: Params) -> anyhow::Result { + let globals = par.layout.state.globals.clone(); + + let mut parser_state = wgui::parser::parse_from_assets(&doc_params(&globals), par.layout, par.id_parent)?; + + let data = parser_state.realize_template( + &doc_params(&globals), + "Cell", + par.layout, + par.id_parent, + Default::default(), + )?; + + let id_image_preview = data.get_widget_id("image_preview")?; + let id_resolution_pips = data.get_widget_id("resolution_pips")?; + + data + .fetch_component_as::("button")? + .on_click(par.on_click); + + { + let mut label_title = data.fetch_widget_as::(&par.layout.state, "label_title")?; + let mut label_author = data.fetch_widget_as::(&par.layout.state, "label_author")?; + + label_title.set_text_simple( + &mut globals.get(), + Translation::from_raw_text_string( + par + .entry + .as_ref() + .map(|e| e.name.clone()) + .unwrap_or_else(|| "Built-in Sky Shader".into()), + ), + ); + label_author.set_text_simple( + &mut globals.get(), + Translation::from_raw_text_string(format!( + "by {}", + par.entry.as_ref().map(|e| e.author.as_str()).unwrap_or("WayVR Team") + )), + ); + } + + let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams { + layout: par.layout, + parent_id: id_image_preview, + with_text: false, + })?; + + // Populate resolution pips + if let Some(entry) = par.entry.as_ref() { + populate_res_pips(par.layout, id_resolution_pips, &mut parser_state, entry)?; + } + + Ok(Self { + parser_state, + id_loading, + id_image_preview, + image: None, + id_resolution_pips, + }) + } + + pub fn refresh_resolution_pips(&mut self, layout: &mut Layout, entry: &SkymapCatalogEntry) -> anyhow::Result<()> { + populate_res_pips(layout, self.id_resolution_pips, &mut self.parser_state, &entry)?; + Ok(()) + } + + pub fn set_image(&mut self, layout: &mut Layout, content: Option) -> anyhow::Result<()> { + layout.remove_widget(self.id_loading); + let mut alt = EventAlterables::default(); + { + let mut image_preview = layout.state.widgets.cast_as::(self.id_image_preview)?; + image_preview.set_content(&mut alt, content.clone()); + } + layout.process_alterables(alt)?; + self.image = content; + Ok(()) + } + + pub fn get_image(&self) -> Option { + return self.image.clone(); + } +} diff --git a/uidev/assets/gui/various_widgets.xml b/uidev/assets/gui/various_widgets.xml index 163e7391..b2eea786 100644 --- a/uidev/assets/gui/various_widgets.xml +++ b/uidev/assets/gui/various_widgets.xml @@ -93,6 +93,11 @@ + + +
diff --git a/uidev/src/main.rs b/uidev/src/main.rs index a812d1ce..69e95fa2 100644 --- a/uidev/src/main.rs +++ b/uidev/src/main.rs @@ -143,7 +143,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseWheel(MouseWheelEvent { delta: Vec2::new(x, y), pos: mouse / scale, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -157,7 +157,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseWheel(MouseWheelEvent { delta: Vec2::new(pos.x as f32 / 5.0, pos.y as f32 / 5.0), pos: mouse / scale, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -177,7 +177,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseDown(MouseButtonEvent { pos: mouse / scale, index: MouseButtonIndex::Left, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -190,7 +190,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseUp(MouseButtonEvent { pos: mouse / scale, index: MouseButtonIndex::Left, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -209,7 +209,7 @@ fn main() -> Result<(), Box> { .push_event( &wgui::event::Event::MouseMotion(MouseMotionEvent { pos: mouse / scale, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), diff --git a/uidev/src/testbed/testbed_generic.rs b/uidev/src/testbed/testbed_generic.rs index 802863f4..d1d078da 100644 --- a/uidev/src/testbed/testbed_generic.rs +++ b/uidev/src/testbed/testbed_generic.rs @@ -46,7 +46,6 @@ pub struct TestbedGeneric { pub parser_state: ParserState, tasks: Tasks, - globals: WguiGlobals, data: Rc>, } @@ -183,7 +182,6 @@ impl TestbedGeneric { layout, parser_state, tasks: Default::default(), - globals: globals.clone(), data: Rc::new(RefCell::new(Data { popup_window: WguiWindow::default(), context_menu: context_menu::ContextMenu::default(), diff --git a/wayvr/Cargo.toml b/wayvr/Cargo.toml index c7ff289e..aedac498 100644 --- a/wayvr/Cargo.toml +++ b/wayvr/Cargo.toml @@ -83,15 +83,13 @@ sysinfo = { version = "0.37" } thiserror = "2.0" tracing = "0.1.43" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } -uuid = { version = "1.19.0", features = ["fast-rng", "v4"] } +uuid = { workspace = true } wayland-client = { workspace = true } winit = { version = "0.30.12", optional = true } xcb = { version = "1.6.0", features = [ "as-raw-xcb-connection", ], optional = true } xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros -arboard = { version="3.6.1", features = ["wayland-data-control", "wl-clipboard-rs"] } -super-swipe-type = "0.2.2" [build-dependencies] regex.workspace = true diff --git a/wayvr/src/assets/gui/keyboard.xml b/wayvr/src/assets/gui/keyboard.xml index 405823d8..4d6fc321 100644 --- a/wayvr/src/assets/gui/keyboard.xml +++ b/wayvr/src/assets/gui/keyboard.xml @@ -13,11 +13,6 @@ width="${width}" height="${height}" min_width="${width}" min_height="${height}" max_width="${width}" max_height="${height}" /> - - @@ -95,13 +90,6 @@
- - - @@ -243,10 +231,7 @@
-
- - -
+
diff --git a/wayvr/src/backend/openvr/mod.rs b/wayvr/src/backend/openvr/mod.rs index 83b479d8..b2773d97 100644 --- a/wayvr/src/backend/openvr/mod.rs +++ b/wayvr/src/backend/openvr/mod.rs @@ -58,7 +58,11 @@ pub fn openvr_uninstall() { } #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] -pub fn openvr_run(show_by_default: bool, headless: bool) -> Result<(), BackendError> { +pub fn openvr_run( + show_by_default: bool, + headless: bool, + no_autostart: 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"); @@ -91,6 +95,8 @@ pub fn openvr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr AppState::from_graphics(gfx, gfx_extras, XrBackend::OpenVR)? }; + app.session.no_autostart = no_autostart; + if show_by_default { app.tasks.enqueue_at( TaskType::Overlay(OverlayTask::ShowHide), diff --git a/wayvr/src/backend/openxr/helpers.rs b/wayvr/src/backend/openxr/helpers.rs index 56ed80ca..0f9b352f 100644 --- a/wayvr/src/backend/openxr/helpers.rs +++ b/wayvr/src/backend/openxr/helpers.rs @@ -212,9 +212,10 @@ pub(super) fn posef_to_transform(pose: &xr::Posef) -> Affine3A { Affine3A::from_rotation_translation(rotation, translation) } -pub(super) fn reconfigure_chroma_key(app: &AppState) { +pub(super) fn try_apply_chroma_key(app: &AppState) -> bool { let Some(monado) = app.monado_state.as_ref() else { - return; + log::warn!("Could not set Chroma Key: no monado_state"); + return false; }; let params = &app.session.config.chroma_key_params; @@ -226,6 +227,9 @@ pub(super) fn reconfigure_chroma_key(app: &AppState) { params.curve, params.despill, ) { - log::warn!("Could not set Chroma Key: {e:?}") + log::warn!("Could not set Chroma Key: {e:?}"); + return false; } + + params.curve > 0.001 } diff --git a/wayvr/src/backend/openxr/mod.rs b/wayvr/src/backend/openxr/mod.rs index 57a6506b..f6ec7a44 100644 --- a/wayvr/src/backend/openxr/mod.rs +++ b/wayvr/src/backend/openxr/mod.rs @@ -17,7 +17,7 @@ use crate::{ backend::{ BackendError, XrBackend, input::interact, - openxr::{helpers::reconfigure_chroma_key, lines::LinePool, overlay::OpenXrOverlayData}, + openxr::{helpers::try_apply_chroma_key, lines::LinePool, overlay::OpenXrOverlayData}, task::{OpenXrTask, OverlayTask, TaskType}, }, config::{save_settings, save_state}, @@ -53,7 +53,11 @@ struct XrState { } #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] -pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendError> { +pub fn openxr_run( + show_by_default: bool, + headless: bool, + no_autostart: bool, +) -> Result<(), BackendError> { let (xr_instance, system) = match helpers::init_xr() { Ok((xr_instance, system)) => (xr_instance, system), Err(e) => { @@ -67,6 +71,8 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr AppState::from_graphics(gfx, gfx_extras, XrBackend::OpenXR)? }; + app.session.no_autostart = no_autostart; + let modes = xr_instance.enumerate_environment_blend_modes(system, VIEW_TYPE)?; if show_by_default { @@ -94,8 +100,6 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr .ok() }); - reconfigure_chroma_key(&app); - let mut blocker = app .monado_state .as_ref() @@ -482,7 +486,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr } } TaskType::OpenXR(task) => { - if matches!(task, OpenXrTask::SettingsChanged) { + if matches!(task, OpenXrTask::EnvironmentChanged) { reconfigure_environment_blend( &app, &xr_state, @@ -491,7 +495,6 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr &mut environment_blend_mode, main_session_visible, ); - reconfigure_chroma_key(&app); } } #[cfg(feature = "openvr")] @@ -532,6 +535,9 @@ pub(super) enum CompositionLayer<'a> { Equirect2(xr::CompositionLayerEquirect2KHR<'a, xr::Vulkan>), } +// applies chroma key settings. +// if chroma key or passthrough is enabled, use alpha env blend +// if alpha blend is not used and skybox is enabled, use skybox fn reconfigure_environment_blend( app: &AppState, xr_state: &XrState, @@ -540,9 +546,11 @@ fn reconfigure_environment_blend( environment_blend_mode: &mut xr::EnvironmentBlendMode, main_session_visible: bool, ) { + let has_chroma_key = try_apply_chroma_key(app); + *environment_blend_mode = { if modes.contains(&xr::EnvironmentBlendMode::ALPHA_BLEND) - && app.session.config.use_passthrough + && (app.session.config.use_passthrough || has_chroma_key) { xr::EnvironmentBlendMode::ALPHA_BLEND } else { @@ -554,13 +562,17 @@ fn reconfigure_environment_blend( && app.session.config.use_skybox && !main_session_visible; - if want_skybox == skybox.is_some() { - return; - } - if want_skybox { - log::debug!("Allocating skybox."); - *skybox = create_skybox(xr_state, app); + if let Some(curr_skybox) = skybox.as_ref() { + if curr_skybox.needs_recreate(app) { + *skybox = None; + log::debug!("Allocating skybox."); + *skybox = create_skybox(xr_state, app); + } + } else { + log::debug!("Allocating skybox."); + *skybox = create_skybox(xr_state, app); + } } else { log::debug!("Destroying skybox."); *skybox = None; diff --git a/wayvr/src/backend/openxr/monado_state.rs b/wayvr/src/backend/openxr/monado_state.rs index 418e5673..ff27fc00 100644 --- a/wayvr/src/backend/openxr/monado_state.rs +++ b/wayvr/src/backend/openxr/monado_state.rs @@ -19,6 +19,8 @@ impl MonadoState { Ok(res) } + #[allow(clippy::missing_const_for_fn)] + #[allow(clippy::unused_self)] pub fn update(&mut self) { #[cfg(feature = "feat-monado-metrics")] if let Some(metrics) = &mut self.metrics { @@ -30,6 +32,10 @@ impl MonadoState { } } + #[allow(clippy::missing_const_for_fn)] + #[allow(clippy::unused_self)] + #[allow(clippy::unnecessary_wraps)] + #[cfg(feature = "feat-monado-metrics")] pub fn set_metrics_enabled(&mut self, enabled: bool) -> anyhow::Result<()> { #[cfg(feature = "feat-monado-metrics")] { diff --git a/wayvr/src/backend/openxr/skybox.rs b/wayvr/src/backend/openxr/skybox.rs index 06b2e970..2d0215e2 100644 --- a/wayvr/src/backend/openxr/skybox.rs +++ b/wayvr/src/backend/openxr/skybox.rs @@ -24,11 +24,12 @@ use super::{ }; pub(super) struct Skybox { - view: Arc, + view: Option>, sky: Option, grid: Option, grid_pose: xr::Posef, grid_color_scale_bias_khr: Option>, + current_skybox: Arc, } impl Skybox { @@ -66,14 +67,18 @@ impl Skybox { } } - if maybe_image.is_none() { - let p = include_bytes!("../../res/table_mountain_2.dds"); - maybe_image = Some(command_buffer.upload_image_dds(p.as_slice())?); - } + let current_skybox = if maybe_image.is_some() { + app.session.config.skybox_texture.clone() + } else { + "".into() + }; - command_buffer.build_and_execute_now()?; - - let view = ImageView::new_default(maybe_image.unwrap())?; // safe unwrap + let view = if let Some(image) = maybe_image { + command_buffer.build_and_execute_now()?; + Some(ImageView::new_default(image)?) + } else { + None + }; let grid_color_scale_bias_khr = xr .instance @@ -99,9 +104,14 @@ impl Skybox { grid: None, grid_pose: translation_rotation_to_posef(Vec3A::ZERO, Quat::from_rotation_x(PI * -0.5)), grid_color_scale_bias_khr, + current_skybox, }) } + pub(super) fn needs_recreate(&self, app: &AppState) -> bool { + *self.current_skybox != *app.session.config.skybox_texture + } + fn prepare_sky<'a>( &'a mut self, xr: &'a XrState, @@ -113,7 +123,12 @@ impl Skybox { } let opts = SwapchainOpts::new().immutable(); - let extent = self.view.extent_u32arr(); + let extent = self + .view + .as_ref() + .map(|v| v.extent_u32arr()) + .unwrap_or([4096, 4096]); + let mut swapchain = create_swapchain(xr, app.gfx.clone(), extent, 1, opts)?; let tgt = swapchain .acquire_wait_image()? @@ -121,23 +136,42 @@ impl Skybox { .into_iter() .next() .unwrap(); - let pipeline = app.gfx.create_pipeline( - app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic - app.gfx_extras.shaders.get("frag_srgb").unwrap(), // want panic - WPipelineCreateInfo::new(app.gfx.surface_format), - )?; - let set0 = pipeline.uniform_sampler(0, self.view.clone(), app.gfx.texture_filter)?; - let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?; - let pass = pipeline.create_pass( - tgt.extent_f32(), - [0.0, 0.0], - app.gfx_extras.quad_verts.clone(), - 0..4, - 0..1, - vec![set0, set1], - &Default::default(), - )?; + let pass = if let Some(view) = self.view.as_ref() { + let pipeline = app.gfx.create_pipeline( + app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic + app.gfx_extras.shaders.get("frag_srgb").unwrap(), // want panic + WPipelineCreateInfo::new(app.gfx.surface_format), + )?; + + let set0 = pipeline.uniform_sampler(0, view.clone(), app.gfx.texture_filter)?; + let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?; + pipeline.create_pass( + tgt.extent_f32(), + [0.0, 0.0], + app.gfx_extras.quad_verts.clone(), + 0..4, + 0..1, + vec![set0, set1], + &Default::default(), + )? + } else { + let pipeline = app.gfx.create_pipeline( + app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic + app.gfx_extras.shaders.get("frag_sky").unwrap(), // want panic + WPipelineCreateInfo::new(app.gfx.surface_format), + )?; + + pipeline.create_pass( + tgt.extent_f32(), + [0.0, 0.0], + app.gfx_extras.quad_verts.clone(), + 0..4, + 0..1, + vec![], + &Default::default(), + )? + }; let mut cmd_buffer = app .gfx diff --git a/wayvr/src/backend/task.rs b/wayvr/src/backend/task.rs index 002881e5..2bc86886 100644 --- a/wayvr/src/backend/task.rs +++ b/wayvr/src/backend/task.rs @@ -56,7 +56,7 @@ pub enum OpenVrTask { #[cfg(feature = "openxr")] pub enum OpenXrTask { - SettingsChanged, + EnvironmentChanged, } pub enum PlayspaceTask { diff --git a/wayvr/src/backend/wayvr/client.rs b/wayvr/src/backend/wayvr/client.rs index 73a2f944..862fcc5d 100644 --- a/wayvr/src/backend/wayvr/client.rs +++ b/wayvr/src/backend/wayvr/client.rs @@ -1,8 +1,6 @@ use std::{io::Read, os::unix::net::UnixStream, path::PathBuf, sync::Arc}; use anyhow::Context; -use smithay::input::Seat; -use smithay::wayland::selection::data_device::set_data_device_selection; use smithay::{ backend::input::Keycode, input::{keyboard::KeyboardHandle, pointer::PointerHandle}, @@ -11,13 +9,13 @@ use smithay::{ }; use xkbcommon::xkb; +use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask}; + use super::{ ProcessWayVREnv, comp::{self, ClientState}, process, }; -use crate::backend::wayvr::comp::Application; -use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask}; pub struct WayVRClient { pub client: wayland_server::Client, @@ -28,7 +26,6 @@ pub struct WayVRCompositor { pub state: comp::Application, pub seat_keyboard: KeyboardHandle, pub seat_pointer: PointerHandle, - pub seat: Seat, pub serial_counter: SerialCounter, pub wayland_env: super::WaylandEnv, @@ -71,7 +68,6 @@ impl WayVRCompositor { display: wayland_server::Display, seat_keyboard: KeyboardHandle, seat_pointer: PointerHandle, - seat: Seat, ) -> anyhow::Result { let (wayland_env, listener) = create_wayland_listener()?; @@ -85,7 +81,6 @@ impl WayVRCompositor { serial_counter: SerialCounter::new(), clients: Vec::new(), toplevel_surf_count: 0, - seat, }) } @@ -205,17 +200,6 @@ impl WayVRCompositor { ); } - pub fn set_clipboard_text(&mut self, text: String) { - set_data_device_selection::( - &self.state.display_handle, - &self.seat, - vec![ - "text/plain;charset=utf-8".to_string(), - "text/plain".to_string(), - ], - text.as_bytes().into(), - ); - } pub fn set_keymap(&mut self, keymap: &xkb::Keymap) -> anyhow::Result<()> { // Smithay only accepts keymaps in a string form due to thread safety concerns self.seat_keyboard diff --git a/wayvr/src/backend/wayvr/mod.rs b/wayvr/src/backend/wayvr/mod.rs index 74674949..5d83f02b 100644 --- a/wayvr/src/backend/wayvr/mod.rs +++ b/wayvr/src/backend/wayvr/mod.rs @@ -241,13 +241,7 @@ impl WvrServerState { }; Ok(Self { - manager: client::WayVRCompositor::new( - state, - display, - seat_keyboard, - seat_pointer, - seat, - )?, + manager: client::WayVRCompositor::new(state, display, seat_keyboard, seat_pointer)?, processes: ProcessVec::new(), wm: window::WindowManager::new(), ticks: 0, @@ -614,9 +608,6 @@ impl WvrServerState { pub fn send_key(&mut self, virtual_key: u32, down: bool) { self.manager.send_key(virtual_key, down); } - pub fn set_clipboard_text(&mut self, text: String) { - self.manager.set_clipboard_text(text); - } pub fn set_keymap(&mut self, keymap: &xkb::Keymap) -> anyhow::Result<()> { self.manager.set_keymap(keymap) diff --git a/wayvr/src/config.rs b/wayvr/src/config.rs index 676dda24..237fca9d 100644 --- a/wayvr/src/config.rs +++ b/wayvr/src/config.rs @@ -1,7 +1,7 @@ use config::{Config, File}; use log::error; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use wayvr_ipc::packet_client::WvrProcessLaunchParams; use wlx_common::{ astr_containers::AStrMap, @@ -45,6 +45,32 @@ where panic!("No usable config found."); } +const SUPPORTED_EXTESIONS: [&'static str; 4] = ["yaml", "yml", "json", "json5"]; + +fn is_supported_config_file(path: &Path) -> bool { + if path.is_dir() { + return false; + } + match path.extension().and_then(|e| e.to_str()) { + Some(ext) => { + for sup in SUPPORTED_EXTESIONS { + if sup.eq_ignore_ascii_case(ext) { + return true; + } + } + + log::error!( + "Unsupported file extension: \".{}\" in {}. Ignoring file.", + ext, + path.to_string_lossy() + ); + debug_assert!(false); + false + } + _ => false, + } +} + pub fn load_config_with_conf_d( root_config_filename: &str, ctype: config_io::ConfigRoot, @@ -68,7 +94,8 @@ where if let Ok(paths_unsorted) = std::fs::read_dir(path_conf_d) { let mut paths: Vec<_> = paths_unsorted .filter_map(|r| match r { - Ok(entry) => Some(entry), + Ok(entry) if is_supported_config_file(&entry.path()) => Some(entry), + Ok(_other) => None, Err(e) => { error!("Failed to read conf.d directory: {e}"); None @@ -140,7 +167,6 @@ pub struct AutoSettings { pub context_menu_hold_and_release: bool, pub capture_method: CaptureMethod, pub keyboard_middle_click_mode: AltModifier, - pub keyboard_swipe_to_type_enabled: bool, pub autostart_apps: Vec, pub handsfree_pointer: HandsfreePointer, pub language: Option, @@ -193,7 +219,6 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> { context_menu_hold_and_release: config.context_menu_hold_and_release, capture_method: config.capture_method, keyboard_middle_click_mode: config.keyboard_middle_click_mode, - keyboard_swipe_to_type_enabled: config.keyboard_swipe_to_type_enabled, autostart_apps: config.autostart_apps.clone(), handsfree_pointer: config.handsfree_pointer, language: config.language, diff --git a/wayvr/src/graphics/mod.rs b/wayvr/src/graphics/mod.rs index 52ffa913..5c1573ba 100644 --- a/wayvr/src/graphics/mod.rs +++ b/wayvr/src/graphics/mod.rs @@ -21,7 +21,7 @@ use wgui::gfx::WGfx; use vulkano::instance::InstanceCreateFlags; use wlx_capture::DrmFormat; -use crate::shaders::{frag_color, frag_grid, frag_screen, frag_srgb, vert_quad}; +use crate::shaders::{frag_color, frag_grid, frag_screen, frag_sky, frag_srgb, vert_quad}; #[cfg(feature = "openxr")] use {ash::vk, std::os::raw::c_void}; @@ -74,6 +74,9 @@ impl WGfxExtras { let shader = frag_srgb::load(gfx.device.clone())?; shaders.insert("frag_srgb", shader); + let shader = frag_sky::load(gfx.device.clone())?; + shaders.insert("frag_sky", shader); + let shader = frag_grid::load(gfx.device.clone())?; shaders.insert("frag_grid", shader); diff --git a/wayvr/src/gui/panel/mod.rs b/wayvr/src/gui/panel/mod.rs index 85b89615..c135e58a 100644 --- a/wayvr/src/gui/panel/mod.rs +++ b/wayvr/src/gui/panel/mod.rs @@ -1,10 +1,20 @@ -use std::{cell::RefCell, rc::Rc}; - +use super::timer::GuiTimer; +use crate::{ + app_misc, + backend::input::{Haptics, HoverResult, PointerHit, PointerMode}, + backend::task::ModifyPanelCommand, + state::AppState, + subsystem::hid::WheelDelta, + windowing::backend::{ + FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, ui_transform, + }, +}; use anyhow::Context; use button::setup_custom_button; use glam::{Affine2, Vec2, vec2}; use idmap::IdMap; use label::setup_custom_label; +use std::{cell::RefCell, rc::Rc}; use wgui::{ assets::AssetPath, components::{ @@ -12,9 +22,9 @@ use wgui::{ slider::ComponentSlider, }, event::{ - CallbackDataCommon, Event as WguiEvent, EventAlterables, EventCallback, EventListenerID, - EventListenerKind, InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex, - MouseLeaveEvent, MouseMotionEvent, MouseWheelEvent, + DeviceBitmask, Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind, + InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, + MouseMotionEvent, MouseWheelEvent, }, gfx::cmd::WGfxClearMode, i18n::Translation, @@ -33,19 +43,6 @@ use wgui::{ use wlx_common::overlays::{BackendAttrib, BackendAttribValue}; use wlx_common::timestep::Timestep; -use crate::{ - app_misc, - backend::input::{Haptics, HoverResult, PointerHit, PointerMode}, - backend::task::ModifyPanelCommand, - state::AppState, - subsystem::hid::WheelDelta, - windowing::backend::{ - FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, ui_transform, - }, -}; - -use super::timer::GuiTimer; - pub mod button; pub mod device_list; mod label; @@ -371,7 +368,7 @@ impl OverlayBackend for GuiPanel { let e = WguiEvent::MouseWheel(MouseWheelEvent { delta: vec2(delta.x, delta.y) / 8.0, pos: hit.uv * self.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(app, &e); } @@ -379,7 +376,7 @@ impl OverlayBackend for GuiPanel { fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> HoverResult { let e = &WguiEvent::MouseMotion(MouseMotionEvent { pos: hit.uv * self.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.has_focus[hit.pointer] = true; @@ -400,7 +397,9 @@ impl OverlayBackend for GuiPanel { } fn on_left(&mut self, app: &mut AppState, pointer: usize) { - let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer }); + let e = WguiEvent::MouseLeave(MouseLeaveEvent { + device: DeviceBitmask::from_usize(pointer), + }); self.has_focus[pointer] = false; self.push_event(app, &e); } @@ -417,13 +416,13 @@ impl OverlayBackend for GuiPanel { WguiEvent::MouseDown(MouseButtonEvent { pos: hit.uv * self.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) } else { WguiEvent::MouseUp(MouseButtonEvent { pos: hit.uv * self.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) }; self.push_event(app, &e); @@ -432,11 +431,11 @@ impl OverlayBackend for GuiPanel { if !pressed && !self.has_focus[hit.pointer] { let e = WguiEvent::MouseMotion(MouseMotionEvent { pos: vec2(-1., -1.), - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(app, &e); let e = WguiEvent::MouseLeave(MouseLeaveEvent { - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(app, &e); } @@ -493,17 +492,13 @@ pub fn apply_custom_command( element: &str, command: &ModifyPanelCommand, ) -> anyhow::Result<()> { - let mut alterables = EventAlterables::default(); - let mut com = CallbackDataCommon { - alterables: &mut alterables, - state: &panel.layout.state, - }; + let mut com = panel.layout.common(); match command { ModifyPanelCommand::SetText(text) => { if let Ok(mut label) = panel .parser_state - .fetch_widget_as::(&panel.layout.state, element) + .fetch_widget_as::(&com.state, element) { label.set_text(&mut com, Translation::from_raw_text(text)); } else if let Ok(button) = panel @@ -516,10 +511,7 @@ pub fn apply_custom_command( } } ModifyPanelCommand::SetImage(path) => { - if let Ok(pair) = panel - .parser_state - .fetch_widget(&panel.layout.state, element) - { + if let Ok(pair) = panel.parser_state.fetch_widget(&com.state, element) { let data = CustomGlyphData::from_assets( &app.wgui_globals, wgui::assets::AssetPath::File(path), @@ -527,9 +519,9 @@ pub fn apply_custom_command( .context("Could not load content from supplied path.")?; if let Some(mut sprite) = pair.widget.get_as::() { - sprite.set_content(&mut com, Some(data)); + sprite.set_content(com.alterables, Some(data)); } else if let Some(mut image) = pair.widget.get_as::() { - image.set_content(&mut com, Some(data)); + image.set_content(com.alterables, Some(data)); } else { anyhow::bail!("No or with such id."); } @@ -541,10 +533,7 @@ pub fn apply_custom_command( let color = parse_color_hex(color) .context("Invalid color format, must be a html hex color!")?; - if let Ok(pair) = panel - .parser_state - .fetch_widget(&panel.layout.state, element) - { + if let Ok(pair) = panel.parser_state.fetch_widget(&com.state, element) { if let Some(mut rect) = pair.widget.get_as::() { rect.set_color(&mut com, color); } else if let Some(mut label) = pair.widget.get_as::() { @@ -592,7 +581,7 @@ pub fn apply_custom_command( .fetch_component_as::(element) { let value_f32 = value_str.parse::().context("Not a valid number")?; - slider.set_value(&mut com, value_f32); + slider.set_value_primary(&mut com, value_f32); } else if let Ok(radio) = panel .parser_state .fetch_component_as::(element) @@ -610,6 +599,5 @@ pub fn apply_custom_command( } } - panel.layout.process_alterables(alterables)?; Ok(()) } diff --git a/wayvr/src/gui/panel/overlay_list.rs b/wayvr/src/gui/panel/overlay_list.rs index 64dc4e8c..90d9044f 100644 --- a/wayvr/src/gui/panel/overlay_list.rs +++ b/wayvr/src/gui/panel/overlay_list.rs @@ -1,15 +1,12 @@ -use std::{collections::HashMap, rc::Rc}; - +use crate::windowing::{OverlayID, backend::OverlayEventData, window::OverlayCategory}; use slotmap::{Key, SecondaryMap}; +use std::{collections::HashMap, rc::Rc}; use wgui::{ components::button::ComponentButton, - event::{CallbackDataCommon, EventAlterables}, layout::Layout, parser::{Fetchable, ParseDocumentParams, ParserState}, }; -use crate::windowing::{OverlayID, backend::OverlayEventData, window::OverlayCategory}; - #[derive(Default)] /// Helper for managing a list of overlays /// Populates `id="panels_root"` with ``, ``, `` templates @@ -25,7 +22,6 @@ impl OverlayList { layout: &mut Layout, parser_state: &mut ParserState, event_data: &OverlayEventData, - alterables: &mut EventAlterables, doc_params: &ParseDocumentParams, ) -> anyhow::Result { let mut elements_changed = false; @@ -98,11 +94,7 @@ impl OverlayList { }; if meta.visible { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; - overlay_button.set_sticky_state(&mut com, true); + overlay_button.set_sticky_state(&mut layout.common(), true); } self.overlay_buttons.insert(meta.id, overlay_button); continue; @@ -121,31 +113,23 @@ impl OverlayList { let overlay_button = parser_state .fetch_component_as::(&format!("overlay_{i}"))?; if meta.visible { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; - overlay_button.set_sticky_state(&mut com, true); + overlay_button.set_sticky_state(&mut layout.common(), true); } self.overlay_buttons.insert(meta.id, overlay_button); } elements_changed = true; } OverlayEventData::VisibleOverlaysChanged(overlays) => { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; let mut overlay_buttons = self.overlay_buttons.clone(); for visible in overlays.as_ref() { if let Some(btn) = overlay_buttons.remove(*visible) { - btn.set_sticky_state(&mut com, true); + btn.set_sticky_state(&mut layout.common(), true); } } for btn in overlay_buttons.values() { - btn.set_sticky_state(&mut com, false); + btn.set_sticky_state(&mut layout.common(), false); } } _ => {} diff --git a/wayvr/src/gui/panel/set_list.rs b/wayvr/src/gui/panel/set_list.rs index 30767c7a..83922efd 100644 --- a/wayvr/src/gui/panel/set_list.rs +++ b/wayvr/src/gui/panel/set_list.rs @@ -2,7 +2,6 @@ use std::{collections::HashMap, rc::Rc}; use wgui::{ components::button::ComponentButton, - event::{CallbackDataCommon, EventAlterables}, layout::Layout, parser::{Fetchable, ParseDocumentParams, ParserState}, }; @@ -23,25 +22,20 @@ impl SetList { layout: &mut Layout, parser_state: &mut ParserState, event_data: &OverlayEventData, - alterables: &mut EventAlterables, doc_params: &ParseDocumentParams, ) -> anyhow::Result { let mut elements_changed = false; match event_data { OverlayEventData::ActiveSetChanged(current_set) => { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; if let Some(old_set) = self.current_set.take() && let Some(old_set) = self.set_buttons.get_mut(old_set) { - old_set.set_sticky_state(&mut com, false); + old_set.set_sticky_state(&mut layout.common(), false); } if let Some(new_set) = current_set && let Some(new_set) = self.set_buttons.get_mut(*new_set) { - new_set.set_sticky_state(&mut com, true); + new_set.set_sticky_state(&mut layout.common(), true); } self.current_set = *current_set; } @@ -59,11 +53,7 @@ impl SetList { let set_button = parser_state.fetch_component_as::(&format!("set_{i}"))?; if self.current_set == Some(i) { - let mut com = CallbackDataCommon { - alterables, - state: &layout.state, - }; - set_button.set_sticky_state(&mut com, true); + set_button.set_sticky_state(&mut layout.common(), true); } self.set_buttons.push(set_button); } diff --git a/wayvr/src/ipc/event_queue.rs b/wayvr/src/ipc/event_queue.rs index c150c4c4..13c9adb4 100644 --- a/wayvr/src/ipc/event_queue.rs +++ b/wayvr/src/ipc/event_queue.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use std::{cell::RefCell, collections::VecDeque, rc::Rc}; #[derive(Debug)] diff --git a/wayvr/src/ipc/ipc_server.rs b/wayvr/src/ipc/ipc_server.rs index 4490ac21..8a33c87f 100644 --- a/wayvr/src/ipc/ipc_server.rs +++ b/wayvr/src/ipc/ipc_server.rs @@ -42,35 +42,40 @@ pub fn send_packet(conn: &mut local_socket::Stream, data: &[u8]) -> anyhow::Resu Ok(()) } -fn read_check(expected_size: u32, res: std::io::Result) -> bool { +fn read_check(expected_size: u32, res: std::io::Result) -> anyhow::Result { match res { Ok(count) => { if count == 0 { - return false; + anyhow::bail!("End of stream"); } if count as u32 == expected_size { - true // read succeeded + return Ok(true); // read succeeded } else { log::error!("count {count} is not {expected_size}"); - false + return Ok(false); } } - Err(_e) => { - //log::error!("failed to get packet size: {}", e); - false - } + Err(e) => match e.kind() { + std::io::ErrorKind::WouldBlock => { + // no incoming data (this socket is non-blocking), try again later + return Ok(false); + } + _ => { + anyhow::bail!("Connection error: {:?}", e); + } + }, } } type Payload = SmallVec<[u8; 64]>; -fn read_payload(conn: &mut local_socket::Stream, size: u32) -> Option { +fn read_payload(conn: &mut local_socket::Stream, size: u32) -> anyhow::Result> { let mut payload = Payload::new(); payload.resize(size as usize, 0); - if read_check(size, conn.read(&mut payload)) { - Some(payload) + if read_check(size, conn.read(&mut payload))? { + Ok(Some(payload)) } else { - None + Ok(None) } } @@ -433,36 +438,33 @@ impl Connection { Ok(()) } - fn process_check_payload(&mut self, params: &mut TickParams, payload: Payload) -> bool { - log::debug!("payload size {}", payload.len()); - + fn process_check_payload( + &mut self, + params: &mut TickParams, + payload: Payload, + ) -> anyhow::Result<()> { if let Err(e) = self.process_payload(params, payload) { - log::error!("Invalid payload from the client, closing connection: {e}"); // send also error message directly to the client before disconnecting self.kill(format!("{e}").as_str()); - false - } else { - true + anyhow::bail!("Invalid payload from the client, closing connection: {e}"); } + Ok(()) } - fn read_packet(&mut self, params: &mut TickParams) -> bool { + fn read_packet(&mut self, params: &mut TickParams) -> anyhow::Result { if let Some(payload_size) = self.next_packet { - let Some(payload) = read_payload(&mut self.conn, payload_size) else { - // still failed to read payload, try in next tick - return false; + let Some(payload) = read_payload(&mut self.conn, payload_size)? else { + // still failed to read payload, try in the next tick + return Ok(false); }; - if !self.process_check_payload(params, payload) { - return false; - } - + self.process_check_payload(params, payload)?; self.next_packet = None; } let mut buf_packet_header: [u8; 4] = [0; 4]; - if !read_check(4, self.conn.read(&mut buf_packet_header)) { - return false; + if !read_check(4, self.conn.read(&mut buf_packet_header))? { + return Ok(false); } let payload_size = u32::from_be_bytes(buf_packet_header[0..4].try_into().unwrap()); // 0-3 bytes (u32 size) @@ -475,30 +477,43 @@ impl Connection { "Client sent a packet header with the size over {size_limit} bytes, closing connection." ); self.kill("Too big packet received (over 128 KiB)"); - return false; + return Ok(false); } - let Some(payload) = read_payload(&mut self.conn, payload_size) else { + let Some(payload) = read_payload(&mut self.conn, payload_size)? else { // failed to read payload, try in next tick self.next_packet = Some(payload_size); - return false; + return Ok(false); }; - if !self.process_check_payload(params, payload) { - return false; - } + self.process_check_payload(params, payload)?; - true + Ok(true) } fn tick(&mut self, params: &mut TickParams) { - while self.read_packet(params) {} + loop { + match self.read_packet(params) { + Ok(loop_further) => { + if !loop_further { + break; + } + } + Err(e) => { + log::debug!("Disconnecting client: {:?}", e); + self.alive = false; + break; + } + } + } } } impl Drop for Connection { fn drop(&mut self) { - log::info!("Connection closed"); + if let Some(auth) = &self.auth { + log::info!("IPC: Client \"{}\" disconnected.", auth.client_name); + } } } diff --git a/wayvr/src/main.rs b/wayvr/src/main.rs index 8c692ca9..6649b633 100644 --- a/wayvr/src/main.rs +++ b/wayvr/src/main.rs @@ -84,6 +84,10 @@ struct Args { #[arg(long)] headless: bool, + /// Do not auto-start registered apps + #[arg(long)] + no_autostart: bool, + /// Path to write logs to #[arg(short, long, value_name = "FILE_PATH")] log_to: Option, @@ -150,7 +154,7 @@ fn auto_run(args: Args, used_backend: &mut Option) { if !args_get_openvr(&args) { use crate::backend::{BackendError, openxr::openxr_run}; tried_xr = true; - match openxr_run(args.show, args.headless) { + match openxr_run(args.show, args.headless, args.no_autostart) { Ok(()) => { used_backend.replace(XrBackend::OpenXR); return; @@ -168,7 +172,7 @@ fn auto_run(args: Args, used_backend: &mut Option) { if !args_get_openxr(&args) { use crate::backend::{BackendError, openvr::openvr_run}; tried_vr = true; - match openvr_run(args.show, args.headless) { + match openvr_run(args.show, args.headless, args.no_autostart) { Ok(()) => { used_backend.replace(XrBackend::OpenVR); return; @@ -281,7 +285,6 @@ fn logging_init(args: &mut Args) { .from_env_lossy() .add_directive("symphonia_core::probe=warn".parse().unwrap()) .add_directive("symphonia_bundle_mp3=warn".parse().unwrap()) - .add_directive("ort=warn".parse().unwrap()) .add_directive("zbus=warn".parse().unwrap()) .add_directive("usvg=error".parse().unwrap()) .add_directive("resvg=error".parse().unwrap()) diff --git a/wayvr/src/overlays/dashboard.rs b/wayvr/src/overlays/dashboard.rs index 4d0b7958..a3eaf6b0 100644 --- a/wayvr/src/overlays/dashboard.rs +++ b/wayvr/src/overlays/dashboard.rs @@ -8,15 +8,15 @@ use wayvr_ipc::{ }; use wgui::{ event::{ - Event as WguiEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, MouseMotionEvent, - MouseWheelEvent, + DeviceBitmask, Event as WguiEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, + MouseMotionEvent, MouseWheelEvent, }, gfx::cmd::WGfxClearMode, renderer_vk::context::Context as WguiContext, widget::EventResult, }; use wlx_common::{ - dash_interface::{self, DashInterface, RecenterMode}, + dash_interface::{self, ConfigChangeKind, DashInterface, RecenterMode}, locale::WayVRLangProvider, overlays::{BackendAttrib, BackendAttribValue}, }; @@ -73,8 +73,12 @@ impl DashFrontend { fn new(app: &mut AppState) -> anyhow::Result { let mut interface = DashInterfaceLive::new(); - for p in app.session.config.autostart_apps.clone() { - let _ = interface.process_launch(app, false, p); + if app.session.no_autostart { + log::info!("Not starting apps due to --no-autostart") + } else { + for p in app.session.config.autostart_apps.clone() { + let _ = interface.process_launch(app, false, p); + } } let frontend = frontend::Frontend::new(frontend::InitParams { @@ -201,7 +205,7 @@ impl OverlayBackend for DashFrontend { let e = WguiEvent::MouseWheel(MouseWheelEvent { delta: vec2(delta.x, delta.y) / 8.0, pos: hit.uv * self.inner.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(&e); } @@ -209,7 +213,7 @@ impl OverlayBackend for DashFrontend { fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> HoverResult { let e = &WguiEvent::MouseMotion(MouseMotionEvent { pos: hit.uv * self.inner.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.has_focus[hit.pointer] = true; @@ -231,7 +235,9 @@ impl OverlayBackend for DashFrontend { } fn on_left(&mut self, _app: &mut AppState, pointer: usize) { - let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer }); + let e = WguiEvent::MouseLeave(MouseLeaveEvent { + device: DeviceBitmask::from_usize(pointer), + }); self.has_focus[pointer] = false; self.push_event(&e); } @@ -248,13 +254,13 @@ impl OverlayBackend for DashFrontend { WguiEvent::MouseDown(MouseButtonEvent { pos: hit.uv * self.inner.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) } else { WguiEvent::MouseUp(MouseButtonEvent { pos: hit.uv * self.inner.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) }; self.push_event(&e); @@ -263,11 +269,11 @@ impl OverlayBackend for DashFrontend { if !pressed && !self.has_focus[hit.pointer] { let e = WguiEvent::MouseMotion(MouseMotionEvent { pos: vec2(-1., -1.), - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(&e); let e = WguiEvent::MouseLeave(MouseLeaveEvent { - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(&e); } @@ -444,16 +450,22 @@ impl DashInterface for DashInterfaceLive { &mut data.session.config } - fn config_changed(&mut self, data: &mut AppState) { + fn config_changed(&mut self, data: &mut AppState, kind: ConfigChangeKind) { data.session.config_dirty = true; - #[cfg(feature = "openxr")] - { - use crate::backend::task::OpenXrTask; - data.tasks - .enqueue(TaskType::OpenXR(OpenXrTask::SettingsChanged)); + + match kind { + ConfigChangeKind::OverlayConfig => data + .tasks + .enqueue(TaskType::Overlay(OverlayTask::SettingsChanged)), + ConfigChangeKind::EnvironmentBlend => { + #[cfg(feature = "openxr")] + { + use crate::backend::task::OpenXrTask; + data.tasks + .enqueue(TaskType::OpenXR(OpenXrTask::EnvironmentChanged)); + } + } } - data.tasks - .enqueue(TaskType::Overlay(OverlayTask::SettingsChanged)); } fn restart(&mut self, _data: &mut AppState) { @@ -466,6 +478,16 @@ impl DashInterface for DashInterfaceLive { .enqueue(TaskType::Overlay(OverlayTask::ToggleDashboard)); } + fn get_feats(&mut self, data: &mut AppState) -> dash_interface::InterfaceFeats { + dash_interface::InterfaceFeats { + openxr: matches!(data.xr_backend, XrBackend::OpenXR), + #[cfg(feature = "openxr")] + monado: data.monado_state.is_some(), + #[cfg(not(feature = "openxr"))] + monado: false, + } + } + #[cfg(feature = "openxr")] fn monado_client_list( &mut self, diff --git a/wayvr/src/overlays/edit/mod.rs b/wayvr/src/overlays/edit/mod.rs index 66b19a40..d9f913b2 100644 --- a/wayvr/src/overlays/edit/mod.rs +++ b/wayvr/src/overlays/edit/mod.rs @@ -10,7 +10,7 @@ use glam::vec2; use slotmap::Key; use wgui::{ components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider}, - event::{CallbackDataCommon, EventAlterables, EventCallback}, + event::EventCallback, i18n::Translation, parser::Fetchable, widget::EventResult, @@ -466,98 +466,82 @@ fn reset_panel( *panel.state.id.borrow_mut() = id; let state = owc.active_state.as_mut().unwrap(); - let mut alterables = EventAlterables::default(); - let mut common = CallbackDataCommon { - alterables: &mut alterables, - state: &panel.layout.state, - }; + let mut com = panel.layout.common(); let c = panel .parser_state .fetch_component_as::("top_grab")?; - c.set_sticky_state(&mut common, !state.grabbable); + c.set_sticky_state(&mut com, !state.grabbable); let c = panel .parser_state .fetch_component_as::("lerp_slider")?; - c.set_value(&mut common, state.positioning.get_lerp().unwrap_or(1.0)); + c.set_value_primary(&mut com, state.positioning.get_lerp().unwrap_or(1.0)); let c = panel .parser_state .fetch_component_as::("alpha_slider")?; - c.set_value(&mut common, state.alpha); + c.set_value_primary(&mut com, state.alpha); let c = panel .parser_state .fetch_component_as::("curve_slider")?; - c.set_value(&mut common, state.curvature.unwrap_or(0.0)); + c.set_value_primary(&mut com, state.curvature.unwrap_or(0.0)); let c = panel .parser_state .fetch_component_as::("additive_box")?; - c.set_checked(&mut common, state.additive); + c.set_checked(&mut com, state.additive); let c = panel .parser_state .fetch_component_as::("align_box")?; - c.set_checked(&mut common, state.positioning.get_align().unwrap_or(false)); + c.set_checked(&mut com, state.positioning.get_align().unwrap_or(false)); let c = panel .parser_state .fetch_component_as::("global_box")?; - c.set_checked(&mut common, owc.global); + c.set_checked(&mut com, owc.global); let c = panel .parser_state .fetch_component_as::("angle_fade_box")?; - c.set_checked(&mut common, state.angle_fade); + c.set_checked(&mut com, state.angle_fade); let c = panel .parser_state .fetch_component_as::("block_input_box")?; - c.set_checked(&mut common, state.block_input); + c.set_checked(&mut com, state.block_input); - panel - .state - .pos - .reset(&mut common, &state.positioning.into()); - panel.state.lock.reset(&mut common, state.interactable); - panel.state.tabs.reset(&mut common); + panel.state.pos.reset(&mut com, &state.positioning.into()); + panel.state.lock.reset(&mut com, state.interactable); + panel.state.tabs.reset(&mut com); if let Some(stereo) = attrib_value!( owc.backend.get_attrib(BackendAttrib::Stereo), BackendAttribValue::Stereo ) { - panel - .state - .tabs - .set_tab_visible(&mut common, "stereo", true); - panel.state.stereo.reset(&mut common, &stereo); + panel.state.tabs.set_tab_visible(&mut com, "stereo", true); + panel.state.stereo.reset(&mut com, &stereo); // Set the checkbox label based on stereo mode let translation = get_stereo_full_frame_translation(stereo); let c = panel .parser_state .fetch_component_as::("stereo_full_frame_box")?; - c.set_text(&mut common, Translation::from_translation_key(translation)); + c.set_text(&mut com, Translation::from_translation_key(translation)); } else { - panel - .state - .tabs - .set_tab_visible(&mut common, "stereo", false); + panel.state.tabs.set_tab_visible(&mut com, "stereo", false); } if let Some(mouse) = attrib_value!( owc.backend.get_attrib(BackendAttrib::MouseTransform), BackendAttribValue::MouseTransform ) { - panel.state.tabs.set_tab_visible(&mut common, "mouse", true); - panel.state.mouse.reset(&mut common, &mouse); + panel.state.tabs.set_tab_visible(&mut com, "mouse", true); + panel.state.mouse.reset(&mut com, &mouse); } else { - panel - .state - .tabs - .set_tab_visible(&mut common, "mouse", false); + panel.state.tabs.set_tab_visible(&mut com, "mouse", false); } if let Some(full_frame) = attrib_value!( @@ -567,7 +551,7 @@ fn reset_panel( let c = panel .parser_state .fetch_component_as::("stereo_full_frame_box")?; - c.set_checked(&mut common, full_frame); + c.set_checked(&mut com, full_frame); } if let Some(adjust_mouse) = attrib_value!( @@ -577,11 +561,9 @@ fn reset_panel( let c = panel .parser_state .fetch_component_as::("stereo_adjust_mouse_box")?; - c.set_checked(&mut common, adjust_mouse); + c.set_checked(&mut com, adjust_mouse); } - panel.layout.process_alterables(alterables)?; - Ok(()) } diff --git a/wayvr/src/overlays/edit/sprite_tab.rs b/wayvr/src/overlays/edit/sprite_tab.rs index 4468ec3e..afc97136 100644 --- a/wayvr/src/overlays/edit/sprite_tab.rs +++ b/wayvr/src/overlays/edit/sprite_tab.rs @@ -105,7 +105,7 @@ where .widgets .get_as::(self.top_sprite_id) { - sprite.set_content(common, Some(new.sprite.clone())); + sprite.set_content(common.alterables, Some(new.sprite.clone())); } } diff --git a/wayvr/src/overlays/keyboard/builder.rs b/wayvr/src/overlays/keyboard/builder.rs index ec72e09c..ced68625 100644 --- a/wayvr/src/overlays/keyboard/builder.rs +++ b/wayvr/src/overlays/keyboard/builder.rs @@ -1,8 +1,5 @@ -use super::{ - KeyButtonData, KeyState, KeyboardState, handle_mouse_motion, handle_press, handle_release, - init_swipe_type_manager, - layout::{self, KeyCapType}, -}; +use std::{collections::HashMap, rc::Rc, time::Duration}; + use crate::{ app_misc, gui::{ @@ -14,18 +11,13 @@ use crate::{ subsystem::hid::XkbKeymap, windowing::backend::OverlayEventData, }; -use anyhow::{Context, bail}; +use anyhow::Context; use glam::{FloatExt, Mat4, Vec2, vec2, vec3}; -use slotmap::Key; -use std::{collections::HashMap, rc::Rc, time::Duration}; -use wgui::event::StyleSetRequest; -use wgui::layout::LayoutTask; -use wgui::taffy::Display; use wgui::{ animation::{Animation, AnimationEasing}, assets::AssetPath, drawing::{self, Color}, - event::{self, CallbackMetadata, EventAlterables, EventListenerKind}, + event::{self, CallbackMetadata, EventListenerKind}, layout::LayoutUpdateParams, log::LogErr, parser::{Fetchable, ParseDocumentParams}, @@ -34,6 +26,11 @@ use wgui::{ widget::{EventResult, div::WidgetDiv, rectangle::WidgetRectangle}, }; +use super::{ + KeyButtonData, KeyState, KeyboardState, handle_press, handle_release, + layout::{self, KeyCapType}, +}; + const PIXELS_PER_UNIT: f32 = 60.; fn new_doc_params(panel: &mut GuiPanel) -> ParseDocumentParams<'static> { @@ -44,134 +41,6 @@ fn new_doc_params(panel: &mut GuiPanel) -> ParseDocumentParams<'s } } -pub(super) fn update_swipe_prediction_bar( - panel: &mut GuiPanel, - app: &mut AppState, -) -> anyhow::Result { - let mut elements_changed = false; - - let (accent_color, anim_mult) = { - let theme = &app.wgui_theme; - (theme.accent_color, theme.animation_mult) - }; - - if let Some(recv) = panel.state.swipe_candidate_receiver.as_mut() - && let Ok(candidates) = recv.try_recv() - { - let predictions_root = panel - .parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - - if predictions_root.is_null() { - return Ok(elements_changed); - } - let doc_params = new_doc_params(panel); - - panel.layout.remove_children(predictions_root); - - let Some(new_suggestions) = candidates else { - return Ok(elements_changed); - }; - - let mut iter = new_suggestions.iter(); - let Some(best_prediction) = iter.next() else { - bail!("not enough swipe predictions"); - }; - if let Some(manager) = panel.state.swipe_typing_manager.as_mut() { - manager.select_word(best_prediction, app, panel.state.modifiers); - } - for (i, prediction) in iter.enumerate() { - let mut params = HashMap::new(); - let id: Rc = Rc::from(format!("Prediction-{i}")); - params.insert("id".into(), id.clone()); - params.insert("text".into(), prediction.clone().into()); - - panel.parser_state.instantiate_template( - &doc_params, - "KeyPrediction", - &mut panel.layout, - predictions_root, - params, - )?; - - if let Ok(widget_id) = panel.parser_state.get_widget_id(&id) { - let key_state = { - let rect = panel - .layout - .state - .widgets - .get_as::(widget_id) - .unwrap(); // want panic - - Rc::new(KeyState { - // fake button state just so we get key state for anims - button_state: KeyButtonData::Modifier { - modifier: 0, - sticky: core::cell::Cell::new(false), - }, - color: rect.params.color, - color2: rect.params.color2, - base_border_color: rect.params.border_color, - cur_border_color: rect.params.border_color.into(), - border: rect.params.border, - drawn_state: false.into(), - }) - }; - panel.add_event_listener( - widget_id, - EventListenerKind::MousePress, - Box::new({ - let k = key_state.clone(); - let pred = prediction.clone(); - move |common, data, app, state| { - if let Some(manager) = state.swipe_typing_manager.as_mut() { - manager.select_alternate_prediction(&pred, app, state.modifiers); - on_press_anim(k.clone(), common, data) - } - Ok(EventResult::Pass) - } - }), - ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseEnter, - Box::new({ - let k = key_state.clone(); - move |common, data, _app, _state| { - on_enter_anim(k.clone(), common, data, accent_color, anim_mult, 0.0); - Ok(EventResult::Pass) - } - }), - ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseLeave, - Box::new({ - let k = key_state.clone(); - move |common, data, _app, _state| { - on_leave_anim(k.clone(), common, data, accent_color, anim_mult, 0.0); - Ok(EventResult::Pass) - } - }), - ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseRelease, - Box::new({ - let k = key_state.clone(); - move |common, data, _app, _state| { - on_release_anim(k.clone(), common, data); - Ok(EventResult::Pass) - } - }), - ); - } - } - elements_changed = true; - } - Ok(elements_changed) -} #[allow(clippy::too_many_lines, clippy::significant_drop_tightening)] pub(super) fn create_keyboard_panel( app: &mut AppState, @@ -244,7 +113,7 @@ pub(super) fn create_keyboard_panel( params.insert(Rc::from("width"), Rc::from(key_width.to_string())); params.insert(Rc::from("height"), Rc::from(key_height.to_string())); - let mut label = key.label.clone().into_iter(); + let mut label = key.label.into_iter(); label .next() .and_then(|s| params.insert("text".into(), s.into())); @@ -300,9 +169,6 @@ pub(super) fn create_keyboard_panel( }) }; - let key_cap_type: Rc = Rc::from(key.cap_type); - let key_label: Rc> = Rc::from(key.label); - let width_mul = 1. / my_size_f32; panel.add_event_listener( @@ -320,7 +186,6 @@ pub(super) fn create_keyboard_panel( anim_mult, width_mul, ); - Ok(EventResult::Pass) } }), @@ -340,7 +205,6 @@ pub(super) fn create_keyboard_panel( anim_mult, width_mul, ); - Ok(EventResult::Pass) } }), @@ -350,69 +214,26 @@ pub(super) fn create_keyboard_panel( EventListenerKind::MousePress, Box::new({ let k = key_state.clone(); - let k_label = key_label.clone(); - let k_cap_type = key_cap_type.clone(); move |common, data, app, state| { let CallbackMetadata::MouseButton(button) = data.metadata else { panic!("CallbackMetadata should contain MouseButton!"); }; - let within_key_pos = data - .metadata - .get_mouse_pos_normalized(&common.alterables.transform_stack); - handle_press( - app, - &k, - &k_label, - &k_cap_type, - &within_key_pos, - state, - button, - button.device, - ); + handle_press(app, &k, state, button); on_press_anim(k.clone(), common, data); Ok(EventResult::Pass) } }), ); - panel.add_event_listener( - widget_id, - EventListenerKind::MouseMotion, - Box::new({ - let k = key_state.clone(); - let k_label = key_label.clone(); - let k_cap_type = key_cap_type.clone(); - move |common, data, _app, state| { - let within_key_pos = data - .metadata - .get_mouse_pos_normalized(&common.alterables.transform_stack); - let CallbackMetadata::MousePosition(position) = data.metadata else { - panic!("CallbackMetadata should contain MousePosition!"); - }; - - handle_mouse_motion( - &k, - &k_label, - &k_cap_type, - state, - &within_key_pos, - position.device, - ); - Ok(EventResult::Pass) - } - }), - ); panel.add_event_listener( widget_id, EventListenerKind::MouseRelease, Box::new({ let k = key_state.clone(); - let k_cap_type = key_cap_type.clone(); move |common, data, app, state| { - if handle_release(app, &k, &k_cap_type, state) { + if handle_release(app, &k, state) { on_release_anim(k.clone(), common, data); } - Ok(EventResult::Pass) } }), @@ -444,13 +265,10 @@ pub(super) fn create_keyboard_panel( panel.on_notify = Some(Box::new({ let name = "kbd"; move |panel, app, event_data| { - let mut alterables = EventAlterables::default(); - let mut elems_changed = panel.state.overlay_list.on_notify( &mut panel.layout, &mut panel.parser_state, &event_data, - &mut alterables, &doc_params, )?; @@ -458,7 +276,6 @@ pub(super) fn create_keyboard_panel( &mut panel.layout, &mut panel.parser_state, &event_data, - &mut alterables, &doc_params, )?; @@ -486,41 +303,6 @@ pub(super) fn create_keyboard_panel( elems_changed = true; } } - if !app.session.config.keyboard_swipe_to_type_enabled { - panel.state.swipe_typing_manager = None; - panel.state.swipe_candidate_receiver = None; - - let predictions_root = panel - .parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - - if !predictions_root.is_null() { - panel.layout.remove_children(predictions_root); - - panel.layout.tasks.push(LayoutTask::SetWidgetStyle( - predictions_root, - StyleSetRequest::Display(Display::None), - )); - } - } - if app.session.config.keyboard_swipe_to_type_enabled - && panel.state.swipe_typing_manager.is_none() - { - init_swipe_type_manager(&mut panel.state); - - let predictions_root = panel - .parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - - if !predictions_root.is_null() { - panel.layout.tasks.push(LayoutTask::SetWidgetStyle( - predictions_root, - StyleSetRequest::Display(Display::Flex), - )); - } - } } OverlayEventData::CustomCommand { element, command } => { @@ -536,7 +318,6 @@ pub(super) fn create_keyboard_panel( panel.process_custom_elems(app); } - panel.layout.process_alterables(alterables)?; Ok(()) } })); diff --git a/wayvr/src/overlays/keyboard/layout.rs b/wayvr/src/overlays/keyboard/layout.rs index 87cfa8c1..2af0590f 100644 --- a/wayvr/src/overlays/keyboard/layout.rs +++ b/wayvr/src/overlays/keyboard/layout.rs @@ -1,13 +1,14 @@ use std::{collections::HashMap, str::FromStr, sync::LazyLock}; +use regex::Regex; +use serde::{Deserialize, Serialize}; + use crate::{ config::{ConfigType, load_known_yaml}, subsystem::hid::{ KEYS_TO_MODS, KeyType, META, NUM_LOCK, SHIFT, VirtualKey, XkbKeymap, get_key_type, }, }; -use regex::Regex; -use serde::{Deserialize, Serialize}; use super::KeyButtonData; @@ -229,7 +230,7 @@ pub(super) struct KeyData { pub(super) cap_type: KeyCapType, } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug)] pub enum KeyCapType { /// Label an SVG Special, diff --git a/wayvr/src/overlays/keyboard/mod.rs b/wayvr/src/overlays/keyboard/mod.rs index dc432395..966f5baa 100644 --- a/wayvr/src/overlays/keyboard/mod.rs +++ b/wayvr/src/overlays/keyboard/mod.rs @@ -1,6 +1,10 @@ -use crate::overlays::keyboard::builder::update_swipe_prediction_bar; -use crate::overlays::keyboard::layout::KeyCapType; -use crate::overlays::keyboard::swipe_type::SwipeTypingManager; +use std::{ + cell::Cell, + collections::HashMap, + process::{Child, Command}, + sync::atomic::Ordering, +}; + use crate::{ KEYMAP_CHANGE, backend::{ @@ -23,20 +27,9 @@ use crate::{ }, }; use anyhow::Context; -use glam::{Affine3A, Quat, Vec2, Vec3, vec3}; +use glam::{Affine3A, Quat, Vec3, vec3}; use regex::Regex; -use slotmap::{Key, SlotMap, new_key_type}; -use std::sync::mpsc::Receiver; -use std::{ - cell::Cell, - collections::HashMap, - process::{Child, Command}, - sync::atomic::Ordering, -}; -use wgui::event::StyleSetRequest; -use wgui::layout::LayoutTask; -use wgui::parser::Fetchable; -use wgui::taffy::Display; +use slotmap::{SlotMap, new_key_type}; use wgui::{ drawing, event::{InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex}, @@ -49,7 +42,6 @@ use wlx_common::{ pub mod builder; mod layout; -mod swipe_type; pub const KEYBOARD_NAME: &str = "kbd"; const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, META]; @@ -64,8 +56,6 @@ pub fn create_keyboard(app: &mut AppState, wayland: bool) -> anyhow::Result anyhow::Result { - state.swipe_typing_manager = Some(engine); - state.swipe_candidate_receiver = Some(receiver); - } - Err(e) => { - log::error!("Error occurred while trying to load swipe engine: {}", e); - } - }; -} -pub(self) fn hide_swipe_type_manager(panel: &mut GuiPanel) { - let predictions_root = panel - .parser_state - .get_widget_id("swipe_predictions_root") - .unwrap_or_default(); - if !predictions_root.is_null() { - panel.layout.tasks.push(LayoutTask::SetWidgetStyle( - predictions_root, - StyleSetRequest::Display(Display::None), - )); - } -} + const fn alt_modifier_to_key(m: AltModifier) -> KeyModifier { match m { AltModifier::Shift => SHIFT, @@ -182,18 +150,8 @@ impl KeyboardBackend { keymap: Option<&XkbKeymap>, app: &mut AppState, ) -> anyhow::Result { - let mut state = self.default_state.take(); - - if app.session.config.keyboard_swipe_to_type_enabled { - init_swipe_type_manager(&mut state); - } - - log::info!("swipe engine created"); - let mut panel = create_keyboard_panel(app, keymap, state, &self.wlx_layout)?; - - if !app.session.config.keyboard_swipe_to_type_enabled { - hide_swipe_type_manager(&mut panel); - } + let panel = + create_keyboard_panel(app, keymap, self.default_state.take(), &self.wlx_layout)?; let id = self.layout_panels.insert(panel); if let Some(layout_name) = keymap.and_then(|k| k.get_name()) { @@ -218,38 +176,30 @@ impl KeyboardBackend { if self.active_layout.eq(new_key) { return Ok(false); } - self.internal_switch_keymap(*new_key, app); + self.internal_switch_keymap(*new_key); } else { let new_key = self.add_new_keymap(Some(keymap), app)?; - self.internal_switch_keymap(new_key, app); + self.internal_switch_keymap(new_key); } app.tasks .enqueue(TaskType::Overlay(OverlayTask::KeyboardChanged)); Ok(true) } - fn internal_switch_keymap(&mut self, new_key: KeyboardPanelKey, app: &AppState) { - let mut state_from = self + fn internal_switch_keymap(&mut self, new_key: KeyboardPanelKey) { + let state_from = self .layout_panels .get_mut(self.active_layout) .unwrap() .state .take(); - if app.session.config.keyboard_swipe_to_type_enabled { - init_swipe_type_manager(&mut state_from); - } - self.active_layout = new_key; self.layout_panels .get_mut(self.active_layout) .unwrap() .state = state_from; - - if !app.session.config.keyboard_swipe_to_type_enabled { - hide_swipe_type_manager(self.layout_panels.get_mut(self.active_layout).unwrap()) - } } fn get_effective_keymap(&mut self) -> anyhow::Result { @@ -283,13 +233,6 @@ impl KeyboardBackend { } } - fn update_swipe_prediction_bar(&mut self, app: &mut AppState) -> anyhow::Result<()> { - if update_swipe_prediction_bar(self.panel(), app)? { - self.panel().process_custom_elems(app); - } - Ok(()) - } - fn auto_switch_keymap(&mut self, app: &mut AppState) -> anyhow::Result { let keymap = self.get_effective_keymap()?; app.hid_provider @@ -323,7 +266,6 @@ impl OverlayBackend for KeyboardBackend { }); } } - self.update_swipe_prediction_bar(app)?; self.panel().should_render(app) } fn render(&mut self, app: &mut AppState, rdr: &mut RenderResources) -> anyhow::Result<()> { @@ -385,8 +327,6 @@ struct KeyboardState { overlay_list: OverlayList, set_list: SetList, clock_12h: bool, - swipe_typing_manager: Option, - swipe_candidate_receiver: Option>>>, } macro_rules! take_and_leave_default { @@ -406,8 +346,6 @@ impl KeyboardState { overlay_list: OverlayList::default(), set_list: SetList::default(), clock_12h: self.clock_12h, - swipe_typing_manager: None, - swipe_candidate_receiver: None, } } } @@ -448,75 +386,26 @@ enum KeyButtonData { }, } -fn handle_mouse_motion( - key: &KeyState, - key_label: &Vec, - key_cap_type: &KeyCapType, - keyboard: &mut KeyboardState, - within_key_pos: &Option, - device: usize, -) { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() - && matches!(*key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) - { - if !swipe_manager.is_current_swipe_empty() { - match &key.button_state { - KeyButtonData::Key { vk, pressed } => { - if let Some(pos) = within_key_pos { - // check because mouse motion can trigger despite hover being false - if pos.x >= 0.0 && pos.x <= 1.0 && pos.y >= 0.0 && pos.y <= 1.0 { - if let Some(label) = key_label.first() { - swipe_manager.add_swipe( - pos, - label.chars().next().unwrap_or_default(), - device, - ); - } - } - } - } - _ => {} - } - } - } -} fn handle_press( app: &mut AppState, key: &KeyState, - key_label: &Vec, - key_cap_type: &KeyCapType, - within_key_pos: &Option, keyboard: &mut KeyboardState, button: MouseButtonEvent, - device: usize, ) { match &key.button_state { KeyButtonData::Key { vk, pressed } => { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() - && matches!(*key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) - { - if let Some(pos) = within_key_pos { - if let Some(label) = key_label.first() { - swipe_manager.add_swipe( - pos, - label.chars().next().unwrap_or_default(), - device, - ); - } - } - } else { - keyboard.modifiers |= match button.index { - MouseButtonIndex::Right => SHIFT, - MouseButtonIndex::Middle => keyboard.alt_modifier, - _ => 0, - }; - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), *vk, true); - pressed.set(true); - play_key_click(app); - } + keyboard.modifiers |= match button.index { + MouseButtonIndex::Right => SHIFT, + MouseButtonIndex::Middle => keyboard.alt_modifier, + _ => 0, + }; + + app.hid_provider + .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); + app.hid_provider + .send_key_routed(app.wvr_server.as_mut(), *vk, true); + pressed.set(true); + play_key_click(app); } KeyButtonData::Modifier { modifier, sticky } => { sticky.set(keyboard.modifiers & *modifier == 0); @@ -546,51 +435,20 @@ fn handle_press( } } -fn handle_release( - app: &mut AppState, - key: &KeyState, - k_cap_type: &KeyCapType, - keyboard: &mut KeyboardState, -) -> bool { +fn handle_release(app: &mut AppState, key: &KeyState, keyboard: &mut KeyboardState) -> bool { match &key.button_state { KeyButtonData::Key { vk, pressed } => { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() - && matches!(*k_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr) - { - if swipe_manager.did_swipe_leave_first_key() { - match swipe_manager.predict() { - Ok(()) => {} - Err(e) => { - log::error!("{}", e) - } - } - } else { - // pointer must have been released on the same key it was pressed on - swipe_manager.reset(); // drop swipe tracking that was started on press + pressed.set(false); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), *vk, true); - pressed.set(true); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), *vk, false); - play_key_click(app); + for m in &AUTO_RELEASE_MODS { + if keyboard.modifiers & *m != 0 { + keyboard.modifiers &= !*m; } - } else { - if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() { - swipe_manager.reset(); - } - pressed.set(false); - - for m in &AUTO_RELEASE_MODS { - if keyboard.modifiers & *m != 0 { - keyboard.modifiers &= !*m; - } - } - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), *vk, false); - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); } + app.hid_provider + .send_key_routed(app.wvr_server.as_mut(), *vk, false); + app.hid_provider + .set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers); true } KeyButtonData::Modifier { modifier, sticky } => { diff --git a/wayvr/src/overlays/keyboard/swipe_type.rs b/wayvr/src/overlays/keyboard/swipe_type.rs deleted file mode 100644 index d4e98e75..00000000 --- a/wayvr/src/overlays/keyboard/swipe_type.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::state::AppState; -use crate::subsystem::hid::{CTRL, KeyModifier, VirtualKey}; -use crate::subsystem::input::KeyboardFocus; -use anyhow::bail; -use arboard::Clipboard; -use glam::Vec2; -use std::mem; -use std::sync::mpsc::{Receiver, Sender, SyncSender, channel, sync_channel}; -use std::thread::{self, JoinHandle}; -use std::time::Instant; -use super_swipe_type::SwipePoint; -use super_swipe_type::keyboard_manager::QwertyKeyboardGrid; -use super_swipe_type::swipe_orchestrator::SwipeOrchestrator; - -const PREDICTION_SUGGESTION_COUNT: usize = 5; - -enum PredictionTask { - Predict { - swipe: Vec, - last_word: Option, - }, - Shutdown, -} - -pub struct SwipeTypingManager { - keyboard_gird: QwertyKeyboardGrid, - current_swipe: Vec, - swipe_candidate_sender: SyncSender>>, - prediction_task_sender: Sender, - worker_thread: Option>, - swipe_start_time: Option, - clipboard: Clipboard, - swipe_left_first_key: bool, - first_swipe_char: char, - current_swipe_device: Option, - last_swiped_word: Option, -} - -impl SwipeTypingManager { - pub fn select_alternate_prediction( - &mut self, - word: &String, - app: &mut AppState, - original_keyboard_mods: KeyModifier, - ) { - Self::undo(app, original_keyboard_mods); - self.select_word(word, app, original_keyboard_mods); - } - - pub fn select_word( - &mut self, - word: &String, - app: &mut AppState, - original_keyboard_mods: KeyModifier, - ) { - self.last_swiped_word = Some(word.clone()); - let text_to_paste = format!("{word} "); - - match app.hid_provider.keyboard_focus { - KeyboardFocus::PhysicalScreen => { - if let Ok(_) = self.clipboard.set_text(text_to_paste) { - Self::paste(app, original_keyboard_mods); - } - } - KeyboardFocus::WayVR => { - if let Some(wvr_server) = app.wvr_server.as_mut() { - wvr_server.set_clipboard_text(text_to_paste); - Self::paste(app, original_keyboard_mods); - } - } - } - } - - fn undo(app: &mut AppState, original_keyboard_mods: KeyModifier) { - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), CTRL); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), VirtualKey::Z, true); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), VirtualKey::Z, false); - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), original_keyboard_mods); - } - - fn paste(app: &mut AppState, original_keyboard_mods: KeyModifier) { - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), CTRL); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), VirtualKey::V, true); - app.hid_provider - .send_key_routed(app.wvr_server.as_mut(), VirtualKey::V, false); - app.hid_provider - .set_modifiers_routed(app.wvr_server.as_mut(), original_keyboard_mods); - } - pub fn new() -> anyhow::Result<(SwipeTypingManager, Receiver>>)> { - let (candidate_sender, candidate_receiver) = sync_channel(1); - let (task_sender, task_receiver) = channel::(); - - // Spawn persistent worker thread - let worker_candidate_sender = candidate_sender.clone(); - let worker_thread = thread::spawn(move || { - let mut swipe_engine = match SwipeOrchestrator::new() { - Ok(engine) => engine, - Err(e) => { - log::error!("Failed to initialize SwipeOrchestrator: {}", e); - return; - } - }; - - while let Ok(task) = task_receiver.recv() { - match task { - PredictionTask::Predict { swipe, last_word } => { - match swipe_engine.predict(swipe, &last_word) { - Ok(candidates) => { - let words: Vec = candidates - .into_iter() - .take(PREDICTION_SUGGESTION_COUNT) - .map(|c| c.word) - .collect(); - - let _ = worker_candidate_sender.send(Some(words)); - } - Err(e) => { - log::error!("Prediction failed: {}", e); - } - } - } - PredictionTask::Shutdown => break, - } - } - }); - - Ok(( - Self { - keyboard_gird: QwertyKeyboardGrid::new(), - current_swipe: Vec::new(), - swipe_candidate_sender: candidate_sender, - prediction_task_sender: task_sender, - worker_thread: Some(worker_thread), - swipe_start_time: None, - clipboard: Clipboard::new()?, - swipe_left_first_key: false, - first_swipe_char: char::default(), - current_swipe_device: None, - last_swiped_word: None, - }, - candidate_receiver, - )) - } - - pub fn predict(&mut self) -> anyhow::Result<()> { - if self.is_current_swipe_empty() { - bail!("nothing to predict"); - } - - let current_swipe = mem::take(&mut self.current_swipe); - let last_word = self.last_swiped_word.clone(); - self.reset_swipe(); - - self.prediction_task_sender.send(PredictionTask::Predict { - swipe: current_swipe, - last_word, - })?; - - Ok(()) - } - - pub fn reset(&mut self) { - self.reset_swipe(); - let _ = self.swipe_candidate_sender.send(None); - self.last_swiped_word = None; - } - - fn reset_swipe(&mut self) { - self.swipe_start_time = None; - self.current_swipe = Vec::new(); - self.first_swipe_char = char::default(); - self.swipe_left_first_key = false; - self.current_swipe_device = None; - } - - fn start_swipe(&mut self, key_label: char, device: usize) -> Instant { - let now = Instant::now(); - self.swipe_start_time = Some(now); - self.first_swipe_char = key_label.to_ascii_lowercase(); - self.current_swipe_device = Some(device); - now - } - - pub fn did_swipe_leave_first_key(&self) -> bool { - self.swipe_left_first_key - } - - pub fn is_current_swipe_empty(&self) -> bool { - self.current_swipe.is_empty() - } - - pub fn add_swipe(&mut self, within_key_pos_normalized: &Vec2, key_label: char, device: usize) { - if let Some(pos) = self - .keyboard_gird - .key_positions - .get(&key_label.to_ascii_lowercase()) - { - if let Some(current_device) = self.current_swipe_device { - if current_device != device { - return; - } - } - - if self.first_swipe_char != char::default() - && self.first_swipe_char != key_label.to_ascii_lowercase() - { - self.swipe_left_first_key = true; - } - - let key_pos = Vec2 { - x: pos.x as f32, - y: pos.y as f32, - }; - - let start_time = match self.swipe_start_time { - Some(time) => time, - None => self.start_swipe(key_label, device), - }; - - let within_key_pos_from_center = Vec2 { - x: within_key_pos_normalized.x - 0.5, - y: within_key_pos_normalized.y - 0.5, - }; - let key_dimensions = Vec2 { - x: QwertyKeyboardGrid::get_key_width() as f32, - y: QwertyKeyboardGrid::get_key_height() as f32, - }; - - let point = within_key_pos_from_center * key_dimensions + key_pos; - let duration = Instant::now().duration_since(start_time).mul_f32(0.8); // multiply by .8 because library is trained on mobile swipes which happen on a smaller keyboard and are faster - self.current_swipe - .push(SwipePoint::new(point.x.into(), point.y.into(), duration)) - } - } -} - -impl Drop for SwipeTypingManager { - fn drop(&mut self) { - let _ = self.prediction_task_sender.send(PredictionTask::Shutdown); - if let Some(handle) = self.worker_thread.take() { - let _ = handle.join(); - } - } -} diff --git a/wayvr/src/overlays/watch.rs b/wayvr/src/overlays/watch.rs index f4f19582..596d2a4d 100644 --- a/wayvr/src/overlays/watch.rs +++ b/wayvr/src/overlays/watch.rs @@ -4,7 +4,7 @@ use glam::{Affine3A, Quat, Vec3, vec3}; use wgui::{ assets::AssetPath, components::button::ComponentButton, - event::{CallbackDataCommon, EventAlterables, StyleSetRequest}, + event::StyleSetRequest, parser::{Fetchable, ParseDocumentParams}, taffy, }; @@ -48,9 +48,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { let mut panel = GuiPanel::new_from_template(app, watch_xml, state, NewGuiPanelParams::default())?; - let mut alterables = EventAlterables::default(); - sets_or_overlays(&panel, app, &mut alterables); - panel.layout.process_alterables(alterables)?; + sets_or_overlays(&mut panel, app); let doc_params = ParseDocumentParams { globals: panel.layout.state.globals.clone(), @@ -61,13 +59,10 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { panel.on_notify = Some(Box::new({ let name = WATCH_NAME; move |panel, app, event_data| { - let mut alterables = EventAlterables::default(); - let mut elems_changed = panel.state.overlay_list.on_notify( &mut panel.layout, &mut panel.parser_state, &event_data, - &mut alterables, &doc_params, )?; @@ -75,7 +70,6 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { &mut panel.layout, &mut panel.parser_state, &event_data, - &mut alterables, &doc_params, )?; @@ -93,16 +87,12 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { .parser_state .fetch_component_as::("btn_edit_mode") { - let mut com = CallbackDataCommon { - alterables: &mut alterables, - state: &panel.layout.state, - }; - btn_edit_mode.set_sticky_state(&mut com, edit_mode); + btn_edit_mode.set_sticky_state(&mut panel.layout.common(), edit_mode); } } OverlayEventData::SettingsChanged => { panel.layout.mark_redraw(); - sets_or_overlays(panel, app, &mut alterables); + sets_or_overlays(panel, app); if app.session.config.clock_12h != panel.state.clock_12h { panel.state.clock_12h = app.session.config.clock_12h; @@ -135,7 +125,6 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { panel.process_custom_elems(app); } - panel.layout.process_alterables(alterables)?; Ok(()) } })); @@ -173,11 +162,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result { }) } -fn sets_or_overlays( - panel: &GuiPanel, - app: &mut AppState, - alterables: &mut EventAlterables, -) { +fn sets_or_overlays(panel: &mut GuiPanel, app: &mut AppState) { let display = if app.session.config.sets_on_watch { [taffy::Display::None, taffy::Display::Flex] } else { @@ -196,6 +181,9 @@ fn sets_or_overlays( ]; for i in 0..2 { - alterables.set_style(widget[i], StyleSetRequest::Display(display[i])); + panel + .layout + .alterables + .set_style(widget[i], StyleSetRequest::Display(display[i])); } } diff --git a/wayvr/src/shaders/grid.frag b/wayvr/src/shaders/grid.frag index 467aec41..d9016ae1 100644 --- a/wayvr/src/shaders/grid.frag +++ b/wayvr/src/shaders/grid.frag @@ -1,18 +1,37 @@ #version 310 es precision highp float; -layout (location = 0) in vec2 in_uv; -layout (location = 0) out vec4 out_color; +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; +const float circle_smoothness = 0.0025; +const float circle_thickness = 0.01; +const float circle_opacity = 0.3; +const float circle_size = 0.1; - 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); +float calc_grid(vec2 coord, float m) { + vec2 grid = fract(coord * m); + return (step(m, grid.x) * step(m, grid.y)); +} + +float calc_circle(float dist, float size) { + float c1 = size; + float c2 = size - circle_thickness; + + return smoothstep(c1, c1 - circle_smoothness, dist) * + smoothstep(c2 - circle_smoothness, c2, dist); +} + +void main() { + float dist = length(in_uv.xy + vec2(-0.5, -0.5)); + float fade = max(1.0 - 2.0 * dist, 0.0); + + float mask = 1.0 - calc_grid(in_uv.xy * 1000.0, 0.02); + + mask = max(mask, (calc_circle(dist, circle_size) + + calc_circle(dist, circle_size * 2.0) + + calc_circle(dist, circle_size * 3.0)) * + circle_opacity); + + out_color = vec4(1.0, 1.0, 1.0, mask * fade); } diff --git a/wayvr/src/shaders/mod.rs b/wayvr/src/shaders/mod.rs index 86e9f5c4..d7c986a3 100644 --- a/wayvr/src/shaders/mod.rs +++ b/wayvr/src/shaders/mod.rs @@ -32,3 +32,10 @@ pub mod frag_srgb { path: "src/shaders/srgb.frag", } } + +pub mod frag_sky { + vulkano_shaders::shader! { + ty: "fragment", + path: "src/shaders/sky.frag", + } +} diff --git a/wayvr/src/shaders/sky.frag b/wayvr/src/shaders/sky.frag new file mode 100644 index 00000000..d26f8110 --- /dev/null +++ b/wayvr/src/shaders/sky.frag @@ -0,0 +1,368 @@ +#version 450 + +layout(location = 0) in vec2 vUV; +layout(location = 0) out vec4 outColor; + +float gauss(float x, float sigma) +{ + float t = x / sigma; + return exp(-t * t); +} + +float smoothBand(float x, float lo, float hi, float featherLo, float featherHi) +{ + float a = smoothstep(lo - featherLo, lo + featherLo, x); + float b = 1.0 - smoothstep(hi - featherHi, hi + featherHi, x); + return a * b; +} + +float thinLine(float x, float halfWidth) +{ + float d = abs(x); + float aa = max(fwidth(x) * 1.5, 1e-5); + + float core = 1.0 - smoothstep(0.0, halfWidth + aa, d); + float edge = 1.0 - smoothstep(halfWidth, halfWidth + aa, d); + + return core * edge; +} + +float hash11(float p) +{ + return fract(sin(p * 127.1 + 311.7) * 43758.5453123); +} + +float hash12(vec2 p) +{ + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); +} + +float noise1Periodic(float x, float period) +{ + float i0 = floor(x); + float i1 = i0 + 1.0; + float f = fract(x); + float u = f * f * (3.0 - 2.0 * f); + + float a = hash11(mod(i0, period)); + float b = hash11(mod(i1, period)); + return mix(a, b, u); +} + +float fbm1Periodic(float x, float period) +{ + float sum = 0.0; + float amp = 0.5; + float norm = 0.0; + + for (int i = 0; i < 5; ++i) + { + sum += amp * noise1Periodic(x, period); + norm += amp; + x *= 2.0; + period *= 2.0; + amp *= 0.5; + } + + return sum / norm; +} + +float noise2TileX(vec2 p, float periodX) +{ + vec2 i = floor(p); + vec2 f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + + float x0 = mod(i.x, periodX); + float x1 = mod(i.x + 1.0, periodX); + float y0 = i.y; + float y1 = i.y + 1.0; + + float a = hash12(vec2(x0, y0)); + float b = hash12(vec2(x1, y0)); + float c = hash12(vec2(x0, y1)); + float d = hash12(vec2(x1, y1)); + + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +float fbm2TileX(vec2 p, float periodX) +{ + float sum = 0.0; + float amp = 0.5; + float norm = 0.0; + + for (int i = 0; i < 4; ++i) + { + sum += amp * noise2TileX(p, periodX); + norm += amp; + p *= 2.0; + periodX *= 2.0; + amp *= 0.5; + } + + return sum / norm; +} + +vec3 tonemapACESApprox(vec3 x) +{ + const float a = 2.51; + const float b = 0.03; + const float c = 2.43; + const float d = 0.59; + const float e = 0.14; + return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0); +} + +float independentPath( + float u, + float baseLat, + float warpAmp0, float warpFreq0, float warpPhase0, + float warpAmp1, float warpFreq1, float warpPhase1, + float amp0, float freq0, float phase0, + float amp1, float freq1, float phase1, + float amp2, float freq2, float phase2, + out float uw) +{ + uw = fract(u + + warpAmp0 * (fbm1Periodic(u * warpFreq0 + warpPhase0, warpFreq0) - 0.5) + + warpAmp1 * (fbm1Periodic(u * warpFreq1 + warpPhase1, warpFreq1) - 0.5)); + + return baseLat + + amp0 * (fbm1Periodic(uw * freq0 + phase0, freq0) - 0.5) + + amp1 * (fbm1Periodic(uw * freq1 + phase1, freq1) - 0.5) + + amp2 * (fbm1Periodic(uw * freq2 + phase2, freq2) - 0.5); +} + +float independentPositive( + float u, + float baseValue, + float amp0, float freq0, float phase0, + float amp1, float freq1, float phase1) +{ + return baseValue + + amp0 * fbm1Periodic(u * freq0 + phase0, freq0) + + amp1 * fbm1Periodic(u * freq1 + phase1, freq1); +} + +void main() +{ + float u = fract(vUV.x); + float v = 0.5 - vUV.y; + + // background gradient + float skyT = smoothstep(-0.50, 0.50, v); + vec3 col = mix(vec3(0.00008, 0.00012, 0.00045), + vec3(0.0220, 0.0470, 0.0980), + skyT); + + col += gauss(v - 0.24, 0.30) * vec3(0.0045, 0.0100, 0.0220) * 0.28; + + // seamless flow field around 360 degrees + float flowA = fbm1Periodic(u * 7.0 + 1.3, 7.0); + float flowB = fbm1Periodic(u * 13.0 + 5.1, 13.0); + + float uu = fract(u + + 0.045 * (flowA - 0.5) + + 0.020 * (flowB - 0.5)); + + float sA = fbm1Periodic(uu * 5.0 + 2.7, 5.0); + float sB = fbm1Periodic(uu * 11.0 + 11.4, 11.0); + float sC = fbm1Periodic(uu * 23.0 + 4.2, 23.0); + float sD = fbm1Periodic(uu * 9.0 + 17.2, 9.0); + float sE = fbm1Periodic(uu * 19.0 + 8.3, 19.0); + + float center = -0.012 + + 0.100 * (sA - 0.5) + + 0.045 * (sB - 0.5) + + 0.018 * (sC - 0.5); + + float thickness = 0.072 + 0.040 * sD; + float skew = 0.014 * (sE - 0.5); + + float mainLo = center - 0.035 - skew; + float mainHi = center + thickness + 0.15 * skew; + + float veilU; + float veilCenter = independentPath( + u, 0.050, + 0.028, 7.0, 1.1, + -0.015, 13.0, 5.4, + 0.050, 3.0, 2.3, + 0.030, 11.0, 8.1, + 0.010, 29.0, 5.7, + veilU); + + float veilThickness = independentPositive( + veilU, 0.011, + 0.010, 9.0, 14.2, + 0.006, 21.0, 3.6); + + float veilLo = veilCenter - 0.004; + float veilHi = veilCenter + veilThickness; + + float auroraU; + float auroraCenter = independentPath( + u, 0.182, + 0.033, 5.0, 3.9, + 0.020, 17.0, 7.7, + 0.060, 4.0, 1.6, + 0.040, 9.0, 12.4, + 0.014, 27.0, 6.9, + auroraU); + + float auroraThickness = independentPositive( + auroraU, 0.034, + 0.014, 7.0, 4.1, + 0.008, 19.0, 15.2); + + float auroraLo = auroraCenter - 0.004; + float auroraHi = auroraCenter + auroraThickness; + + float mistU; + float mistCenter = independentPath( + u, 0.255, + 0.020, 6.0, 2.8, + -0.012, 15.0, 9.1, + 0.030, 4.0, 1.9, + 0.020, 10.0, 13.2, + 0.008, 26.0, 7.4, + mistU); + + float electric0U; + float electric0Center = independentPath( + u, 0.004, + 0.052, 9.0, 2.4, + -0.018, 1.0, 11.6, + 0.098, 3.0, 4.1, + 0.068, 1.0, 9.7, + 0.054, 2.0, 15.3, + electric0U); + + float electric1U; + float electric1Center = independentPath( + u, 0.002, + 0.046, 7.0, 8.3, + 0.020, 7.0, 3.1, + 0.080, 4.0, 6.6, + 0.071, 2.0, 12.2, + 0.064, 1.0, 5.8, + electric1U); + + float electric2U; + float electric2Center = independentPath( + u, 0.006, + 0.044, 3.0, 13.7, + -0.017, 4.0, 6.4, + 0.061, 5.0, 10.9, + 0.082, 6.0, 4.2, + 0.075, 5.0, 14.8, + electric2U); + + float mainTex = fbm2TileX(vec2(uu * 20.0 + 7.0, (v - center) * 8.0 + 13.0), 20.0); + float fineTex = fbm2TileX(vec2(uu * 36.0 + 17.0, (v - center) * 14.0 + 29.0), 36.0); + + float veilTex = fbm2TileX(vec2(veilU * 14.0 + 5.0, (v - veilCenter) * 9.0 + 41.0), 14.0); + float auroraTex = fbm2TileX(vec2(auroraU * 16.0 + 21.0, (v - auroraCenter) * 11.0 + 57.0), 16.0); + float auroraFine = fbm2TileX(vec2(auroraU * 28.0 + 3.0, (v - auroraCenter) * 18.0 + 71.0), 28.0); + + float bodyMod = 0.82 + 0.28 * mainTex + 0.18 * fineTex; + float veilMod = 0.80 + 0.30 * veilTex; + float auroraMod = 0.78 + 0.24 * auroraTex + 0.16 * auroraFine; + + // nasks + float aura = gauss(v - center, 0.14); + + float mainBody = smoothBand(v, mainLo, mainHi, 0.018, 0.024); + float mainMid = gauss(v - mix(mainLo, mainHi, 0.42), 0.55 * thickness); + float mainHiEdge = gauss(v - mainHi, 0.017); + + float veilBody = smoothBand(v, veilLo, veilHi, 0.014, 0.020); + float veilMid = gauss(v - mix(veilLo, veilHi, 0.52), 0.60 * veilThickness); + float veilLoEdge = gauss(v - veilLo, 0.012); + float veilHiEdge = gauss(v - veilHi, 0.018); + + float auroraBody = smoothBand(v, auroraLo, auroraHi, 0.016, 0.022); + float auroraMid = gauss(v - mix(auroraLo, auroraHi, 0.52), 0.62 * auroraThickness); + float auroraLoEdge = gauss(v - auroraLo, 0.013); + float auroraHiEdge = gauss(v - auroraHi, 0.020); + + float mist = gauss(v - mistCenter, 0.085); + + float veilLat = v - veilCenter; + float auroraLat = v - auroraCenter; + + float veilColorNoise = + 0.60 * fbm2TileX(vec2(veilU * 6.0 + 2.0, veilLat * 72.0 + 11.0), 6.0) + + 0.40 * fbm2TileX(vec2(veilU * 14.0 + 7.0, veilLat * 128.0 + 31.0), 14.0); + + float auroraColorU = fract( + auroraU + + 0.035 * (fbm1Periodic(auroraU * 9.0 + 2.7, 9.0) - 0.5) + + 0.018 * (fbm1Periodic(auroraU * 21.0 + 6.1, 21.0) - 0.5) + ); + + vec3 auroraColorNoise = clamp(vec3( + fbm1Periodic(auroraColorU * 5.0 + 1.3, 5.0), // R + fbm1Periodic(auroraColorU * 9.0 + 7.8, 9.0), // G + fbm1Periodic(auroraColorU * 17.0 + 12.4, 17.0) // B + ), 0.0, 1.0); + + veilColorNoise = clamp(veilColorNoise, 0.0, 1.0); + auroraColorNoise = clamp(auroraColorNoise, 0.0, 1.0); + + vec3 veilBaseColor = 0.2 * mix(vec3(0.070, 0.160, 0.460), + vec3(0.140, 0.620, 0.760), + 0.55 * veilColorNoise); + + vec3 veilPeakColor = 0.5 * mix(vec3(0.110, 0.240, 0.620), + vec3(0.180, 0.900, 0.950), + 0.65 * veilColorNoise); + + vec3 auroraBaseColor = 0.2 * vec3(0.100, 0.640, 0.720) * auroraColorNoise * auroraColorNoise; + vec3 auroraPeakColor = 0.5 * vec3(0.180, 0.980, 1.050) * auroraColorNoise * auroraColorNoise; + + float electric0 = thinLine(v - electric0Center, 0.0001); + float electric1 = thinLine(v - electric1Center, 0.00015); + float electric2 = thinLine(v - electric2Center, 0.0002); + + float electric0Glow = gauss(v - electric0Center, 0.0040); + float electric1Glow = gauss(v - electric1Center, 0.0048); + float electric2Glow = gauss(v - electric2Center, 0.0052); + + float e0Energy = 0.82 + 0.28 * fbm1Periodic(electric0U * 41.0 + 6.2, 41.0); + float e1Energy = 0.80 + 0.25 * fbm1Periodic(electric1U * 47.0 + 10.4, 47.0); + float e2Energy = 0.80 + 0.25 * fbm1Periodic(electric2U * 39.0 + 14.9, 39.0); + + // compose + col += aura * (0.85 + 0.25 * mainTex) * vec3(0.014, 0.032, 0.080) * 0.95; + + col += mainBody * bodyMod * vec3(0.070, 0.160, 0.420) * 0.5; + col += mainMid * bodyMod * vec3(0.120, 0.280, 0.740) * 0.4; + col += mainHiEdge * bodyMod * vec3(0.105, 0.240, 0.620) * 0.1; + + col += veilBody * veilMod * veilBaseColor * 1.05; + col += veilMid * veilMod * veilPeakColor * 0.90; + col += veilLoEdge * veilMod * mix(veilBaseColor, veilPeakColor, 0.55) * 0.80; + col += veilHiEdge * veilMod * vec3(0.100, 0.220, 0.580) * 0.20; + + col += auroraBody * auroraMod * auroraBaseColor * 0.95; + col += auroraMid * auroraMod * auroraPeakColor * 0.78; + col += auroraLoEdge * auroraMod * mix(auroraBaseColor, auroraPeakColor, 0.45) * 0.55; + col += auroraHiEdge * auroraMod * vec3(0.080, 0.180, 0.480) * 0.18; + + col += mist * (0.52 + 0.28 * auroraTex) * vec3(0.035, 0.082, 0.220) * 0.20; + + col += electric0Glow * vec3(0.70, 1.10, 1.90) * 0.55; + col += electric1Glow * mix(veilBaseColor, veilPeakColor, 0.60) * 1.20; + col += electric2Glow * mix(auroraBaseColor, auroraPeakColor, 0.65) * 1.35; + + col += electric0 * e0Energy * vec3(10.0, 10.3, 10.8); + col += electric1 * e1Energy * vec3( 6.2, 6.8, 7.4); + col += electric2 * e2Energy * vec3( 5.8, 6.5, 7.2); + + //TODO: skipped if rendering to a HDR swapchain + col = tonemapACESApprox(col); + + outColor = vec4(col, 1.0); +} diff --git a/wayvr/src/state.rs b/wayvr/src/state.rs index 3692f771..478fb261 100644 --- a/wayvr/src/state.rs +++ b/wayvr/src/state.rs @@ -9,7 +9,6 @@ use wgui::{ drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex, renderer_vk::context::SharedContext as WSharedContext, }; -use wlx_common::async_executor::AsyncExecutor; use wlx_common::locale::WayVRLangProvider; use wlx_common::{ audio, @@ -38,7 +37,6 @@ use crate::{ pub struct AppState { pub session: AppSession, pub tasks: TaskContainer, - pub executor: AsyncExecutor, pub gfx: Arc, pub gfx_extras: WGfxExtras, @@ -156,13 +154,11 @@ impl AppState { desktop_finder.refresh(); let lang_provider = WayVRLangProvider::from_config(&session.config); - let executor = Rc::new(smol::LocalExecutor::new()); let mut app_state = Self { session, tasks, - executor, gfx, gfx_extras, hid_provider, @@ -223,6 +219,8 @@ pub struct AppSession { pub config: GeneralConfig, pub config_dirty: bool, + pub no_autostart: bool, + pub toast_topics: IdMap, } @@ -245,6 +243,7 @@ impl AppSession { Self { config, toast_topics, + no_autostart: false, config_dirty: false, } } diff --git a/wayvr/src/subsystem/hid/mod.rs b/wayvr/src/subsystem/hid/mod.rs index 5e50d967..b3d5e611 100644 --- a/wayvr/src/subsystem/hid/mod.rs +++ b/wayvr/src/subsystem/hid/mod.rs @@ -277,7 +277,6 @@ impl HidProvider for UInputProvider { log::error!("send_key: {res}"); } } - fn set_desktop_extent(&mut self, extent: Vec2) { self.desktop_extent = extent; } @@ -361,7 +360,7 @@ pub const META: KeyModifier = 0x80; #[allow(non_camel_case_types)] #[repr(u16)] -#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, IntegerId, EnumString, EnumIter, Hash)] +#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, IntegerId, EnumString, EnumIter)] pub enum VirtualKey { Escape = 9, N1, // number row diff --git a/wayvr/src/subsystem/monado_metrics/metrics_fd.rs b/wayvr/src/subsystem/monado_metrics/metrics_fd.rs index 983a39c9..4e349ca1 100644 --- a/wayvr/src/subsystem/monado_metrics/metrics_fd.rs +++ b/wayvr/src/subsystem/monado_metrics/metrics_fd.rs @@ -8,6 +8,7 @@ use crate::subsystem::monado_metrics::proto; pub struct MonadoMetricsFd { stream_reader: UnixStream, + #[allow(dead_code)] stream_writer: UnixStream, records: VecDeque, diff --git a/wgui/src/animation.rs b/wgui/src/animation.rs index c89d29cb..2269e716 100644 --- a/wgui/src/animation.rs +++ b/wgui/src/animation.rs @@ -133,16 +133,16 @@ impl Animations { anim.pos_prev = anim.pos; anim.pos = pos; - anim.call(state, alterables, 1.0); if anim.last_tick { + anim.call(state, alterables, 1.0); alterables.needs_redraw = true; + } else { + anim.ticks_remaining -= 1; } - - anim.ticks_remaining -= 1; } - self.running_animations.retain(|anim| anim.ticks_remaining > 0); + self.running_animations.retain(|anim| !anim.last_tick); } pub fn process(&mut self, state: &LayoutState, alterables: &mut EventAlterables, alpha: f32) { diff --git a/wgui/src/components/bar_graph.rs b/wgui/src/components/bar_graph.rs index 37026997..6e3df392 100644 --- a/wgui/src/components/bar_graph.rs +++ b/wgui/src/components/bar_graph.rs @@ -70,11 +70,7 @@ impl ComponentTrait for ComponentBarGraph { fn refresh(&self, data: &mut RefreshData) { let state = self.state.borrow(); - - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - self.update_limits_text(&state, &mut lc.common()); - let _ = lc.finish(); + self.update_limits_text(&state, &mut data.layout.common()); } } diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs index e498d736..e543f059 100644 --- a/wgui/src/components/button.rs +++ b/wgui/src/components/button.rs @@ -123,21 +123,16 @@ impl ComponentTrait for ComponentButton { fn refresh(&self, data: &mut RefreshData) { let mut state = self.state.borrow_mut(); - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - if state.active_tooltip.is_some() { - let common = lc.common(); - if let Some(node_id) = common.state.nodes.get(self.base.get_id()) { - if !widget::is_node_visible(&common.state.tree, *node_id) { + let l_state = &data.layout.state; + if let Some(node_id) = l_state.nodes.get(self.base.get_id()) { + if !widget::is_node_visible(&l_state.tree, *node_id) { state.active_tooltip = None; // destroy the tooltip, this button is now hidden } } else { debug_assert!(false); } } - - let _ = lc.finish(); } } @@ -339,6 +334,18 @@ fn register_event_mouse_leave( ) } +fn register_event_mouse_cancel(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { + listeners.register( + EventListenerKind::MouseCancel, + Box::new(move |_common, _event_data, (), ()| { + let mut state = state.borrow_mut(); + state.down = false; + state.hovered = false; + Ok(EventResult::Pass) + }), + ) +} + fn register_event_mouse_press(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { listeners.register( EventListenerKind::MousePress, @@ -557,6 +564,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul let listeners = &mut root.widget.state().event_listeners; let anim_mult = ess.layout.state.theme.animation_mult; vec![ + register_event_mouse_cancel(state.clone(), listeners), register_event_mouse_enter(data.clone(), state.clone(), listeners, params.tooltip, anim_mult), register_event_mouse_leave(state.clone(), listeners, anim_mult), register_event_mouse_press(state.clone(), listeners), diff --git a/wgui/src/components/checkbox.rs b/wgui/src/components/checkbox.rs index 552e7ed6..4a49e775 100644 --- a/wgui/src/components/checkbox.rs +++ b/wgui/src/components/checkbox.rs @@ -240,6 +240,17 @@ fn register_event_mouse_leave( ) } +fn register_event_mouse_cancel(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { + listeners.register( + EventListenerKind::MouseCancel, + Box::new(move |_common, _event_data, (), ()| { + let mut state = state.borrow_mut(); + state.down = false; + Ok(EventResult::Pass) + }), + ) +} + fn register_event_mouse_press(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { listeners.register( EventListenerKind::MousePress, @@ -439,6 +450,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul vec![ register_event_mouse_enter(state.clone(), listeners, params.tooltip, anim_mult), register_event_mouse_leave(state.clone(), listeners, anim_mult), + register_event_mouse_cancel(state.clone(), listeners), register_event_mouse_press(state.clone(), listeners), register_event_mouse_release(data.clone(), state.clone(), listeners), ] diff --git a/wgui/src/components/color_selector.rs b/wgui/src/components/color_selector.rs index bbda2afe..16631c58 100644 --- a/wgui/src/components/color_selector.rs +++ b/wgui/src/components/color_selector.rs @@ -7,10 +7,9 @@ use crate::{ }, drawing::{self}, event::CallbackDataCommon, - globals::WguiGlobals, i18n::Translation, layout::{Layout, WidgetID, WidgetPair}, - parser::{self, Fetchable, ParseDocumentParams, ParserState}, + parser::{self, Fetchable, ParseDocumentParams}, widget::{ConstructEssentials, rectangle::WidgetRectangle, util::WLength}, windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra}, }; @@ -91,18 +90,12 @@ impl ComponentTrait for ComponentColorSelector { } } - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - let mut common = lc.common(); - self.data.button.set_text( - &mut common, + &mut data.layout.common(), Translation::from_raw_text_string(format!("{}", state.color.to_hex_rgb())), ); - self.data.button.set_color(&mut common, state.color); - - let _ = lc.finish(); + self.data.button.set_color(&mut data.layout.common(), state.color); } } @@ -169,12 +162,10 @@ impl ComponentColorSelector { let slider_b = parser_state.fetch_component_as::("slider_b")?; { - let mut lc = layout.start_common(); - let common = &mut lc.common(); - - slider_r.set_value(common, state.color.r * 255.0); - slider_g.set_value(common, state.color.g * 255.0); - slider_b.set_value(common, state.color.b * 255.0); + let mut common = layout.common(); + slider_r.set_value_primary(&mut common, state.color.r * 255.0); + slider_g.set_value_primary(&mut common, state.color.g * 255.0); + slider_b.set_value_primary(&mut common, state.color.b * 255.0); } slider_r.on_value_changed(self.gen_slider_callback(ColorIndex::Red)); diff --git a/wgui/src/components/editbox.rs b/wgui/src/components/editbox.rs index 0b9032bf..c28650e6 100644 --- a/wgui/src/components/editbox.rs +++ b/wgui/src/components/editbox.rs @@ -147,15 +147,9 @@ impl ComponentTrait for ComponentEditBox { } fn refresh(&self, data: &mut RefreshData) { - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - let mut common = lc.common(); - let mut state = self.state.borrow_mut(); - let res = refresh_all(&mut common, &self.data, &mut state); + let res = refresh_all(&mut data.layout.common(), &self.data, &mut state); debug_assert!(res.is_some()); - - let _ = lc.finish(); } fn on_focus_change(&self, data: &mut FocusChangeData) { diff --git a/wgui/src/components/slider.rs b/wgui/src/components/slider.rs index 36ae9421..fa61b8bf 100644 --- a/wgui/src/components/slider.rs +++ b/wgui/src/components/slider.rs @@ -11,8 +11,8 @@ use crate::{ }, drawing::{self}, event::{ - self, CallbackDataCommon, CallbackMetadata, EventAlterables, EventListenerCollection, EventListenerKind, - StyleSetRequest, + self, CallbackDataCommon, CallbackMetadata, DeviceBitmask, EventAlterables, EventListenerCollection, + EventListenerKind, StyleSetRequest, }, i18n::Translation, layout::{WidgetID, WidgetPair}, @@ -29,31 +29,29 @@ use crate::{ }, }; +#[derive(Default, Clone)] +pub struct Value(pub f32); + #[derive(Default)] -pub struct ValuesMinMax { - pub value: f32, +pub struct Limits { pub min_value: f32, pub max_value: f32, pub step: f32, } -impl ValuesMinMax { - fn to_normalized(&self) -> f32 { - (self.value - self.min_value) / (self.max_value - self.min_value) +impl Value { + pub fn get(&self) -> f32 { + self.0 } - fn get_from_normalized(&self, normalized: f32) -> f32 { - normalized * (self.max_value - self.min_value) + self.min_value - } - - fn set_value(&mut self, new_value: f32) -> &mut Self { - let span = self.max_value - self.min_value; - let clamped = new_value.max(self.min_value).min(self.max_value); + pub fn set(&mut self, limits: &Limits, new_value: f32) { + let span = limits.max_value - limits.min_value; + let clamped = new_value.max(limits.min_value).min(limits.max_value); // get the step index from min - let mut k = ((clamped - self.min_value) / self.step).round(); + let mut k = ((clamped - limits.min_value) / limits.step).round(); - let k_max = (span / self.step).floor(); + let k_max = (span / limits.step).floor(); if k < 0.0 { k = 0.0; } @@ -61,25 +59,50 @@ impl ValuesMinMax { k = k_max; } - let snapped = self.min_value + k * self.step; - self.value = snapped.max(self.min_value).min(self.max_value); + let snapped = limits.min_value + k * limits.step; + self.0 = snapped.max(limits.min_value).min(limits.max_value); + } +} - self +impl Limits { + fn to_normalized(&self, value: f32) -> f32 { + (value - self.min_value) / (self.max_value - self.min_value) + } + + fn get_from_normalized(&self, normalized: f32) -> f32 { + normalized * (self.max_value - self.min_value) + self.min_value } } #[derive(Default)] pub struct Params { pub style: taffy::Style, - pub values: ValuesMinMax, + pub limits: Limits, + pub value1: Value, + pub value2: Option, // range slider support pub show_value: bool, pub tooltip: Option, } +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ValueIndex { + Primary, + Secondary, /* for range sliders */ +} + +struct DraggedBy { + index: ValueIndex, + device: DeviceBitmask, +} + struct State { - dragged_by: Option, - hovered: bool, - values: ValuesMinMax, + dragged_by: Option, + hovered_body: bool, + hovered1: bool, + hovered2: bool, + value1: Value, + value2: Option, + limits: Limits, on_value_changed: Option, active_tooltip: Option>, } @@ -90,14 +113,20 @@ impl TooltipTrait for State { } } +struct SliderHandleData { + id_handle_rect: WidgetID, // Rectangle + id_text: Option, // Text + id_handle: WidgetID, +} + struct Data { body_node: taffy::NodeId, - slider_handle_rect_id: WidgetID, // Rectangle - slider_text_id: Option, // Text - slider_handle_id: WidgetID, + handle1: SliderHandleData, + handle2: Option, } pub struct SliderValueChangedEvent { + pub index: ValueIndex, pub value: f32, } @@ -111,13 +140,15 @@ pub struct ComponentSlider { impl ComponentTrait for ComponentSlider { fn refresh(&self, data: &mut RefreshData) { - // FIXME: refactor this after merging feat-skybox-catalog branch - let mut lc = data.layout.start_common(); - let mut common = lc.common(); + let mut common = data.layout.common(); let mut state = self.state.borrow_mut(); - let value = state.values.value; - state.set_value(&mut common, &self.data, value); - let _ = lc.finish(); + + let value1 = state.value1.get(); + state.set_value(&mut common, &self.data, ValueIndex::Primary, value1); + + if let Some(value2) = state.value2.as_ref().map(|v| v.get()) { + state.set_value(&mut common, &self.data, ValueIndex::Secondary, value2); + } } fn base(&self) -> &ComponentBase { @@ -130,13 +161,25 @@ impl ComponentTrait for ComponentSlider { } impl ComponentSlider { - pub fn get_value(&self) -> f32 { - self.state.borrow().values.value + pub fn get_value_primary(&self) -> f32 { + self.get_value(ValueIndex::Primary).unwrap() /* safe */ } - pub fn set_value(&self, common: &mut CallbackDataCommon, value: f32) { + pub fn get_value(&self, index: ValueIndex) -> Option { + let state = self.state.borrow(); + match index { + ValueIndex::Primary => Some(state.value1.get()), + ValueIndex::Secondary => state.value2.as_ref().map(|v| v.get()), + } + } + + pub fn set_value(&self, common: &mut CallbackDataCommon, index: ValueIndex, new_value: f32) { let mut state = self.state.borrow_mut(); - state.set_value(common, &self.data, value); + state.set_value(common, &self.data, index, new_value); + } + + pub fn set_value_primary(&self, common: &mut CallbackDataCommon, new_value: f32) { + self.set_value(common, ValueIndex::Primary, new_value); } pub fn on_value_changed(&self, func: SliderValueChangedCallback) { @@ -156,14 +199,15 @@ fn get_width(slider_body_node: taffy::NodeId, tree: &taffy::tree::TaffyTree, ) -> bool { - /* returns false if nothing changed */ - let norm = values.to_normalized(); + /* returns false if nothing has changed */ + let norm = limits.to_normalized(value); // convert normalized value to taffy percentage margin in percent let width = get_width(body_node, tree); @@ -187,11 +231,22 @@ const HANDLE_WIDTH: f32 = 32.0; const HANDLE_HEIGHT: f32 = 24.0; impl State { + fn get_hovered_index(&self) -> Option { + if self.hovered1 { + Some(ValueIndex::Primary) + } else if self.hovered2 { + Some(ValueIndex::Secondary) + } else { + None + } + } + fn update_value_to_mouse( &mut self, event_data: &event::CallbackData<'_>, data: &Data, common: &mut CallbackDataCommon, + index: ValueIndex, ) { let mouse_pos = event_data .metadata @@ -203,10 +258,10 @@ impl State { get_width(data.body_node, &common.state.tree) - HANDLE_WIDTH, ); - let target_value = self.values.get_from_normalized(norm); + let target_value = self.limits.get_from_normalized(norm); let val = target_value; - self.set_value(common, data, val); + self.set_value(common, data, index, val); } fn update_text(common: &mut CallbackDataCommon, text: &mut WidgetLabel, value: f32) { @@ -220,23 +275,47 @@ impl State { text.set_text(common, Translation::from_raw_text(&pretty)); } - fn set_value(&mut self, common: &mut CallbackDataCommon, data: &Data, value: f32) { - let before = self.values.value; - self.values.set_value(value); + fn set_value(&mut self, common: &mut CallbackDataCommon, data: &Data, index: ValueIndex, new_value: f32) { + let val1 = self.value1.get(); - let changed = self.values.value != before; + let Some(value) = (match index { + ValueIndex::Primary => Some(&mut self.value1), + ValueIndex::Secondary => self.value2.as_mut(), + }) else { + return; + }; - let Some(slider_handle_node_id) = common.state.nodes.get(data.slider_handle_id) else { + // Slider handle widget + let Some(handle_data) = (match index { + ValueIndex::Primary => Some(&data.handle1), + ValueIndex::Secondary => data.handle2.as_ref(), + }) else { + unreachable!(); + }; + + let before = value.get(); + + if index == ValueIndex::Secondary { + value.set(&self.limits, new_value.max(val1)); + } else { + value.set(&self.limits, new_value); + } + + let has_changed = value.get() != before; + + let Some(slider_handle_node_id) = common.state.nodes.get(handle_data.id_handle) else { return; }; let Ok(style) = common.state.tree.style(*slider_handle_node_id) else { return; }; + if !conf_handle_style( common.alterables, - &self.values, - data.slider_handle_id, + &self.limits, + value.get(), + handle_data.id_handle, data.body_node, style, &common.state.tree, @@ -244,20 +323,21 @@ impl State { return; // nothing changed visually } - common.alterables.mark_dirty(data.slider_handle_id); + common.alterables.mark_dirty(handle_data.id_handle); common.alterables.mark_redraw(); - if let Some(slider_text_id) = data.slider_text_id - && let Some(mut label) = common.state.widgets.get_as::(slider_text_id) + if let Some(id_text) = handle_data.id_text + && let Some(mut label) = common.state.widgets.get_as::(id_text) { - Self::update_text(common, &mut label, self.values.value); + Self::update_text(common, &mut label, value.get()); } - if changed && let Some(on_value_changed) = &self.on_value_changed { + if has_changed && let Some(on_value_changed) = &self.on_value_changed { on_value_changed( common, SliderValueChangedEvent { - value: self.values.value, + index: index, + value: value.get(), }, ) } @@ -313,18 +393,16 @@ fn on_leave_anim(common: &mut event::CallbackDataCommon, handle_id: WidgetID, an } fn register_event_mouse_enter( - data: Rc, state: Rc>, listeners: &mut EventListenerCollection, tooltip_info: Option, - anim_mult: f32, ) -> event::EventListenerID { listeners.register( EventListenerKind::MouseEnter, Box::new(move |common, event_data, (), ()| { common.alterables.trigger_haptics(); - state.borrow_mut().hovered = true; - on_enter_anim(common, data.slider_handle_rect_id, anim_mult); + state.borrow_mut().hovered_body = true; + ComponentTooltip::register_hover_in(common, &tooltip_info, event_data.widget_id, state.clone()); Ok(EventResult::Pass) @@ -333,10 +411,8 @@ fn register_event_mouse_enter( } fn register_event_mouse_leave( - data: Rc, state: Rc>, listeners: &mut EventListenerCollection, - anim_mult: f32, ) -> event::EventListenerID { listeners.register( EventListenerKind::MouseLeave, @@ -345,36 +421,110 @@ fn register_event_mouse_leave( { let mut state = state.borrow_mut(); - state.hovered = false; + state.hovered_body = false; state.active_tooltip = None; } - on_leave_anim(common, data.slider_handle_rect_id, anim_mult); Ok(EventResult::Pass) }), ) } +fn get_handle_dist(common: &mut CallbackDataCommon, handle: &SliderHandleData, mouse_pos: Vec2) -> f32 { + let center = common.state.get_widget_boundary(handle.id_handle).center(); + Vec2::distance(center, mouse_pos) +} + +const MAX_HOVER_DIST: f32 = 64.0; + +fn update_handle_hovers( + common: &mut CallbackDataCommon, + data: &Data, + state: &mut State, + anim_mult: f32, + mouse_pos: Vec2, +) { + let hovered1_prev = state.hovered1; + let hovered2_prev = state.hovered2; + + if !state.hovered_body { + state.hovered1 = false; + state.hovered2 = false; + } else { + let dist1 = get_handle_dist(common, &data.handle1, mouse_pos); + + let dist2 = data + .handle2 + .as_ref() + .map(|h| get_handle_dist(common, h, mouse_pos)) + .unwrap_or(std::f32::MAX); + + state.hovered1 = dist1 <= MAX_HOVER_DIST; + state.hovered2 = dist2 <= MAX_HOVER_DIST; + + // both of them are hovered, hover the closest one + if state.hovered1 && state.hovered2 { + if dist1 < dist2 { + state.hovered2 = false; + } else { + state.hovered1 = false; + } + } + } + + // hover state changed, run animations + if state.hovered1 != hovered1_prev { + if state.hovered1 && !hovered1_prev { + on_enter_anim(common, data.handle1.id_handle_rect, anim_mult); + } else { + on_leave_anim(common, data.handle1.id_handle_rect, anim_mult); + } + } + + if state.hovered2 != hovered2_prev + && let Some(handle2) = data.handle2.as_ref() + { + if state.hovered2 && !hovered2_prev { + on_enter_anim(common, handle2.id_handle_rect, anim_mult); + } else { + on_leave_anim(common, handle2.id_handle_rect, anim_mult); + } + } +} + fn register_event_mouse_motion( data: Rc, state: Rc>, listeners: &mut EventListenerCollection, + anim_mult: f32, ) -> event::EventListenerID { listeners.register( EventListenerKind::MouseMotion, Box::new(move |common, event_data, (), ()| { let mut state = state.borrow_mut(); - let CallbackMetadata::MousePosition(pos) = event_data.metadata else { + let Some(pos_relative) = event_data + .metadata + .get_mouse_pos_relative(&common.alterables.transform_stack) + else { unreachable!(); }; - if state.dragged_by.is_some_and(|device| device == pos.device) { - state.update_value_to_mouse(event_data, &data, common); - Ok(EventResult::Consumed) - } else { - Ok(EventResult::Pass) + let CallbackMetadata::MousePosition(pos) = &event_data.metadata else { + unreachable!(); + }; + + update_handle_hovers(common, &data, &mut state, anim_mult, pos_relative); + + if let Some(dragged_by) = &state.dragged_by { + if dragged_by.device == pos.device { + let index = dragged_by.index; + state.update_value_to_mouse(event_data, &data, common, index); + return Ok(EventResult::Consumed); + } } + + Ok(EventResult::Pass) }), ) } @@ -395,13 +545,18 @@ fn register_event_mouse_press( unreachable!(); }; - if state.hovered { - state.dragged_by = Some(btn.device); - state.update_value_to_mouse(event_data, &data, common); - Ok(EventResult::Consumed) - } else { - Ok(EventResult::Pass) + if !state.hovered_body { + // this slider isn't hovered at all? + return Ok(EventResult::Pass); } + + let hovered_index = state.get_hovered_index().unwrap_or(ValueIndex::Primary); + state.dragged_by = Some(DraggedBy { + device: btn.device, + index: hovered_index, + }); + state.update_value_to_mouse(event_data, &data, common, hovered_index); + Ok(EventResult::ConsumedExclusive) }), ) } @@ -426,36 +581,11 @@ fn register_event_mouse_release( ) } -pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc)> { - let mut style = params.style; - style.position = taffy::Position::Relative; - style.min_size = style.size; - style.max_size = style.size; - - let (root, slider_body_node) = ess.layout.add_child(ess.parent, WidgetDiv::create(), style)?; - let body_id = root.id; - - let (_background_id, _) = ess.layout.add_child( - body_id, - WidgetRectangle::create(WidgetRectangleParams { - color: BODY_COLOR, - round: WLength::Percent(1.0), - border_color: BODY_BORDER_COLOR, - border: 2.0, - ..Default::default() - }), - taffy::Style { - size: taffy::Size { - width: percent(1.0), - height: percent(PAD_PERCENT), - }, - position: taffy::Position::Absolute, - align_self: Some(taffy::AlignItems::Center), - justify_self: Some(taffy::JustifySelf::Center), - ..Default::default() - }, - )?; - +fn mount_slider_handle( + ess: &mut ConstructEssentials, + body_id: WidgetID, + show_value: bool, +) -> anyhow::Result { let slider_handle_style = taffy::Style { size: taffy::Size { width: length(0.0), @@ -491,15 +621,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul }, )?; - let state = State { - dragged_by: None, - hovered: false, - values: params.values, - on_value_changed: None, - active_tooltip: None, - }; - - let slider_text: Option<(WidgetPair, taffy::NodeId)> = if params.show_value { + let slider_text: Option<(WidgetPair, taffy::NodeId)> = if show_value { let label = WidgetLabel::create( &mut ess.layout.state, WidgetLabelParams { @@ -517,11 +639,66 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul None }; + Ok(SliderHandleData { + id_handle_rect: slider_handle_rect.id, + id_text: slider_text.map(|s| s.0.id), + id_handle: slider_handle.id, + }) +} + +pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc)> { + let mut style = params.style; + style.position = taffy::Position::Relative; + style.min_size = style.size; + style.max_size = style.size; + + let (root, slider_body_node) = ess.layout.add_child(ess.parent, WidgetDiv::create(), style)?; + let body_id = root.id; + + let (_background_id, _) = ess.layout.add_child( + body_id, + WidgetRectangle::create(WidgetRectangleParams { + color: BODY_COLOR, + round: WLength::Percent(1.0), + border_color: BODY_BORDER_COLOR, + border: 2.0, + ..Default::default() + }), + taffy::Style { + size: taffy::Size { + width: percent(1.0), + height: percent(PAD_PERCENT), + }, + position: taffy::Position::Absolute, + align_self: Some(taffy::AlignItems::Center), + justify_self: Some(taffy::JustifySelf::Center), + ..Default::default() + }, + )?; + + let slider_handle1 = mount_slider_handle(ess, body_id, params.show_value)?; + let slider_handle2 = if params.value2.is_some() { + Some(mount_slider_handle(ess, body_id, params.show_value)?) + } else { + None + }; + + let state = State { + dragged_by: None, + hovered_body: false, + hovered1: false, + hovered2: false, + value1: params.value1, + value2: params.value2, + limits: params.limits, + on_value_changed: None, + active_tooltip: None, + }; + let data = Rc::new(Data { - slider_handle_rect_id: slider_handle_rect.id, body_node: slider_body_node, - slider_handle_id: slider_handle.id, - slider_text_id: slider_text.map(|s| s.0.id), + handle1: slider_handle1, + handle2: slider_handle2, }); let state = Rc::new(RefCell::new(state)); @@ -532,9 +709,9 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul let listeners = &mut root.widget.state().event_listeners; let anim_mult = ess.layout.state.theme.animation_mult; vec![ - register_event_mouse_enter(data.clone(), state.clone(), listeners, params.tooltip, anim_mult), - register_event_mouse_leave(data.clone(), state.clone(), listeners, anim_mult), - register_event_mouse_motion(data.clone(), state.clone(), listeners), + register_event_mouse_enter(state.clone(), listeners, params.tooltip), + register_event_mouse_leave(state.clone(), listeners), + register_event_mouse_motion(data.clone(), state.clone(), listeners, anim_mult), register_event_mouse_press(data.clone(), state.clone(), listeners), register_event_mouse_release(state.clone(), listeners), ] diff --git a/wgui/src/components/tabs.rs b/wgui/src/components/tabs.rs index 521fb842..de6acb66 100644 --- a/wgui/src/components/tabs.rs +++ b/wgui/src/components/tabs.rs @@ -94,6 +94,16 @@ impl ComponentTabs { pub fn on_select(&self, callback: TabSelectCallback) { self.state.borrow_mut().on_select = Some(callback); } + + pub fn get_tab_button(&self, name: &str) -> Option> { + self + .state + .borrow_mut() + .mounted_entries + .iter() + .find(|e| name == &*e.name) + .map(|e| e.button.clone()) + } } pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc)> { diff --git a/wgui/src/event.rs b/wgui/src/event.rs index 1d4c3d5e..5e09650e 100644 --- a/wgui/src/event.rs +++ b/wgui/src/event.rs @@ -19,6 +19,17 @@ use crate::{ widget::{EventResult, WidgetData, WidgetObj}, }; +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub struct DeviceBitmask(pub u8); + +impl DeviceBitmask { + pub fn from_usize(mask: usize) -> Self { + // more than 8 input devices? + debug_assert!(mask & !0xff == 0); + Self(mask as u8) + } +} + #[derive(Debug, Clone, Copy)] pub enum MouseButtonIndex { Left, @@ -30,28 +41,28 @@ pub enum MouseButtonIndex { pub struct MouseButtonEvent { pub index: MouseButtonIndex, pub pos: Vec2, - pub device: usize, + pub device: DeviceBitmask, } #[derive(Debug, Clone, Copy)] pub struct MousePosition { pub pos: Vec2, - pub device: usize, + pub device: DeviceBitmask, } pub struct MouseLeaveEvent { - pub device: usize, + pub device: DeviceBitmask, } pub struct MouseMotionEvent { pub pos: Vec2, - pub device: usize, + pub device: DeviceBitmask, } pub struct MouseWheelEvent { pub pos: Vec2, /* mouse position */ pub delta: Vec2, /* wheel delta */ - pub device: usize, + pub device: DeviceBitmask, } #[derive(Clone)] @@ -70,6 +81,7 @@ pub enum Event { MouseMotion(MouseMotionEvent), MouseUp(MouseButtonEvent), MouseWheel(MouseWheelEvent), + MouseCancel, // Called if the user started scrolling by swiping above the button, to cancel all currently pressed buttons (prevent clicks) TextInput(TextInputEvent), } @@ -106,6 +118,7 @@ pub struct EventAlterables { pub dirty_widgets: Vec, pub components_to_refresh_once: Vec, pub style_set_requests: Vec<(WidgetID, StyleSetRequest)>, + pub global_events_to_emit: Vec, pub animations: Vec, pub widgets_to_tick: HashSet, // widgets which needs to be ticked in the next `Layout::update()` fn pub transform_stack: TransformStack, @@ -129,6 +142,15 @@ impl EventAlterables { self.dirty_widgets.push(widget_id); } + pub fn mark_dirty_and_redraw(&mut self, widget_id: WidgetID) { + self.mark_dirty(widget_id); + self.mark_redraw(); + } + + pub fn emit_global_event(&mut self, event: Event) { + self.global_events_to_emit.push(event); + } + pub fn mark_tick(&mut self, widget_id: WidgetID) { self.widgets_to_tick.insert(widget_id); } @@ -174,8 +196,7 @@ impl CallbackDataCommon<'_> { // helper functions pub fn mark_widget_dirty(&mut self, id: WidgetID) { - self.alterables.mark_dirty(id); - self.alterables.mark_redraw(); + self.alterables.mark_dirty_and_redraw(id); } pub fn globals(&self) -> RefMut<'_, globals::Globals> { @@ -215,18 +236,15 @@ impl CallbackMetadata { let mouse_pos_abs = self.get_mouse_pos_absolute()?; Some(mouse_pos_abs - transform_stack.get().abs_pos) } - pub fn get_mouse_pos_normalized(&self, transform_stack: &TransformStack) -> Option { - let pos_relative = self.get_mouse_pos_relative(transform_stack)?; - Some(pos_relative / transform_stack.parent().raw_dim) - } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EventListenerKind { MousePress, - MouseRelease, - MouseEnter, MouseMotion, + MouseRelease, + MouseCancel, + MouseEnter, MouseLeave, TextInput, InternalStateChange, diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index b3a15aed..0ff8f588 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -11,7 +11,7 @@ use crate::{ drawing::{ self, ANSI_BOLD_CODE, ANSI_RESET_CODE, Boundary, PushScissorStackResult, push_scissor_stack, push_transform_stack, }, - event::{self, CallbackDataCommon, EventAlterables}, + event::{self, CallbackDataCommon, Event, EventAlterables}, globals::WguiGlobals, sound::WguiSoundType, task::Tasks, @@ -86,6 +86,11 @@ impl WidgetMap { self.0.get(handle) } + // same as get(), but with error message + pub fn fetch(&self, handle: WidgetID) -> anyhow::Result { + self.get(handle).cloned().context("Failed to fetch widget") + } + pub fn insert(&mut self, obj: Widget) -> WidgetID { self .0 @@ -163,8 +168,13 @@ pub struct Layout { sounds_to_play_once: Vec, focused_component: Option, + // Global EventAlterables queue, always processed in update() call at the end + pub alterables: EventAlterables, + pub widgets_to_tick: Vec, + global_events_to_emit: Vec, + // *Main root* // contains content_root_widget and topmost widgets pub tree_root_widget: WidgetID, @@ -218,31 +228,11 @@ fn add_child_internal( )) } -pub struct LayoutCommon<'a> { - alterables: EventAlterables, - pub layout: &'a mut Layout, -} - -impl LayoutCommon<'_> { - pub const fn common(&mut self) -> CallbackDataCommon<'_> { +impl Layout { + pub fn common(&mut self) -> CallbackDataCommon<'_> { CallbackDataCommon { alterables: &mut self.alterables, - state: &self.layout.state, - } - } - - pub fn finish(self) -> anyhow::Result<()> { - self.layout.process_alterables(self.alterables)?; - Ok(()) - } -} - -impl Layout { - // helper function - pub fn start_common(&mut self) -> LayoutCommon<'_> { - LayoutCommon { - alterables: EventAlterables::default(), - layout: self, + state: &self.state, } } @@ -338,13 +328,8 @@ impl Layout { self.needs_redraw = true; } - fn process_pending_components(&mut self, alterables: &mut EventAlterables) { + fn process_pending_components(&mut self) { for comp in std::mem::take(&mut self.components_to_refresh_once) { - let mut common = CallbackDataCommon { - state: &self.state, - alterables, - }; - comp.0.refresh(&mut RefreshData { layout: self }); } } @@ -467,19 +452,23 @@ impl Layout { .as_ref() .is_none_or(PushScissorStackResult::should_display) { - // check children first - self.push_event_children(node_id, event, event_result, alterables, user_data)?; + let res_priority = widget.process_event_priority( + &mut self.get_event_params(l, node_id, style, alterables), + widget_id, + event, + )?; - if event_result.can_propagate() { - let mut params = EventParams { - state: &self.state, - layout: l, - alterables, - node_id, - style, - }; + if res_priority.can_propagate() { + // check children first + self.push_event_children(node_id, event, event_result, alterables, user_data)?; - widget.process_event(widget_id, node_id, event, event_result, user_data, &mut params)?; + widget.process_event( + &mut self.get_event_params(l, node_id, style, alterables), + widget_id, + event, + event_result, + user_data, + )?; } } @@ -491,6 +480,22 @@ impl Layout { Ok(()) } + fn get_event_params<'a>( + &'a self, + l: &'a taffy::Layout, + node_id: taffy::NodeId, + style: &'a taffy::Style, + alterables: &'a mut EventAlterables, + ) -> EventParams<'a> { + EventParams { + node_id, + style, + state: &self.state, + alterables, + taffy_layout: l, + } + } + pub const fn check_toggle_needs_redraw(&mut self) -> bool { if self.needs_redraw { self.needs_redraw = false; @@ -525,6 +530,18 @@ impl Layout { &mut (user1, user2), )?; self.process_alterables(alterables)?; + + let mut alterables = EventAlterables::default(); + for event in std::mem::take(&mut self.global_events_to_emit) { + self.push_event_widget( + self.tree_root_node, + &event, + &mut event_result, + &mut alterables, + &mut (user1, user2), + )?; + } + Ok(event_result) } @@ -596,6 +613,8 @@ impl Layout { tasks: LayoutTasks::new(), sounds_to_play_once: Vec::new(), focused_component: None, + alterables: Default::default(), + global_events_to_emit: Vec::new(), }) } @@ -683,10 +702,12 @@ impl Layout { } pub fn update(&mut self, params: &mut LayoutUpdateParams) -> anyhow::Result { - let mut alterables = EventAlterables::default(); + // get all queued alterables and process them + let alterables = std::mem::take(&mut self.alterables); + self .animations - .process(&self.state, &mut alterables, params.timestep_alpha); + .process(&self.state, &mut self.alterables, params.timestep_alpha); self.process_alterables(alterables)?; self.try_recompute_layout(params.size)?; @@ -698,7 +719,7 @@ impl Layout { pub fn tick(&mut self) -> anyhow::Result<()> { let mut alterables = EventAlterables::default(); self.animations.tick(&self.state, &mut alterables); - self.process_pending_components(&mut alterables); + self.process_pending_components(); self.process_pending_widget_ticks(&mut alterables); self.process_alterables(alterables)?; Ok(()) @@ -720,9 +741,7 @@ impl Layout { } } LayoutTask::Dispatch(func) => { - let mut c = self.start_common(); - func(&mut c.common())?; - c.finish()?; + func(&mut self.common())?; } LayoutTask::SetWidgetStyle(widget_id, style_request) => { self.set_style_request(widget_id, &style_request); @@ -740,29 +759,26 @@ impl Layout { } pub fn set_focus(&mut self, to_focus: Option<&Component>) -> anyhow::Result<()> { - let mut c = self.start_common(); - - if let Some(focused) = &c.layout.focused_component + if let Some(focused) = &self.focused_component && let Some(focused) = focused.upgrade() { // Unfocus focused.on_focus_change(&mut FocusChangeData { - common: &mut c.common(), + common: &mut self.common(), focused: false, }); - c.layout.focused_component = None; + self.focused_component = None; } if let Some(to_focus) = to_focus { to_focus.0.on_focus_change(&mut FocusChangeData { - common: &mut c.common(), + common: &mut self.common(), focused: true, }); - c.layout.focused_component = Some(to_focus.weak()); + self.focused_component = Some(to_focus.weak()); } - c.finish()?; Ok(()) } @@ -834,10 +850,12 @@ impl Layout { } } - if !alterables.widgets_to_tick.is_empty() { - for widget_id in &alterables.widgets_to_tick { - self.widgets_to_tick.push(*widget_id); - } + for widget_id in alterables.widgets_to_tick { + self.widgets_to_tick.push(widget_id); + } + + for event in alterables.global_events_to_emit { + self.global_events_to_emit.push(event); } for c in alterables.components_to_refresh_once { diff --git a/wgui/src/parser/component_slider.rs b/wgui/src/parser/component_slider.rs index ef09e8a8..d38a738a 100644 --- a/wgui/src/parser/component_slider.rs +++ b/wgui/src/parser/component_slider.rs @@ -18,7 +18,8 @@ pub fn parse_component_slider( ) -> anyhow::Result { let mut min_value = 0.0; let mut max_value = 1.0; - let mut initial_value = 0.5; + let mut initial_value1 = 0.5; + let mut initial_value2: Option = None; let mut step = 1.0; let mut show_value = 1; let mut tooltip = TooltipAttribs::default(); @@ -35,7 +36,13 @@ pub fn parse_component_slider( ctx.parse_check_f32(tag_name, key, value, &mut max_value); } "value" => { - ctx.parse_check_f32(tag_name, key, value, &mut initial_value); + ctx.parse_check_f32(tag_name, key, value, &mut initial_value1); + } + "value2" => { + let mut val = 0.0; + if ctx.parse_check_f32(tag_name, key, value, &mut val) { + initial_value2 = Some(val); + } } "step" => { ctx.parse_check_f32(tag_name, key, value, &mut step); @@ -56,12 +63,13 @@ pub fn parse_component_slider( }, slider::Params { style, - values: slider::ValuesMinMax { + limits: slider::Limits { min_value, max_value, - value: initial_value, step, }, + value1: slider::Value(initial_value1), + value2: initial_value2.map(|v| slider::Value(v)), show_value: show_value != 0, tooltip: tooltip.get_info(), }, diff --git a/wgui/src/task.rs b/wgui/src/task.rs index e3437cf7..e34be1ad 100644 --- a/wgui/src/task.rs +++ b/wgui/src/task.rs @@ -1,4 +1,4 @@ -use crate::components::button::ComponentButton; +use crate::components::button::{ButtonClickCallback, ComponentButton}; use std::{cell::RefCell, collections::VecDeque, rc::Rc}; pub struct Tasks(Rc>>); @@ -30,21 +30,31 @@ impl Default for Tasks { } } +// copyable tasks only! impl Tasks { - pub fn handle_button(&self, button: &Rc, task: TaskType) { - button.on_click({ - let this = self.clone(); - Rc::new(move |_, _| { - this.push(task.clone()); - Ok(()) - }) - }); + pub fn get_button_click_callback(&self, task: TaskType) -> ButtonClickCallback { + let this = self.clone(); + Rc::new(move |_, _| { + this.push(task.clone()); + Ok(()) + }) } - pub fn make_callback(&self, task: TaskType) -> Rc { + pub fn handle_button(&self, button: &Rc, task: TaskType) { + button.on_click(self.get_button_click_callback(task)); + } + + pub fn make_callback_rc(&self, task: TaskType) -> Rc { let this = self.clone(); Rc::new(move || { this.push(task.clone()); }) } + + pub fn make_callback_box(&self, task: TaskType) -> Box { + let this = self.clone(); + Box::new(move || { + this.push(task.clone()); + }) + } } diff --git a/wgui/src/widget/image.rs b/wgui/src/widget/image.rs index 0b27aa43..0a63ebac 100644 --- a/wgui/src/widget/image.rs +++ b/wgui/src/widget/image.rs @@ -4,7 +4,7 @@ use slotmap::Key; use crate::{ drawing::{self, ImagePrimitive, PrimitiveExtent}, - event::CallbackDataCommon, + event::EventAlterables, globals::Globals, layout::WidgetID, renderer_vk::text::custom_glyph::CustomGlyphData, @@ -44,13 +44,13 @@ impl WidgetImage { ) } - pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option) { + pub fn set_content(&mut self, alterables: &mut EventAlterables, content: Option) { if self.params.glyph_data == content { return; } self.params.glyph_data = content; - common.mark_widget_dirty(self.id); + alterables.mark_dirty_and_redraw(self.id); } pub fn get_content(&self) -> Option { diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs index 08f7aeda..f82aea71 100644 --- a/wgui/src/widget/mod.rs +++ b/wgui/src/widget/mod.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use glam::Vec2; +use glam::{FloatExt, Vec2}; use taffy::{NodeId, TaffyTree}; use super::drawing::RenderPrimitive; @@ -8,7 +8,8 @@ use crate::{ any::AnyTrait, drawing::{self, PrimitiveExtent}, event::{ - self, CallbackData, CallbackDataCommon, CallbackMetadata, Event, EventAlterables, EventListenerCollection, + self, CallbackData, CallbackDataCommon, CallbackMetadata, DeviceBitmask, Event, EventAlterables, + EventListenerCollection, EventListenerKind::{self, InternalStateChange, MouseLeave}, MouseWheelEvent, }, @@ -25,57 +26,66 @@ pub mod rectangle; pub mod sprite; pub mod util; +const SWIPE_START_THRESHOLD_UNITS: f32 = 16.0; +const KINETIC_VELOCITY_DAMPING: f32 = 0.92; +const KINETIC_VELOCITY_BRAKE: f32 = 0.85; +const RUBBERBANDING_DAMP: f32 = 0.8; + pub struct WidgetData { - hovered: usize, - pressed: usize, - pub scrolling_target: Vec2, // normalized, 0.0-1.0. Not used in case if overflow != scroll - pub scrolling_cur: Vec2, // normalized, used for smooth scrolling animation - pub scrolling_cur_prev: Vec2, // for motion interpolation while rendering between ticks + hovered: DeviceBitmask, + pressed: DeviceBitmask, + scrolling_target: Vec2, // normalized, 0.0-1.0. Not used in case if overflow != scroll + scrolling_cur: Vec2, // normalized, used for smooth scrolling animation + scrolling_cur_prev: Vec2, // for motion interpolation while rendering between ticks + scrolling_velocity: Vec2, + press_down_start_mouse_pos: Option, + swipe_running: bool, + swipe_scroll_start: Vec2, // normalized, 0.0-1.0 pub transform: glam::Mat4, pub cached_absolute_boundary: drawing::Boundary, // updated in Layout::push_event_widget } impl WidgetData { - pub const fn set_device_pressed(&mut self, device: usize, pressed: bool) -> bool { - let bit = 1 << device; + pub const fn set_device_pressed(&mut self, device: DeviceBitmask, pressed: bool) -> bool { + let bit = 1 << device.0; let state_changed; if pressed { - state_changed = self.pressed == 0; - self.pressed |= bit; + state_changed = self.pressed.0 == 0; + self.pressed.0 |= bit; } else { - state_changed = self.pressed == bit; - self.pressed &= !bit; + state_changed = self.pressed.0 == bit; + self.pressed.0 &= !bit; } state_changed } - pub const fn set_device_hovered(&mut self, device: usize, hovered: bool) -> bool { - let bit = 1 << device; + pub const fn set_device_hovered(&mut self, device: DeviceBitmask, hovered: bool) -> bool { + let bit = 1 << device.0; let state_changed; if hovered { - state_changed = self.hovered == 0; - self.hovered |= bit; + state_changed = self.hovered.0 == 0; + self.hovered.0 |= bit; } else { - state_changed = self.hovered == bit; - self.hovered &= !bit; + state_changed = self.hovered.0 == bit; + self.hovered.0 &= !bit; } state_changed } - pub const fn get_pressed(&self, device: usize) -> bool { - self.pressed & (1 << device) != 0 + pub const fn get_pressed(&self, device: DeviceBitmask) -> bool { + self.pressed.0 & (1 << device.0) != 0 } - pub const fn get_hovered(&self, device: usize) -> bool { - self.hovered & (1 << device) != 0 + pub const fn get_hovered(&self, device: DeviceBitmask) -> bool { + self.hovered.0 & (1 << device.0) != 0 } pub const fn is_pressed(&self) -> bool { - self.pressed != 0 + self.pressed.0 != 0 } pub const fn is_hovered(&self) -> bool { - self.hovered != 0 + self.hovered.0 != 0 } } @@ -116,13 +126,17 @@ impl WidgetState { fn new(flags: WidgetStateFlags, obj: Box) -> Self { Self { data: WidgetData { - hovered: 0, - pressed: 0, + hovered: DeviceBitmask(0), + pressed: DeviceBitmask(0), scrolling_target: Vec2::default(), scrolling_cur: Vec2::default(), scrolling_cur_prev: Vec2::default(), transform: glam::Mat4::IDENTITY, cached_absolute_boundary: drawing::Boundary::default(), + press_down_start_mouse_pos: None, + swipe_running: false, + swipe_scroll_start: Vec2::default(), + scrolling_velocity: Vec2::default(), }, obj, event_listeners: EventListenerCollection::default(), @@ -192,20 +206,24 @@ pub struct EventParams<'a> { pub style: &'a taffy::Style, pub state: &'a LayoutState, pub alterables: &'a mut EventAlterables, - pub layout: &'a taffy::Layout, + pub taffy_layout: &'a taffy::Layout, } #[derive(Clone, Copy, Eq, PartialEq, PartialOrd)] pub enum EventResult { - NoHit, // event was pushed but has not found a listener (yet) - Pass, // widget acknowledged it and allows the event to propagate further - Consumed, // widget triggered an action, do not propagate further + NoHit, // event was pushed but has not found a listener (yet) + Pass, // widget acknowledged it and allows the event to propagate further + Consumed, // widget triggered an action, do not propagate further + ConsumedExclusive, // same as `Consumed`, but with force-disabled swipe-scroll event (used in sliders for example) } impl EventResult { #[must_use] - pub const fn can_propagate(self) -> bool { - !matches!(self, EventResult::Consumed) + pub const fn can_propagate(&self) -> bool { + match &self { + EventResult::Consumed | EventResult::ConsumedExclusive => false, + _ => true, + } } #[must_use] @@ -221,6 +239,22 @@ fn get_scroll_enabled(style: &taffy::Style) -> (bool, bool) { ) } +const OVERFLOW_START_THRESHOLD_UNITS: f32 = 3.0; // don't show scrollbars for nearly non-scrollable lists + +fn get_scroll_active_axis(style: &taffy::Style, taffy_layout: &taffy::Layout) -> (bool, bool) { + let (enabled_horiz, enabled_vert) = get_scroll_enabled(style); + if !enabled_horiz && !enabled_vert { + return (false, false); + } + + let overflow = Vec2::new(taffy_layout.scroll_width(), taffy_layout.scroll_height()); + + ( + overflow.x >= OVERFLOW_START_THRESHOLD_UNITS, + overflow.y >= OVERFLOW_START_THRESHOLD_UNITS, + ) +} + pub struct ScrollbarInfo { // total contents size of the currently scrolling widget content_size: Vec2, @@ -229,15 +263,13 @@ pub struct ScrollbarInfo { handle_size: Vec2, } -pub fn get_scrollbar_info(l: &taffy::Layout) -> Option { - let overflow_start_threshold_units = 3.0; // don't show scrollbars for nearly non-scrollable lists - - let overflow = Vec2::new(l.scroll_width(), l.scroll_height()); - if overflow.x < overflow_start_threshold_units && overflow.y < overflow_start_threshold_units { +pub fn get_scrollbar_info(taffy_layout: &taffy::Layout) -> Option { + let overflow = Vec2::new(taffy_layout.scroll_width(), taffy_layout.scroll_height()); + if overflow.x < OVERFLOW_START_THRESHOLD_UNITS && overflow.y < OVERFLOW_START_THRESHOLD_UNITS { return None; // not overflowing } - let content_size = Vec2::new(l.content_size.width, l.content_size.height); + let content_size = Vec2::new(taffy_layout.content_size.width, taffy_layout.content_size.height); let handle_size = 1.0 - (overflow / content_size); Some(ScrollbarInfo { @@ -246,6 +278,15 @@ pub fn get_scrollbar_info(l: &taffy::Layout) -> Option { }) } +impl ScrollbarInfo { + fn get_potential_scroll_axis_multiplier(&self, taffy_layout: &taffy::Layout) -> Vec2 { + Vec2::new( + self.content_size.x - taffy_layout.content_box_width(), + self.content_size.y - taffy_layout.content_box_height(), + ) + } +} + impl dyn WidgetObj { pub fn get_as(&self) -> Option<&T> { let any = self.as_any(); @@ -314,8 +355,6 @@ impl WidgetState { } pub fn get_scroll_shift_smooth(&self, info: &ScrollbarInfo, l: &taffy::Layout, timestep_alpha: f32) -> (Vec2, bool) { - //return (self.get_scroll_shift_raw(info, l), false); - let currently_animating = self.data.scrolling_cur != self.data.scrolling_cur_prev; let scrolling = self @@ -348,6 +387,46 @@ impl WidgetState { let scrolling_cur_prev = &mut self.data.scrolling_cur_prev; let scrolling_target = &mut self.data.scrolling_target; + let mut perform_next_tick = false; + + // check for boundaries + if !self.data.swipe_running { + // top bound + if scrolling_target.y < 0.0 { + self.data.scrolling_velocity.y *= KINETIC_VELOCITY_BRAKE; + scrolling_target.y *= RUBBERBANDING_DAMP; + perform_next_tick = true; + } + + // left bound + if scrolling_target.x < 0.0 { + self.data.scrolling_velocity.x *= KINETIC_VELOCITY_BRAKE; + scrolling_target.x *= RUBBERBANDING_DAMP; + perform_next_tick = true; + } + + // right bound + if scrolling_target.x > 1.0 { + self.data.scrolling_velocity.x *= KINETIC_VELOCITY_BRAKE; + scrolling_target.x = 1.0.lerp(scrolling_target.x, RUBBERBANDING_DAMP); + perform_next_tick = true; + } + + // bottom bound + if scrolling_target.y > 1.0 { + self.data.scrolling_velocity.y *= KINETIC_VELOCITY_BRAKE; + scrolling_target.y = 1.0.lerp(scrolling_target.y, RUBBERBANDING_DAMP); + perform_next_tick = true; + } + } + + // Perform kinetic velocity animation + if self.data.scrolling_velocity.length_squared() > 1e-9 { + *scrolling_target += self.data.scrolling_velocity; + self.data.scrolling_velocity *= KINETIC_VELOCITY_DAMPING; + perform_next_tick = true; + } + *scrolling_cur_prev = *scrolling_cur; if scrolling_cur != scrolling_target { @@ -355,8 +434,7 @@ impl WidgetState { *scrolling_cur = scrolling_cur.lerp(*scrolling_target, 0.2); // trigger tick request again - alterables.mark_tick(this_widget_id); - alterables.mark_redraw(); + perform_next_tick = true; let epsilon = 0.00001; if (scrolling_cur.x - scrolling_target.x).abs() < epsilon @@ -365,6 +443,11 @@ impl WidgetState { *scrolling_cur = *scrolling_target; } } + + if perform_next_tick { + alterables.mark_tick(this_widget_id); + alterables.mark_redraw(); + } } pub fn draw_scrollbars(&mut self, state: &mut DrawState, params: &DrawParams, info: &ScrollbarInfo) { @@ -432,13 +515,13 @@ impl WidgetState { return false; } - let l = params.layout; + let l = params.taffy_layout; let overflow = Vec2::new(l.scroll_width(), l.scroll_height()); if overflow.x == 0.0 && overflow.y == 0.0 { return false; // not overflowing } - let Some(info) = get_scrollbar_info(params.layout) else { + let Some(info) = get_scrollbar_info(l) else { return false; }; @@ -451,7 +534,7 @@ impl WidgetState { } let mult = (1.0 / (content_box_length - content_length)) * step_pixels; - let new_scroll = (*scrolling_target + wheel_delta * mult).clamp(0.0, 1.0); + let new_scroll = *scrolling_target + wheel_delta * mult; if *scrolling_target == new_scroll { return; } @@ -479,20 +562,96 @@ impl WidgetState { true } + // this is called before calling children of this widget + pub fn process_event_priority( + &mut self, + params: &mut EventParams, + widget_id: WidgetID, + event: &Event, + ) -> anyhow::Result { + match &event { + Event::MouseUp(_e) => { + if self.data.swipe_running { + self.data.swipe_running = false; + let kinetic_force = (self.data.scrolling_cur - self.data.scrolling_cur_prev) * 20.0; + self.data.scrolling_velocity += kinetic_force; + params.alterables.mark_tick(widget_id); + params.alterables.mark_redraw(); + } + self.data.press_down_start_mouse_pos = None; + } + Event::MouseMotion(e) => { + if let Some(start_mouse_pos) = &self.data.press_down_start_mouse_pos { + let (active_x, active_y) = get_scroll_active_axis(¶ms.style, ¶ms.taffy_layout); + + if !self.data.swipe_running { + if (active_x && (e.pos.x - start_mouse_pos.x).abs() >= SWIPE_START_THRESHOLD_UNITS) + || (active_y && (e.pos.y - start_mouse_pos.y).abs() >= SWIPE_START_THRESHOLD_UNITS) + { + self.data.swipe_running = true; + params.alterables.emit_global_event(Event::MouseCancel); + } + } + + if self.data.swipe_running + && let Some(scrollbar_info) = get_scrollbar_info(params.taffy_layout) + { + let mouse_diff = e.pos - start_mouse_pos; + + let mult = scrollbar_info.get_potential_scroll_axis_multiplier(params.taffy_layout); + + let scroll_diff_x = if mult.x != 0.0 { -mouse_diff.x / mult.x } else { 0.0 }; + let scroll_diff_y = if mult.y != 0.0 { -mouse_diff.y / mult.y } else { 0.0 }; + + self.data.scrolling_target = self.data.swipe_scroll_start + Vec2::new(scroll_diff_x, scroll_diff_y); + params.alterables.mark_tick(self.obj.get_id()); + + return Ok(EventResult::Consumed); + } + } + } + _ => {} + } + + Ok(EventResult::Pass) + } + + fn process_swipe_event(&mut self, event: &Event, params: &EventParams) { + let Event::MouseDown(e) = &event else { + return; + }; + + // firstly, check if this widget is scrollable at all + let (active_x, active_y) = get_scroll_active_axis(¶ms.style, ¶ms.taffy_layout); + if active_x || active_y { + self.data.press_down_start_mouse_pos = Some(e.pos); + self.data.swipe_scroll_start = self.data.scrolling_target; + } + } + pub fn process_event<'a, 'b, U1: 'static, U2: 'static>( &mut self, + params: &'a mut EventParams<'a>, widget_id: WidgetID, - node_id: taffy::NodeId, event: &Event, event_result: &'a mut EventResult, user_data: &'a mut (&'b mut U1, &'b mut U2), - params: &'a mut EventParams<'a>, ) -> anyhow::Result<()> { + // check if we should swipe-scroll this widget + if *event_result != EventResult::ConsumedExclusive { + self.process_swipe_event(event, params); + } + + if !event_result.can_propagate() { + // we don't want to pass any other events + return Ok(()); + } + let hovered = event.test_mouse_within_transform(params.alterables.transform_stack.get()); let mut invoke_data = InvokeData { widget_id, - node_id, + node_id: params.node_id, event_result, user_data, params, @@ -508,6 +667,9 @@ impl WidgetState { CallbackMetadata::TextInput(e.clone()), )?); } + Event::MouseCancel => { + res = Some(self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseCancel, CallbackMetadata::None)?); + } Event::MouseDown(e) => { if hovered && self.data.set_device_pressed(e.device, true) { res = Some(self.invoke_listeners( @@ -528,7 +690,6 @@ impl WidgetState { } Event::MouseMotion(e) => { let hover_state_changed = self.data.set_device_hovered(e.device, hovered); - if hover_state_changed { if self.data.is_hovered() { res = diff --git a/wgui/src/widget/sprite.rs b/wgui/src/widget/sprite.rs index e8063913..f9d3366f 100644 --- a/wgui/src/widget/sprite.rs +++ b/wgui/src/widget/sprite.rs @@ -5,7 +5,7 @@ use slotmap::Key; use crate::{ drawing::{self, PrimitiveExtent}, - event::CallbackDataCommon, + event::{CallbackDataCommon, EventAlterables}, globals::Globals, layout::WidgetID, renderer_vk::text::{ @@ -49,13 +49,13 @@ impl WidgetSprite { self.params.color } - pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option) { + pub fn set_content(&mut self, alterables: &mut EventAlterables, content: Option) { if self.params.glyph_data == content { return; } self.params.glyph_data = content; - common.mark_widget_dirty(self.id); + alterables.mark_dirty_and_redraw(self.id); } pub fn get_content(&self) -> Option { diff --git a/wgui/src/windowing/context_menu.rs b/wgui/src/windowing/context_menu.rs index e9139015..16e62d01 100644 --- a/wgui/src/windowing/context_menu.rs +++ b/wgui/src/windowing/context_menu.rs @@ -117,7 +117,7 @@ impl ContextMenu { par.insert(Rc::from("tooltip_str"), tooltip.generate(&mut globals.i18n())); } - let mut data_cell = inner_parser.realize_template(&doc_params, "Cell", layout, id_buttons, par)?; + let mut data_cell = inner_parser.parse_template_only(&doc_params, "Cell", layout, id_buttons, par)?; let button = data_cell.fetch_component_as::("button")?; let button_id = button.base().get_id(); diff --git a/wgui/src/windowing/window.rs b/wgui/src/windowing/window.rs index 82779273..328ef746 100644 --- a/wgui/src/windowing/window.rs +++ b/wgui/src/windowing/window.rs @@ -9,7 +9,6 @@ use crate::{ components::button::ComponentButton, drawing, event::{EventListenerKind, StyleSetRequest}, - globals::WguiGlobals, i18n::Translation, layout::{Layout, LayoutTask, LayoutTasks, WidgetPair}, parser::{self, Fetchable, ParserState}, @@ -271,21 +270,20 @@ impl WguiWindow { content.id }; - let mut c = params.layout.start_common(); if let Some(width) = params.extra.fixed_width { - c.common() + params + .layout .alterables .set_style(content_id, StyleSetRequest::Width(length(width))); } if let Some(height) = params.extra.fixed_height { - c.common() + params + .layout .alterables .set_style(content_id, StyleSetRequest::Height(length(height))); } - c.finish()?; - Ok(()) } diff --git a/wlx-capture/Cargo.toml b/wlx-capture/Cargo.toml index 8ff3a9e0..5294556e 100644 --- a/wlx-capture/Cargo.toml +++ b/wlx-capture/Cargo.toml @@ -32,8 +32,8 @@ drm-fourcc = "2.2.0" idmap = { workspace = true } libc = "0.2.178" log = { workspace = true } -pipewire = { git = "https://gitlab.freedesktop.org/galister/pipewire-rs.git", rev = "6ef2cd1af41e5e8844c63c37ea9a01f0758079b0", features = [ - "v0_3_33", +pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", rev = "670b0aabf04263ece5dc1e752e5d64aeae3056e8", features = [ + "v0_3_33", "v0_3_65", ], optional = true } #pipewire = { version = "0.8.0", features = ["v0_3_33"], optional = true } rxscreen = { version = "0.1.7", features = [ diff --git a/wlx-capture/src/pipewire.rs b/wlx-capture/src/pipewire.rs index 3d5de1cf..f3e5cdd5 100644 --- a/wlx-capture/src/pipewire.rs +++ b/wlx-capture/src/pipewire.rs @@ -17,14 +17,17 @@ use drm_fourcc::DrmFormat; use drm_fourcc::DrmFourcc; use drm_fourcc::DrmModifier; use pipewire as pw; +use pipewire::context::ContextRc; +use pipewire::main_loop::MainLoopRc; +use pipewire::spa::buffer::meta::MetaVideoTransformValue; +use pipewire::spa::buffer::meta::{MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform}; +use pipewire::stream::StreamRc; use pw::spa; +use pw::Error; use pw::properties::properties; use pw::stream::{Stream, StreamFlags}; -use pw::{Error, context::Context, main_loop::MainLoop}; use spa::buffer::DataType; -use spa::buffer::MetaData; -use spa::buffer::MetaType; use spa::param::ParamType; use spa::param::video::VideoFormat; use spa::param::video::VideoInfoRaw; @@ -278,12 +281,12 @@ where R: Any, { log::debug!("{}: pipewire main_loop start", &name); - let main_loop = MainLoop::new(None)?; - let context = Context::new(&main_loop)?; - let core = context.connect(None)?; + let main_loop = MainLoopRc::new(None)?; + let context = ContextRc::new(&main_loop, None)?; + let core = context.connect_rc(None)?; - let stream = Stream::new( - &core, + let stream = StreamRc::new( + core, &name, properties! { *pw::keys::MEDIA_TYPE => "Video", @@ -377,36 +380,35 @@ where } if let Some(mut buffer) = maybe_buffer { - if let MetaData::Header(header) = buffer.find_meta_data(MetaType::Header) - && header.flags & spa::sys::SPA_META_HEADER_FLAG_CORRUPTED != 0 + if let Some(header) = buffer.find_meta::() + && header.flags().contains(MetaHeaderFlags::CORRUPTED) { log::warn!("{}: PipeWire buffer is corrupt.", &name); return; } - if let MetaData::VideoTransform(transform) = - buffer.find_meta_data(MetaType::VideoTransform) - { - format.transform = match transform.transform { - spa::sys::SPA_META_TRANSFORMATION_None => Transform::Normal, - spa::sys::SPA_META_TRANSFORMATION_90 => Transform::Rotated90, - spa::sys::SPA_META_TRANSFORMATION_180 => Transform::Rotated180, - spa::sys::SPA_META_TRANSFORMATION_270 => Transform::Rotated270, - spa::sys::SPA_META_TRANSFORMATION_Flipped => Transform::Flipped, - spa::sys::SPA_META_TRANSFORMATION_Flipped90 => Transform::Flipped90, - spa::sys::SPA_META_TRANSFORMATION_Flipped180 => Transform::Flipped180, - spa::sys::SPA_META_TRANSFORMATION_Flipped270 => Transform::Flipped270, + + if let Some(transform) = buffer.find_meta::() { + format.transform = match transform.transform() { + MetaVideoTransformValue::NONE => Transform::Normal, + MetaVideoTransformValue::ROTATED90 => Transform::Rotated90, + MetaVideoTransformValue::ROTATED180 => Transform::Rotated180, + MetaVideoTransformValue::ROTATED270 => Transform::Rotated270, + MetaVideoTransformValue::FLIPPED => Transform::Flipped, + MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90, + MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180, + MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270, _ => Transform::Undefined, }; log::debug!("{}: Transform: {:?}", &name, &format.transform); } - let mouse_meta = match buffer.find_meta_data(MetaType::Cursor) { - MetaData::Cursor(cursor) if cursor.id != 0 => Some(MouseMeta { - x: cursor.position.x as f32 / format.width as f32, - y: cursor.position.y as f32 / format.height as f32, - }), - _ => None, - }; + let mouse_meta = buffer + .find_meta::() + .filter(|c| c.is_valid()) + .map(|c| MouseMeta { + x: c.position().x as f32 / format.width as f32, + y: c.position().y as f32 / format.height as f32, + }); let datas = buffer.datas_mut(); if datas.is_empty() { diff --git a/wlx-common/src/config.rs b/wlx-common/src/config.rs index 9ce45c5f..e673bda9 100644 --- a/wlx-common/src/config.rs +++ b/wlx-common/src/config.rs @@ -381,9 +381,6 @@ pub struct GeneralConfig { #[serde(default = "def_one")] pub grid_opacity: f32, - #[serde(default = "def_false")] - pub keyboard_swipe_to_type_enabled: bool, - #[serde(default)] pub chroma_key_params: ChromaKeyParams, } diff --git a/wlx-common/src/config_io.rs b/wlx-common/src/config_io.rs index 961e4b1a..7976a5de 100644 --- a/wlx-common/src/config_io.rs +++ b/wlx-common/src/config_io.rs @@ -21,6 +21,23 @@ pub fn get_config_root() -> PathBuf { CONFIG_ROOT_PATH.clone() } +pub fn get_skymaps_root() -> PathBuf { + get_config_root().join("skymaps") +} + +pub fn get_skymaps_uuids() -> anyhow::Result> { + let data = std::fs::read_to_string(get_skymaps_root().join("skymaps.txt"))?; + Ok(data.lines().filter(|line| !line.is_empty()).map(String::from).collect()) +} + +pub fn set_skymaps_uuids(uuids: &[String]) -> anyhow::Result<()> { + let skymaps_root = get_skymaps_root(); + let _ = std::fs::create_dir_all(&skymaps_root); + let data = String::from_iter(uuids.iter().map(|uuid| format!("{}\n", uuid))); + std::fs::write(skymaps_root.join("skymaps.txt"), data)?; + Ok(()) +} + impl ConfigRoot { pub fn get_conf_d_path(&self) -> PathBuf { get_config_root().join(match self { diff --git a/wlx-common/src/dash_interface.rs b/wlx-common/src/dash_interface.rs index 7f80d557..34990443 100644 --- a/wlx-common/src/dash_interface.rs +++ b/wlx-common/src/dash_interface.rs @@ -43,6 +43,12 @@ pub enum RecenterMode { Reset, } +#[derive(Clone, Copy)] +pub struct InterfaceFeats { + pub openxr: bool, + pub monado: bool, +} + pub trait DashInterface { fn window_list(&mut self, data: &mut T) -> anyhow::Result>; fn window_set_visible(&mut self, data: &mut T, handle: WvrWindowHandle, visible: bool) -> anyhow::Result<()>; @@ -65,9 +71,16 @@ pub trait DashInterface { fn recenter_playspace(&mut self, data: &mut T, mode: RecenterMode) -> anyhow::Result<()>; fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder; fn general_config<'a>(&'a mut self, data: &'a mut T) -> &'a mut GeneralConfig; - fn config_changed(&mut self, data: &mut T); + fn config_changed(&mut self, data: &mut T, kind: ConfigChangeKind); fn restart(&mut self, data: &mut T); fn toggle_dashboard(&mut self, data: &mut T); + fn get_feats(&mut self, data: &mut T) -> InterfaceFeats; +} + +#[derive(Clone, Copy)] +pub enum ConfigChangeKind { + OverlayConfig, + EnvironmentBlend, } pub type BoxDashInterface = Box>; diff --git a/wlx-common/src/dash_interface_emulated.rs b/wlx-common/src/dash_interface_emulated.rs index 3526de94..919054dd 100644 --- a/wlx-common/src/dash_interface_emulated.rs +++ b/wlx-common/src/dash_interface_emulated.rs @@ -5,7 +5,7 @@ use wayvr_ipc::{ use crate::{ config::GeneralConfig, - dash_interface::{self, DashInterface, RecenterMode}, + dash_interface::{self, ConfigChangeKind, DashInterface, RecenterMode}, desktop_finder::DesktopFinder, gen_id, }; @@ -230,12 +230,19 @@ impl DashInterface<()> for DashInterfaceEmulated { &mut self.general_config } - fn config_changed(&mut self, _: &mut ()) {} + fn config_changed(&mut self, _: &mut (), _: ConfigChangeKind) {} fn restart(&mut self, _data: &mut ()) {} fn toggle_dashboard(&mut self, _data: &mut ()) {} + fn get_feats(&mut self, _data: &mut ()) -> dash_interface::InterfaceFeats { + dash_interface::InterfaceFeats { + openxr: true, + monado: true, + } + } + fn monado_client_list( &mut self, _data: &mut (),