diff --git a/Cargo.lock b/Cargo.lock index 5797783a..02207e85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "addr2line" version = "0.25.1" @@ -100,9 +116,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arrayref" @@ -148,9 +164,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "blake3" @@ -178,9 +194,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" @@ -196,9 +212,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -223,9 +239,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -236,9 +252,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -246,9 +262,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -259,9 +275,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.65" +version = "4.5.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" +checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" dependencies = [ "clap", ] @@ -275,14 +291,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -337,9 +353,9 @@ checksum = "24efe21bd9a78102d1225f10f0a41d9d5b43f4df7ae8235f39a9c79e4d476c1e" [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -365,6 +381,72 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ecolor" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" +dependencies = [ + "emath", +] + +[[package]] +name = "egui" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" +dependencies = [ + "ahash", + "bitflags", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", +] + +[[package]] +name = "egui_tiles" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef184e589f0a80560bd3b63017634642d1ba112a8a8d9b29341f7cafd04601f" +dependencies = [ + "ahash", + "egui", + "itertools", + "log", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "emath" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" + +[[package]] +name = "epaint" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" +dependencies = [ + "ab_glyph", + "ahash", + "ecolor", + "emath", + "log", + "nohash-hasher", + "parking_lot", + "profiling", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -426,38 +508,38 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -465,7 +547,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -488,19 +569,19 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -618,6 +699,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1697e6b71679da96d5c41bb9035116141baadbf59a60625fd66cb3c9584e7b0" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -658,6 +748,8 @@ dependencies = [ "clap", "clap_complete", "dirs", + "egui", + "egui_tiles", "futures-util", "gpu-alloc", "gpu-alloc-types", @@ -734,9 +826,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -773,7 +865,7 @@ checksum = "28067e7361c0069c3753795d131653f9ea5333aeb35a3855fb2de66447c48ac8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -790,9 +882,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.181" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -806,19 +898,18 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags", "libc", ] [[package]] name = "linearize" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d5b35550da9461fb8d3acf71c9925afae570bfc45a11857b55138d25c8604d" +checksum = "f6e1430c89633736996fd763822abd252e395dbccaaee33be601b4d59678a93e" dependencies = [ "cfg-if", "linearize-derive", @@ -835,14 +926,14 @@ checksum = "f657db73fbcad5341c5991ddee6c464d4bfd521575c0dc1a47913e0f434defeb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" @@ -875,6 +966,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "num-conv" version = "0.2.0" @@ -889,7 +986,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -943,6 +1040,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -997,7 +1103,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1011,35 +1117,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" @@ -1049,9 +1149,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "png" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ "bitflags", "crc32fast", @@ -1073,7 +1173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1086,19 +1186,25 @@ dependencies = [ ] [[package]] -name = "quick-xml" -version = "0.39.0" +name = "profiling" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2e3bf4aa9d243beeb01a7b3bc30b77cfe2c44e24ec02d751a7104a53c2c49a1" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1109,6 +1215,12 @@ 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.8.5" @@ -1134,7 +1246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -1201,9 +1313,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "repc" @@ -1234,9 +1346,9 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -1314,7 +1426,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1410,9 +1522,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1455,7 +1567,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1538,6 +1650,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "uapi" version = "0.2.13" @@ -1566,9 +1684,15 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" @@ -1636,9 +1760,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1649,9 +1773,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1659,22 +1783,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -1743,7 +1867,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1754,7 +1878,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1967,7 +2091,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.114", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -1983,7 +2107,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2055,26 +2179,26 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 2a7f6bf5..be0de383 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,8 @@ opera = "1.0.1" with_builtin_macros = "0.1.0" blake3 = "1.8.2" run-on-drop = "1.0.0" +egui = { version = "0.33.3", default-features = false } +egui_tiles = { version = "0.14.1", default-features = false } [build-dependencies] repc = "0.1.1" @@ -87,6 +89,38 @@ opt-level = 3 [profile.dev.package."smallvec"] opt-level = 3 +[profile.dev.package."egui"] +opt-level = 3 +debug = "line-tables-only" + +[profile.dev.package."emath"] +opt-level = 3 +debug = "line-tables-only" + +[profile.dev.package."epaint"] +opt-level = 3 +debug = "line-tables-only" + +[profile.dev.package."ab_glyph"] +opt-level = 3 +debug = "line-tables-only" + +[profile.dev.package."ab_glyph_rasterizer"] +opt-level = 3 +debug = "line-tables-only" + +[profile.dev.package."owned_ttf_parser"] +opt-level = 3 +debug = "line-tables-only" + +[profile.dev.package."ttf-parser"] +opt-level = 3 +debug = "line-tables-only" + +[profile.dev.package."ecolor"] +opt-level = 3 +debug = "line-tables-only" + [features] rc_tracking = [] it = [] diff --git a/build/vulkan/hash.rs b/build/vulkan/hash.rs index de6e0692..7a0cb297 100644 --- a/build/vulkan/hash.rs +++ b/build/vulkan/hash.rs @@ -8,23 +8,31 @@ pub struct Tree { pub shaders: &'static [&'static str], } -pub const TREES: &[Tree] = &[Tree { - root: "src/gfx_apis/vulkan/shaders", - hash: "src/gfx_apis/vulkan/shaders_hash.txt", - bin: "src/gfx_apis/vulkan/shaders_bin", - shaders: &[ - "fill.frag", - "fill.vert", - "tex.vert", - "tex.frag", - "out.vert", - "out.frag", - "legacy/fill.frag", - "legacy/fill.vert", - "legacy/tex.vert", - "legacy/tex.frag", - ], -}]; +pub const TREES: &[Tree] = &[ + Tree { + root: "src/gfx_apis/vulkan/shaders", + hash: "src/gfx_apis/vulkan/shaders_hash.txt", + bin: "src/gfx_apis/vulkan/shaders_bin", + shaders: &[ + "fill.frag", + "fill.vert", + "tex.vert", + "tex.frag", + "out.vert", + "out.frag", + "legacy/fill.frag", + "legacy/fill.vert", + "legacy/tex.vert", + "legacy/tex.frag", + ], + }, + Tree { + root: "src/egui_adapter/shaders", + hash: "src/egui_adapter/shaders_hash.txt", + bin: "src/egui_adapter/shaders_bin", + shaders: &["shader.vert", "shader.frag"], + }, +]; fn calculate_hash(tree: &Tree) -> anyhow::Result { let dir = WalkDir::new(tree.root); diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 7c71461e..9c3be1e6 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1035,10 +1035,21 @@ impl ConfigClient { position } + pub fn set_egui_fonts(&self, proportional: Option>, monospace: Option>) { + self.send(&ClientMessage::SetEguiFonts { + proportional, + monospace, + }); + } + pub fn set_middle_click_paste_enabled(&self, enabled: bool) { self.send(&ClientMessage::SetMiddleClickPasteEnabled { enabled }); } + pub fn open_control_center(&self) { + self.send(&ClientMessage::OpenControlCenter); + } + pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) { self.send(&ClientMessage::SetWorkspaceDisplayOrder { order }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index ab1de845..53e3662d 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -841,6 +841,11 @@ pub enum ClientMessage<'a> { fds: Vec<(i32, i32)>, tag: Option<&'a str>, }, + SetEguiFonts { + proportional: Option>, + monospace: Option>, + }, + OpenControlCenter, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index aa975250..0b980479 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -380,3 +380,8 @@ pub fn on_unload(f: impl FnOnce() + 'static) { pub fn set_middle_click_paste_enabled(enabled: bool) { get!().set_middle_click_paste_enabled(enabled); } + +/// Opens the control center. +pub fn open_control_center() { + get!().open_control_center(); +} diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs index 64883b09..d3e3fbb0 100644 --- a/jay-config/src/theme.rs +++ b/jay-config/src/theme.rs @@ -197,6 +197,20 @@ pub fn get_bar_position() -> BarPosition { get!(BarPosition::Top).get_bar_position() } +/// Sets the proportional fonts used by egui windows. +/// +/// The default is `["sans-serif", "Noto Sans", "Noto Color Emoji"]`. +pub fn set_egui_proportional_fonts<'a>(fonts: impl IntoIterator) { + get!().set_egui_fonts(Some(fonts.into_iter().collect()), None); +} + +/// Sets the monospace fonts used by egui windows. +/// +/// The default is `["monospace", "Noto Sans Mono", "Noto Color Emoji"]`. +pub fn set_egui_monospace_fonts<'a>(fonts: impl IntoIterator) { + get!().set_egui_fonts(None, Some(fonts.into_iter().collect())); +} + /// Elements of the compositor whose color can be changed. pub mod colors { use { diff --git a/src/backend.rs b/src/backend.rs index 678b7346..746fc7b4 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -588,7 +588,6 @@ pub trait BackendDrmDevice { fn set_flip_margin(&self, margin: u64) { let _ = margin; } - #[expect(dead_code)] fn flip_margin(&self) -> Option { None } diff --git a/src/cli.rs b/src/cli.rs index 303fe29c..a650e8b3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,7 @@ mod clients; mod color; mod color_management; +mod control_center; mod damage_tracking; mod duration; mod generate; @@ -97,6 +98,8 @@ pub enum Cmd { Clients(ClientsArgs), /// Inspect the surface tree. Tree(TreeArgs), + /// Opens the control center. + ControlCenter, /// Prints the Jay version and exits. Version, /// Prints the Jay PID and exits. @@ -243,5 +246,6 @@ pub fn main() { #[cfg(feature = "it")] Cmd::RunTests => crate::it::run_tests(), Cmd::Reexec(a) => reexec::main(cli.global, a), + Cmd::ControlCenter => control_center::main(cli.global), } } diff --git a/src/cli/control_center.rs b/src/cli/control_center.rs new file mode 100644 index 00000000..3a3f2b93 --- /dev/null +++ b/src/cli/control_center.rs @@ -0,0 +1,32 @@ +use { + crate::{ + cli::GlobalArgs, + tools::tool_client::{Handle, ToolClient, with_tool_client}, + wire::{jay_compositor, jay_open_control_center_request}, + }, + std::rc::Rc, +}; + +pub fn main(global: GlobalArgs) { + with_tool_client(global.log_level, |tc| async move { + let cc = ControlCenter { tc: tc.clone() }; + cc.run().await; + }); +} + +struct ControlCenter { + tc: Rc, +} + +impl ControlCenter { + async fn run(self) { + let tc = &self.tc; + let comp = tc.jay_compositor().await; + let id = tc.id(); + tc.send(jay_compositor::OpenControlCenter { self_id: comp, id }); + jay_open_control_center_request::Failed::handle(&tc, id, (), |_, ev| { + fatal!("Could not open the control center: {}", ev.msg); + }); + tc.round_trip().await; + } +} diff --git a/src/compositor.rs b/src/compositor.rs index 27a96be2..2e10d5bb 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -14,6 +14,7 @@ use { clientmem::{self, ClientMemError}, cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries}, config::ConfigProxy, + control_center::redraw_control_centers, copy_device::CopyDeviceRegistry, cpu_worker::{CpuWorker, CpuWorkerError}, criteria::{ @@ -72,6 +73,7 @@ use { fdcloser::FdCloser, nice::{did_elevate_scheduler, elevate_scheduler}, numcell::NumCell, + object_drop_queue::ObjectDropQueue, oserror::OsError, queue::AsyncQueue, rc_eq::RcEq, @@ -390,6 +392,9 @@ fn start_compositor2( supports_presentation_feedback: Default::default(), eventfd_cache, lazy_event_sources: Default::default(), + bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)), + egg_state: Default::default(), + control_centers: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -414,6 +419,7 @@ fn start_compositor2( let _compositor = engine.spawn("compositor", start_compositor3(state.clone(), test_future)); ring.run()?; state.clear(); + engine.clear(); Ok(()) } @@ -594,6 +600,10 @@ fn start_global_event_handlers(state: &Rc) -> Vec> { "lazy event sources", handle_lazy_event_sources(state.clone()), ), + eng.spawn( + "redraw control centers", + redraw_control_centers(state.clone()), + ), ] } @@ -734,8 +744,7 @@ fn create_dummy_output(state: &Rc) { wlr_output_heads: Default::default(), }); let schedule = Rc::new(OutputSchedule::new( - &state.ring, - &state.eng, + state, &connector_data, &persistent_state, )); diff --git a/src/config/handler.rs b/src/config/handler.rs index 75a1c336..6da5c2d5 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -478,7 +478,7 @@ impl ConfigProxyHandler { } else { Some(self.get_keymap(keymap)?) }; - dev.set_keymap(map); + dev.set_keymap(&self.state, map); Ok(()) } @@ -532,13 +532,13 @@ impl ConfigProxyHandler { ) -> Result<(), CphError> { let dev = self.get_device_handler_data(input_device)?; let output = self.get_output_node(connector)?; - dev.set_output(Some(&output.global)); + dev.set_output(&self.state, Some(&output.global)); Ok(()) } fn handle_remove_input_mapping(&self, input_device: InputDevice) -> Result<(), CphError> { let dev = self.get_device_handler_data(input_device)?; - dev.set_output(None); + dev.set_output(&self.state, None); Ok(()) } @@ -788,7 +788,7 @@ impl ConfigProxyHandler { Some(self.get_seat(seat)?) }; let dev = self.get_device_handler_data(device)?; - dev.set_seat(seat); + dev.set_seat(&self.state, seat); Ok(()) } @@ -798,7 +798,7 @@ impl ConfigProxyHandler { left_handed: bool, ) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_left_handed(left_handed); + dev.set_left_handed(&self.state, left_handed); Ok(()) } @@ -813,31 +813,31 @@ impl ConfigProxyHandler { ACCEL_PROFILE_ADAPTIVE => InputDeviceAccelProfile::Adaptive, _ => return Err(CphError::UnknownAccelProfile(accel_profile)), }; - dev.set_accel_profile(profile); + dev.set_accel_profile(&self.state, profile); Ok(()) } fn handle_set_accel_speed(&self, device: InputDevice, speed: f64) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_accel_speed(speed); + dev.set_accel_speed(&self.state, speed); Ok(()) } fn handle_set_px_per_wheel_scroll(&self, device: InputDevice, px: f64) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_px_per_scroll_wheel(px); + dev.set_px_per_scroll_wheel(&self.state, px); Ok(()) } fn handle_set_tap_enabled(&self, device: InputDevice, enabled: bool) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_tap_enabled(enabled); + dev.set_tap_enabled(&self.state, enabled); Ok(()) } fn handle_set_drag_enabled(&self, device: InputDevice, enabled: bool) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_drag_enabled(enabled); + dev.set_drag_enabled(&self.state, enabled); Ok(()) } @@ -847,7 +847,7 @@ impl ConfigProxyHandler { enabled: bool, ) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_natural_scrolling_enabled(enabled); + dev.set_natural_scrolling_enabled(&self.state, enabled); Ok(()) } @@ -857,7 +857,7 @@ impl ConfigProxyHandler { enabled: bool, ) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_drag_lock_enabled(enabled); + dev.set_drag_lock_enabled(&self.state, enabled); Ok(()) } @@ -867,7 +867,7 @@ impl ConfigProxyHandler { matrix: [[f64; 2]; 2], ) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_transform_matrix(matrix); + dev.set_transform_matrix(&self.state, matrix); Ok(()) } @@ -877,7 +877,7 @@ impl ConfigProxyHandler { matrix: [[f32; 3]; 2], ) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_calibration_matrix(matrix); + dev.set_calibration_matrix(&self.state, matrix); Ok(()) } @@ -893,7 +893,7 @@ impl ConfigProxyHandler { CLICK_METHOD_CLICKFINGER => InputDeviceClickMethod::Clickfinger, _ => return Err(CphError::UnknownClickMethod(click_method)), }; - dev.set_click_method(method); + dev.set_click_method(&self.state, method); Ok(()) } @@ -903,7 +903,7 @@ impl ConfigProxyHandler { enabled: bool, ) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; - dev.set_middle_button_emulation_enabled(enabled); + dev.set_middle_button_emulation_enabled(&self.state, enabled); Ok(()) } @@ -951,8 +951,10 @@ impl ConfigProxyHandler { } fn handle_set_flip_margin(&self, device: DrmDevice, margin: Duration) -> Result<(), CphError> { - self.get_drm_device(device)? - .set_flip_margin(margin.as_nanos().try_into().unwrap_or(u64::MAX)); + self.get_drm_device(device)?.set_flip_margin( + &self.state, + margin.as_nanos().try_into().unwrap_or(u64::MAX), + ); Ok(()) } @@ -987,7 +989,7 @@ impl ConfigProxyHandler { match device { Some(dev) => self .get_drm_device(dev)? - .set_direct_scanout_enabled(enabled), + .set_direct_scanout_enabled(&self.state, enabled), _ => self.state.direct_scanout_enabled.set(enabled), } Ok(()) @@ -1123,11 +1125,11 @@ impl ConfigProxyHandler { } fn handle_set_idle(&self, timeout: Duration) { - self.state.idle.set_timeout(timeout); + self.state.idle.set_timeout(&self.state, timeout); } fn handle_set_idle_grace_period(&self, period: Duration) { - self.state.idle.set_grace_period(period); + self.state.idle.set_grace_period(&self.state, period); } fn handle_set_explicit_sync_enabled(&self, enabled: bool) { @@ -1484,7 +1486,7 @@ impl ConfigProxyHandler { match connector { Some(c) => { let connector = self.get_output_node(c)?; - connector.schedule.set_cursor_hz(hz); + connector.schedule.set_cursor_hz(&self.state, hz); } _ => { let Some((hz, _)) = map_cursor_hz(hz) else { @@ -1807,6 +1809,16 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_egui_fonts(&self, proportional: Option>, monospace: Option>) { + self.state.set_egui_fonts(proportional, monospace); + } + + fn handle_open_control_center(&self) { + if let Err(e) = self.state.open_control_center() { + log::error!("Could not open control center: {}", ErrorFmt(e)); + } + } + fn handle_set_log_level(&self, level: ConfigLogLevel) { self.state.set_log_level(level.into()); } @@ -3311,6 +3323,11 @@ impl ConfigProxyHandler { fds, tag, } => self.handle_run(prog, args, env, fds, tag).wrn("run")?, + ClientMessage::SetEguiFonts { + proportional, + monospace, + } => self.handle_set_egui_fonts(proportional, monospace), + ClientMessage::OpenControlCenter => self.handle_open_control_center(), } Ok(()) } diff --git a/src/control_center.rs b/src/control_center.rs new file mode 100644 index 00000000..203146b6 --- /dev/null +++ b/src/control_center.rs @@ -0,0 +1,681 @@ +use { + crate::{ + control_center::{ + cc_clients::{ClientPane, ClientsPane}, + cc_color_management::ColorManagementPane, + cc_compositor::CompositorPane, + cc_gpus::GpusPane, + cc_idle::IdlePane, + cc_input::InputPane, + cc_look_and_feel::LookAndFeelPane, + cc_outputs::OutputsPane, + cc_window::{WindowPane, WindowSearchPane}, + cc_xwayland::XwaylandPane, + }, + egui_adapter::egui_platform::{ + EggError, EggWindow, EggWindowOwner, + icons::{ICON_CLOSE, ICON_DRAG_INDICATOR, ICON_INFO}, + }, + macros::Bitflag, + state::State, + utils::{ + asyncevent::AsyncEvent, copyhashmap::CopyHashMap, + event_listener::LazyEventSourceListener, numcell::NumCell, static_text::StaticText, + }, + }, + egui::{ + Align, CentralPanel, Checkbox, Color32, ComboBox, Context, CursorIcon, DragValue, Frame, + Grid, InnerResponse, Label, Layout, Response, Rgba, RichText, ScrollArea, Sense, SidePanel, + Stroke, TextBuffer, TextEdit, Ui, UiBuilder, Visuals, Widget, WidgetText, emath::Numeric, + vec2, + }, + egui_tiles::{ResizeState, TabState, Tile, TileId, Tiles, Tree}, + linearize::{Linearize, LinearizeExt}, + std::{ + cell::RefCell, + hash::Hash, + mem, + ops::{Deref, DerefMut, RangeInclusive}, + rc::Rc, + }, + thiserror::Error, +}; + +mod cc_clients; +mod cc_color_management; +mod cc_compositor; +mod cc_criterion; +mod cc_gpus; +mod cc_idle; +mod cc_input; +mod cc_look_and_feel; +mod cc_outputs; +mod cc_sidebar; +mod cc_window; +mod cc_xwayland; + +#[derive(Debug, Error)] +pub enum ControlCenterError { + #[error("Could not get the egg context")] + GetEggContext(#[source] EggError), +} + +linear_ids!(ControlCenterIds, ControlCenterId, u64); + +pub async fn redraw_control_centers(state: Rc) { + let cc = &state.control_centers; + loop { + cc.redraw.triggered().await; + let interests = cc.change.take(); + for cc in cc.control_centers.lock().values() { + if cc.inner.interests.interests.get().intersects(interests) { + cc.inner.window.request_redraw(); + } + } + } +} + +#[derive(Default)] +pub struct ControlCenters { + ids: ControlCenterIds, + change: NumCell, + redraw: AsyncEvent, + control_centers: CopyHashMap>, +} + +bitflags! { + ControlCenterInterest: u32; + CCI_COMPOSITOR, + CCI_IDLE, + CCI_COLOR_MANAGEMENT, + CCI_XWAYLAND, + CCI_OUTPUTS, + CCI_GPUS, + CCI_INPUT, + CCI_LOOK_AND_FEEL, +} + +pub struct ControlCenter { + inner: Rc, +} + +linear_ids!(PaneIds, PaneId, u64); + +struct ControlCenterInner { + id: ControlCenterId, + state: Rc, + tree: RefCell, + window: Rc, + pane_ids: PaneIds, + interests: Rc, +} + +#[derive(Default)] +struct Interests { + interests: NumCell, + interests_array: [NumCell; ::Type::BITS as usize], +} + +struct Settings { + tree: Tree, +} + +struct Pane { + id: PaneId, + ps: PaneState, + own_interests: ControlCenterInterest, + cc_interests: Rc, + ty: PaneType, +} + +struct PaneState { + errors: Vec, +} + +enum PaneType { + Compositor(CompositorPane), + Idle(IdlePane), + ColorManagement(ColorManagementPane), + Xwayland(XwaylandPane), + Outputs(Box), + GPUs(GpusPane), + Input(InputPane), + LookAndFeel(LookAndFeelPane), + Clients(ClientsPane), + Client(ClientPane), + WindowSearch(WindowSearchPane), + Window(WindowPane), +} + +struct CcBehavior<'a> { + cc: &'a Rc, + close: Option, + open: Option, +} + +impl ControlCenters { + pub fn clear(&self) { + self.control_centers.clear(); + } +} + +impl Pane { + fn title(&self, res: &mut String) { + match &self.ty { + PaneType::Compositor(v) => v.title(res), + PaneType::Idle(v) => v.title(res), + PaneType::ColorManagement(v) => v.title(res), + PaneType::Xwayland(v) => v.title(res), + PaneType::Outputs(v) => v.title(res), + PaneType::GPUs(v) => v.title(res), + PaneType::Input(v) => v.title(res), + PaneType::LookAndFeel(v) => v.title(res), + PaneType::Clients(v) => v.title(res), + PaneType::Client(v) => v.title(res), + PaneType::WindowSearch(v) => v.title(res), + PaneType::Window(v) => v.title(res), + } + } + + fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) { + match &mut self.ty { + PaneType::Compositor(p) => p.show(ui), + PaneType::Idle(p) => p.show(ui), + PaneType::ColorManagement(p) => p.show(ui), + PaneType::Xwayland(p) => p.show(behavior, ui), + PaneType::Outputs(p) => p.show(&mut self.ps, ui), + PaneType::GPUs(p) => p.show(ui), + PaneType::Input(p) => p.show(&mut self.ps, ui), + PaneType::LookAndFeel(p) => p.show(ui), + PaneType::Clients(p) => p.show(behavior, ui), + PaneType::Client(p) => p.show(behavior, ui), + PaneType::WindowSearch(p) => p.show(behavior, ui), + PaneType::Window(p) => p.show(behavior, ui), + } + } +} + +impl PaneType { + fn interest(&self) -> ControlCenterInterest { + match self { + PaneType::Compositor(_) => CCI_COMPOSITOR, + PaneType::Idle(_) => CCI_IDLE, + PaneType::ColorManagement(_) => CCI_COLOR_MANAGEMENT, + PaneType::Xwayland(_) => CCI_XWAYLAND, + PaneType::Outputs(_) => CCI_OUTPUTS, + PaneType::GPUs(_) => CCI_GPUS, + PaneType::Input(_) => CCI_INPUT, + PaneType::LookAndFeel(_) => CCI_LOOK_AND_FEEL, + PaneType::Clients(_) => ControlCenterInterest::none(), + PaneType::Client(_) => ControlCenterInterest::none(), + PaneType::WindowSearch(_) => ControlCenterInterest::none(), + PaneType::Window(_) => ControlCenterInterest::none(), + } + } +} + +impl egui_tiles::Behavior for CcBehavior<'_> { + fn pane_ui(&mut self, ui: &mut Ui, tile_id: TileId, pane: &mut Pane) -> egui_tiles::UiResponse { + let mut drag = false; + Frame::central_panel(ui.style()).show(ui, |ui| { + ui.horizontal(|ui| { + drag = ui + .add(icon_label(ICON_DRAG_INDICATOR).sense(Sense::drag())) + .total_drag_delta() + .map(|d| d.length() >= 5.0) + .unwrap_or(false); + let mut title = String::new(); + pane.title(&mut title); + if ui + .add(icon_label(&title).sense(Sense::click())) + .middle_clicked() + { + self.close = Some(tile_id); + } + if ui + .add(icon_label(ICON_CLOSE).sense(Sense::click())) + .clicked() + { + self.close = Some(tile_id); + } + }); + ui.separator(); + show_errors(ui, &mut pane.ps); + ui.scope_builder(UiBuilder::new().id(("pane", pane.id)), |ui| { + ScrollArea::vertical().show(ui, |ui| { + ui.allocate_space(vec2(ui.available_width(), 0.0)); + pane.show(self, ui); + }); + }); + }); + if drag { + egui_tiles::UiResponse::DragStarted + } else { + egui_tiles::UiResponse::None + } + } + + fn tab_title_for_pane(&mut self, _pane: &Pane) -> WidgetText { + "".into() + } + + fn tab_hover_cursor_icon(&self) -> CursorIcon { + CursorIcon::Default + } + + fn tab_title_for_tile(&mut self, tiles: &Tiles, tile_id: TileId) -> WidgetText { + fn add_title(tiles: &Tiles, res: &mut String, first: &mut bool, tile_id: TileId) { + if !mem::take(first) { + res.push_str("/"); + } + let Some(tile) = tiles.get(tile_id) else { + res.push_str("MISSING TILE"); + return; + }; + match tile { + Tile::Pane(p) => p.title(res), + Tile::Container(c) => { + let mut first = true; + for &tile_id in c.children() { + add_title(tiles, res, &mut first, tile_id); + } + } + } + } + let mut res = String::new(); + let mut first = true; + add_title(tiles, &mut res, &mut first, tile_id); + res.into() + } + + fn on_tab_button( + &mut self, + _tiles: &Tiles, + tile_id: TileId, + button_response: Response, + ) -> Response { + if button_response.middle_clicked() { + self.close = Some(tile_id); + } + button_response + } + + fn resize_stroke(&self, style: &egui::Style, resize_state: ResizeState) -> Stroke { + match resize_state { + ResizeState::Idle => style.visuals.widgets.noninteractive.bg_stroke, + ResizeState::Hovering => style.visuals.widgets.hovered.fg_stroke, + ResizeState::Dragging => style.visuals.widgets.active.fg_stroke, + } + } + + fn tab_bar_color(&self, visuals: &Visuals) -> Color32 { + (Rgba::from(visuals.panel_fill) * Rgba::from_gray(0.8)).into() + } + + fn tab_bg_color( + &self, + visuals: &Visuals, + _tiles: &Tiles, + _tile_id: TileId, + state: &TabState, + ) -> Color32 { + match state.active { + true => visuals.panel_fill, + false => self.tab_bar_color(visuals), + } + } +} + +impl EggWindowOwner for ControlCenterInner { + fn close(&self) { + self.close(); + } + + fn render(self: Rc, ctx: &Context) { + let settings = &mut *self.tree.borrow_mut(); + SidePanel::left("sidebar").show(ctx, |ui| self.show_sidebar(&mut settings.tree, ui)); + CentralPanel::default() + .frame( + Frame::central_panel(&ctx.style()) + .outer_margin(0.0) + .inner_margin(0.0), + ) + .show(ctx, |ui| { + let tree = &mut settings.tree; + let mut behavior = CcBehavior { + cc: &self, + close: Default::default(), + open: Default::default(), + }; + tree.ui(&mut behavior, ui); + if let Some(close) = behavior.close { + tree.set_visible(close, false); + tree.remove_recursively(close); + ui.ctx().request_repaint(); + } + if let Some(ty) = behavior.open { + self.open(tree, ty); + ui.ctx().request_repaint(); + } + }); + } +} + +impl State { + pub fn open_control_center(self: &Rc) -> Result, ControlCenterError> { + let ctx = self + .get_egg_context() + .map_err(ControlCenterError::GetEggContext)?; + let window = ctx.create_window("Control Center"); + let cc = Rc::new(ControlCenter { + inner: Rc::new(ControlCenterInner { + id: self.control_centers.ids.next(), + window, + state: self.clone(), + tree: RefCell::new(Settings { + tree: Tree::new_tabs("abcd", vec![]), + }), + pane_ids: Default::default(), + interests: Default::default(), + }), + }); + cc.inner.window.set_owner(Some(cc.inner.clone())); + self.control_centers + .control_centers + .set(cc.inner.id, cc.clone()); + Ok(cc) + } + + pub fn trigger_cci(&self, cci: ControlCenterInterest) { + self.control_centers.change.or_assign(cci); + self.control_centers.redraw.trigger(); + } +} + +impl ControlCenterInner { + fn close(&self) { + self.window.set_owner(None); + self.tree.borrow_mut().tree = Tree::empty(""); + self.state.control_centers.control_centers.remove(&self.id); + } +} + +impl Drop for ControlCenter { + fn drop(&mut self) { + self.inner.close(); + } +} + +impl ControlCenterInner { + fn create_pane(&self, ty: PaneType) -> Pane { + let pane = Pane { + id: self.pane_ids.next(), + ps: PaneState { + errors: Default::default(), + }, + own_interests: ty.interest(), + cc_interests: self.interests.clone(), + ty, + }; + let own = pane.own_interests; + for (idx, v) in pane.cc_interests.interests_array.iter().enumerate() { + let interest = ControlCenterInterest(1 << idx); + if own.intersects(interest) && v.fetch_add(1) == 0 { + pane.cc_interests.interests.or_assign(interest); + } + } + pane + } + + fn open(&self, tree: &mut Tree, ty: PaneType) { + let pane = self.create_pane(ty); + let id = tree.tiles.insert_pane(pane); + if let Some(root) = tree.root + && let Some(tile) = tree.tiles.get_mut(root) + { + match tile { + Tile::Container(c) => { + c.add_child(id); + } + Tile::Pane(_) => { + let root = tree.tiles.insert_tab_tile(vec![root, id]); + tree.root = Some(root); + } + } + } else { + tree.root = Some(id); + } + tree.make_active(|t, _| t == id); + } +} + +impl Drop for Pane { + fn drop(&mut self) { + let own = self.own_interests; + for (idx, v) in self.cc_interests.interests_array.iter().enumerate() { + let interest = ControlCenterInterest(1 << idx); + if own.intersects(interest) && v.fetch_sub(1) == 1 { + self.cc_interests.interests.and_assign(!interest); + } + } + } +} + +fn icon_label(icon: &str) -> Label { + Label::new(icon).selectable(false) +} + +fn grid_label(ui: &mut Ui, label: &str) { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.label(label); + }); +} + +fn grid_label_ui(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { + ui.with_layout(Layout::right_to_left(Align::Center), add_contents) +} + +fn tip(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) { + icon_label(ICON_INFO).ui(ui).on_hover_ui(add_contents); +} + +fn text_edit(ui: &mut Ui, v: &mut dyn TextBuffer) -> Response { + TextEdit::singleline(v) + .clip_text(false) + .min_size(vec2(200.0, 0.0)) + .ui(ui) +} + +fn show_errors(ui: &mut Ui, pane: &mut PaneState) { + if pane.errors.is_empty() { + return; + } + let mut to_remove = None; + for (idx, e) in pane.errors.iter().enumerate() { + ui.horizontal(|ui| { + Frame::new().inner_margin(5.0).show(ui, |ui| { + if ui.button(ICON_CLOSE).clicked() { + to_remove = Some(idx); + } + ui.label( + RichText::new("Error:") + .strong() + .color(ui.style().visuals.error_fg_color), + ); + ui.add(Label::new(e).wrap()); + }); + }); + } + if let Some(idx) = to_remove { + pane.errors.remove(idx); + ui.ctx().request_repaint(); + } + ui.separator(); +} + +fn grid( + ui: &mut Ui, + id_salt: impl Hash, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> InnerResponse { + let mut spacing = ui.spacing().item_spacing; + spacing.x *= 3.0; + Grid::new(id_salt).spacing(spacing).show(ui, add_contents) +} + +fn row(ui: &mut Ui, name: &str, add_contents: impl FnOnce(&mut Ui) -> R) -> R { + row_ui(ui, name, |_| (), add_contents) +} + +fn row_ui( + ui: &mut Ui, + name: &str, + label: impl FnOnce(&mut Ui) -> S, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> R { + let ui = &mut *ui.row(); + grid_label_ui(ui, |ui| { + ui.label(name); + label(ui); + }); + add_contents(ui) +} + +fn bool(ui: &mut Ui, name: &str, old: bool, set: impl FnOnce(bool)) { + bool_ui(ui, name, |_| (), old, set); +} + +fn bool_ui( + ui: &mut Ui, + name: &str, + label: impl FnOnce(&mut Ui) -> R, + mut v: bool, + set: impl FnOnce(bool), +) { + row_ui(ui, name, label, |ui| { + if Checkbox::without_text(&mut v).ui(ui).changed() { + set(v); + } + }); +} + +fn read_only_bool(ui: &mut Ui, name: &str, old: bool) { + read_only_bool_ui(ui, name, |_| (), old); +} + +fn read_only_bool_ui(ui: &mut Ui, name: &str, label: impl FnOnce(&mut Ui) -> R, mut v: bool) { + row_ui(ui, name, label, |ui| { + ui.add_enabled_ui(false, |ui| Checkbox::without_text(&mut v).ui(ui)); + }); +} + +fn combo_box(ui: &mut Ui, name: &str, old: T, set: impl FnOnce(T)) +where + T: StaticText + Linearize + PartialEq + Copy, +{ + combo_box_ui(ui, name, |_| (), old, set); +} + +fn combo_box_ui( + ui: &mut Ui, + name: &str, + label: impl FnOnce(&mut Ui) -> R, + mut v: T, + set: impl FnOnce(T), +) where + T: StaticText + Linearize + PartialEq + Copy, +{ + row_ui(ui, name, label, |ui| { + let old = v; + ComboBox::from_id_salt(name) + .selected_text(v.text()) + .show_ui(ui, |ui| { + for s in T::variants() { + ui.selectable_value(&mut v, s, s.text()); + } + }); + if old != v { + set(v); + } + }); +} + +fn drag_value( + ui: &mut Ui, + name: &str, + old: N, + range: RangeInclusive, + speed: f64, + set: impl FnOnce(N), +) where + N: Numeric, +{ + drag_value_ui(ui, name, |_| (), old, range, speed, set); +} + +fn drag_value_ui( + ui: &mut Ui, + name: &str, + label: impl FnOnce(&mut Ui) -> R, + mut v: N, + range: RangeInclusive, + speed: f64, + set: impl FnOnce(N), +) where + N: Numeric, +{ + row_ui(ui, name, label, |ui| { + if DragValue::new(&mut v) + .range(range) + .speed(speed) + .ui(ui) + .changed() + { + set(v); + } + }); +} + +fn label(ui: &mut Ui, name: &str, text: impl Into) { + row(ui, name, |ui| ui.label(text)); +} + +trait GridExt { + fn row(&mut self) -> impl DerefMut; +} + +impl GridExt for Ui { + fn row(&mut self) -> impl DerefMut { + GridRow { ui: self } + } +} + +struct GridRow<'a> { + ui: &'a mut Ui, +} + +impl Deref for GridRow<'_> { + type Target = Ui; + + fn deref(&self) -> &Self::Target { + self.ui + } +} + +impl DerefMut for GridRow<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *self.ui + } +} + +impl Drop for GridRow<'_> { + fn drop(&mut self) { + self.end_row(); + } +} + +impl LazyEventSourceListener for ControlCenterInner { + fn triggered(self: Rc) { + self.window.request_redraw(); + } +} diff --git a/src/control_center/cc_clients.rs b/src/control_center/cc_clients.rs new file mode 100644 index 00000000..1e002895 --- /dev/null +++ b/src/control_center/cc_clients.rs @@ -0,0 +1,463 @@ +use { + crate::{ + client::{Client, ClientId}, + control_center::{ + CcBehavior, ControlCenterInner, PaneType, + cc_criterion::{CcCriterion, CritImpl, CritRegex}, + cc_window::show_window_collapsible, + grid, icon_label, label, read_only_bool, + }, + criteria::{CritMgrExt, CritUpstreamNode, crit_leaf::CritLeafMatcher}, + egui_adapter::egui_platform::icons::ICON_OPEN_IN_NEW, + state::State, + tree::ToplevelData, + utils::{ + copyhashmap::CopyHashMap, static_text::StaticText, + toplevel_identifier::ToplevelIdentifier, + }, + }, + ahash::AHashMap, + egui::{ + CollapsingHeader, DragValue, Sense, TextFormat, Ui, Widget, cache::CacheTrait, + text::LayoutJob, + }, + linearize::Linearize, + std::{ + any::Any, + rc::{Rc, Weak}, + }, +}; + +pub enum ClientCrit { + SandboxEngine(CritRegex), + SandboxAppId(CritRegex), + SandboxInstanceId(CritRegex), + Sandboxed, + Uid(i32), + Pid(i32), + IsXwayland, + Comm(CritRegex), + Exe(CritRegex), + Tag(CritRegex), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Linearize)] +pub enum ClientCritTy { + SandboxEngine, + SandboxAppId, + SandboxInstanceId, + Sandboxed, + Uid, + Pid, + IsXwayland, + Comm, + Exe, + Tag, +} + +impl Default for ClientCrit { + fn default() -> Self { + ClientCrit::Comm(Default::default()) + } +} + +impl StaticText for ClientCritTy { + fn text(&self) -> &'static str { + match self { + ClientCritTy::SandboxEngine => "Sandbox Engine", + ClientCritTy::SandboxAppId => "Sandbox App ID", + ClientCritTy::SandboxInstanceId => "Sandbox Instance ID", + ClientCritTy::Sandboxed => "Sandboxed", + ClientCritTy::Uid => "UID", + ClientCritTy::Pid => "PID", + ClientCritTy::IsXwayland => "Is Xwayland", + ClientCritTy::Comm => "Comm", + ClientCritTy::Exe => "Exe", + ClientCritTy::Tag => "Tag", + } + } +} + +impl CritImpl for ClientCrit { + type Type = ClientCritTy; + type Target = Rc; + + fn ty(&self) -> Self::Type { + macro_rules! map { + ($($n:ident,)*) => { + match self { + $( + Self::$n { .. } => ClientCritTy::$n, + )* + } + }; + } + map! { + SandboxEngine, + SandboxAppId, + SandboxInstanceId, + Sandboxed, + Uid, + Pid, + IsXwayland, + Comm, + Exe, + Tag, + } + } + + fn from_ty(ty: Self::Type) -> Self { + match ty { + ClientCritTy::SandboxEngine => Self::SandboxEngine(Default::default()), + ClientCritTy::SandboxAppId => Self::SandboxAppId(Default::default()), + ClientCritTy::SandboxInstanceId => Self::SandboxInstanceId(Default::default()), + ClientCritTy::Sandboxed => Self::Sandboxed, + ClientCritTy::Uid => Self::Uid(Default::default()), + ClientCritTy::Pid => Self::Pid(Default::default()), + ClientCritTy::IsXwayland => Self::IsXwayland, + ClientCritTy::Comm => Self::Comm(Default::default()), + ClientCritTy::Exe => Self::Exe(Default::default()), + ClientCritTy::Tag => Self::Tag(Default::default()), + } + } + + fn show(&mut self, ui: &mut Ui) -> bool { + match self { + ClientCrit::SandboxEngine(v) => v.show(ui), + ClientCrit::SandboxAppId(v) => v.show(ui), + ClientCrit::SandboxInstanceId(v) => v.show(ui), + ClientCrit::Sandboxed => false, + ClientCrit::Uid(v) => DragValue::new(v).ui(ui).changed(), + ClientCrit::Pid(v) => DragValue::new(v).ui(ui).changed(), + ClientCrit::IsXwayland => false, + ClientCrit::Comm(v) => v.show(ui), + ClientCrit::Exe(v) => v.show(ui), + ClientCrit::Tag(v) => v.show(ui), + } + } + + fn to_crit(&self, state: &Rc) -> Option>> { + let m = &state.cl_matcher_manager; + let res = match self { + ClientCrit::SandboxEngine(v) => m.sandbox_engine(v.to_crit()?), + ClientCrit::SandboxAppId(v) => m.sandbox_app_id(v.to_crit()?), + ClientCrit::SandboxInstanceId(v) => m.sandbox_instance_id(v.to_crit()?), + ClientCrit::Sandboxed => m.sandboxed(), + ClientCrit::Uid(v) => m.uid(*v), + ClientCrit::Pid(v) => m.pid(*v), + ClientCrit::IsXwayland => m.is_xwayland(), + ClientCrit::Comm(v) => m.comm(v.to_crit()?), + ClientCrit::Exe(v) => m.exe(v.to_crit()?), + ClientCrit::Tag(v) => m.tag(v.to_crit()?), + }; + Some(res) + } + + fn not( + state: &State, + upstream: &Rc>, + ) -> Rc> { + state.cl_matcher_manager.not(upstream) + } + + fn list( + state: &State, + upstream: &[Rc>], + all: bool, + ) -> Rc> { + state.cl_matcher_manager.list(upstream, all) + } + + fn exactly( + state: &State, + n: usize, + upstream: &[Rc>], + ) -> Rc> { + state.cl_matcher_manager.exactly(upstream, n) + } +} + +pub struct ClientsPane { + state: Rc, + filter: bool, + criterion: CcCriterion, + matched: Rc, + leaf: Option>>>, +} + +struct Matched { + slf: Weak, + clients: CopyHashMap, +} + +impl Matched { + fn request_frame(&self) { + if let Some(slf) = self.slf.upgrade() { + slf.window.request_redraw(); + } + } +} + +impl ControlCenterInner { + pub fn create_clients_pane(self: &Rc) -> ClientsPane { + let mut pane = ClientsPane { + state: self.state.clone(), + filter: false, + criterion: Default::default(), + matched: Rc::new(Matched { + slf: Rc::downgrade(self), + clients: Default::default(), + }), + leaf: Default::default(), + }; + pane.update_matcher(); + pane + } +} + +impl ClientsPane { + pub fn title(&self, res: &mut String) { + res.push_str("Clients"); + } + + pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) { + if ui.checkbox(&mut self.filter, "Filter").changed() && !self.filter { + self.criterion = Default::default(); + self.update_matcher(); + } + let mut clear_clients = false; + if self.filter && self.criterion.show(ui) { + clear_clients = self.update_matcher(); + } + ui.separator(); + let mut clients: Vec<_> = self.matched.clients.lock().keys().copied().collect(); + clients.sort(); + for id in clients { + let Ok(client) = self.state.clients.get(id) else { + continue; + }; + show_client_collapsible(behavior, ui, &client); + } + if clear_clients { + self.matched.clients.clear(); + } + } + + fn update_matcher(&mut self) -> bool { + let mut clear_clients = false; + let state = &self.state; + if let Some(new) = self.criterion.to_crit(state) { + clear_clients = true; + let matched = self.matched.clone(); + let leaf = state.cl_matcher_manager.leaf(&new, move |data| { + matched.clients.set(data, ()); + matched.request_frame(); + Box::new({ + let matched = matched.clone(); + move || { + matched.clients.remove(&data); + matched.request_frame(); + } + }) + }); + state.cl_matcher_manager.rematch_all(state); + self.leaf = Some(leaf); + } + clear_clients + } +} + +pub struct ClientPane { + client: Rc, +} + +impl ControlCenterInner { + pub fn create_client_pane(self: &Rc, client: &Rc) -> ClientPane { + ClientPane { + client: client.clone(), + } + } +} + +impl ClientPane { + pub fn title(&self, res: &mut String) { + res.push_str("Client "); + res.push_str(&self.client.pid_info.comm); + } + + pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) { + show_client(behavior, ui, &self.client); + } +} + +pub fn show_client_collapsible(behavior: &mut CcBehavior, ui: &mut Ui, client: &Rc) { + let mut layout_job = LayoutJob::default(); + layout_job.append( + "Client", + 0.0, + TextFormat { + color: ui.style().visuals.widgets.inactive.text_color(), + ..Default::default() + }, + ); + layout_job.append( + &client.id.to_string(), + 10.0, + TextFormat { + color: ui.style().visuals.widgets.active.text_color(), + ..Default::default() + }, + ); + layout_job.append( + &client.pid_info.comm, + 10.0, + TextFormat { + color: ui.style().visuals.widgets.inactive.text_color(), + ..Default::default() + }, + ); + CollapsingHeader::new(layout_job) + .id_salt(("client", client.id)) + .show(ui, |ui| { + if icon_label(ICON_OPEN_IN_NEW) + .sense(Sense::CLICK) + .ui(ui) + .clicked() + { + behavior.open = Some(PaneType::Client(behavior.cc.create_client_pane(client))); + } + show_client(behavior, ui, client) + }); +} + +pub fn show_client(behavior: &mut CcBehavior<'_>, ui: &mut Ui, client: &Client) { + grid(ui, ("client", client.id), |ui| { + label(ui, "ID", client.id.to_string()); + label(ui, "PID", client.pid_info.pid.to_string()); + label(ui, "UID", client.pid_info.uid.to_string()); + label(ui, "comm", &client.pid_info.comm); + label(ui, "exe", &client.pid_info.exe); + if client.acceptor.sandboxed { + read_only_bool(ui, "Sandboxed", true); + } + if client.acceptor.secure { + read_only_bool(ui, "Secure", true); + } + if client.is_xwayland { + read_only_bool(ui, "Xwayland", true); + } + if let Some(v) = &client.acceptor.sandbox_engine { + label(ui, "Sandbox Engine", v); + } + if let Some(v) = &client.acceptor.app_id { + label(ui, "App ID", v); + } + if let Some(v) = &client.acceptor.instance_id { + label(ui, "Instance ID", v); + } + if let Some(v) = &client.acceptor.tag { + label(ui, "Tag", v); + } + }); + if ui.button("Kill").clicked() { + client.state.clients.kill(client.id); + } + ui.collapsing("Capabilities", |ui| { + ui.add_enabled_ui(false, |ui| { + for (k, v) in client.effective_caps.get().to_map() { + if v { + ui.checkbox(&mut true, k.text()); + } + } + }); + }); + ui.collapsing("Windows", |ui| { + let matcher = ui.memory_mut(|m| { + m.caches + .cache::() + .get(behavior.cc, client.id) + .clone() + }); + let mut windows: Vec<_> = matcher.windows.lock().keys().copied().collect(); + windows.sort(); + for id in windows { + let Some(window) = client.state.toplevels.get(&id).and_then(|v| v.upgrade()) else { + continue; + }; + show_window_collapsible(behavior, ui, &window); + } + }); +} + +#[derive(Default)] +struct ClientWindowMatchersCache { + generation: u64, + matchers: AHashMap, +} + +struct CachedWindowMatcher { + generation: u64, + _matcher: Rc>, + matchers: Rc, +} + +struct WindowMatchers { + cc: Weak, + windows: CopyHashMap, +} + +impl ClientWindowMatchersCache { + fn get(&mut self, cc: &Rc, id: ClientId) -> &Rc { + let res = self.matchers.entry(id).or_insert_with(|| { + let state = &cc.state; + let node = state.cl_matcher_manager.id(id); + let node = state.tl_matcher_manager.client(state, &node); + let matchers = Rc::new(WindowMatchers { + cc: Rc::downgrade(&cc), + windows: Default::default(), + }); + let matchers2 = matchers.clone(); + let matcher = state.tl_matcher_manager.leaf(&node, move |id| { + matchers2.windows.set(id, ()); + if let Some(cc) = matchers2.cc.upgrade() { + cc.window.request_redraw(); + } + let matchers2 = matchers2.clone(); + Box::new(move || { + matchers2.windows.remove(&id); + if let Some(cc) = matchers2.cc.upgrade() { + cc.window.request_redraw(); + } + }) + }); + let res = CachedWindowMatcher { + generation: 0, + _matcher: matcher, + matchers, + }; + state.cl_matcher_manager.rematch_all(state); + state.tl_matcher_manager.rematch_all(state); + res + }); + res.generation = self.generation; + &res.matchers + } +} + +unsafe impl Sync for ClientWindowMatchersCache {} +unsafe impl Send for ClientWindowMatchersCache {} + +impl CacheTrait for ClientWindowMatchersCache { + fn update(&mut self) { + self.matchers.retain(|_, m| m.generation == self.generation); + self.generation += 1; + } + + fn len(&self) -> usize { + self.matchers.len() + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} diff --git a/src/control_center/cc_color_management.rs b/src/control_center/cc_color_management.rs new file mode 100644 index 00000000..c4926081 --- /dev/null +++ b/src/control_center/cc_color_management.rs @@ -0,0 +1,36 @@ +use { + crate::{ + control_center::{ControlCenterInner, bool, grid, read_only_bool}, + state::State, + }, + egui::Ui, + std::rc::Rc, +}; + +pub struct ColorManagementPane { + state: Rc, +} + +impl ControlCenterInner { + pub fn create_color_management_pane(self: &Rc) -> ColorManagementPane { + ColorManagementPane { + state: self.state.clone(), + } + } +} + +impl ColorManagementPane { + pub fn title(&self, res: &mut String) { + res.push_str("Color Management"); + } + + pub fn show(&mut self, ui: &mut Ui) { + let s = &self.state; + grid(ui, "settings", |ui| { + bool(ui, "Enabled", s.color_management_enabled.get(), |b| { + s.set_color_management_enabled(b); + }); + read_only_bool(ui, "Available", s.color_management_available()); + }); + } +} diff --git a/src/control_center/cc_compositor.rs b/src/control_center/cc_compositor.rs new file mode 100644 index 00000000..e1bce419 --- /dev/null +++ b/src/control_center/cc_compositor.rs @@ -0,0 +1,88 @@ +use { + crate::{ + compositor::{LIBEI_SOCKET, WAYLAND_DISPLAY}, + control_center::{ControlCenterInner, bool, combo_box, grid, label, row}, + state::State, + version::VERSION, + }, + egui::{DragValue, OpenUrl, Ui, Widget}, + std::rc::Rc, +}; + +pub struct CompositorPane { + state: Rc, + switch_to_vt: u32, +} + +impl ControlCenterInner { + pub fn create_compositor_pane(self: &Rc) -> CompositorPane { + CompositorPane { + state: self.state.clone(), + switch_to_vt: 1, + } + } +} + +impl CompositorPane { + pub fn title(&self, res: &mut String) { + res.push_str("Compositor"); + } + + pub fn show(&mut self, ui: &mut Ui) { + let s = &self.state; + grid(ui, "compositor", |ui| { + row(ui, "Repository", |ui| { + let url = "https://github.com/mahkoh/jay"; + if ui.link(url).clicked() { + ui.ctx().open_url(OpenUrl::new_tab(url)); + } + }); + label(ui, "Version", VERSION); + label(ui, "PID", s.pid.to_string()); + if let Some(acceptor) = s.acceptor.get() { + label(ui, WAYLAND_DISPLAY, acceptor.socket_name()); + } + if let Some(dir) = &s.config_dir { + label(ui, "Config DIR", dir); + } + bool(ui, "Libei Socket", s.enable_ei_acceptor.get(), |v| { + s.set_ei_socket_enabled(v); + }); + if let Some(a) = s.ei_acceptor.get() { + label(ui, LIBEI_SOCKET, a.socket_name()); + } + combo_box( + ui, + "Workspace Display Order", + s.workspace_display_order.get(), + |o| s.set_workspace_display_order(o), + ); + if let Some(logger) = &s.logger { + combo_box(ui, "Log Level", logger.level(), |l| s.set_log_level(l)); + row(ui, "Log File", |ui| { + let path = logger.path().to_string(); + if ui + .link(&path) + .on_hover_text_at_pointer("Copy to clipboard") + .clicked() + { + ui.ctx().copy_text(path); + } + }); + } + }); + if ui.button("Quit").clicked() { + s.quit(); + } + if ui.button("Reload Config").clicked() { + s.reload_config(); + } + ui.horizontal(|ui| { + let button = ui.button("Switch to VT"); + DragValue::new(&mut self.switch_to_vt).ui(ui); + if button.clicked() { + s.backend.get().switch_to(self.switch_to_vt); + } + }); + } +} diff --git a/src/control_center/cc_criterion.rs b/src/control_center/cc_criterion.rs new file mode 100644 index 00000000..ed37aa8a --- /dev/null +++ b/src/control_center/cc_criterion.rs @@ -0,0 +1,253 @@ +use { + crate::{ + criteria::{CritLiteralOrRegex, CritUpstreamNode}, + egui_adapter::egui_platform::icons::ICON_CLOSE, + state::State, + utils::{numcell::NumCell, static_text::StaticText}, + }, + ahash::AHashSet, + egui::{ComboBox, DragValue, Ui, UiBuilder, Widget}, + isnt::std_1::collections::IsntHashSetExt, + linearize::{Linearize, LinearizeExt}, + regex::Regex, + std::rc::Rc, +}; + +pub enum CcCriterion { + Not(Box), + List(Vec, bool), + Exactly(usize, Vec), + T(T), +} + +impl Default for CcCriterion +where + T: Default, +{ + fn default() -> Self { + Self::T(T::default()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Linearize)] +enum CompoundCritTy { + Not, + All, + Any, + Exactly, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum CritTy { + Compound(CompoundCritTy), + T(T), +} + +impl StaticText for CompoundCritTy { + fn text(&self) -> &'static str { + match self { + Self::Not => "Not", + Self::All => "All", + Self::Any => "Any", + Self::Exactly => "Exactly", + } + } +} + +impl StaticText for CritTy +where + T: StaticText, +{ + fn text(&self) -> &'static str { + match self { + Self::Compound(t) => t.text(), + Self::T(t) => t.text(), + } + } +} + +pub trait CritImpl: Default { + type Type: Copy + Eq + PartialEq + StaticText + Linearize; + type Target; + + fn ty(&self) -> Self::Type; + fn from_ty(ty: Self::Type) -> Self; + #[must_use] + fn show(&mut self, ui: &mut Ui) -> bool; + + fn to_crit(&self, state: &Rc) -> Option>>; + fn not( + state: &State, + upstream: &Rc>, + ) -> Rc>; + fn list( + state: &State, + upstream: &[Rc>], + all: bool, + ) -> Rc>; + fn exactly( + state: &State, + n: usize, + upstream: &[Rc>], + ) -> Rc>; +} + +impl CcCriterion +where + T: CritImpl, +{ + #[must_use] + pub fn show(&mut self, ui: &mut Ui) -> bool { + let mut changed = false; + ui.vertical(|ui| { + ui.horizontal(|ui| { + let mut v = self.ty(); + let old = v; + ComboBox::from_id_salt("ty") + .selected_text(v.text()) + .show_ui(ui, |ui| { + for s in CompoundCritTy::variants() { + ui.selectable_value(&mut v, CritTy::Compound(s), s.text()); + } + for s in T::Type::variants() { + ui.selectable_value(&mut v, CritTy::T(s), s.text()); + } + }); + if old != v { + *self = match v { + CritTy::Compound(CompoundCritTy::Not) => { + CcCriterion::Not(Default::default()) + } + CritTy::Compound(CompoundCritTy::All) => { + CcCriterion::List(Default::default(), true) + } + CritTy::Compound(CompoundCritTy::Any) => { + CcCriterion::List(Default::default(), false) + } + CritTy::Compound(CompoundCritTy::Exactly) => { + CcCriterion::Exactly(1, Default::default()) + } + CritTy::T(t) => CcCriterion::T(T::from_ty(t)), + }; + changed = true; + } + match self { + CcCriterion::Not(n) => changed |= n.show(ui), + CcCriterion::List(_, _) => {} + CcCriterion::Exactly(n, _) => { + changed |= DragValue::new(n).ui(ui).changed(); + } + CcCriterion::T(t) => changed |= t.show(ui), + } + }); + match self { + CcCriterion::Not(_) => {} + CcCriterion::List(v, _) | CcCriterion::Exactly(_, v) => { + ui.indent("compound", |ui| { + let mut to_remove = AHashSet::new(); + for (idx, v) in v.iter_mut().enumerate() { + ui.horizontal(|ui| { + if ui.button(ICON_CLOSE).clicked() { + changed = true; + to_remove.insert(idx); + } + ui.scope_builder(UiBuilder::new().id_salt(idx), |ui| { + changed |= v.show(ui); + }); + }); + } + let i = NumCell::new(0); + v.retain(|_| to_remove.not_contains(&i.fetch_add(1))); + if ui.button("Add").clicked() { + v.push(CcCriterion::default()); + changed = true; + } + }); + } + CcCriterion::T(_) => {} + } + }); + changed + } + + fn ty(&self) -> CritTy { + match self { + CcCriterion::Not(_) => CritTy::Compound(CompoundCritTy::Not), + CcCriterion::List(_, true) => CritTy::Compound(CompoundCritTy::All), + CcCriterion::List(_, false) => CritTy::Compound(CompoundCritTy::Any), + CcCriterion::Exactly(_, _) => CritTy::Compound(CompoundCritTy::Exactly), + CcCriterion::T(t) => CritTy::T(t.ty()), + } + } + + pub fn to_crit(&self, state: &Rc) -> Option>> { + match self { + CcCriterion::Not(t) => Some(T::not(state, &t.to_crit(state)?)), + CcCriterion::List(v, all) => { + let mut upstream = Vec::with_capacity(v.len()); + for v in v { + upstream.push(v.to_crit(state)?); + } + Some(T::list(state, &upstream, *all)) + } + CcCriterion::Exactly(n, v) => { + let mut upstream = Vec::with_capacity(v.len()); + for v in v { + upstream.push(v.to_crit(state)?); + } + Some(T::exactly(state, *n, &upstream)) + } + CcCriterion::T(t) => t.to_crit(state), + } + } + + pub fn any(&self, mut any: impl FnMut(&T) -> bool) -> bool { + self.any_(&mut any) + } + + fn any_(&self, any: &mut impl FnMut(&T) -> bool) -> bool { + match self { + CcCriterion::Not(v) => v.any_(any), + CcCriterion::List(v, _) => v.iter().any(|v| v.any_(any)), + CcCriterion::Exactly(_, v) => v.iter().any(|v| v.any_(any)), + CcCriterion::T(t) => any(t), + } + } +} + +pub struct CritRegex { + pub text: String, + pub regex: Option>, +} + +impl Default for CritRegex { + fn default() -> Self { + Self { + text: Default::default(), + regex: Some(Some(Regex::new("").unwrap())), + } + } +} + +impl CritRegex { + pub fn show(&mut self, ui: &mut Ui) -> bool { + let mut is_regex = self.regex.is_some(); + let mut changed = false; + changed |= ui.text_edit_singleline(&mut self.text).changed(); + changed |= ui.checkbox(&mut is_regex, "Regex").changed(); + if changed { + self.regex = is_regex.then(|| Regex::new(&self.text).ok()); + } + if let Some(None) = self.regex { + ui.label("Error: Invalid regex"); + } + changed + } + + pub fn to_crit(&self) -> Option { + match &self.regex { + None => Some(CritLiteralOrRegex::Literal(self.text.clone())), + Some(v) => Some(CritLiteralOrRegex::Regex(v.clone()?)), + } + } +} diff --git a/src/control_center/cc_gpus.rs b/src/control_center/cc_gpus.rs new file mode 100644 index 00000000..8d68a4ff --- /dev/null +++ b/src/control_center/cc_gpus.rs @@ -0,0 +1,146 @@ +use { + crate::{ + control_center::{ + ControlCenterInner, GridExt, bool, combo_box, grid, grid_label, label, row, + }, + egui_adapter::egui_platform::icons::{ICON_ADD, ICON_REMOVE}, + state::{DrmDevData, State}, + }, + egui::{Checkbox, CollapsingHeader, DragValue, TextFormat, Ui, Widget, text::LayoutJob}, + std::rc::Rc, +}; + +pub struct GpusPane { + state: Rc, +} + +impl ControlCenterInner { + pub fn create_gpus_pane(self: &Rc) -> GpusPane { + GpusPane { + state: self.state.clone(), + } + } +} + +impl GpusPane { + pub fn title(&self, res: &mut String) { + res.push_str("GPUs"); + } + + pub fn show(&mut self, ui: &mut Ui) { + let devs = self.state.drm_devs.lock(); + let mut devs: Vec<_> = devs.iter().collect(); + devs.sort_by_key(|d| d.0); + for dev in devs { + self.show_dev(ui, dev.1); + } + } + + fn show_dev(&self, ui: &mut Ui, dev: &DrmDevData) { + let title_buf; + let title = match dev.devnode.as_deref() { + Some(t) => t, + _ => { + let dev_t = dev.dev.dev_t(); + title_buf = format!("{}:{}", uapi::major(dev_t), uapi::minor(dev_t)); + &title_buf + } + }; + let mut layout_job = LayoutJob::default(); + layout_job.append( + title, + 0.0, + TextFormat { + color: ui.style().visuals.widgets.active.text_color(), + ..Default::default() + }, + ); + if let Some(model) = &dev.model { + layout_job.append( + model, + 10.0, + TextFormat { + color: ui.style().visuals.widgets.inactive.text_color(), + ..Default::default() + }, + ); + } + ui.collapsing(layout_job, |ui| { + grid(ui, ("settings", dev.dev.id()), |ui| { + macro_rules! string { + ($field:ident, $name:expr) => { + if let Some(v) = &dev.$field { + label(ui, $name, v); + } + }; + } + string!(vendor, "Vendor"); + string!(model, "Model"); + string!(devnode, "Devnode"); + string!(syspath, "Syspath"); + if let Some(v) = dev.pci_id { + label(ui, "PCI ID", format!("{:x}:{:x}", v.vendor, v.model)); + } + { + let v = dev.dev.dev_t(); + label(ui, "Dev", format!("{}:{}", uapi::major(v), uapi::minor(v))); + } + combo_box(ui, "API", dev.dev.gtx_api(), |v| dev.dev.set_gfx_api(v)); + row(ui, "Primary Device", |ui| { + let mut v = dev.dev.is_render_device(); + let old = v; + ui.add_enabled(!v, Checkbox::without_text(&mut v)); + if v != old { + dev.dev.make_render_device(); + } + }); + bool( + ui, + "Direct Scanout", + dev.dev.direct_scanout_enabled(), + |v| dev.set_direct_scanout_enabled(&self.state, v), + ); + if let Some(mut v) = dev.dev.flip_margin() { + let ui = &mut *ui.row(); + grid_label(ui, "Flip Margin"); + let old = v; + let denom = 1_000_000.0; + ui.horizontal(|ui| { + let mut s = v as f64 / denom; + let res = DragValue::new(&mut s) + .range(0.0..=50.0) + .speed(0.1) + .fixed_decimals(1) + .ui(ui); + if res.changed() { + v = (s * denom) as u64; + } + if ui.button(ICON_REMOVE).clicked() { + v = v.saturating_sub(100_000); + } + if ui.button(ICON_ADD).clicked() { + v += 100_000; + } + }); + if old != v { + dev.set_flip_margin(&self.state, v); + } + } + }); + CollapsingHeader::new("Connectors") + .default_open(true) + .show(ui, |ui| { + let mut cs: Vec<_> = dev + .connectors + .lock() + .values() + .map(|v| v.connector.kernel_id().to_string()) + .collect::>(); + cs.sort(); + for c in cs { + ui.label(c); + } + }); + }); + } +} diff --git a/src/control_center/cc_idle.rs b/src/control_center/cc_idle.rs new file mode 100644 index 00000000..2520463f --- /dev/null +++ b/src/control_center/cc_idle.rs @@ -0,0 +1,72 @@ +use { + crate::{ + control_center::{ControlCenterInner, grid, row}, + state::State, + }, + egui::{CollapsingHeader, DragValue, Ui, Widget}, + std::{rc::Rc, time::Duration}, +}; + +pub struct IdlePane { + state: Rc, +} + +impl ControlCenterInner { + pub fn create_idle_pane(self: &Rc) -> IdlePane { + IdlePane { + state: self.state.clone(), + } + } +} + +impl IdlePane { + pub fn title(&self, res: &mut String) { + res.push_str("Idle"); + } + + pub fn show(&mut self, ui: &mut Ui) { + grid(ui, "sliders", |ui| { + for interval in [true, false] { + let label = match interval { + true => "Interval", + false => "Grace period", + }; + let idle = &self.state.idle; + let field = match interval { + true => &idle.timeout, + false => &idle.grace_period, + }; + row(ui, label, |ui| { + let secs = field.get().as_secs(); + let mut minutes = secs / 60; + let mut seconds = secs % 60; + let mut changed = false; + changed |= DragValue::new(&mut minutes).ui(ui).changed(); + ui.label("minutes"); + changed |= DragValue::new(&mut seconds).range(0..=59).ui(ui).changed(); + ui.label("seconds"); + if changed { + let duration = + Duration::from_secs(minutes.saturating_mul(60).saturating_add(seconds)); + match interval { + true => idle.set_timeout(&self.state, duration), + false => idle.set_grace_period(&self.state, duration), + } + } + }); + } + }); + let inhibitors = self.state.idle.inhibitors.lock(); + let mut is: Vec<_> = inhibitors.values().collect(); + is.sort_by_key(|is| is.inhibit_id); + CollapsingHeader::new(format!("Inhibitors ({})", is.len())) + .id_salt("Inhibitors") + .show(ui, |ui| { + for i in is { + ui.horizontal(|ui| { + ui.label(&i.client.pid_info.comm); + }); + } + }); + } +} diff --git a/src/control_center/cc_input.rs b/src/control_center/cc_input.rs new file mode 100644 index 00000000..40ed942f --- /dev/null +++ b/src/control_center/cc_input.rs @@ -0,0 +1,679 @@ +use { + crate::{ + backend::{InputDeviceCapability, InputDeviceId}, + control_center::{ + ControlCenterInner, GridExt, PaneState, bool, bool_ui, combo_box, combo_box_ui, + drag_value, drag_value_ui, grid, grid_label, grid_label_ui, label, text_edit, tip, + }, + egui_adapter::egui_platform::icons::ICON_PENDING, + ifs::{ + wl_output::WlOutputGlobal, + wl_seat::{SeatId, WlSeatGlobal}, + }, + kbvm::KbvmMap, + state::{DeviceHandlerData, State}, + utils::{errorfmt::ErrorFmt, static_text::StaticText}, + }, + ahash::AHashMap, + egui::{ + CollapsingHeader, ComboBox, DragValue, Event, Grid, Id, TextBuffer, TextFormat, Ui, + UiBuilder, ViewportCommand, Widget, emath::Numeric, text::LayoutJob, + }, + isnt::std_1::string::IsntStringExt, + jay_config::keyboard::syms::KeySym, + kbvm::Keysym, + linearize::LinearizeExt, + rand::random, + std::{mem, rc::Rc}, +}; + +pub struct InputPane { + state: Rc, + paste_requested: Option, + keymaps: AHashMap, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +enum Key { + Seat(SeatId), + Dev(InputDeviceId), +} + +struct KeymapState { + seed: u64, + rules_default: bool, + rules: String, + model_default: bool, + model: String, + layouts: String, + variants: String, + options: String, + backup: Option>, + pointer_revert_key: Keysym, + pointer_revert_key_str: Option, + unknown_pointer_revert_key: bool, +} + +impl Default for KeymapState { + fn default() -> Self { + Self { + seed: random(), + rules_default: true, + rules: Default::default(), + model_default: true, + model: Default::default(), + layouts: Default::default(), + variants: Default::default(), + options: Default::default(), + backup: Default::default(), + pointer_revert_key: Default::default(), + pointer_revert_key_str: None, + unknown_pointer_revert_key: false, + } + } +} + +impl ControlCenterInner { + pub fn create_input_pane(self: &Rc) -> InputPane { + InputPane { + state: self.state.clone(), + paste_requested: Default::default(), + keymaps: Default::default(), + } + } +} + +impl InputPane { + pub fn title(&self, res: &mut String) { + res.push_str("Input"); + } + + pub fn show(&mut self, ps: &mut PaneState, ui: &mut Ui) { + let state = self.state.clone(); + let seats = state.globals.seats.lock(); + let mut seats: Vec<_> = seats.values().collect(); + seats.sort_by_key(|d| d.seat_name()); + for seat in &seats { + self.show_seat(ps, ui, seat); + } + let outputs = state.globals.outputs.lock(); + let mut outputs: Vec<_> = outputs.values().collect(); + outputs.sort_by_key(|o| &o.connector.name); + let dev = &*state.input_device_handlers.borrow(); + let mut dev: Vec<_> = dev.values().collect(); + dev.sort_by_key(|d| d.data.device.name()); + for dev in dev { + self.show_device(ps, ui, &dev.data, &seats, &outputs); + } + } + + fn show_seat(&mut self, ps: &mut PaneState, ui: &mut Ui, seat: &Rc) { + let mut layout_job = LayoutJob::default(); + layout_job.append( + "Seat", + 0.0, + TextFormat { + color: ui.style().visuals.widgets.inactive.text_color(), + ..Default::default() + }, + ); + layout_job.append( + seat.seat_name(), + 10.0, + TextFormat { + color: ui.style().visuals.widgets.active.text_color(), + ..Default::default() + }, + ); + let ks = self.keymaps.entry(Key::Seat(seat.id())).or_default(); + CollapsingHeader::new(layout_job) + .id_salt(("seat", seat.id())) + .show(ui, |ui| { + grid(ui, ("seat", seat.id()), |ui| { + let mut dv = |name: &str, get: &dyn Fn(&mut (i32, i32)) -> &mut i32| { + let ui = &mut *ui.row(); + grid_label(ui, name); + let mut v = seat.get_rate(); + let old = v; + ui.horizontal(|ui| { + let v = get(&mut v); + DragValue::new(v).range(0..=i32::MAX).ui(ui); + if ui.button("-20").clicked() { + *v = v.saturating_sub(20).max(0) + } + if ui.button("+20").clicked() { + *v = v.saturating_add(20); + } + }); + if v != old { + seat.set_rate(v.0, v.1); + } + }; + dv("Repeat Rate", &|v| &mut v.0); + dv("Repeat Delay", &|v| &mut v.1); + drag_value( + ui, + "Cursor Size", + seat.cursor_group().cursor_size(), + 0..=u32::MAX, + 1.0, + |v| seat.cursor_group().set_cursor_size(v), + ); + bool_ui( + ui, + "Simple IM", + |ui| { + tip(ui, |ui| { + ui.label("A simple input method based on Xcompose files."); + ui.label(concat!( + "If you're not using another input method, you should ", + "leave this enabled as it will work for sandboxed ", + "applications, which regular Xcompose will not.", + )); + ui.label(concat!( + "The `enable-unicode-input` action can be used to input ", + "characters by their unicode value.", + )); + }); + }, + seat.simple_im_enabled(), + |b| seat.set_simple_im_enabled(b), + ); + bool_ui( + ui, + "Hardware Cursor", + |ui| { + tip(ui, |ui| { + ui.label( + "Allow this seat to use the hardware cursor, if available.", + ); + ui.label("Only one seat can use the hardware cursor at a time."); + }); + }, + seat.cursor_group().hardware_cursor(), + |b| seat.cursor_group().set_hardware_cursor(b), + ); + { + let ui = &mut *ui.row(); + let v = seat.pointer_revert_key(); + let v = Keysym(v.0); + if mem::replace(&mut ks.pointer_revert_key, v) != v { + ks.pointer_revert_key_str = None; + } + let name = ks + .pointer_revert_key_str + .get_or_insert_with(|| v.name().unwrap_or_default().to_string()); + grid_label_ui(ui, |ui| { + ui.label("Pointer Revert Key"); + tip(ui, |ui| { + ui.label(concat!( + "Pressing this key reverts the pointer to the default state, ", + "breaking grabs, drags, etc.", + )); + ui.label( + "Setting this to `NoSymbol` effectively disables this feature.", + ); + }); + }); + ui.horizontal(|ui| { + if ui.text_edit_singleline(name).changed() { + let v = Keysym::from_str(name); + ks.unknown_pointer_revert_key = v.is_none(); + if let Some(v) = v { + ks.pointer_revert_key = v; + seat.set_pointer_revert_key(KeySym(v.0)); + } + } + if ks.unknown_pointer_revert_key { + ui.label("Error: Unknown key"); + } + }); + } + bool(ui, "Focus Follows Mouse", seat.focus_follows_mouse(), |v| { + seat.set_focus_follows_mouse(v); + }); + combo_box_ui( + ui, + "Fallback Output Mode", + |ui| { + tip(ui, |ui| { + ui.label(concat!( + "This determines the output to use in operations where no ", + "output is explicitly specified.", + )); + ui.label(concat!( + "For example, when a new window is opened, this determines ", + "where the window will be opened.", + )); + ui.label("`Cursor` refers to the output that contains the cursor."); + ui.label( + "`Focus` refers to the output that has the keyboard focus.", + ); + }); + }, + seat.fallback_output_mode(), + |v| seat.set_fallback_output_mode(v), + ); + }); + ui.label("Focus History"); + ui.indent("focus-history", |ui| { + let mut v = seat.focus_history_visible(); + if ui.checkbox(&mut v, "Only Visible").changed() { + seat.focus_history_set_visible(v); + } + let mut v = seat.focus_history_same_workspace(); + if ui.checkbox(&mut v, "Same Workspace").changed() { + seat.focus_history_set_same_workspace(v); + } + }); + if ui.button("Reload Simple IM").clicked() { + seat.reload_simple_im(); + } + show_keymap( + &self.state, + ps, + &mut self.paste_requested, + ks, + ui, + Some(&seat.keymap()), + |m| seat.set_seat_keymap(m), + ); + }); + } + + fn show_device( + &mut self, + ps: &mut PaneState, + ui: &mut Ui, + dev: &Rc, + seats: &[&Rc], + outputs: &[&Rc], + ) { + let mut layout_job = LayoutJob::default(); + layout_job.append( + "Device", + 0.0, + TextFormat { + color: ui.style().visuals.widgets.inactive.text_color(), + ..Default::default() + }, + ); + layout_job.append( + &dev.device.name(), + 10.0, + TextFormat { + color: ui.style().visuals.widgets.active.text_color(), + ..Default::default() + }, + ); + let dev_id = dev.device.id(); + CollapsingHeader::new(layout_job) + .id_salt(("device", dev_id)) + .show(ui, |ui| { + grid(ui, ("device", dev_id), |ui| { + { + let old = dev.seat.get(); + let ui = &mut *ui.row(); + grid_label(ui, "Seat"); + let mut seat = old.as_ref(); + ui.horizontal(|ui| { + let mut cb = ComboBox::from_id_salt("seat"); + if let Some(seat) = seat { + cb = cb.selected_text(seat.seat_name()); + } + cb.show_ui(ui, |ui| { + for s in seats { + ui.selectable_value(&mut seat, Some(s), s.seat_name()); + } + }); + if ui.button("Detach").clicked() { + seat = None; + } + }); + if seat != old.as_ref() { + dev.set_seat(&self.state, seat.cloned()); + } + } + macro_rules! string { + ($field:ident, $name:expr) => { + if let Some(v) = &dev.$field { + label(ui, $name, v); + } + }; + } + string!(syspath, "Syspath"); + string!(devnode, "Devnode"); + { + let ui = &mut *ui.row(); + grid_label(ui, "Capabilities"); + let mut s = String::new(); + for cap in InputDeviceCapability::variants() { + if dev.device.has_capability(cap) { + if s.is_not_empty() { + s.push_str(" | "); + } + s.push_str(cap.text()); + } + } + ui.label(s); + } + if let Some(old) = dev.device.natural_scrolling_enabled() { + bool(ui, "Natural Scrolling", old, |v| { + dev.set_natural_scrolling_enabled(&self.state, v) + }); + } + if dev.device.has_capability(InputDeviceCapability::Pointer) { + drag_value_ui( + ui, + "Scroll Distance (px)", + |ui| { + tip(ui, |ui| { + ui.label(concat!( + "This only applies to applications that use the ", + "legacy px scrolling events.", + )); + }); + }, + dev.px_per_scroll_wheel.get(), + -f64::INFINITY..=f64::INFINITY, + 0.1, + |v| dev.set_px_per_scroll_wheel(&self.state, v), + ); + } + if let Some(old) = dev.device.accel_profile() { + combo_box(ui, "Accel Profile", old, |v| { + dev.set_accel_profile(&self.state, v) + }); + } + if let Some(old) = dev.device.accel_speed() { + drag_value(ui, "Accel Speed", old, 0.0..=1.0, 0.01, |v| { + dev.set_accel_speed(&self.state, v) + }); + } + if let Some(old) = dev.device.click_method() { + combo_box(ui, "Click Method", old, |v| { + dev.set_click_method(&self.state, v) + }); + } + if let Some(old) = dev.device.tap_enabled() { + bool(ui, "Tap Enabled", old, |v| { + dev.set_tap_enabled(&self.state, v) + }); + } + if let Some(old) = dev.device.drag_enabled() { + bool(ui, "Tap Drag Enabled", old, |v| { + dev.set_drag_enabled(&self.state, v) + }); + } + if let Some(old) = dev.device.drag_lock_enabled() { + bool(ui, "Tap Drag Lock Enabled", old, |v| { + dev.set_drag_lock_enabled(&self.state, v) + }); + } + if let Some(old) = dev.device.left_handed() { + bool(ui, "Left Handed", old, |v| { + dev.set_left_handed(&self.state, v) + }); + } + if let Some(old) = dev.device.middle_button_emulation_enabled() { + bool(ui, "Middle Button Emulation", old, |v| { + dev.set_middle_button_emulation_enabled(&self.state, v) + }); + } + { + let ui = &mut *ui.row(); + grid_label_ui(ui, |ui| { + ui.label("Output"); + tip(ui, |ui| { + ui.label("This applies to touch and tablet input."); + }); + }); + ui.horizontal(|ui| { + let old = dev.output.get().and_then(|v| v.global.get()); + let old = old.as_ref(); + let mut v = old; + let mut cb = ComboBox::from_id_salt("output"); + if let Some(v) = v { + cb = cb.selected_text(&*v.connector.name); + } + cb.show_ui(ui, |ui| { + for &output in outputs { + ui.selectable_value( + &mut v, + Some(output), + &*output.connector.name, + ); + } + }); + if v != old { + dev.set_output(&self.state, v.map(|v| &**v)); + } + if ui.button("Detach").clicked() { + dev.set_output(&self.state, None); + } + }); + } + matrix_ui( + ui, + "Transform Matrix", + |ui| { + tip(ui, |ui| { + ui.label("This matrix is applied to relative pointer movements."); + }); + }, + dev.device + .has_capability(InputDeviceCapability::Pointer) + .then(|| { + dev.device + .transform_matrix() + .unwrap_or([[1.0, 0.0], [0.0, 1.0]]) + }), + |v| dev.set_transform_matrix(&self.state, v), + ); + matrix( + ui, + "Calibration Matrix", + dev.device.calibration_matrix(), + |v| dev.set_calibration_matrix(&self.state, v), + ); + }); + if dev.device.has_capability(InputDeviceCapability::Keyboard) { + ui.collapsing("Device Keymap", |ui| { + let ks = self.keymaps.entry(Key::Dev(dev_id)).or_default(); + let map = dev.keymap.get(); + ui.add_enabled_ui(map.is_some(), |ui| { + if ui.button("Use Seat Keymap").clicked() { + ks.backup(map.as_ref()); + dev.set_keymap(&self.state, None); + } + }); + show_keymap( + &self.state, + ps, + &mut self.paste_requested, + ks, + ui, + map.as_ref(), + |m| { + dev.set_keymap(&self.state, Some(m.clone())); + }, + ); + }); + } + }); + } +} + +impl KeymapState { + fn backup(&mut self, map: Option<&Rc>) { + if self.backup.is_none() + && let Some(map) = map + { + self.backup = Some(map.clone()); + } + } +} + +fn show_keymap( + state: &State, + ps: &mut PaneState, + paste_requested: &mut Option, + ks: &mut KeymapState, + ui: &mut Ui, + map: Option<&Rc>, + set_map: impl Fn(&Rc), +) { + ui.scope_builder(UiBuilder::new().id(("keymap-settings", ks.seed)), |ui| { + ui.add_enabled_ui(map.is_some(), |ui| { + if ui.button("Copy Keymap").clicked() + && let Some(map) = map + { + ui.ctx().copy_text(map.map_text.clone()); + } + }); + let backup = |ks: &mut KeymapState| { + ks.backup(map); + }; + if ui.button("Load Default Keymap").clicked() { + backup(ks); + set_map(&state.default_keymap); + } + ui.horizontal(|ui| { + ui.add_enabled_ui(map.is_some(), |ui| { + if ui.button("Backup Keymap").clicked() { + ks.backup = None; + backup(ks); + } + }); + if let Some(backup) = &ks.backup + && ui.button("Restore Keymap").clicked() + { + set_map(backup); + ks.backup = None; + } + }); + let mut label = "Load Keymap from Clipboard".to_string(); + if *paste_requested == Some(ui.id()) { + label.push_str(" "); + label.push_str(ICON_PENDING); + } + let button = ui.button(label); + if button.clicked() { + *paste_requested = Some(ui.id()); + button.request_focus(); + ui.ctx().send_viewport_cmd(ViewportCommand::RequestPaste); + } else if *paste_requested == Some(ui.id()) && button.has_focus() { + ui.input(|e| { + let map = e + .events + .iter() + .filter_map(|e| match e { + Event::Paste(s) => Some(s), + _ => None, + }) + .next(); + let Some(map) = map else { + return; + }; + *paste_requested = None; + let map = match state.kb_ctx.parse_keymap(map.as_bytes()) { + Ok(m) => m, + Err(e) => { + let error = format!("Could not parse keymap: {}", ErrorFmt(e)); + ps.errors.push(error); + return; + } + }; + backup(ks); + set_map(&map); + }); + } else if *paste_requested == Some(ui.id()) { + *paste_requested = None; + } + ui.collapsing("Create Keymap from Names", |ui| { + grid(ui, ("keymap-from-names", ui.id()), |ui| { + let defaulted = + |ui: &mut Ui, name: &str, default: &mut bool, text: &mut dyn TextBuffer| { + let ui = &mut *ui.row(); + grid_label(ui, name); + ui.add_enabled_ui(!*default, |ui| { + text_edit(ui, text); + }); + ui.checkbox(default, "Default"); + }; + let required = |ui: &mut Ui, name, text| { + let ui = &mut *ui.row(); + grid_label(ui, name); + text_edit(ui, text); + }; + defaulted(ui, "Rules", &mut ks.rules_default, &mut ks.rules); + defaulted(ui, "Model", &mut ks.model_default, &mut ks.model); + required(ui, "Layouts", &mut ks.layouts); + required(ui, "Variants", &mut ks.variants); + required(ui, "Options", &mut ks.options); + }); + if ui.button("Load").clicked() { + 'set_map: { + let map = state.kb_ctx.keymap_from_rmlvo( + (!ks.rules_default).then_some(&ks.rules), + (!ks.model_default).then_some(&ks.model), + Some(&ks.layouts), + Some(&ks.variants), + Some(&ks.options), + ); + let map = match map { + Ok(map) => map, + Err(e) => { + let error = format!("Could not parse keymap: {}", ErrorFmt(e)); + ps.errors.push(error); + break 'set_map; + } + }; + backup(ks); + set_map(&map); + } + } + }); + }); +} + +fn matrix( + ui: &mut Ui, + name: &str, + old: Option<[[T; W]; 2]>, + set: impl FnOnce([[T; W]; 2]), +) where + T: Numeric, +{ + matrix_ui(ui, name, |_| (), old, set); +} + +fn matrix_ui( + ui: &mut Ui, + name: &str, + label: impl FnOnce(&mut Ui) -> R, + old: Option<[[T; W]; 2]>, + set: impl FnOnce([[T; W]; 2]), +) where + T: Numeric, +{ + if let Some(mut m) = old { + let old = m; + let ui = &mut *ui.row(); + grid_label_ui(ui, |ui| { + ui.label(name); + label(ui); + }); + Grid::new(name).show(ui, |ui| { + for row in &mut m { + for cell in row { + DragValue::new(cell).speed(0.01).ui(ui); + } + ui.end_row(); + } + }); + if old != m { + set(m); + } + } +} diff --git a/src/control_center/cc_look_and_feel.rs b/src/control_center/cc_look_and_feel.rs new file mode 100644 index 00000000..636e80da --- /dev/null +++ b/src/control_center/cc_look_and_feel.rs @@ -0,0 +1,163 @@ +use { + crate::{ + cmm::cmm_eotf::Eotf, + control_center::{ + ControlCenterInner, bool, bool_ui, combo_box, drag_value, grid, grid_label, row, + text_edit, tip, + }, + gfx_api::AlphaMode, + state::State, + theme::{Color, ThemeColor, ThemeSized}, + utils::static_text::StaticText, + }, + egui::Ui, + isnt::std_1::primitive::IsntStrExt, + linearize::LinearizeExt, + std::rc::Rc, +}; + +pub struct LookAndFeelPane { + state: Rc, +} + +impl ControlCenterInner { + pub fn create_look_and_feel_pane(self: &Rc) -> LookAndFeelPane { + LookAndFeelPane { + state: self.state.clone(), + } + } +} + +impl LookAndFeelPane { + pub fn title(&self, res: &mut String) { + res.push_str("Look and Feel"); + } + + pub fn show(&mut self, ui: &mut Ui) { + let t = &self.state.theme; + grid(ui, "settings", |ui| { + bool(ui, "Show Bar", self.state.show_bar.get(), |v| { + self.state.set_show_bar(v) + }); + combo_box(ui, "Bar Position", t.bar_position.get(), |p| { + self.state.set_bar_position(p); + }); + bool(ui, "Show Titles", t.show_titles.get(), |v| { + self.state.set_show_titles(v) + }); + bool_ui( + ui, + "Primary Selection", + |ui| { + tip(ui, |ui| { + ui.label("Requires applications to be restarted."); + }); + }, + self.state.enable_primary_selection.get(), + |v| self.state.set_primary_selection_enabled(v), + ); + bool_ui( + ui, + "UI Drag", + |ui| { + tip(ui, |ui| { + ui.label("Allows dragging workspaces and tiled windows with the mouse."); + }); + }, + self.state.ui_drag_enabled.get(), + |v| self.state.set_ui_drag_enabled(v), + ); + drag_value( + ui, + "UI Drag Threshold (px)", + self.state.ui_drag_threshold_squared.get().isqrt(), + 1..=i32::MAX, + 1.0, + |v| { + self.state.set_ui_drag_threshold(v); + }, + ); + bool_ui( + ui, + "Float Pin Icon", + |ui| { + tip(ui, |ui| { + ui.label("Show the pin icon even if the window is not pinned."); + ui.label("Pinned floating windows are shown on all workspaces."); + }); + }, + self.state.show_pin_icon.get(), + |v| self.state.set_show_pin_icon(v), + ); + bool_ui( + ui, + "Float Above Fullscreen", + |ui| { + tip(ui, |ui| { + ui.label("Show floating windows above fullscreen windows."); + }); + }, + self.state.float_above_fullscreen.get(), + |v| self.state.set_float_above_fullscreen(v), + ); + row(ui, "Font", |ui| { + let mut v = self.state.theme.font.get().to_string(); + if text_edit(ui, &mut v).changed() { + self.state.set_font(&v); + } + }); + row(ui, "Title Font", |ui| { + let mut v = t + .title_font + .get() + .map(|v| v.to_string()) + .unwrap_or_default(); + if text_edit(ui, &mut v).changed() { + self.state.set_title_font(v.is_not_empty().then_some(&v)); + } + }); + row(ui, "Bar Font", |ui| { + let mut v = t.bar_font.get().map(|v| v.to_string()).unwrap_or_default(); + if text_edit(ui, &mut v).changed() { + self.state.set_bar_font(v.is_not_empty().then_some(&v)); + } + }); + }); + if ui.button("Reset Sizes").clicked() { + self.state.reset_sizes(); + } + if ui.button("Reset Colors").clicked() { + self.state.reset_colors(); + } + if ui.button("Reset Fonts").clicked() { + self.state.reset_fonts(); + } + ui.collapsing("Sizes", |ui| { + grid(ui, "Sizes", |ui| { + for v in ThemeSized::variants() { + let f = v.field(&self.state.theme); + drag_value(ui, v.text(), f.get(), v.min()..=v.max(), 1.0, |i| { + self.state.set_size(v, i); + }); + } + }); + }); + ui.collapsing("Colors", |ui| { + grid(ui, "Colors", |ui| { + for tc in ThemeColor::variants() { + let f = tc.field(t); + let mut v = f.get().to_array(Eotf::Linear); + grid_label(ui, tc.text()); + let changed = ui.color_edit_button_rgba_premultiplied(&mut v).changed(); + ui.end_row(); + if changed { + let [r, g, b, a] = v; + let c = + Color::new(Eotf::Linear, AlphaMode::PremultipliedOptical, r, g, b, a); + self.state.set_color(tc, c); + } + } + }); + }); + } +} diff --git a/src/control_center/cc_outputs.rs b/src/control_center/cc_outputs.rs new file mode 100644 index 00000000..fc3159be --- /dev/null +++ b/src/control_center/cc_outputs.rs @@ -0,0 +1,1711 @@ +use { + crate::{ + backend::{ + BackendColorSpace, BackendEotfs, ConnectorId, Mode, + transaction::{ + BackendConnectorTransactionError, ConnectorTransaction, + PreparedConnectorTransaction, + }, + }, + cmm::cmm_luminance::Luminance, + compositor::{MAX_EXTENTS, MAX_SCALE, MIN_SCALE}, + control_center::{ControlCenterInner, GridExt, PaneState, grid, grid_label, label, tip}, + egui_adapter::{ + egui_oklch::Color32Ext, + egui_platform::icons::{ICON_ADD, ICON_REMOVE}, + }, + ifs::{ + head_management::{HeadName, HeadState, ReadOnlyHeadState}, + wl_output::BlendSpace, + }, + scale::{SCALE_BASE, SCALE_BASEF, Scale}, + state::State, + tree::{TearingMode, Transform, VrrMode}, + utils::errorfmt::ErrorFmt, + }, + ahash::AHashMap, + egui::{ + Align, Button, Checkbox, Color32, ComboBox, DragValue, EventFilter, FontId, Frame, Grid, + Id, Key, Layout, PointerButton, Rect, ScrollArea, Sense, Shadow, Stroke, StrokeKind, Style, + TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, pos2, text::LayoutJob, vec2, + }, + egui_tiles::{ + Behavior, Container, Linear, LinearDir, ResizeState, SimplificationOptions, Tile, TileId, + Tiles, Tree, UiResponse, + }, + linearize::{Linearize, LinearizeExt}, + rand::random, + serde::{Deserialize, Serialize}, + std::{ + cell::{Cell, Ref}, + fmt, + rc::Rc, + }, + thiserror::Error, +}; + +pub struct OutputsPane { + tree: Tree, + root_id: TileId, + arrangement_id: Option, + inner: OutputsPaneInner, +} + +struct OutputsPaneInner { + state: Rc, + in_transaction: Cell, + heads: AHashMap, + ui: UiSettings, + settings: Settings, + seed: u64, +} + +enum Pane { + Arrangement, + Settings, +} + +struct CompleteHead { + id: ConnectorId, + name: HeadName, + live_state: ReadOnlyHeadState, + changed_state: Option, + z: u64, + focus: u64, + drag_pos: Option<(f32, f32)>, +} + +struct UiSettings { + scale: f32, + origin: Vec2, + origin_drag: Option, + next_z: u64, + focus: u64, + zoom_to_fit: bool, + view: View, +} + +struct Settings { + show_guide_lines: bool, + snap_to_neighbor: bool, + show_disconnected: bool, + show_disabled: bool, + show_arrangement: bool, + layout: UiLayout, +} + +impl Default for Settings { + fn default() -> Self { + Self { + show_guide_lines: false, + snap_to_neighbor: true, + show_disconnected: false, + show_disabled: true, + show_arrangement: true, + layout: UiLayout::Auto, + } + } +} + +#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Linearize)] +enum UiLayout { + Auto, + Vertical, + Horizontal, +} + +#[derive(Copy, Clone)] +pub enum View { + Connectors, + Settings, +} + +#[derive(Error, Debug)] +enum HeadTransactionError { + #[error("The connector {} has been removed", .0)] + HeadRemoved(ConnectorId), + #[error("The display connected to connector {} has changed", .0)] + MonitorChanged(ConnectorId), + #[error(transparent)] + Backend(#[from] BackendConnectorTransactionError), +} + +macro_rules! effective { + ($m:expr, $t:expr) => { + $t.as_ref().unwrap_or($m) + }; +} + +macro_rules! modify { + ($m:expr, $t:expr) => { + $t.get_or_insert_with(|| $m.clone()) + }; +} + +impl ControlCenterInner { + pub fn create_outputs_pane(self: &Rc) -> OutputsPane { + let seed = random(); + let mut tiles = Tiles::default(); + let settings_id = tiles.insert_pane(Pane::Settings); + let arrangement_id = tiles.insert_pane(Pane::Arrangement); + let root_id = tiles.insert_container(Linear::new( + LinearDir::Horizontal, + vec![arrangement_id, settings_id], + )); + let tree = Tree::new(Id::new(("cc_outputs", seed)), root_id, tiles); + let mut pane = OutputsPane { + root_id, + arrangement_id: Some(arrangement_id), + tree, + inner: OutputsPaneInner { + state: self.state.clone(), + ui: UiSettings { + scale: 0.1, + origin: Default::default(), + origin_drag: None, + next_z: 0, + focus: 0, + zoom_to_fit: true, + view: View::Connectors, + }, + settings: Default::default(), + in_transaction: Default::default(), + heads: Default::default(), + seed, + }, + }; + pane.inner.reset(); + pane + } +} + +struct B<'a>(&'a mut OutputsPaneInner, &'a mut PaneState); + +impl Behavior for B<'_> { + fn pane_ui(&mut self, ui: &mut Ui, _tile_id: TileId, pane: &mut Pane) -> UiResponse { + Frame::new().inner_margin(5.0).show(ui, |ui| match pane { + Pane::Arrangement => self.0.show_arrangement(ui), + Pane::Settings => self.0.show_main_area(self.1, ui), + }); + UiResponse::None + } + + fn tab_title_for_pane(&mut self, _pane: &Pane) -> WidgetText { + "".into() + } + + fn gap_width(&self, _style: &Style) -> f32 { + 5.0 + } + + fn simplification_options(&self) -> SimplificationOptions { + SimplificationOptions { + prune_empty_tabs: false, + prune_empty_containers: false, + prune_single_child_tabs: false, + prune_single_child_containers: false, + all_panes_must_have_tabs: false, + join_nested_linear_containers: false, + } + } + + fn resize_stroke(&self, style: &Style, resize_state: ResizeState) -> Stroke { + match resize_state { + ResizeState::Idle => style.visuals.widgets.noninteractive.bg_stroke, + ResizeState::Hovering => style.visuals.widgets.hovered.fg_stroke, + ResizeState::Dragging => style.visuals.widgets.active.fg_stroke, + } + } +} + +impl OutputsPane { + pub fn title(&self, res: &mut String) { + res.push_str("Outputs"); + if self.inner.in_transaction.get() { + res.push_str(" (*)"); + } + } + + pub fn show(&mut self, ps: &mut PaneState, ui: &mut Ui) { + self.inner.add_new_heads(); + if let Some(id) = self.arrangement_id { + if !self.inner.settings.show_arrangement { + self.tree.remove_recursively(id); + self.arrangement_id = None; + } + } else { + if self.inner.settings.show_arrangement { + let id = self.tree.tiles.insert_pane(Pane::Arrangement); + self.tree.move_tile_to_container(id, self.root_id, 0, false); + self.arrangement_id = Some(id); + } + } + let show_vertical = match self.inner.settings.layout { + UiLayout::Auto => ui.available_width() < 1024.0, + UiLayout::Vertical => true, + UiLayout::Horizontal => false, + }; + if let Some(root) = self.tree.tiles.get_mut(self.root_id) + && let Tile::Container(root) = root + && let Container::Linear(root) = root + { + root.dir = match show_vertical { + true => LinearDir::Vertical, + false => LinearDir::Horizontal, + }; + } + self.tree.ui(&mut B(&mut self.inner, ps), ui) + } +} + +impl OutputsPaneInner { + fn show_main_area(&mut self, ps: &mut PaneState, ui: &mut Ui) { + ui.scope_builder(UiBuilder::new().id(("main_area", self.seed)), |ui| { + self.show_settings_bar(ps, ui); + ScrollArea::vertical().show(ui, |ui| { + match self.ui.view { + View::Connectors => self.show_connectors(ui), + View::Settings => self.show_settings(ui), + } + ui.allocate_space(ui.available_size()); + }); + }); + } + + fn show_settings_bar(&mut self, ps: &mut PaneState, ui: &mut Ui) { + ui.horizontal_wrapped(|ui| { + if ui.button("Connectors").clicked() { + self.ui.view = View::Connectors; + ui.ctx().request_repaint(); + } + if ui.button("Settings").clicked() { + self.ui.view = View::Settings; + ui.ctx().request_repaint(); + } + if ui + .checkbox(&mut self.ui.zoom_to_fit, "Zoom To Fit") + .changed() + { + ui.ctx().request_repaint(); + } + { + let mut reset = !self.in_transaction.get(); + let reset2 = reset; + let widget = Checkbox::new(&mut reset, "Reset"); + if reset2 { + ui.add_enabled(false, widget); + } else { + if widget.ui(ui).changed() { + self.reset(); + } + } + } + ui.with_layout(Layout::right_to_left(Align::LEFT), |ui| { + let enabled = self.in_transaction.get(); + ui.add_enabled_ui(enabled, |ui| { + if ui.button("Test").clicked() { + if let Err(e) = self.test_transaction() { + ps.errors.push(ErrorFmt(e).to_string()); + } + } + }); + let enabled = self.in_transaction.get(); + ui.add_enabled_ui(enabled, |ui| { + let button = Button::new("Commit").fill(ui.style().visuals.extreme_bg_color); + if ui.add(button).clicked() { + match self.commit_transaction() { + Ok(_) => self.reset(), + Err(e) => { + ps.errors.push(ErrorFmt(e).to_string()); + } + } + } + }); + ui.add_space(ui.available_width()); + }); + }); + ui.separator(); + } + + fn show_connectors(&mut self, ui: &mut Ui) { + let mut heads: Vec<_> = self.heads.values_mut().collect(); + heads.sort_by(|a, b| { + a.live_state + .borrow() + .name + .cmp(&b.live_state.borrow().name) + .then_with(|| a.name.cmp(&b.name)) + }); + let mut is_in_transaction = false; + for head in &mut heads { + show_connector(&self.state, &self.settings, head, ui); + if head.changed_state.is_some() { + is_in_transaction = true; + } + } + self.in_transaction.set(is_in_transaction); + } + + fn show_settings(&mut self, ui: &mut Ui) { + let mut changed = false; + + { + changed |= ui + .checkbox(&mut self.settings.show_guide_lines, "Show guide lines") + .changed(); + } + + { + ui.horizontal(|ui| { + changed |= ui + .checkbox(&mut self.settings.snap_to_neighbor, "Snap to neighbor") + .changed(); + tip(ui, |ui| { + ui.label("Hold Shift to invert this"); + }); + }); + } + + { + ui.checkbox(&mut self.settings.show_arrangement, "Show arrangement area"); + } + + { + let layout_text = |l: UiLayout| match l { + UiLayout::Auto => "Auto", + UiLayout::Vertical => "Vertical", + UiLayout::Horizontal => "Horizontal", + }; + changed |= ComboBox::new("layout", "Layout") + .selected_text(layout_text(self.settings.layout)) + .show_ui(ui, |ui| { + for l in UiLayout::variants() { + ui.selectable_value(&mut self.settings.layout, l, layout_text(l)); + } + }) + .response + .changed(); + } + + { + changed |= ui + .checkbox( + &mut self.settings.show_disconnected, + "Show disconnected heads", + ) + .changed(); + } + + { + changed |= ui + .checkbox(&mut self.settings.show_disabled, "Show disabled heads") + .changed(); + } + + if changed { + ui.ctx().request_repaint(); + } + } + + fn show_arrangement(&mut self, ui: &mut Ui) { + let clip_rect = ui.available_rect_before_wrap(); + let origin = &mut self.ui.origin; + let ox = origin.x.round(); + let oy = origin.y.round(); + let mut heads = vec![]; + let scale = self.ui.scale; + struct PreparedHead<'a> { + name: HeadName, + m: Ref<'a, HeadState>, + changed_state: &'a mut Option, + z: &'a mut u64, + focus: &'a mut u64, + drag_pos: &'a mut Option<(f32, f32)>, + x1: i32, + y1: i32, + x2: i32, + y2: i32, + rect: Rect, + } + for head in self.heads.values_mut() { + let m = head.live_state.borrow(); + let e = effective!(&*m, head.changed_state); + if !e.in_compositor_space { + continue; + } + let (x, y) = e.position; + let (w, h) = e.size; + let x1 = (x as f32 * scale).round() - ox + clip_rect.min.x; + let y1 = (y as f32 * scale).round() - oy + clip_rect.min.y; + let x2 = ((x + w) as f32 * scale).round() - ox + clip_rect.min.x; + let y2 = ((y + h) as f32 * scale).round() - oy + clip_rect.min.y; + heads.push(PreparedHead { + name: head.name, + m, + changed_state: &mut head.changed_state, + z: &mut head.z, + focus: &mut head.focus, + drag_pos: &mut head.drag_pos, + x1: x, + y1: y, + x2: x + w, + y2: y + h, + rect: Rect { + min: pos2(x1, y1), + max: pos2(x2, y2), + }, + }); + } + if self.ui.zoom_to_fit { + let mut x_min = i32::MAX; + let mut x_max = i32::MIN; + let mut y_min = i32::MAX; + let mut y_max = i32::MIN; + for head in &heads { + x_min = x_min.min(head.x1); + x_max = x_max.max(head.x2); + y_min = y_min.min(head.y1); + y_max = y_max.max(head.y2); + } + if x_min > x_max { + x_min = 0; + x_max = 0; + } + if y_min > y_max { + y_min = 0; + y_max = 0; + } + x_min -= 100; + y_min -= 100; + x_max += 100; + y_max += 100; + let dx = x_max - x_min + 1; + let dy = y_max - y_min + 1; + let x_scale = clip_rect.width() / dx as f32; + let y_scale = clip_rect.height() / dy as f32; + let new_scale = x_scale.min(y_scale); + let new_ox = x_min as f32 * new_scale; + let new_oy = y_min as f32 * new_scale; + if new_scale != scale || new_ox != ox || new_oy != oy { + self.ui.scale = new_scale; + origin.x = new_ox; + origin.y = new_oy; + ui.ctx().request_repaint(); + } + } + heads.sort_by_key(|h| *h.z); + let style = &ui.style().visuals; + let mut bg_color = style.panel_fill.to_oklch(); + let mut no_capture_bg_color = bg_color; + let mut disabled_bg_color = bg_color; + if bg_color.l > 0.5 { + bg_color.l -= 0.05; + disabled_bg_color.l -= 0.1; + no_capture_bg_color.l -= 0.075; + } else { + bg_color.l += 0.05; + disabled_bg_color.l += 0.1; + no_capture_bg_color.l += 0.075; + } + let fg_color_base = style.widgets.noninteractive.text_color().to_oklab(); + let fg_color_1 = fg_color_base * 1.0 / 3.0 + bg_color.to_oklab() * 2.0 / 3.0; + let fg_color_2 = fg_color_base * 2.0 / 3.0 + bg_color.to_oklab() * 1.0 / 3.0; + let painter = ui.painter_at(clip_rect); + painter.rect( + Rect::EVERYTHING, + 0.0, + disabled_bg_color, + Stroke::NONE, + StrokeKind::Inside, + ); + { + let x_min = 0.0; + let y_min = 0.0; + let x_max = MAX_EXTENTS as f32; + let y_max = MAX_EXTENTS as f32; + let good = Rect { + min: clip_rect.min + vec2(x_min, y_min) * scale - *origin, + max: clip_rect.min + vec2(x_max, y_max) * scale - *origin, + }; + painter.rect(good, 0.0, bg_color, Stroke::NONE, StrokeKind::Inside); + } + painter.hline( + clip_rect.left()..=clip_rect.right(), + clip_rect.min.y - origin.y, + (1.0, fg_color_1), + ); + painter.vline( + clip_rect.min.x - origin.x, + clip_rect.top()..=clip_rect.bottom(), + (1.0, fg_color_1), + ); + if self.settings.show_guide_lines { + for head in &heads { + let rect = head.rect; + painter.hline( + clip_rect.left()..=clip_rect.right(), + rect.top(), + (1.0, fg_color_2), + ); + painter.hline( + clip_rect.left()..=clip_rect.right(), + rect.bottom(), + (1.0, fg_color_2), + ); + painter.vline( + rect.left(), + clip_rect.top()..=clip_rect.bottom(), + (1.0, fg_color_2), + ); + painter.vline( + rect.right(), + clip_rect.top()..=clip_rect.bottom(), + (1.0, fg_color_2), + ); + } + } + for head in &mut heads { + let rect = head.rect; + let mut color = fg_color_2; + if *head.focus == self.ui.focus { + let shape = Shadow { + offset: [0, 0], + blur: (512.0 * scale).sqrt() as u8, + spread: (255.0 * scale).sqrt() as u8, + color: Color32::from_black_alpha(200), + } + .as_shape(rect, 0.0); + painter.add(shape); + color = fg_color_base; + } + painter.hline(rect.left()..=rect.right() + 1.0, rect.top(), (1.0, color)); + painter.hline( + rect.left()..=rect.right() + 1.0, + rect.bottom(), + (1.0, color), + ); + painter.vline(rect.left(), rect.top()..=rect.bottom() + 1.0, (1.0, color)); + painter.vline(rect.right(), rect.top()..=rect.bottom() + 1.0, (1.0, color)); + let content_rect = Rect { + min: pos2(rect.min.x + 1.0, rect.min.y + 1.0), + max: pos2(rect.max.x, rect.max.y), + }; + let painter = painter.with_clip_rect(content_rect); + painter.rect( + content_rect, + 0.0, + no_capture_bg_color, + Stroke::NONE, + StrokeKind::Inside, + ); + let galley = + painter.layout_no_wrap(head.m.name.to_string(), FontId::default(), Color32::WHITE); + let rect = Rect::from_min_size(content_rect.min, galley.rect.size() + vec2(2.0, 2.0)); + painter.rect(rect, 0.0, Color32::BLUE, Stroke::NONE, StrokeKind::Inside); + painter.galley(rect.min + vec2(1.0, 1.0), galley, Color32::WHITE); + } + ui.allocate_space(ui.available_size()); + macro_rules! interacted { + () => {{ + self.ui.zoom_to_fit = false; + }}; + } + let response = ui.allocate_rect(clip_rect, Sense::all()); + if response.has_focus() { + let mut dx = 0; + let mut dy = 0; + ui.ctx().input(|i| { + if i.key_pressed(Key::ArrowUp) { + dy -= 1; + } + if i.key_pressed(Key::ArrowDown) { + dy += 1; + } + if i.key_pressed(Key::ArrowLeft) { + dx -= 1; + } + if i.key_pressed(Key::ArrowRight) { + dx += 1; + } + }); + if dx != 0 || dy != 0 { + interacted!(); + for head in &mut heads { + if *head.focus == self.ui.focus { + let x = (head.x1 + dx).clamp(0, MAX_EXTENTS); + let y = (head.y1 + dy).clamp(0, MAX_EXTENTS); + let pos = (x, y); + if effective!(&*head.m, head.changed_state).position != pos { + modify!(&*head.m, head.changed_state).position = pos; + ui.ctx().request_repaint(); + } + } + } + } + } + if let Some(pos) = response.hover_pos() { + let scroll = ui.ctx().input(|i| i.smooth_scroll_delta); + let mut new = scale; + if scroll.y != 0.0 { + interacted!(); + } + if scroll.y < 0.0 { + new /= 1.0 - scroll.y / 1000.0; + } else { + new *= 1.0 + scroll.y / 1000.0; + } + new = new.max(0.01); + if new != scale { + self.ui.scale = new; + ui.ctx().request_repaint(); + let relative_pos = pos - clip_rect.min; + let real_pos = (relative_pos + *origin) / scale; + *origin = real_pos * new - relative_pos; + } + } + if ui + .ctx() + .input(|i| i.pointer.button_pressed(PointerButton::Primary)) + { + self.ui.focus += 1; + if let Some(pos) = response.hover_pos() { + interacted!(); + response.request_focus(); + for head in heads.iter_mut().rev() { + if head.rect.contains(pos) { + *head.z = self.ui.next_z; + self.ui.next_z += 1; + *head.focus = self.ui.focus; + ui.ctx().request_repaint(); + break; + } + } + } + } + if response.clicked_elsewhere() { + self.ui.focus += 1; + } + if response.drag_started_by(PointerButton::Middle) + || response.drag_started_by(PointerButton::Secondary) + { + interacted!(); + self.ui.origin_drag = Some(self.ui.origin); + } else if response.drag_started_by(PointerButton::Primary) + && let Some(pos) = response.hover_pos() + { + interacted!(); + for head in heads.iter_mut().rev() { + if head.rect.contains(pos) { + *head.drag_pos = Some((head.x1 as f32, head.y1 as f32)); + break; + } + } + } + let drag_delta = response.drag_delta(); + if drag_delta.x != 0.0 || drag_delta.y != 0.0 { + if let Some(origin_drag) = &mut self.ui.origin_drag { + *origin_drag -= drag_delta; + self.ui.origin = *origin_drag; + ui.ctx().request_repaint(); + } + let snap = self.settings.snap_to_neighbor ^ ui.ctx().input(|i| i.modifiers.shift); + let mut head_positions = vec![]; + struct HeadPosition { + name: HeadName, + x1: i32, + y1: i32, + x2: i32, + y2: i32, + } + if snap { + for head in &heads { + let PreparedHead { + name, + x1, + y1, + x2, + y2, + .. + } = *head; + head_positions.push(HeadPosition { + name, + x1, + y1, + x2, + y2, + }); + } + } + for head in &mut heads { + if let Some((mut x, mut y)) = *head.drag_pos { + x += drag_delta.x / scale; + y += drag_delta.y / scale; + let mut x_int = if x < 0.0 { x.ceil() } else { x.floor() } as i32; + let mut y_int = if y < 0.0 { y.ceil() } else { y.floor() } as i32; + if snap { + for other in &head_positions { + if head.name == other.name { + continue; + } + macro_rules! snap { + ($int:ident, $one:ident, $two:ident) => { + if $int.abs() as f32 * scale <= 10.0 { + $int = 0; + } else if ($int - other.$one).abs() as f32 * scale <= 10.0 { + $int = other.$one; + } else if ($int - other.$two).abs() as f32 * scale <= 10.0 { + $int = other.$two; + } else if ($int + head.$two - head.$one - other.$one).abs() + as f32 + * scale + <= 10.0 + { + $int = other.$one + head.$one - head.$two; + } else if ($int + head.$two - head.$one - other.$two).abs() + as f32 + * scale + <= 10.0 + { + $int = other.$two + head.$one - head.$two; + } + }; + } + snap!(x_int, x1, x2); + snap!(y_int, y1, y2); + } + } + x_int = x_int.clamp(0, MAX_EXTENTS); + y_int = y_int.clamp(0, MAX_EXTENTS); + let pos = (x_int, y_int); + if effective!(&*head.m, head.changed_state).position != pos { + modify!(&*head.m, head.changed_state).position = pos; + ui.ctx().request_repaint(); + } + *head.drag_pos = Some((x, y)); + } + } + } + if response.drag_stopped() { + self.ui.origin_drag = None; + for head in heads.iter_mut().rev() { + *head.drag_pos = None; + } + } + ui.ctx().memory_mut(|mem| { + mem.set_focus_lock_filter( + response.id, + EventFilter { + tab: false, + horizontal_arrows: true, + vertical_arrows: true, + escape: false, + }, + ) + }); + } + + fn prepare_transaction(&self) -> Result { + let mut tran = ConnectorTransaction::new(&self.state); + for head in self.heads.values() { + let Some(desired) = &head.changed_state else { + continue; + }; + let Some(connector) = self.state.connectors.get(&head.id) else { + return Err(HeadTransactionError::HeadRemoved(head.id)); + }; + if head.live_state.borrow().monitor_info != desired.monitor_info { + return Err(HeadTransactionError::MonitorChanged(head.id)); + } + let old = connector.state.borrow().clone(); + let mut new = old.clone(); + new.enabled = desired.connector_enabled; + new.mode = desired.mode; + new.non_desktop_override = desired.override_non_desktop; + new.format = desired.format; + new.color_space = desired.color_space; + new.eotf = desired.eotf; + if old == new { + continue; + } + tran.add(&connector.connector, new)?; + } + Ok(tran.prepare()?) + } + + fn commit_transaction(&self) -> Result<(), HeadTransactionError> { + self.prepare_transaction()?.apply()?.commit(); + for head in self.heads.values() { + let Some(desired) = &head.changed_state else { + continue; + }; + if let Some(output) = self.state.outputs.get(&head.id) + && let Some(node) = &output.node + { + node.set_position(desired.position.0, desired.position.1); + node.set_preferred_scale(desired.scale); + node.update_transform(desired.transform); + node.set_vrr_mode(&desired.vrr_mode); + node.set_tearing_mode(&desired.tearing_mode); + node.set_brightness(desired.brightness); + node.set_blend_space(desired.blend_space); + node.set_use_native_gamut(desired.use_native_gamut); + node.schedule + .set_cursor_hz(&self.state, desired.vrr_cursor_hz.unwrap_or(f64::INFINITY)); + } else if let Some(mi) = &desired.monitor_info { + let pos = &self.state.persistent_output_states; + let pos = pos.lock().entry(mi.output_id.clone()).or_default().clone(); + pos.pos.set(desired.position); + pos.scale.set(desired.scale); + pos.transform.set(desired.transform); + pos.vrr_mode.set(desired.vrr_mode); + pos.tearing_mode.set(desired.tearing_mode); + pos.brightness.set(desired.brightness); + pos.blend_space.set(desired.blend_space); + pos.use_native_gamut.set(desired.use_native_gamut); + pos.vrr_cursor_hz.set(desired.vrr_cursor_hz); + } + } + Ok(()) + } + + fn test_transaction(&self) -> Result<(), HeadTransactionError> { + self.prepare_transaction()?; + Ok(()) + } + + fn reset(&mut self) { + self.in_transaction.set(false); + let mut to_remove = vec![]; + for head in self.heads.values_mut() { + if self.state.connectors.contains(&head.id) { + head.changed_state = None; + } else { + to_remove.push(head.name); + } + } + for name in to_remove { + self.heads.remove(&name); + } + } + + fn add_new_heads(&mut self) { + for connector in self.state.connectors.lock().values() { + let mgrs = &connector.head_managers; + self.heads.entry(mgrs.name).or_insert_with(|| CompleteHead { + id: connector.id, + name: mgrs.name, + live_state: mgrs.state(), + changed_state: None, + z: 0, + focus: 0, + drag_pos: None, + }); + } + } +} + +fn show_connector(state: &State, settings: &Settings, head: &mut CompleteHead, ui: &mut Ui) { + let m = &*head.live_state.borrow(); + let t = &mut head.changed_state; + if t.is_none() { + if !m.connector_enabled && !settings.show_disabled { + return; + } + if !m.connected && !settings.show_disconnected { + return; + } + } + let mut layout_job = LayoutJob::default(); + layout_job.append( + "Connector", + 0.0, + TextFormat { + color: ui.style().visuals.widgets.inactive.text_color(), + ..Default::default() + }, + ); + layout_job.append( + &m.name, + 10.0, + TextFormat { + color: ui.style().visuals.widgets.active.text_color(), + ..Default::default() + }, + ); + let mut name = String::new(); + if let Some(v) = &m.monitor_info { + name.push_str(&v.output_id.manufacturer); + name.push_str(" - "); + name.push_str(&v.output_id.model); + } + layout_job.append( + &name, + 10.0, + TextFormat { + color: ui.style().visuals.widgets.inactive.text_color(), + ..Default::default() + }, + ); + ui.collapsing(layout_job, |ui| { + grid(ui, ("settings", head.name), |ui| { + let mut diff = false; + show_serial_number(ui, m); + diff |= show_enablement(ui, m, t); + diff |= show_position(ui, m, t); + diff |= show_scale(ui, m, t); + diff |= show_mode(ui, m, t); + diff |= show_size(ui, m, t); + diff |= show_transform(ui, m, t); + diff |= show_brightness(ui, m, t); + diff |= show_color_space(ui, m, t); + diff |= show_eotf(ui, m, t); + diff |= show_format(ui, m, t); + diff |= show_tearing(ui, m, t); + diff |= show_vrr(ui, m, t); + diff |= show_non_desktop(ui, m, t); + diff |= show_blend_space(ui, m, t); + diff |= show_use_native_gamut(ui, m, t); + show_native_gamut(ui, m); + diff |= show_cursor_hz(ui, m, t); + show_flip_margin(state, ui, m, t, head.id); + if diff { + let ui = &mut *ui.row(); + ui.label(""); + ui.label(""); + ui.label("^ current"); + } + }); + }); +} + +fn show_serial_number(ui: &mut Ui, m: &HeadState) { + if let Some(info) = &m.monitor_info { + let ui = &mut *ui.row(); + grid_label(ui, "Serial Number"); + ui.label(&info.output_id.serial_number); + } +} + +fn show_enablement(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + let ui = &mut *ui.row(); + grid_label(ui, "Enabled"); + let mut v = effective!(m, t).connector_enabled; + let changed = Checkbox::without_text(&mut v).ui(ui).changed(); + if changed { + let t = modify!(m, t); + t.connector_enabled = v; + t.update_in_compositor_space(m.wl_output); + } + let diff = v != m.connector_enabled; + if diff { + ui.label(match m.connector_enabled { + true => "enabled", + false => "disabled", + }); + } + diff +} + +fn show_position(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + grid_label(ui, "Position"); + let (mut x, mut y) = effective!(m, t).position; + ui.horizontal(|ui| { + let value = |ui: &mut Ui, v, min, max| { + let res = DragValue::new(v).range(min..=max).speed(1.0).ui(ui); + res.changed() + }; + let mut changed = false; + changed |= value(ui, &mut x, 0, MAX_EXTENTS); + ui.label("x"); + changed |= value(ui, &mut y, 0, MAX_EXTENTS); + if changed { + modify!(m, t).position = (x, y); + } + }); + let diff = m.position != (x, y); + if diff { + ui.label(format!("{} x {}", m.position.0, m.position.1)); + } + diff +} + +fn show_scale(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + grid_label(ui, "Scale"); + let mut v = effective!(m, t).scale; + let old = v; + ui.horizontal(|ui| { + let mut s = v.to_f64(); + let res = DragValue::new(&mut s) + .range(MIN_SCALE.to_f64()..=MAX_SCALE.to_f64()) + .speed(1.0 / SCALE_BASEF) + .fixed_decimals(5) + .ui(ui); + if res.changed() { + v = Scale::from_f64(s); + } + if ui.button(ICON_REMOVE).clicked() { + v = Scale::from_wl(v.to_wl().saturating_sub(SCALE_BASE)).clamp(MIN_SCALE, MAX_SCALE); + } + if ui.button(ICON_ADD).clicked() { + v = Scale::from_wl(v.to_wl().saturating_add(SCALE_BASE)).clamp(MIN_SCALE, MAX_SCALE); + } + }); + if old != v { + let t = modify!(m, t); + t.scale = v; + t.update_size(); + } + let diff = m.scale != v; + if diff { + ui.label(format!("{}", m.scale.to_f64())); + } + diff +} + +fn show_mode(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + let mut mode = effective!(m, t).mode; + let old = mode; + grid_label(ui, "Mode"); + let mode_text = |mode: Mode| { + format!( + "{}x{}@{}", + mode.width, + mode.height, + mode.refresh_rate_millihz as f64 / 1000.0 + ) + }; + if let Some(monitor_info) = &m.monitor_info + && monitor_info.modes.len() > 1 + { + ComboBox::from_id_salt("modes") + .selected_text(mode_text(mode)) + .show_ui(ui, |ui| { + for v in &monitor_info.modes { + ui.selectable_value(&mut mode, *v, mode_text(*v)); + } + }); + } else { + ui.label(mode_text(mode)); + } + if old != mode { + let t = modify!(m, t); + t.mode = mode; + t.update_size(); + } + let mut diff = false; + if m.mode != mode { + diff = true; + ui.label(mode_text(m.mode)); + } + diff +} + +fn show_size(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if let Some(info) = &m.monitor_info { + let ui = &mut *ui.row(); + grid_label(ui, "Physical Size (mm)"); + ui.label(format!("{} x {}", info.width_mm, info.height_mm)); + } + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + grid_label(ui, "Size"); + let (w, h) = effective!(m, t).size; + ui.label(format!("{w} x {h}")); + let diff = m.size != (w, h); + if diff { + ui.label(format!("{} x {}", m.size.0, m.size.1)); + } + diff +} + +fn show_transform(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + grid_label(ui, "Transform"); + let mut v = effective!(m, t).transform; + let mut changed = false; + let transform_name = |t: Transform| match t { + Transform::None => "none", + Transform::Rotate90 => "rotate-90", + Transform::Rotate180 => "rotate-180", + Transform::Rotate270 => "rotate-270", + Transform::Flip => "flip", + Transform::FlipRotate90 => "flip-rotate-90", + Transform::FlipRotate180 => "flip-rotate-180", + Transform::FlipRotate270 => "flip-rotate-270", + }; + ComboBox::from_id_salt("transform") + .selected_text(transform_name(v)) + .show_ui(ui, |ui| { + let transforms = [ + Transform::None, + Transform::Rotate90, + Transform::Rotate180, + Transform::Rotate270, + Transform::Flip, + Transform::FlipRotate90, + Transform::FlipRotate180, + Transform::FlipRotate270, + ]; + for s in transforms { + changed |= ui.selectable_value(&mut v, s, transform_name(s)).changed(); + } + }); + if changed { + let t = modify!(m, t); + t.transform = v; + t.update_size(); + } + let diff = m.transform != v; + if diff { + ui.label(transform_name(m.transform)); + } + diff +} + +fn show_brightness(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let old_custom_brightness = effective!(m, t).brightness.is_some(); + let mut custom_brightness = old_custom_brightness; + let mut changed = false; + grid_label(ui, "Custom Brightness"); + Checkbox::without_text(&mut custom_brightness).ui(ui); + changed |= old_custom_brightness != custom_brightness; + let diff1 = m.brightness.is_some() != custom_brightness; + if diff1 { + ui.label(match m.brightness.is_some() { + true => "enabled", + false => "disabled", + }); + } + ui.end_row(); + + if !custom_brightness { + if changed { + modify!(m, t).brightness = None; + } + return diff1; + } + + grid_label(ui, "Brightness"); + ui.vertical(|ui| { + let effective = effective!(m, t); + let default_brightness = match effective.eotf { + BackendEotfs::Default => effective + .monitor_info + .as_ref() + .and_then(|m| m.luminance.as_ref()) + .map(|l| l.max) + .unwrap_or(Luminance::SRGB.white.0), + BackendEotfs::Pq => Luminance::ST2084_PQ.white.0, + }; + let mut brightness = effective.brightness.unwrap_or(default_brightness); + changed |= DragValue::new(&mut brightness) + .range(0.0..=1000.0) + .ui(ui) + .changed(); + ui.label(format!("reference: {default_brightness})")); + if changed { + modify!(m, t).brightness = Some(brightness); + } + }); + let mut diff2 = false; + if let Some(t) = t + && m.brightness != t.brightness + { + diff2 = true; + ui.label(format!( + "{}", + fmt::from_fn(|f| match m.brightness { + None => f.write_str("disabled"), + Some(v) => write!(f, "{}", v), + }) + )); + } + ui.end_row(); + diff1 || diff2 +} + +fn show_color_space(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + grid_label(ui, "Colorimetry"); + let mut v = effective!(m, t).color_space; + ui.horizontal(|ui| { + if let Some(monitor_info) = &effective!(m, t).monitor_info { + if monitor_info.color_spaces.is_empty() { + ui.label(v.name()); + } else { + let mut changed = false; + ComboBox::from_id_salt("colorimetry") + .selected_text(v.name()) + .show_ui(ui, |ui| { + changed |= ui + .selectable_value( + &mut v, + BackendColorSpace::Default, + BackendColorSpace::Default.name(), + ) + .changed(); + for &s in &monitor_info.color_spaces { + changed |= ui.selectable_value(&mut v, s, s.name()).changed(); + } + }); + if changed { + modify!(m, t).color_space = v; + } + } + } + }); + let diff = m.color_space != v; + if diff { + ui.label(m.color_space.name()); + } + diff +} + +fn show_eotf(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + grid_label(ui, "EOTF"); + let mut v = effective!(m, t).eotf; + ui.horizontal(|ui| { + if let Some(monitor_info) = &effective!(m, t).monitor_info { + if monitor_info.eotfs.is_empty() { + ui.label(v.name()); + } else { + let mut changed = false; + ComboBox::from_id_salt("eotf") + .selected_text(v.name()) + .show_ui(ui, |ui| { + changed |= ui + .selectable_value( + &mut v, + BackendEotfs::Default, + BackendEotfs::Default.name(), + ) + .changed(); + for &s in &monitor_info.eotfs { + changed |= ui.selectable_value(&mut v, s, s.name()).changed(); + } + }); + if changed { + modify!(m, t).eotf = v; + } + } + } + }); + let diff = m.eotf != v; + if diff { + ui.label(m.eotf.name()); + } + diff +} + +fn show_format(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + grid_label(ui, "Format"); + let mut v = effective!(m, t).format; + ui.horizontal(|ui| { + if m.supported_formats.len() < 2 { + ui.label(v.name); + } else { + let mut changed = false; + ComboBox::from_id_salt("format") + .selected_text(v.name) + .show_ui(ui, |ui| { + for &s in &*m.supported_formats { + changed |= ui.selectable_value(&mut v, s, s.name).changed(); + } + }); + if changed { + modify!(m, t).format = v; + } + } + }); + let diff = m.format != v; + if diff { + ui.label(m.format.name); + } + diff +} + +fn show_tearing(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let render_settings = |ui: &mut Ui, old: TearingMode| { + #[derive(Copy, Clone, PartialEq, Linearize)] + enum Mode { + Never, + Always, + Fullscreen, + } + fn name(mode: Mode) -> &'static str { + match mode { + Mode::Never => "Never", + Mode::Always => "Always", + Mode::Fullscreen => "Fullscreen", + } + } + let mut mode = match old { + TearingMode::Never => Mode::Never, + TearingMode::Always => Mode::Always, + TearingMode::Fullscreen { .. } => Mode::Fullscreen, + }; + let old_mode = mode; + let mut surface = None; + ui.vertical(|ui| { + ComboBox::from_id_salt("tearing mode") + .selected_text(name(mode)) + .show_ui(ui, |ui| { + for s in Mode::variants() { + ui.selectable_value(&mut mode, s, name(s)); + } + }); + if mode == Mode::Fullscreen { + if old_mode != mode { + surface = Some(Default::default()); + } + if let TearingMode::Fullscreen { surface: s } = old { + surface = s; + } + let mut limit_windows = surface.is_some(); + ui.checkbox(&mut limit_windows, "Limit Windows"); + if !limit_windows { + surface = None; + } else { + ui.indent("limit windows", |ui| { + let surface = surface.get_or_insert_default(); + ui.checkbox(&mut surface.tearing_requested, "Requests Tearing"); + }); + } + } + }); + match mode { + Mode::Never => TearingMode::Never, + Mode::Always => TearingMode::Always, + Mode::Fullscreen => TearingMode::Fullscreen { surface }, + } + }; + let ui = &mut *ui.row(); + grid_label(ui, "Tearing"); + let old = effective!(m, t).tearing_mode; + let v = render_settings(ui, old); + if v != old { + modify!(m, t).tearing_mode = v; + } + let diff = v != m.tearing_mode; + if diff { + ui.add_enabled_ui(false, |ui| { + render_settings(ui, m.tearing_mode); + }); + } + diff +} + +fn show_vrr(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + if let Some(info) = &m.monitor_info + && !info.vrr_capable + { + return false; + } + { + let ui = &mut *ui.row(); + grid_label(ui, "VRR Active"); + ui.label(effective!(m, t).vrr.to_string()); + } + let render_settings = |ui: &mut Ui, old: VrrMode| { + #[derive(Copy, Clone, PartialEq, Linearize)] + enum Mode { + Never, + Always, + Fullscreen, + } + fn name(mode: Mode) -> &'static str { + match mode { + Mode::Never => "Never", + Mode::Always => "Always", + Mode::Fullscreen => "Fullscreen", + } + } + let mut mode = match old { + VrrMode::Never => Mode::Never, + VrrMode::Always => Mode::Always, + VrrMode::Fullscreen { .. } => Mode::Fullscreen, + }; + let mut surface = None; + ui.vertical(|ui| { + ComboBox::from_id_salt("vrr mode") + .selected_text(name(mode)) + .show_ui(ui, |ui| { + for s in Mode::variants() { + ui.selectable_value(&mut mode, s, name(s)); + } + }); + if mode == Mode::Fullscreen { + if let VrrMode::Fullscreen { surface: s } = old { + surface = s; + } + let mut limit_windows = surface.is_some(); + ui.checkbox(&mut limit_windows, "Limit Windows"); + if !limit_windows { + surface = None; + } else { + ui.indent("limit windows", |ui| { + let surface = surface.get_or_insert_default(); + let mut limit_content_type = surface.content_type.is_some(); + ui.checkbox(&mut limit_content_type, "Limit Content Types"); + if !limit_content_type { + surface.content_type = None; + } else { + ui.indent("limit content type", |ui| { + let limit = surface.content_type.get_or_insert_default(); + let fields = [ + ("Photos", &mut limit.photo), + ("Videos", &mut limit.video), + ("Games", &mut limit.game), + ]; + for (name, field) in fields { + ui.checkbox(field, name); + } + }); + } + }); + } + } + }); + match mode { + Mode::Never => VrrMode::Never, + Mode::Always => VrrMode::Always, + Mode::Fullscreen => VrrMode::Fullscreen { surface }, + } + }; + let ui = &mut *ui.row(); + grid_label(ui, "VRR"); + let old = effective!(m, t).vrr_mode; + let v = render_settings(ui, old); + if v != old { + modify!(m, t).vrr_mode = v; + } + let diff = v != m.vrr_mode; + if diff { + ui.add_enabled_ui(false, |ui| { + render_settings(ui, m.vrr_mode); + }); + } + diff +} + +fn show_non_desktop(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + { + let ui = &mut *ui.row(); + grid_label(ui, "Non-desktop"); + if m.inherent_non_desktop { + ui.label("Yes"); + } else { + ui.label("No"); + } + } + + let ui = &mut *ui.row(); + grid_label(ui, "Override"); + let mut v = effective!(m, t).override_non_desktop; + let mut changed = false; + let name = |v: Option| match v { + None => "None", + Some(false) => "Desktop", + Some(true) => "Non-Desktop", + }; + ComboBox::from_id_salt("non-desktop-override") + .selected_text(name(v)) + .show_ui(ui, |ui| { + for s in [None, Some(false), Some(true)] { + changed |= ui.selectable_value(&mut v, s, name(s)).changed(); + } + }); + if changed { + let t = modify!(m, t); + t.override_non_desktop = v; + t.update_in_compositor_space(m.wl_output); + } + let diff = v != m.override_non_desktop; + if diff { + ui.label(name(m.override_non_desktop)); + } + diff +} + +fn show_blend_space(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + grid_label(ui, "Blend Space"); + let mut v = effective!(m, t).blend_space; + ui.horizontal(|ui| { + let mut changed = false; + ComboBox::from_id_salt("blend-space") + .selected_text(v.name()) + .show_ui(ui, |ui| { + for s in BlendSpace::variants() { + changed |= ui.selectable_value(&mut v, s, s.name()).changed(); + } + }); + if changed { + modify!(m, t).blend_space = v; + } + }); + let diff = m.blend_space != v; + if diff { + ui.label(m.blend_space.name()); + } + diff +} + +fn show_use_native_gamut(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let ui = &mut *ui.row(); + grid_label(ui, "Use Native Gamut"); + let mut use_native_gamut = effective!(m, t).use_native_gamut; + if Checkbox::without_text(&mut use_native_gamut) + .ui(ui) + .changed() + { + modify!(m, t).use_native_gamut = use_native_gamut; + } + let diff = m.use_native_gamut != use_native_gamut; + if diff { + let mut old = m.use_native_gamut; + ui.add_enabled(false, Checkbox::without_text(&mut old)); + } + diff +} + +fn show_native_gamut(ui: &mut Ui, m: &HeadState) { + let Some(info) = &m.monitor_info else { + return; + }; + let p = info.primaries; + let ui = &mut *ui.row(); + grid_label(ui, "Native Gamut"); + Grid::new("native gamut").show(ui, |ui| { + let fields = [ + ("red:", p.r), + ("green:", p.g), + ("blue:", p.b), + ("white:", p.wp), + ]; + for (name, field) in fields { + let ui = &mut *ui.row(); + ui.label(name); + ui.label(format!("{:.6}", field.0)); + ui.label(format!("{:.6}", field.1)); + } + }); +} + +fn show_cursor_hz(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { + if !effective!(m, t).in_compositor_space { + return false; + } + let old_cursor_hz = effective!(m, t).vrr_cursor_hz.is_some(); + let mut custom_cursor_hz = old_cursor_hz; + let mut changed = false; + grid_label(ui, "Limit Cursor HZ"); + Checkbox::without_text(&mut custom_cursor_hz).ui(ui); + changed |= old_cursor_hz != custom_cursor_hz; + let diff1 = m.vrr_cursor_hz.is_some() != custom_cursor_hz; + if diff1 { + ui.label(match m.vrr_cursor_hz.is_some() { + true => "enabled", + false => "disabled", + }); + } + ui.end_row(); + + if !custom_cursor_hz { + if changed { + modify!(m, t).vrr_cursor_hz = None; + } + return diff1; + } + + grid_label(ui, "Cursor HZ"); + let mut cursor_hz = effective!(m, t).vrr_cursor_hz.unwrap_or(60.0); + changed |= DragValue::new(&mut cursor_hz) + .range(0.0..=500.0) + .ui(ui) + .changed(); + if changed { + modify!(m, t).vrr_cursor_hz = Some(cursor_hz); + } + let mut diff2 = false; + if let Some(t) = t + && m.vrr_cursor_hz != t.vrr_cursor_hz + { + diff2 = true; + ui.label(format!( + "{}", + fmt::from_fn(|f| match m.vrr_cursor_hz { + None => f.write_str("disabled"), + Some(v) => write!(f, "{}", v), + }) + )); + } + ui.end_row(); + diff1 || diff2 +} + +fn show_flip_margin( + state: &State, + ui: &mut Ui, + m: &HeadState, + t: &mut Option, + connector_id: ConnectorId, +) { + if !effective!(m, t).in_compositor_space { + return; + } + let Some(node) = state.root.outputs.get(&connector_id) else { + return; + }; + let Some(margin) = node.flip_margin_ns.get() else { + return; + }; + label( + ui, + "Flip Margin (ms)", + format!("{}", margin as f64 / 1_000_000.0), + ); +} diff --git a/src/control_center/cc_sidebar.rs b/src/control_center/cc_sidebar.rs new file mode 100644 index 00000000..23f2116b --- /dev/null +++ b/src/control_center/cc_sidebar.rs @@ -0,0 +1,92 @@ +use { + crate::control_center::{ControlCenterInner, Pane, PaneType}, + egui::{Align, Layout, ScrollArea, Ui, ViewportCommand}, + egui_tiles::Tree, + linearize::{Linearize, LinearizeExt}, + std::{rc::Rc, sync::LazyLock}, +}; + +#[derive(Copy, Clone, Linearize)] +enum PaneName { + Compositor, + Idle, + ColorManagement, + Xwayland, + Outputs, + GPUs, + Input, + LookAndFeel, + Clients, + WindowSearch, +} + +impl PaneName { + fn name(self) -> &'static str { + match self { + PaneName::Compositor => "Compositor", + PaneName::Idle => "Idle", + PaneName::ColorManagement => "Color Management", + PaneName::Xwayland => "Xwayland", + PaneName::Outputs => "Outputs", + PaneName::GPUs => "GPUs", + PaneName::Input => "Input", + PaneName::LookAndFeel => "Look and Feel", + PaneName::Clients => "Clients", + PaneName::WindowSearch => "Window Search", + } + } +} + +static TYPES: LazyLock> = LazyLock::new(|| { + let mut res: Vec<_> = PaneName::variants().collect(); + res.sort_by_key(|t| t.name()); + res +}); + +impl ControlCenterInner { + pub fn show_sidebar(self: &Rc, tree: &mut Tree, ui: &mut Ui) { + ui.with_layout( + Layout::top_down(Align::Center).with_cross_justify(true), + |ui| { + ui.add_space(6.0); + if ui.button("Close").clicked() { + ui.ctx().send_viewport_cmd(ViewportCommand::Close); + } + ui.separator(); + ScrollArea::vertical().show(ui, |ui| { + for &ty in &*TYPES { + if ui.button(ty.name()).clicked() { + let ty = match ty { + PaneName::Compositor => { + PaneType::Compositor(self.create_compositor_pane()) + } + PaneName::Idle => PaneType::Idle(self.create_idle_pane()), + PaneName::ColorManagement => { + PaneType::ColorManagement(self.create_color_management_pane()) + } + PaneName::Xwayland => { + PaneType::Xwayland(self.create_xwayland_pane()) + } + PaneName::Outputs => { + PaneType::Outputs(Box::new(self.create_outputs_pane())) + } + PaneName::GPUs => PaneType::GPUs(self.create_gpus_pane()), + PaneName::Input => PaneType::Input(self.create_input_pane()), + PaneName::LookAndFeel => { + PaneType::LookAndFeel(self.create_look_and_feel_pane()) + } + PaneName::Clients => PaneType::Clients(self.create_clients_pane()), + PaneName::WindowSearch => { + PaneType::WindowSearch(self.create_window_search_pane()) + } + }; + self.open(tree, ty); + ui.ctx().request_repaint(); + } + } + ui.add_space(3.0); + }) + }, + ); + } +} diff --git a/src/control_center/cc_window.rs b/src/control_center/cc_window.rs new file mode 100644 index 00000000..80edf4c7 --- /dev/null +++ b/src/control_center/cc_window.rs @@ -0,0 +1,481 @@ +use { + crate::{ + control_center::{ + CcBehavior, ControlCenterInner, PaneType, + cc_clients::{ClientCrit, show_client_collapsible}, + cc_criterion::{CcCriterion, CritImpl, CritRegex}, + grid, icon_label, label, read_only_bool, + }, + criteria::{CritMgrExt, CritUpstreamNode, crit_leaf::CritLeafMatcher}, + egui_adapter::egui_platform::icons::ICON_OPEN_IN_NEW, + state::State, + tree::{NodeId, ToplevelData, ToplevelNode, ToplevelType}, + utils::{ + copyhashmap::CopyHashMap, + event_listener::{EventListener, LazyEventSourceListener}, + static_text::StaticText, + toplevel_identifier::ToplevelIdentifier, + }, + }, + ahash::AHashMap, + egui::{CollapsingHeader, Sense, TextFormat, Ui, Widget, cache::CacheTrait, text::LayoutJob}, + isnt::std_1::primitive::IsntStrExt, + jay_config::window::{ + ContentType, GAME_CONTENT, NO_CONTENT_TYPE, PHOTO_CONTENT, VIDEO_CONTENT, + }, + linearize::Linearize, + std::{ + any::Any, + mem, + rc::{Rc, Weak}, + }, +}; + +enum WindowClit { + Client(CcCriterion), + Title(CritRegex), + AppId(CritRegex), + Floating, + Visible, + Urgent, + Fullscreen, + Tag(CritRegex), + XClass(CritRegex), + XInstance(CritRegex), + XRole(CritRegex), + Workspace(CritRegex), + ContentTypes(ContentType), +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Linearize)] +enum WindowCritTy { + Client, + Title, + AppId, + Floating, + Visible, + Urgent, + Fullscreen, + Tag, + XClass, + XInstance, + XRole, + Workspace, + ContentTypes, +} + +impl Default for WindowClit { + fn default() -> Self { + WindowClit::Title(Default::default()) + } +} + +impl StaticText for WindowCritTy { + fn text(&self) -> &'static str { + match self { + WindowCritTy::Client => "Client", + WindowCritTy::Title => "Title", + WindowCritTy::AppId => "App ID", + WindowCritTy::Floating => "Floating", + WindowCritTy::Visible => "Visible", + WindowCritTy::Urgent => "Urgent", + WindowCritTy::Fullscreen => "Fullscreen", + WindowCritTy::Tag => "Tag", + WindowCritTy::XClass => "X Class", + WindowCritTy::XInstance => "X Instance", + WindowCritTy::XRole => "X Role", + WindowCritTy::Workspace => "Workspace", + WindowCritTy::ContentTypes => "Content Types", + } + } +} + +impl CritImpl for WindowClit { + type Type = WindowCritTy; + type Target = ToplevelData; + + fn ty(&self) -> Self::Type { + macro_rules! map { + ($($n:ident,)*) => { + match self { + $( + Self::$n { .. } => WindowCritTy::$n, + )* + } + }; + } + map! { + Client, + Title, + AppId, + Floating, + Visible, + Urgent, + Fullscreen, + Tag, + XClass, + XInstance, + XRole, + Workspace, + ContentTypes, + } + } + + fn from_ty(ty: Self::Type) -> Self { + match ty { + WindowCritTy::Client => Self::Client(Default::default()), + WindowCritTy::Title => Self::Title(Default::default()), + WindowCritTy::AppId => Self::AppId(Default::default()), + WindowCritTy::Floating => Self::Floating, + WindowCritTy::Visible => Self::Visible, + WindowCritTy::Urgent => Self::Urgent, + WindowCritTy::Fullscreen => Self::Fullscreen, + WindowCritTy::Tag => Self::Tag(Default::default()), + WindowCritTy::XClass => Self::XClass(Default::default()), + WindowCritTy::XInstance => Self::XInstance(Default::default()), + WindowCritTy::XRole => Self::XRole(Default::default()), + WindowCritTy::Workspace => Self::Workspace(Default::default()), + WindowCritTy::ContentTypes => { + Self::ContentTypes(PHOTO_CONTENT | VIDEO_CONTENT | GAME_CONTENT) + } + } + } + + fn show(&mut self, ui: &mut Ui) -> bool { + match self { + WindowClit::Client(v) => v.show(ui), + WindowClit::Title(v) => v.show(ui), + WindowClit::AppId(v) => v.show(ui), + WindowClit::Floating => false, + WindowClit::Visible => false, + WindowClit::Urgent => false, + WindowClit::Fullscreen => false, + WindowClit::Tag(v) => v.show(ui), + WindowClit::XClass(v) => v.show(ui), + WindowClit::XInstance(v) => v.show(ui), + WindowClit::XRole(v) => v.show(ui), + WindowClit::Workspace(v) => v.show(ui), + WindowClit::ContentTypes(v) => show_content_types(ui, v), + } + } + + fn to_crit(&self, state: &Rc) -> Option>> { + let m = &state.tl_matcher_manager; + let res = match self { + WindowClit::Client(v) => m.client(state, &v.to_crit(state)?), + WindowClit::Title(v) => m.title(v.to_crit()?), + WindowClit::AppId(v) => m.app_id(v.to_crit()?), + WindowClit::Floating => m.floating(), + WindowClit::Visible => m.visible(), + WindowClit::Urgent => m.urgent(), + WindowClit::Fullscreen => m.fullscreen(), + WindowClit::Tag(v) => m.tag(v.to_crit()?), + WindowClit::XClass(v) => m.class(v.to_crit()?), + WindowClit::XInstance(v) => m.instance(v.to_crit()?), + WindowClit::XRole(v) => m.role(v.to_crit()?), + WindowClit::Workspace(v) => m.workspace(v.to_crit()?), + WindowClit::ContentTypes(v) => m.content_type(*v), + }; + Some(res) + } + + fn not( + state: &State, + upstream: &Rc>, + ) -> Rc> { + state.tl_matcher_manager.not(upstream) + } + + fn list( + state: &State, + upstream: &[Rc>], + all: bool, + ) -> Rc> { + state.tl_matcher_manager.list(upstream, all) + } + + fn exactly( + state: &State, + n: usize, + upstream: &[Rc>], + ) -> Rc> { + state.tl_matcher_manager.exactly(upstream, n) + } +} + +pub struct WindowSearchPane { + state: Rc, + criterion: CcCriterion, + matched: Rc, + leaf: Option>>, +} + +struct Matched { + slf: Weak, + windows: CopyHashMap, +} + +impl Matched { + fn request_frame(&self) { + if let Some(slf) = self.slf.upgrade() { + slf.window.request_redraw(); + } + } +} + +impl ControlCenterInner { + pub fn create_window_search_pane(self: &Rc) -> WindowSearchPane { + let mut pane = WindowSearchPane { + state: self.state.clone(), + criterion: Default::default(), + matched: Rc::new(Matched { + slf: Rc::downgrade(self), + windows: Default::default(), + }), + leaf: Default::default(), + }; + pane.update_matcher(); + pane + } +} + +impl WindowSearchPane { + pub fn title(&self, res: &mut String) { + res.push_str("Window Search"); + } + + pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) { + let mut clear = false; + if self.criterion.show(ui) { + clear = self.update_matcher(); + } + ui.separator(); + let mut windows: Vec<_> = self.matched.windows.lock().keys().copied().collect(); + windows.sort(); + for id in windows { + let Some(window) = self.state.toplevels.get(&id).and_then(|v| v.upgrade()) else { + continue; + }; + show_window_collapsible(behavior, ui, &window); + } + if clear { + self.matched.windows.clear(); + } + } + + fn update_matcher(&mut self) -> bool { + let mut clear = false; + let state = &self.state; + if let Some(new) = self.criterion.to_crit(state) { + clear = true; + let matched = self.matched.clone(); + let leaf = state.tl_matcher_manager.leaf(&new, move |data| { + matched.windows.set(data, ()); + matched.request_frame(); + Box::new({ + let matched = matched.clone(); + move || { + matched.windows.remove(&data); + matched.request_frame(); + } + }) + }); + state.tl_matcher_manager.rematch_all(state); + if self.criterion.any(|c| matches!(c, WindowClit::Client(_))) { + state.cl_matcher_manager.rematch_all(state); + } + self.leaf = Some(leaf); + } + clear + } +} + +pub struct WindowPane { + window: Rc, +} + +impl ControlCenterInner { + pub fn create_window_pane(self: &Rc, window: &Rc) -> WindowPane { + WindowPane { + window: window.clone(), + } + } +} + +impl WindowPane { + pub fn title(&self, res: &mut String) { + res.push_str("Window"); + } + + pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) { + show_window(behavior, ui, &*self.window) + } +} + +pub fn show_window_collapsible( + behavior: &mut CcBehavior, + ui: &mut Ui, + window: &Rc, +) { + let data = window.tl_data(); + let mut layout_job = LayoutJob::default(); + layout_job.append( + "Window", + 0.0, + TextFormat { + color: ui.style().visuals.widgets.inactive.text_color(), + ..Default::default() + }, + ); + layout_job.append( + &data.title.borrow(), + 10.0, + TextFormat { + color: ui.style().visuals.widgets.active.text_color(), + ..Default::default() + }, + ); + let closed = CollapsingHeader::new(layout_job) + .id_salt(("window", data.identifier.get())) + .show(ui, |ui| { + if icon_label(ICON_OPEN_IN_NEW) + .sense(Sense::CLICK) + .ui(ui) + .clicked() + { + behavior.open = Some(PaneType::Window(behavior.cc.create_window_pane(window))); + } + show_window(behavior, ui, &**window) + }) + .fully_closed(); + if closed { + ensure_listener(ui, behavior, data); + } +} + +pub fn show_window(behavior: &mut CcBehavior<'_>, ui: &mut Ui, window: &dyn ToplevelNode) { + let data = window.tl_data(); + ensure_listener(ui, behavior, data); + grid(ui, ("window", data.identifier.get()), |ui| { + label(ui, "ID", &*data.identifier.get().to_string()); + label(ui, "Title", &*data.title.borrow()); + if let Some(w) = data.workspace.get() { + label(ui, "Workspace", &w.name); + } + match &data.kind { + ToplevelType::Container => { + label(ui, "Type", "Container"); + } + ToplevelType::Placeholder(_) => { + label(ui, "Type", "Placeholder"); + } + ToplevelType::XdgToplevel(t) => { + label(ui, "Type", "xdg_toplevel"); + let tag = &*t.tag.borrow(); + if tag.is_not_empty() { + label(ui, "Tag", tag); + } + } + ToplevelType::XWindow(t) => { + label(ui, "Type", "X Window"); + if let Some(class) = &*t.info.class.borrow() { + label(ui, "Class", class); + } + if let Some(instance) = &*t.info.instance.borrow() { + label(ui, "Instance", instance); + } + if let Some(role) = &*t.info.role.borrow() { + label(ui, "Role", role); + } + } + } + let app_id = &*data.app_id.borrow(); + if app_id.is_not_empty() { + label(ui, "App ID", app_id); + } + read_only_bool(ui, "Floating", data.parent_is_float.get()); + read_only_bool(ui, "Visible", data.visible.get()); + read_only_bool(ui, "Urgent", data.wants_attention.get()); + read_only_bool(ui, "Fullscreen", data.is_fullscreen.get()); + if let Some(ct) = data.content_type.get() { + label(ui, "Content Type", ct.text()); + } + }); + if let Some(client) = &data.client { + show_client_collapsible(behavior, ui, client); + } +} + +fn ensure_listener(ui: &mut Ui, behavior: &CcBehavior<'_>, data: &ToplevelData) { + ui.memory_mut(|m| { + m.caches + .cache::() + .ensure(behavior.cc, data); + }); +} + +#[derive(Default)] +struct WindowPropertyListeners { + generation: u64, + listeners: AHashMap, +} + +struct WindowPropertyListener { + _listener: EventListener, + generation: u64, +} + +impl WindowPropertyListeners { + fn ensure(&mut self, cc: &Rc, data: &ToplevelData) { + let listener = self.listeners.entry(data.node_id).or_insert_with(|| { + let listener = + EventListener::new(Rc::downgrade(cc) as Weak); + listener.attach(data.property_changed_source()); + WindowPropertyListener { + _listener: listener, + generation: 0, + } + }); + listener.generation = self.generation; + } +} + +unsafe impl Sync for WindowPropertyListeners {} +unsafe impl Send for WindowPropertyListeners {} + +impl CacheTrait for WindowPropertyListeners { + fn update(&mut self) { + self.listeners + .retain(|_, m| m.generation == self.generation); + self.generation += 1; + } + + fn len(&self) -> usize { + self.listeners.len() + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +fn show_content_types(ui: &mut Ui, ct: &mut ContentType) -> bool { + let mut v = *ct; + let mut photo = (v & PHOTO_CONTENT).0 != 0; + let mut video = (v & VIDEO_CONTENT).0 != 0; + let mut game = (v & GAME_CONTENT).0 != 0; + ui.checkbox(&mut photo, "Photo"); + ui.checkbox(&mut video, "Video"); + ui.checkbox(&mut game, "Game"); + v = NO_CONTENT_TYPE; + if photo { + v |= PHOTO_CONTENT; + } + if video { + v |= VIDEO_CONTENT; + } + if game { + v |= GAME_CONTENT; + } + mem::replace(ct, v) != v +} diff --git a/src/control_center/cc_xwayland.rs b/src/control_center/cc_xwayland.rs new file mode 100644 index 00000000..2adedeb8 --- /dev/null +++ b/src/control_center/cc_xwayland.rs @@ -0,0 +1,91 @@ +use { + crate::{ + compositor::DISPLAY, + control_center::{ + CcBehavior, ControlCenterInner, bool, cc_clients::show_client_collapsible, + combo_box_ui, grid, label, read_only_bool, tip, + }, + state::State, + utils::{errorfmt::ErrorFmt, oserror::OsError, static_text::StaticText}, + }, + egui::Ui, + linearize::Linearize, + std::rc::Rc, + uapi::c, +}; + +pub struct XwaylandPane { + state: Rc, +} + +impl ControlCenterInner { + pub fn create_xwayland_pane(self: &Rc) -> XwaylandPane { + XwaylandPane { + state: self.state.clone(), + } + } +} + +#[derive(Copy, Clone, PartialEq, Linearize)] +enum ScalingMode { + Default, + Downscaled, +} + +impl StaticText for ScalingMode { + fn text(&self) -> &'static str { + match self { + ScalingMode::Default => "default", + ScalingMode::Downscaled => "downscaled", + } + } +} + +impl XwaylandPane { + pub fn title(&self, res: &mut String) { + res.push_str("Xwayland"); + } + + pub fn show(&mut self, behavior: &mut CcBehavior<'_>, ui: &mut Ui) { + let s = &self.state; + grid(ui, "settings", |ui| { + bool(ui, "Enabled", s.xwayland.enabled.get(), |b| { + s.set_xwayland_enabled(b) + }); + let mode = match self.state.xwayland.use_wire_scale.get() { + true => ScalingMode::Downscaled, + false => ScalingMode::Default, + }; + combo_box_ui( + ui, + "Scaling Mode", + |ui| { + tip(ui, |ui| { + ui.label(r#"`downscaled` is known as "X applications scale themselves""#); + }); + }, + mode, + |v| { + self.state + .set_xwayland_use_wire_scale(v == ScalingMode::Downscaled); + }, + ); + if let Some(display) = self.state.xwayland.display.get() { + label(ui, DISPLAY, &*display); + } + read_only_bool(ui, "Running", self.state.xwayland.running.get()); + if let Some(client) = self.state.xwayland.client.get() { + label(ui, "PID", client.pid_info.pid.to_string()); + } + }); + if let Some(client) = self.state.xwayland.client.get() + && ui.button("Kill").clicked() + && let Err(e) = uapi::kill(client.pid_info.pid, c::SIGTERM) + { + log::error!("Could not kill Xwayland: {}", ErrorFmt(OsError::from(e))); + } + if let Some(client) = self.state.xwayland.client.get() { + show_client_collapsible(behavior, ui, &client); + } + } +} diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs index f9e1c595..7a3175cb 100644 --- a/src/criteria/clm.rs +++ b/src/criteria/clm.rs @@ -234,7 +234,6 @@ impl ClMatcherManager { self.root(ClmMatchTag::new(string)) } - #[expect(dead_code)] pub fn id(&self, id: ClientId) -> Rc { self.root(ClmMatchId(id)) } diff --git a/src/cursor_user.rs b/src/cursor_user.rs index f7ee78b3..e79beade 100644 --- a/src/cursor_user.rs +++ b/src/cursor_user.rs @@ -1,6 +1,7 @@ use { crate::{ backend::HardwareCursorUpdate, + control_center::CCI_INPUT, cursor::{Cursor, DEFAULT_CURSOR_SIZE, KnownCursor}, fixed::Fixed, gfx_api::{AcquireSync, ReleaseSync}, @@ -183,6 +184,7 @@ impl CursorUserGroup { self.remove_hardware_cursor(); self.state.cursor_user_group_hardware_cursor.take(); } + self.state.trigger_cci(CCI_INPUT); } pub fn hardware_cursor(&self) -> bool { @@ -195,10 +197,10 @@ impl CursorUserGroup { self.state.remove_cursor_size(old); self.state.add_cursor_size(size); self.reload_known_cursor(); + self.state.trigger_cci(CCI_INPUT); } } - #[expect(dead_code)] pub fn cursor_size(&self) -> u32 { self.size.get() } diff --git a/src/egui_adapter.rs b/src/egui_adapter.rs new file mode 100644 index 00000000..c9939c56 --- /dev/null +++ b/src/egui_adapter.rs @@ -0,0 +1,3 @@ +pub mod egui_oklch; +pub mod egui_platform; +mod egui_vulkan; diff --git a/src/egui_adapter/egui_oklch.rs b/src/egui_adapter/egui_oklch.rs new file mode 100644 index 00000000..dedbb4d3 --- /dev/null +++ b/src/egui_adapter/egui_oklch.rs @@ -0,0 +1,36 @@ +use { + crate::{ + cmm::cmm_eotf::Eotf, + theme::{Color, Oklab, Oklch}, + }, + egui::{Color32, Rgba}, +}; + +pub trait Color32Ext { + fn to_oklab(self) -> Oklab; + fn to_oklch(self) -> Oklch; +} + +impl Color32Ext for Color32 { + fn to_oklab(self) -> Oklab { + let [r, g, b, a] = self.to_array(); + Color::from_srgba_premultiplied(r, g, b, a).srgb_to_oklab() + } + + fn to_oklch(self) -> Oklch { + self.to_oklab().to_oklch() + } +} + +impl Into for Oklch { + fn into(self) -> Color32 { + self.to_oklab().into() + } +} + +impl Into for Oklab { + fn into(self) -> Color32 { + let [r, g, b, a] = self.to_srgb().to_array(Eotf::Linear); + Rgba::from_rgba_premultiplied(r, g, b, a).into() + } +} diff --git a/src/egui_adapter/egui_platform.rs b/src/egui_adapter/egui_platform.rs new file mode 100644 index 00000000..d6d32744 --- /dev/null +++ b/src/egui_adapter/egui_platform.rs @@ -0,0 +1,1445 @@ +use { + crate::{ + allocator::{Allocator, AllocatorError, BO_USE_RENDERING, BufferObject}, + async_engine::SpawnedFuture, + client::{Client, ClientCaps, ClientError}, + cursor::KnownCursor, + egui_adapter::egui_vulkan::{ + EGV_FORMAT, EgvContext, EgvError, EgvFramebuffer, EgvRenderer, + }, + fixed::Fixed, + fontconfig::match_font, + gfx_api::SyncFile, + globals::{GlobalName, Singleton}, + ifs::wl_seat::{ + BTN_EXTRA, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, BTN_SIDE, + wl_pointer::{self, HORIZONTAL_SCROLL, PendingScroll, VERTICAL_SCROLL}, + }, + object::Version, + scale::Scale, + security_context_acceptor::AcceptorMetadata, + state::State, + utils::{ + asyncevent::AsyncEvent, + buf::Buf, + clonecell::CloneCell, + copyhashmap::CopyHashMap, + double_buffered::DoubleBuffered, + errorfmt::ErrorFmt, + object_drop_queue::ObjectDropQueue, + oserror::OsError, + pipe::{Pipe, pipe}, + rc_eq::rc_eq, + }, + video::{dmabuf::DMA_BUF_SYNC_WRITE, drm::DrmError}, + wire::{ + WlSurfaceId, + wl_pointer::{Button, Enter, Leave, Motion}, + wp_fractional_scale_v1::PreferredScale, + }, + wl_usr::{ + UsrCon, UsrConOwner, + usr_ifs::{ + usr_jay_compositor::UsrJayCompositor, + usr_jay_sync_file_release::UsrJaySyncFileReleaseOwner, + usr_jay_sync_file_surface::UsrJaySyncFileSurface, + usr_wl_buffer::UsrWlBuffer, + usr_wl_callback::UsrWlCallbackOwner, + usr_wl_compositor::UsrWlCompositor, + usr_wl_data_device::UsrWlDataDevice, + usr_wl_data_device_manager::UsrWlDataDeviceManager, + usr_wl_data_source::{UsrWlDataSource, UsrWlDataSourceOwner}, + usr_wl_keyboard::{UsrWlKeyboard, UsrWlKeyboardOwner}, + usr_wl_pointer::{UsrWlPointer, UsrWlPointerOwner}, + usr_wl_registry::UsrWlRegistry, + usr_wl_seat::UsrWlSeat, + usr_wl_surface::UsrWlSurface, + usr_wp_cursor_shape_device_v1::UsrWpCursorShapeDeviceV1, + usr_wp_cursor_shape_manager_v1::UsrWpCursorShapeManagerV1, + usr_wp_fractional_scale::{UsrWpFractionalScale, UsrWpFractionalScaleOwner}, + usr_wp_fractional_scale_manager::UsrWpFractionalScaleManager, + usr_wp_viewport::UsrWpViewport, + usr_wp_viewporter::UsrWpViewporter, + usr_xdg_surface::{UsrXdgSurface, UsrXdgSurfaceOwner}, + usr_xdg_toplevel::{UsrXdgToplevel, UsrXdgToplevelOwner}, + usr_xdg_wm_base::UsrXdgWmBase, + usr_zwp_linux_dmabuf_v1::UsrZwpLinuxDmabufV1, + usr_zwp_primary_selection_device_manager::UsrZwpPrimarySelectionDeviceManagerV1, + }, + }, + }, + egui::{ + CursorIcon, Event, FontData, FontDefinitions, FontFamily, FullOutput, Key, Modifiers, + MouseWheelUnit, OutputCommand, PlatformOutput, PointerButton, Pos2, RawInput, Vec2, + ViewportCommand, ViewportEvent, ViewportId, ViewportInfo, pos2, vec2, + }, + futures_util::{FutureExt, select}, + isnt::std_1::primitive::{IsntCharExt, IsntSliceExt, IsntStrExt}, + kbvm::{Keysym, ModifierMask, lookup::Lookup}, + std::{ + cell::{Cell, RefCell}, + collections::btree_map::Entry, + fs, mem, + rc::{Rc, Weak}, + sync::Arc, + }, + thiserror::Error, + uapi::{OwnedFd, c}, +}; + +#[derive(Debug, Error)] +pub enum EggError { + #[error("Could not create a socket pair")] + CreateSocketPair(#[source] OsError), + #[error("Could not spawn a client")] + SpawnClient(#[source] ClientError), + #[error("Could not create a renderer")] + CreateRenderer(#[source] EgvError), + #[error("There is no render context")] + NoRenderContext, + #[error("Could not allocate a buffer")] + AllocateBuffer(#[source] AllocatorError), + #[error("Could not import a framebuffer")] + ImportFramebuffer(#[source] EgvError), + #[error("Could not render")] + Render(#[source] EgvError), + #[error("No viewport output")] + NoViewportOutput, + #[error("Could not export initial dmabuf sync file")] + ExportBoSyncFile(#[source] DrmError), +} + +pub mod icons { + pub const ICON_ADD: &str = "\u{e145}"; + pub const ICON_CLOSE: &str = "\u{e5cd}"; + pub const ICON_DRAG_INDICATOR: &str = "\u{e945}"; + pub const ICON_INFO: &str = "\u{e88e}"; + pub const ICON_OPEN_IN_NEW: &str = "\u{e89e}"; + pub const ICON_PENDING: &str = "\u{ef64}"; + pub const ICON_REMOVE: &str = "\u{e15b}"; +} + +linear_ids!(EggContextIds, EggContextId, u64); + +pub struct EggState { + fonts: RefCell, + ctx: CloneCell>>, + context_ids: EggContextIds, + cxts: CopyHashMap>, +} + +#[derive(Default)] +struct EggFonts { + definitions: Option, + proportional: Vec, + monospace: Vec, +} + +pub struct EggContext { + inner: Rc, +} + +struct EggContextInner { + id: EggContextId, + renderer: Rc, + allocator: Rc, + state: Rc, + _client: Rc, + con: Rc, + jay_compositor: Rc, + wl_compositor: Rc, + xdg_wm_base: Rc, + wl_data_device_manager: Rc, + _zwp_primary_selection_device_manager_v1: Rc, + wp_viewporter: Rc, + wp_cursor_shape_manager_v1: Rc, + wp_fractional_scale_manager: Rc, + zwp_linux_dmabuf_v1: Rc, + registry: Rc, + windows: CopyHashMap>, + seats: CopyHashMap, +} + +struct EggSeat { + inner: Rc, +} + +pub struct EggSeatInner { + ctx: Rc, + global_name: GlobalName, + wl_seat: Rc, + wl_pointer: Rc, + wl_data_device: Rc, + pointer_window: CloneCell>>, + pointer_enter_serial: Cell, + pointer_serial: Cell, + pointer_pos: Cell, + kb_modifiers: Cell, + wp_cursor_shape_device_v1: Rc, + wl_keyboard: Rc, + kb_window: CloneCell>>, + kb_serial: Cell, + serial: Cell, + wl_data_source: CloneCell>>, + copy_text: RefCell>, + copy_task: Cell>>, + paste_task: Cell>>, +} + +pub struct EggWindow { + _ctx: Rc, + inner: Rc, + _render_task: SpawnedFuture<()>, + _timer_task: SpawnedFuture<()>, +} + +pub trait EggWindowOwner { + fn close(&self); + fn render(self: Rc, ctx: &egui::Context); +} + +struct EggWindowInner { + ctx: Rc, + egv: Rc, + egui: egui::Context, + wl_surface: Rc, + wp_viewport: Rc, + wp_fractional_scale: Rc, + xdg_surface: Rc, + xdg_toplevel: Rc, + jay_sync_file_surface: Rc, + frame_task: AsyncEvent, + want_frame: Cell, + have_frame: Cell, + initial_commit_pending: Cell, + owner: CloneCell>>, + active_seat: CloneCell>>, + raw_input: RefCell>, + close: Cell, + repaint_timeout: Cell, + repaint_timeout_changed: AsyncEvent, + fonts_changed: Cell, + + buffers: DoubleBuffered>>>, + + surface_pending: RefCell, + logical_size: Cell<[i32; 2]>, + physical_size: Cell<[i32; 2]>, + scale: Cell, +} + +struct EggFramebuffer { + client_acquire_fence: CloneCell>>, + size: Cell<[i32; 2]>, + bo: Rc, + egv: Rc, + wl_buffer: Rc, + window: Weak, + drop_queue: Rc>>, +} + +#[derive(Default)] +struct PendingWindowState { + size: Option<(i32, i32)>, +} + +const PROPORTIONAL_FONTS: &[&str] = &["sans-serif", "Noto Sans", "Noto Color Emoji"]; + +const MONOSPACE_FONTS: &[&str] = &["monospace", "Noto Sans Mono", "Noto Color Emoji"]; + +impl Default for EggState { + fn default() -> Self { + let slf = Self { + fonts: Default::default(), + ctx: Default::default(), + context_ids: Default::default(), + cxts: Default::default(), + }; + slf.reset_fonts(); + slf + } +} + +impl EggState { + pub fn reset_fonts(&self) { + self.set_proportional_fonts(PROPORTIONAL_FONTS); + self.set_monospace_fonts(MONOSPACE_FONTS); + } + + pub fn set_proportional_fonts(&self, fonts: &[&str]) { + self.change_fonts(fonts, |f| &mut f.proportional) + } + + pub fn set_monospace_fonts(&self, fonts: &[&str]) { + self.change_fonts(fonts, |f| &mut f.monospace) + } + + fn change_fonts(&self, fonts: &[&str], field: impl Fn(&mut EggFonts) -> &mut Vec) { + let f = &mut *self.fonts.borrow_mut(); + let field = field(f); + if *field == fonts { + return; + } + *field = fonts.iter().map(|s| s.to_string()).collect(); + f.definitions.take(); + for ctx in self.cxts.lock().values() { + for window in ctx.windows.lock().values() { + window.fonts_changed.set(true); + window.want_frame(); + } + } + } + + pub fn clear(&self) { + self.ctx.take(); + } + + fn font_definitions(&self) -> FontDefinitions { + let f = &mut self.fonts.borrow_mut(); + if let Some(d) = &f.definitions { + return d.clone(); + } + let mut d = FontDefinitions::empty(); + for (ff, list) in [ + (FontFamily::Proportional, &f.proportional), + (FontFamily::Monospace, &f.monospace), + ] { + for family in list { + let font = match match_font(family) { + Ok(f) => f, + Err(e) => { + log::warn!("Could not find font family {family}: {}", ErrorFmt(e)); + continue; + } + }; + if let Entry::Vacant(e) = d.font_data.entry(font.fullname.clone()) { + let data = match fs::read(&font.file) { + Ok(f) => f, + Err(e) => { + log::error!("Could not read {}: {}", font.file.display(), ErrorFmt(e)); + continue; + } + }; + let data = Arc::new(FontData { + font: data.into(), + index: font.index.unwrap_or(0) as u32, + tweak: Default::default(), + }); + e.insert(data); + } + let list = d.families.entry(ff.clone()).or_default(); + if list.not_contains(&font.fullname) { + list.push(font.fullname); + } + } + } + { + let name = "material-icons"; + let list = d.families.entry(FontFamily::Proportional).or_default(); + if list.iter().all(|n| n != name) { + if let Entry::Vacant(e) = d.font_data.entry(name.to_string()) { + let data = Arc::new(FontData { + font: include_bytes!("icons.ttf").into(), + index: 0, + tweak: Default::default(), + }); + e.insert(data); + } + list.push(name.to_string()); + } + } + f.definitions = Some(d.clone()); + d + } +} + +impl State { + pub fn get_egg_context(self: &Rc) -> Result, EggError> { + if let Some(ctx) = self.egg_state.ctx.get() { + return Ok(ctx); + } + let Some(ctx) = self.render_ctx.get() else { + return Err(EggError::NoRenderContext); + }; + let (client1, client2) = uapi::socketpair(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0) + .map_err(Into::into) + .map_err(EggError::CreateSocketPair)?; + let allocator = ctx.allocator(); + let dev = allocator.drm().map(|d| d.dev()); + let renderer = EgvRenderer::new(&self.eng, &self.ring, &self.eventfd_cache, dev) + .map_err(EggError::CreateRenderer)?; + let con = UsrCon::from_socket( + &self.ring, + &self.wheel, + &self.eng, + &self.dma_buf_ids, + &Rc::new(client1), + 0, + ); + let client = self + .clients + .spawn2( + self.clients.id(), + self, + Rc::new(client2), + uapi::getuid(), + uapi::getpid(), + ClientCaps::all(), + true, + false, + &Rc::new(AcceptorMetadata::secure()), + ) + .map_err(EggError::SpawnClient)?; + let registry = con.get_registry(); + let jay_compositor = { + let obj = Rc::new(UsrJayCompositor { + id: con.id(), + con: con.clone(), + owner: Default::default(), + caps: Default::default(), + version: Version(27), + }); + registry.bind(self.globals.singletons[Singleton::JayCompositor], &*obj); + con.add_object(obj.clone()); + obj + }; + macro_rules! add_singletons { + ($($name:ident, $global:ident, $ty:ident, $version:expr;)*) => { + $( + let $name = Rc::new($ty { + id: con.id(), + con: con.clone(), + version: Version($version), + }); + registry.bind(self.globals.singletons[Singleton::$global], &*$name); + con.add_object($name.clone()); + )* + }; + } + add_singletons! { + wl_compositor, WlCompositor, UsrWlCompositor, 6; + xdg_wm_base, XdgWmBase, UsrXdgWmBase, 7; + wl_data_device_manager, WlDataDeviceManager, UsrWlDataDeviceManager, 3; + zwp_primary_selection_device_manager_v1, ZwpPrimarySelectionDeviceManagerV1, UsrZwpPrimarySelectionDeviceManagerV1, 1; + wp_viewporter, WpViewporter, UsrWpViewporter, 1; + wp_cursor_shape_manager_v1, WpCursorShapeManagerV1, UsrWpCursorShapeManagerV1, 2; + wp_fractional_scale_manager, WpFractionalScaleManagerV1, UsrWpFractionalScaleManager, 1; + zwp_linux_dmabuf_v1, ZwpLinuxDmabufV1, UsrZwpLinuxDmabufV1, 5; + } + let ctx = Rc::new(EggContext { + inner: Rc::new(EggContextInner { + id: self.egg_state.context_ids.next(), + renderer, + allocator, + state: self.clone(), + _client: client.clone(), + con, + jay_compositor, + wl_compositor, + xdg_wm_base, + wl_data_device_manager, + _zwp_primary_selection_device_manager_v1: zwp_primary_selection_device_manager_v1, + wp_viewporter, + wp_cursor_shape_manager_v1, + wp_fractional_scale_manager, + zwp_linux_dmabuf_v1, + registry, + windows: Default::default(), + seats: Default::default(), + }), + }); + ctx.inner.con.owner.set(Some(ctx.inner.clone())); + self.egg_state.cxts.set(ctx.inner.id, ctx.inner.clone()); + for &global_name in self.globals.seats.lock().keys() { + ctx.inner.add_seat(global_name); + } + self.egg_state.ctx.set(Some(ctx.clone())); + Ok(ctx) + } +} + +impl EggContext { + pub fn create_window(self: &Rc, title: &str) -> Rc { + let i = &self.inner; + let wl_surface = i.wl_compositor.create_surface(); + let jay_sync_file_surface = i.jay_compositor.get_sync_file_surface(&wl_surface); + let xdg_surface = i.xdg_wm_base.get_xdg_surface(&wl_surface); + let xdg_toplevel = xdg_surface.get_toplevel(); + xdg_toplevel.set_title(title); + let wp_fractional_scale = i + .wp_fractional_scale_manager + .get_fractional_scale(&wl_surface); + let wp_viewport = i.wp_viewporter.get_viewport(&wl_surface); + wl_surface.commit(); + let window = Rc::new(EggWindowInner { + ctx: self.inner.clone(), + egv: i.renderer.create_context(), + egui: egui::Context::default(), + wl_surface, + wp_viewport, + wp_fractional_scale, + xdg_surface, + xdg_toplevel, + jay_sync_file_surface, + frame_task: Default::default(), + want_frame: Default::default(), + have_frame: Cell::new(true), + initial_commit_pending: Cell::new(true), + owner: Default::default(), + active_seat: Default::default(), + raw_input: RefCell::new(None), + close: Default::default(), + repaint_timeout: Cell::new(u64::MAX), + repaint_timeout_changed: Default::default(), + fonts_changed: Cell::new(true), + buffers: Default::default(), + surface_pending: Default::default(), + logical_size: Cell::new([800, 600]), + physical_size: Cell::new([800, 600]), + scale: Default::default(), + }); + window + .egui + .all_styles_mut(|s| s.spacing.item_spacing.y = 5.0); + window.xdg_surface.owner.set(Some(window.clone())); + window.xdg_toplevel.owner.set(Some(window.clone())); + window.wp_fractional_scale.owner.set(Some(window.clone())); + i.windows.set(window.wl_surface.id, window.clone()); + let eng = &i.state.eng; + let render_task = eng.spawn("egui-render", window.clone().render_frames()); + let timer_task = eng.spawn("egui-timer", window.clone().handle_timer()); + let window = EggWindow { + _ctx: self.clone(), + inner: window, + _render_task: render_task, + _timer_task: timer_task, + }; + Rc::new(window) + } +} + +impl EggContextInner { + pub fn add_seat(self: &Rc, global_name: GlobalName) { + let wl_seat = Rc::new(UsrWlSeat { + id: self.con.id(), + con: self.con.clone(), + owner: Default::default(), + version: Version(10), + }); + self.registry.bind(global_name, &*wl_seat); + self.con.add_object(wl_seat.clone()); + let wl_pointer = wl_seat.get_pointer(); + let wl_keyboard = wl_seat.get_keyboard(); + let wp_cursor_shape_device_v1 = self.wp_cursor_shape_manager_v1.get_pointer(&wl_pointer); + let wl_data_device = self.wl_data_device_manager.get_data_device(&wl_seat); + let seat = Rc::new(EggSeatInner { + ctx: self.clone(), + global_name, + wl_seat, + wl_pointer, + wl_data_device, + pointer_window: Default::default(), + pointer_enter_serial: Default::default(), + pointer_serial: Default::default(), + pointer_pos: Default::default(), + kb_modifiers: Default::default(), + wp_cursor_shape_device_v1, + wl_keyboard, + kb_window: Default::default(), + kb_serial: Default::default(), + serial: Default::default(), + wl_data_source: Default::default(), + copy_text: Default::default(), + copy_task: Default::default(), + paste_task: Default::default(), + }); + seat.wl_pointer.owner.set(Some(seat.clone())); + seat.wl_keyboard.owner.set(Some(seat.clone())); + let seat = EggSeat { inner: seat }; + self.seats.set(global_name, seat); + } +} + +const TEXT_PLAIN: &str = "text/plain;charset=utf-8"; + +impl EggSeatInner { + pub fn request_paste(self: &Rc) { + let Some(offer) = self.wl_data_device.selection.get() else { + return; + }; + if !offer.mime_types.borrow().contains(TEXT_PLAIN) { + return; + } + let Some(window) = self.kb_window.get() else { + return; + }; + let Pipe { read, write } = match pipe() { + Ok(p) => p.map_read(Rc::new).map_write(Rc::new), + Err(e) => { + log::error!("Could not create pipe: {}", ErrorFmt(e)); + return; + } + }; + offer.receive(TEXT_PLAIN, &write); + let window = Rc::downgrade(&window); + let ring = self.ctx.state.ring.clone(); + let mut buf = Buf::new(1024); + let mut out = Vec::new(); + let task = self.ctx.state.eng.spawn("egui-paste", async move { + loop { + let n = match ring.read(&read, buf.clone()).await { + Ok(n) => n, + Err(e) => { + log::error!("Could not read from peer: {}", ErrorFmt(e)); + return; + } + }; + if n == 0 { + if let Some(window) = window.upgrade() { + let s = match String::from_utf8(out) { + Ok(s) => s, + Err(e) => { + log::error!("Peer did not send UTF-8: {}", ErrorFmt(e)); + return; + } + }; + window.event(Event::Paste(s)); + } + return; + } + out.extend_from_slice(&buf[..n]); + if out.len() >= 1024 * buf.len() { + log::error!("Paste buffer is too large"); + return; + } + } + }); + self.paste_task.set(Some(task)); + } +} + +impl EggWindow { + pub fn request_redraw(&self) { + self.inner.want_frame(); + } + + pub fn set_owner(&self, owner: Option>) { + self.inner.owner.set(owner); + } +} + +impl EggWindowInner { + fn update_physical_size(&self) { + let size = self.logical_size.get(); + let scale = self.scale.get(); + let physical_size = scale.pixel_size(size); + if self.physical_size.replace(physical_size) != physical_size { + self.want_frame(); + } + } + + fn want_frame(&self) { + self.want_frame.set(true); + self.maybe_trigger_frame(); + } + + fn maybe_trigger_frame(&self) { + if self.want_frame.get() && self.have_frame.get() { + self.frame_task.trigger(); + } + } + + async fn handle_timer(self: Rc) { + loop { + let timeout = self.ctx.state.ring.timeout(self.repaint_timeout.get()); + let triggered = || self.repaint_timeout_changed.triggered(); + let timeout = select! { + _ = timeout.fuse() => true, + _ = triggered().fuse() => false, + }; + if timeout { + self.want_frame(); + triggered().await; + } + } + } + + async fn render_frames(self: Rc) { + loop { + self.frame_task.triggered().await; + if let Err(e) = self.render_frame() { + log::error!("Could not render frame: {}", ErrorFmt(e)); + break; + } + } + } + + fn render_frame(self: &Rc) -> Result<(), EggError> { + if self.fonts_changed.take() { + self.egui + .set_fonts(self.ctx.state.egg_state.font_definitions()); + } + if self.initial_commit_pending.get() { + return Ok(()); + } + if !self.have_frame.get() { + return Ok(()); + } + if !self.want_frame.get() { + return Ok(()); + } + let Some(owner) = self.owner.get() else { + return Ok(()); + }; + let Some(render_ctx) = self.ctx.state.render_ctx.get() else { + return Ok(()); + }; + let Some(format) = render_ctx.formats().get(&EGV_FORMAT.drm) else { + return Ok(()); + }; + let logical_size = self.logical_size.get(); + let physical_size = self.physical_size.get(); + let mut fb_opt = self.buffers.back().get(); + 'check: { + if let Some(fb) = &fb_opt { + if fb.size.get() != physical_size { + fb_opt = None; + break 'check; + } + if !format.read_modifiers.contains(&fb.bo.dmabuf().modifier) { + fb_opt = None; + break 'check; + } + } + } + let fb = match fb_opt { + Some(fb) => fb, + _ => { + let modifiers: Vec<_> = self + .ctx + .renderer + .support() + .iter() + .filter(|s| { + s.max_width >= physical_size[0] as u32 + && s.max_height >= physical_size[1] as u32 + && format.read_modifiers.contains(&s.modifier) + }) + .map(|s| s.modifier) + .collect(); + let bo = self + .ctx + .allocator + .create_bo( + &self.ctx.state.dma_buf_ids, + physical_size[0], + physical_size[1], + EGV_FORMAT, + &modifiers, + BO_USE_RENDERING, + ) + .map_err(EggError::AllocateBuffer)?; + let egv = self + .egv + .import_framebuffer(&bo) + .map_err(EggError::ImportFramebuffer)?; + let dmabuf = bo.dmabuf(); + let sync_file = dmabuf + .export_sync_file(DMA_BUF_SYNC_WRITE) + .map_err(EggError::ExportBoSyncFile)?; + let wl_buffer = self.ctx.zwp_linux_dmabuf_v1.create_buffer(dmabuf); + let fb = Rc::new(EggFramebuffer { + client_acquire_fence: CloneCell::new(Some(sync_file)), + size: Cell::new(physical_size), + bo, + egv, + wl_buffer, + window: Rc::downgrade(self), + drop_queue: self.ctx.state.bo_drop_queue.clone(), + }); + self.buffers.back().set(Some(fb.clone())); + fb + } + }; + let Some(sync_file) = fb.client_acquire_fence.get() else { + return Ok(()); + }; + let raw_input = self + .raw_input + .take() + .unwrap_or_else(|| self.default_raw_input()); + let full_output = self.egui.run(raw_input, |ctx| { + owner.clone().render(ctx); + }); + let FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; + let primitives = self.egui.tessellate(shapes, pixels_per_point); + let sync = fb + .egv + .render( + textures_delta, + pixels_per_point, + &primitives, + (0.0, 0.0), + sync_file.as_ref(), + ) + .map_err(EggError::Render)?; + let PlatformOutput { + commands, + cursor_icon, + .. + } = platform_output; + if let Some(seat) = self.active_seat.get() { + 'set_icon: { + let cursor = match cursor_icon { + CursorIcon::None => { + seat.wl_pointer + .set_cursor(seat.pointer_serial.get(), None, 0, 0); + break 'set_icon; + } + CursorIcon::Default => KnownCursor::Default, + CursorIcon::ContextMenu => KnownCursor::ContextMenu, + CursorIcon::Help => KnownCursor::Help, + CursorIcon::PointingHand => KnownCursor::Pointer, + CursorIcon::Progress => KnownCursor::Progress, + CursorIcon::Wait => KnownCursor::Wait, + CursorIcon::Cell => KnownCursor::Cell, + CursorIcon::Crosshair => KnownCursor::Crosshair, + CursorIcon::Text => KnownCursor::Text, + CursorIcon::VerticalText => KnownCursor::VerticalText, + CursorIcon::Alias => KnownCursor::Alias, + CursorIcon::Copy => KnownCursor::Copy, + CursorIcon::Move => KnownCursor::Move, + CursorIcon::NoDrop => KnownCursor::NoDrop, + CursorIcon::NotAllowed => KnownCursor::NotAllowed, + CursorIcon::Grab => KnownCursor::Grab, + CursorIcon::Grabbing => KnownCursor::Grabbing, + CursorIcon::AllScroll => KnownCursor::AllScroll, + CursorIcon::ResizeHorizontal => KnownCursor::EwResize, + CursorIcon::ResizeNeSw => KnownCursor::NeswResize, + CursorIcon::ResizeNwSe => KnownCursor::NwseResize, + CursorIcon::ResizeVertical => KnownCursor::NsResize, + CursorIcon::ResizeEast => KnownCursor::EResize, + CursorIcon::ResizeSouthEast => KnownCursor::SeResize, + CursorIcon::ResizeSouth => KnownCursor::SResize, + CursorIcon::ResizeSouthWest => KnownCursor::SwResize, + CursorIcon::ResizeWest => KnownCursor::WResize, + CursorIcon::ResizeNorthWest => KnownCursor::NwResize, + CursorIcon::ResizeNorth => KnownCursor::NResize, + CursorIcon::ResizeNorthEast => KnownCursor::NeResize, + CursorIcon::ResizeColumn => KnownCursor::ColResize, + CursorIcon::ResizeRow => KnownCursor::RowResize, + CursorIcon::ZoomIn => KnownCursor::ZoomIn, + CursorIcon::ZoomOut => KnownCursor::ZoomOut, + }; + seat.wp_cursor_shape_device_v1 + .set_shape(seat.pointer_serial.get(), cursor); + } + } + for command in commands { + match command { + OutputCommand::CopyText(t) => { + if let Some(seat) = self.active_seat.get() { + let data_src = self.ctx.wl_data_device_manager.create_data_source(); + data_src.offer(TEXT_PLAIN); + data_src.owner.set(Some(seat.clone())); + seat.wl_data_device + .set_selection(seat.serial.get(), &data_src); + if let Some(old) = seat.wl_data_source.set(Some(data_src)) { + old.con.remove_obj(&*old); + } + seat.copy_text.replace(Some(Buf::from_slice(t.as_bytes()))); + } + } + OutputCommand::CopyImage(_) => {} + OutputCommand::OpenUrl(url) => { + if let Some(forker) = self.ctx.state.forker.get() { + forker.spawn("xdg-open".to_string(), vec![url.url], vec![], vec![]); + } + } + } + } + let Some(viewport) = viewport_output.get(&ViewportId::ROOT) else { + return Err(EggError::NoViewportOutput); + }; + for command in &viewport.commands { + match command { + ViewportCommand::Close => self.close.set(true), + ViewportCommand::CancelClose => self.close.set(false), + ViewportCommand::Title(s) => self.xdg_toplevel.set_title(s), + ViewportCommand::Fullscreen(b) => self.xdg_toplevel.set_fullscreen(*b), + ViewportCommand::RequestPaste => { + if let Some(seat) = self.active_seat.get() { + seat.request_paste(); + } + } + _ => {} + } + } + let repaint_delay = u64::try_from(viewport.repaint_delay.as_nanos()).unwrap_or(u64::MAX); + let repaint_timeout = self.ctx.state.now_nsec().saturating_add(repaint_delay); + self.repaint_timeout.set(repaint_timeout); + if repaint_timeout != u64::MAX { + self.repaint_timeout_changed.trigger(); + } + self.wl_surface.attach(&fb.wl_buffer); + self.wl_surface.damage(); + self.jay_sync_file_surface.set_acquire(sync.as_ref()); + self.jay_sync_file_surface + .get_release() + .owner + .set(Some(fb.clone())); + self.wl_surface.frame().owner.set(Some(self.clone())); + self.wp_viewport + .set_destination(logical_size[0], logical_size[1]); + self.wl_surface.commit(); + fb.client_acquire_fence.take(); + self.buffers.flip(); + self.have_frame.set(false); + self.want_frame.set(false); + if self.close.get() { + owner.close(); + } + Ok(()) + } +} + +impl UsrXdgSurfaceOwner for EggWindowInner { + fn configure(&self) { + let pending = mem::take(&mut *self.surface_pending.borrow_mut()); + if let Some((mut w, mut h)) = pending.size { + let [old_w, old_h] = self.logical_size.get(); + w = if w > 0 { w } else { old_w }; + h = if h > 0 { h } else { old_h }; + let size = [w, h]; + if self.logical_size.replace(size) != size { + self.update_physical_size(); + } + } + if self.initial_commit_pending.take() { + self.want_frame(); + } + } +} + +impl UsrXdgToplevelOwner for EggWindowInner { + fn configure(&self, width: i32, height: i32) { + self.surface_pending.borrow_mut().size = Some((width, height)); + } + + fn close(&self) { + let raw_input = &mut *self.raw_input.borrow_mut(); + let raw_input = raw_input.get_or_insert_with(|| self.default_raw_input()); + raw_input + .viewports + .get_mut(&ViewportId::ROOT) + .unwrap() + .events + .push(ViewportEvent::Close); + self.close.set(true); + self.want_frame(); + } +} + +impl UsrWpFractionalScaleOwner for EggWindowInner { + fn preferred_scale(self: Rc, ev: &PreferredScale) { + let scale = Scale::from_wl(ev.scale); + if self.scale.replace(scale) != scale { + self.update_physical_size(); + } + } +} + +impl EggWindowInner { + fn event(&self, event: Event) { + let raw_input = &mut *self.raw_input.borrow_mut(); + let raw_input = raw_input.get_or_insert_with(|| self.default_raw_input()); + raw_input.events.push(event); + self.want_frame(); + } + + fn default_raw_input(&self) -> RawInput { + let viewport_info = ViewportInfo { + native_pixels_per_point: Some(self.scale.get().to_f64() as _), + ..Default::default() + }; + let size = self.logical_size.get(); + let size = + egui::Rect::from_min_size(Pos2::default(), Vec2::new(size[0] as f32, size[1] as f32)); + let mut modifiers = Modifiers::default(); + if let Some(seat) = self.active_seat.get() { + modifiers = seat.kb_modifiers.get(); + } + RawInput { + viewport_id: ViewportId::ROOT, + viewports: std::iter::once((ViewportId::ROOT, viewport_info)).collect(), + screen_rect: Some(size), + max_texture_side: Some(self.ctx.renderer.max_texture_side()), + time: Some(self.ctx.state.now_nsec() as f64 / 1_000_000_000.0), + modifiers, + ..Default::default() + } + } +} + +impl EggSeatInner { + fn activate_pointer_window(self: &Rc) -> Option> { + let window = self.pointer_window.get()?; + window.active_seat.set(Some(self.clone())); + Some(window) + } + + fn activate_kb_window(self: &Rc) -> Option> { + let window = self.kb_window.get()?; + window.active_seat.set(Some(self.clone())); + Some(window) + } + + fn leave(self: &Rc) { + if let Some(window) = self.pointer_window.take() + && let Some(active_seat) = window.active_seat.get() + && rc_eq(&active_seat, &self) + { + window.active_seat.take(); + } + } + + fn unfocus(self: &Rc) { + if let Some(window) = self.kb_window.take() + && let Some(active_seat) = window.active_seat.get() + && rc_eq(&active_seat, &self) + { + window.active_seat.take(); + } + } + + fn motion(self: &Rc, surface_x: Fixed, surface_y: Fixed) { + let Some(window) = self.activate_pointer_window() else { + return; + }; + let pos = pos2(surface_x.to_f32(), surface_y.to_f32()); + self.pointer_pos.set(pos); + window.event(Event::PointerMoved(pos)); + } +} + +impl UsrWlPointerOwner for EggSeatInner { + fn enter(self: Rc, ev: &Enter) { + let Some(window) = self.ctx.windows.get(&ev.surface) else { + return; + }; + self.pointer_window.set(Some(window.clone())); + self.pointer_enter_serial.set(ev.serial); + self.pointer_serial.set(ev.serial); + self.serial.set(ev.serial); + (&self).motion(ev.surface_x, ev.surface_y); + } + + fn leave(self: Rc, _ev: &Leave) { + (&self).leave(); + } + + fn motion(self: Rc, ev: &Motion) { + (&self).motion(ev.surface_x, ev.surface_y); + } + + fn button(self: Rc, ev: &Button) { + let Some(window) = self.activate_pointer_window() else { + return; + }; + self.pointer_serial.set(ev.serial); + self.serial.set(ev.serial); + let button = match ev.button { + BTN_LEFT => PointerButton::Primary, + BTN_RIGHT => PointerButton::Secondary, + BTN_MIDDLE => PointerButton::Middle, + BTN_SIDE => PointerButton::Extra1, + BTN_EXTRA => PointerButton::Extra2, + _ => return, + }; + window.event(Event::PointerButton { + pos: self.pointer_pos.get(), + button, + pressed: ev.state == wl_pointer::PRESSED, + modifiers: self.kb_modifiers.get(), + }); + } + + fn scroll(self: Rc, ps: &PendingScroll) { + let Some(window) = self.activate_pointer_window() else { + return; + }; + let v120_x = ps.v120[HORIZONTAL_SCROLL].get(); + let v120_y = ps.v120[VERTICAL_SCROLL].get(); + let px_x = ps.px[HORIZONTAL_SCROLL].get(); + let px_y = ps.px[VERTICAL_SCROLL].get(); + let unit; + let delta; + if v120_x.is_some() || v120_y.is_some() { + unit = MouseWheelUnit::Line; + delta = vec2( + -v120_x.unwrap_or_default() as f32 / 120.0, + -v120_y.unwrap_or_default() as f32 / 120.0, + ); + } else if px_x.is_some() || px_y.is_some() { + unit = MouseWheelUnit::Point; + delta = vec2( + -px_x.unwrap_or_default().to_f32(), + -px_y.unwrap_or_default().to_f32(), + ); + } else { + return; + } + window.event(Event::MouseWheel { + unit, + delta, + modifiers: self.kb_modifiers.get(), + }); + } +} + +impl EggSeatInner { + fn handle_key(self: &Rc, lookup: Lookup<'_>, serial: u32, down: bool) { + let Some(window) = self.activate_kb_window() else { + return; + }; + self.kb_serial.set(serial); + self.serial.set(serial); + if down { + let mut text = String::new(); + for key in lookup { + if let Some(c) = key.char() + && c.is_not_control() + { + text.push(c); + } + } + if text.is_not_empty() { + window.event(Event::Text(text)); + } + } + for key in lookup { + let mut modifiers = map_mods(lookup.remaining_mods()); + let Some(key) = map_key(key.keysym(), &mut modifiers) else { + continue; + }; + if down && modifiers.ctrl { + match key { + Key::V => self.request_paste(), + Key::C => window.event(Event::Copy), + Key::X => window.event(Event::Cut), + _ => {} + } + } + window.event(Event::Key { + key, + physical_key: None, + pressed: down, + repeat: false, + modifiers, + }); + } + } +} + +impl UsrWlKeyboardOwner for EggSeatInner { + fn focus(self: Rc, surface: WlSurfaceId, serial: u32) { + let Some(window) = self.ctx.windows.get(&surface) else { + return; + }; + self.kb_window.set(Some(window.clone())); + self.kb_serial.set(serial); + self.serial.set(serial); + window.active_seat.set(Some(self.clone())); + } + + fn unfocus(self: Rc) { + (&self).unfocus(); + } + + fn modifiers(self: Rc, mods: ModifierMask) { + self.kb_modifiers.set(map_mods(mods)); + } + + fn down(self: Rc, lookup: Lookup<'_>, serial: u32) { + self.handle_key(lookup, serial, true); + } + + fn repeat(self: Rc, lookup: Lookup<'_>, serial: u32) { + self.handle_key(lookup, serial, true); + } + + fn up(self: Rc, lookup: Lookup<'_>, serial: u32) { + self.handle_key(lookup, serial, false); + } +} + +fn map_mods(mods: ModifierMask) -> Modifiers { + Modifiers { + alt: mods.contains(ModifierMask::ALT), + ctrl: mods.contains(ModifierMask::CONTROL), + shift: mods.contains(ModifierMask::SHIFT), + mac_cmd: false, + command: mods.contains(ModifierMask::CONTROL), + } +} + +impl UsrJaySyncFileReleaseOwner for EggFramebuffer { + fn release(&self, sync_file: Option) { + self.client_acquire_fence.set(Some(sync_file)); + if let Some(window) = self.window.upgrade() { + window.maybe_trigger_frame(); + } + } +} + +impl UsrWlCallbackOwner for EggWindowInner { + fn done(self: Rc) { + self.have_frame.set(true); + self.maybe_trigger_frame(); + } +} + +fn map_key(kc: Keysym, mods: &mut Modifiers) -> Option { + use {Key as K, kbvm::syms as s}; + let mut with_shift = |k| { + mods.shift = true; + k + }; + let key = match kc { + s::Down | s::KP_Down => K::ArrowDown, + s::Left | s::KP_Left => K::ArrowLeft, + s::Right | s::KP_Right => K::ArrowRight, + s::Up | s::KP_Up => K::ArrowUp, + s::Escape => K::Escape, + s::Tab | s::KP_Tab => K::Tab, + s::ISO_Left_Tab => with_shift(K::Tab), + s::BackSpace => K::Backspace, + s::Return | s::KP_Enter => K::Enter, + s::space | s::KP_Space => K::Space, + s::Insert | s::KP_Insert => K::Insert, + s::Delete | s::KP_Delete => K::Delete, + s::Home | s::KP_Home | s::KP_Begin => K::Home, + s::End | s::KP_End => K::End, + s::Page_Up | s::KP_Page_Up => K::PageUp, + s::Page_Down | s::KP_Page_Down => K::PageDown, + s::XF86Copy => K::Copy, + s::XF86Cut => K::Cut, + s::XF86Paste => K::Paste, + s::colon => K::Colon, + s::comma => K::Comma, + s::backslash => K::Backslash, + s::slash | s::KP_Divide => K::Slash, + s::bar => K::Pipe, + s::question => K::Questionmark, + s::exclam => K::Exclamationmark, + s::bracketleft => K::OpenBracket, + s::bracketright => K::CloseBracket, + s::braceleft => K::OpenCurlyBracket, + s::braceright => K::CloseCurlyBracket, + s::grave => K::Backtick, + s::minus | s::KP_Subtract => K::Minus, + s::period | s::KP_Decimal => K::Period, + s::plus | s::KP_Add => K::Plus, + s::equal | s::KP_Equal => K::Equals, + s::semicolon => K::Semicolon, + s::quotedbl => K::Quote, + s::KP_0 | s::_0 => K::Num0, + s::KP_1 | s::_1 => K::Num1, + s::KP_2 | s::_2 => K::Num2, + s::KP_3 | s::_3 => K::Num3, + s::KP_4 | s::_4 => K::Num4, + s::KP_5 | s::_5 => K::Num5, + s::KP_6 | s::_6 => K::Num6, + s::KP_7 | s::_7 => K::Num7, + s::KP_8 | s::_8 => K::Num8, + s::KP_9 | s::_9 => K::Num9, + s::a => K::A, + s::b => K::B, + s::c => K::C, + s::d => K::D, + s::e => K::E, + s::f => K::F, + s::g => K::G, + s::h => K::H, + s::i => K::I, + s::j => K::J, + s::k => K::K, + s::l => K::L, + s::m => K::M, + s::n => K::N, + s::o => K::O, + s::p => K::P, + s::q => K::Q, + s::r => K::R, + s::s => K::S, + s::t => K::T, + s::u => K::U, + s::v => K::V, + s::w => K::W, + s::x => K::X, + s::y => K::Y, + s::z => K::Z, + s::A => with_shift(K::A), + s::B => with_shift(K::B), + s::C => with_shift(K::C), + s::D => with_shift(K::D), + s::E => with_shift(K::E), + s::F => with_shift(K::F), + s::G => with_shift(K::G), + s::H => with_shift(K::H), + s::I => with_shift(K::I), + s::J => with_shift(K::J), + s::K => with_shift(K::K), + s::L => with_shift(K::L), + s::M => with_shift(K::M), + s::N => with_shift(K::N), + s::O => with_shift(K::O), + s::P => with_shift(K::P), + s::Q => with_shift(K::Q), + s::R => with_shift(K::R), + s::S => with_shift(K::S), + s::T => with_shift(K::T), + s::U => with_shift(K::U), + s::V => with_shift(K::V), + s::W => with_shift(K::W), + s::X => with_shift(K::X), + s::Y => with_shift(K::Y), + s::Z => with_shift(K::Z), + s::F1 | s::KP_F1 => K::F1, + s::F2 | s::KP_F2 => K::F2, + s::F3 | s::KP_F3 => K::F3, + s::F4 | s::KP_F4 => K::F4, + s::F5 => K::F5, + s::F6 => K::F6, + s::F7 => K::F7, + s::F8 => K::F8, + s::F9 => K::F9, + s::F10 => K::F10, + s::F11 => K::F11, + s::F12 => K::F12, + s::F13 => K::F13, + s::F14 => K::F14, + s::F15 => K::F15, + s::F16 => K::F16, + s::F17 => K::F17, + s::F18 => K::F18, + s::F19 => K::F19, + s::F20 => K::F20, + s::F21 => K::F21, + s::F22 => K::F22, + s::F23 => K::F23, + s::F24 => K::F24, + s::F25 => K::F25, + s::F26 => K::F26, + s::F27 => K::F27, + s::F28 => K::F28, + s::F29 => K::F29, + s::F30 => K::F30, + s::F31 => K::F31, + s::F32 => K::F32, + s::F33 => K::F33, + s::F34 => K::F34, + s::F35 => K::F35, + s::XF86Back => K::BrowserBack, + _ => return None, + }; + Some(key) +} + +impl Drop for EggSeat { + fn drop(&mut self) { + let s = &self.inner; + s.copy_task.take(); + s.paste_task.take(); + s.leave(); + s.unfocus(); + if let Some(v) = s.wl_data_source.take() { + s.ctx.con.remove_obj(&*v); + } + s.ctx.seats.remove(&s.global_name); + s.ctx.con.remove_obj(&*s.wl_data_device); + s.ctx.con.remove_obj(&*s.wl_keyboard); + s.ctx.con.remove_obj(&*s.wp_cursor_shape_device_v1); + s.ctx.con.remove_obj(&*s.wl_pointer); + s.ctx.con.remove_obj(&*s.wl_seat); + } +} + +impl Drop for EggContext { + fn drop(&mut self) { + let i = &self.inner; + i.state.egg_state.cxts.remove(&self.inner.id); + i.seats.clear(); + i.windows.clear(); + i.con.owner.take(); + i.con.kill(); + } +} + +impl Drop for EggWindow { + fn drop(&mut self) { + let i = &self.inner; + i.ctx.windows.remove(&i.wl_surface.id); + if let Some(seat) = i.active_seat.take() { + for field in [&seat.kb_window, &seat.pointer_window] { + if let Some(w) = field.get() + && rc_eq(&w, i) + { + field.take(); + } + } + } + i.owner.take(); + i.ctx.con.remove_obj(&*i.jay_sync_file_surface); + i.ctx.con.remove_obj(&*i.xdg_toplevel); + i.ctx.con.remove_obj(&*i.xdg_surface); + i.ctx.con.remove_obj(&*i.wp_fractional_scale); + i.ctx.con.remove_obj(&*i.wp_viewport); + i.ctx.con.remove_obj(&*i.wl_surface); + } +} + +impl UsrWlDataSourceOwner for EggSeatInner { + fn send(&self, _mime_type: &str, fd: Rc) { + let Some(buf) = self.copy_text.borrow_mut().as_mut().map(|b| b.clone()) else { + return; + }; + let ring = self.ctx.state.ring.clone(); + let task = self.ctx.state.eng.spawn("egg-copy-text", async move { + if let Err(e) = ring.write(&fd, buf, None).await { + log::error!("Could not send text to client: {}", e); + } + }); + self.copy_task.set(Some(task)); + } +} + +impl UsrConOwner for EggContextInner { + fn killed(&self) { + if let Some(ctx) = self.state.egg_state.ctx.get() + && ctx.inner.id == self.id + { + self.state.egg_state.ctx.take(); + } + for window in self.windows.clear().values() { + if let Some(owner) = window.owner.take() { + owner.close(); + } + } + } +} + +impl Drop for EggFramebuffer { + fn drop(&mut self) { + if let Some(Some(fence)) = self.client_acquire_fence.take() { + self.drop_queue.push(&fence.0, self.bo.clone()); + } + self.wl_buffer.con.remove_obj(&*self.wl_buffer); + } +} diff --git a/src/egui_adapter/egui_vulkan.rs b/src/egui_adapter/egui_vulkan.rs new file mode 100644 index 00000000..aae9f29d --- /dev/null +++ b/src/egui_adapter/egui_vulkan.rs @@ -0,0 +1,2055 @@ +use { + crate::{ + allocator::BufferObject, + async_engine::{AsyncEngine, SpawnedFuture}, + eventfd_cache::EventfdCache, + format::XRGB8888, + gfx_api::{FdSync, SyncFile}, + io_uring::IoUring, + utils::{errorfmt::ErrorFmt, queue::AsyncQueue}, + video::{Modifier, dmabuf::PlaneVec, drm::syncobj::SyncobjCtx}, + vulkan_core::{ + VULKAN_API_VERSION, VulkanCoreError, VulkanCoreInstance, VulkanDeviceFeatures, + device::VulkanDeviceInf, + gpu_alloc_ash::AshMemoryDevice, + map_extension_properties, + sync::{VulkanDeviceSyncExt, VulkanSync}, + timeline_semaphore::{VulkanDeviceTimelineSemaphoreExt, VulkanTimelineSemaphore}, + }, + }, + ahash::AHashMap, + arrayvec::ArrayVec, + ash::{ + Device, + ext::{ + external_memory_dma_buf, image_drm_format_modifier, physical_device_drm, + queue_family_foreign, + }, + khr::{external_fence_fd, external_memory_fd, external_semaphore_fd, push_descriptor}, + util::read_spv, + vk::{ + self, AccessFlags2, AttachmentLoadOp, AttachmentStoreOp, BindImageMemoryInfo, + BindImagePlaneMemoryInfo, BlendFactor, BlendOp, BorderColor, Buffer, BufferCreateInfo, + BufferImageCopy2, BufferMemoryBarrier2, BufferUsageFlags, ColorComponentFlags, + CommandBuffer, CommandBufferAllocateInfo, CommandBufferBeginInfo, CommandBufferLevel, + CommandBufferSubmitInfo, CommandBufferUsageFlags, CommandPool, CommandPoolCreateFlags, + CommandPoolCreateInfo, ComponentMapping, ComponentSwizzle, CopyBufferToImageInfo2, + CullModeFlags, DependencyInfo, DescriptorImageInfo, DescriptorSetLayout, + DescriptorSetLayoutBinding, DescriptorSetLayoutCreateFlags, + DescriptorSetLayoutCreateInfo, DescriptorType, DeviceCreateInfo, DeviceMemory, + DeviceQueueCreateInfo, DrmFormatModifierPropertiesEXT, + DrmFormatModifierPropertiesListEXT, DynamicState, Extent2D, Extent3D, + ExternalFenceFeatureFlags, ExternalFenceHandleTypeFlags, ExternalFenceProperties, + ExternalImageFormatPropertiesKHR, ExternalMemoryFeatureFlags, + ExternalMemoryHandleTypeFlags, ExternalMemoryImageCreateInfo, + ExternalSemaphoreFeatureFlags, ExternalSemaphoreHandleTypeFlags, + ExternalSemaphoreProperties, Filter, Format, FormatFeatureFlags, FormatProperties2, + FrontFace, GraphicsPipelineCreateInfo, Image, ImageAspectFlags, ImageCreateFlags, + ImageCreateInfo, ImageDrmFormatModifierExplicitCreateInfoEXT, ImageFormatProperties2, + ImageLayout, ImageMemoryBarrier2, ImageMemoryRequirementsInfo2, + ImagePlaneMemoryRequirementsInfo, ImageSubresourceLayers, ImageSubresourceRange, + ImageTiling, ImageType, ImageUsageFlags, ImageView, ImageViewCreateInfo, ImageViewType, + ImportMemoryFdInfoKHR, ImportSemaphoreFdInfoKHR, IndexType, MappedMemoryRange, + MemoryAllocateInfo, MemoryDedicatedAllocateInfo, MemoryFdPropertiesKHR, + MemoryRequirements, MemoryRequirements2, Offset2D, Offset3D, + PhysicalDeviceDrmPropertiesEXT, PhysicalDeviceDynamicRenderingFeatures, + PhysicalDeviceExternalFenceInfo, PhysicalDeviceExternalImageFormatInfoKHR, + PhysicalDeviceExternalSemaphoreInfo, PhysicalDeviceFeatures2, + PhysicalDeviceImageDrmFormatModifierInfoEXT, PhysicalDeviceImageFormatInfo2, + PhysicalDeviceProperties2, PhysicalDeviceSynchronization2Features, + PhysicalDeviceTimelineSemaphoreFeatures, PhysicalDeviceType, + PhysicalDeviceVulkan13Properties, Pipeline, PipelineBindPoint, PipelineCache, + PipelineColorBlendAttachmentState, PipelineColorBlendStateCreateInfo, + PipelineDynamicStateCreateInfo, PipelineInputAssemblyStateCreateInfo, PipelineLayout, + PipelineLayoutCreateInfo, PipelineMultisampleStateCreateInfo, + PipelineRasterizationStateCreateInfo, PipelineRenderingCreateInfo, + PipelineShaderStageCreateInfo, PipelineStageFlags2, PipelineVertexInputStateCreateInfo, + PipelineViewportStateCreateInfo, PolygonMode, PrimitiveTopology, + QUEUE_FAMILY_FOREIGN_EXT, Queue, QueueFlags, Rect2D, RenderingAttachmentInfo, + RenderingInfoKHR, SampleCountFlags, Sampler, SamplerAddressMode, SamplerCreateInfo, + SamplerMipmapMode, Semaphore, SemaphoreCreateInfo, SemaphoreImportFlags, + SemaphoreSubmitInfo, ShaderModule, ShaderModuleCreateInfo, ShaderStageFlags, + SharingMode, SubmitInfo2, SubresourceLayout, VertexInputAttributeDescription, + VertexInputBindingDescription, VertexInputRate, Viewport, WHOLE_SIZE, + WriteDescriptorSet, + }, + }, + bstr::ByteSlice, + egui::epaint::{ + ClippedPrimitive, ImageData, ImageDelta, Primitive, TextureId, Vertex, + textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta}, + }, + gpu_alloc::{ + AllocationError, Config, GpuAllocator, MapError, MemoryBlock, Request, UsageFlags, + }, + gpu_alloc_types::MemoryPropertyFlags, + isnt::std_1::{collections::IsntHashMapExt, primitive::IsntSliceExt}, + log::Level, + run_on_drop::on_drop, + std::{ + cell::{Cell, RefCell}, + collections::hash_map::Entry, + ffi::CStr, + io::{self, Cursor}, + mem::{ManuallyDrop, offset_of}, + ptr, + rc::Rc, + slice, + }, + thiserror::Error, + uapi::{AsUstr, AssertPacked, Packed, Pod, c}, +}; + +#[derive(Debug, Error)] +pub enum EgvError { + #[error(transparent)] + Core(#[from] VulkanCoreError), + #[error("could not read spv source")] + ReadSpv(#[source] io::Error), + #[error("could not create a shader module")] + CreateShaderModule(#[source] vk::Result), + #[error("could not create a sampler")] + CreateSampler(#[source] vk::Result), + #[error("could not allocate GPU memory")] + AllocateMemory(#[source] AllocationError), + #[error("could not map GPU memory")] + MapMemory(#[source] MapError), + #[error("could not create a buffer")] + CreateBuffer(#[source] vk::Result), + #[error("could not bind memory to buffer")] + BindBufferMemory(#[source] vk::Result), + #[error("could not bind memory to image")] + BindImageMemory(#[source] vk::Result), + #[error("could not flush GPU memory")] + FlushMemory(#[source] vk::Result), + #[error("could not create an image")] + CreateImage(#[source] vk::Result), + #[error("could not create an image view")] + CreateImageView(#[source] vk::Result), + #[error("tried to render an unknown texture {0:?}")] + UnknownTexture(TextureId), + #[error("could not create a descriptor set layout")] + CreateDescriptorSetLayout(#[source] vk::Result), + #[error("could not create a pipeline layout")] + CreatePipelineLayout(#[source] vk::Result), + #[error("could not create a pipeline")] + CreatePipeline(#[source] vk::Result), + #[error("cannot perform a partial update of unknown texture {0:?}")] + PartialTextureUpdateForUnknownTexture(TextureId), + #[error("cannot perform out-of-bounds texture update for {0:?}")] + TextureUpdateOutOfBounds(TextureId), + #[error("could not allocate a command buffer")] + AllocateCommandBuffer(#[source] vk::Result), + #[error("could not begin a command buffer")] + BeginCommandBuffer(#[source] vk::Result), + #[error("could not end a command buffer")] + EndCommandBuffer(#[source] vk::Result), + #[error("could not create a semaphore")] + CreateSemaphore(#[source] vk::Result), + #[error("could not submit a command buffer")] + Submit(#[source] vk::Result), + #[error("could not get device properties")] + GetDeviceProperties(#[source] vk::Result), + #[error("could not create a command pool")] + CreateCommandPool(#[source] vk::Result), + #[error("driver does not support all required format features")] + MissingFormatFeatures, + #[error("could not get image format properties")] + GetImageFormatProperties(#[source] vk::Result), + #[error("texture is empty")] + EmptyImage, + #[error("texture is too large")] + TexTooLarge, + #[error("driver does not support sufficiently-large buffers")] + BufferTooLarge, + #[error("Could not enumerate the physical devices")] + EnumeratePhysicalDevice(#[source] vk::Result), + #[error("Could not find a corresponding vulkan device")] + NoVulkanDevice, + #[error("Device does not support vulkan 1.3")] + NoVulkan13, + #[error("Device does not support the synchronization2 feature")] + NoSynchronization2, + #[error("Device does not support the dynamic rendering feature")] + NoDynamicRendering, + #[error("Device does not support the device extension {}", .0.as_ustr().as_bytes().as_bstr())] + MissingDeviceExtensions(&'static CStr), + #[error("Device does not support importing sync files")] + NoSyncFileImport, + #[error("Device does not support exporting sync files")] + NoSyncFileExport, + #[error("Device does not have a graphics queue family")] + NoGfxQueueFamily, + #[error("Could not create the device")] + CreateDevice(#[source] vk::Result), + #[error("Only XRGB8888 is supported as the framebuffer format")] + WrongFbFormat, + #[error("The size of FB must be > 0")] + NonPositiveFbSize, + #[error("The modifier is not supported")] + UnsupportedModifier, + #[error("The number of planes is incorrect")] + WrongPlaneCount, + #[error("The FB is too large")] + TooLarge, + #[error("Could not query memory fd properties")] + GetMemoryFdProperties(#[source] vk::Result), + #[error("Could not find a memory type for import")] + NoMemoryTypeForImport, + #[error("Could not dup a dma buf")] + DupDmaBuf(#[source] io::Error), + #[error("Could not import memory")] + ImportMemory(#[source] vk::Result), + #[error("Could not dup a sync file")] + DupSyncFile(#[source] io::Error), + #[error("Could not import a sync file")] + ImportSyncFile(#[source] vk::Result), +} + +pub struct EgvRenderer { + ri: Rc, + timeline_semaphore: Option>>, + _task: SpawnedFuture<()>, +} + +linear_ids!(EgvContextIds, EgvContextId, u64); + +pub struct EgvContext { + renderer: Rc, + id: EgvContextId, +} + +pub struct EgvFramebuffer { + renderer: Rc, + ctx: Rc, + image: Rc>, +} + +pub struct Support { + pub modifier: Modifier, + pub planes: usize, + pub max_width: u32, + pub max_height: u32, +} + +struct EgvRendererInner { + instance: VulkanCoreInstance, + sync_ctx: Option>, + eventfd_cache: Rc, + supports_timeline_opaque_export: bool, + device: Device, + queue: Queue, + queue_family: u32, + external_fence_fd: external_fence_fd::Device, + external_semaphore_fd: external_semaphore_fd::Device, + external_memory_fd: external_memory_fd::Device, + push_descriptor: push_descriptor::Device, + vert: ShaderModule, + frag: ShaderModule, + non_coherent_atom_size: u64, + descriptor_set_layout: DescriptorSetLayout, + pipeline_layout: PipelineLayout, + max_tex_width: u32, + max_tex_height: u32, + max_buffer_size: u64, + allocator: RefCell>, + pool: CommandPool, + cache: RefCell, + submissions: Rc, + pipeline: Pipeline, + dmabuf_support: Vec, + context_ids: EgvContextIds, +} + +#[derive(Default)] +struct EgvRendererCache { + device_local_buffers: Vec, + samplers: AHashMap>, + images: AHashMap<(EgvContextId, TextureId), EgvSampledImage>, + upload_todos: Vec<(Rc>, EgvBuffer, ImageDelta)>, + buffer_memory_barriers: Vec>, + initial_image_memory_barriers: Vec>, + final_image_memory_barriers: Vec>, + semaphores: Vec, +} + +struct EgvBuffer { + ri: Rc, + memory: EgvAllocatedMemory, + buffer: Buffer, + size: u64, + usage: BufferUsageFlags, + mapping: *mut [u8], + host_coherent: bool, +} + +struct EgvImportedMemory { + ri: Rc, + _bo: Rc, + memories: PlaneVec, +} + +struct EgvAllocatedMemory { + ri: Rc, + block: ManuallyDrop>, + mapping: Option<*mut [u8]>, +} + +struct EgvCommandBuffer { + ri: Rc, + buf: CommandBuffer, +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct VkVertex { + pos: [f32; 2], + uv: [f32; 2], + color: [u8; 4], +} + +unsafe impl Pod for VkVertex {} +unsafe impl Packed for VkVertex {} + +struct VkSampler { + ri: Rc, + options: TextureOptions, + sampler: Sampler, +} + +#[derive(Clone)] +struct EgvSampledImage { + image: Rc>, + sampler: Rc, +} + +struct EgvImage { + ri: Rc, + width: u32, + height: u32, + image: Image, + image_view: ImageView, + _memory: M, + layout: Cell, +} + +#[derive(Default)] +struct PendingSubmissions { + task_has_pending: Cell, + pending: AsyncQueue, +} + +struct Pending { + ri: Rc, + sync: Option, + semaphore: Option, + vulkan_sync: VulkanSync, + _cmd: EgvCommandBuffer, + _uploads: Vec<(Rc>, EgvBuffer)>, + _sampled: Vec, + _fb: Rc>, + index_buffer: Option, + vertex_buffer: Option, +} + +struct EgvSemaphore { + ri: Rc, + semaphore: Semaphore, +} + +const SRGB_FORMAT: Format = Format::R8G8B8A8_SRGB; +const SRGB_FORMAT_BPP: u64 = 4; +pub const EGV_FORMAT: &crate::format::Format = XRGB8888; +const VK_FB_FORMAT: Format = Format::B8G8R8A8_SRGB; + +const DEVICE_EXTENSIONS: [&CStr; 7] = [ + external_fence_fd::NAME, + external_semaphore_fd::NAME, + external_memory_fd::NAME, + external_memory_dma_buf::NAME, + image_drm_format_modifier::NAME, + queue_family_foreign::NAME, + push_descriptor::NAME, +]; + +const VERT: &[u8] = include_bytes!("shaders_bin/shader.vert.spv"); +const FRAG: &[u8] = include_bytes!("shaders_bin/shader.frag.spv"); + +const IMAGE_SUBRESOURCE_RANGE: ImageSubresourceRange = ImageSubresourceRange { + aspect_mask: ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, +}; + +const IMAGE_SUBRESOURCE_LAYERS: ImageSubresourceLayers = ImageSubresourceLayers { + aspect_mask: ImageAspectFlags::COLOR, + mip_level: 0, + base_array_layer: 0, + layer_count: 1, +}; + +impl EgvRenderer { + pub fn new( + eng: &Rc, + ring: &Rc, + eventfd_cache: &Rc, + dev: Option, + ) -> Result, EgvError> { + let core_instance = VulkanCoreInstance::new(Level::Debug)?; + let instance = &core_instance.instance; + let mut physical_device; + let mut device_extensions; + let mut device_properties; + 'find_device: { + let devices = unsafe { + instance + .enumerate_physical_devices() + .map_err(EgvError::EnumeratePhysicalDevice)? + }; + 'outer: for phy in devices { + let res = unsafe { instance.enumerate_device_extension_properties(phy) }; + let exts = match res { + Ok(res) => map_extension_properties(res), + Err(e) => { + log::error!( + "Could not enumerate extensions of physical device: {}", + ErrorFmt(e), + ); + continue; + } + }; + let mut drm_props = PhysicalDeviceDrmPropertiesEXT::default(); + let mut props = PhysicalDeviceProperties2::default().push_next(&mut drm_props); + unsafe { + instance.get_physical_device_properties2(phy, &mut props); + } + let props = props.properties; + physical_device = phy; + device_extensions = exts; + device_properties = props; + if let Some(dev) = dev { + if device_extensions.not_contains_key(physical_device_drm::NAME) { + continue 'outer; + } + let major = uapi::major(dev) as i64; + let minor = uapi::minor(dev) as i64; + let matches = (drm_props.has_primary == vk::TRUE + && drm_props.primary_major == major + && drm_props.primary_minor == minor) + || (drm_props.has_render == vk::TRUE + && drm_props.render_major == major + && drm_props.render_minor == minor); + if matches { + break 'find_device; + } + } else { + if device_properties.device_type == PhysicalDeviceType::CPU { + break 'find_device; + } + } + } + return Err(EgvError::NoVulkanDevice); + } + if device_properties.api_version < VULKAN_API_VERSION { + return Err(EgvError::NoVulkan13); + } + for ext in DEVICE_EXTENSIONS { + if device_extensions.not_contains_key(ext) { + return Err(EgvError::MissingDeviceExtensions(ext)); + } + } + let features = { + let mut synchronization2_features = PhysicalDeviceSynchronization2Features::default(); + let mut dynamic_rendering_features = PhysicalDeviceDynamicRenderingFeatures::default(); + let mut timeline_semaphore_features = + PhysicalDeviceTimelineSemaphoreFeatures::default(); + let mut physical_device_features = PhysicalDeviceFeatures2::default() + .push_next(&mut synchronization2_features) + .push_next(&mut dynamic_rendering_features) + .push_next(&mut timeline_semaphore_features); + unsafe { + instance + .get_physical_device_features2(physical_device, &mut physical_device_features); + } + let features = physical_device_features.features; + if synchronization2_features.synchronization2 != vk::TRUE { + return Err(EgvError::NoSynchronization2); + } + if dynamic_rendering_features.dynamic_rendering != vk::TRUE { + return Err(EgvError::NoDynamicRendering); + } + VulkanDeviceFeatures { + features, + semaphore_features: timeline_semaphore_features, + } + }; + { + let info = PhysicalDeviceExternalSemaphoreInfo::default() + .handle_type(ExternalSemaphoreHandleTypeFlags::SYNC_FD); + let mut props = ExternalSemaphoreProperties::default(); + unsafe { + instance.get_physical_device_external_semaphore_properties( + physical_device, + &info, + &mut props, + ); + } + let supported = props + .external_semaphore_features + .contains(ExternalSemaphoreFeatureFlags::IMPORTABLE); + if !supported { + return Err(EgvError::NoSyncFileImport); + } + } + { + let info = PhysicalDeviceExternalFenceInfo::default() + .handle_type(ExternalFenceHandleTypeFlags::SYNC_FD); + let mut props = ExternalFenceProperties::default(); + unsafe { + instance.get_physical_device_external_fence_properties( + physical_device, + &info, + &mut props, + ); + } + let supported = props + .external_fence_features + .contains(ExternalFenceFeatureFlags::EXPORTABLE); + if !supported { + return Err(EgvError::NoSyncFileExport); + } + } + let queue_family = 'queue_family: { + let families = + unsafe { instance.get_physical_device_queue_family_properties(physical_device) }; + for (idx, family) in families.iter().enumerate() { + if family.queue_count > 0 && family.queue_flags.contains(QueueFlags::GRAPHICS) { + break 'queue_family idx as u32; + } + } + return Err(EgvError::NoGfxQueueFamily); + }; + let dmabuf_support = { + let mut list = vec![]; + for attach in [false, true] { + let mut modifiers = DrmFormatModifierPropertiesListEXT::default(); + if attach { + modifiers = modifiers.drm_format_modifier_properties(&mut list); + } + let mut out = FormatProperties2::default().push_next(&mut modifiers); + unsafe { + instance.get_physical_device_format_properties2( + physical_device, + VK_FB_FORMAT, + &mut out, + ); + } + if !attach { + list = vec![ + DrmFormatModifierPropertiesEXT::default(); + modifiers.drm_format_modifier_count as usize + ]; + } + } + let mut support = vec![]; + for modifier in list { + let image_features = modifier.drm_format_modifier_tiling_features; + if !image_features.contains( + FormatFeatureFlags::COLOR_ATTACHMENT + | FormatFeatureFlags::COLOR_ATTACHMENT_BLEND, + ) { + continue; + } + let mut modifier_info = PhysicalDeviceImageDrmFormatModifierInfoEXT::default() + .drm_format_modifier(modifier.drm_format_modifier); + let mut external_memory_info = PhysicalDeviceExternalImageFormatInfoKHR::default() + .handle_type(ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); + let info = PhysicalDeviceImageFormatInfo2::default() + .format(VK_FB_FORMAT) + .ty(ImageType::TYPE_2D) + .tiling(ImageTiling::DRM_FORMAT_MODIFIER_EXT) + .usage(ImageUsageFlags::COLOR_ATTACHMENT) + .push_next(&mut external_memory_info) + .push_next(&mut modifier_info); + let mut external_memory_prop = ExternalImageFormatPropertiesKHR::default(); + let mut prop = + ImageFormatProperties2::default().push_next(&mut external_memory_prop); + let res = unsafe { + instance.get_physical_device_image_format_properties2( + physical_device, + &info, + &mut prop, + ) + }; + if res.is_err() { + continue; + } + let prop = prop.image_format_properties; + let memory_features = external_memory_prop + .external_memory_properties + .external_memory_features; + if !memory_features.contains(ExternalMemoryFeatureFlags::IMPORTABLE) { + continue; + } + let me = prop.max_extent; + if me.width > 0 && me.height > 0 && me.depth > 0 { + support.push(Support { + modifier: modifier.drm_format_modifier, + planes: modifier.drm_format_modifier_plane_count as usize, + max_width: me.width, + max_height: me.height, + }); + } + } + support + }; + let supports_timeline_opaque_export = + core_instance.supports_timeline_opaque_export(physical_device, &features); + let format_properties = + unsafe { instance.get_physical_device_format_properties(physical_device, SRGB_FORMAT) }; + let required_features = FormatFeatureFlags::SAMPLED_IMAGE + | FormatFeatureFlags::SAMPLED_IMAGE_FILTER_LINEAR + | FormatFeatureFlags::TRANSFER_DST; + if !format_properties + .optimal_tiling_features + .contains(required_features) + { + return Err(EgvError::MissingFormatFeatures); + } + let format_properties = unsafe { + instance + .get_physical_device_image_format_properties( + physical_device, + SRGB_FORMAT, + ImageType::TYPE_2D, + ImageTiling::OPTIMAL, + ImageUsageFlags::SAMPLED | ImageUsageFlags::TRANSFER_DST, + ImageCreateFlags::empty(), + ) + .map_err(EgvError::GetImageFormatProperties)? + }; + let max_buffer_size; + { + let mut prop13 = PhysicalDeviceVulkan13Properties::default(); + let mut prop = PhysicalDeviceProperties2::default().push_next(&mut prop13); + unsafe { + instance.get_physical_device_properties2(physical_device, &mut prop); + } + max_buffer_size = prop13.max_buffer_size; + } + let device = { + let queue_create_info = DeviceQueueCreateInfo::default() + .queue_family_index(queue_family) + .queue_priorities(&[1.0]); + let extensions = DEVICE_EXTENSIONS.map(|e| e.as_ptr()); + let mut dynamic_rendering_features = + PhysicalDeviceDynamicRenderingFeatures::default().dynamic_rendering(true); + let mut synchronization2_features = + PhysicalDeviceSynchronization2Features::default().synchronization2(true); + let mut timeline_semaphore_features = + PhysicalDeviceTimelineSemaphoreFeatures::default() + .timeline_semaphore(supports_timeline_opaque_export); + let info = DeviceCreateInfo::default() + .queue_create_infos(slice::from_ref(&queue_create_info)) + .enabled_extension_names(&extensions) + .push_next(&mut synchronization2_features) + .push_next(&mut dynamic_rendering_features) + .push_next(&mut timeline_semaphore_features); + unsafe { + instance + .create_device(physical_device, &info, None) + .map_err(EgvError::CreateDevice)? + } + }; + let destroy_device = on_drop(|| unsafe { device.destroy_device(None) }); + let external_fence_fd = external_fence_fd::Device::new(instance, &device); + let external_semaphore_fd = external_semaphore_fd::Device::new(instance, &device); + let external_memory_fd = external_memory_fd::Device::new(instance, &device); + let push_descriptor = push_descriptor::Device::new(instance, &device); + let queue = unsafe { device.get_device_queue(queue_family, 0) }; + let pool = { + let info = CommandPoolCreateInfo::default() + .queue_family_index(queue_family) + .flags(CommandPoolCreateFlags::RESET_COMMAND_BUFFER); + unsafe { + device + .create_command_pool(&info, None) + .map_err(EgvError::CreateCommandPool)? + } + }; + let destroy_pool = on_drop(|| unsafe { device.destroy_command_pool(pool, None) }); + let create_shader = |src: &[u8]| { + let mut cursor = Cursor::new(src); + let spv = read_spv(&mut cursor).map_err(EgvError::ReadSpv)?; + let create_info = ShaderModuleCreateInfo::default().code(&spv); + unsafe { + device + .create_shader_module(&create_info, None) + .map_err(EgvError::CreateShaderModule) + } + }; + let vert = create_shader(VERT)?; + let destroy_vert = on_drop(|| unsafe { device.destroy_shader_module(vert, None) }); + let frag = create_shader(FRAG)?; + let destroy_frag = on_drop(|| unsafe { device.destroy_shader_module(frag, None) }); + let descriptor_set_layout = { + let binding = DescriptorSetLayoutBinding::default() + .descriptor_type(DescriptorType::COMBINED_IMAGE_SAMPLER) + .descriptor_count(1) + .stage_flags(ShaderStageFlags::FRAGMENT); + let create_info = DescriptorSetLayoutCreateInfo::default() + .flags(DescriptorSetLayoutCreateFlags::PUSH_DESCRIPTOR_KHR) + .bindings(slice::from_ref(&binding)); + unsafe { + device + .create_descriptor_set_layout(&create_info, None) + .map_err(EgvError::CreateDescriptorSetLayout)? + } + }; + let destroy_descriptor_set_layout = on_drop(|| unsafe { + device.destroy_descriptor_set_layout(descriptor_set_layout, None) + }); + let pipeline_layout = { + let create_info = PipelineLayoutCreateInfo::default() + .set_layouts(slice::from_ref(&descriptor_set_layout)); + unsafe { + device + .create_pipeline_layout(&create_info, None) + .map_err(EgvError::CreatePipelineLayout)? + } + }; + let destroy_pipeline_layout = + on_drop(|| unsafe { device.destroy_pipeline_layout(pipeline_layout, None) }); + let mut device_properties = unsafe { + crate::vulkan_core::gpu_alloc_ash::device_properties(instance, physical_device) + .map_err(EgvError::GetDeviceProperties)? + }; + device_properties.buffer_device_address = false; + let non_coherent_atom_size = device_properties.non_coherent_atom_size; + let allocator = GpuAllocator::new(Config::i_am_potato(), device_properties); + let pipeline = { + let stages = [ + PipelineShaderStageCreateInfo::default() + .stage(ShaderStageFlags::VERTEX) + .module(vert) + .name(c"main"), + PipelineShaderStageCreateInfo::default() + .stage(ShaderStageFlags::FRAGMENT) + .module(frag) + .name(c"main"), + ]; + let vertex_input_binding_description = VertexInputBindingDescription { + binding: 0, + stride: size_of::() as _, + input_rate: VertexInputRate::VERTEX, + }; + let vertex_attribute_descriptions = [ + VertexInputAttributeDescription::default() + .location(0) + .format(Format::R32G32_SFLOAT) + .offset(offset_of!(Vertex, pos) as u32), + VertexInputAttributeDescription::default() + .location(1) + .format(Format::R32G32_SFLOAT) + .offset(offset_of!(Vertex, uv) as u32), + VertexInputAttributeDescription::default() + .location(2) + .format(Format::R8G8B8A8_UNORM) + .offset(offset_of!(Vertex, color) as u32), + ]; + let vertex_input_state = PipelineVertexInputStateCreateInfo::default() + .vertex_binding_descriptions(slice::from_ref(&vertex_input_binding_description)) + .vertex_attribute_descriptions(&vertex_attribute_descriptions); + let input_assembly_info = PipelineInputAssemblyStateCreateInfo::default() + .topology(PrimitiveTopology::TRIANGLE_LIST); + let viewport_state = PipelineViewportStateCreateInfo::default() + .viewport_count(1) + .scissor_count(1); + let rasterization_state = PipelineRasterizationStateCreateInfo::default() + .polygon_mode(PolygonMode::FILL) + .cull_mode(CullModeFlags::NONE) + .front_face(FrontFace::CLOCKWISE) + .line_width(1.0); + let multisampling_state = PipelineMultisampleStateCreateInfo::default() + .rasterization_samples(SampleCountFlags::TYPE_1) + .min_sample_shading(1.0); + let color_blend_attachment_state = PipelineColorBlendAttachmentState::default() + .blend_enable(true) + .src_color_blend_factor(BlendFactor::ONE) + .dst_color_blend_factor(BlendFactor::ONE_MINUS_SRC_ALPHA) + .color_blend_op(BlendOp::ADD) + .src_alpha_blend_factor(BlendFactor::ONE) + .dst_alpha_blend_factor(BlendFactor::ONE_MINUS_SRC_ALPHA) + .alpha_blend_op(BlendOp::ADD) + .color_write_mask(ColorComponentFlags::RGBA); + let color_blend_state = PipelineColorBlendStateCreateInfo::default() + .attachments(slice::from_ref(&color_blend_attachment_state)); + let dynamic_state = PipelineDynamicStateCreateInfo::default() + .dynamic_states(&[DynamicState::VIEWPORT, DynamicState::SCISSOR]); + let mut rendering_create_info = PipelineRenderingCreateInfo::default() + .color_attachment_formats(slice::from_ref(&VK_FB_FORMAT)); + let create_info = GraphicsPipelineCreateInfo::default() + .stages(&stages) + .vertex_input_state(&vertex_input_state) + .input_assembly_state(&input_assembly_info) + .viewport_state(&viewport_state) + .rasterization_state(&rasterization_state) + .multisample_state(&multisampling_state) + .color_blend_state(&color_blend_state) + .dynamic_state(&dynamic_state) + .layout(pipeline_layout) + .push_next(&mut rendering_create_info); + let mut pipelines = unsafe { + device + .create_graphics_pipelines( + PipelineCache::null(), + slice::from_ref(&create_info), + None, + ) + .map_err(|e| EgvError::CreatePipeline(e.1))? + }; + pipelines.pop().unwrap() + }; + let destroy_pipeline = on_drop(|| unsafe { device.destroy_pipeline(pipeline, None) }); + let submissions = Rc::new(PendingSubmissions::default()); + let sync_ctx = dev + .and_then(|d| { + SyncobjCtx::from_dev_t(d) + .inspect_err(|e| log::warn!("Could not create a syncobj ctx: {}", ErrorFmt(e))) + .ok() + }) + .map(Rc::new); + destroy_pipeline.forget(); + destroy_pool.forget(); + destroy_pipeline_layout.forget(); + destroy_descriptor_set_layout.forget(); + destroy_frag.forget(); + destroy_vert.forget(); + destroy_device.forget(); + let renderer = Rc::new(EgvRendererInner { + push_descriptor, + sync_ctx, + eventfd_cache: eventfd_cache.clone(), + supports_timeline_opaque_export, + device, + queue, + queue_family, + external_fence_fd, + external_semaphore_fd, + vert, + frag, + non_coherent_atom_size, + descriptor_set_layout, + pipeline_layout, + max_tex_width: format_properties.max_extent.width, + max_tex_height: format_properties.max_extent.height, + max_buffer_size, + allocator: RefCell::new(allocator), + pool, + cache: Default::default(), + submissions: submissions.clone(), + pipeline, + instance: core_instance, + external_memory_fd, + dmabuf_support, + context_ids: Default::default(), + }); + let task = { + let future = wait_for_submissions(submissions, renderer.clone(), ring.clone()); + eng.spawn("egui-vulkan-await-pending", future) + }; + let renderer = Self { + timeline_semaphore: renderer.create_timeline_semaphore_or_log(), + ri: renderer, + _task: task, + }; + Ok(Rc::new(renderer)) + } + + fn create_semaphore(&self) -> Result { + let ri = &self.ri; + let create_info = SemaphoreCreateInfo::default(); + let semaphore = unsafe { + ri.device + .create_semaphore(&create_info, None) + .map_err(EgvError::CreateSemaphore)? + }; + Ok(EgvSemaphore { + ri: ri.clone(), + semaphore, + }) + } + + fn allocate_command_buffer(&self) -> Result { + let ri = &self.ri; + let allocate_info = CommandBufferAllocateInfo::default() + .command_pool(ri.pool) + .command_buffer_count(1) + .level(CommandBufferLevel::PRIMARY); + let mut cmd = unsafe { + ri.device + .allocate_command_buffers(&allocate_info) + .map_err(EgvError::AllocateCommandBuffer)? + }; + Ok(EgvCommandBuffer { + ri: ri.clone(), + buf: cmd.pop().unwrap(), + }) + } + + fn create_image( + self: &Rc, + data: &ImageData, + ) -> Result>, EgvError> { + let extent = Extent3D { + width: data.width() as _, + height: data.height() as _, + depth: 1, + }; + if extent.width == 0 || extent.height == 0 { + return Err(EgvError::EmptyImage); + } + let ri = &self.ri; + if extent.width > ri.max_tex_width || extent.height > ri.max_tex_height { + return Err(EgvError::TexTooLarge); + } + let dev = &ri.device; + let image = { + let create_info = ImageCreateInfo::default() + .image_type(ImageType::TYPE_2D) + .format(SRGB_FORMAT) + .extent(extent) + .mip_levels(1) + .array_layers(1) + .samples(SampleCountFlags::TYPE_1) + .tiling(ImageTiling::OPTIMAL) + .usage(ImageUsageFlags::SAMPLED | ImageUsageFlags::TRANSFER_DST) + .sharing_mode(SharingMode::EXCLUSIVE); + unsafe { + dev.create_image(&create_info, None) + .map_err(EgvError::CreateImage)? + } + }; + let destroy_image = on_drop(|| unsafe { dev.destroy_image(image, None) }); + let memory = { + let req = unsafe { dev.get_image_memory_requirements(image) }; + self.allocate_memory(req, UsageFlags::FAST_DEVICE_ACCESS, false)? + }; + unsafe { + dev.bind_image_memory(image, *memory.block.memory(), memory.block.offset()) + .map_err(EgvError::BindImageMemory)?; + } + let view = { + let create_info = ImageViewCreateInfo::default() + .image(image) + .view_type(ImageViewType::TYPE_2D) + .format(SRGB_FORMAT) + .components(ComponentMapping { + r: ComponentSwizzle::R, + g: ComponentSwizzle::G, + b: ComponentSwizzle::B, + a: ComponentSwizzle::A, + }) + .subresource_range(IMAGE_SUBRESOURCE_RANGE); + unsafe { + dev.create_image_view(&create_info, None) + .map_err(EgvError::CreateImageView)? + } + }; + let destroy_image_view = on_drop(|| unsafe { dev.destroy_image_view(view, None) }); + destroy_image_view.forget(); + destroy_image.forget(); + Ok(Rc::new(EgvImage { + ri: ri.clone(), + width: extent.width, + height: extent.height, + image, + _memory: memory, + image_view: view, + layout: Cell::new(ImageLayout::UNDEFINED), + })) + } + + fn get_device_local_buffer( + &self, + sync: &mut EgvRendererCache, + size: u64, + usage: BufferUsageFlags, + ) -> Result { + { + let mut best = None; + let mut best_size = u64::MAX; + for (i, buf) in sync.device_local_buffers.iter().enumerate() { + if buf.size < size { + continue; + } + if buf.usage != usage { + continue; + } + if buf.size < best_size { + best = Some(i); + best_size = buf.size; + } + } + if let Some(best) = best { + return Ok(sync.device_local_buffers.swap_remove(best)); + } + } + self.create_device_local_buffer(size, usage) + } + + fn create_device_local_buffer( + &self, + size: u64, + usage: BufferUsageFlags, + ) -> Result { + self.create_buffer( + size, + usage, + UsageFlags::FAST_DEVICE_ACCESS | UsageFlags::UPLOAD, + ) + } + + fn create_staging_buffer(&self, size: u64) -> Result { + self.create_buffer( + size, + BufferUsageFlags::TRANSFER_SRC, + UsageFlags::TRANSIENT | UsageFlags::UPLOAD, + ) + } + + fn create_buffer( + &self, + mut size: u64, + usage: BufferUsageFlags, + usage_flags: UsageFlags, + ) -> Result { + const MIN_SIZE: u64 = 1024; + size = size.max(MIN_SIZE); + let ri = &self.ri; + if size > ri.max_buffer_size { + return Err(EgvError::BufferTooLarge); + } + let dev = &ri.device; + let buffer = { + let create_info = BufferCreateInfo::default().size(size).usage(usage); + unsafe { + dev.create_buffer(&create_info, None) + .map_err(EgvError::CreateBuffer)? + } + }; + let destroy_buffer = on_drop(|| unsafe { dev.destroy_buffer(buffer, None) }); + let memory_requirements = unsafe { dev.get_buffer_memory_requirements(buffer) }; + let memory = self.allocate_memory(memory_requirements, usage_flags, true)?; + unsafe { + dev.bind_buffer_memory(buffer, *memory.block.memory(), memory.block.offset()) + .map_err(EgvError::BindBufferMemory)?; + } + destroy_buffer.forget(); + Ok(EgvBuffer { + ri: ri.clone(), + mapping: memory.mapping.unwrap(), + host_coherent: memory + .block + .props() + .contains(MemoryPropertyFlags::HOST_COHERENT), + memory, + buffer, + size, + usage, + }) + } + + fn allocate_memory( + &self, + req: MemoryRequirements, + usage: UsageFlags, + map: bool, + ) -> Result { + let request = Request { + size: req.size, + align_mask: req.alignment - 1, + usage, + memory_types: req.memory_type_bits, + }; + let ri = &self.ri; + let block = unsafe { + ri.allocator + .borrow_mut() + .alloc(AshMemoryDevice::wrap(&ri.device), request) + .map_err(EgvError::AllocateMemory)? + }; + let block = RefCell::new(ManuallyDrop::new(block)); + let deallocate = on_drop(|| unsafe { + ri.allocator.borrow_mut().dealloc( + AshMemoryDevice::wrap(&ri.device), + ManuallyDrop::take(&mut block.borrow_mut()), + ); + }); + let mut block_mut = block.borrow_mut(); + let mut mapping = None; + if map { + let size = block_mut.size() as usize; + let ptr = unsafe { + block_mut + .map(AshMemoryDevice::wrap(&ri.device), 0, size) + .map_err(EgvError::MapMemory)? + }; + let slice = unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), size) }; + mapping = Some(slice as *mut [u8]); + } + drop(block_mut); + deallocate.forget(); + Ok(EgvAllocatedMemory { + ri: ri.clone(), + block: block.into_inner(), + mapping, + }) + } + + fn fill_index_buffer( + &self, + sync: &mut EgvRendererCache, + primitives: &[ClippedPrimitive], + ) -> Result { + let indices: Vec<_> = primitives + .iter() + .filter_map(|c| match &c.primitive { + Primitive::Mesh(m) => Some(&m.indices), + Primitive::Callback(_) => None, + }) + .flat_map(|i| i.iter().copied()) + .collect(); + let indices: &[u8] = uapi::as_bytes(&*indices); + let buffer = self.get_device_local_buffer( + sync, + indices.len() as u64, + BufferUsageFlags::INDEX_BUFFER, + )?; + buffer.upload(indices)?; + Ok(buffer) + } + + fn get_sampler( + self: &Rc, + samplers: &mut AHashMap>, + options: &TextureOptions, + ) -> Result, EgvError> { + let sampler = match samplers.entry(*options) { + Entry::Occupied(o) => o.get().clone(), + Entry::Vacant(v) => { + let s = self.create_sampler(options)?; + v.insert(s).clone() + } + }; + Ok(sampler) + } + + fn create_sampler( + self: &Rc, + options: &TextureOptions, + ) -> Result, EgvError> { + let address_mode = match options.wrap_mode { + TextureWrapMode::ClampToEdge => SamplerAddressMode::CLAMP_TO_EDGE, + TextureWrapMode::Repeat => SamplerAddressMode::REPEAT, + TextureWrapMode::MirroredRepeat => SamplerAddressMode::MIRRORED_REPEAT, + }; + let map_filter = |f: TextureFilter| match f { + TextureFilter::Nearest => Filter::NEAREST, + TextureFilter::Linear => Filter::LINEAR, + }; + let create_info = SamplerCreateInfo::default() + .mag_filter(map_filter(options.magnification)) + .min_filter(map_filter(options.minification)) + .address_mode_u(address_mode) + .address_mode_v(address_mode) + .address_mode_w(address_mode) + .mipmap_mode(SamplerMipmapMode::NEAREST) + .max_anisotropy(1.0) + .min_lod(0.0) + .max_lod(0.25) + .border_color(BorderColor::FLOAT_TRANSPARENT_BLACK); + let ri = &self.ri; + let sampler = unsafe { + ri.device + .create_sampler(&create_info, None) + .map_err(EgvError::CreateSampler)? + }; + Ok(Rc::new(VkSampler { + ri: ri.clone(), + options: *options, + sampler, + })) + } + + pub fn support(&self) -> &[Support] { + &self.ri.dmabuf_support + } + + pub fn max_texture_side(&self) -> usize { + self.ri.max_tex_width.min(self.ri.max_tex_height) as usize + } + + pub fn create_context(self: &Rc) -> Rc { + Rc::new(EgvContext { + renderer: self.clone(), + id: self.ri.context_ids.next(), + }) + } +} + +async fn wait_for_submissions( + submissions: Rc, + dev: Rc, + ring: Rc, +) { + loop { + submissions.task_has_pending.set(false); + let pending = submissions.pending.pop().await; + submissions.task_has_pending.set(true); + if let Some(sync) = &pending.sync + && let Err(e) = sync.try_signaled(&ring).await + { + log::warn!( + "Could not wait for sync file to become readable: {}", + ErrorFmt(e), + ); + dev.wait_idle(); + } + pending.vulkan_sync.handle_validation(); + } +} + +impl EgvRendererInner { + fn wait_idle(&self) { + log::warn!("Blocking"); + let res = unsafe { self.device.device_wait_idle() }; + if let Err(e) = res { + log::error!("Could not wait for device idle: {}", ErrorFmt(e)); + log::error!("This is unsound."); + } + self.submissions.pending.clear(); + } +} + +impl EgvContext { + pub fn import_framebuffer( + self: &Rc, + bo: &Rc, + ) -> Result, EgvError> { + let ri = &self.renderer.ri; + let buf = bo.dmabuf(); + if buf.format != EGV_FORMAT { + return Err(EgvError::WrongFbFormat); + } + if buf.width <= 0 || buf.height <= 0 { + return Err(EgvError::NonPositiveFbSize); + } + let Some(support) = ri + .dmabuf_support + .iter() + .find(|s| s.modifier == buf.modifier) + else { + return Err(EgvError::UnsupportedModifier); + }; + if buf.planes.len() != support.planes { + return Err(EgvError::WrongPlaneCount); + } + let width = buf.width as u32; + let height = buf.height as u32; + if width > support.max_width || height > support.max_height { + return Err(EgvError::TooLarge); + } + let dev = &ri.device; + let disjoint = buf.is_disjoint(); + let image = { + let image_create_flags = match disjoint { + true => ImageCreateFlags::DISJOINT, + false => ImageCreateFlags::empty(), + }; + let plane_layouts: PlaneVec<_> = buf + .planes + .iter() + .map(|p| SubresourceLayout { + offset: p.offset as _, + row_pitch: p.stride as _, + size: 0, + array_pitch: 0, + depth_pitch: 0, + }) + .collect(); + let mut mod_info = ImageDrmFormatModifierExplicitCreateInfoEXT::default() + .drm_format_modifier(buf.modifier) + .plane_layouts(&plane_layouts); + let mut memory_image_create_info = ExternalMemoryImageCreateInfo::default() + .handle_types(ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); + let info = ImageCreateInfo::default() + .flags(image_create_flags) + .image_type(ImageType::TYPE_2D) + .format(VK_FB_FORMAT) + .extent(Extent3D { + width, + height, + depth: 1, + }) + .mip_levels(1) + .array_layers(1) + .samples(SampleCountFlags::TYPE_1) + .tiling(ImageTiling::DRM_FORMAT_MODIFIER_EXT) + .usage(ImageUsageFlags::COLOR_ATTACHMENT) + .sharing_mode(SharingMode::EXCLUSIVE) + .initial_layout(ImageLayout::UNDEFINED) + .push_next(&mut mod_info) + .push_next(&mut memory_image_create_info); + unsafe { + dev.create_image(&info, None) + .map_err(EgvError::CreateImage)? + } + }; + let destroy_image = on_drop(|| unsafe { dev.destroy_image(image, None) }); + let mut memories = PlaneVec::new(); + let mut free_memories = PlaneVec::new(); + { + let num_device_memories = match disjoint { + true => buf.planes.len(), + false => 1, + }; + let mut bind_image_plane_memory_infos = PlaneVec::new(); + for plane_idx in 0..num_device_memories { + let dma_buf_plane = &buf.planes[plane_idx]; + let mut image_memory_requirements_info = + ImageMemoryRequirementsInfo2::default().image(image); + let mut image_plane_memory_requirements_info; + if disjoint { + let plane_aspect = match plane_idx { + 0 => ImageAspectFlags::MEMORY_PLANE_0_EXT, + 1 => ImageAspectFlags::MEMORY_PLANE_1_EXT, + 2 => ImageAspectFlags::MEMORY_PLANE_2_EXT, + 3 => ImageAspectFlags::MEMORY_PLANE_3_EXT, + _ => unreachable!(), + }; + image_plane_memory_requirements_info = + ImagePlaneMemoryRequirementsInfo::default().plane_aspect(plane_aspect); + image_memory_requirements_info = image_memory_requirements_info + .push_next(&mut image_plane_memory_requirements_info); + bind_image_plane_memory_infos + .push(BindImagePlaneMemoryInfo::default().plane_aspect(plane_aspect)); + } + let mut memory_requirements = MemoryRequirements2::default(); + unsafe { + dev.get_image_memory_requirements2( + &image_memory_requirements_info, + &mut memory_requirements, + ); + } + let mut fd_props = MemoryFdPropertiesKHR::default(); + unsafe { + ri.external_memory_fd + .get_memory_fd_properties( + ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, + dma_buf_plane.fd.raw(), + &mut fd_props, + ) + .map_err(EgvError::GetMemoryFdProperties)?; + } + let memory_type_bits = memory_requirements.memory_requirements.memory_type_bits + & fd_props.memory_type_bits; + if memory_type_bits == 0 { + return Err(EgvError::NoMemoryTypeForImport); + } + let fd = uapi::fcntl_dupfd_cloexec(dma_buf_plane.fd.raw(), 0) + .map_err(Into::into) + .map_err(EgvError::DupDmaBuf)?; + let mut memory_dedicated_allocate_info = + MemoryDedicatedAllocateInfo::default().image(image); + let mut import_memory_fd_info = ImportMemoryFdInfoKHR::default() + .fd(fd.raw()) + .handle_type(ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); + let memory_allocate_info = MemoryAllocateInfo::default() + .allocation_size(memory_requirements.memory_requirements.size) + .memory_type_index(memory_type_bits.trailing_zeros() as _) + .push_next(&mut import_memory_fd_info) + .push_next(&mut memory_dedicated_allocate_info); + let device_memory = unsafe { + dev.allocate_memory(&memory_allocate_info, None) + .map_err(EgvError::ImportMemory)? + }; + let _ = fd.unwrap(); + memories.push(device_memory); + free_memories.push(on_drop(move || unsafe { + dev.free_memory(device_memory, None) + })); + } + let mut bind_image_memory_infos = PlaneVec::new(); + let mut bind_image_plane_memory_infos = bind_image_plane_memory_infos.iter_mut(); + for mem in memories.iter().copied() { + let mut info = BindImageMemoryInfo::default().image(image).memory(mem); + if disjoint { + info = info.push_next(bind_image_plane_memory_infos.next().unwrap()); + } + bind_image_memory_infos.push(info); + } + unsafe { + dev.bind_image_memory2(&bind_image_memory_infos) + .map_err(EgvError::BindImageMemory)?; + } + } + let image_view = { + let info = ImageViewCreateInfo::default() + .image(image) + .view_type(ImageViewType::TYPE_2D) + .format(VK_FB_FORMAT) + .components(ComponentMapping { + r: ComponentSwizzle::IDENTITY, + g: ComponentSwizzle::IDENTITY, + b: ComponentSwizzle::IDENTITY, + a: ComponentSwizzle::IDENTITY, + }) + .subresource_range(IMAGE_SUBRESOURCE_RANGE); + unsafe { + dev.create_image_view(&info, None) + .map_err(EgvError::CreateImageView)? + } + }; + let destroy_image_view = on_drop(|| unsafe { dev.destroy_image_view(image_view, None) }); + destroy_image_view.forget(); + free_memories.into_iter().for_each(|f| f.forget()); + destroy_image.forget(); + let image = Rc::new(EgvImage { + ri: ri.clone(), + width, + height, + image, + image_view, + _memory: EgvImportedMemory { + ri: ri.clone(), + _bo: bo.clone(), + memories, + }, + layout: Cell::new(ImageLayout::UNDEFINED), + }); + let fb = Rc::new(EgvFramebuffer { + renderer: self.renderer.clone(), + ctx: self.clone(), + image, + }); + Ok(fb) + } +} + +impl EgvFramebuffer { + fn create_vertex_buffer( + &self, + sync: &mut EgvRendererCache, + pixels_per_point: f32, + primitives: &[ClippedPrimitive], + offset: (f32, f32), + ) -> Result { + let width = self.image.width as f32 / pixels_per_point; + let height = self.image.height as f32 / pixels_per_point; + let vertices: Vec<_> = primitives + .iter() + .filter_map(|c| match &c.primitive { + Primitive::Mesh(m) => Some(&m.vertices), + Primitive::Callback(_) => None, + }) + .flat_map(|i| i.iter().copied()) + .map(|mut v| { + v.pos.x = 2.0 * (v.pos.x + offset.0) / width - 1.0; + v.pos.y = 2.0 * (v.pos.y + offset.1) / height - 1.0; + VkVertex { + pos: [v.pos.x, v.pos.y], + uv: [v.uv.x, v.uv.y], + color: [v.color.r(), v.color.g(), v.color.b(), v.color.a()], + } + }) + .collect(); + let vertices: &[u8] = uapi::as_bytes(&*vertices); + let buffer = self.renderer.get_device_local_buffer( + sync, + vertices.len() as u64, + BufferUsageFlags::VERTEX_BUFFER, + )?; + buffer.upload(vertices)?; + Ok(buffer) + } + + pub fn render( + &self, + delta: TexturesDelta, + pixels_per_point: f32, + primitives: &[ClippedPrimitive], + offset: (f32, f32), + sync_file: Option<&SyncFile>, + ) -> Result, EgvError> { + let renderer = &self.renderer; + let ri = &renderer.ri; + let dev = &ri.device; + let cache = &mut *ri.cache.borrow_mut(); + let index_buffer = self.renderer.fill_index_buffer(cache, primitives)?; + let vertex_buffer = + self.create_vertex_buffer(cache, pixels_per_point, primitives, offset)?; + let uploads = &mut cache.upload_todos; + uploads.clear(); + for (id, delta) in delta.set { + let id = (self.ctx.id, id); + let mut options = delta.options; + options.mipmap_mode = None; + let mut create_sampled_image = || -> Result<_, EgvError> { + let sampler = renderer.get_sampler(&mut cache.samplers, &options)?; + let image = renderer.create_image(&delta.image)?; + let sampled = EgvSampledImage { + image: image.clone(), + sampler, + }; + Ok((image, sampled)) + }; + let image = match cache.images.entry(id) { + Entry::Occupied(mut o) => { + let t = o.get(); + if delta.pos.is_none() + && [t.image.width as usize, t.image.height as usize] != delta.image.size() + { + let (image, sampled) = create_sampled_image()?; + *o.get_mut() = sampled; + image + } else if t.sampler.options != options { + let sampler = self.renderer.get_sampler(&mut cache.samplers, &options)?; + let image = t.image.clone(); + *o.get_mut() = EgvSampledImage { + image: image.clone(), + sampler, + }; + image + } else { + t.image.clone() + } + } + Entry::Vacant(v) => { + if delta.pos.is_some() { + return Err(EgvError::PartialTextureUpdateForUnknownTexture(id.1)); + } + let (image, sampled) = create_sampled_image()?; + v.insert(sampled); + image + } + }; + if let Some(pos) = delta.pos { + let x2 = pos[0].saturating_add(delta.image.width()); + let y2 = pos[1].saturating_add(delta.image.height()); + if x2 > image.width as usize || y2 > image.height as usize { + return Err(EgvError::TextureUpdateOutOfBounds(id.1)); + } + } + let size = delta.image.width() as u64 * delta.image.height() as u64 * SRGB_FORMAT_BPP; + uploads.push((image.clone(), renderer.create_staging_buffer(size)?, delta)); + } + let buffer_memory_barriers = &mut cache.buffer_memory_barriers; + buffer_memory_barriers.clear(); + let initial_image_barriers = &mut cache.initial_image_memory_barriers; + initial_image_barriers.clear(); + let final_image_barriers = &mut cache.final_image_memory_barriers; + final_image_barriers.clear(); + for (image, buf, delta) in &*uploads { + match &delta.image { + ImageData::Color(c) => { + let pixels = unsafe { AssertPacked::new(&c.pixels as &[_]) }; + buf.upload(uapi::as_bytes(pixels))?; + } + } + buffer_memory_barriers.push( + BufferMemoryBarrier2::default() + .src_access_mask(AccessFlags2::HOST_WRITE) + .src_stage_mask(PipelineStageFlags2::HOST) + .dst_access_mask(AccessFlags2::TRANSFER_READ) + .dst_stage_mask(PipelineStageFlags2::TRANSFER) + .buffer(buf.buffer) + .size(WHOLE_SIZE), + ); + initial_image_barriers.push( + ImageMemoryBarrier2::default() + .src_access_mask(AccessFlags2::SHADER_READ) + .src_stage_mask(PipelineStageFlags2::FRAGMENT_SHADER) + .dst_access_mask(AccessFlags2::TRANSFER_WRITE) + .dst_stage_mask(PipelineStageFlags2::TRANSFER) + .old_layout(image.layout.get()) + .new_layout(ImageLayout::TRANSFER_DST_OPTIMAL) + .image(image.image) + .subresource_range(IMAGE_SUBRESOURCE_RANGE), + ); + final_image_barriers.push( + ImageMemoryBarrier2::default() + .src_access_mask(AccessFlags2::TRANSFER_WRITE) + .src_stage_mask(PipelineStageFlags2::TRANSFER) + .dst_access_mask(AccessFlags2::SHADER_READ) + .dst_stage_mask(PipelineStageFlags2::FRAGMENT_SHADER) + .old_layout(ImageLayout::TRANSFER_DST_OPTIMAL) + .new_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL) + .image(image.image) + .subresource_range(IMAGE_SUBRESOURCE_RANGE), + ); + } + let cmd = renderer.allocate_command_buffer()?; + let buf = cmd.buf; + { + let begin_info = + CommandBufferBeginInfo::default().flags(CommandBufferUsageFlags::ONE_TIME_SUBMIT); + unsafe { + dev.begin_command_buffer(buf, &begin_info) + .map_err(EgvError::BeginCommandBuffer)?; + } + } + unsafe { + let info = DependencyInfo::default() + .buffer_memory_barriers(&buffer_memory_barriers) + .image_memory_barriers(&initial_image_barriers); + dev.cmd_pipeline_barrier2(buf, &info); + } + for (image, staging, delta) in &*uploads { + let x = delta.pos.unwrap_or_default()[0] as i32; + let y = delta.pos.unwrap_or_default()[1] as i32; + let region = BufferImageCopy2::default() + .image_subresource(IMAGE_SUBRESOURCE_LAYERS) + .image_offset(Offset3D { x, y, z: 0 }) + .image_extent(Extent3D { + width: delta.image.width() as u32, + height: delta.image.height() as u32, + depth: 1, + }); + let info = CopyBufferToImageInfo2::default() + .src_buffer(staging.buffer) + .dst_image(image.image) + .dst_image_layout(ImageLayout::TRANSFER_DST_OPTIMAL) + .regions(slice::from_ref(®ion)); + unsafe { + dev.cmd_copy_buffer_to_image2(buf, &info); + } + } + { + final_image_barriers.push( + ImageMemoryBarrier2::default() + .dst_access_mask(AccessFlags2::COLOR_ATTACHMENT_WRITE) + .dst_stage_mask(PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .old_layout(ImageLayout::GENERAL) + .new_layout(ImageLayout::GENERAL) + .src_queue_family_index(QUEUE_FAMILY_FOREIGN_EXT) + .dst_queue_family_index(ri.queue_family) + .image(self.image.image) + .subresource_range(IMAGE_SUBRESOURCE_RANGE), + ); + final_image_barriers.push( + ImageMemoryBarrier2::default() + .src_access_mask(AccessFlags2::COLOR_ATTACHMENT_WRITE) + .src_stage_mask(PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .dst_access_mask(AccessFlags2::COLOR_ATTACHMENT_WRITE) + .dst_stage_mask(PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .old_layout(ImageLayout::GENERAL) + .new_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .src_queue_family_index(ri.queue_family) + .dst_queue_family_index(ri.queue_family) + .image(self.image.image) + .subresource_range(IMAGE_SUBRESOURCE_RANGE), + ) + } + unsafe { + let info = DependencyInfo::default().image_memory_barriers(&final_image_barriers); + dev.cmd_pipeline_barrier2(buf, &info); + } + for primitive in primitives { + match &primitive.primitive { + Primitive::Mesh(m) => { + if cache.images.not_contains_key(&(self.ctx.id, m.texture_id)) { + return Err(EgvError::UnknownTexture(m.texture_id)); + } + } + Primitive::Callback(_) => { + unreachable!() + } + } + } + { + let rendering_attachment_info = RenderingAttachmentInfo::default() + .image_view(self.image.image_view) + .image_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .load_op(AttachmentLoadOp::DONT_CARE) + .store_op(AttachmentStoreOp::STORE); + let rendering_info = RenderingInfoKHR::default() + .render_area(Rect2D { + offset: Default::default(), + extent: Extent2D { + width: self.image.width, + height: self.image.height, + }, + }) + .layer_count(1) + .color_attachments(slice::from_ref(&rendering_attachment_info)); + unsafe { + dev.cmd_begin_rendering(buf, &rendering_info); + } + } + if primitives.is_not_empty() { + unsafe { + dev.cmd_bind_pipeline(buf, PipelineBindPoint::GRAPHICS, ri.pipeline); + dev.cmd_bind_index_buffer(buf, index_buffer.buffer, 0, IndexType::UINT32); + dev.cmd_bind_vertex_buffers(buf, 0, &[vertex_buffer.buffer], &[0]); + dev.cmd_set_viewport( + buf, + 0, + &[Viewport { + x: 0.0, + y: 0.0, + width: self.image.width as f32, + height: self.image.height as f32, + min_depth: 0.0, + max_depth: 1.0, + }], + ); + } + } + let mut first_index = 0; + let mut vertex_offset = 0; + let mut sampled_images = Vec::with_capacity(primitives.len()); + for primitive in primitives { + let mesh = match &primitive.primitive { + Primitive::Mesh(m) => m, + Primitive::Callback(_) => unreachable!(), + }; + let sampled = cache.images.get(&(self.ctx.id, mesh.texture_id)).unwrap(); + sampled_images.push(sampled.clone()); + let image_info = DescriptorImageInfo::default() + .sampler(sampled.sampler.sampler) + .image_view(sampled.image.image_view) + .image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL); + let write_descriptor_set = WriteDescriptorSet::default() + .descriptor_type(DescriptorType::COMBINED_IMAGE_SAMPLER) + .image_info(slice::from_ref(&image_info)); + unsafe { + ri.push_descriptor.cmd_push_descriptor_set( + buf, + PipelineBindPoint::GRAPHICS, + ri.pipeline_layout, + 0, + slice::from_ref(&write_descriptor_set), + ); + } + { + let c = primitive.clip_rect; + let x1 = ((c.min.x + offset.0) * pixels_per_point).floor().max(0.0) as i32; + let y1 = ((c.min.y + offset.1) * pixels_per_point).floor().max(0.0) as i32; + let x2 = ((c.max.x + offset.0) * pixels_per_point).ceil().max(0.0) as i32; + let y2 = ((c.max.y + offset.1) * pixels_per_point).ceil().max(0.0) as i32; + unsafe { + dev.cmd_set_scissor( + buf, + 0, + &[Rect2D { + offset: Offset2D { x: x1, y: y1 }, + extent: Extent2D { + width: x2.wrapping_sub(x1) as u32, + height: y2.wrapping_sub(y1) as u32, + }, + }], + ); + } + } + let index_count = mesh.indices.len() as u32; + unsafe { + dev.cmd_draw_indexed(buf, index_count, 1, first_index, vertex_offset, 0); + } + first_index += index_count; + vertex_offset += mesh.vertices.len() as i32; + } + unsafe { + dev.cmd_end_rendering(buf); + } + { + final_image_barriers.clear(); + final_image_barriers.push( + ImageMemoryBarrier2::default() + .src_access_mask(AccessFlags2::COLOR_ATTACHMENT_WRITE) + .src_stage_mask(PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .dst_access_mask(AccessFlags2::COLOR_ATTACHMENT_WRITE) + .dst_stage_mask(PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .old_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .new_layout(ImageLayout::GENERAL) + .src_queue_family_index(ri.queue_family) + .dst_queue_family_index(ri.queue_family) + .image(self.image.image) + .subresource_range(IMAGE_SUBRESOURCE_RANGE), + ); + final_image_barriers.push( + ImageMemoryBarrier2::default() + .src_access_mask(AccessFlags2::COLOR_ATTACHMENT_WRITE) + .src_stage_mask(PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT) + .old_layout(ImageLayout::GENERAL) + .new_layout(ImageLayout::GENERAL) + .src_queue_family_index(ri.queue_family) + .dst_queue_family_index(QUEUE_FAMILY_FOREIGN_EXT) + .image(self.image.image) + .subresource_range(IMAGE_SUBRESOURCE_RANGE), + ); + unsafe { + let info = DependencyInfo::default().image_memory_barriers(&final_image_barriers); + dev.cmd_pipeline_barrier2(buf, &info); + } + } + unsafe { + dev.end_command_buffer(buf) + .map_err(EgvError::EndCommandBuffer)?; + } + let mut semaphore = None; + let mut vk_semaphores = ArrayVec::<_, 1>::new(); + if let Some(sync_file) = sync_file { + let s = match cache.semaphores.pop() { + Some(f) => f, + None => renderer.create_semaphore()?, + }; + s.import(sync_file)?; + let info = SemaphoreSubmitInfo::default() + .semaphore(s.semaphore) + .stage_mask(PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT); + vk_semaphores.push(info); + semaphore = Some(s); + } + let command_buffer_info = CommandBufferSubmitInfo::default().command_buffer(buf); + let mut submit_info = SubmitInfo2::default() + .command_buffer_infos(slice::from_ref(&command_buffer_info)) + .wait_semaphore_infos(&vk_semaphores); + let mut semaphore_submit_info = SemaphoreSubmitInfo::default(); + let vulkan_sync = ri.create_sync( + renderer.timeline_semaphore.as_ref(), + &mut semaphore_submit_info, + &mut submit_info, + )?; + unsafe { + dev.queue_submit2(ri.queue, slice::from_ref(&submit_info), vulkan_sync.fence()) + .map_err(EgvError::Submit)?; + } + for id in delta.free { + cache.images.remove(&(self.ctx.id, id)); + } + let mut used_uploads = Vec::with_capacity(uploads.len()); + for (image, staging, _) in uploads.drain(..) { + image.layout.set(ImageLayout::SHADER_READ_ONLY_OPTIMAL); + used_uploads.push((image, staging)); + } + let sync = vulkan_sync.to_sync(|| ri.wait_idle()); + let pending = Pending { + ri: ri.clone(), + sync: sync.clone(), + semaphore, + vulkan_sync, + _cmd: cmd, + _uploads: used_uploads, + _sampled: sampled_images, + _fb: self.image.clone(), + index_buffer: Some(index_buffer), + vertex_buffer: Some(vertex_buffer), + }; + ri.submissions.pending.push(pending); + Ok(sync) + } +} + +impl EgvBuffer { + fn upload(&self, data: &[u8]) -> Result<(), EgvError> { + assert!(self.mapping.len() >= data.len()); + unsafe { + ptr::copy_nonoverlapping(data.as_ptr(), self.mapping.cast(), data.len()); + } + if !self.host_coherent { + let m = &self.memory; + let mask = m.ri.non_coherent_atom_size - 1; + let lo = m.block.offset() & !mask; + let hi = (m.block.offset() + data.len() as u64 + mask) & !mask; + let range = MappedMemoryRange::default() + .memory(*m.block.memory()) + .offset(lo) + .size(hi - lo); + unsafe { + m.ri.device + .flush_mapped_memory_ranges(slice::from_ref(&range)) + .map_err(EgvError::FlushMemory)?; + } + } + Ok(()) + } +} + +impl EgvSemaphore { + fn import(&self, sync_file: &SyncFile) -> Result<(), EgvError> { + let fd = uapi::fcntl_dupfd_cloexec(sync_file.raw(), 0) + .map_err(Into::into) + .map_err(EgvError::DupSyncFile)?; + let info = ImportSemaphoreFdInfoKHR::default() + .flags(SemaphoreImportFlags::TEMPORARY) + .semaphore(self.semaphore) + .handle_type(ExternalSemaphoreHandleTypeFlags::SYNC_FD) + .fd(fd.raw()); + unsafe { + self.ri + .external_semaphore_fd + .import_semaphore_fd(&info) + .map_err(EgvError::ImportSyncFile)?; + } + let _ = fd.unwrap(); + Ok(()) + } +} + +impl Drop for EgvBuffer { + fn drop(&mut self) { + unsafe { + self.ri.device.destroy_buffer(self.buffer, None); + } + } +} + +impl Drop for EgvRendererInner { + fn drop(&mut self) { + let dev = &self.device; + unsafe { + self.allocator + .borrow_mut() + .cleanup(AshMemoryDevice::wrap(dev)); + dev.destroy_pipeline(self.pipeline, None); + dev.destroy_command_pool(self.pool, None); + dev.destroy_pipeline_layout(self.pipeline_layout, None); + dev.destroy_descriptor_set_layout(self.descriptor_set_layout, None); + dev.destroy_shader_module(self.vert, None); + dev.destroy_shader_module(self.frag, None); + dev.destroy_device(None); + } + } +} + +impl Drop for EgvImportedMemory { + fn drop(&mut self) { + unsafe { + for &memory in &self.memories { + self.ri.device.free_memory(memory, None); + } + } + } +} + +impl Drop for EgvAllocatedMemory { + fn drop(&mut self) { + if self.mapping.is_some() { + unsafe { + self.block.unmap(AshMemoryDevice::wrap(&self.ri.device)); + } + } + unsafe { + self.ri.allocator.borrow_mut().dealloc( + AshMemoryDevice::wrap(&self.ri.device), + ManuallyDrop::take(&mut self.block), + ); + } + } +} + +impl Drop for EgvCommandBuffer { + fn drop(&mut self) { + let ri = &self.ri; + unsafe { + ri.device + .free_command_buffers(ri.pool, slice::from_ref(&self.buf)); + } + } +} + +impl Drop for VkSampler { + fn drop(&mut self) { + unsafe { + self.ri.device.destroy_sampler(self.sampler, None); + } + } +} + +impl Drop for EgvImage { + fn drop(&mut self) { + let dev = &self.ri.device; + unsafe { + dev.destroy_image_view(self.image_view, None); + dev.destroy_image(self.image, None); + } + } +} + +impl Drop for Pending { + fn drop(&mut self) { + let cache = &mut *self.ri.cache.borrow_mut(); + if let Some(v) = self.semaphore.take() { + cache.semaphores.push(v); + } + if let Some(v) = self.index_buffer.take() { + cache.device_local_buffers.push(v); + } + if let Some(v) = self.vertex_buffer.take() { + cache.device_local_buffers.push(v); + } + } +} + +impl Drop for EgvSemaphore { + fn drop(&mut self) { + let dev = &self.ri.device; + unsafe { + dev.destroy_semaphore(self.semaphore, None); + } + } +} + +impl Drop for EgvRenderer { + fn drop(&mut self) { + let ri = &self.ri; + if ri.submissions.pending.is_not_empty() || ri.submissions.task_has_pending.get() { + ri.wait_idle(); + } + ri.cache.take(); + } +} + +impl Drop for EgvContext { + fn drop(&mut self) { + self.renderer + .ri + .cache + .borrow_mut() + .images + .retain(|&(id, _), _| id != self.id); + } +} + +impl VulkanDeviceInf for EgvRendererInner { + fn instance(&self) -> &VulkanCoreInstance { + &self.instance + } + + fn device(&self) -> &Device { + &self.device + } + + fn external_fence_fd(&self) -> &external_fence_fd::Device { + &self.external_fence_fd + } + + fn external_semaphore_fd(&self) -> &external_semaphore_fd::Device { + &self.external_semaphore_fd + } + + fn supports_timeline_opaque_export(&self) -> bool { + self.supports_timeline_opaque_export + } + + fn sync_ctx(&self) -> Option<&Rc> { + self.sync_ctx.as_ref() + } + + fn eventfd_cache(&self) -> &Rc { + &self.eventfd_cache + } +} diff --git a/src/egui_adapter/icons.ttf b/src/egui_adapter/icons.ttf new file mode 100644 index 00000000..0abd161b Binary files /dev/null and b/src/egui_adapter/icons.ttf differ diff --git a/src/egui_adapter/shaders/shader.frag b/src/egui_adapter/shaders/shader.frag new file mode 100644 index 00000000..56defac2 --- /dev/null +++ b/src/egui_adapter/shaders/shader.frag @@ -0,0 +1,13 @@ +#version 450 + +layout(location = 0) in vec4 color; +layout(location = 1) in vec2 pos; + +layout(binding = 0, set = 0) uniform sampler2D tex; + +layout(location = 0) out vec4 res; + +void main() { + vec4 src = texture(tex, pos); + res = color * src; +} diff --git a/src/egui_adapter/shaders/shader.vert b/src/egui_adapter/shaders/shader.vert new file mode 100644 index 00000000..73584e1a --- /dev/null +++ b/src/egui_adapter/shaders/shader.vert @@ -0,0 +1,19 @@ +#version 450 + +layout(location = 0) in vec2 if_pos; +layout(location = 1) in vec2 it_pos; +layout(location = 2) in vec4 i_color; + +layout(location = 0) out vec4 o_color; +layout(location = 1) out vec2 ot_pos; + +void main() { + o_color = i_color; + o_color.rgb = mix( + o_color.rgb / vec3(12.92), + pow((o_color.rgb + vec3(0.055)) / vec3(1.055), vec3(2.4)), + greaterThan(o_color.rgb, vec3(0.04045)) + ); + ot_pos = it_pos; + gl_Position = vec4(if_pos.x, if_pos.y, 0.0, 1.0); +} diff --git a/src/egui_adapter/shaders_bin/shader.frag.spv b/src/egui_adapter/shaders_bin/shader.frag.spv new file mode 100644 index 00000000..28f8c6f5 Binary files /dev/null and b/src/egui_adapter/shaders_bin/shader.frag.spv differ diff --git a/src/egui_adapter/shaders_bin/shader.vert.spv b/src/egui_adapter/shaders_bin/shader.vert.spv new file mode 100644 index 00000000..20e61987 Binary files /dev/null and b/src/egui_adapter/shaders_bin/shader.vert.spv differ diff --git a/src/egui_adapter/shaders_hash.txt b/src/egui_adapter/shaders_hash.txt new file mode 100644 index 00000000..fab5b02f --- /dev/null +++ b/src/egui_adapter/shaders_hash.txt @@ -0,0 +1,2 @@ +7eb8fae39ae513bc4f6973c12227aa4aa43734bdf34c90e1b3b69294ad98db87 src/egui_adapter/shaders/shader.frag +501f4d0c5c5f10a371659b89f12d87abb03e5b57a31dbae5f3c6ca5726e4db01 src/egui_adapter/shaders/shader.vert diff --git a/src/fontconfig.rs b/src/fontconfig.rs index 413557a0..76275bee 100644 --- a/src/fontconfig.rs +++ b/src/fontconfig.rs @@ -34,14 +34,12 @@ pub enum FontconfigError { } #[derive(Debug)] -#[expect(dead_code)] pub struct Font { pub fullname: String, pub file: PathBuf, pub index: Option, } -#[expect(dead_code)] pub fn match_font(family: &str) -> Result { thread_local! { static CONFIG: *mut FcConfig = FcConfigGetCurrent(); diff --git a/src/globals.rs b/src/globals.rs index 07c18464..ad4b64be 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -256,7 +256,7 @@ pub struct Globals { removed: CopyHashMap>, pub outputs: CopyHashMap>, pub seats: CopyHashMap>, - singletons: StaticMap, + pub singletons: StaticMap, exposed: StaticMap>, } diff --git a/src/ifs.rs b/src/ifs.rs index 2814a144..f1168908 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -21,6 +21,7 @@ pub mod jay_ei_session_builder; pub mod jay_idle; pub mod jay_input; pub mod jay_log_file; +pub mod jay_open_control_center_request; pub mod jay_output; pub mod jay_pointer; pub mod jay_popup_ext_manager_v1; diff --git a/src/ifs/head_management.rs b/src/ifs/head_management.rs index 8b1bf6bc..46dd514b 100644 --- a/src/ifs/head_management.rs +++ b/src/ifs/head_management.rs @@ -105,14 +105,13 @@ pub struct ReadOnlyHeadState { } impl ReadOnlyHeadState { - #[expect(dead_code)] pub fn borrow(&self) -> Ref<'_, HeadState> { self.state.borrow() } } impl HeadState { - fn update_in_compositor_space(&mut self, wl_output: Option) { + pub fn update_in_compositor_space(&mut self, wl_output: Option) { self.in_compositor_space = false; self.wl_output = None; if !self.connector_enabled { @@ -131,7 +130,7 @@ impl HeadState { self.wl_output = wl_output; } - fn update_size(&mut self) { + pub fn update_size(&mut self) { self.size = OutputNode::calculate_extents_(self.mode, self.transform, self.scale, self.position) .size(); @@ -213,7 +212,7 @@ pub enum HeadCommonError { } pub struct HeadManagers { - name: HeadName, + pub name: HeadName, state: Rc>, managers: CopyHashMap<(ClientId, JayHeadManagerSessionV1Id), Rc>, } @@ -235,7 +234,6 @@ impl HeadManagers { } } - #[expect(dead_code)] pub fn state(&self) -> ReadOnlyHeadState { ReadOnlyHeadState { state: self.state.clone(), diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index d0f8c3d4..e37c11a3 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -11,6 +11,7 @@ use { jay_idle::JayIdle, jay_input::JayInput, jay_log_file::JayLogFile, + jay_open_control_center_request::JayOpenControlCenterRequest, jay_output::JayOutput, jay_pointer::JayPointer, jay_randr::JayRandr, @@ -77,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError); impl Global for JayCompositorGlobal { fn version(&self) -> u32 { - 27 + 28 } fn required_caps(&self) -> ClientCaps { @@ -541,6 +542,25 @@ impl JayCompositorRequestHandler for JayCompositor { }); Ok(()) } + + fn open_control_center( + &self, + req: OpenControlCenter, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let obj = Rc::new(JayOpenControlCenterRequest { + id: req.id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + }); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + if let Err(e) = self.client.state.open_control_center() { + obj.send_failed(e); + } + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_idle.rs b/src/ifs/jay_idle.rs index a91a8b20..48cbd7a4 100644 --- a/src/ifs/jay_idle.rs +++ b/src/ifs/jay_idle.rs @@ -68,14 +68,14 @@ impl JayIdleRequestHandler for JayIdle { fn set_interval(&self, req: SetInterval, _slf: &Rc) -> Result<(), Self::Error> { let interval = Duration::from_secs(req.interval); let state = &self.client.state; - state.idle.set_timeout(interval); + state.idle.set_timeout(state, interval); Ok(()) } fn set_grace_period(&self, req: SetGracePeriod, _slf: &Rc) -> Result<(), Self::Error> { let period = Duration::from_secs(req.period); let state = &self.client.state; - state.idle.set_grace_period(period); + state.idle.set_grace_period(state, period); Ok(()) } } diff --git a/src/ifs/jay_input.rs b/src/ifs/jay_input.rs index 9fdbac8a..26a3bcec 100644 --- a/src/ifs/jay_input.rs +++ b/src/ifs/jay_input.rs @@ -14,7 +14,7 @@ use { LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER, LIBINPUT_CONFIG_CLICK_METHOD_NONE, }, object::{Object, Version}, - state::{DeviceHandlerData, InputDeviceData}, + state::{DeviceHandlerData, InputDeviceData, State}, utils::errorfmt::ErrorFmt, wire::{JayInputId, jay_input::*}, }, @@ -28,6 +28,7 @@ use { pub struct JayInput { pub id: JayInputId, pub client: Rc, + pub state: Rc, pub tracker: Tracker, pub version: Version, } @@ -41,6 +42,7 @@ impl JayInput { Self { id, client: client.clone(), + state: client.state.clone(), tracker: Default::default(), version, } @@ -309,7 +311,7 @@ impl JayInputRequestHandler for JayInput { LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE => InputDeviceAccelProfile::Adaptive, _ => return Err(JayInputError::UnknownAccelerationProfile(req.profile)), }; - dev.set_accel_profile(profile); + dev.set_accel_profile(&self.state, profile); Ok(()) }) } @@ -317,7 +319,7 @@ impl JayInputRequestHandler for JayInput { fn set_accel_speed(&self, req: SetAccelSpeed, _slf: &Rc) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_accel_speed(req.speed); + dev.set_accel_speed(&self.state, req.speed); Ok(()) }) } @@ -325,7 +327,7 @@ impl JayInputRequestHandler for JayInput { fn set_tap_enabled(&self, req: SetTapEnabled, _slf: &Rc) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_tap_enabled(req.enabled != 0); + dev.set_tap_enabled(&self.state, req.enabled != 0); Ok(()) }) } @@ -337,7 +339,7 @@ impl JayInputRequestHandler for JayInput { ) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_drag_enabled(req.enabled != 0); + dev.set_drag_enabled(&self.state, req.enabled != 0); Ok(()) }) } @@ -349,7 +351,7 @@ impl JayInputRequestHandler for JayInput { ) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_drag_lock_enabled(req.enabled != 0); + dev.set_drag_lock_enabled(&self.state, req.enabled != 0); Ok(()) }) } @@ -357,7 +359,7 @@ impl JayInputRequestHandler for JayInput { fn set_left_handed(&self, req: SetLeftHanded, _slf: &Rc) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_left_handed(req.enabled != 0); + dev.set_left_handed(&self.state, req.enabled != 0); Ok(()) }) } @@ -369,7 +371,7 @@ impl JayInputRequestHandler for JayInput { ) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_natural_scrolling_enabled(req.enabled != 0); + dev.set_natural_scrolling_enabled(&self.state, req.enabled != 0); Ok(()) }) } @@ -381,7 +383,7 @@ impl JayInputRequestHandler for JayInput { ) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_px_per_scroll_wheel(req.px); + dev.set_px_per_scroll_wheel(&self.state, req.px); Ok(()) }) } @@ -393,7 +395,7 @@ impl JayInputRequestHandler for JayInput { ) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_transform_matrix([[req.m11, req.m12], [req.m21, req.m22]]); + dev.set_transform_matrix(&self.state, [[req.m11, req.m12], [req.m21, req.m22]]); Ok(()) }) } @@ -410,7 +412,7 @@ impl JayInputRequestHandler for JayInput { self.or_error(|| { let seat = self.seat(req.seat)?; let dev = self.device(req.id)?; - dev.set_seat(Some(seat)); + dev.set_seat(&self.state, Some(seat)); Ok(()) }) } @@ -418,7 +420,7 @@ impl JayInputRequestHandler for JayInput { fn detach(&self, req: Detach, _slf: &Rc) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_seat(None); + dev.set_seat(&self.state, None); Ok(()) }) } @@ -459,7 +461,7 @@ impl JayInputRequestHandler for JayInput { fn set_device_keymap(&self, req: SetDeviceKeymap, _slf: &Rc) -> Result<(), Self::Error> { self.set_keymap_impl(&req.keymap, req.keymap_len, |map| { let dev = self.device(req.id)?; - dev.set_keymap(Some(map.clone())); + dev.set_keymap(&self.state, Some(map.clone())); Ok(()) }) } @@ -490,11 +492,11 @@ impl JayInputRequestHandler for JayInput { .find(|c| c.global.connector.name.to_ascii_lowercase() == namelc) .cloned(); match c { - Some(c) => dev.set_output(Some(&c.global)), + Some(c) => dev.set_output(&self.state, Some(&c.global)), _ => return Err(JayInputError::OutputNotConnected), } } - _ => dev.set_output(None), + _ => dev.set_output(&self.state, None), } Ok(()) }) @@ -507,7 +509,10 @@ impl JayInputRequestHandler for JayInput { ) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_calibration_matrix([[req.m00, req.m01, req.m02], [req.m10, req.m11, req.m12]]); + dev.set_calibration_matrix( + &self.state, + [[req.m00, req.m01, req.m02], [req.m10, req.m11, req.m12]], + ); Ok(()) }) } @@ -521,7 +526,7 @@ impl JayInputRequestHandler for JayInput { LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER => InputDeviceClickMethod::Clickfinger, _ => return Err(JayInputError::UnknownClickMethod(req.method)), }; - dev.set_click_method(method); + dev.set_click_method(&self.state, method); Ok(()) }) } @@ -533,7 +538,7 @@ impl JayInputRequestHandler for JayInput { ) -> Result<(), Self::Error> { self.or_error(|| { let dev = self.device(req.id)?; - dev.set_middle_button_emulation_enabled(req.enabled != 0); + dev.set_middle_button_emulation_enabled(&self.state, req.enabled != 0); Ok(()) }) } @@ -594,7 +599,7 @@ impl JayInputRequestHandler for JayInput { req.options, |map| { let dev = self.device(req.id)?; - dev.set_keymap(Some(map.clone())); + dev.set_keymap(&self.state, Some(map.clone())); Ok(()) }, ) diff --git a/src/ifs/jay_open_control_center_request.rs b/src/ifs/jay_open_control_center_request.rs new file mode 100644 index 00000000..a5c0ef5d --- /dev/null +++ b/src/ifs/jay_open_control_center_request.rs @@ -0,0 +1,53 @@ +use { + crate::{ + client::{Client, ClientError}, + leaks::Tracker, + object::{Object, Version}, + utils::errorfmt::ErrorFmt, + wire::{JayOpenControlCenterRequestId, jay_open_control_center_request::*}, + }, + std::{error::Error, rc::Rc}, + thiserror::Error, +}; + +pub struct JayOpenControlCenterRequest { + pub id: JayOpenControlCenterRequestId, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, +} + +impl JayOpenControlCenterRequest { + pub fn send_failed(&self, err: impl Error) { + let msg = &ErrorFmt(err).to_string(); + self.client.event(Failed { + self_id: self.id, + msg, + }); + } +} + +impl JayOpenControlCenterRequestRequestHandler for JayOpenControlCenterRequest { + type Error = JayOpenControlCenterRequestError; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = JayOpenControlCenterRequest; + version = self.version; +} + +impl Object for JayOpenControlCenterRequest {} + +simple_add_obj!(JayOpenControlCenterRequest); + +#[derive(Debug, Error)] +pub enum JayOpenControlCenterRequestError { + #[error(transparent)] + ClientError(Box), +} +efrom!(JayOpenControlCenterRequestError, ClientError); diff --git a/src/ifs/jay_randr.rs b/src/ifs/jay_randr.rs index 310b1716..156c7432 100644 --- a/src/ifs/jay_randr.rs +++ b/src/ifs/jay_randr.rs @@ -350,7 +350,7 @@ impl JayRandrRequestHandler for JayRandr { let Some(dev) = self.get_device(req.dev) else { return Ok(()); }; - dev.set_direct_scanout_enabled(req.enabled != 0); + dev.set_direct_scanout_enabled(&self.state, req.enabled != 0); Ok(()) } @@ -456,7 +456,7 @@ impl JayRandrRequestHandler for JayRandr { let Some(c) = self.get_output_node(req.output) else { return Ok(()); }; - c.schedule.set_cursor_hz(req.hz); + c.schedule.set_cursor_hz(&self.state, req.hz); Ok(()) } @@ -493,7 +493,7 @@ impl JayRandrRequestHandler for JayRandr { let Some(dev) = self.get_device(req.dev) else { return Ok(()); }; - dev.set_flip_margin(req.margin_ns); + dev.set_flip_margin(&self.state, req.margin_ns); Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 7816843d..dc94c098 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -28,6 +28,7 @@ use { ButtonState, InputDeviceAccelProfile, InputDeviceClickMethod, Leds, TransformMatrix, }, client::{Client, ClientError, ClientId}, + control_center::CCI_INPUT, cursor_user::{CursorUser, CursorUserGroup, CursorUserOwner}, ei::ei_ifs::ei_seat::EiSeat, fixed::Fixed, @@ -98,6 +99,7 @@ use { numcell::NumCell, rc_eq::{rc_eq, rc_weak_eq}, smallmap::SmallMap, + static_text::StaticText, }, wire::{ ExtIdleNotificationV1Id, WlDataDeviceId, WlKeyboardId, WlPointerId, WlSeatId, @@ -138,6 +140,9 @@ const MISSING_CAPABILITY: u32 = 0; pub const BTN_LEFT: u32 = 0x110; pub const BTN_RIGHT: u32 = 0x111; +pub const BTN_MIDDLE: u32 = 0x112; +pub const BTN_SIDE: u32 = 0x113; +pub const BTN_EXTRA: u32 = 0x114; pub const SEAT_NAME_SINCE: Version = Version(2); @@ -273,6 +278,15 @@ pub enum FallbackOutputMode { Focus, } +impl StaticText for FallbackOutputMode { + fn text(&self) -> &'static str { + match self { + FallbackOutputMode::Cursor => "Cursor", + FallbackOutputMode::Focus => "Focus", + } + } +} + impl TryFrom for FallbackOutputMode { type Error = (); @@ -765,6 +779,7 @@ impl WlSeatGlobal { if let Some(grab) = self.input_method_grab.get() { grab.on_repeat_info(); } + self.state.trigger_cci(CCI_INPUT); } pub fn close(self: &Rc) { @@ -960,18 +975,18 @@ impl WlSeatGlobal { pub fn focus_history_set_visible(&self, visible: bool) { self.focus_history_visible_only.set(visible); + self.state.trigger_cci(CCI_INPUT); } - #[expect(dead_code)] pub fn focus_history_visible(&self) -> bool { self.focus_history_visible_only.get() } pub fn focus_history_set_same_workspace(&self, same_workspace: bool) { self.focus_history_same_workspace.set(same_workspace); + self.state.trigger_cci(CCI_INPUT); } - #[expect(dead_code)] pub fn focus_history_same_workspace(&self) -> bool { self.focus_history_same_workspace.get() } @@ -1476,18 +1491,18 @@ impl WlSeatGlobal { pub fn set_focus_follows_mouse(&self, focus_follows_mouse: bool) { self.focus_follows_mouse.set(focus_follows_mouse); + self.state.trigger_cci(CCI_INPUT); } - #[expect(dead_code)] pub fn focus_follows_mouse(&self) -> bool { self.focus_follows_mouse.get() } pub fn set_fallback_output_mode(&self, fallback_output_mode: FallbackOutputMode) { self.fallback_output_mode.set(fallback_output_mode); + self.state.trigger_cci(CCI_INPUT); } - #[expect(dead_code)] pub fn fallback_output_mode(&self) -> FallbackOutputMode { self.fallback_output_mode.get() } @@ -1607,9 +1622,9 @@ impl WlSeatGlobal { pub fn set_pointer_revert_key(&self, key: KeySym) { self.revert_key.set(key); + self.state.trigger_cci(CCI_INPUT); } - #[expect(dead_code)] pub fn pointer_revert_key(&self) -> KeySym { self.revert_key.get() } @@ -1806,7 +1821,7 @@ pub fn collect_kb_foci(node: Rc) -> SmallVec<[Rc; 3]> { } impl DeviceHandlerData { - pub fn set_seat(&self, seat: Option>) { + pub fn set_seat(&self, state: &State, seat: Option>) { if let Some(new) = &seat { if let Some(old) = self.seat.get() && old.id() == new.id() @@ -1845,6 +1860,7 @@ impl DeviceHandlerData { } } self.attach_event_listeners(); + state.trigger_cci(CCI_INPUT); } fn destroy_physical_keyboard_state(&self) { @@ -1866,13 +1882,14 @@ impl DeviceHandlerData { }; } - pub fn set_keymap(&self, keymap: Option>) { + pub fn set_keymap(&self, state: &State, keymap: Option>) { self.destroy_physical_keyboard_state(); self.keymap.set(keymap); self.attach_event_listeners(); + state.trigger_cci(CCI_INPUT); } - pub fn set_output(&self, output: Option<&WlOutputGlobal>) { + pub fn set_output(&self, state: &State, output: Option<&WlOutputGlobal>) { match output { None => { log::info!("Removing output mapping of {}", self.device.name()); @@ -1883,6 +1900,7 @@ impl DeviceHandlerData { self.output.set(Some(o.opt.clone())); } } + state.trigger_cci(CCI_INPUT); } pub fn get_rect(&self, state: &State) -> Rect { @@ -1894,52 +1912,64 @@ impl DeviceHandlerData { state.root.extents.get() } - pub fn set_accel_profile(&self, v: InputDeviceAccelProfile) { + pub fn set_accel_profile(&self, state: &State, v: InputDeviceAccelProfile) { self.device.set_accel_profile(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_accel_speed(&self, v: f64) { + pub fn set_accel_speed(&self, state: &State, v: f64) { self.device.set_accel_speed(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_tap_enabled(&self, v: bool) { + pub fn set_tap_enabled(&self, state: &State, v: bool) { self.device.set_tap_enabled(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_drag_enabled(&self, v: bool) { + pub fn set_drag_enabled(&self, state: &State, v: bool) { self.device.set_drag_enabled(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_drag_lock_enabled(&self, v: bool) { + pub fn set_drag_lock_enabled(&self, state: &State, v: bool) { self.device.set_drag_lock_enabled(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_left_handed(&self, v: bool) { + pub fn set_left_handed(&self, state: &State, v: bool) { self.device.set_left_handed(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_natural_scrolling_enabled(&self, v: bool) { + pub fn set_natural_scrolling_enabled(&self, state: &State, v: bool) { self.device.set_natural_scrolling_enabled(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_px_per_scroll_wheel(&self, v: f64) { + pub fn set_px_per_scroll_wheel(&self, state: &State, v: f64) { self.px_per_scroll_wheel.set(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_transform_matrix(&self, v: TransformMatrix) { + pub fn set_transform_matrix(&self, state: &State, v: TransformMatrix) { self.device.set_transform_matrix(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_calibration_matrix(&self, v: [[f32; 3]; 2]) { + pub fn set_calibration_matrix(&self, state: &State, v: [[f32; 3]; 2]) { self.device.set_calibration_matrix(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_click_method(&self, v: InputDeviceClickMethod) { + pub fn set_click_method(&self, state: &State, v: InputDeviceClickMethod) { self.device.set_click_method(v); + state.trigger_cci(CCI_INPUT); } - pub fn set_middle_button_emulation_enabled(&self, v: bool) { + pub fn set_middle_button_emulation_enabled(&self, state: &State, v: bool) { self.device.set_middle_button_emulation_enabled(v); + state.trigger_cci(CCI_INPUT); } } diff --git a/src/ifs/wl_seat/text_input.rs b/src/ifs/wl_seat/text_input.rs index 51cea1d3..77d2d09e 100644 --- a/src/ifs/wl_seat/text_input.rs +++ b/src/ifs/wl_seat/text_input.rs @@ -1,6 +1,7 @@ use { crate::{ backend::KeyState, + control_center::CCI_INPUT, ifs::{ wl_seat::{ WlSeatGlobal, @@ -89,6 +90,7 @@ impl WlSeatGlobal { im.cancel_simple(self); } } + self.state.trigger_cci(CCI_INPUT); } pub fn simple_im_enabled(&self) -> bool { diff --git a/src/ifs/wl_surface/zwp_idle_inhibitor_v1.rs b/src/ifs/wl_surface/zwp_idle_inhibitor_v1.rs index 34d26581..73a6fec1 100644 --- a/src/ifs/wl_surface/zwp_idle_inhibitor_v1.rs +++ b/src/ifs/wl_surface/zwp_idle_inhibitor_v1.rs @@ -44,12 +44,12 @@ impl ZwpIdleInhibitorV1 { pub fn activate(self: &Rc) { let state = &self.client.state; - state.idle.add_inhibitor(self); + state.idle.add_inhibitor(state, self); } pub fn deactivate(&self) { let state = &self.client.state; - state.idle.remove_inhibitor(self); + state.idle.remove_inhibitor(state, self); } } diff --git a/src/kbvm.rs b/src/kbvm.rs index b7026ae6..db16b124 100644 --- a/src/kbvm.rs +++ b/src/kbvm.rs @@ -52,7 +52,6 @@ pub struct KbvmMap { pub id: KbvmMapId, pub state_machine: StateMachine, pub lookup_table: LookupTable, - #[expect(dead_code)] pub map_text: String, pub map: KeymapFd, pub xwayland_map: KeymapFd, diff --git a/src/logger.rs b/src/logger.rs index 22c4118a..a344abbd 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -80,7 +80,6 @@ impl Logger { log::set_max_level(filter); } - #[expect(dead_code)] pub fn level(&self) -> LogLevel { self.level.load(Relaxed) } diff --git a/src/main.rs b/src/main.rs index ca0a0d56..17b1662b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,7 @@ mod clientmem; mod cmm; mod compositor; mod config; +mod control_center; mod copy_device; mod cpu_worker; mod criteria; @@ -67,6 +68,7 @@ mod damage; mod dbus; mod drm_feedback; mod edid; +mod egui_adapter; mod ei; mod eventfd_cache; mod fixed; diff --git a/src/output_schedule.rs b/src/output_schedule.rs index c9420c21..dec598e8 100644 --- a/src/output_schedule.rs +++ b/src/output_schedule.rs @@ -2,9 +2,10 @@ use { crate::{ async_engine::AsyncEngine, backend::HardwareCursor, + control_center::CCI_OUTPUTS, ifs::wl_output::PersistentOutputState, io_uring::{IoUring, IoUringError}, - state::ConnectorData, + state::{ConnectorData, State}, utils::{ asyncevent::AsyncEvent, cell_ext::CellExt, clonecell::CloneCell, errorfmt::ErrorFmt, numcell::NumCell, @@ -51,8 +52,7 @@ pub struct OutputSchedule { impl OutputSchedule { pub fn new( - ring: &Rc, - eng: &Rc, + state: &State, connector: &Rc, persistent: &Rc, ) -> Self { @@ -60,8 +60,8 @@ impl OutputSchedule { changed: Default::default(), run: Default::default(), connector: connector.clone(), - ring: ring.clone(), - eng: eng.clone(), + ring: state.ring.clone(), + eng: state.eng.clone(), vrr_enabled: Default::default(), hardware_cursor_change: Cell::new(Change::None), software_cursor_change: Cell::new(Change::None), @@ -72,7 +72,7 @@ impl OutputSchedule { iteration: Default::default(), }; if let Some(hz) = persistent.vrr_cursor_hz.get() { - slf.set_cursor_hz(hz); + slf.set_cursor_hz(state, hz); } slf } @@ -118,7 +118,7 @@ impl OutputSchedule { self.trigger(); } - pub fn set_cursor_hz(&self, hz: f64) { + pub fn set_cursor_hz(&self, state: &State, hz: f64) { let (hz, delta) = match map_cursor_hz(hz) { None => { log::warn!("Ignoring cursor frequency {hz}"); @@ -128,6 +128,7 @@ impl OutputSchedule { }; self.persistent.vrr_cursor_hz.set(hz); self.connector.head_managers.handle_cursor_hz_change(hz); + state.trigger_cci(CCI_OUTPUTS); self.cursor_delta_nsec.set(delta); self.trigger(); } diff --git a/src/security_context_acceptor.rs b/src/security_context_acceptor.rs index 0a5f9bb0..bf164643 100644 --- a/src/security_context_acceptor.rs +++ b/src/security_context_acceptor.rs @@ -42,6 +42,15 @@ pub struct AcceptorMetadata { pub tag: Option, } +impl AcceptorMetadata { + pub fn secure() -> Self { + Self { + secure: true, + ..Default::default() + } + } +} + impl SecurityContextAcceptors { pub fn clear(&self) { for acceptor in self.acceptors.lock().drain_values() { diff --git a/src/state.rs b/src/state.rs index 652f50a0..ad537ee1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,6 +1,7 @@ use { crate::{ acceptor::Acceptor, + allocator::BufferObject, async_engine::{AsyncEngine, SpawnedFuture}, backend::{ Backend, BackendConnectorState, BackendConnectorStateSerials, BackendDrmDevice, @@ -15,6 +16,10 @@ use { cmm::{cmm_description::ColorDescription, cmm_manager::ColorManager}, compositor::{LIBEI_SOCKET, LogLevel}, config::ConfigProxy, + control_center::{ + CCI_COLOR_MANAGEMENT, CCI_COMPOSITOR, CCI_GPUS, CCI_IDLE, CCI_LOOK_AND_FEEL, + CCI_OUTPUTS, CCI_XWAYLAND, ControlCenters, + }, copy_device::CopyDeviceRegistry, cpu_worker::CpuWorker, criteria::{clm::ClMatcherManager, tlm::TlMatcherManager}, @@ -23,6 +28,7 @@ use { damage::DamageVisualizer, dbus::Dbus, drm_feedback::{DrmFeedback, DrmFeedbackIds}, + egui_adapter::egui_platform::EggState, ei::{ ei_acceptor::EiAcceptor, ei_client::{EiClient, EiClients}, @@ -115,6 +121,7 @@ use { hash_map_ext::HashMapExt, linkedlist::LinkedList, numcell::NumCell, + object_drop_queue::ObjectDropQueue, queue::AsyncQueue, refcounted::RefCounted, run_toplevel::RunToplevel, @@ -224,7 +231,7 @@ pub struct State { pub activation_tokens: CopyHashMap, pub toplevel_lists: CopyHashMap<(ClientId, ExtForeignToplevelListV1Id), Rc>, - pub dma_buf_ids: DmaBufIds, + pub dma_buf_ids: Rc, pub drm_feedback_ids: DrmFeedbackIds, pub direct_scanout_enabled: Cell, pub persistent_output_states: CopyHashMap, Rc>, @@ -292,6 +299,9 @@ pub struct State { pub supports_presentation_feedback: Cell, pub eventfd_cache: Rc, pub lazy_event_sources: Rc, + pub bo_drop_queue: Rc>>, + pub egg_state: EggState, + pub control_centers: ControlCenters, } // impl Drop for State { @@ -340,37 +350,39 @@ pub struct IdleState { } impl IdleState { - pub fn set_timeout(&self, timeout: Duration) { + pub fn set_timeout(&self, state: &State, timeout: Duration) { self.timeout.set(timeout); - self.timeout_changed(); + self.timeout_changed(state); } - pub fn set_grace_period(&self, grace_period: Duration) { + pub fn set_grace_period(&self, state: &State, grace_period: Duration) { self.grace_period.set(grace_period); - self.timeout_changed(); + self.timeout_changed(state); } - fn timeout_changed(&self) { + fn timeout_changed(&self, state: &State) { self.timeout_changed.set(true); self.change.trigger(); + state.trigger_cci(CCI_IDLE); } - pub fn add_inhibitor(&self, inhibitor: &Rc) { + pub fn add_inhibitor(&self, state: &State, inhibitor: &Rc) { self.inhibitors.set(inhibitor.inhibit_id, inhibitor.clone()); - self.inhibitors_changed(); + self.inhibitors_changed(state); } - pub fn remove_inhibitor(&self, inhibitor: &ZwpIdleInhibitorV1) { + pub fn remove_inhibitor(&self, state: &State, inhibitor: &ZwpIdleInhibitorV1) { self.inhibitors.remove(&inhibitor.inhibit_id); - self.inhibitors_changed(); + self.inhibitors_changed(state); if self.inhibitors.is_empty() { self.resume_inhibited_notifications(); } } - fn inhibitors_changed(&self) { + fn inhibitors_changed(&self, state: &State) { self.inhibitors_changed.set(true); self.change.trigger(); + state.trigger_cci(CCI_IDLE); } fn resume_inhibited_notifications(&self) { @@ -482,30 +494,39 @@ impl ConnectorData { return; } *self.state.borrow_mut() = s.clone(); - if old.enabled != s.enabled { + macro_rules! b { + ($expr:expr) => {{ + let e = $expr; + if e { + state.trigger_cci(CCI_OUTPUTS); + } + e + }}; + } + if b!(old.enabled != s.enabled) { self.head_managers.handle_enabled_change(s.enabled); } - if old.active != s.active { + if b!(old.active != s.active) { self.head_managers.handle_active_change(s.active); } - if old.non_desktop_override != s.non_desktop_override { + if b!(old.non_desktop_override != s.non_desktop_override) { self.head_managers .handle_non_desktop_override_changed(s.non_desktop_override); } - if old.vrr != s.vrr { + if b!(old.vrr != s.vrr) { self.head_managers.handle_vrr_change(s.vrr); } - if old.tearing != s.tearing { + if b!(old.tearing != s.tearing) { self.head_managers.handle_tearing_enabled_change(s.tearing); } - if old.format != s.format { + if b!(old.format != s.format) { self.head_managers.handle_format_change(s.format); } - if (old.color_space, old.eotf) != (s.color_space, s.eotf) { + if b!((old.color_space, old.eotf) != (s.color_space, s.eotf)) { self.head_managers .handle_colors_change(s.color_space, s.eotf); } - if old.mode != s.mode { + if b!(old.mode != s.mode) { self.head_managers.handle_mode_change(s.mode); for head in self.wlr_output_heads.lock().values() { head.handle_mode_change(s.mode); @@ -528,12 +549,14 @@ impl DrmDevData { self.dev.clone().make_render_device(); } - pub fn set_direct_scanout_enabled(&self, enabled: bool) { + pub fn set_direct_scanout_enabled(&self, state: &State, enabled: bool) { self.dev.set_direct_scanout_enabled(enabled); + state.trigger_cci(CCI_GPUS); } - pub fn set_flip_margin(&self, margin: u64) { + pub fn set_flip_margin(&self, state: &State, margin: u64) { self.dev.set_flip_margin(margin); + state.trigger_cci(CCI_GPUS); } } @@ -642,6 +665,7 @@ impl State { } pub fn set_render_ctx(&self, ctx: Option>) { + self.egg_state.clear(); self.explicit_sync_supported.set(false); self.render_ctx.set(ctx.clone()); self.render_ctx_version.fetch_add(1); @@ -756,6 +780,7 @@ impl State { } self.expose_new_singletons(); + self.trigger_cci(CCI_COLOR_MANAGEMENT | CCI_GPUS); } fn reload_cursors(&self) { @@ -1000,6 +1025,7 @@ impl State { } else { self.stop_xwayland(); } + self.trigger_cci(CCI_XWAYLAND); } pub fn set_xwayland_use_wire_scale(&self, use_wire_scale: bool) { @@ -1007,6 +1033,7 @@ impl State { return; } self.update_xwayland_wire_scale(); + self.trigger_cci(CCI_XWAYLAND); } pub fn next_serial(&self, client: Option<&Client>) -> u64 { @@ -1154,6 +1181,9 @@ impl State { self.wait_for_syncobj.clear(); self.xdg_surface_configure_events.clear(); self.lazy_event_sources.clear(); + self.bo_drop_queue.kill(); + self.egg_state.clear(); + self.control_centers.clear(); } pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) { @@ -1703,11 +1733,13 @@ impl State { pub fn set_color_management_enabled(&self, enabled: bool) { self.color_management_enabled.set(enabled); self.expose_new_singletons(); + self.trigger_cci(CCI_COLOR_MANAGEMENT); } pub fn set_primary_selection_enabled(&self, enabled: bool) { self.enable_primary_selection.set(enabled); self.expose_new_singletons(); + self.trigger_cci(CCI_LOOK_AND_FEEL); } pub fn set_explicit_sync_enabled(&self, enabled: bool) { @@ -1718,6 +1750,7 @@ impl State { pub fn set_log_level(&self, level: LogLevel) { if let Some(logger) = &self.logger { logger.set_level(level); + self.trigger_cci(CCI_COMPOSITOR); } } @@ -1742,6 +1775,7 @@ impl State { self.root.clone().node_visit(&mut V); self.damage(self.root.extents.get()); self.icons.clear(); + self.trigger_cci(CCI_LOOK_AND_FEEL); } pub fn reset_colors(&self) { @@ -1776,6 +1810,7 @@ impl State { pub fn set_ei_socket_enabled(self: &Rc, enabled: bool) { self.enable_ei_acceptor.set(enabled); self.update_ei_acceptor(); + self.trigger_cci(CCI_COMPOSITOR); } pub fn set_workspace_display_order(&self, order: WorkspaceDisplayOrder) { @@ -1783,6 +1818,7 @@ impl State { for output in self.root.outputs.lock().values() { output.handle_workspace_display_order_update(); } + self.trigger_cci(CCI_COMPOSITOR); } fn spaces_changed(&self) { @@ -1804,6 +1840,7 @@ impl State { self.root.clone().node_visit(&mut V); self.damage(self.root.extents.get()); self.icons.update_sizes(self); + self.trigger_cci(CCI_LOOK_AND_FEEL); } pub fn set_show_bar(&self, show: bool) { @@ -1818,15 +1855,18 @@ impl State { pub fn set_ui_drag_enabled(&self, enabled: bool) { self.ui_drag_enabled.set(enabled); + self.trigger_cci(CCI_LOOK_AND_FEEL); } pub fn set_ui_drag_threshold(&self, threshold: i32) { self.ui_drag_threshold_squared .set(threshold.saturating_mul(threshold)); + self.trigger_cci(CCI_LOOK_AND_FEEL); } pub fn set_show_pin_icon(&self, show: bool) { self.show_pin_icon.set(show); + self.trigger_cci(CCI_LOOK_AND_FEEL); for stacked in self.root.stacked.iter() { if let Some(float) = stacked.deref().clone().node_into_float() { float.schedule_render_titles(); @@ -1836,6 +1876,7 @@ impl State { pub fn set_float_above_fullscreen(&self, v: bool) { self.float_above_fullscreen.set(v); + self.trigger_cci(CCI_LOOK_AND_FEEL); for seat in self.globals.seats.lock().values() { seat.emulate_cursor_moved(); seat.trigger_tree_changed(false); @@ -1849,6 +1890,7 @@ impl State { } fn fonts_changed(&self) { + self.trigger_cci(CCI_LOOK_AND_FEEL); struct V; impl NodeVisitorBase for V { fn visit_container(&mut self, node: &Rc) { @@ -1872,6 +1914,7 @@ impl State { theme.font.set(self.theme.default_font.clone()); theme.bar_font.set(None); theme.title_font.set(None); + self.egg_state.reset_fonts(); self.fonts_changed(); } @@ -1892,6 +1935,16 @@ impl State { self.fonts_changed(); } + pub fn set_egui_fonts(&self, proportional: Option>, monospace: Option>) { + if let Some(fonts) = &proportional { + self.egg_state.set_proportional_fonts(fonts); + } + if let Some(fonts) = &monospace { + self.egg_state.set_monospace_fonts(fonts); + } + self.fonts_changed(); + } + pub fn set_bar_position(&self, p: BarPosition) { self.theme.bar_position.set(p); self.spaces_changed(); diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index aee14494..82bfc4ce 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -4,6 +4,7 @@ use { BackendConnectorState, BackendConnectorStateSerial, Connector, ConnectorEvent, ConnectorId, MonitorInfo, }, + control_center::CCI_OUTPUTS, format::XRGB8888, globals::GlobalName, ifs::{ @@ -108,6 +109,7 @@ pub fn handle(state: &Rc, connector: &Rc) { for mgr in state.head_managers.lock().values() { mgr.announce(&data); } + state.trigger_cci(CCI_OUTPUTS); if state.connectors.set(id, data).is_some() { panic!("Connector id has been reused"); } @@ -147,6 +149,7 @@ impl ConnectorHandler { self.data.handler.set(None); self.state.connectors.remove(&self.id); self.data.head_managers.handle_removed(); + self.state.trigger_cci(CCI_OUTPUTS); } async fn handle_connected(&self, info: MonitorInfo) { @@ -162,6 +165,7 @@ impl ConnectorHandler { } self.data.connected.set(false); self.data.head_managers.handle_output_disconnected(); + self.state.trigger_cci(CCI_OUTPUTS); for head in self.data.wlr_output_heads.lock().drain_values() { head.handle_disconnected(); } @@ -213,12 +217,7 @@ impl ConnectorHandler { info.primaries, info.luminance, )); - let schedule = Rc::new(OutputSchedule::new( - &self.state.ring, - &self.state.eng, - &self.data, - &desired_state, - )); + let schedule = Rc::new(OutputSchedule::new(&self.state, &self.data, &desired_state)); let _schedule = self .state .eng @@ -341,6 +340,7 @@ impl ConnectorHandler { self.data .head_managers .handle_output_connected(&output_data); + self.state.trigger_cci(CCI_OUTPUTS); self.state.wlr_output_managers.announce_head(&output_data); 'outer: loop { while let Some(event) = self.data.connector.event() { @@ -353,6 +353,7 @@ impl ConnectorHandler { } ConnectorEvent::FormatsChanged(formats) => { self.data.head_managers.handle_formats_change(&formats); + self.state.trigger_cci(CCI_OUTPUTS); on.global.formats.set(formats); } ConnectorEvent::State(state) => { @@ -466,6 +467,7 @@ impl ConnectorHandler { self.data .head_managers .handle_output_connected(&output_data); + self.state.trigger_cci(CCI_OUTPUTS); self.state.wlr_output_managers.announce_head(&output_data); 'outer: loop { while let Some(event) = self.data.connector.event() { diff --git a/src/tasks/input_device.rs b/src/tasks/input_device.rs index f9bf3f5a..61550def 100644 --- a/src/tasks/input_device.rs +++ b/src/tasks/input_device.rs @@ -67,7 +67,7 @@ impl DeviceHandler { } for seat in self.state.globals.seats.lock().values() { if seat.seat_name() == DEFAULT_SEAT_NAME { - self.data.set_seat(Some(seat.clone())); + self.data.set_seat(&self.state, Some(seat.clone())); break; } } @@ -102,6 +102,6 @@ impl DeviceHandler { .input_device_handlers .borrow_mut() .remove(&self.dev.id()); - self.data.set_seat(None); + self.data.set_seat(&self.state, None); } } diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 3464fed8..dc680d88 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -334,7 +334,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(27), + version: s.jay_compositor.1.min(28), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/src/tree/output.rs b/src/tree/output.rs index bf047663..79b70dc2 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -6,6 +6,7 @@ use { }, client::ClientId, cmm::cmm_description::ColorDescription, + control_center::CCI_OUTPUTS, cursor::KnownCursor, fixed::Fixed, gfx_api::{AcquireSync, BufferResv, GfxTexture, ReleaseSync}, @@ -243,6 +244,7 @@ impl OutputNode { .connector .head_managers .handle_tearing_active_change(tearing); + self.state.trigger_cci(CCI_OUTPUTS); } } @@ -501,6 +503,7 @@ impl OutputNode { .connector .head_managers .handle_scale_change(scale); + self.state.trigger_cci(CCI_OUTPUTS); for head in self.global.connector.wlr_output_heads.lock().values() { head.handle_new_scale(scale); } @@ -873,6 +876,7 @@ impl OutputNode { .connector .head_managers .handle_transform_change(transform); + self.state.trigger_cci(CCI_OUTPUTS); for head in self.global.connector.wlr_output_heads.lock().values() { head.hande_transform_change(transform); } @@ -935,6 +939,7 @@ impl OutputNode { .connector .head_managers .handle_position_size_change(self); + self.state.trigger_cci(CCI_OUTPUTS); } pub fn update_state(self: &Rc, old: BackendConnectorState, state: BackendConnectorState) { @@ -989,6 +994,7 @@ impl OutputNode { .connector .head_managers .handle_brightness_change(brightness); + self.state.trigger_cci(CCI_OUTPUTS); } } @@ -1004,6 +1010,7 @@ impl OutputNode { .connector .head_managers .handle_use_native_gamut_change(use_native_gamut); + self.state.trigger_cci(CCI_OUTPUTS); } } @@ -1015,6 +1022,7 @@ impl OutputNode { .connector .head_managers .handle_blend_space_change(blend_space); + self.state.trigger_cci(CCI_OUTPUTS); } } fn find_stacked_at( @@ -1480,6 +1488,7 @@ impl OutputNode { .connector .head_managers .handle_vrr_mode_change(mode); + self.state.trigger_cci(CCI_OUTPUTS); for head in self.global.connector.wlr_output_heads.lock().values() { head.handle_vrr_mode_change(mode); } @@ -1494,6 +1503,7 @@ impl OutputNode { .connector .head_managers .handle_tearing_mode_change(mode); + self.state.trigger_cci(CCI_OUTPUTS); } } @@ -1543,6 +1553,7 @@ impl OutputNode { pub fn set_flip_margin(&self, margin_ns: u64) { self.flip_margin_ns.set(Some(margin_ns)); + self.state.trigger_cci(CCI_OUTPUTS); } } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index fdf4f1b0..ec609e37 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -936,7 +936,6 @@ impl ToplevelData { parent.node_is_workspace() } - #[expect(dead_code)] pub fn property_changed_source(&self) -> &Rc { self.property_changed_source .get_or_init(|| self.state.lazy_event_sources.create_source()) diff --git a/src/utils.rs b/src/utils.rs index 7c956c82..31cadb59 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -31,6 +31,7 @@ pub mod nice; pub mod nonblock; pub mod num_cpus; pub mod numcell; +pub mod object_drop_queue; pub mod on_change; pub mod on_drop_event; pub mod once; diff --git a/src/utils/numcell.rs b/src/utils/numcell.rs index a8fc11c1..9f9a3ab5 100644 --- a/src/utils/numcell.rs +++ b/src/utils/numcell.rs @@ -100,6 +100,14 @@ impl NumCell { { !self.is_zero() } + + #[inline(always)] + pub fn take(&self) -> T + where + T: Default, + { + self.t.replace(T::default()) + } } impl + Copy> BitOr for &'_ NumCell { diff --git a/src/utils/object_drop_queue.rs b/src/utils/object_drop_queue.rs new file mode 100644 index 00000000..f14eabbb --- /dev/null +++ b/src/utils/object_drop_queue.rs @@ -0,0 +1,83 @@ +use { + crate::{ + io_uring::{IoUring, PendingPoll, PollCallback}, + utils::{errorfmt::ErrorFmt, oserror::OsError, stack::Stack}, + }, + std::{ + cell::{Cell, RefCell}, + rc::Rc, + }, + uapi::{OwnedFd, c::c_short}, +}; + +pub struct ObjectDropQueue { + ring: Rc, + killed: Cell, + pending: RefCell>>, + stack: Stack>>, +} + +struct Pollable { + queue: Rc>, + idx: usize, +} + +impl ObjectDropQueue { + pub fn new(ring: &Rc) -> Self { + Self { + ring: ring.clone(), + killed: Default::default(), + pending: Default::default(), + stack: Default::default(), + } + } + + pub fn push(self: &Rc, fd: &Rc, t: T) + where + T: 'static, + { + if self.killed.get() { + return; + } + let pending = &mut *self.pending.borrow_mut(); + let pollable = match self.stack.pop() { + Some(p) => p, + None => { + let pollable = Rc::new(Pollable { + queue: self.clone(), + idx: pending.len(), + }); + pending.push(None); + pollable + } + }; + let idx = pollable.idx; + match self.ring.readable_external(fd, pollable) { + Ok(p) => { + pending[idx] = Some((t, p)); + } + Err(e) => { + log::error!("Could not register object: {}", ErrorFmt(e)); + } + } + } + + pub fn kill(&self) { + self.killed.set(true); + self.pending.take(); + self.stack.take(); + } +} + +impl PollCallback for Pollable { + fn completed(self: Rc, res: Result) { + if let Err(e) = res { + log::error!("Could not wait for fd to become readable: {}", ErrorFmt(e)); + } + let q = &self.queue; + if !q.killed.get() { + q.pending.borrow_mut()[self.idx] = None; + q.stack.push(self.clone()); + } + } +} diff --git a/src/utils/opaque.rs b/src/utils/opaque.rs index 5e54001c..209e77a9 100644 --- a/src/utils/opaque.rs +++ b/src/utils/opaque.rs @@ -10,7 +10,7 @@ use { thiserror::Error, }; -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] pub struct Opaque { lo: u64, hi: u64, diff --git a/src/utils/pipe.rs b/src/utils/pipe.rs index ed720856..2d645bc9 100644 --- a/src/utils/pipe.rs +++ b/src/utils/pipe.rs @@ -14,7 +14,6 @@ pub fn pipe() -> Result, OsError> { } impl Pipe { - #[expect(dead_code)] pub fn map_read(self, map: impl FnOnce(L) -> Lprime) -> Pipe { Pipe { read: map(self.read), @@ -22,7 +21,6 @@ impl Pipe { } } - #[expect(dead_code)] pub fn map_write(self, map: impl FnOnce(R) -> Rprime) -> Pipe { Pipe { read: self.read, diff --git a/src/utils/toplevel_identifier.rs b/src/utils/toplevel_identifier.rs index 09d894ee..e9b1f30b 100644 --- a/src/utils/toplevel_identifier.rs +++ b/src/utils/toplevel_identifier.rs @@ -10,7 +10,7 @@ use { }, }; -#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Ord, PartialOrd)] pub struct ToplevelIdentifier(Opaque); unsafe impl UnsafeCellCloneSafe for ToplevelIdentifier {} diff --git a/src/video/dmabuf.rs b/src/video/dmabuf.rs index e19def66..9ff8b1b8 100644 --- a/src/video/dmabuf.rs +++ b/src/video/dmabuf.rs @@ -1,8 +1,12 @@ use { crate::{ format::Format, + gfx_api::SyncFile, utils::{compat::IoctlNumber, oserror::OsError}, - video::{LINEAR_MODIFIER, Modifier}, + video::{ + LINEAR_MODIFIER, Modifier, + drm::{DrmError, syncobj::merge_sync_files}, + }, }, arrayvec::ArrayVec, std::{cell::OnceCell, rc::Rc, sync::OnceLock}, @@ -113,6 +117,22 @@ impl DmaBuf { } Ok(()) } + + pub fn export_sync_file(&self, flags: u32) -> Result, DrmError> { + let mut sf = PlaneVec::new(); + for plane in &self.planes { + sf.push( + dma_buf_export_sync_file(&plane.fd, flags) + .map(Rc::new) + .map(SyncFile) + .map_err(DrmError::ExportSyncFile)?, + ); + if self.is_one_file() { + break; + } + } + merge_sync_files(sf.iter()) + } } const DMA_BUF_BASE: u64 = b'b' as _; diff --git a/src/wl_usr/usr_ifs/usr_jay_compositor.rs b/src/wl_usr/usr_ifs/usr_jay_compositor.rs index 3d0a7287..63102b64 100644 --- a/src/wl_usr/usr_ifs/usr_jay_compositor.rs +++ b/src/wl_usr/usr_ifs/usr_jay_compositor.rs @@ -189,7 +189,6 @@ impl UsrJayCompositor { obj } - #[expect(dead_code)] pub fn get_sync_file_surface(&self, surface: &UsrWlSurface) -> Rc { let obj = Rc::new(UsrJaySyncFileSurface { id: self.con.id(), diff --git a/src/wl_usr/usr_ifs/usr_jay_sync_file_surface.rs b/src/wl_usr/usr_ifs/usr_jay_sync_file_surface.rs index 8aa184f4..bd07ef76 100644 --- a/src/wl_usr/usr_ifs/usr_jay_sync_file_surface.rs +++ b/src/wl_usr/usr_ifs/usr_jay_sync_file_surface.rs @@ -18,7 +18,6 @@ pub struct UsrJaySyncFileSurface { } impl UsrJaySyncFileSurface { - #[expect(dead_code)] pub fn set_acquire(&self, sf: Option<&FdSync>) { match sf.and_then(|s| s.get_sync_file()) { None => { @@ -33,7 +32,6 @@ impl UsrJaySyncFileSurface { } } - #[expect(dead_code)] pub fn get_release(&self) -> Rc { let obj = Rc::new(UsrJaySyncFileRelease { id: self.con.id(), diff --git a/src/wl_usr/usr_ifs/usr_wl_data_device.rs b/src/wl_usr/usr_ifs/usr_wl_data_device.rs index 7b3cb57e..6309e86f 100644 --- a/src/wl_usr/usr_ifs/usr_wl_data_device.rs +++ b/src/wl_usr/usr_ifs/usr_wl_data_device.rs @@ -21,7 +21,6 @@ pub struct UsrWlDataDevice { } impl UsrWlDataDevice { - #[expect(dead_code)] pub fn set_selection(&self, serial: u32, source: &UsrWlDataSource) { self.con.request(SetSelection { self_id: self.id, diff --git a/src/wl_usr/usr_ifs/usr_wl_data_device_manager.rs b/src/wl_usr/usr_ifs/usr_wl_data_device_manager.rs index 0957a93a..58847c9f 100644 --- a/src/wl_usr/usr_ifs/usr_wl_data_device_manager.rs +++ b/src/wl_usr/usr_ifs/usr_wl_data_device_manager.rs @@ -21,7 +21,6 @@ pub struct UsrWlDataDeviceManager { } impl UsrWlDataDeviceManager { - #[expect(dead_code)] pub fn create_data_source(&self) -> Rc { let obj = Rc::new(UsrWlDataSource { id: self.con.id(), @@ -37,7 +36,6 @@ impl UsrWlDataDeviceManager { obj } - #[expect(dead_code)] pub fn get_data_device(&self, seat: &UsrWlSeat) -> Rc { let obj = Rc::new(UsrWlDataDevice { id: self.con.id(), diff --git a/src/wl_usr/usr_ifs/usr_wl_data_offer.rs b/src/wl_usr/usr_ifs/usr_wl_data_offer.rs index 4c500e36..e757180a 100644 --- a/src/wl_usr/usr_ifs/usr_wl_data_offer.rs +++ b/src/wl_usr/usr_ifs/usr_wl_data_offer.rs @@ -17,7 +17,6 @@ pub struct UsrWlDataOffer { } impl UsrWlDataOffer { - #[expect(dead_code)] pub fn receive(&self, mime_type: &str, fd: &Rc) { self.con.request(Receive { self_id: self.id, diff --git a/src/wl_usr/usr_ifs/usr_wl_data_source.rs b/src/wl_usr/usr_ifs/usr_wl_data_source.rs index e7109346..99c07f67 100644 --- a/src/wl_usr/usr_ifs/usr_wl_data_source.rs +++ b/src/wl_usr/usr_ifs/usr_wl_data_source.rs @@ -21,7 +21,6 @@ pub trait UsrWlDataSourceOwner { } impl UsrWlDataSource { - #[expect(dead_code)] pub fn offer(&self, mime_type: &str) { self.con.request(Offer { self_id: self.id, diff --git a/src/wl_usr/usr_ifs/usr_wl_pointer.rs b/src/wl_usr/usr_ifs/usr_wl_pointer.rs index c8dcf486..661d9db9 100644 --- a/src/wl_usr/usr_ifs/usr_wl_pointer.rs +++ b/src/wl_usr/usr_ifs/usr_wl_pointer.rs @@ -41,7 +41,6 @@ pub trait UsrWlPointerOwner { } impl UsrWlPointer { - #[expect(dead_code)] pub fn set_cursor(&self, serial: u32, cursor: Option<&UsrWlSurface>, hot_x: i32, hot_y: i32) { self.con.request(SetCursor { self_id: self.id, diff --git a/src/wl_usr/usr_ifs/usr_wl_seat.rs b/src/wl_usr/usr_ifs/usr_wl_seat.rs index d1f4ba6f..76cfcab6 100644 --- a/src/wl_usr/usr_ifs/usr_wl_seat.rs +++ b/src/wl_usr/usr_ifs/usr_wl_seat.rs @@ -47,7 +47,6 @@ impl UsrWlSeat { ptr } - #[expect(dead_code)] pub fn get_keyboard(&self) -> Rc { let kb = Rc::new(UsrWlKeyboard { id: self.con.id(), diff --git a/src/wl_usr/usr_ifs/usr_wp_cursor_shape_device_v1.rs b/src/wl_usr/usr_ifs/usr_wp_cursor_shape_device_v1.rs index 49e8c816..623f085f 100644 --- a/src/wl_usr/usr_ifs/usr_wp_cursor_shape_device_v1.rs +++ b/src/wl_usr/usr_ifs/usr_wp_cursor_shape_device_v1.rs @@ -15,7 +15,6 @@ pub struct UsrWpCursorShapeDeviceV1 { } impl UsrWpCursorShapeDeviceV1 { - #[expect(dead_code)] pub fn set_shape(&self, serial: u32, cursor: KnownCursor) { self.con.request(SetShape { self_id: self.id, diff --git a/src/wl_usr/usr_ifs/usr_wp_cursor_shape_manager_v1.rs b/src/wl_usr/usr_ifs/usr_wp_cursor_shape_manager_v1.rs index 69c9cd55..49549cb9 100644 --- a/src/wl_usr/usr_ifs/usr_wp_cursor_shape_manager_v1.rs +++ b/src/wl_usr/usr_ifs/usr_wp_cursor_shape_manager_v1.rs @@ -21,7 +21,6 @@ pub struct UsrWpCursorShapeManagerV1 { } impl UsrWpCursorShapeManagerV1 { - #[expect(dead_code)] pub fn get_pointer(&self, pointer: &UsrWlPointer) -> Rc { let obj = Rc::new(UsrWpCursorShapeDeviceV1 { id: self.con.id(), diff --git a/src/wl_usr/usr_ifs/usr_xdg_surface.rs b/src/wl_usr/usr_ifs/usr_xdg_surface.rs index e706c5c2..04eebaf3 100644 --- a/src/wl_usr/usr_ifs/usr_xdg_surface.rs +++ b/src/wl_usr/usr_ifs/usr_xdg_surface.rs @@ -22,7 +22,6 @@ pub trait UsrXdgSurfaceOwner { } impl UsrXdgSurface { - #[expect(dead_code)] pub fn get_toplevel(&self) -> Rc { let obj = Rc::new(UsrXdgToplevel { id: self.con.id(), diff --git a/src/wl_usr/usr_ifs/usr_xdg_toplevel.rs b/src/wl_usr/usr_ifs/usr_xdg_toplevel.rs index 4e4d7491..3705abc6 100644 --- a/src/wl_usr/usr_ifs/usr_xdg_toplevel.rs +++ b/src/wl_usr/usr_ifs/usr_xdg_toplevel.rs @@ -16,7 +16,6 @@ pub struct UsrXdgToplevel { } impl UsrXdgToplevel { - #[expect(dead_code)] pub fn set_title(&self, title: &str) { self.con.request(SetTitle { self_id: self.id, @@ -24,7 +23,6 @@ impl UsrXdgToplevel { }); } - #[expect(dead_code)] pub fn set_fullscreen(&self, fullscreen: bool) { match fullscreen { true => { diff --git a/src/wl_usr/usr_ifs/usr_xdg_wm_base.rs b/src/wl_usr/usr_ifs/usr_xdg_wm_base.rs index b9d7faf0..95b5f16a 100644 --- a/src/wl_usr/usr_ifs/usr_xdg_wm_base.rs +++ b/src/wl_usr/usr_ifs/usr_xdg_wm_base.rs @@ -18,7 +18,6 @@ pub struct UsrXdgWmBase { } impl UsrXdgWmBase { - #[expect(dead_code)] pub fn get_xdg_surface(&self, surface: &UsrWlSurface) -> Rc { let obj = Rc::new(UsrXdgSurface { id: self.con.id(), diff --git a/src/wl_usr/usr_ifs/usr_zwp_linux_dmabuf_v1.rs b/src/wl_usr/usr_ifs/usr_zwp_linux_dmabuf_v1.rs index f2017a51..f39e85b4 100644 --- a/src/wl_usr/usr_ifs/usr_zwp_linux_dmabuf_v1.rs +++ b/src/wl_usr/usr_ifs/usr_zwp_linux_dmabuf_v1.rs @@ -22,7 +22,6 @@ pub struct UsrZwpLinuxDmabufV1 { } impl UsrZwpLinuxDmabufV1 { - #[expect(dead_code)] pub fn create_buffer(&self, buffer: &DmaBuf) -> Rc { let params = Rc::new(UsrZwpLinuxBufferParamsV1 { id: self.con.id(), diff --git a/src/xwayland.rs b/src/xwayland.rs index a5deb612..08a69d89 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -5,6 +5,7 @@ use { crate::{ client::{ClientCaps, ClientError}, compositor::DISPLAY, + control_center::CCI_XWAYLAND, forker::{ForkerError, ForkerProxy}, ifs::{ ipc::{DataOfferId, DataSourceId, IpcLocation, x_data_offer::XDataOffer}, @@ -117,9 +118,11 @@ pub async fn manage(state: Rc) { let display = Rc::new(format!(":{}", xsocket.id)); forker.setenv(DISPLAY.as_bytes(), display.as_bytes()); state.xwayland.display.set(Some(display.clone())); + state.trigger_cci(CCI_XWAYLAND); let _unsetenv = on_drop(|| { forker.unsetenv(DISPLAY.as_bytes()); state.xwayland.display.take(); + state.trigger_cci(CCI_XWAYLAND); }); log::info!("Allocated display :{} for Xwayland", xsocket.id); log::info!("Waiting for connection attempt"); @@ -212,9 +215,11 @@ async fn run( state.xwayland.queue.clear(); state.xwayland.pidfd.set(Some(pidfd.clone())); state.xwayland.client.set(Some(client.clone())); + state.trigger_cci(CCI_XWAYLAND); let _remove_pidfd = on_drop(|| { state.xwayland.pidfd.take(); state.xwayland.client.take(); + state.trigger_cci(CCI_XWAYLAND); }); { let shared = Rc::new(XwmShared::default()); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 45a62ab6..915c686b 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -90,6 +90,7 @@ pub enum SimpleCommand { ToggleSimpleImEnabled, ReloadSimpleIm, EnableUnicodeInput, + OpenControlCenter, } #[derive(Debug, Clone)] @@ -211,6 +212,12 @@ pub struct Theme { pub bar_separator_width: Option, } +#[derive(Debug, Clone, Default)] +pub struct Egui { + pub proportional_fonts: Option>, + pub monospace_fonts: Option>, +} + #[derive(Debug, Clone)] pub struct Status { pub format: MessageFormat, @@ -510,6 +517,7 @@ pub struct Config { pub auto_reload: Option, pub log_level: Option, pub theme: Theme, + pub egui: Egui, pub gfx_api: Option, pub direct_scanout_enabled: Option, pub drm_devices: Vec, diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 1a4319c3..27a4ea8a 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -19,6 +19,7 @@ mod connector_match; mod content_type; mod drm_device; mod drm_device_match; +mod egui; mod env; pub mod exec; mod fallback_output_mode; diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 7ea8fec4..3e0e4702 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -167,6 +167,7 @@ impl ActionParser<'_> { "toggle-simple-im-enabled" => ToggleSimpleImEnabled, "reload-simple-im" => ReloadSimpleIm, "enable-unicode-input" => EnableUnicodeInput, + "open-control-center" => OpenControlCenter, _ => { return Err( ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 10e12fca..1f0651b2 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -1,7 +1,7 @@ use { crate::{ config::{ - Action, Config, Libei, Theme, UiDrag, + Action, Config, Egui, Libei, Theme, UiDrag, context::Context, extractor::{Extractor, ExtractorError, arr, bol, int, opt, recover, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, @@ -13,6 +13,7 @@ use { connector::ConnectorsParser, drm_device::DrmDevicesParser, drm_device_match::DrmDeviceMatchParser, + egui::EguiParser, env::EnvParser, fallback_output_mode::FallbackOutputModeParser, float::FloatParser, @@ -150,6 +151,7 @@ impl Parser for ConfigParser<'_> { simple_im_val, show_titles, fallback_output_mode_val, + egui_val, ), ) = ext.extract(( ( @@ -208,6 +210,7 @@ impl Parser for ConfigParser<'_> { opt(val("simple-im")), recover(opt(bol("show-titles"))), opt(val("fallback-output-mode")), + opt(val("egui")), ), ))?; let mut keymap = None; @@ -313,6 +316,15 @@ impl Parser for ConfigParser<'_> { } } } + let mut egui = Egui::default(); + if let Some(value) = egui_val { + match value.parse(&mut EguiParser(self.0)) { + Ok(v) => egui = v, + Err(e) => { + log::warn!("Could not parse the egui settings: {}", self.0.error(e)); + } + } + } let mut gfx_api = None; if let Some(value) = gfx_api_val { match value.parse(&mut GfxApiParser) { @@ -556,6 +568,7 @@ impl Parser for ConfigParser<'_> { auto_reload: auto_reload.despan(), log_level, theme, + egui, gfx_api, drm_devices, direct_scanout_enabled: direct_scanout.despan(), diff --git a/toml-config/src/config/parsers/egui.rs b/toml-config/src/config/parsers/egui.rs new file mode 100644 index 00000000..6c77606e --- /dev/null +++ b/toml-config/src/config/parsers/egui.rs @@ -0,0 +1,63 @@ +use { + crate::{ + config::{ + Egui, + context::Context, + extractor::{Extractor, ExtractorError, arr, opt}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + }, + toml::{ + toml_span::{Span, Spanned}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +pub struct EguiParser<'a>(pub &'a Context<'a>); + +#[derive(Debug, Error)] +pub enum EguiParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extractor(#[from] ExtractorError), +} + +impl Parser for EguiParser<'_> { + type Value = Egui; + type Error = EguiParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (proportional_fonts_arr, monospace_fonts_arr) = + ext.extract((opt(arr("proportional-fonts")), opt(arr("monospace-fonts"))))?; + let mut proportional_fonts = None; + let mut monospace_fonts = None; + for (out, f) in [ + (&mut proportional_fonts, proportional_fonts_arr), + (&mut monospace_fonts, monospace_fonts_arr), + ] { + if let Some(f) = f { + let fonts = out.insert(vec![]); + for f in f.value { + let Value::String(s) = &f.value else { + log::error!("Expected a string: {}", self.0.error3(f.span)); + continue; + }; + fonts.push(s.clone()); + } + } + } + Ok(Egui { + proportional_fonts, + monospace_fonts, + }) + } +} diff --git a/toml-config/src/default-config.toml b/toml-config/src/default-config.toml index fd572891..f8c3026f 100644 --- a/toml-config/src/default-config.toml +++ b/toml-config/src/default-config.toml @@ -31,6 +31,7 @@ alt-m = "toggle-mono" alt-u = "toggle-fullscreen" alt-f = "focus-parent" +alt-c = "open-control-center" alt-shift-c = "close" alt-shift-f = "toggle-floating" Super_L = { type = "exec", exec = "alacritty" } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index db9058e6..04222433 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -36,16 +36,17 @@ use { is_reload, keyboard::Keymap, logging::set_log_level, - on_devices_enumerated, on_idle, on_unload, quit, reload, set_color_management_enabled, - set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, - set_idle, set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar, - set_show_float_pin_icon, set_show_titles, set_ui_drag_enabled, set_ui_drag_threshold, + on_devices_enumerated, on_idle, on_unload, open_control_center, quit, reload, + set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled, + set_float_above_fullscreen, set_idle, set_idle_grace_period, + set_middle_click_paste_enabled, set_show_bar, set_show_float_pin_icon, set_show_titles, + set_ui_drag_enabled, set_ui_drag_threshold, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, switch_to_vt, tasks::{self, JoinHandle}, theme::{ - reset_colors, reset_font, reset_sizes, set_bar_font, set_bar_position, set_font, - set_title_font, + reset_colors, reset_font, reset_sizes, set_bar_font, set_bar_position, + set_egui_monospace_fonts, set_egui_proportional_fonts, set_font, set_title_font, }, toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles, video::{ @@ -245,6 +246,7 @@ impl Action { let persistent = state.persistent.clone(); b.new(move || persistent.seat.enable_unicode_input()) } + SimpleCommand::OpenControlCenter => b.new(open_control_center), }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); @@ -1633,6 +1635,12 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 54a26aea..c1b93a0a 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1081,6 +1081,10 @@ "fallback-output-mode": { "description": "Sets the fallback output mode.\n\nThe default is `cursor`.\n\n- Example:\n\n ```toml\n fallback-output-mode = \"focus\"\n ```\n", "$ref": "#/$defs/FallbackOutputMode" + }, + "egui": { + "description": "Sets the egui settings of the compositor.\n", + "$ref": "#/$defs/Egui" } }, "required": [] @@ -1237,6 +1241,29 @@ } ] }, + "Egui": { + "description": "The egui settings.\n", + "type": "object", + "properties": { + "proportional-fonts": { + "type": "array", + "description": "The list of proportional fonts.\n\nThe default is `[\"sans-serif\", \"Noto Sans\", \"Noto Color Emoji\"]`.\n", + "items": { + "type": "string", + "description": "" + } + }, + "monospace-fonts": { + "type": "array", + "description": "The list of monospace fonts.\n\nThe default is `[\"monospace\", \"Noto Sans Mono\", \"Noto Color Emoji\"]`.\n", + "items": { + "type": "string", + "description": "" + } + } + }, + "required": [] + }, "Eotf": { "type": "string", "description": "The EOTF of an output.\n", @@ -1901,7 +1928,8 @@ "disable-simple-im", "toggle-simple-im-enabled", "reload-simple-im", - "enable-unicode-input" + "enable-unicode-input", + "open-control-center" ] }, "SimpleIm": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 1c4b1c94..2bf15c1e 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -2248,6 +2248,12 @@ The table has the following fields: The value of this field should be a [FallbackOutputMode](#types-FallbackOutputMode). +- `egui` (optional): + + Sets the egui settings of the compositor. + + The value of this field should be a [Egui](#types-Egui). + ### `Connector` @@ -2588,6 +2594,32 @@ The table has the following fields: The numbers should be integers. + +### `Egui` + +The egui settings. + +Values of this type should be tables. + +The table has the following fields: + +- `proportional-fonts` (optional): + + The list of proportional fonts. + + The default is `["sans-serif", "Noto Sans", "Noto Color Emoji"]`. + + The value of this field should be an array of strings. + +- `monospace-fonts` (optional): + + The list of monospace fonts. + + The default is `["monospace", "Noto Sans Mono", "Noto Color Emoji"]`. + + The value of this field should be an array of strings. + + ### `Eotf` @@ -4395,6 +4427,10 @@ The string should have one of the following values: This has no effect if the simple IM is not currently active. +- `open-control-center`: + + Opens the control center. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index cdb0767a..d4a1d12d 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -1089,6 +1089,8 @@ SimpleActionName: Enables Unicode input in the simple, XCompose based input method. This has no effect if the simple IM is not currently active. + - value: open-control-center + description: Opens the control center. Color: @@ -3004,6 +3006,11 @@ Config: ```toml fallback-output-mode = "focus" ``` + egui: + ref: Egui + required: false + description: | + Sets the egui settings of the compositor. Idle: @@ -4426,3 +4433,28 @@ FallbackOutputMode: description: Use the output the cursor is on. - value: focus description: Use the output the focus is on (highlighted window). + + +Egui: + kind: table + description: | + The egui settings. + fields: + proportional-fonts: + kind: array + items: + kind: string + required: false + description: | + The list of proportional fonts. + + The default is `["sans-serif", "Noto Sans", "Noto Color Emoji"]`. + monospace-fonts: + kind: array + items: + kind: string + required: false + description: | + The list of monospace fonts. + + The default is `["monospace", "Noto Sans Mono", "Noto Color Emoji"]`. diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index 019f9ea3..45f917ca 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -135,6 +135,10 @@ request get_pid (since = 27) { } +request open_control_center (since = 28) { + id: id(jay_open_control_center_request), +} + # events event client_id { diff --git a/wire/jay_open_control_center_request.txt b/wire/jay_open_control_center_request.txt new file mode 100644 index 00000000..7f1cfb3a --- /dev/null +++ b/wire/jay_open_control_center_request.txt @@ -0,0 +1,7 @@ +request destroy { + +} + +event failed { + msg: str, +}